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 6f37658e0e..acb7a3eb98 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 @@ -32,6 +32,15 @@ const onApiGroupChange = prevValues => ({ authAttributePath: undefined, }); +/** + * Callback to invoke when the Object Type changes - used to reset the dependent fields. + */ +const onObjectTypeChange = prevValues => ({ + ...prevValues, + objecttypeVersion: undefined, + authAttributePath: undefined, +}); + const LegacyConfigFields = ({apiGroupChoices}) => { const { values: { @@ -68,6 +77,7 @@ const LegacyConfigFields = ({apiGroupChoices}) => { defaultMessage="The registration result will be an object from the selected type." /> } + onObjectTypeChange={onObjectTypeChange} /> { + play: async ({canvasElement, step}) => { const canvas = within(canvasElement); - const v2Tab = canvas.getByRole('tab', {name: 'Variabelekoppelingen'}); - await userEvent.click(v2Tab); - - await waitFor(async () => { + await step('Activate v2 tab', async () => { + const v2Tab = canvas.getByRole('tab', {name: 'Variabelekoppelingen'}); + await userEvent.click(v2Tab); // Close the confirmation modal await userEvent.click( - within(await canvas.findByRole('dialog')).getByRole('button', { - name: 'Accepteren', - }) + within(await canvas.findByRole('dialog')).getByRole('button', {name: 'Accepteren'}) ); - - // Expect v2Tab to be the selected tab - expect(v2Tab).toHaveAttribute('aria-selected', 'true'); + // Wait for v2Tab to be the selected tab + await waitFor(() => { + expect(v2Tab).toHaveAttribute('aria-selected', 'true'); + }); }); const groupSelect = canvas.getByLabelText('API-groep'); await rsSelect(groupSelect, 'Objects API group 1'); - const testForm = await canvas.findByTestId('test-form'); + const objectTypeSelect = canvas.getByLabelText('Objecttype'); await waitFor(() => { - expect(testForm).toHaveFormValues({ - objecttype: '2c77babf-a967-4057-9969-0200320d23f1', - objecttypeVersion: '2', - }); + expect(objectTypeSelect).toBeVisible(); }); - expect(canvas.getByText('Tree (open)')).toBeVisible(); - expect(canvas.getByText('2 (draft)')).toBeVisible(); - - const v1Tab = canvas.getByRole('tab', {name: 'Verouderd (sjabloon)'}); - await userEvent.click(v1Tab); + await rsSelect(objectTypeSelect, 'Tree (open)'); - await waitFor(async () => { - // Close the confirmation modal - await userEvent.click( - within(await canvas.findByRole('dialog')).getByRole('button', { - name: 'Accepteren', - }) - ); - - // Expect v1Tab to be the selected tab - expect(v1Tab).toHaveAttribute('aria-selected', 'true'); + const objectTypeVersionSelect = canvas.getByLabelText('Versie'); + await waitFor(() => { + expect(objectTypeVersionSelect).toBeVisible(); }); + await rsSelect(objectTypeVersionSelect, '2 (draft)'); - await waitFor(() => { - expect(testForm).toHaveFormValues({ - objecttype: '2c77babf-a967-4057-9969-0200320d23f1', - objecttypeVersion: '2', - }); + const testForm = await canvas.findByTestId('test-form'); + expect(testForm).toHaveFormValues({ + objecttype: '2c77babf-a967-4057-9969-0200320d23f1', + objecttypeVersion: '2', }); }, }; @@ -206,53 +191,27 @@ export const SwitchToV2NonExisting = { objecttypeVersion: 1, }, }, - play: async ({canvasElement}) => { + play: async ({canvasElement, step}) => { const canvas = within(canvasElement); - const v2Tab = canvas.getByRole('tab', {name: 'Variabelekoppelingen'}); - await userEvent.click(v2Tab); - - await waitFor(async () => { + await step('Activate v2 tab', async () => { + const v2Tab = canvas.getByRole('tab', {name: 'Variabelekoppelingen'}); + await userEvent.click(v2Tab); // Close the confirmation modal await userEvent.click( - within(await canvas.findByRole('dialog')).getByRole('button', { - name: 'Accepteren', - }) + within(await canvas.findByRole('dialog')).getByRole('button', {name: 'Accepteren'}) ); - - // Expect v2Tab to be the selected tab - expect(v2Tab).toHaveAttribute('aria-selected', 'true'); - }); - - const testForm = await canvas.findByTestId('test-form'); - await waitFor(() => { - expect(testForm).toHaveFormValues({ - objecttype: '2c77babf-a967-4057-9969-0200320d23f1', - objecttypeVersion: '2', + // Wait for v2Tab to be the selected tab + await waitFor(() => { + expect(v2Tab).toHaveAttribute('aria-selected', 'true'); }); }); - expect(canvas.getByText('Tree (open)')).toBeVisible(); - expect(canvas.getByText('2 (draft)')).toBeVisible(); - - const v1Tab = canvas.getByRole('tab', {name: 'Verouderd (sjabloon)'}); - await userEvent.click(v1Tab); - - await waitFor(async () => { - // Close the confirmation modal - await userEvent.click( - within(await canvas.findByRole('dialog')).getByRole('button', { - name: 'Accepteren', - }) - ); - - // Expect v2Tab to be the selected tab - expect(v1Tab).toHaveAttribute('aria-selected', 'true'); - }); + const testForm = await canvas.findByTestId('test-form'); await waitFor(() => { expect(testForm).toHaveFormValues({ - objecttype: '2c77babf-a967-4057-9969-0200320d23f1', - objecttypeVersion: '2', + objecttype: '', + objecttypeVersion: '', }); }); }, 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 a3f2e2dac1..49f55bf670 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 @@ -31,6 +31,15 @@ const onApiGroupChange = prevValues => ({ variablesMapping: [], }); +/** + * Callback to invoke when the Object Type changes - used to reset the dependent fields. + */ +const onObjectTypeChange = prevValues => ({ + ...prevValues, + objecttypeVersion: undefined, + authAttributePath: undefined, +}); + const V2ConfigFields = ({apiGroupChoices}) => { const { values: { @@ -96,6 +105,7 @@ const V2ConfigFields = ({apiGroupChoices}) => { setFieldValue('variablesMapping', []); return true; }} + onObjectTypeChange={onObjectTypeChange} /> diff --git a/src/openforms/js/components/admin/form_design/registrations/objectsapi/mocks.js b/src/openforms/js/components/admin/form_design/registrations/objectsapi/mocks.js index 0ff842274f..5279ccfe63 100644 --- a/src/openforms/js/components/admin/form_design/registrations/objectsapi/mocks.js +++ b/src/openforms/js/components/admin/form_design/registrations/objectsapi/mocks.js @@ -6,9 +6,12 @@ export const mockObjecttypesGet = objecttypes => http.get(`${API_BASE_URL}/api/v2/objects-api/object-types`, () => HttpResponse.json(objecttypes)); export const mockObjecttypeVersionsGet = versions => - http.get(`${API_BASE_URL}/api/v2/objects-api/object-types/:uuid/versions`, () => - HttpResponse.json(versions) - ); + http.get(`${API_BASE_URL}/api/v2/objects-api/object-types/:uuid/versions`, ({params}) => { + if (params.uuid === 'a-non-existing-uuid') { + return HttpResponse.json([]); + } + return HttpResponse.json(versions); + }); export const mockObjecttypesError = () => http.all(`${API_BASE_URL}/api/v2/*`, () => 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 16387356ca..629bd8fe21 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 @@ -192,6 +192,20 @@ export default { {targetPath: ['species'], jsonSchema: {type: 'string', description: 'Species'}}, ], }, + '209e0341-834d-4060-bd19-a3419d19ed74': { + 1: [ + { + targetPath: ['path', 'to.the', 'target'], + jsonSchema: {type: 'string', description: 'Path to the target'}, + }, + ], + 2: [ + { + targetPath: ['path', 'to.the', 'target'], + jsonSchema: {type: 'string', description: 'Path to the target'}, + }, + ], + }, }), ], objectsAPIPrefill: [ @@ -210,6 +224,13 @@ export default { namePlural: 'Persons', dataClassification: 'open', }, + { + url: 'https://objecttypen.nl/api/v1/objecttypes/209e0341-834d-4060-bd19-a3419d19ed74', + uuid: '209e0341-834d-4060-bd19-a3419d19ed74', + name: 'Other objecttype', + namePlural: 'Other objecttypes', + dataClassification: 'open', + }, ]), mockObjecttypeVersionsGet([ {version: 1, status: 'published'}, @@ -711,6 +732,7 @@ export const ConfigurePrefillObjectsAPIWithCopyButton = { objectsApiGroup: 1, objecttype: '209e0341-834d-4060-bd19-a3419d19ed74', objecttypeVersion: 2, + authAttributePath: ['path', 'to', 'bsn'], variablesMapping: [ { variableKey: 'formioComponent', @@ -751,7 +773,7 @@ export const ConfigurePrefillObjectsAPIWithCopyButton = { expect(copyButton).toBeDisabled(); const copyDropdown = await modal.findByLabelText('Registratie-instellingen overnemen'); expect(copyDropdown).toBeVisible(); - await rsSelect(copyDropdown, 'Example Objects API reg.'); + await rsSelect(copyDropdown, 'Other Objects API registration with a long name'); expect(copyButton).toBeVisible(); expect(copyButton).not.toBeDisabled(); @@ -764,9 +786,7 @@ export const ConfigurePrefillObjectsAPIWithCopyButton = { const modalForm = await canvas.findByTestId('modal-form'); expect(modalForm).toBeVisible(); - const propertyDropdowns = await modal.findAllByLabelText( - 'Selecteer een attribuut uit het objecttype' - ); + await modal.findAllByLabelText('Selecteer een attribuut uit het objecttype'); // Wait until the API call to retrieve the prefillAttributes is done await modal.findByText('path > to > bsn', undefined, {timeout: 2000}); @@ -775,11 +795,10 @@ export const ConfigurePrefillObjectsAPIWithCopyButton = { () => { expect(modalForm).toHaveFormValues({ 'options.objectsApiGroup': '1', - 'options.objecttypeUuid': '2c77babf-a967-4057-9969-0200320d23f1', + 'options.objecttypeUuid': '209e0341-834d-4060-bd19-a3419d19ed74', 'options.objecttypeVersion': '2', 'options.authAttributePath': JSON.stringify(['path', 'to', 'bsn']), - 'options.variablesMapping.0.targetPath': serializeValue(['height']), - 'options.variablesMapping.1.targetPath': serializeValue(['species']), + 'options.variablesMapping.0.targetPath': serializeValue(['path', 'to.the', 'target']), }); }, {timeout: 5000} 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 1fe974a474..2f33e284be 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,6 +1,5 @@ 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'; @@ -51,19 +50,10 @@ const PrefillConfigurationForm = ({ }} > {({handleSubmit, values}) => { - const PluginFormComponent = - 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); - }; + const pluginConfiguration = + PLUGIN_COMPONENT_MAPPING[values.plugin] ?? PLUGIN_COMPONENT_MAPPING.default; + const {component: PluginFormComponent, pluginFieldExtra: PluginFieldExtra = null} = + pluginConfiguration; return ( <> @@ -80,22 +70,17 @@ const PrefillConfigurationForm = ({ > <> - {ToggleCopyComponent ? ( + {PluginFieldExtra && (
- +
- ) : null} + )} - + { +const CopyConfigurationFromRegistrationBackend = ({backends, onCopyDone}) => { 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(); + const {ConfirmationModal, confirmationModalProps, openConfirmationModal} = useConfirm(); return ( { e.preventDefault(); - const confirmSwitch = await openCopyConfigurationConfirmationModal(); + const confirmSwitch = await openConfirmationModal(); if (confirmSwitch) { setValues(prevValues => ({ ...prevValues, @@ -71,9 +67,7 @@ const CopyConfigurationFromRegistrationBackend = ({backends, setShowCopyButton}) variablesMapping: selectedBackend.options.variablesMapping, }, })); - - // Collapse the registration backend selection row - setShowCopyButton(false); + onCopyDone(); } }} disabled={!selectedBackend} @@ -93,8 +87,8 @@ const CopyConfigurationFromRegistrationBackend = ({backends, setShowCopyButton}) - ({ }, }); +/** + * Callback to invoke when the Object Type changes - used to reset the dependent fields. + */ +const onObjectTypeChange = prevValues => ({ + ...prevValues, + options: { + ...prevValues.options, + objecttypeVersion: undefined, + authAttributePath: undefined, + variablesMapping: [], + }, +}); + // Load the possible prefill properties // XXX: this would benefit from client-side caching const getProperties = async (objectsApiGroup, objecttypeUuid, objecttypeVersion) => { @@ -56,7 +69,7 @@ const getProperties = async (objectsApiGroup, objecttypeUuid, objecttypeVersion) return response.data.map(property => [property.targetPath, property.targetPath.join(' > ')]); }; -const ObjectsAPIFields = ({showCopyButton, setShowCopyButton}) => { +const ObjectsAPIFields = () => { const intl = useIntl(); // Object with keys the plugin/attribute/options, we process these further to set up // the required context for the fields. @@ -71,6 +84,7 @@ const ObjectsAPIFields = ({showCopyButton, setShowCopyButton}) => { plugin, options: {objecttypeUuid, objecttypeVersion, objectsApiGroup}, } = values; + const {showCopyButton, toggleShowCopyButton} = useStatus(); const defaults = { objectsApiGroup: null, @@ -125,12 +139,12 @@ const ObjectsAPIFields = ({showCopyButton, setShowCopyButton}) => { return ( - {showCopyButton ? ( + {showCopyButton && ( - ) : null} + )}
{ { })); return true; }} + onObjectTypeChange={onObjectTypeChange} /> @@ -283,9 +297,6 @@ const ObjectsAPIFields = ({showCopyButton, setShowCopyButton}) => { ); }; -ObjectsAPIFields.propTypes = { - showCopyButton: PropTypes.bool.isRequired, - setShowCopyButton: PropTypes.func.isRequired, -}; +ObjectsAPIFields.propTypes = {}; export default ObjectsAPIFields; diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ToggleCopyButton.js b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ToggleCopyButton.js index 998fa8e67d..d3123b0047 100644 --- a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ToggleCopyButton.js +++ b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/ToggleCopyButton.js @@ -1,8 +1,25 @@ +import {useContext} from 'react'; import {FormattedMessage} from 'react-intl'; -const ToggleCopyButton = ({handleToggle}) => { +import {FormContext} from 'components/admin/form_design/Context'; + +import useStatus from './useStatus'; + +const ToggleCopyButton = () => { + const {registrationBackends = []} = useContext(FormContext); + const {toggleShowCopyButton} = useStatus(); + const backends = registrationBackends.filter(elem => elem.backend === 'objects_api'); + // don't render a toggle if there's nothing to copy from + if (!backends.length) return null; + return ( - + { + event.preventDefault(); + toggleShowCopyButton(); + }} + > { ); }; +ToggleCopyButton.propTypes = {}; + export default ToggleCopyButton; diff --git a/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/useStatus.js b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/useStatus.js new file mode 100644 index 0000000000..cdbb1a6ff7 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/variables/prefill/objects_api/useStatus.js @@ -0,0 +1,22 @@ +import {useFormikContext} from 'formik'; + +/** + * Convenience hook that wraps around Formik's status. + * + * This centralizes the shape of the status tracked in the Formik state. We use status + * to track some configuration/state that affects the configuration modal without it + * directly being a form field or requiring to be managed at a higher level. + */ +const useStatus = () => { + const {status = {}, setStatus} = useFormikContext(); + const {showCopyButton = false} = status; + const toggleShowCopyButton = () => { + setStatus({...status, showCopyButton: !showCopyButton}); + }; + return { + showCopyButton, + toggleShowCopyButton, + }; +}; + +export default useStatus; 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 a7c19eee49..1d3e4c03c6 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,6 @@ import {useField, useFormikContext} from 'formik'; import PropTypes from 'prop-types'; -import {usePrevious, useUpdateEffect} from 'react-use'; +import {flushSync} from 'react-dom'; import useAsync from 'react-use/esm/useAsync'; import {OBJECTS_API_OBJECTTYPES_ENDPOINT} from 'components/admin/form_design/constants'; @@ -9,8 +9,6 @@ import FormRow from 'components/admin/forms/FormRow'; import ReactSelect from 'components/admin/forms/ReactSelect'; import {get} from 'utils/fetch'; -import {useSynchronizeSelect} from './hooks'; - const getAvailableObjectTypes = async apiGroupID => { const response = await get(OBJECTS_API_OBJECTTYPES_ENDPOINT, {objects_api_group: apiGroupID}); if (!response.ok) { @@ -25,16 +23,11 @@ const ObjectTypeSelect = ({ onChangeCheck, label, helpText, - versionFieldName = 'objecttypeVersion', + onObjectTypeChange = undefined, }) => { - const [fieldProps, , fieldHelpers] = useField(name); - const { - setFieldValue, - getFieldProps, - initialValues: {objecttype: initialObjecttype}, - } = useFormikContext(); + const [{value}, , fieldHelpers] = useField(name); + const {values, getFieldProps, setValues} = useFormikContext(); const objectsApiGroup = getFieldProps(apiGroupFieldName).value ?? null; - const {value} = fieldProps; const {setValue} = fieldHelpers; const { @@ -55,17 +48,6 @@ const ObjectTypeSelect = ({ ]); const options = choices.map(([value, label]) => ({value, label})); - useSynchronizeSelect(name, loading, choices); - - const previousValue = usePrevious(value); - - // when a different object type is selected, ensure that the version is reset - useUpdateEffect(() => { - if (loading) return; - if (value === initialObjecttype || value === previousValue) return; - setFieldValue(versionFieldName, undefined); // clears the value - }, [loading, value]); - return ( @@ -76,8 +58,20 @@ const ObjectTypeSelect = ({ isDisabled={!objectsApiGroup} required onChange={async selectedOption => { + const hasChanged = selectedOption.value !== value; + // don't trigger Formik state changes and dependent effects if the user + // selects the same value that's already selected. + if (!hasChanged) return; + const okToProceed = onChangeCheck === undefined || (await onChangeCheck()); - if (okToProceed) setValue(selectedOption.value); + if (okToProceed) { + // flush sync needed to ensure that onApiGroupChange gets the updated + // state + flushSync(() => { + setValue(selectedOption.value); + }); + if (onObjectTypeChange) setValues(onObjectTypeChange); + } }} /> @@ -103,16 +97,26 @@ ObjectTypeSelect.propTypes = { * The help text to explain what the field is for */ helpText: PropTypes.node.isRequired, + /** + * Callback to invoke when the object type value changes, e.g. to reset any dependent + * fields. + * + * The function will be called with Formik's previous values so you can construct a new + * values state from that. + * + * **NOTE** + * + * It's best to define this callback at the module level, or make use of `useCallback` + * to obtain a stable reference to the callback, otherwise the callback will likely + * fire unexpectedly during re-renders. + */ + onObjectTypeChange: PropTypes.func, + /** * Name of the field holding the selected API group. The value is used in the API * call to get the available object types. */ apiGroupFieldName: PropTypes.string, - /** - * Name of the field to select the object type version. When the selected object type - * changes, the version will be reset/unset. - */ - versionFieldName: PropTypes.string, }; export default ObjectTypeSelect; 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 e33e7b3c64..cd9518b0e5 100644 --- a/src/openforms/js/components/admin/forms/objects_api/ObjectTypeVersionSelect.js +++ b/src/openforms/js/components/admin/forms/objects_api/ObjectTypeVersionSelect.js @@ -1,6 +1,5 @@ import {useFormikContext} from 'formik'; import PropTypes from 'prop-types'; -import {FormattedMessage} from 'react-intl'; import useAsync from 'react-use/esm/useAsync'; import Field from 'components/admin/forms/Field'; @@ -8,8 +7,6 @@ import FormRow from 'components/admin/forms/FormRow'; import ReactSelect from 'components/admin/forms/ReactSelect'; import {get} from 'utils/fetch'; -import {useSynchronizeSelect} from './hooks'; - const getObjecttypeVersionsEndpoint = uuid => { const bits = ['/api/v2/objects-api/object-types', encodeURIComponent(uuid), 'versions']; return bits.join('/'); @@ -50,8 +47,6 @@ const ObjectTypeVersionSelect = ({ ? [] : versions.map(version => [version.version, `${version.version} (${version.status})`]); - useSynchronizeSelect(name, loading, choices); - const options = choices.map(([value, label]) => ({value, label})); return ( diff --git a/src/openforms/js/components/admin/forms/objects_api/ObjectsAPIGroup.js b/src/openforms/js/components/admin/forms/objects_api/ObjectsAPIGroup.js index ca00a8be71..91aad9c8dd 100644 --- a/src/openforms/js/components/admin/forms/objects_api/ObjectsAPIGroup.js +++ b/src/openforms/js/components/admin/forms/objects_api/ObjectsAPIGroup.js @@ -1,7 +1,7 @@ import {useField, useFormikContext} from 'formik'; import PropTypes from 'prop-types'; +import {flushSync} from 'react-dom'; import {FormattedMessage} from 'react-intl'; -import {useUpdateEffect} from 'react-use'; import Field from 'components/admin/forms/Field'; import FormRow from 'components/admin/forms/FormRow'; @@ -19,12 +19,6 @@ const ObjectsAPIGroup = ({ const {setValues} = useFormikContext(); const {value} = fieldProps; - // Call `onApiGroupChange` to get the 'reset' values whenever the API group changes. - useUpdateEffect(() => { - if (!onApiGroupChange) return; - setValues(onApiGroupChange); - }, [setValues, onApiGroupChange, value]); - const options = apiGroupChoices.map(([value, label]) => ({value, label})); // React doesn't like null/undefined as it leads to uncontrolled component warnings, @@ -60,11 +54,22 @@ const ObjectsAPIGroup = ({ value={normalizedOptions.find(option => option.value === normalizedValue)} required={required} onChange={async selectedOption => { + // Normalize empty string values back to null + const newValue = selectedOption ? selectedOption.value : null; + const hasChanged = newValue !== value; + // don't trigger Formik state changes and dependent effects if the user + // selects the same value that's already selected. + if (!hasChanged) return; + const okToProceed = onChangeCheck === undefined || (await onChangeCheck()); if (okToProceed) { - // normalize empty string back to null - const newValue = selectedOption ? selectedOption.value : null; - setValue(newValue); + // flush sync needed to ensure that onApiGroupChange gets the updated + // state + flushSync(() => { + setValue(newValue); + }); + // Call `onApiGroupChange` to get the 'reset' values whenever the API group changes. + if (onApiGroupChange) setValues(onApiGroupChange); } }} isClearable={isClearable} diff --git a/src/openforms/js/components/admin/forms/objects_api/hooks.js b/src/openforms/js/components/admin/forms/objects_api/hooks.js deleted file mode 100644 index 038cce653f..0000000000 --- a/src/openforms/js/components/admin/forms/objects_api/hooks.js +++ /dev/null @@ -1,29 +0,0 @@ -import {useFormikContext} from 'formik'; -import {useEffect} from 'react'; - -/** - * Ensure that if no valid value is set, the first possible option is selected. - * - * This synchronizes the form field state with the UI state, since selects with no - * empty/blank option display the first option as if it was selected, which isn't - * guaranteed to be the case in the form state. - */ -// synchronizing the UI state back to the form state, because a select displays the -// first possible option as if it's actually selected) -export const useSynchronizeSelect = (name, loading, choices) => { - const {getFieldProps, getFieldHelpers} = useFormikContext(); - - const {value: currentValue} = getFieldProps(name); - const {setValue} = getFieldHelpers(name); - - useEffect(() => { - // do nothing if no options have been loaded - if (loading || !choices.length) return; - // check if a valid option is selected, if this is the case -> do nothing - const isOptionPresent = choices.find(([optionValue]) => optionValue === currentValue); - if (isOptionPresent) return; - - // otherwise select the first possible option and persist that back into the state - setValue(choices[0][0]); - }); -};