From 27e49769abcc6820e091b0c13694b9ad98437350 Mon Sep 17 00:00:00 2001 From: Steven Bal Date: Mon, 27 Jan 2025 09:22:40 +0100 Subject: [PATCH 1/9] :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 | 29 ++++++++ .../builder/values/referentielijsten/index.ts | 9 +++ .../values/referentielijsten/service.tsx | 68 +++++++++++++++++++ .../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 | 57 ++++++++++------ src/registry/select/edit-validation.ts | 6 +- src/registry/select/helpers.ts | 25 +++++++ src/registry/select/preview.tsx | 56 +++++++++------ src/registry/selectboxes/edit-validation.ts | 6 +- src/registry/selectboxes/helpers.ts | 25 +++++++ src/registry/selectboxes/preview.tsx | 57 ++++++++++------ 16 files changed, 347 insertions(+), 61 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..511e7b56 --- /dev/null +++ b/src/components/builder/values/referentielijsten/code.tsx @@ -0,0 +1,29 @@ +import {FormattedMessage, useIntl} from 'react-intl'; + +import {TextField} from '@/components/formio'; + +/** + * 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(); + 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.`, + })} + 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..e9d45797 --- /dev/null +++ b/src/components/builder/values/referentielijsten/service.tsx @@ -0,0 +1,68 @@ +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'; + +// 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 intl = useIntl(); + const {getServices} = useContext(BuilderContext); + 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..f9e83d54 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,24 +22,38 @@ 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( - { - description: 'Selectboxes dummy option for itemsExpression', - defaultMessage: 'Options from expression: {expression}', - }, - { - expression: JSON.stringify(component.openForms.itemsExpression), - code: chunks => {chunks}, - } - ), - }, - ]; + + let options: Option[] = []; + if (checkIsManualOptions(component)) { + options = component?.values || []; + } else if (checkIsReferentielijstenOptions(component)) { + options = [ + { + value: 'option1', + label: intl.formatMessage({ + description: 'Radio dummy option1 from referentielijsten', + defaultMessage: 'Option from referentielijsten: option1', + }), + }, + ]; + } else if (checkIsVariableOptions(component)) { + options = [ + { + value: 'itemsExpression', + label: intl.formatMessage( + { + description: 'Selectboxes dummy option for itemsExpression', + defaultMessage: 'Options from expression: {expression}', + }, + { + expression: JSON.stringify(component.openForms.itemsExpression), + code: chunks => {chunks}, + } + ), + }, + ]; + } + return ( Date: Tue, 28 Jan 2025 10:19:46 +0100 Subject: [PATCH 2/9] :heavy_plus_sign: [open-formulieren/open-forms#5016] Add react-select-event to requirements --- package-lock.json | 19 +++++++++++++++++++ package.json | 1 + src/components/formio/select.tsx | 7 +++++++ src/registry/select/preview.tsx | 3 ++- 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 3276bdac..f7791b73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,6 +82,7 @@ "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-select-event": "^5.5.1", "sass": "^1.75.0", "sass-loader": "^14.2.0", "storybook": "^8.3.5", @@ -18109,6 +18110,15 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-select-event": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/react-select-event/-/react-select-event-5.5.1.tgz", + "integrity": "sha512-goAx28y0+iYrbqZA2FeRTreHHs/ZtSuKxtA+J5jpKT5RHPCbVZJ4MqACfPnWyFXsEec+3dP5bCrNTxIX8oYe9A==", + "dev": true, + "dependencies": { + "@testing-library/dom": ">=7" + } + }, "node_modules/react-signature-canvas": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/react-signature-canvas/-/react-signature-canvas-1.0.6.tgz", @@ -34618,6 +34628,15 @@ "use-isomorphic-layout-effect": "^1.1.2" } }, + "react-select-event": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/react-select-event/-/react-select-event-5.5.1.tgz", + "integrity": "sha512-goAx28y0+iYrbqZA2FeRTreHHs/ZtSuKxtA+J5jpKT5RHPCbVZJ4MqACfPnWyFXsEec+3dP5bCrNTxIX8oYe9A==", + "dev": true, + "requires": { + "@testing-library/dom": ">=7" + } + }, "react-signature-canvas": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/react-signature-canvas/-/react-signature-canvas-1.0.6.tgz", diff --git a/package.json b/package.json index 78c783ed..cefd2e82 100644 --- a/package.json +++ b/package.json @@ -106,6 +106,7 @@ "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-select-event": "^5.5.1", "sass": "^1.75.0", "sass-loader": "^14.2.0", "storybook": "^8.3.5", diff --git a/src/components/formio/select.tsx b/src/components/formio/select.tsx index bf652d01..06d732c9 100644 --- a/src/components/formio/select.tsx +++ b/src/components/formio/select.tsx @@ -1,5 +1,6 @@ import {useField} from 'formik'; import React from 'react'; +import {ReactNode} from 'react'; import ReactSelect from 'react-select'; import type { GroupBase, @@ -35,6 +36,12 @@ export interface SelectProps< onChange?: (event: {target: {name: string; value: ValueType}}) => void; } +export interface Option { + value: string; + label: ReactNode; + description?: string; +} + function isOption = GroupBase