Skip to content

Commit

Permalink
Merge pull request #5030 from open-formulieren/feature/4871-error-mes…
Browse files Browse the repository at this point in the history
…sages-for-variable-mapping

Showing error messages in variable mapping
  • Loading branch information
robinmolen authored Mar 5, 2025
2 parents 509e4d2 + 653eca2 commit 4657f86
Show file tree
Hide file tree
Showing 16 changed files with 308 additions and 82 deletions.
6 changes: 6 additions & 0 deletions src/openforms/js/compiled-lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/openforms/js/compiled-lang/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/openforms/js/components/admin/form_design/Tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const Tab = ({hasErrors = false, children, ...props}) => {
return (
<ReactTab {...allProps}>
{children}
{hasErrors ? <ErrorIcon extraClassname="react-tabs__error-badge" texxt={title} /> : null}
{hasErrors ? <ErrorIcon extraClassname="react-tabs__error-badge" text={title} /> : null}
</ReactTab>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React from 'react';
import ErrorList from 'components/admin/forms/ErrorList';

const DSLEditorNode = ({errors, children}) => (
<div className={classNames('dsl-editor__node', {'dsl-editor__node--errors': !!errors})}>
<div className={classNames('dsl-editor__node', {'dsl-editor__node--errors': !!errors?.length})}>
<ErrorList classNamePrefix="logic-action">{errors}</ErrorList>
{children}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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);
});
});
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<DSLEditorNode errors={errors.action?.value}>
<DSLEditorNode errors={getRelevantErrors(errors)}>
<label className="required" htmlFor="dmn_config_button">
<FormattedMessage
description="Configuration button DMN label"
Expand Down Expand Up @@ -287,7 +306,11 @@ const ActionEvaluateDMN = ({action, errors, onChange}) => {
}
contentModifiers={['with-form', 'large']}
>
<DMNActionConfig initialValues={config} onSave={onConfigSave} />
<DMNActionConfig
initialValues={config}
onSave={onConfigSave}
errors={errors.action?.config}
/>
</Modal>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import {
} from 'components/admin/form_design/constants';
import Field from 'components/admin/forms/Field';
import Select from 'components/admin/forms/Select';
import ValidationErrorsProvider from 'components/admin/forms/ValidationErrors';
import ErrorBoundary from 'components/errors/ErrorBoundary';
import {get} from 'utils/fetch';

import {ActionConfigError} from '../types';
import DMNParametersForm from './DMNParametersForm';
import {inputValuesType} from './types';

Expand Down Expand Up @@ -162,7 +164,7 @@ const DecisionDefinitionVersionField = () => {
);
};

const DMNActionConfig = ({initialValues, onSave}) => {
const DMNActionConfig = ({initialValues, onSave, errors = {}}) => {
const {plugins} = useContext(FormContext);

const validate = values => {
Expand All @@ -180,82 +182,85 @@ const DMNActionConfig = ({initialValues, onSave}) => {
};

return (
<div className="dmn-action-config">
<Formik
initialValues={{
...initialValues,
pluginId:
plugins.availableDMNPlugins.length === 1
? plugins.availableDMNPlugins[0].id
: initialValues.pluginId,
}}
onSubmit={values => onSave(values)}
validate={validate}
>
{formik => (
<Form>
<fieldset className="aligned">
<div className="form-row form-row--no-bottom-line">
<Field
name="pluginId"
htmlFor="pluginId"
label={
<FormattedMessage defaultMessage="Plugin ID" description="Plugin ID label" />
}
errors={
formik.touched.pluginId && formik.errors.pluginId
? [ERRORS[formik.errors.pluginId]]
: []
}
>
<Select
id="pluginId"
name="pluginId"
allowBlank={true}
choices={plugins.availableDMNPlugins.map(choice => [choice.id, choice.label])}
{...formik.getFieldProps('pluginId')}
onChange={(...args) => {
// Otherwise the field is set as 'touched' only on the blur event
formik.setFieldTouched('pluginId');
formik.handleChange(...args);
}}
/>
</Field>
</div>
</fieldset>

<ErrorBoundary
errorMessage={
<FormattedMessage
description="Admin error for API error when configuring Camunda actions"
defaultMessage="Could not retrieve the decision definitions IDs/versions. Is the selected DMN plugin running and properly configured?"
/>
}
>
<ValidationErrorsProvider errors={Object.entries(errors)}>
<div className="dmn-action-config">
<Formik
initialValues={{
...initialValues,
pluginId:
plugins.availableDMNPlugins.length === 1
? plugins.availableDMNPlugins[0].id
: initialValues.pluginId,
}}
onSubmit={values => onSave(values)}
validate={validate}
>
{formik => (
<Form>
<fieldset className="aligned">
<div className="form-row form-row--no-bottom-line">
<DecisionDefinitionIdField />
</div>
<div className="form-row">
<DecisionDefinitionVersionField />
<div className="form-row form-row--display-block form-row--no-bottom-line">
<Field
name="pluginId"
htmlFor="pluginId"
label={
<FormattedMessage defaultMessage="Plugin ID" description="Plugin ID label" />
}
errors={
formik.touched.pluginId && formik.errors.pluginId
? [ERRORS[formik.errors.pluginId]]
: []
}
>
<Select
id="pluginId"
name="pluginId"
allowBlank={true}
choices={plugins.availableDMNPlugins.map(choice => [choice.id, choice.label])}
{...formik.getFieldProps('pluginId')}
onChange={(...args) => {
// Otherwise the field is set as 'touched' only on the blur event
formik.setFieldTouched('pluginId');
formik.handleChange(...args);
}}
/>
</Field>
</div>
</fieldset>

<DMNParametersForm />
</ErrorBoundary>
<div className="submit-row">
<input type="submit" name="_save" value="Save" />
</div>
</Form>
)}
</Formik>
</div>
<ErrorBoundary
errorMessage={
<FormattedMessage
description="Admin error for API error when configuring Camunda actions"
defaultMessage="Could not retrieve the decision definitions IDs/versions. Is the selected DMN plugin running and properly configured?"
/>
}
>
<fieldset className="aligned">
<div className="form-row form-row--display-block form-row--no-bottom-line">
<DecisionDefinitionIdField />
</div>
<div className="form-row form-row--display-block">
<DecisionDefinitionVersionField />
</div>
</fieldset>

<DMNParametersForm />
</ErrorBoundary>
<div className="submit-row">
<input type="submit" name="_save" value="Save" />
</div>
</Form>
)}
</Formik>
</div>
</ValidationErrorsProvider>
);
};

DMNActionConfig.propTypes = {
initialValues: inputValuesType,
onSave: PropTypes.func.isRequired,
errors: ActionConfigError,
};

export default DMNActionConfig;
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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};
Loading

0 comments on commit 4657f86

Please sign in to comment.