@defra/forms-model 3.0.685 → 3.0.687
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/dist/module/components/types.js.map +1 -1
- package/dist/module/conditions/migration.js +74 -19
- package/dist/module/conditions/migration.js.map +1 -1
- package/dist/module/conditions/types.js.map +1 -1
- package/dist/module/form/form-definition/helpers.js +14 -0
- package/dist/module/form/form-definition/helpers.js.map +1 -1
- package/dist/module/form/form-definition/index.js +14 -3
- package/dist/module/form/form-definition/index.js.map +1 -1
- package/dist/module/form/form-editor/index.js +2 -0
- package/dist/module/form/form-editor/index.js.map +1 -1
- package/dist/module/form/form-editor/types.js.map +1 -1
- package/dist/types/components/types.d.ts +7 -0
- package/dist/types/components/types.d.ts.map +1 -1
- package/dist/types/conditions/migration.d.ts.map +1 -1
- package/dist/types/conditions/types.d.ts +13 -1
- package/dist/types/conditions/types.d.ts.map +1 -1
- package/dist/types/form/form-definition/helpers.d.ts +8 -0
- package/dist/types/form/form-definition/helpers.d.ts.map +1 -1
- package/dist/types/form/form-definition/index.d.ts.map +1 -1
- package/dist/types/form/form-editor/index.d.ts +2 -0
- package/dist/types/form/form-editor/index.d.ts.map +1 -1
- package/dist/types/form/form-editor/types.d.ts +5 -1
- package/dist/types/form/form-editor/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/schemas/component-schema-v2.json +13 -0
- package/schemas/component-schema.json +13 -0
- package/schemas/form-definition-schema.json +39 -0
- package/schemas/form-definition-v2-schema.json +65 -0
- package/schemas/list-schema-v2.json +26 -0
- package/schemas/list-schema.json +26 -0
- package/schemas/page-schema-v2.json +39 -0
- package/schemas/page-schema.json +13 -0
- package/src/components/types.ts +8 -0
- package/src/conditions/migration.ts +111 -25
- package/src/conditions/types.ts +13 -1
- package/src/form/form-definition/helpers.ts +17 -0
- package/src/form/form-definition/index.ts +40 -6
- package/src/form/form-editor/index.ts +6 -0
- package/src/form/form-editor/types.ts +6 -0
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { ComponentType } from '~/src/components/enums.js'
|
|
2
|
+
import { isConditionalType } from '~/src/components/helpers.js'
|
|
3
|
+
import { type ComponentDef } from '~/src/components/types.js'
|
|
2
4
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
ConditionType,
|
|
6
|
+
Coordinator,
|
|
7
|
+
OperatorName
|
|
8
|
+
} from '~/src/conditions/enums.js'
|
|
7
9
|
import {
|
|
8
10
|
type ConditionData,
|
|
9
11
|
type ConditionDataV2,
|
|
12
|
+
type ConditionGroupData,
|
|
10
13
|
type ConditionListItemRefValueDataV2,
|
|
11
14
|
type ConditionRefData,
|
|
12
15
|
type ConditionRefDataV2,
|
|
@@ -14,6 +17,7 @@ import {
|
|
|
14
17
|
type RelativeDateValueData,
|
|
15
18
|
type RelativeDateValueDataV2
|
|
16
19
|
} from '~/src/conditions/types.js'
|
|
20
|
+
import { getConditionListItemIds } from '~/src/form/form-definition/helpers.js'
|
|
17
21
|
import {
|
|
18
22
|
type ConditionWrapper,
|
|
19
23
|
type ConditionWrapperV2,
|
|
@@ -57,29 +61,97 @@ function getListItem(model: RuntimeFormModel, listId: string, itemId: string) {
|
|
|
57
61
|
const foundList = model.getListById(listId)
|
|
58
62
|
|
|
59
63
|
if (!foundList) {
|
|
60
|
-
throw Error(`List ${listId} not found`)
|
|
64
|
+
throw new Error(`List ${listId} not found`)
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
const item = foundList.items.find((item) => item.id === itemId)
|
|
64
68
|
|
|
65
69
|
if (!item) {
|
|
66
|
-
throw Error(`List item ${itemId} not found`)
|
|
70
|
+
throw new Error(`List item ${itemId} not found`)
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
return item
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
function
|
|
76
|
+
function createConditionValueDataListFromListItemRefV2(
|
|
73
77
|
condition: ConditionDataV2,
|
|
74
78
|
model: RuntimeFormModel
|
|
75
|
-
): ConditionValueData {
|
|
79
|
+
): ConditionValueData[] {
|
|
76
80
|
const value = condition.value as ConditionListItemRefValueDataV2
|
|
77
|
-
const
|
|
81
|
+
const itemIds = getConditionListItemIds(value)
|
|
82
|
+
|
|
83
|
+
return itemIds.map((itemId) => {
|
|
84
|
+
const refValue = getListItem(model, value.listId, itemId)
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
display: refValue.text,
|
|
88
|
+
type: ConditionType.Value,
|
|
89
|
+
value: refValue.value.toString()
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Determine how multiple selected list items are combined within a single
|
|
96
|
+
* V2 condition once expanded into individual V1 conditions.
|
|
97
|
+
* - Checkbox questions use the author-chosen coordinator (default OR).
|
|
98
|
+
* - Single-answer questions (radio/autocomplete/select) combine with OR for a
|
|
99
|
+
* positive match and AND for a negative ("is not any of these") match.
|
|
100
|
+
*/
|
|
101
|
+
function getListItemsInnerCoordinator(
|
|
102
|
+
componentType: ComponentDef['type'],
|
|
103
|
+
operator: OperatorName,
|
|
104
|
+
itemsCoordinator: Coordinator | undefined
|
|
105
|
+
): Coordinator {
|
|
106
|
+
if (componentType === ComponentType.CheckboxesField) {
|
|
107
|
+
return itemsCoordinator ?? Coordinator.OR
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return operator === OperatorName.IsNot ? Coordinator.AND : Coordinator.OR
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Convert a V2 list item ref condition to V1. A single selected item produces
|
|
115
|
+
* a single condition (unchanged from legacy behaviour); multiple selected
|
|
116
|
+
* items produce a nested condition group so they are correctly parenthesised
|
|
117
|
+
* relative to sibling conditions.
|
|
118
|
+
*/
|
|
119
|
+
function convertListItemRefConditionV2(
|
|
120
|
+
model: RuntimeFormModel,
|
|
121
|
+
condition: ConditionDataV2,
|
|
122
|
+
component: ComponentDef,
|
|
123
|
+
field: ConditionData['field'],
|
|
124
|
+
coordinator: Coordinator | undefined
|
|
125
|
+
): ConditionData | ConditionGroupData {
|
|
126
|
+
const value = condition.value as ConditionListItemRefValueDataV2
|
|
127
|
+
const values = createConditionValueDataListFromListItemRefV2(condition, model)
|
|
128
|
+
|
|
129
|
+
if (!values.length) {
|
|
130
|
+
throw new Error('List item ref condition has no selected items')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (values.length === 1) {
|
|
134
|
+
return {
|
|
135
|
+
field,
|
|
136
|
+
operator: condition.operator,
|
|
137
|
+
value: values[0],
|
|
138
|
+
coordinator
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const innerCoordinator = getListItemsInnerCoordinator(
|
|
143
|
+
component.type,
|
|
144
|
+
condition.operator,
|
|
145
|
+
value.itemsCoordinator
|
|
146
|
+
)
|
|
78
147
|
|
|
79
148
|
return {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
149
|
+
conditions: values.map((itemValue, index) => ({
|
|
150
|
+
field,
|
|
151
|
+
operator: condition.operator,
|
|
152
|
+
value: itemValue,
|
|
153
|
+
coordinator: index === 0 ? coordinator : innerCoordinator
|
|
154
|
+
}))
|
|
83
155
|
}
|
|
84
156
|
}
|
|
85
157
|
|
|
@@ -157,17 +229,35 @@ function convertConditionDataV2(
|
|
|
157
229
|
model: RuntimeFormModel,
|
|
158
230
|
condition: ConditionDataV2,
|
|
159
231
|
coordinator: Coordinator | undefined
|
|
160
|
-
): ConditionData {
|
|
232
|
+
): ConditionData | ConditionGroupData {
|
|
161
233
|
const component = model.getComponentById(condition.componentId)
|
|
162
234
|
|
|
163
235
|
if (!component) {
|
|
164
|
-
throw Error('Component not found')
|
|
236
|
+
throw new Error('Component not found')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!isConditionalType(component.type)) {
|
|
240
|
+
throw new Error(`Component ${component.name} does not support conditions`)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const field = {
|
|
244
|
+
name: component.name,
|
|
245
|
+
type: component.type,
|
|
246
|
+
display: component.title
|
|
165
247
|
}
|
|
166
248
|
|
|
167
|
-
let newValue
|
|
168
249
|
if (isConditionListItemRefValueDataV2(condition)) {
|
|
169
|
-
|
|
170
|
-
|
|
250
|
+
return convertListItemRefConditionV2(
|
|
251
|
+
model,
|
|
252
|
+
condition,
|
|
253
|
+
component,
|
|
254
|
+
field,
|
|
255
|
+
coordinator
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let newValue
|
|
260
|
+
if (isConditionStringValueDataV2(condition)) {
|
|
171
261
|
newValue = createConditionValueDataFromStringValueDataV2(condition)
|
|
172
262
|
} else if (isConditionBooleanValueDataV2(condition)) {
|
|
173
263
|
newValue =
|
|
@@ -181,15 +271,11 @@ function convertConditionDataV2(
|
|
|
181
271
|
} else if (isConditionRelativeDateValueDataV2(condition)) {
|
|
182
272
|
newValue = createConditionValueDataFromRelativeDateValueDataV2(condition)
|
|
183
273
|
} else {
|
|
184
|
-
throw Error('Unsupported condition type')
|
|
274
|
+
throw new Error('Unsupported condition type')
|
|
185
275
|
}
|
|
186
276
|
|
|
187
277
|
return {
|
|
188
|
-
field
|
|
189
|
-
name: component.name,
|
|
190
|
-
type: component.type as ConditionalComponentType /** @todo fix this */,
|
|
191
|
-
display: component.title
|
|
192
|
-
},
|
|
278
|
+
field,
|
|
193
279
|
operator: condition.operator,
|
|
194
280
|
value: newValue,
|
|
195
281
|
coordinator
|
|
@@ -204,7 +290,7 @@ function convertConditionRefDataFromV2(
|
|
|
204
290
|
const refCondition = model.getConditionById(condition.conditionId)
|
|
205
291
|
|
|
206
292
|
if (!refCondition) {
|
|
207
|
-
throw Error('Component not found')
|
|
293
|
+
throw new Error('Component not found')
|
|
208
294
|
}
|
|
209
295
|
|
|
210
296
|
return {
|
|
@@ -244,7 +330,7 @@ export function convertConditionWrapperFromV2(
|
|
|
244
330
|
value: {
|
|
245
331
|
name: conditionWrapper.id,
|
|
246
332
|
conditions: conditionWrapper.items.map((condition, index) => {
|
|
247
|
-
let newCondition: ConditionData | ConditionRefData
|
|
333
|
+
let newCondition: ConditionData | ConditionRefData | ConditionGroupData
|
|
248
334
|
|
|
249
335
|
if (isConditionDataV2(condition)) {
|
|
250
336
|
newCondition = convertConditionDataV2(
|
package/src/conditions/types.ts
CHANGED
|
@@ -26,7 +26,19 @@ export interface RelativeDateValueData {
|
|
|
26
26
|
|
|
27
27
|
export interface ConditionListItemRefValueDataV2 {
|
|
28
28
|
listId: string
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* One or more referenced list item ids (radio, checkbox, autocomplete,
|
|
31
|
+
* select). New conditions are always written as an array, even for a single
|
|
32
|
+
* selection. A bare `string` is only retained for backwards compatibility so
|
|
33
|
+
* conditions saved before multi-select support continue to load, display and
|
|
34
|
+
* evaluate.
|
|
35
|
+
*/
|
|
36
|
+
itemId: string[] | string
|
|
37
|
+
/**
|
|
38
|
+
* How multiple checkbox selections are combined (AND/OR). Only set for
|
|
39
|
+
* checkbox questions; radio/autocomplete/select combine implicitly.
|
|
40
|
+
*/
|
|
41
|
+
itemsCoordinator?: Coordinator
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
export interface RelativeDateValueDataV2 {
|
|
@@ -44,6 +44,23 @@ export function isConditionListItemRefValueData(
|
|
|
44
44
|
)
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Normalises a list item ref condition value to an array of item ids. `itemId`
|
|
49
|
+
* is normally an array, but may be a bare string for legacy conditions saved
|
|
50
|
+
* before multi-select support.
|
|
51
|
+
* @param { ConditionListItemRefValueDataV2 | undefined } value
|
|
52
|
+
* @returns { string[] }
|
|
53
|
+
*/
|
|
54
|
+
export function getConditionListItemIds(
|
|
55
|
+
value: ConditionListItemRefValueDataV2 | undefined
|
|
56
|
+
): string[] {
|
|
57
|
+
if (!value?.itemId) {
|
|
58
|
+
return []
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Array.isArray(value.itemId) ? value.itemId : [value.itemId]
|
|
62
|
+
}
|
|
63
|
+
|
|
47
64
|
/**
|
|
48
65
|
* Returns an array of all hidden fields in a form
|
|
49
66
|
* @param definition - form definition
|
|
@@ -15,7 +15,11 @@ import {
|
|
|
15
15
|
type ContentComponentsDef,
|
|
16
16
|
type FileUploadFieldComponent
|
|
17
17
|
} from '~/src/components/types.js'
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
ConditionType,
|
|
20
|
+
Coordinator,
|
|
21
|
+
OperatorName
|
|
22
|
+
} from '~/src/conditions/enums.js'
|
|
19
23
|
import {
|
|
20
24
|
type ConditionData,
|
|
21
25
|
type ConditionDataV2,
|
|
@@ -112,7 +116,12 @@ export const listItemIdValidator = (
|
|
|
112
116
|
return value
|
|
113
117
|
}
|
|
114
118
|
|
|
115
|
-
|
|
119
|
+
// `itemId` is normally an array, so the condition value is one level up from
|
|
120
|
+
// each entry being validated. For a legacy bare-string `itemId` it is the
|
|
121
|
+
// nearest ancestor instead, so locate it by shape rather than fixed position.
|
|
122
|
+
const conditionValue = helpers.state.ancestors.find(
|
|
123
|
+
isConditionListItemRefValueData
|
|
124
|
+
)
|
|
116
125
|
|
|
117
126
|
if (!isConditionListItemRefValueData(conditionValue)) {
|
|
118
127
|
return value
|
|
@@ -254,11 +263,17 @@ const conditionListItemRefDataSchemaV2 =
|
|
|
254
263
|
})
|
|
255
264
|
.description('The id of the list')
|
|
256
265
|
.error(checkErrors(FormDefinitionError.RefConditionListId)),
|
|
257
|
-
itemId: Joi.
|
|
258
|
-
.
|
|
266
|
+
itemId: Joi.array()
|
|
267
|
+
.single()
|
|
268
|
+
.items(Joi.string().trim().custom(listItemIdValidator))
|
|
269
|
+
.min(1)
|
|
259
270
|
.required()
|
|
260
|
-
.description('The
|
|
261
|
-
|
|
271
|
+
.description('The ids of the selected list items.'),
|
|
272
|
+
itemsCoordinator: Joi.string()
|
|
273
|
+
.trim()
|
|
274
|
+
.valid(...Object.values(Coordinator))
|
|
275
|
+
.optional()
|
|
276
|
+
.description('How multiple checkbox selections are combined (and/or)')
|
|
262
277
|
})
|
|
263
278
|
|
|
264
279
|
const relativeDateValueDataSchemaV2 = Joi.object<RelativeDateValueDataV2>()
|
|
@@ -665,6 +680,25 @@ export const componentSchema = Joi.object<ComponentDef>()
|
|
|
665
680
|
'Geometry types for geospatial data - for GeospatialField only'
|
|
666
681
|
)
|
|
667
682
|
}).description('Geometry types - for GeospatialField only'),
|
|
683
|
+
mapLayers: Joi.when('type', {
|
|
684
|
+
is: Joi.string()
|
|
685
|
+
.trim()
|
|
686
|
+
.valid(
|
|
687
|
+
ComponentType.GeospatialField,
|
|
688
|
+
ComponentType.EastingNorthingField,
|
|
689
|
+
ComponentType.LatLongField,
|
|
690
|
+
ComponentType.OsGridRefField
|
|
691
|
+
)
|
|
692
|
+
.required(),
|
|
693
|
+
then: Joi.object()
|
|
694
|
+
.keys({
|
|
695
|
+
sssi: Joi.boolean()
|
|
696
|
+
.optional()
|
|
697
|
+
.description('Sites of Special Scientific Interest layer')
|
|
698
|
+
})
|
|
699
|
+
.optional()
|
|
700
|
+
.description('Supported map layers')
|
|
701
|
+
}).description('Map layers'),
|
|
668
702
|
format: Joi.when('type', {
|
|
669
703
|
is: Joi.string()
|
|
670
704
|
.trim()
|
|
@@ -471,6 +471,11 @@ export const countriesSchema = Joi.array()
|
|
|
471
471
|
.single()
|
|
472
472
|
.description('The country to be included in a geospatial field')
|
|
473
473
|
|
|
474
|
+
export const mapLayersSchema = Joi.array()
|
|
475
|
+
.items(Joi.string().valid('sssi'))
|
|
476
|
+
.single()
|
|
477
|
+
.description('The map layers to be displayed')
|
|
478
|
+
|
|
474
479
|
export const exactFeaturesSchema = Joi.number()
|
|
475
480
|
.empty('')
|
|
476
481
|
.integer()
|
|
@@ -693,6 +698,7 @@ export const questionDetailsFullSchema = {
|
|
|
693
698
|
maxChecksSchema,
|
|
694
699
|
exactChecksSchema,
|
|
695
700
|
countriesSchema,
|
|
701
|
+
mapLayersSchema,
|
|
696
702
|
minFeaturesSchema,
|
|
697
703
|
maxFeaturesSchema,
|
|
698
704
|
exactFeaturesSchema,
|
|
@@ -423,6 +423,11 @@ export interface FormEditor {
|
|
|
423
423
|
*/
|
|
424
424
|
geometryTypes?: GeospatialFieldGeometryTypesEnum[]
|
|
425
425
|
|
|
426
|
+
/**
|
|
427
|
+
* The layers to include for map-based geospatial questions
|
|
428
|
+
*/
|
|
429
|
+
mapLayers?: string[]
|
|
430
|
+
|
|
426
431
|
/**
|
|
427
432
|
* The format restriction for telephone number questions
|
|
428
433
|
*/
|
|
@@ -519,6 +524,7 @@ export type FormEditorInputQuestion = Pick<
|
|
|
519
524
|
| 'minFeatures'
|
|
520
525
|
| 'maxFeatures'
|
|
521
526
|
| 'telephoneNumberFormat'
|
|
527
|
+
| 'mapLayers'
|
|
522
528
|
>
|
|
523
529
|
|
|
524
530
|
export type FormEditorInputPageSettings = Pick<
|