From a24253687d9f58adcec70b0eb6eba952c5d5646e Mon Sep 17 00:00:00 2001 From: Thomas Lathuiliere <40292402+balzdur@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:28:30 +0200 Subject: [PATCH] feat(validation): better evaluation errors (#226) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: better evaluation errors (#211) * feat: disable activate button if errors (#214) * feat(ScenarioValidationError): aggregate errors (#213) * feat(ScenarioValidation): remove error code from error message * refactor(EvaluationError): use code * feat(ScenarioValidationError): aggregate errors * refactor(evaluation): refactor evaluation errors (#216) * refactor(evaluation): refactor evaluation errors * misc fixes --------- Co-authored-by: Zoé Cadé --------- Co-authored-by: Zoé Cadé --- .../app-builder/public/locales/en/common.json | 2 +- .../public/locales/en/scenarios.json | 41 +++-- .../AggregationEdit/EditFilters.tsx | 16 ++ .../AstBuilderNode/Operand/Operand.tsx | 11 +- .../TwoOperandsLine/TwoOperandsLine.tsx | 26 +-- .../Scenario/AstBuilder/ErrorMessage.tsx | 14 +- .../AstBuilder/RootAstBuilderNode/RootAnd.tsx | 17 +- .../RootAstBuilderNode/RootOrWithAnd.tsx | 27 ++- ...oError.tsx => ScenarioValidationError.tsx} | 0 .../i/$iterationId/__edit-view.tsx | 13 +- .../i/$iterationId/__edit-view/decision.tsx | 2 +- .../i/$iterationId/__edit-view/rules.tsx | 2 +- .../i/$iterationId/__edit-view/trigger.tsx | 2 +- .../i/$iterationId/rules.$ruleId.tsx | 2 +- .../ressources/scenarios/deployment.tsx | 60 +++++- .../src/services/editor/ast-editor.ts | 47 ++++- .../scenario-validation-error-messages.ts | 172 ++++++++++++++++-- packages/marble-api/scripts/openapi.yaml | 5 + .../marble-api/src/generated/marble-api.ts | 2 +- 19 files changed, 379 insertions(+), 82 deletions(-) rename packages/app-builder/src/components/Scenario/{ScenarioValidatioError.tsx => ScenarioValidationError.tsx} (100%) diff --git a/packages/app-builder/public/locales/en/common.json b/packages/app-builder/public/locales/en/common.json index 474d92454..ca2088b49 100644 --- a/packages/app-builder/public/locales/en/common.json +++ b/packages/app-builder/public/locales/en/common.json @@ -12,7 +12,7 @@ "errors.data.duplicate_field_name": "A field with this name already exist", "errors.data.duplicate_table_name": "A table with this name already exist", "errors.data.duplicate_link_name": "A link with this name already exist", - "errors.draft.invalid": "Invalid draft. Please check your draft for error messages, fix them and try again.", + "errors.draft.invalid": "This draft can't be published because it contains errors.", "cancel": "Cancel", "save": "Save", "delete": "Delete", diff --git a/packages/app-builder/public/locales/en/scenarios.json b/packages/app-builder/public/locales/en/scenarios.json index d3ad13916..7413b1a43 100644 --- a/packages/app-builder/public/locales/en/scenarios.json +++ b/packages/app-builder/public/locales/en/scenarios.json @@ -99,18 +99,37 @@ "validation.decision.score_review_threshold_required": "Required", "validation.decision.score_reject_threshold_required": "Required", "validation.decision.score_reject_review_thresholds_missmatch": "Reject threshold must be greater than review threshold", - "validation.evaluation_error.unknown_function": "required", + "validation.evaluation_error.undefined_function_one": "required", + "validation.evaluation_error.undefined_function_other": "{{count}} required", "validation.evaluation_error.wrong_number_of_arguments": "wrong number of arguments", - "validation.evaluation_error.missing_named_argument": "missing named argument", - "validation.evaluation_error.arguments_must_be_int_or_float": "arguments must be an integer or a float", - "validation.evaluation_error.argument_must_be_integer": "argument must be an integer", - "validation.evaluation_error.argument_must_be_string": "argument must be a string", - "validation.evaluation_error.argument_must_be_boolean": "argument must be a boolean", - "validation.evaluation_error.argument_must_be_list": "argument must be a list", - "validation.evaluation_error.argument_must_be_convertible_to_duration": "argument must be a duration", - "validation.evaluation_error.argument_must_be_time": "argument must be a time", - "validation.evaluation_error.argument_required": "argument is required", - "validation.evaluation_error.function_error": "function is incorrect", + "validation.evaluation_error.missing_named_argument_one": "missing named argument", + "validation.evaluation_error.missing_named_argument_other": "{{count}} missing named arguments", + "validation.evaluation_error.arguments_must_be_int_or_float_one": "argument must be an integer or a float", + "validation.evaluation_error.arguments_must_be_int_or_float_other": "{{count}} arguments must be integers or floats", + "validation.evaluation_error.arguments_must_be_int_float_or_time_one": "argument must be an integer, a float or a time", + "validation.evaluation_error.arguments_must_be_int_float_or_time_other": "{{count}} arguments must be integers, floats or times", + "validation.evaluation_error.argument_must_be_integer_one": "argument must be an integer", + "validation.evaluation_error.argument_must_be_integer_other": "{{count}} arguments must be integers", + "validation.evaluation_error.argument_must_be_string_one": "argument must be a string", + "validation.evaluation_error.argument_must_be_string_other": "{{count}} arguments must be strings", + "validation.evaluation_error.argument_must_be_boolean_one": "argument must be a boolean", + "validation.evaluation_error.argument_must_be_boolean_other": "{{count}} arguments must be booleans", + "validation.evaluation_error.argument_must_be_list_one": "argument must be a list", + "validation.evaluation_error.argument_must_be_list_other": "{{count}} arguments must be lists", + "validation.evaluation_error.argument_must_be_convertible_to_duration_one": "argument must be a duration", + "validation.evaluation_error.argument_must_be_convertible_to_duration_other": "{{count}} arguments must be durations", + "validation.evaluation_error.argument_must_be_time_one": "argument must be a time", + "validation.evaluation_error.argument_must_be_time_other": "{{count}} arguments must be times", + "validation.evaluation_error.argument_required_one": "argument is required", + "validation.evaluation_error.argument_required_other": "{{count}} arguments are required", + "validation.evaluation_error.argument_invalid_type_one": "argument has an invalid type", + "validation.evaluation_error.argument_invalid_type_other": "{{count}} arguments have invalid types", + "validation.evaluation_error.list_not_found_one": "list not found", + "validation.evaluation_error.list_not_found_other": "{{count}} lists not found", + "validation.evaluation_error.field_not_found_one": "field not found", + "validation.evaluation_error.field_not_found_other": "{{count}} fields not found", + "validation.evaluation_error.function_error_one": "function contains errors", + "validation.evaluation_error.function_error_other": "{{count}} functions contain error", "edit_aggregation.title": "Create a variable", "edit_aggregation.subtitle": "From Marble database", "edit_aggregation.label_title": "Variable name", diff --git a/packages/app-builder/src/components/Scenario/AstBuilder/AstBuilderNode/AggregationEdit/EditFilters.tsx b/packages/app-builder/src/components/Scenario/AstBuilder/AstBuilderNode/AggregationEdit/EditFilters.tsx index 250c77e84..fa1b77ca1 100644 --- a/packages/app-builder/src/components/Scenario/AstBuilder/AstBuilderNode/AggregationEdit/EditFilters.tsx +++ b/packages/app-builder/src/components/Scenario/AstBuilder/AstBuilderNode/AggregationEdit/EditFilters.tsx @@ -1,9 +1,14 @@ import { scenarioI18n } from '@app-builder/components/Scenario'; +import { ScenarioValidationError } from '@app-builder/components/Scenario/ScenarioValidationError'; import { NewUndefinedAstNode } from '@app-builder/models'; import { adaptEditorNodeViewModel, type AstBuilder, } from '@app-builder/services/editor/ast-editor'; +import { + adaptEvaluationErrorViewModels, + useGetNodeEvaluationErrorMessage, +} from '@app-builder/services/validation'; import { Button } from '@ui-design-system'; import { Plus } from '@ui-icons'; import { useTranslation } from 'react-i18next'; @@ -36,6 +41,7 @@ export const EditFilters = ({ value: FilterViewModel[]; }) => { const { t } = useTranslation(scenarioI18n); + const getNodeEvaluationErrorMessage = useGetNodeEvaluationErrorMessage(); const filteredDataModalFieldOptions = aggregatedField?.tableName ? dataModelFieldOptions.filter( @@ -80,6 +86,9 @@ export const EditFilters = ({
{value.map((filter, filterIndex) => { + const valueErrorMessages = adaptEvaluationErrorViewModels( + filter.value.errors + ).map((error) => getNodeEvaluationErrorMessage(error)); return (
@@ -120,6 +129,13 @@ export const EditFilters = ({ {filter.errors.filter.length > 0 && ( )} +
+ {valueErrorMessages.map((error) => ( + + {error} + + ))} +
); })} diff --git a/packages/app-builder/src/components/Scenario/AstBuilder/AstBuilderNode/Operand/Operand.tsx b/packages/app-builder/src/components/Scenario/AstBuilder/AstBuilderNode/Operand/Operand.tsx index 749a5f44d..688191889 100644 --- a/packages/app-builder/src/components/Scenario/AstBuilder/AstBuilderNode/Operand/Operand.tsx +++ b/packages/app-builder/src/components/Scenario/AstBuilder/AstBuilderNode/Operand/Operand.tsx @@ -26,9 +26,7 @@ export const computeOperandErrors = ( viewModel: EditorNodeViewModel ): EvaluationError[] => { if (viewModel.funcName && functionNodeNames.includes(viewModel.funcName)) { - return hasNestedErrors(viewModel) - ? [{ error: 'FUNCTION_ERROR', message: 'function has error' }] - : []; + return viewModel.errors.filter((error) => error.argumentName === undefined); } else { return [ ...viewModel.errors, @@ -38,13 +36,6 @@ export const computeOperandErrors = ( } }; -function hasNestedErrors(viewModel: EditorNodeViewModel): boolean { - if (viewModel.errors.length > 0) return true; - if (viewModel.children.some(hasNestedErrors)) return true; - if (Object.values(viewModel.namedChildren).some(hasNestedErrors)) return true; - return false; -} - export function isEditableOperand(node: AstNode): boolean { return ( isConstant(node) || diff --git a/packages/app-builder/src/components/Scenario/AstBuilder/AstBuilderNode/TwoOperandsLine/TwoOperandsLine.tsx b/packages/app-builder/src/components/Scenario/AstBuilder/AstBuilderNode/TwoOperandsLine/TwoOperandsLine.tsx index 842f8e87c..cfd9f54e5 100644 --- a/packages/app-builder/src/components/Scenario/AstBuilder/AstBuilderNode/TwoOperandsLine/TwoOperandsLine.tsx +++ b/packages/app-builder/src/components/Scenario/AstBuilder/AstBuilderNode/TwoOperandsLine/TwoOperandsLine.tsx @@ -1,11 +1,14 @@ -import { ScenarioValidationError } from '@app-builder/components/Scenario/ScenarioValidatioError'; -import { type EvaluationError } from '@app-builder/models'; +import { ScenarioValidationError } from '@app-builder/components/Scenario/ScenarioValidationError'; import { type AstBuilder, type EditorNodeViewModel, findArgumentIndexErrorsFromParent, } from '@app-builder/services/editor/ast-editor'; -import { useGetNodeEvaluationErrorMessage } from '@app-builder/services/validation'; +import { + adaptEvaluationErrorViewModels, + type EvaluationErrorViewModel, + useGetNodeEvaluationErrorMessage, +} from '@app-builder/services/validation'; import { computeOperandErrors, @@ -22,7 +25,7 @@ interface TwoOperandsLineViewModel { left: OperandViewModel; operator: OperatorViewModel; right: OperandViewModel; - errors: EvaluationError[]; + errors: EvaluationErrorViewModel[]; } export function TwoOperandsLine({ @@ -36,6 +39,10 @@ export function TwoOperandsLine({ }) { const getNodeEvaluationErrorMessage = useGetNodeEvaluationErrorMessage(); + const errorMessages = twoOperandsViewModel.errors.map((error) => + getNodeEvaluationErrorMessage(error) + ); + return (
@@ -67,11 +74,8 @@ export function TwoOperandsLine({ />
- {twoOperandsViewModel.errors.map((error, index) => ( - // TODO: find a better way to compute error key (flatten errors make it hard) - - {getNodeEvaluationErrorMessage(error)} - + {errorMessages.map((error) => ( + {error} ))}
@@ -93,11 +97,11 @@ export function adaptTwoOperandsLineViewModel( left, operator: operatorVm, right, - errors: [ + errors: adaptEvaluationErrorViewModels([ ...computeOperandErrors(left), ...vm.errors, ...computeOperandErrors(right), ...findArgumentIndexErrorsFromParent(vm), - ], + ]), }; } diff --git a/packages/app-builder/src/components/Scenario/AstBuilder/ErrorMessage.tsx b/packages/app-builder/src/components/Scenario/AstBuilder/ErrorMessage.tsx index 8f6aa412f..3f242338b 100644 --- a/packages/app-builder/src/components/Scenario/AstBuilder/ErrorMessage.tsx +++ b/packages/app-builder/src/components/Scenario/AstBuilder/ErrorMessage.tsx @@ -1,10 +1,16 @@ import { type EvaluationError } from '@app-builder/models'; -import { useGetNodeEvaluationErrorMessage } from '@app-builder/services/validation'; +import { + adaptEvaluationErrorViewModels, + useGetNodeEvaluationErrorMessage, +} from '@app-builder/services/validation'; export interface ErrorMessageProps { errors?: EvaluationError[]; } +/** + * @deprecated Use ScenarioValidationError instead + */ export function ErrorMessage({ errors }: ErrorMessageProps) { const getNodeEvaluationErrorMessage = useGetNodeEvaluationErrorMessage(); @@ -12,7 +18,11 @@ export function ErrorMessage({ errors }: ErrorMessageProps) { return (

- {firstError && getNodeEvaluationErrorMessage(firstError)} + {firstError && + getNodeEvaluationErrorMessage( + // glitch for ISO compatibility with former code + adaptEvaluationErrorViewModels([firstError])[0] + )}

); } diff --git a/packages/app-builder/src/components/Scenario/AstBuilder/RootAstBuilderNode/RootAnd.tsx b/packages/app-builder/src/components/Scenario/AstBuilder/RootAstBuilderNode/RootAnd.tsx index 43c5fc018..96f0a13b4 100644 --- a/packages/app-builder/src/components/Scenario/AstBuilder/RootAstBuilderNode/RootAnd.tsx +++ b/packages/app-builder/src/components/Scenario/AstBuilder/RootAstBuilderNode/RootAnd.tsx @@ -9,11 +9,14 @@ import { type EditorNodeViewModel, hasArgumentIndexErrorsFromParent, } from '@app-builder/services/editor/ast-editor'; -import { useGetOrAndNodeEvaluationErrorMessage } from '@app-builder/services/validation'; +import { + adaptEvaluationErrorViewModels, + useGetOrAndNodeEvaluationErrorMessage, +} from '@app-builder/services/validation'; import clsx from 'clsx'; import { Fragment } from 'react'; -import { ScenarioValidationError } from '../../ScenarioValidatioError'; +import { ScenarioValidationError } from '../../ScenarioValidationError'; import { AstBuilderNode } from '../AstBuilderNode/AstBuilderNode'; import { RemoveButton } from '../RemoveButton'; import { AddLogicalOperatorButton } from './AddLogicalOperatorButton'; @@ -60,6 +63,10 @@ export function RootAnd({ rootAndViewModel.errors ); + const andErrorMessages = adaptEvaluationErrorViewModels( + andNonChildrenErrors + ).map(getEvaluationErrorMessage); + function appendAndChild() { builder.appendChild(rootAndViewModel.nodeId, NewAndChild()); } @@ -143,10 +150,8 @@ export function RootAnd({ {!viewOnly && ( )} - {andNonChildrenErrors.map((error, index) => ( - - {getEvaluationErrorMessage(error)} - + {andErrorMessages.map((error) => ( + {error} ))}
diff --git a/packages/app-builder/src/components/Scenario/AstBuilder/RootAstBuilderNode/RootOrWithAnd.tsx b/packages/app-builder/src/components/Scenario/AstBuilder/RootAstBuilderNode/RootOrWithAnd.tsx index 7eb71835b..a61303073 100644 --- a/packages/app-builder/src/components/Scenario/AstBuilder/RootAstBuilderNode/RootOrWithAnd.tsx +++ b/packages/app-builder/src/components/Scenario/AstBuilder/RootAstBuilderNode/RootOrWithAnd.tsx @@ -10,11 +10,14 @@ import { type EditorNodeViewModel, hasArgumentIndexErrorsFromParent, } from '@app-builder/services/editor/ast-editor'; -import { useGetOrAndNodeEvaluationErrorMessage } from '@app-builder/services/validation'; +import { + adaptEvaluationErrorViewModels, + useGetOrAndNodeEvaluationErrorMessage, +} from '@app-builder/services/validation'; import clsx from 'clsx'; import React from 'react'; -import { ScenarioValidationError } from '../../ScenarioValidatioError'; +import { ScenarioValidationError } from '../../ScenarioValidationError'; import { AstBuilderNode } from '../AstBuilderNode/AstBuilderNode'; import { RemoveButton } from '../RemoveButton'; import { AddLogicalOperatorButton } from './AddLogicalOperatorButton'; @@ -82,6 +85,10 @@ export function RootOrWithAnd({ rootOrWithAndViewModel.orErrors ); + const rootOrErrorMessages = adaptEvaluationErrorViewModels( + rootOrNonChildrenErrors + ).map(getEvaluationErrorMessage); + return (
{rootOrWithAndViewModel.ands.map((andChild, childIndex) => { @@ -90,6 +97,10 @@ export function RootOrWithAnd({ andChild.errors ); + const andErrorMessages = adaptEvaluationErrorViewModels( + andNonChildrenErrors + ).map(getEvaluationErrorMessage); + function appendAndChild() { builder.appendChild(andChild.nodeId, NewAndChild()); } @@ -157,9 +168,9 @@ export function RootOrWithAnd({
)} - {andNonChildrenErrors.map((error, index) => ( - - {getEvaluationErrorMessage(error)} + {andErrorMessages.map((error) => ( + + {error} ))}
@@ -172,10 +183,8 @@ export function RootOrWithAnd({ )} - {rootOrNonChildrenErrors.map((error, index) => ( - - {getEvaluationErrorMessage(error)} - + {rootOrErrorMessages.map((error) => ( + {error} ))}
diff --git a/packages/app-builder/src/components/Scenario/ScenarioValidatioError.tsx b/packages/app-builder/src/components/Scenario/ScenarioValidationError.tsx similarity index 100% rename from packages/app-builder/src/components/Scenario/ScenarioValidatioError.tsx rename to packages/app-builder/src/components/Scenario/ScenarioValidationError.tsx diff --git a/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view.tsx b/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view.tsx index 172e059b7..020559f9e 100644 --- a/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view.tsx +++ b/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view.tsx @@ -9,7 +9,7 @@ import { VersionSelect } from '@app-builder/components/Scenario/Iteration/Versio import { sortScenarioIterations } from '@app-builder/models/scenario-iteration'; import { useCurrentScenario } from '@app-builder/routes/__builder/scenarios/$scenarioId'; import { CreateDraftIteration } from '@app-builder/routes/ressources/scenarios/$scenarioId/$iterationId/create_draft'; -import { DeploymentModal } from '@app-builder/routes/ressources/scenarios/deployment'; +import { DeploymentActions } from '@app-builder/routes/ressources/scenarios/deployment'; import { useEditorMode } from '@app-builder/services/editor'; import { serverServices } from '@app-builder/services/init.server'; import { @@ -76,7 +76,7 @@ export default function ScenarioEditLayout() { const withEditTag = editorMode === 'edit'; const withCreateDraftIteration = canManageScenario && currentIteration.type !== 'draft'; - const withDeploymentModal = canPublishScenario; + const withDeploymentActions = canPublishScenario; return ( @@ -104,11 +104,16 @@ export default function ScenarioEditLayout() { draftId={draftIteration?.id} /> )} - {withDeploymentModal && ( - )} diff --git a/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view/decision.tsx b/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view/decision.tsx index 0c041c474..e9d4298fd 100644 --- a/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view/decision.tsx +++ b/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view/decision.tsx @@ -13,7 +13,7 @@ import { FormLabel, } from '@app-builder/components/Form'; import { setToastMessage } from '@app-builder/components/MarbleToaster'; -import { ScenarioValidationError } from '@app-builder/components/Scenario/ScenarioValidatioError'; +import { ScenarioValidationError } from '@app-builder/components/Scenario/ScenarioValidationError'; import { useCurrentScenarioIteration, useEditorMode, diff --git a/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view/rules.tsx b/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view/rules.tsx index 5f2eaa4c5..dddfcc60c 100644 --- a/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view/rules.tsx +++ b/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view/rules.tsx @@ -1,5 +1,5 @@ import { Ping } from '@app-builder/components/Ping'; -import { ScenarioValidationError } from '@app-builder/components/Scenario/ScenarioValidatioError'; +import { ScenarioValidationError } from '@app-builder/components/Scenario/ScenarioValidationError'; import { CreateRule } from '@app-builder/routes/ressources/scenarios/$scenarioId/$iterationId/rules/create'; import { useEditorMode } from '@app-builder/services/editor'; import { serverServices } from '@app-builder/services/init.server'; diff --git a/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view/trigger.tsx b/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view/trigger.tsx index 19fe31c4e..3c5594bdd 100644 --- a/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view/trigger.tsx +++ b/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/__edit-view/trigger.tsx @@ -6,7 +6,7 @@ import { } from '@app-builder/components'; import { setToastMessage } from '@app-builder/components/MarbleToaster'; import { AstBuilder } from '@app-builder/components/Scenario/AstBuilder'; -import { ScenarioValidationError } from '@app-builder/components/Scenario/ScenarioValidatioError'; +import { ScenarioValidationError } from '@app-builder/components/Scenario/ScenarioValidationError'; import { ScheduleOption } from '@app-builder/components/Scenario/Trigger'; import { adaptDataModelDto, diff --git a/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/rules.$ruleId.tsx b/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/rules.$ruleId.tsx index ef78506eb..6d38c4223 100644 --- a/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/rules.$ruleId.tsx +++ b/packages/app-builder/src/routes/__builder/scenarios/$scenarioId/i/$iterationId/rules.$ruleId.tsx @@ -13,7 +13,7 @@ import { } from '@app-builder/components/Form'; import { setToastMessage } from '@app-builder/components/MarbleToaster'; import { AstBuilder } from '@app-builder/components/Scenario/AstBuilder'; -import { ScenarioValidationError } from '@app-builder/components/Scenario/ScenarioValidatioError'; +import { ScenarioValidationError } from '@app-builder/components/Scenario/ScenarioValidationError'; import { type AstNode, NewEmptyRuleAstNode, diff --git a/packages/app-builder/src/routes/ressources/scenarios/deployment.tsx b/packages/app-builder/src/routes/ressources/scenarios/deployment.tsx index 579d8e624..c44e505e9 100644 --- a/packages/app-builder/src/routes/ressources/scenarios/deployment.tsx +++ b/packages/app-builder/src/routes/ressources/scenarios/deployment.tsx @@ -12,6 +12,7 @@ import { Checkbox, HiddenInputs, Modal, + Tooltip, } from '@ui-design-system'; import { Play, Pushtolive, Stop, Tick } from '@ui-icons'; import { type Namespace, type ParseKeys } from 'i18next'; @@ -256,20 +257,19 @@ function ModalContent({ ); } -export function DeploymentModal({ +const DeploymentModal = ({ scenarioId, liveVersionId, currentIteration, + deploymentType, }: { scenarioId: string; liveVersionId?: string; currentIteration: SortedScenarioIteration; -}) { + deploymentType: DeploymentType; +}) => { const { t } = useTranslation(handle.i18n); - - const deploymentType = getDeploymentType(currentIteration.type); const buttonConfig = getButtonConfig(deploymentType); - return ( @@ -287,6 +287,56 @@ export function DeploymentModal({ ); +}; + +const DisabledDeploymentButton = ({ + deploymentType, +}: { + deploymentType: DeploymentType; +}) => { + const { t } = useTranslation(handle.i18n); + const buttonConfig = getButtonConfig(deploymentType); + return ( + + + + ); +}; + +export function DeploymentActions({ + scenarioId, + liveVersionId, + currentIteration, + hasScenarioErrors, +}: { + scenarioId: string; + liveVersionId?: string; + currentIteration: SortedScenarioIteration; + hasScenarioErrors: boolean; +}) { + const deploymentType = getDeploymentType(currentIteration.type); + + return ( + <> + {hasScenarioErrors && + ['activate', 'deactivate'].includes(deploymentType) ? ( + + ) : ( + + )} + + ); } function getDeploymentType( diff --git a/packages/app-builder/src/services/editor/ast-editor.ts b/packages/app-builder/src/services/editor/ast-editor.ts index 8e98aaaf1..d316d95be 100644 --- a/packages/app-builder/src/services/editor/ast-editor.ts +++ b/packages/app-builder/src/services/editor/ast-editor.ts @@ -5,6 +5,7 @@ import { type EditorIdentifiersByType, type EvaluationError, findDataModelTableByName, + functionNodeNames, type NodeEvaluation, type TableModel, } from '@app-builder/models'; @@ -47,10 +48,11 @@ export function adaptEditorNodeViewModel({ parent: parent ?? null, funcName: ast.name, constant: ast.constant, - errors: evaluation.errors ?? [], + errors: computeEvaluationErrors(ast.name, evaluation), children: [], namedChildren: {}, }; + currentNode.children = ast.children.map((child, i) => adaptEditorNodeViewModel({ ast: child, @@ -339,7 +341,7 @@ function updateValidation({ const currentNode: EditorNodeViewModel = { ...editorNodeViewModel, - errors: validation.errors ?? [], + errors: computeEvaluationErrors(editorNodeViewModel.funcName, validation), parent: parent ?? null, children: [], namedChildren: {}, @@ -357,8 +359,49 @@ function updateValidation({ updateValidation({ editorNodeViewModel: child, validation: validation.namedChildren[namedKey], + parent: currentNode, }) ); return currentNode; } + +const computeEvaluationErrors = ( + funcName: EditorNodeViewModel['funcName'], + validation: NodeEvaluation +): EvaluationError[] => { + const errors: EvaluationError[] = []; + if (validation.errors) { + errors.push(...validation.errors); + } + if ( + funcName && + functionNodeNames.includes(funcName) && + hasNestedErrors(validation) + ) { + errors.push({ error: 'FUNCTION_ERROR', message: 'function has error' }); + } + + return errors; +}; + +function hasNestedErrors(validation: NodeEvaluation): boolean { + if (validation.errors && validation.errors.length > 0) { + return true; + } + if ( + validation.children.some((childValidation) => + hasNestedErrors(childValidation) + ) + ) { + return true; + } + if ( + Object.values(validation.namedChildren).some((namedChildValidation) => + hasNestedErrors(namedChildValidation) + ) + ) { + return true; + } + return false; +} diff --git a/packages/app-builder/src/services/validation/scenario-validation-error-messages.ts b/packages/app-builder/src/services/validation/scenario-validation-error-messages.ts index f84d3f205..bac19eff1 100644 --- a/packages/app-builder/src/services/validation/scenario-validation-error-messages.ts +++ b/packages/app-builder/src/services/validation/scenario-validation-error-messages.ts @@ -4,12 +4,97 @@ import { assertNever } from '@typescript-utils'; import { type TFunction } from 'i18next'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import * as R from 'remeda'; + +// Edit this type to handle contextual data for each error code +export type EvaluationErrorViewModel = + | { + error: 'UNEXPECTED_ERROR'; + message: string; + } + | { + error: + | 'UNDEFINED_FUNCTION' + | 'MISSING_NAMED_ARGUMENT' + | 'ARGUMENTS_MUST_BE_INT_OR_FLOAT' + | 'ARGUMENTS_MUST_BE_INT_FLOAT_OR_TIME' + | 'ARGUMENT_MUST_BE_INTEGER' + | 'ARGUMENT_MUST_BE_STRING' + | 'ARGUMENT_MUST_BE_BOOLEAN' + | 'ARGUMENT_MUST_BE_LIST' + | 'ARGUMENT_MUST_BE_CONVERTIBLE_TO_DURATION' + | 'ARGUMENT_MUST_BE_TIME' + | 'FUNCTION_ERROR' + | 'ARGUMENT_INVALID_TYPE' + | 'LIST_NOT_FOUND' + | 'FIELD_NOT_FOUND' + | 'ARGUMENT_REQUIRED'; + count: number; + } + | { + error: 'WRONG_NUMBER_OF_ARGUMENTS'; + }; + +export function adaptEvaluationErrorViewModels( + evaluationErrors: EvaluationError[] +): EvaluationErrorViewModel[] { + const { + UNEXPECTED_ERROR, + WRONG_NUMBER_OF_ARGUMENTS, + DATABASE_ACCESS_NOT_FOUND, + PAYLOAD_FIELD_NOT_FOUND, + ...expectedErrors + } = R.groupBy.strict(evaluationErrors, ({ error }) => error); + + const evaluationErrorVMs: EvaluationErrorViewModel[] = []; + + if (UNEXPECTED_ERROR) { + const unexpectedErrorVMs = R.pipe( + UNEXPECTED_ERROR, + R.map((error) => ({ + error: 'UNEXPECTED_ERROR' as const, + message: error.message, + })) + ); + + evaluationErrorVMs.push(...unexpectedErrorVMs); + } + + if (WRONG_NUMBER_OF_ARGUMENTS) { + evaluationErrorVMs.push({ + error: 'WRONG_NUMBER_OF_ARGUMENTS', + }); + } + + const FIELD_NOT_FOUND = [ + ...(PAYLOAD_FIELD_NOT_FOUND ?? []), + ...(DATABASE_ACCESS_NOT_FOUND ?? []), + ]; + if (FIELD_NOT_FOUND.length > 0) { + evaluationErrorVMs.push({ + error: 'FIELD_NOT_FOUND', + count: FIELD_NOT_FOUND.length, + }); + } + + const expectedErrorVMs = R.pipe( + expectedErrors, + R.toPairs.strict, + R.map(([error, evaluationErrors]) => ({ + error, + count: evaluationErrors.length, + })) + ); + evaluationErrorVMs.push(...expectedErrorVMs); + + return evaluationErrorVMs; +} export function useGetNodeEvaluationErrorMessage() { const { t } = useTranslation(['scenarios']); return useCallback( - (evaluationError: EvaluationError) => + (evaluationError: EvaluationErrorViewModel) => commonErrorMessages(t)(evaluationError), [t] ); @@ -19,7 +104,7 @@ export function useGetOrAndNodeEvaluationErrorMessage() { const { t } = useTranslation(['scenarios']); return useCallback( - (evaluationError: EvaluationError) => { + (evaluationError: EvaluationErrorViewModel) => { switch (evaluationError.error) { case 'WRONG_NUMBER_OF_ARGUMENTS': return t('scenarios:validation.decision.rule_formula_required'); @@ -32,52 +117,107 @@ export function useGetOrAndNodeEvaluationErrorMessage() { } const commonErrorMessages = - (t: TFunction<['scenarios']>) => (evaluationError: EvaluationError) => { + (t: TFunction<['scenarios']>) => + (evaluationError: EvaluationErrorViewModel) => { switch (evaluationError.error) { case 'UNDEFINED_FUNCTION': - return t('scenarios:validation.evaluation_error.unknown_function'); + return t('scenarios:validation.evaluation_error.undefined_function', { + count: evaluationError.count, + }); case 'WRONG_NUMBER_OF_ARGUMENTS': return t( 'scenarios:validation.evaluation_error.wrong_number_of_arguments' ); case 'MISSING_NAMED_ARGUMENT': return t( - 'scenarios:validation.evaluation_error.missing_named_argument' + 'scenarios:validation.evaluation_error.missing_named_argument', + { + count: evaluationError.count, + } ); case 'ARGUMENTS_MUST_BE_INT_OR_FLOAT': return t( - 'scenarios:validation.evaluation_error.arguments_must_be_int_or_float' + 'scenarios:validation.evaluation_error.arguments_must_be_int_or_float', + { + count: evaluationError.count, + } + ); + case 'ARGUMENTS_MUST_BE_INT_FLOAT_OR_TIME': + return t( + 'scenarios:validation.evaluation_error.arguments_must_be_int_float_or_time', + { count: evaluationError.count } ); case 'ARGUMENT_MUST_BE_INTEGER': return t( - 'scenarios:validation.evaluation_error.argument_must_be_integer' + 'scenarios:validation.evaluation_error.argument_must_be_integer', + { + count: evaluationError.count, + } ); case 'ARGUMENT_MUST_BE_STRING': return t( - 'scenarios:validation.evaluation_error.argument_must_be_string' + 'scenarios:validation.evaluation_error.argument_must_be_string', + { + count: evaluationError.count, + } ); case 'ARGUMENT_MUST_BE_BOOLEAN': return t( - 'scenarios:validation.evaluation_error.argument_must_be_boolean' + 'scenarios:validation.evaluation_error.argument_must_be_boolean', + { + count: evaluationError.count, + } ); case 'ARGUMENT_MUST_BE_LIST': - return t('scenarios:validation.evaluation_error.argument_must_be_list'); + return t( + 'scenarios:validation.evaluation_error.argument_must_be_list', + { + count: evaluationError.count, + } + ); case 'ARGUMENT_MUST_BE_CONVERTIBLE_TO_DURATION': return t( - 'scenarios:validation.evaluation_error.argument_must_be_convertible_to_duration' + 'scenarios:validation.evaluation_error.argument_must_be_convertible_to_duration', + { + count: evaluationError.count, + } ); case 'ARGUMENT_MUST_BE_TIME': - return t('scenarios:validation.evaluation_error.argument_must_be_time'); + return t( + 'scenarios:validation.evaluation_error.argument_must_be_time', + { + count: evaluationError.count, + } + ); case 'FUNCTION_ERROR': - return t('scenarios:validation.evaluation_error.function_error'); + return t('scenarios:validation.evaluation_error.function_error', { + count: evaluationError.count, + }); case 'ARGUMENT_REQUIRED': - return t('scenarios:validation.evaluation_error.argument_required'); + return t('scenarios:validation.evaluation_error.argument_required', { + count: evaluationError.count, + }); + case 'ARGUMENT_INVALID_TYPE': + return t( + 'scenarios:validation.evaluation_error.argument_invalid_type', + { + count: evaluationError.count, + } + ); + case 'LIST_NOT_FOUND': + return t('scenarios:validation.evaluation_error.list_not_found', { + count: evaluationError.count, + }); + case 'FIELD_NOT_FOUND': + return t('scenarios:validation.evaluation_error.field_not_found', { + count: evaluationError.count, + }); case 'UNEXPECTED_ERROR': - return `${evaluationError.error}:${evaluationError.message}`; + return evaluationError.message; default: assertNever( '[EvaluationError] unhandled error code', - evaluationError.error + evaluationError['code'] ); } }; diff --git a/packages/marble-api/scripts/openapi.yaml b/packages/marble-api/scripts/openapi.yaml index d950157b5..18b30c47a 100644 --- a/packages/marble-api/scripts/openapi.yaml +++ b/packages/marble-api/scripts/openapi.yaml @@ -2605,6 +2605,7 @@ components: - WRONG_NUMBER_OF_ARGUMENTS - MISSING_NAMED_ARGUMENT - ARGUMENTS_MUST_BE_INT_OR_FLOAT + - ARGUMENTS_MUST_BE_INT_FLOAT_OR_TIME - ARGUMENT_MUST_BE_INTEGER - ARGUMENT_MUST_BE_STRING - ARGUMENT_MUST_BE_BOOLEAN @@ -2612,6 +2613,10 @@ components: - ARGUMENT_MUST_BE_CONVERTIBLE_TO_DURATION - ARGUMENT_MUST_BE_TIME - ARGUMENT_REQUIRED + - ARGUMENT_INVALID_TYPE + - LIST_NOT_FOUND + - DATABASE_ACCESS_NOT_FOUND + - PAYLOAD_FIELD_NOT_FOUND FuncAttributes: type: object required: diff --git a/packages/marble-api/src/generated/marble-api.ts b/packages/marble-api/src/generated/marble-api.ts index c2f219dc4..05ca3ef1e 100644 --- a/packages/marble-api/src/generated/marble-api.ts +++ b/packages/marble-api/src/generated/marble-api.ts @@ -187,7 +187,7 @@ export type ScenarioValidationErrorDto = { error: ScenarioValidationErrorCodeDto; message: string; }; -export type EvaluationErrorCodeDto = "UNEXPECTED_ERROR" | "UNDEFINED_FUNCTION" | "WRONG_NUMBER_OF_ARGUMENTS" | "MISSING_NAMED_ARGUMENT" | "ARGUMENTS_MUST_BE_INT_OR_FLOAT" | "ARGUMENT_MUST_BE_INTEGER" | "ARGUMENT_MUST_BE_STRING" | "ARGUMENT_MUST_BE_BOOLEAN" | "ARGUMENT_MUST_BE_LIST" | "ARGUMENT_MUST_BE_CONVERTIBLE_TO_DURATION" | "ARGUMENT_MUST_BE_TIME" | "ARGUMENT_REQUIRED"; +export type EvaluationErrorCodeDto = "UNEXPECTED_ERROR" | "UNDEFINED_FUNCTION" | "WRONG_NUMBER_OF_ARGUMENTS" | "MISSING_NAMED_ARGUMENT" | "ARGUMENTS_MUST_BE_INT_OR_FLOAT" | "ARGUMENTS_MUST_BE_INT_FLOAT_OR_TIME" | "ARGUMENT_MUST_BE_INTEGER" | "ARGUMENT_MUST_BE_STRING" | "ARGUMENT_MUST_BE_BOOLEAN" | "ARGUMENT_MUST_BE_LIST" | "ARGUMENT_MUST_BE_CONVERTIBLE_TO_DURATION" | "ARGUMENT_MUST_BE_TIME" | "ARGUMENT_REQUIRED" | "ARGUMENT_INVALID_TYPE" | "LIST_NOT_FOUND" | "DATABASE_ACCESS_NOT_FOUND" | "PAYLOAD_FIELD_NOT_FOUND"; export type EvaluationErrorDto = { error: EvaluationErrorCodeDto; message: string;