@planning-inspectorate/dynamic-forms 1.7.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/multi-field-input/index.njk +3 -1
- package/src/components/multi-field-input/question.js +8 -0
- package/src/components/text-entry-redact/index.njk +61 -60
- package/src/components/text-entry-redact/question.js +15 -1
- package/src/components/text-entry-redact/question.test.js +49 -7
- package/src/validator/date-time-validator.js +12 -2
- package/src/validator/date-time-validator.test.js +27 -2
package/package.json
CHANGED
|
@@ -60,11 +60,13 @@
|
|
|
60
60
|
errorMessage: errors[inputField.fieldName] and {
|
|
61
61
|
text: errors[inputField.fieldName].msg
|
|
62
62
|
},
|
|
63
|
+
prefix: inputField.prefix,
|
|
64
|
+
suffix: inputField.suffix,
|
|
63
65
|
value: inputField.value,
|
|
64
66
|
hint: {
|
|
65
67
|
text: inputField.hint
|
|
66
68
|
},
|
|
67
|
-
classes: "govuk-input-!-width-three-quarters",
|
|
69
|
+
classes: inputField.classes or "govuk-input-!-width-three-quarters",
|
|
68
70
|
attributes: inputField.attributes
|
|
69
71
|
}) }}
|
|
70
72
|
{% endfor %}
|
|
@@ -10,6 +10,12 @@ import { nl2br } from '../../lib/utils.js';
|
|
|
10
10
|
* @typedef {import('../../validator/base-validator')} BaseValidator
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} Suffix
|
|
15
|
+
* @property {string} text
|
|
16
|
+
* @property {string} [classes] optional property, used to add classes to the suffix/prefix
|
|
17
|
+
*/
|
|
18
|
+
|
|
13
19
|
/**
|
|
14
20
|
* @typedef {Object} InputField
|
|
15
21
|
* @property {string} fieldName
|
|
@@ -18,6 +24,8 @@ import { nl2br } from '../../lib/utils.js';
|
|
|
18
24
|
* @property {string} [formatPrefix] optional property, used by formatAnswerForSummary (eg task list display), to prefix answer
|
|
19
25
|
* @property {function} [formatTextFunction] optional property, used to format the answer for display and value in question
|
|
20
26
|
* @property {Record<string, string>} [attributes] optional property, used to add html attributes to the field
|
|
27
|
+
* @property {Suffix} [suffix] optional property, used to add a suffix to the field
|
|
28
|
+
* @property {Suffix} [prefix] optional property, used to add a prefix to the field
|
|
21
29
|
*/
|
|
22
30
|
|
|
23
31
|
/**
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{% extends layoutTemplate %}
|
|
2
2
|
|
|
3
|
-
{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
|
|
4
|
-
{% from "govuk/components/textarea/macro.njk" import govukTextarea %}
|
|
5
|
-
{% from "govuk/components/label/macro.njk" import govukLabel %}
|
|
6
3
|
{% from "govuk/components/button/macro.njk" import govukButton %}
|
|
7
4
|
{% from "govuk/components/checkboxes/macro.njk" import govukCheckboxes %}
|
|
8
|
-
{% from "govuk/components/
|
|
5
|
+
{% from "govuk/components/details/macro.njk" import govukDetails %}
|
|
6
|
+
{% from "govuk/components/error-summary/macro.njk" import govukErrorSummary %}
|
|
7
|
+
{% from "govuk/components/label/macro.njk" import govukLabel %}
|
|
9
8
|
{% from "govuk/components/table/macro.njk" import govukTable %}
|
|
9
|
+
{% from "govuk/components/textarea/macro.njk" import govukTextarea %}
|
|
10
10
|
|
|
11
11
|
{% block before_content %}
|
|
12
12
|
<div class="govuk-grid-row">
|
|
@@ -24,14 +24,9 @@
|
|
|
24
24
|
{{ super() }}
|
|
25
25
|
{% endblock before_content %}
|
|
26
26
|
|
|
27
|
-
{% set columnWidth = 'govuk-grid-column-two-thirds' %}
|
|
28
|
-
{% if showSuggestionsUi %}
|
|
29
|
-
{% set columnWidth = 'govuk-grid-column-one-half' %}
|
|
30
|
-
{% endif %}
|
|
31
|
-
|
|
32
27
|
{% block content %}
|
|
33
28
|
<div class="govuk-grid-row">
|
|
34
|
-
<div class="
|
|
29
|
+
<div class="govuk-grid-column-two-thirds">
|
|
35
30
|
<form action="" method="post" novalidate>
|
|
36
31
|
<input type="hidden" name="_csrf" value="{{ _csrf }}">
|
|
37
32
|
|
|
@@ -53,15 +48,59 @@
|
|
|
53
48
|
{% include question.html ignore missing %}
|
|
54
49
|
{% endif %}
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
{% if showSuggestionsUi %}
|
|
52
|
+
<h2 class="govuk-heading-m">Redaction suggestions</h2>
|
|
53
|
+
{% set hasSuggestions = redactionSuggestions.length > 0 %}
|
|
54
|
+
{% if hasSuggestions %}
|
|
55
|
+
<p class="govuk-body">
|
|
56
|
+
We have suggested some common redactions and hidden these for you.
|
|
57
|
+
To make these visible again, 'Reject' each suggestion.
|
|
58
|
+
</p>
|
|
59
|
+
{% set suggestionTableRows = [] %}
|
|
60
|
+
{% for s in redactionSuggestions %}
|
|
61
|
+
{% set category = s.category %}
|
|
62
|
+
{% if s.subCategory %}
|
|
63
|
+
{% set category = category + " (" + s.subCategory + ")" %}
|
|
64
|
+
{% endif %}
|
|
65
|
+
{% set action = 'Accept' %}
|
|
66
|
+
{% if s.accepted %}
|
|
67
|
+
{% set action = 'Reject' %}
|
|
68
|
+
{% endif %}
|
|
69
|
+
{% set suggestionTableRows = (suggestionTableRows.push([
|
|
70
|
+
{text: s.text},
|
|
71
|
+
{text: category },
|
|
72
|
+
{
|
|
73
|
+
html: '<a href="#" class="govuk-link" data-redact="suggestion-reject-' + loop.index0 + '">' + action + '</a>'
|
|
74
|
+
}
|
|
75
|
+
]), suggestionTableRows) %}
|
|
76
|
+
{% endfor %}
|
|
77
|
+
{{ govukTable({
|
|
78
|
+
head: [
|
|
79
|
+
{
|
|
80
|
+
text: "Redaction suggestion"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
text: "Type of information"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
text: "Action"
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
rows: suggestionTableRows
|
|
90
|
+
}) }}
|
|
91
|
+
{% else %}
|
|
92
|
+
<p class="govuk-body">
|
|
93
|
+
No redaction suggestions found.
|
|
94
|
+
</p>
|
|
95
|
+
{% endif %}
|
|
96
|
+
{% endif %}
|
|
60
97
|
|
|
61
|
-
|
|
98
|
+
<h2 class="govuk-heading-m">Redaction preview</h2>
|
|
99
|
+
|
|
100
|
+
{{ govukDetails({
|
|
101
|
+
summaryText: question.summaryText or "Original",
|
|
62
102
|
html: question.value,
|
|
63
|
-
id: question.fieldName + "-unredacted"
|
|
64
|
-
classes: "govuk-!-margin-top-0"
|
|
103
|
+
id: question.fieldName + "-unredacted"
|
|
65
104
|
}) }}
|
|
66
105
|
|
|
67
106
|
{{ govukTextarea({
|
|
@@ -75,7 +114,11 @@
|
|
|
75
114
|
errorMessage: errors[question.fieldName] and {
|
|
76
115
|
text: errors[question.fieldName].msg
|
|
77
116
|
},
|
|
78
|
-
classes: question.inputClasses
|
|
117
|
+
classes: question.inputClasses,
|
|
118
|
+
attributes: {
|
|
119
|
+
readonly: true
|
|
120
|
+
},
|
|
121
|
+
rows: textAreaRows or 8
|
|
79
122
|
}) }}
|
|
80
123
|
|
|
81
124
|
<div class="govuk-button-group">
|
|
@@ -108,48 +151,6 @@
|
|
|
108
151
|
}) }}
|
|
109
152
|
</form>
|
|
110
153
|
</div>
|
|
111
|
-
{% if showSuggestionsUi %}
|
|
112
|
-
<div class="govuk-grid-column-one-half">
|
|
113
|
-
<h2 class="govuk-heading-m">Redaction suggestions</h2>
|
|
114
|
-
<p class="govuk-body">
|
|
115
|
-
This section includes system suggestions for redactions.
|
|
116
|
-
Each row shows the text that is suggested for redaction and the category detected.
|
|
117
|
-
They are all redacted by default, you can reject any that should not be redacted by clicking the "Reject" link next to each suggestion.
|
|
118
|
-
</p>
|
|
119
|
-
{% set suggestionTableRows = [] %}
|
|
120
|
-
{% for s in redactionSuggestions %}
|
|
121
|
-
{% set category = s.category %}
|
|
122
|
-
{% if s.subCategory %}
|
|
123
|
-
{% set category = category + " (" + s.subCategory + ")" %}
|
|
124
|
-
{% endif %}
|
|
125
|
-
{% set action = 'Accept' %}
|
|
126
|
-
{% if s.accepted %}
|
|
127
|
-
{% set action = 'Reject' %}
|
|
128
|
-
{% endif %}
|
|
129
|
-
{% set suggestionTableRows = (suggestionTableRows.push([
|
|
130
|
-
{text: s.text},
|
|
131
|
-
{text: category },
|
|
132
|
-
{
|
|
133
|
-
html: '<a href="#" class="govuk-link" data-redact="suggestion-reject-' + loop.index0 + '">' + action + '</a>'
|
|
134
|
-
}
|
|
135
|
-
]), suggestionTableRows) %}
|
|
136
|
-
{% endfor %}
|
|
137
|
-
{{ govukTable({
|
|
138
|
-
head: [
|
|
139
|
-
{
|
|
140
|
-
text: "Text"
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
text: "Category"
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
text: "Action"
|
|
147
|
-
}
|
|
148
|
-
],
|
|
149
|
-
rows: suggestionTableRows
|
|
150
|
-
}) }}
|
|
151
|
-
</div>
|
|
152
|
-
{% endif %}
|
|
153
154
|
|
|
154
155
|
<script {% if cspNonce %}nonce={{ cspNonce }}{% endif %}>
|
|
155
156
|
/**
|
|
@@ -27,8 +27,18 @@ export default class TextEntryRedactQuestion extends Question {
|
|
|
27
27
|
* @param {string|undefined} [params.label] if defined this show as a label for the input and the question will just be a standard h1
|
|
28
28
|
* @param {boolean} [params.onlyShowRedactedValueForSummary] whether to only show redacted value for summary
|
|
29
29
|
* @param {boolean} [params.useRedactedFieldNameForSave] whether to use the redacted field name when saving answers
|
|
30
|
+
* @param {boolean} [params.showSuggestionsUi] use the suggestions UI for this question
|
|
31
|
+
* @param {string} [params.summaryText] summaryText to use with the details component
|
|
30
32
|
*/
|
|
31
|
-
constructor({
|
|
33
|
+
constructor({
|
|
34
|
+
textEntryCheckbox,
|
|
35
|
+
label,
|
|
36
|
+
onlyShowRedactedValueForSummary,
|
|
37
|
+
useRedactedFieldNameForSave,
|
|
38
|
+
showSuggestionsUi,
|
|
39
|
+
summaryText,
|
|
40
|
+
...params
|
|
41
|
+
}) {
|
|
32
42
|
super({
|
|
33
43
|
...params,
|
|
34
44
|
viewFolder: 'text-entry-redact'
|
|
@@ -38,6 +48,8 @@ export default class TextEntryRedactQuestion extends Question {
|
|
|
38
48
|
this.label = label;
|
|
39
49
|
this.onlyShowRedactedValueForSummary = onlyShowRedactedValueForSummary;
|
|
40
50
|
this.useRedactedFieldNameForSave = useRedactedFieldNameForSave;
|
|
51
|
+
this.showSuggestionsUi = showSuggestionsUi;
|
|
52
|
+
this.summaryText = summaryText;
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
async getDataToSave(req, journeyResponse) {
|
|
@@ -58,6 +70,8 @@ export default class TextEntryRedactQuestion extends Question {
|
|
|
58
70
|
viewModel.question.value = payload ? payload[viewModel.question.fieldName] : viewModel.question.value;
|
|
59
71
|
viewModel.question.valueRedacted =
|
|
60
72
|
journey.response.answers[this.fieldName + 'Redacted'] || viewModel.question.value;
|
|
73
|
+
viewModel.question.summaryText = this.summaryText;
|
|
74
|
+
viewModel.showSuggestionsUi = this.showSuggestionsUi;
|
|
61
75
|
return viewModel;
|
|
62
76
|
}
|
|
63
77
|
|
|
@@ -12,7 +12,7 @@ describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
|
|
|
12
12
|
const HINT = 'hint';
|
|
13
13
|
const LABEL = 'A label';
|
|
14
14
|
|
|
15
|
-
function createQuestion() {
|
|
15
|
+
function createQuestion(showSuggestionsUi = false) {
|
|
16
16
|
return new TextEntryRedactQuestion({
|
|
17
17
|
title: TITLE,
|
|
18
18
|
question: QUESTION,
|
|
@@ -20,7 +20,8 @@ describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
|
|
|
20
20
|
validators: VALIDATORS,
|
|
21
21
|
html: HTML,
|
|
22
22
|
hint: HINT,
|
|
23
|
-
label: LABEL
|
|
23
|
+
label: LABEL,
|
|
24
|
+
showSuggestionsUi
|
|
24
25
|
});
|
|
25
26
|
}
|
|
26
27
|
it('should create', () => {
|
|
@@ -72,11 +73,10 @@ describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
|
|
|
72
73
|
const view = mockRes.render.mock.calls[0].result;
|
|
73
74
|
assert.ok(view);
|
|
74
75
|
assert.match(view, /Redaction Question/);
|
|
75
|
-
assert.match(view, /govuk-grid-column-two-thirds">\s+<form/);
|
|
76
76
|
assert.doesNotMatch(view, /Redaction suggestions/);
|
|
77
77
|
});
|
|
78
78
|
it('should render suggestions view', () => {
|
|
79
|
-
const question = createQuestion();
|
|
79
|
+
const question = createQuestion(true);
|
|
80
80
|
const section = {
|
|
81
81
|
name: 'section-name'
|
|
82
82
|
};
|
|
@@ -94,7 +94,6 @@ describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
|
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
96
|
const customViewData = {
|
|
97
|
-
showSuggestionsUi: true,
|
|
98
97
|
layoutTemplate: 'lib/test-layout.njk',
|
|
99
98
|
question: {
|
|
100
99
|
question: 'Redaction Question',
|
|
@@ -113,8 +112,51 @@ describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
|
|
|
113
112
|
const view = mockRes.render.mock.calls[0].result;
|
|
114
113
|
assert.ok(view);
|
|
115
114
|
assert.match(view, /Redaction Question/);
|
|
116
|
-
assert.match(view, /govuk-grid-column-one-half">\s+<form/);
|
|
117
|
-
assert.doesNotMatch(view, /govuk-grid-column-two-thirds">\s+<form/);
|
|
118
115
|
assert.match(view, /Redaction suggestions/);
|
|
116
|
+
assert.match(view, /No redaction suggestions found\./);
|
|
117
|
+
});
|
|
118
|
+
it('should render suggestions view with data', () => {
|
|
119
|
+
const question = createQuestion(true);
|
|
120
|
+
const section = {
|
|
121
|
+
name: 'section-name'
|
|
122
|
+
};
|
|
123
|
+
const journey = {
|
|
124
|
+
baseUrl: '',
|
|
125
|
+
taskListUrl: 'task',
|
|
126
|
+
journeyTemplate: 'template',
|
|
127
|
+
journeyTitle: 'title',
|
|
128
|
+
journeyId: 'manage-representations',
|
|
129
|
+
getBackLink: () => {
|
|
130
|
+
return 'back';
|
|
131
|
+
},
|
|
132
|
+
response: {
|
|
133
|
+
answers: {}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const customViewData = {
|
|
137
|
+
layoutTemplate: 'lib/test-layout.njk',
|
|
138
|
+
question: {
|
|
139
|
+
question: 'Redaction Question',
|
|
140
|
+
fieldName: 'field-name',
|
|
141
|
+
value: 'value',
|
|
142
|
+
valueRedacted: 'value-redacted'
|
|
143
|
+
},
|
|
144
|
+
redactionSuggestions: [
|
|
145
|
+
{ category: 'Person', suggestion: 'Test Person' },
|
|
146
|
+
{ category: 'Address', suggestion: '123 Fake Street' }
|
|
147
|
+
]
|
|
148
|
+
};
|
|
149
|
+
const nunjucks = configureNunjucks();
|
|
150
|
+
const mockRes = {
|
|
151
|
+
render: mock.fn((view, data) => nunjucks.render(view + '.njk', data))
|
|
152
|
+
};
|
|
153
|
+
const viewModel = question.prepQuestionForRendering(section, journey, customViewData);
|
|
154
|
+
question.renderAction(mockRes, viewModel);
|
|
155
|
+
assert.strictEqual(mockRes.render.mock.callCount(), 1);
|
|
156
|
+
const view = mockRes.render.mock.calls[0].result;
|
|
157
|
+
assert.ok(view);
|
|
158
|
+
assert.match(view, /Redaction Question/);
|
|
159
|
+
assert.match(view, /Redaction suggestions/);
|
|
160
|
+
assert.match(view, /We have suggested some common redactions/);
|
|
119
161
|
});
|
|
120
162
|
});
|
|
@@ -14,9 +14,19 @@ export default class DateTimeValidator extends DateValidator {
|
|
|
14
14
|
* creates an instance of a DateTimeValidator
|
|
15
15
|
* @param {string} timeInputLabel - string representing the time fields as displayed on the UI as part of an error message
|
|
16
16
|
* @param {string} [dateInputLabel] - string representing the date fields as displayed on the UI as part of an error message
|
|
17
|
+
* @param {DateValidationSettings} [dateValidationSettings] - object containing rules to apply
|
|
18
|
+
* @param {Object} [dateErrorMessages] - object containing custom date error messages to show on validation failure
|
|
17
19
|
*/
|
|
18
|
-
constructor(
|
|
19
|
-
|
|
20
|
+
constructor(
|
|
21
|
+
timeInputLabel,
|
|
22
|
+
dateInputLabel = timeInputLabel,
|
|
23
|
+
dateValidationSettings = {
|
|
24
|
+
ensureFuture: false,
|
|
25
|
+
ensurePast: false
|
|
26
|
+
},
|
|
27
|
+
dateErrorMessages
|
|
28
|
+
) {
|
|
29
|
+
super(dateInputLabel, dateValidationSettings, dateErrorMessages);
|
|
20
30
|
this.timeInputLabel = timeInputLabel;
|
|
21
31
|
}
|
|
22
32
|
|
|
@@ -253,10 +253,35 @@ describe('./lib/forms/custom-components/date-time/date-time-validator.js', () =>
|
|
|
253
253
|
assert.strictEqual(errors[`${question.fieldName}_day`].msg, 'Enter Site visit');
|
|
254
254
|
assert.strictEqual(errors[`${question.fieldName}_hour`].msg, 'Enter the site visit time');
|
|
255
255
|
});
|
|
256
|
+
it('dateErrorMessages overrides errors returned by date validator', async () => {
|
|
257
|
+
const req = {
|
|
258
|
+
body: {
|
|
259
|
+
['date-time-question_day']: undefined,
|
|
260
|
+
['date-time-question_month']: undefined,
|
|
261
|
+
['date-time-question_year']: undefined,
|
|
262
|
+
['date-time-question_hour']: undefined,
|
|
263
|
+
['date-time-question_minutes']: undefined,
|
|
264
|
+
['date-time-question_period']: ''
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const errors = await _validationMappedErrors(req, question, 'Site visit', 'site visit date', {
|
|
269
|
+
emptyErrorMessage: 'Site visit date error override'
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
assert.strictEqual(Object.keys(errors).length, 6);
|
|
273
|
+
assert.strictEqual(errors[`${question.fieldName}_day`].msg, 'Site visit date error override');
|
|
274
|
+
assert.strictEqual(errors[`${question.fieldName}_hour`].msg, 'Enter the site visit time');
|
|
275
|
+
});
|
|
256
276
|
});
|
|
257
277
|
|
|
258
|
-
const _validationMappedErrors = async (req, question, inputLabel, dateInputLabel) => {
|
|
259
|
-
const dateTimeValidator = new DateTimeValidator(
|
|
278
|
+
const _validationMappedErrors = async (req, question, inputLabel, dateInputLabel, dateErrorMessages) => {
|
|
279
|
+
const dateTimeValidator = new DateTimeValidator(
|
|
280
|
+
inputLabel,
|
|
281
|
+
dateInputLabel,
|
|
282
|
+
{ ensureFuture: false, ensurePast: false },
|
|
283
|
+
dateErrorMessages
|
|
284
|
+
);
|
|
260
285
|
|
|
261
286
|
const validationRules = dateTimeValidator.validate(question);
|
|
262
287
|
|