Skip to content

Commit

Permalink
🚧 [#5016] Referentielijsten dataSrc for options
Browse files Browse the repository at this point in the history
  • Loading branch information
stevenbal committed Jan 24, 2025
1 parent edb202c commit e462b1c
Show file tree
Hide file tree
Showing 18 changed files with 384 additions and 13 deletions.
7 changes: 7 additions & 0 deletions .storybook/decorators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
DEFAULT_PREFILL_ATTRIBUTES,
DEFAULT_PREFILL_PLUGINS,
DEFAULT_REGISTRATION_ATTRIBUTES,
DEFAULT_SERVICES,
DEFAULT_VALIDATOR_PLUGINS,
sleep,
} from '@/tests/sharedUtils';
Expand Down Expand Up @@ -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 =
Expand All @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/components/ComponentConfiguration.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {AnyComponentSchema} from '@/types';
import ComponentConfiguration from './ComponentConfiguration';
import {BuilderInfo} from './ComponentEditForm';
import {PrefillAttributeOption, PrefillPluginOption} from './builder/prefill';
import {ReferentielijstenServiceOption} from './builder/referentielijsten/referentielijsten-services';
import {RegistrationAttributeOption} from './builder/registration/registration-attribute';
import {ValidatorOption} from './builder/validate/validator-select';

Expand Down Expand Up @@ -75,6 +76,7 @@ interface TemplateArgs {
validatorPlugins: ValidatorOption[];
registrationAttributes: RegistrationAttributeOption[];
prefillPlugins: PrefillPluginOption[];
services: ReferentielijstenServiceOption[];
prefillAttributes: Record<string, PrefillAttributeOption[]>;
fileTypes: Array<{value: string; label: string}>;
isNew: boolean;
Expand All @@ -91,6 +93,7 @@ const Template: StoryFn<TemplateArgs> = ({
registrationAttributes,
prefillPlugins,
prefillAttributes,
services,
supportedLanguageCodes,
fileTypes,
isNew,
Expand All @@ -107,6 +110,7 @@ const Template: StoryFn<TemplateArgs> = ({
getFormComponents={() => otherComponents}
getValidatorPlugins={async () => validatorPlugins}
getRegistrationAttributes={async () => registrationAttributes}
getServices={async () => services}
getPrefillPlugins={async () => prefillPlugins}
getPrefillAttributes={async (plugin: string) => prefillAttributes[plugin]}
getFileTypes={async () => fileTypes}
Expand Down
2 changes: 2 additions & 0 deletions src/components/ComponentConfiguration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const ComponentConfiguration: React.FC<ComponentConfigurationProps> = ({
getFormComponents,
getValidatorPlugins,
getRegistrationAttributes,
getServices,
getPrefillPlugins,
getPrefillAttributes,
getFileTypes,
Expand Down Expand Up @@ -66,6 +67,7 @@ const ComponentConfiguration: React.FC<ComponentConfigurationProps> = ({
getFormComponents,
getValidatorPlugins,
getRegistrationAttributes,
getServices,
getPrefillPlugins,
getPrefillAttributes,
getFileTypes,
Expand Down
1 change: 1 addition & 0 deletions src/components/builder/referentielijsten/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {default as ReferentielijstenServiceSelect} from './referentielijsten-services';
Original file line number Diff line number Diff line change
@@ -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-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(
<Formik onSubmit={jest.fn()} initialValues={{registration: {attribute: ''}}}>
<RegistrationAttributeSelect />
</Formik>,
{
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();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {Meta, StoryObj} from '@storybook/react';

import {withFormik} from '@/sb-decorators';
import {DEFAULT_SERVICES} from '@/tests/sharedUtils';

import ReferentielijstenServiceSelect, {
ReferentielijstenServiceOption,
} from './referentielijsten-services';

export default {
title: 'Formio/Builder/Referentielijsten/ReferentielijstenServiceSelect',
component: ReferentielijstenServiceSelect,
decorators: [withFormik],
parameters: {
controls: {hideNoControlsWarning: true},
modal: {noModal: true},
builder: {enableContext: true, registrationAttributesDelay: 100},
formik: {initialValues: {registration: {attribute: ''}}},
},
args: {
services: DEFAULT_SERVICES,
},
} as Meta<typeof ReferentielijstenServiceSelect>;

type Story = StoryObj<typeof ReferentielijstenServiceOption>;

export const Default: Story = {};
Original file line number Diff line number Diff line change
@@ -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';

// 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;
}

interface ReferentielijstenServiceSelectProps {
name: string;
}

/**
* Fetch the available Referentielijsten Services and display them in 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<ReferentielijstenServiceSelectProps> = ({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 (
<Select
name={name}
label={
<FormattedMessage
description="Label for 'openForms.service' builder field"
defaultMessage="Referentielijsten service"
/>
}
tooltip={intl.formatMessage({
description: "Description for the 'openForms.service' builder field",
defaultMessage: `The identifier of the Referentielijsten service from which the options will be retrieved.`,
})}
isLoading={loading}
isClearable
options={_options}
valueProperty="slug"
// TODO not hardcoded name
onChange={event => setFieldValue('openForms.service', event.target.value)}
/>
);
};

export default ReferentielijstenServiceSelect;
34 changes: 34 additions & 0 deletions src/components/builder/values/referentielijsten/code.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<TextField
name={name}
label={
<FormattedMessage
description="Label for 'openForms.code' builder field"
defaultMessage="Referentielijsten table code"
/>
}
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;
9 changes: 9 additions & 0 deletions src/components/builder/values/referentielijsten/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Components to manage options/values for fields that support this, such as:
*
* - select
* - selectboxes
* - radio
*/
export {default as ReferentielijstenService} from './service';
export {default as ReferentielijstenTabelCode} from './code';
18 changes: 18 additions & 0 deletions src/components/builder/values/referentielijsten/service.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// import {useFormikContext} from 'formik';
// import {FormattedMessage, useIntl} from 'react-intl';
import {ReferentielijstenServiceSelect} from '../../referentielijsten';

const NAME = 'openForms.service';

/**
* The `ReferentielijstenService` component is used to specify the slug of the service
* that is used to retrieve options from
*/
export const ReferentielijstenService: React.FC = () => {
// const intl = useIntl();
// const {setFieldValue} = useFormikContext();
const name = `editform-${NAME}`;
return <ReferentielijstenServiceSelect name={name} />;
};

export default ReferentielijstenService;
20 changes: 20 additions & 0 deletions src/components/builder/values/values-config.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {Meta, StoryObj} from '@storybook/react';
import {expect, fireEvent, fn, userEvent, waitFor, within} from '@storybook/test';
import {Form, Formik} from 'formik';

import {BuilderContextDecorator} from '@/sb-decorators';
import {withFormik} from '@/sb-decorators';
import {DEFAULT_SERVICES} from '@/tests/sharedUtils';

import ValuesConfig from './values-config';

Expand Down Expand Up @@ -300,3 +302,21 @@ export const SelectVariable: SelectStory = {
},
},
};

export const SelectReferentielijsten: SelectStory = {
...Select,

decorators: [withFormik, BuilderContextDecorator],
parameters: {
formik: {
initialValues: {
openForms: {
dataSrc: 'referentielijsten',
itemsExpression: {code: 'table1', service: 'referentielijsten-api'},
},
data: {},
},
},
builder: {enableContext: true},
},
};
25 changes: 25 additions & 0 deletions src/components/builder/values/values-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {useLayoutEffect} from 'react';
import {hasOwnProperty} from '@/types';

import ItemsExpression from './items-expression';
import {ReferentielijstenService, ReferentielijstenTabelCode} from './referentielijsten';
import {SchemaWithDataSrc} from './types';
import ValuesSrc from './values-src';
import ValuesTable, {ValuesTableProps} from './values-table';
Expand Down Expand Up @@ -59,6 +60,12 @@ export function ValuesConfig<T extends SchemaWithDataSrc>({
if (values.openForms.hasOwnProperty('itemsExpression')) {
setFieldValue('openForms.itemsExpression', undefined);
}
if (values.openForms.hasOwnProperty('code')) {
setFieldValue('openForms.itemsExpression', undefined);
}
if (values.openForms.hasOwnProperty('service')) {
setFieldValue('openForms.itemsExpression', undefined);
}
if (!isNestedKeySet(values, name)) {
setFieldValue(name, [{value: '', label: '', openForms: {translations: {}}}]);
}
Expand All @@ -68,6 +75,18 @@ export function ValuesConfig<T extends SchemaWithDataSrc>({
if (isNestedKeySet(values, name)) {
setFieldValue(name, undefined);
}
if (values.openForms.hasOwnProperty('code')) {
setFieldValue('openForms.itemsExpression', undefined);
}
if (values.openForms.hasOwnProperty('service')) {
setFieldValue('openForms.itemsExpression', undefined);
}
break;
}
case 'referentielijsten': {

Check failure on line 86 in src/components/builder/values/values-config.tsx

View workflow job for this annotation

GitHub Actions / Create 'production' build

Type '"referentielijsten"' is not comparable to type '"manual" | "variable"'.
if (values.openForms.hasOwnProperty('itemsExpression')) {

Check failure on line 87 in src/components/builder/values/values-config.tsx

View workflow job for this annotation

GitHub Actions / Create 'production' build

Property 'hasOwnProperty' does not exist on type 'never'.
setFieldValue('openForms.itemsExpression', undefined);
}
break;
}
}
Expand All @@ -82,6 +101,12 @@ export function ValuesConfig<T extends SchemaWithDataSrc>({
<ValuesTable<T> name={name} withOptionDescription={withOptionDescription} />
)}
{dataSrc === 'variable' && <ItemsExpression />}
{dataSrc === 'referentielijsten' && (

Check failure on line 104 in src/components/builder/values/values-config.tsx

View workflow job for this annotation

GitHub Actions / Create 'production' build

This comparison appears to be unintentional because the types '"manual" | "variable"' and '"referentielijsten"' have no overlap.
<>
<ReferentielijstenService />
<ReferentielijstenTabelCode />
</>
)}
</>
);
}
Expand Down
Loading

0 comments on commit e462b1c

Please sign in to comment.