Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Showing error messages in variable mapping #5030

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
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 @@ -4159,6 +4159,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 @@ -4173,6 +4173,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
Loading