@defra/forms-engine-plugin 5.0.0-alpha.10 → 5.0.0-alpha.11

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 (60) 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/EastingNorthingField.d.ts +3 -0
  19. package/.server/server/plugins/engine/components/EastingNorthingField.js +17 -0
  20. package/.server/server/plugins/engine/components/EastingNorthingField.js.map +1 -1
  21. package/.server/server/plugins/engine/components/FileUploadField.js +3 -1
  22. package/.server/server/plugins/engine/components/FileUploadField.js.map +1 -1
  23. package/.server/server/plugins/engine/components/LatLongField.d.ts +2 -0
  24. package/.server/server/plugins/engine/components/LocationFieldHelpers.d.ts +3 -1
  25. package/.server/server/plugins/engine/components/LocationFieldHelpers.js +3 -1
  26. package/.server/server/plugins/engine/components/LocationFieldHelpers.js.map +1 -1
  27. package/.server/server/plugins/engine/components/helpers/geospatial.d.ts +3 -1
  28. package/.server/server/plugins/engine/components/helpers/geospatial.js +32 -0
  29. package/.server/server/plugins/engine/components/helpers/geospatial.js.map +1 -1
  30. package/.server/server/plugins/engine/i18n/translations/cy.json +13 -3
  31. package/.server/server/plugins/engine/i18n/translations/en-GB.json +13 -3
  32. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.d.ts +6 -0
  33. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +34 -1
  34. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  35. package/.server/server/plugins/engine/types.d.ts +4 -0
  36. package/.server/server/plugins/engine/types.js.map +1 -1
  37. package/.server/server/plugins/engine/views/components/_location-field-base.html +2 -1
  38. package/.server/server/plugins/engine/views/file-upload.html +1 -1
  39. package/.server/server/utils/utils.d.ts +6 -0
  40. package/.server/server/utils/utils.js +18 -1
  41. package/.server/server/utils/utils.js.map +1 -1
  42. package/.server/server/utils/utils.test.js +21 -1
  43. package/.server/server/utils/utils.test.js.map +1 -1
  44. package/package.json +2 -1
  45. package/src/client/javascripts/file-upload.js +1 -0
  46. package/src/client/javascripts/geospatial-map.js +2 -31
  47. package/src/client/javascripts/govuk.js +2 -0
  48. package/src/client/javascripts/location-map.js +3 -1
  49. package/src/client/javascripts/map.js +42 -0
  50. package/src/server/plugins/engine/components/EastingNorthingField.ts +31 -0
  51. package/src/server/plugins/engine/components/FileUploadField.ts +3 -1
  52. package/src/server/plugins/engine/components/LocationFieldHelpers.ts +6 -2
  53. package/src/server/plugins/engine/components/helpers/geospatial.ts +53 -0
  54. package/src/server/plugins/engine/i18n/translations/cy.json +13 -3
  55. package/src/server/plugins/engine/i18n/translations/en-GB.json +13 -3
  56. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +42 -1
  57. package/src/server/plugins/engine/types.ts +4 -0
  58. package/src/server/plugins/engine/views/components/_location-field-base.html +2 -1
  59. package/src/server/plugins/engine/views/file-upload.html +1 -1
  60. package/src/server/utils/utils.js +19 -1
@@ -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: 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({
@@ -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,7 +190,11 @@ 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) {
@@ -5,8 +5,13 @@ import {
5
5
  } from '@defra/forms-model'
6
6
  import Bourne from '@hapi/bourne'
7
7
  import { booleanWithin } from '@turf/boolean-within'
8
+ import { point } from '@turf/helpers'
9
+ // @ts-expect-error - no types
10
+ import OsGridRef from 'geodesy/osgridref.js'
8
11
  import JoiBase, { type CustomValidator } from 'joi'
9
12
 
13
+ import { type EastingNorthingField } from '../EastingNorthingField.js'
14
+ import { type EastingNorthingState } from '../types.js'
10
15
  import {
11
16
  type Coordinates,
12
17
  type Feature,
@@ -171,3 +176,51 @@ export function getGeospatialSchema(
171
176
  def
172
177
  )
173
178
  }
179
+
180
+ export function getEastingNorthingCountryValidator(
181
+ field: EastingNorthingField,
182
+ country: GeospatialFieldOptionsCountry
183
+ ) {
184
+ const validateCountryBounds: CustomValidator = (value, helpers) => {
185
+ const countryFeature = countries.features.find(
186
+ (feature) => feature.id === country
187
+ )
188
+
189
+ if (!countryFeature) {
190
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
191
+ return value
192
+ }
193
+
194
+ const eastingNorthing = field.getFormValueFromState(
195
+ value as EastingNorthingState
196
+ )
197
+
198
+ if (!eastingNorthing) {
199
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
200
+ return value
201
+ }
202
+
203
+ const gridRef = new OsGridRef(
204
+ eastingNorthing.easting,
205
+ eastingNorthing.northing
206
+ )
207
+ const latLong = gridRef.toLatLon()
208
+ const coord = point([latLong.longitude, latLong.latitude])
209
+ const within = booleanWithin(coord, countryFeature)
210
+
211
+ if (!within) {
212
+ return helpers.error(
213
+ 'any.custom',
214
+ {
215
+ country: countriesDesc[country as GeospatialFieldOptionsCountryEnum]
216
+ },
217
+ { path: [field.name] }
218
+ )
219
+ }
220
+
221
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
222
+ return value
223
+ }
224
+
225
+ return validateCountryBounds
226
+ }
@@ -129,7 +129,13 @@
129
129
  "removeFileTitle": "Ydych chi'n siŵr eich bod chi eisiau dileu'r ffeil hon?",
130
130
  "removeFileBody": "Ni allwch adfer ffeiliau a dynnwyd.",
131
131
  "removeFileButton": "Dileu ffeil",
132
- "cancel": "Canslo"
132
+ "cancel": "Canslo",
133
+ "FILE_VIRUS": "Mae'r ffeil a ddewiswyd yn cynnwys firws",
134
+ "FILE_EMPTY": "Mae'r ffeil a ddewiswyd yn wag",
135
+ "FILE_TOO_LARGE": "Rhaid i'r ffeil a ddewiswyd fod yn llai na [[maxFileSizeFormatted]]",
136
+ "FILE_INVALID_TYPE": "Rhaid i'r ffeil a ddewiswyd fod yn [[fileExtensions]]",
137
+ "FILE_UPLOAD_FAILED": "Ni ellid uwchlwytho'r ffeil a ddewiswyd - rhowch gynnig arall arni",
138
+ "FILE_ERROR_UNKNOWN": "Gwall anhysbys"
133
139
  },
134
140
 
135
141
  "exit": {
@@ -244,7 +250,8 @@
244
250
  "eastingDigits": "Rhaid i {{#label}} ar gyfer [[fieldLabel]] fod rhwng 1 a 6 digid",
245
251
  "northingDigits": "Rhaid i {{#label}} ar gyfer [[fieldLabel]] fod rhwng 1 a 7 digid",
246
252
  "eastingRange": "Rhaid i {{#label}} ar gyfer [[fieldLabel]] fod rhwng [[min]] a [[max]]",
247
- "northingRange": "Rhaid i {{#label}} ar gyfer [[fieldLabel]] fod rhwng [[min]] a [[max]]"
253
+ "northingRange": "Rhaid i {{#label}} ar gyfer [[fieldLabel]] fod rhwng [[min]] a [[max]]",
254
+ "wrongCountry": "Rhaid i leoliad fod y tu mewn i [[country]]"
248
255
  },
249
256
 
250
257
  "nationalGridField": {
@@ -319,7 +326,10 @@
319
326
  "filesCount_two": "Uwchlwythwyd [[count]] ffeil",
320
327
  "filesCount_few": "Uwchlwythwyd [[count]] ffeil",
321
328
  "filesCount_many": "Uwchlwythwyd [[count]] o ffeiliau",
322
- "filesCount_other": "Uwchlwythwyd [[count]] ffeil"
329
+ "filesCount_other": "Uwchlwythwyd [[count]] ffeil",
330
+ "chooseFile": "Dewiswch ffeil",
331
+ "dropFile": "neu ollwng ffeil",
332
+ "noFileChosen": "Dim ffeil wedi'i dewis"
323
333
  },
324
334
 
325
335
  "characterCount": {
@@ -113,7 +113,13 @@
113
113
  "removeFileTitle": "Are you sure you want to remove this file?",
114
114
  "removeFileBody": "You cannot recover removed files.",
115
115
  "removeFileButton": "Remove file",
116
- "cancel": "Cancel"
116
+ "cancel": "Cancel",
117
+ "FILE_VIRUS": "The selected file contains a virus",
118
+ "FILE_EMPTY": "The selected file is empty",
119
+ "FILE_TOO_LARGE": "The selected file must be smaller than [[maxFileSizeFormatted]]",
120
+ "FILE_INVALID_TYPE": "The selected file must be a [[fileExtensions]]",
121
+ "FILE_UPLOAD_FAILED": "The selected file could not be uploaded - try again",
122
+ "FILE_ERROR_UNKNOWN": "Unknown error"
117
123
  },
118
124
 
119
125
  "exit": {
@@ -224,7 +230,8 @@
224
230
  "eastingDigits": "{{#label}} for [[fieldLabel]] must be between 1 and 6 digits",
225
231
  "northingDigits": "{{#label}} for [[fieldLabel]] must be between 1 and 7 digits",
226
232
  "eastingRange": "{{#label}} for [[fieldLabel]] must be between [[min]] and [[max]]",
227
- "northingRange": "{{#label}} for [[fieldLabel]] must be between [[min]] and [[max]]"
233
+ "northingRange": "{{#label}} for [[fieldLabel]] must be between [[min]] and [[max]]",
234
+ "wrongCountry": "Location must be within [[country]]"
228
235
  },
229
236
 
230
237
  "nationalGridField": {
@@ -291,7 +298,10 @@
291
298
  "uploadFailed": "There was a problem with your uploaded files. Re-upload them before submitting the form again.",
292
299
  "uploadedFilesHeading": "Uploaded files",
293
300
  "filesCount_one": "[[count]] file uploaded",
294
- "filesCount_other": "[[count]] files uploaded"
301
+ "filesCount_other": "[[count]] files uploaded",
302
+ "chooseFile": "Choose file",
303
+ "dropFile": "or drop file",
304
+ "noFileChosen": "No file chosen"
295
305
  },
296
306
 
297
307
  "characterCount": {
@@ -42,6 +42,7 @@ import {
42
42
  type FormRequestPayload,
43
43
  type FormResponseToolkit
44
44
  } from '../../../routes/types.js'
45
+ import { joinWithOr } from '../../../utils/utils.js'
45
46
 
46
47
  const MAX_UPLOADS = 25
47
48
  const CDP_UPLOAD_TIMEOUT_MS = 60000 // 1 minute
@@ -285,6 +286,13 @@ export class FileUploadPageController extends QuestionPageController {
285
286
  summaryList: formComponent.model.upload?.summaryList ?? { rows: [] },
286
287
  uploadingLabel: t('components.fileUploadField.uploading')
287
288
  }
289
+ formComponent.model = {
290
+ ...formComponent.model,
291
+ javascript: true,
292
+ chooseFilesButtonText: t('components.fileUploadField.chooseFile'),
293
+ dropInstructionText: t('components.fileUploadField.dropFile'),
294
+ noFileChosenText: t('components.fileUploadField.noFileChosen')
295
+ }
288
296
 
289
297
  const index = components.indexOf(formComponent)
290
298
 
@@ -451,6 +459,9 @@ export class FileUploadPageController extends QuestionPageController {
451
459
 
452
460
  const allErrors: FormSubmissionError[] = []
453
461
 
462
+ const translator = this.getTranslator(request)
463
+ const { t } = translator
464
+
454
465
  for (const file of uploadedFiles) {
455
466
  if (file.fileStatus === FileStatus.complete) {
456
467
  const perFileState: FileState = {
@@ -465,7 +476,14 @@ export class FileUploadPageController extends QuestionPageController {
465
476
  // Collect the error for rejected/pending files.
466
477
  const { fileUpload } = this
467
478
  const name = fileUpload.name
468
- const text = file.errorMessage ?? 'Unknown error'
479
+ const errorParams = this.expandErrorParams(
480
+ file.errorParams,
481
+ t('common.or')
482
+ )
483
+ const text = t(
484
+ `pages.fileUpload.${file.errorCode ?? 'FILE_ERROR_UNKNOWN'}`,
485
+ errorParams
486
+ )
469
487
  allErrors.push({ path: [name], href: `#${name}`, name, text })
470
488
  }
471
489
  }
@@ -482,6 +500,29 @@ export class FileUploadPageController extends QuestionPageController {
482
500
  }
483
501
  }
484
502
 
503
+ /**
504
+ * Process the error parameters so any arrays get expanded (joined), ready to be passed into the error message
505
+ * @param errorParams - received from the file upload (may be a number, a string, or a string array)
506
+ * @param finalSeparator - 'or' in English, and 'neu' in Welsh
507
+ */
508
+ private expandErrorParams(
509
+ errorParams: Record<string, number | string | string[]> | undefined,
510
+ finalSeparator: string
511
+ ) {
512
+ if (!errorParams) {
513
+ return {}
514
+ }
515
+ const expanded = {} as Record<string, number | string>
516
+ for (const [key, value] of Object.entries(errorParams)) {
517
+ if (Array.isArray(value)) {
518
+ expanded[key] = joinWithOr(value, finalSeparator)
519
+ } else {
520
+ expanded[key] = value
521
+ }
522
+ }
523
+ return expanded
524
+ }
525
+
485
526
  /**
486
527
  * Checks the payload for a file getting removed
487
528
  * and removes it from the upload files if found
@@ -245,11 +245,15 @@ export type FileUpload = {
245
245
  } & (
246
246
  | {
247
247
  fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending
248
+ errorCode?: string
248
249
  errorMessage?: string
250
+ errorParams?: Record<string, number | string | string[]>
249
251
  }
250
252
  | {
251
253
  fileStatus: FileStatus.complete
254
+ errorCode?: string
252
255
  errorMessage?: undefined
256
+ errorParams?: Record<string, number | string | string[]>
253
257
  }
254
258
  )
255
259
 
@@ -70,8 +70,9 @@
70
70
 
71
71
  {% set hasErrors = showFieldsetError and component.model.errorMessage %}
72
72
 
73
- <div class="govuk-form-group app-location-field {{ "govuk-form-group--error" if hasErrors }}"
73
+ <div id="{{component.model.name}}" class="govuk-form-group app-location-field {{ "govuk-form-group--error" if hasErrors }}"
74
74
  data-locationtype="{{component.type | lower}}"
75
+ data-country="{{component.model.country}}"
75
76
  data-maplayers="{{component.model.mapLayers}}">
76
77
  {{ govukFieldset({
77
78
  legend: {
@@ -15,7 +15,7 @@
15
15
  {% if formAction %}
16
16
  {{ govukButton({
17
17
  text: t('pages.fileUpload.upload'),
18
- classes: "govuk-button--secondary upload-file-button",
18
+ classes: "govuk-button--secondary upload-file-button js-hidden",
19
19
  preventDoubleClick: true,
20
20
  disabled: context.isForceAccess
21
21
  }) }}
@@ -44,6 +44,24 @@ export function resolveLanguage(request) {
44
44
  }
45
45
 
46
46
  /**
47
- * @import { FormMetadata } from '@defra/forms-model'
47
+ * Create a comma-separated list with the last seperator being 'or'
48
+ * @param { string[] | undefined } items
49
+ * @param {string} [finalSeparator]
50
+ */
51
+ export function joinWithOr(items, finalSeparator = 'or') {
52
+ if (!items || items.length === 0) {
53
+ return ''
54
+ }
55
+ if (items.length === 1) {
56
+ return items[0]
57
+ }
58
+ if (items.length === 2) {
59
+ return `${items[0]} ${finalSeparator} ${items[1]}`
60
+ }
61
+
62
+ return `${items.slice(0, -1).join(', ')} ${finalSeparator} ${items.at(-1)}`
63
+ }
64
+
65
+ /**
48
66
  * @import { AnyFormRequest } from '../plugins/engine/types.js'
49
67
  */