From 5d1fd23cb0c02ea3d86c56fcde5fe28b8490e7fc Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 19 Aug 2024 16:17:36 +0200 Subject: [PATCH 1/2] :recycle: [open-formulieren/open-forms#4542] Refactor how openForms.* extensions are defined for a component type Instead of having to re-define the openForms key itself and re-compose it with the translatable keys, we can instead use a type variable to specify the extensions to add via the OFExtensions helper type. This removes the amount of possible errors and repetition w/r to the translation keys that need to be passed, and the re-use of the generic types is greater. --- src/formio/base.ts | 18 +++++++++++++---- src/formio/components/addressNL.ts | 21 ++++++++++---------- src/formio/components/date.ts | 21 +++++++++++--------- src/formio/components/datetime.ts | 17 ++++++++++------ src/formio/components/radio.ts | 23 +++++++++++++--------- src/formio/components/select.ts | 21 +++++++++++--------- src/formio/components/selectboxes.ts | 29 ++++++++++++++++------------ src/formio/util.ts | 8 ++++++++ 8 files changed, 99 insertions(+), 59 deletions(-) create mode 100644 src/formio/util.ts diff --git a/src/formio/base.ts b/src/formio/base.ts index ba8e8cd..317e8e7 100644 --- a/src/formio/base.ts +++ b/src/formio/base.ts @@ -82,12 +82,15 @@ export interface PrefillConfig { /** * @group Open Forms schema extensions + * + * The `Extra` type variable allows specifying additional, component-specific, + * extensions namespaced under the `openForms` key. */ -export interface OFExtensions { +export interface OFExtensions { isSensitiveData?: boolean; openForms?: { translations: ComponentTranslations; - }; + } & Extra; registration?: { attribute: string; }; @@ -173,12 +176,19 @@ export type MultipleCapable = S extends {defaultValue?: infer DV} /** * @group Schema primitives + * + * The `ExtraExtensions` type variable allows specifying additional, component-specific, + * extensions namespaced under the `openForms` key. */ export type InputComponentSchema< T = unknown, VN extends CuratedValidatorNames = CuratedValidatorNames, - TK extends string = string -> = StrictComponentSchema & DisplayConfig & OFExtensions & HasValidation; + TK extends string = string, + ExtraExtensions = {} +> = StrictComponentSchema & + DisplayConfig & + OFExtensions & + HasValidation; /** * @group Schema primitives diff --git a/src/formio/components/addressNL.ts b/src/formio/components/addressNL.ts index b1598a1..3c5a049 100644 --- a/src/formio/components/addressNL.ts +++ b/src/formio/components/addressNL.ts @@ -1,5 +1,4 @@ import {HasValidation, InputComponentSchema} from '..'; -import {ComponentTranslations} from '../i18n'; type Validator = 'required'; type TranslatableKeys = 'label' | 'description' | 'tooltip'; @@ -19,22 +18,24 @@ export interface AddressComponents { city?: HasValidation<'pattern', false>; } -export type AddressNLInputSchema = InputComponentSchema; +export interface AddressNLExtensions { + components?: AddressComponents; +} + +export type AddressNLInputSchema = InputComponentSchema< + AddressData, + Validator, + TranslatableKeys, + AddressNLExtensions +>; /** * @group Form.io components * @category Concrete types */ export interface AddressNLComponentSchema - extends Omit< - AddressNLInputSchema, - 'hideLabel' | 'placeholder' | 'disabled' | 'validateOn' | 'openForms' - > { + extends Omit { type: 'addressNL'; deriveAddress: boolean; layout: 'singleColumn' | 'doubleColumn'; - openForms?: { - components?: AddressComponents; - translations?: ComponentTranslations; - }; } diff --git a/src/formio/components/date.ts b/src/formio/components/date.ts index 915374b..413faa4 100644 --- a/src/formio/components/date.ts +++ b/src/formio/components/date.ts @@ -1,5 +1,4 @@ import {InputComponentSchema, MultipleCapable, PrefillConfig} from '..'; -import {OFExtensions} from '../base'; import { FutureDateConstraint as BaseFutureDateConstraint, PastDateConstraint as BasePastDateConstraint, @@ -11,8 +10,6 @@ import { type Validator = 'required' | 'minDate' | 'maxDate'; type TranslatableKeys = 'label' | 'description' | 'tooltip'; -export type DateInputSchema = InputComponentSchema; - export interface IncludeToday { includeToday: boolean | null; } @@ -21,18 +18,24 @@ type FutureOrPastDateConstraint = BaseFutureDateConstraint | BasePastDateConstra type FutureDateConstraint = BaseFutureDateConstraint & IncludeToday; type PastDateConstraint = BasePastDateConstraint & IncludeToday; +export interface DateExtensions { + minDate?: Exclude | FutureDateConstraint; + maxDate?: Exclude | PastDateConstraint; +} + +export type DateInputSchema = InputComponentSchema< + string, + Validator, + TranslatableKeys, + DateExtensions +>; + /** * @group Form.io components * @category Base types */ export interface BaseDateComponentSchema extends Omit, PrefillConfig { type: 'date'; - openForms?: OFExtensions['openForms'] & { - minDate?: - | Exclude - | FutureDateConstraint; - maxDate?: Exclude | PastDateConstraint; - }; datePicker?: DatePickerConfig; customOptions?: PickerCustomOptions; } diff --git a/src/formio/components/datetime.ts b/src/formio/components/datetime.ts index d1adf1f..35e9853 100644 --- a/src/formio/components/datetime.ts +++ b/src/formio/components/datetime.ts @@ -1,5 +1,4 @@ import {InputComponentSchema, MultipleCapable, PrefillConfig} from '..'; -import {OFExtensions} from '../base'; import { DateConstraintConfiguration, DatePickerConfig, @@ -11,7 +10,17 @@ import { type Validator = 'required' | 'minDate' | 'maxDate'; type TranslatableKeys = 'label' | 'description' | 'tooltip'; -export type DateTimeInputSchema = InputComponentSchema; +export interface DateTimeExtensions { + minDate?: Exclude; + maxDate?: Exclude; +} + +export type DateTimeInputSchema = InputComponentSchema< + string, + Validator, + TranslatableKeys, + DateTimeExtensions +>; /** * @group Form.io components @@ -21,10 +30,6 @@ export interface BaseDateTimeComponentSchema extends Omit, PrefillConfig { type: 'datetime'; - openForms?: OFExtensions['openForms'] & { - minDate?: Exclude; - maxDate?: Exclude; - }; datePicker?: DatePickerConfig; customOptions?: PickerCustomOptions; } diff --git a/src/formio/components/radio.ts b/src/formio/components/radio.ts index 5cfe308..7ff4cc2 100644 --- a/src/formio/components/radio.ts +++ b/src/formio/components/radio.ts @@ -1,11 +1,16 @@ import {InputComponentSchema} from '..'; -import {OFExtensions} from '../base'; import {ManualValues, Option, VariableValues} from '../common'; +import {Require} from '../util'; type Validator = 'required'; type TranslatableKeys = 'label' | 'description' | 'tooltip'; -export type RadioInputSchema = InputComponentSchema; +export type RadioInputSchema = InputComponentSchema< + string | null, + Validator, + TranslatableKeys, + Extensions +>; /** * @group Form.io components @@ -20,9 +25,8 @@ interface BaseRadioSchema { * @group Form.io components * @category Base types */ -type RadioManualValuesSchema = Omit & +type RadioManualValuesSchema = Omit, 'hideLabel' | 'disabled'> & BaseRadioSchema & { - openForms: OFExtensions['openForms'] & ManualValues; values: Option[]; }; @@ -30,13 +34,14 @@ type RadioManualValuesSchema = Omit * @group Form.io components * @category Base types */ -type RadioVariableValuesSchema = Omit & - BaseRadioSchema & { - openForms: OFExtensions['openForms'] & VariableValues; - }; +type RadioVariableValuesSchema = Omit, 'hideLabel' | 'disabled'> & + BaseRadioSchema; /** * @group Form.io components * @category Concrete types */ -export type RadioComponentSchema = RadioManualValuesSchema | RadioVariableValuesSchema; +export type RadioComponentSchema = Require< + RadioManualValuesSchema | RadioVariableValuesSchema, + 'openForms' +>; diff --git a/src/formio/components/select.ts b/src/formio/components/select.ts index fd84a6c..18a0314 100644 --- a/src/formio/components/select.ts +++ b/src/formio/components/select.ts @@ -1,11 +1,17 @@ import {InputComponentSchema} from '..'; -import {MultipleCapable, OFExtensions} from '../base'; +import {MultipleCapable} from '../base'; import {ManualValues, Option, VariableValues} from '../common'; +import {Require} from '../util'; type Validator = 'required'; type TranslatableKeys = 'label' | 'description' | 'tooltip'; -export type SelectInputSchema = InputComponentSchema; +export type SelectInputSchema = InputComponentSchema< + string, + Validator, + TranslatableKeys, + Extensions +>; export type SelectUnsupported = 'hideLabel' | 'disabled' | 'placeholder'; @@ -26,9 +32,8 @@ interface BaseSelectSchema { * @group Form.io components * @category Base types */ -type SelectManualValuesSchema = Omit & +type SelectManualValuesSchema = Omit, SelectUnsupported> & BaseSelectSchema & { - openForms: OFExtensions['openForms'] & ManualValues; data: { values: Option[]; }; @@ -38,15 +43,13 @@ type SelectManualValuesSchema = Omit & * @group Form.io components * @category Base types */ -type SelectVariableValuesSchema = Omit & - BaseSelectSchema & { - openForms: OFExtensions['openForms'] & VariableValues; - }; +type SelectVariableValuesSchema = Omit, SelectUnsupported> & + BaseSelectSchema; /** * @group Form.io components * @category Concrete types */ export type SelectComponentSchema = MultipleCapable< - SelectManualValuesSchema | SelectVariableValuesSchema + Require >; diff --git a/src/formio/components/selectboxes.ts b/src/formio/components/selectboxes.ts index 342fd2f..d44a1c8 100644 --- a/src/formio/components/selectboxes.ts +++ b/src/formio/components/selectboxes.ts @@ -1,14 +1,15 @@ import {InputComponentSchema} from '..'; -import {OFExtensions} from '../base'; import {ManualValues, Option, VariableValues} from '../common'; +import {Require} from '../util'; type Validator = 'required' | 'minSelectedCount' | 'maxSelectedCount'; type TranslatableKeys = 'label' | 'description' | 'tooltip'; -export type SelectboxesInputSchema = InputComponentSchema< +export type SelectboxesInputSchema = InputComponentSchema< Record, Validator, - TranslatableKeys + TranslatableKeys, + Extensions >; /** @@ -24,9 +25,11 @@ interface BaseSelectboxesSchema { * @group Form.io components * @category Base types */ -type SelectboxesManualValuesSchema = Omit & +type SelectboxesManualValuesSchema = Omit< + SelectboxesInputSchema, + 'hideLabel' | 'disabled' +> & BaseSelectboxesSchema & { - openForms: OFExtensions['openForms'] & ManualValues; values: Option[]; }; @@ -34,15 +37,17 @@ type SelectboxesManualValuesSchema = Omit & - BaseSelectboxesSchema & { - openForms: OFExtensions['openForms'] & VariableValues; - }; +type SelectboxesVariableValuesSchema = Omit< + SelectboxesInputSchema, + 'hideLabel' | 'disabled' +> & + BaseSelectboxesSchema; /** * @group Form.io components * @category Concrete types */ -export type SelectboxesComponentSchema = - | SelectboxesManualValuesSchema - | SelectboxesVariableValuesSchema; +export type SelectboxesComponentSchema = Require< + SelectboxesManualValuesSchema | SelectboxesVariableValuesSchema, + 'openForms' +>; diff --git a/src/formio/util.ts b/src/formio/util.ts new file mode 100644 index 0000000..9608692 --- /dev/null +++ b/src/formio/util.ts @@ -0,0 +1,8 @@ +/** + * Given a type `T` with optional key(s) `K`, make the key(s) `K` required. + * + * The ternary is to force distribution over unions in `T`. + */ +export type Require = T extends any + ? Omit & Required> + : never; From 58edc62797fc92f137ee1720bd610f60ec3238e0 Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Mon, 19 Aug 2024 16:20:04 +0200 Subject: [PATCH 2/2] :sparkles: [open-formulieren/open-forms#4542] Added the optional requireVerification email extension The extension is added under the openForms namespace to prevent possible future collissions, and to make it explicit this is an additional feature added by Open Forms. The property is optional so that existing email components do not require a data migration - not specifying it is falsy, which is the default behaviour. The property is not added under the 'validate' namespace, because it's something that can only be properly validated server-side - it does not make sense to configure client-side validation error messages for this validation step because of that reason. We can still emit validation errors server-side in a way that they will probably be displayed with the component as if it was client-side validation. --- src/formio/components/email.ts | 11 ++++++++++- test-d/formio/components/email.test-d.ts | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/formio/components/email.ts b/src/formio/components/email.ts index 1d00500..f51f7d1 100644 --- a/src/formio/components/email.ts +++ b/src/formio/components/email.ts @@ -3,7 +3,16 @@ import {InputComponentSchema, MultipleCapable} from '..'; type Validator = 'required'; type TranslatableKeys = 'label' | 'description' | 'tooltip'; -export type EmailInputSchema = InputComponentSchema; +export interface EmailExtensions { + requireVerification?: boolean; +} + +export type EmailInputSchema = InputComponentSchema< + string, + Validator, + TranslatableKeys, + EmailExtensions +>; /** * @group Form.io components diff --git a/test-d/formio/components/email.test-d.ts b/test-d/formio/components/email.test-d.ts index fe177b3..128c214 100644 --- a/test-d/formio/components/email.test-d.ts +++ b/test-d/formio/components/email.test-d.ts @@ -85,6 +85,7 @@ expectAssignable({ translations: { nl: {label: 'foo'}, }, + requireVerification: true, }, // fixed but not editable validateOn: 'blur',