From bb610d9a7f7e223bcd78274711dafa78117ad79e Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:40:12 +0100 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8=20Add=20`currency`=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 14 +- package.json | 2 +- src/registry/currency/edit-validation.ts | 36 ++++ src/registry/currency/edit.tsx | 214 +++++++++++++++++++++++ src/registry/currency/index.ts | 10 ++ src/registry/currency/preview.tsx | 29 +++ src/registry/index.tsx | 2 + src/registry/number/edit-validation.ts | 14 +- src/registry/number/preview.tsx | 2 +- 9 files changed, 303 insertions(+), 20 deletions(-) create mode 100644 src/registry/currency/edit-validation.ts create mode 100644 src/registry/currency/edit.tsx create mode 100644 src/registry/currency/index.ts create mode 100644 src/registry/currency/preview.tsx diff --git a/package-lock.json b/package-lock.json index 4b39bbcc..fcb13767 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,7 @@ "@formatjs/cli": "^6.1.1", "@formatjs/ts-transformer": "^3.12.0", "@fortawesome/fontawesome-free": "^6.4.0", - "@open-formulieren/types": "^0.13.0", + "@open-formulieren/types": "^0.14.1", "@storybook/addon-actions": "^7.3.2", "@storybook/addon-essentials": "^7.3.2", "@storybook/addon-interactions": "^7.3.2", @@ -5840,9 +5840,9 @@ } }, "node_modules/@open-formulieren/types": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@open-formulieren/types/-/types-0.13.0.tgz", - "integrity": "sha512-zG5eHaSqont3nSO2wzSO8fJL43E3pePbStenoISZuFR4dDV0sjdyUFwfmujZd0qgj089rJDE+MAZB3SwF4hZyg==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@open-formulieren/types/-/types-0.14.1.tgz", + "integrity": "sha512-QyKgk4WfabXhWFVfJTM69v7SjWzcMlDclaJ4IY5YtEKANJqyPMMKgHyeavX1kBSQ/hLyqEIoEtogfjScRNb9Ug==", "dev": true }, "node_modules/@pkgjs/parseargs": { @@ -35200,9 +35200,9 @@ } }, "@open-formulieren/types": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@open-formulieren/types/-/types-0.13.0.tgz", - "integrity": "sha512-zG5eHaSqont3nSO2wzSO8fJL43E3pePbStenoISZuFR4dDV0sjdyUFwfmujZd0qgj089rJDE+MAZB3SwF4hZyg==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@open-formulieren/types/-/types-0.14.1.tgz", + "integrity": "sha512-QyKgk4WfabXhWFVfJTM69v7SjWzcMlDclaJ4IY5YtEKANJqyPMMKgHyeavX1kBSQ/hLyqEIoEtogfjScRNb9Ug==", "dev": true }, "@pkgjs/parseargs": { diff --git a/package.json b/package.json index d6520f0b..d61a6411 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@formatjs/cli": "^6.1.1", "@formatjs/ts-transformer": "^3.12.0", "@fortawesome/fontawesome-free": "^6.4.0", - "@open-formulieren/types": "^0.13.0", + "@open-formulieren/types": "^0.14.1", "@storybook/addon-actions": "^7.3.2", "@storybook/addon-essentials": "^7.3.2", "@storybook/addon-interactions": "^7.3.2", diff --git a/src/registry/currency/edit-validation.ts b/src/registry/currency/edit-validation.ts new file mode 100644 index 00000000..4f3d3a2b --- /dev/null +++ b/src/registry/currency/edit-validation.ts @@ -0,0 +1,36 @@ +import {IntlShape} from 'react-intl'; +import {z} from 'zod'; + +import {buildCommonSchema} from '@/registry/validation'; + +// undefined (optional) for unspecified, otherwise a finite numeric value. Note that +// null would be nicer, but formio's schema does not support null for validate.min, +// validate.max or defaultValue +const currencySchema = z.number().finite().optional(); + +// case for when component.multiple=false +const singleValueSchema = z + .object({multiple: z.literal(false)}) + .and(z.object({defaultValue: currencySchema})); + +// case for when component.multiple=true +const multipleValueSchema = z + .object({multiple: z.literal(true)}) + .and(z.object({defaultValue: currencySchema.array()})); + +const defaultValueSchema = singleValueSchema.or(multipleValueSchema); + +const currencySpecific = z.object({ + decimalLimit: z.number().int().positive().optional(), + validate: z + .object({ + min: currencySchema, + max: currencySchema, + }) + .optional(), +}); + +const schema = (intl: IntlShape) => + buildCommonSchema(intl).and(defaultValueSchema).and(currencySpecific); + +export default schema; diff --git a/src/registry/currency/edit.tsx b/src/registry/currency/edit.tsx new file mode 100644 index 00000000..ad5f7720 --- /dev/null +++ b/src/registry/currency/edit.tsx @@ -0,0 +1,214 @@ +import {CurrencyComponentSchema} from '@open-formulieren/types'; +import {useFormikContext} from 'formik'; +import {FormattedMessage, useIntl} from 'react-intl'; + +import { + BuilderTabs, + ClearOnHide, + Description, + Hidden, + IsSensitiveData, + Key, + Label, + PresentationConfig, + Registration, + SimpleConditional, + Suffix, + Tooltip, + Translations, + Validate, + useDeriveComponentKey, +} from '@/components/builder'; +import {LABELS} from '@/components/builder/messages'; +import {Checkbox, NumberField, TabList, TabPanel, Tabs} from '@/components/formio'; +import {getErrorNames} from '@/utils/errors'; + +import {EditFormDefinition} from '../types'; + +/** + * Form to configure a Formio 'currency' type component. + */ +const EditForm: EditFormDefinition = () => { + const intl = useIntl(); + const [isKeyManuallySetRef, generatedKey] = useDeriveComponentKey(); + const {errors} = useFormikContext(); + + const erroredFields = Object.keys(errors).length + ? getErrorNames(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(['required', 'min', 'max']); + + return ( + + + + + + + + + + {/* Basic tab */} + + + + {/* Advanced tab */} + + + + + {/* Validation tab */} + + + + + + + + + {/* Registration tab */} + + + + + {/* Translations */} + + + propertyLabels={{ + label: intl.formatMessage(LABELS.label), + description: intl.formatMessage(LABELS.description), + tooltip: intl.formatMessage(LABELS.tooltip), + }} + /> + + + ); +}; + +EditForm.defaultValues = { + // basic tab + label: '', + key: '', + description: '', + tooltip: '', + showInSummary: true, + showInEmail: false, + showInPDF: true, + hidden: false, + clearOnHide: true, + isSensitiveData: false, + defaultValue: undefined, + decimalLimit: undefined, + allowNegative: false, + currency: 'EUR', + // Advanced tab + conditional: { + show: undefined, + when: '', + eq: '', + }, + // Validation tab + validate: { + required: false, + plugins: [], + min: undefined, + max: undefined, + }, + translatedErrors: {}, + // Registration tab + registration: { + attribute: '', + }, +}; + +const DefaultValue: React.FC = () => { + 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} + /> + ); +}; + +const DecimalLimit: React.FC = () => { + const intl = useIntl(); + const tooltip = intl.formatMessage({ + description: "Tooltip for 'decimalLimit' builder field", + defaultMessage: 'The maximum number of decimal places.', + }); + return ( + + } + tooltip={tooltip} + /> + ); +}; + +const AllowNegative: React.FC = () => { + const intl = useIntl(); + const tooltip = intl.formatMessage({ + description: "Tooltip for 'allowNegative' builder field", + defaultMessage: 'Allow negative values.', + }); + return ( + + } + tooltip={tooltip} + /> + ); +}; + +export default EditForm; diff --git a/src/registry/currency/index.ts b/src/registry/currency/index.ts new file mode 100644 index 00000000..414d9d18 --- /dev/null +++ b/src/registry/currency/index.ts @@ -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: undefined, // formik field value +}; diff --git a/src/registry/currency/preview.tsx b/src/registry/currency/preview.tsx new file mode 100644 index 00000000..0c77de6b --- /dev/null +++ b/src/registry/currency/preview.tsx @@ -0,0 +1,29 @@ +import {CurrencyComponentSchema} from '@open-formulieren/types'; + +import {NumberField} from '@/components/formio'; + +import {ComponentPreviewProps} from '../types'; + +/** + * Show a formio currency 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}) => { + // FIXME: incorporate decimalLimit and allowNegative + const {key, label, description, tooltip, validate = {}} = component; + const {required = false} = validate; + return ( + + ); +}; + +export default Preview; diff --git a/src/registry/index.tsx b/src/registry/index.tsx index 3b7a5885..3f1f5f5a 100644 --- a/src/registry/index.tsx +++ b/src/registry/index.tsx @@ -1,5 +1,6 @@ import {AnyComponentSchema, FallbackSchema, hasOwnProperty} from '@/types'; +import Currency from './currency'; import DateField from './date'; import DateTimeField from './datetime'; import Email from './email'; @@ -36,6 +37,7 @@ const REGISTRY: Registry = { phoneNumber: PhoneNumber, postcode: Postcode, file: FileUpload, + currency: Currency, }; export {Fallback}; diff --git a/src/registry/number/edit-validation.ts b/src/registry/number/edit-validation.ts index 5621462f..456da1d1 100644 --- a/src/registry/number/edit-validation.ts +++ b/src/registry/number/edit-validation.ts @@ -8,17 +8,9 @@ import {buildCommonSchema} from '@/registry/validation'; // validate.max or defaultValue const numberSchema = z.number().finite().optional(); -// case for when component.multiple=false -const singleValueSchema = z - .object({multiple: z.literal(false)}) - .and(z.object({defaultValue: numberSchema})); - -// case for when component.multiple=true -const multipleValueSchema = z - .object({multiple: z.literal(true)}) - .and(z.object({defaultValue: numberSchema.array()})); - -const defaultValueSchema = singleValueSchema.or(multipleValueSchema); +const defaultValueSchema = z.object({ + defaultValue: numberSchema, +}); const numberSpecific = z.object({ decimalLimit: z.number().int().positive().optional(), diff --git a/src/registry/number/preview.tsx b/src/registry/number/preview.tsx index 5995bd15..7b6d6485 100644 --- a/src/registry/number/preview.tsx +++ b/src/registry/number/preview.tsx @@ -5,7 +5,7 @@ import {NumberField} from '@/components/formio'; import {ComponentPreviewProps} from '../types'; /** - * Show a formio textfield component preview. + * Show a formio number 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 From 5d8d2588ba2edd8c811aa83938347b60355b3ca4 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:51:46 +0100 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20unused=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/registry/currency/edit.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registry/currency/edit.tsx b/src/registry/currency/edit.tsx index ad5f7720..e9b40204 100644 --- a/src/registry/currency/edit.tsx +++ b/src/registry/currency/edit.tsx @@ -13,7 +13,6 @@ import { PresentationConfig, Registration, SimpleConditional, - Suffix, Tooltip, Translations, Validate, From 08ddf1efd60da19265c51c28d76f7e749879e43c Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Wed, 8 Nov 2023 16:51:57 +0100 Subject: [PATCH 3/7] PR feedback --- src/components/formio/number.stories.tsx | 7 +++++++ src/components/formio/number.tsx | 24 +++++++++++++++++------- src/registry/currency/edit-validation.ts | 14 ++------------ src/registry/currency/edit.tsx | 3 ++- src/registry/currency/preview.tsx | 1 + 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/components/formio/number.stories.tsx b/src/components/formio/number.stories.tsx index b3af9bac..58e99ee6 100644 --- a/src/components/formio/number.stories.tsx +++ b/src/components/formio/number.stories.tsx @@ -43,6 +43,13 @@ export const WithToolTip: Story = { }, }; +export const WithPrefix: Story = { + args: { + label: 'With prefix', + prefix: 'm3', + }, +}; + export const WithSuffix: Story = { args: { label: 'With suffix', diff --git a/src/components/formio/number.tsx b/src/components/formio/number.tsx index 3c602c91..02e9a622 100644 --- a/src/components/formio/number.tsx +++ b/src/components/formio/number.tsx @@ -14,6 +14,7 @@ export interface NumberProps { required?: boolean; tooltip?: string; description?: string; + prefix?: string; suffix?: string; } @@ -27,6 +28,7 @@ export const NumberField: React.FC required = false, tooltip = '', description = '', + prefix = '', suffix = '', ...props }) => { @@ -49,7 +51,7 @@ export const NumberField: React.FC tooltip={tooltip} >
- + }; interface WrapperProps { - suffix: string; + prefix?: string; + suffix?: string; children: React.ReactNode; } -const Wrapper: React.FC = ({suffix, children}) => { - if (!suffix) return <>{children}; +const Wrapper: React.FC = ({prefix, suffix, children}) => { + if (!(prefix || suffix)) return <>{children}; return (
+ {prefix && ( +
+ {prefix} +
+ )} {children} -
- {suffix} -
+ {suffix && ( +
+ {suffix} +
+ )}
); }; diff --git a/src/registry/currency/edit-validation.ts b/src/registry/currency/edit-validation.ts index 4f3d3a2b..d43d8b48 100644 --- a/src/registry/currency/edit-validation.ts +++ b/src/registry/currency/edit-validation.ts @@ -8,20 +8,10 @@ import {buildCommonSchema} from '@/registry/validation'; // validate.max or defaultValue const currencySchema = z.number().finite().optional(); -// case for when component.multiple=false -const singleValueSchema = z - .object({multiple: z.literal(false)}) - .and(z.object({defaultValue: currencySchema})); - -// case for when component.multiple=true -const multipleValueSchema = z - .object({multiple: z.literal(true)}) - .and(z.object({defaultValue: currencySchema.array()})); - -const defaultValueSchema = singleValueSchema.or(multipleValueSchema); +const defaultValueSchema = z.object({defaultValue: currencySchema}); const currencySpecific = z.object({ - decimalLimit: z.number().int().positive().optional(), + decimalLimit: z.union(Array.from({length: 11}, (_, i) => z.literal(i)) as any).optional(), validate: z .object({ min: currencySchema, diff --git a/src/registry/currency/edit.tsx b/src/registry/currency/edit.tsx index e9b40204..468a0f62 100644 --- a/src/registry/currency/edit.tsx +++ b/src/registry/currency/edit.tsx @@ -120,6 +120,7 @@ const EditForm: EditFormDefinition = () => { }; EditForm.defaultValues = { + currency: 'EUR', // basic tab label: '', key: '', @@ -134,7 +135,6 @@ EditForm.defaultValues = { defaultValue: undefined, decimalLimit: undefined, allowNegative: false, - currency: 'EUR', // Advanced tab conditional: { show: undefined, @@ -166,6 +166,7 @@ const DefaultValue: React.FC = () => { name="defaultValue" label={} tooltip={tooltip} + prefix="€" /> ); }; diff --git a/src/registry/currency/preview.tsx b/src/registry/currency/preview.tsx index 0c77de6b..324420a9 100644 --- a/src/registry/currency/preview.tsx +++ b/src/registry/currency/preview.tsx @@ -22,6 +22,7 @@ const Preview: React.FC> = ({comp description={description} tooltip={tooltip} required={required} + prefix="€" /> ); }; From ce98f0ca7efabb75dbb38c32f3ad4691658e2b04 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Thu, 9 Nov 2023 10:55:15 +0100 Subject: [PATCH 4/7] Trigger CI From b4049d7a48183e3754dab00d5b17cff1fd97e4e4 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:11:38 +0100 Subject: [PATCH 5/7] Fix unstable test --- src/registry/datetime/datetime-component.stories.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/registry/datetime/datetime-component.stories.ts b/src/registry/datetime/datetime-component.stories.ts index fdda3b81..63b7b19d 100644 --- a/src/registry/datetime/datetime-component.stories.ts +++ b/src/registry/datetime/datetime-component.stories.ts @@ -1,6 +1,6 @@ import {expect} from '@storybook/jest'; import {Meta, StoryObj} from '@storybook/react'; -import {userEvent, within} from '@storybook/testing-library'; +import {userEvent, waitFor, within} from '@storybook/testing-library'; import ComponentEditForm from '@/components/ComponentEditForm'; import {withFormik} from '@/sb-decorators'; @@ -42,7 +42,9 @@ export const ValidateDeltaConstraintConfiguration: Story = { await step('Navigate to validation tab and open maxDate configuration', async () => { await userEvent.click(canvas.getByRole('link', {name: 'Validation'})); await userEvent.click(canvas.getByText(/Maximum date/)); - expect(await canvas.findByText('Mode preset')).toBeVisible(); + await waitFor(async () => { + expect(await canvas.findByText('Mode preset')).toBeVisible(); + }); }); await step('Configure relative to variable', async () => { From da142069ec54bf55e9cc836bc39956142e9c9a21 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Thu, 9 Nov 2023 11:30:56 +0100 Subject: [PATCH 6/7] More unstable tests fixes --- src/registry/file/file-validation.stories.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/registry/file/file-validation.stories.ts b/src/registry/file/file-validation.stories.ts index 4eb0273b..5796dded 100644 --- a/src/registry/file/file-validation.stories.ts +++ b/src/registry/file/file-validation.stories.ts @@ -1,6 +1,6 @@ import {expect} from '@storybook/jest'; import {Meta, StoryObj} from '@storybook/react'; -import {fireEvent, userEvent, within} from '@storybook/testing-library'; +import {fireEvent, userEvent, waitFor, within} from '@storybook/testing-library'; import ComponentEditForm from '@/components/ComponentEditForm'; import {BuilderContextDecorator} from '@/sb-decorators'; @@ -66,8 +66,10 @@ export const ResizeOptions: Story = { await userEvent.keyboard('[Tab]'); - expect(await canvas.findByText('Number must be greater than 0')).toBeVisible(); - expect(await canvas.findByText('Expected integer, received float')).toBeVisible(); + await waitFor(async () => { + expect(await canvas.findByText('Number must be greater than 0')).toBeVisible(); + expect(await canvas.findByText('Expected integer, received float')).toBeVisible(); + }); }); }, }; @@ -82,7 +84,9 @@ export const MaxNumberOfFiles: Story = { await userEvent.type(canvas.getByLabelText('Maximum number of files'), '0'); await userEvent.keyboard('[Tab]'); - expect(await canvas.findByText('Number must be greater than 0')).toBeVisible(); + await waitFor(async () => { + expect(await canvas.findByText('Number must be greater than 0')).toBeVisible(); + }); }, }; From 9242eb7d152cc04673a2e8114f6ca64d4728a677 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Thu, 9 Nov 2023 19:27:58 +0100 Subject: [PATCH 7/7] :ok_hand: Use correct bootstrap classname for prefix --- src/components/formio/number.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/formio/number.tsx b/src/components/formio/number.tsx index 02e9a622..dd3cfcaf 100644 --- a/src/components/formio/number.tsx +++ b/src/components/formio/number.tsx @@ -90,7 +90,7 @@ const Wrapper: React.FC = ({prefix, suffix, children}) => { return (
{prefix && ( -
+
{prefix}
)}