From cb70661159acb1ce15b84b964ed3336d6686638b Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Tue, 14 Jan 2025 16:55:52 +0100 Subject: [PATCH] :construction: [#5016] Referentielijsten dataSrc for options --- .storybook/decorators.tsx | 7 ++ .../ComponentConfiguration.stories.tsx | 4 + src/components/ComponentConfiguration.tsx | 2 + .../builder/values/referentielijsten/code.tsx | 34 ++++++++ .../builder/values/referentielijsten/index.ts | 9 +++ .../referentielijsten-services.spec.tsx | 56 +++++++++++++ .../values/referentielijsten/service.tsx | 78 +++++++++++++++++++ .../builder/values/values-config.stories.tsx | 20 +++++ .../builder/values/values-config.tsx | 25 ++++++ src/components/builder/values/values-src.tsx | 6 +- src/context.ts | 3 + src/registry/select/edit-validation.ts | 6 +- src/registry/select/helpers.ts | 25 ++++++ src/registry/select/preview.tsx | 61 ++++++++++++--- src/tests/sharedUtils.tsx | 18 +++++ 15 files changed, 341 insertions(+), 13 deletions(-) create mode 100644 src/components/builder/values/referentielijsten/code.tsx create mode 100644 src/components/builder/values/referentielijsten/index.ts create mode 100644 src/components/builder/values/referentielijsten/referentielijsten-services.spec.tsx create mode 100644 src/components/builder/values/referentielijsten/service.tsx diff --git a/.storybook/decorators.tsx b/.storybook/decorators.tsx index 2ad69c94..6960f92f 100644 --- a/.storybook/decorators.tsx +++ b/.storybook/decorators.tsx @@ -14,6 +14,7 @@ import { DEFAULT_PREFILL_ATTRIBUTES, DEFAULT_PREFILL_PLUGINS, DEFAULT_REGISTRATION_ATTRIBUTES, + DEFAULT_SERVICES, DEFAULT_VALIDATOR_PLUGINS, sleep, } from '@/tests/sharedUtils'; @@ -61,6 +62,7 @@ export const BuilderContextDecorator: Decorator = (Story, context) => { context.parameters.builder?.defaultValidatorPlugins || DEFAULT_VALIDATOR_PLUGINS; const defaultRegistrationAttributes = context.parameters.builder?.defaultRegistrationAttributes || DEFAULT_REGISTRATION_ATTRIBUTES; + const defaultServices = context.parameters.builder?.defaultServices || DEFAULT_SERVICES; const defaultPrefillPlugins = context.parameters.builder?.defaultPrefillPlugins || DEFAULT_PREFILL_PLUGINS; const defaultPrefillAttributes = @@ -86,6 +88,11 @@ export const BuilderContextDecorator: Decorator = (Story, context) => { await sleep(context.parameters?.builder?.registrationAttributesDelay || 0); return context?.args?.registrationAttributes || defaultRegistrationAttributes; }, + getServices: async () => { + await sleep(context.parameters?.builder?.servicesDelay || 0); + console.log('foo'); + return context?.args?.services || defaultServices; + }, getPrefillPlugins: async () => { await sleep(context.parameters?.builder?.prefillPluginsDelay || 0); return context?.args?.prefillPlugins || defaultPrefillPlugins; diff --git a/src/components/ComponentConfiguration.stories.tsx b/src/components/ComponentConfiguration.stories.tsx index 2810edb1..a5bd2da8 100644 --- a/src/components/ComponentConfiguration.stories.tsx +++ b/src/components/ComponentConfiguration.stories.tsx @@ -7,6 +7,7 @@ import {Meta, StoryFn, StoryObj} from '@storybook/react'; import {expect, fireEvent, fn, userEvent, waitFor, within} from '@storybook/test'; import React from 'react'; +import {ReferentielijstenServiceOption} from '@/components/builder/values/referentielijsten/service'; import { CONFIDENTIALITY_LEVELS, DEFAULT_AUTH_PLUGINS, @@ -75,6 +76,7 @@ interface TemplateArgs { validatorPlugins: ValidatorOption[]; registrationAttributes: RegistrationAttributeOption[]; prefillPlugins: PrefillPluginOption[]; + services: ReferentielijstenServiceOption[]; prefillAttributes: Record; fileTypes: Array<{value: string; label: string}>; isNew: boolean; @@ -91,6 +93,7 @@ const Template: StoryFn = ({ registrationAttributes, prefillPlugins, prefillAttributes, + services, supportedLanguageCodes, fileTypes, isNew, @@ -107,6 +110,7 @@ const Template: StoryFn = ({ getFormComponents={() => otherComponents} getValidatorPlugins={async () => validatorPlugins} getRegistrationAttributes={async () => registrationAttributes} + getServices={async () => services} getPrefillPlugins={async () => prefillPlugins} getPrefillAttributes={async (plugin: string) => prefillAttributes[plugin]} getFileTypes={async () => fileTypes} diff --git a/src/components/ComponentConfiguration.tsx b/src/components/ComponentConfiguration.tsx index 7fec9d2a..67fe9cb1 100644 --- a/src/components/ComponentConfiguration.tsx +++ b/src/components/ComponentConfiguration.tsx @@ -38,6 +38,7 @@ const ComponentConfiguration: React.FC = ({ getFormComponents, getValidatorPlugins, getRegistrationAttributes, + getServices, getPrefillPlugins, getPrefillAttributes, getFileTypes, @@ -66,6 +67,7 @@ const ComponentConfiguration: React.FC = ({ getFormComponents, getValidatorPlugins, getRegistrationAttributes, + getServices, getPrefillPlugins, getPrefillAttributes, getFileTypes, diff --git a/src/components/builder/values/referentielijsten/code.tsx b/src/components/builder/values/referentielijsten/code.tsx new file mode 100644 index 00000000..ad142564 --- /dev/null +++ b/src/components/builder/values/referentielijsten/code.tsx @@ -0,0 +1,34 @@ +import {useFormikContext} from 'formik'; +import {FormattedMessage, useIntl} from 'react-intl'; + +import {TextField} from '@/components/formio'; + +const NAME = 'openForms.code'; + +/** + * The `ReferentielijstenTabelCode` component is used to specify the code of the tabel + * in Referentielijsten API for which the items will be fetched + */ +export const ReferentielijstenTabelCode: React.FC = () => { + const intl = useIntl(); + const {setFieldValue} = useFormikContext(); + const name = `editform-${NAME}`; + return ( + + } + tooltip={intl.formatMessage({ + description: "Description for the 'openForms.code' builder field", + defaultMessage: `The code of the table from which the options will be retrieved.`, + })} + onChange={event => setFieldValue(NAME, event.target.value)} + /> + ); +}; + +export default ReferentielijstenTabelCode; diff --git a/src/components/builder/values/referentielijsten/index.ts b/src/components/builder/values/referentielijsten/index.ts new file mode 100644 index 00000000..fd4c6892 --- /dev/null +++ b/src/components/builder/values/referentielijsten/index.ts @@ -0,0 +1,9 @@ +/** + * Components to manage options/values for fields that support this, such as: + * + * - select + * - selectboxes + * - radio + */ +export {default as ReferentielijstenServiceSelect} from './service'; +export {default as ReferentielijstenTabelCode} from './code'; diff --git a/src/components/builder/values/referentielijsten/referentielijsten-services.spec.tsx b/src/components/builder/values/referentielijsten/referentielijsten-services.spec.tsx new file mode 100644 index 00000000..3a0f1e35 --- /dev/null +++ b/src/components/builder/values/referentielijsten/referentielijsten-services.spec.tsx @@ -0,0 +1,56 @@ +import userEvent from '@testing-library/user-event'; +import {Formik} from 'formik'; + +import {act, contextRender, screen} from '@/tests/test-utils'; + +import RegistrationAttributeSelect, { + RegistrationAttributeOption, +} from '../../referentielijsten/referentielijsten-services'; + +const REGISTRATION_ATTRIBUTES: RegistrationAttributeOption[] = [ + {id: 'bsn', label: 'BSN'}, + {id: 'firstName', label: 'First name'}, + {id: 'dob', label: 'Date of Birth'}, +]; + +beforeAll(() => { + jest.useFakeTimers(); +}); + +afterAll(() => { + jest.useRealTimers(); +}); + +test('Available registration attributes are retrieved via context', async () => { + const user = userEvent.setup({advanceTimers: jest.advanceTimersByTime}); + + contextRender( + + + , + { + enableContext: true, + locale: 'en', + builderOptions: { + registrationAttributes: REGISTRATION_ATTRIBUTES, + registrationAttributesDelay: 100, + }, + } + ); + + // open the dropdown + const input = await screen.findByLabelText('Registration attribute'); + await act(async () => { + input.focus(); + await user.keyboard('[ArrowDown]'); + }); + + // options are loaded async, while doing network IO the loading state is displayed + expect(await screen.findByText('Loading...')).toBeVisible(); + + // once resolved, the options are visible and the loading state is no longer + expect(await screen.findByText('BSN')).toBeVisible(); + expect(await screen.findByText('First name')).toBeVisible(); + expect(await screen.findByText('Date of Birth')).toBeVisible(); + expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); +}); diff --git a/src/components/builder/values/referentielijsten/service.tsx b/src/components/builder/values/referentielijsten/service.tsx new file mode 100644 index 00000000..5d7eb6a7 --- /dev/null +++ b/src/components/builder/values/referentielijsten/service.tsx @@ -0,0 +1,78 @@ +// import {useFormikContext} from 'formik'; +// import {FormattedMessage, useIntl} from 'react-intl'; +import {useFormikContext} from 'formik'; +import {useContext} from 'react'; +import {FormattedMessage, useIntl} from 'react-intl'; +import useAsync from 'react-use/esm/useAsync'; + +import Select from '@/components/formio/select'; +import {BuilderContext} from '@/context'; + +const NAME = 'openForms.service'; + +// TODO transform this to id and label? +export interface ReferentielijstenServiceOption { + url: string; + slug: string; + label: string; + apiRoot: string; + apiType: string; +} + +function isServiceOptions( + options: ReferentielijstenServiceOption[] | undefined +): options is ReferentielijstenServiceOption[] { + return options !== undefined; +} + +/** + * Fetch the available Referentielijsten Services and display them in a Select + * + * The selected service is used at runtime to retrieve options to populate a Select + * + * This requires an async function `getServices` to be provided to the + * BuilderContext which is responsible for retrieving the list of available plugins. + * + * If a fetch error occurs, it is thrown during rendering - you should provide your + * own error boundary to catch this. + */ +const ReferentielijstenServiceSelect: React.FC = () => { + const name = `editform-${NAME}`; + const intl = useIntl(); + const {getServices} = useContext(BuilderContext); + const {setFieldValue} = useFormikContext(); + + const { + value: options, + loading, + error, + } = useAsync(async () => await getServices('referentielijsten'), []); + if (error) { + throw error; + } + const _options = isServiceOptions(options) ? options : []; + + return ( +