@defra/forms-engine-plugin 0.1.11 → 0.1.12
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/.public/javascripts/file-upload.min.js +1 -1
- package/.public/javascripts/file-upload.min.js.map +1 -1
- package/.server/client/javascripts/file-upload.js +45 -4
- package/.server/client/javascripts/file-upload.js.map +1 -1
- package/.server/server/constants.js +2 -0
- package/.server/server/constants.js.map +1 -1
- package/.server/server/index.js +1 -1
- package/.server/server/index.js.map +1 -1
- package/.server/server/plugins/engine/components/AutocompleteField.js +2 -0
- package/.server/server/plugins/engine/components/AutocompleteField.js.map +1 -1
- package/.server/server/plugins/engine/components/CheckboxesField.js +3 -4
- package/.server/server/plugins/engine/components/CheckboxesField.js.map +1 -1
- package/.server/server/plugins/engine/components/ComponentCollection.js +37 -16
- package/.server/server/plugins/engine/components/ComponentCollection.js.map +1 -1
- package/.server/server/plugins/engine/components/DatePartsField.js +36 -2
- package/.server/server/plugins/engine/components/DatePartsField.js.map +1 -1
- package/.server/server/plugins/engine/components/EmailAddressField.js +19 -3
- package/.server/server/plugins/engine/components/EmailAddressField.js.map +1 -1
- package/.server/server/plugins/engine/components/FileUploadField.js +44 -4
- package/.server/server/plugins/engine/components/FileUploadField.js.map +1 -1
- package/.server/server/plugins/engine/components/FormComponent.js +14 -2
- package/.server/server/plugins/engine/components/FormComponent.js.map +1 -1
- package/.server/server/plugins/engine/components/ListFormComponent.js +16 -3
- package/.server/server/plugins/engine/components/ListFormComponent.js.map +1 -1
- package/.server/server/plugins/engine/components/Markdown.js +24 -0
- package/.server/server/plugins/engine/components/Markdown.js.map +1 -0
- package/.server/server/plugins/engine/components/MonthYearField.js +30 -2
- package/.server/server/plugins/engine/components/MonthYearField.js.map +1 -1
- package/.server/server/plugins/engine/components/MultilineTextField.js +32 -3
- package/.server/server/plugins/engine/components/MultilineTextField.js.map +1 -1
- package/.server/server/plugins/engine/components/NumberField.js +28 -3
- package/.server/server/plugins/engine/components/NumberField.js.map +1 -1
- package/.server/server/plugins/engine/components/SelectionControlField.js +14 -0
- package/.server/server/plugins/engine/components/SelectionControlField.js.map +1 -1
- package/.server/server/plugins/engine/components/TelephoneNumberField.js +19 -3
- package/.server/server/plugins/engine/components/TelephoneNumberField.js.map +1 -1
- package/.server/server/plugins/engine/components/TextField.js +22 -3
- package/.server/server/plugins/engine/components/TextField.js.map +1 -1
- package/.server/server/plugins/engine/components/UkAddressField.js +29 -0
- package/.server/server/plugins/engine/components/UkAddressField.js.map +1 -1
- package/.server/server/plugins/engine/components/YesNoField.js +18 -0
- package/.server/server/plugins/engine/components/YesNoField.js.map +1 -1
- package/.server/server/plugins/engine/components/helpers.js +16 -0
- package/.server/server/plugins/engine/components/helpers.js.map +1 -1
- package/.server/server/plugins/engine/components/index.js +1 -0
- package/.server/server/plugins/engine/components/index.js.map +1 -1
- package/.server/server/plugins/engine/configureEnginePlugin.js +3 -1
- package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
- package/.server/server/plugins/engine/helpers.js +38 -18
- package/.server/server/plugins/engine/helpers.js.map +1 -1
- package/.server/server/plugins/engine/models/FormModel.js +60 -2
- package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
- package/.server/server/plugins/engine/models/SummaryViewModel.js +3 -2
- package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
- package/.server/server/plugins/engine/outputFormatters/human/v1.js +1 -1
- package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/PageController.js +13 -5
- package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +2 -2
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +19 -5
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/validationOptions.js +6 -11
- package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
- package/.server/server/plugins/engine/plugin.js +5 -4
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/services/notifyService.js +1 -4
- package/.server/server/plugins/engine/services/notifyService.js.map +1 -1
- package/.server/server/plugins/engine/services/uploadService.js +5 -3
- package/.server/server/plugins/engine/services/uploadService.js.map +1 -1
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/plugins/engine/views/components/html.html +1 -1
- package/.server/server/plugins/engine/views/components/markdown.html +5 -0
- package/.server/server/plugins/engine/views/summary.html +7 -1
- package/.server/server/plugins/nunjucks/context.js +6 -5
- package/.server/server/plugins/nunjucks/context.js.map +1 -1
- package/.server/server/plugins/nunjucks/enviroment.test.js +6 -3
- package/.server/server/plugins/nunjucks/enviroment.test.js.map +1 -1
- package/.server/server/utils/type-utils.js +8 -0
- package/.server/server/utils/type-utils.js.map +1 -0
- package/.server/typings/joi/index.d.js.map +1 -1
- package/package.json +3 -3
- package/src/client/javascripts/file-upload.js +60 -4
- package/src/server/constants.js +2 -0
- package/src/server/index.test.ts +34 -29
- package/src/server/index.ts +2 -1
- package/src/server/plugins/engine/components/AutocompleteField.test.ts +71 -3
- package/src/server/plugins/engine/components/AutocompleteField.ts +6 -2
- package/src/server/plugins/engine/components/CheckboxesField.test.ts +40 -8
- package/src/server/plugins/engine/components/CheckboxesField.ts +7 -3
- package/src/server/plugins/engine/components/ComponentCollection.ts +45 -18
- package/src/server/plugins/engine/components/DatePartsField.test.ts +13 -4
- package/src/server/plugins/engine/components/DatePartsField.ts +29 -8
- package/src/server/plugins/engine/components/EmailAddressField.test.ts +51 -1
- package/src/server/plugins/engine/components/EmailAddressField.ts +17 -2
- package/src/server/plugins/engine/components/FileUploadField.test.ts +53 -0
- package/src/server/plugins/engine/components/FileUploadField.ts +52 -3
- package/src/server/plugins/engine/components/FormComponent.ts +24 -2
- package/src/server/plugins/engine/components/ListFormComponent.ts +16 -2
- package/src/server/plugins/engine/components/Markdown.test.ts +48 -0
- package/src/server/plugins/engine/components/Markdown.ts +29 -0
- package/src/server/plugins/engine/components/MonthYearField.test.ts +35 -0
- package/src/server/plugins/engine/components/MonthYearField.ts +34 -9
- package/src/server/plugins/engine/components/MultilineTextField.test.ts +83 -5
- package/src/server/plugins/engine/components/MultilineTextField.ts +37 -2
- package/src/server/plugins/engine/components/NumberField.test.ts +24 -2
- package/src/server/plugins/engine/components/NumberField.ts +23 -3
- package/src/server/plugins/engine/components/RadiosField.test.ts +10 -1
- package/src/server/plugins/engine/components/SelectField.test.ts +2 -1
- package/src/server/plugins/engine/components/SelectionControlField.ts +14 -0
- package/src/server/plugins/engine/components/TelephoneNumberField.test.ts +30 -2
- package/src/server/plugins/engine/components/TelephoneNumberField.ts +17 -2
- package/src/server/plugins/engine/components/TextField.test.ts +33 -1
- package/src/server/plugins/engine/components/TextField.ts +17 -2
- package/src/server/plugins/engine/components/UkAddressField.test.ts +46 -3
- package/src/server/plugins/engine/components/UkAddressField.ts +28 -0
- package/src/server/plugins/engine/components/YesNoField.test.ts +9 -1
- package/src/server/plugins/engine/components/YesNoField.ts +24 -0
- package/src/server/plugins/engine/components/helpers.test.ts +24 -0
- package/src/server/plugins/engine/components/helpers.ts +39 -0
- package/src/server/plugins/engine/components/index.ts +1 -0
- package/src/server/plugins/engine/configureEnginePlugin.ts +13 -3
- package/src/server/plugins/engine/helpers.test.ts +71 -20
- package/src/server/plugins/engine/helpers.ts +46 -19
- package/src/server/plugins/engine/models/FormModel.test.ts +91 -1
- package/src/server/plugins/engine/models/FormModel.ts +86 -3
- package/src/server/plugins/engine/models/SummaryViewModel.test.ts +46 -7
- package/src/server/plugins/engine/models/SummaryViewModel.ts +7 -3
- package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +1 -2
- package/src/server/plugins/engine/outputFormatters/human/v1.ts +1 -1
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +1 -0
- package/src/server/plugins/engine/pageControllers/PageController.test.ts +9 -6
- package/src/server/plugins/engine/pageControllers/PageController.ts +15 -5
- package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +2 -2
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +21 -6
- package/src/server/plugins/engine/pageControllers/validationOptions.ts +31 -17
- package/src/server/plugins/engine/plugin.ts +9 -5
- package/src/server/plugins/engine/services/notifyService.ts +1 -2
- package/src/server/plugins/engine/services/uploadService.js +10 -6
- package/src/server/plugins/engine/types.ts +10 -1
- package/src/server/plugins/engine/views/components/html.html +1 -1
- package/src/server/plugins/engine/views/components/markdown.html +5 -0
- package/src/server/plugins/engine/views/summary.html +7 -1
- package/src/server/plugins/nunjucks/context.js +4 -4
- package/src/server/plugins/nunjucks/enviroment.test.js +9 -3
- package/src/server/utils/type-utils.ts +15 -0
- package/src/typings/joi/index.d.ts +8 -0
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
ComponentType,
|
|
3
3
|
type AutocompleteFieldComponent
|
|
4
4
|
} from '@defra/forms-model'
|
|
5
|
+
import lowerFirst from 'lodash/lowerFirst.js'
|
|
5
6
|
|
|
6
7
|
import { AutocompleteField } from '~/src/server/plugins/engine/components/AutocompleteField.js'
|
|
7
8
|
import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
|
|
@@ -32,7 +33,8 @@ describe.each([
|
|
|
32
33
|
options: {
|
|
33
34
|
list: listString,
|
|
34
35
|
examples: listStringExamples,
|
|
35
|
-
allow: ['1', '2', '3', '4']
|
|
36
|
+
allow: ['1', '2', '3', '4'],
|
|
37
|
+
shortDescription: 'My string list'
|
|
36
38
|
}
|
|
37
39
|
},
|
|
38
40
|
{
|
|
@@ -47,7 +49,8 @@ describe.each([
|
|
|
47
49
|
options: {
|
|
48
50
|
list: listNumber,
|
|
49
51
|
examples: listNumberExamples,
|
|
50
|
-
allow: [1, 2, 3, 4]
|
|
52
|
+
allow: [1, 2, 3, 4],
|
|
53
|
+
shortDescription: 'My number list'
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
56
|
])('AutocompleteField: $component.title', ({ component: def, options }) => {
|
|
@@ -153,7 +156,35 @@ describe.each([
|
|
|
153
156
|
|
|
154
157
|
expect(result.errors).toEqual([
|
|
155
158
|
expect.objectContaining({
|
|
156
|
-
text: `Enter ${def.title
|
|
159
|
+
text: `Enter ${lowerFirst(def.title)}`
|
|
160
|
+
})
|
|
161
|
+
])
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('adds errors for empty value if shortDescription exists', () => {
|
|
165
|
+
collection = new ComponentCollection(
|
|
166
|
+
[{ ...def, shortDescription: options.shortDescription }],
|
|
167
|
+
{ model }
|
|
168
|
+
)
|
|
169
|
+
const result = collection.validate(getFormData(''))
|
|
170
|
+
|
|
171
|
+
expect(result.errors).toEqual([
|
|
172
|
+
expect.objectContaining({
|
|
173
|
+
text: `Enter ${lowerFirst(options.shortDescription)}`
|
|
174
|
+
})
|
|
175
|
+
])
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('adds errors for empty value if shortDescription exists but is empty', () => {
|
|
179
|
+
collection = new ComponentCollection(
|
|
180
|
+
[{ ...def, shortDescription: '' }],
|
|
181
|
+
{ model }
|
|
182
|
+
)
|
|
183
|
+
const result = collection.validate(getFormData(''))
|
|
184
|
+
|
|
185
|
+
expect(result.errors).toEqual([
|
|
186
|
+
expect.objectContaining({
|
|
187
|
+
text: `Enter ${lowerFirst(def.title)}`
|
|
157
188
|
})
|
|
158
189
|
])
|
|
159
190
|
})
|
|
@@ -290,5 +321,42 @@ describe.each([
|
|
|
290
321
|
expect(items).toEqual([])
|
|
291
322
|
})
|
|
292
323
|
})
|
|
324
|
+
|
|
325
|
+
describe('AllPossibleErrors', () => {
|
|
326
|
+
it('should return errors', () => {
|
|
327
|
+
const errors = field.getAllPossibleErrors()
|
|
328
|
+
expect(errors.baseErrors).not.toBeEmpty()
|
|
329
|
+
expect(errors.advancedSettingsErrors).toBeEmpty()
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
describe('Validation', () => {
|
|
334
|
+
describe.each([
|
|
335
|
+
{
|
|
336
|
+
description: 'Use short description if it exists',
|
|
337
|
+
component: {
|
|
338
|
+
title: 'What is your example text?',
|
|
339
|
+
shortDescription: 'Your example text',
|
|
340
|
+
name: 'myComponent',
|
|
341
|
+
type: ComponentType.AutocompleteField,
|
|
342
|
+
list: 'ABCE',
|
|
343
|
+
options: {}
|
|
344
|
+
} satisfies AutocompleteFieldComponent,
|
|
345
|
+
assertions: [
|
|
346
|
+
{
|
|
347
|
+
input: getFormData(''),
|
|
348
|
+
output: {
|
|
349
|
+
value: getFormData(''),
|
|
350
|
+
errors: [
|
|
351
|
+
expect.objectContaining({
|
|
352
|
+
text: 'Enter your example text'
|
|
353
|
+
})
|
|
354
|
+
]
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
]
|
|
358
|
+
}
|
|
359
|
+
])
|
|
360
|
+
})
|
|
293
361
|
})
|
|
294
362
|
})
|
|
@@ -23,8 +23,12 @@ export class AutocompleteField extends SelectField {
|
|
|
23
23
|
const messages = options.customValidationMessages
|
|
24
24
|
|
|
25
25
|
formSchema = formSchema.messages({
|
|
26
|
-
|
|
27
|
-
'any.
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
27
|
+
'any.only':
|
|
28
|
+
messages?.['any.only'] ?? (messageTemplate.required as string),
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
30
|
+
'any.required':
|
|
31
|
+
messages?.['any.required'] ?? (messageTemplate.required as string)
|
|
28
32
|
})
|
|
29
33
|
}
|
|
30
34
|
|
|
@@ -2,6 +2,8 @@ import {
|
|
|
2
2
|
ComponentType,
|
|
3
3
|
type CheckboxesFieldComponent
|
|
4
4
|
} from '@defra/forms-model'
|
|
5
|
+
import { toLower } from 'lodash'
|
|
6
|
+
import lowerFirst from 'lodash/lowerFirst.js'
|
|
5
7
|
import { outdent } from 'outdent'
|
|
6
8
|
|
|
7
9
|
import { CheckboxesField } from '~/src/server/plugins/engine/components/CheckboxesField.js'
|
|
@@ -23,7 +25,8 @@ import { getFormData, getFormState } from '~/test/helpers/component-helpers.js'
|
|
|
23
25
|
describe.each([
|
|
24
26
|
{
|
|
25
27
|
component: {
|
|
26
|
-
title: 'String list',
|
|
28
|
+
title: 'String list title',
|
|
29
|
+
shortDescription: 'String list',
|
|
27
30
|
name: 'myComponent',
|
|
28
31
|
type: ComponentType.CheckboxesField,
|
|
29
32
|
list: 'listString',
|
|
@@ -31,6 +34,7 @@ describe.each([
|
|
|
31
34
|
} satisfies CheckboxesFieldComponent,
|
|
32
35
|
|
|
33
36
|
options: {
|
|
37
|
+
label: 'string list',
|
|
34
38
|
list: listString,
|
|
35
39
|
examples: listStringExamples,
|
|
36
40
|
allow: ['1', '2', '3', '4'],
|
|
@@ -39,14 +43,34 @@ describe.each([
|
|
|
39
43
|
},
|
|
40
44
|
{
|
|
41
45
|
component: {
|
|
42
|
-
title: '
|
|
46
|
+
title: 'String list title',
|
|
47
|
+
shortDescription: 'String list',
|
|
43
48
|
name: 'myComponent',
|
|
44
49
|
type: ComponentType.CheckboxesField,
|
|
50
|
+
list: 'listString',
|
|
51
|
+
options: {}
|
|
52
|
+
} satisfies CheckboxesFieldComponent,
|
|
53
|
+
|
|
54
|
+
options: {
|
|
55
|
+
label: 'string list',
|
|
56
|
+
list: listString,
|
|
57
|
+
examples: listStringExamples,
|
|
58
|
+
allow: ['1', '2', '3', '4'],
|
|
59
|
+
deny: ['5', '6', '7', '8']
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
component: {
|
|
64
|
+
title: 'Number list title',
|
|
65
|
+
name: 'myComponent',
|
|
66
|
+
shortDescription: 'Number list',
|
|
67
|
+
type: ComponentType.CheckboxesField,
|
|
45
68
|
list: 'listNumber',
|
|
46
69
|
options: {}
|
|
47
70
|
} satisfies CheckboxesFieldComponent,
|
|
48
71
|
|
|
49
72
|
options: {
|
|
73
|
+
label: 'number list',
|
|
50
74
|
list: listNumber,
|
|
51
75
|
examples: listNumberExamples,
|
|
52
76
|
allow: [1, 2, 3, 4],
|
|
@@ -72,14 +96,14 @@ describe.each([
|
|
|
72
96
|
|
|
73
97
|
describe('Defaults', () => {
|
|
74
98
|
describe('Schema', () => {
|
|
75
|
-
it('uses component
|
|
99
|
+
it('uses component short description as label', () => {
|
|
76
100
|
const { formSchema } = collection
|
|
77
101
|
const { keys } = formSchema.describe()
|
|
78
102
|
|
|
79
103
|
expect(keys).toHaveProperty(
|
|
80
104
|
'myComponent',
|
|
81
105
|
expect.objectContaining({
|
|
82
|
-
flags: expect.objectContaining({ label: def.
|
|
106
|
+
flags: expect.objectContaining({ label: def.shortDescription })
|
|
83
107
|
})
|
|
84
108
|
)
|
|
85
109
|
})
|
|
@@ -157,7 +181,7 @@ describe.each([
|
|
|
157
181
|
{
|
|
158
182
|
allow: options.allow,
|
|
159
183
|
flags: {
|
|
160
|
-
label: def.
|
|
184
|
+
label: def.shortDescription,
|
|
161
185
|
only: true
|
|
162
186
|
},
|
|
163
187
|
type: options.list.type
|
|
@@ -172,7 +196,7 @@ describe.each([
|
|
|
172
196
|
|
|
173
197
|
expect(result.errors).toEqual([
|
|
174
198
|
expect.objectContaining({
|
|
175
|
-
text: `Select ${
|
|
199
|
+
text: `Select ${lowerFirst(options.label)}`
|
|
176
200
|
})
|
|
177
201
|
])
|
|
178
202
|
})
|
|
@@ -200,7 +224,7 @@ describe.each([
|
|
|
200
224
|
|
|
201
225
|
expect(result.errors).toEqual([
|
|
202
226
|
expect.objectContaining({
|
|
203
|
-
text: `Select ${def.
|
|
227
|
+
text: `Select ${toLower(def.shortDescription)}`
|
|
204
228
|
})
|
|
205
229
|
])
|
|
206
230
|
}
|
|
@@ -213,7 +237,7 @@ describe.each([
|
|
|
213
237
|
|
|
214
238
|
expect(result.errors).toEqual([
|
|
215
239
|
expect.objectContaining({
|
|
216
|
-
text: `Select ${
|
|
240
|
+
text: `Select ${lowerFirst(options.label)}`
|
|
217
241
|
})
|
|
218
242
|
])
|
|
219
243
|
}
|
|
@@ -375,5 +399,13 @@ describe.each([
|
|
|
375
399
|
expect(items).toEqual([])
|
|
376
400
|
})
|
|
377
401
|
})
|
|
402
|
+
|
|
403
|
+
describe('AllPossibleErrors', () => {
|
|
404
|
+
it('should return errors', () => {
|
|
405
|
+
const errors = field.getAllPossibleErrors()
|
|
406
|
+
expect(errors.baseErrors).not.toBeEmpty()
|
|
407
|
+
expect(errors.advancedSettingsErrors).toBeEmpty()
|
|
408
|
+
})
|
|
409
|
+
})
|
|
378
410
|
})
|
|
379
411
|
})
|
|
@@ -23,16 +23,20 @@ export class CheckboxesField extends SelectionControlField {
|
|
|
23
23
|
super(def, props)
|
|
24
24
|
|
|
25
25
|
const { listType: type } = this
|
|
26
|
-
const { options
|
|
26
|
+
const { options } = def
|
|
27
27
|
|
|
28
28
|
let formSchema =
|
|
29
29
|
type === 'string' ? joi.array<string>() : joi.array<number>()
|
|
30
30
|
|
|
31
31
|
const itemsSchema = joi[type]()
|
|
32
32
|
.valid(...this.values)
|
|
33
|
-
.label(
|
|
33
|
+
.label(this.label)
|
|
34
34
|
|
|
35
|
-
formSchema = formSchema
|
|
35
|
+
formSchema = formSchema
|
|
36
|
+
.items(itemsSchema)
|
|
37
|
+
.single()
|
|
38
|
+
.label(this.label)
|
|
39
|
+
.required()
|
|
36
40
|
|
|
37
41
|
if (options.required === false) {
|
|
38
42
|
formSchema = formSchema.optional()
|
|
@@ -124,7 +124,7 @@ export class ComponentCollection {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
// Update error with parent title
|
|
127
|
-
error.local.title ??= parent?.
|
|
127
|
+
error.local.title ??= parent?.label
|
|
128
128
|
|
|
129
129
|
return error
|
|
130
130
|
})
|
|
@@ -212,25 +212,22 @@ export class ComponentCollection {
|
|
|
212
212
|
return context
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Get all errors for all fields in this collection
|
|
217
|
+
*/
|
|
215
218
|
getErrors(errors?: FormSubmissionError[]): FormSubmissionError[] | undefined {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const list: FormSubmissionError[] = []
|
|
219
|
-
|
|
220
|
-
// Add only one error per field
|
|
221
|
-
for (const field of fields) {
|
|
222
|
-
const error = field.getError(errors)
|
|
223
|
-
|
|
224
|
-
if (error) {
|
|
225
|
-
list.push(error)
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (!list.length) {
|
|
230
|
-
return
|
|
231
|
-
}
|
|
219
|
+
return this.getFieldErrors((field) => field.getErrors(errors), errors)
|
|
220
|
+
}
|
|
232
221
|
|
|
233
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Get view errors for all fields in this collection.
|
|
224
|
+
* For most fields this means filtering to the first error in the list.
|
|
225
|
+
* Composite fields like UKAddress can choose to return more than one error.
|
|
226
|
+
*/
|
|
227
|
+
getViewErrors(
|
|
228
|
+
errors?: FormSubmissionError[]
|
|
229
|
+
): FormSubmissionError[] | undefined {
|
|
230
|
+
return this.getFieldErrors((field) => field.getViewErrors(errors), errors)
|
|
234
231
|
}
|
|
235
232
|
|
|
236
233
|
getViewModel(
|
|
@@ -266,6 +263,36 @@ export class ComponentCollection {
|
|
|
266
263
|
errors: this.page?.getErrors(details) ?? getErrors(details)
|
|
267
264
|
}
|
|
268
265
|
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Helper to get errors from all fields
|
|
269
|
+
*/
|
|
270
|
+
private getFieldErrors(
|
|
271
|
+
callback: (field: Field) => FormSubmissionError[] | undefined,
|
|
272
|
+
errors?: FormSubmissionError[]
|
|
273
|
+
): FormSubmissionError[] | undefined {
|
|
274
|
+
const { fields } = this
|
|
275
|
+
|
|
276
|
+
if (!errors?.length) {
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const list: FormSubmissionError[] = []
|
|
281
|
+
|
|
282
|
+
for (const field of fields) {
|
|
283
|
+
const fieldErrors = callback(field)
|
|
284
|
+
|
|
285
|
+
if (fieldErrors?.length) {
|
|
286
|
+
list.push(...fieldErrors)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!list.length) {
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return list
|
|
295
|
+
}
|
|
269
296
|
}
|
|
270
297
|
|
|
271
298
|
/**
|
|
@@ -31,6 +31,7 @@ describe('DatePartsField', () => {
|
|
|
31
31
|
beforeEach(() => {
|
|
32
32
|
def = {
|
|
33
33
|
title: 'Example date parts field',
|
|
34
|
+
shortDescription: 'Example date parts',
|
|
34
35
|
name: 'myComponent',
|
|
35
36
|
type: ComponentType.DatePartsField,
|
|
36
37
|
options: {}
|
|
@@ -199,7 +200,7 @@ describe('DatePartsField', () => {
|
|
|
199
200
|
expect(result3.errors).toBeUndefined()
|
|
200
201
|
})
|
|
201
202
|
|
|
202
|
-
it('adds errors for empty value', () => {
|
|
203
|
+
it('adds errors for empty value when short description exists', () => {
|
|
203
204
|
const result = collection.validate(
|
|
204
205
|
getFormData({
|
|
205
206
|
day: '',
|
|
@@ -210,13 +211,13 @@ describe('DatePartsField', () => {
|
|
|
210
211
|
|
|
211
212
|
expect(result.errors).toEqual([
|
|
212
213
|
expect.objectContaining({
|
|
213
|
-
text: 'Example date parts
|
|
214
|
+
text: 'Example date parts must include a day'
|
|
214
215
|
}),
|
|
215
216
|
expect.objectContaining({
|
|
216
|
-
text: 'Example date parts
|
|
217
|
+
text: 'Example date parts must include a month'
|
|
217
218
|
}),
|
|
218
219
|
expect.objectContaining({
|
|
219
|
-
text: 'Example date parts
|
|
220
|
+
text: 'Example date parts must include a year'
|
|
220
221
|
})
|
|
221
222
|
])
|
|
222
223
|
})
|
|
@@ -409,6 +410,14 @@ describe('DatePartsField', () => {
|
|
|
409
410
|
})
|
|
410
411
|
})
|
|
411
412
|
})
|
|
413
|
+
|
|
414
|
+
describe('AllPossibleErrors', () => {
|
|
415
|
+
it('should return errors', () => {
|
|
416
|
+
const errors = field.getAllPossibleErrors()
|
|
417
|
+
expect(errors.baseErrors).not.toBeEmpty()
|
|
418
|
+
expect(errors.advancedSettingsErrors).not.toBeEmpty()
|
|
419
|
+
})
|
|
420
|
+
})
|
|
412
421
|
})
|
|
413
422
|
|
|
414
423
|
describe('Validation', () => {
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { ComponentType, type DatePartsFieldComponent } from '@defra/forms-model'
|
|
2
2
|
import { add, format, isValid, parse, startOfToday, sub } from 'date-fns'
|
|
3
|
-
import {
|
|
4
|
-
type Context,
|
|
5
|
-
type CustomValidator,
|
|
6
|
-
type LanguageMessages,
|
|
7
|
-
type ObjectSchema
|
|
8
|
-
} from 'joi'
|
|
3
|
+
import { type Context, type CustomValidator, type ObjectSchema } from 'joi'
|
|
9
4
|
|
|
10
5
|
import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
|
|
11
6
|
import {
|
|
@@ -17,12 +12,14 @@ import { NumberField } from '~/src/server/plugins/engine/components/NumberField.
|
|
|
17
12
|
import { type DateInputItem } from '~/src/server/plugins/engine/components/types.js'
|
|
18
13
|
import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
|
|
19
14
|
import {
|
|
15
|
+
type ErrorMessageTemplateList,
|
|
20
16
|
type FormPayload,
|
|
21
17
|
type FormState,
|
|
22
18
|
type FormStateValue,
|
|
23
19
|
type FormSubmissionError,
|
|
24
20
|
type FormSubmissionState
|
|
25
21
|
} from '~/src/server/plugins/engine/types.js'
|
|
22
|
+
import { convertToLanguageMessages } from '~/src/server/utils/type-utils.js'
|
|
26
23
|
|
|
27
24
|
export class DatePartsField extends FormComponent {
|
|
28
25
|
declare options: DatePartsFieldComponent['options']
|
|
@@ -40,15 +37,17 @@ export class DatePartsField extends FormComponent {
|
|
|
40
37
|
|
|
41
38
|
const isRequired = options.required !== false
|
|
42
39
|
|
|
43
|
-
const customValidationMessages
|
|
40
|
+
const customValidationMessages = convertToLanguageMessages({
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
44
42
|
'any.required': messageTemplate.objectMissing,
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
45
44
|
'number.base': messageTemplate.objectMissing,
|
|
46
45
|
'number.precision': messageTemplate.dateFormat,
|
|
47
46
|
'number.integer': messageTemplate.dateFormat,
|
|
48
47
|
'number.unsafe': messageTemplate.dateFormat,
|
|
49
48
|
'number.min': messageTemplate.dateFormat,
|
|
50
49
|
'number.max': messageTemplate.dateFormat
|
|
51
|
-
}
|
|
50
|
+
})
|
|
52
51
|
|
|
53
52
|
this.collection = new ComponentCollection(
|
|
54
53
|
[
|
|
@@ -192,6 +191,28 @@ export class DatePartsField extends FormComponent {
|
|
|
192
191
|
return DatePartsField.isDateParts(value)
|
|
193
192
|
}
|
|
194
193
|
|
|
194
|
+
/**
|
|
195
|
+
* For error preview page that shows all possible errors on a component
|
|
196
|
+
*/
|
|
197
|
+
getAllPossibleErrors(): ErrorMessageTemplateList {
|
|
198
|
+
return {
|
|
199
|
+
baseErrors: [
|
|
200
|
+
{ type: 'required', template: messageTemplate.required },
|
|
201
|
+
{ type: 'dateFormat', template: messageTemplate.dateFormat },
|
|
202
|
+
{ type: 'dateFormatDay', template: '{{#label}} must include a day' },
|
|
203
|
+
{
|
|
204
|
+
type: 'dateFormatMonth',
|
|
205
|
+
template: '{{#label}} must include a month'
|
|
206
|
+
},
|
|
207
|
+
{ type: 'dateFormatYear', template: '{{#label}} must include a year' }
|
|
208
|
+
],
|
|
209
|
+
advancedSettingsErrors: [
|
|
210
|
+
{ type: 'dateMin', template: messageTemplate.dateMin },
|
|
211
|
+
{ type: 'dateMax', template: messageTemplate.dateMax }
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
195
216
|
static isDateParts(
|
|
196
217
|
value?: FormStateValue | FormState
|
|
197
218
|
): value is DatePartsState {
|
|
@@ -29,6 +29,7 @@ describe('EmailAddressField', () => {
|
|
|
29
29
|
beforeEach(() => {
|
|
30
30
|
def = {
|
|
31
31
|
title: 'Example email address field',
|
|
32
|
+
shortDescription: 'Example email address',
|
|
32
33
|
name: 'myComponent',
|
|
33
34
|
type: ComponentType.EmailAddressField,
|
|
34
35
|
options: {}
|
|
@@ -47,7 +48,7 @@ describe('EmailAddressField', () => {
|
|
|
47
48
|
'myComponent',
|
|
48
49
|
expect.objectContaining({
|
|
49
50
|
flags: expect.objectContaining({
|
|
50
|
-
label: 'Example email address
|
|
51
|
+
label: 'Example email address'
|
|
51
52
|
})
|
|
52
53
|
})
|
|
53
54
|
)
|
|
@@ -111,6 +112,24 @@ describe('EmailAddressField', () => {
|
|
|
111
112
|
it('adds errors for empty value', () => {
|
|
112
113
|
const result = collection.validate(getFormData(''))
|
|
113
114
|
|
|
115
|
+
expect(result.errors).toEqual([
|
|
116
|
+
expect.objectContaining({
|
|
117
|
+
text: 'Enter example email address'
|
|
118
|
+
})
|
|
119
|
+
])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('adds errors for empty value given no shortDescription', () => {
|
|
123
|
+
def = {
|
|
124
|
+
title: 'Example email address field',
|
|
125
|
+
name: 'myComponent',
|
|
126
|
+
type: ComponentType.EmailAddressField,
|
|
127
|
+
options: {}
|
|
128
|
+
} satisfies EmailAddressFieldComponent
|
|
129
|
+
|
|
130
|
+
collection = new ComponentCollection([def], { model })
|
|
131
|
+
const result = collection.validate(getFormData(''))
|
|
132
|
+
|
|
114
133
|
expect(result.errors).toEqual([
|
|
115
134
|
expect.objectContaining({
|
|
116
135
|
text: 'Enter example email address field'
|
|
@@ -205,6 +224,14 @@ describe('EmailAddressField', () => {
|
|
|
205
224
|
)
|
|
206
225
|
})
|
|
207
226
|
})
|
|
227
|
+
|
|
228
|
+
describe('AllPossibleErrors', () => {
|
|
229
|
+
it('should return errors', () => {
|
|
230
|
+
const errors = field.getAllPossibleErrors()
|
|
231
|
+
expect(errors.baseErrors).not.toBeEmpty()
|
|
232
|
+
expect(errors.advancedSettingsErrors).toBeEmpty()
|
|
233
|
+
})
|
|
234
|
+
})
|
|
208
235
|
})
|
|
209
236
|
|
|
210
237
|
describe('Validation', () => {
|
|
@@ -269,6 +296,29 @@ describe('EmailAddressField', () => {
|
|
|
269
296
|
}
|
|
270
297
|
]
|
|
271
298
|
},
|
|
299
|
+
{
|
|
300
|
+
description: 'Email address validation',
|
|
301
|
+
component: {
|
|
302
|
+
title: 'Example email address field',
|
|
303
|
+
shortDescription: 'Example email address',
|
|
304
|
+
name: 'myComponent',
|
|
305
|
+
type: ComponentType.EmailAddressField,
|
|
306
|
+
options: {}
|
|
307
|
+
} satisfies EmailAddressFieldComponent,
|
|
308
|
+
assertions: [
|
|
309
|
+
{
|
|
310
|
+
input: getFormData('defra.helpline'),
|
|
311
|
+
output: {
|
|
312
|
+
value: getFormData('defra.helpline'),
|
|
313
|
+
errors: [
|
|
314
|
+
expect.objectContaining({
|
|
315
|
+
text: 'Enter example email address in the correct format'
|
|
316
|
+
})
|
|
317
|
+
]
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
]
|
|
321
|
+
},
|
|
272
322
|
{
|
|
273
323
|
description: 'Custom validation message',
|
|
274
324
|
component: {
|
|
@@ -2,7 +2,9 @@ import { type EmailAddressFieldComponent } from '@defra/forms-model'
|
|
|
2
2
|
import joi from 'joi'
|
|
3
3
|
|
|
4
4
|
import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
|
|
5
|
+
import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
|
|
5
6
|
import {
|
|
7
|
+
type ErrorMessageTemplateList,
|
|
6
8
|
type FormPayload,
|
|
7
9
|
type FormSubmissionError
|
|
8
10
|
} from '~/src/server/plugins/engine/types.js'
|
|
@@ -16,9 +18,9 @@ export class EmailAddressField extends FormComponent {
|
|
|
16
18
|
) {
|
|
17
19
|
super(def, props)
|
|
18
20
|
|
|
19
|
-
const { options
|
|
21
|
+
const { options } = def
|
|
20
22
|
|
|
21
|
-
let formSchema = joi.string().email().trim().label(
|
|
23
|
+
let formSchema = joi.string().email().trim().label(this.label).required()
|
|
22
24
|
|
|
23
25
|
if (options.required === false) {
|
|
24
26
|
formSchema = formSchema.allow('')
|
|
@@ -52,4 +54,17 @@ export class EmailAddressField extends FormComponent {
|
|
|
52
54
|
type: 'email'
|
|
53
55
|
}
|
|
54
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* For error preview page that shows all possible errors on a component
|
|
60
|
+
*/
|
|
61
|
+
getAllPossibleErrors(): ErrorMessageTemplateList {
|
|
62
|
+
return {
|
|
63
|
+
baseErrors: [
|
|
64
|
+
{ type: 'required', template: messageTemplate.required },
|
|
65
|
+
{ type: 'format', template: messageTemplate.format }
|
|
66
|
+
],
|
|
67
|
+
advancedSettingsErrors: []
|
|
68
|
+
}
|
|
69
|
+
}
|
|
55
70
|
}
|
|
@@ -157,6 +157,7 @@ describe('FileUploadField', () => {
|
|
|
157
157
|
beforeEach(() => {
|
|
158
158
|
def = {
|
|
159
159
|
title: 'Example file upload field',
|
|
160
|
+
shortDescription: 'Example file upload',
|
|
160
161
|
name: 'myComponent',
|
|
161
162
|
type: ComponentType.FileUploadField,
|
|
162
163
|
options: {},
|
|
@@ -169,7 +170,32 @@ describe('FileUploadField', () => {
|
|
|
169
170
|
})
|
|
170
171
|
|
|
171
172
|
describe('Schema', () => {
|
|
173
|
+
it('uses component short description as label', () => {
|
|
174
|
+
const { formSchema } = collection
|
|
175
|
+
const { keys } = formSchema.describe()
|
|
176
|
+
|
|
177
|
+
expect(keys).toHaveProperty(
|
|
178
|
+
'myComponent',
|
|
179
|
+
expect.objectContaining({
|
|
180
|
+
flags: expect.objectContaining({
|
|
181
|
+
label: 'Example file upload'
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
)
|
|
185
|
+
})
|
|
186
|
+
|
|
172
187
|
it('uses component title as label', () => {
|
|
188
|
+
def = {
|
|
189
|
+
title: 'Example file upload field',
|
|
190
|
+
name: 'myComponent',
|
|
191
|
+
type: ComponentType.FileUploadField,
|
|
192
|
+
options: {},
|
|
193
|
+
schema: {}
|
|
194
|
+
} satisfies FileUploadFieldComponent
|
|
195
|
+
|
|
196
|
+
page = createPage(model, definition.pages[0])
|
|
197
|
+
collection = new ComponentCollection([def], { page, model })
|
|
198
|
+
|
|
173
199
|
const { formSchema } = collection
|
|
174
200
|
const { keys } = formSchema.describe()
|
|
175
201
|
|
|
@@ -246,6 +272,25 @@ describe('FileUploadField', () => {
|
|
|
246
272
|
it('adds errors for empty value', () => {
|
|
247
273
|
const result = collection.validate(getFormData())
|
|
248
274
|
|
|
275
|
+
expect(result.errors).toEqual([
|
|
276
|
+
expect.objectContaining({
|
|
277
|
+
text: 'Select example file upload'
|
|
278
|
+
})
|
|
279
|
+
])
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('adds errors for empty value with no shortDescription', () => {
|
|
283
|
+
def = {
|
|
284
|
+
title: 'Example file upload field',
|
|
285
|
+
name: 'myComponent',
|
|
286
|
+
type: ComponentType.FileUploadField,
|
|
287
|
+
options: {},
|
|
288
|
+
schema: {}
|
|
289
|
+
} satisfies FileUploadFieldComponent
|
|
290
|
+
|
|
291
|
+
collection = new ComponentCollection([def], { model })
|
|
292
|
+
const result = collection.validate(getFormData())
|
|
293
|
+
|
|
249
294
|
expect(result.errors).toEqual([
|
|
250
295
|
expect.objectContaining({
|
|
251
296
|
text: 'Select example file upload field'
|
|
@@ -545,6 +590,14 @@ describe('FileUploadField', () => {
|
|
|
545
590
|
)
|
|
546
591
|
})
|
|
547
592
|
})
|
|
593
|
+
|
|
594
|
+
describe('AllPossibleErrors', () => {
|
|
595
|
+
it('should return errors', () => {
|
|
596
|
+
const errors = field.getAllPossibleErrors()
|
|
597
|
+
expect(errors.baseErrors).not.toBeEmpty()
|
|
598
|
+
expect(errors.advancedSettingsErrors).not.toBeEmpty()
|
|
599
|
+
})
|
|
600
|
+
})
|
|
548
601
|
})
|
|
549
602
|
|
|
550
603
|
describe('Validation', () => {
|