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 (
<>
-
+
{
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}
@@ -165,7 +213,8 @@ const ObjectsAPIFields = ({errors}) => {
name="options.variablesMapping"
loading={loading}
directionIcon={}
- propertyName="prefillProperty"
+ variableName="variableKey"
+ propertyName="targetPath"
propertyChoices={prefillProperties}
propertyHeading={
{
+ return (
+
+
+
+ );
+};
+
+export default ToggleCopyButton;
diff --git a/src/openforms/js/components/admin/forms/ReactSelect.js b/src/openforms/js/components/admin/forms/ReactSelect.js
index 263d9c838f..582fe2722e 100644
--- a/src/openforms/js/components/admin/forms/ReactSelect.js
+++ b/src/openforms/js/components/admin/forms/ReactSelect.js
@@ -2,6 +2,7 @@ import {getReactSelectStyles} from '@open-formulieren/formio-builder/esm/compone
import classNames from 'classnames';
import {useField} from 'formik';
import PropTypes from 'prop-types';
+import React, {createContext, useContext} from 'react';
import ReactSelect from 'react-select';
const initialStyles = getReactSelectStyles();
@@ -66,6 +67,9 @@ const getValue = (options, value) => {
return null;
};
+export const ReactSelectContext = createContext({parentSelector: () => document.body});
+ReactSelectContext.displayName = 'ReactSelectContext';
+
/**
* A select dropdown backed by react-select for legacy usage.
*
@@ -74,22 +78,26 @@ const getValue = (options, value) => {
* @deprecated - if possible, refactor the form to use Formik and use the Formik-enabled
* variant.
*/
-const SelectWithoutFormik = ({name, options, value, className, onChange, ...props}) => (
- {
- onChange(selectedOption === null ? undefined : selectedOption.value);
- }}
- {...props}
- />
-);
+const SelectWithoutFormik = ({name, options, value, className, onChange, ...props}) => {
+ const {parentSelector} = useContext(ReactSelectContext);
+ return (
+ {
+ onChange(selectedOption === null ? undefined : selectedOption.value);
+ }}
+ {...props}
+ />
+ );
+};
/**
* A select dropdown backed by react-select for Formik forms.
@@ -97,6 +105,7 @@ const SelectWithoutFormik = ({name, options, value, className, onChange, ...prop
* Any additional props are forwarded to the underlying ReactSelect component.
*/
const SelectWithFormik = ({name, options, className, ...props}) => {
+ const {parentSelector} = useContext(ReactSelectContext);
const [fieldProps, , fieldHelpers] = useField(name);
const {value} = fieldProps;
const {setValue} = fieldHelpers;
@@ -107,6 +116,7 @@ const SelectWithFormik = ({name, options, className, ...props}) => {
classNamePrefix="admin-react-select"
styles={styles}
menuPlacement="auto"
+ menuPortalTarget={parentSelector()}
options={options}
{...fieldProps}
value={getValue(options, value)}
diff --git a/src/openforms/js/components/admin/forms/Select.js b/src/openforms/js/components/admin/forms/Select.js
index 0c901c270c..ba5e29832d 100644
--- a/src/openforms/js/components/admin/forms/Select.js
+++ b/src/openforms/js/components/admin/forms/Select.js
@@ -63,10 +63,15 @@ const Value = PropTypes.oneOfType([PropTypes.string, PropTypes.number]);
// Not excatly the 2-tuple we mean, but hey...
const Choice = PropTypes.arrayOf(PropTypes.oneOfType([Value, Label]));
+export const SelectChoicesType = PropTypes.oneOfType([
+ PropTypes.arrayOf(Choice),
+ PropTypes.objectOf(Label),
+]);
+
Select.propTypes = {
name: PropTypes.string, // typically injected by the wrapping component
allowBlank: PropTypes.bool,
- choices: PropTypes.oneOfType([PropTypes.arrayOf(Choice), PropTypes.objectOf(Label)]).isRequired,
+ choices: SelectChoicesType.isRequired,
translateChoices: PropTypes.bool,
};
diff --git a/src/openforms/js/components/admin/forms/VariableMapping.js b/src/openforms/js/components/admin/forms/VariableMapping.js
index 0b0602e59c..825abd82d5 100644
--- a/src/openforms/js/components/admin/forms/VariableMapping.js
+++ b/src/openforms/js/components/admin/forms/VariableMapping.js
@@ -39,6 +39,7 @@ const VariableMappingRow = ({
prefix,
loading,
directionIcon,
+ variableName,
propertyName,
propertyChoices,
propertySelectLabel,
@@ -50,6 +51,7 @@ const VariableMappingRow = ({
const intl = useIntl();
const {getFieldProps, setFieldValue} = useFormikContext();
+ const fullVariableName = `${prefix}.${variableName}`;
const fullPropertyName = `${prefix}.${propertyName}`;
const {value: propertyValue, ...propertyProps} = getFieldProps(fullPropertyName);
const serializedPropertyValue = serializeValue(propertyValue);
@@ -66,10 +68,10 @@ const VariableMappingRow = ({
return (
-
+
{
// TODO update
- const initial = {formVariable: '', [propertyName]: ''};
+ const initial = {[variableName]: '', [propertyName]: ''};
const mapping = get(values, name);
arrayHelpers.insert(mapping.length, initial);
}}
@@ -274,6 +286,13 @@ VariableMapping.propTypes = {
*/
directionIcon: PropTypes.node,
+ /**
+ * Name of the variable nested inside each mapping item.
+ *
+ * This is the form variable to which the property will be mapped.
+ */
+ variableName: PropTypes.string.isRequired,
+
/**
* Name of the property nested inside each mapping item.
*
diff --git a/src/openforms/js/components/admin/forms/VariableMapping.stories.js b/src/openforms/js/components/admin/forms/VariableMapping.stories.js
index a999071807..b6800d5c49 100644
--- a/src/openforms/js/components/admin/forms/VariableMapping.stories.js
+++ b/src/openforms/js/components/admin/forms/VariableMapping.stories.js
@@ -1,9 +1,9 @@
-import {expect, fn, userEvent, waitFor, within} from '@storybook/test';
+import {expect, fn, userEvent, within} from '@storybook/test';
import selectEvent from 'react-select-event';
import {FormDecorator, FormikDecorator} from 'components/admin/form_design/story-decorators';
import {VARIABLE_SOURCES} from 'components/admin/form_design/variables/constants';
-import {getReactSelectContainer} from 'utils/storybookTestHelpers';
+import {findReactSelectMenu} from 'utils/storybookTestHelpers';
import VariableMapping, {serializeValue} from './VariableMapping';
@@ -149,9 +149,9 @@ export const SelectOptions = {
const formVariableDropdown = canvas.getByLabelText('Formuliervariabele');
selectEvent.openMenu(formVariableDropdown);
- const formVarOptions = await within(
- getReactSelectContainer(formVariableDropdown)
- ).findAllByRole('option');
+ const formVarOptions = await within(await findReactSelectMenu(canvas)).findAllByRole(
+ 'option'
+ );
expect(formVarOptions).toHaveLength(2);
expect(formVarOptions[0]).toHaveTextContent('key2');
diff --git a/src/openforms/js/components/admin/forms/VariableSelection.stories.js b/src/openforms/js/components/admin/forms/VariableSelection.stories.js
index 0c9b6ccb94..8da4d86efc 100644
--- a/src/openforms/js/components/admin/forms/VariableSelection.stories.js
+++ b/src/openforms/js/components/admin/forms/VariableSelection.stories.js
@@ -3,6 +3,7 @@ import {useArgs} from '@storybook/preview-api';
import {FormDecorator} from 'components/admin/form_design/story-decorators';
import {VARIABLE_SOURCES} from '../form_design/variables/constants';
+import {ReactSelectContext} from './ReactSelect';
import VariableSelection from './VariableSelection';
const render = ({name, includeStaticVariables, filter, menuIsOpen = false}) => {
@@ -117,6 +118,14 @@ export default {
export const Default = {};
export const menuOpen = {
+ decorators: [
+ // workaround for https://github.com/JedWatson/react-select/issues/3708
+ Story => (
+ undefined}}>
+
+
+ ),
+ ],
args: {
menuIsOpen: true,
},
diff --git a/src/openforms/js/components/admin/forms/objects_api/ObjectTypeSelect.js b/src/openforms/js/components/admin/forms/objects_api/ObjectTypeSelect.js
index 5e80d60cd3..157b3ba7d2 100644
--- a/src/openforms/js/components/admin/forms/objects_api/ObjectTypeSelect.js
+++ b/src/openforms/js/components/admin/forms/objects_api/ObjectTypeSelect.js
@@ -1,6 +1,5 @@
import {useField, useFormikContext} from 'formik';
import PropTypes from 'prop-types';
-import {FormattedMessage} from 'react-intl';
import {usePrevious, useUpdateEffect} from 'react-use';
import useAsync from 'react-use/esm/useAsync';
@@ -24,6 +23,8 @@ const ObjectTypeSelect = ({
name = 'objecttype',
apiGroupFieldName = 'objectsApiGroup',
onChangeCheck,
+ label,
+ helpText,
versionFieldName = 'objecttypeVersion',
}) => {
const [fieldProps, , fieldHelpers] = useField(name);
@@ -67,31 +68,15 @@ const ObjectTypeSelect = ({
return (
-
- }
- helpText={
-
- }
- noManageChildProps
- >
+
{
- const okToProceed = onChangeCheck === undefined || onChangeCheck();
+ onChange={async selectedOption => {
+ const okToProceed = onChangeCheck === undefined || (await onChangeCheck());
if (okToProceed) setValue(selectedOption.value);
}}
/>
@@ -107,9 +92,17 @@ ObjectTypeSelect.propTypes = {
name: PropTypes.string,
/**
* Optional callback to confirm the change. Return `true` to continue with the change,
- * return `false` to abort it.
+ * return `false` to abort it. The callback function must be async.
*/
onChangeCheck: PropTypes.func,
+ /**
+ * The label that will be shown before the field
+ */
+ label: PropTypes.string.isRequired,
+ /**
+ * The help text to explain what the field is for
+ */
+ helpText: PropTypes.string.isRequired,
/**
* Name of the field holding the selected API group. The value is used in the API
* call to get the available object types.
diff --git a/src/openforms/js/components/admin/forms/objects_api/ObjectTypeVersionSelect.js b/src/openforms/js/components/admin/forms/objects_api/ObjectTypeVersionSelect.js
index 63a4b7028a..126e08275b 100644
--- a/src/openforms/js/components/admin/forms/objects_api/ObjectTypeVersionSelect.js
+++ b/src/openforms/js/components/admin/forms/objects_api/ObjectTypeVersionSelect.js
@@ -29,6 +29,7 @@ const ObjectTypeVersionSelect = ({
name = 'objecttypeVersion',
apiGroupFieldName = 'objectsApiGroup',
objectTypeFieldName = 'objecttype',
+ label,
}) => {
const {getFieldProps} = useFormikContext();
@@ -54,17 +55,7 @@ const ObjectTypeVersionSelect = ({
const options = choices.map(([value, label]) => ({value, label}));
return (
-
- }
- noManageChildProps
- >
+
option.value === normalizedValue)}
required={required}
- onChange={selectedOption => {
- const okToProceed = onChangeCheck === undefined || onChangeCheck();
+ onChange={async selectedOption => {
+ const okToProceed = onChangeCheck === undefined || (await onChangeCheck());
if (okToProceed) {
// normalize empty string back to null
const newValue = selectedOption ? selectedOption.value : null;
diff --git a/src/openforms/js/lang/en.json b/src/openforms/js/lang/en.json
index a575ce961b..35192bab32 100644
--- a/src/openforms/js/lang/en.json
+++ b/src/openforms/js/lang/en.json
@@ -69,6 +69,11 @@
"description": "Email registration options 'attachmentFormats' label",
"originalDefault": "The format(s) of the attachment(s) containing the submission details"
},
+ "0EYS7D": {
+ "defaultMessage": "Objecttype",
+ "description": "Objects API prefill options 'Objecttype' label",
+ "originalDefault": "Objecttype"
+ },
"0M2B7B": {
"defaultMessage": "DMN variable",
"description": "DMN variable label",
@@ -199,6 +204,11 @@
"description": "Logic expression generated description",
"originalDefault": "When {desc}"
},
+ "3SCiQR": {
+ "defaultMessage": "Copy configuration from registration",
+ "description": "Objects API prefill options: link to show copy from registration button",
+ "originalDefault": "Copy configuration from registration"
+ },
"3W3Px9": {
"defaultMessage": "Add another",
"description": "Button text to add extra item",
@@ -319,6 +329,11 @@
"description": "Variable table key title",
"originalDefault": "Key"
},
+ "6qPITC": {
+ "defaultMessage": "Something went wrong while retrieving the available object types.",
+ "description": "Objects API prefill options: object type select error",
+ "originalDefault": "Something went wrong while retrieving the available object types."
+ },
"7/Bhpw": {
"defaultMessage": "Please fill in the name of the previous variable before adding another",
"description": "Warning to finish invalid user defined variable",
@@ -674,6 +689,11 @@
"description": "Move down icon title",
"originalDefault": "Move down"
},
+ "DJ8XX2": {
+ "defaultMessage": "Select a registration backend and click the button to copy the configuration.",
+ "description": "Copy Objects API prefill configuration from registration backend help text",
+ "originalDefault": "Select a registration backend and click the button to copy the configuration."
+ },
"DRG6XN": {
"defaultMessage": "invalid input mapping(s)",
"description": "Warning message: DMN input mapping problems detected",
@@ -944,6 +964,11 @@
"description": "Form 'BRP Personen purpose limitation header value' field label",
"originalDefault": "BRP Personen purpose limitation header value"
},
+ "HdRpKT": {
+ "defaultMessage": "Version",
+ "description": "Objects API prefill options 'objecttypeVersion' label",
+ "originalDefault": "Version"
+ },
"Hfdfv2": {
"defaultMessage": "Which version of the process definition to start. The latest version is used if not specified.",
"description": "Camunda 'process definition version' help text",
@@ -2079,6 +2104,11 @@
"description": "Dynamic pricing fieldset title",
"originalDefault": "Pricing logic"
},
+ "fQuLvm": {
+ "defaultMessage": "Copy configuration from registration backend",
+ "description": "Copy Objects API prefill configuration from registration backend label",
+ "originalDefault": "Copy configuration from registration backend"
+ },
"fTmdl/": {
"defaultMessage": "Payment status update variable mapping",
"description": "StUF-ZDS registration paymentStatusUpdateMapping label",
@@ -2314,6 +2344,11 @@
"description": "Camunda complex process var configuration modal title",
"originalDefault": "Edit complex variable"
},
+ "jU6W0l": {
+ "defaultMessage": "Copy",
+ "description": "Copy Objects API prefill configuration from registration backend button",
+ "originalDefault": "Copy"
+ },
"jUPlH5": {
"defaultMessage": "Resulting string",
"description": "Interpolation expression preview label",
@@ -2384,11 +2419,6 @@
"description": "Email registration options 'emailPaymentSubject' helpText",
"originalDefault": "Subject of the email sent to the registration backend to notify a change in the payment status."
},
- "kWw9qL": {
- "defaultMessage": "This product will be send allong form submits.",
- "description": "ZGW APIs registration options 'product' helpText",
- "originalDefault": "This product will be send allong form submits."
- },
"klKZV5": {
"defaultMessage": "Indicates whether the existing object (retrieved from an optional initial data reference) should be updated, instead of creating a new one. If no existing object exists, a new one will be created instead.",
"description": "Objects API registration: updateExistingObject helpText",
@@ -2434,6 +2464,11 @@
"description": "ZGW APIs registration options 'objecttype' help text",
"originalDefault": "URL to the object type in the objecttypes API. If provided, an object will be created and a case object relation will be added to the case."
},
+ "m83ECr": {
+ "defaultMessage": "Copying the configuration from the registration backend will clear the existing configuration. Are you sure you want to continue?",
+ "description": "Objects API prefill configuration: warning message when copying the config from registration backend",
+ "originalDefault": "Copying the configuration from the registration backend will clear the existing configuration. Are you sure you want to continue?"
+ },
"mCHS+l": {
"defaultMessage": "You have selected a catalogue - the document type URLs configured here will be ignored.",
"description": "Objects registration: warning that legacy document types will be ignored",
@@ -2539,6 +2574,11 @@
"description": "Include confirmation page content in PDF",
"originalDefault": "Whether to include the content of the confirmation page in the PDF."
},
+ "oqv4fM": {
+ "defaultMessage": "The prefill values will be taken from an object of the selected type.",
+ "description": "Objects API prefill options 'Objecttype' helpText",
+ "originalDefault": "The prefill values will be taken from an object of the selected type."
+ },
"oxYXJX": {
"defaultMessage": "How successful submissions of this form will be removed after the limit. Leave blank to use value in General Configuration.",
"description": "Successful Submissions Removal Method help text",
diff --git a/src/openforms/js/lang/nl.json b/src/openforms/js/lang/nl.json
index d601762c7a..cc64bb79cd 100644
--- a/src/openforms/js/lang/nl.json
+++ b/src/openforms/js/lang/nl.json
@@ -69,6 +69,11 @@
"description": "Email registration options 'attachmentFormats' label",
"originalDefault": "The format(s) of the attachment(s) containing the submission details"
},
+ "0EYS7D": {
+ "defaultMessage": "Objecttype",
+ "description": "Objects API prefill options 'Objecttype' label",
+ "originalDefault": "Objecttype"
+ },
"0M2B7B": {
"defaultMessage": "DMN-variabele",
"description": "DMN variable label",
@@ -200,6 +205,11 @@
"description": "Logic expression generated description",
"originalDefault": "When {desc}"
},
+ "3SCiQR": {
+ "defaultMessage": "Copy configuration from registration",
+ "description": "Objects API prefill options: link to show copy from registration button",
+ "originalDefault": "Copy configuration from registration"
+ },
"3W3Px9": {
"defaultMessage": "Nog één toevoegen",
"description": "Button text to add extra item",
@@ -322,6 +332,11 @@
"description": "Variable table key title",
"originalDefault": "Key"
},
+ "6qPITC": {
+ "defaultMessage": "Something went wrong while retrieving the available object types.",
+ "description": "Objects API prefill options: object type select error",
+ "originalDefault": "Something went wrong while retrieving the available object types."
+ },
"7/Bhpw": {
"defaultMessage": "Vul eerst de naam van de vorige variabele in voor u een nieuwe toevoegt",
"description": "Warning to finish invalid user defined variable",
@@ -679,6 +694,11 @@
"description": "Move down icon title",
"originalDefault": "Move down"
},
+ "DJ8XX2": {
+ "defaultMessage": "Select a registration backend and click the button to copy the configuration.",
+ "description": "Copy Objects API prefill configuration from registration backend help text",
+ "originalDefault": "Select a registration backend and click the button to copy the configuration."
+ },
"DRG6XN": {
"defaultMessage": "ongeldige invoerparameters",
"description": "Warning message: DMN input mapping problems detected",
@@ -951,6 +971,11 @@
"description": "Form 'BRP Personen purpose limitation header value' field label",
"originalDefault": "BRP Personen purpose limitation header value"
},
+ "HdRpKT": {
+ "defaultMessage": "Version",
+ "description": "Objects API prefill options 'objecttypeVersion' label",
+ "originalDefault": "Version"
+ },
"Hfdfv2": {
"defaultMessage": "Welke versie van de procesdefinitie moet gebruikt worden. Indien niet opgegeven dan wordt altijd de laatste versie gebruikt.",
"description": "Camunda 'process definition version' help text",
@@ -2096,6 +2121,11 @@
"description": "Dynamic pricing fieldset title",
"originalDefault": "Pricing logic"
},
+ "fQuLvm": {
+ "defaultMessage": "Copy configuration from registration backend",
+ "description": "Copy Objects API prefill configuration from registration backend label",
+ "originalDefault": "Copy configuration from registration backend"
+ },
"fTmdl/": {
"defaultMessage": "Payment status update variable mapping",
"description": "StUF-ZDS registration paymentStatusUpdateMapping label",
@@ -2332,6 +2362,11 @@
"description": "Camunda complex process var configuration modal title",
"originalDefault": "Edit complex variable"
},
+ "jU6W0l": {
+ "defaultMessage": "Copy",
+ "description": "Copy Objects API prefill configuration from registration backend button",
+ "originalDefault": "Copy"
+ },
"jUPlH5": {
"defaultMessage": "Tekstsjabloon",
"description": "Interpolation expression preview label",
@@ -2402,11 +2437,6 @@
"description": "Email registration options 'emailPaymentSubject' helpText",
"originalDefault": "Subject of the email sent to the registration backend to notify a change in the payment status."
},
- "kWw9qL": {
- "defaultMessage": "Dit product wordt met inzendingen meegestuurd.",
- "description": "ZGW APIs registration options 'product' helpText",
- "originalDefault": "This product will be send allong form submits."
- },
"klKZV5": {
"defaultMessage": "Geeft aan of een bestaande record (verkregen van de eventuele voor-invulreferentie) bijgewerkt moet worden in plaats van een nieuw object aan te maken. Als er geen bestaand object is, dan wordt een nieuwe aangemaakt.",
"description": "Objects API registration: updateExistingObject helpText",
@@ -2452,6 +2482,11 @@
"description": "ZGW APIs registration options 'objecttype' help text",
"originalDefault": "URL to the object type in the objecttypes API. If provided, an object will be created and a case object relation will be added to the case."
},
+ "m83ECr": {
+ "defaultMessage": "Copying the configuration from the registration backend will clear the existing configuration. Are you sure you want to continue?",
+ "description": "Objects API prefill configuration: warning message when copying the config from registration backend",
+ "originalDefault": "Copying the configuration from the registration backend will clear the existing configuration. Are you sure you want to continue?"
+ },
"mCHS+l": {
"defaultMessage": "Je hebt een catalogus geselecteerd - de hier ingestelde documenttype-URLs worden genegeerd.",
"description": "Objects registration: warning that legacy document types will be ignored",
@@ -2557,6 +2592,11 @@
"description": "Include confirmation page content in PDF",
"originalDefault": "Whether to include the content of the confirmation page in the PDF."
},
+ "oqv4fM": {
+ "defaultMessage": "The prefill values will be taken from an object of the selected type.",
+ "description": "Objects API prefill options 'Objecttype' helpText",
+ "originalDefault": "The prefill values will be taken from an object of the selected type."
+ },
"oxYXJX": {
"defaultMessage": "Geeft aan hoe voltooide inzendingen worden opgeschoond na de bewaartermijn. Laat leeg om de waarde van de algemene configuratie te gebruiken.",
"description": "Successful Submissions Removal Method help text",
diff --git a/src/openforms/js/utils/storybookTestHelpers.js b/src/openforms/js/utils/storybookTestHelpers.js
index 09fb2a8849..dbc92fdf04 100644
--- a/src/openforms/js/utils/storybookTestHelpers.js
+++ b/src/openforms/js/utils/storybookTestHelpers.js
@@ -1,3 +1,14 @@
+import selectEvent from 'react-select-event';
+
+const SB_ROOT = document.getElementById('storybook-root');
+
+/**
+ * Wrapper around selectEvent.select to ensure the portal option is used.
+ */
+const rsSelect = async (input, optionOrOptions) => {
+ await selectEvent.select(input, optionOrOptions, {container: SB_ROOT});
+};
+
/**
* From the input field (retrieved by accessible queries), find the react-select container.
*
@@ -16,4 +27,11 @@ const getReactSelectContainer = comboboxInput => {
return container;
};
-export {getReactSelectContainer};
+/**
+ * Get the (portaled) opened react select menu.
+ */
+const findReactSelectMenu = async canvas => {
+ return await canvas.findByRole('listbox');
+};
+
+export {rsSelect, getReactSelectContainer, findReactSelectMenu};
diff --git a/src/openforms/scss/components/admin/_ReactModal.scss b/src/openforms/scss/components/admin/_ReactModal.scss
index 51f867573b..0f8e2eb91a 100644
--- a/src/openforms/scss/components/admin/_ReactModal.scss
+++ b/src/openforms/scss/components/admin/_ReactModal.scss
@@ -7,7 +7,9 @@
align-items: center;
position: fixed;
- z-index: 1000;
+ // the base admin layout gets a z-index up until 2, so this ensures it's layered on top of the admin
+ // while leaving enough space for other UI elements on top of that (like react-select)
+ z-index: 3;
top: 0;
right: 0;
bottom: 0;
diff --git a/src/openforms/scss/vendor/_react-select.scss b/src/openforms/scss/vendor/_react-select.scss
index 7742660e24..42649d6b99 100644
--- a/src/openforms/scss/vendor/_react-select.scss
+++ b/src/openforms/scss/vendor/_react-select.scss
@@ -26,4 +26,8 @@
var(--of-admin-select-max-inline-size)
);
max-inline-size: var(--of-admin-select-max-inline-size);
+
+ &__menu-portal {
+ z-index: 4 !important; // one higher than the react-modal z-index
+ }
}
diff --git a/src/openforms/tests/e2e/base.py b/src/openforms/tests/e2e/base.py
index d821669090..77fd95ac16 100644
--- a/src/openforms/tests/e2e/base.py
+++ b/src/openforms/tests/e2e/base.py
@@ -150,7 +150,9 @@ async def rs_select_option(
await dropdown.focus()
await page.keyboard.press("ArrowDown")
- listbox = dropdown_root.get_by_role("listbox")
+ # The options are appended to document.body
+ listbox = page.get_by_role("listbox")
+
await expect(listbox).to_be_visible()
option = listbox.get_by_role("option", name=option_label, exact=exact)
|