Skip to content

Commit

Permalink
Merge pull request #63 from open-formulieren/feature/62-bsn-edit-form
Browse files Browse the repository at this point in the history
Implement edit form form BSN component
  • Loading branch information
sergei-maertens authored Nov 24, 2023
2 parents a9725b3 + bf97dc3 commit 0367e61
Show file tree
Hide file tree
Showing 7 changed files with 409 additions and 0 deletions.
82 changes: 82 additions & 0 deletions src/components/ComponentConfiguration.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1386,3 +1386,85 @@ export const Radio: Story = {
});
},
};

export const BSN: Story = {
render: Template,
name: 'type: bsn',

args: {
component: {
id: 'wekruya',
inputMask: '999999999',
validateOn: 'blur',
type: 'bsn',
key: 'bsn',
label: 'A BSN field',
validate: {
required: false,
},
},

builderInfo: {
title: 'BSN Field',
icon: 'id-card-o',
group: 'basic',
weight: 10,
schema: {},
},
},

play: async ({canvasElement, args}) => {
const canvas = within(canvasElement);

await expect(canvas.getByLabelText('Label')).toHaveValue('A BSN field');
await waitFor(async () => {
await expect(canvas.getByLabelText('Property Name')).toHaveValue('aBsnField');
});
await expect(canvas.getByLabelText('Description')).toHaveValue('');
await expect(canvas.getByLabelText('Show in summary')).toBeChecked();
await expect(canvas.getByLabelText('Show in email')).not.toBeChecked();
await expect(canvas.getByLabelText('Show in PDF')).toBeChecked();
await expect(canvas.queryByLabelText('Placeholder')).not.toBeInTheDocument();

// ensure that changing fields in the edit form properly update the preview
const preview = within(canvas.getByTestId('componentPreview'));

await userEvent.clear(canvas.getByLabelText('Label'));
await userEvent.type(canvas.getByLabelText('Label'), 'Updated preview label');
expect(await preview.findByText('Updated preview label'));

const previewInput = preview.getByLabelText<HTMLInputElement>('Updated preview label');
await expect(previewInput).toHaveDisplayValue('');
await expect(previewInput.type).toEqual('text');

// Ensure that the manually entered key is kept instead of derived from the label,
// even when key/label components are not mounted.
const keyInput = canvas.getByLabelText('Property Name');
fireEvent.change(keyInput, {target: {value: 'customKey'}});
await userEvent.click(canvas.getByRole('tab', {name: 'Advanced'}));
await userEvent.click(canvas.getByRole('tab', {name: 'Basic'}));
await userEvent.clear(canvas.getByLabelText('Label'));
await userEvent.type(canvas.getByLabelText('Label'), 'Other label', {delay: 50});
await expect(canvas.getByLabelText('Property Name')).toHaveDisplayValue('customKey');

// check that toggling the 'multiple' checkbox properly updates the preview and default
// value field. We use fireEvent because firefox borks on userEvent.click, see:
// https://github.com/testing-library/user-event/issues/1149
fireEvent.click(canvas.getByLabelText<HTMLInputElement>('Multiple values'));
await userEvent.click(preview.getByRole('button', {name: 'Add another'}));
// await expect(preview.getByTestId('input-customKey[0]')).toHaveDisplayValue('');
// test for the default value inputs -> these don't have accessible labels/names :(
const addButtons = canvas.getAllByRole('button', {name: 'Add another'});
await userEvent.click(addButtons[0]);
await waitFor(async () => {
await expect(await canvas.findByTestId('input-defaultValue[0]')).toBeVisible();
});

// check that default value is e-mail validated
const defaultInput0 = canvas.getByTestId<HTMLInputElement>('input-defaultValue[0]');
await expect(defaultInput0.type).toEqual('text');

await userEvent.click(canvas.getByRole('button', {name: 'Save'}));
expect(args.onSubmit).toHaveBeenCalled();
},
};
74 changes: 74 additions & 0 deletions src/components/ComponentPreview.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -753,3 +753,77 @@ export const RadioVariable: Story = {
},
},
};

export const BSN: Story = {
name: 'BSN',
render: Template,

args: {
component: {
type: 'bsn',
id: 'bsn',
key: 'bsnPreview',
label: 'BSN preview',
description: 'A preview of the BSN Formio component',
hidden: true, // must be ignored
inputMask: '999999999',
},
},

play: async ({canvasElement, args}) => {
const canvas = within(canvasElement);

// check that the user-controlled content is visible
await canvas.findByText('BSN preview');
await canvas.findByText('A preview of the BSN Formio component');

// check that the input name is set correctly
const input = canvas.getByLabelText('BSN preview');
// @ts-ignore
await expect(input.getAttribute('name')).toBe(args.component.key);

expect(input).toHaveAttribute('placeholder', '_________');
await userEvent.type(input, '123456789');
expect(input).toHaveDisplayValue('123456789');
},
};

export const BSNMultiple: Story = {
name: 'BSN Multiple',
render: Template,

args: {
component: {
type: 'bsn',
id: 'bsn',
key: 'bsnPreview',
label: 'BSN preview',
description: 'Description only once',
hidden: true, // must be ignored
multiple: true,
},
},

play: async ({canvasElement}) => {
const canvas = within(canvasElement);

// check that new items can be added
await userEvent.click(canvas.getByRole('button', {name: 'Add another'}));
const input1 = canvas.getByTestId<HTMLInputElement>('input-bsnPreview[0]');
await expect(input1).toHaveDisplayValue('');
await expect(input1.type).toEqual('text');

// the description should be rendered only once, even with > 1 inputs
await userEvent.click(canvas.getByRole('button', {name: 'Add another'}));
const input2 = canvas.getByTestId<HTMLInputElement>('input-bsnPreview[1]');
await expect(input2).toHaveDisplayValue('');
await expect(canvas.queryAllByText('Description only once')).toHaveLength(1);

// finally, it should be possible delete rows again
const removeButtons = await canvas.findAllByRole('button', {name: 'Remove item'});
await expect(removeButtons.length).toBe(2);
await userEvent.click(removeButtons[0]);
await expect(canvas.getByTestId('input-bsnPreview[0]')).toHaveDisplayValue('');
await expect(canvas.queryByTestId('input-bsnPreview[1]')).not.toBeInTheDocument();
},
};
7 changes: 7 additions & 0 deletions src/registry/bsn/edit-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {IntlShape} from 'react-intl';

import {buildCommonSchema} from '@/registry/validation';

const schema = (intl: IntlShape) => buildCommonSchema(intl);

export default schema;
194 changes: 194 additions & 0 deletions src/registry/bsn/edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import {BsnComponentSchema} from '@open-formulieren/types';
import {useFormikContext} from 'formik';
import {FormattedMessage, useIntl} from 'react-intl';

import {
BuilderTabs,
ClearOnHide,
Description,
Hidden,
IsSensitiveData,
Key,
Label,
Multiple,
Prefill,
PresentationConfig,
ReadOnly,
Registration,
SimpleConditional,
Tooltip,
Translations,
Validate,
useDeriveComponentKey,
} from '@/components/builder';
import {LABELS} from '@/components/builder/messages';
import {TabList, TabPanel, Tabs, TextField} from '@/components/formio';
import {getErrorNames} from '@/utils/errors';

import {EditFormDefinition} from '../types';

/**
* Form to configure a Formio 'bsn' type component.
*/
const EditForm: EditFormDefinition<BsnComponentSchema> = () => {
const intl = useIntl();
const [isKeyManuallySetRef, generatedKey] = useDeriveComponentKey();
const {values, errors} = useFormikContext<BsnComponentSchema>();

const erroredFields = Object.keys(errors).length ? getErrorNames<BsnComponentSchema>(errors) : [];
// TODO: pattern match instead of just string inclusion?
// TODO: move into more generically usuable utility when we implement other component
// types
const hasAnyError = (...fieldNames: string[]): boolean => {
if (!erroredFields.length) return false;
return fieldNames.some(name => erroredFields.includes(name));
};

Validate.useManageValidatorsTranslations<BsnComponentSchema>(['required']);
return (
<Tabs>
<TabList>
<BuilderTabs.Basic
hasErrors={hasAnyError(
'label',
'key',
'description',
'tooltip',
'showInSummary',
'showInEmail',
'showInPDF',
'multiple',
'hidden',
'clearOnHide',
'isSensitiveData',
'defaultValue',
'disabled'
)}
/>
<BuilderTabs.Advanced hasErrors={hasAnyError('conditional')} />
<BuilderTabs.Validation hasErrors={hasAnyError('validate')} />
<BuilderTabs.Registration hasErrors={hasAnyError('registration')} />
<BuilderTabs.Prefill hasErrors={hasAnyError('prefill')} />
<BuilderTabs.Translations hasErrors={hasAnyError('openForms.translations')} />
</TabList>

{/* Basic tab */}
<TabPanel>
<Label />
<Key isManuallySetRef={isKeyManuallySetRef} generatedValue={generatedKey} />
<Description />
<Tooltip />
<PresentationConfig />
<Multiple<BsnComponentSchema> />
<Hidden />
<ClearOnHide />
<IsSensitiveData />
<DefaultValue multiple={!!values.multiple} />
<ReadOnly />
</TabPanel>

{/* Advanced tab */}
<TabPanel>
<SimpleConditional />
</TabPanel>

{/* Validation tab */}
<TabPanel>
<Validate.Required />
<Validate.ValidatorPluginSelect />
<Validate.ValidationErrorTranslations />
</TabPanel>

{/* Registration tab */}
<TabPanel>
<Registration.RegistrationAttributeSelect />
</TabPanel>

{/* Prefill tab */}
<TabPanel>
<Prefill.PrefillConfiguration />
</TabPanel>

{/* Translations */}
<TabPanel>
<Translations.ComponentTranslations<BsnComponentSchema>
propertyLabels={{
label: intl.formatMessage(LABELS.label),
description: intl.formatMessage(LABELS.description),
tooltip: intl.formatMessage(LABELS.tooltip),
}}
/>
</TabPanel>
</Tabs>
);
};

/*
Making this introspected or declarative doesn't seem advisable, as React is calling
React.Children and related API's legacy API - this may get removed in future
versions.
Explicitly specifying the schema and default values is therefore probbaly best, at
the cost of some repetition.
*/
EditForm.defaultValues = {
validateOn: 'blur',
inputMask: '999999999',
// basic tab
label: '',
key: '',
description: '',
tooltip: '',
showInSummary: true,
showInEmail: false,
showInPDF: true,
multiple: false,
hidden: false,
clearOnHide: true,
isSensitiveData: true,
defaultValue: '',
disabled: false,
// Advanced tab
conditional: {
show: undefined,
when: '',
eq: '',
},
// Validation tab
validate: {
required: false,
plugins: [],
},
translatedErrors: {},
registration: {
attribute: '',
},
prefill: {
plugin: null,
attribute: null,
identifierRole: 'main',
},
};

interface DefaultValueProps {
multiple: boolean;
}

const DefaultValue: React.FC<DefaultValueProps> = ({multiple}) => {
const intl = useIntl();
const tooltip = intl.formatMessage({
description: "Tooltip for 'defaultValue' builder field",
defaultMessage: 'This will be the initial value for this field before user interaction.',
});
return (
<TextField
name="defaultValue"
label={<FormattedMessage {...LABELS.defaultValue} />}
tooltip={tooltip}
multiple={multiple}
inputMask="999999999"
/>
);
};

export default EditForm;
10 changes: 10 additions & 0 deletions src/registry/bsn/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import EditForm from './edit';
import validationSchema from './edit-validation';
import Preview from './preview';

export default {
edit: EditForm,
editSchema: validationSchema,
preview: Preview,
defaultValue: '',
};
Loading

0 comments on commit 0367e61

Please sign in to comment.