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 (
+
+
+
+ {t(buttonConfig.label)}
+
+
+ );
+};
+
+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;