@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,380 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ComponentType,
|
|
3
|
+
type GeospatialFieldComponent
|
|
4
|
+
} from '@defra/forms-model'
|
|
5
|
+
|
|
6
|
+
import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js'
|
|
7
|
+
import { type GeospatialField } from '~/src/server/plugins/engine/components/GeospatialField.js'
|
|
8
|
+
import {
|
|
9
|
+
validSingleState,
|
|
10
|
+
validState
|
|
11
|
+
} from '~/src/server/plugins/engine/components/helpers/__stubs__/geospatial.js'
|
|
12
|
+
import {
|
|
13
|
+
getAnswer,
|
|
14
|
+
type Field
|
|
15
|
+
} from '~/src/server/plugins/engine/components/helpers/components.js'
|
|
16
|
+
import { FormModel } from '~/src/server/plugins/engine/models/FormModel.js'
|
|
17
|
+
import { type GeospatialState } from '~/src/server/plugins/engine/types.js'
|
|
18
|
+
import definition from '~/test/form/definitions/blank.js'
|
|
19
|
+
import { getFormData, getFormState } from '~/test/helpers/component-helpers.js'
|
|
20
|
+
|
|
21
|
+
describe('GeospatialField', () => {
|
|
22
|
+
let model: FormModel
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
model = new FormModel(definition, {
|
|
26
|
+
basePath: 'test'
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('Defaults', () => {
|
|
31
|
+
let def: GeospatialFieldComponent
|
|
32
|
+
let collection: ComponentCollection
|
|
33
|
+
let field: Field
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
def = {
|
|
37
|
+
title: 'Example geospatial title',
|
|
38
|
+
shortDescription: 'Example geospatial',
|
|
39
|
+
name: 'myComponent',
|
|
40
|
+
type: ComponentType.GeospatialField,
|
|
41
|
+
options: {}
|
|
42
|
+
} satisfies GeospatialFieldComponent
|
|
43
|
+
|
|
44
|
+
collection = new ComponentCollection([def], { model })
|
|
45
|
+
field = collection.fields[0]
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('Schema', () => {
|
|
49
|
+
it('uses component short description as label', () => {
|
|
50
|
+
const { formSchema } = collection
|
|
51
|
+
const { keys } = formSchema.describe()
|
|
52
|
+
|
|
53
|
+
expect(keys).toHaveProperty(
|
|
54
|
+
'myComponent',
|
|
55
|
+
expect.objectContaining({
|
|
56
|
+
flags: expect.objectContaining({
|
|
57
|
+
label: 'Example geospatial'
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('uses component name as keys', () => {
|
|
64
|
+
const { formSchema } = collection
|
|
65
|
+
const { keys } = formSchema.describe()
|
|
66
|
+
|
|
67
|
+
expect(field.keys).toEqual(['myComponent'])
|
|
68
|
+
expect(field.collection).toBeUndefined()
|
|
69
|
+
|
|
70
|
+
for (const key of field.keys) {
|
|
71
|
+
expect(keys).toHaveProperty(key)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('is required by default', () => {
|
|
76
|
+
const { formSchema } = collection
|
|
77
|
+
const { keys } = formSchema.describe()
|
|
78
|
+
|
|
79
|
+
expect(keys).toHaveProperty(
|
|
80
|
+
'myComponent',
|
|
81
|
+
expect.objectContaining({
|
|
82
|
+
flags: expect.objectContaining({
|
|
83
|
+
presence: 'required'
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('is optional when configured', () => {
|
|
90
|
+
const collectionOptional = new ComponentCollection(
|
|
91
|
+
[{ ...def, options: { required: false } }],
|
|
92
|
+
{ model }
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const result = collectionOptional.validate(getFormData('[]'))
|
|
96
|
+
expect(result.errors).toBeUndefined()
|
|
97
|
+
|
|
98
|
+
const result2 = collectionOptional.validate(getFormData([]))
|
|
99
|
+
expect(result2.errors).toBeUndefined()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('accepts valid values', () => {
|
|
103
|
+
const result1 = collection.validate(getFormData(validState))
|
|
104
|
+
|
|
105
|
+
expect(result1.errors).toBeUndefined()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('adds errors for empty value', () => {
|
|
109
|
+
const result = collection.validate(getFormData(''))
|
|
110
|
+
|
|
111
|
+
expect(result.errors).toEqual([
|
|
112
|
+
expect.objectContaining({
|
|
113
|
+
text: 'Select example geospatial'
|
|
114
|
+
})
|
|
115
|
+
])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('adds errors for empty value given no short description', () => {
|
|
119
|
+
def = {
|
|
120
|
+
title: 'Example geospatial title',
|
|
121
|
+
name: 'myComponent',
|
|
122
|
+
type: ComponentType.GeospatialField,
|
|
123
|
+
options: {}
|
|
124
|
+
} satisfies GeospatialFieldComponent
|
|
125
|
+
|
|
126
|
+
collection = new ComponentCollection([def], { model })
|
|
127
|
+
const result = collection.validate(getFormData(''))
|
|
128
|
+
|
|
129
|
+
expect(result.errors).toEqual([
|
|
130
|
+
expect.objectContaining({
|
|
131
|
+
text: 'Select example geospatial title'
|
|
132
|
+
})
|
|
133
|
+
])
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('adds errors for invalid values', () => {
|
|
137
|
+
const result1 = collection.validate(getFormData(['invalid']))
|
|
138
|
+
const result2 = collection.validate(
|
|
139
|
+
// @ts-expect-error - Allow invalid param for test
|
|
140
|
+
getFormData({ unknown: 'invalid' })
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
expect(result1.errors).toBeTruthy()
|
|
144
|
+
expect(result2.errors).toBeTruthy()
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('State', () => {
|
|
149
|
+
it('returns text from single feature state', () => {
|
|
150
|
+
const state1 = getFormState(validSingleState)
|
|
151
|
+
const state2 = getFormState(null)
|
|
152
|
+
|
|
153
|
+
const answer1 = getAnswer(field, state1)
|
|
154
|
+
const answer2 = getAnswer(field, state2)
|
|
155
|
+
|
|
156
|
+
expect(answer1).toBe('Added 1 location')
|
|
157
|
+
expect(answer2).toBe('')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('returns text from multiple features state', () => {
|
|
161
|
+
const state1 = getFormState(validState)
|
|
162
|
+
const state2 = getFormState(null)
|
|
163
|
+
|
|
164
|
+
const answer1 = getAnswer(field, state1)
|
|
165
|
+
const answer2 = getAnswer(field, state2)
|
|
166
|
+
|
|
167
|
+
expect(answer1).toBe('Added 4 locations')
|
|
168
|
+
expect(answer2).toBe('')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('returns payload from state', () => {
|
|
172
|
+
const state1 = getFormState(validState)
|
|
173
|
+
const state2 = getFormState(null)
|
|
174
|
+
|
|
175
|
+
const payload1 = field.getFormDataFromState(state1)
|
|
176
|
+
const payload2 = field.getFormDataFromState(state2)
|
|
177
|
+
|
|
178
|
+
expect(payload1).toEqual(getFormData(validState))
|
|
179
|
+
expect(payload2).toEqual(getFormData())
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('returns value from state', () => {
|
|
183
|
+
const state1 = getFormState(validState)
|
|
184
|
+
const state2 = getFormState(null)
|
|
185
|
+
|
|
186
|
+
const value1 = field.getFormValueFromState(state1)
|
|
187
|
+
const value2 = field.getFormValueFromState(state2)
|
|
188
|
+
|
|
189
|
+
expect(value1).toBe(validState)
|
|
190
|
+
expect(value2).toBeUndefined()
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('returns context for conditions and form submission', () => {
|
|
194
|
+
const state1 = getFormState(validState)
|
|
195
|
+
const state2 = getFormState(null)
|
|
196
|
+
|
|
197
|
+
const value1 = field.getContextValueFromState(state1)
|
|
198
|
+
const value2 = field.getContextValueFromState(state2)
|
|
199
|
+
|
|
200
|
+
const { id: id1 } = validState[0]
|
|
201
|
+
const { id: id2 } = validState[1]
|
|
202
|
+
const { id: id3 } = validState[2]
|
|
203
|
+
const { id: id4 } = validState[3]
|
|
204
|
+
|
|
205
|
+
expect(value1).toEqual([id1, id2, id3, id4])
|
|
206
|
+
expect(value2).toBeNull()
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('returns state from payload', () => {
|
|
210
|
+
const payload1 = getFormData(validState)
|
|
211
|
+
const payload2 = getFormData()
|
|
212
|
+
|
|
213
|
+
const value1 = field.getStateFromValidForm(payload1)
|
|
214
|
+
const value2 = field.getStateFromValidForm(payload2)
|
|
215
|
+
|
|
216
|
+
expect(value1).toEqual(getFormState(validState))
|
|
217
|
+
expect(value2).toEqual(getFormState(null))
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
describe('View model', () => {
|
|
222
|
+
it('sets Nunjucks component defaults', () => {
|
|
223
|
+
const viewModel = field.getViewModel(getFormData('Geospatial'))
|
|
224
|
+
|
|
225
|
+
expect(viewModel).toEqual(
|
|
226
|
+
expect.objectContaining({
|
|
227
|
+
label: { text: def.title },
|
|
228
|
+
name: 'myComponent',
|
|
229
|
+
id: 'myComponent',
|
|
230
|
+
value: 'Geospatial'
|
|
231
|
+
})
|
|
232
|
+
)
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
describe('AllPossibleErrors', () => {
|
|
237
|
+
it('should return errors', () => {
|
|
238
|
+
const errors = field.getAllPossibleErrors()
|
|
239
|
+
expect(errors.baseErrors).not.toBeEmpty()
|
|
240
|
+
expect(errors.advancedSettingsErrors).toBeEmpty()
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
describe('Validation', () => {
|
|
246
|
+
describe.each([
|
|
247
|
+
{
|
|
248
|
+
description: 'Required',
|
|
249
|
+
component: {
|
|
250
|
+
title: 'Example geospatial field',
|
|
251
|
+
name: 'myComponent',
|
|
252
|
+
type: ComponentType.GeospatialField,
|
|
253
|
+
options: {
|
|
254
|
+
required: true
|
|
255
|
+
}
|
|
256
|
+
} satisfies GeospatialFieldComponent,
|
|
257
|
+
assertions: [
|
|
258
|
+
{
|
|
259
|
+
input: getFormData([]),
|
|
260
|
+
output: {
|
|
261
|
+
value: getFormData([]),
|
|
262
|
+
errors: [
|
|
263
|
+
expect.objectContaining({
|
|
264
|
+
text: 'Example geospatial field must contain at least 1 items'
|
|
265
|
+
})
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
input: getFormData(),
|
|
271
|
+
output: {
|
|
272
|
+
value: getFormData(),
|
|
273
|
+
errors: [
|
|
274
|
+
expect.objectContaining({
|
|
275
|
+
text: 'Select example geospatial field'
|
|
276
|
+
})
|
|
277
|
+
]
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
input: getFormData(validSingleState),
|
|
282
|
+
output: {
|
|
283
|
+
value: getFormData(validSingleState)
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
input: getFormData(validState),
|
|
288
|
+
output: {
|
|
289
|
+
value: getFormData(validState)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
]
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
description: 'Optional',
|
|
296
|
+
component: {
|
|
297
|
+
title: 'Example geospatial field',
|
|
298
|
+
name: 'myComponent',
|
|
299
|
+
type: ComponentType.GeospatialField,
|
|
300
|
+
options: {
|
|
301
|
+
required: false
|
|
302
|
+
}
|
|
303
|
+
} satisfies GeospatialFieldComponent,
|
|
304
|
+
assertions: [
|
|
305
|
+
{
|
|
306
|
+
input: getFormData([]),
|
|
307
|
+
output: {
|
|
308
|
+
value: getFormData([])
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
input: getFormData(),
|
|
313
|
+
output: {
|
|
314
|
+
value: getFormData(),
|
|
315
|
+
errors: [
|
|
316
|
+
expect.objectContaining({
|
|
317
|
+
text: 'Select example geospatial field'
|
|
318
|
+
})
|
|
319
|
+
]
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
]
|
|
323
|
+
}
|
|
324
|
+
])('$description', ({ component: def, assertions }) => {
|
|
325
|
+
let collection: ComponentCollection
|
|
326
|
+
|
|
327
|
+
beforeEach(() => {
|
|
328
|
+
collection = new ComponentCollection([def], { model })
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it.each([...assertions])(
|
|
332
|
+
'validates custom example',
|
|
333
|
+
({ input, output }) => {
|
|
334
|
+
const result = collection.validate(input)
|
|
335
|
+
expect(result).toEqual(output)
|
|
336
|
+
}
|
|
337
|
+
)
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('getErrors formats description errors', () => {
|
|
341
|
+
const component = {
|
|
342
|
+
title: 'Example geospatial field',
|
|
343
|
+
name: 'myComponent',
|
|
344
|
+
type: ComponentType.GeospatialField,
|
|
345
|
+
options: {
|
|
346
|
+
required: true
|
|
347
|
+
}
|
|
348
|
+
} satisfies GeospatialFieldComponent
|
|
349
|
+
|
|
350
|
+
const collection = new ComponentCollection([component], { model })
|
|
351
|
+
const invalidSingleState: GeospatialState = [
|
|
352
|
+
{
|
|
353
|
+
type: 'Feature',
|
|
354
|
+
properties: {
|
|
355
|
+
coordinateGridReference: 'ST 00001',
|
|
356
|
+
centroidGridReference: 'ST 00001',
|
|
357
|
+
description: '' // Missing description should trigger error with href to description field and custom text
|
|
358
|
+
},
|
|
359
|
+
geometry: {
|
|
360
|
+
coordinates: [-2.5723699109417737, 53.2380485215034],
|
|
361
|
+
type: 'Point'
|
|
362
|
+
},
|
|
363
|
+
id: 'a'
|
|
364
|
+
}
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
const result = collection.validate(getFormData(invalidSingleState))
|
|
368
|
+
const geospatialField = collection.components.at(0) as GeospatialField
|
|
369
|
+
|
|
370
|
+
const errors = geospatialField.getErrors(result.errors)
|
|
371
|
+
expect(errors).toEqual([
|
|
372
|
+
expect.objectContaining({
|
|
373
|
+
name: 'description',
|
|
374
|
+
href: '#description_0',
|
|
375
|
+
text: 'Enter description for location 1'
|
|
376
|
+
})
|
|
377
|
+
])
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
})
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { type GeospatialFieldComponent } from '@defra/forms-model'
|
|
2
|
+
import { type ArraySchema } from 'joi'
|
|
3
|
+
|
|
4
|
+
import { type ComponentBase } from '~/src/server/plugins/engine/components/ComponentBase.js'
|
|
5
|
+
import {
|
|
6
|
+
FormComponent,
|
|
7
|
+
isGeospatialState
|
|
8
|
+
} from '~/src/server/plugins/engine/components/FormComponent.js'
|
|
9
|
+
import { geospatialSchema } from '~/src/server/plugins/engine/components/helpers/geospatial.js'
|
|
10
|
+
import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
|
|
11
|
+
import {
|
|
12
|
+
type ErrorMessageTemplateList,
|
|
13
|
+
type FormPayload,
|
|
14
|
+
type FormState,
|
|
15
|
+
type FormStateValue,
|
|
16
|
+
type FormSubmissionError,
|
|
17
|
+
type FormSubmissionState,
|
|
18
|
+
type GeospatialState
|
|
19
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
20
|
+
|
|
21
|
+
export class GeospatialField extends FormComponent {
|
|
22
|
+
declare options: GeospatialFieldComponent['options']
|
|
23
|
+
declare formSchema: ArraySchema<GeospatialState>
|
|
24
|
+
declare stateSchema: ArraySchema<GeospatialState>
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
def: GeospatialFieldComponent,
|
|
28
|
+
props: ConstructorParameters<typeof ComponentBase>[1]
|
|
29
|
+
) {
|
|
30
|
+
super(def, props)
|
|
31
|
+
|
|
32
|
+
const { options } = def
|
|
33
|
+
|
|
34
|
+
let formSchema = geospatialSchema.label(this.label).required()
|
|
35
|
+
|
|
36
|
+
if (options.required !== false) {
|
|
37
|
+
formSchema = formSchema.min(1)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.formSchema = formSchema
|
|
41
|
+
this.stateSchema = formSchema.default(null)
|
|
42
|
+
this.options = options
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getFormValueFromState(state: FormSubmissionState) {
|
|
46
|
+
const { name } = this
|
|
47
|
+
return this.getFormValue(state[name])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getFormValue(value?: FormStateValue | FormState) {
|
|
51
|
+
return this.isValue(value) ? value : undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getDisplayStringFromFormValue(features: GeospatialState | undefined): string {
|
|
55
|
+
if (!features?.length) {
|
|
56
|
+
return ''
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const unit = features.length === 1 ? 'location' : 'locations'
|
|
60
|
+
|
|
61
|
+
return `Added ${features.length} ${unit}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getDisplayStringFromState(state: FormSubmissionState) {
|
|
65
|
+
const features = this.getFormValueFromState(state)
|
|
66
|
+
|
|
67
|
+
return this.getDisplayStringFromFormValue(features)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getContextValueFromFormValue(
|
|
71
|
+
features: GeospatialState | undefined
|
|
72
|
+
): string[] | null {
|
|
73
|
+
return features?.map(({ id }) => id) ?? null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getContextValueFromState(state: FormSubmissionState) {
|
|
77
|
+
const features = this.getFormValueFromState(state)
|
|
78
|
+
|
|
79
|
+
return this.getContextValueFromFormValue(features)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {
|
|
83
|
+
const viewModel = super.getViewModel(payload, errors)
|
|
84
|
+
const value =
|
|
85
|
+
typeof viewModel.value === 'string'
|
|
86
|
+
? viewModel.value
|
|
87
|
+
: JSON.stringify(viewModel.value, null, 2)
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
...viewModel,
|
|
91
|
+
value
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
getErrors(errors?: FormSubmissionError[]): FormSubmissionError[] | undefined {
|
|
96
|
+
const fieldErrors = super.getErrors(errors)
|
|
97
|
+
|
|
98
|
+
fieldErrors?.forEach((err) => {
|
|
99
|
+
if (err.name === 'description') {
|
|
100
|
+
err.href = `#description_${err.path[1]}`
|
|
101
|
+
err.text = `Enter description for location ${Number(err.path[1]) + 1}`
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
return fieldErrors
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
getViewErrors(
|
|
109
|
+
errors?: FormSubmissionError[]
|
|
110
|
+
): FormSubmissionError[] | undefined {
|
|
111
|
+
return this.getErrors(errors)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
isValue(value?: FormStateValue | FormState): value is GeospatialState {
|
|
115
|
+
return isGeospatialState(value)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* For error preview page that shows all possible errors on a component
|
|
120
|
+
*/
|
|
121
|
+
getAllPossibleErrors(): ErrorMessageTemplateList {
|
|
122
|
+
const staticErrors = GeospatialField.getAllPossibleErrors()
|
|
123
|
+
return {
|
|
124
|
+
...staticErrors,
|
|
125
|
+
advancedSettingsErrors: [...staticErrors.advancedSettingsErrors]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Static version of getAllPossibleErrors that doesn't require a component instance.
|
|
131
|
+
*/
|
|
132
|
+
static getAllPossibleErrors(): ErrorMessageTemplateList {
|
|
133
|
+
return {
|
|
134
|
+
baseErrors: [
|
|
135
|
+
{ type: 'required', template: messageTemplate.selectRequired },
|
|
136
|
+
{
|
|
137
|
+
type: 'array.min',
|
|
138
|
+
template: '{{#title}} must contain at least 1 items'
|
|
139
|
+
},
|
|
140
|
+
{ type: 'object.invalidjson', template: messageTemplate.format }
|
|
141
|
+
],
|
|
142
|
+
advancedSettingsErrors: []
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type GeospatialState } from '~/src/server/plugins/engine/types.js'
|
|
2
|
+
|
|
3
|
+
export const validState: GeospatialState = [
|
|
4
|
+
{
|
|
5
|
+
type: 'Feature',
|
|
6
|
+
properties: {
|
|
7
|
+
description: 'My farm house',
|
|
8
|
+
coordinateGridReference: 'ST 00001',
|
|
9
|
+
centroidGridReference: 'ST 00001'
|
|
10
|
+
},
|
|
11
|
+
geometry: {
|
|
12
|
+
coordinates: [-2.5723699109417737, 53.2380485215034],
|
|
13
|
+
type: 'Point'
|
|
14
|
+
},
|
|
15
|
+
id: 'a'
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
type: 'Feature',
|
|
19
|
+
properties: {
|
|
20
|
+
description: 'Main gas line',
|
|
21
|
+
coordinateGridReference: 'ST 00001',
|
|
22
|
+
centroidGridReference: 'ST 00001'
|
|
23
|
+
},
|
|
24
|
+
geometry: {
|
|
25
|
+
coordinates: [
|
|
26
|
+
[-2.570496516462896, 53.239162468888566],
|
|
27
|
+
[-2.5722447488110447, 53.238174174285746]
|
|
28
|
+
],
|
|
29
|
+
type: 'LineString'
|
|
30
|
+
},
|
|
31
|
+
id: 'b'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: 'Feature',
|
|
35
|
+
properties: {
|
|
36
|
+
description: 'My Pony Paddock',
|
|
37
|
+
coordinateGridReference: 'ST 00001',
|
|
38
|
+
centroidGridReference: 'ST 00001'
|
|
39
|
+
},
|
|
40
|
+
geometry: {
|
|
41
|
+
coordinates: [
|
|
42
|
+
[
|
|
43
|
+
[-2.573552894955583, 53.238229751360706],
|
|
44
|
+
[-2.5738557065633643, 53.23812342993719],
|
|
45
|
+
[-2.5737507318720247, 53.23797119653088],
|
|
46
|
+
[-2.573411582871387, 53.23785037598134],
|
|
47
|
+
[-2.5727575097991178, 53.23787454011864],
|
|
48
|
+
[-2.572858447002119, 53.23825391528342],
|
|
49
|
+
[-2.573552894955583, 53.238229751360706]
|
|
50
|
+
]
|
|
51
|
+
],
|
|
52
|
+
type: 'Polygon'
|
|
53
|
+
},
|
|
54
|
+
id: 'c'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'Feature',
|
|
58
|
+
properties: {
|
|
59
|
+
description: 'My farm house #2',
|
|
60
|
+
coordinateGridReference: 'ST 00001',
|
|
61
|
+
centroidGridReference: 'ST 00001'
|
|
62
|
+
},
|
|
63
|
+
geometry: {
|
|
64
|
+
coordinates: [-2.5724, 53.239],
|
|
65
|
+
type: 'Point'
|
|
66
|
+
},
|
|
67
|
+
id: 'd'
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
export const validSingleState: GeospatialState = [
|
|
72
|
+
{
|
|
73
|
+
type: 'Feature',
|
|
74
|
+
properties: {
|
|
75
|
+
description: 'My farm house',
|
|
76
|
+
coordinateGridReference: 'ST 00001',
|
|
77
|
+
centroidGridReference: 'ST 00001'
|
|
78
|
+
},
|
|
79
|
+
geometry: {
|
|
80
|
+
coordinates: [-2.5723699109417737, 53.2380485215034],
|
|
81
|
+
type: 'Point'
|
|
82
|
+
},
|
|
83
|
+
id: 'a'
|
|
84
|
+
}
|
|
85
|
+
]
|
|
@@ -2,11 +2,13 @@ import {
|
|
|
2
2
|
ComponentType,
|
|
3
3
|
type DeclarationFieldComponent,
|
|
4
4
|
type EastingNorthingFieldComponent,
|
|
5
|
+
type GeospatialFieldComponent,
|
|
5
6
|
type LatLongFieldComponent,
|
|
6
7
|
type NationalGridFieldNumberFieldComponent,
|
|
7
8
|
type OsGridRefFieldComponent
|
|
8
9
|
} from '@defra/forms-model'
|
|
9
10
|
|
|
11
|
+
import { validState } from '~/src/server/plugins/engine/components/helpers/__stubs__/geospatial.js'
|
|
10
12
|
import {
|
|
11
13
|
getAnswer,
|
|
12
14
|
getAnswerMarkdown
|
|
@@ -14,6 +16,7 @@ import {
|
|
|
14
16
|
import {
|
|
15
17
|
DeclarationField,
|
|
16
18
|
EastingNorthingField,
|
|
19
|
+
GeospatialField,
|
|
17
20
|
LatLongField,
|
|
18
21
|
NationalGridFieldNumberField,
|
|
19
22
|
OsGridRefField
|
|
@@ -218,6 +221,47 @@ describe('Location field formatting', () => {
|
|
|
218
221
|
})
|
|
219
222
|
})
|
|
220
223
|
|
|
224
|
+
describe('GeospatialField', () => {
|
|
225
|
+
let field: GeospatialField
|
|
226
|
+
|
|
227
|
+
beforeEach(() => {
|
|
228
|
+
const def: GeospatialFieldComponent = {
|
|
229
|
+
type: ComponentType.GeospatialField,
|
|
230
|
+
name: 'geoField',
|
|
231
|
+
title: 'Geospatial',
|
|
232
|
+
options: {}
|
|
233
|
+
}
|
|
234
|
+
field = new GeospatialField(def, { model })
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('formats for email output as single value', () => {
|
|
238
|
+
const state = {
|
|
239
|
+
geoField: validState
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const answer = getAnswer(field, state, { format: 'email' })
|
|
243
|
+
expect(answer).toBe('Added 4 locations\n')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('formats for data output', () => {
|
|
247
|
+
const state = {
|
|
248
|
+
geoField: validState
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const answer = getAnswer(field, state, { format: 'data' })
|
|
252
|
+
expect(answer).toBe('a,b,c,d')
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('formats for summary display', () => {
|
|
256
|
+
const state = {
|
|
257
|
+
geoField: validState
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const answer = getAnswer(field, state, { format: 'summary' })
|
|
261
|
+
expect(answer).toBe('Added 4 locations')
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
221
265
|
describe('DeclarationField', () => {
|
|
222
266
|
let field: DeclarationField
|
|
223
267
|
|
|
@@ -36,6 +36,7 @@ export type Field = InstanceType<
|
|
|
36
36
|
| typeof Components.FileUploadField
|
|
37
37
|
| typeof Components.HiddenField
|
|
38
38
|
| typeof Components.PaymentField
|
|
39
|
+
| typeof Components.GeospatialField
|
|
39
40
|
>
|
|
40
41
|
|
|
41
42
|
// Guidance component instances only
|
|
@@ -196,6 +197,10 @@ export function createComponent(
|
|
|
196
197
|
case ComponentType.PaymentField:
|
|
197
198
|
component = new Components.PaymentField(def, options)
|
|
198
199
|
break
|
|
200
|
+
|
|
201
|
+
case ComponentType.GeospatialField:
|
|
202
|
+
component = new Components.GeospatialField(def, options)
|
|
203
|
+
break
|
|
199
204
|
}
|
|
200
205
|
|
|
201
206
|
if (typeof component === 'undefined') {
|
|
@@ -332,6 +337,11 @@ export function getAnswerMarkdown(
|
|
|
332
337
|
) {
|
|
333
338
|
const contextValue = field.getContextValueFromState(state)
|
|
334
339
|
answerEscaped = contextValue ? `${contextValue}\n` : ''
|
|
340
|
+
} else if (field instanceof Components.GeospatialField) {
|
|
341
|
+
const features = field.getFormValueFromState(state)
|
|
342
|
+
const value = field.getDisplayStringFromFormValue(features)
|
|
343
|
+
|
|
344
|
+
answerEscaped = value ? `${value}\n` : ''
|
|
335
345
|
}
|
|
336
346
|
|
|
337
347
|
return answerEscaped
|