diff --git a/packages/app-builder/src/components/AstBuilder/RootOrWithAnd/RootOrWithAnd.tsx b/packages/app-builder/src/components/AstBuilder/RootOrWithAnd/RootOrWithAnd.tsx index 7a85b05dd..97c2fc62f 100644 --- a/packages/app-builder/src/components/AstBuilder/RootOrWithAnd/RootOrWithAnd.tsx +++ b/packages/app-builder/src/components/AstBuilder/RootOrWithAnd/RootOrWithAnd.tsx @@ -2,7 +2,7 @@ import { LogicalOperatorLabel } from '@app-builder/components/Scenario/LogicalOp import { NewAstNode, NewUndefinedAstNode, - type NodeEvaluation, + type Validation, } from '@app-builder/models'; import { type AstBuilder, @@ -18,29 +18,29 @@ import { AstBuilderNode } from '../AstBuilderNode'; export interface RootOrWithAndViewModel { orNodeId: string; - orValidation: NodeEvaluation; + orValidation: Validation; ands: { nodeId: string; - validation: NodeEvaluation; + validation: Validation; children: EditorNodeViewModel[]; }[]; } export function adaptRootOrWithAndViewModel( - astNode: EditorNodeViewModel + viewModel: EditorNodeViewModel ): RootOrWithAndViewModel | null { - if (astNode.funcName !== 'Or') { + if (viewModel.funcName !== 'Or') { return null; } - for (const child of astNode.children) { + for (const child of viewModel.children) { if (child.funcName !== 'And') { return null; } } return { - orNodeId: astNode.nodeId, - orValidation: astNode.validation, - ands: astNode.children.map((andNode) => ({ + orNodeId: viewModel.nodeId, + orValidation: viewModel.validation, + ands: viewModel.children.map((andNode) => ({ nodeId: andNode.nodeId, validation: andNode.validation, children: andNode.children, diff --git a/packages/app-builder/src/components/AstBuilder/TwoOerandsLine/OperandEditor.tsx b/packages/app-builder/src/components/AstBuilder/TwoOerandsLine/OperandEditor.tsx index a932c5afb..91c6ac96a 100644 --- a/packages/app-builder/src/components/AstBuilder/TwoOerandsLine/OperandEditor.tsx +++ b/packages/app-builder/src/components/AstBuilder/TwoOerandsLine/OperandEditor.tsx @@ -1,7 +1,7 @@ import { - adaptAstNodeToViewModel, - adaptEditorIdentifierToViewModel, - type AstViewModel, + adaptLabelledAst, + adaptLabelledAstFromIdentifier, + type LabelledAst, NewAstNode, } from '@app-builder/models'; import { @@ -21,16 +21,16 @@ export function OperandEditor({ builder: AstBuilder; operandViewModel: OperandViewModel; }) { - const identifiersOptions: AstViewModel[] = useMemo( + const identifiersOptions: LabelledAst[] = useMemo( () => [ ...builder.identifiers.databaseAccessors.map( - adaptEditorIdentifierToViewModel + adaptLabelledAstFromIdentifier ), ...builder.identifiers.payloadAccessors.map( - adaptEditorIdentifierToViewModel + adaptLabelledAstFromIdentifier ), ...builder.identifiers.customListAccessors.map( - adaptEditorIdentifierToViewModel + adaptLabelledAstFromIdentifier ), ], [builder.identifiers] @@ -39,21 +39,20 @@ export function OperandEditor({ (search: string) => { if (!search) return identifiersOptions; const constantNode = coerceToConstant(search); - return [...identifiersOptions, adaptAstNodeToViewModel(constantNode)]; + return [...identifiersOptions, adaptLabelledAst(constantNode)]; }, [identifiersOptions] ); const [inputValue, setInputValue] = useState( - adaptAstNodeToViewModel(adaptAstNodeFromEditorViewModel(operandViewModel)) - .label + adaptLabelledAst(adaptAstNodeFromEditorViewModel(operandViewModel)).label ); const items = getIdentifierOptions(inputValue); const filteredItems = items.filter((item) => item.label.includes(inputValue)); - const [selectedItem, setSelectedItem] = useState(null); + const [selectedItem, setSelectedItem] = useState(null); return ( diff --git a/packages/app-builder/src/components/AstBuilder/TwoOerandsLine/OperatorEditor.tsx b/packages/app-builder/src/components/AstBuilder/TwoOerandsLine/OperatorEditor.tsx index 527102177..507d65cad 100644 --- a/packages/app-builder/src/components/AstBuilder/TwoOerandsLine/OperatorEditor.tsx +++ b/packages/app-builder/src/components/AstBuilder/TwoOerandsLine/OperatorEditor.tsx @@ -16,7 +16,6 @@ const operatorEditorFunctions = [ '>', '*', '/', - '/', 'IsInList', ] as const; type OperatorEditorFunctions = (typeof operatorEditorFunctions)[number]; @@ -95,7 +94,6 @@ export function useGetOperatorName() { if (operatorName === '*') return '×'; if (operatorName === '/') return '÷'; - if (operatorName === '/') return '÷'; if (operatorName === 'IsInList') return t('scenarios:operator.is_in'); // eslint-disable-next-line no-restricted-properties diff --git a/packages/app-builder/src/components/Edit/EditAstNode.tsx b/packages/app-builder/src/components/Edit/EditAstNode.tsx index 97a40efcd..a81d40217 100644 --- a/packages/app-builder/src/components/Edit/EditAstNode.tsx +++ b/packages/app-builder/src/components/Edit/EditAstNode.tsx @@ -1,5 +1,5 @@ import { - adaptAstNodeToViewModelFromIdentifier, + adaptLabelledAstFromAllIdentifiers, type AstNode, NewUndefinedAstNode, } from '@app-builder/models'; @@ -104,7 +104,7 @@ const EditOperand = forwardRef< const editorIdentifier = useEditorIdentifiers(); const getIdentifierOptions = useGetIdentifierOptions(); const selectedItem = value - ? adaptAstNodeToViewModelFromIdentifier(value, editorIdentifier) + ? adaptLabelledAstFromAllIdentifiers(value, editorIdentifier) : null; const [inputValue, setInputValue] = useState(selectedItem?.label ?? ''); diff --git a/packages/app-builder/src/components/Scenario/Formula/Operators/Identifier.tsx b/packages/app-builder/src/components/Scenario/Formula/Operators/Identifier.tsx index 89b556a3d..9674c2c20 100644 --- a/packages/app-builder/src/components/Scenario/Formula/Operators/Identifier.tsx +++ b/packages/app-builder/src/components/Scenario/Formula/Operators/Identifier.tsx @@ -1,5 +1,5 @@ import { - adaptAstNodeToViewModelFromIdentifier, + adaptLabelledAstFromAllIdentifiers, type AstNode, } from '@app-builder/models'; import { useEditorIdentifiers } from '@app-builder/services/editor'; @@ -13,11 +13,8 @@ interface CustomListProps { } export function Identifier({ node, isRoot }: CustomListProps) { - const editorIdentifier = useEditorIdentifiers(); - const viewModel = adaptAstNodeToViewModelFromIdentifier( - node, - editorIdentifier - ); + const allIdentifiers = useEditorIdentifiers(); + const viewModel = adaptLabelledAstFromAllIdentifiers(node, allIdentifiers); return ( diff --git a/packages/app-builder/src/components/Scenario/Formula/Operators/Payload.tsx b/packages/app-builder/src/components/Scenario/Formula/Operators/Payload.tsx index 8559708de..1130d088d 100644 --- a/packages/app-builder/src/components/Scenario/Formula/Operators/Payload.tsx +++ b/packages/app-builder/src/components/Scenario/Formula/Operators/Payload.tsx @@ -1,5 +1,5 @@ import { - adaptAstNodeToViewModelFromIdentifier, + adaptLabelledAstFromAllIdentifiers, type AstNode, } from '@app-builder/models'; import { useEditorIdentifiers } from '@app-builder/services/editor'; @@ -21,10 +21,7 @@ function format(label: string) { export function Payload({ node, isRoot }: PayloadProps) { const editorIdentifier = useEditorIdentifiers(); - const viewModel = adaptAstNodeToViewModelFromIdentifier( - node, - editorIdentifier - ); + const viewModel = adaptLabelledAstFromAllIdentifiers(node, editorIdentifier); const { tooltip, inline } = format(viewModel.label); return ( diff --git a/packages/app-builder/src/models/ast-view-model.ts b/packages/app-builder/src/models/ast-view-model.ts index 4bb5e5875..7619860cc 100644 --- a/packages/app-builder/src/models/ast-view-model.ts +++ b/packages/app-builder/src/models/ast-view-model.ts @@ -11,13 +11,13 @@ import { getIdentifiersFromAstNode, } from './identifier'; -export interface AstViewModel { +export interface LabelledAst { label: string; tooltip: string; astNode: AstNode; } -export function adaptAstNodeToViewModel(astNode: AstNode): AstViewModel { +export function adaptLabelledAst(astNode: AstNode): LabelledAst { return { label: getAstNodeDisplayName(astNode), tooltip: '', @@ -25,13 +25,13 @@ export function adaptAstNodeToViewModel(astNode: AstNode): AstViewModel { }; } -export function adaptAstNodeToViewModelFromIdentifier( +export function adaptLabelledAstFromAllIdentifiers( astNode: AstNode, identifiers: EditorIdentifiersByType -): AstViewModel { +): LabelledAst { const identifier = getIdentifiersFromAstNode(astNode, identifiers); if (identifier) { - return adaptEditorIdentifierToViewModel(identifier); + return adaptLabelledAstFromIdentifier(identifier); } return { label: getAstNodeDisplayName(astNode), @@ -40,20 +40,15 @@ export function adaptAstNodeToViewModelFromIdentifier( }; } -export function adaptEditorIdentifierToViewModel( +export function adaptLabelledAstFromIdentifier( identifier: EditorIdentifier -): AstViewModel { +): LabelledAst { return { label: identifier.name, tooltip: identifier.description, astNode: identifier.node, }; } -export function adaptAstViewModelToAstNode( - astViewModel: AstViewModel -): AstNode { - return astViewModel.astNode; -} function getConstantDisplayName(constant: ConstantType) { if (constant === null) return ''; diff --git a/packages/app-builder/src/models/scenario-validation.ts b/packages/app-builder/src/models/scenario-validation.ts index ee25f795a..c8e6a9f35 100644 --- a/packages/app-builder/src/models/scenario-validation.ts +++ b/packages/app-builder/src/models/scenario-validation.ts @@ -5,7 +5,7 @@ import { } from '@marble-api'; import * as R from 'remeda'; -import type { AstNode, ConstantType } from './ast-node'; +import type { ConstantType } from './ast-node'; export type EvaluationErrorCode = | 'UNEXPECTED_ERROR' @@ -34,37 +34,30 @@ export function isUndefinedFunctionError(evaluationError: { return evaluationError.error === 'UNDEFINED_FUNCTION'; } -interface CommonNodeEvaluation { - returnValue?: ConstantType; +export interface NodeEvaluation { + returnValue: ConstantType; + errors: EvaluationError[] | null; children: NodeEvaluation[]; namedChildren: Record; } -interface ValidNodeEvaluation extends CommonNodeEvaluation { +interface ValidationSuccess { state: 'valid'; } -interface PendingNodeEvaluation extends CommonNodeEvaluation { +interface PendingValidation { state: 'pending'; } -interface InvalidNodeEvaluation extends CommonNodeEvaluation { - state: 'invalid'; +interface ValidationFailure { + state: 'fail'; errors: EvaluationError[]; } -export function NewPendingNodeEvaluation(ast: AstNode): PendingNodeEvaluation { - return { - state: 'pending', - children: ast.children.map(NewPendingNodeEvaluation), - namedChildren: R.mapValues(ast.namedChildren, NewPendingNodeEvaluation), - }; -} - -export type NodeEvaluation = - | ValidNodeEvaluation - | PendingNodeEvaluation - | InvalidNodeEvaluation; +export type Validation = + | ValidationSuccess + | PendingValidation + | ValidationFailure; export interface ScenarioValidation { errors: string[]; @@ -82,22 +75,25 @@ function adaptEvaluationError(dto: EvaluationErrorDto): EvaluationError { } function adaptNodeEvaluation(dto: NodeEvaluationDto): NodeEvaluation { - const commonNodeEvaluation = { + return { returnValue: dto.return_value, + errors: dto.errors ? dto.errors.map(adaptEvaluationError) : null, children: dto.children ? dto.children.map(adaptNodeEvaluation) : [], namedChildren: dto.named_children ? R.mapValues(dto.named_children, adaptNodeEvaluation) : {}, }; - if (dto.errors === null) { - return { ...commonNodeEvaluation, state: 'pending' }; - } else if (dto.errors.length === 0) { - return { ...commonNodeEvaluation, state: 'valid' }; +} + +export function adaptValidation(evaluation: NodeEvaluation): Validation { + if (evaluation.errors === null) { + return { state: 'pending' }; + } else if (evaluation.errors.length === 0) { + return { state: 'valid' }; } else { return { - ...commonNodeEvaluation, - state: 'invalid', - errors: dto.errors.map(adaptEvaluationError), + state: 'fail', + errors: evaluation.errors, }; } } @@ -112,16 +108,16 @@ export function adaptScenarioValidation( }; } -type NodeEvaluationErrors = NodeEvaluation & { path: string }; +type ValidationErrors = Validation & { path: string }; -export function adaptNodeEvaluationErrors( +export function adaptValidationErrors( path: string, evaluation: NodeEvaluation -): NodeEvaluationErrors[] { +): ValidationErrors[] { const childrenPathErrors = R.pipe( evaluation.children, R.map.indexed((child, index) => { - return adaptNodeEvaluationErrors(`${path}.children.${index}`, child); + return adaptValidationErrors(`${path}.children.${index}`, child); }), R.flatten() ); @@ -129,12 +125,12 @@ export function adaptNodeEvaluationErrors( const namedChildrenPathErrors = R.pipe( R.toPairs(evaluation.namedChildren), R.flatMap(([key, child]) => { - return adaptNodeEvaluationErrors(`${path}.namedChildren.${key}`, child); + return adaptValidationErrors(`${path}.namedChildren.${key}`, child); }) ); return [ - { path, ...evaluation }, + { ...adaptValidation(evaluation), path }, ...childrenPathErrors, ...namedChildrenPathErrors, ]; diff --git a/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/edit.rules.$ruleId.tsx b/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/edit.rules.$ruleId.tsx index 6733ea6b6..aaf6e202f 100644 --- a/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/edit.rules.$ruleId.tsx +++ b/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/edit.rules.$ruleId.tsx @@ -6,7 +6,7 @@ import { } from '@app-builder/components'; import { EditAstNode, RootOrOperator } from '@app-builder/components/Edit'; import { setToastMessage } from '@app-builder/components/MarbleToaster'; -import { adaptNodeEvaluationErrors, type AstNode } from '@app-builder/models'; +import { adaptValidationErrors, type AstNode } from '@app-builder/models'; import { EditRule } from '@app-builder/routes/ressources/scenarios/$scenarioId/$iterationId/rules/$ruleId/edit'; import { DeleteRule } from '@app-builder/routes/ressources/scenarios/$scenarioId/$iterationId/rules/delete'; import { @@ -154,12 +154,12 @@ export default function RuleEdit() { const { setError } = formMethods; const getNodeEvaluationErrorMessage = useGetNodeEvaluationErrorMessage(); useEffect(() => { - const allEvaluationErrors = adaptNodeEvaluationErrors( + const allEvaluationErrors = adaptValidationErrors( 'astNode', ruleValidation ); allEvaluationErrors.forEach((flattenNodeEvaluationErrors) => { - if (flattenNodeEvaluationErrors.state === 'invalid') { + if (flattenNodeEvaluationErrors.state === 'fail') { const firstError = flattenNodeEvaluationErrors.errors[0]; //@ts-expect-error path is a string setError(flattenNodeEvaluationErrors.path, { diff --git a/packages/app-builder/src/services/editor/ast-editor.ts b/packages/app-builder/src/services/editor/ast-editor.ts index 07ab1f01e..0dd37f102 100644 --- a/packages/app-builder/src/services/editor/ast-editor.ts +++ b/packages/app-builder/src/services/editor/ast-editor.ts @@ -1,10 +1,11 @@ import { + adaptValidation, type AstNode, type AstOperator, type ConstantType, type EditorIdentifiersByType, - NewPendingNodeEvaluation, type NodeEvaluation, + type Validation, } from '@app-builder/models'; import { nanoid } from 'nanoid'; import { useCallback, useState } from 'react'; @@ -15,8 +16,7 @@ export interface EditorNodeViewModel { nodeId: string; funcName: string | null; constant?: ConstantType; - // TODO: rename validation pour quelque chose de plus parlant (error, pending, success ...) - validation: NodeEvaluation; + validation: Validation; children: EditorNodeViewModel[]; namedChildren: Record; } @@ -26,23 +26,32 @@ function adaptEditorNodeViewModel({ validation, }: { ast: AstNode; - validation: NodeEvaluation; + validation?: NodeEvaluation; }): EditorNodeViewModel { + const evaluation = validation + ? validation + : { + returnValue: null, + errors: null, + children: [], + namedChildren: {}, + }; + return { nodeId: nanoid(), funcName: ast.name, constant: ast.constant, - validation: validation, + validation: adaptValidation(evaluation), children: ast.children.map((child, i) => adaptEditorNodeViewModel({ ast: child, - validation: validation.children[i], + validation: evaluation.children[i], }) ), namedChildren: R.mapValues(ast.namedChildren, (child, namedKey) => adaptEditorNodeViewModel({ ast: child, - validation: validation.namedChildren[namedKey], + validation: evaluation.namedChildren[namedKey], }) ), }; @@ -133,7 +142,6 @@ export function useAstBuilder({ replaceOneNode(nodeId, () => { const newOperand = adaptEditorNodeViewModel({ ast: operandAst, - validation: NewPendingNodeEvaluation(operandAst), }); return newOperand; @@ -165,7 +173,6 @@ export function useAstBuilder({ replaceOneNode(nodeId, (node) => { const newChild = adaptEditorNodeViewModel({ ast: childAst, - validation: NewPendingNodeEvaluation(childAst), }); return { diff --git a/packages/app-builder/src/services/editor/identifiers.tsx b/packages/app-builder/src/services/editor/identifiers.tsx index 84dd062b0..a86ba36e8 100644 --- a/packages/app-builder/src/services/editor/identifiers.tsx +++ b/packages/app-builder/src/services/editor/identifiers.tsx @@ -1,6 +1,6 @@ import { - adaptAstNodeToViewModel, - adaptEditorIdentifierToViewModel, + adaptLabelledAst, + adaptLabelledAstFromIdentifier, NewAstNode, } from '@app-builder/models'; import { type EditorIdentifiersByType } from '@app-builder/models/identifier'; @@ -45,9 +45,9 @@ export function useGetIdentifierOptions() { const identifiers = useEditorIdentifiers(); const identifiersOptions = useMemo( () => [ - ...identifiers.databaseAccessors.map(adaptEditorIdentifierToViewModel), - ...identifiers.payloadAccessors.map(adaptEditorIdentifierToViewModel), - ...identifiers.customListAccessors.map(adaptEditorIdentifierToViewModel), + ...identifiers.databaseAccessors.map(adaptLabelledAstFromIdentifier), + ...identifiers.payloadAccessors.map(adaptLabelledAstFromIdentifier), + ...identifiers.customListAccessors.map(adaptLabelledAstFromIdentifier), ], [identifiers] ); @@ -56,7 +56,7 @@ export function useGetIdentifierOptions() { (search: string) => { if (!search) return identifiersOptions; const constantNode = coerceToConstant(search); - return [...identifiersOptions, adaptAstNodeToViewModel(constantNode)]; + return [...identifiersOptions, adaptLabelledAst(constantNode)]; }, [identifiersOptions] ); diff --git a/packages/app-builder/src/services/validation/scenario-validation.ts b/packages/app-builder/src/services/validation/scenario-validation.ts index 5544423dc..4cf22bd94 100644 --- a/packages/app-builder/src/services/validation/scenario-validation.ts +++ b/packages/app-builder/src/services/validation/scenario-validation.ts @@ -14,7 +14,7 @@ function flattenNodeEvaluationErrors( evaluation: NodeEvaluation ): EvaluationError[] { return [ - ...(evaluation.state === 'invalid' ? evaluation.errors : []), + ...(evaluation.errors ?? []), ...evaluation.children.flatMap(flattenNodeEvaluationErrors), ...Object.values(evaluation.namedChildren).flatMap( flattenNodeEvaluationErrors