@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
@@ -1,5 +1,6 @@
1
1
  import { type ResponseToolkit } from '@hapi/hapi'
2
2
 
3
+ import { FORM_PREFIX } from '~/src/server/constants.js'
3
4
  import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
4
5
  import { PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
5
6
  import { type FormRequest } from '~/src/server/routes/types.js'
@@ -10,6 +11,8 @@ describe('PageController', () => {
10
11
  let controller1: PageController
11
12
  let controller2: PageController
12
13
 
14
+ const testBasePath = `${FORM_PREFIX}/test`
15
+
13
16
  beforeEach(() => {
14
17
  const { pages } = definition
15
18
 
@@ -17,7 +20,7 @@ describe('PageController', () => {
17
20
  const page2 = pages[1]
18
21
 
19
22
  model = new FormModel(definition, {
20
- basePath: 'test'
23
+ basePath: testBasePath
21
24
  })
22
25
 
23
26
  controller1 = new PageController(model, page1)
@@ -31,8 +34,8 @@ describe('PageController', () => {
31
34
  })
32
35
 
33
36
  it('returns href', () => {
34
- expect(controller1).toHaveProperty('href', '/test/licence')
35
- expect(controller2).toHaveProperty('href', '/test/full-name')
37
+ expect(controller1).toHaveProperty('href', `${testBasePath}/licence`)
38
+ expect(controller2).toHaveProperty('href', `${testBasePath}/full-name`)
36
39
  })
37
40
 
38
41
  it('returns keys (empty)', () => {
@@ -99,11 +102,11 @@ describe('PageController', () => {
99
102
  describe('Path methods', () => {
100
103
  describe('Link href', () => {
101
104
  it('prefixes paths into link hrefs', () => {
102
- const href1 = controller1.getHref('/')
105
+ const href1 = controller1.getHref('')
103
106
  const href2 = controller1.getHref('/page-one')
104
107
 
105
- expect(href1).toBe('/test')
106
- expect(href2).toBe('/test/page-one')
108
+ expect(href1).toBe(testBasePath)
109
+ expect(href2).toBe(`${testBasePath}/page-one`)
107
110
  })
108
111
  })
109
112
 
@@ -135,12 +135,22 @@ export class PageController {
135
135
  return def.phaseBanner?.phase
136
136
  }
137
137
 
138
- getHref(path: string) {
139
- const { model } = this
138
+ getHref(path: string): string {
139
+ const basePath = this.model.basePath
140
140
 
141
- return path === '/'
142
- ? `/${model.basePath}` // Strip trailing slash
143
- : `/${model.basePath}${path}`
141
+ if (path === '/') {
142
+ return `/${basePath}`
143
+ }
144
+
145
+ // if ever the path is not prefixed with a slash, add it
146
+ const relativeTargetPath = path.startsWith('/') ? path.substring(1) : path
147
+ let finalPath = `/${basePath}`
148
+ if (relativeTargetPath) {
149
+ finalPath += `/${relativeTargetPath}`
150
+ }
151
+ finalPath = finalPath.replace(/\/{2,}/g, '/')
152
+
153
+ return finalPath
144
154
  }
145
155
 
146
156
  getStartPath() {
@@ -398,7 +398,7 @@ export class QuestionPageController extends PageController {
398
398
  const { evaluationState } = context
399
399
 
400
400
  const viewModel = this.getViewModel(request, context)
401
- viewModel.errors = collection.getErrors(viewModel.errors)
401
+ viewModel.errors = collection.getViewErrors(viewModel.errors)
402
402
 
403
403
  /**
404
404
  * Content components can be hidden based on a condition. If the condition evaluates to true, it is safe to be kept, otherwise discard it
@@ -498,7 +498,7 @@ export class QuestionPageController extends PageController {
498
498
  */
499
499
  if (context.errors || isForceAccess) {
500
500
  const viewModel = this.getViewModel(request, context)
501
- viewModel.errors = collection.getErrors(viewModel.errors)
501
+ viewModel.errors = collection.getViewErrors(viewModel.errors)
502
502
 
503
503
  // Filter our components based on their conditions using our evaluated state
504
504
  viewModel.components = this.filterConditionalComponents(
@@ -1,7 +1,12 @@
1
- import { type PageSummary, type SubmitPayload } from '@defra/forms-model'
1
+ import {
2
+ hasComponentsEvenIfNoNext,
3
+ type Page,
4
+ type SubmitPayload
5
+ } from '@defra/forms-model'
2
6
  import Boom from '@hapi/boom'
3
7
  import { type ResponseToolkit, type RouteOptions } from '@hapi/hapi'
4
8
 
9
+ import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
5
10
  import { FileUploadField } from '~/src/server/plugins/engine/components/FileUploadField.js'
6
11
  import { getAnswer } from '~/src/server/plugins/engine/components/helpers.js'
7
12
  import {
@@ -30,15 +35,21 @@ import {
30
35
  } from '~/src/server/routes/types.js'
31
36
 
32
37
  export class SummaryPageController extends QuestionPageController {
33
- declare pageDef: PageSummary
38
+ declare pageDef: Page
34
39
 
35
40
  /**
36
41
  * The controller which is used when Page["controller"] is defined as "./pages/summary.js"
37
42
  */
38
43
 
39
- constructor(model: FormModel, pageDef: PageSummary) {
44
+ constructor(model: FormModel, pageDef: Page) {
40
45
  super(model, pageDef)
41
46
  this.viewName = 'summary'
47
+
48
+ // Components collection
49
+ this.collection = new ComponentCollection(
50
+ hasComponentsEvenIfNoNext(pageDef) ? pageDef.components : [],
51
+ { model, page: this }
52
+ )
42
53
  }
43
54
 
44
55
  getSummaryViewModel(
@@ -47,11 +58,16 @@ export class SummaryPageController extends QuestionPageController {
47
58
  ): SummaryViewModel {
48
59
  const viewModel = new SummaryViewModel(request, this, context)
49
60
 
61
+ const { query } = request
62
+ const { payload, errors } = context
63
+ const components = this.collection.getViewModel(payload, errors, query)
64
+
50
65
  // We already figure these out in the base page controller. Take them and apply them to our page-specific model.
51
66
  // This is a stop-gap until we can add proper inheritance in place.
52
67
  viewModel.backLink = this.getBackLink(request, context)
53
68
  viewModel.feedbackLink = this.feedbackLink
54
69
  viewModel.phaseTag = this.phaseTag
70
+ viewModel.components = components
55
71
 
56
72
  return viewModel
57
73
  }
@@ -96,7 +112,7 @@ export class SummaryPageController extends QuestionPageController {
96
112
 
97
113
  // Get the form metadata using the `slug` param
98
114
  const { notificationEmail } = await getFormMetadata(params.slug)
99
- const { isPreview } = checkFormStatus(request.path)
115
+ const { isPreview } = checkFormStatus(request.params)
100
116
  const emailAddress = notificationEmail ?? this.model.def.outputEmail
101
117
 
102
118
  checkEmailAddressForLiveFormSubmission(emailAddress, isPreview)
@@ -138,8 +154,7 @@ async function submitForm(
138
154
  ) {
139
155
  await extendFileRetention(model, state, emailAddress)
140
156
 
141
- const { path } = request
142
- const formStatus = checkFormStatus(path)
157
+ const formStatus = checkFormStatus(request.params)
143
158
  const logTags = ['submit', 'submissionApi']
144
159
 
145
160
  request.logger.info(logTags, 'Preparing email', formStatus)
@@ -1,32 +1,45 @@
1
1
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
2
  // Declaration above is needed for: https://github.com/hapijs/joi/issues/3064
3
3
 
4
- import joi, { type LanguageMessages, type ValidationOptions } from 'joi'
4
+ import joi, {
5
+ type JoiExpression,
6
+ type LanguageMessages,
7
+ type LanguageMessagesExt,
8
+ type ReferenceOptions,
9
+ type ValidationOptions
10
+ } from 'joi'
5
11
  import lowerFirst from 'lodash/lowerFirst.js'
6
12
 
7
13
  const opts = {
8
14
  functions: {
9
15
  lowerFirst
10
16
  }
11
- }
17
+ } as ReferenceOptions
12
18
 
13
19
  /**
14
20
  * see @link https://joi.dev/api/?v=17.4.2#template-syntax for template syntax
15
21
  */
16
- export const messageTemplate = {
17
- // @ts-expect-error - joi.expression options type issue
18
- required: joi.expression('Enter {{lowerFirst(#label)}}', opts),
19
- // @ts-expect-error - joi.expression options type issue
20
- selectRequired: joi.expression('Select {{lowerFirst(#label)}}', opts),
22
+ export const messageTemplate: Record<string, JoiExpression> = {
23
+ required: joi.expression(
24
+ 'Enter {{lowerFirst(#label)}}',
25
+ opts
26
+ ) as JoiExpression,
27
+ selectRequired: joi.expression(
28
+ 'Select {{lowerFirst(#label)}}',
29
+ opts
30
+ ) as JoiExpression,
31
+ selectYesNoRequired: '{{#label}} - select yes or no',
21
32
  max: '{{#label}} must be {{#limit}} characters or less',
22
33
  min: '{{#label}} must be {{#limit}} characters or more',
23
- // @ts-expect-error - joi.expression options type issue
24
- pattern: joi.expression('Enter a valid {{lowerFirst(#label)}}', opts),
34
+ minMax: '{{#label}} must be between {{#min}} and {{#max}} characters',
35
+ pattern: joi.expression(
36
+ 'Enter a valid {{lowerFirst(#label)}}',
37
+ opts
38
+ ) as JoiExpression,
25
39
  format: joi.expression(
26
40
  'Enter {{lowerFirst(#label)}} in the correct format',
27
- // @ts-expect-error - joi.expression options type issue
28
41
  opts
29
- ),
42
+ ) as JoiExpression,
30
43
  number: '{{#label}} must be a number',
31
44
  numberPrecision: '{{#label}} must have {{#limit}} or fewer decimal places',
32
45
  numberInteger: '{{#label}} must be a whole number',
@@ -36,19 +49,17 @@ export const messageTemplate = {
36
49
 
37
50
  // Nested fields use component title
38
51
 
39
- // @ts-expect-error - joi.expression options type issue
40
- objectRequired: joi.expression('Enter {{#label}}', opts),
52
+ objectRequired: joi.expression('Enter {{#label}}', opts) as JoiExpression,
41
53
  objectMissing: joi.expression(
42
54
  '{{#title}} must include a {{lowerFirst(#label)}}',
43
- // @ts-expect-error - joi.expression options type issue
44
55
  opts
45
- ),
56
+ ) as JoiExpression,
46
57
  dateFormat: '{{#title}} must be a real date',
47
58
  dateMin: '{{#title}} must be the same as or after {{#limit}}',
48
59
  dateMax: '{{#title}} must be the same as or before {{#limit}}'
49
60
  }
50
61
 
51
- export const messages: LanguageMessages = {
62
+ export const messages: LanguageMessagesExt = {
52
63
  'string.base': messageTemplate.required,
53
64
  'string.min': messageTemplate.min,
54
65
  'string.empty': messageTemplate.required,
@@ -77,9 +88,12 @@ export const messages: LanguageMessages = {
77
88
  'date.max': messageTemplate.dateMax
78
89
  }
79
90
 
91
+ export const messagesPre: LanguageMessages =
92
+ messages as unknown as LanguageMessages
93
+
80
94
  export const validationOptions: ValidationOptions = {
81
95
  abortEarly: false,
82
- messages,
96
+ messages: messagesPre,
83
97
  errors: {
84
98
  wrap: {
85
99
  array: false,
@@ -105,6 +105,8 @@ export const plugin = {
105
105
  dependencies: ['@hapi/crumb', '@hapi/yar', 'hapi-pino'],
106
106
  multiple: true,
107
107
  async register(server: Server, options: PluginOptions) {
108
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- hapi types are wrong
109
+ const prefix = server.realm.modifiers.route.prefix ?? ''
108
110
  const {
109
111
  model,
110
112
  services = defaultServices,
@@ -193,9 +195,9 @@ export const plugin = {
193
195
  return h.continue
194
196
  }
195
197
 
196
- const { params, path } = request
198
+ const { params } = request
197
199
  const { slug } = params
198
- const { isPreview, state: formState } = checkFormStatus(path)
200
+ const { isPreview, state: formState } = checkFormStatus(params)
199
201
 
200
202
  // Get the form metadata using the `slug` param
201
203
  const metadata = await formsService.getFormMetadata(slug)
@@ -241,9 +243,11 @@ export const plugin = {
241
243
  )
242
244
 
243
245
  // Set up the basePath for the model
244
- const basePath = isPreview
245
- ? `${PREVIEW_PATH_PREFIX.substring(1)}/${formState}/${slug}`
246
- : slug
246
+ const basePath = (
247
+ isPreview
248
+ ? `${prefix}${PREVIEW_PATH_PREFIX}/${formState}/${slug}`
249
+ : `${prefix}/${slug}`
250
+ ).substring(1)
247
251
 
248
252
  // Construct the form model
249
253
  const model = new FormModel(
@@ -19,8 +19,7 @@ export async function submit(
19
19
  submitResponse: SubmitResponsePayload
20
20
  ) {
21
21
  const logTags = ['submit', 'email']
22
- const { path } = request
23
- const formStatus = checkFormStatus(path)
22
+ const formStatus = checkFormStatus(request.params)
24
23
 
25
24
  // Get submission email personalisation
26
25
  request.logger.info(logTags, 'Getting personalisation data')
@@ -10,12 +10,19 @@ const stagingPrefix = config.get('stagingPrefix')
10
10
  * Initiates a CDP file upload
11
11
  * @param {string} path - the path of the page in the form
12
12
  * @param {string} retrievalKey - the retrieval key for the files
13
- * @param {string} [mimeTypes] - the csv string of accepted mimeTypes
13
+ * @param {string} [mimeTypesCsv] - the csv string of accepted mimeTypes
14
14
  */
15
- export async function initiateUpload(path, retrievalKey, mimeTypes) {
15
+ export async function initiateUpload(path, retrievalKey, mimeTypesCsv) {
16
16
  const postJsonByType =
17
17
  /** @type {typeof postJson<UploadInitiateResponse>} */ (postJson)
18
18
 
19
+ const mimeTypesList = mimeTypesCsv
20
+ ?.split(',')
21
+ .map((type) => type.trim())
22
+ .filter((type) => type !== '')
23
+
24
+ const mimeTypes = mimeTypesList?.length ? mimeTypesList : undefined
25
+
19
26
  const payload = {
20
27
  redirect: path,
21
28
  callback: `${submissionUrl}/file`,
@@ -24,10 +31,7 @@ export async function initiateUpload(path, retrievalKey, mimeTypes) {
24
31
  metadata: {
25
32
  retrievalKey
26
33
  },
27
- mimeTypes: mimeTypes
28
- ?.split(',')
29
- .map((type) => type.trim())
30
- .filter((type) => type !== '')
34
+ mimeTypes
31
35
  // maxFileSize: 25 * 1000 * 1000
32
36
  }
33
37
 
@@ -4,7 +4,7 @@ import {
4
4
  type List,
5
5
  type Page
6
6
  } from '@defra/forms-model'
7
- import { type ValidationErrorItem } from 'joi'
7
+ import { type JoiExpression, type ValidationErrorItem } from 'joi'
8
8
 
9
9
  import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
10
10
  import { type Component } from '~/src/server/plugins/engine/components/helpers.js'
@@ -316,3 +316,12 @@ export type PageViewModel =
316
316
  | FeaturedFormPageViewModel
317
317
 
318
318
  export type FilterFunction = (value: unknown) => unknown
319
+ export interface ErrorMessageTemplate {
320
+ type: string
321
+ template: JoiExpression
322
+ }
323
+
324
+ export interface ErrorMessageTemplateList {
325
+ baseErrors: ErrorMessageTemplate[]
326
+ advancedSettingsErrors: ErrorMessageTemplate[]
327
+ }
@@ -1,3 +1,3 @@
1
1
  {% macro Html(component) %}
2
- {{ component.model.content | safe }}
2
+ {{ component.model.content | safe }}
3
3
  {% endmacro %}
@@ -0,0 +1,5 @@
1
+ {% macro Markdown(component) %}
2
+ <div class="app-prose-scope">
3
+ {{ component.model.content | markdown | safe }}
4
+ </div>
5
+ {% endmacro %}
@@ -2,6 +2,7 @@
2
2
 
3
3
  {% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
4
4
  {% from "govuk/components/summary-list/macro.njk" import govukSummaryList %}
5
+ {% from "partials/components.html" import componentList with context %}
5
6
 
6
7
  {% block content %}
7
8
  <div class="govuk-grid-row">
@@ -34,11 +35,16 @@
34
35
 
35
36
  {% if declaration %}
36
37
  <h2 class="govuk-heading-m" id="declaration">Declaration</h2>
38
+ <div class="govuk-body">
37
39
  {{ declaration | safe }}
40
+ </div>
38
41
  {% endif %}
39
42
 
43
+ {{ componentList(components) }}
44
+
45
+ {% set isDeclaration = declaration or components | length %}
40
46
  <button data-prevent-double-click="true" class="govuk-button" data-module="govuk-button">
41
- {{ "Accept and send" if declaration else "Send" }}
47
+ {{ "Accept and send" if isDeclaration else "Send" }}
42
48
  </button>
43
49
  </form>
44
50
  </div>
@@ -7,8 +7,8 @@ import { StatusCodes } from 'http-status-codes'
7
7
  import pkg from '~/package.json' with { type: 'json' }
8
8
  import { config } from '~/src/config/index.js'
9
9
  import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
10
- import { PREVIEW_PATH_PREFIX } from '~/src/server/constants.js'
11
10
  import {
11
+ checkFormStatus,
12
12
  encodeUrl,
13
13
  safeGenerateCrumb
14
14
  } from '~/src/server/plugins/engine/helpers.js'
@@ -22,9 +22,9 @@ let webpackManifest
22
22
  * @param {FormRequest | FormRequestPayload | null} request
23
23
  */
24
24
  export function context(request) {
25
- const { params, path, response } = request ?? {}
25
+ const { params, response } = request ?? {}
26
26
 
27
- const isPreviewMode = path?.startsWith(PREVIEW_PATH_PREFIX)
27
+ const { isPreview: isPreviewMode, state: formState } = checkFormStatus(params)
28
28
 
29
29
  // Only add the slug in to the context if the response is OK.
30
30
  // Footer meta links are not rendered when the slug is missing.
@@ -62,7 +62,7 @@ export function context(request) {
62
62
  },
63
63
  crumb: safeGenerateCrumb(request),
64
64
  currentPath: `${request.path}${request.url.search}`,
65
- previewMode: isPreviewMode ? params?.state : undefined,
65
+ previewMode: isPreviewMode ? formState : undefined,
66
66
  slug: isResponseOK ? params?.slug : undefined
67
67
  }
68
68
 
@@ -88,7 +88,9 @@ describe('Nunjucks environment', () => {
88
88
  }
89
89
  }
90
90
 
91
- const result = checkComponentTemplates.call(nunjucksCtx, component)
91
+ const result = /** @type {{ model: { content: string } }} */ (
92
+ checkComponentTemplates.call(nunjucksCtx, component)
93
+ )
92
94
 
93
95
  expect(helpers.evaluateTemplate).toHaveBeenCalledWith(
94
96
  'Some {{ context.someData }} content',
@@ -114,7 +116,9 @@ describe('Nunjucks environment', () => {
114
116
  }
115
117
  }
116
118
 
117
- const result = checkComponentTemplates.call(nunjucksCtx, component)
119
+ const result = /** @type {{ model: { content: string } }} */ (
120
+ checkComponentTemplates.call(nunjucksCtx, component)
121
+ )
118
122
 
119
123
  expect(helpers.evaluateTemplate).not.toHaveBeenCalled()
120
124
 
@@ -136,7 +140,9 @@ describe('Nunjucks environment', () => {
136
140
  }
137
141
  }
138
142
 
139
- const result = checkComponentTemplates.call(nunjucksCtx, component)
143
+ const result = /** @type {{ model: { label?: { text: string } } }} */ (
144
+ checkComponentTemplates.call(nunjucksCtx, component)
145
+ )
140
146
 
141
147
  expect(helpers.evaluateTemplate).toHaveBeenCalledWith(
142
148
  'Label with {{ context.someData }}',
@@ -0,0 +1,15 @@
1
+ import Joi, {
2
+ type JoiExpression,
3
+ type LanguageMessages,
4
+ type LanguageMessagesExt
5
+ } from 'joi'
6
+
7
+ export function convertToLanguageMessages(
8
+ extLanguageMessages: LanguageMessagesExt
9
+ ): LanguageMessages {
10
+ return extLanguageMessages as unknown as LanguageMessages
11
+ }
12
+
13
+ export function createJoiExpression(expr: string): JoiExpression {
14
+ return Joi.expression(expr) as unknown as JoiExpression
15
+ }
@@ -19,4 +19,12 @@ declare module 'joi' {
19
19
  title?: string
20
20
  }
21
21
  }
22
+
23
+ interface JoiExpressionReturn {
24
+ render: (p1, p2, p3, p4, p5) => string
25
+ }
26
+
27
+ type JoiExpression = JoiExpressionReturn | string
28
+
29
+ type LanguageMessagesExt = Record<string, JoiExpression>
22
30
  }