From e29c09adf9bc08c0b9c5921a5aada88e6b39535e Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Fri, 24 Nov 2023 15:45:35 +0100 Subject: [PATCH] [#65] Add new `address` component --- .../ComponentConfiguration.stories.tsx | 58 ++++++++++++++++- src/components/formio/fieldset.tsx | 64 +++++++++++++++++++ src/components/formio/index.ts | 1 + src/registry/address/edit-validation.ts | 7 +- src/registry/address/edit.tsx | 29 ++------- src/registry/address/preview.tsx | 57 +++++++++++++---- src/registry/index.tsx | 3 + 7 files changed, 180 insertions(+), 39 deletions(-) create mode 100644 src/components/formio/fieldset.tsx diff --git a/src/components/ComponentConfiguration.stories.tsx b/src/components/ComponentConfiguration.stories.tsx index ab1b7b71..744845c2 100644 --- a/src/components/ComponentConfiguration.stories.tsx +++ b/src/components/ComponentConfiguration.stories.tsx @@ -186,7 +186,7 @@ export const TextField: Story = { await userEvent.type(canvas.getByLabelText('Label'), 'Updated preview label'); expect(await preview.findByText('Updated preview label')); - const previewInput = preview.getByLabelText('Updated preview label'); + const previewInput = preview.getByText('Updated preview label'); await expect(previewInput).toHaveDisplayValue(''); // Ensure that the manually entered key is kept instead of derived from the label, @@ -1386,3 +1386,59 @@ export const Radio: Story = { }); }, }; + +export const Address: Story = { + render: Template, + name: 'type: address', + + args: { + component: { + id: 'wekruya', + type: 'address', + key: 'address', + label: 'An address', + validate: { + required: false, + }, + }, + }, + + play: async ({canvasElement, args}) => { + const canvas = within(canvasElement); + + await expect(canvas.getByLabelText('Label')).toHaveValue('An address'); + await waitFor(async () => { + await expect(canvas.getByLabelText('Property Name')).toHaveValue('anAddress'); + }); + await expect(canvas.getByLabelText('Description')).toHaveValue(''); + await expect(canvas.getByLabelText('Tooltip')).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(); + + // 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.getByText('Updated preview label'); + await expect(previewInput).toHaveDisplayValue(''); + + // 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 is deliberate, as userEvent.clear + userEvent.type briefly makes the field + // not have any value, which triggers the generate-key-from-label behaviour. + fireEvent.change(keyInput, {target: {value: 'customKey'}}); + await userEvent.click(canvas.getByRole('tab', {name: 'Location'})); + 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'); + + await userEvent.click(canvas.getByRole('button', {name: 'Save'})); + expect(args.onSubmit).toHaveBeenCalled(); + }, +}; diff --git a/src/components/formio/fieldset.tsx b/src/components/formio/fieldset.tsx new file mode 100644 index 00000000..dd19bed5 --- /dev/null +++ b/src/components/formio/fieldset.tsx @@ -0,0 +1,64 @@ +import {css} from '@emotion/css'; +import clsx from 'clsx'; +import React from 'react'; + +import {AnyComponentSchema} from '@/types'; +import {useValidationErrors} from '@/utils/errors'; +import {ErrorList} from '@/utils/errors'; + +import Tooltip from './tooltip'; + +export interface FieldsetProps { + // XXX: eventually (most) of these literals will be included in AnyComponentType + type: AnyComponentSchema['type'] | 'datagrid' | 'datamap' | 'select' | 'columns' | 'textarea'; + field?: string; + required?: boolean; + label?: React.ReactNode; + tooltip?: string; + htmlId?: string; + children: React.ReactNode; +} + +// Fix the overlapping icons/text when the error icon is shown. +// XXX: once we've moved away from bootstrap/formio 'component library', this fix and +// @emotion/css can be removed again. +const PAD_ERROR_ICON = css` + .form-control.is-invalid { + padding-inline-end: calc(1.5em + 0.75rem); + } +`; + +const Fieldset: React.FC = ({ + type, + field = '', + required = false, + label, + tooltip = '', + children, +}) => { + const {errors} = useValidationErrors(field); + const className = clsx('form-group', 'has-feedback', 'formio-component', PAD_ERROR_ICON, { + [`formio-component-${type}`]: type, + 'has-error': field && errors.length > 0, + required: required, + }); + // const labelClassName = clsx('col-form-label', {'field-required': required}); + + return ( +
+
+ {label && ( + + {label} + {tooltip && ' '} + + + )} + {children} + +
+
+ ); +}; + +export default Fieldset; diff --git a/src/components/formio/index.ts b/src/components/formio/index.ts index 44d6b8e6..94e221e7 100644 --- a/src/components/formio/index.ts +++ b/src/components/formio/index.ts @@ -27,3 +27,4 @@ export {default as TextArea} from './textarea'; export * from './datagrid'; export {default as DataGrid} from './datagrid'; export {default as DataMap} from './datamap'; +export {default as FieldSet} from './fieldset'; diff --git a/src/registry/address/edit-validation.ts b/src/registry/address/edit-validation.ts index b20ca3d1..a239ecf1 100644 --- a/src/registry/address/edit-validation.ts +++ b/src/registry/address/edit-validation.ts @@ -6,9 +6,12 @@ import {buildCommonSchema} from '@/registry/validation'; // Constraints taken from the BRK API (apart from postcode which comes from our postcode component) const addressSchema = z.object({ postcode: z.string().regex(/^[1-9][0-9]{3} ?(?!sa|sd|ss|SA|SD|SS)[a-zA-Z]{2}$/), - housenumber: z.number().int().min(1).max(99999), + housenumber: z.string().regex(/^\d{1,5}$/), houseletter: z.string().regex(/^[a-zA-Z]$/), - housenumberaddition: z.string().regex(/^([a-z,A-Z,0-9]){1,4}$/).optional(), + housenumberaddition: z + .string() + .regex(/^([a-z,A-Z,0-9]){1,4}$/) + .optional(), }); const defaultValueSchema = z.object({defaultValue: addressSchema}); diff --git a/src/registry/address/edit.tsx b/src/registry/address/edit.tsx index 34adb76e..aaebf71e 100644 --- a/src/registry/address/edit.tsx +++ b/src/registry/address/edit.tsx @@ -1,6 +1,6 @@ import {AddressComponentSchema} from '@open-formulieren/types'; import {useFormikContext} from 'formik'; -import {FormattedMessage, useIntl} from 'react-intl'; +import {useIntl} from 'react-intl'; import { BuilderTabs, @@ -10,7 +10,6 @@ import { IsSensitiveData, Key, Label, - Multiple, PresentationConfig, Registration, SimpleConditional, @@ -20,18 +19,18 @@ import { useDeriveComponentKey, } from '@/components/builder'; import {LABELS} from '@/components/builder/messages'; -import {TabList, TabPanel, Tabs, TextField} from '@/components/formio'; +import {TabList, TabPanel, Tabs} from '@/components/formio'; import {getErrorNames} from '@/utils/errors'; import {EditFormDefinition} from '../types'; /** - * Form to configure a Formio 'licenseplate' type component. + * Form to configure a Formio 'address' type component. */ const EditForm: EditFormDefinition = () => { const intl = useIntl(); const [isKeyManuallySetRef, generatedKey] = useDeriveComponentKey(); - const {values, errors} = useFormikContext(); + const {errors} = useFormikContext(); const erroredFields = Object.keys(errors).length ? getErrorNames(errors) @@ -77,11 +76,9 @@ const EditForm: EditFormDefinition = () => { - /> - {/* Advanced tab */} @@ -132,7 +129,6 @@ EditForm.defaultValues = { showInSummary: true, showInEmail: false, showInPDF: true, - multiple: false, hidden: false, clearOnHide: true, isSensitiveData: true, @@ -160,21 +156,4 @@ EditForm.defaultValues = { }, }; - -const DefaultValue: React.FC = ({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 ( - } - tooltip={tooltip} - multiple={multiple} - /> - ); -}; - export default EditForm; diff --git a/src/registry/address/preview.tsx b/src/registry/address/preview.tsx index 78392852..3d085b20 100644 --- a/src/registry/address/preview.tsx +++ b/src/registry/address/preview.tsx @@ -1,23 +1,23 @@ -import {LicensePlateComponentSchema} from '@open-formulieren/types'; - -import {Component, Description} from '@/components/formio'; +import {AddressComponentSchema} from '@open-formulieren/types'; +import {FormattedMessage} from 'react-intl'; +import {Description, FieldSet, TextField} from '@/components/formio'; import {ComponentPreviewProps} from '../types'; /** - * Show a formio iban component preview. + * Show a formio address component preview. * * NOTE: for the time being, this is rendered in the default Formio bootstrap style, * however at some point this should use the components of * @open-formulieren/formio-renderer instead for a more accurate preview. */ -const Preview: React.FC> = ({component}) => { - const {key, label, description, tooltip, validate} = component; +const Preview: React.FC> = ({component}) => { + const {key, label, description, tooltip, validate = {}} = component; const {required = false} = validate; return ( - > = ({ label={label} tooltip={tooltip} > -
- -
-
+ {description && } + + } + inputMask="9999 AA" + required={required} + /> + + } + required={required} + /> + + } + inputMask="A" + /> + + } + /> + ); }; diff --git a/src/registry/index.tsx b/src/registry/index.tsx index c3e26cbc..64e09d4d 100644 --- a/src/registry/index.tsx +++ b/src/registry/index.tsx @@ -1,5 +1,6 @@ import {AnyComponentSchema, FallbackSchema, hasOwnProperty} from '@/types'; +import Address from './address'; import Currency from './currency'; import DateField from './date'; import DateTimeField from './datetime'; @@ -46,6 +47,8 @@ const REGISTRY: Registry = { selectboxes: Selectboxes, currency: Currency, radio: Radio, + // Composed types: + address: Address, // Special types: iban: Iban, licenseplate: Licenseplate,