@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.
- package/package.json +3 -2
- package/src/client/javascripts/application.js +87 -0
- package/src/client/javascripts/file-upload.js +386 -0
- package/src/client/stylesheets/_code.scss +33 -0
- package/src/client/stylesheets/_govuk-frontend.scss +4 -0
- package/src/client/stylesheets/_prose.scss +56 -0
- package/src/client/stylesheets/_service-banner.scss +24 -0
- package/src/client/stylesheets/_summary-list.scss +28 -0
- package/src/client/stylesheets/_tag-env.scss +24 -0
- package/src/client/stylesheets/application.scss +14 -0
- package/src/common/cookies.js +58 -0
- package/src/common/cookies.test.js +23 -0
- package/src/common/types.js +5 -0
- package/src/config/index.ts +271 -0
- package/src/index.ts +31 -0
- package/src/server/common/helpers/logging/logger-options.test.ts +50 -0
- package/src/server/common/helpers/logging/logger-options.ts +46 -0
- package/src/server/common/helpers/logging/logger.ts +7 -0
- package/src/server/common/helpers/logging/request-logger.ts +9 -0
- package/src/server/common/helpers/logging/request-tracing.js +10 -0
- package/src/server/common/helpers/redis-client.js +70 -0
- package/src/server/constants.js +1 -0
- package/src/server/forms/README.md +10 -0
- package/src/server/forms/components.json +1015 -0
- package/src/server/forms/report-a-terrorist.json +270 -0
- package/src/server/forms/runner-components-test.json +365 -0
- package/src/server/forms/test.json +581 -0
- package/src/server/index.test.ts +582 -0
- package/src/server/index.ts +140 -0
- package/src/server/plugins/blankie.test.ts +73 -0
- package/src/server/plugins/blankie.ts +48 -0
- package/src/server/plugins/crumb.ts +20 -0
- package/src/server/plugins/engine/README.md +87 -0
- package/src/server/plugins/engine/components/AutocompleteField.test.ts +294 -0
- package/src/server/plugins/engine/components/AutocompleteField.ts +49 -0
- package/src/server/plugins/engine/components/CheckboxesField.test.ts +379 -0
- package/src/server/plugins/engine/components/CheckboxesField.ts +106 -0
- package/src/server/plugins/engine/components/ComponentBase.ts +97 -0
- package/src/server/plugins/engine/components/ComponentCollection.ts +278 -0
- package/src/server/plugins/engine/components/DatePartsField.test.ts +822 -0
- package/src/server/plugins/engine/components/DatePartsField.ts +264 -0
- package/src/server/plugins/engine/components/Details.test.ts +49 -0
- package/src/server/plugins/engine/components/Details.ts +30 -0
- package/src/server/plugins/engine/components/EmailAddressField.test.ts +395 -0
- package/src/server/plugins/engine/components/EmailAddressField.ts +55 -0
- package/src/server/plugins/engine/components/FileUploadField.test.ts +778 -0
- package/src/server/plugins/engine/components/FileUploadField.ts +262 -0
- package/src/server/plugins/engine/components/FormComponent.ts +249 -0
- package/src/server/plugins/engine/components/Html.test.ts +48 -0
- package/src/server/plugins/engine/components/Html.ts +29 -0
- package/src/server/plugins/engine/components/InsetText.test.ts +48 -0
- package/src/server/plugins/engine/components/InsetText.ts +27 -0
- package/src/server/plugins/engine/components/List.test.ts +76 -0
- package/src/server/plugins/engine/components/List.ts +72 -0
- package/src/server/plugins/engine/components/ListFormComponent.ts +140 -0
- package/src/server/plugins/engine/components/MonthYearField.test.ts +567 -0
- package/src/server/plugins/engine/components/MonthYearField.ts +222 -0
- package/src/server/plugins/engine/components/MultilineTextField.test.ts +558 -0
- package/src/server/plugins/engine/components/MultilineTextField.ts +138 -0
- package/src/server/plugins/engine/components/NumberField.test.ts +701 -0
- package/src/server/plugins/engine/components/NumberField.ts +163 -0
- package/src/server/plugins/engine/components/RadiosField.test.ts +288 -0
- package/src/server/plugins/engine/components/RadiosField.ts +24 -0
- package/src/server/plugins/engine/components/SelectField.test.ts +288 -0
- package/src/server/plugins/engine/components/SelectField.ts +47 -0
- package/src/server/plugins/engine/components/SelectionControlField.ts +43 -0
- package/src/server/plugins/engine/components/TelephoneNumberField.test.ts +356 -0
- package/src/server/plugins/engine/components/TelephoneNumberField.ts +67 -0
- package/src/server/plugins/engine/components/TextField.test.ts +489 -0
- package/src/server/plugins/engine/components/TextField.ts +96 -0
- package/src/server/plugins/engine/components/UkAddressField.test.ts +623 -0
- package/src/server/plugins/engine/components/UkAddressField.ts +172 -0
- package/src/server/plugins/engine/components/YesNoField.test.ts +248 -0
- package/src/server/plugins/engine/components/YesNoField.ts +31 -0
- package/src/server/plugins/engine/components/constants.ts +1 -0
- package/src/server/plugins/engine/components/helpers.ts +330 -0
- package/src/server/plugins/engine/components/index.ts +24 -0
- package/src/server/plugins/engine/components/types.ts +117 -0
- package/src/server/plugins/engine/configureEnginePlugin.ts +47 -0
- package/src/server/plugins/engine/helpers.test.ts +791 -0
- package/src/server/plugins/engine/helpers.ts +379 -0
- package/src/server/plugins/engine/index.ts +7 -0
- package/src/server/plugins/engine/models/FormModel.test.ts +42 -0
- package/src/server/plugins/engine/models/FormModel.ts +443 -0
- package/src/server/plugins/engine/models/RepeatingSummaryViewModel.ts +0 -0
- package/src/server/plugins/engine/models/Section.ts +0 -0
- package/src/server/plugins/engine/models/SummaryViewModel.test.ts +209 -0
- package/src/server/plugins/engine/models/SummaryViewModel.ts +220 -0
- package/src/server/plugins/engine/models/index.ts +2 -0
- package/src/server/plugins/engine/models/types.ts +114 -0
- package/src/server/plugins/engine/outputFormatters/human/v1.test.ts +143 -0
- package/src/server/plugins/engine/outputFormatters/human/v1.ts +73 -0
- package/src/server/plugins/engine/outputFormatters/index.test.ts +17 -0
- package/src/server/plugins/engine/outputFormatters/index.ts +44 -0
- package/src/server/plugins/engine/outputFormatters/machine/v1.test.ts +229 -0
- package/src/server/plugins/engine/outputFormatters/machine/v1.ts +140 -0
- package/src/server/plugins/engine/outputFormatters/machine/v2.test.ts +229 -0
- package/src/server/plugins/engine/outputFormatters/machine/v2.ts +153 -0
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +1108 -0
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +446 -0
- package/src/server/plugins/engine/pageControllers/PageController.test.ts +205 -0
- package/src/server/plugins/engine/pageControllers/PageController.ts +176 -0
- package/src/server/plugins/engine/pageControllers/QuestionPageController.test.ts +1264 -0
- package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +561 -0
- package/src/server/plugins/engine/pageControllers/README.md +28 -0
- package/src/server/plugins/engine/pageControllers/RepeatPageController.test.ts +264 -0
- package/src/server/plugins/engine/pageControllers/RepeatPageController.ts +458 -0
- package/src/server/plugins/engine/pageControllers/StartPageController.ts +18 -0
- package/src/server/plugins/engine/pageControllers/StatusPageController.ts +50 -0
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +261 -0
- package/src/server/plugins/engine/pageControllers/TerminalController.test.ts +28 -0
- package/src/server/plugins/engine/pageControllers/TerminalPageController.ts +19 -0
- package/src/server/plugins/engine/pageControllers/helpers.test.ts +198 -0
- package/src/server/plugins/engine/pageControllers/helpers.ts +101 -0
- package/src/server/plugins/engine/pageControllers/index.ts +10 -0
- package/src/server/plugins/engine/pageControllers/validationOptions.ts +89 -0
- package/src/server/plugins/engine/plugin.ts +673 -0
- package/src/server/plugins/engine/services/formSubmissionService.js +46 -0
- package/src/server/plugins/engine/services/formsService.js +46 -0
- package/src/server/plugins/engine/services/formsService.test.js +90 -0
- package/src/server/plugins/engine/services/index.js +3 -0
- package/src/server/plugins/engine/services/notifyService.test.ts +132 -0
- package/src/server/plugins/engine/services/notifyService.ts +64 -0
- package/src/server/plugins/engine/services/uploadService.js +60 -0
- package/src/server/plugins/engine/types.ts +315 -0
- package/src/server/plugins/engine/views/components/autocompletefield.html +5 -0
- package/src/server/plugins/engine/views/components/checkboxesfield.html +5 -0
- package/src/server/plugins/engine/views/components/datepartsfield.html +5 -0
- package/src/server/plugins/engine/views/components/details.html +6 -0
- package/src/server/plugins/engine/views/components/emailaddressfield.html +5 -0
- package/src/server/plugins/engine/views/components/fileuploadfield-key.html +8 -0
- package/src/server/plugins/engine/views/components/fileuploadfield-value.html +3 -0
- package/src/server/plugins/engine/views/components/fileuploadfield.html +24 -0
- package/src/server/plugins/engine/views/components/html.html +3 -0
- package/src/server/plugins/engine/views/components/insettext.html +7 -0
- package/src/server/plugins/engine/views/components/list.html +36 -0
- package/src/server/plugins/engine/views/components/monthyearfield.html +5 -0
- package/src/server/plugins/engine/views/components/multilinetextfield.html +10 -0
- package/src/server/plugins/engine/views/components/numberfield.html +5 -0
- package/src/server/plugins/engine/views/components/radiosfield.html +5 -0
- package/src/server/plugins/engine/views/components/selectfield.html +5 -0
- package/src/server/plugins/engine/views/components/telephonenumberfield.html +5 -0
- package/src/server/plugins/engine/views/components/textfield.html +5 -0
- package/src/server/plugins/engine/views/components/ukaddressfield.html +25 -0
- package/src/server/plugins/engine/views/components/yesnofield.html +5 -0
- package/src/server/plugins/engine/views/file-upload.html +45 -0
- package/src/server/plugins/engine/views/index.html +39 -0
- package/src/server/plugins/engine/views/item-delete.html +56 -0
- package/src/server/plugins/engine/views/partials/components.html +6 -0
- package/src/server/plugins/engine/views/partials/conditional-components.html +3 -0
- package/src/server/plugins/engine/views/partials/debug.html +44 -0
- package/src/server/plugins/engine/views/partials/form.html +15 -0
- package/src/server/plugins/engine/views/partials/heading.html +16 -0
- package/src/server/plugins/engine/views/partials/preview-banner.html +32 -0
- package/src/server/plugins/engine/views/partials/preview-banner.test.js +122 -0
- package/src/server/plugins/engine/views/partials/warn-missing-notification-email.html +10 -0
- package/src/server/plugins/engine/views/repeat-list-summary.html +53 -0
- package/src/server/plugins/errorPages.ts +58 -0
- package/src/server/plugins/nunjucks/context.js +88 -0
- package/src/server/plugins/nunjucks/context.test.js +142 -0
- package/src/server/plugins/nunjucks/enviroment.test.js +201 -0
- package/src/server/plugins/nunjucks/environment.js +116 -0
- package/src/server/plugins/nunjucks/filters/answer.js +27 -0
- package/src/server/plugins/nunjucks/filters/answer.test.js +89 -0
- package/src/server/plugins/nunjucks/filters/evaluate.js +21 -0
- package/src/server/plugins/nunjucks/filters/field.js +28 -0
- package/src/server/plugins/nunjucks/filters/field.test.js +75 -0
- package/src/server/plugins/nunjucks/filters/highlight.js +11 -0
- package/src/server/plugins/nunjucks/filters/href.js +30 -0
- package/src/server/plugins/nunjucks/filters/href.test.js +80 -0
- package/src/server/plugins/nunjucks/filters/index.js +8 -0
- package/src/server/plugins/nunjucks/filters/inspect.js +15 -0
- package/src/server/plugins/nunjucks/filters/page.js +24 -0
- package/src/server/plugins/nunjucks/filters/page.test.js +65 -0
- package/src/server/plugins/nunjucks/index.js +3 -0
- package/src/server/plugins/nunjucks/plugin.js +40 -0
- package/src/server/plugins/nunjucks/render.js +42 -0
- package/src/server/plugins/nunjucks/types.js +40 -0
- package/src/server/plugins/pulse.ts +11 -0
- package/src/server/plugins/router.ts +201 -0
- package/src/server/plugins/session.ts +28 -0
- package/src/server/routes/health.js +13 -0
- package/src/server/routes/health.test.js +35 -0
- package/src/server/routes/index.test.ts +125 -0
- package/src/server/routes/index.ts +2 -0
- package/src/server/routes/public.ts +47 -0
- package/src/server/routes/types.ts +48 -0
- package/src/server/schemas/index.ts +34 -0
- package/src/server/secure-context.js +43 -0
- package/src/server/services/cacheService.test.ts +276 -0
- package/src/server/services/cacheService.ts +131 -0
- package/src/server/services/httpService.test.js +491 -0
- package/src/server/services/httpService.ts +50 -0
- package/src/server/services/index.ts +1 -0
- package/src/server/types.ts +54 -0
- package/src/server/utils/notify.test.ts +37 -0
- package/src/server/utils/notify.ts +50 -0
- package/src/server/utils/secure-context/get-trust-store-certs.js +11 -0
- package/src/server/utils/secure-context/get-trust-store-certs.test.js +19 -0
- package/src/server/utils/utils.js +24 -0
- package/src/server/utils/utils.test.js +54 -0
- package/src/server/views/404.html +16 -0
- package/src/server/views/500.html +19 -0
- package/src/server/views/components/debug/macro.njk +3 -0
- package/src/server/views/components/debug/template.njk +13 -0
- package/src/server/views/components/service-banner/macro.njk +3 -0
- package/src/server/views/components/service-banner/template.njk +20 -0
- package/src/server/views/components/service-banner/template.test.js +43 -0
- package/src/server/views/components/tag-env/macro.njk +3 -0
- package/src/server/views/components/tag-env/template.njk +30 -0
- package/src/server/views/components/tag-env/template.test.js +66 -0
- package/src/server/views/confirmation.html +19 -0
- package/src/server/views/help/accessibility-statement.html +58 -0
- package/src/server/views/help/cookie-preferences.html +57 -0
- package/src/server/views/help/cookies.html +71 -0
- package/src/server/views/help/get-support.html +37 -0
- package/src/server/views/help/privacy-notice.html +68 -0
- package/src/server/views/help/terms-and-conditions.html +83 -0
- package/src/server/views/layout.html +199 -0
- package/src/server/views/summary.html +50 -0
- package/src/typings/hapi/index.d.ts +95 -0
- package/src/typings/hapi-tracing/index.d.ts +6 -0
- package/src/typings/index.d.ts +3 -0
- 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
|
+
}
|