@defra/forms-model 3.0.429 → 3.0.431
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/README.md +163 -1
- package/dist/module/common/pagination/index.js +2 -2
- package/dist/module/common/pagination/index.js.map +1 -1
- package/dist/module/common/search/index.js +4 -4
- package/dist/module/common/search/index.js.map +1 -1
- package/dist/module/common/sorting/index.js +2 -2
- package/dist/module/common/sorting/index.js.map +1 -1
- package/dist/module/form/form-definition/index.js +156 -156
- package/dist/module/form/form-definition/index.js.map +1 -1
- package/dist/module/form/form-editor/index.js +47 -37
- package/dist/module/form/form-editor/index.js.map +1 -1
- package/dist/module/form/form-editor/types.js.map +1 -1
- package/dist/module/form/form-manager/index.js +3 -3
- package/dist/module/form/form-manager/index.js.map +1 -1
- package/dist/module/form/form-metadata/index.js +34 -34
- package/dist/module/form/form-metadata/index.js.map +1 -1
- package/dist/module/form/form-submission/index.js +13 -13
- package/dist/module/form/form-submission/index.js.map +1 -1
- package/dist/module/types/joi-to-json.d.js +2 -0
- package/dist/module/types/joi-to-json.d.js.map +1 -0
- package/dist/types/common/pagination/index.d.ts.map +1 -1
- package/dist/types/common/search/index.d.ts.map +1 -1
- package/dist/types/common/sorting/index.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 +17 -7
- package/dist/types/form/form-editor/index.d.ts.map +1 -1
- package/dist/types/form/form-editor/types.d.ts +45 -1
- package/dist/types/form/form-editor/types.d.ts.map +1 -1
- package/dist/types/form/form-manager/index.d.ts.map +1 -1
- package/dist/types/form/form-metadata/index.d.ts.map +1 -1
- package/dist/types/form/form-submission/index.d.ts.map +1 -1
- package/package.json +6 -4
- package/scripts/generate-schemas.js +238 -0
- package/scripts/schema-modules/constants.js +39 -0
- package/scripts/schema-modules/schema-processors.js +109 -0
- package/scripts/schema-modules/schema-simplifiers.js +351 -0
- package/scripts/schema-modules/title-processors.js +327 -0
- package/scripts/schema-modules/types.js +21 -0
- package/scripts/schema-modules/utils.js +41 -0
- package/src/common/pagination/index.ts +8 -1
- package/src/common/search/index.ts +17 -3
- package/src/common/sorting/index.ts +8 -2
- package/src/form/form-definition/index.ts +567 -238
- package/src/form/form-editor/index.ts +207 -24
- package/src/form/form-editor/types.ts +69 -0
- package/src/form/form-manager/index.ts +11 -2
- package/src/form/form-metadata/index.ts +118 -40
- package/src/form/form-submission/index.ts +33 -10
- package/src/types/joi-to-json.d.ts +15 -0
@@ -29,78 +29,168 @@ import {
|
|
29
29
|
type Section
|
30
30
|
} from '~/src/form/form-definition/types.js'
|
31
31
|
|
32
|
-
const sectionsSchema = Joi.object<Section>()
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
32
|
+
const sectionsSchema = Joi.object<Section>()
|
33
|
+
.description('A form section grouping related pages together')
|
34
|
+
.keys({
|
35
|
+
name: Joi.string()
|
36
|
+
.required()
|
37
|
+
.description(
|
38
|
+
'Unique identifier for the section, used in code and page references'
|
39
|
+
),
|
40
|
+
title: Joi.string()
|
41
|
+
.required()
|
42
|
+
.description('Human-readable section title displayed to users'),
|
43
|
+
hideTitle: Joi.boolean()
|
44
|
+
.optional()
|
45
|
+
.default(false)
|
46
|
+
.description(
|
47
|
+
'When true, the section title will not be displayed in the UI'
|
48
|
+
)
|
49
|
+
})
|
50
|
+
|
51
|
+
const conditionFieldSchema = Joi.object<ConditionFieldData>()
|
52
|
+
.description('Field reference used in a condition')
|
53
|
+
.keys({
|
54
|
+
name: Joi.string()
|
55
|
+
.required()
|
56
|
+
.description('Component name referenced by this condition'),
|
57
|
+
type: Joi.string()
|
58
|
+
.required()
|
59
|
+
.description('Data type of the field (e.g., string, number, date)'),
|
60
|
+
display: Joi.string()
|
61
|
+
.required()
|
62
|
+
.description('Human-readable name of the field for display purposes')
|
63
|
+
})
|
64
|
+
|
65
|
+
const conditionValueSchema = Joi.object<ConditionValueData>()
|
66
|
+
.description('Value specification for a condition')
|
67
|
+
.keys({
|
68
|
+
type: Joi.string()
|
69
|
+
.required()
|
70
|
+
.description('Data type of the value (e.g., string, number, date)'),
|
71
|
+
value: Joi.string()
|
72
|
+
.required()
|
73
|
+
.description('The actual value to compare against'),
|
74
|
+
display: Joi.string()
|
75
|
+
.required()
|
76
|
+
.description('Human-readable version of the value for display purposes')
|
77
|
+
})
|
78
|
+
|
79
|
+
const relativeDateValueSchema = Joi.object<RelativeDateValueData>()
|
80
|
+
.description('Relative date specification for date-based conditions')
|
81
|
+
.keys({
|
82
|
+
type: Joi.string()
|
83
|
+
.required()
|
84
|
+
.description('Data type identifier, should be "RelativeDate"'),
|
85
|
+
period: Joi.string()
|
86
|
+
.required()
|
87
|
+
.description('Numeric amount of the time period, as a string'),
|
88
|
+
unit: Joi.string()
|
89
|
+
.required()
|
90
|
+
.description('Time unit (e.g., days, weeks, months, years)'),
|
91
|
+
direction: Joi.string()
|
92
|
+
.required()
|
93
|
+
.description('Temporal direction, either "past" or "future"')
|
94
|
+
})
|
95
|
+
|
96
|
+
const conditionRefSchema = Joi.object<ConditionRefData>()
|
97
|
+
.description('Reference to a named condition defined elsewhere')
|
98
|
+
.keys({
|
99
|
+
conditionName: Joi.string()
|
100
|
+
.required()
|
101
|
+
.description('Name of the referenced condition'),
|
102
|
+
conditionDisplayName: Joi.string()
|
103
|
+
.required()
|
104
|
+
.description('Human-readable name of the condition for display purposes'),
|
105
|
+
coordinator: Joi.string()
|
106
|
+
.optional()
|
107
|
+
.description(
|
108
|
+
'Logical operator connecting this condition with others (AND, OR)'
|
109
|
+
)
|
110
|
+
})
|
111
|
+
|
112
|
+
const conditionSchema = Joi.object<ConditionData>()
|
113
|
+
.description('Condition definition specifying a logical comparison')
|
114
|
+
.keys({
|
115
|
+
field: conditionFieldSchema.description(
|
116
|
+
'The form field being evaluated in this condition'
|
117
|
+
),
|
118
|
+
operator: Joi.string()
|
119
|
+
.required()
|
120
|
+
.description('Comparison operator (equals, greaterThan, contains, etc.)'),
|
121
|
+
value: Joi.alternatives()
|
122
|
+
.try(conditionValueSchema, relativeDateValueSchema)
|
123
|
+
.description(
|
124
|
+
'Value to compare the field against, either fixed or relative date'
|
125
|
+
),
|
126
|
+
coordinator: Joi.string()
|
127
|
+
.optional()
|
128
|
+
.description(
|
129
|
+
'Logical operator connecting this condition with others (AND, OR)'
|
130
|
+
)
|
131
|
+
})
|
69
132
|
|
70
133
|
const conditionGroupSchema = Joi.object<ConditionGroupData>()
|
134
|
+
.description('Group of conditions combined with logical operators')
|
71
135
|
.keys({
|
72
|
-
conditions: Joi.array()
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
136
|
+
conditions: Joi.array()
|
137
|
+
.items(
|
138
|
+
Joi.alternatives().try(
|
139
|
+
conditionSchema,
|
140
|
+
conditionRefSchema,
|
141
|
+
Joi.link('#conditionGroupSchema')
|
142
|
+
)
|
77
143
|
)
|
78
|
-
|
144
|
+
.description('Array of conditions or condition references in this group')
|
79
145
|
})
|
80
146
|
.id('conditionGroupSchema')
|
81
147
|
|
82
|
-
const conditionsModelSchema = Joi.object<ConditionsModelData>()
|
83
|
-
name
|
84
|
-
|
85
|
-
Joi.
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
148
|
+
const conditionsModelSchema = Joi.object<ConditionsModelData>()
|
149
|
+
.description('Complete condition model with name and condition set')
|
150
|
+
.keys({
|
151
|
+
name: Joi.string()
|
152
|
+
.required()
|
153
|
+
.description('Unique identifier for the condition set'),
|
154
|
+
conditions: Joi.array()
|
155
|
+
.items(
|
156
|
+
Joi.alternatives().try(
|
157
|
+
conditionSchema,
|
158
|
+
conditionRefSchema,
|
159
|
+
conditionGroupSchema
|
160
|
+
)
|
161
|
+
)
|
162
|
+
.description(
|
163
|
+
'Array of conditions, condition references, or condition groups'
|
164
|
+
)
|
165
|
+
})
|
92
166
|
|
93
|
-
const conditionWrapperSchema = Joi.object<ConditionWrapper>()
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
167
|
+
const conditionWrapperSchema = Joi.object<ConditionWrapper>()
|
168
|
+
.description('Container for a named condition with its definition')
|
169
|
+
.keys({
|
170
|
+
name: Joi.string()
|
171
|
+
.required()
|
172
|
+
.description('Unique identifier used to reference this condition'),
|
173
|
+
displayName: Joi.string().description(
|
174
|
+
'Human-readable name for display in the UI'
|
175
|
+
),
|
176
|
+
value: conditionsModelSchema
|
177
|
+
.required()
|
178
|
+
.description('The complete condition definition')
|
179
|
+
})
|
98
180
|
|
99
181
|
export const componentSchema = Joi.object<ComponentDef>()
|
182
|
+
.description('Form component definition specifying UI element behavior')
|
100
183
|
.keys({
|
101
|
-
id: Joi.string()
|
102
|
-
|
103
|
-
|
184
|
+
id: Joi.string()
|
185
|
+
.uuid()
|
186
|
+
.optional()
|
187
|
+
.description('Unique identifier for the component'),
|
188
|
+
type: Joi.string<ComponentType>()
|
189
|
+
.required()
|
190
|
+
.description('Component type (TextField, RadioButtons, DateField, etc.)'),
|
191
|
+
shortDescription: Joi.string()
|
192
|
+
.optional()
|
193
|
+
.description('Brief description of the component purpose'),
|
104
194
|
name: Joi.when('type', {
|
105
195
|
is: Joi.string().valid(
|
106
196
|
ComponentType.Details,
|
@@ -110,8 +200,11 @@ export const componentSchema = Joi.object<ComponentDef>()
|
|
110
200
|
),
|
111
201
|
then: Joi.string()
|
112
202
|
.pattern(/^[a-zA-Z]+$/)
|
113
|
-
.optional()
|
114
|
-
|
203
|
+
.optional()
|
204
|
+
.description('Optional identifier for display-only components'),
|
205
|
+
otherwise: Joi.string()
|
206
|
+
.pattern(/^[a-zA-Z]+$/)
|
207
|
+
.description('Unique identifier for the component, used in form data')
|
115
208
|
}),
|
116
209
|
title: Joi.when('type', {
|
117
210
|
is: Joi.string().valid(
|
@@ -120,240 +213,476 @@ export const componentSchema = Joi.object<ComponentDef>()
|
|
120
213
|
ComponentType.InsetText,
|
121
214
|
ComponentType.Markdown
|
122
215
|
),
|
123
|
-
then: Joi.string()
|
124
|
-
|
216
|
+
then: Joi.string()
|
217
|
+
.optional()
|
218
|
+
.description('Optional title for display-only components'),
|
219
|
+
otherwise: Joi.string()
|
220
|
+
.allow('')
|
221
|
+
.description('Label displayed above the component')
|
125
222
|
}),
|
126
|
-
hint: Joi.string()
|
223
|
+
hint: Joi.string()
|
224
|
+
.allow('')
|
225
|
+
.optional()
|
226
|
+
.description(
|
227
|
+
'Additional guidance text displayed below the component title'
|
228
|
+
),
|
127
229
|
options: Joi.object({
|
128
|
-
rows: Joi.number()
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
230
|
+
rows: Joi.number()
|
231
|
+
.empty('')
|
232
|
+
.description('Number of rows for textarea components'),
|
233
|
+
maxWords: Joi.number()
|
234
|
+
.empty('')
|
235
|
+
.description('Maximum number of words allowed in text inputs'),
|
236
|
+
maxDaysInPast: Joi.number()
|
237
|
+
.empty('')
|
238
|
+
.description('Maximum days in the past allowed for date inputs'),
|
239
|
+
maxDaysInFuture: Joi.number()
|
240
|
+
.empty('')
|
241
|
+
.description('Maximum days in the future allowed for date inputs'),
|
242
|
+
customValidationMessage: Joi.string()
|
243
|
+
.allow('')
|
244
|
+
.description('Custom error message for validation failures'),
|
133
245
|
customValidationMessages: Joi.object<LanguageMessages>()
|
134
246
|
.unknown(true)
|
135
247
|
.optional()
|
248
|
+
.description('Custom error messages keyed by validation rule name')
|
136
249
|
})
|
137
250
|
.default({})
|
138
|
-
.unknown(true)
|
251
|
+
.unknown(true)
|
252
|
+
.description('Component-specific configuration options'),
|
139
253
|
schema: Joi.object({
|
140
|
-
min: Joi.number()
|
141
|
-
|
142
|
-
|
254
|
+
min: Joi.number()
|
255
|
+
.empty('')
|
256
|
+
.description('Minimum value or length for validation'),
|
257
|
+
max: Joi.number()
|
258
|
+
.empty('')
|
259
|
+
.description('Maximum value or length for validation'),
|
260
|
+
length: Joi.number()
|
261
|
+
.empty('')
|
262
|
+
.description('Exact length required for validation')
|
143
263
|
})
|
144
264
|
.unknown(true)
|
145
|
-
.default({})
|
146
|
-
|
265
|
+
.default({})
|
266
|
+
.description('Validation rules for the component'),
|
267
|
+
list: Joi.string()
|
268
|
+
.optional()
|
269
|
+
.description(
|
270
|
+
'Reference to a predefined list of options for select components'
|
271
|
+
)
|
147
272
|
})
|
148
273
|
.unknown(true)
|
149
274
|
|
150
|
-
export const componentSchemaV2 = componentSchema
|
151
|
-
|
152
|
-
.
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
options
|
180
|
-
|
181
|
-
|
182
|
-
.
|
275
|
+
export const componentSchemaV2 = componentSchema
|
276
|
+
.keys({
|
277
|
+
id: Joi.string()
|
278
|
+
.uuid()
|
279
|
+
.default(() => uuidV4())
|
280
|
+
.description('Auto-generated unique identifier for the component')
|
281
|
+
})
|
282
|
+
.description('Enhanced component schema for V2 forms with auto-generated IDs')
|
283
|
+
|
284
|
+
const nextSchema = Joi.object<Link>()
|
285
|
+
.description('Navigation link defining where to go after completing a page')
|
286
|
+
.keys({
|
287
|
+
path: Joi.string()
|
288
|
+
.required()
|
289
|
+
.description('The target page path to navigate to'),
|
290
|
+
condition: Joi.string()
|
291
|
+
.allow('')
|
292
|
+
.optional()
|
293
|
+
.description(
|
294
|
+
'Optional condition that determines if this path should be taken'
|
295
|
+
),
|
296
|
+
redirect: Joi.string()
|
297
|
+
.optional()
|
298
|
+
.description(
|
299
|
+
'Optional external URL to redirect to instead of an internal page'
|
300
|
+
)
|
301
|
+
})
|
302
|
+
|
303
|
+
const repeatOptions = Joi.object<RepeatOptions>()
|
304
|
+
.description('Configuration options for a repeatable page section')
|
305
|
+
.keys({
|
306
|
+
name: Joi.string()
|
307
|
+
.required()
|
308
|
+
.description(
|
309
|
+
'Identifier for the repeatable section, used in data structure'
|
310
|
+
),
|
311
|
+
title: Joi.string()
|
312
|
+
.required()
|
313
|
+
.description('Title displayed for each repeatable item')
|
314
|
+
})
|
315
|
+
|
316
|
+
const repeatSchema = Joi.object<RepeatSchema>()
|
317
|
+
.description('Validation rules for a repeatable section')
|
318
|
+
.keys({
|
319
|
+
min: Joi.number()
|
320
|
+
.empty('')
|
321
|
+
.required()
|
322
|
+
.description('Minimum number of repetitions required'),
|
323
|
+
max: Joi.number()
|
324
|
+
.empty('')
|
325
|
+
.required()
|
326
|
+
.description('Maximum number of repetitions allowed')
|
327
|
+
})
|
328
|
+
|
329
|
+
const pageRepeatSchema = Joi.object<Repeat>()
|
330
|
+
.description('Complete configuration for a repeatable page')
|
331
|
+
.keys({
|
332
|
+
options: repeatOptions
|
333
|
+
.required()
|
334
|
+
.description('Display and identification options for the repetition'),
|
335
|
+
schema: repeatSchema
|
183
336
|
.required()
|
337
|
+
.description('Validation constraints for the number of repetitions')
|
184
338
|
})
|
185
|
-
})
|
186
339
|
|
187
|
-
const
|
188
|
-
|
189
|
-
|
190
|
-
|
340
|
+
const eventSchema = Joi.object<Event>()
|
341
|
+
.description('Event handler configuration for page lifecycle events')
|
342
|
+
.keys({
|
343
|
+
type: Joi.string()
|
344
|
+
.allow('http')
|
345
|
+
.required()
|
346
|
+
.description(
|
347
|
+
'Type of the event handler (currently only "http" supported)'
|
348
|
+
),
|
349
|
+
options: Joi.object<EventOptions>()
|
350
|
+
.description('Options specific to the event handler type')
|
351
|
+
.keys({
|
352
|
+
method: Joi.string()
|
353
|
+
.allow('POST')
|
354
|
+
.required()
|
355
|
+
.description('HTTP method to use for the request'),
|
356
|
+
url: Joi.string()
|
357
|
+
.uri({ scheme: ['http', 'https'] })
|
358
|
+
.required()
|
359
|
+
.description('URL endpoint to call when the event fires')
|
360
|
+
})
|
361
|
+
})
|
362
|
+
|
363
|
+
const eventsSchema = Joi.object<Events>()
|
364
|
+
.description(
|
365
|
+
'Collection of event handlers for different page lifecycle events'
|
366
|
+
)
|
367
|
+
.keys({
|
368
|
+
onLoad: eventSchema
|
369
|
+
.optional()
|
370
|
+
.description('Event handler triggered when the page is loaded'),
|
371
|
+
onSave: eventSchema
|
372
|
+
.optional()
|
373
|
+
.description('Event handler triggered when the page data is saved')
|
374
|
+
})
|
191
375
|
|
192
376
|
/**
|
193
377
|
* `/status` is a special route for providing a user's application status.
|
194
378
|
* It should not be configured via the designer.
|
195
379
|
*/
|
196
|
-
export const pageSchema = Joi.object<Page>()
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
380
|
+
export const pageSchema = Joi.object<Page>()
|
381
|
+
.description('Form page definition specifying content and behavior')
|
382
|
+
.keys({
|
383
|
+
id: Joi.string()
|
384
|
+
.uuid()
|
385
|
+
.optional()
|
386
|
+
.description('Unique identifier for the page'),
|
387
|
+
path: Joi.string()
|
388
|
+
.required()
|
389
|
+
.disallow('/status')
|
390
|
+
.description(
|
391
|
+
'URL path for this page, must not be the reserved "/status" path'
|
392
|
+
),
|
393
|
+
title: Joi.string()
|
394
|
+
.required()
|
395
|
+
.description('Page title displayed at the top of the page'),
|
396
|
+
section: Joi.string().description('Section this page belongs to'),
|
397
|
+
controller: Joi.string()
|
398
|
+
.optional()
|
399
|
+
.description('Custom controller class name for special page behavior'),
|
400
|
+
components: Joi.array<ComponentDef>()
|
401
|
+
.items(componentSchema)
|
402
|
+
.unique('name')
|
403
|
+
.description('UI components displayed on this page'),
|
404
|
+
repeat: Joi.when('controller', {
|
405
|
+
is: Joi.string().valid('RepeatPageController').required(),
|
406
|
+
then: pageRepeatSchema
|
407
|
+
.required()
|
408
|
+
.description(
|
409
|
+
'Configuration for repeatable pages, required when using RepeatPageController'
|
410
|
+
),
|
411
|
+
otherwise: Joi.any().strip()
|
412
|
+
}),
|
413
|
+
condition: Joi.string()
|
414
|
+
.allow('')
|
415
|
+
.optional()
|
416
|
+
.description('Optional condition that determines if this page is shown'),
|
417
|
+
next: Joi.array<Link>()
|
418
|
+
.items(nextSchema)
|
419
|
+
.default([])
|
420
|
+
.description('Possible navigation paths after this page'),
|
421
|
+
events: eventsSchema
|
422
|
+
.optional()
|
423
|
+
.description('Event handlers for page lifecycle events'),
|
424
|
+
view: Joi.string()
|
425
|
+
.optional()
|
426
|
+
.description(
|
427
|
+
'Optional custom view template to use for rendering this page'
|
428
|
+
)
|
429
|
+
})
|
213
430
|
|
214
431
|
/**
|
215
432
|
* V2 engine schema - used with new editor
|
216
433
|
*/
|
217
|
-
export const pageSchemaV2 = pageSchema
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
434
|
+
export const pageSchemaV2 = pageSchema
|
435
|
+
.append({
|
436
|
+
title: Joi.string()
|
437
|
+
.allow('')
|
438
|
+
.required()
|
439
|
+
.description('Page title with enhanced support for empty titles in V2')
|
440
|
+
})
|
441
|
+
.description(
|
442
|
+
'Enhanced page schema for V2 forms with support for empty titles'
|
443
|
+
)
|
444
|
+
|
445
|
+
export const pageSchemaPayloadV2 = pageSchemaV2
|
446
|
+
.keys({
|
447
|
+
id: Joi.string()
|
448
|
+
.uuid()
|
449
|
+
.default(() => uuidV4())
|
450
|
+
.description('Auto-generated unique identifier for the page'),
|
451
|
+
components: Joi.array<ComponentDef>()
|
452
|
+
.items(componentSchemaV2)
|
453
|
+
.unique('name')
|
454
|
+
.unique('id', { ignoreUndefined: true })
|
455
|
+
.description('Components with auto-generated IDs')
|
456
|
+
})
|
457
|
+
.description(
|
458
|
+
'Page schema for payload data with auto-generated IDs for pages and components'
|
459
|
+
)
|
460
|
+
|
461
|
+
const baseListItemSchema = Joi.object<Item>()
|
462
|
+
.description('Base schema for list items with common properties')
|
463
|
+
.keys({
|
464
|
+
text: Joi.string().allow('').description('Display text shown to the user'),
|
465
|
+
description: Joi.string()
|
466
|
+
.allow('')
|
467
|
+
.optional()
|
468
|
+
.description('Optional additional descriptive text for the item'),
|
469
|
+
conditional: Joi.object<Item['conditional']>()
|
470
|
+
.description('Optional components to show when this item is selected')
|
471
|
+
.keys({
|
472
|
+
components: Joi.array<ComponentDef>()
|
473
|
+
.required()
|
474
|
+
.items(componentSchema.unknown(true))
|
475
|
+
.unique('name')
|
476
|
+
.description('Components to display conditionally')
|
477
|
+
})
|
478
|
+
.optional(),
|
479
|
+
condition: Joi.string()
|
480
|
+
.allow('')
|
481
|
+
.optional()
|
482
|
+
.description('Condition that determines if this item is shown')
|
483
|
+
})
|
484
|
+
|
485
|
+
const stringListItemSchema = baseListItemSchema
|
486
|
+
.append({
|
487
|
+
value: Joi.string()
|
488
|
+
.required()
|
489
|
+
.description('String value stored when this item is selected')
|
490
|
+
})
|
491
|
+
.description('List item with string value')
|
492
|
+
|
493
|
+
const numberListItemSchema = baseListItemSchema
|
494
|
+
.append({
|
495
|
+
value: Joi.number()
|
496
|
+
.required()
|
497
|
+
.description('Numeric value stored when this item is selected')
|
498
|
+
})
|
499
|
+
.description('List item with numeric value')
|
500
|
+
|
501
|
+
export const listSchema = Joi.object<List>()
|
502
|
+
.description('Reusable list of options for select components')
|
503
|
+
.keys({
|
504
|
+
id: Joi.string()
|
505
|
+
.uuid()
|
506
|
+
.optional()
|
507
|
+
.description('Unique identifier for the list'),
|
508
|
+
name: Joi.string()
|
509
|
+
.required()
|
510
|
+
.description('Name used to reference this list from components'),
|
511
|
+
title: Joi.string()
|
512
|
+
.required()
|
513
|
+
.description('Human-readable title for the list'),
|
514
|
+
type: Joi.string()
|
515
|
+
.required()
|
516
|
+
.valid('string', 'number')
|
517
|
+
.description('Data type for list values (string or number)'),
|
518
|
+
items: Joi.when('type', {
|
519
|
+
is: 'string',
|
520
|
+
then: Joi.array()
|
521
|
+
.items(stringListItemSchema)
|
522
|
+
.unique('text')
|
523
|
+
.unique('value')
|
524
|
+
.description('Array of items with string values'),
|
525
|
+
otherwise: Joi.array()
|
526
|
+
.items(numberListItemSchema)
|
527
|
+
.unique('text')
|
528
|
+
.unique('value')
|
529
|
+
.description('Array of items with numeric values')
|
240
530
|
})
|
241
|
-
.optional(),
|
242
|
-
condition: Joi.string().allow('').optional()
|
243
|
-
})
|
244
|
-
|
245
|
-
const stringListItemSchema = baseListItemSchema.append({
|
246
|
-
value: Joi.string().required()
|
247
|
-
})
|
248
|
-
|
249
|
-
const numberListItemSchema = baseListItemSchema.append({
|
250
|
-
value: Joi.number().required()
|
251
|
-
})
|
252
|
-
|
253
|
-
export const listSchema = Joi.object<List>().keys({
|
254
|
-
id: Joi.string().uuid().optional(),
|
255
|
-
name: Joi.string().required(),
|
256
|
-
title: Joi.string().required(),
|
257
|
-
type: Joi.string().required().valid('string', 'number'),
|
258
|
-
items: Joi.when('type', {
|
259
|
-
is: 'string',
|
260
|
-
then: Joi.array()
|
261
|
-
.items(stringListItemSchema)
|
262
|
-
.unique('text')
|
263
|
-
.unique('value'),
|
264
|
-
otherwise: Joi.array()
|
265
|
-
.items(numberListItemSchema)
|
266
|
-
.unique('text')
|
267
|
-
.unique('value')
|
268
531
|
})
|
269
|
-
})
|
270
532
|
|
271
533
|
/**
|
272
534
|
* v2 Joi schema for Lists
|
273
535
|
*/
|
274
|
-
export const listSchemaV2 = listSchema
|
275
|
-
|
276
|
-
.
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
url: Joi.when('feedbackForm', {
|
283
|
-
is: Joi.boolean().valid(false),
|
284
|
-
then: Joi.string().optional().allow('')
|
285
|
-
}),
|
286
|
-
emailAddress: Joi.string()
|
287
|
-
.email({
|
288
|
-
tlds: {
|
289
|
-
allow: false
|
290
|
-
}
|
291
|
-
})
|
292
|
-
.optional()
|
293
|
-
})
|
536
|
+
export const listSchemaV2 = listSchema
|
537
|
+
.keys({
|
538
|
+
id: Joi.string()
|
539
|
+
.uuid()
|
540
|
+
.default(() => uuidV4())
|
541
|
+
.description('Auto-generated unique identifier for the list')
|
542
|
+
})
|
543
|
+
.description('Enhanced list schema for V2 forms with auto-generated IDs')
|
294
544
|
|
295
|
-
const
|
296
|
-
|
297
|
-
|
545
|
+
const feedbackSchema = Joi.object<FormDefinition['feedback']>()
|
546
|
+
.description('Feedback configuration for the form')
|
547
|
+
.keys({
|
548
|
+
feedbackForm: Joi.boolean()
|
549
|
+
.default(false)
|
550
|
+
.description('Whether to show the built-in feedback form'),
|
551
|
+
url: Joi.when('feedbackForm', {
|
552
|
+
is: Joi.boolean().valid(false),
|
553
|
+
then: Joi.string()
|
554
|
+
.optional()
|
555
|
+
.allow('')
|
556
|
+
.description(
|
557
|
+
'URL to an external feedback form when not using built-in feedback'
|
558
|
+
)
|
559
|
+
}),
|
560
|
+
emailAddress: Joi.string()
|
561
|
+
.email({
|
562
|
+
tlds: {
|
563
|
+
allow: false
|
564
|
+
}
|
565
|
+
})
|
566
|
+
.optional()
|
567
|
+
.description('Email address where feedback is sent')
|
568
|
+
})
|
298
569
|
|
299
|
-
const
|
300
|
-
|
301
|
-
|
302
|
-
|
570
|
+
const phaseBannerSchema = Joi.object<PhaseBanner>()
|
571
|
+
.description('Phase banner configuration showing development status')
|
572
|
+
.keys({
|
573
|
+
phase: Joi.string()
|
574
|
+
.valid('alpha', 'beta')
|
575
|
+
.description('Development phase of the service (alpha or beta)')
|
576
|
+
})
|
577
|
+
|
578
|
+
const outputSchema = Joi.object<FormDefinition['output']>()
|
579
|
+
.description('Configuration for form submission output')
|
580
|
+
.keys({
|
581
|
+
audience: Joi.string()
|
582
|
+
.valid('human', 'machine')
|
583
|
+
.required()
|
584
|
+
.description(
|
585
|
+
'Target audience for the output (human readable or machine processable)'
|
586
|
+
),
|
587
|
+
version: Joi.string()
|
588
|
+
.required()
|
589
|
+
.description('Version identifier for the output format')
|
590
|
+
})
|
303
591
|
|
304
592
|
/**
|
305
593
|
* Joi schema for `FormDefinition` interface
|
306
594
|
* @see {@link FormDefinition}
|
307
595
|
*/
|
308
596
|
export const formDefinitionSchema = Joi.object<FormDefinition>()
|
597
|
+
.description('Complete form definition describing all aspects of a form')
|
309
598
|
.required()
|
310
599
|
.keys({
|
311
|
-
engine: Joi.string()
|
312
|
-
|
313
|
-
|
314
|
-
|
600
|
+
engine: Joi.string()
|
601
|
+
.allow('V1', 'V2')
|
602
|
+
.default('V1')
|
603
|
+
.description('Form engine version to use (V1 or V2)'),
|
604
|
+
name: Joi.string()
|
605
|
+
.allow('')
|
606
|
+
.optional()
|
607
|
+
.description('Unique name identifying the form'),
|
608
|
+
feedback: feedbackSchema
|
609
|
+
.optional()
|
610
|
+
.description('Feedback mechanism configuration'),
|
611
|
+
startPage: Joi.string()
|
612
|
+
.optional()
|
613
|
+
.description('Path of the first page to show when starting the form'),
|
315
614
|
pages: Joi.array<Page>()
|
316
615
|
.required()
|
317
616
|
.when('engine', {
|
318
617
|
is: 'V2',
|
319
|
-
then: Joi.array<Page>()
|
320
|
-
|
618
|
+
then: Joi.array<Page>()
|
619
|
+
.items(pageSchemaV2)
|
620
|
+
.description('Pages using V2 schema with enhanced features'),
|
621
|
+
otherwise: Joi.array<Page>()
|
622
|
+
.items(pageSchema)
|
623
|
+
.description('Pages using standard V1 schema')
|
321
624
|
})
|
322
625
|
.unique('path')
|
323
|
-
.unique('id', { ignoreUndefined: true })
|
626
|
+
.unique('id', { ignoreUndefined: true })
|
627
|
+
.description('All pages within the form'),
|
324
628
|
sections: Joi.array<Section>()
|
325
629
|
.items(sectionsSchema)
|
326
630
|
.unique('name')
|
327
631
|
.unique('title')
|
328
|
-
.required()
|
632
|
+
.required()
|
633
|
+
.description('Sections grouping related pages together'),
|
329
634
|
conditions: Joi.array<ConditionWrapper>()
|
330
635
|
.items(conditionWrapperSchema)
|
331
636
|
.unique('name')
|
332
|
-
.unique('displayName')
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
637
|
+
.unique('displayName')
|
638
|
+
.description('Named conditions used for form logic'),
|
639
|
+
lists: Joi.array<List>()
|
640
|
+
.items(listSchema)
|
641
|
+
.unique('name')
|
642
|
+
.unique('title')
|
643
|
+
.description('Reusable lists of options for select components'),
|
644
|
+
metadata: Joi.object({ a: Joi.any() })
|
645
|
+
.unknown()
|
646
|
+
.optional()
|
647
|
+
.description('Custom metadata for the form'),
|
648
|
+
declaration: Joi.string()
|
649
|
+
.allow('')
|
650
|
+
.optional()
|
651
|
+
.description('Declaration text shown on the summary page'),
|
652
|
+
skipSummary: Joi.any()
|
653
|
+
.strip()
|
654
|
+
.description('option to skip the summary page'),
|
655
|
+
phaseBanner: phaseBannerSchema
|
656
|
+
.optional()
|
657
|
+
.description('Phase banner configuration'),
|
338
658
|
outputEmail: Joi.string()
|
339
659
|
.email({ tlds: { allow: ['uk'] } })
|
340
660
|
.trim()
|
341
|
-
.optional()
|
342
|
-
|
661
|
+
.optional()
|
662
|
+
.description('Email address where form submissions are sent'),
|
663
|
+
output: outputSchema
|
664
|
+
.optional()
|
665
|
+
.description('Configuration for submission output format')
|
343
666
|
})
|
344
667
|
|
345
|
-
export const formDefinitionV2PayloadSchema = formDefinitionSchema
|
346
|
-
|
347
|
-
.
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
.
|
354
|
-
|
355
|
-
|
356
|
-
|
668
|
+
export const formDefinitionV2PayloadSchema = formDefinitionSchema
|
669
|
+
.keys({
|
670
|
+
pages: Joi.array<Page>()
|
671
|
+
.items(pageSchemaPayloadV2)
|
672
|
+
.required()
|
673
|
+
.unique('path')
|
674
|
+
.unique('id', { ignoreUndefined: true })
|
675
|
+
.description('Pages with auto-generated IDs for V2 forms'),
|
676
|
+
lists: Joi.array<List>()
|
677
|
+
.items(listSchemaV2)
|
678
|
+
.unique('name')
|
679
|
+
.unique('title')
|
680
|
+
.unique('id', { ignoreUndefined: true })
|
681
|
+
.description('Lists with auto-generated IDs for V2 forms')
|
682
|
+
})
|
683
|
+
.description(
|
684
|
+
'Enhanced form definition schema for V2 payloads with auto-generated IDs'
|
685
|
+
)
|
357
686
|
|
358
687
|
// Maintain compatibility with legacy named export
|
359
688
|
// E.g. `import { Schema } from '@defra/forms-model'`
|