@planning-inspectorate/dynamic-forms 2.4.0 → 3.1.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/README.md +12 -1
- package/package.json +3 -2
- package/src/components/address/question.js +25 -19
- package/src/components/address/question.test.js +1 -1
- package/src/components/date/question.js +7 -21
- package/src/components/date/question.test.js +1 -1
- package/src/components/date-period/question.js +17 -25
- package/src/components/date-period/question.test.js +1 -1
- package/src/components/date-time/question.js +10 -23
- package/src/components/identifier/question.js +3 -6
- package/src/components/manage-list/README.md +97 -0
- package/src/components/manage-list/answer-summary-list.njk +10 -0
- package/src/components/manage-list/index.njk +61 -0
- package/src/components/manage-list/manage-list-section.js +39 -0
- package/src/components/manage-list/manage-list-section.test.js +52 -0
- package/src/components/manage-list/question.js +147 -0
- package/src/components/manage-list/question.test.js +139 -0
- package/src/components/multi-field-input/index.njk +1 -1
- package/src/components/multi-field-input/question.js +12 -13
- package/src/components/multi-field-input/question.test.js +8 -8
- package/src/components/number-entry/question.js +7 -13
- package/src/components/radio/question.js +2 -4
- package/src/components/select/question.js +2 -4
- package/src/components/single-line-input/question.js +5 -6
- package/src/components/text-entry/question.js +5 -5
- package/src/components/text-entry-redact/question.js +18 -9
- package/src/components/text-entry-redact/question.test.js +12 -19
- package/src/components/unit-option-entry/question.js +9 -8
- package/src/components/utils/nunjucks.js +20 -0
- package/src/controller.js +39 -10
- package/src/controller.test.js +90 -24
- package/src/index.js +3 -2
- package/src/journey/journey-response.js +2 -2
- package/src/journey/journey-types.d.ts +21 -1
- package/src/journey/journey.js +64 -18
- package/src/journey/journey.test.js +80 -15
- package/src/questions/options-question.js +6 -7
- package/src/questions/question-props.d.ts +2 -1
- package/src/questions/question-types.d.ts +26 -0
- package/src/questions/question.js +119 -15
- package/src/questions/question.test.js +98 -5
- package/src/questions/questions.js +3 -1
- package/src/section-types.d.ts +18 -0
- package/src/section.js +40 -11
- package/src/section.test.js +89 -0
- package/test/journey.js +5 -0
- package/test/mock/journey.js +17 -0
- package/test/questions.js +30 -1
- package/test/questions.test.js +104 -40
- package/test/snapshots/check-your-answers-not-started.html +11 -0
- package/test/snapshots/check-your-answers-with-answers.html +11 -0
- package/test/snapshots/manage-list-answer-summary.html +14 -0
- package/test/snapshots/manage-list-render-questions.html +97 -0
- package/test/snapshots/manage-list-render.html +91 -0
- package/test/snapshots/travel-companions.html +19 -0
- package/test/snapshots/travel-companions_travel-companion-email.html +21 -0
- package/test/snapshots/travel-companions_travel-companion-name.html +21 -0
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Question } from '#question';
|
|
2
|
+
import { Uuid } from '#src/lib/uuid.js';
|
|
3
|
+
import { nunjucksEnv } from '#src/components/utils/nunjucks.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} ManageListQuestionParameters
|
|
7
|
+
* @property {string} titleSingular - the single name of the list item, e.g. "Holiday activity"
|
|
8
|
+
* @property {boolean} [showManageListQuestions] - whether to show the question titles as well as answers on the manage list summary page
|
|
9
|
+
* @property {boolean} [showAnswersInSummary] - whether to show the answers on the main check-your-answers page (or just a count)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export default class ManageListQuestion extends Question {
|
|
13
|
+
/** @type {import('../../section.js').Section} */
|
|
14
|
+
#section;
|
|
15
|
+
/** @type {boolean} */
|
|
16
|
+
#showAnswersInSummary;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {import('#question-types').QuestionParameters & ManageListQuestionParameters} params
|
|
20
|
+
*/
|
|
21
|
+
constructor(params) {
|
|
22
|
+
super({
|
|
23
|
+
...params,
|
|
24
|
+
pageTitle: params.title,
|
|
25
|
+
viewFolder: 'manage-list',
|
|
26
|
+
viewData: {
|
|
27
|
+
...params.viewData,
|
|
28
|
+
titleSingular: params.titleSingular,
|
|
29
|
+
showManageListQuestions: params.showManageListQuestions
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
this.#showAnswersInSummary = params.showAnswersInSummary || false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Is this question a manage list question?
|
|
37
|
+
*
|
|
38
|
+
* Used by controller and other logic.
|
|
39
|
+
* Added as a getter so custom components can also be a manage list question.
|
|
40
|
+
*
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
get isManageListQuestion() {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {import('#question').QuestionViewModel} viewModel
|
|
49
|
+
*/
|
|
50
|
+
addCustomDataToViewModel(viewModel) {
|
|
51
|
+
viewModel.question.addAnotherLink = this.#addAnotherLink;
|
|
52
|
+
viewModel.question.firstQuestionUrl = this.#firstQuestionUrl;
|
|
53
|
+
viewModel.question.valueSummary = [];
|
|
54
|
+
if (viewModel.question.value && Array.isArray(viewModel.question.value)) {
|
|
55
|
+
viewModel.question.valueSummary = viewModel.question.value.map((v) => {
|
|
56
|
+
return {
|
|
57
|
+
id: v.id,
|
|
58
|
+
value: this.#formatItemAnswers(v)
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Format the answers to each of the manage list questions
|
|
66
|
+
* @param {{id: string, [k: string]: string}} answer
|
|
67
|
+
* @returns {{question: string, answer: string}[]}
|
|
68
|
+
*/
|
|
69
|
+
#formatItemAnswers(answer) {
|
|
70
|
+
if (this.section.questions.length === 0) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const mockJourney = {
|
|
74
|
+
getCurrentQuestionUrl() {}
|
|
75
|
+
};
|
|
76
|
+
return this.section.questions.map((q) => {
|
|
77
|
+
const formatted = q
|
|
78
|
+
.formatAnswerForSummary('', mockJourney, answer[q.fieldName])
|
|
79
|
+
.map((a) => a.value)
|
|
80
|
+
.join(', ');
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
question: q.title,
|
|
84
|
+
answer: formatted
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
formatAnswerForSummary(sectionSegment, journey, answer) {
|
|
90
|
+
let formattedAnswer = this.notStartedText;
|
|
91
|
+
if (answer && Array.isArray(answer)) {
|
|
92
|
+
if (this.#showAnswersInSummary) {
|
|
93
|
+
const answers = answer.map((a) => this.#formatItemAnswers(a));
|
|
94
|
+
formattedAnswer = nunjucksEnv().render('components/manage-list/answer-summary-list.njk', { answers });
|
|
95
|
+
} else if (answer.length > 0) {
|
|
96
|
+
formattedAnswer = `${answer.length} ${this.title}`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const action = this.getAction(sectionSegment, journey, answer);
|
|
100
|
+
const key = this.title ?? this.question;
|
|
101
|
+
return [
|
|
102
|
+
{
|
|
103
|
+
key: key,
|
|
104
|
+
value: formattedAnswer,
|
|
105
|
+
action: action
|
|
106
|
+
}
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create an 'add another' link,
|
|
112
|
+
* to the first question in the manage list section
|
|
113
|
+
*
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
get #addAnotherLink() {
|
|
117
|
+
if (this.section.questions.length === 0) {
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
const nextItemId = Uuid.randomUUID();
|
|
121
|
+
const firstQuestion = this.#firstQuestionUrl;
|
|
122
|
+
return `add/${nextItemId}/${firstQuestion}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* First question URL
|
|
127
|
+
*
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
get #firstQuestionUrl() {
|
|
131
|
+
if (this.section.questions.length === 0) {
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
return this.section.questions[0].url;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get section() {
|
|
138
|
+
if (!this.#section) {
|
|
139
|
+
throw new Error('manage list section not set');
|
|
140
|
+
}
|
|
141
|
+
return this.#section;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
set section(section) {
|
|
145
|
+
this.#section = section;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, mock } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import ManageListQuestion from '#src/components/manage-list/question.js';
|
|
4
|
+
import { ManageListSection } from '#src/components/manage-list/manage-list-section.js';
|
|
5
|
+
import { mockJourney } from '#test/mock/journey.js';
|
|
6
|
+
import { mockRandomUUID } from '#src/lib/uuid.test.js';
|
|
7
|
+
import { configureNunjucksTestEnv } from '#test/utils/nunjucks.js';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { snapshotsDir } from '#test/utils/utils.js';
|
|
10
|
+
|
|
11
|
+
describe('components/manage-list/question', () => {
|
|
12
|
+
const TITLE = 'Things';
|
|
13
|
+
const QUESTION = 'Manage list';
|
|
14
|
+
const DESCRIPTION = 'A list of things';
|
|
15
|
+
const FIELDNAME = 'manageListTest';
|
|
16
|
+
const newQuestion = (options = {}) => {
|
|
17
|
+
return new ManageListQuestion({
|
|
18
|
+
title: TITLE,
|
|
19
|
+
question: QUESTION,
|
|
20
|
+
description: DESCRIPTION,
|
|
21
|
+
fieldName: FIELDNAME,
|
|
22
|
+
titleSingular: 'Thing',
|
|
23
|
+
...options
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
it('should create', () => {
|
|
27
|
+
const q = newQuestion();
|
|
28
|
+
assert.strictEqual(q.title, TITLE);
|
|
29
|
+
assert.strictEqual(q.question, QUESTION);
|
|
30
|
+
assert.strictEqual(q.description, DESCRIPTION);
|
|
31
|
+
assert.strictEqual(q.fieldName, FIELDNAME);
|
|
32
|
+
assert.strictEqual(q.viewFolder, 'manage-list');
|
|
33
|
+
assert.strictEqual(q.isManageListQuestion, true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should populate addAnotherLink & firstQuestionUrl', (ctx) => {
|
|
37
|
+
const q = newQuestion();
|
|
38
|
+
mockRandomUUID(ctx);
|
|
39
|
+
q.section = new ManageListSection().addQuestion({ url: 'first-question' });
|
|
40
|
+
const viewModel = q.prepQuestionForRendering({}, mockJourney());
|
|
41
|
+
// add/<v4-guid>/<question-url>
|
|
42
|
+
assert.match(viewModel?.question?.addAnotherLink, /^add\/00000000-0000-0000-0000-000000000000\/first-question/);
|
|
43
|
+
assert.strictEqual(viewModel?.question?.firstQuestionUrl, 'first-question');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const questionWithManageQuestions = (ctx, options) => {
|
|
47
|
+
const q = newQuestion(options);
|
|
48
|
+
const innerQ1 = {
|
|
49
|
+
url: 'first-question',
|
|
50
|
+
title: 'Q 1',
|
|
51
|
+
formatAnswerForSummary() {
|
|
52
|
+
return [{ value: 'mock answer' }];
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const innerQ2 = {
|
|
56
|
+
url: 'second-question',
|
|
57
|
+
title: 'Q 2',
|
|
58
|
+
formatAnswerForSummary() {
|
|
59
|
+
return [{ value: 'mock answer 2' }];
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
mockRandomUUID(ctx);
|
|
63
|
+
const journey = mockJourney();
|
|
64
|
+
journey.response.answers = {
|
|
65
|
+
[FIELDNAME]: [{ id: 'id-1' }, { id: 'id-2' }, { id: 'id-3' }]
|
|
66
|
+
};
|
|
67
|
+
q.section = new ManageListSection().addQuestion(innerQ1).addQuestion(innerQ2);
|
|
68
|
+
return { q, journey };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
it('should format answers to questions in the section', (ctx) => {
|
|
72
|
+
const { q, journey } = questionWithManageQuestions(ctx);
|
|
73
|
+
const viewModel = q.prepQuestionForRendering({}, journey);
|
|
74
|
+
assert.ok(Array.isArray(viewModel?.question?.valueSummary));
|
|
75
|
+
assert.strictEqual(viewModel?.question?.valueSummary.length, 3);
|
|
76
|
+
assert.deepStrictEqual(viewModel?.question?.valueSummary[0], {
|
|
77
|
+
id: 'id-1',
|
|
78
|
+
value: [
|
|
79
|
+
{ question: 'Q 1', answer: 'mock answer' },
|
|
80
|
+
{ question: 'Q 2', answer: 'mock answer 2' }
|
|
81
|
+
]
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should format list answer for summary', (ctx) => {
|
|
86
|
+
const q = newQuestion();
|
|
87
|
+
mockRandomUUID(ctx);
|
|
88
|
+
const answerForSummary = q.formatAnswerForSummary('section-1', mockJourney(), [{}, {}, {}]);
|
|
89
|
+
assert.strictEqual(answerForSummary.length, 1);
|
|
90
|
+
assert.strictEqual(answerForSummary[0].value, '3 Things');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should support showing all answers in the format list answer for summary', (ctx) => {
|
|
94
|
+
const { q } = questionWithManageQuestions(ctx, { showAnswersInSummary: true });
|
|
95
|
+
const answerForSummary = q.formatAnswerForSummary('section-1', mockJourney(), [{}, {}, {}]);
|
|
96
|
+
assert.strictEqual(answerForSummary.length, 1);
|
|
97
|
+
ctx.assert.fileSnapshot(
|
|
98
|
+
answerForSummary[0].value.replaceAll('\r\n', '\n'),
|
|
99
|
+
path.join(snapshotsDir(), 'manage-list-answer-summary.html'),
|
|
100
|
+
{
|
|
101
|
+
serializers: [(v) => v]
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should render with answers', (ctx) => {
|
|
107
|
+
const { q, journey } = questionWithManageQuestions(ctx);
|
|
108
|
+
const viewModel = q.prepQuestionForRendering({}, journey);
|
|
109
|
+
const nunjucks = configureNunjucksTestEnv();
|
|
110
|
+
const res = {
|
|
111
|
+
render: mock.fn((view, data) => nunjucks.render(view + '.njk', data))
|
|
112
|
+
};
|
|
113
|
+
q.renderAction(res, viewModel);
|
|
114
|
+
assert.strictEqual(res.render.mock.callCount(), 1);
|
|
115
|
+
ctx.assert.fileSnapshot(res.render.mock.calls[0].result, path.join(snapshotsDir(), 'manage-list-render.html'), {
|
|
116
|
+
serializers: [(v) => v]
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should render with answers & questions', (ctx) => {
|
|
121
|
+
const { q, journey } = questionWithManageQuestions(ctx, {
|
|
122
|
+
showManageListQuestions: true
|
|
123
|
+
});
|
|
124
|
+
const viewModel = q.prepQuestionForRendering({}, journey);
|
|
125
|
+
const nunjucks = configureNunjucksTestEnv();
|
|
126
|
+
const res = {
|
|
127
|
+
render: mock.fn((view, data) => nunjucks.render(view + '.njk', data))
|
|
128
|
+
};
|
|
129
|
+
q.renderAction(res, viewModel);
|
|
130
|
+
assert.strictEqual(res.render.mock.callCount(), 1);
|
|
131
|
+
ctx.assert.fileSnapshot(
|
|
132
|
+
res.render.mock.calls[0].result,
|
|
133
|
+
path.join(snapshotsDir(), 'manage-list-render-questions.html'),
|
|
134
|
+
{
|
|
135
|
+
serializers: [(v) => v]
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
{% from "govuk/components/fieldset/macro.njk" import govukFieldset %}
|
|
6
6
|
{% from "govuk/components/button/macro.njk" import govukButton %}
|
|
7
7
|
|
|
8
|
-
{% set inputFields = question.
|
|
8
|
+
{% set inputFields = question.value %}
|
|
9
9
|
|
|
10
10
|
{% block dynQuestionContent %}
|
|
11
11
|
<div class="govuk-grid-row">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Question } from '
|
|
1
|
+
import { Question } from '#question';
|
|
2
2
|
import escape from 'escape-html';
|
|
3
3
|
import { nl2br } from '../../lib/utils.js';
|
|
4
4
|
|
|
@@ -56,22 +56,21 @@ export default class MultiFieldInputQuestion extends Question {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
: {
|
|
66
|
-
...inputField,
|
|
67
|
-
value: this.#formatValue(journey.response.answers[inputField.fieldName], inputField.formatTextFunction)
|
|
68
|
-
};
|
|
59
|
+
answerForViewModel(answers) {
|
|
60
|
+
return this.inputFields.map((inputField) => {
|
|
61
|
+
return {
|
|
62
|
+
...inputField,
|
|
63
|
+
value: this.#formatValue(answers[inputField.fieldName], inputField.formatTextFunction)
|
|
64
|
+
};
|
|
69
65
|
});
|
|
66
|
+
}
|
|
70
67
|
|
|
71
|
-
|
|
68
|
+
/**
|
|
69
|
+
* @param {import('#question').QuestionViewModel} viewModel
|
|
70
|
+
*/
|
|
71
|
+
addCustomDataToViewModel(viewModel) {
|
|
72
72
|
viewModel.question.label = this.label;
|
|
73
73
|
viewModel.question.attributes = this.inputAttributes;
|
|
74
|
-
return viewModel;
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
/**
|
|
@@ -73,8 +73,8 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
|
|
|
73
73
|
const result = question.prepQuestionForRendering({}, journey, customViewData);
|
|
74
74
|
|
|
75
75
|
assert.strictEqual(result.question?.fieldName, FIELDNAME);
|
|
76
|
-
assert.strictEqual(result.question?.
|
|
77
|
-
assert.strictEqual(result.question?.
|
|
76
|
+
assert.strictEqual(result.question?.value[0].fieldName, INPUTFIELDS[0].fieldName);
|
|
77
|
+
assert.strictEqual(result.question?.value[1].fieldName, INPUTFIELDS[1].fieldName);
|
|
78
78
|
assert.strictEqual(result.hello, 'hi');
|
|
79
79
|
});
|
|
80
80
|
it('should set inputFields with values from payload if provided', () => {
|
|
@@ -94,8 +94,8 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
|
|
|
94
94
|
|
|
95
95
|
const result = question.prepQuestionForRendering({}, journey, {}, payload);
|
|
96
96
|
|
|
97
|
-
assert.strictEqual(result.question?.
|
|
98
|
-
assert.strictEqual(result.question?.
|
|
97
|
+
assert.strictEqual(result.question?.value[0].value, 'payloadValue1');
|
|
98
|
+
assert.strictEqual(result.question?.value[1].value, 'payloadValue2');
|
|
99
99
|
});
|
|
100
100
|
it('should set inputFields with values from journey response if payload is not provided', () => {
|
|
101
101
|
const question = createMultiFieldInputQuestion();
|
|
@@ -113,8 +113,8 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
|
|
|
113
113
|
|
|
114
114
|
const result = question.prepQuestionForRendering({}, journey, {});
|
|
115
115
|
|
|
116
|
-
assert.strictEqual(result.question?.
|
|
117
|
-
assert.strictEqual(result.question?.
|
|
116
|
+
assert.strictEqual(result.question?.value[0].value, 'responseValue1');
|
|
117
|
+
assert.strictEqual(result.question?.value[1].value, 'responseValue2');
|
|
118
118
|
});
|
|
119
119
|
it('should set inputFields with formatted values from journey response if formatTextFunction is provided', () => {
|
|
120
120
|
const formatTextFunction = (value) => `Formatted: ${value}`;
|
|
@@ -136,8 +136,8 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
|
|
|
136
136
|
|
|
137
137
|
const result = question.prepQuestionForRendering({}, journey, {});
|
|
138
138
|
|
|
139
|
-
assert.strictEqual(result.question?.
|
|
140
|
-
assert.strictEqual(result.question?.
|
|
139
|
+
assert.strictEqual(result.question?.value[0].value, 'Formatted: responseValue1');
|
|
140
|
+
assert.strictEqual(result.question?.value[1].value, 'Formatted: responseValue2');
|
|
141
141
|
});
|
|
142
142
|
});
|
|
143
143
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Question } from '
|
|
1
|
+
import { Question } from '#question';
|
|
2
2
|
import { getPersistedNumberAnswer } from '../utils/persisted-number-answer.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -21,22 +21,16 @@ export default class NumberEntryQuestion extends Question {
|
|
|
21
21
|
this.label = label;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
answerForViewModel(answers) {
|
|
25
|
+
return getPersistedNumberAnswer(answers[this.fieldName] || '');
|
|
26
|
+
}
|
|
27
|
+
|
|
24
28
|
/**
|
|
25
|
-
*
|
|
29
|
+
* @param {import('#question').QuestionViewModel} viewModel
|
|
26
30
|
*/
|
|
27
|
-
|
|
28
|
-
let viewModel = super.prepQuestionForRendering(section, journey, customViewData, payload);
|
|
29
|
-
|
|
30
|
-
const answer = journey.response.answers[this.fieldName];
|
|
31
|
-
const persistedAnswer = getPersistedNumberAnswer(answer);
|
|
32
|
-
|
|
33
|
-
viewModel.question.value = persistedAnswer;
|
|
34
|
-
viewModel.answer = persistedAnswer;
|
|
35
|
-
|
|
31
|
+
addCustomDataToViewModel(viewModel) {
|
|
36
32
|
viewModel.question.label = this.label;
|
|
37
33
|
viewModel.question.suffix = this.suffix;
|
|
38
|
-
|
|
39
|
-
return viewModel;
|
|
40
34
|
}
|
|
41
35
|
|
|
42
36
|
/**
|
|
@@ -60,13 +60,11 @@ export default class RadioQuestion extends OptionsQuestion {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
*
|
|
63
|
+
* @param {import('#question').QuestionViewModel} viewModel
|
|
64
64
|
*/
|
|
65
|
-
|
|
66
|
-
let viewModel = super.prepQuestionForRendering(section, journey, customViewData, payload);
|
|
65
|
+
addCustomDataToViewModel(viewModel) {
|
|
67
66
|
viewModel.question.label = this.label;
|
|
68
67
|
viewModel.question.legend = this.legend;
|
|
69
|
-
return viewModel;
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
/**
|
|
@@ -54,13 +54,11 @@ export default class SelectQuestion extends OptionsQuestion {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
57
|
+
* @param {import('#question').QuestionViewModel} viewModel
|
|
58
58
|
*/
|
|
59
|
-
|
|
60
|
-
let viewModel = super.prepQuestionForRendering(section, journey, customViewData, payload);
|
|
59
|
+
addCustomDataToViewModel(viewModel) {
|
|
61
60
|
viewModel.question.label = this.label;
|
|
62
61
|
viewModel.question.legend = this.legend;
|
|
63
|
-
return viewModel;
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Question } from '
|
|
1
|
+
import { Question } from '#question';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @typedef {import('../../questions/question.js').QuestionViewModel} QuestionViewModel
|
|
@@ -32,17 +32,16 @@ export default class SingleLineInputQuestion extends Question {
|
|
|
32
32
|
this.classes = params.classes || '';
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* @param {import('#question').QuestionViewModel} viewModel
|
|
37
|
+
*/
|
|
38
|
+
addCustomDataToViewModel(viewModel) {
|
|
37
39
|
viewModel.question.label = this.label;
|
|
38
|
-
viewModel.question.value = payload ? payload[viewModel.question.fieldName] : viewModel.question.value;
|
|
39
|
-
|
|
40
40
|
// Extract type from attributes to pass separately to avoid duplication
|
|
41
41
|
const { type, ...otherAttributes } = this.inputAttributes;
|
|
42
42
|
viewModel.question.attributes = otherAttributes;
|
|
43
43
|
viewModel.question.type = type;
|
|
44
44
|
|
|
45
45
|
viewModel.question.classes = this.classes;
|
|
46
|
-
return viewModel;
|
|
47
46
|
}
|
|
48
47
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Question } from '
|
|
1
|
+
import { Question } from '#question';
|
|
2
2
|
/**
|
|
3
3
|
* @typedef {import('../../questions/question.js').QuestionViewModel} QuestionViewModel
|
|
4
4
|
* @typedef {import('../../journey/journey.js').Journey} Journey
|
|
@@ -33,11 +33,11 @@ export default class TextEntryQuestion extends Question {
|
|
|
33
33
|
this.label = label;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
/**
|
|
37
|
+
* @param {import('#question').QuestionViewModel} viewModel
|
|
38
|
+
*/
|
|
39
|
+
addCustomDataToViewModel(viewModel) {
|
|
38
40
|
viewModel.question.label = this.label;
|
|
39
41
|
viewModel.question.textEntryCheckbox = this.textEntryCheckbox;
|
|
40
|
-
viewModel.question.value = payload ? payload[viewModel.question.fieldName] : viewModel.question.value;
|
|
41
|
-
return viewModel;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Question } from '
|
|
1
|
+
import { Question } from '#question';
|
|
2
2
|
import { nl2br } from '../../lib/utils.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -68,18 +68,27 @@ export default class TextEntryRedactQuestion extends Question {
|
|
|
68
68
|
return super.getDataToSave(req, journeyResponse);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
prepQuestionForRendering(section, journey, customViewData, payload) {
|
|
72
|
-
|
|
71
|
+
prepQuestionForRendering(section, journey, customViewData, payload, options) {
|
|
72
|
+
const viewModel = super.prepQuestionForRendering(section, journey, customViewData, payload, options);
|
|
73
|
+
const answers = this.answerObjectFromJourneyResponse(journey.response, options);
|
|
74
|
+
const answer = viewModel.question.value;
|
|
75
|
+
viewModel.question.valueRedacted = answers[this.fieldName + 'Redacted'] || answer;
|
|
76
|
+
viewModel.question.valueOriginal = answers[this.fieldName + 'Original'] || answer;
|
|
77
|
+
return viewModel;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
answerForViewModel(answers) {
|
|
81
|
+
return nl2br(answers[this.fieldName]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {import('#question').QuestionViewModel} viewModel
|
|
86
|
+
*/
|
|
87
|
+
addCustomDataToViewModel(viewModel) {
|
|
73
88
|
viewModel.question.label = this.label;
|
|
74
89
|
viewModel.question.textEntryCheckbox = this.textEntryCheckbox;
|
|
75
|
-
viewModel.question.value = nl2br(payload ? payload[viewModel.question.fieldName] : viewModel.question.value);
|
|
76
|
-
viewModel.question.valueRedacted =
|
|
77
|
-
journey.response.answers[this.fieldName + 'Redacted'] || viewModel.question.value;
|
|
78
|
-
viewModel.question.valueOriginal =
|
|
79
|
-
journey.response.answers[this.fieldName + 'Original'] || viewModel.question.value;
|
|
80
90
|
viewModel.question.summaryText = this.summaryText;
|
|
81
91
|
viewModel.showSuggestionsUi = this.showSuggestionsUi;
|
|
82
|
-
return viewModel;
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
formatAnswerForSummary(sectionSegment, journey, answer, capitals = true) {
|
|
@@ -5,7 +5,7 @@ import { configureNunjucksTestEnv } from '../../../test/utils/nunjucks.js';
|
|
|
5
5
|
|
|
6
6
|
describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
|
|
7
7
|
const TITLE = 'title';
|
|
8
|
-
const QUESTION = 'Question
|
|
8
|
+
const QUESTION = 'Redaction Question';
|
|
9
9
|
const FIELDNAME = 'field-name';
|
|
10
10
|
const VALIDATORS = [1, 2];
|
|
11
11
|
const HTML = '/path/to/html.njk';
|
|
@@ -130,18 +130,15 @@ describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
|
|
|
130
130
|
return 'back';
|
|
131
131
|
},
|
|
132
132
|
response: {
|
|
133
|
-
answers: {
|
|
133
|
+
answers: {
|
|
134
|
+
'field-name': 'value',
|
|
135
|
+
'field-nameRedacted': 'value-redacted',
|
|
136
|
+
'field-nameOriginal': 'value'
|
|
137
|
+
}
|
|
134
138
|
}
|
|
135
139
|
};
|
|
136
140
|
const customViewData = {
|
|
137
141
|
layoutTemplate: 'views/layout-journey.njk',
|
|
138
|
-
question: {
|
|
139
|
-
question: 'Redaction Question',
|
|
140
|
-
fieldName: 'field-name',
|
|
141
|
-
value: 'value',
|
|
142
|
-
valueRedacted: 'value-redacted',
|
|
143
|
-
valueOriginal: 'value'
|
|
144
|
-
},
|
|
145
142
|
redactionSuggestions: [
|
|
146
143
|
{ category: 'Person', suggestion: 'Test Person' },
|
|
147
144
|
{ category: 'Address', suggestion: '123 Fake Street' }
|
|
@@ -177,17 +174,13 @@ describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
|
|
|
177
174
|
return 'back';
|
|
178
175
|
},
|
|
179
176
|
response: {
|
|
180
|
-
answers: {
|
|
177
|
+
answers: {
|
|
178
|
+
'field-name': 'This is my comment.\nIt has multiple lines.\r\nHere is another line.'
|
|
179
|
+
}
|
|
181
180
|
}
|
|
182
181
|
};
|
|
183
182
|
const customViewData = {
|
|
184
183
|
layoutTemplate: 'views/layout-journey.njk',
|
|
185
|
-
question: {
|
|
186
|
-
question: 'Redaction Question',
|
|
187
|
-
fieldName: 'field-name',
|
|
188
|
-
value: 'This is my comment.\nIt has multiple lines.\r\nHere is another line.',
|
|
189
|
-
valueRedacted: 'value-redacted'
|
|
190
|
-
},
|
|
191
184
|
redactionSuggestions: [
|
|
192
185
|
{ category: 'Person', suggestion: 'Test Person' },
|
|
193
186
|
{ category: 'Address', suggestion: '123 Fake Street' }
|
|
@@ -235,7 +228,7 @@ describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
|
|
|
235
228
|
action: {
|
|
236
229
|
href: '/redacted-comment',
|
|
237
230
|
text: 'Change',
|
|
238
|
-
visuallyHiddenText: 'Question
|
|
231
|
+
visuallyHiddenText: 'Redaction Question'
|
|
239
232
|
}
|
|
240
233
|
}
|
|
241
234
|
]);
|
|
@@ -271,7 +264,7 @@ describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
|
|
|
271
264
|
action: {
|
|
272
265
|
href: '/redacted-comment',
|
|
273
266
|
text: 'Change',
|
|
274
|
-
visuallyHiddenText: 'Question
|
|
267
|
+
visuallyHiddenText: 'Redaction Question'
|
|
275
268
|
}
|
|
276
269
|
}
|
|
277
270
|
]);
|
|
@@ -308,7 +301,7 @@ describe('./src/dynamic-forms/components/text-entry-redact/question.js', () => {
|
|
|
308
301
|
action: {
|
|
309
302
|
href: '/redacted-comment',
|
|
310
303
|
text: 'Change',
|
|
311
|
-
visuallyHiddenText: 'Question
|
|
304
|
+
visuallyHiddenText: 'Redaction Question'
|
|
312
305
|
}
|
|
313
306
|
}
|
|
314
307
|
]);
|
|
@@ -71,19 +71,20 @@ export default class UnitOptionEntryQuestion extends Question {
|
|
|
71
71
|
* @param {Journey} journey - the journey we are in
|
|
72
72
|
* @param {Record<string, unknown>} [customViewData] additional data to send to view
|
|
73
73
|
* @param {Record<string, unknown>} [payload]
|
|
74
|
+
* @param {import('#src/questions/question-types.d.ts').PrepQuestionForRenderingOptions} options
|
|
74
75
|
* @returns {QuestionViewModel & {
|
|
75
76
|
* question: QuestionViewModel['question'] & {
|
|
76
77
|
* options:UnitOption[]
|
|
77
78
|
* }
|
|
78
79
|
* }}
|
|
79
80
|
*/
|
|
80
|
-
prepQuestionForRendering(section, journey, customViewData, payload) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const
|
|
81
|
+
prepQuestionForRendering(section, journey, customViewData, payload, options) {
|
|
82
|
+
const viewModel = super.prepQuestionForRendering(section, journey, customViewData, payload, options);
|
|
83
|
+
const answer = viewModel.question.value;
|
|
84
|
+
const answers = this.answerObjectFromJourneyResponse(journey.response, options);
|
|
84
85
|
|
|
85
86
|
/** @type {Array<UnitOption>} */
|
|
86
|
-
|
|
87
|
+
viewModel.question.options = [];
|
|
87
88
|
|
|
88
89
|
for (const option of this.options) {
|
|
89
90
|
let optionData = { ...option };
|
|
@@ -101,7 +102,7 @@ export default class UnitOptionEntryQuestion extends Question {
|
|
|
101
102
|
if (conditionalIsJustHTML(conditionalField)) continue;
|
|
102
103
|
|
|
103
104
|
const conversionFactor = conditionalField.conversionFactor || 1;
|
|
104
|
-
const unconvertedAnswer =
|
|
105
|
+
const unconvertedAnswer = answers[this.conditionalFieldName];
|
|
105
106
|
|
|
106
107
|
const existingValue =
|
|
107
108
|
answer === optionData.value && typeof unconvertedAnswer === 'number'
|
|
@@ -119,10 +120,10 @@ export default class UnitOptionEntryQuestion extends Question {
|
|
|
119
120
|
};
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
options.push(optionData);
|
|
123
|
+
viewModel.question.options.push(optionData);
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
return
|
|
126
|
+
return viewModel;
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
/**
|