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);
+ },
+};