@defra/forms-engine-plugin 0.0.4 → 0.0.6

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 (260) hide show
  1. package/.server/server/index.js +0 -4
  2. package/.server/server/index.js.map +1 -1
  3. package/.server/server/plugins/engine/helpers.js +3 -0
  4. package/.server/server/plugins/engine/helpers.js.map +1 -1
  5. package/.server/server/plugins/engine/index.js +27 -1
  6. package/.server/server/plugins/engine/index.js.map +1 -1
  7. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +2 -4
  8. package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
  9. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +4 -10
  10. package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
  11. package/.server/server/plugins/engine/pageControllers/StatusPageController.js +2 -3
  12. package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
  13. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +2 -4
  14. package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
  15. package/.server/server/plugins/engine/plugin.js +65 -6
  16. package/.server/server/plugins/engine/plugin.js.map +1 -1
  17. package/.server/server/plugins/engine/types.js.map +1 -1
  18. package/.server/server/{views → plugins/engine/views}/components/service-banner/template.test.js +1 -1
  19. package/.server/server/plugins/engine/views/components/service-banner/template.test.js.map +1 -0
  20. package/.server/server/{views → plugins/engine/views}/components/tag-env/template.test.js +1 -1
  21. package/.server/server/plugins/engine/views/components/tag-env/template.test.js.map +1 -0
  22. package/.server/server/services/cacheService.js +5 -2
  23. package/.server/server/services/cacheService.js.map +1 -1
  24. package/.server/typings/hapi/index.d.js.map +1 -1
  25. package/README.md +215 -4
  26. package/package.json +3 -3
  27. package/src/client/javascripts/application.js +87 -0
  28. package/src/client/javascripts/file-upload.js +386 -0
  29. package/src/client/stylesheets/_code.scss +33 -0
  30. package/src/client/stylesheets/_govuk-frontend.scss +4 -0
  31. package/src/client/stylesheets/_prose.scss +56 -0
  32. package/src/client/stylesheets/_service-banner.scss +24 -0
  33. package/src/client/stylesheets/_summary-list.scss +28 -0
  34. package/src/client/stylesheets/_tag-env.scss +24 -0
  35. package/src/client/stylesheets/application.scss +14 -0
  36. package/src/common/cookies.js +58 -0
  37. package/src/common/cookies.test.js +23 -0
  38. package/src/common/types.js +5 -0
  39. package/src/config/index.ts +271 -0
  40. package/src/index.ts +31 -0
  41. package/src/server/common/helpers/logging/logger-options.test.ts +50 -0
  42. package/src/server/common/helpers/logging/logger-options.ts +46 -0
  43. package/src/server/common/helpers/logging/logger.ts +7 -0
  44. package/src/server/common/helpers/logging/request-logger.ts +9 -0
  45. package/src/server/common/helpers/logging/request-tracing.js +10 -0
  46. package/src/server/common/helpers/redis-client.js +70 -0
  47. package/src/server/constants.js +1 -0
  48. package/src/server/forms/README.md +10 -0
  49. package/src/server/forms/components.json +1015 -0
  50. package/src/server/forms/report-a-terrorist.json +270 -0
  51. package/src/server/forms/runner-components-test.json +365 -0
  52. package/src/server/forms/test.json +581 -0
  53. package/src/server/index.test.ts +582 -0
  54. package/src/server/index.ts +135 -0
  55. package/src/server/plugins/blankie.test.ts +73 -0
  56. package/src/server/plugins/blankie.ts +48 -0
  57. package/src/server/plugins/crumb.ts +20 -0
  58. package/src/server/plugins/engine/README.md +87 -0
  59. package/src/server/plugins/engine/components/AutocompleteField.test.ts +294 -0
  60. package/src/server/plugins/engine/components/AutocompleteField.ts +49 -0
  61. package/src/server/plugins/engine/components/CheckboxesField.test.ts +379 -0
  62. package/src/server/plugins/engine/components/CheckboxesField.ts +106 -0
  63. package/src/server/plugins/engine/components/ComponentBase.ts +97 -0
  64. package/src/server/plugins/engine/components/ComponentCollection.ts +278 -0
  65. package/src/server/plugins/engine/components/DatePartsField.test.ts +822 -0
  66. package/src/server/plugins/engine/components/DatePartsField.ts +264 -0
  67. package/src/server/plugins/engine/components/Details.test.ts +49 -0
  68. package/src/server/plugins/engine/components/Details.ts +30 -0
  69. package/src/server/plugins/engine/components/EmailAddressField.test.ts +395 -0
  70. package/src/server/plugins/engine/components/EmailAddressField.ts +55 -0
  71. package/src/server/plugins/engine/components/FileUploadField.test.ts +778 -0
  72. package/src/server/plugins/engine/components/FileUploadField.ts +262 -0
  73. package/src/server/plugins/engine/components/FormComponent.ts +249 -0
  74. package/src/server/plugins/engine/components/Html.test.ts +48 -0
  75. package/src/server/plugins/engine/components/Html.ts +29 -0
  76. package/src/server/plugins/engine/components/InsetText.test.ts +48 -0
  77. package/src/server/plugins/engine/components/InsetText.ts +27 -0
  78. package/src/server/plugins/engine/components/List.test.ts +76 -0
  79. package/src/server/plugins/engine/components/List.ts +72 -0
  80. package/src/server/plugins/engine/components/ListFormComponent.ts +140 -0
  81. package/src/server/plugins/engine/components/MonthYearField.test.ts +567 -0
  82. package/src/server/plugins/engine/components/MonthYearField.ts +222 -0
  83. package/src/server/plugins/engine/components/MultilineTextField.test.ts +558 -0
  84. package/src/server/plugins/engine/components/MultilineTextField.ts +138 -0
  85. package/src/server/plugins/engine/components/NumberField.test.ts +701 -0
  86. package/src/server/plugins/engine/components/NumberField.ts +163 -0
  87. package/src/server/plugins/engine/components/RadiosField.test.ts +288 -0
  88. package/src/server/plugins/engine/components/RadiosField.ts +24 -0
  89. package/src/server/plugins/engine/components/SelectField.test.ts +288 -0
  90. package/src/server/plugins/engine/components/SelectField.ts +47 -0
  91. package/src/server/plugins/engine/components/SelectionControlField.ts +43 -0
  92. package/src/server/plugins/engine/components/TelephoneNumberField.test.ts +356 -0
  93. package/src/server/plugins/engine/components/TelephoneNumberField.ts +67 -0
  94. package/src/server/plugins/engine/components/TextField.test.ts +489 -0
  95. package/src/server/plugins/engine/components/TextField.ts +96 -0
  96. package/src/server/plugins/engine/components/UkAddressField.test.ts +623 -0
  97. package/src/server/plugins/engine/components/UkAddressField.ts +172 -0
  98. package/src/server/plugins/engine/components/YesNoField.test.ts +248 -0
  99. package/src/server/plugins/engine/components/YesNoField.ts +31 -0
  100. package/src/server/plugins/engine/components/constants.ts +1 -0
  101. package/src/server/plugins/engine/components/helpers.ts +330 -0
  102. package/src/server/plugins/engine/components/index.ts +24 -0
  103. package/src/server/plugins/engine/components/types.ts +117 -0
  104. package/src/server/plugins/engine/configureEnginePlugin.ts +47 -0
  105. package/src/server/plugins/engine/helpers.test.ts +791 -0
  106. package/src/server/plugins/engine/helpers.ts +384 -0
  107. package/src/server/plugins/engine/index.ts +47 -0
  108. package/src/server/plugins/engine/models/FormModel.test.ts +42 -0
  109. package/src/server/plugins/engine/models/FormModel.ts +443 -0
  110. package/src/server/plugins/engine/models/RepeatingSummaryViewModel.ts +0 -0
  111. package/src/server/plugins/engine/models/Section.ts +0 -0
  112. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +209 -0
  113. package/src/server/plugins/engine/models/SummaryViewModel.ts +220 -0
  114. package/src/server/plugins/engine/models/index.ts +2 -0
  115. package/src/server/plugins/engine/models/types.ts +114 -0
  116. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +143 -0
  117. package/src/server/plugins/engine/outputFormatters/human/v1.ts +73 -0
  118. package/src/server/plugins/engine/outputFormatters/index.test.ts +17 -0
  119. package/src/server/plugins/engine/outputFormatters/index.ts +44 -0
  120. package/src/server/plugins/engine/outputFormatters/machine/v1.test.ts +229 -0
  121. package/src/server/plugins/engine/outputFormatters/machine/v1.ts +140 -0
  122. package/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +229 -0
  123. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +153 -0
  124. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +1116 -0
  125. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +447 -0
  126. package/src/server/plugins/engine/pageControllers/PageController.test.ts +205 -0
  127. package/src/server/plugins/engine/pageControllers/PageController.ts +176 -0
  128. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +1264 -0
  129. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +565 -0
  130. package/src/server/plugins/engine/pageControllers/README.md +28 -0
  131. package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +264 -0
  132. package/src/server/plugins/engine/pageControllers/RepeatPageController.ts +458 -0
  133. package/src/server/plugins/engine/pageControllers/StartPageController.ts +18 -0
  134. package/src/server/plugins/engine/pageControllers/StatusPageController.ts +51 -0
  135. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +262 -0
  136. package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +28 -0
  137. package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +19 -0
  138. package/src/server/plugins/engine/pageControllers/helpers.test.ts +198 -0
  139. package/src/server/plugins/engine/pageControllers/helpers.ts +101 -0
  140. package/src/server/plugins/engine/pageControllers/index.ts +10 -0
  141. package/src/server/plugins/engine/pageControllers/validationOptions.ts +89 -0
  142. package/src/server/plugins/engine/plugin.ts +753 -0
  143. package/src/server/plugins/engine/services/formSubmissionService.js +46 -0
  144. package/src/server/plugins/engine/services/formsService.js +46 -0
  145. package/src/server/plugins/engine/services/formsService.test.js +90 -0
  146. package/src/server/plugins/engine/services/index.js +3 -0
  147. package/src/server/plugins/engine/services/notifyService.test.ts +132 -0
  148. package/src/server/plugins/engine/services/notifyService.ts +64 -0
  149. package/src/server/plugins/engine/services/uploadService.js +60 -0
  150. package/src/server/plugins/engine/types.ts +317 -0
  151. package/src/server/plugins/engine/views/components/autocompletefield.html +5 -0
  152. package/src/server/plugins/engine/views/components/checkboxesfield.html +5 -0
  153. package/src/server/plugins/engine/views/components/datepartsfield.html +5 -0
  154. package/src/server/plugins/engine/views/components/debug/macro.njk +3 -0
  155. package/src/server/plugins/engine/views/components/debug/template.njk +13 -0
  156. package/src/server/plugins/engine/views/components/details.html +6 -0
  157. package/src/server/plugins/engine/views/components/emailaddressfield.html +5 -0
  158. package/src/server/plugins/engine/views/components/fileuploadfield-key.html +8 -0
  159. package/src/server/plugins/engine/views/components/fileuploadfield-value.html +3 -0
  160. package/src/server/plugins/engine/views/components/fileuploadfield.html +24 -0
  161. package/src/server/plugins/engine/views/components/html.html +3 -0
  162. package/src/server/plugins/engine/views/components/insettext.html +7 -0
  163. package/src/server/plugins/engine/views/components/list.html +36 -0
  164. package/src/server/plugins/engine/views/components/monthyearfield.html +5 -0
  165. package/src/server/plugins/engine/views/components/multilinetextfield.html +10 -0
  166. package/src/server/plugins/engine/views/components/numberfield.html +5 -0
  167. package/src/server/plugins/engine/views/components/radiosfield.html +5 -0
  168. package/src/server/plugins/engine/views/components/selectfield.html +5 -0
  169. package/src/server/plugins/engine/views/components/service-banner/macro.njk +3 -0
  170. package/src/server/plugins/engine/views/components/service-banner/template.njk +20 -0
  171. package/src/server/plugins/engine/views/components/service-banner/template.test.js +43 -0
  172. package/src/server/plugins/engine/views/components/tag-env/macro.njk +3 -0
  173. package/src/server/plugins/engine/views/components/tag-env/template.njk +30 -0
  174. package/src/server/plugins/engine/views/components/tag-env/template.test.js +66 -0
  175. package/src/server/plugins/engine/views/components/telephonenumberfield.html +5 -0
  176. package/src/server/plugins/engine/views/components/textfield.html +5 -0
  177. package/src/server/plugins/engine/views/components/ukaddressfield.html +25 -0
  178. package/src/server/plugins/engine/views/components/yesnofield.html +5 -0
  179. package/src/server/plugins/engine/views/confirmation.html +19 -0
  180. package/src/server/plugins/engine/views/file-upload.html +45 -0
  181. package/src/server/plugins/engine/views/index.html +39 -0
  182. package/src/server/plugins/engine/views/item-delete.html +56 -0
  183. package/src/server/plugins/engine/views/layout.html +199 -0
  184. package/src/server/plugins/engine/views/partials/components.html +6 -0
  185. package/src/server/plugins/engine/views/partials/conditional-components.html +3 -0
  186. package/src/server/plugins/engine/views/partials/debug.html +44 -0
  187. package/src/server/plugins/engine/views/partials/form.html +15 -0
  188. package/src/server/plugins/engine/views/partials/heading.html +16 -0
  189. package/src/server/plugins/engine/views/partials/preview-banner.html +32 -0
  190. package/src/server/plugins/engine/views/partials/preview-banner.test.js +122 -0
  191. package/src/server/plugins/engine/views/partials/warn-missing-notification-email.html +10 -0
  192. package/src/server/plugins/engine/views/repeat-list-summary.html +53 -0
  193. package/src/server/plugins/engine/views/summary.html +50 -0
  194. package/src/server/plugins/errorPages.ts +58 -0
  195. package/src/server/plugins/nunjucks/context.js +88 -0
  196. package/src/server/plugins/nunjucks/context.test.js +142 -0
  197. package/src/server/plugins/nunjucks/enviroment.test.js +201 -0
  198. package/src/server/plugins/nunjucks/environment.js +116 -0
  199. package/src/server/plugins/nunjucks/filters/answer.js +27 -0
  200. package/src/server/plugins/nunjucks/filters/answer.test.js +89 -0
  201. package/src/server/plugins/nunjucks/filters/evaluate.js +21 -0
  202. package/src/server/plugins/nunjucks/filters/field.js +28 -0
  203. package/src/server/plugins/nunjucks/filters/field.test.js +75 -0
  204. package/src/server/plugins/nunjucks/filters/highlight.js +11 -0
  205. package/src/server/plugins/nunjucks/filters/href.js +30 -0
  206. package/src/server/plugins/nunjucks/filters/href.test.js +80 -0
  207. package/src/server/plugins/nunjucks/filters/index.js +8 -0
  208. package/src/server/plugins/nunjucks/filters/inspect.js +15 -0
  209. package/src/server/plugins/nunjucks/filters/page.js +24 -0
  210. package/src/server/plugins/nunjucks/filters/page.test.js +65 -0
  211. package/src/server/plugins/nunjucks/index.js +3 -0
  212. package/src/server/plugins/nunjucks/plugin.js +40 -0
  213. package/src/server/plugins/nunjucks/render.js +42 -0
  214. package/src/server/plugins/nunjucks/types.js +40 -0
  215. package/src/server/plugins/pulse.ts +11 -0
  216. package/src/server/plugins/router.ts +201 -0
  217. package/src/server/plugins/session.ts +28 -0
  218. package/src/server/routes/health.js +13 -0
  219. package/src/server/routes/health.test.js +35 -0
  220. package/src/server/routes/index.test.ts +125 -0
  221. package/src/server/routes/index.ts +2 -0
  222. package/src/server/routes/public.ts +47 -0
  223. package/src/server/routes/types.ts +48 -0
  224. package/src/server/schemas/index.ts +34 -0
  225. package/src/server/secure-context.js +43 -0
  226. package/src/server/services/cacheService.test.ts +277 -0
  227. package/src/server/services/cacheService.ts +138 -0
  228. package/src/server/services/httpService.test.js +491 -0
  229. package/src/server/services/httpService.ts +50 -0
  230. package/src/server/services/index.ts +1 -0
  231. package/src/server/types.ts +54 -0
  232. package/src/server/utils/notify.test.ts +37 -0
  233. package/src/server/utils/notify.ts +50 -0
  234. package/src/server/utils/secure-context/get-trust-store-certs.js +11 -0
  235. package/src/server/utils/secure-context/get-trust-store-certs.test.js +19 -0
  236. package/src/server/utils/utils.js +24 -0
  237. package/src/server/utils/utils.test.js +54 -0
  238. package/src/server/views/404.html +16 -0
  239. package/src/server/views/500.html +19 -0
  240. package/src/server/views/help/accessibility-statement.html +58 -0
  241. package/src/server/views/help/cookie-preferences.html +57 -0
  242. package/src/server/views/help/cookies.html +71 -0
  243. package/src/server/views/help/get-support.html +37 -0
  244. package/src/server/views/help/privacy-notice.html +68 -0
  245. package/src/server/views/help/terms-and-conditions.html +83 -0
  246. package/src/typings/hapi/index.d.ts +87 -0
  247. package/src/typings/hapi-tracing/index.d.ts +6 -0
  248. package/src/typings/index.d.ts +3 -0
  249. package/src/typings/joi/index.d.ts +22 -0
  250. package/.server/server/views/components/service-banner/template.test.js.map +0 -1
  251. package/.server/server/views/components/tag-env/template.test.js.map +0 -1
  252. /package/.server/server/{views → plugins/engine/views}/components/debug/macro.njk +0 -0
  253. /package/.server/server/{views → plugins/engine/views}/components/debug/template.njk +0 -0
  254. /package/.server/server/{views → plugins/engine/views}/components/service-banner/macro.njk +0 -0
  255. /package/.server/server/{views → plugins/engine/views}/components/service-banner/template.njk +0 -0
  256. /package/.server/server/{views → plugins/engine/views}/components/tag-env/macro.njk +0 -0
  257. /package/.server/server/{views → plugins/engine/views}/components/tag-env/template.njk +0 -0
  258. /package/.server/server/{views → plugins/engine/views}/confirmation.html +0 -0
  259. /package/.server/server/{views → plugins/engine/views}/layout.html +0 -0
  260. /package/.server/server/{views → plugins/engine/views}/summary.html +0 -0
@@ -0,0 +1,565 @@
1
+ import {
2
+ ComponentType,
3
+ ControllerType,
4
+ Engine,
5
+ hasComponents,
6
+ hasNext,
7
+ hasRepeater,
8
+ type Link,
9
+ type Page
10
+ } from '@defra/forms-model'
11
+ import { type ResponseToolkit, type RouteOptions } from '@hapi/hapi'
12
+ import { type ValidationErrorItem } from 'joi'
13
+
14
+ import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
15
+ import { optionalText } from '~/src/server/plugins/engine/components/constants.js'
16
+ import { type BackLink } from '~/src/server/plugins/engine/components/types.js'
17
+ import {
18
+ getCacheService,
19
+ getErrors,
20
+ normalisePath,
21
+ proceed
22
+ } from '~/src/server/plugins/engine/helpers.js'
23
+ import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
24
+ import { PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
25
+ import {
26
+ type FormContext,
27
+ type FormContextRequest,
28
+ type FormPageViewModel,
29
+ type FormParams,
30
+ type FormPayload,
31
+ type FormState,
32
+ type FormStateValue,
33
+ type FormSubmissionState
34
+ } from '~/src/server/plugins/engine/types.js'
35
+ import {
36
+ type FormRequest,
37
+ type FormRequestPayload,
38
+ type FormRequestPayloadRefs,
39
+ type FormRequestRefs
40
+ } from '~/src/server/routes/types.js'
41
+ import {
42
+ actionSchema,
43
+ crumbSchema,
44
+ paramsSchema
45
+ } from '~/src/server/schemas/index.js'
46
+ import { merge } from '~/src/server/services/cacheService.js'
47
+
48
+ export class QuestionPageController extends PageController {
49
+ collection: ComponentCollection
50
+ errorSummaryTitle = 'There is a problem'
51
+
52
+ constructor(model: FormModel, pageDef: Page) {
53
+ super(model, pageDef)
54
+
55
+ // Components collection
56
+ this.collection = new ComponentCollection(
57
+ hasComponents(pageDef) ? pageDef.components : [],
58
+ { model, page: this }
59
+ )
60
+
61
+ this.collection.formSchema = this.collection.formSchema.keys({
62
+ crumb: crumbSchema,
63
+ action: actionSchema
64
+ })
65
+ }
66
+
67
+ get next(): Link[] {
68
+ const { def, pageDef } = this
69
+
70
+ if (!hasNext(pageDef)) {
71
+ return []
72
+ }
73
+
74
+ // Remove stale links
75
+ return pageDef.next.filter(({ path }) => {
76
+ const linkPath = normalisePath(path)
77
+
78
+ return def.pages.some((page) => {
79
+ const pagePath = normalisePath(page.path)
80
+ return pagePath === linkPath
81
+ })
82
+ })
83
+ }
84
+
85
+ get allowContinue(): boolean {
86
+ if (this.model.engine === Engine.V2) {
87
+ return this.pageDef.controller !== ControllerType.Terminal
88
+ }
89
+
90
+ return this.next.length > 0
91
+ }
92
+
93
+ getItemId(request?: FormContextRequest) {
94
+ const { itemId } = this.getFormParams(request)
95
+ return itemId ?? request?.params.itemId
96
+ }
97
+
98
+ /**
99
+ * Used for mapping form payloads and errors to govuk-frontend's template api, so a page can be rendered
100
+ * @param request - the hapi request
101
+ * @param context - the form context
102
+ */
103
+ getViewModel(
104
+ request: FormContextRequest,
105
+ context: FormContext
106
+ ): FormPageViewModel {
107
+ const { collection, viewModel } = this
108
+ const { query } = request
109
+ const { payload, errors } = context
110
+
111
+ let { pageTitle, showTitle } = viewModel
112
+
113
+ const components = collection.getViewModel(payload, errors, query)
114
+ const formComponents = components.filter(
115
+ ({ isFormComponent }) => isFormComponent
116
+ )
117
+
118
+ // Single form component? Hide title and customise label or legend instead
119
+ if (formComponents.length === 1) {
120
+ const { model } = formComponents[0]
121
+ const { fieldset, label } = model
122
+
123
+ // Set as page heading when not following other content
124
+ const isPageHeading = formComponents[0] === components[0]
125
+
126
+ // Check for legend or label
127
+ const labelOrLegend = fieldset?.legend ?? label
128
+
129
+ // Use legend or label as page heading
130
+ if (labelOrLegend) {
131
+ const size = isPageHeading ? 'l' : 'm'
132
+
133
+ labelOrLegend.classes =
134
+ labelOrLegend === label
135
+ ? `govuk-label--${size}`
136
+ : `govuk-fieldset__legend--${size}`
137
+
138
+ if (isPageHeading) {
139
+ labelOrLegend.isPageHeading = isPageHeading
140
+
141
+ // Check for optional in label
142
+ const isOptional =
143
+ this.collection.fields.at(0)?.options.required === false
144
+
145
+ if (pageTitle) {
146
+ labelOrLegend.text = isOptional
147
+ ? `${pageTitle}${optionalText}`
148
+ : pageTitle
149
+ }
150
+
151
+ pageTitle = pageTitle || labelOrLegend.text
152
+ }
153
+ }
154
+
155
+ showTitle = !isPageHeading
156
+ } else if (formComponents.length > 1) {
157
+ // When there is more than one form component,
158
+ // adjust the label/legends to give equal prominence
159
+ for (const { model } of formComponents) {
160
+ if (model.fieldset?.legend) {
161
+ model.fieldset.legend.classes = 'govuk-fieldset__legend--m'
162
+ }
163
+ if (model.label) {
164
+ model.label.classes = 'govuk-label--m'
165
+ }
166
+ }
167
+ }
168
+
169
+ return {
170
+ ...viewModel,
171
+ backLink: this.getBackLink(request, context),
172
+ context,
173
+ showTitle,
174
+ components,
175
+ errors
176
+ }
177
+ }
178
+
179
+ getRelevantPath(
180
+ request: FormRequest | FormRequestPayload,
181
+ context: FormContext
182
+ ) {
183
+ const { paths } = context
184
+
185
+ const startPath = this.getStartPath()
186
+ const relevantPath = paths.at(-1) ?? startPath
187
+
188
+ return !paths.length
189
+ ? startPath // First possible path
190
+ : relevantPath // Last possible path
191
+ }
192
+
193
+ /**
194
+ * Apply conditions to evaluation state to determine next page path
195
+ */
196
+ getNextPath(context: FormContext) {
197
+ const { model, next, path } = this
198
+ const { evaluationState } = context
199
+
200
+ const summaryPath = this.getSummaryPath()
201
+ const statusPath = this.getStatusPath()
202
+
203
+ // Walk from summary page (no next links) to status page
204
+ let defaultPath = path === summaryPath ? statusPath : undefined
205
+
206
+ if (model.engine === Engine.V2) {
207
+ if (this.pageDef.controller !== ControllerType.Terminal) {
208
+ const { pages } = this.model
209
+ const pageIndex = pages.indexOf(this)
210
+
211
+ // The "next" page is the first found after the current which is
212
+ // either unconditional or has a condition that evaluates to "true"
213
+ const nextPage = pages.slice(pageIndex + 1).find((page) => {
214
+ const { condition } = page
215
+
216
+ if (condition) {
217
+ const conditionResult = condition.fn(evaluationState)
218
+
219
+ if (!conditionResult) {
220
+ return false
221
+ }
222
+ }
223
+
224
+ return true
225
+ })
226
+
227
+ return nextPage?.path ?? defaultPath
228
+ } else {
229
+ return defaultPath
230
+ }
231
+ }
232
+
233
+ const nextLink = next.find((link) => {
234
+ const { condition } = link
235
+
236
+ if (condition) {
237
+ return model.conditions[condition]?.fn(evaluationState) ?? false
238
+ }
239
+
240
+ defaultPath = link.path
241
+ return false
242
+ })
243
+
244
+ return nextLink?.path ?? defaultPath
245
+ }
246
+
247
+ /**
248
+ * Gets the form payload (from state) for this page only
249
+ */
250
+ getFormDataFromState(
251
+ request: FormContextRequest | undefined,
252
+ state: FormSubmissionState
253
+ ): FormPayload {
254
+ const { collection } = this
255
+
256
+ // Form params from request
257
+ const params = this.getFormParams(request)
258
+
259
+ // Form payload from state
260
+ const payload = collection.getFormDataFromState(state)
261
+
262
+ return {
263
+ ...params,
264
+ ...payload
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Gets form params (from payload) for this page only
270
+ */
271
+ getFormParams(request?: FormContextRequest): FormParams {
272
+ const { payload } = request ?? {}
273
+
274
+ const result = paramsSchema.validate(payload, {
275
+ abortEarly: false,
276
+ stripUnknown: true
277
+ })
278
+
279
+ return result.value as FormParams
280
+ }
281
+
282
+ getStateFromValidForm(
283
+ request: FormContextRequest,
284
+ state: FormSubmissionState,
285
+ payload: FormPayload
286
+ ): FormState {
287
+ return this.collection.getStateFromValidForm(payload)
288
+ }
289
+
290
+ getErrors(details?: ValidationErrorItem[]) {
291
+ return getErrors(details)
292
+ }
293
+
294
+ async getState(request: FormRequest | FormRequestPayload) {
295
+ const { query } = request
296
+
297
+ // Skip get for preview URL direct access
298
+ if ('force' in query) {
299
+ return {}
300
+ }
301
+
302
+ const cacheService = getCacheService(request.server)
303
+
304
+ return cacheService.getState(request)
305
+ }
306
+
307
+ async setState(
308
+ request: FormRequest | FormRequestPayload,
309
+ state: FormSubmissionState
310
+ ) {
311
+ const { query } = request
312
+
313
+ // Skip set for preview URL direct access
314
+ if ('force' in query) {
315
+ return state
316
+ }
317
+
318
+ const cacheService = getCacheService(request.server)
319
+
320
+ return cacheService.setState(request, state)
321
+ }
322
+
323
+ async mergeState(
324
+ request: FormRequest | FormRequestPayload,
325
+ state: FormSubmissionState,
326
+ update: object
327
+ ) {
328
+ const { query } = request
329
+
330
+ // Merge state before set
331
+ const updated = merge(state, update)
332
+
333
+ // Skip set for preview URL direct access
334
+ if ('force' in query) {
335
+ return updated
336
+ }
337
+
338
+ const cacheService = getCacheService(request.server)
339
+
340
+ return cacheService.setState(request, updated)
341
+ }
342
+
343
+ filterConditionalComponents(
344
+ viewModel: FormPageViewModel,
345
+ model: FormModel,
346
+ evaluationState: Partial<Record<string, FormStateValue>>
347
+ ) {
348
+ // Filter our components based on their conditions using our evaluated state
349
+ let filtered = viewModel.components.filter((component) => {
350
+ if (
351
+ (!!component.model.content ||
352
+ component.type === ComponentType.Details) &&
353
+ component.model.condition
354
+ ) {
355
+ const condition = model.conditions[component.model.condition]
356
+ return condition?.fn(evaluationState)
357
+ }
358
+ return true
359
+ })
360
+
361
+ /**
362
+ * For conditional reveal components (which we no longer support until GDS resolves the related accessibility issues {@link https://github.com/alphagov/govuk-frontend/issues/1991}
363
+ */
364
+ filtered = filtered.map((component) => {
365
+ const evaluatedComponent = component
366
+ const content = evaluatedComponent.model.content
367
+ if (Array.isArray(content)) {
368
+ evaluatedComponent.model.content = content.filter((item) =>
369
+ item.condition
370
+ ? model.conditions[item.condition]?.fn(evaluationState)
371
+ : true
372
+ )
373
+ }
374
+ // apply condition to items for radios, checkboxes etc
375
+ const items = evaluatedComponent.model.items
376
+
377
+ if (Array.isArray(items)) {
378
+ evaluatedComponent.model.items = items.filter((item) =>
379
+ item.condition
380
+ ? model.conditions[item.condition]?.fn(evaluationState)
381
+ : true
382
+ )
383
+ }
384
+
385
+ return evaluatedComponent
386
+ })
387
+
388
+ return filtered
389
+ }
390
+
391
+ makeGetRouteHandler() {
392
+ return async (
393
+ request: FormRequest,
394
+ context: FormContext,
395
+ h: Pick<ResponseToolkit, 'redirect' | 'view'>
396
+ ) => {
397
+ const { collection, model, viewName } = this
398
+ const { evaluationState } = context
399
+
400
+ const viewModel = this.getViewModel(request, context)
401
+ viewModel.errors = collection.getErrors(viewModel.errors)
402
+
403
+ /**
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
405
+ */
406
+
407
+ // Filter our components based on their conditions using our evaluated state
408
+ viewModel.components = this.filterConditionalComponents(
409
+ viewModel,
410
+ model,
411
+ evaluationState
412
+ )
413
+
414
+ viewModel.hasMissingNotificationEmail =
415
+ await this.hasMissingNotificationEmail(request, context)
416
+
417
+ return h.view(viewName, viewModel)
418
+ }
419
+ }
420
+
421
+ async hasMissingNotificationEmail(
422
+ request: FormRequest,
423
+ context: FormContext
424
+ ) {
425
+ const { path } = this
426
+ const { params } = request
427
+ const { isForceAccess } = context
428
+
429
+ const startPath = this.getStartPath()
430
+ const summaryPath = this.getSummaryPath()
431
+ const { formsService } = this.model.services
432
+ const { getFormMetadata } = formsService
433
+
434
+ // Warn the user if the form has no notification email set only on start page and summary page
435
+ if ([startPath, summaryPath].includes(path) && !isForceAccess) {
436
+ const { notificationEmail } = await getFormMetadata(params.slug)
437
+ return !notificationEmail
438
+ }
439
+
440
+ return false
441
+ }
442
+
443
+ /**
444
+ * Get the back link for a given progress.
445
+ */
446
+ protected getBackLink(
447
+ request: FormContextRequest,
448
+ context: FormContext
449
+ ): BackLink | undefined {
450
+ const { pageDef } = this
451
+ const { path, query } = request
452
+ const { returnUrl } = query
453
+ const { paths } = context
454
+
455
+ const itemId = this.getItemId(request)
456
+
457
+ // Check answers back link
458
+ if (returnUrl) {
459
+ return {
460
+ text:
461
+ hasRepeater(pageDef) && itemId
462
+ ? 'Go back to add another'
463
+ : 'Go back to check answers',
464
+ href: returnUrl
465
+ }
466
+ }
467
+
468
+ // Item delete pages etc
469
+ const backPath =
470
+ itemId && !path.endsWith(itemId)
471
+ ? paths.at(-1) // Back to main page
472
+ : paths.at(-2) // Back to previous page
473
+
474
+ // No back link
475
+ if (!backPath) {
476
+ return
477
+ }
478
+
479
+ // Default back link
480
+ return {
481
+ text: 'Back',
482
+ href: this.getHref(backPath)
483
+ }
484
+ }
485
+
486
+ makePostRouteHandler() {
487
+ return async (
488
+ request: FormRequestPayload,
489
+ context: FormContext,
490
+ h: Pick<ResponseToolkit, 'redirect' | 'view'>
491
+ ) => {
492
+ const { collection, viewName, model } = this
493
+ const { isForceAccess, state, evaluationState } = context
494
+
495
+ /**
496
+ * If there are any errors, render the page with the parsed errors
497
+ * @todo Refactor to match POST REDIRECT GET pattern
498
+ */
499
+ if (context.errors || isForceAccess) {
500
+ const viewModel = this.getViewModel(request, context)
501
+ viewModel.errors = collection.getErrors(viewModel.errors)
502
+
503
+ // Filter our components based on their conditions using our evaluated state
504
+ viewModel.components = this.filterConditionalComponents(
505
+ viewModel,
506
+ model,
507
+ evaluationState
508
+ )
509
+
510
+ return h.view(viewName, viewModel)
511
+ }
512
+
513
+ // Save and proceed
514
+ await this.setState(request, state)
515
+ return this.proceed(request, h, this.getNextPath(context))
516
+ }
517
+ }
518
+
519
+ proceed(
520
+ request: FormContextRequest,
521
+ h: Pick<ResponseToolkit, 'redirect' | 'view'>,
522
+ nextPath?: string
523
+ ) {
524
+ const nextUrl = nextPath
525
+ ? this.getHref(nextPath) // Redirect to next page
526
+ : this.href // Redirect to current page (refresh)
527
+
528
+ return proceed(request, h, nextUrl)
529
+ }
530
+
531
+ /**
532
+ * {@link https://hapi.dev/api/?v=20.1.2#route-options}
533
+ */
534
+ get getRouteOptions(): RouteOptions<FormRequestRefs> {
535
+ return {
536
+ ext: {
537
+ onPostHandler: {
538
+ method(_request, h) {
539
+ return h.continue
540
+ }
541
+ }
542
+ }
543
+ }
544
+ }
545
+
546
+ /**
547
+ * {@link https://hapi.dev/api/?v=20.1.2#route-options}
548
+ */
549
+ get postRouteOptions(): RouteOptions<FormRequestPayloadRefs> {
550
+ return {
551
+ payload: {
552
+ parse: true,
553
+ maxBytes: Number.MAX_SAFE_INTEGER,
554
+ failAction: 'ignore'
555
+ },
556
+ ext: {
557
+ onPostHandler: {
558
+ method(_request, h) {
559
+ return h.continue
560
+ }
561
+ }
562
+ }
563
+ }
564
+ }
565
+ }
@@ -0,0 +1,28 @@
1
+ # Page Controllers
2
+
3
+ Form pages could have specific controllers and this is specified inside the Form JSON, please see below a sample where the page summary is specifying it's controller via `controller` property.
4
+
5
+ ```javascript
6
+ {
7
+ "pages": [
8
+ {
9
+ "path": "/summary",
10
+ "controller": "./pages/summary.js", // legacy controller path
11
+ "title": "Summary",
12
+ "components": [],
13
+ "next": []
14
+ },
15
+ {
16
+ "path": "/summary",
17
+ "controller": "SummaryPageController", // we now use the controller class name
18
+ "title": "Summary",
19
+ "components": [],
20
+ "next": []
21
+ }
22
+ ]
23
+ }
24
+ ```
25
+
26
+ Previously controllers were dynamically loaded from the file system, this is why you see a path such as `./pages/summary.js`. This feature has never been used since the application was forked and so it has been deprecated.
27
+
28
+ To keep backward compatibility with legacy forms JSON we have the `getPageController` helper dealing with the possibility of a controller value being a file path or a class name (see `helpers.ts`)