@defra/forms-engine-plugin 4.2.0 → 4.4.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.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/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/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/models/FormModel.js +0 -4
- package/.server/server/plugins/engine/models/FormModel.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/QuestionPageController.js +2 -4
- package/.server/server/plugins/engine/pageControllers/QuestionPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/helpers/state.d.ts +8 -7
- package/.server/server/plugins/engine/pageControllers/helpers/state.js +39 -12
- package/.server/server/plugins/engine/pageControllers/helpers/state.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/routes/index.js +8 -1
- package/.server/server/plugins/engine/routes/index.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/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/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/models/FormModel.ts +0 -4
- package/src/server/plugins/engine/pageControllers/PageController.ts +2 -0
- package/src/server/plugins/engine/pageControllers/QuestionPageController.ts +2 -6
- package/src/server/plugins/engine/pageControllers/helpers/state.test.ts +80 -16
- package/src/server/plugins/engine/pageControllers/helpers/state.ts +57 -17
- 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/routes/index.test.ts +4 -2
- package/src/server/plugins/engine/routes/index.ts +13 -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'
|
|
@@ -48,7 +48,6 @@ import {
|
|
|
48
48
|
createPage,
|
|
49
49
|
type PageControllerClass
|
|
50
50
|
} from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
|
|
51
|
-
import { copyNotYetValidatedState } from '~/src/server/plugins/engine/pageControllers/helpers/state.js'
|
|
52
51
|
import { validationOptions as opts } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
|
|
53
52
|
import * as defaultServices from '~/src/server/plugins/engine/services/index.js'
|
|
54
53
|
import {
|
|
@@ -402,9 +401,6 @@ export class FormModel {
|
|
|
402
401
|
// Add paths for navigation
|
|
403
402
|
this.assignPaths(context)
|
|
404
403
|
|
|
405
|
-
// Handle restoration of payload from say a 'save-and-exit' request
|
|
406
|
-
copyNotYetValidatedState(request, context)
|
|
407
|
-
|
|
408
404
|
return context
|
|
409
405
|
}
|
|
410
406
|
|
|
@@ -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
|
|
|
@@ -31,10 +31,7 @@ import {
|
|
|
31
31
|
} from '~/src/server/plugins/engine/helpers.js'
|
|
32
32
|
import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
|
|
33
33
|
import { PageController } from '~/src/server/plugins/engine/pageControllers/PageController.js'
|
|
34
|
-
import {
|
|
35
|
-
clearNotYetValidatedState,
|
|
36
|
-
prefillStateFromQueryParameters
|
|
37
|
-
} from '~/src/server/plugins/engine/pageControllers/helpers/state.js'
|
|
34
|
+
import { prefillStateFromQueryParameters } from '~/src/server/plugins/engine/pageControllers/helpers/state.js'
|
|
38
35
|
import {
|
|
39
36
|
type AnyFormRequest,
|
|
40
37
|
type FormContext,
|
|
@@ -342,8 +339,7 @@ export class QuestionPageController extends PageController {
|
|
|
342
339
|
|
|
343
340
|
const cacheService = getCacheService(request.server)
|
|
344
341
|
|
|
345
|
-
|
|
346
|
-
return cacheService.setState(request, clearNotYetValidatedState(state))
|
|
342
|
+
return cacheService.setState(request, state)
|
|
347
343
|
}
|
|
348
344
|
|
|
349
345
|
async mergeState(
|
|
@@ -1,19 +1,27 @@
|
|
|
1
|
-
import { ComponentType, type Page } from '@defra/forms-model'
|
|
1
|
+
import { ComponentType, ControllerType, type Page } from '@defra/forms-model'
|
|
2
2
|
|
|
3
3
|
import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
4
4
|
import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
|
|
5
5
|
import {
|
|
6
|
+
checkSaveAndExitRepeater,
|
|
6
7
|
copyNotYetValidatedState,
|
|
7
8
|
prefillStateFromQueryParameters,
|
|
8
9
|
stripParam
|
|
9
10
|
} from '~/src/server/plugins/engine/pageControllers/helpers/state.js'
|
|
10
11
|
import {
|
|
11
12
|
type AnyFormRequest,
|
|
12
|
-
type FormContext
|
|
13
|
-
type FormContextRequest
|
|
13
|
+
type FormContext
|
|
14
14
|
} from '~/src/server/plugins/engine/types.js'
|
|
15
15
|
import { type FormsService, type Services } from '~/src/server/types.js'
|
|
16
16
|
|
|
17
|
+
const mockGetCacheService = jest.fn()
|
|
18
|
+
const mockCacheService = { setState: jest.fn() }
|
|
19
|
+
|
|
20
|
+
jest.mock('~/src/server/plugins/engine/helpers.ts', () => ({
|
|
21
|
+
__esModule: true,
|
|
22
|
+
getCacheService: (...args: unknown[]) => mockGetCacheService(...args)
|
|
23
|
+
}))
|
|
24
|
+
|
|
17
25
|
function buildMockPage(
|
|
18
26
|
pagesOverride = {},
|
|
19
27
|
stateOverride = {},
|
|
@@ -225,23 +233,26 @@ describe('State helpers', () => {
|
|
|
225
233
|
})
|
|
226
234
|
|
|
227
235
|
describe('copyNotYetValidatedState', () => {
|
|
228
|
-
|
|
229
|
-
|
|
236
|
+
beforeEach(() => {
|
|
237
|
+
mockGetCacheService.mockReturnValue(mockCacheService)
|
|
238
|
+
})
|
|
239
|
+
it('should ignore if no invalid state', async () => {
|
|
240
|
+
const mockRequest = {} as AnyFormRequest
|
|
230
241
|
const mockContext = {
|
|
231
242
|
state: { abc: '123' },
|
|
232
243
|
payload: {}
|
|
233
244
|
} as unknown as FormContext
|
|
234
|
-
copyNotYetValidatedState(mockRequest, mockContext)
|
|
245
|
+
await copyNotYetValidatedState(mockRequest, mockContext)
|
|
235
246
|
expect(mockContext.state).toEqual({ abc: '123' })
|
|
236
247
|
expect(mockContext.payload).toEqual({})
|
|
237
248
|
})
|
|
238
249
|
|
|
239
|
-
it('should ignore if wrong path', () => {
|
|
250
|
+
it('should ignore if wrong path', async () => {
|
|
240
251
|
const mockRequest = {
|
|
241
252
|
url: {
|
|
242
253
|
pathname: '/form-page1'
|
|
243
254
|
}
|
|
244
|
-
} as unknown as
|
|
255
|
+
} as unknown as AnyFormRequest
|
|
245
256
|
const mockContext = {
|
|
246
257
|
state: {
|
|
247
258
|
abc: '123',
|
|
@@ -252,7 +263,7 @@ describe('State helpers', () => {
|
|
|
252
263
|
},
|
|
253
264
|
payload: {}
|
|
254
265
|
} as unknown as FormContext
|
|
255
|
-
copyNotYetValidatedState(mockRequest, mockContext)
|
|
266
|
+
await copyNotYetValidatedState(mockRequest, mockContext)
|
|
256
267
|
expect(mockContext.state).toEqual({
|
|
257
268
|
abc: '123',
|
|
258
269
|
__stateNotYetValidated: {
|
|
@@ -263,12 +274,12 @@ describe('State helpers', () => {
|
|
|
263
274
|
expect(mockContext.payload).toEqual({})
|
|
264
275
|
})
|
|
265
276
|
|
|
266
|
-
it('should apply if correct path', () => {
|
|
277
|
+
it('should apply if correct path', async () => {
|
|
267
278
|
const mockRequest = {
|
|
268
279
|
url: {
|
|
269
280
|
pathname: '/form-page1'
|
|
270
281
|
}
|
|
271
|
-
} as unknown as
|
|
282
|
+
} as unknown as AnyFormRequest
|
|
272
283
|
const mockContext = {
|
|
273
284
|
state: {
|
|
274
285
|
abc: '123',
|
|
@@ -279,17 +290,70 @@ describe('State helpers', () => {
|
|
|
279
290
|
},
|
|
280
291
|
payload: {}
|
|
281
292
|
} as unknown as FormContext
|
|
282
|
-
copyNotYetValidatedState(mockRequest, mockContext)
|
|
293
|
+
await copyNotYetValidatedState(mockRequest, mockContext)
|
|
283
294
|
expect(mockContext.state).toEqual({
|
|
284
295
|
abc: '123',
|
|
285
|
-
__stateNotYetValidated:
|
|
286
|
-
def: '456',
|
|
287
|
-
__currentPagePath: '/form-page1'
|
|
288
|
-
}
|
|
296
|
+
__stateNotYetValidated: undefined
|
|
289
297
|
})
|
|
290
298
|
expect(mockContext.payload).toEqual({
|
|
291
299
|
def: '456'
|
|
292
300
|
})
|
|
293
301
|
})
|
|
294
302
|
})
|
|
303
|
+
|
|
304
|
+
describe('checkSaveAndExitRepeater', () => {
|
|
305
|
+
function createMockContextWithPath(path: string) {
|
|
306
|
+
return {
|
|
307
|
+
state: {
|
|
308
|
+
abc: '123',
|
|
309
|
+
__stateNotYetValidated: {
|
|
310
|
+
def: '456',
|
|
311
|
+
__currentPagePath: path
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
payload: {}
|
|
315
|
+
} as unknown as FormContext
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const mockModel = {
|
|
319
|
+
def: {
|
|
320
|
+
pages: [
|
|
321
|
+
{
|
|
322
|
+
controller: ControllerType.Repeat,
|
|
323
|
+
path: '/personal_details'
|
|
324
|
+
}
|
|
325
|
+
]
|
|
326
|
+
},
|
|
327
|
+
basePath: 'form/preview/draft/repeater-test'
|
|
328
|
+
} as unknown as FormModel
|
|
329
|
+
|
|
330
|
+
it('should return undefined if url does not end in a guid', () => {
|
|
331
|
+
const mockContext = createMockContextWithPath(
|
|
332
|
+
'/form/preview/draft/repeater-test/personal_details'
|
|
333
|
+
)
|
|
334
|
+
expect(checkSaveAndExitRepeater(mockContext, mockModel)).toBeUndefined()
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
it('should return undefined if url ends in a guid but not a repeater path', () => {
|
|
338
|
+
const mockContext = createMockContextWithPath(
|
|
339
|
+
'/form/preview/draft/repeater-test/wrong_page/7d27fe6e-73e8-4265-84bd-1e118c92470b'
|
|
340
|
+
)
|
|
341
|
+
expect(checkSaveAndExitRepeater(mockContext, mockModel)).toBeUndefined()
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('should return undefined if url is not a string', () => {
|
|
345
|
+
// @ts-expect-error - invalid dataype on purpose for this test
|
|
346
|
+
const mockContext = createMockContextWithPath({})
|
|
347
|
+
expect(checkSaveAndExitRepeater(mockContext, mockModel)).toBeUndefined()
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('should return correct urls if url ends in a guid and is a repeater path', () => {
|
|
351
|
+
const mockContext = createMockContextWithPath(
|
|
352
|
+
'/form/preview/draft/repeater-test/personal_details/7d27fe6e-73e8-4265-84bd-1e118c92470b'
|
|
353
|
+
)
|
|
354
|
+
expect(checkSaveAndExitRepeater(mockContext, mockModel)).toBe(
|
|
355
|
+
'/form/preview/draft/repeater-test/personal_details/7d27fe6e-73e8-4265-84bd-1e118c92470b'
|
|
356
|
+
)
|
|
357
|
+
})
|
|
358
|
+
})
|
|
295
359
|
})
|
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
import { getHiddenFields } from '@defra/forms-model'
|
|
1
|
+
import { ControllerType, getHiddenFields } from '@defra/forms-model'
|
|
2
|
+
import { validate as isValidUUID } from 'uuid'
|
|
2
3
|
|
|
4
|
+
import { getCacheService } from '~/src/server/plugins/engine/helpers.js'
|
|
3
5
|
import {
|
|
4
6
|
CURRENT_PAGE_PATH_KEY,
|
|
5
7
|
STATE_NOT_YET_VALIDATED
|
|
6
8
|
} from '~/src/server/plugins/engine/index.js'
|
|
9
|
+
import { type FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
7
10
|
import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
|
|
8
11
|
import {
|
|
9
12
|
type AnyFormRequest,
|
|
10
13
|
type FormContext,
|
|
11
|
-
type FormContextRequest,
|
|
12
14
|
type FormStateValue,
|
|
13
|
-
type FormSubmissionState,
|
|
14
15
|
type FormValue
|
|
15
16
|
} from '~/src/server/plugins/engine/types.js'
|
|
16
17
|
import { type FormQuery } from '~/src/server/routes/types.js'
|
|
17
18
|
import { type Services } from '~/src/server/types.js'
|
|
18
19
|
|
|
20
|
+
const GUID_LENGTH = 36
|
|
21
|
+
|
|
19
22
|
/**
|
|
20
23
|
* A series of functions that can transform a pre-fill input parameter e.g lookup a form title based on form id
|
|
21
24
|
*/
|
|
@@ -100,14 +103,56 @@ export async function prefillStateFromQueryParameters(
|
|
|
100
103
|
return true
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Checks whether the save-and-exit finished on a repeater with partial state
|
|
108
|
+
* @param context - the form context
|
|
109
|
+
*/
|
|
110
|
+
export function checkSaveAndExitRepeater(
|
|
111
|
+
context: FormContext,
|
|
112
|
+
model: FormModel
|
|
113
|
+
) {
|
|
114
|
+
const potentiallyInvalidState = context.state[STATE_NOT_YET_VALIDATED] as
|
|
115
|
+
| Record<string, FormValue>
|
|
116
|
+
| undefined
|
|
117
|
+
if (!potentiallyInvalidState) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const originalPath = potentiallyInvalidState[CURRENT_PAGE_PATH_KEY]
|
|
122
|
+
|
|
123
|
+
const repeaterPaths = model.def.pages
|
|
124
|
+
.filter((page) => page.controller === ControllerType.Repeat)
|
|
125
|
+
.map((p) => `/${model.basePath}${p.path}/`)
|
|
126
|
+
|
|
127
|
+
if (typeof originalPath !== 'string') {
|
|
128
|
+
return undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const segments = originalPath.split('/')
|
|
132
|
+
const lastSegment = segments.at(-1) ?? ''
|
|
133
|
+
|
|
134
|
+
if (!isValidUUID(lastSegment)) {
|
|
135
|
+
return undefined
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const guidStartIndex = originalPath.length - GUID_LENGTH
|
|
139
|
+
const originalPathWithoutGuid = originalPath.substring(0, guidStartIndex)
|
|
140
|
+
|
|
141
|
+
if (!repeaterPaths.includes(originalPathWithoutGuid)) {
|
|
142
|
+
return undefined
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return originalPath
|
|
146
|
+
}
|
|
147
|
+
|
|
103
148
|
/**
|
|
104
149
|
* Copies any potentially invalid state into the payload, and removes those values from state
|
|
105
150
|
* NOTE - this method has a side-effect on 'context.state' and 'context.payload'
|
|
106
151
|
* @param request - the form request
|
|
107
152
|
* @param context - the form context
|
|
108
153
|
*/
|
|
109
|
-
export function copyNotYetValidatedState(
|
|
110
|
-
request:
|
|
154
|
+
export async function copyNotYetValidatedState(
|
|
155
|
+
request: AnyFormRequest,
|
|
111
156
|
context: FormContext
|
|
112
157
|
) {
|
|
113
158
|
const potentiallyInvalidState = context.state[STATE_NOT_YET_VALIDATED] as
|
|
@@ -125,18 +170,13 @@ export function copyNotYetValidatedState(
|
|
|
125
170
|
...potentiallyInvalidState,
|
|
126
171
|
[CURRENT_PAGE_PATH_KEY]: undefined
|
|
127
172
|
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
173
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (state[STATE_NOT_YET_VALIDATED]) {
|
|
139
|
-
state[STATE_NOT_YET_VALIDATED] = undefined
|
|
174
|
+
// Remove any temporary 'not yet validated' state now it's been copied to the payload
|
|
175
|
+
if (context.state[STATE_NOT_YET_VALIDATED]) {
|
|
176
|
+
context.state[STATE_NOT_YET_VALIDATED] = undefined
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const cacheService = getCacheService(request.server)
|
|
180
|
+
await cacheService.setState(request, context.state)
|
|
140
181
|
}
|
|
141
|
-
return state
|
|
142
182
|
}
|
|
@@ -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 =
|
|
@@ -86,7 +86,8 @@ describe('redirectOrMakeHandler', () => {
|
|
|
86
86
|
// Reset mock model
|
|
87
87
|
mockModel.getFormContext = jest.fn().mockReturnValue({
|
|
88
88
|
isForceAccess: false,
|
|
89
|
-
data: {}
|
|
89
|
+
data: {},
|
|
90
|
+
state: {}
|
|
90
91
|
})
|
|
91
92
|
|
|
92
93
|
// Setup mocks
|
|
@@ -225,7 +226,8 @@ describe('redirectOrMakeHandler', () => {
|
|
|
225
226
|
it('should call makeHandler when context has force access', async () => {
|
|
226
227
|
mockModel.getFormContext = jest.fn().mockReturnValue({
|
|
227
228
|
isForceAccess: true,
|
|
228
|
-
data: {}
|
|
229
|
+
data: {},
|
|
230
|
+
state: {}
|
|
229
231
|
})
|
|
230
232
|
|
|
231
233
|
await redirectOrMakeHandler(
|