diff --git a/src/openforms/js/compiled-lang/en.json b/src/openforms/js/compiled-lang/en.json index 9f47e5745e..17bb7f831d 100644 --- a/src/openforms/js/compiled-lang/en.json +++ b/src/openforms/js/compiled-lang/en.json @@ -4201,6 +4201,12 @@ "value": "Changing the data type requires the initial value to be changed. This will reset the initial value back to the empty value. Are you sure that you want to do this?" } ], + "ag/AZx": [ + { + "type": 0, + "value": "There are errors in the DMN configuration." + } + ], "aqYeqv": [ { "type": 0, diff --git a/src/openforms/js/compiled-lang/nl.json b/src/openforms/js/compiled-lang/nl.json index 90ffdd12fb..fde943f9f8 100644 --- a/src/openforms/js/compiled-lang/nl.json +++ b/src/openforms/js/compiled-lang/nl.json @@ -4215,6 +4215,12 @@ "value": "Het veranderen van het datatype vereist een verandering aan de beginwaarde. Dit zal de beginwaarde terugbrengen naar de standaardwaarde. Weet je zeker dat je dit wilt doen?" } ], + "ag/AZx": [ + { + "type": 0, + "value": "De DMN-instellingen zijn niet geldig." + } + ], "aqYeqv": [ { "type": 0, diff --git a/src/openforms/js/components/admin/form_design/Tab.js b/src/openforms/js/components/admin/form_design/Tab.js index c7c1b4df45..e9f863f5c5 100644 --- a/src/openforms/js/components/admin/form_design/Tab.js +++ b/src/openforms/js/components/admin/form_design/Tab.js @@ -18,7 +18,7 @@ const Tab = ({hasErrors = false, children, ...props}) => { return ( {children} - {hasErrors ? : null} + {hasErrors ? : null} ); }; diff --git a/src/openforms/js/components/admin/form_design/logic/DSLEditorNode.js b/src/openforms/js/components/admin/form_design/logic/DSLEditorNode.js index 2752dda579..3dd123bb52 100644 --- a/src/openforms/js/components/admin/form_design/logic/DSLEditorNode.js +++ b/src/openforms/js/components/admin/form_design/logic/DSLEditorNode.js @@ -5,7 +5,7 @@ import React from 'react'; import ErrorList from 'components/admin/forms/ErrorList'; const DSLEditorNode = ({errors, children}) => ( -
+
{errors} {children}
diff --git a/src/openforms/js/components/admin/form_design/logic/actions/Action.stories.js b/src/openforms/js/components/admin/form_design/logic/actions/Action.stories.js index 051a7a1aed..6d17a59815 100644 --- a/src/openforms/js/components/admin/form_design/logic/actions/Action.stories.js +++ b/src/openforms/js/components/admin/form_design/logic/actions/Action.stories.js @@ -1,4 +1,5 @@ import {useArgs} from '@storybook/preview-api'; +import {expect, userEvent, waitFor, within} from '@storybook/test'; import {produce} from 'immer'; import set from 'lodash/set'; @@ -197,3 +198,96 @@ export const EvaluateDMN = { }, }, }; + +export const EvaluateDMNWithInitialErrors = { + render, + name: 'Evaluate DMN with initial errors', + args: { + prefixText: 'Action', + + action: { + component: '', + variable: 'bar', + formStep: '', + formStepUuid: '', + + action: { + config: { + pluginId: '', + decisionDefinitionId: '', + }, + type: 'evaluate-dmn', + value: '', + }, + }, + errors: { + action: { + config: { + pluginId: 'This field is required.', + decisionDefinitionId: 'This field is required.', + }, + }, + }, + availableDMNPlugins: [ + {id: 'camunda7', label: 'Camunda 7'}, + {id: 'some-other-engine', label: 'Some other engine'}, + ], + availableFormVariables: [ + {type: 'textfield', key: 'name', name: 'Name'}, + {type: 'textfield', key: 'surname', name: 'Surname'}, + {type: 'number', key: 'income', name: 'Income'}, + {type: 'checkbox', key: 'canApply', name: 'Can apply?'}, + ], + }, + decorators: [FormDecorator], + + parameters: { + msw: { + handlers: [ + mockDMNDecisionDefinitionsGet({ + camunda7: [ + { + id: 'approve-payment', + label: 'Approve payment', + }, + { + id: 'invoiceClassification', + label: 'Invoice Classification', + }, + ], + 'some-other-engine': [{id: 'some-definition-id', label: 'Some definition id'}], + }), + mockDMNDecisionDefinitionVersionsGet, + ], + }, + }, + play: async ({canvasElement, step}) => { + const canvas = within(canvasElement); + + step('Verify that global DMN config error is shown', () => { + expect( + canvas.getByRole('listitem', {text: 'De DMN-instellingen zijn niet geldig.'}) + ).toBeVisible(); + }); + + step('Open configuration modal', async () => { + await userEvent.click(canvas.getByRole('button', {name: 'Instellen'})); + + const dialog = within(canvas.getByRole('dialog')); + + const pluginDropdown = dialog.getByLabelText('Plugin'); + const decisionDefDropdown = dialog.getByLabelText('Beslisdefinitie-ID'); + + // Mark dropdowns as touched + await userEvent.click(pluginDropdown); + await userEvent.click(decisionDefDropdown); + await userEvent.tab(); + + await waitFor(async () => { + const errorMessages = await dialog.getAllByRole('listitem'); + + await expect(errorMessages.length).toBe(2); + }); + }); + }, +}; diff --git a/src/openforms/js/components/admin/form_design/logic/actions/Actions.js b/src/openforms/js/components/admin/form_design/logic/actions/Actions.js index 5e1612551c..0a752215d6 100644 --- a/src/openforms/js/components/admin/form_design/logic/actions/Actions.js +++ b/src/openforms/js/components/admin/form_design/logic/actions/Actions.js @@ -246,9 +246,28 @@ const ActionEvaluateDMN = ({action, errors, onChange}) => { ...(action?.action?.config || {}), }; + const getRelevantErrors = errors => { + const relevantErrors = errors.action?.value ? [errors.action.value] : []; + if (!errors.action?.config) { + return relevantErrors; + } + + // Global errors about the config should be shown at the top level. + // Otherwise, there are some errors in the config, that should be announced. + relevantErrors.push( + typeof errors.action.config === 'string' + ? errors.action.config + : intl.formatMessage({ + description: 'DMN evaluation configuration errors message', + defaultMessage: 'There are errors in the DMN configuration.', + }) + ); + return relevantErrors; + }; + return ( <> - +
+ ); }; DMNActionConfig.propTypes = { initialValues: inputValuesType, onSave: PropTypes.func.isRequired, + errors: ActionConfigError, }; export default DMNActionConfig; diff --git a/src/openforms/js/components/admin/form_design/logic/actions/types.js b/src/openforms/js/components/admin/form_design/logic/actions/types.js index abd128493d..e46a3e5bc9 100644 --- a/src/openforms/js/components/admin/form_design/logic/actions/types.js +++ b/src/openforms/js/components/admin/form_design/logic/actions/types.js @@ -25,6 +25,21 @@ const Action = PropTypes.shape({ formStepUuid: PropTypes.string, }); +const ActionConfigMappingError = PropTypes.arrayOf( + PropTypes.shape({ + dmnVariable: PropTypes.string, + formVariable: PropTypes.string, + }) +); + +const ActionConfigError = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + inputMapping: ActionConfigMappingError, + outputMapping: ActionConfigMappingError, + }), +]); + const ActionError = PropTypes.shape({ action: PropTypes.shape({ state: PropTypes.string, @@ -34,10 +49,11 @@ const ActionError = PropTypes.shape({ value: PropTypes.string, }), value: PropTypes.string, + config: ActionConfigError, }), component: PropTypes.string, formStep: PropTypes.string, formStepUuid: PropTypes.string, }); -export {jsonLogicVar, Action, ActionError}; +export {jsonLogicVar, Action, ActionError, ActionConfigError}; diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js index b5df92bdef..e0c377d516 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js @@ -275,7 +275,7 @@ const ObjectsAPIFields = () => { /> } > - + { +const FormRow = ({fields = [], children, preventErrorsModifier = false}) => { const fieldClasses = fields.map(field => `field-${field}`); let hasErrors = false; @@ -45,7 +45,7 @@ const FormRow = ({fields = [], children}) => { const className = classNames( 'form-row', - {errors: processedChildren.length === 1 && hasErrors}, + {errors: processedChildren.length === 1 && hasErrors && !preventErrorsModifier}, ...fieldClasses ); const inner = hasAnyFieldBox ? ( @@ -59,6 +59,11 @@ const FormRow = ({fields = [], children}) => { FormRow.propTypes = { fields: PropTypes.arrayOf(PropTypes.string), children: PropTypes.node, + + /** + * Prevents the 'errors' modifier from being added to the 'form-row' component. + */ + preventErrorsModifier: PropTypes.bool, }; export default FormRow; diff --git a/src/openforms/js/components/admin/forms/VariableMapping.js b/src/openforms/js/components/admin/forms/VariableMapping.js index 61bb4e8760..9b38ea5379 100644 --- a/src/openforms/js/components/admin/forms/VariableMapping.js +++ b/src/openforms/js/components/admin/forms/VariableMapping.js @@ -1,12 +1,13 @@ import {FieldArray, useFormikContext} from 'formik'; import get from 'lodash/get'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, {useContext} from 'react'; import {FormattedMessage, useIntl} from 'react-intl'; import ButtonContainer from 'components/admin/forms/ButtonContainer'; import Field from 'components/admin/forms/Field'; import Select from 'components/admin/forms/Select'; +import {ValidationErrorContext} from 'components/admin/forms/ValidationErrors'; import VariableSelection from 'components/admin/forms/VariableSelection'; import {DeleteIcon, WarningIcon} from 'components/admin/icons'; @@ -47,6 +48,7 @@ const VariableMappingRow = ({ includeStaticVariables = false, alreadyMapped = [], rowCheck = undefined, + errors = undefined, }) => { const intl = useIntl(); const {getFieldProps, setFieldValue} = useFormikContext(); @@ -63,12 +65,14 @@ const VariableMappingRow = ({ ); const mapping = getFieldProps(prefix).value; - const errors = rowCheck?.(intl, mapping).join(', ') ?? ''; + const rowErrors = rowCheck?.(intl, mapping).join(', ') ?? ''; + const variableError = errors?.[variableName]; + const propertyError = errors?.[propertyName]; return ( - + {directionIcon}} - +