@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,46 @@
|
|
|
1
|
+
import { formMetadataSchema } from '@defra/forms-model'
|
|
2
|
+
|
|
3
|
+
import { config } from '~/src/config/index.js'
|
|
4
|
+
import { FormStatus } from '~/src/server/routes/types.js'
|
|
5
|
+
import { getJson } from '~/src/server/services/httpService.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Retrieves a form definition from the form manager for a given slug
|
|
9
|
+
* @param {string} slug - the slug of the form
|
|
10
|
+
*/
|
|
11
|
+
export async function getFormMetadata(slug) {
|
|
12
|
+
const getJsonByType = /** @type {typeof getJson<FormMetadata>} */ (getJson)
|
|
13
|
+
|
|
14
|
+
const { payload: metadata } = await getJsonByType(
|
|
15
|
+
`${config.get('managerUrl')}/forms/slug/${slug}`
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// Run it through the schema to coerce dates
|
|
19
|
+
const result = formMetadataSchema.validate(metadata)
|
|
20
|
+
|
|
21
|
+
if (result.error) {
|
|
22
|
+
throw result.error
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return result.value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Retrieves a form definition from the form manager for a given id
|
|
30
|
+
* @param {string} id - the id of the form
|
|
31
|
+
* @param {FormStatus} state - the state of the form
|
|
32
|
+
*/
|
|
33
|
+
export async function getFormDefinition(id, state) {
|
|
34
|
+
const getJsonByType = /** @type {typeof getJson<FormDefinition>} */ (getJson)
|
|
35
|
+
|
|
36
|
+
const suffix = state === FormStatus.Draft ? `/${state}` : ''
|
|
37
|
+
const { payload: definition } = await getJsonByType(
|
|
38
|
+
`${config.get('managerUrl')}/forms/${id}/definition${suffix}`
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return definition
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @import { FormDefinition, FormMetadata } from '@defra/forms-model'
|
|
46
|
+
*/
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { StatusCodes } from 'http-status-codes'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getFormDefinition,
|
|
5
|
+
getFormMetadata
|
|
6
|
+
} from '~/src/server/plugins/engine/services/formsService.js'
|
|
7
|
+
import { FormStatus } from '~/src/server/routes/types.js'
|
|
8
|
+
import { getJson } from '~/src/server/services/httpService.js'
|
|
9
|
+
import * as fixtures from '~/test/fixtures/index.js'
|
|
10
|
+
|
|
11
|
+
const { MANAGER_URL } = process.env
|
|
12
|
+
|
|
13
|
+
jest.mock('~/src/server/services/httpService')
|
|
14
|
+
|
|
15
|
+
describe('Forms service', () => {
|
|
16
|
+
const { definition, metadata } = fixtures.form
|
|
17
|
+
|
|
18
|
+
describe('getFormMetadata', () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.mocked(getJson).mockResolvedValue({
|
|
21
|
+
res: /** @type {IncomingMessage} */ ({
|
|
22
|
+
statusCode: StatusCodes.OK
|
|
23
|
+
}),
|
|
24
|
+
payload: metadata
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('requests JSON via form slug', async () => {
|
|
29
|
+
await getFormMetadata(metadata.slug)
|
|
30
|
+
|
|
31
|
+
expect(getJson).toHaveBeenCalledWith(
|
|
32
|
+
`${MANAGER_URL}/forms/slug/${metadata.slug}`
|
|
33
|
+
)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('coerces timestamps from string to Date', async () => {
|
|
37
|
+
const payload = {
|
|
38
|
+
...structuredClone(metadata),
|
|
39
|
+
|
|
40
|
+
// JSON payload uses string dates in transit
|
|
41
|
+
createdAt: metadata.createdAt.toISOString(),
|
|
42
|
+
updatedAt: metadata.updatedAt.toISOString()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
jest.mocked(getJson).mockResolvedValue({
|
|
46
|
+
res: /** @type {IncomingMessage} */ ({
|
|
47
|
+
statusCode: StatusCodes.OK
|
|
48
|
+
}),
|
|
49
|
+
payload
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
await expect(getFormMetadata(metadata.slug)).resolves.toEqual({
|
|
53
|
+
...metadata,
|
|
54
|
+
createdAt: expect.any(Date),
|
|
55
|
+
updatedAt: expect.any(Date)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('getFormDefinition', () => {
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
jest.mocked(getJson).mockResolvedValue({
|
|
63
|
+
res: /** @type {IncomingMessage} */ ({
|
|
64
|
+
statusCode: StatusCodes.OK
|
|
65
|
+
}),
|
|
66
|
+
payload: definition
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('requests JSON via form ID (draft)', async () => {
|
|
71
|
+
await getFormDefinition(metadata.id, FormStatus.Draft)
|
|
72
|
+
|
|
73
|
+
expect(getJson).toHaveBeenCalledWith(
|
|
74
|
+
`${MANAGER_URL}/forms/${metadata.id}/definition/draft`
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('requests JSON via form ID (live)', async () => {
|
|
79
|
+
await getFormDefinition(metadata.id, FormStatus.Live)
|
|
80
|
+
|
|
81
|
+
expect(getJson).toHaveBeenCalledWith(
|
|
82
|
+
`${MANAGER_URL}/forms/${metadata.id}/definition`
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @import { IncomingMessage } from 'node:http'
|
|
90
|
+
*/
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export * as formsService from '~/src/server/plugins/engine/services/formsService.js'
|
|
2
|
+
export * as formSubmissionService from '~/src/server/plugins/engine/services/formSubmissionService.js'
|
|
3
|
+
export * as outputService from '~/src/server/plugins/engine/services/notifyService.js'
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { checkFormStatus } from '~/src/server/plugins/engine/helpers.js'
|
|
2
|
+
import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
|
|
3
|
+
import { type DetailItem } from '~/src/server/plugins/engine/models/types.js'
|
|
4
|
+
import { getFormatter } from '~/src/server/plugins/engine/outputFormatters/index.js'
|
|
5
|
+
import { submit } from '~/src/server/plugins/engine/services/notifyService.js'
|
|
6
|
+
import {
|
|
7
|
+
FormStatus,
|
|
8
|
+
type FormRequestPayload
|
|
9
|
+
} from '~/src/server/routes/types.js'
|
|
10
|
+
import { sendNotification } from '~/src/server/utils/notify.js'
|
|
11
|
+
|
|
12
|
+
jest.mock('~/src/server/utils/notify')
|
|
13
|
+
jest.mock('~/src/server/plugins/engine/helpers')
|
|
14
|
+
jest.mock('~/src/server/plugins/engine/outputFormatters/index')
|
|
15
|
+
|
|
16
|
+
describe('notifyService', () => {
|
|
17
|
+
const submitResponse = {
|
|
18
|
+
message: 'Submit completed',
|
|
19
|
+
result: {
|
|
20
|
+
files: {
|
|
21
|
+
main: '00000000-0000-0000-0000-000000000000',
|
|
22
|
+
repeaters: {
|
|
23
|
+
pizza: '11111111-1111-1111-1111-111111111111'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const items: DetailItem[] = []
|
|
30
|
+
|
|
31
|
+
const mockRequest: FormRequestPayload = jest.mocked<FormRequestPayload>({
|
|
32
|
+
path: 'test',
|
|
33
|
+
logger: {
|
|
34
|
+
info: jest.fn()
|
|
35
|
+
}
|
|
36
|
+
} as unknown as FormRequestPayload)
|
|
37
|
+
let model: FormModel
|
|
38
|
+
const sendNotificationMock = jest.mocked(sendNotification)
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
jest.resetAllMocks()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('creates a subject line for real forms', async () => {
|
|
45
|
+
model = {
|
|
46
|
+
name: 'foobar',
|
|
47
|
+
def: {
|
|
48
|
+
output: {
|
|
49
|
+
audience: 'human',
|
|
50
|
+
version: '1'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} as FormModel
|
|
54
|
+
|
|
55
|
+
jest.mocked(checkFormStatus).mockReturnValue({
|
|
56
|
+
isPreview: false,
|
|
57
|
+
state: FormStatus.Draft
|
|
58
|
+
})
|
|
59
|
+
jest.mocked(getFormatter).mockReturnValue(() => 'dummy-live')
|
|
60
|
+
|
|
61
|
+
await submit(mockRequest, model, 'test@defra.gov.uk', items, submitResponse)
|
|
62
|
+
|
|
63
|
+
expect(sendNotificationMock).toHaveBeenCalledWith(
|
|
64
|
+
expect.objectContaining({
|
|
65
|
+
personalisation: {
|
|
66
|
+
subject: `Form submission: foobar`,
|
|
67
|
+
body: 'dummy-live'
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('creates a subject line for preview forms', async () => {
|
|
74
|
+
model = {
|
|
75
|
+
name: 'foobar',
|
|
76
|
+
def: {
|
|
77
|
+
output: {
|
|
78
|
+
audience: 'human',
|
|
79
|
+
version: '1'
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} as FormModel
|
|
83
|
+
|
|
84
|
+
jest.mocked(checkFormStatus).mockReturnValue({
|
|
85
|
+
isPreview: true,
|
|
86
|
+
state: FormStatus.Draft
|
|
87
|
+
})
|
|
88
|
+
jest.mocked(getFormatter).mockReturnValue(() => 'dummy-preview')
|
|
89
|
+
|
|
90
|
+
await submit(mockRequest, model, 'test@defra.gov.uk', items, submitResponse)
|
|
91
|
+
|
|
92
|
+
expect(sendNotificationMock).toHaveBeenCalledWith(
|
|
93
|
+
expect.objectContaining({
|
|
94
|
+
personalisation: {
|
|
95
|
+
subject: `TEST FORM SUBMISSION: foobar`,
|
|
96
|
+
body: 'dummy-preview'
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('base64 encodes form data when aimed at machines', async () => {
|
|
103
|
+
model = {
|
|
104
|
+
name: 'foobar',
|
|
105
|
+
def: {
|
|
106
|
+
output: {
|
|
107
|
+
audience: 'machine',
|
|
108
|
+
version: '1'
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} as FormModel
|
|
112
|
+
|
|
113
|
+
jest.mocked(checkFormStatus).mockReturnValue({
|
|
114
|
+
isPreview: true,
|
|
115
|
+
state: FormStatus.Draft
|
|
116
|
+
})
|
|
117
|
+
jest
|
|
118
|
+
.mocked(getFormatter)
|
|
119
|
+
.mockReturnValue(() => 'dummy-preview " Hello world \' !@/')
|
|
120
|
+
|
|
121
|
+
await submit(mockRequest, model, 'test@defra.gov.uk', items, submitResponse)
|
|
122
|
+
|
|
123
|
+
expect(sendNotificationMock).toHaveBeenCalledWith(
|
|
124
|
+
expect.objectContaining({
|
|
125
|
+
personalisation: {
|
|
126
|
+
subject: `TEST FORM SUBMISSION: foobar`,
|
|
127
|
+
body: 'ZHVtbXktcHJldmlldyAiIEhlbGxvIHdvcmxkICcgIUAv'
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type SubmitResponsePayload } from '@defra/forms-model'
|
|
2
|
+
|
|
3
|
+
import { config } from '~/src/config/index.js'
|
|
4
|
+
import { escapeMarkdown } from '~/src/server/plugins/engine/components/helpers.js'
|
|
5
|
+
import { checkFormStatus } from '~/src/server/plugins/engine/helpers.js'
|
|
6
|
+
import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
|
|
7
|
+
import { type DetailItem } from '~/src/server/plugins/engine/models/types.js'
|
|
8
|
+
import { getFormatter } from '~/src/server/plugins/engine/outputFormatters/index.js'
|
|
9
|
+
import { type FormRequestPayload } from '~/src/server/routes/types.js'
|
|
10
|
+
import { sendNotification } from '~/src/server/utils/notify.js'
|
|
11
|
+
|
|
12
|
+
const templateId = config.get('notifyTemplateId')
|
|
13
|
+
|
|
14
|
+
export async function submit(
|
|
15
|
+
request: FormRequestPayload,
|
|
16
|
+
model: FormModel,
|
|
17
|
+
emailAddress: string,
|
|
18
|
+
items: DetailItem[],
|
|
19
|
+
submitResponse: SubmitResponsePayload
|
|
20
|
+
) {
|
|
21
|
+
const logTags = ['submit', 'email']
|
|
22
|
+
const { path } = request
|
|
23
|
+
const formStatus = checkFormStatus(path)
|
|
24
|
+
|
|
25
|
+
// Get submission email personalisation
|
|
26
|
+
request.logger.info(logTags, 'Getting personalisation data')
|
|
27
|
+
|
|
28
|
+
const formName = escapeMarkdown(model.name)
|
|
29
|
+
const subject = formStatus.isPreview
|
|
30
|
+
? `TEST FORM SUBMISSION: ${formName}`
|
|
31
|
+
: `Form submission: ${formName}`
|
|
32
|
+
|
|
33
|
+
const outputAudience = model.def.output?.audience ?? 'human'
|
|
34
|
+
const outputVersion = model.def.output?.version ?? '1'
|
|
35
|
+
|
|
36
|
+
const outputFormatter = getFormatter(outputAudience, outputVersion)
|
|
37
|
+
let body = outputFormatter(items, model, submitResponse, formStatus)
|
|
38
|
+
|
|
39
|
+
// GOV.UK Notify transforms quotes into curly quotes, so we can't just send the raw payload
|
|
40
|
+
// This is logic specific to Notify, so we include the logic here rather than in the formatter
|
|
41
|
+
if (outputAudience === 'machine') {
|
|
42
|
+
body = Buffer.from(body).toString('base64')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
request.logger.info(logTags, 'Sending email')
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Send submission email
|
|
49
|
+
await sendNotification({
|
|
50
|
+
templateId,
|
|
51
|
+
emailAddress,
|
|
52
|
+
personalisation: {
|
|
53
|
+
subject,
|
|
54
|
+
body
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
request.logger.info(logTags, 'Email sent successfully')
|
|
59
|
+
} catch (err) {
|
|
60
|
+
request.logger.error(logTags, 'Error sending email', err)
|
|
61
|
+
|
|
62
|
+
throw err
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { config } from '~/src/config/index.js'
|
|
2
|
+
import { getJson, postJson } from '~/src/server/services/httpService.js'
|
|
3
|
+
|
|
4
|
+
const uploaderUrl = config.get('uploaderUrl')
|
|
5
|
+
const submissionUrl = config.get('submissionUrl')
|
|
6
|
+
const uploaderBucketName = config.get('uploaderBucketName')
|
|
7
|
+
const stagingPrefix = config.get('stagingPrefix')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Initiates a CDP file upload
|
|
11
|
+
* @param {string} path - the path of the page in the form
|
|
12
|
+
* @param {string} retrievalKey - the retrieval key for the files
|
|
13
|
+
* @param {string} [mimeTypes] - the csv string of accepted mimeTypes
|
|
14
|
+
*/
|
|
15
|
+
export async function initiateUpload(path, retrievalKey, mimeTypes) {
|
|
16
|
+
const postJsonByType =
|
|
17
|
+
/** @type {typeof postJson<UploadInitiateResponse>} */ (postJson)
|
|
18
|
+
|
|
19
|
+
const payload = {
|
|
20
|
+
redirect: path,
|
|
21
|
+
callback: `${submissionUrl}/file`,
|
|
22
|
+
s3Bucket: uploaderBucketName,
|
|
23
|
+
s3Path: stagingPrefix,
|
|
24
|
+
metadata: {
|
|
25
|
+
retrievalKey
|
|
26
|
+
},
|
|
27
|
+
mimeTypes: mimeTypes
|
|
28
|
+
?.split(',')
|
|
29
|
+
.map((type) => type.trim())
|
|
30
|
+
.filter((type) => type !== '')
|
|
31
|
+
// maxFileSize: 25 * 1000 * 1000
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { payload: initiate } = await postJsonByType(
|
|
35
|
+
`${uploaderUrl}/initiate`,
|
|
36
|
+
{ payload }
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return initiate
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the status of a CDP file upload
|
|
44
|
+
* @param {string} uploadId - the ID of the upload
|
|
45
|
+
*/
|
|
46
|
+
export async function getUploadStatus(uploadId) {
|
|
47
|
+
const getJsonByType = /** @type {typeof getJson<UploadStatusResponse>} */ (
|
|
48
|
+
getJson
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const { payload: status } = await getJsonByType(
|
|
52
|
+
`${uploaderUrl}/status/${uploadId}`
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return status
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @import { UploadInitiateResponse, UploadStatusResponse } from '~/src/server/plugins/engine/types.js'
|
|
60
|
+
*/
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComponentDef,
|
|
3
|
+
type Item,
|
|
4
|
+
type List,
|
|
5
|
+
type Page
|
|
6
|
+
} from '@defra/forms-model'
|
|
7
|
+
import { type ValidationErrorItem } from 'joi'
|
|
8
|
+
|
|
9
|
+
import { FormComponent } from '~/src/server/plugins/engine/components/FormComponent.js'
|
|
10
|
+
import { type Component } from '~/src/server/plugins/engine/components/helpers.js'
|
|
11
|
+
import {
|
|
12
|
+
type BackLink,
|
|
13
|
+
type ComponentText,
|
|
14
|
+
type ComponentViewModel
|
|
15
|
+
} from '~/src/server/plugins/engine/components/types.js'
|
|
16
|
+
import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
|
|
17
|
+
import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers.js'
|
|
18
|
+
import { type ViewContext } from '~/src/server/plugins/nunjucks/types.js'
|
|
19
|
+
import { type FormAction, type FormRequest } from '~/src/server/routes/types.js'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Form submission state stores the following in Redis:
|
|
23
|
+
* Props containing user's submitted values as `{ [inputId]: value }` or as `{ [sectionName]: { [inputName]: value } }`
|
|
24
|
+
* a) . e.g:
|
|
25
|
+
* ```ts
|
|
26
|
+
* {
|
|
27
|
+
* _C9PRHmsgt: 'Ben',
|
|
28
|
+
* WfLk9McjzX: 'Music',
|
|
29
|
+
* IK7jkUFCBL: 'Royal Academy of Music'
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* b)
|
|
34
|
+
* ```ts
|
|
35
|
+
* {
|
|
36
|
+
* checkBeforeYouStart: { ukPassport: true },
|
|
37
|
+
* applicantDetails: {
|
|
38
|
+
* numberOfApplicants: 1,
|
|
39
|
+
* phoneNumber: '77777777',
|
|
40
|
+
* emailAddress: 'aaa@aaa.com'
|
|
41
|
+
* },
|
|
42
|
+
* applicantOneDetails: {
|
|
43
|
+
* firstName: 'a',
|
|
44
|
+
* middleName: 'a',
|
|
45
|
+
* lastName: 'a',
|
|
46
|
+
* address: { addressLine1: 'a', addressLine2: 'a', town: 'a', postcode: 'a' }
|
|
47
|
+
* }
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Form submission state
|
|
54
|
+
*/
|
|
55
|
+
export type FormSubmissionState = {
|
|
56
|
+
upload?: Record<string, TempFileState>
|
|
57
|
+
} & FormState
|
|
58
|
+
|
|
59
|
+
export interface FormSubmissionError
|
|
60
|
+
extends Pick<ValidationErrorItem, 'context' | 'path'> {
|
|
61
|
+
href: string // e.g: '#dateField__day'
|
|
62
|
+
name: string // e.g: 'dateField__day'
|
|
63
|
+
text: string // e.g: 'Date field must be a real date'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface FormParams {
|
|
67
|
+
action?: FormAction
|
|
68
|
+
confirm?: true
|
|
69
|
+
crumb?: string
|
|
70
|
+
itemId?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Form POST for question pages
|
|
75
|
+
* (after Joi has converted value types)
|
|
76
|
+
*/
|
|
77
|
+
export type FormPayload = FormParams & Partial<Record<string, FormValue>>
|
|
78
|
+
|
|
79
|
+
export type FormValue =
|
|
80
|
+
| Item['value']
|
|
81
|
+
| Item['value'][]
|
|
82
|
+
| UploadState
|
|
83
|
+
| RepeatListState
|
|
84
|
+
| undefined
|
|
85
|
+
|
|
86
|
+
export type FormState = Partial<Record<string, FormStateValue>>
|
|
87
|
+
export type FormStateValue = Exclude<FormValue, undefined> | null
|
|
88
|
+
|
|
89
|
+
export interface FormValidationResult<
|
|
90
|
+
ValueType extends FormPayload | FormSubmissionState
|
|
91
|
+
> {
|
|
92
|
+
value: ValueType
|
|
93
|
+
errors: FormSubmissionError[] | undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface FormContext {
|
|
97
|
+
/**
|
|
98
|
+
* Evaluation form state only (filtered by visited paths),
|
|
99
|
+
* with values formatted for condition evaluation using
|
|
100
|
+
* {@link FormComponent.getContextValueFromState}
|
|
101
|
+
*/
|
|
102
|
+
evaluationState: FormState
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Relevant form state only (filtered by visited paths)
|
|
106
|
+
*/
|
|
107
|
+
relevantState: FormState
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Relevant pages only (filtered by visited paths)
|
|
111
|
+
*/
|
|
112
|
+
relevantPages: PageControllerClass[]
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Form submission payload (single page)
|
|
116
|
+
*/
|
|
117
|
+
payload: FormPayload
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Form submission state (entire form)
|
|
121
|
+
*/
|
|
122
|
+
state: FormSubmissionState
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Validation errors (entire form)
|
|
126
|
+
*/
|
|
127
|
+
errors?: FormSubmissionError[]
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Visited paths evaluated from form state
|
|
131
|
+
*/
|
|
132
|
+
paths: string[]
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Preview URL direct access is allowed
|
|
136
|
+
*/
|
|
137
|
+
isForceAccess: boolean
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Miscellaneous extra data from event responses
|
|
141
|
+
*/
|
|
142
|
+
data: object
|
|
143
|
+
|
|
144
|
+
pageDefMap: Map<string, Page>
|
|
145
|
+
listDefMap: Map<string, List>
|
|
146
|
+
componentDefMap: Map<string, ComponentDef>
|
|
147
|
+
pageMap: Map<string, PageControllerClass>
|
|
148
|
+
componentMap: Map<string, Component>
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type FormContextRequest = (
|
|
152
|
+
| {
|
|
153
|
+
method: 'get'
|
|
154
|
+
payload?: undefined
|
|
155
|
+
}
|
|
156
|
+
| {
|
|
157
|
+
method: 'post'
|
|
158
|
+
payload: FormPayload
|
|
159
|
+
}
|
|
160
|
+
| {
|
|
161
|
+
method: FormRequest['method']
|
|
162
|
+
payload?: object | undefined
|
|
163
|
+
}
|
|
164
|
+
) &
|
|
165
|
+
Pick<FormRequest, 'app' | 'method' | 'params' | 'path' | 'query' | 'url'>
|
|
166
|
+
|
|
167
|
+
export interface UploadInitiateResponse {
|
|
168
|
+
uploadId: string
|
|
169
|
+
uploadUrl: string
|
|
170
|
+
statusUrl: string
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export enum UploadStatus {
|
|
174
|
+
initiated = 'initiated',
|
|
175
|
+
pending = 'pending',
|
|
176
|
+
ready = 'ready'
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export enum FileStatus {
|
|
180
|
+
complete = 'complete',
|
|
181
|
+
rejected = 'rejected',
|
|
182
|
+
pending = 'pending'
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export type UploadState = FileState[]
|
|
186
|
+
|
|
187
|
+
export type FileUpload = {
|
|
188
|
+
fileId: string
|
|
189
|
+
filename: string
|
|
190
|
+
contentLength: number
|
|
191
|
+
} & (
|
|
192
|
+
| {
|
|
193
|
+
fileStatus: FileStatus.complete | FileStatus.rejected | FileStatus.pending
|
|
194
|
+
errorMessage?: string
|
|
195
|
+
}
|
|
196
|
+
| {
|
|
197
|
+
fileStatus: FileStatus.complete
|
|
198
|
+
errorMessage?: undefined
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
export interface FileUploadMetadata {
|
|
203
|
+
retrievalKey: string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export type UploadStatusResponse =
|
|
207
|
+
| {
|
|
208
|
+
uploadStatus: UploadStatus.initiated
|
|
209
|
+
metadata: FileUploadMetadata
|
|
210
|
+
form: { file?: undefined }
|
|
211
|
+
}
|
|
212
|
+
| {
|
|
213
|
+
uploadStatus: UploadStatus.pending | UploadStatus.ready
|
|
214
|
+
metadata: FileUploadMetadata
|
|
215
|
+
form: { file: FileUpload }
|
|
216
|
+
numberOfRejectedFiles?: number
|
|
217
|
+
}
|
|
218
|
+
| {
|
|
219
|
+
uploadStatus: UploadStatus.ready
|
|
220
|
+
metadata: FileUploadMetadata
|
|
221
|
+
form: { file: FileUpload }
|
|
222
|
+
numberOfRejectedFiles: 0
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export type UploadStatusFileResponse = Exclude<
|
|
226
|
+
UploadStatusResponse,
|
|
227
|
+
{ uploadStatus: UploadStatus.initiated }
|
|
228
|
+
>
|
|
229
|
+
|
|
230
|
+
export interface FileState {
|
|
231
|
+
uploadId: string
|
|
232
|
+
status: UploadStatusFileResponse
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface TempFileState {
|
|
236
|
+
upload?: UploadInitiateResponse
|
|
237
|
+
files: UploadState
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export interface RepeatItemState extends FormPayload {
|
|
241
|
+
itemId: string
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export type RepeatListState = RepeatItemState[]
|
|
245
|
+
|
|
246
|
+
export interface CheckAnswers {
|
|
247
|
+
title?: ComponentText
|
|
248
|
+
summaryList: SummaryList
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface SummaryList {
|
|
252
|
+
classes?: string
|
|
253
|
+
rows: SummaryListRow[]
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface SummaryListRow {
|
|
257
|
+
key: ComponentText
|
|
258
|
+
value: ComponentText
|
|
259
|
+
actions?: { items: SummaryListAction[] }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export type SummaryListAction = ComponentText & {
|
|
263
|
+
href: string
|
|
264
|
+
visuallyHiddenText: string
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export interface PageViewModelBase extends Partial<ViewContext> {
|
|
268
|
+
page: PageController
|
|
269
|
+
name?: string
|
|
270
|
+
pageTitle: string
|
|
271
|
+
sectionTitle?: string
|
|
272
|
+
showTitle: boolean
|
|
273
|
+
isStartPage: boolean
|
|
274
|
+
backLink?: BackLink
|
|
275
|
+
feedbackLink?: string
|
|
276
|
+
serviceUrl: string
|
|
277
|
+
phaseTag?: string
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export interface ItemDeletePageViewModel extends PageViewModelBase {
|
|
281
|
+
context: FormContext
|
|
282
|
+
itemTitle: string
|
|
283
|
+
confirmation?: ComponentText
|
|
284
|
+
buttonConfirm: ComponentText
|
|
285
|
+
buttonCancel: ComponentText
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export interface FormPageViewModel extends PageViewModelBase {
|
|
289
|
+
components: ComponentViewModel[]
|
|
290
|
+
context: FormContext
|
|
291
|
+
errors?: FormSubmissionError[]
|
|
292
|
+
hasMissingNotificationEmail?: boolean
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export interface RepeaterSummaryPageViewModel extends PageViewModelBase {
|
|
296
|
+
context: FormContext
|
|
297
|
+
errors?: FormSubmissionError[]
|
|
298
|
+
checkAnswers: CheckAnswers[]
|
|
299
|
+
repeatTitle: string
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface FeaturedFormPageViewModel extends FormPageViewModel {
|
|
303
|
+
formAction?: string
|
|
304
|
+
formComponent: ComponentViewModel
|
|
305
|
+
componentsBefore: ComponentViewModel[]
|
|
306
|
+
uploadId: string | undefined
|
|
307
|
+
proxyUrl: string | null
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export type PageViewModel =
|
|
311
|
+
| PageViewModelBase
|
|
312
|
+
| ItemDeletePageViewModel
|
|
313
|
+
| FormPageViewModel
|
|
314
|
+
| RepeaterSummaryPageViewModel
|
|
315
|
+
| FeaturedFormPageViewModel
|