@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,443 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ComponentType,
|
|
3
|
+
ConditionsModel,
|
|
4
|
+
ControllerPath,
|
|
5
|
+
ControllerType,
|
|
6
|
+
Engine,
|
|
7
|
+
formDefinitionSchema,
|
|
8
|
+
hasComponents,
|
|
9
|
+
hasRepeater,
|
|
10
|
+
type ComponentDef,
|
|
11
|
+
type ConditionWrapper,
|
|
12
|
+
type ConditionsModelData,
|
|
13
|
+
type DateUnits,
|
|
14
|
+
type FormDefinition,
|
|
15
|
+
type List,
|
|
16
|
+
type Page
|
|
17
|
+
} from '@defra/forms-model'
|
|
18
|
+
import { add } from 'date-fns'
|
|
19
|
+
import { Parser, type Value } from 'expr-eval'
|
|
20
|
+
import joi from 'joi'
|
|
21
|
+
|
|
22
|
+
import { type Component } from '~/src/server/plugins/engine/components/helpers.js'
|
|
23
|
+
import {
|
|
24
|
+
findPage,
|
|
25
|
+
getError,
|
|
26
|
+
getPage
|
|
27
|
+
} from '~/src/server/plugins/engine/helpers.js'
|
|
28
|
+
import { type ExecutableCondition } from '~/src/server/plugins/engine/models/types.js'
|
|
29
|
+
import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
|
|
30
|
+
import {
|
|
31
|
+
createPage,
|
|
32
|
+
type PageControllerClass
|
|
33
|
+
} from '~/src/server/plugins/engine/pageControllers/helpers.js'
|
|
34
|
+
import { validationOptions as opts } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
|
|
35
|
+
import * as defaultServices from '~/src/server/plugins/engine/services/index.js'
|
|
36
|
+
import {
|
|
37
|
+
type FormContext,
|
|
38
|
+
type FormContextRequest,
|
|
39
|
+
type FormState,
|
|
40
|
+
type FormSubmissionError,
|
|
41
|
+
type FormSubmissionState
|
|
42
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
43
|
+
import { FormAction } from '~/src/server/routes/types.js'
|
|
44
|
+
import { merge } from '~/src/server/services/cacheService.js'
|
|
45
|
+
import { type Services } from '~/src/server/types.js'
|
|
46
|
+
|
|
47
|
+
export class FormModel {
|
|
48
|
+
/** The runtime engine that should be used */
|
|
49
|
+
engine?: Engine
|
|
50
|
+
|
|
51
|
+
/** the entire form JSON as an object */
|
|
52
|
+
def: FormDefinition
|
|
53
|
+
|
|
54
|
+
lists: FormDefinition['lists']
|
|
55
|
+
sections: FormDefinition['sections'] = []
|
|
56
|
+
name: string
|
|
57
|
+
values: FormDefinition
|
|
58
|
+
basePath: string
|
|
59
|
+
conditions: Partial<Record<string, ExecutableCondition>>
|
|
60
|
+
pages: PageControllerClass[]
|
|
61
|
+
services: Services
|
|
62
|
+
|
|
63
|
+
controllers?: Record<string, typeof PageController>
|
|
64
|
+
pageDefMap: Map<string, Page>
|
|
65
|
+
listDefMap: Map<string, List>
|
|
66
|
+
componentDefMap: Map<string, ComponentDef>
|
|
67
|
+
pageMap: Map<string, PageControllerClass>
|
|
68
|
+
componentMap: Map<string, Component>
|
|
69
|
+
|
|
70
|
+
constructor(
|
|
71
|
+
def: typeof this.def,
|
|
72
|
+
options: { basePath: string },
|
|
73
|
+
services: Services = defaultServices,
|
|
74
|
+
controllers?: Record<string, typeof PageController>
|
|
75
|
+
) {
|
|
76
|
+
const result = formDefinitionSchema.validate(def, { abortEarly: false })
|
|
77
|
+
|
|
78
|
+
if (result.error) {
|
|
79
|
+
throw result.error
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Make a clone of the shallow copy returned
|
|
83
|
+
// by joi so as not to change the source data.
|
|
84
|
+
def = structuredClone(result.value)
|
|
85
|
+
|
|
86
|
+
// Add default lists
|
|
87
|
+
def.lists.push({
|
|
88
|
+
name: '__yesNo',
|
|
89
|
+
title: 'Yes/No',
|
|
90
|
+
type: 'boolean',
|
|
91
|
+
items: [
|
|
92
|
+
{
|
|
93
|
+
text: 'Yes',
|
|
94
|
+
value: true
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
text: 'No',
|
|
98
|
+
value: false
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
this.engine = def.engine
|
|
104
|
+
this.def = def
|
|
105
|
+
this.lists = def.lists
|
|
106
|
+
this.sections = def.sections
|
|
107
|
+
this.name = def.name ?? ''
|
|
108
|
+
this.values = result.value
|
|
109
|
+
this.basePath = options.basePath
|
|
110
|
+
this.conditions = {}
|
|
111
|
+
this.services = services
|
|
112
|
+
this.controllers = controllers
|
|
113
|
+
|
|
114
|
+
def.conditions.forEach((conditionDef) => {
|
|
115
|
+
const condition = this.makeCondition(conditionDef)
|
|
116
|
+
this.conditions[condition.name] = condition
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
this.pages = def.pages.map((pageDef) => createPage(this, pageDef))
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
!def.pages.some(
|
|
123
|
+
({ controller }) =>
|
|
124
|
+
// Check for user-provided status page (optional)
|
|
125
|
+
controller === ControllerType.Status
|
|
126
|
+
)
|
|
127
|
+
) {
|
|
128
|
+
this.pages.push(
|
|
129
|
+
createPage(this, {
|
|
130
|
+
title: 'Form submitted',
|
|
131
|
+
path: ControllerPath.Status,
|
|
132
|
+
controller: ControllerType.Status
|
|
133
|
+
})
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
this.pageDefMap = new Map(def.pages.map((page) => [page.path, page]))
|
|
138
|
+
this.listDefMap = new Map(def.lists.map((list) => [list.name, list]))
|
|
139
|
+
this.componentDefMap = new Map(
|
|
140
|
+
def.pages
|
|
141
|
+
.filter(hasComponents)
|
|
142
|
+
.flatMap((page) =>
|
|
143
|
+
page.components.map((component) => [component.name, component])
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
this.pageMap = new Map(this.pages.map((page) => [page.path, page]))
|
|
148
|
+
this.componentMap = new Map(
|
|
149
|
+
this.pages.flatMap((page) =>
|
|
150
|
+
page.collection.components.map((component) => [
|
|
151
|
+
component.name,
|
|
152
|
+
component
|
|
153
|
+
])
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* build the entire model schema from individual pages/sections
|
|
160
|
+
*/
|
|
161
|
+
makeSchema() {
|
|
162
|
+
return this.makeFilteredSchema(this.pages)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* build the entire model schema from individual pages/sections and filter out answers
|
|
167
|
+
* for pages which are no longer accessible due to an answer that has been changed
|
|
168
|
+
*/
|
|
169
|
+
makeFilteredSchema(relevantPages: PageControllerClass[]) {
|
|
170
|
+
// Build the entire model schema
|
|
171
|
+
// from the individual pages/sections
|
|
172
|
+
let schema = joi.object<FormSubmissionState>().required()
|
|
173
|
+
|
|
174
|
+
relevantPages.forEach((page) => {
|
|
175
|
+
schema = schema.concat(page.collection.stateSchema)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
return schema
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Instantiates a Condition based on {@link ConditionWrapper}
|
|
183
|
+
* @param condition
|
|
184
|
+
*/
|
|
185
|
+
makeCondition(condition: ConditionWrapper): ExecutableCondition {
|
|
186
|
+
const parser = new Parser({
|
|
187
|
+
operators: {
|
|
188
|
+
logical: true
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
Object.assign(parser.functions, {
|
|
193
|
+
dateForComparison(timePeriod: number, timeUnit: DateUnits) {
|
|
194
|
+
return add(new Date(), { [timeUnit]: timePeriod }).toISOString()
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
const { name, displayName, value } = condition
|
|
199
|
+
const expr = this.toConditionExpression(value, parser)
|
|
200
|
+
|
|
201
|
+
const fn = (evaluationState: FormState) => {
|
|
202
|
+
const ctx = this.toConditionContext(evaluationState, this.conditions)
|
|
203
|
+
try {
|
|
204
|
+
return expr.evaluate(ctx) as boolean
|
|
205
|
+
} catch {
|
|
206
|
+
return false
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
name,
|
|
212
|
+
displayName,
|
|
213
|
+
value,
|
|
214
|
+
expr,
|
|
215
|
+
fn
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
toConditionContext(
|
|
220
|
+
evaluationState: FormState,
|
|
221
|
+
conditions: Partial<Record<string, ExecutableCondition>>
|
|
222
|
+
) {
|
|
223
|
+
const context = { ...evaluationState }
|
|
224
|
+
|
|
225
|
+
for (const key in conditions) {
|
|
226
|
+
Object.defineProperty(context, key, {
|
|
227
|
+
get() {
|
|
228
|
+
return conditions[key]?.fn(evaluationState)
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return context as Extract<Value, Record<string, Value>>
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
toConditionExpression(value: ConditionsModelData, parser: Parser) {
|
|
237
|
+
const conditions = ConditionsModel.from(value)
|
|
238
|
+
return parser.parse(conditions.toExpression())
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getList(name: string): List | undefined {
|
|
242
|
+
return this.lists.find((list) => list.name === name)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Form context for the current page
|
|
247
|
+
*/
|
|
248
|
+
getFormContext(
|
|
249
|
+
request: FormContextRequest,
|
|
250
|
+
state: FormState,
|
|
251
|
+
errors?: FormSubmissionError[]
|
|
252
|
+
): FormContext {
|
|
253
|
+
const { query } = request
|
|
254
|
+
|
|
255
|
+
const page = getPage(this, request)
|
|
256
|
+
|
|
257
|
+
// Determine form paths
|
|
258
|
+
const currentPath = page.path
|
|
259
|
+
const startPath = page.getStartPath()
|
|
260
|
+
|
|
261
|
+
// Preview URL direct access is allowed
|
|
262
|
+
const isForceAccess = 'force' in query
|
|
263
|
+
|
|
264
|
+
let context: FormContext = {
|
|
265
|
+
evaluationState: {},
|
|
266
|
+
relevantState: {},
|
|
267
|
+
relevantPages: [],
|
|
268
|
+
payload: page.getFormDataFromState(request, state),
|
|
269
|
+
state,
|
|
270
|
+
paths: [],
|
|
271
|
+
errors,
|
|
272
|
+
isForceAccess,
|
|
273
|
+
data: {},
|
|
274
|
+
pageDefMap: this.pageDefMap,
|
|
275
|
+
listDefMap: this.listDefMap,
|
|
276
|
+
componentDefMap: this.componentDefMap,
|
|
277
|
+
pageMap: this.pageMap,
|
|
278
|
+
componentMap: this.componentMap
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Validate current page
|
|
282
|
+
context = validateFormPayload(request, page, context)
|
|
283
|
+
|
|
284
|
+
// Find start page
|
|
285
|
+
let nextPage = findPage(this, startPath)
|
|
286
|
+
|
|
287
|
+
this.initialiseContext(context)
|
|
288
|
+
|
|
289
|
+
// Walk form pages from start
|
|
290
|
+
while (nextPage) {
|
|
291
|
+
// Add page to context
|
|
292
|
+
context.relevantPages.push(nextPage)
|
|
293
|
+
|
|
294
|
+
this.assignEvaluationState(context, nextPage)
|
|
295
|
+
|
|
296
|
+
this.assignRelevantState(context, nextPage)
|
|
297
|
+
|
|
298
|
+
// Stop at current page
|
|
299
|
+
if (nextPage.path === currentPath) {
|
|
300
|
+
break
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Apply conditions to determine next page
|
|
304
|
+
nextPage = findPage(this, nextPage.getNextPath(context))
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Validate form state
|
|
308
|
+
context = validateFormState(request, page, context)
|
|
309
|
+
|
|
310
|
+
// Add paths for navigation
|
|
311
|
+
this.assignPaths(context)
|
|
312
|
+
|
|
313
|
+
return context
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private initialiseContext(context: FormContext) {
|
|
317
|
+
// For the V2 engine, we need to initialise `evaluationState` to null
|
|
318
|
+
// for all keys. This is because the current condition evaluation
|
|
319
|
+
// library (eval-expr) will throw if an expression uses a key that is undefined.
|
|
320
|
+
if (this.engine === Engine.V2) {
|
|
321
|
+
for (const page of this.pages) {
|
|
322
|
+
for (const key of page.keys) {
|
|
323
|
+
context.evaluationState[key] = null
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private assignEvaluationState(
|
|
330
|
+
context: FormContext,
|
|
331
|
+
page: PageControllerClass
|
|
332
|
+
) {
|
|
333
|
+
const { collection, pageDef } = page
|
|
334
|
+
// Skip evaluation state for repeater pages
|
|
335
|
+
|
|
336
|
+
if (!hasRepeater(pageDef)) {
|
|
337
|
+
Object.assign(
|
|
338
|
+
context.evaluationState,
|
|
339
|
+
collection.getContextValueFromState(context.state)
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private assignRelevantState(context: FormContext, page: PageControllerClass) {
|
|
345
|
+
// Copy relevant state by expected keys
|
|
346
|
+
for (const key of page.keys) {
|
|
347
|
+
if (typeof context.state[key] !== 'undefined') {
|
|
348
|
+
context.relevantState[key] = context.state[key]
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private assignPaths(context: FormContext) {
|
|
354
|
+
for (const { keys, path } of context.relevantPages) {
|
|
355
|
+
context.paths.push(path)
|
|
356
|
+
|
|
357
|
+
// Stop at page with errors
|
|
358
|
+
if (
|
|
359
|
+
context.errors?.some(({ name, path }) => {
|
|
360
|
+
return keys.includes(name) || keys.some((key) => path.includes(key))
|
|
361
|
+
})
|
|
362
|
+
) {
|
|
363
|
+
break
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Validate current page only
|
|
371
|
+
*/
|
|
372
|
+
function validateFormPayload(
|
|
373
|
+
request: FormContextRequest,
|
|
374
|
+
page: PageControllerClass,
|
|
375
|
+
context: FormContext
|
|
376
|
+
): FormContext {
|
|
377
|
+
const { collection } = page
|
|
378
|
+
const { payload, state } = context
|
|
379
|
+
|
|
380
|
+
const { action } = page.getFormParams(request)
|
|
381
|
+
|
|
382
|
+
// Skip validation GET requests or other actions
|
|
383
|
+
if (!request.payload || action !== FormAction.Validate) {
|
|
384
|
+
return context
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// For checkbox fields missing in the payload (i.e. unchecked),
|
|
388
|
+
// explicitly set their value to undefined so that any previously
|
|
389
|
+
// stored value is cleared and required field validation is enforced.
|
|
390
|
+
const update = { ...request.payload }
|
|
391
|
+
collection.fields.forEach((field) => {
|
|
392
|
+
if (
|
|
393
|
+
field.type === ComponentType.CheckboxesField &&
|
|
394
|
+
!(field.name in update)
|
|
395
|
+
) {
|
|
396
|
+
update[field.name] = undefined
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
const { value, errors } = collection.validate({
|
|
401
|
+
...payload,
|
|
402
|
+
...update
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
// Add sanitised payload (ready to save)
|
|
406
|
+
const formState = page.getStateFromValidForm(request, state, value)
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
...context,
|
|
410
|
+
payload: merge(payload, value),
|
|
411
|
+
state: merge(state, formState),
|
|
412
|
+
errors
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Validate entire form state
|
|
418
|
+
*/
|
|
419
|
+
function validateFormState(
|
|
420
|
+
request: FormContextRequest,
|
|
421
|
+
page: PageControllerClass,
|
|
422
|
+
context: FormContext
|
|
423
|
+
): FormContext {
|
|
424
|
+
const { errors = [], relevantPages, relevantState } = context
|
|
425
|
+
|
|
426
|
+
// Exclude current page
|
|
427
|
+
const previousPages = relevantPages.filter(
|
|
428
|
+
(relevantPage) => relevantPage !== page
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
// Validate relevant state
|
|
432
|
+
const { error } = page.model
|
|
433
|
+
.makeFilteredSchema(previousPages)
|
|
434
|
+
.validate(relevantState, { ...opts, stripUnknown: true })
|
|
435
|
+
|
|
436
|
+
// Add relevant state errors
|
|
437
|
+
if (error) {
|
|
438
|
+
const errorsState = error.details.map(getError)
|
|
439
|
+
return { ...context, errors: errors.concat(errorsState) }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return context
|
|
443
|
+
}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FormModel,
|
|
3
|
+
SummaryViewModel
|
|
4
|
+
} from '~/src/server/plugins/engine/models/index.js'
|
|
5
|
+
import {
|
|
6
|
+
createPage,
|
|
7
|
+
type PageControllerClass
|
|
8
|
+
} from '~/src/server/plugins/engine/pageControllers/helpers.js'
|
|
9
|
+
import {
|
|
10
|
+
type FormContext,
|
|
11
|
+
type FormContextRequest,
|
|
12
|
+
type FormState
|
|
13
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
14
|
+
import definition from '~/test/form/definitions/repeat-mixed.js'
|
|
15
|
+
|
|
16
|
+
const basePath = '/test'
|
|
17
|
+
|
|
18
|
+
describe('SummaryViewModel', () => {
|
|
19
|
+
const itemId1 = 'abc-123'
|
|
20
|
+
const itemId2 = 'xyz-987'
|
|
21
|
+
|
|
22
|
+
let model: FormModel
|
|
23
|
+
let page: PageControllerClass
|
|
24
|
+
let pageUrl: URL
|
|
25
|
+
let request: FormContextRequest
|
|
26
|
+
let context: FormContext
|
|
27
|
+
let summaryViewModel: SummaryViewModel
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
model = new FormModel(definition, {
|
|
31
|
+
basePath: 'test'
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
page = createPage(model, definition.pages[2])
|
|
35
|
+
pageUrl = new URL('http://example.com/repeat/pizza-order/summary')
|
|
36
|
+
|
|
37
|
+
request = {
|
|
38
|
+
method: 'get',
|
|
39
|
+
url: pageUrl,
|
|
40
|
+
path: pageUrl.pathname,
|
|
41
|
+
params: {
|
|
42
|
+
path: 'pizza-order',
|
|
43
|
+
slug: 'repeat'
|
|
44
|
+
},
|
|
45
|
+
query: {},
|
|
46
|
+
app: { model }
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe.each([
|
|
51
|
+
{
|
|
52
|
+
description: '0 items',
|
|
53
|
+
state: {
|
|
54
|
+
orderType: 'collection',
|
|
55
|
+
pizza: []
|
|
56
|
+
} satisfies FormState,
|
|
57
|
+
keys: ['How would you like to receive your pizza?', 'Pizzas'],
|
|
58
|
+
values: ['Collection', 'Not supplied']
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
description: '1 item',
|
|
62
|
+
state: {
|
|
63
|
+
orderType: 'delivery',
|
|
64
|
+
pizza: [
|
|
65
|
+
{
|
|
66
|
+
toppings: 'Ham',
|
|
67
|
+
quantity: 2,
|
|
68
|
+
itemId: itemId1
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
} satisfies FormState,
|
|
72
|
+
keys: ['How would you like to receive your pizza?', 'Pizza added'],
|
|
73
|
+
values: ['Delivery', 'You added 1 Pizza']
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
description: '2 items',
|
|
77
|
+
state: {
|
|
78
|
+
orderType: 'delivery',
|
|
79
|
+
pizza: [
|
|
80
|
+
{
|
|
81
|
+
toppings: 'Ham',
|
|
82
|
+
quantity: 2,
|
|
83
|
+
itemId: itemId1
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
toppings: 'Pepperoni',
|
|
87
|
+
quantity: 1,
|
|
88
|
+
itemId: itemId2
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
} satisfies FormState,
|
|
92
|
+
keys: ['How would you like to receive your pizza?', 'Pizzas added'],
|
|
93
|
+
values: ['Delivery', 'You added 2 Pizzas']
|
|
94
|
+
}
|
|
95
|
+
])('Check answers ($description)', ({ state, keys, values }) => {
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
context = model.getFormContext(request, state)
|
|
98
|
+
summaryViewModel = new SummaryViewModel(request, page, context)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should add title for each section', () => {
|
|
102
|
+
const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
|
|
103
|
+
|
|
104
|
+
// 1st summary list has no title
|
|
105
|
+
expect(checkAnswers1).toHaveProperty('title', undefined)
|
|
106
|
+
|
|
107
|
+
// 2nd summary list has section title
|
|
108
|
+
expect(checkAnswers2).toHaveProperty('title', {
|
|
109
|
+
text: 'Food'
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should add summary list for each section', () => {
|
|
114
|
+
expect(summaryViewModel.checkAnswers).toHaveLength(2)
|
|
115
|
+
|
|
116
|
+
const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
|
|
117
|
+
|
|
118
|
+
const { summaryList: summaryList1 } = checkAnswers1
|
|
119
|
+
const { summaryList: summaryList2 } = checkAnswers2
|
|
120
|
+
|
|
121
|
+
expect(summaryList1).toHaveProperty('rows', [
|
|
122
|
+
{
|
|
123
|
+
key: {
|
|
124
|
+
text: keys[0]
|
|
125
|
+
},
|
|
126
|
+
value: {
|
|
127
|
+
classes: 'app-prose-scope',
|
|
128
|
+
html: values[0]
|
|
129
|
+
},
|
|
130
|
+
actions: {
|
|
131
|
+
items: [
|
|
132
|
+
{
|
|
133
|
+
classes: 'govuk-link--no-visited-state',
|
|
134
|
+
href: `${basePath}/delivery-or-collection?returnUrl=${encodeURIComponent(`${basePath}/summary`)}`,
|
|
135
|
+
text: 'Change',
|
|
136
|
+
visuallyHiddenText: keys[0]
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
])
|
|
142
|
+
|
|
143
|
+
expect(summaryList2).toHaveProperty('rows', [
|
|
144
|
+
{
|
|
145
|
+
key: {
|
|
146
|
+
text: keys[1]
|
|
147
|
+
},
|
|
148
|
+
value: {
|
|
149
|
+
classes: 'app-prose-scope',
|
|
150
|
+
html: values[1]
|
|
151
|
+
},
|
|
152
|
+
actions: {
|
|
153
|
+
items: [
|
|
154
|
+
{
|
|
155
|
+
classes: 'govuk-link--no-visited-state',
|
|
156
|
+
href: `${basePath}/pizza-order/summary?returnUrl=${encodeURIComponent(`${basePath}/summary`)}`,
|
|
157
|
+
text: 'Change',
|
|
158
|
+
visuallyHiddenText: 'Pizza'
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
])
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should add summary list for each section (preview URL direct access)', () => {
|
|
167
|
+
request.query.force = '' // Preview URL '?force'
|
|
168
|
+
context = model.getFormContext(request, state)
|
|
169
|
+
summaryViewModel = new SummaryViewModel(request, page, context)
|
|
170
|
+
|
|
171
|
+
expect(summaryViewModel.checkAnswers).toHaveLength(2)
|
|
172
|
+
|
|
173
|
+
const [checkAnswers1, checkAnswers2] = summaryViewModel.checkAnswers
|
|
174
|
+
|
|
175
|
+
const { summaryList: summaryList1 } = checkAnswers1
|
|
176
|
+
const { summaryList: summaryList2 } = checkAnswers2
|
|
177
|
+
|
|
178
|
+
expect(summaryList1).toHaveProperty('rows', [
|
|
179
|
+
{
|
|
180
|
+
key: {
|
|
181
|
+
text: keys[0]
|
|
182
|
+
},
|
|
183
|
+
value: {
|
|
184
|
+
classes: 'app-prose-scope',
|
|
185
|
+
html: values[0]
|
|
186
|
+
},
|
|
187
|
+
actions: {
|
|
188
|
+
items: []
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
])
|
|
192
|
+
|
|
193
|
+
expect(summaryList2).toHaveProperty('rows', [
|
|
194
|
+
{
|
|
195
|
+
key: {
|
|
196
|
+
text: keys[1]
|
|
197
|
+
},
|
|
198
|
+
value: {
|
|
199
|
+
classes: 'app-prose-scope',
|
|
200
|
+
html: values[1]
|
|
201
|
+
},
|
|
202
|
+
actions: {
|
|
203
|
+
items: []
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
])
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
})
|