diff --git a/.storybook/preview.js b/.storybook/preview.js index 8f3d671c9b..00ca0a9215 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,6 +1,6 @@ import '../src/openforms/scss/screen.scss'; import '../src/openforms/scss/admin/admin_overrides.scss'; -import {withModalDecorator} from 'components/admin/form_design/story-decorators'; +import {withModalDecorator, withReactSelectDecorator} from 'components/admin/form_design/story-decorators'; import {initialize, mswDecorator, mswLoader} from 'msw-storybook-addon'; import {reactIntl} from './reactIntl.js'; import ReactModal from 'react-modal'; @@ -15,7 +15,7 @@ initialize({ ReactModal.setAppElement(document.getElementById('storybook-root')); export default { - decorators: [mswDecorator, withModalDecorator], + decorators: [mswDecorator, withModalDecorator, withReactSelectDecorator], parameters: { controls: { matchers: { diff --git a/package.json b/package.json index 5f82ebe1da..d375b3872f 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "trailingComma": "es5", "useTabs": false, "importOrder": [ - "^((api-mocks|components|data|formio|hooks|map|story-utils|types|utils)/(.*)|(api|api-mocks|cache|Context|errors|headers|i18n|sdk|sentry|types))$", + "^((api-mocks|components|data|formio|hooks|map|story-utils|types|utils)/(.*)|(api|api-mocks|cache|Context|errors|headers|i18n|sdk|sentry|story-utils|types))$", "^[./]" ], "importOrderSeparation": true, diff --git a/src/openforms/forms/api/serializers/form_variable.py b/src/openforms/forms/api/serializers/form_variable.py index 102db35cdf..a6a827d559 100644 --- a/src/openforms/forms/api/serializers/form_variable.py +++ b/src/openforms/forms/api/serializers/form_variable.py @@ -205,15 +205,13 @@ def validate(self, attrs): } ) - if (prefill_plugin and not prefill_attribute) or ( - not prefill_plugin and prefill_attribute + if (prefill_plugin and not (prefill_attribute or prefill_options)) or ( + not prefill_plugin and (prefill_attribute or prefill_options) ): raise ValidationError( - { - "prefill_attribute": _( - "Prefill plugin and attribute must both be specified." - ), - } + _( + "Prefill plugin must be specified with either prefill attribute or prefill options." + ) ) # check the specific validation options of the prefill plugin diff --git a/src/openforms/forms/tests/variables/test_viewset.py b/src/openforms/forms/tests/variables/test_viewset.py index 72938cf511..8796f70483 100644 --- a/src/openforms/forms/tests/variables/test_viewset.py +++ b/src/openforms/forms/tests/variables/test_viewset.py @@ -977,7 +977,9 @@ def test_bulk_create_and_update_with_prefill_constraints(self): self.assertEqual(response.json()["invalidParams"][0]["code"], "invalid") self.assertEqual( response.json()["invalidParams"][0]["reason"], - _("Prefill plugin and attribute must both be specified."), + _( + "Prefill plugin must be specified with either prefill attribute or prefill options." + ), ) with self.subTest( @@ -1019,6 +1021,34 @@ def test_bulk_create_and_update_with_prefill_constraints(self): ), ) + with self.subTest( + "user_defined with prefill plugin and prefill options is allowed" + ): + data = [ + { + "form": form_url, + "form_definition": form_definition_url, + "key": "userdefined", + "name": "Test", + "service_fetch_configuration": None, + "data_type": FormVariableDataTypes.string, + "source": FormVariableSources.user_defined, + "prefill_plugin": "objects_api", + "prefill_attribute": "", + "prefill_options": {"foo": "bar"}, + } + ] + + response = self.client.put( + reverse( + "api:form-variables", + kwargs={"uuid_or_slug": form.uuid}, + ), + data=data, + ) + + self.assertEqual(status.HTTP_200_OK, response.status_code) + def test_bulk_create_and_update_with_non_camel_case_initial_values(self): user = StaffUserFactory.create(user_permissions=["change_form"]) self.client.force_authenticate(user) diff --git a/src/openforms/js/compiled-lang/en.json b/src/openforms/js/compiled-lang/en.json index dd1788e7a2..06f1ee8658 100644 --- a/src/openforms/js/compiled-lang/en.json +++ b/src/openforms/js/compiled-lang/en.json @@ -149,6 +149,12 @@ "value": "Regular expression for city" } ], + "0EYS7D": [ + { + "type": 0, + "value": "Objecttype" + } + ], "0FD2pY": [ { "type": 0, @@ -445,6 +451,12 @@ "value": "Maximum value" } ], + "3SCiQR": [ + { + "type": 0, + "value": "Copy configuration from registration" + } + ], "3W3Px9": [ { "type": 0, @@ -753,6 +765,12 @@ "value": "Key" } ], + "6qPITC": [ + { + "type": 0, + "value": "Something went wrong while retrieving the available object types." + } + ], "7/Bhpw": [ { "type": 0, @@ -1583,6 +1601,12 @@ "value": "Move down" } ], + "DJ8XX2": [ + { + "type": 0, + "value": "Select a registration backend and click the button to copy the configuration." + } + ], "DJWATl": [ { "type": 0, @@ -2079,6 +2103,12 @@ "value": "BRP Personen purpose limitation header value" } ], + "HdRpKT": [ + { + "type": 0, + "value": "Version" + } + ], "Hfdfv2": [ { "type": 0, @@ -4491,6 +4521,12 @@ "value": "Pricing logic" } ], + "fQuLvm": [ + { + "type": 0, + "value": "Copy configuration from registration backend" + } + ], "fRMCJI": [ { "type": 0, @@ -4943,6 +4979,12 @@ "value": "Edit complex variable" } ], + "jU6W0l": [ + { + "type": 0, + "value": "Copy" + } + ], "jUPlH5": [ { "type": 0, @@ -5091,12 +5133,6 @@ "value": "Subject of the email sent to the registration backend to notify a change in the payment status." } ], - "kWw9qL": [ - { - "type": 0, - "value": "This product will be send allong form submits." - } - ], "kg/eh1": [ { "type": 0, @@ -5283,6 +5319,12 @@ "value": "Is sensitive data" } ], + "m83ECr": [ + { + "type": 0, + "value": "Copying the configuration from the registration backend will clear the existing configuration. Are you sure you want to continue?" + } + ], "mCHS+l": [ { "type": 0, @@ -5499,6 +5541,12 @@ "value": "Whether to include the content of the confirmation page in the PDF." } ], + "oqv4fM": [ + { + "type": 0, + "value": "The prefill values will be taken from an object of the selected type." + } + ], "osSl3z": [ { "type": 0, diff --git a/src/openforms/js/compiled-lang/nl.json b/src/openforms/js/compiled-lang/nl.json index 0521676ebb..11b9a0809d 100644 --- a/src/openforms/js/compiled-lang/nl.json +++ b/src/openforms/js/compiled-lang/nl.json @@ -149,6 +149,12 @@ "value": "Patroon of waarde voor de stad" } ], + "0EYS7D": [ + { + "type": 0, + "value": "Objecttype" + } + ], "0FD2pY": [ { "type": 0, @@ -445,6 +451,12 @@ "value": "Maximale waarde" } ], + "3SCiQR": [ + { + "type": 0, + "value": "Copy configuration from registration" + } + ], "3W3Px9": [ { "type": 0, @@ -753,6 +765,12 @@ "value": "Sleutel" } ], + "6qPITC": [ + { + "type": 0, + "value": "Something went wrong while retrieving the available object types." + } + ], "7/Bhpw": [ { "type": 0, @@ -1604,6 +1622,12 @@ "value": "Verplaats omlaag" } ], + "DJ8XX2": [ + { + "type": 0, + "value": "Select a registration backend and click the button to copy the configuration." + } + ], "DJWATl": [ { "type": 0, @@ -2100,6 +2124,12 @@ "value": "BRP Personen 'doelbinding' header-waarde" } ], + "HdRpKT": [ + { + "type": 0, + "value": "Version" + } + ], "Hfdfv2": [ { "type": 0, @@ -4513,6 +4543,12 @@ "value": "Prijslogica" } ], + "fQuLvm": [ + { + "type": 0, + "value": "Copy configuration from registration backend" + } + ], "fRMCJI": [ { "type": 0, @@ -4965,6 +5001,12 @@ "value": "Wijzig complexe waarde" } ], + "jU6W0l": [ + { + "type": 0, + "value": "Copy" + } + ], "jUPlH5": [ { "type": 0, @@ -5113,12 +5155,6 @@ "value": "Subject of the email sent to the registration backend to notify a change in the payment status." } ], - "kWw9qL": [ - { - "type": 0, - "value": "Dit product wordt met inzendingen meegestuurd." - } - ], "kg/eh1": [ { "type": 0, @@ -5305,6 +5341,12 @@ "value": "(Privacy)gevoelige gegevens" } ], + "m83ECr": [ + { + "type": 0, + "value": "Copying the configuration from the registration backend will clear the existing configuration. Are you sure you want to continue?" + } + ], "mCHS+l": [ { "type": 0, @@ -5521,6 +5563,12 @@ "value": "Vink aan om de inhoud toe te voegen aan de PDF die mensen op het eind kunnen downloaden." } ], + "oqv4fM": [ + { + "type": 0, + "value": "The prefill values will be taken from an object of the selected type." + } + ], "osSl3z": [ { "type": 0, diff --git a/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js b/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js index 631595ccd9..a2ba06bcbc 100644 --- a/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js +++ b/src/openforms/js/components/admin/form_design/RegistrationFields.stories.js @@ -1,5 +1,4 @@ import {expect, fn, screen, userEvent, waitFor, within} from '@storybook/test'; -import selectEvent from 'react-select-event'; import {mockProcessDefinitionsGet} from 'components/admin/form_design/registrations/camunda/mocks'; import { @@ -17,6 +16,7 @@ import { FormDecorator, ValidationErrorsDecorator, } from 'components/admin/form_design/story-decorators'; +import {rsSelect} from 'utils/storybookTestHelpers'; import RegistrationFields from './RegistrationFields'; @@ -729,7 +729,7 @@ export const ObjectsAPI = { await userEvent.click(within(fieldsetTitle).getByRole('link', {name: '(Tonen)'})); const catalogueSelect = modal.getByLabelText('Catalogus'); - await selectEvent.select(catalogueSelect, 'Catalogus 2'); + await rsSelect(catalogueSelect, 'Catalogus 2'); }); await step('Submit the form', async () => { diff --git a/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNActionConfig.stories.js b/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNActionConfig.stories.js index 2638f48e77..a94e86a518 100644 --- a/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNActionConfig.stories.js +++ b/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNActionConfig.stories.js @@ -1,5 +1,4 @@ import {expect, fireEvent, fn, userEvent, waitFor, within} from '@storybook/test'; -import selectEvent from 'react-select-event'; import { mockDMNDecisionDefinitionVersionsGet, @@ -8,7 +7,7 @@ import { } from 'components/admin/form_design/mocks'; import {FormDecorator} from 'components/admin/form_design/story-decorators'; import {serializeValue} from 'components/admin/forms/VariableMapping'; -import {getReactSelectContainer} from 'utils/storybookTestHelpers'; +import {getReactSelectContainer, rsSelect} from 'utils/storybookTestHelpers'; import DMNActionConfig from './DMNActionConfig'; @@ -232,7 +231,7 @@ export const Empty = { const [formVarsDropdowns, dmnVarsDropdown] = dropdowns; - await selectEvent.select(formVarsDropdowns, 'Name'); + await rsSelect(formVarsDropdowns, 'Name'); // this is super flaky for some reason on both Chromium and Firefox :/ await waitFor(async () => { await userEvent.selectOptions(dmnVarsDropdown, 'Camunda variable'); 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 6774ccd44c..8d277e1c4f 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 @@ -43,8 +43,28 @@ const LegacyConfigFields = ({apiGroupChoices}) => ( /> } > - - + + } + helpText={ + + } + /> + + } + /> diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsFormFields.stories.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsFormFields.stories.js index fa0d755abc..ddee0bdd9d 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsFormFields.stories.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/ObjectsApiOptionsFormFields.stories.js @@ -1,11 +1,11 @@ import {expect, fn, userEvent, waitFor, within} from '@storybook/test'; import {Form, Formik} from 'formik'; -import selectEvent from 'react-select-event'; import { FeatureFlagsDecorator, ValidationErrorsDecorator, } from 'components/admin/form_design/story-decorators'; +import {rsSelect} from 'utils/storybookTestHelpers'; import ObjectsApiOptionsFormFields from './ObjectsApiOptionsFormFields'; import { @@ -92,7 +92,7 @@ export const SwitchToV2Empty = { }); const groupSelect = canvas.getByLabelText('API-groep'); - await selectEvent.select(groupSelect, 'Objects API group 1'); + await rsSelect(groupSelect, 'Objects API group 1'); const testForm = await canvas.findByTestId('test-form'); await waitFor(() => { @@ -280,7 +280,7 @@ export const APIFetchError = { await step('Retrieving object types', async () => { const groupSelect = canvas.getByLabelText('API-groep'); - await selectEvent.select(groupSelect, 'Objects API group 1'); + await rsSelect(groupSelect, 'Objects API group 1'); const errorMessage = await canvas.findByText( 'Er ging iets fout bij het ophalen van de objecttypes.' @@ -350,9 +350,9 @@ export const SelectDocumentType = { await userEvent.click(within(fieldsetTitle).getByRole('link', {name: '(Tonen)'})); const catalogueSelect = canvas.getByLabelText('Catalogus'); - await selectEvent.select(catalogueSelect, 'Catalogus 1'); + await rsSelect(catalogueSelect, 'Catalogus 1'); const pdfSelect = canvas.getByLabelText('Informatieobjecttype inzendings-PDF'); - await selectEvent.select(pdfSelect, 'Test PDF'); + await rsSelect(pdfSelect, 'Test PDF'); const testForm = await canvas.findByTestId('test-form'); await waitFor(() => { @@ -363,7 +363,7 @@ export const SelectDocumentType = { }); }); - await selectEvent.select(catalogueSelect, 'Catalogus 2'); + await rsSelect(catalogueSelect, 'Catalogus 2'); await waitFor(() => { expect(testForm).toHaveFormValues({ iotSubmissionReport: '', diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js index c2a8ad52ba..a65c8a0717 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/V2ConfigFields.js @@ -69,6 +69,18 @@ const V2ConfigFields = ({apiGroupChoices}) => { } > + } + helpText={ + + } onChangeCheck={async () => { if (variablesMapping.length === 0) return true; const confirmSwitch = await openObjectTypeConfirmation(); @@ -87,7 +99,14 @@ const V2ConfigFields = ({apiGroupChoices}) => { /> } > - + + } + /> diff --git a/src/openforms/js/components/admin/form_design/registrations/zgw/ZGWOptionsFormFields.stories.js b/src/openforms/js/components/admin/form_design/registrations/zgw/ZGWOptionsFormFields.stories.js index c12994c9d2..3d6705ac98 100644 --- a/src/openforms/js/components/admin/form_design/registrations/zgw/ZGWOptionsFormFields.stories.js +++ b/src/openforms/js/components/admin/form_design/registrations/zgw/ZGWOptionsFormFields.stories.js @@ -1,12 +1,12 @@ import {fn, userEvent, within} from '@storybook/test'; import {Form, Formik} from 'formik'; -import selectEvent from 'react-select-event'; import { FeatureFlagsDecorator, FormDecorator, ValidationErrorsDecorator, } from 'components/admin/form_design/story-decorators'; +import {rsSelect} from 'utils/storybookTestHelpers'; import ZGWFormFields from './ZGWOptionsFormFields'; import {mockCaseTypesGet, mockCataloguesGet, mockProductsGet} from './mocks'; @@ -119,11 +119,11 @@ export const SelectCaseType = { const canvas = within(canvasElement); const catalogueSelect = canvas.getByLabelText('Catalogus'); - await selectEvent.select(catalogueSelect, 'Catalogus 1'); + await rsSelect(catalogueSelect, 'Catalogus 1'); const caseTypeSelect = canvas.getByLabelText('Zaaktype', { selector: '#id_caseTypeIdentification', }); - await selectEvent.select(caseTypeSelect, 'Request passport'); + await rsSelect(caseTypeSelect, 'Request passport'); }, }; diff --git a/src/openforms/js/components/admin/form_design/story-decorators.js b/src/openforms/js/components/admin/form_design/story-decorators.js index a24646460d..208ea5bb43 100644 --- a/src/openforms/js/components/admin/form_design/story-decorators.js +++ b/src/openforms/js/components/admin/form_design/story-decorators.js @@ -2,6 +2,7 @@ import {fn} from '@storybook/test'; import {Form, Formik} from 'formik'; import {FeatureFlagsContext, FormContext} from 'components/admin/form_design/Context'; +import {ReactSelectContext} from 'components/admin/forms/ReactSelect'; import {ValidationErrorsProvider} from 'components/admin/forms/ValidationErrors'; import {ModalContext} from 'components/admin/modals'; @@ -104,3 +105,14 @@ export const withModalDecorator = Story => ( ); + +export const withReactSelectDecorator = Story => ( + document.getElementById('storybook-root'), + }} + > + + +); diff --git a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js index 8c9244a808..610d5f44b6 100644 --- a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js +++ b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js @@ -9,7 +9,7 @@ import {mockTargetPathsPost} from 'components/admin/form_design/registrations/ob import {serializeValue} from '../../forms/VariableMapping'; import {mockObjecttypeVersionsGet, mockObjecttypesGet} from '../registrations/objectsapi/mocks'; -import {FormDecorator} from '../story-decorators'; +import {FormDecorator, withReactSelectDecorator} from '../story-decorators'; import VariablesEditor from './VariablesEditor'; BACKEND_OPTIONS_FORMS.testPlugin = { @@ -80,9 +80,9 @@ const VARIABLES = [ initialValue: [], prefillOptions: { objectsApiGroup: 1, - objecttype: '2c77babf-a967-4057-9969-0200320d23f2', + objecttypeUuid: '2c77babf-a967-4057-9969-0200320d23f2', objecttypeVersion: 1, - variablesMapping: [{formVariable: 'formioComponent', prefillProperty: ['firstName']}], + variablesMapping: [{variableKey: 'formioComponent', targetPath: ['firstName']}], }, }, ]; @@ -626,6 +626,125 @@ export const ConfigurePrefillObjectsAPI = { }, }; +export const ConfigurePrefillObjectsAPIWithCopyButton = { + // decorators: [FormDecorator, withReactSelectDecorator], + args: { + registrationBackends: [ + { + backend: 'objects_api', + key: 'objects_api_1', + name: 'Example Objects API reg.', + options: { + version: 2, + objectsApiGroup: 1, + objecttype: '2c77babf-a967-4057-9969-0200320d23f1', + objecttypeVersion: 2, + variablesMapping: [ + { + variableKey: 'formioComponent', + targetPath: ['height'], + }, + { + variableKey: 'userDefined', + targetPath: ['species'], + }, + ], + }, + }, + { + backend: 'objects_api', + key: 'objects_api_2', + name: 'Other Objects API registration with a long name', + options: { + version: 2, + objectsApiGroup: 1, + objecttype: '209e0341-834d-4060-bd19-a3419d19ed74', + objecttypeVersion: 2, + variablesMapping: [ + { + variableKey: 'formioComponent', + targetPath: ['path', 'to.the', 'target'], + }, + ], + }, + }, + ], + }, + play: async ({canvasElement, step}) => { + const canvas = within(canvasElement); + + await step('Open configuration modal', async () => { + const userDefinedVarsTab = await canvas.findByRole('tab', {name: 'Gebruikersvariabelen'}); + expect(userDefinedVarsTab).toBeVisible(); + await userEvent.click(userDefinedVarsTab); + + // open modal for configuration + const editIcon = canvas.getByTitle('Prefill instellen'); + await userEvent.click(editIcon); + expect(await screen.findByRole('dialog')).toBeVisible(); + }); + + await step('Configure Objects API prefill with copy button', async () => { + const modal = within(await screen.findByRole('dialog')); + const pluginDropdown = await screen.findByLabelText('Plugin'); + expect(pluginDropdown).toBeVisible(); + await userEvent.selectOptions(pluginDropdown, 'Objects API'); + + const toggleCopyDropdown = await modal.findByRole('link', { + name: 'Copy configuration from registration', + }); + expect(toggleCopyDropdown).toBeVisible(); + await userEvent.click(toggleCopyDropdown); + + const copyDropdown = await modal.findByLabelText( + 'Copy configuration from registration backend' + ); + expect(copyDropdown).toBeVisible(); + await userEvent.click(copyDropdown); + + // Cannot do selectOption with react-select + const options = await canvas.findAllByText('Example Objects API reg.'); + const option = options[1]; + console.log(option); + await userEvent.click(option); + + const copyButton = await canvas.findByRole('button', { + name: 'Copy', + }); + expect(copyButton).toBeVisible(); + await userEvent.click(copyButton); + + // Click the confirmation button + const button = canvas.getByRole('button', { + name: 'Accepteren', + }); + expect(button).toBeVisible(); + await userEvent.click(button); + + const modalForm = await screen.findByTestId('modal-form'); + expect(modalForm).toBeVisible(); + const propertyDropdowns = await modal.findAllByLabelText( + 'Selecteer een attribuut uit het objecttype' + ); + + // Wait until the API call to retrieve the prefillAttributes is done + await waitFor( + async () => { + expect(modalForm).toHaveFormValues({ + 'options.objectsApiGroup': '1', + 'options.objecttypeUuid': '2c77babf-a967-4057-9969-0200320d23f1', + 'options.objecttypeVersion': '2', + }); + + expect(propertyDropdowns[0]).toHaveValue(serializeValue(['height'])); + expect(propertyDropdowns[1]).toHaveValue(serializeValue(['species'])); + }, + {timeout: 2000} + ); + }); + }, +}; + export const WithValidationErrors = { args: { variables: [ diff --git a/src/openforms/js/components/admin/form_design/variables/VariablesTable.js b/src/openforms/js/components/admin/form_design/variables/VariablesTable.js index 683893efc6..49d7d35f89 100644 --- a/src/openforms/js/components/admin/form_design/variables/VariablesTable.js +++ b/src/openforms/js/components/admin/form_design/variables/VariablesTable.js @@ -162,11 +162,12 @@ const EditableVariableRow = ({index, variable, onDelete, onChange, onFieldChange identifierRole={variable.prefillIdentifierRole} errors={variable.errors} options={variable.prefillOptions} - onChange={({plugin, attribute, identifierRole}) => + onChange={({plugin, attribute, identifierRole, options}) => onChange(variable.key, '', { prefillPlugin: plugin, prefillAttribute: attribute, prefillIdentifierRole: identifierRole, + prefillOptions: options, }) } /> diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/PrefillConfigurationForm.js b/src/openforms/js/components/admin/form_design/variables/prefill/PrefillConfigurationForm.js index 2236202572..3a25d33491 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/PrefillConfigurationForm.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/PrefillConfigurationForm.js @@ -1,5 +1,6 @@ import {Formik} from 'formik'; import PropTypes from 'prop-types'; +import {useState} from 'react'; import {FormattedMessage} from 'react-intl'; import {SubmitAction} from 'components/admin/forms/ActionButton'; @@ -8,14 +9,8 @@ import Fieldset from 'components/admin/forms/Fieldset'; import FormRow from 'components/admin/forms/FormRow'; import SubmitRow from 'components/admin/forms/SubmitRow'; -import DefaultFields from './DefaultFields'; -import ObjectsAPIFields from './ObjectsAPIFields'; import PluginField from './PluginField'; - -const PLUGIN_COMPONENT_MAPPING = { - objects_api: ObjectsAPIFields, - default: DefaultFields, -}; +import PLUGIN_COMPONENT_MAPPING from './constants'; const PrefillConfigurationForm = ({ onSubmit, @@ -23,12 +18,7 @@ const PrefillConfigurationForm = ({ attribute = '', identifierRole = 'main', // TODO: find a better way to specify this based on the selected plugin - options = { - objectsApiGroup: '', - objecttype: '', - objecttypeVersion: null, - variablesMapping: [], - }, + options = {}, errors, }) => { return ( @@ -40,15 +30,25 @@ const PrefillConfigurationForm = ({ options, }} onSubmit={(values, actions) => { - // TODO should be implemented in https://github.com/open-formulieren/open-forms/issues/4693 - console.log(values); onSubmit(values); actions.setSubmitting(false); }} > {({handleSubmit, values}) => { const PluginFormComponent = - PLUGIN_COMPONENT_MAPPING[values.plugin] ?? PLUGIN_COMPONENT_MAPPING.default; + PLUGIN_COMPONENT_MAPPING[values.plugin]?.component ?? + PLUGIN_COMPONENT_MAPPING.default.component; + const ToggleCopyComponent = + PLUGIN_COMPONENT_MAPPING[values.plugin]?.toggleCopyComponent ?? + PLUGIN_COMPONENT_MAPPING.default.toggleCopyComponent; + + const [showCopyButton, setShowCopyButton] = useState(false); + + const handleToggle = event => { + event.preventDefault(); + setShowCopyButton(!showCopyButton); + }; + return ( <>
@@ -63,12 +63,25 @@ const PrefillConfigurationForm = ({ } errors={errors.plugin} > - + <> + + {ToggleCopyComponent ? ( +
+ +
+ ) : null} +
- + { const [fieldProps] = useField('attribute'); @@ -21,7 +21,7 @@ const AttributeField = ({prefillAttributes}) => { }; AttributeField.propTypes = { - prefillAttributes: PropTypes.arrayOf(PropTypes.string).isRequired, + prefillAttributes: SelectChoicesType.isRequired, }; export default AttributeField; diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/DefaultFields.js b/src/openforms/js/components/admin/form_design/variables/prefill/default/DefaultFields.js similarity index 98% rename from src/openforms/js/components/admin/form_design/variables/prefill/DefaultFields.js rename to src/openforms/js/components/admin/form_design/variables/prefill/default/DefaultFields.js index 49ebdd0bc0..0d1c80d5dc 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/DefaultFields.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/default/DefaultFields.js @@ -9,9 +9,9 @@ import FormRow from 'components/admin/forms/FormRow'; import {LOADING_OPTION} from 'components/admin/forms/Select'; import {get} from 'utils/fetch'; +import {ErrorsType} from '../types'; import AttributeField from './AttributeField'; import IdentifierRoleField from './IdentifierRoleField'; -import {ErrorsType} from './types'; // Load the possible prefill attributes // XXX: this would benefit from client-side caching diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/IdentifierRoleField.js b/src/openforms/js/components/admin/form_design/variables/prefill/default/IdentifierRoleField.js similarity index 88% rename from src/openforms/js/components/admin/form_design/variables/prefill/IdentifierRoleField.js rename to src/openforms/js/components/admin/form_design/variables/prefill/default/IdentifierRoleField.js index 201538643d..5014d3784a 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/IdentifierRoleField.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/default/IdentifierRoleField.js @@ -2,7 +2,7 @@ import {useField} from 'formik'; import Select from 'components/admin/forms/Select'; -import {IDENTIFIER_ROLE_CHOICES} from '../constants'; +import {IDENTIFIER_ROLE_CHOICES} from '../../constants'; const IdentifierRoleField = () => { const [fieldProps] = useField('identifierRole'); diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/CopyConfigurationFromRegistrationBackend.js b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/CopyConfigurationFromRegistrationBackend.js new file mode 100644 index 0000000000..1292b6aba4 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/CopyConfigurationFromRegistrationBackend.js @@ -0,0 +1,107 @@ +/** + * Prefill configuration form specific to the Objects API prefill plugin. + * + * Most other plugins can be configured with the generic form in `./DefaultFields`. + */ +import {useField, useFormikContext} from 'formik'; +import {FormattedMessage} from 'react-intl'; + +import useConfirm from 'components/admin/form_design/useConfirm'; +import Field from 'components/admin/forms/Field'; +import FormRow from 'components/admin/forms/FormRow'; +import ReactSelect from 'components/admin/forms/ReactSelect'; + +const CopyConfigurationFromRegistrationBackend = ({backends, setShowCopyButton}) => { + const name = 'copyConfigurationFromBackend'; + const {setFieldValue, setValues} = useFormikContext(); + const options = backends.map(elem => ({value: elem.key, label: elem.name})); + const [fieldProps] = useField(name); + const {value} = fieldProps; + const selectedBackend = backends.find(elem => elem.key === value); + const { + ConfirmationModal: CopyConfigurationConfirmationModal, + confirmationModalProps: copyConfigurationConfirmationModalProps, + openConfirmationModal: openCopyConfigurationConfirmationModal, + } = useConfirm(); + return ( + + + } + helpText={ + + } + noManageChildProps + > + <> + { + setFieldValue(name, selectedOption.value); + }} + maxMenuHeight="16em" + menuPlacement="bottom" + /> + + + + + + + } + /> + + ); +}; + +export default CopyConfigurationFromRegistrationBackend; diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/ObjectsAPIFields.js b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js similarity index 70% rename from src/openforms/js/components/admin/form_design/variables/prefill/ObjectsAPIFields.js rename to src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js index 269ae7f441..5f7b0d375f 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/ObjectsAPIFields.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ObjectsAPIFields.js @@ -5,13 +5,12 @@ */ import {useFormikContext} from 'formik'; import PropTypes from 'prop-types'; -import {useContext} from 'react'; +import {useContext, useEffect, useState} from 'react'; import {FormattedMessage, useIntl} from 'react-intl'; import useAsync from 'react-use/esm/useAsync'; import {FormContext} from 'components/admin/form_design/Context'; import useConfirm from 'components/admin/form_design/useConfirm'; -import Field from 'components/admin/forms/Field'; import Fieldset from 'components/admin/forms/Fieldset'; import FormRow from 'components/admin/forms/FormRow'; import {LOADING_OPTION} from 'components/admin/forms/Select'; @@ -25,7 +24,8 @@ import {FAIcon} from 'components/admin/icons'; import ErrorBoundary from 'components/errors/ErrorBoundary'; import {get} from 'utils/fetch'; -import {ErrorsType} from './types'; +import {ErrorsType} from '../types'; +import CopyConfigurationFromRegistrationBackend from './CopyConfigurationFromRegistrationBackend'; const PLUGIN_ID = 'objects_api'; @@ -36,7 +36,7 @@ const onApiGroupChange = prevValues => ({ ...prevValues, options: { ...prevValues.options, - objecttype: '', + objecttypeUuid: '', objecttypeVersion: undefined, variablesMapping: [], }, @@ -44,8 +44,8 @@ const onApiGroupChange = prevValues => ({ // Load the possible prefill properties // XXX: this would benefit from client-side caching -const getProperties = async (objectsApiGroup, objecttype, objecttypeVersion) => { - const endpoint = `/api/v2/prefill/plugins/objects-api/objecttypes/${objecttype}/versions/${objecttypeVersion}/properties`; +const getProperties = async (objectsApiGroup, objecttypeUuid, objecttypeVersion) => { + const endpoint = `/api/v2/prefill/plugins/objects-api/objecttypes/${objecttypeUuid}/versions/${objecttypeVersion}/properties`; // XXX: clean up error handling here at some point... const response = await get(endpoint, {objects_api_group: objectsApiGroup}); if (!response.ok) throw response.data; @@ -53,15 +53,33 @@ const getProperties = async (objectsApiGroup, objecttype, objecttypeVersion) => return response.data.map(property => [property.targetPath, property.targetPath.join(' > ')]); }; -const ObjectsAPIFields = ({errors}) => { +const ObjectsAPIFields = ({errors, showCopyButton, setShowCopyButton}) => { + const intl = useIntl(); + const { + values, values: { plugin, - options: {objecttype, objecttypeVersion, objectsApiGroup}, + options: {objecttypeUuid, objecttypeVersion, objectsApiGroup, variablesMapping}, }, setFieldValue, } = useFormikContext(); - const intl = useIntl(); + + const defaults = { + objectsApiGroup: '', + objecttypeUuid: '', + objecttypeVersion: null, + variablesMapping: [], + }; + + // Merge defaults into options if not already set + useEffect(() => { + if (!values.options) { + setFieldValue('options', defaults); + } else { + setFieldValue('options', {...defaults, ...values.options}); + } + }, []); const { ConfirmationModal: ApiGroupConfirmationModal, @@ -76,9 +94,11 @@ const ObjectsAPIFields = ({errors}) => { const { plugins: {availablePrefillPlugins}, + registrationBackends, } = useContext(FormContext); const objectsPlugin = availablePrefillPlugins.find(elem => elem.id === PLUGIN_ID); + const backends = registrationBackends.filter(elem => elem.backend === 'objects_api'); const {apiGroups} = objectsPlugin.configurationContext; const { @@ -86,13 +106,13 @@ const ObjectsAPIFields = ({errors}) => { value = [], error, } = useAsync(async () => { - if (!plugin || !objecttype || !objecttypeVersion || !objectsApiGroup) return []; + if (!plugin || !objecttypeUuid || !objecttypeVersion || !objectsApiGroup) return []; try { - return await getProperties(objectsApiGroup, objecttype, objecttypeVersion); + return await getProperties(objectsApiGroup, objecttypeUuid, objecttypeVersion); } catch (e) { throw e; } - }, [plugin, objecttype, objecttypeVersion, objectsApiGroup]); + }, [plugin, objecttypeUuid, objecttypeVersion, objectsApiGroup]); // throw errors to the nearest error boundary if (error) throw error; @@ -100,11 +120,17 @@ const ObjectsAPIFields = ({errors}) => { return ( <> + {showCopyButton && backends.length ? ( + + ) : null}
{ - if (values.options.variablesMapping.length === 0) return true; + if (!objecttypeUuid) return true; const confirmSwitch = await openApiGroupConfirmationModal(); if (!confirmSwitch) return false; setFieldValue('options.variablesMapping', []); @@ -115,17 +141,31 @@ const ObjectsAPIFields = ({errors}) => { /> } > + } + helpText={ + + } onChangeCheck={async () => { if (values.options.variablesMapping.length === 0) return true; const confirmSwitch = await openObjectTypeConfirmationModal(); @@ -137,6 +177,8 @@ const ObjectsAPIFields = ({errors}) => { { > + } apiGroupFieldName="options.objectsApiGroup" - objectTypeFieldName="options.objecttype" + objectTypeFieldName="options.objecttypeUuid" />
@@ -165,7 +213,8 @@ const ObjectsAPIFields = ({errors}) => { name="options.variablesMapping" loading={loading} directionIcon={