From 67a005e22b36229fb1316ac5884e0bb412e14608 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Wed, 30 Aug 2023 17:08:34 +0200 Subject: [PATCH 01/21] :sparkles: [#31] Implement a basic date field Opted to not use a datepicker library or anything like that, but instead rely on the native browser behaviour. The resulting values are in yyyy-mm-dd format which is exactly what we want - and leave it to the user's browser locale to format the dates as they want. --- src/components/formio/component.tsx | 4 +- src/components/formio/datefield.stories.ts | 101 ++++++++++++++++++++ src/components/formio/datefield.tsx | 81 ++++++++++++++++ src/components/formio/textfield.stories.tsx | 4 +- 4 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 src/components/formio/datefield.stories.ts create mode 100644 src/components/formio/datefield.tsx diff --git a/src/components/formio/component.tsx b/src/components/formio/component.tsx index 9d979a8e..1e40977b 100644 --- a/src/components/formio/component.tsx +++ b/src/components/formio/component.tsx @@ -1,13 +1,15 @@ import clsx from 'clsx'; import React from 'react'; +import {AnyComponentSchema} from '@/types'; import {useValidationErrors} from '@/utils/errors'; import {ErrorList} from '@/utils/errors'; import ComponentLabel from './component-label'; export interface ComponentProps { - type: 'textfield' | 'select' | 'checkbox' | 'number' | 'datagrid' | 'datamap'; // TODO: can this be inferred from somewhere? + // XXX: eventually (most) of these literals will be included in AnyComponentType + type: AnyComponentSchema['type'] | 'checkbox' | 'datagrid' | 'datamap' | 'select'; field?: string; required?: boolean; label?: React.ReactNode; diff --git a/src/components/formio/datefield.stories.ts b/src/components/formio/datefield.stories.ts new file mode 100644 index 00000000..dfbeafe4 --- /dev/null +++ b/src/components/formio/datefield.stories.ts @@ -0,0 +1,101 @@ +import {expect} from '@storybook/jest'; +import {Meta, StoryObj} from '@storybook/react'; +import {userEvent, within} from '@storybook/testing-library'; + +import {withFormik} from '@/sb-decorators'; + +import DateField from './datefield'; + +export default { + title: 'Formio/Components/DateField', + component: DateField, + decorators: [withFormik], + parameters: { + modal: {noModal: true}, + formik: {initialValues: {'my-datefield': '1980-01-01'}}, + }, + args: { + name: 'my-datefield', + }, +} as Meta; + +type Story = StoryObj; + +export const Required: Story = { + args: { + required: true, + label: 'A required datefield', + }, +}; + +export const WithoutLabel: Story = { + args: { + label: '', + }, +}; + +export const WithToolTip: Story = { + args: { + label: 'With tooltip', + tooltip: 'Hiya!', + required: false, + }, +}; + +export const Multiple: Story = { + args: { + label: 'Multiple inputs', + description: 'Array of dates instead of a single date value', + multiple: true, + }, + + parameters: { + formik: { + initialValues: {'my-datefield': ['1980-01-01']}, + }, + }, + + 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('input-my-datefield[0]'); + await expect(input1).toHaveDisplayValue('1980-01-01'); + + await userEvent.clear(input1); + await expect(input1).toHaveDisplayValue(''); + + // the label & description should be rendered only once, even with > 1 inputs + await expect(canvas.queryAllByText('Multiple inputs')).toHaveLength(1); + await expect( + canvas.queryAllByText('Array of dates instead of a single date value') + ).toHaveLength(1); + + // finally, it should be possible delete rows again + const removeButtons = await canvas.findAllByRole('button', {name: 'Remove item'}); + await expect(removeButtons).toHaveLength(2); + await userEvent.click(removeButtons[0]); + await expect(canvas.getByTestId('input-my-datefield[0]')).toHaveDisplayValue(''); + await expect(canvas.queryByTestId('input-my-datefield[1]')).not.toBeInTheDocument(); + }, +}; + +export const WithErrors: Story = { + args: { + label: 'With errors', + }, + + parameters: { + formik: { + initialValues: {'my-datefield': ''}, + initialErrors: {'my-datefield': 'Example error', 'other-field': 'Other error'}, + }, + }, + + play: async ({canvasElement}) => { + const canvas = within(canvasElement); + await expect(canvas.queryByText('Other error')).not.toBeInTheDocument(); + await expect(canvas.queryByText('Example error')).toBeInTheDocument(); + }, +}; diff --git a/src/components/formio/datefield.tsx b/src/components/formio/datefield.tsx new file mode 100644 index 00000000..78f69815 --- /dev/null +++ b/src/components/formio/datefield.tsx @@ -0,0 +1,81 @@ +import clsx from 'clsx'; +import {Field} from 'formik'; +import {useContext} from 'react'; + +import {RenderContext} from '@/context'; +import {ErrorList, useValidationErrors} from '@/utils/errors'; + +import Component from './component'; +import Description from './description'; +import {withMultiple} from './multiple'; + +export interface DateFieldProps { + name: string; + label?: React.ReactNode; + required?: boolean; + tooltip?: string; + description?: string; +} + +export const DateField: React.FC = ({ + name, + label, + required = false, + tooltip = '', + description = '', + ...props +}) => { + const {bareInput} = useContext(RenderContext); + const {errors, hasErrors} = useValidationErrors(name); + + const htmlId = `editform-${name}`; + + // let's not bother with date pickers - use the native browser date input instead. + const inputField = ( + + ); + + // 'bare input' is actually a little bit more than just the input, looking at the + // vanillay formio implementation. + if (bareInput) { + return ( + <> + {inputField} + + + ); + } + + // default-mode, wrapping the field with label, description etc. + return ( + +
{inputField}
+ {description && } +
+ ); +}; + +export const DateFieldMultiple = withMultiple(DateField, ''); +export default DateFieldMultiple; diff --git a/src/components/formio/textfield.stories.tsx b/src/components/formio/textfield.stories.tsx index 0bcc3125..d02f2f4b 100644 --- a/src/components/formio/textfield.stories.tsx +++ b/src/components/formio/textfield.stories.tsx @@ -61,13 +61,13 @@ export const Multiple: Story = { // check that new items can be added await userEvent.click(canvas.getByRole('button', {name: 'Add another'})); - const input1 = await canvas.getByTestId('input-my-textfield[0]'); + const input1 = canvas.getByTestId('input-my-textfield[0]'); await expect(input1).toHaveDisplayValue('first value'); await userEvent.clear(input1); await userEvent.type(input1, 'Foo'); await expect(input1).toHaveDisplayValue('Foo'); - const input2 = await canvas.getByTestId('input-my-textfield[1]'); + const input2 = canvas.getByTestId('input-my-textfield[1]'); await expect(input2).toHaveDisplayValue(''); // the label & description should be rendered only once, even with > 1 inputs From 71570dbfc8c76524f86ea8a077379eaa39efc6e8 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Wed, 30 Aug 2023 17:26:45 +0200 Subject: [PATCH 02/21] :pencil: [#31] Add story for date component --- .../ComponentConfiguration.stories.tsx | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/components/ComponentConfiguration.stories.tsx b/src/components/ComponentConfiguration.stories.tsx index 60ff4fd7..58f605d3 100644 --- a/src/components/ComponentConfiguration.stories.tsx +++ b/src/components/ComponentConfiguration.stories.tsx @@ -286,7 +286,7 @@ export const Email: Story = { export const NumberField: Story = { render: Template, - name: 'type: numberfield', + name: 'type: number', args: { component: { @@ -343,3 +343,28 @@ export const NumberField: Story = { await expect(canvas.getByLabelText('Property Name')).toHaveDisplayValue('customKey'); }, }; + +export const DateField: Story = { + render: Template, + name: 'type: date', + + args: { + component: { + id: 'wekruya', + type: 'date', + key: 'date', + label: 'A date field', + validate: { + required: false, + }, + }, + + builderInfo: { + title: 'Date Field', + icon: 'calendar', + group: 'basic', + weight: 10, + schema: {}, + }, + }, +}; From 1a698dbaa450b7d6e1885c657dff6e5bda1d267b Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Wed, 30 Aug 2023 17:46:29 +0200 Subject: [PATCH 03/21] :tada: [#31] Add date field edit form boilerplate --- src/registry/date/edit-validation.ts | 25 ++++++++ src/registry/date/edit.tsx | 92 ++++++++++++++++++++++++++++ src/registry/date/index.ts | 10 +++ src/registry/date/preview.tsx | 11 ++++ src/registry/index.tsx | 2 + 5 files changed, 140 insertions(+) create mode 100644 src/registry/date/edit-validation.ts create mode 100644 src/registry/date/edit.tsx create mode 100644 src/registry/date/index.ts create mode 100644 src/registry/date/preview.tsx diff --git a/src/registry/date/edit-validation.ts b/src/registry/date/edit-validation.ts new file mode 100644 index 00000000..0b28a890 --- /dev/null +++ b/src/registry/date/edit-validation.ts @@ -0,0 +1,25 @@ +import {IntlShape} from 'react-intl'; +import {z} from 'zod'; + +import {buildCommonSchema} from '@/registry/validation'; + +const dateSchema = z.coerce.date().optional(); + +// case for when component.multiple=false +const singleValueSchema = z + .object({multiple: z.literal(false)}) + .and(z.object({defaultValue: dateSchema})); + +// case for when component.multiple=true +const multipleValueSchema = z + .object({multiple: z.literal(true)}) + .and(z.object({defaultValue: dateSchema.array()})); + +const defaultValueSchema = singleValueSchema.or(multipleValueSchema); + +const dateSpecific = z.object({}); + +const schema = (intl: IntlShape) => + buildCommonSchema(intl).and(defaultValueSchema).and(dateSpecific); + +export default schema; diff --git a/src/registry/date/edit.tsx b/src/registry/date/edit.tsx new file mode 100644 index 00000000..f60f760c --- /dev/null +++ b/src/registry/date/edit.tsx @@ -0,0 +1,92 @@ +import {DateComponentSchema} from '@open-formulieren/types'; + +// import {useFormikContext} from 'formik'; +// import {FormattedMessage, useIntl} from 'react-intl'; +import {EditFormDefinition} from '@/registry/types'; + +/** + * Form to configure a Formio 'date' type component. + */ +const EditForm: EditFormDefinition = () => { + // const {errors} = useFormikContext(); + return <>henlo; +}; + +EditForm.defaultValues = { + // basic tab + label: '', + key: '', + description: '', + tooltip: '', + showInSummary: true, + showInEmail: false, + showInPDF: true, + multiple: false, + hidden: false, + clearOnHide: true, + isSensitiveData: false, + defaultValue: '', + disabled: false, + // Advanced tab + conditional: { + show: undefined, + when: '', + eq: '', + }, + // Validation tab + validate: { + required: false, + plugins: [], + }, + translatedErrors: {}, + // Defaults from https://github.com/formio/formio.js/blob/ + // bebc2ad73cad138a6de0a8247df47f0085a314cc/src/components/datetime/DateTime.js#L22 + datePicker: { + showWeeks: true, + startingDay: 0, + initDate: '', + minMode: 'day', + maxMode: 'year', + yearRows: 4, + yearColumns: 5, + minDate: null, + maxDate: null, + }, + openForms: { + translations: {}, + minDate: { + mode: '', + includeToday: null, + operator: 'add', + variable: 'now', + delta: { + years: null, + months: null, + days: null, + }, + }, + maxDate: { + mode: '', + includeToday: null, + operator: 'add', + variable: 'now', + delta: { + years: null, + months: null, + days: null, + }, + }, + }, + // Registration tab + registration: { + attribute: '', + }, + // Prefill tab + prefill: { + plugin: null, + attribute: null, + identifierRole: 'main', + }, +}; + +export default EditForm; diff --git a/src/registry/date/index.ts b/src/registry/date/index.ts new file mode 100644 index 00000000..f636dcf3 --- /dev/null +++ b/src/registry/date/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: '', +}; diff --git a/src/registry/date/preview.tsx b/src/registry/date/preview.tsx new file mode 100644 index 00000000..99b3518b --- /dev/null +++ b/src/registry/date/preview.tsx @@ -0,0 +1,11 @@ +import {DateComponentSchema} from '@open-formulieren/types'; + +import JSONPreview from '@/components/JSONPreview'; + +import {ComponentPreviewProps} from '../types'; + +const Preview: React.FC> = ({component}) => ( + +); + +export default Preview; diff --git a/src/registry/index.tsx b/src/registry/index.tsx index 27a7d9ad..b14f1d4d 100644 --- a/src/registry/index.tsx +++ b/src/registry/index.tsx @@ -1,5 +1,6 @@ import {AnyComponentSchema, FallbackSchema, hasOwnProperty} from '@/types'; +import DateField from './date'; import Email from './email'; import Fallback from './fallback'; import NumberField from './number'; @@ -24,6 +25,7 @@ const REGISTRY: Registry = { textfield: TextField, email: Email, number: NumberField, + date: DateField, }; export default REGISTRY; From 0e4a6c9e1ff4f17e6e3721d3f1e9a45d4ebe0dc5 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Thu, 31 Aug 2023 11:34:35 +0200 Subject: [PATCH 04/21] :art: await fireEvent does not make sense It's sync, so await doesn't hurt, but it's also not needed. --- src/components/ComponentConfiguration.stories.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ComponentConfiguration.stories.tsx b/src/components/ComponentConfiguration.stories.tsx index 58f605d3..4e3b06b2 100644 --- a/src/components/ComponentConfiguration.stories.tsx +++ b/src/components/ComponentConfiguration.stories.tsx @@ -252,7 +252,7 @@ export const Email: Story = { // 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'); - await fireEvent.change(keyInput, {target: {value: 'customKey'}}); + 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')); @@ -335,7 +335,7 @@ export const NumberField: Story = { // 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'); - await fireEvent.change(keyInput, {target: {value: 'customKey'}}); + 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')); From 09947234e8d513141f572e921b2ef9c63266b3bf Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Thu, 31 Aug 2023 12:08:25 +0200 Subject: [PATCH 05/21] :sparkles: [#31] Wire up common builder fields for date component --- src/components/builder/i18n.tsx | 3 + src/components/formio/index.ts | 1 + src/registry/date/edit.tsx | 136 +++++++++++++++++++++++++++++++- 3 files changed, 136 insertions(+), 4 deletions(-) diff --git a/src/components/builder/i18n.tsx b/src/components/builder/i18n.tsx index b67753e6..233adb88 100644 --- a/src/components/builder/i18n.tsx +++ b/src/components/builder/i18n.tsx @@ -37,6 +37,9 @@ function getEmptyTranslationsObject( * * useManageTranslations(['label', 'description', 'defaultValue']); * + * @todo: only consider field names where the value is a string -> translating defaultValue + * number makes no sense + * * @param forProperties Array of form field names to monitor for translation. */ export function useManageTranslations( diff --git a/src/components/formio/index.ts b/src/components/formio/index.ts index 3c780116..3cc93ba1 100644 --- a/src/components/formio/index.ts +++ b/src/components/formio/index.ts @@ -15,6 +15,7 @@ export {default as Description} from './description'; export {default as Tooltip} from './tooltip'; export {default as TextField} from './textfield'; export {default as Checkbox} from './checkbox'; +export {default as DateField} from './datefield'; export {default as Panel} from './panel'; export {default as Select} from './select'; export {default as NumberField} from './number'; diff --git a/src/registry/date/edit.tsx b/src/registry/date/edit.tsx index f60f760c..a6906fbe 100644 --- a/src/registry/date/edit.tsx +++ b/src/registry/date/edit.tsx @@ -1,15 +1,118 @@ import {DateComponentSchema} from '@open-formulieren/types'; +import {useFormikContext} from 'formik'; +import {FormattedMessage, useIntl} from 'react-intl'; -// 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 {DateField, TabList, TabPanel, Tabs} from '@/components/formio'; import {EditFormDefinition} from '@/registry/types'; +import {getErrorNames} from '@/utils/errors'; /** * Form to configure a Formio 'date' type component. */ const EditForm: EditFormDefinition = () => { - // const {errors} = useFormikContext(); - return <>henlo; + const [isKeyManuallySetRef, generatedKey] = useDeriveComponentKey(); + const { + values: {multiple = false}, + 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)); + }; + + Translations.useManageTranslations(['label', 'description', 'tooltip']); + Validate.useManageValidatorsTranslations(['required']); + + return ( + + + + + + + + + + + {/* Basic tab */} + + + {/* Advanced tab */} + + + + {/* Validation tab */} + + + + {/* TODO advanced validation */} + + + {/* Registration tab */} + + + + {/* Prefill tab */} + + + + {/* Translations */} + + + + + ); }; EditForm.defaultValues = { @@ -89,4 +192,29 @@ EditForm.defaultValues = { }, }; +interface DefaultValueProps { + multiple: boolean; +} + +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; From 9b5827f7300002c3c57cbb9c2595fe33e2a7e774 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Thu, 31 Aug 2023 12:23:42 +0200 Subject: [PATCH 06/21] :sparkles: [#31] Implement date field component preview --- .../ComponentConfiguration.stories.tsx | 53 +++++++++++++++++++ src/registry/date/preview.tsx | 37 +++++++++++-- 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/components/ComponentConfiguration.stories.tsx b/src/components/ComponentConfiguration.stories.tsx index 4e3b06b2..712b4189 100644 --- a/src/components/ComponentConfiguration.stories.tsx +++ b/src/components/ComponentConfiguration.stories.tsx @@ -367,4 +367,57 @@ export const DateField: Story = { schema: {}, }, }, + + play: async ({canvasElement}) => { + const canvas = within(canvasElement); + + await expect(canvas.getByLabelText('Label')).toHaveValue('A date field'); + await waitFor(async () => { + await expect(canvas.getByLabelText('Property Name')).toHaveValue('aDateField'); + }); + 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('Updated preview label'); + await expect(previewInput).toHaveDisplayValue(''); + await expect(previewInput.type).toEqual('date'); + + // 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 + await userEvent.click(canvas.getByLabelText('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('input-defaultValue[0]'); + await expect(defaultInput0.type).toEqual('date'); + // userEvent.type does not reliably work with date input, and the native browser + // datepicker helps in enforcing only valid dates. + }, }; diff --git a/src/registry/date/preview.tsx b/src/registry/date/preview.tsx index 99b3518b..4c795002 100644 --- a/src/registry/date/preview.tsx +++ b/src/registry/date/preview.tsx @@ -1,11 +1,40 @@ import {DateComponentSchema} from '@open-formulieren/types'; -import JSONPreview from '@/components/JSONPreview'; +import {DateField} from '@/components/formio'; import {ComponentPreviewProps} from '../types'; -const Preview: React.FC> = ({component}) => ( - -); +/** + * Show a formio date 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, + placeholder, + tooltip, + validate = {}, + disabled = false, + multiple = false, + } = component; + const {required = false} = validate; + return ( + + ); +}; export default Preview; From 0f06e67b4fa558738d9c00949884945ed9ada5df Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Thu, 31 Aug 2023 12:56:11 +0200 Subject: [PATCH 07/21] :lipstick: Only apply input padding when error icon is visible --- package-lock.json | 197 ++++++++++++++++------------ package.json | 1 + src/components/formio/component.tsx | 12 +- src/components/formio/datefield.tsx | 5 - tsconfig.json | 1 + 5 files changed, 124 insertions(+), 92 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4b2ac390..a80c546c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.6.0", "license": "EUPL-1.2", "dependencies": { + "@emotion/css": "^11.11.2", "clsx": "^1.2.1", "formik": "^2.2.9", "lodash.camelcase": "^4.3.0", @@ -2528,21 +2529,21 @@ } }, "node_modules/@emotion/babel-plugin": { - "version": "11.10.6", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", - "integrity": "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/serialize": "^1.1.1", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", - "stylis": "4.1.3" + "stylis": "4.2.0" } }, "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": { @@ -2557,26 +2558,38 @@ } }, "node_modules/@emotion/cache": { - "version": "11.10.7", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.7.tgz", - "integrity": "sha512-VLl1/2D6LOjH57Y8Vem1RoZ9haWF4jesHDGiHtKozDQuBIkJm2gimVo0I02sWCuzZtVACeixTVB4jeE8qvCBoQ==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", "dependencies": { - "@emotion/memoize": "^0.8.0", - "@emotion/sheet": "^1.2.1", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", - "stylis": "4.1.3" + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/css": { + "version": "11.11.2", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.11.2.tgz", + "integrity": "sha512-VJxe1ucoMYMS7DkiMdC2T7PWNbrEI0a39YRiyDvK2qq4lXwjRbVP/z4lpG+odCsRzadlR+1ywwrTzhdm5HNdew==", + "dependencies": { + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1" } }, "node_modules/@emotion/hash": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" }, "node_modules/@emotion/memoize": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "node_modules/@emotion/react": { "version": "11.10.6", @@ -2602,26 +2615,26 @@ } }, "node_modules/@emotion/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", "dependencies": { - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/unitless": "^0.8.0", - "@emotion/utils": "^1.2.0", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", "csstype": "^3.0.2" } }, "node_modules/@emotion/sheet": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", - "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" }, "node_modules/@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.0", @@ -2632,14 +2645,14 @@ } }, "node_modules/@emotion/utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" }, "node_modules/@emotion/weak-memoize": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, "node_modules/@esbuild/android-arm": { "version": "0.18.20", @@ -29372,9 +29385,9 @@ } }, "node_modules/stylis": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", - "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, "node_modules/supports-color": { "version": "5.5.0", @@ -32780,21 +32793,21 @@ "dev": true }, "@emotion/babel-plugin": { - "version": "11.10.6", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz", - "integrity": "sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", "requires": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/serialize": "^1.1.1", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", - "stylis": "4.1.3" + "stylis": "4.2.0" }, "dependencies": { "escape-string-regexp": { @@ -32805,26 +32818,38 @@ } }, "@emotion/cache": { - "version": "11.10.7", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.7.tgz", - "integrity": "sha512-VLl1/2D6LOjH57Y8Vem1RoZ9haWF4jesHDGiHtKozDQuBIkJm2gimVo0I02sWCuzZtVACeixTVB4jeE8qvCBoQ==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", "requires": { - "@emotion/memoize": "^0.8.0", - "@emotion/sheet": "^1.2.1", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", - "stylis": "4.1.3" + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "@emotion/css": { + "version": "11.11.2", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.11.2.tgz", + "integrity": "sha512-VJxe1ucoMYMS7DkiMdC2T7PWNbrEI0a39YRiyDvK2qq4lXwjRbVP/z4lpG+odCsRzadlR+1ywwrTzhdm5HNdew==", + "requires": { + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1" } }, "@emotion/hash": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" }, "@emotion/memoize": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "@emotion/react": { "version": "11.10.6", @@ -32842,26 +32867,26 @@ } }, "@emotion/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", "requires": { - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/unitless": "^0.8.0", - "@emotion/utils": "^1.2.0", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", "csstype": "^3.0.2" } }, "@emotion/sheet": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", - "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" }, "@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, "@emotion/use-insertion-effect-with-fallbacks": { "version": "1.0.0", @@ -32869,14 +32894,14 @@ "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==" }, "@emotion/utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" }, "@emotion/weak-memoize": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" }, "@esbuild/android-arm": { "version": "0.18.20", @@ -52747,9 +52772,9 @@ "dev": true }, "stylis": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", - "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, "supports-color": { "version": "5.5.0", diff --git a/package.json b/package.json index d949ae19..b3636f74 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "typescript": "^4.9.5" }, "dependencies": { + "@emotion/css": "^11.11.2", "clsx": "^1.2.1", "formik": "^2.2.9", "lodash.camelcase": "^4.3.0", diff --git a/src/components/formio/component.tsx b/src/components/formio/component.tsx index 1e40977b..5bcf46a6 100644 --- a/src/components/formio/component.tsx +++ b/src/components/formio/component.tsx @@ -1,3 +1,4 @@ +import {css} from '@emotion/css'; import clsx from 'clsx'; import React from 'react'; @@ -18,6 +19,15 @@ export interface ComponentProps { 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 Component: React.FC = ({ type, field = '', @@ -28,7 +38,7 @@ const Component: React.FC = ({ ...props }) => { const {errors} = useValidationErrors(field); - const className = clsx('form-group', 'has-feedback', 'formio-component', { + const className = clsx('form-group', 'has-feedback', 'formio-component', PAD_ERROR_ICON, { [`formio-component-${type}`]: type, 'has-error': field && errors.length > 0, required: required, diff --git a/src/components/formio/datefield.tsx b/src/components/formio/datefield.tsx index 78f69815..c1c71f2e 100644 --- a/src/components/formio/datefield.tsx +++ b/src/components/formio/datefield.tsx @@ -41,11 +41,6 @@ export const DateField: React.FC ); diff --git a/tsconfig.json b/tsconfig.json index ac56c2c8..957327d1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "target": "es2017", "lib": ["dom"], "jsx": "react-jsx", + "jsxImportSource": "@emotion/react", "declaration": true, "moduleResolution": "node", "noUnusedLocals": true, From c3e8aa9f642493d02ba526c465e63f8e26fff546 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Thu, 31 Aug 2023 15:33:27 +0200 Subject: [PATCH 08/21] :arrow_up: [#31] Bump to more precise type definitions for date constraint validation --- package-lock.json | 14 +++++++------- package.json | 2 +- src/types/index.ts | 19 +++++++++++++++++++ src/types/schemas.ts | 23 +---------------------- 4 files changed, 28 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index a80c546c..3621530a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "@formatjs/cli": "^6.1.1", "@formatjs/ts-transformer": "^3.12.0", "@fortawesome/fontawesome-free": "^6.4.0", - "@open-formulieren/types": "^0.8.0", + "@open-formulieren/types": "^0.9.0", "@storybook/addon-actions": "^7.3.2", "@storybook/addon-essentials": "^7.3.2", "@storybook/addon-interactions": "^7.3.2", @@ -5870,9 +5870,9 @@ } }, "node_modules/@open-formulieren/types": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@open-formulieren/types/-/types-0.8.0.tgz", - "integrity": "sha512-05hmkoA1AykPezChN5fWqUqZFYXFUG7ljuV0Oido/bD3ylBIcfyQlhmLENRdC0jrKgeY/WAvKLcksDXNMbQ8GQ==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@open-formulieren/types/-/types-0.9.0.tgz", + "integrity": "sha512-zYsktHug8lLJNBa+JBlGKhLLif1d+R0D67kl1ZZg4dh/Ma4qYwppVjYCZZopre+ox5i53MeZwj2aUdCWrVE0kQ==", "dev": true }, "node_modules/@pkgjs/parseargs": { @@ -35284,9 +35284,9 @@ } }, "@open-formulieren/types": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@open-formulieren/types/-/types-0.8.0.tgz", - "integrity": "sha512-05hmkoA1AykPezChN5fWqUqZFYXFUG7ljuV0Oido/bD3ylBIcfyQlhmLENRdC0jrKgeY/WAvKLcksDXNMbQ8GQ==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@open-formulieren/types/-/types-0.9.0.tgz", + "integrity": "sha512-zYsktHug8lLJNBa+JBlGKhLLif1d+R0D67kl1ZZg4dh/Ma4qYwppVjYCZZopre+ox5i53MeZwj2aUdCWrVE0kQ==", "dev": true }, "@pkgjs/parseargs": { diff --git a/package.json b/package.json index b3636f74..213acc4b 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.8.0", + "@open-formulieren/types": "^0.9.0", "@storybook/addon-actions": "^7.3.2", "@storybook/addon-essentials": "^7.3.2", "@storybook/addon-interactions": "^7.3.2", diff --git a/src/types/index.ts b/src/types/index.ts index 753ac6e7..9e1f42bb 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -7,3 +7,22 @@ export function hasOwnProperty( ): obj is X & Record { return obj.hasOwnProperty(prop); } + +/** + * Filter a given type by a given expected value type. + * + * E.g. with a type: + * + * interface Test { + * foo: string; + * bar: string; + * baz: number; + * }; + * + * FilterByValueType == {foo: string, bar: string} + * + * See https://stackoverflow.com/a/63990350 + */ +export type FilterByValueType = { + [P in keyof T as T[P] extends VT ? P : never]: T[P]; +}; diff --git a/src/types/schemas.ts b/src/types/schemas.ts index d75b5549..4f4e377e 100644 --- a/src/types/schemas.ts +++ b/src/types/schemas.ts @@ -1,25 +1,4 @@ /** * Open Forms specific Formio component schema extensions. */ -import { - ContentSchema, - DateComponentSchema, - EmailComponentSchema, - NumberComponentSchema, - TextFieldComponentSchema, -} from '@open-formulieren/types'; - -// A fallback, minimal schema to handle the cases where component.type is not in our -// registry. -export interface FallbackSchema { - type?: string; -} - -export type AnyComponentSchema = - // inputs - | TextFieldComponentSchema - | EmailComponentSchema - | DateComponentSchema - | NumberComponentSchema - // layout - | ContentSchema; +export type {AnyComponentSchema, FallbackSchema} from '@open-formulieren/types'; From ef158e66b0cabe1c4e21293c8d4b9a559bd04f58 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Thu, 31 Aug 2023 16:49:14 +0200 Subject: [PATCH 09/21] :sparkles: [#31] min/max date validation panels: mode selection --- src/registry/date/edit.tsx | 29 ++------ src/registry/date/validation.tsx | 120 +++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 23 deletions(-) create mode 100644 src/registry/date/validation.tsx diff --git a/src/registry/date/edit.tsx b/src/registry/date/edit.tsx index a6906fbe..656227c4 100644 --- a/src/registry/date/edit.tsx +++ b/src/registry/date/edit.tsx @@ -25,6 +25,8 @@ import {DateField, TabList, TabPanel, Tabs} from '@/components/formio'; import {EditFormDefinition} from '@/registry/types'; import {getErrorNames} from '@/utils/errors'; +import DateConstraintValidation from './validation'; + /** * Form to configure a Formio 'date' type component. */ @@ -96,7 +98,8 @@ const EditForm: EditFormDefinition = () => { - {/* TODO advanced validation */} + + {/* Registration tab */} @@ -157,28 +160,8 @@ EditForm.defaultValues = { }, openForms: { translations: {}, - minDate: { - mode: '', - includeToday: null, - operator: 'add', - variable: 'now', - delta: { - years: null, - months: null, - days: null, - }, - }, - maxDate: { - mode: '', - includeToday: null, - operator: 'add', - variable: 'now', - delta: { - years: null, - months: null, - days: null, - }, - }, + minDate: {mode: ''}, + maxDate: {mode: ''}, }, // Registration tab registration: { diff --git a/src/registry/date/validation.tsx b/src/registry/date/validation.tsx new file mode 100644 index 00000000..8d1bba46 --- /dev/null +++ b/src/registry/date/validation.tsx @@ -0,0 +1,120 @@ +import {DateComponentSchema} from '@open-formulieren/types'; +import {DateConstraintConfiguration} from '@open-formulieren/types/lib/formio/dates'; +import {useFormikContext} from 'formik'; +import { + FormattedMessage, + MessageDescriptor, + defineMessage, + defineMessages, + useIntl, +} from 'react-intl'; + +import {Panel, Select} from '@/components/formio'; +import {FilterByValueType} from '@/types'; + +type AllDateExtensions = Required>; +type AllModes = DateConstraintConfiguration['mode']; +type NonEmptyModes = Exclude; +type DateConstraintKey = keyof FilterByValueType; + +// Mappings of value-label to produce dropdown options. Note that you need to filter out +// only the options relevant to the particular field. +const ALL_DATE_CONSTRAINT_MODE_OPTIONS: Array<{ + label: MessageDescriptor; + value: NonEmptyModes; +}> = [ + { + value: 'fixedValue', + label: defineMessage({ + description: "Date constraint mode 'fixedValue' label", + defaultMessage: 'Fixed value', + }), + }, + { + value: 'future', + label: defineMessage({ + description: "Date constraint mode 'future' label", + defaultMessage: 'In the future', + }), + }, + { + value: 'past', + label: defineMessage({ + description: "Date constraint mode 'past' label", + defaultMessage: 'In the past', + }), + }, + { + value: 'relativeToVariable', + label: defineMessage({ + description: "Date constraint mode 'relativeToVariable' label", + defaultMessage: 'Relative to variable', + }), + }, +]; + +interface DateConstraintProps { + constraint: DateConstraintKey; +} + +const MODES_TO_EXCLUDE: Record = { + minDate: ['past'], + maxDate: ['future'], +}; + +const ModeSelect: React.FC = ({constraint}) => { + const intl = useIntl(); + + // filter out the validation modes not relevant for this particular constraint + const modesToExclude = MODES_TO_EXCLUDE[constraint]; + const options = ALL_DATE_CONSTRAINT_MODE_OPTIONS.filter( + opt => !modesToExclude.includes(opt.value) + ); + + return ( + = ({constraint}) => { ...item, label: intl.formatMessage(item.label), }))} + isClearable + onChange={event => { + // the select *always* sets the selected value in the state, we can just use an + // additional onChange here (as better alternative to useEffect). + // The idea here is that we set the default, empty values that are mode-specific + // so that we don't get "uncontrolled-component-becomes-controlled" warning + // from React. It also helps in clearing out any left-over state from other + // modes when switching between them. + const mode: AllModes = event.target.value ?? ''; + const emptyDefaults = {mode, ...DEFAULT_VALUES[mode]}; + setFieldValue(`openForms.${constraint}`, emptyDefaults); + }} /> ); }; @@ -103,6 +138,9 @@ const DateConstraintValidation: React.FC = ({constraint}) = const intl = useIntl(); const {values} = useFormikContext(); const hasMode = !!values?.openForms?.[constraint]?.mode; + + console.log(values?.openForms?.[constraint]); + return ( Date: Fri, 1 Sep 2023 09:17:01 +0200 Subject: [PATCH 12/21] :sparkles: [#31] Enable showing date picker for fixedValue mode The fixed value date field/date picker is shown only when this mode is set. Without mode selected, no additional fields may be shown. --- src/components/formio/datefield.tsx | 6 ++- .../date/validation-dateconstraint.stories.ts | 48 +++++++++++++++++ src/registry/date/validation.tsx | 53 ++++++++++++++++--- 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/src/components/formio/datefield.tsx b/src/components/formio/datefield.tsx index c1c71f2e..35231b8d 100644 --- a/src/components/formio/datefield.tsx +++ b/src/components/formio/datefield.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import {Field} from 'formik'; +import {Field, useFormikContext} from 'formik'; import {useContext} from 'react'; import {RenderContext} from '@/context'; @@ -25,11 +25,14 @@ export const DateField: React.FC { + const {getFieldProps} = useFormikContext(); const {bareInput} = useContext(RenderContext); const {errors, hasErrors} = useValidationErrors(name); const htmlId = `editform-${name}`; + const {value} = getFieldProps(name); + // let's not bother with date pickers - use the native browser date input instead. const inputField = ( ); diff --git a/src/registry/date/validation-dateconstraint.stories.ts b/src/registry/date/validation-dateconstraint.stories.ts index dd777410..61059d6d 100644 --- a/src/registry/date/validation-dateconstraint.stories.ts +++ b/src/registry/date/validation-dateconstraint.stories.ts @@ -1,4 +1,6 @@ +import {expect} from '@storybook/jest'; import {Meta, StoryObj} from '@storybook/react'; +import {within} from '@storybook/testing-library'; import {withFormik} from '@/sb-decorators'; @@ -16,6 +18,17 @@ export default { minDate: {mode: ''}, maxDate: {mode: ''}, }, + datePicker: { + showWeeks: true, + startingDay: 0, + initDate: '', + minMode: 'day', + maxMode: 'year', + yearRows: 4, + yearColumns: 5, + minDate: null, + maxDate: null, + }, }, }, }, @@ -27,3 +40,38 @@ export default { type Story = StoryObj; export const Default: Story = {}; + +export const FixedValue: Story = { + parameters: { + formik: { + initialValues: { + openForms: { + minDate: { + mode: 'fixedValue', + }, + maxDate: {mode: ''}, + }, + datePicker: { + showWeeks: true, + startingDay: 0, + initDate: '', + minMode: 'day', + maxMode: 'year', + yearRows: 4, + yearColumns: 5, + minDate: '2023-01-01', + maxDate: null, + }, + }, + }, + }, + + play: async ({canvasElement}) => { + const canvas = within(canvasElement); + + expect(await canvas.findByText('Fixed value')).toBeVisible(); + const datefield = await canvas.findByLabelText('Minimum date'); + expect(datefield).toBeVisible(); + expect(datefield).toHaveDisplayValue('2023-01-01'); + }, +}; diff --git a/src/registry/date/validation.tsx b/src/registry/date/validation.tsx index 7401235b..52e3fa85 100644 --- a/src/registry/date/validation.tsx +++ b/src/registry/date/validation.tsx @@ -9,7 +9,7 @@ import { useIntl, } from 'react-intl'; -import {Panel, Select} from '@/components/formio'; +import {DateField, Panel, Select} from '@/components/formio'; import {FilterByValueType} from '@/types'; // A bunch of derived types from the DateComponentSchema that makes working with the @@ -118,11 +118,53 @@ const ModeSelect: React.FC = ({constraint}) => { const mode: AllModes = event.target.value ?? ''; const emptyDefaults = {mode, ...DEFAULT_VALUES[mode]}; setFieldValue(`openForms.${constraint}`, emptyDefaults); + // whenever the mode *changes*, clear the datePicker fixed value to prevent + // stale values from being in the form state while visually hidden + setFieldValue(`datePicker.${constraint}`, null); }} /> ); }; +type FixedValueTranslations = { + label: MessageDescriptor; + tooltip: MessageDescriptor; +}; + +const FIXED_VALUE_MESSAGES: Record = { + minDate: defineMessages({ + label: { + description: "Date field 'minDate' fixed value label", + defaultMessage: 'Minimum date', + }, + tooltip: { + description: "Date field 'minDate' fixed value tooltip", + defaultMessage: 'The minimum date that can be picked.', + }, + }), + maxDate: defineMessages({ + label: { + description: "Date field 'maxDate' fixed value label", + defaultMessage: 'Maximum date', + }, + tooltip: { + description: "Date field 'maxDate' fixed value tooltip", + defaultMessage: 'The maximum date that can be picked.', + }, + }), +}; + +const FixedValueDateField: React.FC = ({constraint}) => { + const intl = useIntl(); + return ( + + ); +}; + const PANEL_TITLES: Record = defineMessages({ minDate: { description: "Date field 'minDate' validation panel title", @@ -137,20 +179,19 @@ const PANEL_TITLES: Record = defineMessage const DateConstraintValidation: React.FC = ({constraint}) => { const intl = useIntl(); const {values} = useFormikContext(); - const hasMode = !!values?.openForms?.[constraint]?.mode; - - console.log(values?.openForms?.[constraint]); - + const mode = values?.openForms?.[constraint]?.mode || ''; + console.log(values); return ( + {mode === 'fixedValue' && } ); }; From d97574b2ef365d9d4d5a2942a6e9abc350b62d35 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 1 Sep 2023 10:00:03 +0200 Subject: [PATCH 13/21] :sparkles: [#31] Implement future/past mode with includeToday checkbox --- .../date/validation-dateconstraint.stories.ts | 36 +++++++++++++++++++ src/registry/date/validation.tsx | 23 +++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/registry/date/validation-dateconstraint.stories.ts b/src/registry/date/validation-dateconstraint.stories.ts index 61059d6d..36f80bab 100644 --- a/src/registry/date/validation-dateconstraint.stories.ts +++ b/src/registry/date/validation-dateconstraint.stories.ts @@ -75,3 +75,39 @@ export const FixedValue: Story = { expect(datefield).toHaveDisplayValue('2023-01-01'); }, }; + +export const FutureOrPastIncludingToday: Story = { + parameters: { + formik: { + initialValues: { + openForms: { + minDate: { + mode: 'future', + includeToday: true, + }, + maxDate: {mode: ''}, + }, + datePicker: { + showWeeks: true, + startingDay: 0, + initDate: '', + minMode: 'day', + maxMode: 'year', + yearRows: 4, + yearColumns: 5, + minDate: '2023-01-01', + maxDate: null, + }, + }, + }, + }, + + play: async ({canvasElement}) => { + const canvas = within(canvasElement); + + expect(await canvas.findByText('In the future')).toBeVisible(); + const checkbox = await canvas.findByLabelText('Including today'); + expect(checkbox).toBeVisible(); + expect(checkbox).toBeChecked(); + }, +}; diff --git a/src/registry/date/validation.tsx b/src/registry/date/validation.tsx index 52e3fa85..988826ab 100644 --- a/src/registry/date/validation.tsx +++ b/src/registry/date/validation.tsx @@ -9,7 +9,7 @@ import { useIntl, } from 'react-intl'; -import {DateField, Panel, Select} from '@/components/formio'; +import {Checkbox, DateField, Panel, Select} from '@/components/formio'; import {FilterByValueType} from '@/types'; // A bunch of derived types from the DateComponentSchema that makes working with the @@ -165,6 +165,26 @@ const FixedValueDateField: React.FC = ({constraint}) => { ); }; +const IncludeToday: React.FC = ({constraint}) => { + const intl = useIntl(); + const tooltip = intl.formatMessage({ + description: "Tooltip for 'date constraint includeToday' builder validation field", + defaultMessage: 'If checked, the current day is an allowed value.', + }); + return ( + + } + tooltip={tooltip} + /> + ); +}; + const PANEL_TITLES: Record = defineMessages({ minDate: { description: "Date field 'minDate' validation panel title", @@ -192,6 +212,7 @@ const DateConstraintValidation: React.FC = ({constraint}) = > {mode === 'fixedValue' && } + {['future', 'past'].includes(mode) && } ); }; From 4313529735d586dc8ebce202bce1ab14784cbc7c Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Fri, 1 Sep 2023 10:53:55 +0200 Subject: [PATCH 14/21] :sparkles: [#31] Implement configuration for relativeToVariable mode --- .../date/validation-dateconstraint.stories.ts | 32 +++++++++++- src/registry/date/validation.tsx | 52 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/registry/date/validation-dateconstraint.stories.ts b/src/registry/date/validation-dateconstraint.stories.ts index 36f80bab..fadca63d 100644 --- a/src/registry/date/validation-dateconstraint.stories.ts +++ b/src/registry/date/validation-dateconstraint.stories.ts @@ -39,7 +39,7 @@ export default { type Story = StoryObj; -export const Default: Story = {}; +export const InitialState: Story = {}; export const FixedValue: Story = { parameters: { @@ -111,3 +111,33 @@ export const FutureOrPastIncludingToday: Story = { expect(checkbox).toBeChecked(); }, }; + +export const RelativeToVariable: Story = { + parameters: { + formik: { + initialValues: { + openForms: { + minDate: { + mode: 'relativeToVariable', + delta: { + years: null, + months: null, + days: null, + }, + operator: 'add', + }, + maxDate: {mode: ''}, + }, + }, + }, + }, + + play: async ({canvasElement}) => { + const canvas = within(canvasElement); + + expect(await canvas.findByText('Relative to variable')).toBeVisible(); + const operator = await canvas.findByLabelText('Add/subtract duration'); + expect(operator).toBeVisible(); + expect(await canvas.findByText('Add')).toBeVisible(); + }, +}; diff --git a/src/registry/date/validation.tsx b/src/registry/date/validation.tsx index 988826ab..c39db173 100644 --- a/src/registry/date/validation.tsx +++ b/src/registry/date/validation.tsx @@ -185,6 +185,57 @@ const IncludeToday: React.FC = ({constraint}) => { ); }; +export interface OperatorSelectProps { + name: `openForms.${DateConstraintKey}.operator`; +} + +const OperatorSelect: React.FC = ({name}) => { + const intl = useIntl(); + const tooltip = intl.formatMessage({ + description: "Tooltip for 'operator' in relative delta date constraint validation", + defaultMessage: 'Specify whether to add or subtract a time delta to/from the variable.', + }); + return ( + - } - options={options.map(item => ({ - ...item, - label: intl.formatMessage(item.label), - }))} - isClearable - onChange={event => { - // the select *always* sets the selected value in the state, we can just use an - // additional onChange here (as better alternative to useEffect). - // The idea here is that we set the default, empty values that are mode-specific - // so that we don't get "uncontrolled-component-becomes-controlled" warning - // from React. It also helps in clearing out any left-over state from other - // modes when switching between them. - const mode: AllModes = event.target.value ?? ''; - const emptyDefaults = {mode, ...DEFAULT_VALUES[mode]}; - setFieldValue(`openForms.${constraint}`, emptyDefaults); - // whenever the mode *changes*, clear the datePicker fixed value to prevent - // stale values from being in the form state while visually hidden - setFieldValue(`datePicker.${constraint}`, null); - }} - /> - ); -}; - -type FixedValueTranslations = { - label: MessageDescriptor; - tooltip: MessageDescriptor; -}; - -const FIXED_VALUE_MESSAGES: Record = { - minDate: defineMessages({ - label: { - description: "Date field 'minDate' fixed value label", - defaultMessage: 'Minimum date', - }, - tooltip: { - description: "Date field 'minDate' fixed value tooltip", - defaultMessage: 'The minimum date that can be picked.', - }, - }), - maxDate: defineMessages({ - label: { - description: "Date field 'maxDate' fixed value label", - defaultMessage: 'Maximum date', - }, - tooltip: { - description: "Date field 'maxDate' fixed value tooltip", - defaultMessage: 'The maximum date that can be picked.', - }, - }), -}; - -const FixedValueDateField: React.FC = ({constraint}) => { - const intl = useIntl(); - return ( - - ); -}; - -const IncludeToday: React.FC = ({constraint}) => { - const intl = useIntl(); - const tooltip = intl.formatMessage({ - description: "Tooltip for 'date constraint includeToday' builder validation field", - defaultMessage: 'If checked, the current day is an allowed value.', - }); - return ( - - } - tooltip={tooltip} - /> - ); -}; - -export interface OperatorSelectProps { - name: `openForms.${DateConstraintKey}.operator`; -} - -const OperatorSelect: React.FC = ({name}) => { - const intl = useIntl(); - const tooltip = intl.formatMessage({ - description: "Tooltip for 'operator' in relative delta date constraint validation", - defaultMessage: 'Specify whether to add or subtract a time delta to/from the variable.', - }); - return ( - + } + options={options.map(item => ({ + ...item, + label: intl.formatMessage(item.label), + }))} + isClearable + onChange={event => { + // the select *always* sets the selected value in the state, we can just use an + // additional onChange here (as better alternative to useEffect). + // The idea here is that we set the default, empty values that are mode-specific + // so that we don't get "uncontrolled-component-becomes-controlled" warning + // from React. It also helps in clearing out any left-over state from other + // modes when switching between them. + const mode: AllModes = event.target.value ?? ''; + const emptyDefaults = {mode, ...DEFAULT_VALUES[mode]}; + setFieldValue(`openForms.${constraint}`, emptyDefaults); + // whenever the mode *changes*, clear the datePicker fixed value to prevent + // stale values from being in the form state while visually hidden + setFieldValue(`datePicker.${constraint}`, null); + }} + /> + ); +}; + +export default ModeSelect; diff --git a/src/registry/date/validation/fixed-value-datefield.tsx b/src/registry/date/validation/fixed-value-datefield.tsx new file mode 100644 index 00000000..3043f02d --- /dev/null +++ b/src/registry/date/validation/fixed-value-datefield.tsx @@ -0,0 +1,50 @@ +import {MessageDescriptor, defineMessages, useIntl} from 'react-intl'; + +import {DateField} from '@/components/formio'; + +import {DateConstraintKey} from './types'; + +type FixedValueTranslations = { + label: MessageDescriptor; + tooltip: MessageDescriptor; +}; + +const FIXED_VALUE_MESSAGES: Record = { + minDate: defineMessages({ + label: { + description: "Date field 'minDate' fixed value label", + defaultMessage: 'Minimum date', + }, + tooltip: { + description: "Date field 'minDate' fixed value tooltip", + defaultMessage: 'The minimum date that can be picked.', + }, + }), + maxDate: defineMessages({ + label: { + description: "Date field 'maxDate' fixed value label", + defaultMessage: 'Maximum date', + }, + tooltip: { + description: "Date field 'maxDate' fixed value tooltip", + defaultMessage: 'The maximum date that can be picked.', + }, + }), +}; + +export interface FixedValueDateFieldProps { + constraint: DateConstraintKey; +} + +const FixedValueDateField: React.FC = ({constraint}) => { + const intl = useIntl(); + return ( + + ); +}; + +export default FixedValueDateField; diff --git a/src/registry/date/validation/include-today.tsx b/src/registry/date/validation/include-today.tsx new file mode 100644 index 00000000..8d14ed7b --- /dev/null +++ b/src/registry/date/validation/include-today.tsx @@ -0,0 +1,31 @@ +import {FormattedMessage, useIntl} from 'react-intl'; + +import {Checkbox} from '@/components/formio'; + +import {DateConstraintKey} from './types'; + +export interface IncludeTodayProps { + constraint: DateConstraintKey; +} + +const IncludeToday: React.FC = ({constraint}) => { + const intl = useIntl(); + const tooltip = intl.formatMessage({ + description: "Tooltip for 'date constraint includeToday' builder validation field", + defaultMessage: 'If checked, the current day is an allowed value.', + }); + return ( + + } + tooltip={tooltip} + /> + ); +}; + +export default IncludeToday; diff --git a/src/registry/date/validation/index.tsx b/src/registry/date/validation/index.tsx new file mode 100644 index 00000000..1d9fc4f7 --- /dev/null +++ b/src/registry/date/validation/index.tsx @@ -0,0 +1,47 @@ +import {DateComponentSchema} from '@open-formulieren/types'; +import {useFormikContext} from 'formik'; +import {MessageDescriptor, defineMessages, useIntl} from 'react-intl'; + +import {Panel} from '@/components/formio'; + +import ModeSelect from './constraint-mode'; +import FixedValueDateField from './fixed-value-datefield'; +import IncludeToday from './include-today'; +import RelativeDelta from './relative-delta'; +import {DateConstraintKey} from './types'; + +const PANEL_TITLES: Record = defineMessages({ + minDate: { + description: "Date field 'minDate' validation panel title", + defaultMessage: 'Minimum date {configured, select, false {(not set)} other {}}', + }, + maxDate: { + description: "Date field 'maxDate' validation panel title", + defaultMessage: 'Maximum date {configured, select, false {(not set)} other {}}', + }, +}); + +export interface DateConstraintProps { + constraint: DateConstraintKey; +} + +const DateConstraintValidation: React.FC = ({constraint}) => { + const intl = useIntl(); + const {values} = useFormikContext(); + const mode = values?.openForms?.[constraint]?.mode || ''; + console.log(values); + return ( + + + {mode === 'fixedValue' && } + {['future', 'past'].includes(mode) && } + {mode === 'relativeToVariable' && } + + ); +}; + +export default DateConstraintValidation; diff --git a/src/registry/date/validation/relative-delta.tsx b/src/registry/date/validation/relative-delta.tsx new file mode 100644 index 00000000..b4e7548a --- /dev/null +++ b/src/registry/date/validation/relative-delta.tsx @@ -0,0 +1,62 @@ +import {FormattedMessage, useIntl} from 'react-intl'; + +import {Select} from '@/components/formio'; + +import {DateConstraintKey} from './types'; + +export interface OperatorSelectProps { + name: `openForms.${DateConstraintKey}.operator`; +} + +const OperatorSelect: React.FC = ({name}) => { + const intl = useIntl(); + const tooltip = intl.formatMessage({ + description: "Tooltip for 'operator' in relative delta date constraint validation", + defaultMessage: 'Specify whether to add or subtract a time delta to/from the variable.', + }); + return ( +