@defra/forms-engine-plugin 4.6.1 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.server/server/plugins/engine/components/CheckboxesField.d.ts +10 -1
- package/.server/server/plugins/engine/components/CheckboxesField.js +39 -0
- package/.server/server/plugins/engine/components/CheckboxesField.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/validationOptions.js +4 -1
- package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
- package/package.json +2 -2
- package/src/server/plugins/engine/components/CheckboxesField.test.ts +56 -1
- package/src/server/plugins/engine/components/CheckboxesField.ts +40 -0
- package/src/server/plugins/engine/pageControllers/validationOptions.ts +4 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type CheckboxesFieldComponent, type Item } from '@defra/forms-model';
|
|
2
2
|
import { type ArraySchema } from 'joi';
|
|
3
3
|
import { SelectionControlField } from '~/src/server/plugins/engine/components/SelectionControlField.js';
|
|
4
|
-
import { type FormState, type FormStateValue, type FormSubmissionState } from '~/src/server/plugins/engine/types.js';
|
|
4
|
+
import { type ErrorMessageTemplateList, type FormState, type FormStateValue, type FormSubmissionState } from '~/src/server/plugins/engine/types.js';
|
|
5
5
|
export declare class CheckboxesField extends SelectionControlField {
|
|
6
6
|
options: CheckboxesFieldComponent['options'];
|
|
7
|
+
schema: CheckboxesFieldComponent['schema'];
|
|
7
8
|
formSchema: ArraySchema<string> | ArraySchema<number>;
|
|
8
9
|
stateSchema: ArraySchema<string> | ArraySchema<number>;
|
|
9
10
|
constructor(def: CheckboxesFieldComponent, props: ConstructorParameters<typeof SelectionControlField>[1]);
|
|
@@ -13,5 +14,13 @@ export declare class CheckboxesField extends SelectionControlField {
|
|
|
13
14
|
getContextValueFromFormValue(values: (string | number | boolean)[] | undefined): (string | number | boolean)[];
|
|
14
15
|
getDisplayStringFromState(state: FormSubmissionState): string;
|
|
15
16
|
getContextValueFromState(state: FormSubmissionState): (string | number | boolean)[];
|
|
17
|
+
/**
|
|
18
|
+
* For error preview page that shows all possible errors on a component
|
|
19
|
+
*/
|
|
20
|
+
getAllPossibleErrors(): ErrorMessageTemplateList;
|
|
21
|
+
/**
|
|
22
|
+
* Static version of getAllPossibleErrors that doesn't require a component instance.
|
|
23
|
+
*/
|
|
24
|
+
static getAllPossibleErrors(): ErrorMessageTemplateList;
|
|
16
25
|
isValue(value?: FormStateValue | FormState): value is Item['value'][];
|
|
17
26
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import joi from 'joi';
|
|
2
2
|
import { isFormValue } from "./FormComponent.js";
|
|
3
3
|
import { SelectionControlField } from "./SelectionControlField.js";
|
|
4
|
+
import { messageTemplate } from "../pageControllers/validationOptions.js";
|
|
4
5
|
export class CheckboxesField extends SelectionControlField {
|
|
5
6
|
constructor(def, props) {
|
|
6
7
|
super(def, props);
|
|
@@ -10,12 +11,23 @@ export class CheckboxesField extends SelectionControlField {
|
|
|
10
11
|
const {
|
|
11
12
|
options
|
|
12
13
|
} = def;
|
|
14
|
+
const schema = 'schema' in def ? def.schema : {};
|
|
13
15
|
let formSchema = type === 'string' ? joi.array() : joi.array();
|
|
14
16
|
const itemsSchema = joi[type]().valid(...this.values).label(this.label);
|
|
15
17
|
formSchema = formSchema.items(itemsSchema).single().label(this.label).required();
|
|
16
18
|
if (options.required === false) {
|
|
17
19
|
formSchema = formSchema.optional();
|
|
18
20
|
}
|
|
21
|
+
if (typeof schema?.length === 'number') {
|
|
22
|
+
formSchema = formSchema.length(schema.length);
|
|
23
|
+
} else {
|
|
24
|
+
if (typeof schema?.min === 'number') {
|
|
25
|
+
formSchema = formSchema.min(schema.min);
|
|
26
|
+
}
|
|
27
|
+
if (typeof schema?.max === 'number') {
|
|
28
|
+
formSchema = formSchema.max(schema.max);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
19
31
|
this.formSchema = formSchema.default([]);
|
|
20
32
|
this.stateSchema = formSchema.default(null).allow(null);
|
|
21
33
|
this.options = options;
|
|
@@ -72,6 +84,33 @@ export class CheckboxesField extends SelectionControlField {
|
|
|
72
84
|
const values = this.getFormValueFromState(state);
|
|
73
85
|
return this.getContextValueFromFormValue(values);
|
|
74
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* For error preview page that shows all possible errors on a component
|
|
90
|
+
*/
|
|
91
|
+
getAllPossibleErrors() {
|
|
92
|
+
return CheckboxesField.getAllPossibleErrors();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Static version of getAllPossibleErrors that doesn't require a component instance.
|
|
97
|
+
*/
|
|
98
|
+
static getAllPossibleErrors() {
|
|
99
|
+
const parentErrors = SelectionControlField.getAllPossibleErrors();
|
|
100
|
+
return {
|
|
101
|
+
...parentErrors,
|
|
102
|
+
advancedSettingsErrors: [...parentErrors.advancedSettingsErrors, {
|
|
103
|
+
type: 'array.min',
|
|
104
|
+
template: messageTemplate.arrayMin
|
|
105
|
+
}, {
|
|
106
|
+
type: 'array.max',
|
|
107
|
+
template: messageTemplate.arrayMax
|
|
108
|
+
}, {
|
|
109
|
+
type: 'array.length',
|
|
110
|
+
template: messageTemplate.arrayLength
|
|
111
|
+
}]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
75
114
|
isValue(value) {
|
|
76
115
|
if (!Array.isArray(value)) {
|
|
77
116
|
return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CheckboxesField.js","names":["joi","isFormValue","SelectionControlField","CheckboxesField","constructor","def","props","listType","type","options","formSchema","array","itemsSchema","valid","values","label","items","single","required","optional","default","stateSchema","allow","getFormValueFromState","state","name","getFormValue","selected","filter","item","includes","value","map","
|
|
1
|
+
{"version":3,"file":"CheckboxesField.js","names":["joi","isFormValue","SelectionControlField","messageTemplate","CheckboxesField","constructor","def","props","listType","type","options","schema","formSchema","array","itemsSchema","valid","values","label","items","single","required","optional","length","min","max","default","stateSchema","allow","getFormValueFromState","state","name","getFormValue","selected","filter","item","includes","value","map","undefined","isValue","getDisplayStringFromFormValue","text","join","getContextValueFromFormValue","getDisplayStringFromState","getContextValueFromState","getAllPossibleErrors","parentErrors","advancedSettingsErrors","template","arrayMin","arrayMax","arrayLength","Array","isArray","every"],"sources":["../../../../../src/server/plugins/engine/components/CheckboxesField.ts"],"sourcesContent":["import { type CheckboxesFieldComponent, type Item } from '@defra/forms-model'\nimport joi, { type ArraySchema } from 'joi'\n\nimport { isFormValue } from '~/src/server/plugins/engine/components/FormComponent.js'\nimport { SelectionControlField } from '~/src/server/plugins/engine/components/SelectionControlField.js'\nimport { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'\nimport { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'\nimport { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'\nimport {\n type ErrorMessageTemplateList,\n type FormState,\n type FormStateValue,\n type FormSubmissionState\n} from '~/src/server/plugins/engine/types.js'\n\nexport class CheckboxesField extends SelectionControlField {\n declare options: CheckboxesFieldComponent['options']\n declare schema: CheckboxesFieldComponent['schema']\n declare formSchema: ArraySchema<string> | ArraySchema<number>\n declare stateSchema: ArraySchema<string> | ArraySchema<number>\n\n constructor(\n def: CheckboxesFieldComponent,\n props: ConstructorParameters<typeof SelectionControlField>[1]\n ) {\n super(def, props)\n\n const { listType: type } = this\n const { options } = def\n const schema = 'schema' in def ? def.schema : {}\n\n let formSchema =\n type === 'string' ? joi.array<string>() : joi.array<number>()\n\n const itemsSchema = joi[type]()\n .valid(...this.values)\n .label(this.label)\n\n formSchema = formSchema\n .items(itemsSchema)\n .single()\n .label(this.label)\n .required()\n\n if (options.required === false) {\n formSchema = formSchema.optional()\n }\n\n if (typeof schema?.length === 'number') {\n formSchema = formSchema.length(schema.length)\n } else {\n if (typeof schema?.min === 'number') {\n formSchema = formSchema.min(schema.min)\n }\n\n if (typeof schema?.max === 'number') {\n formSchema = formSchema.max(schema.max)\n }\n }\n\n this.formSchema = formSchema.default([])\n this.stateSchema = formSchema.default(null).allow(null)\n this.options = options\n }\n\n getFormValueFromState(state: FormSubmissionState) {\n const { items, name } = this\n\n // State checkbox values\n const values = this.getFormValue(state[name]) ?? []\n\n // Map (or discard) state values to item values\n const selected = items\n .filter((item) => values.includes(item.value))\n .map((item) => item.value)\n\n return selected.length ? selected : undefined\n }\n\n getFormValue(value?: FormStateValue | FormState) {\n return this.isValue(value) ? value : undefined\n }\n\n getDisplayStringFromFormValue(\n selected: (string | number | boolean)[] | undefined\n ) {\n const { items } = this\n\n if (!selected) {\n return ''\n }\n\n // Map selected values to text\n return items\n .filter((item) => selected.includes(item.value))\n .map((item) => item.text)\n .join(', ')\n }\n\n getContextValueFromFormValue(\n values: (string | number | boolean)[] | undefined\n ): (string | number | boolean)[] {\n /**\n * For evaluation context purposes, optional {@link CheckboxesField}\n * with an undefined value (i.e. nothing selected) should default to [].\n * This way conditions are not evaluated against `undefined` which throws errors.\n * Currently these errors are caught and the evaluation returns default `false`.\n * @see {@link QuestionPageController.getNextPath} for `undefined` return value\n * @see {@link FormModel.makeCondition} for try/catch block with default `false`\n * For negative conditions this is a problem because E.g.\n * The condition: 'selectedchecks' does not contain 'someval'\n * should return true IF 'selectedchecks' is undefined, not throw and return false.\n */\n return values ?? []\n }\n\n getDisplayStringFromState(state: FormSubmissionState) {\n // Selected checkbox values\n const selected = this.getFormValueFromState(state) ?? []\n\n // Map selected values to text\n return this.getDisplayStringFromFormValue(selected)\n }\n\n getContextValueFromState(state: FormSubmissionState) {\n const values = this.getFormValueFromState(state)\n\n return this.getContextValueFromFormValue(values)\n }\n\n /**\n * For error preview page that shows all possible errors on a component\n */\n getAllPossibleErrors(): ErrorMessageTemplateList {\n return CheckboxesField.getAllPossibleErrors()\n }\n\n /**\n * Static version of getAllPossibleErrors that doesn't require a component instance.\n */\n static getAllPossibleErrors(): ErrorMessageTemplateList {\n const parentErrors = SelectionControlField.getAllPossibleErrors()\n\n return {\n ...parentErrors,\n advancedSettingsErrors: [\n ...parentErrors.advancedSettingsErrors,\n { type: 'array.min', template: messageTemplate.arrayMin },\n { type: 'array.max', template: messageTemplate.arrayMax },\n { type: 'array.length', template: messageTemplate.arrayLength }\n ]\n }\n }\n\n isValue(value?: FormStateValue | FormState): value is Item['value'][] {\n if (!Array.isArray(value)) {\n return false\n }\n\n // Skip checks when empty\n if (!value.length) {\n return true\n }\n\n return value.every(isFormValue)\n }\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -34,7 +34,10 @@ export const messageTemplate = {
|
|
|
34
34
|
objectMissing: joi.expression('{{#title}} must include a {{lowerFirst(#label)}}', opts),
|
|
35
35
|
dateFormat: '{{#title}} must be a real date',
|
|
36
36
|
dateMin: '{{#title}} must be the same as or after {{#limit}}',
|
|
37
|
-
dateMax: '{{#title}} must be the same as or before {{#limit}}'
|
|
37
|
+
dateMax: '{{#title}} must be the same as or before {{#limit}}',
|
|
38
|
+
arrayMax: 'Only {{#limit}} can be selected from the list',
|
|
39
|
+
arrayMin: 'Select at least {{#limit}} options from the list',
|
|
40
|
+
arrayLength: 'Select only {{#limit}} options from the list'
|
|
38
41
|
};
|
|
39
42
|
export const messages = {
|
|
40
43
|
'string.base': messageTemplate.required,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validationOptions.js","names":["joi","lowerFirstPreserveProperNouns","opts","functions","lowerFirst","messageTemplate","declarationRequired","expression","required","selectRequired","selectYesNoRequired","max","min","minMax","pattern","format","unicode","number","numberPrecision","numberInteger","numberMin","numberMax","maxWords","objectRequired","objectMissing","dateFormat","dateMin","dateMax","messages","messagesPre","validationOptions","abortEarly","errors","wrap","array","label"],"sources":["../../../../../src/server/plugins/engine/pageControllers/validationOptions.ts"],"sourcesContent":["// Declaration above is needed for: https://github.com/hapijs/joi/issues/3064\n\nimport joi, {\n type JoiExpression,\n type LanguageMessages,\n type LanguageMessagesExt,\n type ReferenceOptions,\n type ValidationOptions\n} from 'joi'\n\nimport { lowerFirstPreserveProperNouns } from '~/src/server/plugins/engine/components/helpers/index.js'\n\nconst opts = {\n functions: {\n lowerFirst: lowerFirstPreserveProperNouns\n }\n} as ReferenceOptions\n\n/**\n * see @link https://joi.dev/api/?v=17.4.2#template-syntax for template syntax\n */\nexport const messageTemplate: Record<string, JoiExpression> = {\n declarationRequired: joi.expression(\n 'You must confirm you understand and agree with the {{lowerFirst(#label)}} to continue',\n opts\n ) as JoiExpression,\n required: joi.expression(\n 'Enter {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n selectRequired: joi.expression(\n 'Select {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n selectYesNoRequired: '{{#label}} - select yes or no',\n max: '{{#label}} must be {{#limit}} characters or less',\n min: '{{#label}} must be {{#limit}} characters or more',\n minMax: '{{#label}} must be between {{#min}} and {{#max}} characters',\n pattern: joi.expression(\n 'Enter a valid {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n format: joi.expression(\n 'Enter {{lowerFirst(#label)}} in the correct format',\n opts\n ) as JoiExpression,\n unicode: '{{#label}} includes invalid characters, for example, long dashes',\n number: '{{#label}} must be a number',\n numberPrecision: '{{#label}} must have {{#limit}} or fewer decimal places',\n numberInteger: '{{#label}} must be a whole number',\n numberMin: '{{#label}} must be {{#limit}} or higher',\n numberMax: '{{#label}} must be {{#limit}} or lower',\n maxWords: '{{#label}} must be {{#limit}} words or fewer',\n\n // Nested fields use component title\n\n objectRequired: joi.expression('Enter {{#label}}', opts) as JoiExpression,\n objectMissing: joi.expression(\n '{{#title}} must include a {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n dateFormat: '{{#title}} must be a real date',\n dateMin: '{{#title}} must be the same as or after {{#limit}}',\n dateMax: '{{#title}} must be the same as or before {{#limit}}'\n}\n\nexport const messages: LanguageMessagesExt = {\n 'string.base': messageTemplate.required,\n 'string.min': messageTemplate.min,\n 'string.empty': messageTemplate.required,\n 'string.max': messageTemplate.max,\n 'string.email': messageTemplate.format,\n 'string.unicode': messageTemplate.unicode,\n 'string.pattern.base': messageTemplate.pattern,\n 'string.maxWords': messageTemplate.maxWords,\n\n 'number.base': messageTemplate.number,\n 'number.precision': messageTemplate.numberPrecision,\n 'number.integer': messageTemplate.numberInteger,\n 'number.unsafe': messageTemplate.format,\n 'number.min': messageTemplate.numberMin,\n 'number.max': messageTemplate.numberMax,\n\n 'object.required': messageTemplate.objectRequired,\n 'object.and': messageTemplate.objectMissing,\n\n 'any.only': messageTemplate.selectRequired,\n 'any.required': messageTemplate.selectRequired,\n 'any.empty': messageTemplate.required,\n\n 'date.base': messageTemplate.dateFormat,\n 'date.format': messageTemplate.dateFormat,\n 'date.min': messageTemplate.dateMin,\n 'date.max': messageTemplate.dateMax,\n\n 'object.invalidjson': messageTemplate.format\n}\n\nexport const messagesPre: LanguageMessages =\n messages as unknown as LanguageMessages\n\nexport const validationOptions: ValidationOptions = {\n abortEarly: false,\n messages: messagesPre,\n errors: {\n wrap: {\n array: false,\n label: false\n }\n }\n}\n"],"mappings":"AAAA;;AAEA,OAAOA,GAAG,MAMH,KAAK;AAEZ,SAASC,6BAA6B;AAEtC,MAAMC,IAAI,GAAG;EACXC,SAAS,EAAE;IACTC,UAAU,EAAEH;EACd;AACF,CAAqB;;AAErB;AACA;AACA;AACA,OAAO,MAAMI,eAA8C,GAAG;EAC5DC,mBAAmB,EAAEN,GAAG,CAACO,UAAU,CACjC,uFAAuF,EACvFL,IACF,CAAkB;EAClBM,QAAQ,EAAER,GAAG,CAACO,UAAU,CACtB,8BAA8B,EAC9BL,IACF,CAAkB;EAClBO,cAAc,EAAET,GAAG,CAACO,UAAU,CAC5B,+BAA+B,EAC/BL,IACF,CAAkB;EAClBQ,mBAAmB,EAAE,+BAA+B;EACpDC,GAAG,EAAE,kDAAkD;EACvDC,GAAG,EAAE,kDAAkD;EACvDC,MAAM,EAAE,6DAA6D;EACrEC,OAAO,EAAEd,GAAG,CAACO,UAAU,CACrB,sCAAsC,EACtCL,IACF,CAAkB;EAClBa,MAAM,EAAEf,GAAG,CAACO,UAAU,CACpB,oDAAoD,EACpDL,IACF,CAAkB;EAClBc,OAAO,EAAE,kEAAkE;EAC3EC,MAAM,EAAE,6BAA6B;EACrCC,eAAe,EAAE,yDAAyD;EAC1EC,aAAa,EAAE,mCAAmC;EAClDC,SAAS,EAAE,yCAAyC;EACpDC,SAAS,EAAE,wCAAwC;EACnDC,QAAQ,EAAE,8CAA8C;EAExD;;EAEAC,cAAc,EAAEvB,GAAG,CAACO,UAAU,CAAC,kBAAkB,EAAEL,IAAI,CAAkB;EACzEsB,aAAa,EAAExB,GAAG,CAACO,UAAU,CAC3B,kDAAkD,EAClDL,IACF,CAAkB;EAClBuB,UAAU,EAAE,gCAAgC;EAC5CC,OAAO,EAAE,oDAAoD;EAC7DC,OAAO,EAAE;
|
|
1
|
+
{"version":3,"file":"validationOptions.js","names":["joi","lowerFirstPreserveProperNouns","opts","functions","lowerFirst","messageTemplate","declarationRequired","expression","required","selectRequired","selectYesNoRequired","max","min","minMax","pattern","format","unicode","number","numberPrecision","numberInteger","numberMin","numberMax","maxWords","objectRequired","objectMissing","dateFormat","dateMin","dateMax","arrayMax","arrayMin","arrayLength","messages","messagesPre","validationOptions","abortEarly","errors","wrap","array","label"],"sources":["../../../../../src/server/plugins/engine/pageControllers/validationOptions.ts"],"sourcesContent":["// Declaration above is needed for: https://github.com/hapijs/joi/issues/3064\n\nimport joi, {\n type JoiExpression,\n type LanguageMessages,\n type LanguageMessagesExt,\n type ReferenceOptions,\n type ValidationOptions\n} from 'joi'\n\nimport { lowerFirstPreserveProperNouns } from '~/src/server/plugins/engine/components/helpers/index.js'\n\nconst opts = {\n functions: {\n lowerFirst: lowerFirstPreserveProperNouns\n }\n} as ReferenceOptions\n\n/**\n * see @link https://joi.dev/api/?v=17.4.2#template-syntax for template syntax\n */\nexport const messageTemplate: Record<string, JoiExpression> = {\n declarationRequired: joi.expression(\n 'You must confirm you understand and agree with the {{lowerFirst(#label)}} to continue',\n opts\n ) as JoiExpression,\n required: joi.expression(\n 'Enter {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n selectRequired: joi.expression(\n 'Select {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n selectYesNoRequired: '{{#label}} - select yes or no',\n max: '{{#label}} must be {{#limit}} characters or less',\n min: '{{#label}} must be {{#limit}} characters or more',\n minMax: '{{#label}} must be between {{#min}} and {{#max}} characters',\n pattern: joi.expression(\n 'Enter a valid {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n format: joi.expression(\n 'Enter {{lowerFirst(#label)}} in the correct format',\n opts\n ) as JoiExpression,\n unicode: '{{#label}} includes invalid characters, for example, long dashes',\n number: '{{#label}} must be a number',\n numberPrecision: '{{#label}} must have {{#limit}} or fewer decimal places',\n numberInteger: '{{#label}} must be a whole number',\n numberMin: '{{#label}} must be {{#limit}} or higher',\n numberMax: '{{#label}} must be {{#limit}} or lower',\n maxWords: '{{#label}} must be {{#limit}} words or fewer',\n\n // Nested fields use component title\n\n objectRequired: joi.expression('Enter {{#label}}', opts) as JoiExpression,\n objectMissing: joi.expression(\n '{{#title}} must include a {{lowerFirst(#label)}}',\n opts\n ) as JoiExpression,\n dateFormat: '{{#title}} must be a real date',\n dateMin: '{{#title}} must be the same as or after {{#limit}}',\n dateMax: '{{#title}} must be the same as or before {{#limit}}',\n arrayMax: 'Only {{#limit}} can be selected from the list',\n arrayMin: 'Select at least {{#limit}} options from the list',\n arrayLength: 'Select only {{#limit}} options from the list'\n}\n\nexport const messages: LanguageMessagesExt = {\n 'string.base': messageTemplate.required,\n 'string.min': messageTemplate.min,\n 'string.empty': messageTemplate.required,\n 'string.max': messageTemplate.max,\n 'string.email': messageTemplate.format,\n 'string.unicode': messageTemplate.unicode,\n 'string.pattern.base': messageTemplate.pattern,\n 'string.maxWords': messageTemplate.maxWords,\n\n 'number.base': messageTemplate.number,\n 'number.precision': messageTemplate.numberPrecision,\n 'number.integer': messageTemplate.numberInteger,\n 'number.unsafe': messageTemplate.format,\n 'number.min': messageTemplate.numberMin,\n 'number.max': messageTemplate.numberMax,\n\n 'object.required': messageTemplate.objectRequired,\n 'object.and': messageTemplate.objectMissing,\n\n 'any.only': messageTemplate.selectRequired,\n 'any.required': messageTemplate.selectRequired,\n 'any.empty': messageTemplate.required,\n\n 'date.base': messageTemplate.dateFormat,\n 'date.format': messageTemplate.dateFormat,\n 'date.min': messageTemplate.dateMin,\n 'date.max': messageTemplate.dateMax,\n\n 'object.invalidjson': messageTemplate.format\n}\n\nexport const messagesPre: LanguageMessages =\n messages as unknown as LanguageMessages\n\nexport const validationOptions: ValidationOptions = {\n abortEarly: false,\n messages: messagesPre,\n errors: {\n wrap: {\n array: false,\n label: false\n }\n }\n}\n"],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defra/forms-engine-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"description": "Defra forms engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
},
|
|
84
84
|
"license": "SEE LICENSE IN LICENSE",
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@defra/forms-model": "^3.0.
|
|
86
|
+
"@defra/forms-model": "^3.0.647",
|
|
87
87
|
"@defra/hapi-tracing": "^1.29.0",
|
|
88
88
|
"@defra/interactive-map": "^0.0.17-alpha",
|
|
89
89
|
"@elastic/ecs-pino-format": "^1.5.0",
|
|
@@ -173,6 +173,61 @@ describe.each([
|
|
|
173
173
|
)
|
|
174
174
|
})
|
|
175
175
|
|
|
176
|
+
it('is configured with min/max items', () => {
|
|
177
|
+
const collectionLimited = new ComponentCollection(
|
|
178
|
+
[{ ...def, schema: { min: 2, max: 4 } }],
|
|
179
|
+
{ model }
|
|
180
|
+
)
|
|
181
|
+
const { formSchema } = collectionLimited
|
|
182
|
+
const { keys } = formSchema.describe()
|
|
183
|
+
|
|
184
|
+
expect(keys).toHaveProperty(
|
|
185
|
+
'myComponent',
|
|
186
|
+
expect.objectContaining({
|
|
187
|
+
items: [
|
|
188
|
+
{
|
|
189
|
+
allow: options.allow,
|
|
190
|
+
flags: {
|
|
191
|
+
label: def.shortDescription,
|
|
192
|
+
only: true
|
|
193
|
+
},
|
|
194
|
+
type: options.list.type
|
|
195
|
+
}
|
|
196
|
+
],
|
|
197
|
+
rules: [
|
|
198
|
+
{ args: { limit: 2 }, name: 'min' },
|
|
199
|
+
{ args: { limit: 4 }, name: 'max' }
|
|
200
|
+
]
|
|
201
|
+
})
|
|
202
|
+
)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('is configured with length items', () => {
|
|
206
|
+
const collectionLimited = new ComponentCollection(
|
|
207
|
+
[{ ...def, schema: { length: 3 } }],
|
|
208
|
+
{ model }
|
|
209
|
+
)
|
|
210
|
+
const { formSchema } = collectionLimited
|
|
211
|
+
const { keys } = formSchema.describe()
|
|
212
|
+
|
|
213
|
+
expect(keys).toHaveProperty(
|
|
214
|
+
'myComponent',
|
|
215
|
+
expect.objectContaining({
|
|
216
|
+
items: [
|
|
217
|
+
{
|
|
218
|
+
allow: options.allow,
|
|
219
|
+
flags: {
|
|
220
|
+
label: def.shortDescription,
|
|
221
|
+
only: true
|
|
222
|
+
},
|
|
223
|
+
type: options.list.type
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
rules: [{ args: { limit: 3 }, name: 'length' }]
|
|
227
|
+
})
|
|
228
|
+
)
|
|
229
|
+
})
|
|
230
|
+
|
|
176
231
|
it('adds errors for empty value', () => {
|
|
177
232
|
const result = collection.validate(getFormData())
|
|
178
233
|
|
|
@@ -386,7 +441,7 @@ describe.each([
|
|
|
386
441
|
it('should return errors', () => {
|
|
387
442
|
const errors = field.getAllPossibleErrors()
|
|
388
443
|
expect(errors.baseErrors).not.toBeEmpty()
|
|
389
|
-
expect(errors.advancedSettingsErrors).toBeEmpty()
|
|
444
|
+
expect(errors.advancedSettingsErrors).not.toBeEmpty()
|
|
390
445
|
})
|
|
391
446
|
})
|
|
392
447
|
|
|
@@ -5,7 +5,9 @@ import { isFormValue } from '~/src/server/plugins/engine/components/FormComponen
|
|
|
5
5
|
import { SelectionControlField } from '~/src/server/plugins/engine/components/SelectionControlField.js'
|
|
6
6
|
import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
7
7
|
import { type QuestionPageController } from '~/src/server/plugins/engine/pageControllers/QuestionPageController.js'
|
|
8
|
+
import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
|
|
8
9
|
import {
|
|
10
|
+
type ErrorMessageTemplateList,
|
|
9
11
|
type FormState,
|
|
10
12
|
type FormStateValue,
|
|
11
13
|
type FormSubmissionState
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
|
|
14
16
|
export class CheckboxesField extends SelectionControlField {
|
|
15
17
|
declare options: CheckboxesFieldComponent['options']
|
|
18
|
+
declare schema: CheckboxesFieldComponent['schema']
|
|
16
19
|
declare formSchema: ArraySchema<string> | ArraySchema<number>
|
|
17
20
|
declare stateSchema: ArraySchema<string> | ArraySchema<number>
|
|
18
21
|
|
|
@@ -24,6 +27,7 @@ export class CheckboxesField extends SelectionControlField {
|
|
|
24
27
|
|
|
25
28
|
const { listType: type } = this
|
|
26
29
|
const { options } = def
|
|
30
|
+
const schema = 'schema' in def ? def.schema : {}
|
|
27
31
|
|
|
28
32
|
let formSchema =
|
|
29
33
|
type === 'string' ? joi.array<string>() : joi.array<number>()
|
|
@@ -42,6 +46,18 @@ export class CheckboxesField extends SelectionControlField {
|
|
|
42
46
|
formSchema = formSchema.optional()
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
if (typeof schema?.length === 'number') {
|
|
50
|
+
formSchema = formSchema.length(schema.length)
|
|
51
|
+
} else {
|
|
52
|
+
if (typeof schema?.min === 'number') {
|
|
53
|
+
formSchema = formSchema.min(schema.min)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof schema?.max === 'number') {
|
|
57
|
+
formSchema = formSchema.max(schema.max)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
this.formSchema = formSchema.default([])
|
|
46
62
|
this.stateSchema = formSchema.default(null).allow(null)
|
|
47
63
|
this.options = options
|
|
@@ -112,6 +128,30 @@ export class CheckboxesField extends SelectionControlField {
|
|
|
112
128
|
return this.getContextValueFromFormValue(values)
|
|
113
129
|
}
|
|
114
130
|
|
|
131
|
+
/**
|
|
132
|
+
* For error preview page that shows all possible errors on a component
|
|
133
|
+
*/
|
|
134
|
+
getAllPossibleErrors(): ErrorMessageTemplateList {
|
|
135
|
+
return CheckboxesField.getAllPossibleErrors()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Static version of getAllPossibleErrors that doesn't require a component instance.
|
|
140
|
+
*/
|
|
141
|
+
static getAllPossibleErrors(): ErrorMessageTemplateList {
|
|
142
|
+
const parentErrors = SelectionControlField.getAllPossibleErrors()
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
...parentErrors,
|
|
146
|
+
advancedSettingsErrors: [
|
|
147
|
+
...parentErrors.advancedSettingsErrors,
|
|
148
|
+
{ type: 'array.min', template: messageTemplate.arrayMin },
|
|
149
|
+
{ type: 'array.max', template: messageTemplate.arrayMax },
|
|
150
|
+
{ type: 'array.length', template: messageTemplate.arrayLength }
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
115
155
|
isValue(value?: FormStateValue | FormState): value is Item['value'][] {
|
|
116
156
|
if (!Array.isArray(value)) {
|
|
117
157
|
return false
|
|
@@ -61,7 +61,10 @@ export const messageTemplate: Record<string, JoiExpression> = {
|
|
|
61
61
|
) as JoiExpression,
|
|
62
62
|
dateFormat: '{{#title}} must be a real date',
|
|
63
63
|
dateMin: '{{#title}} must be the same as or after {{#limit}}',
|
|
64
|
-
dateMax: '{{#title}} must be the same as or before {{#limit}}'
|
|
64
|
+
dateMax: '{{#title}} must be the same as or before {{#limit}}',
|
|
65
|
+
arrayMax: 'Only {{#limit}} can be selected from the list',
|
|
66
|
+
arrayMin: 'Select at least {{#limit}} options from the list',
|
|
67
|
+
arrayLength: 'Select only {{#limit}} options from the list'
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
export const messages: LanguageMessagesExt = {
|