@defra/forms-engine-plugin 4.3.0 → 4.5.0
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/.public/javascripts/vendor/accessible-autocomplete.min.js.map +1 -1
- package/.public/stylesheets/application.min.css +1 -1
- package/.public/stylesheets/application.min.css.map +1 -1
- package/.server/client/javascripts/file-upload.js +13 -8
- package/.server/client/javascripts/file-upload.js.map +1 -1
- package/.server/client/javascripts/geospatial-map.d.ts +189 -0
- package/.server/client/javascripts/geospatial-map.js +1068 -0
- package/.server/client/javascripts/geospatial-map.js.map +1 -0
- package/.server/client/javascripts/location-map.d.ts +6 -91
- package/.server/client/javascripts/location-map.js +78 -385
- package/.server/client/javascripts/location-map.js.map +1 -1
- package/.server/client/javascripts/map.d.ts +199 -0
- package/.server/client/javascripts/map.js +384 -0
- package/.server/client/javascripts/map.js.map +1 -0
- package/.server/client/javascripts/shared.d.ts +3 -1
- package/.server/client/javascripts/shared.js +3 -1
- package/.server/client/javascripts/shared.js.map +1 -1
- package/.server/client/stylesheets/shared.scss +7 -0
- package/.server/server/plugins/engine/components/ComponentBase.d.ts +1 -0
- package/.server/server/plugins/engine/components/ComponentBase.js +2 -0
- package/.server/server/plugins/engine/components/ComponentBase.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/components/FormComponent.d.ts +9 -1
- package/.server/server/plugins/engine/components/FormComponent.js +22 -0
- package/.server/server/plugins/engine/components/FormComponent.js.map +1 -1
- package/.server/server/plugins/engine/components/GeospatialField.d.ts +77 -0
- package/.server/server/plugins/engine/components/GeospatialField.js +102 -0
- package/.server/server/plugins/engine/components/GeospatialField.js.map +1 -0
- package/.server/server/plugins/engine/components/helpers/__stubs__/geospatial.d.ts +3 -0
- package/.server/server/plugins/engine/components/helpers/__stubs__/geospatial.js +63 -0
- package/.server/server/plugins/engine/components/helpers/__stubs__/geospatial.js.map +1 -0
- package/.server/server/plugins/engine/components/helpers/components.d.ts +1 -1
- package/.server/server/plugins/engine/components/helpers/components.js +7 -0
- package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
- package/.server/server/plugins/engine/components/helpers/geospatial.d.ts +6 -0
- package/.server/server/plugins/engine/components/helpers/geospatial.js +71 -0
- package/.server/server/plugins/engine/components/helpers/geospatial.js.map +1 -0
- package/.server/server/plugins/engine/components/helpers/geospatial.test.js +42 -0
- package/.server/server/plugins/engine/components/helpers/geospatial.test.js.map +1 -0
- package/.server/server/plugins/engine/components/index.d.ts +1 -0
- package/.server/server/plugins/engine/components/index.js +1 -0
- package/.server/server/plugins/engine/components/index.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/.server/server/plugins/engine/pageControllers/PageController.d.ts +1 -0
- package/.server/server/plugins/engine/pageControllers/PageController.js +2 -0
- package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/helpers/submission.js +13 -1
- package/.server/server/plugins/engine/pageControllers/helpers/submission.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/validationOptions.js +2 -1
- package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
- package/.server/server/plugins/engine/types.d.ts +63 -2
- package/.server/server/plugins/engine/types.js +33 -0
- package/.server/server/plugins/engine/types.js.map +1 -1
- package/.server/server/plugins/engine/views/components/geospatialfield.html +7 -0
- package/.server/server/plugins/nunjucks/context.test.js.map +1 -1
- package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
- package/.server/server/routes/types.js.map +1 -1
- package/.server/server/services/cacheService.js +3 -0
- package/.server/server/services/cacheService.js.map +1 -1
- package/package.json +9 -5
- package/src/client/javascripts/file-upload.js +12 -8
- package/src/client/javascripts/geospatial-map.js +1023 -0
- package/src/client/javascripts/location-map.js +94 -390
- package/src/client/javascripts/map.js +389 -0
- package/src/client/javascripts/shared.js +3 -1
- package/src/client/stylesheets/shared.scss +7 -0
- package/src/server/plugins/engine/components/ComponentBase.ts +2 -0
- 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/components/FormComponent.ts +29 -0
- package/src/server/plugins/engine/components/GeospatialField.test.ts +380 -0
- package/src/server/plugins/engine/components/GeospatialField.ts +145 -0
- package/src/server/plugins/engine/components/helpers/__stubs__/geospatial.ts +85 -0
- package/src/server/plugins/engine/components/helpers/components.test.ts +44 -0
- package/src/server/plugins/engine/components/helpers/components.ts +10 -0
- package/src/server/plugins/engine/components/helpers/geospatial.test.js +55 -0
- package/src/server/plugins/engine/components/helpers/geospatial.ts +93 -0
- package/src/server/plugins/engine/components/index.ts +1 -0
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.test.ts +109 -5
- package/src/server/plugins/engine/pageControllers/FileUploadPageController.ts +69 -21
- package/src/server/plugins/engine/pageControllers/PageController.ts +2 -0
- package/src/server/plugins/engine/pageControllers/helpers/submission.test.ts +74 -0
- package/src/server/plugins/engine/pageControllers/helpers/submission.ts +17 -1
- package/src/server/plugins/engine/pageControllers/validationOptions.ts +3 -1
- package/src/server/plugins/engine/types.ts +77 -4
- package/src/server/plugins/engine/views/components/geospatialfield.html +7 -0
- package/src/server/plugins/nunjucks/context.test.js +2 -3
- package/src/server/routes/types.ts +4 -2
- package/src/server/services/cacheService.ts +2 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { validState } from '~/src/server/plugins/engine/components/helpers/__stubs__/geospatial.js'
|
|
2
|
+
import { geospatialSchema } from '~/src/server/plugins/engine/components/helpers/geospatial.js'
|
|
3
|
+
|
|
4
|
+
describe('Geospatial validation helpers', () => {
|
|
5
|
+
test('it should not have errors for valid geojson object', () => {
|
|
6
|
+
const result = geospatialSchema.validate(validState)
|
|
7
|
+
|
|
8
|
+
expect(result.error).toBeUndefined()
|
|
9
|
+
expect(result.value).toBeDefined()
|
|
10
|
+
expect(result.value).toHaveLength(4)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('it should not have errors for valid geojson string', () => {
|
|
14
|
+
const result = geospatialSchema.validate(JSON.stringify(validState))
|
|
15
|
+
|
|
16
|
+
expect(result.error).toBeUndefined()
|
|
17
|
+
expect(result.value).toBeDefined()
|
|
18
|
+
expect(result.value).toHaveLength(4)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('it should have errors for invalid json string', () => {
|
|
22
|
+
const result = geospatialSchema.validate('{')
|
|
23
|
+
|
|
24
|
+
expect(result.error).toBeDefined()
|
|
25
|
+
expect(result.value).toBe('{')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('it should have errors for invalid geojson string', () => {
|
|
29
|
+
const result = geospatialSchema.validate('[')
|
|
30
|
+
|
|
31
|
+
expect(result.error).toBeDefined()
|
|
32
|
+
expect(result.value).toBe('[')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('it should validate an empty array', () => {
|
|
36
|
+
const result = geospatialSchema.validate('[]')
|
|
37
|
+
|
|
38
|
+
expect(result.error).toBeUndefined()
|
|
39
|
+
expect(result.value).toEqual([])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('it should not validate an empty object', () => {
|
|
43
|
+
const result = geospatialSchema.validate('{}')
|
|
44
|
+
|
|
45
|
+
expect(result.error).toBeDefined()
|
|
46
|
+
expect(result.value).toBeUndefined()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('it should validate an empty string', () => {
|
|
50
|
+
const result = geospatialSchema.validate('')
|
|
51
|
+
|
|
52
|
+
expect(result.error).toBeDefined()
|
|
53
|
+
expect(result.value).toBeUndefined()
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import Bourne from '@hapi/bourne'
|
|
2
|
+
import JoiBase from 'joi'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type Coordinates,
|
|
6
|
+
type Feature,
|
|
7
|
+
type FeatureProperties,
|
|
8
|
+
type Geometry
|
|
9
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
10
|
+
|
|
11
|
+
const Joi = JoiBase.extend({
|
|
12
|
+
type: 'array',
|
|
13
|
+
base: JoiBase.array(),
|
|
14
|
+
messages: {
|
|
15
|
+
'object.invalidjson': '{{#label}} must be a valid json array string'
|
|
16
|
+
},
|
|
17
|
+
coerce: {
|
|
18
|
+
from: 'string',
|
|
19
|
+
method(value, helpers) {
|
|
20
|
+
if (typeof value === 'string') {
|
|
21
|
+
if (value.trim() === '') {
|
|
22
|
+
return {
|
|
23
|
+
value: undefined
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
29
|
+
return { value: Bourne.parse(value) }
|
|
30
|
+
} catch {
|
|
31
|
+
const result = {
|
|
32
|
+
value,
|
|
33
|
+
errors: [helpers.error('object.invalidjson')]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
return {
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
41
|
+
value
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}) as JoiBase.Root
|
|
47
|
+
|
|
48
|
+
const coordinatesSchema = Joi.array<Coordinates[]>()
|
|
49
|
+
.items(Joi.number().required(), Joi.number().required())
|
|
50
|
+
.required()
|
|
51
|
+
|
|
52
|
+
const featurePropertiesSchema = Joi.object<FeatureProperties>()
|
|
53
|
+
.keys({
|
|
54
|
+
description: Joi.string().required(),
|
|
55
|
+
coordinateGridReference: Joi.string().required(),
|
|
56
|
+
centroidGridReference: Joi.string().required()
|
|
57
|
+
})
|
|
58
|
+
.required()
|
|
59
|
+
|
|
60
|
+
const featureGeometrySchema = Joi.object<Geometry>().keys({
|
|
61
|
+
type: Joi.string().valid('Point', 'LineString', 'Polygon').required(),
|
|
62
|
+
coordinates: Joi.array()
|
|
63
|
+
.when('type', {
|
|
64
|
+
switch: [
|
|
65
|
+
{ is: 'Point', then: coordinatesSchema },
|
|
66
|
+
{
|
|
67
|
+
is: 'LineString',
|
|
68
|
+
then: Joi.array().items(coordinatesSchema).min(2)
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
is: 'Polygon',
|
|
72
|
+
then: Joi.array().items(Joi.array().items(coordinatesSchema).min(3))
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
})
|
|
76
|
+
.required()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const featureSchema = Joi.object<Feature>().keys({
|
|
80
|
+
id: Joi.string().required(),
|
|
81
|
+
type: Joi.string().valid('Feature').required(),
|
|
82
|
+
properties: featurePropertiesSchema,
|
|
83
|
+
geometry: featureGeometrySchema
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
export const geospatialSchema = Joi.array<Feature[]>()
|
|
87
|
+
.items(featureSchema)
|
|
88
|
+
.unique('id')
|
|
89
|
+
.required()
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @import { CustomHelpers } from 'joi'
|
|
93
|
+
*/
|
|
@@ -30,3 +30,4 @@ export { NationalGridFieldNumberField } from '~/src/server/plugins/engine/compon
|
|
|
30
30
|
export { LatLongField } from '~/src/server/plugins/engine/components/LatLongField.js'
|
|
31
31
|
export { HiddenField } from '~/src/server/plugins/engine/components/HiddenField.js'
|
|
32
32
|
export { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'
|
|
33
|
+
export { GeospatialField } from '~/src/server/plugins/engine/components/GeospatialField.js'
|
|
@@ -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) {
|
|
@@ -37,6 +37,7 @@ export class PageController {
|
|
|
37
37
|
name?: string
|
|
38
38
|
model: FormModel
|
|
39
39
|
pageDef: Page
|
|
40
|
+
id?: string
|
|
40
41
|
title: string
|
|
41
42
|
section?: Section
|
|
42
43
|
condition?: ExecutableCondition
|
|
@@ -52,6 +53,7 @@ export class PageController {
|
|
|
52
53
|
this.name = def.name
|
|
53
54
|
this.model = model
|
|
54
55
|
this.pageDef = pageDef
|
|
56
|
+
this.id = pageDef.id
|
|
55
57
|
this.title = pageDef.title
|
|
56
58
|
this.events = pageDef.events
|
|
57
59
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { GeospatialField } from '~/src/server/plugins/engine/components/GeospatialField.js'
|
|
1
2
|
import { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'
|
|
2
3
|
import { TextField } from '~/src/server/plugins/engine/components/TextField.js'
|
|
4
|
+
import { validSingleState } from '~/src/server/plugins/engine/components/helpers/__stubs__/geospatial.js'
|
|
3
5
|
import { type DetailItemField } from '~/src/server/plugins/engine/models/types.js'
|
|
4
6
|
import {
|
|
5
7
|
buildMainRecords,
|
|
@@ -242,6 +244,34 @@ describe('Submission helpers', () => {
|
|
|
242
244
|
|
|
243
245
|
expect(result).toEqual([])
|
|
244
246
|
})
|
|
247
|
+
|
|
248
|
+
it('should JSON stringify GeospatialField', () => {
|
|
249
|
+
const mockGeospatialField = Object.create(GeospatialField.prototype)
|
|
250
|
+
mockGeospatialField.name = 'geospatial'
|
|
251
|
+
|
|
252
|
+
const items = [
|
|
253
|
+
{
|
|
254
|
+
name: 'geospatial',
|
|
255
|
+
label: 'Site features',
|
|
256
|
+
field: mockGeospatialField,
|
|
257
|
+
state: {
|
|
258
|
+
geospatial: validSingleState
|
|
259
|
+
} as FormSubmissionState
|
|
260
|
+
}
|
|
261
|
+
] as unknown as DetailItemField[]
|
|
262
|
+
|
|
263
|
+
const result = buildMainRecords(items)
|
|
264
|
+
|
|
265
|
+
expect(result).toHaveLength(1)
|
|
266
|
+
expect(result).toEqual([
|
|
267
|
+
{
|
|
268
|
+
name: 'geospatial',
|
|
269
|
+
title: 'Site features',
|
|
270
|
+
value:
|
|
271
|
+
'[{"type":"Feature","properties":{"description":"My farm house","coordinateGridReference":"ST 00001","centroidGridReference":"ST 00001"},"geometry":{"coordinates":[-2.5723699109417737,53.2380485215034],"type":"Point"},"id":"a"}]'
|
|
272
|
+
}
|
|
273
|
+
])
|
|
274
|
+
})
|
|
245
275
|
})
|
|
246
276
|
|
|
247
277
|
describe('buildRepeaterRecords', () => {
|
|
@@ -295,5 +325,49 @@ describe('Submission helpers', () => {
|
|
|
295
325
|
expect(result[0].title).toBe('Addresses')
|
|
296
326
|
expect(result[0].value).toHaveLength(1)
|
|
297
327
|
})
|
|
328
|
+
|
|
329
|
+
it('should JSON stringify GeospatialField', () => {
|
|
330
|
+
const mockGeospatialField = Object.create(GeospatialField.prototype)
|
|
331
|
+
mockGeospatialField.name = 'geospatial'
|
|
332
|
+
|
|
333
|
+
const items = [
|
|
334
|
+
{
|
|
335
|
+
name: 'features',
|
|
336
|
+
label: 'Site features repeater',
|
|
337
|
+
subItems: [
|
|
338
|
+
[
|
|
339
|
+
{
|
|
340
|
+
name: 'geospatial',
|
|
341
|
+
label: 'Site features',
|
|
342
|
+
field: mockGeospatialField,
|
|
343
|
+
state: {
|
|
344
|
+
geospatial: validSingleState
|
|
345
|
+
} as FormSubmissionState
|
|
346
|
+
} as unknown as DetailItemField[]
|
|
347
|
+
]
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
] as unknown as DetailItemField[]
|
|
351
|
+
|
|
352
|
+
const result = buildRepeaterRecords(items)
|
|
353
|
+
|
|
354
|
+
expect(result).toHaveLength(1)
|
|
355
|
+
expect(result).toEqual([
|
|
356
|
+
{
|
|
357
|
+
name: 'features',
|
|
358
|
+
title: 'Site features repeater',
|
|
359
|
+
value: [
|
|
360
|
+
[
|
|
361
|
+
{
|
|
362
|
+
name: 'geospatial',
|
|
363
|
+
title: 'Site features',
|
|
364
|
+
value:
|
|
365
|
+
'[{"type":"Feature","properties":{"description":"My farm house","coordinateGridReference":"ST 00001","centroidGridReference":"ST 00001"},"geometry":{"coordinates":[-2.5723699109417737,53.2380485215034],"type":"Point"},"id":"a"}]'
|
|
366
|
+
}
|
|
367
|
+
]
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
])
|
|
371
|
+
})
|
|
298
372
|
})
|
|
299
373
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type SubmitPayload } from '@defra/forms-model'
|
|
2
2
|
|
|
3
|
+
import { GeospatialField } from '~/src/server/plugins/engine/components/GeospatialField.js'
|
|
3
4
|
import { PaymentField } from '~/src/server/plugins/engine/components/PaymentField.js'
|
|
4
5
|
import { getAnswer } from '~/src/server/plugins/engine/components/helpers/components.js'
|
|
5
6
|
import {
|
|
@@ -32,6 +33,14 @@ export function buildMainRecords(items: DetailItem[]): SubmitRecord[] {
|
|
|
32
33
|
for (const item of fieldItems) {
|
|
33
34
|
if (item.field instanceof PaymentField) {
|
|
34
35
|
records.push(...buildPaymentRecords(item))
|
|
36
|
+
} else if (item.field instanceof GeospatialField) {
|
|
37
|
+
// Stringify of GeoJSON is done here rather than inside `getContextValueFromState`
|
|
38
|
+
// so we don't incur the overhead of JSON.stringify on every request when building context
|
|
39
|
+
records.push({
|
|
40
|
+
name: item.name,
|
|
41
|
+
title: item.label,
|
|
42
|
+
value: JSON.stringify(item.field.getFormValueFromState(item.state))
|
|
43
|
+
})
|
|
35
44
|
} else {
|
|
36
45
|
records.push({
|
|
37
46
|
name: item.name,
|
|
@@ -103,7 +112,14 @@ export function buildRepeaterRecords(
|
|
|
103
112
|
detailItems.map((subItem) => ({
|
|
104
113
|
name: subItem.name,
|
|
105
114
|
title: subItem.label,
|
|
106
|
-
value:
|
|
115
|
+
value:
|
|
116
|
+
// Stringify of GeoJSON is done here rather than inside `getContextValueFromState`
|
|
117
|
+
// so we don't incur the overhead of JSON.stringify on every request when building context
|
|
118
|
+
subItem.field instanceof GeospatialField
|
|
119
|
+
? JSON.stringify(
|
|
120
|
+
subItem.field.getFormValueFromState(subItem.state)
|
|
121
|
+
)
|
|
122
|
+
: getAnswer(subItem.field, subItem.state, { format: 'data' })
|
|
107
123
|
}))
|
|
108
124
|
)
|
|
109
125
|
}))
|
|
@@ -89,7 +89,9 @@ export const messages: LanguageMessagesExt = {
|
|
|
89
89
|
'date.base': messageTemplate.dateFormat,
|
|
90
90
|
'date.format': messageTemplate.dateFormat,
|
|
91
91
|
'date.min': messageTemplate.dateMin,
|
|
92
|
-
'date.max': messageTemplate.dateMax
|
|
92
|
+
'date.max': messageTemplate.dateMax,
|
|
93
|
+
|
|
94
|
+
'object.invalidjson': messageTemplate.format
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
export const messagesPre: LanguageMessages =
|