@defra/forms-engine-plugin 0.1.25 → 0.1.27

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 (124) hide show
  1. package/.public/assets/images/govuk-crest.svg +1 -1
  2. package/.public/assets/rebrand/images/favicon.ico +0 -0
  3. package/.public/assets/rebrand/images/favicon.svg +1 -0
  4. package/.public/assets/rebrand/images/govuk-crest.svg +1 -0
  5. package/.public/assets/rebrand/images/govuk-icon-180.png +0 -0
  6. package/.public/assets/rebrand/images/govuk-icon-192.png +0 -0
  7. package/.public/assets/rebrand/images/govuk-icon-512.png +0 -0
  8. package/.public/assets/rebrand/images/govuk-icon-mask.svg +1 -0
  9. package/.public/assets/rebrand/images/govuk-opengraph-image.png +0 -0
  10. package/.public/assets/rebrand/manifest.json +39 -0
  11. package/.public/assets-manifest.json +12 -2
  12. package/.public/javascripts/application.min.js +1 -1
  13. package/.public/javascripts/application.min.js.LICENSE.txt +5 -1
  14. package/.public/javascripts/application.min.js.map +1 -1
  15. package/.public/javascripts/shared.min.js +3 -0
  16. package/.public/javascripts/shared.min.js.LICENSE.txt +62 -0
  17. package/.public/javascripts/shared.min.js.map +1 -0
  18. package/.public/stylesheets/application.min.css +3 -3
  19. package/.public/stylesheets/application.min.css.map +1 -1
  20. package/.server/client/javascripts/application.js +2 -67
  21. package/.server/client/javascripts/application.js.map +1 -1
  22. package/.server/client/javascripts/autocomplete.d.ts +1 -0
  23. package/.server/client/javascripts/autocomplete.js +49 -0
  24. package/.server/client/javascripts/autocomplete.js.map +1 -0
  25. package/.server/client/javascripts/file-upload.js +8 -1
  26. package/.server/client/javascripts/file-upload.js.map +1 -1
  27. package/.server/client/javascripts/govuk.d.ts +1 -0
  28. package/.server/client/javascripts/govuk.js +12 -0
  29. package/.server/client/javascripts/govuk.js.map +1 -0
  30. package/.server/client/javascripts/preview-close-link.d.ts +1 -0
  31. package/.server/client/javascripts/preview-close-link.js +12 -0
  32. package/.server/client/javascripts/preview-close-link.js.map +1 -0
  33. package/.server/client/javascripts/shared.d.ts +9 -0
  34. package/.server/client/javascripts/shared.js +15 -0
  35. package/.server/client/javascripts/shared.js.map +1 -0
  36. package/.server/client/stylesheets/_tag-env.scss +9 -0
  37. package/.server/config/index.d.ts +1 -0
  38. package/.server/config/index.js +9 -0
  39. package/.server/config/index.js.map +1 -1
  40. package/.server/index.js +6 -2
  41. package/.server/index.js.map +1 -1
  42. package/.server/server/common/helpers/logging/request-tracing.js +1 -1
  43. package/.server/server/common/helpers/logging/request-tracing.js.map +1 -1
  44. package/.server/server/common/helpers/redis-client.js +5 -3
  45. package/.server/server/common/helpers/redis-client.js.map +1 -1
  46. package/.server/server/constants.d.ts +0 -1
  47. package/.server/server/constants.js +0 -1
  48. package/.server/server/constants.js.map +1 -1
  49. package/.server/server/index.js +3 -1
  50. package/.server/server/index.js.map +1 -1
  51. package/.server/server/plugins/engine/components/DatePartsField.d.ts +1 -6
  52. package/.server/server/plugins/engine/components/DatePartsField.js +2 -1
  53. package/.server/server/plugins/engine/components/DatePartsField.js.map +1 -1
  54. package/.server/server/plugins/engine/components/MonthYearField.d.ts +1 -5
  55. package/.server/server/plugins/engine/components/MonthYearField.js +3 -2
  56. package/.server/server/plugins/engine/components/MonthYearField.js.map +1 -1
  57. package/.server/server/plugins/engine/components/YesNoField.js +2 -1
  58. package/.server/server/plugins/engine/components/YesNoField.js.map +1 -1
  59. package/.server/server/plugins/engine/components/types.d.ts +9 -0
  60. package/.server/server/plugins/engine/components/types.js.map +1 -1
  61. package/.server/server/plugins/engine/date-helper.d.ts +12 -0
  62. package/.server/server/plugins/engine/date-helper.js +21 -0
  63. package/.server/server/plugins/engine/date-helper.js.map +1 -0
  64. package/.server/server/plugins/engine/helpers.js +4 -3
  65. package/.server/server/plugins/engine/helpers.js.map +1 -1
  66. package/.server/server/plugins/engine/index.d.ts +0 -1
  67. package/.server/server/plugins/engine/index.js +0 -1
  68. package/.server/server/plugins/engine/index.js.map +1 -1
  69. package/.server/server/plugins/engine/models/FormModel.d.ts +13 -6
  70. package/.server/server/plugins/engine/models/FormModel.js +51 -18
  71. package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
  72. package/.server/server/plugins/engine/outputFormatters/machine/v2.js.map +1 -1
  73. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +3 -2
  74. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  75. package/.server/server/plugins/engine/plugin.js +6 -5
  76. package/.server/server/plugins/engine/plugin.js.map +1 -1
  77. package/.server/server/plugins/engine/services/notifyService.js +3 -1
  78. package/.server/server/plugins/engine/services/notifyService.js.map +1 -1
  79. package/.server/server/plugins/engine/views/components/tag-env/template.njk +6 -2
  80. package/.server/server/plugins/engine/views/file-upload.html +0 -13
  81. package/.server/server/plugins/errorPages.js +1 -1
  82. package/.server/server/plugins/errorPages.js.map +1 -1
  83. package/.server/server/plugins/nunjucks/context.js +1 -1
  84. package/.server/server/plugins/nunjucks/context.js.map +1 -1
  85. package/.server/server/plugins/nunjucks/environment.js +1 -0
  86. package/.server/server/plugins/nunjucks/environment.js.map +1 -1
  87. package/package.json +21 -25
  88. package/src/client/javascripts/application.js +2 -86
  89. package/src/client/javascripts/autocomplete.js +57 -0
  90. package/src/client/javascripts/file-upload.js +9 -1
  91. package/src/client/javascripts/govuk.js +22 -0
  92. package/src/client/javascripts/preview-close-link.js +12 -0
  93. package/src/client/javascripts/shared.js +16 -0
  94. package/src/client/stylesheets/_tag-env.scss +9 -0
  95. package/src/config/index.ts +10 -0
  96. package/src/index.ts +7 -2
  97. package/src/server/common/helpers/logging/request-tracing.js +1 -1
  98. package/src/server/common/helpers/redis-client.js +5 -3
  99. package/src/server/constants.js +0 -1
  100. package/src/server/index.ts +5 -2
  101. package/src/server/plugins/engine/components/DatePartsField.test.ts +17 -0
  102. package/src/server/plugins/engine/components/DatePartsField.ts +7 -8
  103. package/src/server/plugins/engine/components/MonthYearField.test.ts +15 -0
  104. package/src/server/plugins/engine/components/MonthYearField.ts +12 -8
  105. package/src/server/plugins/engine/components/YesNoField.ts +16 -2
  106. package/src/server/plugins/engine/components/types.ts +11 -0
  107. package/src/server/plugins/engine/date-helper.test.ts +47 -0
  108. package/src/server/plugins/engine/date-helper.ts +32 -0
  109. package/src/server/plugins/engine/helpers.ts +9 -2
  110. package/src/server/plugins/engine/index.ts +0 -1
  111. package/src/server/plugins/engine/models/FormModel.test.ts +163 -1
  112. package/src/server/plugins/engine/models/FormModel.ts +90 -23
  113. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +4 -2
  114. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +2 -1
  115. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +6 -2
  116. package/src/server/plugins/engine/plugin.ts +11 -7
  117. package/src/server/plugins/engine/services/notifyService.ts +6 -2
  118. package/src/server/plugins/engine/views/components/tag-env/template.njk +6 -2
  119. package/src/server/plugins/engine/views/file-upload.html +0 -13
  120. package/src/server/plugins/errorPages.ts +5 -1
  121. package/src/server/plugins/nunjucks/context.js +3 -1
  122. package/src/server/plugins/nunjucks/environment.js +1 -0
  123. package/.public/javascripts/file-upload.min.js +0 -2
  124. package/.public/javascripts/file-upload.min.js.map +0 -1
@@ -1,87 +1,3 @@
1
- import {
2
- Button,
3
- CharacterCount,
4
- Checkboxes,
5
- ErrorSummary,
6
- Header,
7
- NotificationBanner,
8
- Radios,
9
- SkipLink,
10
- createAll
11
- } from 'govuk-frontend'
1
+ import { initAll } from '~/src/client/javascripts/shared.js'
12
2
 
13
- createAll(Button)
14
- createAll(CharacterCount)
15
- createAll(Checkboxes)
16
- createAll(ErrorSummary)
17
- createAll(Header)
18
- createAll(NotificationBanner)
19
- createAll(Radios)
20
- createAll(SkipLink)
21
-
22
- // Show preview close link via `rel="opener"`
23
- if (window.opener) {
24
- const $closeLink = document.querySelector('.js-preview-banner-close')
25
-
26
- $closeLink?.removeAttribute('hidden')
27
- $closeLink?.addEventListener('click', (event) => {
28
- event.preventDefault()
29
- window.close()
30
- })
31
- }
32
-
33
- /**
34
- * Initialise autocomplete
35
- * @param {HTMLSelectElement | null} $select
36
- * @param {(config: object) => void} init
37
- */
38
- function initAutocomplete($select, init) {
39
- if (!$select) {
40
- return
41
- }
42
-
43
- const config = {
44
- id: $select.id,
45
- selectElement: $select
46
- }
47
-
48
- init(config)
49
-
50
- /** @type {HTMLInputElement | null} */
51
- const $input = document.querySelector(`#${config.id}`)
52
-
53
- // Allowed values for input
54
- const inputValues = [...$select.options].map((option) => option.text)
55
-
56
- // Reset select when input value is not allowed
57
- $input?.addEventListener('blur', () => {
58
- if (!$input.value || !inputValues.includes($input.value)) {
59
- $select.value = ''
60
- }
61
- })
62
- }
63
-
64
- // Find all autocompletes
65
- const $autocompletes = document.querySelectorAll(
66
- `[data-module="govuk-accessible-autocomplete"]`
67
- )
68
-
69
- // Lazy load autocomplete component
70
- if ($autocompletes.length) {
71
- // @ts-expect-error -- No types available
72
- import('accessible-autocomplete')
73
- .then((component) => {
74
- const { default: accessibleAutocomplete } = component
75
-
76
- // Initialise each autocomplete
77
- $autocompletes.forEach(($module) =>
78
- initAutocomplete(
79
- $module.querySelector('select'),
80
- accessibleAutocomplete.enhanceSelectElement
81
- )
82
- )
83
- })
84
-
85
- // eslint-disable-next-line no-console
86
- .catch(console.error)
87
- }
3
+ initAll()
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Initialise autocomplete
3
+ * @param {HTMLSelectElement | null} $select
4
+ * @param {(config: object) => void} init
5
+ */
6
+ function initAutocomplete($select, init) {
7
+ if (!$select) {
8
+ return
9
+ }
10
+
11
+ const config = {
12
+ id: $select.id,
13
+ selectElement: $select
14
+ }
15
+
16
+ init(config)
17
+
18
+ /** @type {HTMLInputElement | null} */
19
+ const $input = document.querySelector(`#${config.id}`)
20
+
21
+ // Allowed values for input
22
+ const inputValues = [...$select.options].map((option) => option.text)
23
+
24
+ // Reset select when input value is not allowed
25
+ $input?.addEventListener('blur', () => {
26
+ if (!$input.value || !inputValues.includes($input.value)) {
27
+ $select.value = ''
28
+ }
29
+ })
30
+ }
31
+
32
+ export function initAllAutocomplete() {
33
+ // Find all autocompletes
34
+ const $autocompletes = document.querySelectorAll(
35
+ `[data-module="govuk-accessible-autocomplete"]`
36
+ )
37
+
38
+ // Lazy load autocomplete component
39
+ if ($autocompletes.length) {
40
+ // @ts-expect-error -- No types available
41
+ import('accessible-autocomplete')
42
+ .then((component) => {
43
+ const { default: accessibleAutocomplete } = component
44
+
45
+ // Initialise each autocomplete
46
+ $autocompletes.forEach(($module) =>
47
+ initAutocomplete(
48
+ $module.querySelector('select'),
49
+ accessibleAutocomplete.enhanceSelectElement
50
+ )
51
+ )
52
+ })
53
+
54
+ // eslint-disable-next-line no-console
55
+ .catch(console.error)
56
+ }
57
+ }
@@ -369,7 +369,7 @@ function handleAjaxFormSubmission(
369
369
  return true
370
370
  }
371
371
 
372
- export function initFileUpload() {
372
+ function initUpload() {
373
373
  const form = document.querySelector('form:has(input[type="file"])')
374
374
  /** @type {HTMLInputElement | null} */
375
375
  const fileInput = form ? form.querySelector('input[type="file"]') : null
@@ -440,3 +440,11 @@ export function initFileUpload() {
440
440
  )
441
441
  })
442
442
  }
443
+
444
+ export function initFileUpload() {
445
+ if (document.readyState === 'loading') {
446
+ document.addEventListener('DOMContentLoaded', initUpload)
447
+ } else {
448
+ initUpload()
449
+ }
450
+ }
@@ -0,0 +1,22 @@
1
+ import {
2
+ Button,
3
+ CharacterCount,
4
+ Checkboxes,
5
+ ErrorSummary,
6
+ Header,
7
+ NotificationBanner,
8
+ Radios,
9
+ SkipLink,
10
+ createAll
11
+ } from 'govuk-frontend'
12
+
13
+ export function initAllGovuk() {
14
+ createAll(Button)
15
+ createAll(CharacterCount)
16
+ createAll(Checkboxes)
17
+ createAll(ErrorSummary)
18
+ createAll(Header)
19
+ createAll(NotificationBanner)
20
+ createAll(Radios)
21
+ createAll(SkipLink)
22
+ }
@@ -0,0 +1,12 @@
1
+ export function initPreviewCloseLink() {
2
+ // Show preview close link via `rel="opener"`
3
+ if (window.opener) {
4
+ const $closeLink = document.querySelector('.js-preview-banner-close')
5
+
6
+ $closeLink?.removeAttribute('hidden')
7
+ $closeLink?.addEventListener('click', (event) => {
8
+ event.preventDefault()
9
+ window.close()
10
+ })
11
+ }
12
+ }
@@ -0,0 +1,16 @@
1
+ import { initAllAutocomplete as initAllAutocompleteImp } from '~/src/client/javascripts/autocomplete.js'
2
+ import { initFileUpload as initFileUploadImp } from '~/src/client/javascripts/file-upload.js'
3
+ import { initAllGovuk as initAllGovukImp } from '~/src/client/javascripts/govuk.js'
4
+ import { initPreviewCloseLink as initPreviewCloseLinkImp } from '~/src/client/javascripts/preview-close-link.js'
5
+
6
+ export const initAllGovuk = initAllGovukImp
7
+ export const initAllAutocomplete = initAllAutocompleteImp
8
+ export const initFileUpload = initFileUploadImp
9
+ export const initPreviewCloseLink = initPreviewCloseLinkImp
10
+
11
+ export function initAll() {
12
+ initAllGovuk()
13
+ initAllAutocomplete()
14
+ initFileUpload()
15
+ initPreviewCloseLink()
16
+ }
@@ -21,4 +21,13 @@
21
21
  margin: 0 0 0 govuk-spacing(2);
22
22
  }
23
23
  }
24
+
25
+ &-rebrand {
26
+ vertical-align: middle;
27
+
28
+ // Align with product name
29
+ @include govuk-media-query($from: tablet) {
30
+ margin: -3px 0 2px;
31
+ }
32
+ }
24
33
  }
@@ -78,6 +78,16 @@ export const config = convict({
78
78
  default: isTest
79
79
  },
80
80
 
81
+ /**
82
+ * Feature flags
83
+ */
84
+ showRebrand: {
85
+ doc: 'If this app should show the 2025 rebrand',
86
+ format: Boolean,
87
+ env: 'SHOW_GOVUK_REBRAND',
88
+ default: false
89
+ },
90
+
81
91
  /**
82
92
  * Service
83
93
  */
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { getErrorMessage } from '@defra/forms-model'
2
+
1
3
  import { config } from '~/src/config/index.js'
2
4
  import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
3
5
  import { createServer } from '~/src/server/index.js'
@@ -5,8 +7,9 @@ import { createServer } from '~/src/server/index.js'
5
7
  const logger = createLogger()
6
8
 
7
9
  process.on('unhandledRejection', (error) => {
10
+ const err = getErrorMessage(error)
8
11
  logger.info('Unhandled rejection')
9
- logger.error(error)
12
+ logger.error(err, `[unhandledRejection] Unhandled promise rejection: ${err}`)
10
13
  throw error
11
14
  })
12
15
 
@@ -26,6 +29,8 @@ async function startServer() {
26
29
  }
27
30
 
28
31
  startServer().catch((error: unknown) => {
32
+ const err = getErrorMessage(error)
29
33
  logger.info('Server failed to start :(')
30
- logger.error(error)
34
+ logger.error(err, `[serverStartup] Server failed to start: ${err}`)
35
+ throw error
31
36
  })
@@ -6,5 +6,5 @@ import { config } from '~/src/config/index.js'
6
6
 
7
7
  export const requestTracing = {
8
8
  plugin: tracing,
9
- options: { tracingHeader: config.get('tracing').header }
9
+ options: { tracingHeader: config.get('tracing.header') }
10
10
  }
@@ -1,3 +1,4 @@
1
+ import { getErrorMessage } from '@defra/forms-model'
1
2
  import { Cluster, Redis } from 'ioredis'
2
3
 
3
4
  import { config } from '~/src/config/index.js'
@@ -53,17 +54,18 @@ export function buildRedisClient() {
53
54
  }
54
55
 
55
56
  redisClient.on('connect', () => {
56
- logger.info('Connected to Redis server')
57
+ logger.info('[redisConnected] Connected to Redis server')
57
58
  })
58
59
 
59
60
  redisClient.on('close', () => {
60
61
  logger.warn(
61
- 'Redis connection closed attempting reconnect with default behavior'
62
+ '[redisDisconnected] Redis connection closed attempting reconnect with default behavior'
62
63
  )
63
64
  })
64
65
 
65
66
  redisClient.on('error', (error) => {
66
- logger.error(error, `Redis connection error ${error}.`)
67
+ const err = getErrorMessage(error)
68
+ logger.error(err, `[redisConnectionError] Redis connection error - ${err}`)
67
69
  })
68
70
 
69
71
  return redisClient
@@ -1,3 +1,2 @@
1
1
  export const PREVIEW_PATH_PREFIX = '/preview'
2
- export const ERROR_PREVIEW_PATH_PREFIX = '/error-preview'
3
2
  export const FORM_PREFIX = ''
@@ -89,7 +89,6 @@ export async function createServer(routeConfig?: RouteConfig) {
89
89
  await server.register(inert)
90
90
  await server.register(Scooter)
91
91
  await server.register(pluginCrumb)
92
-
93
92
  await server.register(pluginEngine)
94
93
 
95
94
  server.ext('onPreResponse', (request: Request, h: ResponseToolkit) => {
@@ -126,7 +125,11 @@ export async function createServer(routeConfig?: RouteConfig) {
126
125
  })
127
126
 
128
127
  await server.register(pluginErrorPages)
129
- await server.register(blipp)
128
+
129
+ if (config.get('cdpEnvironment') === 'local') {
130
+ await server.register(blipp)
131
+ }
132
+
130
133
  await server.register(requestTracing)
131
134
 
132
135
  return server
@@ -242,9 +242,26 @@ describe('DatePartsField', () => {
242
242
  })
243
243
  )
244
244
 
245
+ // Check a non-4-digit year shows as an error
246
+ const state4 = getFormState({
247
+ day: 1,
248
+ month: 2,
249
+ year: 20
250
+ })
251
+ const result4 = field.getContextValueFromState(state4)
252
+
253
+ const state5 = getFormState({
254
+ day: 1,
255
+ month: 2,
256
+ year: 2000
257
+ })
258
+ const result5 = field.getContextValueFromState(state5)
259
+
245
260
  expect(result1.errors).toBeTruthy()
246
261
  expect(result2.errors).toBeTruthy()
247
262
  expect(result3.errors).toBeTruthy()
263
+ expect(result4).toBeNull()
264
+ expect(result5).toBe('2000-02-01')
248
265
  })
249
266
  })
250
267
 
@@ -9,7 +9,11 @@ import {
9
9
  isFormValue
10
10
  } from '~/src/server/plugins/engine/components/FormComponent.js'
11
11
  import { NumberField } from '~/src/server/plugins/engine/components/NumberField.js'
12
- import { type DateInputItem } from '~/src/server/plugins/engine/components/types.js'
12
+ import {
13
+ type DateInputItem,
14
+ type DatePartsState
15
+ } from '~/src/server/plugins/engine/components/types.js'
16
+ import { parseStrictDate } from '~/src/server/plugins/engine/date-helper.js'
13
17
  import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
14
18
  import {
15
19
  type ErrorMessageTemplateList,
@@ -121,7 +125,8 @@ export class DatePartsField extends FormComponent {
121
125
  if (
122
126
  !value ||
123
127
  !isValid(
124
- parse(
128
+ parseStrictDate(
129
+ value,
125
130
  `${value.year}-${value.month}-${value.day}`,
126
131
  'yyyy-MM-dd',
127
132
  new Date()
@@ -232,12 +237,6 @@ export class DatePartsField extends FormComponent {
232
237
  }
233
238
  }
234
239
 
235
- export interface DatePartsState extends Record<string, number> {
236
- day: number
237
- month: number
238
- year: number
239
- }
240
-
241
240
  export function getValidatorDate(component: DatePartsField) {
242
241
  const validator: CustomValidator = (payload: FormPayload, helpers) => {
243
242
  const { collection, name, options } = component
@@ -222,9 +222,24 @@ describe('MonthYearField', () => {
222
222
  })
223
223
  )
224
224
 
225
+ // Check a non-4-digit year shows as an error
226
+ const state4 = getFormState({
227
+ month: 2,
228
+ year: 20
229
+ })
230
+ const result4 = field.getContextValueFromState(state4)
231
+
232
+ const state5 = getFormState({
233
+ month: 5,
234
+ year: 2000
235
+ })
236
+ const result5 = field.getContextValueFromState(state5)
237
+
225
238
  expect(result1.errors).toBeTruthy()
226
239
  expect(result2.errors).toBeTruthy()
227
240
  expect(result3.errors).toBeTruthy()
241
+ expect(result4).toBeNull()
242
+ expect(result5).toBe('2000-05')
228
243
  })
229
244
  })
230
245
 
@@ -1,5 +1,5 @@
1
1
  import { ComponentType, type MonthYearFieldComponent } from '@defra/forms-model'
2
- import { format, isValid, parse } from 'date-fns'
2
+ import { format, isValid } from 'date-fns'
3
3
  import {
4
4
  type Context,
5
5
  type CustomValidator,
@@ -14,7 +14,11 @@ import {
14
14
  isFormValue
15
15
  } from '~/src/server/plugins/engine/components/FormComponent.js'
16
16
  import { NumberField } from '~/src/server/plugins/engine/components/NumberField.js'
17
- import { type DateInputItem } from '~/src/server/plugins/engine/components/types.js'
17
+ import {
18
+ type DateInputItem,
19
+ type MonthYearState
20
+ } from '~/src/server/plugins/engine/components/types.js'
21
+ import { parseStrictDate } from '~/src/server/plugins/engine/date-helper.js'
18
22
  import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
19
23
  import {
20
24
  type ErrorMessageTemplateList,
@@ -119,7 +123,12 @@ export class MonthYearField extends FormComponent {
119
123
  if (
120
124
  !value ||
121
125
  !isValid(
122
- parse(`${value.year}-${value.month}-01`, 'yyyy-MM-dd', new Date())
126
+ parseStrictDate(
127
+ value,
128
+ `${value.year}-${value.month}-01`,
129
+ 'yyyy-MM-dd',
130
+ new Date()
131
+ )
123
132
  )
124
133
  ) {
125
134
  return null
@@ -223,11 +232,6 @@ export class MonthYearField extends FormComponent {
223
232
  }
224
233
  }
225
234
 
226
- export interface MonthYearState extends Record<string, number> {
227
- month: number
228
- year: number
229
- }
230
-
231
235
  export function getValidatorMonthYear(component: MonthYearField) {
232
236
  const validator: CustomValidator = (payload: FormPayload, helpers) => {
233
237
  const { collection, name, options } = component
@@ -1,4 +1,9 @@
1
- import { type YesNoFieldComponent } from '@defra/forms-model'
1
+ import {
2
+ SchemaVersion,
3
+ yesNoListId,
4
+ yesNoListName,
5
+ type YesNoFieldComponent
6
+ } from '@defra/forms-model'
2
7
 
3
8
  import { SelectionControlField } from '~/src/server/plugins/engine/components/SelectionControlField.js'
4
9
  import { addClassOptionIfNone } from '~/src/server/plugins/engine/components/helpers.js'
@@ -17,7 +22,16 @@ export class YesNoField extends SelectionControlField {
17
22
  def: YesNoFieldComponent,
18
23
  props: ConstructorParameters<typeof SelectionControlField>[1]
19
24
  ) {
20
- super({ ...def, list: '__yesNo' }, props)
25
+ super(
26
+ {
27
+ ...def,
28
+ list:
29
+ props.model.schemaVersion === SchemaVersion.V1
30
+ ? yesNoListName
31
+ : yesNoListId
32
+ },
33
+ props
34
+ )
21
35
 
22
36
  const { options } = def
23
37
  let { formSchema } = this
@@ -115,3 +115,14 @@ export interface ComponentViewModel {
115
115
  isFormComponent: boolean
116
116
  model: ViewModel
117
117
  }
118
+
119
+ export interface DatePartsState extends Record<string, number> {
120
+ day: number
121
+ month: number
122
+ year: number
123
+ }
124
+
125
+ export interface MonthYearState extends Record<string, number> {
126
+ month: number
127
+ year: number
128
+ }
@@ -0,0 +1,47 @@
1
+ import { startOfToday } from 'date-fns'
2
+
3
+ import {
4
+ parseStrictDate,
5
+ todayAsDateOnly
6
+ } from '~/src/server/plugins/engine/date-helper.js'
7
+
8
+ describe('todayAsDateOnly()', () => {
9
+ test('should return today with no time element', () => {
10
+ expect(todayAsDateOnly()).toEqual(startOfToday())
11
+ })
12
+ })
13
+
14
+ describe('parseStrictDate()', () => {
15
+ test('should parse valid date', () => {
16
+ const dateObj = {
17
+ year: 2025,
18
+ month: 5,
19
+ day: 21
20
+ }
21
+ expect(
22
+ parseStrictDate(dateObj, '2025-05-21', 'yyyy-MM-dd', new Date())
23
+ ).toEqual(new Date(2025, 4, 21))
24
+ })
25
+
26
+ test('should fail to parse invalid date with 4-digit year', () => {
27
+ const dateObj = {
28
+ year: 2025,
29
+ month: 15,
30
+ day: 21
31
+ }
32
+ expect(
33
+ parseStrictDate(dateObj, '2025-15-21', 'yyyy-MM-dd', new Date())
34
+ ).toBeNaN()
35
+ })
36
+
37
+ test('should fail to parse valid date that has non-4-digit year', () => {
38
+ const dateObj = {
39
+ year: 25,
40
+ month: 5,
41
+ day: 21
42
+ }
43
+ expect(
44
+ parseStrictDate(dateObj, '25-15-21', 'yyyy-MM-dd', new Date())
45
+ ).toBeNaN()
46
+ })
47
+ })
@@ -0,0 +1,32 @@
1
+ import { parse, startOfToday } from 'date-fns'
2
+
3
+ import {
4
+ type DatePartsState,
5
+ type MonthYearState
6
+ } from '~/src/server/plugins/engine/components/types.js'
7
+
8
+ /**
9
+ * This function is just a wrapper for startOfToday() but, since it's in a separate file, allows
10
+ * the function to be easily mocked for unit testing.
11
+ * @returns {Date}
12
+ */
13
+ export function todayAsDateOnly() {
14
+ return startOfToday()
15
+ }
16
+
17
+ /**
18
+ * Wrapper for date-fns parse() method to ensure the year is 4-digits. It seems parse() allows a non-4-digit year
19
+ * despite the format mask enforcing it.
20
+ */
21
+ export function parseStrictDate(
22
+ dateObj: DatePartsState | MonthYearState,
23
+ dateStr: string,
24
+ formatStr: string,
25
+ referenceDate: Date
26
+ ) {
27
+ if (!dateObj.year || dateObj.year < 1000 || dateObj.year > 9999) {
28
+ return NaN
29
+ }
30
+
31
+ return parse(dateStr, formatStr, referenceDate)
32
+ }
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  ControllerPath,
3
3
  Engine,
4
+ getErrorMessage,
4
5
  hasComponents,
5
6
  isFormType,
6
7
  type ComponentDef,
@@ -148,7 +149,11 @@ export function encodeUrl(link?: string) {
148
149
  try {
149
150
  return new URL(link).toString() // escape the search params without breaking the ? and & reserved characters in rfc2368
150
151
  } catch (err) {
151
- logger.error(err, `Failed to encode ${link}`)
152
+ const errMsg = getErrorMessage(err)
153
+ logger.error(
154
+ errMsg,
155
+ `[urlEncodingFailed] Failed to encode URL: ${link} - ${errMsg}`
156
+ )
152
157
  throw err
153
158
  }
154
159
  }
@@ -404,7 +409,9 @@ export function setPageTitles(def: FormDefinition) {
404
409
  if (!page.title) {
405
410
  const formNameMsg = def.name ? ` in form '${def.name}'` : ''
406
411
 
407
- logger.warn(`Page '${page.path}' has no title${formNameMsg}`)
412
+ logger.info(
413
+ `[pageTitleMissing] Page '${page.path}' has no title${formNameMsg}`
414
+ )
408
415
  }
409
416
  }
410
417
  })
@@ -11,7 +11,6 @@ import {
11
11
  import * as filters from '~/src/server/plugins/nunjucks/filters/index.js'
12
12
 
13
13
  export { getPageHref } from '~/src/server/plugins/engine/helpers.js'
14
- export { configureEnginePlugin } from '~/src/server/plugins/engine/configureEnginePlugin.js'
15
14
  export { context } from '~/src/server/plugins/nunjucks/context.js'
16
15
 
17
16
  const globals = {