Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for soft required validation in builder #188

Merged
merged 6 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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