From 08aaef7a1b83d279da0c39f57181ab72c4804bbc Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 14 Oct 2024 11:42:02 +0200 Subject: [PATCH] :sparkles: [open-formulieren/open-forms#4546] Implement new formio component to display soft required validation errors The component is similar to the content component, but needs to be its own type so that we can process it in the SDK and find a target to display the form validation errors. The layout/warning scaffolding will also be fixed, and it has no meaning in the backend data, so a number of configuration options from the content component are not available (such as conditional logic, hidden/visible, display configuration, custom class). --- .../ComponentConfiguration.stories.tsx | 60 ++++++++- src/registry/index.ts | 2 + .../softRequiredErrors/edit-validation.ts | 9 ++ src/registry/softRequiredErrors/edit.tsx | 122 ++++++++++++++++++ src/registry/softRequiredErrors/index.ts | 14 ++ .../softRequiredErrors.stories.ts | 84 ++++++++++++ 6 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 src/registry/softRequiredErrors/edit-validation.ts create mode 100644 src/registry/softRequiredErrors/edit.tsx create mode 100644 src/registry/softRequiredErrors/index.ts create mode 100644 src/registry/softRequiredErrors/softRequiredErrors.stories.ts diff --git a/src/components/ComponentConfiguration.stories.tsx b/src/components/ComponentConfiguration.stories.tsx index 22ae3e3f..5d87405f 100644 --- a/src/components/ComponentConfiguration.stories.tsx +++ b/src/components/ComponentConfiguration.stories.tsx @@ -1,4 +1,8 @@ -import {ContentComponentSchema, SupportedLocales} from '@open-formulieren/types'; +import { + ContentComponentSchema, + SoftRequiredErrorsComponentSchema, + SupportedLocales, +} from '@open-formulieren/types'; import {Meta, StoryFn, StoryObj} from '@storybook/react'; import {expect, fireEvent, fn, userEvent, waitFor, within} from '@storybook/test'; import React from 'react'; @@ -3025,3 +3029,57 @@ export const Content: Story = { }); }, }; + +export const SoftRequiredErrors: Story = { + render: Template, + name: 'type: softRequiredErrors', + + args: { + component: { + id: 'wekruya', + type: 'softRequiredErrors', + key: 'softRequiredErrors', + html: '

Niet alle velden zijn ingevuld.

\n{{ missingFields }}', + openForms: { + translations: { + nl: { + html: '

Niet alle velden zijn ingevuld.

\n{{ missingFields }}', + }, + }, + }, + }, + + builderInfo: { + title: 'Soft required errors', + icon: 'html5', + group: 'layout', + weight: 10, + schema: {}, + }, + }, + + play: async ({canvasElement, step, args}) => { + const canvas = within(canvasElement); + + await step('Submit form', async () => { + await userEvent.click(canvas.getByRole('button', {name: 'Save'})); + expect(args.onSubmit).toHaveBeenCalledWith({ + id: 'wekruya', + type: 'softRequiredErrors', + label: '', + html: '

Niet alle velden zijn ingevuld.

\n{{ missingFields }}', + openForms: { + translations: { + nl: { + html: '

Niet alle velden zijn ingevuld.

\n{{ missingFields }}', + }, + en: { + html: '', + }, + }, + }, + key: 'softRequiredErrors', + } satisfies SoftRequiredErrorsComponentSchema); + }); + }, +}; diff --git a/src/registry/index.ts b/src/registry/index.ts index ea142781..919345b9 100644 --- a/src/registry/index.ts +++ b/src/registry/index.ts @@ -29,6 +29,7 @@ import Radio from './radio'; import Select from './select'; import Selectboxes from './selectboxes'; import Signature from './signature'; +import SoftRequiredErrors from './softRequiredErrors'; import Textarea from './textarea'; import TextField from './textfield'; import TimeField from './time'; @@ -84,6 +85,7 @@ const REGISTRY: Registry = { content: Content, columns: Columns, fieldset: FieldSet, + softRequiredErrors: SoftRequiredErrors, // deprecated coSign: CosignV1, password: Password, diff --git a/src/registry/softRequiredErrors/edit-validation.ts b/src/registry/softRequiredErrors/edit-validation.ts new file mode 100644 index 00000000..607568cb --- /dev/null +++ b/src/registry/softRequiredErrors/edit-validation.ts @@ -0,0 +1,9 @@ +import {z} from 'zod'; + +import {buildKeySchema} from '@/registry/validation'; + +import {EditSchema} from '../types'; + +const schema: EditSchema = ({intl}) => z.object({key: buildKeySchema(intl)}); + +export default schema; diff --git a/src/registry/softRequiredErrors/edit.tsx b/src/registry/softRequiredErrors/edit.tsx new file mode 100644 index 00000000..fe38d969 --- /dev/null +++ b/src/registry/softRequiredErrors/edit.tsx @@ -0,0 +1,122 @@ +import {JSONEditor} from '@open-formulieren/monaco-json-editor'; +import {SoftRequiredErrorsComponentSchema} from '@open-formulieren/types'; +import {useFormikContext} from 'formik'; +import React, {useContext, useEffect, useRef, useState} from 'react'; + +import PreviewModeToggle, {PreviewState} from '@/components/PreviewModeToggle'; +import {Key, RichText} from '@/components/builder'; +import {Tab, TabList, TabPanel, Tabs} from '@/components/formio'; +import {BuilderContext} from '@/context'; +import {useErrorChecker} from '@/utils/errors'; + +import {EditFormDefinition} from '../types'; + +/** + * Form to configure a Formio 'content' type component. + * + * W/r to translations, the 'NL' language is considered the default, and the main html + * value is populated from that field (TODO: implement this). + */ +const EditForm: EditFormDefinition = () => { + const {uniquifyKey, supportedLanguageCodes, theme} = useContext(BuilderContext); + const isKeyManuallySetRef = useRef(false); + const {values, setFieldValue, setValues} = useFormikContext(); + const generatedKey = uniquifyKey(values.key); + + const [previewMode, setPreviewMode] = useState('rich'); + + const defaultLanguageCode = supportedLanguageCodes[0] ?? 'nl'; + + // Synchronize the default/first language tab value to the main `html` field. + useEffect(() => { + const currentValue = values.openForms?.translations?.[defaultLanguageCode]?.html; + if (currentValue === undefined && values.html) { + // if we have a 'html' value, but no default translation -> store the default translation + setFieldValue(`openForms.translations.${defaultLanguageCode}.html`, values.html); + } else if (values.html !== currentValue) { + // otherwise sync the value of the translation field to the main field. + setFieldValue('html', currentValue); + } + }); + + // the `html` property edits in the JSON edit don't behave as expected, you need to + // edit the language specific values, so remove it. + const {html, ...jsonEditValues} = values; + + return ( + <> +
+
+ +
+
+ {previewMode === 'JSON' ? ( + + ) : ( + <> + + + + )} +
+
+ + ); +}; + +const DEFAULT_CONTENT_NL: string = ` +

+ Je hebt niet alle velden ingevuld +
+ Het lijkt erop dat niet alle verplichte velden ingevuld zijn. Als je deze velden + niet van een antwoord voorziet, dan kan je aanvraag mogelijk niet worden behandeld. + En, als gevolg daarvan, krijg je ook de eventuele kosten die je hebt betaald voor + de aanvraag niet terug. Zorg er dus voor dat je alle verplichte velden invult. +

+

Dit zijn de velden die nog geen antwoord hebben:

+ + {{ missingFields }} + +

Wil je doorgaan met de aanvraag?

+`; + +EditForm.defaultValues = { + // Display tab + html: DEFAULT_CONTENT_NL, + label: '', + key: '', + openForms: { + translations: { + nl: { + html: DEFAULT_CONTENT_NL, + }, + en: { + html: '', + }, + }, + }, +}; + +const RichTextTranslations: React.FC = () => { + const {supportedLanguageCodes} = useContext(BuilderContext); + const {hasAnyError} = useErrorChecker(); + return ( + + + {supportedLanguageCodes.map(code => ( + + {code.toUpperCase()} + + ))} + + + {supportedLanguageCodes.map((code, index) => ( + + + + ))} + + ); +}; + +export default EditForm; diff --git a/src/registry/softRequiredErrors/index.ts b/src/registry/softRequiredErrors/index.ts new file mode 100644 index 00000000..05b73e43 --- /dev/null +++ b/src/registry/softRequiredErrors/index.ts @@ -0,0 +1,14 @@ +import {SoftRequiredErrorsComponentSchema} from '@open-formulieren/types'; + +import {RegistryEntry} from '../types'; +import EditForm from './edit'; +import validationSchema from './edit-validation'; + +export default { + edit: EditForm, + editSchema: validationSchema, + preview: { + panel: null, + }, + defaultValue: undefined, // a softRequiredErrors component does not hold a value itself +} satisfies RegistryEntry; diff --git a/src/registry/softRequiredErrors/softRequiredErrors.stories.ts b/src/registry/softRequiredErrors/softRequiredErrors.stories.ts new file mode 100644 index 00000000..f7409b5a --- /dev/null +++ b/src/registry/softRequiredErrors/softRequiredErrors.stories.ts @@ -0,0 +1,84 @@ +import {SoftRequiredErrorsComponentSchema} from '@open-formulieren/types'; +import {Meta, StoryObj} from '@storybook/react'; +import {expect, fn, userEvent, waitFor, within} from '@storybook/test'; + +import ComponentEditForm from '@/components/ComponentEditForm'; +import {BuilderContextDecorator} from '@/sb-decorators'; + +export default { + title: 'Builder components/Soft required errors', + component: ComponentEditForm, + decorators: [BuilderContextDecorator], + parameters: { + builder: {enableContext: true}, + }, + args: { + isNew: true, + component: { + id: 'wekruya', + type: 'softRequiredErrors', + key: 'softRequiredErrors', + html: '', + openForms: { + translations: { + nl: { + html: '

Niet alle velden zijn ingevuld.

\n{{ missingFields }}', + }, + en: { + html: '

Not all fields have been filled.

\n{{ missingFields }}', + }, + }, + }, + }, + onCancel: fn(), + onRemove: fn(), + onSubmit: fn(), + + builderInfo: { + title: 'Soft required errors', + icon: 'html5', + group: 'layout', + weight: 10, + schema: {}, + }, + }, +} as Meta; + +type Story = StoryObj; + +export const MultipleLanguages: Story = { + play: async ({canvasElement, args, step}) => { + const canvas = within(canvasElement); + + await step('Inspect content translations', async () => { + await userEvent.click(canvas.getByRole('link', {name: 'NL'})); + await waitFor(() => { + expect(canvas.getByText('Niet alle velden zijn ingevuld.')).toBeVisible(); + }); + + await userEvent.click(canvas.getByRole('link', {name: 'EN'})); + await waitFor(() => { + expect(canvas.getByText('Not all fields have been filled.')).toBeVisible(); + }); + }); + + await userEvent.click(canvas.getByRole('button', {name: 'Save'})); + expect(args.onSubmit).toHaveBeenCalledWith({ + id: 'wekruya', + type: 'softRequiredErrors', + label: '', + html: '

Niet alle velden zijn ingevuld.

\n{{ missingFields }}', + openForms: { + translations: { + nl: { + html: '

Niet alle velden zijn ingevuld.

\n{{ missingFields }}', + }, + en: { + html: '

Not all fields have been filled.

\n{{ missingFields }}', + }, + }, + }, + key: 'softRequiredErrors', + } satisfies SoftRequiredErrorsComponentSchema); + }, +};