@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.
- package/.server/server/index.js +0 -4
- package/.server/server/index.js.map +1 -1
- package/.server/server/plugins/engine/helpers.js +3 -0
- package/.server/server/plugins/engine/helpers.js.map +1 -1
- package/.server/server/plugins/engine/index.js +27 -1
- package/.server/server/plugins/engine/index.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +2 -4
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js +4 -10
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/StatusPageController.js +2 -3
- package/.server/server/plugins/engine/pageControllers/StatusPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +2 -4
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
- package/.server/server/plugins/engine/plugin.js +65 -6
- package/.server/server/plugins/engine/plugin.js.map +1 -1
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/{views → plugins/engine/views}/components/service-banner/template.test.js +1 -1
- package/.server/server/plugins/engine/views/components/service-banner/template.test.js.map +1 -0
- package/.server/server/{views → plugins/engine/views}/components/tag-env/template.test.js +1 -1
- package/.server/server/plugins/engine/views/components/tag-env/template.test.js.map +1 -0
- package/.server/server/services/cacheService.js +5 -2
- package/.server/server/services/cacheService.js.map +1 -1
- package/.server/typings/hapi/index.d.js.map +1 -1
- package/README.md +215 -4
- package/package.json +3 -3
- 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 +135 -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 +384 -0
- package/src/server/plugins/engine/index.ts +47 -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 +1116 -0
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +447 -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 +565 -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 +51 -0
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +262 -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 +753 -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 +317 -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/debug/macro.njk +3 -0
- package/src/server/plugins/engine/views/components/debug/template.njk +13 -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/service-banner/macro.njk +3 -0
- package/src/server/plugins/engine/views/components/service-banner/template.njk +20 -0
- package/src/server/plugins/engine/views/components/service-banner/template.test.js +43 -0
- package/src/server/plugins/engine/views/components/tag-env/macro.njk +3 -0
- package/src/server/plugins/engine/views/components/tag-env/template.njk +30 -0
- package/src/server/plugins/engine/views/components/tag-env/template.test.js +66 -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/confirmation.html +19 -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/layout.html +199 -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/engine/views/summary.html +50 -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 +277 -0
- package/src/server/services/cacheService.ts +138 -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/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/typings/hapi/index.d.ts +87 -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
- package/.server/server/views/components/service-banner/template.test.js.map +0 -1
- package/.server/server/views/components/tag-env/template.test.js.map +0 -1
- /package/.server/server/{views → plugins/engine/views}/components/debug/macro.njk +0 -0
- /package/.server/server/{views → plugins/engine/views}/components/debug/template.njk +0 -0
- /package/.server/server/{views → plugins/engine/views}/components/service-banner/macro.njk +0 -0
- /package/.server/server/{views → plugins/engine/views}/components/service-banner/template.njk +0 -0
- /package/.server/server/{views → plugins/engine/views}/components/tag-env/macro.njk +0 -0
- /package/.server/server/{views → plugins/engine/views}/components/tag-env/template.njk +0 -0
- /package/.server/server/{views → plugins/engine/views}/confirmation.html +0 -0
- /package/.server/server/{views → plugins/engine/views}/layout.html +0 -0
- /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`)
|