@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.
Files changed (147) hide show
  1. package/.public/javascripts/file-upload.min.js +1 -1
  2. package/.public/javascripts/file-upload.min.js.map +1 -1
  3. package/.server/client/javascripts/file-upload.js +45 -4
  4. package/.server/client/javascripts/file-upload.js.map +1 -1
  5. package/.server/server/constants.js +2 -0
  6. package/.server/server/constants.js.map +1 -1
  7. package/.server/server/index.js +1 -1
  8. package/.server/server/index.js.map +1 -1
  9. package/.server/server/plugins/engine/components/AutocompleteField.js +2 -0
  10. package/.server/server/plugins/engine/components/AutocompleteField.js.map +1 -1
  11. package/.server/server/plugins/engine/components/CheckboxesField.js +3 -4
  12. package/.server/server/plugins/engine/components/CheckboxesField.js.map +1 -1
  13. package/.server/server/plugins/engine/components/ComponentCollection.js +37 -16
  14. package/.server/server/plugins/engine/components/ComponentCollection.js.map +1 -1
  15. package/.server/server/plugins/engine/components/DatePartsField.js +36 -2
  16. package/.server/server/plugins/engine/components/DatePartsField.js.map +1 -1
  17. package/.server/server/plugins/engine/components/EmailAddressField.js +19 -3
  18. package/.server/server/plugins/engine/components/EmailAddressField.js.map +1 -1
  19. package/.server/server/plugins/engine/components/FileUploadField.js +44 -4
  20. package/.server/server/plugins/engine/components/FileUploadField.js.map +1 -1
  21. package/.server/server/plugins/engine/components/FormComponent.js +14 -2
  22. package/.server/server/plugins/engine/components/FormComponent.js.map +1 -1
  23. package/.server/server/plugins/engine/components/ListFormComponent.js +16 -3
  24. package/.server/server/plugins/engine/components/ListFormComponent.js.map +1 -1
  25. package/.server/server/plugins/engine/components/Markdown.js +24 -0
  26. package/.server/server/plugins/engine/components/Markdown.js.map +1 -0
  27. package/.server/server/plugins/engine/components/MonthYearField.js +30 -2
  28. package/.server/server/plugins/engine/components/MonthYearField.js.map +1 -1
  29. package/.server/server/plugins/engine/components/MultilineTextField.js +32 -3
  30. package/.server/server/plugins/engine/components/MultilineTextField.js.map +1 -1
  31. package/.server/server/plugins/engine/components/NumberField.js +28 -3
  32. package/.server/server/plugins/engine/components/NumberField.js.map +1 -1
  33. package/.server/server/plugins/engine/components/SelectionControlField.js +14 -0
  34. package/.server/server/plugins/engine/components/SelectionControlField.js.map +1 -1
  35. package/.server/server/plugins/engine/components/TelephoneNumberField.js +19 -3
  36. package/.server/server/plugins/engine/components/TelephoneNumberField.js.map +1 -1
  37. package/.server/server/plugins/engine/components/TextField.js +22 -3
  38. package/.server/server/plugins/engine/components/TextField.js.map +1 -1
  39. package/.server/server/plugins/engine/components/UkAddressField.js +29 -0
  40. package/.server/server/plugins/engine/components/UkAddressField.js.map +1 -1
  41. package/.server/server/plugins/engine/components/YesNoField.js +18 -0
  42. package/.server/server/plugins/engine/components/YesNoField.js.map +1 -1
  43. package/.server/server/plugins/engine/components/helpers.js +16 -0
  44. package/.server/server/plugins/engine/components/helpers.js.map +1 -1
  45. package/.server/server/plugins/engine/components/index.js +1 -0
  46. package/.server/server/plugins/engine/components/index.js.map +1 -1
  47. package/.server/server/plugins/engine/configureEnginePlugin.js +3 -1
  48. package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
  49. package/.server/server/plugins/engine/helpers.js +38 -18
  50. package/.server/server/plugins/engine/helpers.js.map +1 -1
  51. package/.server/server/plugins/engine/models/FormModel.js +60 -2
  52. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  53. package/.server/server/plugins/engine/models/SummaryViewModel.js +3 -2
  54. package/.server/server/plugins/engine/models/SummaryViewModel.js.map +1 -1
  55. package/.server/server/plugins/engine/outputFormatters/human/v1.js +1 -1
  56. package/.server/server/plugins/engine/outputFormatters/human/v1.js.map +1 -1
  57. package/.server/server/plugins/engine/pageControllers/PageController.js +13 -5
  58. package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
  59. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +2 -2
  60. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  61. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +19 -5
  62. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  63. package/.server/server/plugins/engine/pageControllers/validationOptions.js +6 -11
  64. package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
  65. package/.server/server/plugins/engine/plugin.js +5 -4
  66. package/.server/server/plugins/engine/plugin.js.map +1 -1
  67. package/.server/server/plugins/engine/services/notifyService.js +1 -4
  68. package/.server/server/plugins/engine/services/notifyService.js.map +1 -1
  69. package/.server/server/plugins/engine/services/uploadService.js +5 -3
  70. package/.server/server/plugins/engine/services/uploadService.js.map +1 -1
  71. package/.server/server/plugins/engine/types.js.map +1 -1
  72. package/.server/server/plugins/engine/views/components/html.html +1 -1
  73. package/.server/server/plugins/engine/views/components/markdown.html +5 -0
  74. package/.server/server/plugins/engine/views/summary.html +7 -1
  75. package/.server/server/plugins/nunjucks/context.js +6 -5
  76. package/.server/server/plugins/nunjucks/context.js.map +1 -1
  77. package/.server/server/plugins/nunjucks/enviroment.test.js +6 -3
  78. package/.server/server/plugins/nunjucks/enviroment.test.js.map +1 -1
  79. package/.server/server/utils/type-utils.js +8 -0
  80. package/.server/server/utils/type-utils.js.map +1 -0
  81. package/.server/typings/joi/index.d.js.map +1 -1
  82. package/package.json +3 -3
  83. package/src/client/javascripts/file-upload.js +60 -4
  84. package/src/server/constants.js +2 -0
  85. package/src/server/index.test.ts +34 -29
  86. package/src/server/index.ts +2 -1
  87. package/src/server/plugins/engine/components/AutocompleteField.test.ts +71 -3
  88. package/src/server/plugins/engine/components/AutocompleteField.ts +6 -2
  89. package/src/server/plugins/engine/components/CheckboxesField.test.ts +40 -8
  90. package/src/server/plugins/engine/components/CheckboxesField.ts +7 -3
  91. package/src/server/plugins/engine/components/ComponentCollection.ts +45 -18
  92. package/src/server/plugins/engine/components/DatePartsField.test.ts +13 -4
  93. package/src/server/plugins/engine/components/DatePartsField.ts +29 -8
  94. package/src/server/plugins/engine/components/EmailAddressField.test.ts +51 -1
  95. package/src/server/plugins/engine/components/EmailAddressField.ts +17 -2
  96. package/src/server/plugins/engine/components/FileUploadField.test.ts +53 -0
  97. package/src/server/plugins/engine/components/FileUploadField.ts +52 -3
  98. package/src/server/plugins/engine/components/FormComponent.ts +24 -2
  99. package/src/server/plugins/engine/components/ListFormComponent.ts +16 -2
  100. package/src/server/plugins/engine/components/Markdown.test.ts +48 -0
  101. package/src/server/plugins/engine/components/Markdown.ts +29 -0
  102. package/src/server/plugins/engine/components/MonthYearField.test.ts +35 -0
  103. package/src/server/plugins/engine/components/MonthYearField.ts +34 -9
  104. package/src/server/plugins/engine/components/MultilineTextField.test.ts +83 -5
  105. package/src/server/plugins/engine/components/MultilineTextField.ts +37 -2
  106. package/src/server/plugins/engine/components/NumberField.test.ts +24 -2
  107. package/src/server/plugins/engine/components/NumberField.ts +23 -3
  108. package/src/server/plugins/engine/components/RadiosField.test.ts +10 -1
  109. package/src/server/plugins/engine/components/SelectField.test.ts +2 -1
  110. package/src/server/plugins/engine/components/SelectionControlField.ts +14 -0
  111. package/src/server/plugins/engine/components/TelephoneNumberField.test.ts +30 -2
  112. package/src/server/plugins/engine/components/TelephoneNumberField.ts +17 -2
  113. package/src/server/plugins/engine/components/TextField.test.ts +33 -1
  114. package/src/server/plugins/engine/components/TextField.ts +17 -2
  115. package/src/server/plugins/engine/components/UkAddressField.test.ts +46 -3
  116. package/src/server/plugins/engine/components/UkAddressField.ts +28 -0
  117. package/src/server/plugins/engine/components/YesNoField.test.ts +9 -1
  118. package/src/server/plugins/engine/components/YesNoField.ts +24 -0
  119. package/src/server/plugins/engine/components/helpers.test.ts +24 -0
  120. package/src/server/plugins/engine/components/helpers.ts +39 -0
  121. package/src/server/plugins/engine/components/index.ts +1 -0
  122. package/src/server/plugins/engine/configureEnginePlugin.ts +13 -3
  123. package/src/server/plugins/engine/helpers.test.ts +71 -20
  124. package/src/server/plugins/engine/helpers.ts +46 -19
  125. package/src/server/plugins/engine/models/FormModel.test.ts +91 -1
  126. package/src/server/plugins/engine/models/FormModel.ts +86 -3
  127. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +46 -7
  128. package/src/server/plugins/engine/models/SummaryViewModel.ts +7 -3
  129. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +1 -2
  130. package/src/server/plugins/engine/outputFormatters/human/v1.ts +1 -1
  131. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +1 -0
  132. package/src/server/plugins/engine/pageControllers/PageController.test.ts +9 -6
  133. package/src/server/plugins/engine/pageControllers/PageController.ts +15 -5
  134. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +2 -2
  135. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +21 -6
  136. package/src/server/plugins/engine/pageControllers/validationOptions.ts +31 -17
  137. package/src/server/plugins/engine/plugin.ts +9 -5
  138. package/src/server/plugins/engine/services/notifyService.ts +1 -2
  139. package/src/server/plugins/engine/services/uploadService.js +10 -6
  140. package/src/server/plugins/engine/types.ts +10 -1
  141. package/src/server/plugins/engine/views/components/html.html +1 -1
  142. package/src/server/plugins/engine/views/components/markdown.html +5 -0
  143. package/src/server/plugins/engine/views/summary.html +7 -1
  144. package/src/server/plugins/nunjucks/context.js +4 -4
  145. package/src/server/plugins/nunjucks/enviroment.test.js +9 -3
  146. package/src/server/utils/type-utils.ts +15 -0
  147. package/src/typings/joi/index.d.ts +8 -0
@@ -5,9 +5,11 @@ import {
5
5
  FormComponent,
6
6
  isUploadState
7
7
  } from '~/src/server/plugins/engine/components/FormComponent.js'
8
+ import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
8
9
  import {
9
10
  FileStatus,
10
11
  UploadStatus,
12
+ type ErrorMessageTemplateList,
11
13
  type FileState,
12
14
  type FileUpload,
13
15
  type FileUploadMetadata,
@@ -104,9 +106,13 @@ export class FileUploadField extends FormComponent {
104
106
  ) {
105
107
  super(def, props)
106
108
 
107
- const { options, schema, title } = def
109
+ const { options, schema } = def
108
110
 
109
- let formSchema = joi.array<FileState>().label(title).single().required()
111
+ let formSchema = joi
112
+ .array<FileState>()
113
+ .label(this.label)
114
+ .single()
115
+ .required()
110
116
 
111
117
  if (options.required === false) {
112
118
  formSchema = formSchema.optional()
@@ -231,7 +237,7 @@ export class FileUploadField extends FormComponent {
231
237
  })
232
238
 
233
239
  // Set up the `accept` attribute
234
- if ('accept' in options) {
240
+ if ('accept' in options && options.accept) {
235
241
  attributes.accept = options.accept
236
242
  }
237
243
 
@@ -259,4 +265,47 @@ export class FileUploadField extends FormComponent {
259
265
  isValue(value?: FormStateValue | FormState): value is UploadState {
260
266
  return isUploadState(value)
261
267
  }
268
+
269
+ /**
270
+ * For error preview page that shows all possible errors on a component
271
+ */
272
+ getAllPossibleErrors(): ErrorMessageTemplateList {
273
+ return {
274
+ baseErrors: [
275
+ { type: 'selectRequired', template: messageTemplate.selectRequired },
276
+ {
277
+ type: 'filesMimes',
278
+ template: 'The selected file must be a {{#limit}}'
279
+ },
280
+ {
281
+ type: 'filesSize',
282
+ template: 'The selected file must be smaller than 100MB'
283
+ },
284
+ { type: 'filesEmpty', template: 'The selected file is empty' },
285
+ { type: 'filesVirus', template: 'The selected file contains a virus' },
286
+ {
287
+ type: 'filesPartial',
288
+ template: 'The selected file has not fully uploaded'
289
+ },
290
+ {
291
+ type: 'filesError',
292
+ template: 'The selected file could not be uploaded – try again'
293
+ }
294
+ ],
295
+ advancedSettingsErrors: [
296
+ {
297
+ type: 'filesMin',
298
+ template: 'You must upload {{#limit}} files or more'
299
+ },
300
+ {
301
+ type: 'filesMax',
302
+ template: 'You can only upload {{#limit}} files or less'
303
+ },
304
+ {
305
+ type: 'filesExact',
306
+ template: 'You must upload exactly {{#limit}} files'
307
+ }
308
+ ]
309
+ }
310
+ }
262
311
  }
@@ -3,6 +3,7 @@ import { type FormComponentsDef, type Item } from '@defra/forms-model'
3
3
  import { ComponentBase } from '~/src/server/plugins/engine/components/ComponentBase.js'
4
4
  import { optionalText } from '~/src/server/plugins/engine/components/constants.js'
5
5
  import {
6
+ type ErrorMessageTemplateList,
6
7
  type FileState,
7
8
  type FormPayload,
8
9
  type FormState,
@@ -18,6 +19,7 @@ import {
18
19
  export class FormComponent extends ComponentBase {
19
20
  type: FormComponentsDef['type']
20
21
  hint: FormComponentsDef['hint']
22
+ label: string
21
23
 
22
24
  isFormComponent = true
23
25
 
@@ -31,6 +33,10 @@ export class FormComponent extends ComponentBase {
31
33
 
32
34
  this.type = type
33
35
  this.hint = hint
36
+ this.label =
37
+ 'shortDescription' in def && def.shortDescription
38
+ ? def.shortDescription
39
+ : def.title
34
40
  }
35
41
 
36
42
  get keys() {
@@ -100,10 +106,19 @@ export class FormComponent extends ComponentBase {
100
106
  return list
101
107
  }
102
108
 
103
- getError(errors?: FormSubmissionError[]): FormSubmissionError | undefined {
109
+ getFirstError(
110
+ errors?: FormSubmissionError[]
111
+ ): FormSubmissionError | undefined {
104
112
  return this.getErrors(errors)?.[0]
105
113
  }
106
114
 
115
+ getViewErrors(
116
+ errors?: FormSubmissionError[]
117
+ ): FormSubmissionError[] | undefined {
118
+ const firstError = this.getFirstError(errors)
119
+ return firstError && [firstError]
120
+ }
121
+
107
122
  getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {
108
123
  const { hint, name, options = {}, title, viewModel } = this
109
124
 
@@ -119,7 +134,7 @@ export class FormComponent extends ComponentBase {
119
134
 
120
135
  // Filter component errors only
121
136
  const componentErrors = this.getErrors(errors)
122
- const componentError = this.getError(componentErrors)
137
+ const componentError = this.getFirstError(componentErrors)
123
138
 
124
139
  if (componentErrors) {
125
140
  viewModel.errors = componentErrors
@@ -175,6 +190,13 @@ export class FormComponent extends ComponentBase {
175
190
  isState(value?: FormStateValue | FormState): value is FormState {
176
191
  return isFormState(value)
177
192
  }
193
+
194
+ getAllPossibleErrors(): ErrorMessageTemplateList {
195
+ return {
196
+ baseErrors: [],
197
+ advancedSettingsErrors: []
198
+ }
199
+ }
178
200
  }
179
201
 
180
202
  /**
@@ -14,7 +14,9 @@ import joi, {
14
14
 
15
15
  import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
16
16
  import { type ListItem } from '~/src/server/plugins/engine/components/types.js'
17
+ import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
17
18
  import {
19
+ type ErrorMessageTemplateList,
18
20
  type FormPayload,
19
21
  type FormSubmissionError,
20
22
  type FormSubmissionState
@@ -61,7 +63,7 @@ export class ListFormComponent extends FormComponent {
61
63
  ) {
62
64
  super(def, props)
63
65
 
64
- const { options, title } = def
66
+ const { options } = def
65
67
  const { model } = props
66
68
 
67
69
  if ('list' in def) {
@@ -71,7 +73,7 @@ export class ListFormComponent extends FormComponent {
71
73
 
72
74
  let formSchema = joi[this.listType]()
73
75
  .valid(...this.values)
74
- .label(title)
76
+ .label(this.label)
75
77
  .required()
76
78
 
77
79
  if (options.customValidationMessages) {
@@ -137,4 +139,16 @@ export class ListFormComponent extends FormComponent {
137
139
  items
138
140
  }
139
141
  }
142
+
143
+ /**
144
+ * For error preview page that shows all possible errors on a component
145
+ */
146
+ getAllPossibleErrors(): ErrorMessageTemplateList {
147
+ return {
148
+ baseErrors: [
149
+ { type: 'selectRequired', template: messageTemplate.selectRequired }
150
+ ],
151
+ advancedSettingsErrors: []
152
+ }
153
+ }
140
154
  }
@@ -0,0 +1,48 @@
1
+ import { ComponentType, type MarkdownComponent } from '@defra/forms-model'
2
+
3
+ import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
4
+ import { type Guidance } from '~/src/server/plugins/engine/components/helpers.js'
5
+ import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
6
+ import definition from '~/test/form/definitions/basic.js'
7
+
8
+ describe('Markdown', () => {
9
+ let model: FormModel
10
+
11
+ beforeEach(() => {
12
+ model = new FormModel(definition, {
13
+ basePath: 'test'
14
+ })
15
+ })
16
+
17
+ describe('Defaults', () => {
18
+ let def: MarkdownComponent
19
+ let collection: ComponentCollection
20
+ let guidance: Guidance
21
+
22
+ beforeEach(() => {
23
+ def = {
24
+ title: 'Markdown guidance',
25
+ name: 'myComponent',
26
+ type: ComponentType.Markdown,
27
+ content: '# Heading 1 ## Heading 2',
28
+ options: {}
29
+ } satisfies MarkdownComponent
30
+
31
+ collection = new ComponentCollection([def], { model })
32
+ guidance = collection.guidance[0]
33
+ })
34
+
35
+ describe('View model', () => {
36
+ it('sets Nunjucks component defaults', () => {
37
+ const viewModel = guidance.getViewModel()
38
+
39
+ expect(viewModel).toEqual(
40
+ expect.objectContaining({
41
+ attributes: {},
42
+ content: def.content
43
+ })
44
+ )
45
+ })
46
+ })
47
+ })
48
+ })
@@ -0,0 +1,29 @@
1
+ import { type MarkdownComponent } from '@defra/forms-model'
2
+
3
+ import { ComponentBase } from '~/src/server/plugins/engine/components/ComponentBase.js'
4
+
5
+ export class Markdown extends ComponentBase {
6
+ declare options: MarkdownComponent['options']
7
+ content: MarkdownComponent['content']
8
+
9
+ constructor(
10
+ def: MarkdownComponent,
11
+ props: ConstructorParameters<typeof ComponentBase>[1]
12
+ ) {
13
+ super(def, props)
14
+
15
+ const { content, options } = def
16
+
17
+ this.content = content
18
+ this.options = options
19
+ }
20
+
21
+ getViewModel() {
22
+ const { content, viewModel } = this
23
+
24
+ return {
25
+ ...viewModel,
26
+ content
27
+ }
28
+ }
29
+ }
@@ -31,6 +31,7 @@ describe('MonthYearField', () => {
31
31
  beforeEach(() => {
32
32
  def = {
33
33
  title: 'Example month/year field',
34
+ shortDescription: 'Example month/year',
34
35
  name: 'myComponent',
35
36
  type: ComponentType.MonthYearField,
36
37
  options: {}
@@ -168,6 +169,32 @@ describe('MonthYearField', () => {
168
169
  })
169
170
  )
170
171
 
172
+ expect(result.errors).toEqual([
173
+ expect.objectContaining({
174
+ text: 'Example month/year must include a month'
175
+ }),
176
+ expect.objectContaining({
177
+ text: 'Example month/year must include a year'
178
+ })
179
+ ])
180
+ })
181
+
182
+ it('adds errors for empty value given no short desc exists', () => {
183
+ def = {
184
+ title: 'Example month/year field',
185
+ name: 'myComponent',
186
+ type: ComponentType.MonthYearField,
187
+ options: {}
188
+ } satisfies MonthYearFieldComponent
189
+
190
+ collection = new ComponentCollection([def], { model })
191
+ const result = collection.validate(
192
+ getFormData({
193
+ month: '',
194
+ year: ''
195
+ })
196
+ )
197
+
171
198
  expect(result.errors).toEqual([
172
199
  expect.objectContaining({
173
200
  text: 'Example month/year field must include a month'
@@ -347,6 +374,14 @@ describe('MonthYearField', () => {
347
374
  })
348
375
  })
349
376
  })
377
+
378
+ describe('AllPossibleErrors', () => {
379
+ it('should return errors', () => {
380
+ const errors = field.getAllPossibleErrors()
381
+ expect(errors.baseErrors).not.toBeEmpty()
382
+ expect(errors.advancedSettingsErrors).not.toBeEmpty()
383
+ })
384
+ })
350
385
  })
351
386
 
352
387
  describe('Validation', () => {
@@ -17,12 +17,14 @@ import { NumberField } from '~/src/server/plugins/engine/components/NumberField.
17
17
  import { type DateInputItem } from '~/src/server/plugins/engine/components/types.js'
18
18
  import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
19
19
  import {
20
+ type ErrorMessageTemplateList,
20
21
  type FormPayload,
21
22
  type FormState,
22
23
  type FormStateValue,
23
24
  type FormSubmissionError,
24
25
  type FormSubmissionState
25
26
  } from '~/src/server/plugins/engine/types.js'
27
+ import { convertToLanguageMessages } from '~/src/server/utils/type-utils.js'
26
28
 
27
29
  export class MonthYearField extends FormComponent {
28
30
  declare options: MonthYearFieldComponent['options']
@@ -40,15 +42,18 @@ export class MonthYearField extends FormComponent {
40
42
 
41
43
  const isRequired = options.required !== false
42
44
 
43
- const customValidationMessages: LanguageMessages = {
44
- 'any.required': messageTemplate.objectMissing,
45
- 'number.base': messageTemplate.objectMissing,
46
- 'number.precision': messageTemplate.dateFormat,
47
- 'number.integer': messageTemplate.dateFormat,
48
- 'number.unsafe': messageTemplate.dateFormat,
49
- 'number.min': messageTemplate.dateFormat,
50
- 'number.max': messageTemplate.dateFormat
51
- }
45
+ const customValidationMessages: LanguageMessages =
46
+ convertToLanguageMessages({
47
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
48
+ 'any.required': messageTemplate.objectMissing,
49
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
50
+ 'number.base': messageTemplate.objectMissing,
51
+ 'number.precision': messageTemplate.dateFormat,
52
+ 'number.integer': messageTemplate.dateFormat,
53
+ 'number.unsafe': messageTemplate.dateFormat,
54
+ 'number.min': messageTemplate.dateFormat,
55
+ 'number.max': messageTemplate.dateFormat
56
+ })
52
57
 
53
58
  this.collection = new ComponentCollection(
54
59
  [
@@ -180,6 +185,26 @@ export class MonthYearField extends FormComponent {
180
185
  return MonthYearField.isMonthYear(value)
181
186
  }
182
187
 
188
+ /**
189
+ * For error preview page that shows all possible errors on a component
190
+ */
191
+ getAllPossibleErrors(): ErrorMessageTemplateList {
192
+ return {
193
+ baseErrors: [
194
+ { type: 'required', template: messageTemplate.required },
195
+ {
196
+ type: 'dateFormatMonth',
197
+ template: '{{#label}} must include a month'
198
+ },
199
+ { type: 'dateFormatYear', template: '{{#label}} must include a year' }
200
+ ],
201
+ advancedSettingsErrors: [
202
+ { type: 'dateMin', template: messageTemplate.dateMin },
203
+ { type: 'dateMax', template: messageTemplate.dateMax }
204
+ ]
205
+ }
206
+ }
207
+
183
208
  static isMonthYear(
184
209
  value?: FormStateValue | FormState
185
210
  ): value is MonthYearState {
@@ -29,7 +29,8 @@ describe('MultilineTextField', () => {
29
29
 
30
30
  beforeEach(() => {
31
31
  def = {
32
- title: 'Example textarea',
32
+ title: 'Example textarea title',
33
+ shortDescription: 'Example textarea',
33
34
  name: 'myComponent',
34
35
  type: ComponentType.MultilineTextField,
35
36
  options: {},
@@ -41,7 +42,7 @@ describe('MultilineTextField', () => {
41
42
  })
42
43
 
43
44
  describe('Schema', () => {
44
- it('uses component title as label', () => {
45
+ it('uses component short description as label', () => {
45
46
  const { formSchema } = collection
46
47
  const { keys } = formSchema.describe()
47
48
 
@@ -117,6 +118,25 @@ describe('MultilineTextField', () => {
117
118
  ])
118
119
  })
119
120
 
121
+ it('adds errors for empty value given no short description', () => {
122
+ def = {
123
+ title: 'Example textarea title',
124
+ name: 'myComponent',
125
+ type: ComponentType.MultilineTextField,
126
+ options: {},
127
+ schema: {}
128
+ } satisfies MultilineTextFieldComponent
129
+
130
+ collection = new ComponentCollection([def], { model })
131
+ const result = collection.validate(getFormData(''))
132
+
133
+ expect(result.errors).toEqual([
134
+ expect.objectContaining({
135
+ text: 'Enter example textarea title'
136
+ })
137
+ ])
138
+ })
139
+
120
140
  it('adds errors for invalid values', () => {
121
141
  const result1 = collection.validate(getFormData(['invalid']))
122
142
  const result2 = collection.validate(
@@ -234,6 +254,14 @@ describe('MultilineTextField', () => {
234
254
  )
235
255
  })
236
256
  })
257
+
258
+ describe('AllPossibleErrors', () => {
259
+ it('should return errors', () => {
260
+ const errors = field.getAllPossibleErrors()
261
+ expect(errors.baseErrors).not.toBeEmpty()
262
+ expect(errors.advancedSettingsErrors).not.toBeEmpty()
263
+ })
264
+ })
237
265
  })
238
266
 
239
267
  describe('Validation', () => {
@@ -294,7 +322,57 @@ describe('MultilineTextField', () => {
294
322
  ]
295
323
  },
296
324
  {
297
- description: 'Schema min and max',
325
+ description: 'Schema min',
326
+ component: {
327
+ title: 'Example textarea',
328
+ name: 'myComponent',
329
+ type: ComponentType.MultilineTextField,
330
+ options: {},
331
+ schema: {
332
+ min: 5
333
+ }
334
+ } satisfies MultilineTextFieldComponent,
335
+ assertions: [
336
+ {
337
+ input: getFormData('Text'),
338
+ output: {
339
+ value: getFormData('Text'),
340
+ errors: [
341
+ expect.objectContaining({
342
+ text: 'Example textarea must be 5 characters or more'
343
+ })
344
+ ]
345
+ }
346
+ }
347
+ ]
348
+ },
349
+ {
350
+ description: 'Schema max',
351
+ component: {
352
+ title: 'Example textarea',
353
+ name: 'myComponent',
354
+ type: ComponentType.MultilineTextField,
355
+ options: {},
356
+ schema: {
357
+ max: 8
358
+ }
359
+ } satisfies MultilineTextFieldComponent,
360
+ assertions: [
361
+ {
362
+ input: getFormData('Text too long'),
363
+ output: {
364
+ value: getFormData('Text too long'),
365
+ errors: [
366
+ expect.objectContaining({
367
+ text: 'Example textarea must be 8 characters or less'
368
+ })
369
+ ]
370
+ }
371
+ }
372
+ ]
373
+ },
374
+ {
375
+ description: 'Schema min and max together',
298
376
  component: {
299
377
  title: 'Example textarea',
300
378
  name: 'myComponent',
@@ -312,7 +390,7 @@ describe('MultilineTextField', () => {
312
390
  value: getFormData('Text'),
313
391
  errors: [
314
392
  expect.objectContaining({
315
- text: 'Example textarea must be 5 characters or more'
393
+ text: 'Example textarea must be between 5 and 8 characters'
316
394
  })
317
395
  ]
318
396
  }
@@ -323,7 +401,7 @@ describe('MultilineTextField', () => {
323
401
  value: getFormData('Textarea too long'),
324
402
  errors: [
325
403
  expect.objectContaining({
326
- text: 'Example textarea must be 8 characters or less'
404
+ text: 'Example textarea must be between 5 and 8 characters'
327
405
  })
328
406
  ]
329
407
  }
@@ -3,7 +3,9 @@ import Joi, { type CustomValidator, type StringSchema } from 'joi'
3
3
 
4
4
  import { type ComponentBase } from '~/src/server/plugins/engine/components/ComponentBase.js'
5
5
  import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
6
+ import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
6
7
  import {
8
+ type ErrorMessageTemplateList,
7
9
  type FormPayload,
8
10
  type FormSubmissionError
9
11
  } from '~/src/server/plugins/engine/types.js'
@@ -22,9 +24,9 @@ export class MultilineTextField extends FormComponent {
22
24
  ) {
23
25
  super(def, props)
24
26
 
25
- const { schema, options, title } = def
27
+ const { schema, options } = def
26
28
 
27
- let formSchema = Joi.string().trim().label(title).required()
29
+ let formSchema = Joi.string().trim().label(this.label).required()
28
30
 
29
31
  if (options.required === false) {
30
32
  formSchema = formSchema.allow('')
@@ -71,6 +73,15 @@ export class MultilineTextField extends FormComponent {
71
73
  })
72
74
  } else if (options.customValidationMessages) {
73
75
  formSchema = formSchema.messages(options.customValidationMessages)
76
+ } else if (
77
+ typeof schema.max === 'number' &&
78
+ typeof schema.min === 'number'
79
+ ) {
80
+ const minMaxErrorText = this.buildMinMaxText(schema.min, schema.max)
81
+ formSchema = formSchema.ruleset
82
+ .min(schema.min)
83
+ .max(schema.max)
84
+ .message(minMaxErrorText)
74
85
  }
75
86
 
76
87
  this.formSchema = formSchema.default('')
@@ -105,6 +116,30 @@ export class MultilineTextField extends FormComponent {
105
116
  rows
106
117
  }
107
118
  }
119
+
120
+ buildMinMaxText(min?: number, max?: number): string {
121
+ const minMaxError = messageTemplate.minMax as string
122
+ return minMaxError
123
+ .replace('{{#min}}', min ? min.toString() : '[min length]')
124
+ .replace('{{#max}}', max ? max.toString() : '[max length]')
125
+ }
126
+
127
+ /**
128
+ * For error preview page that shows all possible errors on a component
129
+ */
130
+ getAllPossibleErrors(): ErrorMessageTemplateList {
131
+ return {
132
+ baseErrors: [{ type: 'required', template: messageTemplate.required }],
133
+ advancedSettingsErrors: [
134
+ { type: 'min', template: messageTemplate.min },
135
+ { type: 'max', template: messageTemplate.max },
136
+ {
137
+ type: 'minMax',
138
+ template: this.buildMinMaxText(this.schema.min, this.schema.max)
139
+ }
140
+ ]
141
+ }
142
+ }
108
143
  }
109
144
 
110
145
  function getValidatorMaxWords(component: MultilineTextField) {
@@ -27,6 +27,7 @@ describe('NumberField', () => {
27
27
  beforeEach(() => {
28
28
  def = {
29
29
  title: 'Example number field',
30
+ shortDescription: 'Example number',
30
31
  name: 'myComponent',
31
32
  type: ComponentType.NumberField,
32
33
  options: {},
@@ -46,7 +47,7 @@ describe('NumberField', () => {
46
47
  'myComponent',
47
48
  expect.objectContaining({
48
49
  flags: expect.objectContaining({
49
- label: 'Example number field'
50
+ label: 'Example number'
50
51
  })
51
52
  })
52
53
  )
@@ -113,9 +114,22 @@ describe('NumberField', () => {
113
114
 
114
115
  expect(result.errors).toEqual([
115
116
  expect.objectContaining({
116
- text: 'Enter example number field'
117
+ text: 'Enter example number'
117
118
  })
118
119
  ])
120
+ })
121
+
122
+ it('adds errors for empty value given no short description exists', () => {
123
+ def = {
124
+ title: 'Example number field',
125
+ name: 'myComponent',
126
+ type: ComponentType.NumberField,
127
+ options: {},
128
+ schema: {}
129
+ } satisfies NumberFieldComponent
130
+
131
+ collection = new ComponentCollection([def], { model })
132
+ const result = collection.validate(getFormData(''))
119
133
 
120
134
  expect(result.errors).toEqual([
121
135
  expect.objectContaining({
@@ -258,6 +272,14 @@ describe('NumberField', () => {
258
272
 
259
273
  expect(viewModel).toHaveProperty('value', 'AA')
260
274
  })
275
+
276
+ describe('AllPossibleErrors', () => {
277
+ it('should return errors', () => {
278
+ const errors = field.getAllPossibleErrors()
279
+ expect(errors.baseErrors).not.toBeEmpty()
280
+ expect(errors.advancedSettingsErrors).not.toBeEmpty()
281
+ })
282
+ })
261
283
  })
262
284
 
263
285
  describe('Validation', () => {