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, 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", 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", diff --git a/src/components/ComponentConfiguration.stories.tsx b/src/components/ComponentConfiguration.stories.tsx index f8846716..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'; @@ -1057,6 +1061,11 @@ export const FileUpload: Story = { docVertrouwelijkheidaanduiding: '', titel: '', }, + // custom extensions + openForms: { + softRequired: false, + translations: {}, + }, }); }); }, @@ -3020,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/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/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/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) => ( - + ))} 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: { 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); + }, +};