@defra/forms-engine-plugin 4.4.0 → 4.5.1
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.js +13 -8
- package/.server/client/javascripts/file-upload.js.map +1 -1
- package/.server/server/constants.d.ts +1 -0
- package/.server/server/constants.js +1 -0
- package/.server/server/constants.js.map +1 -1
- package/.server/server/plugins/engine/beta/form-context.d.ts +0 -1
- package/.server/server/plugins/engine/beta/form-context.js +4 -3
- package/.server/server/plugins/engine/beta/form-context.js.map +1 -1
- package/.server/server/plugins/engine/components/FileUploadField.d.ts +3 -2
- package/.server/server/plugins/engine/components/FileUploadField.js +11 -3
- package/.server/server/plugins/engine/components/FileUploadField.js.map +1 -1
- package/.server/server/plugins/engine/helpers.d.ts +8 -0
- package/.server/server/plugins/engine/helpers.js +8 -0
- package/.server/server/plugins/engine/helpers.js.map +1 -1
- package/.server/server/plugins/engine/outputFormatters/adapter/v1.js +2 -1
- package/.server/server/plugins/engine/outputFormatters/adapter/v1.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.d.ts +11 -0
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js +65 -28
- package/.server/server/plugins/engine/pageControllers/FileUploadPageController.js.map +1 -1
- package/package.json +1 -1
- package/src/client/javascripts/file-upload.js +12 -8
- package/src/server/constants.js +1 -0
- package/src/server/plugins/engine/beta/form-context.test.ts +22 -8
- package/src/server/plugins/engine/beta/form-context.ts +7 -6
- package/src/server/plugins/engine/components/FileUploadField.test.ts +11 -8
- package/src/server/plugins/engine/components/FileUploadField.ts +14 -5
- package/src/server/plugins/engine/helpers.ts +17 -0
- package/src/server/plugins/engine/outputFormatters/adapter/v1.test.ts +54 -0
- package/src/server/plugins/engine/outputFormatters/adapter/v1.ts +7 -5
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +109 -5
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +69 -21
|
@@ -5,7 +5,8 @@ import { isEqual } from 'date-fns'
|
|
|
5
5
|
import { PREVIEW_PATH_PREFIX } from '~/src/server/constants.js'
|
|
6
6
|
import {
|
|
7
7
|
checkEmailAddressForLiveFormSubmission,
|
|
8
|
-
getCacheService
|
|
8
|
+
getCacheService,
|
|
9
|
+
getFormVersion
|
|
9
10
|
} from '~/src/server/plugins/engine/helpers.js'
|
|
10
11
|
import { FormModel } from '~/src/server/plugins/engine/models/index.js'
|
|
11
12
|
import { type PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
|
|
@@ -27,7 +28,6 @@ export interface FormModelOptions {
|
|
|
27
28
|
services?: Services
|
|
28
29
|
controllers?: Record<string, typeof PageController>
|
|
29
30
|
basePath?: string
|
|
30
|
-
versionNumber?: number
|
|
31
31
|
ordnanceSurveyApiKey?: string
|
|
32
32
|
formId?: string
|
|
33
33
|
routePrefix?: string
|
|
@@ -53,8 +53,6 @@ export async function getFormModel(
|
|
|
53
53
|
const formState = resolveState(state)
|
|
54
54
|
|
|
55
55
|
const metadata = await formsService.getFormMetadata(slug)
|
|
56
|
-
const versionNumber =
|
|
57
|
-
options.versionNumber ?? metadata.versions?.[0]?.versionNumber
|
|
58
56
|
|
|
59
57
|
const definition = await formsService.getFormDefinition(
|
|
60
58
|
metadata.id,
|
|
@@ -67,6 +65,8 @@ export async function getFormModel(
|
|
|
67
65
|
)
|
|
68
66
|
}
|
|
69
67
|
|
|
68
|
+
const versionNumber = getFormVersion(definition)?.versionNumber
|
|
69
|
+
|
|
70
70
|
return new FormModel(
|
|
71
71
|
definition,
|
|
72
72
|
{
|
|
@@ -182,14 +182,15 @@ export async function resolveFormModel(
|
|
|
182
182
|
const routePrefix =
|
|
183
183
|
options.routePrefix ?? server.realm.modifiers.route.prefix
|
|
184
184
|
|
|
185
|
+
const versionNumber = getFormVersion(definition)?.versionNumber
|
|
186
|
+
|
|
185
187
|
const model = new FormModel(
|
|
186
188
|
definition,
|
|
187
189
|
{
|
|
188
190
|
basePath:
|
|
189
191
|
options.basePath ??
|
|
190
192
|
buildBasePath(routePrefix, slug, formState, isPreview),
|
|
191
|
-
versionNumber
|
|
192
|
-
options.versionNumber ?? metadata.versions?.[0]?.versionNumber,
|
|
193
|
+
versionNumber,
|
|
193
194
|
ordnanceSurveyApiKey: options.ordnanceSurveyApiKey,
|
|
194
195
|
formId: options.formId ?? metadata.id
|
|
195
196
|
},
|
|
@@ -405,7 +405,7 @@ describe('FileUploadField', () => {
|
|
|
405
405
|
actions: {
|
|
406
406
|
items: [
|
|
407
407
|
{
|
|
408
|
-
href: `/test/file-upload-component/${validState[0].
|
|
408
|
+
href: `/test/file-upload-component/${validState[0].status.form.file.fileId}/confirm-delete`,
|
|
409
409
|
text: 'Remove',
|
|
410
410
|
attributes: { id: 'myComponent__0' },
|
|
411
411
|
classes: 'govuk-link--no-visited-state',
|
|
@@ -424,7 +424,7 @@ describe('FileUploadField', () => {
|
|
|
424
424
|
actions: {
|
|
425
425
|
items: [
|
|
426
426
|
{
|
|
427
|
-
href: `/test/file-upload-component/${validState[1].
|
|
427
|
+
href: `/test/file-upload-component/${validState[1].status.form.file.fileId}/confirm-delete`,
|
|
428
428
|
text: 'Remove',
|
|
429
429
|
attributes: { id: 'myComponent__1' },
|
|
430
430
|
classes: 'govuk-link--no-visited-state',
|
|
@@ -443,7 +443,7 @@ describe('FileUploadField', () => {
|
|
|
443
443
|
actions: {
|
|
444
444
|
items: [
|
|
445
445
|
{
|
|
446
|
-
href: `/test/file-upload-component/${validState[2].
|
|
446
|
+
href: `/test/file-upload-component/${validState[2].status.form.file.fileId}/confirm-delete`,
|
|
447
447
|
text: 'Remove',
|
|
448
448
|
attributes: { id: 'myComponent__2' },
|
|
449
449
|
classes: 'govuk-link--no-visited-state',
|
|
@@ -454,7 +454,8 @@ describe('FileUploadField', () => {
|
|
|
454
454
|
}
|
|
455
455
|
]
|
|
456
456
|
}
|
|
457
|
-
}
|
|
457
|
+
},
|
|
458
|
+
multiple: true
|
|
458
459
|
})
|
|
459
460
|
)
|
|
460
461
|
})
|
|
@@ -543,7 +544,7 @@ describe('FileUploadField', () => {
|
|
|
543
544
|
actions: {
|
|
544
545
|
items: [
|
|
545
546
|
{
|
|
546
|
-
href: `/test/file-upload-component/${validState[2].
|
|
547
|
+
href: `/test/file-upload-component/${validState[2].status.form.file.fileId}/confirm-delete`,
|
|
547
548
|
text: 'Remove',
|
|
548
549
|
attributes: { id: 'myComponent__0' },
|
|
549
550
|
classes: 'govuk-link--no-visited-state',
|
|
@@ -554,7 +555,8 @@ describe('FileUploadField', () => {
|
|
|
554
555
|
}
|
|
555
556
|
]
|
|
556
557
|
}
|
|
557
|
-
}
|
|
558
|
+
},
|
|
559
|
+
multiple: true
|
|
558
560
|
})
|
|
559
561
|
)
|
|
560
562
|
})
|
|
@@ -583,7 +585,7 @@ describe('FileUploadField', () => {
|
|
|
583
585
|
actions: {
|
|
584
586
|
items: [
|
|
585
587
|
{
|
|
586
|
-
href: `/test/file-upload-component/${validState[2].
|
|
588
|
+
href: `/test/file-upload-component/${validState[2].status.form.file.fileId}/confirm-delete`,
|
|
587
589
|
text: 'Remove',
|
|
588
590
|
attributes: { id: 'myComponent__0' },
|
|
589
591
|
classes: 'govuk-link--no-visited-state',
|
|
@@ -594,7 +596,8 @@ describe('FileUploadField', () => {
|
|
|
594
596
|
}
|
|
595
597
|
]
|
|
596
598
|
}
|
|
597
|
-
}
|
|
599
|
+
},
|
|
600
|
+
multiple: true
|
|
598
601
|
})
|
|
599
602
|
)
|
|
600
603
|
})
|
|
@@ -73,9 +73,12 @@ export const tempStatusSchema = joi
|
|
|
73
73
|
.valid(UploadStatus.ready, UploadStatus.pending)
|
|
74
74
|
.required(),
|
|
75
75
|
metadata: metadataSchema,
|
|
76
|
-
form: joi
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
form: joi
|
|
77
|
+
.object()
|
|
78
|
+
.required()
|
|
79
|
+
.keys({
|
|
80
|
+
file: joi.array().items(tempFileSchema).single().required()
|
|
81
|
+
}),
|
|
79
82
|
numberOfRejectedFiles: joi.number().optional()
|
|
80
83
|
})
|
|
81
84
|
.required()
|
|
@@ -191,7 +194,7 @@ export class FileUploadField extends FormComponent {
|
|
|
191
194
|
errors?: FormSubmissionError[],
|
|
192
195
|
query: FormQuery = {}
|
|
193
196
|
) {
|
|
194
|
-
const { options, page } = this
|
|
197
|
+
const { options, page, schema } = this
|
|
195
198
|
|
|
196
199
|
// Allow preview URL direct access
|
|
197
200
|
const isForceAccess = 'force' in query
|
|
@@ -233,7 +236,7 @@ export class FileUploadField extends FormComponent {
|
|
|
233
236
|
|
|
234
237
|
// Remove summary list actions from previews
|
|
235
238
|
if (!isForceAccess) {
|
|
236
|
-
const path = `/${
|
|
239
|
+
const path = `/${file.fileId}/confirm-delete`
|
|
237
240
|
const href = page?.getHref(`${page.path}${path}`) ?? '#'
|
|
238
241
|
|
|
239
242
|
items.push({
|
|
@@ -263,6 +266,9 @@ export class FileUploadField extends FormComponent {
|
|
|
263
266
|
attributes.accept = options.accept
|
|
264
267
|
}
|
|
265
268
|
|
|
269
|
+
// Allow multiple file selection when schema permits more than 1 file
|
|
270
|
+
const allowsMultiple = schema.max !== 1 && schema.length !== 1
|
|
271
|
+
|
|
266
272
|
const summaryList: SummaryList = {
|
|
267
273
|
classes: 'govuk-summary-list--long-key',
|
|
268
274
|
rows
|
|
@@ -277,6 +283,9 @@ export class FileUploadField extends FormComponent {
|
|
|
277
283
|
// Override the component name we send to CDP
|
|
278
284
|
name: 'file',
|
|
279
285
|
|
|
286
|
+
// Enable multi-file selection in the file picker
|
|
287
|
+
...(allowsMultiple && { multiple: true }),
|
|
288
|
+
|
|
280
289
|
upload: {
|
|
281
290
|
count,
|
|
282
291
|
summaryList
|
|
@@ -16,6 +16,7 @@ import { type Schema, type ValidationErrorItem } from 'joi'
|
|
|
16
16
|
import { Liquid } from 'liquidjs'
|
|
17
17
|
|
|
18
18
|
import { createLogger } from '~/src/server/common/helpers/logging/logger.js'
|
|
19
|
+
import { FORM_VERSION_METADATA_KEY } from '~/src/server/constants.js'
|
|
19
20
|
import {
|
|
20
21
|
getAnswer,
|
|
21
22
|
type Field
|
|
@@ -416,6 +417,22 @@ export function handleLegacyRedirect(h: ResponseToolkit, targetUrl: string) {
|
|
|
416
417
|
* If the page doesn't have a title, set it from the title of the first form component
|
|
417
418
|
* @param def - the form definition
|
|
418
419
|
*/
|
|
420
|
+
export interface FormVersionMetadata {
|
|
421
|
+
versionNumber: number
|
|
422
|
+
createdAt: Date
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Extracts form version metadata from a form definition
|
|
427
|
+
*/
|
|
428
|
+
export function getFormVersion(
|
|
429
|
+
definition: Pick<FormDefinition, 'metadata'>
|
|
430
|
+
): FormVersionMetadata | undefined {
|
|
431
|
+
return definition.metadata?.[FORM_VERSION_METADATA_KEY] as
|
|
432
|
+
| FormVersionMetadata
|
|
433
|
+
| undefined
|
|
434
|
+
}
|
|
435
|
+
|
|
419
436
|
export function setPageTitles(def: FormDefinition) {
|
|
420
437
|
def.pages.forEach((page) => {
|
|
421
438
|
if (!page.title) {
|
|
@@ -764,6 +764,60 @@ describe('Adapter v1 formatter', () => {
|
|
|
764
764
|
})
|
|
765
765
|
|
|
766
766
|
describe('version metadata handling', () => {
|
|
767
|
+
it('should prefer $$__formVersion from definition metadata over formMetadata.versions', () => {
|
|
768
|
+
const definitionWithFormVersion = {
|
|
769
|
+
...definition,
|
|
770
|
+
metadata: {
|
|
771
|
+
$$__formVersion: {
|
|
772
|
+
versionNumber: 42,
|
|
773
|
+
createdAt: new Date('2024-06-01T00:00:00.000Z')
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const modelWithFormVersion = new FormModel(definitionWithFormVersion, {
|
|
779
|
+
basePath: 'test'
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
const contextWithFormVersion = modelWithFormVersion.getFormContext(
|
|
783
|
+
request,
|
|
784
|
+
state
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
const formMetadata: Partial<FormMetadata> = {
|
|
788
|
+
id: 'form-123',
|
|
789
|
+
slug: 'test-form',
|
|
790
|
+
title: 'Test Form',
|
|
791
|
+
notificationEmail: 'test@example.com',
|
|
792
|
+
versions: [
|
|
793
|
+
{
|
|
794
|
+
versionNumber: 1,
|
|
795
|
+
createdAt: new Date('2024-01-01T00:00:00.000Z')
|
|
796
|
+
}
|
|
797
|
+
]
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const formStatus = {
|
|
801
|
+
isPreview: false,
|
|
802
|
+
state: FormStatus.Live
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const body = format(
|
|
806
|
+
contextWithFormVersion,
|
|
807
|
+
items,
|
|
808
|
+
modelWithFormVersion,
|
|
809
|
+
submitResponse,
|
|
810
|
+
formStatus,
|
|
811
|
+
formMetadata as FormMetadata
|
|
812
|
+
)
|
|
813
|
+
const parsedBody = JSON.parse(body) as FormAdapterSubmissionMessagePayload
|
|
814
|
+
|
|
815
|
+
expect(parsedBody.meta.versionMetadata).toEqual({
|
|
816
|
+
versionNumber: 42,
|
|
817
|
+
createdAt: '2024-06-01T00:00:00.000Z'
|
|
818
|
+
})
|
|
819
|
+
})
|
|
820
|
+
|
|
767
821
|
it('should include versionMetadata when context has submittedVersionNumber and formMetadata has versions', () => {
|
|
768
822
|
const formMetadata: Partial<FormMetadata> = {
|
|
769
823
|
id: 'form-123',
|
|
@@ -3,7 +3,10 @@ import {
|
|
|
3
3
|
type SubmitResponsePayload
|
|
4
4
|
} from '@defra/forms-model'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getFormVersion,
|
|
8
|
+
type checkFormStatus
|
|
9
|
+
} from '~/src/server/plugins/engine/helpers.js'
|
|
7
10
|
import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
8
11
|
import { type DetailItem } from '~/src/server/plugins/engine/models/types.js'
|
|
9
12
|
import { categoriseData } from '~/src/server/plugins/engine/outputFormatters/machine/v2.js'
|
|
@@ -28,10 +31,9 @@ export function format(
|
|
|
28
31
|
|
|
29
32
|
const { main: v2Main, ...v2Data } = categoriseData(items)
|
|
30
33
|
|
|
31
|
-
const versionMetadata =
|
|
32
|
-
|
|
33
|
-
formMetadata
|
|
34
|
-
)
|
|
34
|
+
const versionMetadata =
|
|
35
|
+
getFormVersion(model.def) ??
|
|
36
|
+
getVersionMetadata(context.submittedVersionNumber, formMetadata)
|
|
35
37
|
|
|
36
38
|
const meta: FormAdapterSubmissionMessageMeta = {
|
|
37
39
|
schemaVersion: FormAdapterSubmissionSchemaVersion.V1,
|
|
@@ -795,6 +795,83 @@ describe('FileUploadPageController', () => {
|
|
|
795
795
|
})
|
|
796
796
|
})
|
|
797
797
|
|
|
798
|
+
it('collects all file errors into a single flash when multiple files fail', async () => {
|
|
799
|
+
const state = {
|
|
800
|
+
upload: {
|
|
801
|
+
[controller.path]: {
|
|
802
|
+
upload: {
|
|
803
|
+
uploadId: 'some-id',
|
|
804
|
+
uploadUrl: 'some-url',
|
|
805
|
+
statusUrl: 'some-status-url'
|
|
806
|
+
},
|
|
807
|
+
files: []
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
} as unknown as FormSubmissionState
|
|
811
|
+
|
|
812
|
+
const errorStatus = {
|
|
813
|
+
uploadStatus: UploadStatus.ready,
|
|
814
|
+
form: {
|
|
815
|
+
file: [
|
|
816
|
+
{
|
|
817
|
+
fileStatus: FileStatus.rejected,
|
|
818
|
+
errorMessage: 'File too large'
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
fileStatus: FileStatus.rejected,
|
|
822
|
+
errorMessage: 'Invalid file type'
|
|
823
|
+
}
|
|
824
|
+
]
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
jest
|
|
829
|
+
.spyOn(uploadService, 'getUploadStatus')
|
|
830
|
+
.mockResolvedValue(errorStatus as unknown as UploadStatusResponse)
|
|
831
|
+
|
|
832
|
+
jest.spyOn(tempItemSchema, 'validate').mockReturnValue({
|
|
833
|
+
value: {
|
|
834
|
+
status: errorStatus,
|
|
835
|
+
uploadId: 'some-id'
|
|
836
|
+
},
|
|
837
|
+
error: undefined
|
|
838
|
+
} as ValidationResult)
|
|
839
|
+
|
|
840
|
+
const testController = controller as TestableFileUploadPageController
|
|
841
|
+
|
|
842
|
+
const initiateSpy = jest.spyOn(
|
|
843
|
+
testController,
|
|
844
|
+
'initiateAndStoreNewUpload'
|
|
845
|
+
) as jest.SpyInstance<
|
|
846
|
+
Promise<FormSubmissionState>,
|
|
847
|
+
[FormRequest, FormSubmissionState]
|
|
848
|
+
>
|
|
849
|
+
|
|
850
|
+
initiateSpy.mockResolvedValue(state)
|
|
851
|
+
|
|
852
|
+
const cacheService = getCacheService(request.server)
|
|
853
|
+
|
|
854
|
+
await controller['checkUploadStatus'](request, state, 1)
|
|
855
|
+
|
|
856
|
+
expect(cacheService.setFlash).toHaveBeenCalledTimes(1)
|
|
857
|
+
expect(cacheService.setFlash).toHaveBeenCalledWith(request, {
|
|
858
|
+
errors: [
|
|
859
|
+
{
|
|
860
|
+
path: ['fileUpload'],
|
|
861
|
+
href: '#fileUpload',
|
|
862
|
+
name: 'fileUpload',
|
|
863
|
+
text: 'File too large'
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
path: ['fileUpload'],
|
|
867
|
+
href: '#fileUpload',
|
|
868
|
+
name: 'fileUpload',
|
|
869
|
+
text: 'Invalid file type'
|
|
870
|
+
}
|
|
871
|
+
]
|
|
872
|
+
})
|
|
873
|
+
})
|
|
874
|
+
|
|
798
875
|
it('sets default error message when none provided', async () => {
|
|
799
876
|
const state = {
|
|
800
877
|
upload: {
|
|
@@ -859,7 +936,16 @@ describe('FileUploadPageController', () => {
|
|
|
859
936
|
|
|
860
937
|
describe('file removal', () => {
|
|
861
938
|
it('returns early when no file is removed', async () => {
|
|
862
|
-
const files = [
|
|
939
|
+
const files = [
|
|
940
|
+
{
|
|
941
|
+
uploadId: 'upload1',
|
|
942
|
+
status: { form: { file: { fileId: 'file1' } } }
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
uploadId: 'upload2',
|
|
946
|
+
status: { form: { file: { fileId: 'file2' } } }
|
|
947
|
+
}
|
|
948
|
+
]
|
|
863
949
|
|
|
864
950
|
Object.defineProperty(request, 'params', {
|
|
865
951
|
value: { itemId: 'nonexistent-file' },
|
|
@@ -892,7 +978,16 @@ describe('FileUploadPageController', () => {
|
|
|
892
978
|
})
|
|
893
979
|
|
|
894
980
|
it('merges state when file is removed', async () => {
|
|
895
|
-
const files = [
|
|
981
|
+
const files = [
|
|
982
|
+
{
|
|
983
|
+
uploadId: 'upload1',
|
|
984
|
+
status: { form: { file: { fileId: 'file1' } } }
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
uploadId: 'upload2',
|
|
988
|
+
status: { form: { file: { fileId: 'file2' } } }
|
|
989
|
+
}
|
|
990
|
+
]
|
|
896
991
|
|
|
897
992
|
Object.defineProperty(request, 'params', {
|
|
898
993
|
value: { itemId: 'file1' },
|
|
@@ -924,7 +1019,12 @@ describe('FileUploadPageController', () => {
|
|
|
924
1019
|
expect(mergeStateSpy).toHaveBeenCalledWith(request, state, {
|
|
925
1020
|
upload: {
|
|
926
1021
|
[controller.path]: {
|
|
927
|
-
files: [
|
|
1022
|
+
files: [
|
|
1023
|
+
{
|
|
1024
|
+
uploadId: 'upload2',
|
|
1025
|
+
status: { form: { file: { fileId: 'file2' } } }
|
|
1026
|
+
}
|
|
1027
|
+
],
|
|
928
1028
|
upload: {
|
|
929
1029
|
uploadId: 'upload-123',
|
|
930
1030
|
uploadUrl: 'some-url',
|
|
@@ -1121,11 +1221,15 @@ describe('FileUploadPageController', () => {
|
|
|
1121
1221
|
files: [
|
|
1122
1222
|
{
|
|
1123
1223
|
uploadId: 'file-1',
|
|
1124
|
-
status: {
|
|
1224
|
+
status: {
|
|
1225
|
+
form: { file: { fileId: 'file-1', filename: 'file-1.pdf' } }
|
|
1226
|
+
}
|
|
1125
1227
|
},
|
|
1126
1228
|
{
|
|
1127
1229
|
uploadId: 'file-2',
|
|
1128
|
-
status: {
|
|
1230
|
+
status: {
|
|
1231
|
+
form: { file: { fileId: 'file-2', filename: 'file-2.pdf' } }
|
|
1232
|
+
}
|
|
1129
1233
|
}
|
|
1130
1234
|
]
|
|
1131
1235
|
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type AnyFormRequest,
|
|
28
28
|
type FeaturedFormPageViewModel,
|
|
29
29
|
type FileState,
|
|
30
|
+
type FileUpload,
|
|
30
31
|
type FormContext,
|
|
31
32
|
type FormContextRequest,
|
|
32
33
|
type FormSubmissionError,
|
|
@@ -177,7 +178,7 @@ export class FileUploadPageController extends QuestionPageController {
|
|
|
177
178
|
const files = this.getFilesFromState(state)
|
|
178
179
|
|
|
179
180
|
const fileToRemove = files.find(
|
|
180
|
-
({
|
|
181
|
+
({ status }) => status.form.file.fileId === params.itemId
|
|
181
182
|
)
|
|
182
183
|
|
|
183
184
|
if (!fileToRemove) {
|
|
@@ -385,39 +386,86 @@ export class FileUploadPageController extends QuestionPageController {
|
|
|
385
386
|
|
|
386
387
|
// Only add to files state if the file validates.
|
|
387
388
|
// This secures against html tampering of the file input
|
|
388
|
-
//
|
|
389
|
-
// changed to a simple text field or similar.
|
|
389
|
+
// (e.g. changing it to a simple text field or similar).
|
|
390
390
|
const validationResult = tempItemSchema.validate(
|
|
391
391
|
{ uploadId, status: statusResponse },
|
|
392
392
|
{ stripUnknown: true }
|
|
393
393
|
)
|
|
394
394
|
const error = validationResult.error
|
|
395
|
-
const fileState = validationResult.value as FileState
|
|
396
395
|
|
|
397
396
|
if (error) {
|
|
398
397
|
return this.initiateAndStoreNewUpload(request, state)
|
|
399
398
|
}
|
|
400
399
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
400
|
+
// CDP returns form.file as a single object for one file,
|
|
401
|
+
// or an array for multiple files. The Joi schema normalises
|
|
402
|
+
// both to an array via .single().
|
|
403
|
+
await this.processUploadedFiles(
|
|
404
|
+
request,
|
|
405
|
+
state,
|
|
406
|
+
validationResult.value,
|
|
407
|
+
files,
|
|
408
|
+
upload
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
return this.initiateAndStoreNewUpload(request, state)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Processes the uploaded files from a CDP status response.
|
|
416
|
+
* Complete files are added to state, rejected/pending files
|
|
417
|
+
* have their error messages flashed.
|
|
418
|
+
* @param request - the hapi request
|
|
419
|
+
* @param state - the form state
|
|
420
|
+
* @param validatedItem - the Joi-validated upload item
|
|
421
|
+
* @param files - the current files array from state
|
|
422
|
+
* @param upload - the current upload initiation response
|
|
423
|
+
*/
|
|
424
|
+
private async processUploadedFiles(
|
|
425
|
+
request: AnyFormRequest,
|
|
426
|
+
state: FormSubmissionState,
|
|
427
|
+
validatedItem: FileState,
|
|
428
|
+
files: FileState[],
|
|
429
|
+
upload: UploadInitiateResponse | undefined
|
|
430
|
+
) {
|
|
431
|
+
const { uploadId } = validatedItem
|
|
432
|
+
const validatedStatus = validatedItem.status
|
|
433
|
+
const rawFile = validatedStatus.form.file as unknown as
|
|
434
|
+
| FileUpload
|
|
435
|
+
| FileUpload[]
|
|
436
|
+
const uploadedFiles = Array.isArray(rawFile) ? rawFile : [rawFile]
|
|
437
|
+
|
|
438
|
+
const allErrors: FormSubmissionError[] = []
|
|
439
|
+
|
|
440
|
+
for (const file of uploadedFiles) {
|
|
441
|
+
if (file.fileStatus === FileStatus.complete) {
|
|
442
|
+
const perFileState: FileState = {
|
|
443
|
+
uploadId,
|
|
444
|
+
status: {
|
|
445
|
+
...validatedStatus,
|
|
446
|
+
form: { file }
|
|
447
|
+
} as FileState['status']
|
|
448
|
+
}
|
|
449
|
+
files.unshift(prepareFileState(perFileState))
|
|
450
|
+
} else {
|
|
451
|
+
// Collect the error for rejected/pending files.
|
|
452
|
+
const { fileUpload } = this
|
|
453
|
+
const name = fileUpload.name
|
|
454
|
+
const text = file.errorMessage ?? 'Unknown error'
|
|
455
|
+
allErrors.push({ path: [name], href: `#${name}`, name, text })
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (allErrors.length) {
|
|
460
|
+
const cacheService = getCacheService(request.server)
|
|
461
|
+
cacheService.setFlash(request, { errors: allErrors })
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (uploadedFiles.some((f) => f.fileStatus === FileStatus.complete)) {
|
|
404
465
|
await this.mergeState(request, state, {
|
|
405
466
|
upload: { [this.path]: { files, upload } }
|
|
406
467
|
})
|
|
407
|
-
} else {
|
|
408
|
-
// Flash the error message.
|
|
409
|
-
const { fileUpload } = this
|
|
410
|
-
const cacheService = getCacheService(request.server)
|
|
411
|
-
|
|
412
|
-
const name = fileUpload.name
|
|
413
|
-
const text = file.errorMessage ?? 'Unknown error'
|
|
414
|
-
const errors: FormSubmissionError[] = [
|
|
415
|
-
{ path: [name], href: `#${name}`, name, text }
|
|
416
|
-
]
|
|
417
|
-
cacheService.setFlash(request, { errors })
|
|
418
468
|
}
|
|
419
|
-
|
|
420
|
-
return this.initiateAndStoreNewUpload(request, state)
|
|
421
469
|
}
|
|
422
470
|
|
|
423
471
|
/**
|
|
@@ -438,7 +486,7 @@ export class FileUploadPageController extends QuestionPageController {
|
|
|
438
486
|
const files = this.getFilesFromState(state)
|
|
439
487
|
|
|
440
488
|
const filesUpdated = files.filter(
|
|
441
|
-
({
|
|
489
|
+
({ status }) => status.form.file.fileId !== params.itemId
|
|
442
490
|
)
|
|
443
491
|
|
|
444
492
|
if (filesUpdated.length === files.length) {
|