@defra/forms-engine-plugin 0.0.4 → 0.0.5

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 (224) hide show
  1. package/package.json +3 -2
  2. package/src/client/javascripts/application.js +87 -0
  3. package/src/client/javascripts/file-upload.js +386 -0
  4. package/src/client/stylesheets/_code.scss +33 -0
  5. package/src/client/stylesheets/_govuk-frontend.scss +4 -0
  6. package/src/client/stylesheets/_prose.scss +56 -0
  7. package/src/client/stylesheets/_service-banner.scss +24 -0
  8. package/src/client/stylesheets/_summary-list.scss +28 -0
  9. package/src/client/stylesheets/_tag-env.scss +24 -0
  10. package/src/client/stylesheets/application.scss +14 -0
  11. package/src/common/cookies.js +58 -0
  12. package/src/common/cookies.test.js +23 -0
  13. package/src/common/types.js +5 -0
  14. package/src/config/index.ts +271 -0
  15. package/src/index.ts +31 -0
  16. package/src/server/common/helpers/logging/logger-options.test.ts +50 -0
  17. package/src/server/common/helpers/logging/logger-options.ts +46 -0
  18. package/src/server/common/helpers/logging/logger.ts +7 -0
  19. package/src/server/common/helpers/logging/request-logger.ts +9 -0
  20. package/src/server/common/helpers/logging/request-tracing.js +10 -0
  21. package/src/server/common/helpers/redis-client.js +70 -0
  22. package/src/server/constants.js +1 -0
  23. package/src/server/forms/README.md +10 -0
  24. package/src/server/forms/components.json +1015 -0
  25. package/src/server/forms/report-a-terrorist.json +270 -0
  26. package/src/server/forms/runner-components-test.json +365 -0
  27. package/src/server/forms/test.json +581 -0
  28. package/src/server/index.test.ts +582 -0
  29. package/src/server/index.ts +140 -0
  30. package/src/server/plugins/blankie.test.ts +73 -0
  31. package/src/server/plugins/blankie.ts +48 -0
  32. package/src/server/plugins/crumb.ts +20 -0
  33. package/src/server/plugins/engine/README.md +87 -0
  34. package/src/server/plugins/engine/components/AutocompleteField.test.ts +294 -0
  35. package/src/server/plugins/engine/components/AutocompleteField.ts +49 -0
  36. package/src/server/plugins/engine/components/CheckboxesField.test.ts +379 -0
  37. package/src/server/plugins/engine/components/CheckboxesField.ts +106 -0
  38. package/src/server/plugins/engine/components/ComponentBase.ts +97 -0
  39. package/src/server/plugins/engine/components/ComponentCollection.ts +278 -0
  40. package/src/server/plugins/engine/components/DatePartsField.test.ts +822 -0
  41. package/src/server/plugins/engine/components/DatePartsField.ts +264 -0
  42. package/src/server/plugins/engine/components/Details.test.ts +49 -0
  43. package/src/server/plugins/engine/components/Details.ts +30 -0
  44. package/src/server/plugins/engine/components/EmailAddressField.test.ts +395 -0
  45. package/src/server/plugins/engine/components/EmailAddressField.ts +55 -0
  46. package/src/server/plugins/engine/components/FileUploadField.test.ts +778 -0
  47. package/src/server/plugins/engine/components/FileUploadField.ts +262 -0
  48. package/src/server/plugins/engine/components/FormComponent.ts +249 -0
  49. package/src/server/plugins/engine/components/Html.test.ts +48 -0
  50. package/src/server/plugins/engine/components/Html.ts +29 -0
  51. package/src/server/plugins/engine/components/InsetText.test.ts +48 -0
  52. package/src/server/plugins/engine/components/InsetText.ts +27 -0
  53. package/src/server/plugins/engine/components/List.test.ts +76 -0
  54. package/src/server/plugins/engine/components/List.ts +72 -0
  55. package/src/server/plugins/engine/components/ListFormComponent.ts +140 -0
  56. package/src/server/plugins/engine/components/MonthYearField.test.ts +567 -0
  57. package/src/server/plugins/engine/components/MonthYearField.ts +222 -0
  58. package/src/server/plugins/engine/components/MultilineTextField.test.ts +558 -0
  59. package/src/server/plugins/engine/components/MultilineTextField.ts +138 -0
  60. package/src/server/plugins/engine/components/NumberField.test.ts +701 -0
  61. package/src/server/plugins/engine/components/NumberField.ts +163 -0
  62. package/src/server/plugins/engine/components/RadiosField.test.ts +288 -0
  63. package/src/server/plugins/engine/components/RadiosField.ts +24 -0
  64. package/src/server/plugins/engine/components/SelectField.test.ts +288 -0
  65. package/src/server/plugins/engine/components/SelectField.ts +47 -0
  66. package/src/server/plugins/engine/components/SelectionControlField.ts +43 -0
  67. package/src/server/plugins/engine/components/TelephoneNumberField.test.ts +356 -0
  68. package/src/server/plugins/engine/components/TelephoneNumberField.ts +67 -0
  69. package/src/server/plugins/engine/components/TextField.test.ts +489 -0
  70. package/src/server/plugins/engine/components/TextField.ts +96 -0
  71. package/src/server/plugins/engine/components/UkAddressField.test.ts +623 -0
  72. package/src/server/plugins/engine/components/UkAddressField.ts +172 -0
  73. package/src/server/plugins/engine/components/YesNoField.test.ts +248 -0
  74. package/src/server/plugins/engine/components/YesNoField.ts +31 -0
  75. package/src/server/plugins/engine/components/constants.ts +1 -0
  76. package/src/server/plugins/engine/components/helpers.ts +330 -0
  77. package/src/server/plugins/engine/components/index.ts +24 -0
  78. package/src/server/plugins/engine/components/types.ts +117 -0
  79. package/src/server/plugins/engine/configureEnginePlugin.ts +47 -0
  80. package/src/server/plugins/engine/helpers.test.ts +791 -0
  81. package/src/server/plugins/engine/helpers.ts +379 -0
  82. package/src/server/plugins/engine/index.ts +7 -0
  83. package/src/server/plugins/engine/models/FormModel.test.ts +42 -0
  84. package/src/server/plugins/engine/models/FormModel.ts +443 -0
  85. package/src/server/plugins/engine/models/RepeatingSummaryViewModel.ts +0 -0
  86. package/src/server/plugins/engine/models/Section.ts +0 -0
  87. package/src/server/plugins/engine/models/SummaryViewModel.test.ts +209 -0
  88. package/src/server/plugins/engine/models/SummaryViewModel.ts +220 -0
  89. package/src/server/plugins/engine/models/index.ts +2 -0
  90. package/src/server/plugins/engine/models/types.ts +114 -0
  91. package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +143 -0
  92. package/src/server/plugins/engine/outputFormatters/human/v1.ts +73 -0
  93. package/src/server/plugins/engine/outputFormatters/index.test.ts +17 -0
  94. package/src/server/plugins/engine/outputFormatters/index.ts +44 -0
  95. package/src/server/plugins/engine/outputFormatters/machine/v1.test.ts +229 -0
  96. package/src/server/plugins/engine/outputFormatters/machine/v1.ts +140 -0
  97. package/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +229 -0
  98. package/src/server/plugins/engine/outputFormatters/machine/v2.ts +153 -0
  99. package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +1108 -0
  100. package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +446 -0
  101. package/src/server/plugins/engine/pageControllers/PageController.test.ts +205 -0
  102. package/src/server/plugins/engine/pageControllers/PageController.ts +176 -0
  103. package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +1264 -0
  104. package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +561 -0
  105. package/src/server/plugins/engine/pageControllers/README.md +28 -0
  106. package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +264 -0
  107. package/src/server/plugins/engine/pageControllers/RepeatPageController.ts +458 -0
  108. package/src/server/plugins/engine/pageControllers/StartPageController.ts +18 -0
  109. package/src/server/plugins/engine/pageControllers/StatusPageController.ts +50 -0
  110. package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +261 -0
  111. package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +28 -0
  112. package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +19 -0
  113. package/src/server/plugins/engine/pageControllers/helpers.test.ts +198 -0
  114. package/src/server/plugins/engine/pageControllers/helpers.ts +101 -0
  115. package/src/server/plugins/engine/pageControllers/index.ts +10 -0
  116. package/src/server/plugins/engine/pageControllers/validationOptions.ts +89 -0
  117. package/src/server/plugins/engine/plugin.ts +673 -0
  118. package/src/server/plugins/engine/services/formSubmissionService.js +46 -0
  119. package/src/server/plugins/engine/services/formsService.js +46 -0
  120. package/src/server/plugins/engine/services/formsService.test.js +90 -0
  121. package/src/server/plugins/engine/services/index.js +3 -0
  122. package/src/server/plugins/engine/services/notifyService.test.ts +132 -0
  123. package/src/server/plugins/engine/services/notifyService.ts +64 -0
  124. package/src/server/plugins/engine/services/uploadService.js +60 -0
  125. package/src/server/plugins/engine/types.ts +315 -0
  126. package/src/server/plugins/engine/views/components/autocompletefield.html +5 -0
  127. package/src/server/plugins/engine/views/components/checkboxesfield.html +5 -0
  128. package/src/server/plugins/engine/views/components/datepartsfield.html +5 -0
  129. package/src/server/plugins/engine/views/components/details.html +6 -0
  130. package/src/server/plugins/engine/views/components/emailaddressfield.html +5 -0
  131. package/src/server/plugins/engine/views/components/fileuploadfield-key.html +8 -0
  132. package/src/server/plugins/engine/views/components/fileuploadfield-value.html +3 -0
  133. package/src/server/plugins/engine/views/components/fileuploadfield.html +24 -0
  134. package/src/server/plugins/engine/views/components/html.html +3 -0
  135. package/src/server/plugins/engine/views/components/insettext.html +7 -0
  136. package/src/server/plugins/engine/views/components/list.html +36 -0
  137. package/src/server/plugins/engine/views/components/monthyearfield.html +5 -0
  138. package/src/server/plugins/engine/views/components/multilinetextfield.html +10 -0
  139. package/src/server/plugins/engine/views/components/numberfield.html +5 -0
  140. package/src/server/plugins/engine/views/components/radiosfield.html +5 -0
  141. package/src/server/plugins/engine/views/components/selectfield.html +5 -0
  142. package/src/server/plugins/engine/views/components/telephonenumberfield.html +5 -0
  143. package/src/server/plugins/engine/views/components/textfield.html +5 -0
  144. package/src/server/plugins/engine/views/components/ukaddressfield.html +25 -0
  145. package/src/server/plugins/engine/views/components/yesnofield.html +5 -0
  146. package/src/server/plugins/engine/views/file-upload.html +45 -0
  147. package/src/server/plugins/engine/views/index.html +39 -0
  148. package/src/server/plugins/engine/views/item-delete.html +56 -0
  149. package/src/server/plugins/engine/views/partials/components.html +6 -0
  150. package/src/server/plugins/engine/views/partials/conditional-components.html +3 -0
  151. package/src/server/plugins/engine/views/partials/debug.html +44 -0
  152. package/src/server/plugins/engine/views/partials/form.html +15 -0
  153. package/src/server/plugins/engine/views/partials/heading.html +16 -0
  154. package/src/server/plugins/engine/views/partials/preview-banner.html +32 -0
  155. package/src/server/plugins/engine/views/partials/preview-banner.test.js +122 -0
  156. package/src/server/plugins/engine/views/partials/warn-missing-notification-email.html +10 -0
  157. package/src/server/plugins/engine/views/repeat-list-summary.html +53 -0
  158. package/src/server/plugins/errorPages.ts +58 -0
  159. package/src/server/plugins/nunjucks/context.js +88 -0
  160. package/src/server/plugins/nunjucks/context.test.js +142 -0
  161. package/src/server/plugins/nunjucks/enviroment.test.js +201 -0
  162. package/src/server/plugins/nunjucks/environment.js +116 -0
  163. package/src/server/plugins/nunjucks/filters/answer.js +27 -0
  164. package/src/server/plugins/nunjucks/filters/answer.test.js +89 -0
  165. package/src/server/plugins/nunjucks/filters/evaluate.js +21 -0
  166. package/src/server/plugins/nunjucks/filters/field.js +28 -0
  167. package/src/server/plugins/nunjucks/filters/field.test.js +75 -0
  168. package/src/server/plugins/nunjucks/filters/highlight.js +11 -0
  169. package/src/server/plugins/nunjucks/filters/href.js +30 -0
  170. package/src/server/plugins/nunjucks/filters/href.test.js +80 -0
  171. package/src/server/plugins/nunjucks/filters/index.js +8 -0
  172. package/src/server/plugins/nunjucks/filters/inspect.js +15 -0
  173. package/src/server/plugins/nunjucks/filters/page.js +24 -0
  174. package/src/server/plugins/nunjucks/filters/page.test.js +65 -0
  175. package/src/server/plugins/nunjucks/index.js +3 -0
  176. package/src/server/plugins/nunjucks/plugin.js +40 -0
  177. package/src/server/plugins/nunjucks/render.js +42 -0
  178. package/src/server/plugins/nunjucks/types.js +40 -0
  179. package/src/server/plugins/pulse.ts +11 -0
  180. package/src/server/plugins/router.ts +201 -0
  181. package/src/server/plugins/session.ts +28 -0
  182. package/src/server/routes/health.js +13 -0
  183. package/src/server/routes/health.test.js +35 -0
  184. package/src/server/routes/index.test.ts +125 -0
  185. package/src/server/routes/index.ts +2 -0
  186. package/src/server/routes/public.ts +47 -0
  187. package/src/server/routes/types.ts +48 -0
  188. package/src/server/schemas/index.ts +34 -0
  189. package/src/server/secure-context.js +43 -0
  190. package/src/server/services/cacheService.test.ts +276 -0
  191. package/src/server/services/cacheService.ts +131 -0
  192. package/src/server/services/httpService.test.js +491 -0
  193. package/src/server/services/httpService.ts +50 -0
  194. package/src/server/services/index.ts +1 -0
  195. package/src/server/types.ts +54 -0
  196. package/src/server/utils/notify.test.ts +37 -0
  197. package/src/server/utils/notify.ts +50 -0
  198. package/src/server/utils/secure-context/get-trust-store-certs.js +11 -0
  199. package/src/server/utils/secure-context/get-trust-store-certs.test.js +19 -0
  200. package/src/server/utils/utils.js +24 -0
  201. package/src/server/utils/utils.test.js +54 -0
  202. package/src/server/views/404.html +16 -0
  203. package/src/server/views/500.html +19 -0
  204. package/src/server/views/components/debug/macro.njk +3 -0
  205. package/src/server/views/components/debug/template.njk +13 -0
  206. package/src/server/views/components/service-banner/macro.njk +3 -0
  207. package/src/server/views/components/service-banner/template.njk +20 -0
  208. package/src/server/views/components/service-banner/template.test.js +43 -0
  209. package/src/server/views/components/tag-env/macro.njk +3 -0
  210. package/src/server/views/components/tag-env/template.njk +30 -0
  211. package/src/server/views/components/tag-env/template.test.js +66 -0
  212. package/src/server/views/confirmation.html +19 -0
  213. package/src/server/views/help/accessibility-statement.html +58 -0
  214. package/src/server/views/help/cookie-preferences.html +57 -0
  215. package/src/server/views/help/cookies.html +71 -0
  216. package/src/server/views/help/get-support.html +37 -0
  217. package/src/server/views/help/privacy-notice.html +68 -0
  218. package/src/server/views/help/terms-and-conditions.html +83 -0
  219. package/src/server/views/layout.html +199 -0
  220. package/src/server/views/summary.html +50 -0
  221. package/src/typings/hapi/index.d.ts +95 -0
  222. package/src/typings/hapi-tracing/index.d.ts +6 -0
  223. package/src/typings/index.d.ts +3 -0
  224. package/src/typings/joi/index.d.ts +22 -0
@@ -0,0 +1,262 @@
1
+ import { type FileUploadFieldComponent } from '@defra/forms-model'
2
+ import joi, { type ArraySchema } from 'joi'
3
+
4
+ import {
5
+ FormComponent,
6
+ isUploadState
7
+ } from '~/src/server/plugins/engine/components/FormComponent.js'
8
+ import {
9
+ FileStatus,
10
+ UploadStatus,
11
+ type FileState,
12
+ type FileUpload,
13
+ type FileUploadMetadata,
14
+ type FormPayload,
15
+ type FormState,
16
+ type FormStateValue,
17
+ type FormSubmissionError,
18
+ type FormSubmissionState,
19
+ type SummaryList,
20
+ type SummaryListAction,
21
+ type SummaryListRow,
22
+ type UploadState,
23
+ type UploadStatusFileResponse,
24
+ type UploadStatusResponse
25
+ } from '~/src/server/plugins/engine/types.js'
26
+ import { render } from '~/src/server/plugins/nunjucks/index.js'
27
+ import { type FormQuery } from '~/src/server/routes/types.js'
28
+
29
+ export const uploadIdSchema = joi.string().uuid().required()
30
+
31
+ export const fileSchema = joi
32
+ .object<FileUpload>({
33
+ fileId: joi.string().uuid().required(),
34
+ filename: joi.string().required(),
35
+ contentLength: joi.number().required()
36
+ })
37
+ .required()
38
+
39
+ export const tempFileSchema = fileSchema.append({
40
+ fileStatus: joi
41
+ .string()
42
+ .valid(FileStatus.complete, FileStatus.rejected, FileStatus.pending)
43
+ .required(),
44
+ errorMessage: joi.string().optional()
45
+ })
46
+
47
+ export const formFileSchema = fileSchema.append({
48
+ fileStatus: joi.string().valid(FileStatus.complete).required()
49
+ })
50
+
51
+ export const metadataSchema = joi
52
+ .object<FileUploadMetadata>()
53
+ .keys({
54
+ retrievalKey: joi.string().email().required()
55
+ })
56
+ .required()
57
+
58
+ export const tempStatusSchema = joi
59
+ .object<UploadStatusFileResponse>({
60
+ uploadStatus: joi
61
+ .string()
62
+ .valid(UploadStatus.ready, UploadStatus.pending)
63
+ .required(),
64
+ metadata: metadataSchema,
65
+ form: joi.object().required().keys({
66
+ file: tempFileSchema
67
+ }),
68
+ numberOfRejectedFiles: joi.number().optional()
69
+ })
70
+ .required()
71
+
72
+ export const formStatusSchema = joi
73
+ .object<UploadStatusResponse>({
74
+ uploadStatus: joi.string().valid(UploadStatus.ready).required(),
75
+ metadata: metadataSchema,
76
+ form: joi.object().required().keys({
77
+ file: formFileSchema
78
+ }),
79
+ numberOfRejectedFiles: joi.number().valid(0).required()
80
+ })
81
+ .required()
82
+
83
+ export const itemSchema = joi.object<FileState>({
84
+ uploadId: uploadIdSchema
85
+ })
86
+
87
+ export const tempItemSchema = itemSchema.append({
88
+ status: tempStatusSchema
89
+ })
90
+
91
+ export const formItemSchema = itemSchema.append({
92
+ status: formStatusSchema
93
+ })
94
+
95
+ export class FileUploadField extends FormComponent {
96
+ declare options: FileUploadFieldComponent['options']
97
+ declare schema: FileUploadFieldComponent['schema']
98
+ declare formSchema: ArraySchema<FileState>
99
+ declare stateSchema: ArraySchema<FileState>
100
+
101
+ constructor(
102
+ def: FileUploadFieldComponent,
103
+ props: ConstructorParameters<typeof FormComponent>[1]
104
+ ) {
105
+ super(def, props)
106
+
107
+ const { options, schema, title } = def
108
+
109
+ let formSchema = joi.array<FileState>().label(title).single().required()
110
+
111
+ if (options.required === false) {
112
+ formSchema = formSchema.optional()
113
+ }
114
+
115
+ if (typeof schema.length !== 'number') {
116
+ if (typeof schema.max === 'number') {
117
+ formSchema = formSchema.max(schema.max)
118
+ }
119
+
120
+ if (typeof schema.min === 'number') {
121
+ formSchema = formSchema.min(schema.min)
122
+ }
123
+ } else {
124
+ formSchema = formSchema.length(schema.length)
125
+ }
126
+
127
+ this.formSchema = formSchema.items(formItemSchema)
128
+ this.stateSchema = formSchema
129
+ .items(formItemSchema)
130
+ .default(null)
131
+ .allow(null)
132
+
133
+ this.options = options
134
+ this.schema = schema
135
+ }
136
+
137
+ getFormValueFromState(state: FormSubmissionState) {
138
+ const { name } = this
139
+ return this.getFormValue(state[name])
140
+ }
141
+
142
+ getFormValue(value?: FormStateValue | FormState) {
143
+ return this.isValue(value) ? value : undefined
144
+ }
145
+
146
+ getDisplayStringFromState(state: FormSubmissionState) {
147
+ const files = this.getFormValueFromState(state)
148
+ if (!files?.length) {
149
+ return ''
150
+ }
151
+
152
+ const unit = files.length === 1 ? 'file' : 'files'
153
+ return `Uploaded ${files.length} ${unit}`
154
+ }
155
+
156
+ getContextValueFromState(state: FormSubmissionState) {
157
+ const files = this.getFormValueFromState(state)
158
+ return files?.map(({ status }) => status.form.file.fileId) ?? null
159
+ }
160
+
161
+ getViewModel(
162
+ payload: FormPayload,
163
+ errors?: FormSubmissionError[],
164
+ query: FormQuery = {}
165
+ ) {
166
+ const { options, page } = this
167
+
168
+ // Allow preview URL direct access
169
+ const isForceAccess = 'force' in query
170
+
171
+ const viewModel = super.getViewModel(payload, errors)
172
+ const { attributes, id, value } = viewModel
173
+
174
+ const files = this.getFormValue(value) ?? []
175
+ const filtered = files.filter(
176
+ (file) => file.status.form.file.fileStatus === FileStatus.complete
177
+ )
178
+ const count = filtered.length
179
+
180
+ const rows: SummaryListRow[] = filtered.map((item, index) => {
181
+ const { status } = item
182
+ const { form } = status
183
+ const { file } = form
184
+
185
+ const tag = { classes: 'govuk-tag--green', text: 'Uploaded' }
186
+
187
+ const valueHtml = render
188
+ .view('components/fileuploadfield-value.html', {
189
+ context: { params: { tag } }
190
+ })
191
+ .trim()
192
+
193
+ const keyHtml = render
194
+ .view('components/fileuploadfield-key.html', {
195
+ context: {
196
+ params: {
197
+ name: file.filename,
198
+ errorMessage: errors && file.errorMessage
199
+ }
200
+ }
201
+ })
202
+ .trim()
203
+
204
+ const items: SummaryListAction[] = []
205
+
206
+ // Remove summary list actions from previews
207
+ if (!isForceAccess) {
208
+ const path = `/${item.uploadId}/confirm-delete`
209
+ const href = page?.getHref(`${page.path}${path}`) ?? '#'
210
+
211
+ items.push({
212
+ href,
213
+ text: 'Remove',
214
+ classes: 'govuk-link--no-visited-state',
215
+ attributes: { id: `${id}__${index}` },
216
+ visuallyHiddenText: file.filename
217
+ })
218
+ }
219
+
220
+ return {
221
+ key: {
222
+ html: keyHtml
223
+ },
224
+ value: {
225
+ html: valueHtml
226
+ },
227
+ actions: {
228
+ items
229
+ }
230
+ } satisfies SummaryListRow
231
+ })
232
+
233
+ // Set up the `accept` attribute
234
+ if ('accept' in options) {
235
+ attributes.accept = options.accept
236
+ }
237
+
238
+ const summaryList: SummaryList = {
239
+ classes: 'govuk-summary-list--long-key',
240
+ rows
241
+ }
242
+
243
+ return {
244
+ ...viewModel,
245
+
246
+ // File input can't have a initial value
247
+ value: '',
248
+
249
+ // Override the component name we send to CDP
250
+ name: 'file',
251
+
252
+ upload: {
253
+ count,
254
+ summaryList
255
+ }
256
+ }
257
+ }
258
+
259
+ isValue(value?: FormStateValue | FormState): value is UploadState {
260
+ return isUploadState(value)
261
+ }
262
+ }
@@ -0,0 +1,249 @@
1
+ import { type FormComponentsDef, type Item } from '@defra/forms-model'
2
+
3
+ import { ComponentBase } from '~/src/server/plugins/engine/components/ComponentBase.js'
4
+ import { optionalText } from '~/src/server/plugins/engine/components/constants.js'
5
+ import {
6
+ type FileState,
7
+ type FormPayload,
8
+ type FormState,
9
+ type FormStateValue,
10
+ type FormSubmissionError,
11
+ type FormSubmissionState,
12
+ type FormValue,
13
+ type RepeatItemState,
14
+ type RepeatListState,
15
+ type UploadState
16
+ } from '~/src/server/plugins/engine/types.js'
17
+
18
+ export class FormComponent extends ComponentBase {
19
+ type: FormComponentsDef['type']
20
+ hint: FormComponentsDef['hint']
21
+
22
+ isFormComponent = true
23
+
24
+ constructor(
25
+ def: FormComponentsDef,
26
+ props: ConstructorParameters<typeof ComponentBase>[1]
27
+ ) {
28
+ super(def, props)
29
+
30
+ const { hint, type } = def
31
+
32
+ this.type = type
33
+ this.hint = hint
34
+ }
35
+
36
+ get keys() {
37
+ const { collection, name } = this
38
+
39
+ if (collection) {
40
+ const { fields } = collection
41
+ return [name, ...fields.map(({ name }) => name)]
42
+ }
43
+
44
+ return [name]
45
+ }
46
+
47
+ getFormDataFromState(state: FormSubmissionState): FormPayload {
48
+ const { collection, name } = this
49
+
50
+ if (collection) {
51
+ return collection.getFormDataFromState(state)
52
+ }
53
+
54
+ return {
55
+ [name]: this.getFormValue(state[name])
56
+ }
57
+ }
58
+
59
+ getFormValueFromState(state: FormSubmissionState): FormValue | FormPayload {
60
+ const { collection, name } = this
61
+
62
+ if (collection) {
63
+ return collection.getFormValueFromState(state)
64
+ }
65
+
66
+ return this.getFormValue(state[name])
67
+ }
68
+
69
+ getFormValue(value?: FormStateValue | FormState) {
70
+ return this.isValue(value) ? value : undefined
71
+ }
72
+
73
+ getStateFromValidForm(payload: FormPayload): FormState {
74
+ const { collection, name } = this
75
+
76
+ if (collection) {
77
+ return collection.getStateFromValidForm(payload)
78
+ }
79
+
80
+ return {
81
+ [name]: this.getFormValue(payload[name]) ?? null
82
+ }
83
+ }
84
+
85
+ getErrors(errors?: FormSubmissionError[]): FormSubmissionError[] | undefined {
86
+ const { name } = this
87
+
88
+ // Filter component and child errors only
89
+ const list = errors?.filter(
90
+ (error) =>
91
+ error.name === name ||
92
+ error.path.includes(name) ||
93
+ this.keys.includes(error.name)
94
+ )
95
+
96
+ if (!list?.length) {
97
+ return
98
+ }
99
+
100
+ return list
101
+ }
102
+
103
+ getError(errors?: FormSubmissionError[]): FormSubmissionError | undefined {
104
+ return this.getErrors(errors)?.[0]
105
+ }
106
+
107
+ getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {
108
+ const { hint, name, options = {}, title, viewModel } = this
109
+
110
+ const isRequired = !('required' in options) || options.required !== false
111
+ const hideOptional = 'optionalText' in options && options.optionalText
112
+ const label = `${title}${!isRequired && !hideOptional ? optionalText : ''}`
113
+
114
+ if (hint) {
115
+ viewModel.hint = {
116
+ text: hint
117
+ }
118
+ }
119
+
120
+ // Filter component errors only
121
+ const componentErrors = this.getErrors(errors)
122
+ const componentError = this.getError(componentErrors)
123
+
124
+ if (componentErrors) {
125
+ viewModel.errors = componentErrors
126
+ }
127
+
128
+ if (componentError) {
129
+ viewModel.errorMessage = {
130
+ text: componentError.text
131
+ }
132
+ }
133
+
134
+ return {
135
+ ...viewModel,
136
+ label: {
137
+ text: label
138
+ },
139
+ id: name,
140
+ name,
141
+ value: payload[name]
142
+ }
143
+ }
144
+
145
+ getDisplayStringFromState(state: FormSubmissionState): string {
146
+ const value = this.getFormValueFromState(state)
147
+ return this.isValue(value) ? value.toString() : ''
148
+ }
149
+
150
+ getContextValueFromState(
151
+ state: FormSubmissionState
152
+ ): Item['value'] | Item['value'][] | null {
153
+ const value = this.getFormValueFromState(state)
154
+
155
+ // Filter object field values
156
+ if (this.isState(value)) {
157
+ const values = Object.values(value).filter(isFormValue)
158
+ return values.length ? values : null
159
+ }
160
+
161
+ // Filter array field values
162
+ if (this.isValue(value) && Array.isArray(value)) {
163
+ return value.filter(isFormValue)
164
+ }
165
+
166
+ return this.isValue(value) ? value : null
167
+ }
168
+
169
+ isValue(
170
+ value?: FormStateValue | FormState
171
+ ): value is NonNullable<FormStateValue> {
172
+ return isFormValue(value)
173
+ }
174
+
175
+ isState(value?: FormStateValue | FormState): value is FormState {
176
+ return isFormState(value)
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Check for form value
182
+ */
183
+ export function isFormValue(
184
+ value?: unknown
185
+ ): value is string | number | boolean {
186
+ return (
187
+ (typeof value === 'string' && value.length > 0) ||
188
+ typeof value === 'number' ||
189
+ typeof value === 'boolean'
190
+ )
191
+ }
192
+
193
+ /**
194
+ * Check for form state with nested values
195
+ */
196
+ export function isFormState(value?: unknown): value is FormState {
197
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
198
+ return false
199
+ }
200
+
201
+ // Skip empty objects
202
+ return !!Object.values(value).length
203
+ }
204
+
205
+ /**
206
+ * Check for repeat list state
207
+ */
208
+ export function isRepeatState(value?: unknown): value is RepeatListState {
209
+ if (!Array.isArray(value)) {
210
+ return false
211
+ }
212
+
213
+ // Skip checks when empty
214
+ if (!value.length) {
215
+ return true
216
+ }
217
+
218
+ return value.every(isRepeatValue)
219
+ }
220
+
221
+ /**
222
+ * Check for repeat list value
223
+ */
224
+ export function isRepeatValue(value?: unknown): value is RepeatItemState {
225
+ return isFormState(value) && typeof value.itemId === 'string'
226
+ }
227
+
228
+ /**
229
+ * Check for upload state
230
+ */
231
+ export function isUploadState(value?: unknown): value is UploadState {
232
+ if (!Array.isArray(value)) {
233
+ return false
234
+ }
235
+
236
+ // Skip checks when empty
237
+ if (!value.length) {
238
+ return true
239
+ }
240
+
241
+ return value.every(isUploadValue)
242
+ }
243
+
244
+ /**
245
+ * Check for upload state value
246
+ */
247
+ export function isUploadValue(value?: unknown): value is FileState {
248
+ return isFormState(value) && typeof value.uploadId === 'string'
249
+ }
@@ -0,0 +1,48 @@
1
+ import { ComponentType, type HtmlComponent } from '@defra/forms-model'
2
+
3
+ import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
4
+ import { type Guidance } from '~/src/server/plugins/engine/components/helpers.js'
5
+ import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
6
+ import definition from '~/test/form/definitions/basic.js'
7
+
8
+ describe('HTML', () => {
9
+ let model: FormModel
10
+
11
+ beforeEach(() => {
12
+ model = new FormModel(definition, {
13
+ basePath: 'test'
14
+ })
15
+ })
16
+
17
+ describe('Defaults', () => {
18
+ let def: HtmlComponent
19
+ let collection: ComponentCollection
20
+ let guidance: Guidance
21
+
22
+ beforeEach(() => {
23
+ def = {
24
+ title: 'HTML guidance',
25
+ name: 'myComponent',
26
+ type: ComponentType.Html,
27
+ content: '<p class="govuk-body">\nLorem ipsum dolor sit amet</p>',
28
+ options: {}
29
+ } satisfies HtmlComponent
30
+
31
+ collection = new ComponentCollection([def], { model })
32
+ guidance = collection.guidance[0]
33
+ })
34
+
35
+ describe('View model', () => {
36
+ it('sets Nunjucks component defaults', () => {
37
+ const viewModel = guidance.getViewModel()
38
+
39
+ expect(viewModel).toEqual(
40
+ expect.objectContaining({
41
+ attributes: {},
42
+ content: def.content
43
+ })
44
+ )
45
+ })
46
+ })
47
+ })
48
+ })
@@ -0,0 +1,29 @@
1
+ import { type HtmlComponent } from '@defra/forms-model'
2
+
3
+ import { ComponentBase } from '~/src/server/plugins/engine/components/ComponentBase.js'
4
+
5
+ export class Html extends ComponentBase {
6
+ declare options: HtmlComponent['options']
7
+ content: HtmlComponent['content']
8
+
9
+ constructor(
10
+ def: HtmlComponent,
11
+ props: ConstructorParameters<typeof ComponentBase>[1]
12
+ ) {
13
+ super(def, props)
14
+
15
+ const { content, options } = def
16
+
17
+ this.content = content
18
+ this.options = options
19
+ }
20
+
21
+ getViewModel() {
22
+ const { content, viewModel } = this
23
+
24
+ return {
25
+ ...viewModel,
26
+ content
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,48 @@
1
+ import { ComponentType, type InsetTextComponent } from '@defra/forms-model'
2
+
3
+ import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
4
+ import { type Guidance } from '~/src/server/plugins/engine/components/helpers.js'
5
+ import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
6
+ import definition from '~/test/form/definitions/basic.js'
7
+
8
+ describe('InsetText', () => {
9
+ let model: FormModel
10
+
11
+ beforeEach(() => {
12
+ model = new FormModel(definition, {
13
+ basePath: 'test'
14
+ })
15
+ })
16
+
17
+ describe('Defaults', () => {
18
+ let def: InsetTextComponent
19
+ let collection: ComponentCollection
20
+ let guidance: Guidance
21
+
22
+ beforeEach(() => {
23
+ def = {
24
+ title: 'Inset text guidance',
25
+ name: 'myComponent',
26
+ type: ComponentType.InsetText,
27
+ content: 'Lorem ipsum dolor sit amet',
28
+ options: {}
29
+ } satisfies InsetTextComponent
30
+
31
+ collection = new ComponentCollection([def], { model })
32
+ guidance = collection.guidance[0]
33
+ })
34
+
35
+ describe('View model', () => {
36
+ it('sets Nunjucks component defaults', () => {
37
+ const viewModel = guidance.getViewModel()
38
+
39
+ expect(viewModel).toEqual(
40
+ expect.objectContaining({
41
+ attributes: {},
42
+ content: def.content
43
+ })
44
+ )
45
+ })
46
+ })
47
+ })
48
+ })
@@ -0,0 +1,27 @@
1
+ import { type InsetTextComponent } from '@defra/forms-model'
2
+
3
+ import { ComponentBase } from '~/src/server/plugins/engine/components/ComponentBase.js'
4
+
5
+ export class InsetText extends ComponentBase {
6
+ content: InsetTextComponent['content']
7
+
8
+ constructor(
9
+ def: InsetTextComponent,
10
+ props: ConstructorParameters<typeof ComponentBase>[1]
11
+ ) {
12
+ super(def, props)
13
+
14
+ const { content } = def
15
+
16
+ this.content = content
17
+ }
18
+
19
+ getViewModel() {
20
+ const { content, viewModel } = this
21
+
22
+ return {
23
+ ...viewModel,
24
+ content
25
+ }
26
+ }
27
+ }