@defra/forms-engine-plugin 3.0.7 → 3.0.9
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/.public/javascripts/application.min.js +1 -1
- package/.public/javascripts/application.min.js.map +1 -1
- package/.public/javascripts/shared.min.js +1 -1
- package/.public/javascripts/shared.min.js.map +1 -1
- package/.server/client/javascripts/file-upload.d.ts +1 -0
- package/.server/client/javascripts/file-upload.js +8 -2
- package/.server/client/javascripts/file-upload.js.map +1 -1
- package/.server/server/forms/register-as-a-unicorn-breeder.yaml +1 -0
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +3 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +40 -37
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
- package/.server/server/plugins/engine/routes/file-upload.js +3 -1
- package/.server/server/plugins/engine/routes/file-upload.js.map +1 -1
- package/.server/server/plugins/engine/types/schema.js +2 -1
- package/.server/server/plugins/engine/types/schema.js.map +1 -1
- package/.server/server/plugins/engine/types.d.ts +1 -0
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/plugins/engine/views/summary.html +12 -2
- package/package.json +2 -2
- package/src/client/javascripts/file-upload.js +16 -2
- package/src/server/forms/register-as-a-unicorn-breeder.yaml +1 -0
- package/src/server/index.test.ts +3 -1
- package/src/server/plugins/engine/pageControllers/SummaryPageController.test.ts +85 -0
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +42 -33
- package/src/server/plugins/engine/routes/file-upload.ts +3 -1
- package/src/server/plugins/engine/types/schema.test.ts +34 -0
- package/src/server/plugins/engine/types/schema.ts +6 -1
- package/src/server/plugins/engine/types.ts +1 -0
- package/src/server/plugins/engine/views/summary.html +12 -2
|
@@ -230,13 +230,27 @@ function reloadPage() {
|
|
|
230
230
|
|
|
231
231
|
/**
|
|
232
232
|
* Build the upload status URL given the current pathname and the upload ID.
|
|
233
|
+
* This only works when called on a file upload page that has a maximum depth of 1 URL segments following the slug.
|
|
233
234
|
* @param {string} pathname – e.g. window.location.pathname
|
|
234
235
|
* @param {string} uploadId
|
|
235
236
|
* @returns {string} e.g. "/form/upload-status/abc123"
|
|
236
237
|
*/
|
|
237
238
|
export function buildUploadStatusUrl(pathname, uploadId) {
|
|
238
|
-
|
|
239
|
-
const
|
|
239
|
+
// Remove preview markers and duplicate slashes
|
|
240
|
+
const normalisedPath = pathname
|
|
241
|
+
.replace(/\/preview\/(draft|live)/g, '')
|
|
242
|
+
.replace(/\/{2,}/g, '/')
|
|
243
|
+
.replace(/\/$/, '')
|
|
244
|
+
|
|
245
|
+
const segments = normalisedPath.split('/').filter(Boolean)
|
|
246
|
+
|
|
247
|
+
// The slug is always the second to last segment
|
|
248
|
+
// The prefix is everything before the slug
|
|
249
|
+
const prefix =
|
|
250
|
+
segments.length > 2
|
|
251
|
+
? `/${segments.slice(0, segments.length - 2).join('/')}`
|
|
252
|
+
: ''
|
|
253
|
+
|
|
240
254
|
return `${prefix}/upload-status/${uploadId}`
|
|
241
255
|
}
|
|
242
256
|
|
package/src/server/index.test.ts
CHANGED
|
@@ -508,7 +508,9 @@ describe('Upload status route', () => {
|
|
|
508
508
|
const res = await server.inject(options)
|
|
509
509
|
|
|
510
510
|
expect(res.statusCode).toBe(StatusCodes.OK)
|
|
511
|
-
expect(res.result).toEqual(
|
|
511
|
+
expect(res.result).toEqual({
|
|
512
|
+
uploadStatus: UploadStatus.ready
|
|
513
|
+
})
|
|
512
514
|
expect(getUploadStatus).toHaveBeenCalledWith(
|
|
513
515
|
'123e4567-e89b-12d3-a456-426614174000'
|
|
514
516
|
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
2
|
+
import { SummaryPageController } from '~/src/server/plugins/engine/pageControllers/SummaryPageController.js'
|
|
3
|
+
import { buildFormRequest } from '~/src/server/plugins/engine/pageControllers/__stubs__/request.js'
|
|
4
|
+
import { type FormSubmissionState } from '~/src/server/plugins/engine/types.js'
|
|
5
|
+
import {
|
|
6
|
+
type FormRequest,
|
|
7
|
+
type FormRequestPayload,
|
|
8
|
+
type FormResponseToolkit
|
|
9
|
+
} from '~/src/server/routes/types.js'
|
|
10
|
+
import { type CacheService } from '~/src/server/services/cacheService.js'
|
|
11
|
+
import definition from '~/test/form/definitions/basic.js'
|
|
12
|
+
|
|
13
|
+
describe('SummaryPageController', () => {
|
|
14
|
+
let model: FormModel
|
|
15
|
+
let controller: SummaryPageController
|
|
16
|
+
let requestPage: FormRequest
|
|
17
|
+
|
|
18
|
+
const response = {
|
|
19
|
+
code: jest.fn().mockImplementation(() => response)
|
|
20
|
+
}
|
|
21
|
+
const h: FormResponseToolkit = {
|
|
22
|
+
redirect: jest.fn().mockReturnValue(response),
|
|
23
|
+
view: jest.fn()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
model = new FormModel(definition, {
|
|
28
|
+
basePath: 'test'
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Create a mock page for SummaryPageController
|
|
32
|
+
const mockPage = {
|
|
33
|
+
...definition.pages[0],
|
|
34
|
+
controller: 'summary'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
controller = new SummaryPageController(model, mockPage as any)
|
|
39
|
+
|
|
40
|
+
requestPage = buildFormRequest({
|
|
41
|
+
method: 'get',
|
|
42
|
+
url: new URL('http://example.com/test/summary'),
|
|
43
|
+
path: '/test/summary',
|
|
44
|
+
params: {
|
|
45
|
+
path: 'summary',
|
|
46
|
+
slug: 'test'
|
|
47
|
+
},
|
|
48
|
+
query: {},
|
|
49
|
+
app: { model }
|
|
50
|
+
} as FormRequest)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('handleSaveAndExit', () => {
|
|
54
|
+
it('should invoke saveAndExit plugin option', async () => {
|
|
55
|
+
const saveAndExitMock = jest.fn(() => ({}))
|
|
56
|
+
const state: FormSubmissionState = {
|
|
57
|
+
$$__referenceNumber: 'foobar',
|
|
58
|
+
licenceLength: 365,
|
|
59
|
+
fullName: 'John Smith'
|
|
60
|
+
}
|
|
61
|
+
const request = {
|
|
62
|
+
...requestPage,
|
|
63
|
+
server: {
|
|
64
|
+
plugins: {
|
|
65
|
+
'forms-engine-plugin': {
|
|
66
|
+
saveAndExit: saveAndExitMock,
|
|
67
|
+
cacheService: {
|
|
68
|
+
clearState: jest.fn()
|
|
69
|
+
} as unknown as CacheService
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
method: 'post',
|
|
74
|
+
payload: { fullName: 'John Smith', action: 'save-and-exit' }
|
|
75
|
+
} as unknown as FormRequestPayload
|
|
76
|
+
|
|
77
|
+
const context = model.getFormContext(request, state)
|
|
78
|
+
|
|
79
|
+
const postHandler = controller.makePostRouteHandler()
|
|
80
|
+
await postHandler(request, context, h)
|
|
81
|
+
|
|
82
|
+
expect(saveAndExitMock).toHaveBeenCalledWith(request, h, context)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
})
|
|
@@ -73,6 +73,7 @@ export class SummaryPageController extends QuestionPageController {
|
|
|
73
73
|
viewModel.phaseTag = this.phaseTag
|
|
74
74
|
viewModel.components = components
|
|
75
75
|
viewModel.allowSaveAndExit = this.shouldShowSaveAndExit(request.server)
|
|
76
|
+
viewModel.errors = errors
|
|
76
77
|
|
|
77
78
|
return viewModel
|
|
78
79
|
}
|
|
@@ -107,48 +108,56 @@ export class SummaryPageController extends QuestionPageController {
|
|
|
107
108
|
context: FormContext,
|
|
108
109
|
h: FormResponseToolkit
|
|
109
110
|
) => {
|
|
110
|
-
const { model } = this
|
|
111
|
-
const { params } = request
|
|
112
|
-
|
|
113
111
|
// Check if this is a save-and-exit action
|
|
114
112
|
const { action } = request.payload
|
|
115
113
|
if (action === FormAction.SaveAndExit) {
|
|
116
114
|
return this.handleSaveAndExit(request, context, h)
|
|
117
115
|
}
|
|
118
116
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// Send submission email
|
|
133
|
-
if (emailAddress) {
|
|
134
|
-
const viewModel = this.getSummaryViewModel(request, context)
|
|
135
|
-
await submitForm(
|
|
136
|
-
context,
|
|
137
|
-
request,
|
|
138
|
-
viewModel,
|
|
139
|
-
model,
|
|
140
|
-
emailAddress,
|
|
141
|
-
formMetadata
|
|
142
|
-
)
|
|
143
|
-
}
|
|
117
|
+
return this.handleFormSubmit(request, context, h)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async handleFormSubmit(
|
|
122
|
+
request: FormRequestPayload,
|
|
123
|
+
context: FormContext,
|
|
124
|
+
h: FormResponseToolkit
|
|
125
|
+
) {
|
|
126
|
+
const { model } = this
|
|
127
|
+
const { params } = request
|
|
128
|
+
|
|
129
|
+
const cacheService = getCacheService(request.server)
|
|
144
130
|
|
|
145
|
-
|
|
131
|
+
const { formsService } = this.model.services
|
|
132
|
+
const { getFormMetadata } = formsService
|
|
146
133
|
|
|
147
|
-
|
|
148
|
-
|
|
134
|
+
// Get the form metadata using the `slug` param
|
|
135
|
+
const formMetadata = await getFormMetadata(params.slug)
|
|
136
|
+
const { notificationEmail } = formMetadata
|
|
137
|
+
const { isPreview } = checkFormStatus(request.params)
|
|
138
|
+
const emailAddress = notificationEmail ?? this.model.def.outputEmail
|
|
149
139
|
|
|
150
|
-
|
|
140
|
+
checkEmailAddressForLiveFormSubmission(emailAddress, isPreview)
|
|
141
|
+
|
|
142
|
+
// Send submission email
|
|
143
|
+
if (emailAddress) {
|
|
144
|
+
const viewModel = this.getSummaryViewModel(request, context)
|
|
145
|
+
await submitForm(
|
|
146
|
+
context,
|
|
147
|
+
request,
|
|
148
|
+
viewModel,
|
|
149
|
+
model,
|
|
150
|
+
emailAddress,
|
|
151
|
+
formMetadata
|
|
152
|
+
)
|
|
151
153
|
}
|
|
154
|
+
|
|
155
|
+
await cacheService.setConfirmationState(request, { confirmed: true })
|
|
156
|
+
|
|
157
|
+
// Clear all form data
|
|
158
|
+
await cacheService.clearState(request)
|
|
159
|
+
|
|
160
|
+
return this.proceed(request, h, this.getStatusPath())
|
|
152
161
|
}
|
|
153
162
|
|
|
154
163
|
get postRouteOptions(): RouteOptions<FormRequestPayloadRefs> {
|
|
@@ -164,7 +173,7 @@ export class SummaryPageController extends QuestionPageController {
|
|
|
164
173
|
}
|
|
165
174
|
}
|
|
166
175
|
|
|
167
|
-
async function submitForm(
|
|
176
|
+
export async function submitForm(
|
|
168
177
|
context: FormContext,
|
|
169
178
|
request: FormRequestPayload,
|
|
170
179
|
summaryViewModel: SummaryViewModel,
|
|
@@ -78,6 +78,29 @@ describe('Schema validation', () => {
|
|
|
78
78
|
expect(error).toBeUndefined()
|
|
79
79
|
})
|
|
80
80
|
|
|
81
|
+
it('should validate valid meta object with valid custom properties', () => {
|
|
82
|
+
const validMetaWithCustom = {
|
|
83
|
+
...validMeta,
|
|
84
|
+
custom: {
|
|
85
|
+
property1: 'value 1',
|
|
86
|
+
property2: 'value2'
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const { error } =
|
|
90
|
+
formAdapterSubmissionMessageMetaSchema.validate(validMetaWithCustom)
|
|
91
|
+
expect(error).toBeUndefined()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should validate valid meta object with empty custom properties', () => {
|
|
95
|
+
const validMetaWithCustom = {
|
|
96
|
+
...validMeta,
|
|
97
|
+
custom: {}
|
|
98
|
+
}
|
|
99
|
+
const { error } =
|
|
100
|
+
formAdapterSubmissionMessageMetaSchema.validate(validMetaWithCustom)
|
|
101
|
+
expect(error).toBeUndefined()
|
|
102
|
+
})
|
|
103
|
+
|
|
81
104
|
it('should reject invalid schema version', () => {
|
|
82
105
|
const invalidMeta = { ...validMeta, schemaVersion: 'invalid' }
|
|
83
106
|
const { error } =
|
|
@@ -92,6 +115,17 @@ describe('Schema validation', () => {
|
|
|
92
115
|
formAdapterSubmissionMessageMetaSchema.validate(metaWithoutTimestamp)
|
|
93
116
|
expect(error).toBeDefined()
|
|
94
117
|
})
|
|
118
|
+
|
|
119
|
+
it('should reject invalid custom structure', () => {
|
|
120
|
+
const validMetaWithInvalidCustom = {
|
|
121
|
+
...validMeta,
|
|
122
|
+
custom: 'invalid'
|
|
123
|
+
}
|
|
124
|
+
const { error } = formAdapterSubmissionMessageMetaSchema.validate(
|
|
125
|
+
validMetaWithInvalidCustom
|
|
126
|
+
)
|
|
127
|
+
expect(error).toBeDefined()
|
|
128
|
+
})
|
|
95
129
|
})
|
|
96
130
|
|
|
97
131
|
describe('formAdapterSubmissionMessageDataSchema', () => {
|
|
@@ -31,7 +31,12 @@ export const formAdapterSubmissionMessageMetaSchema =
|
|
|
31
31
|
.required(),
|
|
32
32
|
isPreview: Joi.boolean().required(),
|
|
33
33
|
notificationEmail: notificationEmailAddressSchema.required(),
|
|
34
|
-
versionMetadata: formVersionMetadataSchema.optional()
|
|
34
|
+
versionMetadata: formVersionMetadataSchema.optional(),
|
|
35
|
+
custom: Joi.object()
|
|
36
|
+
.pattern(/^/, Joi.any())
|
|
37
|
+
.unknown()
|
|
38
|
+
.optional()
|
|
39
|
+
.description('Custom properties for the message')
|
|
35
40
|
})
|
|
36
41
|
|
|
37
42
|
export const formAdapterSubmissionMessageDataSchema =
|
|
@@ -409,6 +409,7 @@ export interface FormAdapterSubmissionMessageMeta {
|
|
|
409
409
|
isPreview: boolean
|
|
410
410
|
notificationEmail: string
|
|
411
411
|
versionMetadata?: FormVersionMetadata
|
|
412
|
+
custom?: Record<string, unknown>
|
|
412
413
|
}
|
|
413
414
|
|
|
414
415
|
export type FormAdapterSubmissionMessageMetaSerialised = Omit<
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
{% from "govuk/components/summary-list/macro.njk" import govukSummaryList %}
|
|
5
5
|
{% from "govuk/components/button/macro.njk" import govukButton %}
|
|
6
6
|
{% from "partials/components.html" import componentList with context %}
|
|
7
|
+
{% from "govuk/components/input/macro.njk" import govukInput %}
|
|
7
8
|
|
|
8
9
|
{% block content %}
|
|
9
10
|
<div class="govuk-grid-row">
|
|
@@ -12,6 +13,13 @@
|
|
|
12
13
|
{% include "partials/preview-banner.html" %}
|
|
13
14
|
{% endif %}
|
|
14
15
|
|
|
16
|
+
{% if errors %}
|
|
17
|
+
{{ govukErrorSummary({
|
|
18
|
+
titleText: "There is a problem",
|
|
19
|
+
errorList: checkErrorTemplates(errors)
|
|
20
|
+
}) }}
|
|
21
|
+
{% endif %}
|
|
22
|
+
|
|
15
23
|
{% if hasMissingNotificationEmail %}
|
|
16
24
|
{% include "partials/warn-missing-notification-email.html" %}
|
|
17
25
|
{% endif %}
|
|
@@ -33,6 +41,10 @@
|
|
|
33
41
|
<form method="post" novalidate>
|
|
34
42
|
<input type="hidden" name="crumb" value="{{ crumb }}">
|
|
35
43
|
|
|
44
|
+
{{ componentList(components) }}
|
|
45
|
+
|
|
46
|
+
{% block customPageContent %}{% endblock %}
|
|
47
|
+
|
|
36
48
|
{% if declaration %}
|
|
37
49
|
<h2 class="govuk-heading-m" id="declaration">Declaration</h2>
|
|
38
50
|
<div class="govuk-body">
|
|
@@ -40,8 +52,6 @@
|
|
|
40
52
|
</div>
|
|
41
53
|
{% endif %}
|
|
42
54
|
|
|
43
|
-
{{ componentList(components) }}
|
|
44
|
-
|
|
45
55
|
<div class="govuk-button-group">
|
|
46
56
|
{% set isDeclaration = declaration or components | length %}
|
|
47
57
|
|