From 6ed53157293b446cd60eeaae2d7a1fd4afb60808 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Mon, 27 Jan 2025 09:22:40 +0100 Subject: [PATCH] :sparkles: [open-formulieren/open-forms#5016] Referentielijsten as dataSrc dynamically fetch options for select, selectboxes and radio components --- src/components/ComponentConfiguration.tsx | 2 + .../builder/values/referentielijsten/code.tsx | 35 +++++++++ .../builder/values/referentielijsten/index.ts | 9 +++ .../values/referentielijsten/service.tsx | 75 +++++++++++++++++++ .../builder/values/values-config.tsx | 28 +++++++ src/components/builder/values/values-src.tsx | 6 +- src/context.ts | 3 + src/registry/radio/edit-validation.ts | 6 +- src/registry/radio/helpers.ts | 25 +++++++ src/registry/radio/preview.tsx | 64 +++++++++++++--- src/registry/select/edit-validation.ts | 6 +- src/registry/select/helpers.ts | 25 +++++++ src/registry/select/preview.tsx | 61 ++++++++++++--- src/registry/selectboxes/edit-validation.ts | 6 +- src/registry/selectboxes/helpers.ts | 25 +++++++ src/registry/selectboxes/preview.tsx | 64 +++++++++++++--- 16 files changed, 403 insertions(+), 37 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/service.tsx 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..fdd60a1f --- /dev/null +++ b/src/components/builder/values/referentielijsten/code.tsx @@ -0,0 +1,35 @@ +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)} + required + /> + ); +}; + +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/service.tsx b/src/components/builder/values/referentielijsten/service.tsx new file mode 100644 index 00000000..a69a21db --- /dev/null +++ b/src/components/builder/values/referentielijsten/service.tsx @@ -0,0 +1,75 @@ +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 ( + z.object({ values: optionSchema(intl).array().min(1).optional(), openForms: z.object({ - dataSrc: z.union([z.literal('manual'), z.literal('variable')]), + dataSrc: z.union([ + z.literal('manual'), + z.literal('variable'), + z.literal('referentielijsten'), + ]), // TODO: wire up infernologic type checking itemsExpression: jsonSchema.optional(), }), diff --git a/src/registry/selectboxes/helpers.ts b/src/registry/selectboxes/helpers.ts index 8ba0956b..71dd2089 100644 --- a/src/registry/selectboxes/helpers.ts +++ b/src/registry/selectboxes/helpers.ts @@ -1,5 +1,6 @@ import {SelectboxesComponentSchema} from '@open-formulieren/types'; import {Option} from '@open-formulieren/types/lib/formio/common'; +import {JSONObject} from '@open-formulieren/types/lib/types'; // A type guard is needed because TS cannot figure out it's a discriminated union // when the discriminator is nested. @@ -9,3 +10,27 @@ export const checkIsManualOptions = ( ): component is SelectboxesComponentSchema & {values: Option[] | undefined} => { return component.openForms.dataSrc === 'manual'; }; + +// A type guard is needed because TS cannot figure out it's a discriminated union +// when the discriminator is nested. +// See https://github.com/microsoft/TypeScript/issues/18758 +export const checkIsReferentielijstenOptions = ( + component: SelectboxesComponentSchema +): component is SelectboxesComponentSchema & { + data: {values: Option[] | undefined}; + openForms: {code: string; service: string}; +} => { + return component.openForms.dataSrc === 'referentielijsten'; +}; + +// A type guard is needed because TS cannot figure out it's a discriminated union +// when the discriminator is nested. +// See https://github.com/microsoft/TypeScript/issues/18758 +export const checkIsVariableOptions = ( + component: SelectboxesComponentSchema +): component is SelectboxesComponentSchema & { + data: {values: Option[] | undefined}; + openForms: {itemsExpression: string | JSONObject}; +} => { + return component.openForms.dataSrc === 'variable'; +}; diff --git a/src/registry/selectboxes/preview.tsx b/src/registry/selectboxes/preview.tsx index 6e3c3901..a2b676cc 100644 --- a/src/registry/selectboxes/preview.tsx +++ b/src/registry/selectboxes/preview.tsx @@ -2,9 +2,14 @@ import {SelectboxesComponentSchema} from '@open-formulieren/types'; import {useIntl} from 'react-intl'; import {SelectBoxes} from '@/components/formio'; +import {Option} from '@/components/formio/selectboxes'; import {ComponentPreviewProps} from '../types'; -import {checkIsManualOptions} from './helpers'; +import { + checkIsManualOptions, + checkIsReferentielijstenOptions, + checkIsVariableOptions, +} from './helpers'; /** * Show a formio selectboxes component preview. @@ -17,13 +22,47 @@ const Preview: React.FC> = ({c const intl = useIntl(); const {key, label, description, tooltip, validate} = component; const {required = false} = validate || {}; - const isManualOptions = checkIsManualOptions(component); - const options = isManualOptions - ? component.values || [] - : [ - { - value: 'itemsExpression', - label: intl.formatMessage( + + let options: Option[] = []; + if (checkIsManualOptions(component)) { + options = component?.values || []; + } else if (checkIsReferentielijstenOptions(component)) { + options = [ + { + value: 'service', + label: intl + .formatMessage( + { + description: 'Selectboxes dummy option for service', + defaultMessage: 'Options from service: {service}', + }, + { + service: component.openForms.service, + } + ) + .toString(), + }, + { + value: 'code', + label: intl + .formatMessage( + { + description: 'Selectboxes dummy option for code', + defaultMessage: 'Options from code: {code}', + }, + { + code: component.openForms.code, + } + ) + .toString(), + }, + ]; + } else if (checkIsVariableOptions(component)) { + options = [ + { + value: 'itemsExpression', + label: intl + .formatMessage( { description: 'Selectboxes dummy option for itemsExpression', defaultMessage: 'Options from expression: {expression}', @@ -32,9 +71,12 @@ const Preview: React.FC> = ({c expression: JSON.stringify(component.openForms.itemsExpression), code: chunks => {chunks}, } - ), - }, - ]; + ) + .toString(), + }, + ]; + } + return (