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 (