From 306111064cf4f54ed3a19653284d2845c728960e Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 19 Aug 2024 09:26:06 +0200 Subject: [PATCH 1/9] :recycle: [#4606] Implement ZGW config fields as Formik fields There are some slight tweaks in literals here and there, and the selects are now react-selects that can be searched, but other than that they're pretty much equivalent to what we had before. --- .../registrations/zgw/fields/CaseType.js | 39 ++++++++++ .../zgw/fields/ConfidentialityLevel.js | 42 +++++++++++ .../registrations/zgw/fields/DocumentType.js | 39 ++++++++++ .../zgw/fields/MedewerkerRoltype.js | 36 +++++++++ .../registrations/zgw/fields/ObjectType.js | 40 ++++++++++ .../zgw/fields/ObjectTypeVersion.js | 33 +++++++++ .../zgw/fields/OrganisationRSIN.js | 35 +++++++++ .../registrations/zgw/fields/ZGWAPIGroup.js | 73 +++++++++++++++++++ .../registrations/zgw/fields/index.js | 8 ++ 9 files changed, 345 insertions(+) create mode 100644 src/openforms/js/components/admin/form_design/registrations/zgw/fields/CaseType.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/zgw/fields/ConfidentialityLevel.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/zgw/fields/DocumentType.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/zgw/fields/MedewerkerRoltype.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/zgw/fields/ObjectType.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/zgw/fields/ObjectTypeVersion.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/zgw/fields/OrganisationRSIN.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/zgw/fields/ZGWAPIGroup.js create mode 100644 src/openforms/js/components/admin/form_design/registrations/zgw/fields/index.js diff --git a/src/openforms/js/components/admin/form_design/registrations/zgw/fields/CaseType.js b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/CaseType.js new file mode 100644 index 0000000000..695b9749dd --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/CaseType.js @@ -0,0 +1,39 @@ +import {useField} from 'formik'; +import {FormattedMessage} from 'react-intl'; + +import Field from 'components/admin/forms/Field'; +import FormRow from 'components/admin/forms/FormRow'; +import {TextInput} from 'components/admin/forms/Inputs'; + +/** + * @todo - convert to omschrijving & use URL-based field as legacy/deprecated option + */ +const CaseType = () => { + const [fieldProps] = useField('zaaktype'); + return ( + + + } + helpText={ + + } + > + + + + ); +}; + +CaseType.propTypes = {}; + +export default CaseType; diff --git a/src/openforms/js/components/admin/form_design/registrations/zgw/fields/ConfidentialityLevel.js b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/ConfidentialityLevel.js new file mode 100644 index 0000000000..32d3d2bab6 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/ConfidentialityLevel.js @@ -0,0 +1,42 @@ +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; + +import Field from 'components/admin/forms/Field'; +import FormRow from 'components/admin/forms/FormRow'; +import ReactSelect from 'components/admin/forms/ReactSelect'; + +const ConfidentialityLevel = ({options}) => ( + + + } + helpText={ + + } + > + ({ + value, + label, + }))} + isClearable + /> + + +); + +ConfidentialityLevel.propTypes = { + options: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), +}; + +export default ConfidentialityLevel; diff --git a/src/openforms/js/components/admin/form_design/registrations/zgw/fields/DocumentType.js b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/DocumentType.js new file mode 100644 index 0000000000..bad5127248 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/DocumentType.js @@ -0,0 +1,39 @@ +import {useField} from 'formik'; +import {FormattedMessage} from 'react-intl'; + +import Field from 'components/admin/forms/Field'; +import FormRow from 'components/admin/forms/FormRow'; +import {TextInput} from 'components/admin/forms/Inputs'; + +/** + * @todo - convert to omschrijving & use URL-based field as legacy/deprecated option + */ +const DocumentType = () => { + const [fieldProps] = useField('informatieobjecttype'); + return ( + + + } + helpText={ + + } + > + + + + ); +}; + +DocumentType.propTypes = {}; + +export default DocumentType; diff --git a/src/openforms/js/components/admin/form_design/registrations/zgw/fields/MedewerkerRoltype.js b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/MedewerkerRoltype.js new file mode 100644 index 0000000000..dfbcb0a187 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/MedewerkerRoltype.js @@ -0,0 +1,36 @@ +import {useField} from 'formik'; +import {FormattedMessage} from 'react-intl'; + +import Field from 'components/admin/forms/Field'; +import FormRow from 'components/admin/forms/FormRow'; +import {TextInput} from 'components/admin/forms/Inputs'; + +const MedewerkerRoltype = () => { + const [fieldProps] = useField('medewerkerRoltype'); + return ( + + + } + helpText={ + + } + > + + + + ); +}; + +MedewerkerRoltype.propTypes = {}; + +export default MedewerkerRoltype; diff --git a/src/openforms/js/components/admin/form_design/registrations/zgw/fields/ObjectType.js b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/ObjectType.js new file mode 100644 index 0000000000..dcc979eeda --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/ObjectType.js @@ -0,0 +1,40 @@ +import {useField} from 'formik'; +import {FormattedMessage} from 'react-intl'; + +import Field from 'components/admin/forms/Field'; +import FormRow from 'components/admin/forms/FormRow'; +import {TextInput} from 'components/admin/forms/Inputs'; + +/** + * @todo - deprecate in favour of dropdown like in the objects API + */ +const ObjectType = () => { + const [fieldProps] = useField('objecttype'); + return ( + + + } + helpText={ + + } + > + + + + ); +}; + +ObjectType.propTypes = {}; + +export default ObjectType; diff --git a/src/openforms/js/components/admin/form_design/registrations/zgw/fields/ObjectTypeVersion.js b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/ObjectTypeVersion.js new file mode 100644 index 0000000000..751aa32068 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/ObjectTypeVersion.js @@ -0,0 +1,33 @@ +import {useField} from 'formik'; +import {FormattedMessage} from 'react-intl'; + +import Field from 'components/admin/forms/Field'; +import FormRow from 'components/admin/forms/FormRow'; +import {NumberInput} from 'components/admin/forms/Inputs'; + +/** + * @todo - deprecate in favour of dropdown like in the objects API + * @todo - make required if an object type is selected? + */ +const ObjectTypeVersion = () => { + const [fieldProps] = useField('objecttypeVersion'); + return ( + + + } + > + + + + ); +}; + +ObjectTypeVersion.propTypes = {}; + +export default ObjectTypeVersion; diff --git a/src/openforms/js/components/admin/form_design/registrations/zgw/fields/OrganisationRSIN.js b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/OrganisationRSIN.js new file mode 100644 index 0000000000..74ba55cea7 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/OrganisationRSIN.js @@ -0,0 +1,35 @@ +import {useField} from 'formik'; +import {FormattedMessage} from 'react-intl'; + +import Field from 'components/admin/forms/Field'; +import FormRow from 'components/admin/forms/FormRow'; +import {TextInput} from 'components/admin/forms/Inputs'; + +const OrganisationRSIN = () => { + const [fieldProps] = useField('organisatieRsin'); + return ( + + + } + helpText={ + + } + > + + + + ); +}; + +OrganisationRSIN.propTypes = {}; + +export default OrganisationRSIN; diff --git a/src/openforms/js/components/admin/form_design/registrations/zgw/fields/ZGWAPIGroup.js b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/ZGWAPIGroup.js new file mode 100644 index 0000000000..75142b1737 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/ZGWAPIGroup.js @@ -0,0 +1,73 @@ +import {useField, useFormikContext} from 'formik'; +import PropTypes from 'prop-types'; +import {FormattedMessage} from 'react-intl'; +import {useUpdateEffect} from 'react-use'; + +import Field from 'components/admin/forms/Field'; +import FormRow from 'components/admin/forms/FormRow'; +import ReactSelect from 'components/admin/forms/ReactSelect'; + +const ZGWAPIGroup = ({apiGroupChoices, onChangeCheck}) => { + const [{onChange: onChangeFormik, ...fieldProps}, , {setValue}] = useField('zgwApiGroup'); + const {setValues} = useFormikContext(); + const {value} = fieldProps; + + // reset the zaaktype/objecttype specific-configuration whenever the API group changes + useUpdateEffect(() => { + setValues(prevValues => ({ + ...prevValues, + zaaktype: '', + informatieobjecttype: '', + medewerkerRoltype: '', + propertyMappings: [], + // objects API integration + objecttype: undefined, + objecttypeVersion: undefined, + contentJson: undefined, + })); + }, [setValues, value]); + + const options = apiGroupChoices.map(([value, label]) => ({value, label})); + return ( + + + } + helpText={ + + } + noManageChildProps + > + { + const okToProceed = onChangeCheck === undefined || onChangeCheck(); + if (okToProceed) setValue(selectedOption.value); + }} + /> + + + ); +}; + +ZGWAPIGroup.propTypes = { + apiGroupChoices: PropTypes.arrayOf( + PropTypes.arrayOf( + PropTypes.oneOfType([ + PropTypes.number, // value + PropTypes.string, // label + ]) + ) + ).isRequired, + onChangeCheck: PropTypes.func, +}; + +export default ZGWAPIGroup; diff --git a/src/openforms/js/components/admin/form_design/registrations/zgw/fields/index.js b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/index.js new file mode 100644 index 0000000000..36cb5e12ad --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/zgw/fields/index.js @@ -0,0 +1,8 @@ +export {default as ZGWAPIGroup} from './ZGWAPIGroup'; +export {default as CaseType} from './CaseType'; +export {default as DocumentType} from './DocumentType'; +export {default as OrganisationRSIN} from './OrganisationRSIN'; +export {default as ConfidentialityLevel} from './ConfidentialityLevel'; +export {default as MedewerkerRoltype} from './MedewerkerRoltype'; +export {default as ObjectType} from './ObjectType'; +export {default as ObjectTypeVersion} from './ObjectTypeVersion'; From 5295cd6fec9c25e7f453c61cdc3948577521a12c Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 19 Aug 2024 09:26:45 +0200 Subject: [PATCH 2/9] :rotating_light: [#4606] Remove unused variable --- src/openforms/js/components/admin/forms/Field.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openforms/js/components/admin/forms/Field.js b/src/openforms/js/components/admin/forms/Field.js index ea2007ec99..23e99583fd 100644 --- a/src/openforms/js/components/admin/forms/Field.js +++ b/src/openforms/js/components/admin/forms/Field.js @@ -16,7 +16,7 @@ export const normalizeErrors = (errors = [], intl) => { if (error.defaultMessage) return intl.formatMessage(error); - const [key, msg] = error; + const [, msg] = error; return msg; }); return [hasErrors, formattedErrors]; From d488b1c3132f85ffb2295b36d7eca8960027603b Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 19 Aug 2024 09:27:55 +0200 Subject: [PATCH 3/9] :bug: [#4606] Fix default react-select behaviour when clearing the value react-select sets the value to null, which doesn't have a property 'value', leading to crashes. Instead, we set the formik field value to 'undefined', which effectively clears it in the formik state. --- src/openforms/js/components/admin/forms/ReactSelect.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/openforms/js/components/admin/forms/ReactSelect.js b/src/openforms/js/components/admin/forms/ReactSelect.js index bbb9e7e24b..92f06a7174 100644 --- a/src/openforms/js/components/admin/forms/ReactSelect.js +++ b/src/openforms/js/components/admin/forms/ReactSelect.js @@ -51,7 +51,12 @@ const Select = ({name, options, ...props}) => { {...fieldProps} value={options.find(opt => opt.value === value) || null} onChange={selectedOption => { - setValue(selectedOption.value); + // clear the value + if (selectedOption == null) { + setValue(undefined); + } else { + setValue(selectedOption.value); + } }} {...props} /> From a8fde14612fa89943e9f16d0ee14ce1bef2eb1df Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 19 Aug 2024 09:30:48 +0200 Subject: [PATCH 4/9] :lipstick: [#4606] Amend styles for error state * The modal spacing is a cheeky workaround for tab error icons that would otherwise be cut off by the overflow: auto due to the desired scroll behaviour inside a form modal. * The react-select styles ensure the border color of the select is consistent with other input types in the django admin style (red border if there are validation errors) * The changelist styles ensure that items in a table row with validation errors don't break the (vertical) alignment between them - as soon as any has a validation error, its content will be pushed down so we need to align to the bottom of the table cell (like in the variables table styles). --- src/openforms/scss/components/admin/_ReactModal.scss | 8 ++++---- src/openforms/scss/components/admin/_changelist.scss | 4 ++++ src/openforms/scss/vendor/_react-select.scss | 4 ++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/openforms/scss/components/admin/_ReactModal.scss b/src/openforms/scss/components/admin/_ReactModal.scss index 274eb0c434..ded2494645 100644 --- a/src/openforms/scss/components/admin/_ReactModal.scss +++ b/src/openforms/scss/components/admin/_ReactModal.scss @@ -53,10 +53,10 @@ } } - &__header { - &:not(:last-child) { - margin-bottom: 1em; - } + &__header + * { + // leave room for absolute positioned elements that otherwise get cut off by the + // overflow: auto + padding-block-start: 1em; } &__close { diff --git a/src/openforms/scss/components/admin/_changelist.scss b/src/openforms/scss/components/admin/_changelist.scss index b9a9788819..610ac29921 100644 --- a/src/openforms/scss/components/admin/_changelist.scss +++ b/src/openforms/scss/components/admin/_changelist.scss @@ -37,6 +37,10 @@ padding-right: 1em; } } + + tr.has-errors td { + vertical-align: bottom !important; + } } } diff --git a/src/openforms/scss/vendor/_react-select.scss b/src/openforms/scss/vendor/_react-select.scss index 3f6f48a5c9..79525cc9ae 100644 --- a/src/openforms/scss/vendor/_react-select.scss +++ b/src/openforms/scss/vendor/_react-select.scss @@ -16,5 +16,9 @@ overflow: visible; } + @at-root .errors &__control { + --border-color: var(--error-fg); + } + inline-size: clamp(200px, 100%, var(--of-admin-select-max-inline-size)); } From 4e44f40e85944fcc616a64b79c49b24e18799d8a Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 19 Aug 2024 09:33:16 +0200 Subject: [PATCH 5/9] :recycle: [#4606] Implement variable-case property mappings based on Formik The layout is still the same, except this table will no longer live in its own modal, but be a tab in the main modal for the registration options instead. The fields now make use of Formik's FieldArray to manage a list of nested objects. --- .../zgw/ManageVariableToPropertyMappings.js | 169 +++++++++ .../zgw/ZGWOptionsVariablesProperties.js | 320 ------------------ 2 files changed, 169 insertions(+), 320 deletions(-) create mode 100644 src/openforms/js/components/admin/form_design/registrations/zgw/ManageVariableToPropertyMappings.js delete mode 100644 src/openforms/js/components/admin/form_design/registrations/zgw/ZGWOptionsVariablesProperties.js diff --git a/src/openforms/js/components/admin/form_design/registrations/zgw/ManageVariableToPropertyMappings.js b/src/openforms/js/components/admin/form_design/registrations/zgw/ManageVariableToPropertyMappings.js new file mode 100644 index 0000000000..876781a018 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/zgw/ManageVariableToPropertyMappings.js @@ -0,0 +1,169 @@ +import classNames from 'classnames'; +import {FieldArray, useField, useFormikContext} from 'formik'; +import PropTypes from 'prop-types'; +import React, {useContext} from 'react'; +import {FormattedMessage, useIntl} from 'react-intl'; + +import {FormContext} from 'components/admin/form_design/Context'; +import {getComponentDatatype} from 'components/admin/form_design/variables/utils'; +import ButtonContainer from 'components/admin/forms/ButtonContainer'; +import ComponentSelection from 'components/admin/forms/ComponentSelection'; +import Field from 'components/admin/forms/Field'; +import {TextInput} from 'components/admin/forms/Inputs'; +import {ValidationErrorContext} from 'components/admin/forms/ValidationErrors'; +import {DeleteIcon} from 'components/admin/icons'; +import {ChangelistTableWrapper, HeadColumn, TableRow} from 'components/admin/tables'; + +import {filterErrors} from './utils'; + +const HeadColumns = () => { + const intl = useIntl(); + + const componentText = intl.formatMessage({ + description: 'Column title for variable to map to property', + defaultMessage: 'Variable', + }); + const componentHelp = intl.formatMessage({ + description: 'Column help text for variable to map to property', + defaultMessage: 'The value of the selected field will be the process variable value.', + }); + + const eigenschapText = intl.formatMessage({ + description: 'Column title for case property that a variable is mapped to', + defaultMessage: 'Property', + }); + + const eigenschapHelp = intl.formatMessage({ + description: 'Column help text for case property that a variable is mapped to', + defaultMessage: 'Specify a ZGW property name.', + }); + + return ( + <> + + + + + ); +}; + +const VariableProperty = ({index, componentKey, componentFilterFunc, onDelete}) => { + const intl = useIntl(); + const errors = useContext(ValidationErrorContext); + const relevantErrors = filterErrors(`propertyMappings.${index}`, errors); + + const variableName = `propertyMappings.${index}.componentKey`; + const [variableProps] = useField(variableName); + const variableErrors = relevantErrors.filter(([key]) => key === 'componentKey'); + + const eigenschapName = `propertyMappings.${index}.eigenschap`; + const [eigenschapProps] = useField(eigenschapName); + const eigenschapErrors = relevantErrors.filter(([key]) => key === 'eigenschap'); + + const confirmDeleteMessage = intl.formatMessage({ + description: 'Delete confirmation message for variable mapped to case property', + defaultMessage: 'Are you sure you want to remove this mapping?', + }); + + return ( + 0})}> +
+ +
+ + + + + + + {/* TODO - lookup available properties from the selected zaaktype */} + + +
+ ); +}; + +VariableProperty.propTypes = { + index: PropTypes.number.isRequired, + componentKey: PropTypes.string, + componentFilterFunc: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired, +}; + +const getSimpleComponentsLength = allComponents => { + const filteredComponents = Object.keys(allComponents).filter(comp => { + const componentDataType = getComponentDatatype(allComponents[comp]); + return !['array', 'object'].includes(componentDataType) && comp !== 'columns'; + }); + + return filteredComponents.length; +}; + +/** + * @todo - check how validation errors are displayed now + */ +const ManageVariableToPropertyMappings = () => { + const { + values: {propertyMappings = []}, + } = useFormikContext(); + const {components: allComponents} = useContext(FormContext); + + const usedComponents = propertyMappings + .filter(mapping => mapping.componentKey !== '') + .map(mapping => mapping.componentKey); + + const filterFunc = (componentKey, component) => { + const isSimpleType = + !['array', 'object'].includes(getComponentDatatype(component)) && component.key !== 'columns'; + const componentNotUsed = + componentKey === component.key || !usedComponents.includes(component.key); + + return isSimpleType && componentNotUsed; + }; + + const numSimpleComponents = getSimpleComponentsLength(allComponents); + return ( + + {arrayHelpers => ( + <> + }> + {propertyMappings.map(({componentKey}, index) => ( + arrayHelpers.remove(index)} + /> + ))} + + + {usedComponents.length < numSimpleComponents ? ( + + arrayHelpers.insert(propertyMappings.length, { + componentKey: '', + eigenschap: '', + }) + } + > + + + ) : ( + + )} + + )} + + ); +}; + +ManageVariableToPropertyMappings.propTypes = {}; + +export default ManageVariableToPropertyMappings; diff --git a/src/openforms/js/components/admin/form_design/registrations/zgw/ZGWOptionsVariablesProperties.js b/src/openforms/js/components/admin/form_design/registrations/zgw/ZGWOptionsVariablesProperties.js deleted file mode 100644 index 6b24f5aa65..0000000000 --- a/src/openforms/js/components/admin/form_design/registrations/zgw/ZGWOptionsVariablesProperties.js +++ /dev/null @@ -1,320 +0,0 @@ -import {produce} from 'immer'; -import PropTypes from 'prop-types'; -import React, {useContext, useState} from 'react'; -import {FormattedMessage, useIntl} from 'react-intl'; - -import {CustomFieldTemplate} from 'components/admin/RJSFWrapper'; -import {FormContext} from 'components/admin/form_design/Context'; -import {getComponentDatatype} from 'components/admin/form_design/variables/utils'; -import ActionButton, {SubmitAction} from 'components/admin/forms/ActionButton'; -import ButtonContainer from 'components/admin/forms/ButtonContainer'; -import ComponentSelection from 'components/admin/forms/ComponentSelection'; -import {TextInput} from 'components/admin/forms/Inputs'; -import SubmitRow from 'components/admin/forms/SubmitRow'; -import {ValidationErrorContext} from 'components/admin/forms/ValidationErrors'; -import {DeleteIcon} from 'components/admin/icons'; -import {FormModal} from 'components/admin/modals'; -import {ChangelistTableWrapper, HeadColumn, TableRow} from 'components/admin/tables'; - -import {getErrorMarkup} from './utils'; - -const EMPTY_VARIABLE_PROPERTY = { - propertyMappings: [{componentKey: '', eigenschap: ''}], -}; - -const HeadColumns = () => { - const intl = useIntl(); - - const componentText = intl.formatMessage({ - description: 'Column title for variable to map to property', - defaultMessage: 'Variable', - }); - const componentHelp = intl.formatMessage({ - description: 'Column help text for variable to map to property', - defaultMessage: 'The value of the selected field will be the process variable value.', - }); - - const eigenschapText = intl.formatMessage({ - description: 'Column title for case property that a variable is mapped to', - defaultMessage: 'Property', - }); - - const eigenschapHelp = intl.formatMessage({ - description: 'Column help text for case property that a variable is mapped to', - defaultMessage: 'Specify a ZGW property name.', - }); - - return ( - <> - - - - - ); -}; - -const VariableProperty = ({ - index, - componentKey = '', - eigenschap = '', - componentFilterFunc, - onChange, - onDelete, -}) => { - const intl = useIntl(); - - const confirmDeleteMessage = intl.formatMessage({ - description: 'Delete confirmation message for variable mapped to case property', - defaultMessage: 'Are you sure you want to remove this mapping?', - }); - - return ( - -
- -
- - - -
- ); -}; - -VariableProperty.propTypes = { - index: PropTypes.number.isRequired, - componentKey: PropTypes.string, - eigenschap: PropTypes.string, - componentFilterFunc: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, -}; - -const ManageVariableToPropertyMappings = ({propertyMappings = [], onChange, onAdd, onDelete}) => { - const formContext = useContext(FormContext); - const allComponents = formContext.components; - - const usedComponents = propertyMappings - .filter(mapping => mapping.componentKey !== '') - .map(mapping => mapping.componentKey); - - const filterFunc = (componentKey, component) => { - const isSimpleType = - !['array', 'object'].includes(getComponentDatatype(component)) && component.key !== 'columns'; - const componentNotUsed = - componentKey === component.key || !usedComponents.includes(component.key); - - return isSimpleType && componentNotUsed; - }; - - const getSimpleComponentsLength = () => { - const filteredComponents = Object.keys(allComponents).filter(comp => { - const componentDataType = getComponentDatatype(allComponents[comp]); - return !['array', 'object'].includes(componentDataType) && comp !== 'columns'; - }); - - return filteredComponents.length; - }; - - return ( - <> - }> - {propertyMappings.map((mappedVariable, index) => ( - - ))} - - - {usedComponents.length < getSimpleComponentsLength() ? ( - - - - ) : ( - - )} - - ); -}; - -ManageVariableToPropertyMappings.propTypes = { - propertyMappings: PropTypes.arrayOf( - PropTypes.shape({ - componentKey: PropTypes.string, - eigenschap: PropTypes.string, - }) - ).isRequired, - onChange: PropTypes.func.isRequired, - onAdd: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, -}; - -const VariablePropertyModal = ({index, name, formData, onChange}) => { - const intl = useIntl(); - const [modalOpen, setModalOpen] = useState(false); - const validationErrors = useContext(ValidationErrorContext); - - const {propertyMappings = []} = formData; - - const onAddVariableProperty = () => { - const nextFormData = produce(formData, draft => { - if (!draft.propertyMappings) draft.propertyMappings = []; - draft.propertyMappings.push(EMPTY_VARIABLE_PROPERTY); - }); - onChange(nextFormData); - }; - - const onChangeVariableProperty = (index, event) => { - const {name, value} = event.target; - const nextFormData = produce(formData, draft => { - draft.propertyMappings[index][name] = value; - }); - onChange(nextFormData); - }; - - const onDeleteVariableProperty = index => { - const nextFormData = produce(formData, draft => { - draft.propertyMappings.splice(index, 1); - }); - onChange(nextFormData); - }; - - /** - * Handle property mappings errors and show them to the main page, not inside the modal. - */ - const getCombinedErrors = (name, index, errors) => { - const errorMessages = []; - - for (const [errorName, errorReason] of errors) { - if (errorName.startsWith(name + '.propertyMappings')) { - const errorNameBits = errorName.split('.'); - const lastNameBit = errorNameBits[errorNameBits.length - 1]; - - if (errorNameBits[2] === String(index)) { - // Custom error in validate method from the API call - if (lastNameBit === 'propertyMappings') { - errorMessages.push(errorReason); - } else { - // Errors raised from the serializer's definition - if (lastNameBit === 'componentKey') { - errorMessages.push('Component key: ' + errorReason); - } else if (lastNameBit === 'eigenschap') { - errorMessages.push('Property: ' + errorReason); - } - } - } - } - } - - return errorMessages.length > 0 ? errorMessages : null; - }; - - return ( - <> - - setModalOpen(!modalOpen)} - /> -   - - - - - } - closeModal={() => setModalOpen(false)} - > - - - { - event.preventDefault(); - setModalOpen(false); - }} - /> - - - - ); -}; - -VariablePropertyModal.propTypes = { - index: PropTypes.number, - name: PropTypes.string, - formData: PropTypes.shape({ - contentJson: PropTypes.string, - informatieobjecttype: PropTypes.string, - medewerkerRoltype: PropTypes.string, - objecttype: PropTypes.string, - objecttypeVersion: PropTypes.string, - organisatieRsin: PropTypes.string, - propertyMappings: PropTypes.arrayOf( - PropTypes.shape({ - componentKey: PropTypes.string, - eigenschap: PropTypes.string, - }) - ).isRequired, - zaakVertrouwelijkheidaanduiding: PropTypes.string, - zaaktype: PropTypes.string, - zgwApiGroup: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - }), - onChange: PropTypes.func.isRequired, -}; - -export {VariablePropertyModal}; From 00bd4260d3e859641fd4effad7075ea8db708971 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 19 Aug 2024 09:36:45 +0200 Subject: [PATCH 6/9] :children_crossing: [#4606] Move ZGW registration options into modal Moving the ZGW options into a modal allows us to make use of Formik, since the fields themselves are now properly isolated and we can add an explicit 'submit' button to commit the configuration back to the main state. This applies a similar set up like in the objects API registration options. The configuration is now broken into parts too - base ZGW options go in the first tab, while the case property configuration is moved to its own tab rather than sticking it in a modal itself ( modal in modal is not a user friendly UI). I'm not sure if putting the objects API options in a fieldset or maybe its own tab is the best approach, we can discuss. --- .../test_registration_backend_conf.py | 21 +- .../objectsapi/LegacyConfigFields.js | 2 +- .../registrations/zgw/ZGWOptionsForm.js | 151 ++++-- .../registrations/zgw/ZGWOptionsFormFields.js | 439 ++++++------------ .../zgw/ZGWOptionsFormFields.stories.js | 142 +++--- .../form_design/registrations/zgw/utils.js | 33 +- 6 files changed, 371 insertions(+), 417 deletions(-) diff --git a/src/openforms/forms/tests/e2e_tests/test_registration_backend_conf.py b/src/openforms/forms/tests/e2e_tests/test_registration_backend_conf.py index 81a47e2339..45dc808aa5 100644 --- a/src/openforms/forms/tests/e2e_tests/test_registration_backend_conf.py +++ b/src/openforms/forms/tests/e2e_tests/test_registration_backend_conf.py @@ -121,7 +121,14 @@ def collect_requests(request): await page.get_by_role( "combobox", name="Select registration backend" ).select_option(label="ZGW API's") - await page.get_by_label("ZGW API group").select_option(label="Group 1") + await page.get_by_role("button", name="Configure options").click() + + config_modal = page.get_by_role("dialog") + await rs_select_option( + config_modal.get_by_role("combobox", name="API group"), + option_label="Group 1", + ) + await config_modal.get_by_role("button", name="Save").click() with phase("Configure upload component"): await page.get_by_role("tab", name="Steps and fields").click() @@ -132,10 +139,14 @@ def collect_requests(request): with phase("Update the ZGW API group configured"): await page.get_by_role("tab", name="Registration").click() - await page.get_by_role( - "combobox", name="Select registration backend" - ).select_option(label="ZGW API's") - await page.get_by_label("ZGW API group").select_option(label="Group 2") + await page.get_by_role("button", name="Configure options").click() + + config_modal = page.get_by_role("dialog") + await rs_select_option( + config_modal.get_by_role("combobox", name="API group"), + option_label="Group 2", + ) + await config_modal.get_by_role("button", name="Save").click() with phase("Reopen the upload component"): await page.get_by_role("tab", name="Steps and fields").click() diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js index dd2df5de4d..54dce6fbfc 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/LegacyConfigFields.js @@ -132,7 +132,7 @@ JSONTemplateField.propTypes = { helpText: PropTypes.node, }; -const ContentJSON = () => ( +export const ContentJSON = () => ( { + const intl = useIntl(); + const [modalOpen, setModalOpen] = useState(false); + const validationErrors = useContext(ValidationErrorContext); + + const {zgwApiGroup, zaakVertrouwelijkheidaanduiding} = schema.properties; + const apiGroupChoices = getChoicesFromSchema(zgwApiGroup.enum, zgwApiGroup.enumNames); + const confidentialityLevelChoices = getChoicesFromSchema( + zaakVertrouwelijkheidaanduiding.enum, + zaakVertrouwelijkheidaanduiding.enumNames + ); + + const numErrors = filterErrors(name, validationErrors).length; + const defaultGroup = apiGroupChoices.length === 1 ? apiGroupChoices[0][0] : undefined; return ( - onChange({formData})} - /> + <> + + + + {numErrors > 0 && ( + + )} + + + + } + closeModal={() => setModalOpen(false)} + extraModifiers={['large']} + > + { + onChange({formData: values}); + actions.setSubmitting(false); + setModalOpen(false); + }} + > + {({handleSubmit}) => ( + <> + + + + { + event.preventDefault(); + handleSubmit(event); + }} + /> + + + )} + + + ); }; @@ -24,38 +120,33 @@ ZGWOptionsForm.propTypes = { name: PropTypes.string, label: PropTypes.node, schema: PropTypes.shape({ - contentJson: PropTypes.string, + properties: PropTypes.shape({ + zgwApiGroup: PropTypes.shape({ + enum: PropTypes.arrayOf(PropTypes.number).isRequired, + enumNames: PropTypes.arrayOf(PropTypes.string).isRequired, + }).isRequired, + zaakVertrouwelijkheidaanduiding: PropTypes.shape({ + enum: PropTypes.arrayOf(PropTypes.string).isRequired, + enumNames: PropTypes.arrayOf(PropTypes.string).isRequired, + }).isRequired, + }).isRequired, + }).isRequired, + formData: PropTypes.shape({ + zgwApiGroup: PropTypes.number, + zaaktype: PropTypes.string, informatieobjecttype: PropTypes.string, - medewerkerRoltype: PropTypes.string, - objecttype: PropTypes.string, - objecttypeVersion: PropTypes.number, organisatieRsin: PropTypes.string, - propertyMappings: PropTypes.arrayOf( - PropTypes.shape({ - componentKey: PropTypes.string, - eigenschap: PropTypes.string, - }) - ), zaakVertrouwelijkheidaanduiding: PropTypes.string, - zaaktype: PropTypes.string, - zgwApiGroup: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - }), - formData: PropTypes.shape({ - contentJson: PropTypes.string, - informatieobjecttype: PropTypes.string, medewerkerRoltype: PropTypes.string, - objecttype: PropTypes.string, - objecttypeVersion: PropTypes.string, - organisatieRsin: PropTypes.string, propertyMappings: PropTypes.arrayOf( PropTypes.shape({ componentKey: PropTypes.string, eigenschap: PropTypes.string, }) ).isRequired, - zaakVertrouwelijkheidaanduiding: PropTypes.string, - zaaktype: PropTypes.string, - zgwApiGroup: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + objecttype: PropTypes.string, + objecttypeVersion: PropTypes.string, + contentJson: PropTypes.string, }), onChange: PropTypes.func.isRequired, }; diff --git a/src/openforms/js/components/admin/form_design/registrations/zgw/ZGWOptionsFormFields.js b/src/openforms/js/components/admin/form_design/registrations/zgw/ZGWOptionsFormFields.js index 0594a2bb51..a74eeaab6f 100644 --- a/src/openforms/js/components/admin/form_design/registrations/zgw/ZGWOptionsFormFields.js +++ b/src/openforms/js/components/admin/form_design/registrations/zgw/ZGWOptionsFormFields.js @@ -1,310 +1,161 @@ -import {produce} from 'immer'; +import {useFormikContext} from 'formik'; import PropTypes from 'prop-types'; import React, {useContext} from 'react'; -import {useIntl} from 'react-intl'; - -import {CustomFieldTemplate} from 'components/admin/RJSFWrapper'; -import {TextArea, TextInput} from 'components/admin/forms/Inputs'; -import Select from 'components/admin/forms/Select'; -import {ValidationErrorContext} from 'components/admin/forms/ValidationErrors'; - -import {VariablePropertyModal} from './ZGWOptionsVariablesProperties'; -import {getChoicesFromSchema, getErrorMarkup, getFieldErrors} from './utils'; - -const Wrapper = ({children}) => ( -
- -
{children}
-
-
-); - -const ZGWFormFields = ({index, name, schema, formData, onChange}) => { +import {FormattedMessage, useIntl} from 'react-intl'; +import {TabList, TabPanel, Tabs} from 'react-tabs'; + +import Tab from 'components/admin/form_design/Tab'; +import {ContentJSON} from 'components/admin/form_design/registrations/objectsapi/LegacyConfigFields'; +import Fieldset from 'components/admin/forms/Fieldset'; +import { + ValidationErrorContext, + ValidationErrorsProvider, +} from 'components/admin/forms/ValidationErrors'; + +import ManageVariableToPropertyMappings from './ManageVariableToPropertyMappings'; +import { + CaseType, + ConfidentialityLevel, + DocumentType, + MedewerkerRoltype, + ObjectType, + ObjectTypeVersion, + OrganisationRSIN, + ZGWAPIGroup, +} from './fields'; +import {filterErrors} from './utils'; + +const ZGWFormFields = ({name, apiGroupChoices, confidentialityLevelChoices}) => { const intl = useIntl(); - const validationErrors = useContext(ValidationErrorContext); - const { - zgwApiGroup = '', - zaakVertrouwelijkheidaanduiding = '', - zaaktype = '', - informatieobjecttype = '', - organisatieRsin = '', - medewerkerRoltype = '', - objecttype = '', - objecttypeVersion = '', - contentJson = '', - } = formData; + values: { + zaaktype, + informatieobjecttype, + medewerkerRoltype, + propertyMappings = [], + objecttype, + objecttypeVersion, + contentJson, + }, + } = useFormikContext(); + const validationErrors = useContext(ValidationErrorContext); + const relevantErrors = filterErrors(name, validationErrors); - const onFieldChange = event => { - const {name, value} = event.target; - const updatedFormData = produce(formData, draft => { - draft[name] = value; - }); - onChange(updatedFormData); - }; + const hasAnyFieldConfigured = + [ + zaaktype, + informatieobjecttype, + medewerkerRoltype, + objecttype, + objecttypeVersion, + contentJson, + ].some(v => !!v) || propertyMappings.length > 0; - const buildErrorsComponent = field => { - const rawErrors = getFieldErrors(name, index, validationErrors, field); - return rawErrors ? getErrorMarkup(rawErrors) : null; - }; + const numCasePropertyErrors = filterErrors(`${name}.propertyMappings`, validationErrors).length; + const numBaseErrors = relevantErrors.length - numCasePropertyErrors; return ( - - - - - - - - - - - - - - - - - - -