@defra/forms-engine-plugin 4.0.7 → 4.0.8
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/.server/server/forms/components.json +7 -0
- package/.server/server/forms/register-as-a-unicorn-breeder.yaml +12 -1
- package/.server/server/plugins/engine/components/ComponentBase.d.ts +1 -1
- package/.server/server/plugins/engine/components/ComponentBase.js.map +1 -1
- package/.server/server/plugins/engine/components/ComponentCollection.js.map +1 -1
- package/.server/server/plugins/engine/components/DeclarationField.d.ts +81 -0
- package/.server/server/plugins/engine/components/DeclarationField.js +123 -0
- package/.server/server/plugins/engine/components/DeclarationField.js.map +1 -0
- package/.server/server/plugins/engine/components/helpers/components.d.ts +1 -1
- package/.server/server/plugins/engine/components/helpers/components.js +3 -0
- package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
- package/.server/server/plugins/engine/components/index.d.ts +1 -0
- package/.server/server/plugins/engine/components/index.js +1 -0
- package/.server/server/plugins/engine/components/index.js.map +1 -1
- package/.server/server/plugins/engine/pageControllers/validationOptions.js +1 -0
- package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
- package/.server/server/plugins/engine/views/components/declarationfield.html +14 -0
- package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
- package/.server/server/plugins/nunjucks/filters/index.d.ts +1 -0
- package/.server/server/plugins/nunjucks/filters/index.js +1 -0
- package/.server/server/plugins/nunjucks/filters/index.js.map +1 -1
- package/.server/server/plugins/nunjucks/filters/merge.d.ts +7 -0
- package/.server/server/plugins/nunjucks/filters/merge.js +16 -0
- package/.server/server/plugins/nunjucks/filters/merge.js.map +1 -0
- package/.server/server/plugins/nunjucks/filters/merge.test.js +19 -0
- package/.server/server/plugins/nunjucks/filters/merge.test.js.map +1 -0
- package/package.json +2 -2
- package/src/server/forms/components.json +7 -0
- package/src/server/forms/page-events.yaml +1 -1
- package/src/server/forms/register-as-a-unicorn-breeder.yaml +12 -1
- package/src/server/index.test.ts +1 -0
- package/src/server/plugins/engine/components/ComponentBase.ts +1 -0
- package/src/server/plugins/engine/components/ComponentCollection.ts +1 -0
- package/src/server/plugins/engine/components/DeclarationField.test.ts +426 -0
- package/src/server/plugins/engine/components/DeclarationField.ts +167 -0
- package/src/server/plugins/engine/components/helpers/components.ts +5 -0
- package/src/server/plugins/engine/components/index.ts +1 -0
- package/src/server/plugins/engine/models/SummaryViewModel.test.ts +76 -3
- package/src/server/plugins/engine/models/SummaryViewModel.ts +5 -1
- package/src/server/plugins/engine/pageControllers/validationOptions.ts +4 -0
- package/src/server/plugins/engine/views/components/declarationfield.html +14 -0
- package/src/server/plugins/nunjucks/filters/index.js +1 -0
- package/src/server/plugins/nunjucks/filters/merge.js +16 -0
- package/src/server/plugins/nunjucks/filters/merge.test.js +15 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { type DeclarationFieldComponent, type Item } from '@defra/forms-model'
|
|
2
|
+
import joi, {
|
|
3
|
+
type ArraySchema,
|
|
4
|
+
type BooleanSchema,
|
|
5
|
+
type StringSchema
|
|
6
|
+
} from 'joi'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
FormComponent,
|
|
10
|
+
isFormValue
|
|
11
|
+
} from '~/src/server/plugins/engine/components/FormComponent.js'
|
|
12
|
+
import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js'
|
|
13
|
+
import {
|
|
14
|
+
type ErrorMessageTemplateList,
|
|
15
|
+
type FormPayload,
|
|
16
|
+
type FormState,
|
|
17
|
+
type FormStateValue,
|
|
18
|
+
type FormSubmissionError,
|
|
19
|
+
type FormSubmissionState,
|
|
20
|
+
type FormValue
|
|
21
|
+
} from '~/src/server/plugins/engine/types.js'
|
|
22
|
+
|
|
23
|
+
export class DeclarationField extends FormComponent {
|
|
24
|
+
private readonly DEFAULT_DECLARATION_LABEL = 'I understand and agree'
|
|
25
|
+
|
|
26
|
+
declare options: DeclarationFieldComponent['options']
|
|
27
|
+
|
|
28
|
+
declare declarationConfirmationLabel: string
|
|
29
|
+
|
|
30
|
+
declare formSchema: ArraySchema<StringSchema[]>
|
|
31
|
+
declare stateSchema: BooleanSchema
|
|
32
|
+
declare content: string
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
def: DeclarationFieldComponent,
|
|
36
|
+
props: ConstructorParameters<typeof FormComponent>[1]
|
|
37
|
+
) {
|
|
38
|
+
super(def, props)
|
|
39
|
+
|
|
40
|
+
const { options, content } = def
|
|
41
|
+
|
|
42
|
+
let checkboxSchema = joi.string().valid('true')
|
|
43
|
+
|
|
44
|
+
if (options.required !== false) {
|
|
45
|
+
checkboxSchema = checkboxSchema.required()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const formSchema = joi
|
|
49
|
+
.array()
|
|
50
|
+
.items(checkboxSchema, joi.string().valid('unchecked').strip())
|
|
51
|
+
.label(this.label)
|
|
52
|
+
.single()
|
|
53
|
+
.messages({
|
|
54
|
+
'any.required': messageTemplate.declarationRequired as string,
|
|
55
|
+
'any.unknown': messageTemplate.declarationRequired as string,
|
|
56
|
+
'array.includesRequiredUnknowns':
|
|
57
|
+
messageTemplate.declarationRequired as string
|
|
58
|
+
}) as ArraySchema<StringSchema[]>
|
|
59
|
+
|
|
60
|
+
this.formSchema = formSchema
|
|
61
|
+
this.stateSchema = joi.boolean().cast('string').label(this.label).required()
|
|
62
|
+
|
|
63
|
+
this.options = options
|
|
64
|
+
this.content = content
|
|
65
|
+
this.declarationConfirmationLabel =
|
|
66
|
+
options.declarationConfirmationLabel ?? this.DEFAULT_DECLARATION_LABEL
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getFormValueFromState(state: FormSubmissionState) {
|
|
70
|
+
const { name } = this
|
|
71
|
+
return state[name] === true ? 'true' : undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getFormDataFromState(state: FormSubmissionState): FormPayload {
|
|
75
|
+
const { name } = this
|
|
76
|
+
return { [name]: state[name] === true ? 'true' : undefined }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getStateFromValidForm(payload: FormPayload): FormState {
|
|
80
|
+
const { name } = this
|
|
81
|
+
const payloadValue = payload[name]
|
|
82
|
+
const value =
|
|
83
|
+
this.isValue(payloadValue) &&
|
|
84
|
+
payloadValue.length > 0 &&
|
|
85
|
+
payloadValue.every((v) => {
|
|
86
|
+
return v === 'true'
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
return { [name]: value }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getContextValueFromFormValue(value: FormValue | FormPayload): boolean {
|
|
93
|
+
return value === 'true'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getFormValue(value?: FormStateValue | FormState) {
|
|
97
|
+
return this.isValue(value) ? value : undefined
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getDisplayStringFromFormValue(value: FormValue | FormPayload): string {
|
|
101
|
+
return value ? this.declarationConfirmationLabel : ''
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) {
|
|
105
|
+
const defaultDeclarationConfirmationLabel =
|
|
106
|
+
'I confirm that I understand and accept this declaration'
|
|
107
|
+
const {
|
|
108
|
+
title,
|
|
109
|
+
hint,
|
|
110
|
+
content,
|
|
111
|
+
declarationConfirmationLabel = defaultDeclarationConfirmationLabel
|
|
112
|
+
} = this
|
|
113
|
+
return {
|
|
114
|
+
...super.getViewModel(payload, errors),
|
|
115
|
+
hint: hint ? { text: hint } : undefined,
|
|
116
|
+
fieldset: {
|
|
117
|
+
legend: {
|
|
118
|
+
text: title
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
content,
|
|
122
|
+
values: payload[this.name],
|
|
123
|
+
items: [
|
|
124
|
+
{
|
|
125
|
+
text: declarationConfirmationLabel,
|
|
126
|
+
value: 'true'
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
isValue(value?: FormStateValue | FormState): value is Item['value'][] {
|
|
133
|
+
if (!Array.isArray(value)) {
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Skip checks when empty
|
|
138
|
+
if (!value.length) {
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return value.every(isFormValue)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* For error preview page that shows all possible errors on a component
|
|
147
|
+
*/
|
|
148
|
+
getAllPossibleErrors(): ErrorMessageTemplateList {
|
|
149
|
+
return DeclarationField.getAllPossibleErrors()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Static version of getAllPossibleErrors that doesn't require a component instance.
|
|
154
|
+
*/
|
|
155
|
+
static getAllPossibleErrors(): ErrorMessageTemplateList {
|
|
156
|
+
return {
|
|
157
|
+
baseErrors: [
|
|
158
|
+
{ type: 'required', template: messageTemplate.declarationRequired }
|
|
159
|
+
],
|
|
160
|
+
advancedSettingsErrors: []
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
static isBool(value?: FormStateValue | FormState): value is boolean {
|
|
165
|
+
return isFormValue(value) && typeof value === 'boolean'
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -20,6 +20,7 @@ export type Field = InstanceType<
|
|
|
20
20
|
| typeof Components.YesNoField
|
|
21
21
|
| typeof Components.CheckboxesField
|
|
22
22
|
| typeof Components.DatePartsField
|
|
23
|
+
| typeof Components.DeclarationField
|
|
23
24
|
| typeof Components.EastingNorthingField
|
|
24
25
|
| typeof Components.EmailAddressField
|
|
25
26
|
| typeof Components.LatLongField
|
|
@@ -102,6 +103,10 @@ export function createComponent(
|
|
|
102
103
|
component = new Components.DatePartsField(def, options)
|
|
103
104
|
break
|
|
104
105
|
|
|
106
|
+
case ComponentType.DeclarationField:
|
|
107
|
+
component = new Components.DeclarationField(def, options)
|
|
108
|
+
break
|
|
109
|
+
|
|
105
110
|
case ComponentType.Details:
|
|
106
111
|
component = new Components.Details(def, options)
|
|
107
112
|
break
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
export { AutocompleteField } from '~/src/server/plugins/engine/components/AutocompleteField.js'
|
|
8
8
|
export { CheckboxesField } from '~/src/server/plugins/engine/components/CheckboxesField.js'
|
|
9
9
|
export { DatePartsField } from '~/src/server/plugins/engine/components/DatePartsField.js'
|
|
10
|
+
export { DeclarationField } from '~/src/server/plugins/engine/components/DeclarationField.js'
|
|
10
11
|
export { Details } from '~/src/server/plugins/engine/components/Details.js'
|
|
11
12
|
export { EmailAddressField } from '~/src/server/plugins/engine/components/EmailAddressField.js'
|
|
12
13
|
export { FileUploadField } from '~/src/server/plugins/engine/components/FileUploadField.js'
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type FormContextRequest,
|
|
16
16
|
type FormState
|
|
17
17
|
} from '~/src/server/plugins/engine/types.js'
|
|
18
|
+
import v2Definition from '~/test/form/definitions/conditions-relative-dates-v2.js'
|
|
18
19
|
import definition from '~/test/form/definitions/repeat-mixed.js'
|
|
19
20
|
const basePath = `${FORM_PREFIX}/test`
|
|
20
21
|
|
|
@@ -326,7 +327,7 @@ describe('SummaryPageController', () => {
|
|
|
326
327
|
expect(viewModel).toHaveProperty('allowSaveAndExit', true)
|
|
327
328
|
})
|
|
328
329
|
|
|
329
|
-
it('should display correct page title', () => {
|
|
330
|
+
it('should display correct page title for v1 form', () => {
|
|
330
331
|
const state: FormState = {
|
|
331
332
|
$$__referenceNumber: 'foobar',
|
|
332
333
|
orderType: 'collection',
|
|
@@ -334,9 +335,81 @@ describe('SummaryPageController', () => {
|
|
|
334
335
|
}
|
|
335
336
|
|
|
336
337
|
const context = model.getFormContext(request, state)
|
|
337
|
-
const viewModel = controller.
|
|
338
|
+
const viewModel = controller.getSummaryViewModel(request, context)
|
|
339
|
+
|
|
340
|
+
expect(viewModel.pageTitle).toBe(
|
|
341
|
+
'Check your answers before sending your form'
|
|
342
|
+
)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('should display default page title for v2 form when title not supplied', () => {
|
|
346
|
+
const state: FormState = {
|
|
347
|
+
$$__referenceNumber: 'foobar',
|
|
348
|
+
orderType: 'collection',
|
|
349
|
+
pizza: []
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const titleModel = new FormModel(v2Definition, {
|
|
353
|
+
basePath: `${FORM_PREFIX}/test`
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
controller = new SummaryPageController(titleModel, v2Definition.pages[5])
|
|
357
|
+
|
|
358
|
+
request = {
|
|
359
|
+
method: 'get',
|
|
360
|
+
url: new URL('http://example.com/repeat/pizza-order/summary'),
|
|
361
|
+
path: '/test/summary',
|
|
362
|
+
params: {
|
|
363
|
+
path: 'summary',
|
|
364
|
+
slug: 'test'
|
|
365
|
+
},
|
|
366
|
+
query: {},
|
|
367
|
+
app: { model: titleModel },
|
|
368
|
+
server: serverWithSaveAndExit
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const context = titleModel.getFormContext(request, state)
|
|
372
|
+
const viewModel = controller.getSummaryViewModel(request, context)
|
|
373
|
+
|
|
374
|
+
expect(viewModel.pageTitle).toBe(
|
|
375
|
+
'Check your answers before sending your form'
|
|
376
|
+
)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('should display override page title for v2 form when title supplied', () => {
|
|
380
|
+
const state: FormState = {
|
|
381
|
+
$$__referenceNumber: 'foobar',
|
|
382
|
+
orderType: 'collection',
|
|
383
|
+
pizza: []
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const v2DefinitionWithSummaryTitle = structuredClone(v2Definition)
|
|
387
|
+
const summaryPage = v2DefinitionWithSummaryTitle.pages[5]
|
|
388
|
+
summaryPage.title = 'Override summary title'
|
|
389
|
+
|
|
390
|
+
const titleModel = new FormModel(v2DefinitionWithSummaryTitle, {
|
|
391
|
+
basePath: `${FORM_PREFIX}/test`
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
controller = new SummaryPageController(titleModel, summaryPage)
|
|
395
|
+
|
|
396
|
+
request = {
|
|
397
|
+
method: 'get',
|
|
398
|
+
url: new URL('http://example.com/repeat/pizza-order/summary'),
|
|
399
|
+
path: '/test/summary',
|
|
400
|
+
params: {
|
|
401
|
+
path: 'summary',
|
|
402
|
+
slug: 'test'
|
|
403
|
+
},
|
|
404
|
+
query: {},
|
|
405
|
+
app: { model: titleModel },
|
|
406
|
+
server: serverWithSaveAndExit
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const context = titleModel.getFormContext(request, state)
|
|
410
|
+
const viewModel = controller.getSummaryViewModel(request, context)
|
|
338
411
|
|
|
339
|
-
expect(viewModel.pageTitle).toBe('
|
|
412
|
+
expect(viewModel.pageTitle).toBe('Override summary title')
|
|
340
413
|
})
|
|
341
414
|
})
|
|
342
415
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Section } from '@defra/forms-model'
|
|
1
|
+
import { SchemaVersion, type Section } from '@defra/forms-model'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
getAnswer,
|
|
@@ -64,6 +64,10 @@ export class SummaryViewModel {
|
|
|
64
64
|
|
|
65
65
|
this.page = page
|
|
66
66
|
this.pageTitle = page.title
|
|
67
|
+
if (def.schema === SchemaVersion.V2 && !page.title) {
|
|
68
|
+
this.pageTitle = 'Check your answers before sending your form'
|
|
69
|
+
}
|
|
70
|
+
|
|
67
71
|
this.serviceUrl = `/${basePath}`
|
|
68
72
|
this.name = def.name
|
|
69
73
|
this.declaration = def.declaration
|
|
@@ -20,6 +20,10 @@ const opts = {
|
|
|
20
20
|
* see @link https://joi.dev/api/?v=17.4.2#template-syntax for template syntax
|
|
21
21
|
*/
|
|
22
22
|
export const messageTemplate: Record<string, JoiExpression> = {
|
|
23
|
+
declarationRequired: joi.expression(
|
|
24
|
+
'You must confirm you understand and agree with the {{lowerFirst(#label)}} to continue',
|
|
25
|
+
opts
|
|
26
|
+
) as JoiExpression,
|
|
23
27
|
required: joi.expression(
|
|
24
28
|
'Enter {{lowerFirst(#label)}}',
|
|
25
29
|
opts
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{% from "govuk/components/fieldset/macro.njk" import govukFieldset %}
|
|
2
|
+
{% from "govuk/components/checkboxes/macro.njk" import govukCheckboxes %}
|
|
3
|
+
{% from "govuk/components/hint/macro.njk" import govukHint %}
|
|
4
|
+
|
|
5
|
+
{% macro DeclarationField(component) %}
|
|
6
|
+
{% set content %}
|
|
7
|
+
<div class="app-prose-scope">
|
|
8
|
+
{{ component.model.content | markdown | safe }}
|
|
9
|
+
</div>
|
|
10
|
+
{% endset %}
|
|
11
|
+
{% set checkboxes = component.model | merge({ formGroup: { beforeInputs: { html: content } } }) %}
|
|
12
|
+
{{ govukCheckboxes(checkboxes) }}
|
|
13
|
+
<input type="hidden" name="{{ component.model.name }}" value="unchecked">
|
|
14
|
+
{% endmacro %}
|
|
@@ -5,3 +5,4 @@ export { answer } from '~/src/server/plugins/nunjucks/filters/answer.js'
|
|
|
5
5
|
export { href } from '~/src/server/plugins/nunjucks/filters/href.js'
|
|
6
6
|
export { field } from '~/src/server/plugins/nunjucks/filters/field.js'
|
|
7
7
|
export { page } from '~/src/server/plugins/nunjucks/filters/page.js'
|
|
8
|
+
export { merge } from '~/src/server/plugins/nunjucks/filters/merge.js'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nunjucks filter to get the page for a given path
|
|
3
|
+
* @param {Record<string, any>} targetDictionary - Object to extend
|
|
4
|
+
* @param {Record<string, any> | string} sourceDictionary - Object to merge into target
|
|
5
|
+
* @returns {Record<string, any>}
|
|
6
|
+
*/
|
|
7
|
+
export function merge(targetDictionary, sourceDictionary) {
|
|
8
|
+
if (typeof sourceDictionary !== 'object') {
|
|
9
|
+
return targetDictionary
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
...targetDictionary,
|
|
14
|
+
...sourceDictionary
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { merge } from '~/src/server/plugins/nunjucks/filters/merge.js'
|
|
2
|
+
|
|
3
|
+
describe('merge', () => {
|
|
4
|
+
const propertyToMerge = { lorem: 'ipsum' }
|
|
5
|
+
it('should return the target if source is not an object', () => {
|
|
6
|
+
expect(merge(propertyToMerge, 'dolar')).toBe(propertyToMerge)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('should merge the properties if they are valid', () => {
|
|
10
|
+
expect(merge({ lorem: 'dolar', dolar: 'sit' }, propertyToMerge)).toEqual({
|
|
11
|
+
lorem: 'ipsum',
|
|
12
|
+
dolar: 'sit'
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
})
|