@defra/forms-engine-plugin 5.0.0-alpha.11 → 5.0.0-alpha.13
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 +153 -5
- package/.server/client/javascripts/geospatial-map.js +147 -46
- package/.server/client/javascripts/geospatial-map.js.map +1 -1
- package/.server/client/javascripts/location-map.d.ts +38 -0
- package/.server/client/javascripts/location-map.js +42 -2
- package/.server/client/javascripts/location-map.js.map +1 -1
- package/.server/client/javascripts/map-constants.d.ts +3 -0
- package/.server/client/javascripts/map-constants.js +4 -0
- package/.server/client/javascripts/map-constants.js.map +1 -0
- package/.server/client/javascripts/map.d.ts +4 -0
- package/.server/client/javascripts/map.js +5 -0
- package/.server/client/javascripts/map.js.map +1 -1
- package/.server/server/plugins/engine/__stubs__/definitions.d.ts +29 -0
- package/.server/server/plugins/engine/__stubs__/definitions.js +125 -0
- package/.server/server/plugins/engine/__stubs__/definitions.js.map +1 -0
- package/.server/server/plugins/engine/components/CheckboxesField.d.ts +20 -2
- package/.server/server/plugins/engine/components/CheckboxesField.js +24 -12
- package/.server/server/plugins/engine/components/CheckboxesField.js.map +1 -1
- package/.server/server/plugins/engine/components/EastingNorthingField.d.ts +4 -0
- package/.server/server/plugins/engine/components/EastingNorthingField.js +1 -1
- package/.server/server/plugins/engine/components/EastingNorthingField.js.map +1 -1
- package/.server/server/plugins/engine/components/FileUploadField.d.ts +10 -1
- package/.server/server/plugins/engine/components/FileUploadField.js +18 -6
- package/.server/server/plugins/engine/components/FileUploadField.js.map +1 -1
- package/.server/server/plugins/engine/components/GeospatialField.d.ts +22 -0
- package/.server/server/plugins/engine/components/GeospatialField.js +33 -9
- package/.server/server/plugins/engine/components/GeospatialField.js.map +1 -1
- package/.server/server/plugins/engine/components/LatLongField.d.ts +4 -0
- package/.server/server/plugins/engine/components/LocationFieldBase.js +7 -1
- package/.server/server/plugins/engine/components/LocationFieldBase.js.map +1 -1
- package/.server/server/plugins/engine/components/LocationFieldHelpers.d.ts +4 -0
- package/.server/server/plugins/engine/components/LocationFieldHelpers.js +9 -2
- package/.server/server/plugins/engine/components/LocationFieldHelpers.js.map +1 -1
- package/.server/server/plugins/engine/components/helpers/components.js +2 -1
- package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
- package/.server/server/plugins/engine/components/helpers/geospatial.d.ts +5 -0
- package/.server/server/plugins/engine/components/helpers/geospatial.js +25 -16
- package/.server/server/plugins/engine/components/helpers/geospatial.js.map +1 -1
- package/.server/server/plugins/engine/errors.d.ts +49 -0
- package/.server/server/plugins/engine/errors.js +75 -0
- package/.server/server/plugins/engine/errors.js.map +1 -0
- package/.server/server/plugins/engine/form-availability.d.ts +3 -2
- package/.server/server/plugins/engine/form-availability.js +3 -2
- package/.server/server/plugins/engine/form-availability.js.map +1 -1
- package/.server/server/plugins/engine/form-context.js +2 -2
- package/.server/server/plugins/engine/form-context.js.map +1 -1
- package/.server/server/plugins/engine/i18n/buildValidationMessages.d.ts +1 -1
- package/.server/server/plugins/engine/i18n/buildValidationMessages.js +30 -9
- package/.server/server/plugins/engine/i18n/buildValidationMessages.js.map +1 -1
- package/.server/server/plugins/engine/i18n/translations/cy.json +117 -11
- package/.server/server/plugins/engine/i18n/translations/en-GB.json +80 -11
- package/.server/server/plugins/engine/i18n/types.d.ts +1 -0
- package/.server/server/plugins/engine/i18n/types.js.map +1 -1
- package/.server/server/plugins/engine/models/FormModel.js +10 -2
- package/.server/server/plugins/engine/models/FormModel.js.map +1 -1
- package/.server/server/plugins/engine/models/unavailable-view-model.d.ts +11 -2
- package/.server/server/plugins/engine/models/unavailable-view-model.js +23 -4
- package/.server/server/plugins/engine/models/unavailable-view-model.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/helpers/pages.js +2 -1
- package/.server/server/plugins/engine/pageControllers/helpers/pages.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/validationOptions.d.ts +1 -1
- package/.server/server/plugins/engine/pageControllers/validationOptions.js +1 -1
- package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
- package/.server/server/plugins/engine/unavailable-response.js +5 -2
- package/.server/server/plugins/engine/unavailable-response.js.map +1 -1
- package/.server/server/plugins/engine/views/components/_location-field-base.html +2 -1
- package/.server/server/plugins/engine/views/components/geospatialfield.html +1 -0
- package/.server/server/plugins/engine/views/components/osgridreffield.html +3 -1
- package/.server/server/plugins/engine/views/unavailable.html +14 -11
- package/package.json +1 -1
- package/src/client/javascripts/geospatial-map.js +169 -47
- package/src/client/javascripts/location-map.js +50 -2
- package/src/client/javascripts/map-constants.js +3 -0
- package/src/client/javascripts/map.js +5 -0
- package/src/server/plugins/engine/__stubs__/definitions.ts +155 -0
- package/src/server/plugins/engine/components/CheckboxesField.ts +34 -13
- package/src/server/plugins/engine/components/EastingNorthingField.ts +1 -1
- package/src/server/plugins/engine/components/FileUploadField.ts +26 -6
- package/src/server/plugins/engine/components/GeospatialField.ts +44 -11
- package/src/server/plugins/engine/components/LocationFieldBase.ts +7 -1
- package/src/server/plugins/engine/components/LocationFieldHelpers.ts +9 -2
- package/src/server/plugins/engine/components/helpers/components.ts +2 -1
- package/src/server/plugins/engine/components/helpers/geospatial.ts +32 -22
- package/src/server/plugins/engine/errors.ts +78 -0
- package/src/server/plugins/engine/form-availability.ts +8 -2
- package/src/server/plugins/engine/form-context.ts +4 -2
- package/src/server/plugins/engine/i18n/buildValidationMessages.ts +15 -10
- package/src/server/plugins/engine/i18n/translations/cy.json +117 -11
- package/src/server/plugins/engine/i18n/translations/en-GB.json +80 -11
- package/src/server/plugins/engine/i18n/types.ts +1 -0
- package/src/server/plugins/engine/models/FormModel.ts +12 -2
- package/src/server/plugins/engine/models/unavailable-view-model.ts +26 -5
- package/src/server/plugins/engine/pageControllers/helpers/pages.ts +2 -1
- package/src/server/plugins/engine/pageControllers/validationOptions.ts +2 -2
- package/src/server/plugins/engine/unavailable-response.ts +6 -2
- package/src/server/plugins/engine/views/components/_location-field-base.html +2 -1
- package/src/server/plugins/engine/views/components/geospatialfield.html +1 -0
- package/src/server/plugins/engine/views/components/osgridreffield.html +3 -1
- package/src/server/plugins/engine/views/unavailable.html +14 -11
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ComponentType,
|
|
3
|
+
ConditionType,
|
|
4
|
+
ControllerType,
|
|
5
|
+
Engine,
|
|
6
|
+
OperatorName,
|
|
7
|
+
SchemaVersion,
|
|
8
|
+
type FormDefinition,
|
|
9
|
+
type PageQuestion
|
|
10
|
+
} from '@defra/forms-model'
|
|
11
|
+
|
|
12
|
+
const YES_NO_COMPONENT_ID = 'a7c0242f-2a31-45b2-8c71-ff2ac7f53288'
|
|
13
|
+
const USER_TYPE_LIST_ID = '4fa26e9c-07cf-47cd-a9dd-5cec0dd3f544'
|
|
14
|
+
const EXISTING_USER_ITEM_ID = '55fe0067-d011-4d33-886c-e1aa266637c3'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A minimal, schema-valid V2 definition: one YesNo question page and a
|
|
18
|
+
* summary page. Each call returns a fresh object, so tests can mutate their
|
|
19
|
+
* copy freely without affecting each other.
|
|
20
|
+
*/
|
|
21
|
+
export function buildDefinition(): FormDefinition {
|
|
22
|
+
return {
|
|
23
|
+
name: 'Stub definition',
|
|
24
|
+
engine: Engine.V2,
|
|
25
|
+
schema: SchemaVersion.V2,
|
|
26
|
+
startPage: '/summary',
|
|
27
|
+
pages: [
|
|
28
|
+
{
|
|
29
|
+
id: '449c053b-9201-4312-9a75-187afc6ba48b',
|
|
30
|
+
path: '/licence',
|
|
31
|
+
title: 'Licence',
|
|
32
|
+
components: [
|
|
33
|
+
{
|
|
34
|
+
id: YES_NO_COMPONENT_ID,
|
|
35
|
+
name: 'xVrYaJ',
|
|
36
|
+
type: ComponentType.YesNoField,
|
|
37
|
+
title: 'Do you have a licence?',
|
|
38
|
+
shortDescription: 'Licence',
|
|
39
|
+
options: { required: true }
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
next: []
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: '449c053b-9201-4312-9a75-187afc6ba48c',
|
|
46
|
+
path: '/summary',
|
|
47
|
+
title: 'Summary',
|
|
48
|
+
controller: ControllerType.Summary,
|
|
49
|
+
components: []
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
lists: [],
|
|
53
|
+
sections: [],
|
|
54
|
+
conditions: []
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A definition that passes schema validation but cannot be used by the
|
|
60
|
+
* engine: a ListItemRef condition points at the YesNoField (boolean), so the
|
|
61
|
+
* condition expression builder produces an unparseable expression. Distilled
|
|
62
|
+
* from a real production incident.
|
|
63
|
+
*/
|
|
64
|
+
export function buildBrokenConditionDefinition(): FormDefinition {
|
|
65
|
+
const definition = buildDefinition()
|
|
66
|
+
|
|
67
|
+
definition.name = 'Broken condition fixture'
|
|
68
|
+
|
|
69
|
+
const questionPage = definition.pages[0] as PageQuestion
|
|
70
|
+
// A YesNoField cannot legitimately carry a custom list — that is the
|
|
71
|
+
// corruption this fixture models — so the TS component types (rightly)
|
|
72
|
+
// have no `list` property here and a cast is required.
|
|
73
|
+
;(questionPage.components[0] as { list?: string }).list = USER_TYPE_LIST_ID
|
|
74
|
+
definition.lists = [
|
|
75
|
+
{
|
|
76
|
+
id: USER_TYPE_LIST_ID,
|
|
77
|
+
name: 'XtfRYR',
|
|
78
|
+
title: 'User type list',
|
|
79
|
+
type: 'string',
|
|
80
|
+
items: [
|
|
81
|
+
{
|
|
82
|
+
id: EXISTING_USER_ITEM_ID,
|
|
83
|
+
text: 'existing user',
|
|
84
|
+
value: 'existing user'
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: '2277c7e5-7fef-46c6-993b-d294116d6d6b',
|
|
88
|
+
text: 'new user',
|
|
89
|
+
value: 'new user'
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
definition.conditions = [
|
|
95
|
+
{
|
|
96
|
+
id: '3f9d3a35-6dee-4706-806c-3f776129f631',
|
|
97
|
+
displayName: 'Existing user',
|
|
98
|
+
items: [
|
|
99
|
+
{
|
|
100
|
+
id: '7d7f58ee-c860-4d24-8a13-de5cb9af53d8',
|
|
101
|
+
componentId: YES_NO_COMPONENT_ID,
|
|
102
|
+
operator: OperatorName.Is,
|
|
103
|
+
type: ConditionType.ListItemRef,
|
|
104
|
+
value: {
|
|
105
|
+
listId: USER_TYPE_LIST_ID,
|
|
106
|
+
itemId: [EXISTING_USER_ITEM_ID]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
return definition
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A schema-valid definition whose first page names a controller that is
|
|
118
|
+
* neither built in nor registered by the host application.
|
|
119
|
+
*/
|
|
120
|
+
export function buildUnknownControllerDefinition(): FormDefinition {
|
|
121
|
+
const definition = buildDefinition()
|
|
122
|
+
|
|
123
|
+
definition.name = 'Unknown controller fixture'
|
|
124
|
+
definition.pages[0].controller =
|
|
125
|
+
'NoSuchPageController' as unknown as ControllerType
|
|
126
|
+
|
|
127
|
+
return definition
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Schema-valid (component types are free strings in the schema), but the
|
|
132
|
+
* question uses a type the engine has no component class for.
|
|
133
|
+
*/
|
|
134
|
+
export function buildUnknownComponentDefinition(): FormDefinition {
|
|
135
|
+
const definition = buildDefinition()
|
|
136
|
+
|
|
137
|
+
definition.name = 'Unknown component fixture'
|
|
138
|
+
const questionPage = definition.pages[0] as PageQuestion
|
|
139
|
+
questionPage.components[0].type = 'MyUnknownField' as unknown as ComponentType
|
|
140
|
+
|
|
141
|
+
return definition
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A definition that fails schema validation: the question page appears
|
|
146
|
+
* twice, violating the pages uniqueness rule.
|
|
147
|
+
*/
|
|
148
|
+
export function buildSchemaInvalidDefinition(): FormDefinition {
|
|
149
|
+
const definition = buildDefinition()
|
|
150
|
+
|
|
151
|
+
definition.name = 'Schema invalid fixture'
|
|
152
|
+
definition.pages.push(buildDefinition().pages[0])
|
|
153
|
+
|
|
154
|
+
return definition
|
|
155
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type CheckboxesFieldComponent, type Item } from '@defra/forms-model'
|
|
2
|
-
import joi, { type ArraySchema
|
|
2
|
+
import joi, { type ArraySchema } from 'joi'
|
|
3
3
|
|
|
4
|
+
import { EN_GB } from '../../../constants.js'
|
|
4
5
|
import { isFormValue } from './FormComponent.js'
|
|
5
6
|
import { SelectionControlField } from './SelectionControlField.js'
|
|
6
|
-
import {
|
|
7
|
+
import { t as tPlugin } from '../i18n/index.js'
|
|
7
8
|
import { type Translator } from '../i18n/types.js'
|
|
8
9
|
import { type FormModel } from '../models/FormModel.js'
|
|
9
10
|
import { type QuestionPageController } from '../pageControllers/QuestionPageController.js'
|
|
@@ -20,6 +21,7 @@ export class CheckboxesField extends SelectionControlField {
|
|
|
20
21
|
declare schema: CheckboxesFieldComponent['schema']
|
|
21
22
|
declare formSchema: ArraySchema<string> | ArraySchema<number>
|
|
22
23
|
declare stateSchema: ArraySchema<string> | ArraySchema<number>
|
|
24
|
+
declare limits: { min?: number; max?: number; length?: number }
|
|
23
25
|
|
|
24
26
|
constructor(
|
|
25
27
|
def: CheckboxesFieldComponent,
|
|
@@ -42,11 +44,8 @@ export class CheckboxesField extends SelectionControlField {
|
|
|
42
44
|
.single()
|
|
43
45
|
.label(this.label)
|
|
44
46
|
.required()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
'array.max': messageTemplate.arrayMax,
|
|
48
|
-
'array.length': messageTemplate.arrayLength
|
|
49
|
-
})
|
|
47
|
+
|
|
48
|
+
const limits: { min?: number; max?: number; length?: number } = {}
|
|
50
49
|
|
|
51
50
|
if (options.required === false) {
|
|
52
51
|
formSchema = formSchema.optional()
|
|
@@ -54,16 +53,25 @@ export class CheckboxesField extends SelectionControlField {
|
|
|
54
53
|
|
|
55
54
|
if (typeof schema?.length === 'number') {
|
|
56
55
|
formSchema = formSchema.length(schema.length)
|
|
56
|
+
limits.length = schema.length
|
|
57
57
|
} else {
|
|
58
58
|
if (typeof schema?.min === 'number') {
|
|
59
59
|
formSchema = formSchema.min(schema.min)
|
|
60
|
+
limits.min = schema.min
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
if (typeof schema?.max === 'number') {
|
|
63
64
|
formSchema = formSchema.max(schema.max)
|
|
65
|
+
limits.max = schema.max
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
this.limits = limits
|
|
70
|
+
|
|
71
|
+
formSchema = formSchema.messages(
|
|
72
|
+
CheckboxesField.buildErrorMessages(EN_GB, limits)
|
|
73
|
+
)
|
|
74
|
+
|
|
67
75
|
this.formSchema = formSchema.default([])
|
|
68
76
|
this.stateSchema = formSchema.default(null).allow(null)
|
|
69
77
|
this.options = options
|
|
@@ -135,13 +143,9 @@ export class CheckboxesField extends SelectionControlField {
|
|
|
135
143
|
}
|
|
136
144
|
|
|
137
145
|
getValidationMessagesOverride(translator: Translator) {
|
|
138
|
-
const {
|
|
146
|
+
const { language } = translator
|
|
139
147
|
return {
|
|
140
|
-
[this.name]:
|
|
141
|
-
'array.min': buildValidationMessages(t).arrayMin,
|
|
142
|
-
'array.max': buildValidationMessages(t).arrayMax,
|
|
143
|
-
'array.length': buildValidationMessages(t).arrayLength
|
|
144
|
-
} as LanguageMessages
|
|
148
|
+
[this.name]: CheckboxesField.buildErrorMessages(language, this.limits)
|
|
145
149
|
}
|
|
146
150
|
}
|
|
147
151
|
|
|
@@ -181,4 +185,21 @@ export class CheckboxesField extends SelectionControlField {
|
|
|
181
185
|
|
|
182
186
|
return value.every(isFormValue)
|
|
183
187
|
}
|
|
188
|
+
|
|
189
|
+
static buildErrorMessages(
|
|
190
|
+
language: string,
|
|
191
|
+
limits: { min?: number; max?: number; length?: number } = {}
|
|
192
|
+
) {
|
|
193
|
+
return {
|
|
194
|
+
'array.min': tPlugin('validation.arrayMin', language, {
|
|
195
|
+
count: limits.min ?? 1
|
|
196
|
+
}),
|
|
197
|
+
'array.max': tPlugin('validation.arrayMax', language, {
|
|
198
|
+
count: limits.max ?? 1
|
|
199
|
+
}),
|
|
200
|
+
'array.length': tPlugin('validation.arrayLength', language, {
|
|
201
|
+
count: limits.length ?? 1
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
}
|
|
184
205
|
}
|
|
@@ -187,7 +187,7 @@ export class EastingNorthingField extends FormComponent {
|
|
|
187
187
|
if (err.context?.country) {
|
|
188
188
|
err.text = translator.t(
|
|
189
189
|
'components.eastingNorthingField.wrongCountry',
|
|
190
|
-
{ country: err.context.country }
|
|
190
|
+
{ country: translator.t(`common.${err.context.country}`) }
|
|
191
191
|
)
|
|
192
192
|
}
|
|
193
193
|
})
|
|
@@ -113,6 +113,7 @@ export class FileUploadField extends FormComponent {
|
|
|
113
113
|
declare schema: FileUploadFieldComponent['schema']
|
|
114
114
|
declare formSchema: ArraySchema<FileState>
|
|
115
115
|
declare stateSchema: ArraySchema<FileState>
|
|
116
|
+
declare limits: { min?: number; max?: number; length?: number }
|
|
116
117
|
|
|
117
118
|
constructor(
|
|
118
119
|
def: FileUploadFieldComponent,
|
|
@@ -132,21 +133,31 @@ export class FileUploadField extends FormComponent {
|
|
|
132
133
|
formSchema = formSchema.optional()
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
const limits: { min?: number; max?: number; length?: number } = {}
|
|
137
|
+
|
|
135
138
|
if (typeof schema.length !== 'number') {
|
|
136
139
|
if (typeof schema.max === 'number') {
|
|
137
140
|
formSchema = formSchema.max(schema.max)
|
|
141
|
+
limits.max = schema.max
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
if (typeof schema.min === 'number') {
|
|
141
145
|
formSchema = formSchema.min(schema.min)
|
|
146
|
+
limits.min = schema.min
|
|
142
147
|
} else if (options.required !== false) {
|
|
143
148
|
formSchema = formSchema.min(1)
|
|
149
|
+
limits.min = 1
|
|
144
150
|
}
|
|
145
151
|
} else {
|
|
146
152
|
formSchema = formSchema.length(schema.length)
|
|
153
|
+
limits.length = schema.length
|
|
147
154
|
}
|
|
148
155
|
|
|
149
|
-
|
|
156
|
+
this.limits = limits
|
|
157
|
+
|
|
158
|
+
formSchema = formSchema.messages(
|
|
159
|
+
FileUploadField.buildErrorMessages(EN_GB, limits)
|
|
160
|
+
)
|
|
150
161
|
|
|
151
162
|
this.formSchema = formSchema.items(formItemSchema)
|
|
152
163
|
this.stateSchema = formSchema
|
|
@@ -309,7 +320,7 @@ export class FileUploadField extends FormComponent {
|
|
|
309
320
|
getValidationMessagesOverride(translator: Translator) {
|
|
310
321
|
const { language } = translator
|
|
311
322
|
return {
|
|
312
|
-
[this.name]: FileUploadField.buildErrorMessages(language)
|
|
323
|
+
[this.name]: FileUploadField.buildErrorMessages(language, this.limits)
|
|
313
324
|
}
|
|
314
325
|
}
|
|
315
326
|
|
|
@@ -417,11 +428,20 @@ export class FileUploadField extends FormComponent {
|
|
|
417
428
|
}
|
|
418
429
|
}
|
|
419
430
|
|
|
420
|
-
static buildErrorMessages(
|
|
431
|
+
static buildErrorMessages(
|
|
432
|
+
language: string,
|
|
433
|
+
limits: { min?: number; max?: number; length?: number } = {}
|
|
434
|
+
) {
|
|
421
435
|
return {
|
|
422
|
-
'array.min': tPlugin('validation.filesMin', language
|
|
423
|
-
|
|
424
|
-
|
|
436
|
+
'array.min': tPlugin('validation.filesMin', language, {
|
|
437
|
+
count: limits.min ?? 1
|
|
438
|
+
}),
|
|
439
|
+
'array.max': tPlugin('validation.filesMax', language, {
|
|
440
|
+
count: limits.max ?? 1
|
|
441
|
+
}),
|
|
442
|
+
'array.length': tPlugin('validation.filesLength', language, {
|
|
443
|
+
count: limits.length ?? 1
|
|
444
|
+
})
|
|
425
445
|
}
|
|
426
446
|
}
|
|
427
447
|
}
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { type GeospatialFieldComponent } from '@defra/forms-model'
|
|
2
2
|
import { type ArraySchema } from 'joi'
|
|
3
3
|
|
|
4
|
+
import { EN_GB } from '../../../constants.js'
|
|
4
5
|
import { type ComponentBase } from './ComponentBase.js'
|
|
5
6
|
import {
|
|
6
7
|
FormComponent,
|
|
7
8
|
isGeospatialState
|
|
8
9
|
} from './FormComponent.js'
|
|
9
10
|
import { getMapLayers } from './LocationFieldHelpers.js'
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
determineLimits,
|
|
13
|
+
getGeospatialSchema
|
|
14
|
+
} from './helpers/geospatial.js'
|
|
11
15
|
import { type RenderContext } from './types.js'
|
|
16
|
+
import { t as tPlugin } from '../i18n/index.js'
|
|
12
17
|
import { type Translator } from '../i18n/types.js'
|
|
13
18
|
import { messageTemplate } from '../pageControllers/validationOptions.js'
|
|
14
19
|
import {
|
|
@@ -24,6 +29,7 @@ export class GeospatialField extends FormComponent {
|
|
|
24
29
|
declare options: GeospatialFieldComponent['options']
|
|
25
30
|
declare formSchema: ArraySchema<GeospatialState>
|
|
26
31
|
declare stateSchema: ArraySchema<GeospatialState>
|
|
32
|
+
declare limits: { min?: number; max?: number; length?: number }
|
|
27
33
|
|
|
28
34
|
constructor(
|
|
29
35
|
def: GeospatialFieldComponent,
|
|
@@ -33,20 +39,24 @@ export class GeospatialField extends FormComponent {
|
|
|
33
39
|
|
|
34
40
|
const { options } = def
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
let formSchema = getGeospatialSchema(def).label(this.label)
|
|
43
|
+
|
|
44
|
+
const isOptional = options.required === false
|
|
45
|
+
const limits = determineLimits(def, isOptional)
|
|
46
|
+
|
|
47
|
+
formSchema = formSchema.messages(
|
|
48
|
+
GeospatialField.buildErrorMessages(EN_GB, limits)
|
|
49
|
+
)
|
|
41
50
|
|
|
42
51
|
this.formSchema = formSchema
|
|
43
52
|
this.stateSchema = formSchema.default(null)
|
|
44
53
|
|
|
45
|
-
if (
|
|
54
|
+
if (isOptional) {
|
|
46
55
|
this.stateSchema = this.stateSchema.allow(null)
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
this.options = options
|
|
59
|
+
this.limits = limits
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
getFormValueFromState(state: FormSubmissionState) {
|
|
@@ -101,6 +111,7 @@ export class GeospatialField extends FormComponent {
|
|
|
101
111
|
|
|
102
112
|
return {
|
|
103
113
|
...viewModel,
|
|
114
|
+
lang: context.translator.language,
|
|
104
115
|
country: this.options.countries?.at(0),
|
|
105
116
|
geometryTypes: this.options.geometryTypes,
|
|
106
117
|
mapLayers: getMapLayers(this),
|
|
@@ -127,9 +138,7 @@ export class GeospatialField extends FormComponent {
|
|
|
127
138
|
'components.geospatialField.validation.wrongCountry',
|
|
128
139
|
{
|
|
129
140
|
count: Number(err.path[1]) + 1,
|
|
130
|
-
country: translator.t(
|
|
131
|
-
`common.${(err.context.country as string).toLowerCase()}`
|
|
132
|
-
)
|
|
141
|
+
country: translator.t(`common.${err.context.country}`)
|
|
133
142
|
}
|
|
134
143
|
)
|
|
135
144
|
}
|
|
@@ -149,6 +158,13 @@ export class GeospatialField extends FormComponent {
|
|
|
149
158
|
return isGeospatialState(value)
|
|
150
159
|
}
|
|
151
160
|
|
|
161
|
+
getValidationMessagesOverride(translator: Translator) {
|
|
162
|
+
const { language } = translator
|
|
163
|
+
return {
|
|
164
|
+
[this.name]: GeospatialField.buildErrorMessages(language, this.limits)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
152
168
|
/**
|
|
153
169
|
* For error preview page that shows all possible errors on a component
|
|
154
170
|
*/
|
|
@@ -169,11 +185,28 @@ export class GeospatialField extends FormComponent {
|
|
|
169
185
|
{ type: 'required', template: messageTemplate.selectRequired },
|
|
170
186
|
{
|
|
171
187
|
type: 'array.min',
|
|
172
|
-
template: '{{#title}} must contain at least 1
|
|
188
|
+
template: '{{#title}} must contain at least 1 item'
|
|
173
189
|
},
|
|
174
190
|
{ type: 'object.invalidjson', template: messageTemplate.format }
|
|
175
191
|
],
|
|
176
192
|
advancedSettingsErrors: []
|
|
177
193
|
}
|
|
178
194
|
}
|
|
195
|
+
|
|
196
|
+
static buildErrorMessages(
|
|
197
|
+
language: string,
|
|
198
|
+
limits: { min?: number; max?: number; length?: number } = {}
|
|
199
|
+
) {
|
|
200
|
+
return {
|
|
201
|
+
'array.min': tPlugin('validation.featuresMin', language, {
|
|
202
|
+
count: limits.min ?? 1
|
|
203
|
+
}),
|
|
204
|
+
'array.max': tPlugin('validation.featuresMax', language, {
|
|
205
|
+
count: limits.max ?? 1
|
|
206
|
+
}),
|
|
207
|
+
'array.length': tPlugin('validation.featuresLength', language, {
|
|
208
|
+
count: limits.length ?? 1
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
}
|
|
179
212
|
}
|
|
@@ -124,10 +124,16 @@ export abstract class LocationFieldBase extends FormComponent {
|
|
|
124
124
|
getViewModel(context: RenderContext) {
|
|
125
125
|
const viewModel = super.getViewModel(context)
|
|
126
126
|
|
|
127
|
+
Object.assign(viewModel, {
|
|
128
|
+
lang: context.translator.language
|
|
129
|
+
})
|
|
130
|
+
|
|
127
131
|
if (this.instructionText) {
|
|
132
|
+
const { tComponent } = context.translator
|
|
128
133
|
return {
|
|
129
134
|
...viewModel,
|
|
130
|
-
instructionText:
|
|
135
|
+
instructionText:
|
|
136
|
+
tComponent(this.def, 'instructionText') || this.instructionText
|
|
131
137
|
}
|
|
132
138
|
}
|
|
133
139
|
|
|
@@ -194,13 +194,20 @@ export function getLocationFieldViewModel(
|
|
|
194
194
|
country:
|
|
195
195
|
component instanceof EastingNorthingField
|
|
196
196
|
? component.options.countries?.at(0)
|
|
197
|
-
: undefined
|
|
197
|
+
: undefined,
|
|
198
|
+
lang: context.translator.language
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
if (component.options.instructionText) {
|
|
202
|
+
const { t, tComponent } = context.translator
|
|
203
|
+
|
|
201
204
|
return {
|
|
202
205
|
...result,
|
|
203
|
-
|
|
206
|
+
instructionSummary: t('components.locationFieldBase.howToFind'),
|
|
207
|
+
instructionText:
|
|
208
|
+
tComponent(component.def, 'instructionText') ||
|
|
209
|
+
component.options.instructionText,
|
|
210
|
+
t
|
|
204
211
|
}
|
|
205
212
|
}
|
|
206
213
|
|
|
@@ -6,6 +6,7 @@ import { ListFormComponent } from '../ListFormComponent.js'
|
|
|
6
6
|
import { escapeMarkdown } from './index.js'
|
|
7
7
|
import * as Components from '../index.js'
|
|
8
8
|
import { markdown } from '../markdownParser.js'
|
|
9
|
+
import { UnknownComponentTypeError } from '../../errors.js'
|
|
9
10
|
import { type Translator } from '../../i18n/types.js'
|
|
10
11
|
import { type FormState } from '../../types.js'
|
|
11
12
|
|
|
@@ -210,7 +211,7 @@ export function createComponent(
|
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
if (typeof component === 'undefined') {
|
|
213
|
-
throw new
|
|
214
|
+
throw new UnknownComponentTypeError(def.type)
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
return component
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
GeospatialFieldOptionsCountryEnum,
|
|
3
2
|
type GeospatialFieldComponent,
|
|
4
3
|
type GeospatialFieldOptionsCountry
|
|
5
4
|
} from '@defra/forms-model'
|
|
@@ -20,13 +19,6 @@ import {
|
|
|
20
19
|
} from '../../types.js'
|
|
21
20
|
import { countries } from '../../../map/routes/index.js'
|
|
22
21
|
|
|
23
|
-
const countriesDesc: Record<GeospatialFieldOptionsCountryEnum, string> = {
|
|
24
|
-
[GeospatialFieldOptionsCountryEnum.England]: 'England',
|
|
25
|
-
[GeospatialFieldOptionsCountryEnum.NorthernIreland]: 'Northern Ireland',
|
|
26
|
-
[GeospatialFieldOptionsCountryEnum.Scotland]: 'Scotland',
|
|
27
|
-
[GeospatialFieldOptionsCountryEnum.Wales]: 'Wales'
|
|
28
|
-
}
|
|
29
|
-
|
|
30
22
|
const Joi = JoiBase.extend({
|
|
31
23
|
type: 'array',
|
|
32
24
|
base: JoiBase.array(),
|
|
@@ -103,25 +95,43 @@ const featureSchema = Joi.object<Feature>().keys({
|
|
|
103
95
|
geometry: featureGeometrySchema
|
|
104
96
|
})
|
|
105
97
|
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
|
|
98
|
+
export function determineLimits(
|
|
99
|
+
def: GeospatialFieldComponent,
|
|
100
|
+
isOptional: boolean
|
|
109
101
|
) {
|
|
110
|
-
const {
|
|
111
|
-
const
|
|
112
|
-
|
|
102
|
+
const { schema: constraints } = def
|
|
103
|
+
const limits: { min?: number; max?: number; length?: number } = {}
|
|
113
104
|
if (typeof constraints?.length === 'number') {
|
|
114
|
-
|
|
105
|
+
limits.length = constraints.length
|
|
115
106
|
} else {
|
|
116
107
|
if (typeof constraints?.min === 'number') {
|
|
117
|
-
|
|
108
|
+
limits.min = constraints.min
|
|
118
109
|
} else if (!isOptional) {
|
|
119
|
-
|
|
110
|
+
limits.min = 1
|
|
120
111
|
}
|
|
121
112
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
113
|
+
limits.max = typeof constraints?.max === 'number' ? constraints.max : 50
|
|
114
|
+
}
|
|
115
|
+
return limits
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function applySchemaConstraints(
|
|
119
|
+
schema: JoiBase.ArraySchema<Feature[]>,
|
|
120
|
+
def: GeospatialFieldComponent
|
|
121
|
+
) {
|
|
122
|
+
const { options } = def
|
|
123
|
+
const isOptional = options.required === false
|
|
124
|
+
|
|
125
|
+
const limits = determineLimits(def, isOptional)
|
|
126
|
+
|
|
127
|
+
if (limits.length !== undefined) {
|
|
128
|
+
schema = schema.length(limits.length)
|
|
129
|
+
}
|
|
130
|
+
if (limits.min !== undefined) {
|
|
131
|
+
schema = schema.min(limits.min)
|
|
132
|
+
}
|
|
133
|
+
if (limits.max !== undefined) {
|
|
134
|
+
schema = schema.max(limits.max)
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
if (isOptional) {
|
|
@@ -161,7 +171,7 @@ export function getGeospatialSchema(
|
|
|
161
171
|
|
|
162
172
|
if (!result) {
|
|
163
173
|
return helpers.error('any.custom', {
|
|
164
|
-
country
|
|
174
|
+
country
|
|
165
175
|
})
|
|
166
176
|
}
|
|
167
177
|
|
|
@@ -212,7 +222,7 @@ export function getEastingNorthingCountryValidator(
|
|
|
212
222
|
return helpers.error(
|
|
213
223
|
'any.custom',
|
|
214
224
|
{
|
|
215
|
-
country
|
|
225
|
+
country
|
|
216
226
|
},
|
|
217
227
|
{ path: [field.name] }
|
|
218
228
|
)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type ValidationError } from 'joi'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base class for errors caused by an invalid form definition — whether it
|
|
5
|
+
* fails schema validation or passes the schema but cannot be used by the
|
|
6
|
+
* engine. Never thrown directly — throw a subclass. Consumers (e.g.
|
|
7
|
+
* forms-runner error pages) detect the family with
|
|
8
|
+
* `instanceof InvalidFormDefinitionError`.
|
|
9
|
+
*
|
|
10
|
+
* When subclassing, remember that the message and the messages of any
|
|
11
|
+
* `cause` chain are shown to form authors on preview error pages. Only
|
|
12
|
+
* describe the problem with the form itself — never include sensitive
|
|
13
|
+
* details such as file paths, configuration values or secrets.
|
|
14
|
+
*/
|
|
15
|
+
export class InvalidFormDefinitionError extends Error {
|
|
16
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
17
|
+
super(message, options)
|
|
18
|
+
this.name = 'InvalidFormDefinitionError'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Thrown when a condition in the definition cannot be compiled into an
|
|
24
|
+
* evaluatable expression (e.g. it references the wrong component or list).
|
|
25
|
+
*/
|
|
26
|
+
export class ConditionBuildError extends InvalidFormDefinitionError {
|
|
27
|
+
public readonly conditionName: string
|
|
28
|
+
|
|
29
|
+
constructor(conditionName: string, options?: ErrorOptions) {
|
|
30
|
+
super(`Failed to build condition '${conditionName}'`, options)
|
|
31
|
+
this.name = 'ConditionBuildError'
|
|
32
|
+
this.conditionName = conditionName
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Thrown when a page names a controller that is neither built in nor
|
|
38
|
+
* registered by the host application.
|
|
39
|
+
*/
|
|
40
|
+
export class UnknownPageControllerError extends InvalidFormDefinitionError {
|
|
41
|
+
public readonly controllerName: string
|
|
42
|
+
|
|
43
|
+
constructor(controllerName: string) {
|
|
44
|
+
super(`Page controller ${controllerName} does not exist`)
|
|
45
|
+
this.name = 'UnknownPageControllerError'
|
|
46
|
+
this.controllerName = controllerName
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Thrown when a component declares a type with no registered implementation.
|
|
52
|
+
*/
|
|
53
|
+
export class UnknownComponentTypeError extends InvalidFormDefinitionError {
|
|
54
|
+
public readonly componentType: string
|
|
55
|
+
|
|
56
|
+
constructor(componentType: string) {
|
|
57
|
+
super(`Component type ${componentType} does not exist`)
|
|
58
|
+
this.name = 'UnknownComponentTypeError'
|
|
59
|
+
this.componentType = componentType
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Thrown when a form definition fails Joi schema validation. The raw
|
|
65
|
+
* ValidationError is preserved as `cause` so consumers can reach the
|
|
66
|
+
* per-field details; the message carries Joi's own summary so log lines
|
|
67
|
+
* remain diagnostic for consumers that only read `error.message`.
|
|
68
|
+
*/
|
|
69
|
+
export class SchemaValidationError extends InvalidFormDefinitionError {
|
|
70
|
+
// Error types `cause` as unknown; this constructor only accepts a Joi
|
|
71
|
+
// ValidationError, so narrow the declaration for consumers (type-only).
|
|
72
|
+
declare cause: ValidationError
|
|
73
|
+
|
|
74
|
+
constructor(cause: ValidationError) {
|
|
75
|
+
super(`Invalid form definition: ${cause.message}`, { cause })
|
|
76
|
+
this.name = 'SchemaValidationError'
|
|
77
|
+
}
|
|
78
|
+
}
|