@defra/forms-engine-plugin 4.12.0 → 4.14.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/shared.min.js +1 -1
- package/.public/javascripts/shared.min.js.map +1 -1
- package/.server/client/javascripts/geospatial-map.d.ts +14 -0
- package/.server/client/javascripts/geospatial-map.js +161 -70
- package/.server/client/javascripts/geospatial-map.js.map +1 -1
- package/.server/client/javascripts/map.d.ts +6 -0
- package/.server/client/javascripts/map.js +5 -0
- package/.server/client/javascripts/map.js.map +1 -1
- package/.server/client/javascripts/utils.d.ts +7 -0
- package/.server/client/javascripts/utils.js +21 -0
- package/.server/client/javascripts/utils.js.map +1 -0
- package/.server/server/forms/simple-form.yaml +9 -0
- package/.server/server/plugins/engine/components/GeospatialField.d.ts +1 -0
- package/.server/server/plugins/engine/components/GeospatialField.js +9 -5
- package/.server/server/plugins/engine/components/GeospatialField.js.map +1 -1
- package/.server/server/plugins/engine/components/helpers/geospatial.d.ts +2 -2
- package/.server/server/plugins/engine/components/helpers/geospatial.js +32 -5
- package/.server/server/plugins/engine/components/helpers/geospatial.js.map +1 -1
- package/.server/server/plugins/engine/components/helpers/geospatial.test.js +37 -6
- package/.server/server/plugins/engine/components/helpers/geospatial.test.js.map +1 -1
- package/.server/server/plugins/engine/models/FormModel.d.ts +4 -0
- package/.server/server/plugins/engine/models/FormModel.js +14 -1
- package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.d.ts +1 -1
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js +5 -5
- package/.server/server/plugins/engine/pageControllers/SummaryPageController.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/validationOptions.js +4 -1
- package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
- package/.server/server/plugins/engine/routes/questions.js +1 -1
- package/.server/server/plugins/engine/routes/questions.js.map +1 -1
- package/.server/server/plugins/engine/views/components/geospatialfield.html +1 -1
- package/.server/server/plugins/engine/views/file-upload.html +2 -1
- package/package.json +2 -2
- package/src/client/javascripts/geospatial-map.js +159 -53
- package/src/client/javascripts/map.js +5 -0
- package/src/client/javascripts/utils.js +23 -0
- package/src/server/forms/simple-form.yaml +9 -0
- package/src/server/plugins/engine/components/GeospatialField.ts +11 -7
- package/src/server/plugins/engine/components/helpers/geospatial.ts +49 -11
- package/src/server/plugins/engine/models/FormModel.ts +20 -0
- package/src/server/plugins/engine/pageControllers/SummaryPageController.ts +13 -6
- package/src/server/plugins/engine/pageControllers/validationOptions.ts +4 -1
- package/src/server/plugins/engine/routes/questions.ts +5 -1
- package/src/server/plugins/engine/views/components/geospatialfield.html +1 -1
- package/src/server/plugins/engine/views/file-upload.html +2 -1
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getCentroidGridRef,
|
|
8
8
|
getCoordinateGridRef
|
|
9
9
|
} from './map.js'
|
|
10
|
+
import { formatDelimtedList } from './utils.js'
|
|
10
11
|
|
|
11
12
|
const helpPanelConfig = {
|
|
12
13
|
showLabel: true,
|
|
@@ -28,8 +29,63 @@ const helpPanelConfig = {
|
|
|
28
29
|
open: true,
|
|
29
30
|
dismissible: true,
|
|
30
31
|
modal: false
|
|
31
|
-
}
|
|
32
|
-
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {boolean} allowLine
|
|
37
|
+
* @param {boolean} allowShape
|
|
38
|
+
*/
|
|
39
|
+
function getLineOrShapeText(allowLine, allowShape) {
|
|
40
|
+
if (allowLine && allowShape) {
|
|
41
|
+
return 'a line or shape'
|
|
42
|
+
}
|
|
43
|
+
if (allowLine) {
|
|
44
|
+
return 'a line'
|
|
45
|
+
}
|
|
46
|
+
if (allowShape) {
|
|
47
|
+
return 'a shape'
|
|
48
|
+
}
|
|
49
|
+
return ''
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {boolean} allowPoint
|
|
54
|
+
* @param {boolean} allowLine
|
|
55
|
+
* @param {boolean} allowShape
|
|
56
|
+
*/
|
|
57
|
+
function getAllowedTypesPhrase(allowPoint, allowLine, allowShape) {
|
|
58
|
+
const items = []
|
|
59
|
+
|
|
60
|
+
if (allowPoint) {
|
|
61
|
+
items.push('points')
|
|
62
|
+
}
|
|
63
|
+
if (allowLine) {
|
|
64
|
+
items.push('lines')
|
|
65
|
+
}
|
|
66
|
+
if (allowShape) {
|
|
67
|
+
items.push('shapes')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return formatDelimtedList(items, ',', 'or')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {boolean} allowPoint
|
|
75
|
+
* @param {boolean} allowLine
|
|
76
|
+
* @param {boolean} allowShape
|
|
77
|
+
*/
|
|
78
|
+
export function getHelpPanelHtml(allowPoint, allowLine, allowShape) {
|
|
79
|
+
const lineOrShapeText = getLineOrShapeText(allowLine, allowShape)
|
|
80
|
+
const doneExtra = lineOrShapeText
|
|
81
|
+
? `<li>Double‑click, or select 'Done', when you have finished drawing ${lineOrShapeText}</li>`
|
|
82
|
+
: ''
|
|
83
|
+
const allowedTypesText = getAllowedTypesPhrase(
|
|
84
|
+
allowPoint,
|
|
85
|
+
allowLine,
|
|
86
|
+
allowShape
|
|
87
|
+
)
|
|
88
|
+
return `<p class="govuk-body-s govuk-!-margin-bottom-2">You can add ${allowedTypesText} to the map.</p><ul class="govuk-list govuk-list--number govuk-body-s"><li>Search for a county, place or postcode</li><li>Use the + and - icons to zoom in and out</li>${doneExtra}<li>Give the location a name</li></ul>`
|
|
33
89
|
}
|
|
34
90
|
|
|
35
91
|
const lineFeatureProperties = {
|
|
@@ -157,7 +213,18 @@ export function processGeospatial(config, geospatial, index) {
|
|
|
157
213
|
const { map, interactPlugin } = createMap(mapId, initConfig, config)
|
|
158
214
|
const featuresManager = getFeaturesManager(geojson)
|
|
159
215
|
const activeFeatureManager = getActiveFeatureManager()
|
|
160
|
-
const
|
|
216
|
+
const geometryTypes = geospatial.dataset.geometrytypes ?? 'point,line,shape'
|
|
217
|
+
const options = {
|
|
218
|
+
geometryTypes
|
|
219
|
+
}
|
|
220
|
+
const uiManager = getUIManager(
|
|
221
|
+
geojson,
|
|
222
|
+
map,
|
|
223
|
+
mapId,
|
|
224
|
+
listEl,
|
|
225
|
+
geospatialInput,
|
|
226
|
+
options
|
|
227
|
+
)
|
|
161
228
|
|
|
162
229
|
/**
|
|
163
230
|
* @type {Context}
|
|
@@ -492,16 +559,32 @@ function getValueRenderer(geojson, geospatialInput) {
|
|
|
492
559
|
* @param {string} mapId - the ID of the map
|
|
493
560
|
* @param {HTMLDivElement} listEl - where to render the feature list
|
|
494
561
|
* @param {HTMLTextAreaElement} geospatialInput - the geospatial textarea
|
|
562
|
+
* @param {UIManagerOptions} options - extra options such as allowable geometry types
|
|
495
563
|
*/
|
|
496
|
-
function getUIManager(geojson, map, mapId, listEl, geospatialInput) {
|
|
564
|
+
function getUIManager(geojson, map, mapId, listEl, geospatialInput, options) {
|
|
565
|
+
/**
|
|
566
|
+
* Get a CSV list of geometry types the user can create
|
|
567
|
+
* @returns {string[]}
|
|
568
|
+
*/
|
|
569
|
+
function getAllowableGeometryTypes() {
|
|
570
|
+
return options.geometryTypes ? options.geometryTypes.split(',') : []
|
|
571
|
+
}
|
|
572
|
+
|
|
497
573
|
/**
|
|
498
574
|
* Toggle the hidden state of the action buttons
|
|
499
575
|
* @type {ToggleActionButtons}
|
|
500
576
|
*/
|
|
501
577
|
function toggleActionButtons(hidden) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
578
|
+
const types = getAllowableGeometryTypes()
|
|
579
|
+
if (types.includes('point')) {
|
|
580
|
+
map.toggleButtonState('btnAddPoint', 'hidden', hidden)
|
|
581
|
+
}
|
|
582
|
+
if (types.includes('shape')) {
|
|
583
|
+
map.toggleButtonState('btnAddPolygon', 'hidden', hidden)
|
|
584
|
+
}
|
|
585
|
+
if (types.includes('line')) {
|
|
586
|
+
map.toggleButtonState('btnAddLine', 'hidden', hidden)
|
|
587
|
+
}
|
|
505
588
|
}
|
|
506
589
|
|
|
507
590
|
/**
|
|
@@ -528,7 +611,8 @@ function getUIManager(geojson, map, mapId, listEl, geospatialInput) {
|
|
|
528
611
|
renderValue,
|
|
529
612
|
listEl,
|
|
530
613
|
toggleActionButtons,
|
|
531
|
-
focusDescriptionInput
|
|
614
|
+
focusDescriptionInput,
|
|
615
|
+
getAllowableGeometryTypes
|
|
532
616
|
}
|
|
533
617
|
}
|
|
534
618
|
|
|
@@ -572,7 +656,8 @@ function createContainers(geospatialInput, index) {
|
|
|
572
656
|
function onMapReadyFactory(context) {
|
|
573
657
|
const { map, activeFeatureManager, uiManager, interactPlugin, drawPlugin } =
|
|
574
658
|
context
|
|
575
|
-
const { toggleActionButtons, renderList } =
|
|
659
|
+
const { toggleActionButtons, renderList, getAllowableGeometryTypes } =
|
|
660
|
+
uiManager
|
|
576
661
|
const { resetActiveFeature } = activeFeatureManager
|
|
577
662
|
|
|
578
663
|
/**
|
|
@@ -581,53 +666,67 @@ function onMapReadyFactory(context) {
|
|
|
581
666
|
* @param {MapLibreMap} e.map - the map provider instance
|
|
582
667
|
*/
|
|
583
668
|
return function onMapReady(e) {
|
|
669
|
+
const types = getAllowableGeometryTypes()
|
|
670
|
+
const allowPoint = types.includes('point')
|
|
671
|
+
const allowLine = types.includes('line')
|
|
672
|
+
const allowShape = types.includes('shape')
|
|
673
|
+
|
|
584
674
|
// Add info panel
|
|
585
|
-
map.addPanel('info',
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
variant: 'tertiary',
|
|
589
|
-
label: 'Add point',
|
|
590
|
-
iconSvgContent: POINT_SVG,
|
|
591
|
-
onClick: () => {
|
|
592
|
-
resetActiveFeature()
|
|
593
|
-
toggleActionButtons(true)
|
|
594
|
-
renderList(true)
|
|
595
|
-
interactPlugin.enable()
|
|
596
|
-
},
|
|
597
|
-
mobile: { slot: 'actions' },
|
|
598
|
-
tablet: { slot: 'actions' },
|
|
599
|
-
desktop: { slot: 'actions' }
|
|
675
|
+
map.addPanel('info', {
|
|
676
|
+
...helpPanelConfig,
|
|
677
|
+
html: getHelpPanelHtml(allowPoint, allowLine, allowShape)
|
|
600
678
|
})
|
|
601
679
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
680
|
+
if (allowPoint) {
|
|
681
|
+
map.addButton('btnAddPoint', {
|
|
682
|
+
variant: 'tertiary',
|
|
683
|
+
label: 'Add point',
|
|
684
|
+
iconSvgContent: POINT_SVG,
|
|
685
|
+
onClick: () => {
|
|
686
|
+
resetActiveFeature()
|
|
687
|
+
toggleActionButtons(true)
|
|
688
|
+
renderList(true)
|
|
689
|
+
interactPlugin.enable()
|
|
690
|
+
},
|
|
691
|
+
mobile: { slot: 'actions' },
|
|
692
|
+
tablet: { slot: 'actions' },
|
|
693
|
+
desktop: { slot: 'actions' }
|
|
694
|
+
})
|
|
695
|
+
}
|
|
616
696
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
697
|
+
if (allowShape) {
|
|
698
|
+
map.addButton('btnAddPolygon', {
|
|
699
|
+
variant: 'tertiary',
|
|
700
|
+
label: 'Add shape',
|
|
701
|
+
iconSvgContent: POLYGON_SVG,
|
|
702
|
+
onClick: () => {
|
|
703
|
+
resetActiveFeature()
|
|
704
|
+
toggleActionButtons(true)
|
|
705
|
+
renderList(true)
|
|
706
|
+
drawPlugin.newPolygon(generateID(), polygonFeatureProperties)
|
|
707
|
+
},
|
|
708
|
+
mobile: { slot: 'actions' },
|
|
709
|
+
tablet: { slot: 'actions' },
|
|
710
|
+
desktop: { slot: 'actions' }
|
|
711
|
+
})
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (allowLine) {
|
|
715
|
+
map.addButton('btnAddLine', {
|
|
716
|
+
variant: 'tertiary',
|
|
717
|
+
label: 'Add line',
|
|
718
|
+
iconSvgContent: LINE_SVG,
|
|
719
|
+
onClick: () => {
|
|
720
|
+
resetActiveFeature()
|
|
721
|
+
toggleActionButtons(true)
|
|
722
|
+
renderList(true)
|
|
723
|
+
drawPlugin.newLine(generateID(), lineFeatureProperties)
|
|
724
|
+
},
|
|
725
|
+
mobile: { slot: 'actions' },
|
|
726
|
+
tablet: { slot: 'actions' },
|
|
727
|
+
desktop: { slot: 'actions' }
|
|
728
|
+
})
|
|
729
|
+
}
|
|
631
730
|
|
|
632
731
|
// Set the map provider on the context
|
|
633
732
|
context.mapProvider = e.map
|
|
@@ -1055,6 +1154,12 @@ function onListElKeydownFactory() {
|
|
|
1055
1154
|
* @returns {void}
|
|
1056
1155
|
*/
|
|
1057
1156
|
|
|
1157
|
+
/**
|
|
1158
|
+
* Returns the list of geometry types a user can create
|
|
1159
|
+
* @callback GetAllowableGeometryTypes
|
|
1160
|
+
* @returns {string[]}
|
|
1161
|
+
*/
|
|
1162
|
+
|
|
1058
1163
|
/**
|
|
1059
1164
|
* Set focus to the last description input
|
|
1060
1165
|
* @callback FocusDescriptionInput
|
|
@@ -1084,6 +1189,7 @@ function onListElKeydownFactory() {
|
|
|
1084
1189
|
* @property {HTMLDivElement} listEl - the summary list of features
|
|
1085
1190
|
* @property {ToggleActionButtons} toggleActionButtons - function that toggles the action buttons
|
|
1086
1191
|
* @property {FocusDescriptionInput} focusDescriptionInput - function that sets focus to a description input element
|
|
1192
|
+
* @property {GetAllowableGeometryTypes} getAllowableGeometryTypes - function that returns the array of geometry types a user can create
|
|
1087
1193
|
*/
|
|
1088
1194
|
|
|
1089
1195
|
/**
|
|
@@ -1098,5 +1204,5 @@ function onListElKeydownFactory() {
|
|
|
1098
1204
|
*/
|
|
1099
1205
|
|
|
1100
1206
|
/**
|
|
1101
|
-
* @import { MapLibreMap } from './map.js'
|
|
1207
|
+
* @import { MapLibreMap, UIManagerOptions } from './map.js'
|
|
1102
1208
|
*/
|
|
@@ -395,6 +395,11 @@ export function centerMap(map, mapProvider, center) {
|
|
|
395
395
|
* @property {TileData} data - the tile data config
|
|
396
396
|
*/
|
|
397
397
|
|
|
398
|
+
/**
|
|
399
|
+
* @typedef {object} UIManagerOptions
|
|
400
|
+
* @property {string} [geometryTypes] - the CSV list of geometry types that a user can create
|
|
401
|
+
*/
|
|
402
|
+
|
|
398
403
|
/**
|
|
399
404
|
* @import { Feature } from '../../server/plugins/engine/types.js'
|
|
400
405
|
*/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a text representation of a list in the form 'a, b, c, d or e'
|
|
3
|
+
* @param {string[]} items
|
|
4
|
+
* @param {string} separator
|
|
5
|
+
* @param {string} lastSpearator
|
|
6
|
+
*/
|
|
7
|
+
export function formatDelimtedList(items, separator, lastSpearator) {
|
|
8
|
+
if (items.length === 0) {
|
|
9
|
+
return ''
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (items.length === 1) {
|
|
13
|
+
return items[0]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (items.length === 2) {
|
|
17
|
+
return `${items[0]} ${lastSpearator} ${items[1]}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const last = items.pop()
|
|
21
|
+
const separatorAndSpace = `${separator} `
|
|
22
|
+
return `${items.join(separatorAndSpace)} ${lastSpearator} ${last}`
|
|
23
|
+
}
|
|
@@ -25,6 +25,15 @@ pages:
|
|
|
25
25
|
required: true
|
|
26
26
|
schema: {}
|
|
27
27
|
id: b68df7f1-d4f4-4c17-83c8-402f584906c9
|
|
28
|
+
- type: GeospatialField
|
|
29
|
+
title: Where do you live?
|
|
30
|
+
name: applicantLocation
|
|
31
|
+
shortDescription: Your location
|
|
32
|
+
hint: ''
|
|
33
|
+
options:
|
|
34
|
+
required: true
|
|
35
|
+
schema: {}
|
|
36
|
+
id: e18116e0-7c3e-416a-af42-6f229017c5b1
|
|
28
37
|
next: []
|
|
29
38
|
id: 622a35ec-3795-418a-81f3-a45746959045
|
|
30
39
|
- title: Upload a copy of your passport
|
|
@@ -31,18 +31,21 @@ export class GeospatialField extends FormComponent {
|
|
|
31
31
|
|
|
32
32
|
const { options } = def
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
const formSchema = getGeospatialSchema(def)
|
|
35
35
|
.label(this.label)
|
|
36
|
-
.
|
|
36
|
+
.messages({
|
|
37
|
+
'array.min': messageTemplate.featuresMin as string,
|
|
38
|
+
'array.max': messageTemplate.featuresMax as string,
|
|
39
|
+
'array.length': messageTemplate.featuresLength as string
|
|
40
|
+
})
|
|
37
41
|
|
|
38
|
-
formSchema = formSchema
|
|
42
|
+
this.formSchema = formSchema
|
|
43
|
+
this.stateSchema = formSchema.default(null)
|
|
39
44
|
|
|
40
|
-
if (options.required
|
|
41
|
-
|
|
45
|
+
if (options.required === false) {
|
|
46
|
+
this.stateSchema = this.stateSchema.allow(null)
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
this.formSchema = formSchema
|
|
45
|
-
this.stateSchema = formSchema.default(null)
|
|
46
49
|
this.options = options
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -93,6 +96,7 @@ export class GeospatialField extends FormComponent {
|
|
|
93
96
|
return {
|
|
94
97
|
...viewModel,
|
|
95
98
|
country: this.options.countries?.at(0),
|
|
99
|
+
geometryTypes: this.options.geometryTypes,
|
|
96
100
|
value
|
|
97
101
|
}
|
|
98
102
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GeospatialFieldOptionsCountryEnum,
|
|
3
|
+
type GeospatialFieldComponent,
|
|
3
4
|
type GeospatialFieldOptionsCountry
|
|
4
5
|
} from '@defra/forms-model'
|
|
5
6
|
import Bourne from '@hapi/bourne'
|
|
@@ -31,7 +32,8 @@ const Joi = JoiBase.extend({
|
|
|
31
32
|
from: 'string',
|
|
32
33
|
method(value, helpers) {
|
|
33
34
|
if (typeof value === 'string') {
|
|
34
|
-
|
|
35
|
+
const trimmed = value.trim()
|
|
36
|
+
if (trimmed === '' || trimmed === '[]') {
|
|
35
37
|
return {
|
|
36
38
|
value: undefined
|
|
37
39
|
}
|
|
@@ -96,14 +98,48 @@ const featureSchema = Joi.object<Feature>().keys({
|
|
|
96
98
|
geometry: featureGeometrySchema
|
|
97
99
|
})
|
|
98
100
|
|
|
99
|
-
|
|
100
|
-
.
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
function applySchemaConstraints(
|
|
102
|
+
schema: JoiBase.ArraySchema<Feature[]>,
|
|
103
|
+
def: GeospatialFieldComponent
|
|
104
|
+
) {
|
|
105
|
+
const { options, schema: constraints } = def
|
|
106
|
+
const isOptional = options.required === false
|
|
107
|
+
|
|
108
|
+
if (typeof constraints?.length === 'number') {
|
|
109
|
+
schema = schema.length(constraints.length)
|
|
110
|
+
} else {
|
|
111
|
+
if (typeof constraints?.min === 'number') {
|
|
112
|
+
schema = schema.min(constraints.min)
|
|
113
|
+
} else if (!isOptional) {
|
|
114
|
+
schema = schema.min(1)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
schema = schema.max(
|
|
118
|
+
typeof constraints?.max === 'number' ? constraints.max : 50
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isOptional) {
|
|
123
|
+
schema = schema.optional()
|
|
124
|
+
} else {
|
|
125
|
+
schema = schema.required()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return schema
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function getGeospatialSchema(
|
|
132
|
+
def: GeospatialFieldComponent
|
|
133
|
+
): JoiBase.ArraySchema<Feature[]> {
|
|
134
|
+
const { options = {} } = def
|
|
135
|
+
const country: GeospatialFieldOptionsCountry | undefined =
|
|
136
|
+
options.countries?.at(0)
|
|
103
137
|
|
|
104
|
-
export function getGeospatialSchema(country?: GeospatialFieldOptionsCountry) {
|
|
105
138
|
if (!country) {
|
|
106
|
-
return
|
|
139
|
+
return applySchemaConstraints(
|
|
140
|
+
Joi.array<Feature[]>().items(featureSchema).unique('id'),
|
|
141
|
+
def
|
|
142
|
+
)
|
|
107
143
|
}
|
|
108
144
|
|
|
109
145
|
const validateCountryBounds: CustomValidator = (value, helpers) => {
|
|
@@ -128,10 +164,12 @@ export function getGeospatialSchema(country?: GeospatialFieldOptionsCountry) {
|
|
|
128
164
|
return value
|
|
129
165
|
}
|
|
130
166
|
|
|
131
|
-
return
|
|
132
|
-
.
|
|
133
|
-
|
|
134
|
-
|
|
167
|
+
return applySchemaConstraints(
|
|
168
|
+
Joi.array<Feature[]>()
|
|
169
|
+
.items(featureSchema.custom(validateCountryBounds))
|
|
170
|
+
.unique('id'),
|
|
171
|
+
def
|
|
172
|
+
)
|
|
135
173
|
}
|
|
136
174
|
|
|
137
175
|
/**
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
formDefinitionV2Schema,
|
|
10
10
|
generateConditionAlias,
|
|
11
11
|
hasComponents,
|
|
12
|
+
hasComponentsEvenIfNoNext,
|
|
12
13
|
hasRepeater,
|
|
13
14
|
isConditionWrapperV2,
|
|
14
15
|
yesNoListId,
|
|
@@ -159,6 +160,13 @@ export class FormModel {
|
|
|
159
160
|
this.services = services
|
|
160
161
|
this.controllers = controllers
|
|
161
162
|
|
|
163
|
+
// Assert that there is only one payment question (if any)
|
|
164
|
+
if (this.moreThanOnePaymentQuestion()) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
'Invalid form definition: Only one payment question is allowed per form'
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
162
170
|
this.pageDefMap = new Map(def.pages.map((page) => [page.path, page]))
|
|
163
171
|
this.listDefMap = new Map(def.lists.map((list) => [list.name, list]))
|
|
164
172
|
this.listDefIdMap = new Map(
|
|
@@ -543,6 +551,18 @@ export class FormModel {
|
|
|
543
551
|
.filter(isConditionWrapperV2)
|
|
544
552
|
.find((condition) => condition.id === conditionId)
|
|
545
553
|
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Checks that only one payment field exists (if any payments fields exist)
|
|
557
|
+
*/
|
|
558
|
+
moreThanOnePaymentQuestion() {
|
|
559
|
+
const numOfPaymentFields = this.def.pages
|
|
560
|
+
.flatMap((page) =>
|
|
561
|
+
hasComponentsEvenIfNoNext(page) ? page.components : []
|
|
562
|
+
)
|
|
563
|
+
.filter((comp) => comp.type === ComponentType.PaymentField).length
|
|
564
|
+
return numOfPaymentFields > 1
|
|
565
|
+
}
|
|
546
566
|
}
|
|
547
567
|
|
|
548
568
|
/**
|
|
@@ -433,7 +433,8 @@ export async function submitForm(
|
|
|
433
433
|
|
|
434
434
|
const items = getFormSubmissionData(
|
|
435
435
|
summaryViewModel.context,
|
|
436
|
-
summaryViewModel.details
|
|
436
|
+
summaryViewModel.details,
|
|
437
|
+
model
|
|
437
438
|
)
|
|
438
439
|
|
|
439
440
|
try {
|
|
@@ -531,11 +532,14 @@ function submitData(
|
|
|
531
532
|
main: buildMainRecords(items),
|
|
532
533
|
repeaters: buildRepeaterRecords(items)
|
|
533
534
|
}
|
|
534
|
-
|
|
535
535
|
return submit(payload)
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
-
export function getFormSubmissionData(
|
|
538
|
+
export function getFormSubmissionData(
|
|
539
|
+
context: FormContext,
|
|
540
|
+
details: Detail[],
|
|
541
|
+
model: FormModel
|
|
542
|
+
) {
|
|
539
543
|
const items = context.relevantPages
|
|
540
544
|
.map(({ href }) =>
|
|
541
545
|
details.flatMap(({ items }) =>
|
|
@@ -544,7 +548,7 @@ export function getFormSubmissionData(context: FormContext, details: Detail[]) {
|
|
|
544
548
|
)
|
|
545
549
|
.flat()
|
|
546
550
|
|
|
547
|
-
const paymentItems = getPaymentFieldItems(context)
|
|
551
|
+
const paymentItems = getPaymentFieldItems(context, model)
|
|
548
552
|
|
|
549
553
|
return [...items, ...paymentItems]
|
|
550
554
|
}
|
|
@@ -553,10 +557,13 @@ export function getFormSubmissionData(context: FormContext, details: Detail[]) {
|
|
|
553
557
|
* Gets DetailItems for PaymentField components
|
|
554
558
|
* PaymentField is excluded from summaryDetails for UI but needs to be in submission data
|
|
555
559
|
*/
|
|
556
|
-
function getPaymentFieldItems(
|
|
560
|
+
function getPaymentFieldItems(
|
|
561
|
+
context: FormContext,
|
|
562
|
+
model: FormModel
|
|
563
|
+
): DetailItemField[] {
|
|
557
564
|
const items: DetailItemField[] = []
|
|
558
565
|
|
|
559
|
-
for (const page of
|
|
566
|
+
for (const page of model.pages) {
|
|
560
567
|
for (const field of page.collection.fields) {
|
|
561
568
|
if (field instanceof PaymentField) {
|
|
562
569
|
items.push({
|
|
@@ -64,7 +64,10 @@ export const messageTemplate: Record<string, JoiExpression> = {
|
|
|
64
64
|
dateMax: '{{#title}} must be the same as or before {{#limit}}',
|
|
65
65
|
arrayMin: 'Select at least {{#limit}} options from the list',
|
|
66
66
|
arrayMax: 'Only {{#limit}} can be selected from the list',
|
|
67
|
-
arrayLength: 'Select only {{#limit}} options from the list'
|
|
67
|
+
arrayLength: 'Select only {{#limit}} options from the list',
|
|
68
|
+
featuresMin: 'Define at least {{#limit}} features',
|
|
69
|
+
featuresMax: 'Only {{#limit}} features can be defined',
|
|
70
|
+
featuresLength: 'Define exactly {{#limit}} features'
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
export const messages: LanguageMessagesExt = {
|
|
@@ -59,7 +59,11 @@ async function handleHttpEvent(
|
|
|
59
59
|
// TODO: Update structured data POST payload with when helper
|
|
60
60
|
// is updated to removing the dependency on `SummaryViewModel` etc.
|
|
61
61
|
const viewModel = new SummaryViewModel(request, page, context)
|
|
62
|
-
const items = getFormSubmissionData(
|
|
62
|
+
const items = getFormSubmissionData(
|
|
63
|
+
viewModel.context,
|
|
64
|
+
viewModel.details,
|
|
65
|
+
model
|
|
66
|
+
)
|
|
63
67
|
|
|
64
68
|
// @ts-expect-error - function signature will be refactored in the next iteration of the formatter
|
|
65
69
|
const payload = format(context, items, model, undefined, undefined)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{% from "govuk/components/textarea/macro.njk" import govukTextarea %}
|
|
2
2
|
|
|
3
3
|
{% macro GeospatialField(component) %}
|
|
4
|
-
<div class="app-geospatial-field" data-country="{{component.model.country}}">
|
|
4
|
+
<div class="app-geospatial-field" data-country="{{component.model.country}}" data-geometryTypes="{{component.model.geometryTypes}}">
|
|
5
5
|
{{ govukTextarea(component.model) }}
|
|
6
6
|
</div>
|
|
7
7
|
{% endmacro %}
|