@defra/forms-engine-plugin 5.0.0-alpha.10 → 5.0.0-alpha.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 (99) hide show
  1. package/.public/javascripts/application.min.js +1 -1
  2. package/.public/javascripts/application.min.js.LICENSE.txt +7 -0
  3. package/.public/javascripts/application.min.js.map +1 -1
  4. package/.public/javascripts/shared.min.js +1 -1
  5. package/.public/javascripts/shared.min.js.LICENSE.txt +7 -0
  6. package/.public/javascripts/shared.min.js.map +1 -1
  7. package/.server/client/javascripts/file-upload.js +1 -0
  8. package/.server/client/javascripts/file-upload.js.map +1 -1
  9. package/.server/client/javascripts/geospatial-map.js +2 -28
  10. package/.server/client/javascripts/geospatial-map.js.map +1 -1
  11. package/.server/client/javascripts/govuk.js +2 -1
  12. package/.server/client/javascripts/govuk.js.map +1 -1
  13. package/.server/client/javascripts/location-map.js +3 -2
  14. package/.server/client/javascripts/location-map.js.map +1 -1
  15. package/.server/client/javascripts/map.d.ts +6 -0
  16. package/.server/client/javascripts/map.js +37 -0
  17. package/.server/client/javascripts/map.js.map +1 -1
  18. package/.server/server/plugins/engine/components/CheckboxesField.d.ts +20 -2
  19. package/.server/server/plugins/engine/components/CheckboxesField.js +24 -12
  20. package/.server/server/plugins/engine/components/CheckboxesField.js.map +1 -1
  21. package/.server/server/plugins/engine/components/EastingNorthingField.d.ts +5 -0
  22. package/.server/server/plugins/engine/components/EastingNorthingField.js +17 -0
  23. package/.server/server/plugins/engine/components/EastingNorthingField.js.map +1 -1
  24. package/.server/server/plugins/engine/components/FileUploadField.d.ts +10 -1
  25. package/.server/server/plugins/engine/components/FileUploadField.js +21 -7
  26. package/.server/server/plugins/engine/components/FileUploadField.js.map +1 -1
  27. package/.server/server/plugins/engine/components/GeospatialField.d.ts +21 -0
  28. package/.server/server/plugins/engine/components/GeospatialField.js +32 -9
  29. package/.server/server/plugins/engine/components/GeospatialField.js.map +1 -1
  30. package/.server/server/plugins/engine/components/LatLongField.d.ts +4 -0
  31. package/.server/server/plugins/engine/components/LocationFieldBase.js +4 -1
  32. package/.server/server/plugins/engine/components/LocationFieldBase.js.map +1 -1
  33. package/.server/server/plugins/engine/components/LocationFieldHelpers.d.ts +5 -1
  34. package/.server/server/plugins/engine/components/LocationFieldHelpers.js +10 -2
  35. package/.server/server/plugins/engine/components/LocationFieldHelpers.js.map +1 -1
  36. package/.server/server/plugins/engine/components/helpers/geospatial.d.ts +8 -1
  37. package/.server/server/plugins/engine/components/helpers/geospatial.js +56 -15
  38. package/.server/server/plugins/engine/components/helpers/geospatial.js.map +1 -1
  39. package/.server/server/plugins/engine/form-availability.d.ts +3 -2
  40. package/.server/server/plugins/engine/form-availability.js +3 -2
  41. package/.server/server/plugins/engine/form-availability.js.map +1 -1
  42. package/.server/server/plugins/engine/form-context.js +2 -2
  43. package/.server/server/plugins/engine/form-context.js.map +1 -1
  44. package/.server/server/plugins/engine/i18n/buildValidationMessages.d.ts +1 -1
  45. package/.server/server/plugins/engine/i18n/buildValidationMessages.js +30 -9
  46. package/.server/server/plugins/engine/i18n/buildValidationMessages.js.map +1 -1
  47. package/.server/server/plugins/engine/i18n/translations/cy.json +79 -13
  48. package/.server/server/plugins/engine/i18n/translations/en-GB.json +42 -13
  49. package/.server/server/plugins/engine/i18n/types.d.ts +1 -0
  50. package/.server/server/plugins/engine/i18n/types.js.map +1 -1
  51. package/.server/server/plugins/engine/models/unavailable-view-model.d.ts +11 -2
  52. package/.server/server/plugins/engine/models/unavailable-view-model.js +23 -4
  53. package/.server/server/plugins/engine/models/unavailable-view-model.js.map +1 -1
  54. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.d.ts +6 -0
  55. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +34 -1
  56. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  57. package/.server/server/plugins/engine/pageControllers/validationOptions.d.ts +1 -1
  58. package/.server/server/plugins/engine/pageControllers/validationOptions.js +1 -1
  59. package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
  60. package/.server/server/plugins/engine/types.d.ts +4 -0
  61. package/.server/server/plugins/engine/types.js.map +1 -1
  62. package/.server/server/plugins/engine/unavailable-response.js +5 -2
  63. package/.server/server/plugins/engine/unavailable-response.js.map +1 -1
  64. package/.server/server/plugins/engine/views/components/_location-field-base.html +3 -2
  65. package/.server/server/plugins/engine/views/file-upload.html +1 -1
  66. package/.server/server/plugins/engine/views/unavailable.html +14 -11
  67. package/.server/server/utils/utils.d.ts +6 -0
  68. package/.server/server/utils/utils.js +18 -1
  69. package/.server/server/utils/utils.js.map +1 -1
  70. package/.server/server/utils/utils.test.js +21 -1
  71. package/.server/server/utils/utils.test.js.map +1 -1
  72. package/package.json +2 -1
  73. package/src/client/javascripts/file-upload.js +1 -0
  74. package/src/client/javascripts/geospatial-map.js +2 -31
  75. package/src/client/javascripts/govuk.js +2 -0
  76. package/src/client/javascripts/location-map.js +3 -1
  77. package/src/client/javascripts/map.js +42 -0
  78. package/src/server/plugins/engine/components/CheckboxesField.ts +34 -13
  79. package/src/server/plugins/engine/components/EastingNorthingField.ts +31 -0
  80. package/src/server/plugins/engine/components/FileUploadField.ts +29 -7
  81. package/src/server/plugins/engine/components/GeospatialField.ts +43 -11
  82. package/src/server/plugins/engine/components/LocationFieldBase.ts +3 -1
  83. package/src/server/plugins/engine/components/LocationFieldHelpers.ts +13 -3
  84. package/src/server/plugins/engine/components/helpers/geospatial.ts +84 -21
  85. package/src/server/plugins/engine/form-availability.ts +8 -2
  86. package/src/server/plugins/engine/form-context.ts +4 -2
  87. package/src/server/plugins/engine/i18n/buildValidationMessages.ts +15 -10
  88. package/src/server/plugins/engine/i18n/translations/cy.json +79 -13
  89. package/src/server/plugins/engine/i18n/translations/en-GB.json +42 -13
  90. package/src/server/plugins/engine/i18n/types.ts +1 -0
  91. package/src/server/plugins/engine/models/unavailable-view-model.ts +26 -5
  92. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +42 -1
  93. package/src/server/plugins/engine/pageControllers/validationOptions.ts +2 -2
  94. package/src/server/plugins/engine/types.ts +4 -0
  95. package/src/server/plugins/engine/unavailable-response.ts +6 -2
  96. package/src/server/plugins/engine/views/components/_location-field-base.html +3 -2
  97. package/src/server/plugins/engine/views/file-upload.html +1 -1
  98. package/src/server/plugins/engine/views/unavailable.html +14 -11
  99. package/src/server/utils/utils.js +19 -1
@@ -1,9 +1,10 @@
1
1
  import { type CheckboxesFieldComponent, type Item } from '@defra/forms-model'
2
- import joi, { type ArraySchema, type LanguageMessages } from 'joi'
2
+ import joi, { type ArraySchema } from 'joi'
3
3
 
4
+ import { EN_GB } from '../../../constants.js'
4
5
  import { isFormValue } from './FormComponent.js'
5
6
  import { SelectionControlField } from './SelectionControlField.js'
6
- import { buildValidationMessages } from '../i18n/buildValidationMessages.js'
7
+ import { t as tPlugin } from '../i18n/index.js'
7
8
  import { type Translator } from '../i18n/types.js'
8
9
  import { type FormModel } from '../models/FormModel.js'
9
10
  import { type QuestionPageController } from '../pageControllers/QuestionPageController.js'
@@ -20,6 +21,7 @@ export class CheckboxesField extends SelectionControlField {
20
21
  declare schema: CheckboxesFieldComponent['schema']
21
22
  declare formSchema: ArraySchema<string> | ArraySchema<number>
22
23
  declare stateSchema: ArraySchema<string> | ArraySchema<number>
24
+ declare limits: { min?: number; max?: number; length?: number }
23
25
 
24
26
  constructor(
25
27
  def: CheckboxesFieldComponent,
@@ -42,11 +44,8 @@ export class CheckboxesField extends SelectionControlField {
42
44
  .single()
43
45
  .label(this.label)
44
46
  .required()
45
- .messages({
46
- 'array.min': messageTemplate.arrayMin,
47
- 'array.max': messageTemplate.arrayMax,
48
- 'array.length': messageTemplate.arrayLength
49
- })
47
+
48
+ const limits: { min?: number; max?: number; length?: number } = {}
50
49
 
51
50
  if (options.required === false) {
52
51
  formSchema = formSchema.optional()
@@ -54,16 +53,25 @@ export class CheckboxesField extends SelectionControlField {
54
53
 
55
54
  if (typeof schema?.length === 'number') {
56
55
  formSchema = formSchema.length(schema.length)
56
+ limits.length = schema.length
57
57
  } else {
58
58
  if (typeof schema?.min === 'number') {
59
59
  formSchema = formSchema.min(schema.min)
60
+ limits.min = schema.min
60
61
  }
61
62
 
62
63
  if (typeof schema?.max === 'number') {
63
64
  formSchema = formSchema.max(schema.max)
65
+ limits.max = schema.max
64
66
  }
65
67
  }
66
68
 
69
+ this.limits = limits
70
+
71
+ formSchema = formSchema.messages(
72
+ CheckboxesField.buildErrorMessages(EN_GB, limits)
73
+ )
74
+
67
75
  this.formSchema = formSchema.default([])
68
76
  this.stateSchema = formSchema.default(null).allow(null)
69
77
  this.options = options
@@ -135,13 +143,9 @@ export class CheckboxesField extends SelectionControlField {
135
143
  }
136
144
 
137
145
  getValidationMessagesOverride(translator: Translator) {
138
- const { t } = translator
146
+ const { language } = translator
139
147
  return {
140
- [this.name]: {
141
- 'array.min': buildValidationMessages(t).arrayMin,
142
- 'array.max': buildValidationMessages(t).arrayMax,
143
- 'array.length': buildValidationMessages(t).arrayLength
144
- } as LanguageMessages
148
+ [this.name]: CheckboxesField.buildErrorMessages(language, this.limits)
145
149
  }
146
150
  }
147
151
 
@@ -181,4 +185,21 @@ export class CheckboxesField extends SelectionControlField {
181
185
 
182
186
  return value.every(isFormValue)
183
187
  }
188
+
189
+ static buildErrorMessages(
190
+ language: string,
191
+ limits: { min?: number; max?: number; length?: number } = {}
192
+ ) {
193
+ return {
194
+ 'array.min': tPlugin('validation.arrayMin', language, {
195
+ count: limits.min ?? 1
196
+ }),
197
+ 'array.max': tPlugin('validation.arrayMax', language, {
198
+ count: limits.max ?? 1
199
+ }),
200
+ 'array.length': tPlugin('validation.arrayLength', language, {
201
+ count: limits.length ?? 1
202
+ })
203
+ }
204
+ }
184
205
  }
@@ -15,6 +15,7 @@ import {
15
15
  getLocationFieldViewModel
16
16
  } from './LocationFieldHelpers.js'
17
17
  import { NumberField } from './NumberField.js'
18
+ import { getEastingNorthingCountryValidator } from './helpers/geospatial.js'
18
19
  import {
19
20
  createLowerFirstExpression,
20
21
  getTranslatedLabel
@@ -112,6 +113,18 @@ export class EastingNorthingField extends FormComponent {
112
113
  )
113
114
 
114
115
  this.options = options
116
+
117
+ const country = options.countries?.at(0)
118
+
119
+ if (country) {
120
+ this.collection.formSchema = this.collection.formSchema.custom(
121
+ getEastingNorthingCountryValidator(this, country)
122
+ )
123
+ this.collection.stateSchema = this.collection.stateSchema.custom(
124
+ getEastingNorthingCountryValidator(this, country)
125
+ )
126
+ }
127
+
115
128
  this.formSchema = this.collection.formSchema
116
129
  this.stateSchema = this.collection.stateSchema
117
130
  }
@@ -164,6 +177,24 @@ export class EastingNorthingField extends FormComponent {
164
177
  return getLocationFieldViewModel(this, viewModel, context)
165
178
  }
166
179
 
180
+ getErrors(
181
+ translator: Translator,
182
+ errors?: FormSubmissionError[]
183
+ ): FormSubmissionError[] | undefined {
184
+ const fieldErrors = super.getErrors(translator, errors)
185
+
186
+ fieldErrors?.forEach((err) => {
187
+ if (err.context?.country) {
188
+ err.text = translator.t(
189
+ 'components.eastingNorthingField.wrongCountry',
190
+ { country: translator.t(`common.${err.context.country}`) }
191
+ )
192
+ }
193
+ })
194
+
195
+ return fieldErrors
196
+ }
197
+
167
198
  getViewErrors(
168
199
  translator: Translator,
169
200
  errors?: FormSubmissionError[]
@@ -52,7 +52,9 @@ export const tempFileSchema = fileSchema.append({
52
52
  .string()
53
53
  .valid(FileStatus.complete, FileStatus.rejected, FileStatus.pending)
54
54
  .required(),
55
- errorMessage: joi.string().optional()
55
+ errorMessage: joi.string().optional(),
56
+ errorCode: joi.string().optional(),
57
+ errorParams: joi.any().optional()
56
58
  })
57
59
 
58
60
  export const formFileSchema = fileSchema.append({
@@ -111,6 +113,7 @@ export class FileUploadField extends FormComponent {
111
113
  declare schema: FileUploadFieldComponent['schema']
112
114
  declare formSchema: ArraySchema<FileState>
113
115
  declare stateSchema: ArraySchema<FileState>
116
+ declare limits: { min?: number; max?: number; length?: number }
114
117
 
115
118
  constructor(
116
119
  def: FileUploadFieldComponent,
@@ -130,21 +133,31 @@ export class FileUploadField extends FormComponent {
130
133
  formSchema = formSchema.optional()
131
134
  }
132
135
 
136
+ const limits: { min?: number; max?: number; length?: number } = {}
137
+
133
138
  if (typeof schema.length !== 'number') {
134
139
  if (typeof schema.max === 'number') {
135
140
  formSchema = formSchema.max(schema.max)
141
+ limits.max = schema.max
136
142
  }
137
143
 
138
144
  if (typeof schema.min === 'number') {
139
145
  formSchema = formSchema.min(schema.min)
146
+ limits.min = schema.min
140
147
  } else if (options.required !== false) {
141
148
  formSchema = formSchema.min(1)
149
+ limits.min = 1
142
150
  }
143
151
  } else {
144
152
  formSchema = formSchema.length(schema.length)
153
+ limits.length = schema.length
145
154
  }
146
155
 
147
- formSchema = formSchema.messages(FileUploadField.buildErrorMessages(EN_GB))
156
+ this.limits = limits
157
+
158
+ formSchema = formSchema.messages(
159
+ FileUploadField.buildErrorMessages(EN_GB, limits)
160
+ )
148
161
 
149
162
  this.formSchema = formSchema.items(formItemSchema)
150
163
  this.stateSchema = formSchema
@@ -307,7 +320,7 @@ export class FileUploadField extends FormComponent {
307
320
  getValidationMessagesOverride(translator: Translator) {
308
321
  const { language } = translator
309
322
  return {
310
- [this.name]: FileUploadField.buildErrorMessages(language)
323
+ [this.name]: FileUploadField.buildErrorMessages(language, this.limits)
311
324
  }
312
325
  }
313
326
 
@@ -415,11 +428,20 @@ export class FileUploadField extends FormComponent {
415
428
  }
416
429
  }
417
430
 
418
- static buildErrorMessages(language: string) {
431
+ static buildErrorMessages(
432
+ language: string,
433
+ limits: { min?: number; max?: number; length?: number } = {}
434
+ ) {
419
435
  return {
420
- 'array.min': tPlugin('validation.filesMin', language),
421
- 'array.max': tPlugin('validation.filesMax', language),
422
- 'array.length': tPlugin('validation.filesLength', language)
436
+ 'array.min': tPlugin('validation.filesMin', language, {
437
+ count: limits.min ?? 1
438
+ }),
439
+ 'array.max': tPlugin('validation.filesMax', language, {
440
+ count: limits.max ?? 1
441
+ }),
442
+ 'array.length': tPlugin('validation.filesLength', language, {
443
+ count: limits.length ?? 1
444
+ })
423
445
  }
424
446
  }
425
447
  }
@@ -1,14 +1,19 @@
1
1
  import { type GeospatialFieldComponent } from '@defra/forms-model'
2
2
  import { type ArraySchema } from 'joi'
3
3
 
4
+ import { EN_GB } from '../../../constants.js'
4
5
  import { type ComponentBase } from './ComponentBase.js'
5
6
  import {
6
7
  FormComponent,
7
8
  isGeospatialState
8
9
  } from './FormComponent.js'
9
10
  import { getMapLayers } from './LocationFieldHelpers.js'
10
- import { getGeospatialSchema } from './helpers/geospatial.js'
11
+ import {
12
+ determineLimits,
13
+ getGeospatialSchema
14
+ } from './helpers/geospatial.js'
11
15
  import { type RenderContext } from './types.js'
16
+ import { t as tPlugin } from '../i18n/index.js'
12
17
  import { type Translator } from '../i18n/types.js'
13
18
  import { messageTemplate } from '../pageControllers/validationOptions.js'
14
19
  import {
@@ -24,6 +29,7 @@ export class GeospatialField extends FormComponent {
24
29
  declare options: GeospatialFieldComponent['options']
25
30
  declare formSchema: ArraySchema<GeospatialState>
26
31
  declare stateSchema: ArraySchema<GeospatialState>
32
+ declare limits: { min?: number; max?: number; length?: number }
27
33
 
28
34
  constructor(
29
35
  def: GeospatialFieldComponent,
@@ -33,20 +39,24 @@ export class GeospatialField extends FormComponent {
33
39
 
34
40
  const { options } = def
35
41
 
36
- const formSchema = getGeospatialSchema(def).label(this.label).messages({
37
- 'array.min': messageTemplate.featuresMin,
38
- 'array.max': messageTemplate.featuresMax,
39
- 'array.length': messageTemplate.featuresLength
40
- })
42
+ let formSchema = getGeospatialSchema(def).label(this.label)
43
+
44
+ const isOptional = options.required === false
45
+ const limits = determineLimits(def, isOptional)
46
+
47
+ formSchema = formSchema.messages(
48
+ GeospatialField.buildErrorMessages(EN_GB, limits)
49
+ )
41
50
 
42
51
  this.formSchema = formSchema
43
52
  this.stateSchema = formSchema.default(null)
44
53
 
45
- if (options.required === false) {
54
+ if (isOptional) {
46
55
  this.stateSchema = this.stateSchema.allow(null)
47
56
  }
48
57
 
49
58
  this.options = options
59
+ this.limits = limits
50
60
  }
51
61
 
52
62
  getFormValueFromState(state: FormSubmissionState) {
@@ -127,9 +137,7 @@ export class GeospatialField extends FormComponent {
127
137
  'components.geospatialField.validation.wrongCountry',
128
138
  {
129
139
  count: Number(err.path[1]) + 1,
130
- country: translator.t(
131
- `common.${(err.context.country as string).toLowerCase()}`
132
- )
140
+ country: translator.t(`common.${err.context.country}`)
133
141
  }
134
142
  )
135
143
  }
@@ -149,6 +157,13 @@ export class GeospatialField extends FormComponent {
149
157
  return isGeospatialState(value)
150
158
  }
151
159
 
160
+ getValidationMessagesOverride(translator: Translator) {
161
+ const { language } = translator
162
+ return {
163
+ [this.name]: GeospatialField.buildErrorMessages(language, this.limits)
164
+ }
165
+ }
166
+
152
167
  /**
153
168
  * For error preview page that shows all possible errors on a component
154
169
  */
@@ -169,11 +184,28 @@ export class GeospatialField extends FormComponent {
169
184
  { type: 'required', template: messageTemplate.selectRequired },
170
185
  {
171
186
  type: 'array.min',
172
- template: '{{#title}} must contain at least 1 items'
187
+ template: '{{#title}} must contain at least 1 item'
173
188
  },
174
189
  { type: 'object.invalidjson', template: messageTemplate.format }
175
190
  ],
176
191
  advancedSettingsErrors: []
177
192
  }
178
193
  }
194
+
195
+ static buildErrorMessages(
196
+ language: string,
197
+ limits: { min?: number; max?: number; length?: number } = {}
198
+ ) {
199
+ return {
200
+ 'array.min': tPlugin('validation.featuresMin', language, {
201
+ count: limits.min ?? 1
202
+ }),
203
+ 'array.max': tPlugin('validation.featuresMax', language, {
204
+ count: limits.max ?? 1
205
+ }),
206
+ 'array.length': tPlugin('validation.featuresLength', language, {
207
+ count: limits.length ?? 1
208
+ })
209
+ }
210
+ }
179
211
  }
@@ -125,9 +125,11 @@ export abstract class LocationFieldBase extends FormComponent {
125
125
  const viewModel = super.getViewModel(context)
126
126
 
127
127
  if (this.instructionText) {
128
+ const { tComponent } = context.translator
128
129
  return {
129
130
  ...viewModel,
130
- instructionText: this.instructionText
131
+ instructionText:
132
+ tComponent(this.def, 'instructionText') || this.instructionText
131
133
  }
132
134
  }
133
135
 
@@ -1,6 +1,6 @@
1
1
  import { type Context, type CustomValidator } from 'joi'
2
2
 
3
- import { type EastingNorthingField } from './EastingNorthingField.js'
3
+ import { EastingNorthingField } from './EastingNorthingField.js'
4
4
  import { isFormValue } from './FormComponent.js'
5
5
  import { type GeospatialField } from './GeospatialField.js'
6
6
  import { type LatLongField } from './LatLongField.js'
@@ -190,13 +190,23 @@ export function getLocationFieldViewModel(
190
190
  ...viewModel,
191
191
  fieldset,
192
192
  items,
193
- mapLayers: getMapLayers(component)
193
+ mapLayers: getMapLayers(component),
194
+ country:
195
+ component instanceof EastingNorthingField
196
+ ? component.options.countries?.at(0)
197
+ : undefined
194
198
  }
195
199
 
196
200
  if (component.options.instructionText) {
201
+ const { t, tComponent } = context.translator
202
+
197
203
  return {
198
204
  ...result,
199
- instructionText: component.options.instructionText
205
+ instructionSummary: t('components.locationFieldBase.howToFind'),
206
+ instructionText:
207
+ tComponent(component.def, 'instructionText') ||
208
+ component.options.instructionText,
209
+ t
200
210
  }
201
211
  }
202
212
 
@@ -1,12 +1,16 @@
1
1
  import {
2
- GeospatialFieldOptionsCountryEnum,
3
2
  type GeospatialFieldComponent,
4
3
  type GeospatialFieldOptionsCountry
5
4
  } from '@defra/forms-model'
6
5
  import Bourne from '@hapi/bourne'
7
6
  import { booleanWithin } from '@turf/boolean-within'
7
+ import { point } from '@turf/helpers'
8
+ // @ts-expect-error - no types
9
+ import OsGridRef from 'geodesy/osgridref.js'
8
10
  import JoiBase, { type CustomValidator } from 'joi'
9
11
 
12
+ import { type EastingNorthingField } from '../EastingNorthingField.js'
13
+ import { type EastingNorthingState } from '../types.js'
10
14
  import {
11
15
  type Coordinates,
12
16
  type Feature,
@@ -15,13 +19,6 @@ import {
15
19
  } from '../../types.js'
16
20
  import { countries } from '../../../map/routes/index.js'
17
21
 
18
- const countriesDesc: Record<GeospatialFieldOptionsCountryEnum, string> = {
19
- [GeospatialFieldOptionsCountryEnum.England]: 'England',
20
- [GeospatialFieldOptionsCountryEnum.NorthernIreland]: 'Northern Ireland',
21
- [GeospatialFieldOptionsCountryEnum.Scotland]: 'Scotland',
22
- [GeospatialFieldOptionsCountryEnum.Wales]: 'Wales'
23
- }
24
-
25
22
  const Joi = JoiBase.extend({
26
23
  type: 'array',
27
24
  base: JoiBase.array(),
@@ -98,25 +95,43 @@ const featureSchema = Joi.object<Feature>().keys({
98
95
  geometry: featureGeometrySchema
99
96
  })
100
97
 
101
- function applySchemaConstraints(
102
- schema: JoiBase.ArraySchema<Feature[]>,
103
- def: GeospatialFieldComponent
98
+ export function determineLimits(
99
+ def: GeospatialFieldComponent,
100
+ isOptional: boolean
104
101
  ) {
105
- const { options, schema: constraints } = def
106
- const isOptional = options.required === false
107
-
102
+ const { schema: constraints } = def
103
+ const limits: { min?: number; max?: number; length?: number } = {}
108
104
  if (typeof constraints?.length === 'number') {
109
- schema = schema.length(constraints.length)
105
+ limits.length = constraints.length
110
106
  } else {
111
107
  if (typeof constraints?.min === 'number') {
112
- schema = schema.min(constraints.min)
108
+ limits.min = constraints.min
113
109
  } else if (!isOptional) {
114
- schema = schema.min(1)
110
+ limits.min = 1
115
111
  }
116
112
 
117
- schema = schema.max(
118
- typeof constraints?.max === 'number' ? constraints.max : 50
119
- )
113
+ limits.max = typeof constraints?.max === 'number' ? constraints.max : 50
114
+ }
115
+ return limits
116
+ }
117
+
118
+ function applySchemaConstraints(
119
+ schema: JoiBase.ArraySchema<Feature[]>,
120
+ def: GeospatialFieldComponent
121
+ ) {
122
+ const { options } = def
123
+ const isOptional = options.required === false
124
+
125
+ const limits = determineLimits(def, isOptional)
126
+
127
+ if (limits.length !== undefined) {
128
+ schema = schema.length(limits.length)
129
+ }
130
+ if (limits.min !== undefined) {
131
+ schema = schema.min(limits.min)
132
+ }
133
+ if (limits.max !== undefined) {
134
+ schema = schema.max(limits.max)
120
135
  }
121
136
 
122
137
  if (isOptional) {
@@ -156,7 +171,7 @@ export function getGeospatialSchema(
156
171
 
157
172
  if (!result) {
158
173
  return helpers.error('any.custom', {
159
- country: countriesDesc[country as GeospatialFieldOptionsCountryEnum]
174
+ country
160
175
  })
161
176
  }
162
177
 
@@ -171,3 +186,51 @@ export function getGeospatialSchema(
171
186
  def
172
187
  )
173
188
  }
189
+
190
+ export function getEastingNorthingCountryValidator(
191
+ field: EastingNorthingField,
192
+ country: GeospatialFieldOptionsCountry
193
+ ) {
194
+ const validateCountryBounds: CustomValidator = (value, helpers) => {
195
+ const countryFeature = countries.features.find(
196
+ (feature) => feature.id === country
197
+ )
198
+
199
+ if (!countryFeature) {
200
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
201
+ return value
202
+ }
203
+
204
+ const eastingNorthing = field.getFormValueFromState(
205
+ value as EastingNorthingState
206
+ )
207
+
208
+ if (!eastingNorthing) {
209
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
210
+ return value
211
+ }
212
+
213
+ const gridRef = new OsGridRef(
214
+ eastingNorthing.easting,
215
+ eastingNorthing.northing
216
+ )
217
+ const latLong = gridRef.toLatLon()
218
+ const coord = point([latLong.longitude, latLong.latitude])
219
+ const within = booleanWithin(coord, countryFeature)
220
+
221
+ if (!within) {
222
+ return helpers.error(
223
+ 'any.custom',
224
+ {
225
+ country
226
+ },
227
+ { path: [field.name] }
228
+ )
229
+ }
230
+
231
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
232
+ return value
233
+ }
234
+
235
+ return validateCountryBounds
236
+ }
@@ -1,9 +1,14 @@
1
- import { FormStatus, type FormMetadata } from '@defra/forms-model'
1
+ import {
2
+ FormStatus,
3
+ type FormDefinition,
4
+ type FormMetadata
5
+ } from '@defra/forms-model'
2
6
  import Boom from '@hapi/boom'
3
7
 
4
8
  export interface OfflineBoomData {
5
9
  offline: true
6
10
  metadata: FormMetadata
11
+ definition: FormDefinition | undefined
7
12
  }
8
13
 
9
14
  /**
@@ -13,6 +18,7 @@ export interface OfflineBoomData {
13
18
  */
14
19
  export function assertFormAvailable(
15
20
  metadata: FormMetadata,
21
+ definition: FormDefinition | undefined,
16
22
  formState: FormStatus,
17
23
  isPreview: boolean
18
24
  ): void {
@@ -21,7 +27,7 @@ export function assertFormAvailable(
21
27
  formState === FormStatus.Live &&
22
28
  !isPreview
23
29
  ) {
24
- const data: OfflineBoomData = { offline: true, metadata }
30
+ const data: OfflineBoomData = { offline: true, metadata, definition }
25
31
  throw Boom.boomify(new Error(`Form ${metadata.slug} is offline`), {
26
32
  statusCode: 503,
27
33
  data
@@ -56,13 +56,14 @@ export async function getFormModel(
56
56
  const formState = resolveState(state)
57
57
 
58
58
  const metadata = await formsService.getFormMetadata(slug)
59
- assertFormAvailable(metadata, formState, isPreview)
60
59
 
61
60
  const definition = await formsService.getFormDefinition(
62
61
  metadata.id,
63
62
  formState
64
63
  )
65
64
 
65
+ assertFormAvailable(metadata, definition, formState, isPreview)
66
+
66
67
  if (!definition) {
67
68
  throw Boom.notFound(
68
69
  `No definition found for form metadata ${metadata.id} (${slug}) ${state}`
@@ -145,7 +146,6 @@ export async function resolveFormModel(
145
146
  const metadata = await formsService.getFormMetadata(slug)
146
147
  const formState = resolveState(state)
147
148
  const isPreview = options.isPreview ?? isPreviewState(state, options)
148
- assertFormAvailable(metadata, formState, isPreview)
149
149
  const stateMetadata = metadata[formState]
150
150
 
151
151
  if (!stateMetadata) {
@@ -199,6 +199,8 @@ export async function resolveFormModel(
199
199
  cache.set(cacheKey, entry)
200
200
  }
201
201
 
202
+ assertFormAvailable(metadata, entry.model.def, formState, isPreview)
203
+
202
204
  return entry.model
203
205
  }
204
206
 
@@ -16,7 +16,9 @@ const opts = {
16
16
  * because Joi cannot resolve custom functions from a plain string.
17
17
  * Plain strings with only built-in tokens ({{#label}}, {{#limit}}) work without wrapping.
18
18
  */
19
- export function buildValidationMessages(t: (key: string) => string) {
19
+ export function buildValidationMessages(
20
+ t: (key: string, options?: Record<string, unknown>) => string
21
+ ) {
20
22
  return {
21
23
  // Expressions — contain lowerFirst custom function
22
24
  required: joi.expression(t('validation.required'), opts) as JoiExpression,
@@ -57,15 +59,18 @@ export function buildValidationMessages(t: (key: string) => string) {
57
59
  dateMax: t('components.dateField.validation.dateMax'),
58
60
 
59
61
  unicode: t('validation.unicode'),
60
- arrayMin: t('validation.arrayMin'),
61
- arrayMax: t('validation.arrayMax'),
62
- arrayLength: t('validation.arrayLength'),
63
- featuresMin: t('validation.featuresMin'),
64
- featuresMax: t('validation.featuresMax'),
65
- featuresLength: t('validation.featuresLength'),
66
- filesMin: t('validation.filesMin'),
67
- filesMax: t('validation.filesMax'),
68
- filesLength: t('validation.filesLength')
62
+ // These generic templates are shared across fields with differing limits, so a
63
+ // fixed singular count is used here — components needing accurate singular/plural
64
+ // text (e.g. FileUploadField) build their own per-instance messages instead.
65
+ arrayMin: t('validation.arrayMin', { count: 1 }),
66
+ arrayMax: t('validation.arrayMax', { count: 1 }),
67
+ arrayLength: t('validation.arrayLength', { count: 1 }),
68
+ featuresMin: t('validation.featuresMin', { count: 1 }),
69
+ featuresMax: t('validation.featuresMax', { count: 1 }),
70
+ featuresLength: t('validation.featuresLength', { count: 1 }),
71
+ filesMin: t('validation.filesMin', { count: 1 }),
72
+ filesMax: t('validation.filesMax', { count: 1 }),
73
+ filesLength: t('validation.filesLength', { count: 1 })
69
74
  }
70
75
  }
71
76