Skip to content

Commit

Permalink
Merge pull request #188 from open-formulieren/feature/4546-soft-requi…
Browse files Browse the repository at this point in the history
…red-validation

Add support for soft required validation in builder
  • Loading branch information
sergei-maertens authored Oct 16, 2024
2 parents a15ba1f + 6311885 commit ac96225
Show file tree
Hide file tree
Showing 23 changed files with 497 additions and 17 deletions.
12 changes: 12 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions i18n/messages/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
65 changes: 64 additions & 1 deletion src/components/ComponentConfiguration.stories.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -1057,6 +1061,11 @@ export const FileUpload: Story = {
docVertrouwelijkheidaanduiding: '',
titel: '',
},
// custom extensions
openForms: {
softRequired: false,
translations: {},
},
});
});
},
Expand Down Expand Up @@ -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: '<p>Niet alle velden zijn ingevuld.</p>\n{{ missingFields }}',
openForms: {
translations: {
nl: {
html: '<p>Niet alle velden zijn ingevuld.</p>\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: '<p>Niet alle velden zijn ingevuld.</p>\n{{ missingFields }}',
openForms: {
translations: {
nl: {
html: '<p>Niet alle velden zijn ingevuld.</p>\n{{ missingFields }}',
},
en: {
html: '',
},
},
},
key: 'softRequiredErrors',
} satisfies SoftRequiredErrorsComponentSchema);
});
},
};
1 change: 1 addition & 0 deletions src/components/builder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
31 changes: 31 additions & 0 deletions src/components/builder/rich-text.stories.ts
Original file line number Diff line number Diff line change
@@ -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<typeof RichText>;

type Story = StoryObj<typeof RichText>;

export const Default: Story = {};

export const WithBackendTemplatingSupport: Story = {
args: {
supportsBackendTemplating: true,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -26,6 +27,7 @@ export type ColorOption = Required<
export interface RichTextProps {
name: string;
required?: boolean;
supportsBackendTemplating?: boolean;
}

/**
Expand All @@ -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<RichTextProps> = ({name, required}) => {
const RichText: React.FC<RichTextProps> = ({name, required, supportsBackendTemplating = false}) => {
const {richTextColors} = useContext(BuilderContext);
const {
values: {type},
} = useFormikContext<AnyComponentSchema>();
const [props, , helpers] = useField<string>(name);
return (
<Component type="content" field={name} required={required} className="offb-rich-text">
<Component type={type} field={name} required={required} className="offb-rich-text">
<CKEditor
editor={ClassicEditor}
config={{
Expand Down Expand Up @@ -69,7 +74,7 @@ const RichText: React.FC<RichTextProps> = ({name, required}) => {
helpers.setTouched(true);
}}
/>
<Description text={<TemplatingHint />} />
{supportsBackendTemplating && <Description text={<TemplatingHint />} />}
</Component>
);
};
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions src/components/builder/validate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
52 changes: 52 additions & 0 deletions src/components/builder/validate/soft-required.tsx
Original file line number Diff line number Diff line change
@@ -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<ComponentWithRequiredOptions>();

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 (
<Checkbox
name="openForms.softRequired"
label={
<FormattedMessage
description="Label for 'openForms.softRequired' builder field"
defaultMessage="Soft required"
/>
}
tooltip={tooltip}
disabled={isRequired}
/>
);
};

export default SoftRequired;
6 changes: 6 additions & 0 deletions src/components/formio/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ export interface CheckboxInputProps {
label?: React.ReactNode;
onChange?: FormikHandlers['handleChange'];
optionDescription?: string;
disabled?: boolean;
}

export const CheckboxInput: React.FC<CheckboxInputProps> = ({
name,
label,
onChange,
optionDescription,
disabled = false,
}) => {
const {getFieldProps} = useFormikContext();
const {onChange: formikOnChange} = getFieldProps(name);
Expand All @@ -34,6 +36,7 @@ export const CheckboxInput: React.FC<CheckboxInputProps> = ({
formikOnChange(e);
onChange?.(e);
}}
disabled={disabled}
/>
<span>{label}</span>
{optionDescription && <Description text={optionDescription} />}
Expand All @@ -49,6 +52,7 @@ export interface CheckboxProps {
description?: string;
onChange?: FormikHandlers['handleChange'];
optionDescription?: string;
disabled?: boolean;
}

const Checkbox: React.FC<CheckboxProps> = ({
Expand All @@ -59,6 +63,7 @@ const Checkbox: React.FC<CheckboxProps> = ({
description = '',
onChange,
optionDescription,
disabled = false,
}) => (
<Component field={name} required={required} type="checkbox">
<div className="form-check checkbox">
Expand All @@ -68,6 +73,7 @@ const Checkbox: React.FC<CheckboxProps> = ({
label={label}
onChange={onChange}
optionDescription={optionDescription}
disabled={disabled}
/>
{tooltip && ' '}
<Tooltip text={tooltip} />
Expand Down
2 changes: 1 addition & 1 deletion src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Loading

0 comments on commit ac96225

Please sign in to comment.