From 08f31bf9b727a3f723918e65eafb65c3893a0eec Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 14 Oct 2024 09:44:55 +0200 Subject: [PATCH 1/6] :pencil: Ensure stories are properly sorted in Storybook --- .storybook/preview.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 1fba59a6..2ef662a7 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -19,6 +19,18 @@ const preview: Preview = { date: /Date$/, }, }, + options: { + storySort: { + order: [ + 'Introduction', + 'Public API', + 'Edit form', + 'Generic', + 'Formio', + 'Builder components', + ], + } + } }, initialGlobals: { locale: reactIntl.defaultLocale, From 9592dd2785e9bda925e367a0f50e63699eeb15d5 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Tue, 15 Oct 2024 15:29:41 +0200 Subject: [PATCH 2/6] :arrow_up: [open-formulieren/open-forms#4546] Upgrade to types 0.31.0 Types contain the softRequired extension and custom component to display related errors. --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 358d3937..6c299f5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "@formatjs/cli": "^6.1.1", "@formatjs/ts-transformer": "^3.12.0", "@fortawesome/fontawesome-free": "^6.4.0", - "@open-formulieren/types": "^0.30.0", + "@open-formulieren/types": "^0.31.0", "@storybook/addon-actions": "^8.3.5", "@storybook/addon-essentials": "^8.3.5", "@storybook/addon-interactions": "^8.3.5", @@ -5087,9 +5087,9 @@ } }, "node_modules/@open-formulieren/types": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/@open-formulieren/types/-/types-0.30.0.tgz", - "integrity": "sha512-ur7G3pIme3ljfc/NbF2GrLRvJ7Em7kFPzl+S6SG14vwjORJKt4K7nsZWHHG0iLdwJx7T93efxW2cUtpArr4B4w==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@open-formulieren/types/-/types-0.31.0.tgz", + "integrity": "sha512-ahdTHgSACvHvrQPwXh/1bkq7+8J9m4b8m1UdvaVirP8cMJwfvUJ1hdgMuOhwPoYJUlQ/tVrrL9rRl0cQ/bC79Q==", "dev": true }, "node_modules/@pkgjs/parseargs": { @@ -24606,9 +24606,9 @@ } }, "@open-formulieren/types": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/@open-formulieren/types/-/types-0.30.0.tgz", - "integrity": "sha512-ur7G3pIme3ljfc/NbF2GrLRvJ7Em7kFPzl+S6SG14vwjORJKt4K7nsZWHHG0iLdwJx7T93efxW2cUtpArr4B4w==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@open-formulieren/types/-/types-0.31.0.tgz", + "integrity": "sha512-ahdTHgSACvHvrQPwXh/1bkq7+8J9m4b8m1UdvaVirP8cMJwfvUJ1hdgMuOhwPoYJUlQ/tVrrL9rRl0cQ/bC79Q==", "dev": true }, "@pkgjs/parseargs": { diff --git a/package.json b/package.json index ae4a0e24..751e5c1b 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@formatjs/cli": "^6.1.1", "@formatjs/ts-transformer": "^3.12.0", "@fortawesome/fontawesome-free": "^6.4.0", - "@open-formulieren/types": "^0.30.0", + "@open-formulieren/types": "^0.31.0", "@storybook/addon-actions": "^8.3.5", "@storybook/addon-essentials": "^8.3.5", "@storybook/addon-interactions": "^8.3.5", From bd1afb041d155d8f641bc29f864b82711dfc6f7e Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 14 Oct 2024 10:00:51 +0200 Subject: [PATCH 3/6] :sparkles: [open-formulieren/open-forms#4546] Implement configuration option for soft-required fields Hard and soft required at the same time is not possible, so when the 'required' checkbox is enabled, the soft-required one is unchecked (if that's relevant) and the checkbox itself is disabled. --- .../ComponentConfiguration.stories.tsx | 5 ++ src/components/builder/validate/index.ts | 1 + .../builder/validate/soft-required.tsx | 52 +++++++++++++++++++ src/components/formio/checkbox.tsx | 6 +++ src/registry/file/edit.stories.ts | 49 ++++++++++++++++- src/registry/file/edit.tsx | 5 ++ src/registry/file/file-validation.stories.ts | 2 + 7 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/components/builder/validate/soft-required.tsx diff --git a/src/components/ComponentConfiguration.stories.tsx b/src/components/ComponentConfiguration.stories.tsx index f8846716..22ae3e3f 100644 --- a/src/components/ComponentConfiguration.stories.tsx +++ b/src/components/ComponentConfiguration.stories.tsx @@ -1057,6 +1057,11 @@ export const FileUpload: Story = { docVertrouwelijkheidaanduiding: '', titel: '', }, + // custom extensions + openForms: { + softRequired: false, + translations: {}, + }, }); }); }, diff --git a/src/components/builder/validate/index.ts b/src/components/builder/validate/index.ts index 75a6c7d8..f4658718 100644 --- a/src/components/builder/validate/index.ts +++ b/src/components/builder/validate/index.ts @@ -5,3 +5,4 @@ export {default as Min} from './min'; export {default as RegexValidation} from './regex'; export {default as ValidatorPluginSelect} from './validator-select'; export {default as ValidationErrorTranslations, useManageValidatorsTranslations} from './i18n'; +export {default as SoftRequired} from './soft-required'; diff --git a/src/components/builder/validate/soft-required.tsx b/src/components/builder/validate/soft-required.tsx new file mode 100644 index 00000000..df4d0444 --- /dev/null +++ b/src/components/builder/validate/soft-required.tsx @@ -0,0 +1,52 @@ +import {useFormikContext} from 'formik'; +import {useEffect} from 'react'; +import {FormattedMessage, useIntl} from 'react-intl'; + +import {Checkbox} from '../../formio'; + +type ComponentWithRequiredOptions = { + validate?: { + required?: boolean; + }; + openForms?: { + softRequired?: boolean; + }; +}; + +const SoftRequired = () => { + const intl = useIntl(); + const {values, setFieldValue} = useFormikContext(); + + const isRequired = values?.validate?.required ?? false; + const isSoftRequired = values?.openForms?.softRequired ?? false; + + // if the field is hard required, we must disable the soft required option, and + // synchronize the softRequired to uncheck the option (if needed) + useEffect(() => { + if (isRequired && isSoftRequired) { + setFieldValue('openForms.softRequired', false); + } + }, [setFieldValue, isRequired, isSoftRequired]); + + const tooltip = intl.formatMessage({ + description: "Tooltip for 'openForms.softRequired' builder field", + defaultMessage: `Soft required fields should be filled out, but empty values don't + block the users' progress. Sometimes this is needed for legal reasons. A component + cannot be hard and soft required at the same time.`, + }); + return ( + + } + tooltip={tooltip} + disabled={isRequired} + /> + ); +}; + +export default SoftRequired; diff --git a/src/components/formio/checkbox.tsx b/src/components/formio/checkbox.tsx index 3b5c1974..57170015 100644 --- a/src/components/formio/checkbox.tsx +++ b/src/components/formio/checkbox.tsx @@ -12,6 +12,7 @@ export interface CheckboxInputProps { label?: React.ReactNode; onChange?: FormikHandlers['handleChange']; optionDescription?: string; + disabled?: boolean; } export const CheckboxInput: React.FC = ({ @@ -19,6 +20,7 @@ export const CheckboxInput: React.FC = ({ label, onChange, optionDescription, + disabled = false, }) => { const {getFieldProps} = useFormikContext(); const {onChange: formikOnChange} = getFieldProps(name); @@ -34,6 +36,7 @@ export const CheckboxInput: React.FC = ({ formikOnChange(e); onChange?.(e); }} + disabled={disabled} /> {label} {optionDescription && } @@ -49,6 +52,7 @@ export interface CheckboxProps { description?: string; onChange?: FormikHandlers['handleChange']; optionDescription?: string; + disabled?: boolean; } const Checkbox: React.FC = ({ @@ -59,6 +63,7 @@ const Checkbox: React.FC = ({ description = '', onChange, optionDescription, + disabled = false, }) => (
@@ -68,6 +73,7 @@ const Checkbox: React.FC = ({ label={label} onChange={onChange} optionDescription={optionDescription} + disabled={disabled} /> {tooltip && ' '} diff --git a/src/registry/file/edit.stories.ts b/src/registry/file/edit.stories.ts index 10af45cd..12d3507a 100644 --- a/src/registry/file/edit.stories.ts +++ b/src/registry/file/edit.stories.ts @@ -1,5 +1,5 @@ import {Meta, StoryObj} from '@storybook/react'; -import {expect, fireEvent, fn, userEvent, within} from '@storybook/test'; +import {expect, fireEvent, fn, userEvent, waitFor, within} from '@storybook/test'; import ComponentEditForm from '@/components/ComponentEditForm'; import {BuilderContextDecorator} from '@/sb-decorators'; @@ -31,6 +31,7 @@ export default { allowedTypesLabels: [], }, filePattern: '', + // @ts-expect-error this is what is actually produced by Formio defaultValue: null, }, onCancel: fn(), @@ -68,3 +69,49 @@ export const ToggleToMultiple: Story = { expect(defaultValue).toBeNull(); }, }; + +export const SoftRequiredValidation: Story = { + play: async ({canvasElement, step}) => { + const canvas = within(canvasElement); + + await step('Navigate to validate tab', async () => { + await userEvent.click(canvas.getByRole('tab', {name: 'Validation'})); + }); + + // establish base state + const hardRequired = await canvas.findByRole('checkbox', {name: 'Required'}); + expect(hardRequired).not.toBeChecked(); + const softRequired = await canvas.findByRole('checkbox', {name: 'Soft required'}); + expect(softRequired).not.toBeChecked(); + + await step('Mark component soft required', async () => { + // We use fireEvent because firefox borks on userEvent.click, see: + // https://github.com/testing-library/user-event/issues/1149 + fireEvent.click(softRequired); + expect(softRequired).toBeChecked(); + expect(hardRequired).not.toBeChecked(); + }); + + await step('Mark component hard required', async () => { + // We use fireEvent because firefox borks on userEvent.click, see: + // https://github.com/testing-library/user-event/issues/1149 + fireEvent.click(hardRequired); + await waitFor(() => { + expect(hardRequired).toBeChecked(); + expect(softRequired).not.toBeChecked(); + expect(softRequired).toBeDisabled(); + }); + }); + + await step('Mark component not hard required', async () => { + // We use fireEvent because firefox borks on userEvent.click, see: + // https://github.com/testing-library/user-event/issues/1149 + fireEvent.click(hardRequired); + await waitFor(() => { + expect(hardRequired).not.toBeChecked(); + expect(softRequired).not.toBeChecked(); + expect(softRequired).not.toBeDisabled(); + }); + }); + }, +}; diff --git a/src/registry/file/edit.tsx b/src/registry/file/edit.tsx index ec97de8a..6de37416 100644 --- a/src/registry/file/edit.tsx +++ b/src/registry/file/edit.tsx @@ -95,6 +95,7 @@ const EditForm: EditFormDefinition = () => { {/* Validation tab */} + @@ -156,6 +157,10 @@ EditForm.defaultValues = { required: false, }, translatedErrors: {}, + openForms: { + softRequired: false, + translations: {}, + }, // file tab file: { name: '', diff --git a/src/registry/file/file-validation.stories.ts b/src/registry/file/file-validation.stories.ts index c5561eb6..882b45df 100644 --- a/src/registry/file/file-validation.stories.ts +++ b/src/registry/file/file-validation.stories.ts @@ -18,6 +18,8 @@ export default { storage: 'url', url: '', type: 'file', + options: {withCredentials: true}, + webcam: false, key: 'file', label: 'A file upload', file: { From acc635e72e356e1bbeaef6a2a3e1f0e19901c7d0 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 14 Oct 2024 11:30:16 +0200 Subject: [PATCH 4/6] :truck: [open-formulieren/open-forms#4546] Move RichText component to 'base building blocks' While it initially was only relevant for the content component, we will make use of this same component in the display component for the soft required validation errors. The component translations wrapper around it remains component-type specific since we can leverage the type-safety when checking validation error keys, at the cost of a little bit of code duplication. --- src/components/builder/index.ts | 1 + src/components/builder/rich-text.stories.ts | 31 +++++++++++++++++++ .../builder}/rich-text.tsx | 13 +++++--- .../builder}/richText.scss | 0 src/context.ts | 2 +- src/registry/content/edit.tsx | 8 +++-- 6 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 src/components/builder/rich-text.stories.ts rename src/{registry/content => components/builder}/rich-text.tsx (80%) rename src/{registry/content => components/builder}/richText.scss (100%) diff --git a/src/components/builder/index.ts b/src/components/builder/index.ts index ae81440f..fcc6f22e 100644 --- a/src/components/builder/index.ts +++ b/src/components/builder/index.ts @@ -18,6 +18,7 @@ export {default as ReadOnly} from './readonly'; export {default as ShowCharCount} from './show-char-count'; export {default as PresentationConfig} from './presentation-config'; export {default as ComponentSelect} from './component-select'; +export {default as RichText} from './rich-text'; export {default as SimpleConditional} from './simple-conditional'; export {default as Suffix} from './suffix'; export {default as TemplatingHint} from './templating-hint'; diff --git a/src/components/builder/rich-text.stories.ts b/src/components/builder/rich-text.stories.ts new file mode 100644 index 00000000..69d288af --- /dev/null +++ b/src/components/builder/rich-text.stories.ts @@ -0,0 +1,31 @@ +import {Meta, StoryObj} from '@storybook/react'; + +import {withFormik} from '@/sb-decorators'; + +import RichText from './rich-text'; + +export default { + title: 'Formio/Builder/RichText', + component: RichText, + decorators: [withFormik], + args: { + name: 'richText', + required: false, + supportsBackendTemplating: false, + }, + parameters: { + controls: {hideNoControlsWarning: true}, + modal: {noModal: true}, + formik: {initialValues: {richText: ''}}, + }, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithBackendTemplatingSupport: Story = { + args: { + supportsBackendTemplating: true, + }, +}; diff --git a/src/registry/content/rich-text.tsx b/src/components/builder/rich-text.tsx similarity index 80% rename from src/registry/content/rich-text.tsx rename to src/components/builder/rich-text.tsx index 89cd144f..d634f557 100644 --- a/src/registry/content/rich-text.tsx +++ b/src/components/builder/rich-text.tsx @@ -8,7 +8,8 @@ */ import {CKEditor} from '@ckeditor/ckeditor5-react'; import ClassicEditor from '@open-formulieren/ckeditor5-build-classic'; -import {useField} from 'formik'; +import {AnyComponentSchema} from '@open-formulieren/types'; +import {useField, useFormikContext} from 'formik'; import {useContext} from 'react'; import {TemplatingHint} from '@/components/builder'; @@ -26,6 +27,7 @@ export type ColorOption = Required< export interface RichTextProps { name: string; required?: boolean; + supportsBackendTemplating?: boolean; } /** @@ -35,11 +37,14 @@ export interface RichTextProps { * classic editor build, with some extra plugins enabled to match the features used/exposed * by Formio.js. */ -const RichText: React.FC = ({name, required}) => { +const RichText: React.FC = ({name, required, supportsBackendTemplating = false}) => { const {richTextColors} = useContext(BuilderContext); + const { + values: {type}, + } = useFormikContext(); const [props, , helpers] = useField(name); return ( - + = ({name, required}) => { helpers.setTouched(true); }} /> - } /> + {supportsBackendTemplating && } />} ); }; diff --git a/src/registry/content/richText.scss b/src/components/builder/richText.scss similarity index 100% rename from src/registry/content/richText.scss rename to src/components/builder/richText.scss diff --git a/src/context.ts b/src/context.ts index a4921afd..8748f503 100644 --- a/src/context.ts +++ b/src/context.ts @@ -3,8 +3,8 @@ import React from 'react'; import {PrefillAttributeOption, PrefillPluginOption} from '@/components/builder/prefill/types'; import {RegistrationAttributeOption} from '@/components/builder/registration/registration-attribute'; +import type {ColorOption} from '@/components/builder/rich-text'; import {ValidatorOption} from '@/components/builder/validate/validator-select'; -import type {ColorOption} from '@/registry/content/rich-text'; import {AuthPluginOption} from '@/registry/cosignV1/edit'; import {AnyComponentSchema} from '@/types'; diff --git a/src/registry/content/edit.tsx b/src/registry/content/edit.tsx index 4a16ed29..5d6a2c02 100644 --- a/src/registry/content/edit.tsx +++ b/src/registry/content/edit.tsx @@ -10,6 +10,7 @@ import { Hidden, Key, PresentationConfig, + RichText, SimpleConditional, } from '@/components/builder'; import {Select, Tab, TabList, TabPanel, Tabs} from '@/components/formio'; @@ -17,7 +18,6 @@ import {BuilderContext} from '@/context'; import {useErrorChecker} from '@/utils/errors'; import {EditFormDefinition} from '../types'; -import RichText from './rich-text'; /** * Form to configure a Formio 'content' type component. @@ -191,7 +191,11 @@ const RichTextTranslations: React.FC = () => { {supportedLanguageCodes.map((code, index) => ( - + ))} From 08aaef7a1b83d279da0c39f57181ab72c4804bbc Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 14 Oct 2024 11:42:02 +0200 Subject: [PATCH 5/6] :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); + }, +}; From 6311885e5a81f559f1e7bf1056449808783cad0b Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 14 Oct 2024 11:46:24 +0200 Subject: [PATCH 6/6] :globe_with_meridians: [open-formulieren/open-forms#4546] Update translations --- i18n/messages/en.json | 10 ++++++++++ i18n/messages/nl.json | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/i18n/messages/en.json b/i18n/messages/en.json index b18cd655..2af642ab 100644 --- a/i18n/messages/en.json +++ b/i18n/messages/en.json @@ -269,6 +269,11 @@ "description": "Fallback label for option with empty label", "originalDefault": "(missing label)" }, + "8M403q": { + "defaultMessage": "Soft required fields should be filled out, but empty values don't block the users' progress. Sometimes this is needed for legal reasons. A component cannot be hard and soft required at the same time.", + "description": "Tooltip for 'openForms.softRequired' builder field", + "originalDefault": "Soft required fields should be filled out, but empty values don't block the users' progress. Sometimes this is needed for legal reasons. A component cannot be hard and soft required at the same time." + }, "9lk1eS": { "defaultMessage": "Subtract", "description": "Operator 'subtract' option label", @@ -589,6 +594,11 @@ "description": "Invalid email address format validation error", "originalDefault": "{field} must be a valid email." }, + "QL4SGQ": { + "defaultMessage": "Soft required", + "description": "Label for 'openForms.softRequired' builder field", + "originalDefault": "Soft required" + }, "QW32Dd": { "defaultMessage": "Specify a positive, non-zero file size without decimals, e.g. 10MB.", "description": "File component 'fileMaxSize' validation error", diff --git a/i18n/messages/nl.json b/i18n/messages/nl.json index 5927559f..2824c045 100644 --- a/i18n/messages/nl.json +++ b/i18n/messages/nl.json @@ -272,6 +272,11 @@ "description": "Fallback label for option with empty label", "originalDefault": "(missing label)" }, + "8M403q": { + "defaultMessage": "Aangeraden velden moeten in principe ingevuld worden, maar ontbrekende waarden blokkeren de voortgang niet. Dit is soms nodig voor juridische redenen. Een component kan niet tegelijk verplicht en aangeraden zijn.", + "description": "Tooltip for 'openForms.softRequired' builder field", + "originalDefault": "Soft required fields should be filled out, but empty values don't block the users' progress. Sometimes this is needed for legal reasons. A component cannot be hard and soft required at the same time." + }, "9lk1eS": { "defaultMessage": "Aftrekken", "description": "Operator 'subtract' option label", @@ -598,6 +603,11 @@ "description": "Invalid email address format validation error", "originalDefault": "{field} must be a valid email." }, + "QL4SGQ": { + "defaultMessage": "Aangeraden (niet-blokkerend verplicht)", + "description": "Label for 'openForms.softRequired' builder field", + "originalDefault": "Soft required" + }, "QW32Dd": { "defaultMessage": "Geef een bestandsgrootte groter dan nul zonder decimalen op, bijvoorbeeld '10MB'.", "description": "File component 'fileMaxSize' validation error",