@planning-inspectorate/dynamic-forms 3.0.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/index.njk +1 -1
- package/src/components/manage-list/question.js +6 -6
- 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/journey/journey-response.js +2 -2
- package/src/journey/journey-types.d.ts +10 -0
- package/src/questions/options-question.js +6 -7
- package/src/questions/question-types.d.ts +26 -0
- package/src/questions/question.js +65 -9
- package/src/questions/question.test.js +72 -2
- package/tsconfig.json +2 -1
package/README.md
CHANGED
|
@@ -296,4 +296,15 @@ There are some lightweight tests in the `test` directory which sets up a basic j
|
|
|
296
296
|
|
|
297
297
|
When adding a new question type, be sure to add an example question into `test/questions.js`, and mock answers into `test/questions.test.js#mockAnswerBody` and `test/questions.test.js#mockAnswer`. Also, the question should be added to the journey in `test/journey.js`.
|
|
298
298
|
|
|
299
|
-
To update any snapshots with rendering changes (or new questions), run `node --test --test-update-snapshots`.
|
|
299
|
+
To update any snapshots with rendering changes (or new questions), run `node --test --test-update-snapshots`.
|
|
300
|
+
|
|
301
|
+
### Components
|
|
302
|
+
|
|
303
|
+
When implement a new component, extend the base `Question` class. Common methods to override are:
|
|
304
|
+
|
|
305
|
+
* `formatAnswerForSummary` - used for check-your-answers display
|
|
306
|
+
* `getDataToSave` - answer data to save
|
|
307
|
+
* `addCustomDataToViewModel` - customise the view model with extra data/configuration
|
|
308
|
+
* `answerForViewModel` - customise the view model answer value
|
|
309
|
+
|
|
310
|
+
Where possible `addCustomDataToViewModel` and `answerForViewModel` should be overridden instead of `prepQuestionForRendering`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planning-inspectorate/dynamic-forms",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "This package is for building GDS forms using a configuration-based approach.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"imports": {
|
|
28
28
|
"#question-types": "./src/questions/question-types.d.ts",
|
|
29
29
|
"#src/*": "./src/*",
|
|
30
|
-
"#test/*": "./test/*"
|
|
30
|
+
"#test/*": "./test/*",
|
|
31
|
+
"#question": "./src/questions/question.js"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@commitlint/cli": "^20.0.0",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Question } from '
|
|
1
|
+
import { Question } from '#question';
|
|
2
2
|
|
|
3
3
|
import escape from 'escape-html';
|
|
4
4
|
import { Address } from '../../lib/address.js';
|
|
@@ -40,29 +40,35 @@ export default class AddressQuestion extends Question {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* @param {
|
|
44
|
-
* @
|
|
45
|
-
* @param {Record<string, unknown>} customViewData
|
|
46
|
-
* @returns {QuestionViewModel}
|
|
43
|
+
* @param {Record<string, any>} answers
|
|
44
|
+
* @returns {*|string}
|
|
47
45
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
townCity: address.townCity || '',
|
|
58
|
-
county: address.county || '',
|
|
59
|
-
postcode: address.postcode || ''
|
|
46
|
+
answerForViewModel(answers) {
|
|
47
|
+
let address = answers[this.fieldName];
|
|
48
|
+
if (!address) {
|
|
49
|
+
address = {
|
|
50
|
+
addressLine1: answers[this.fieldName + '_addressLine1'],
|
|
51
|
+
addressLine2: answers[this.fieldName + '_addressLine2'],
|
|
52
|
+
townCity: answers[this.fieldName + '_townCity'],
|
|
53
|
+
county: answers[this.fieldName + '_county'],
|
|
54
|
+
postcode: answers[this.fieldName + '_postcode']
|
|
60
55
|
};
|
|
61
56
|
}
|
|
62
57
|
|
|
63
|
-
|
|
58
|
+
return {
|
|
59
|
+
addressLine1: address?.addressLine1 || '',
|
|
60
|
+
addressLine2: address?.addressLine2 || '',
|
|
61
|
+
townCity: address?.townCity || '',
|
|
62
|
+
county: address?.county || '',
|
|
63
|
+
postcode: address?.postcode || ''
|
|
64
|
+
};
|
|
65
|
+
}
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
/**
|
|
68
|
+
* @param {import('#question').QuestionViewModel} viewModel
|
|
69
|
+
*/
|
|
70
|
+
addCustomDataToViewModel(viewModel) {
|
|
71
|
+
viewModel.question.labels = this.addressLabels;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
/**
|
|
@@ -3,7 +3,7 @@ import assert from 'node:assert';
|
|
|
3
3
|
import AddressQuestion from './question.js';
|
|
4
4
|
import { Address } from '../../lib/address.js';
|
|
5
5
|
import AddressValidator from '../../validator/address-validator.js';
|
|
6
|
-
import { configureNunjucksTestEnv } from '
|
|
6
|
+
import { configureNunjucksTestEnv } from '#test/utils/nunjucks.js';
|
|
7
7
|
|
|
8
8
|
describe('AddressQuestion', () => {
|
|
9
9
|
const TITLE = 'What is the site address?';
|
|
@@ -50,29 +50,17 @@ export default class DateQuestion extends Question {
|
|
|
50
50
|
return responseToSave;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
* gets the view model for this question
|
|
55
|
-
* @param {Section} section - the current section
|
|
56
|
-
* @param {Journey} journey - the journey we are in
|
|
57
|
-
* @param {Object|undefined} [customViewData] additional data to send to view
|
|
58
|
-
* @returns {QuestionViewModel & { answer: Record<string, unknown> }}
|
|
59
|
-
*/
|
|
60
|
-
prepQuestionForRendering(section, journey, customViewData, payload) {
|
|
61
|
-
let viewModel = super.prepQuestionForRendering(section, journey, customViewData);
|
|
62
|
-
|
|
63
|
-
/** @type {Record<string, unknown>} */
|
|
64
|
-
let answer = {};
|
|
53
|
+
answerForViewModel(answers, isPayload) {
|
|
65
54
|
let day;
|
|
66
55
|
let month;
|
|
67
56
|
let year;
|
|
68
57
|
|
|
69
|
-
if (
|
|
70
|
-
day =
|
|
71
|
-
month =
|
|
72
|
-
year =
|
|
58
|
+
if (isPayload && answers) {
|
|
59
|
+
day = answers[`${this.fieldName}_day`];
|
|
60
|
+
month = answers[`${this.fieldName}_month`];
|
|
61
|
+
year = answers[`${this.fieldName}_year`];
|
|
73
62
|
} else {
|
|
74
|
-
const answerDateString =
|
|
75
|
-
|
|
63
|
+
const answerDateString = answers[this.fieldName];
|
|
76
64
|
if (answerDateString && (typeof answerDateString === 'string' || answerDateString instanceof Date)) {
|
|
77
65
|
const answerDate = new Date(answerDateString);
|
|
78
66
|
day = formatDateForDisplay(answerDate, { format: 'd' });
|
|
@@ -81,13 +69,11 @@ export default class DateQuestion extends Question {
|
|
|
81
69
|
}
|
|
82
70
|
}
|
|
83
71
|
|
|
84
|
-
|
|
72
|
+
return {
|
|
85
73
|
[`${this.fieldName}_day`]: day,
|
|
86
74
|
[`${this.fieldName}_month`]: month,
|
|
87
75
|
[`${this.fieldName}_year`]: year
|
|
88
76
|
};
|
|
89
|
-
|
|
90
|
-
return { ...viewModel, answer, question: { ...viewModel.question, value: answer } };
|
|
91
77
|
}
|
|
92
78
|
|
|
93
79
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatDateForDisplay, parseDateInput } from '../../lib/date-utils.js';
|
|
2
|
-
import { Question } from '
|
|
2
|
+
import { Question } from '#question';
|
|
3
3
|
import { nl2br } from '../../lib/utils.js';
|
|
4
4
|
import escape from 'escape-html';
|
|
5
5
|
|
|
@@ -77,20 +77,7 @@ export default class DatePeriodQuestion extends Question {
|
|
|
77
77
|
return responseToSave;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
* gets the view model for this question
|
|
82
|
-
* @param {Section} section - the current section
|
|
83
|
-
* @param {Journey} journey - the journey we are in
|
|
84
|
-
* @param {Object|undefined} [customViewData] additional data to send to view
|
|
85
|
-
* @param {Object|undefined} [payload] the current payload
|
|
86
|
-
* @returns {QuestionViewModel & { answer: Record<string, unknown> }}
|
|
87
|
-
*/
|
|
88
|
-
prepQuestionForRendering(section, journey, customViewData, payload) {
|
|
89
|
-
let viewModel = super.prepQuestionForRendering(section, journey, customViewData);
|
|
90
|
-
viewModel.labels = this.labels;
|
|
91
|
-
|
|
92
|
-
/** @type {Record<string, unknown>} */
|
|
93
|
-
let answer;
|
|
80
|
+
answerForViewModel(answers, isPayload) {
|
|
94
81
|
let startDay;
|
|
95
82
|
let startMonth;
|
|
96
83
|
let startYear;
|
|
@@ -98,15 +85,15 @@ export default class DatePeriodQuestion extends Question {
|
|
|
98
85
|
let endMonth;
|
|
99
86
|
let endYear;
|
|
100
87
|
|
|
101
|
-
if (
|
|
102
|
-
startDay =
|
|
103
|
-
startMonth =
|
|
104
|
-
startYear =
|
|
105
|
-
endDay =
|
|
106
|
-
endMonth =
|
|
107
|
-
endYear =
|
|
88
|
+
if (isPayload) {
|
|
89
|
+
startDay = answers[`${this.fieldName}_start_day`];
|
|
90
|
+
startMonth = answers[`${this.fieldName}_start_month`];
|
|
91
|
+
startYear = answers[`${this.fieldName}_start_year`];
|
|
92
|
+
endDay = answers[`${this.fieldName}_end_day`];
|
|
93
|
+
endMonth = answers[`${this.fieldName}_end_month`];
|
|
94
|
+
endYear = answers[`${this.fieldName}_end_year`];
|
|
108
95
|
} else {
|
|
109
|
-
const answerPeriod =
|
|
96
|
+
const answerPeriod = answers[this.fieldName];
|
|
110
97
|
if (answerPeriod && answerPeriod.start) {
|
|
111
98
|
const startDate = new Date(answerPeriod.start);
|
|
112
99
|
startDay = formatDateForDisplay(startDate, { format: 'd' });
|
|
@@ -121,7 +108,7 @@ export default class DatePeriodQuestion extends Question {
|
|
|
121
108
|
}
|
|
122
109
|
}
|
|
123
110
|
|
|
124
|
-
|
|
111
|
+
return {
|
|
125
112
|
[`${this.fieldName}_start_day`]: startDay,
|
|
126
113
|
[`${this.fieldName}_start_month`]: startMonth,
|
|
127
114
|
[`${this.fieldName}_start_year`]: startYear,
|
|
@@ -129,8 +116,13 @@ export default class DatePeriodQuestion extends Question {
|
|
|
129
116
|
[`${this.fieldName}_end_month`]: endMonth,
|
|
130
117
|
[`${this.fieldName}_end_year`]: endYear
|
|
131
118
|
};
|
|
119
|
+
}
|
|
132
120
|
|
|
133
|
-
|
|
121
|
+
/**
|
|
122
|
+
* @param {import('#question').QuestionViewModel} viewModel
|
|
123
|
+
*/
|
|
124
|
+
addCustomDataToViewModel(viewModel) {
|
|
125
|
+
viewModel.labels = this.labels;
|
|
134
126
|
}
|
|
135
127
|
|
|
136
128
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
3
|
|
|
4
|
-
import { Question } from '
|
|
4
|
+
import { Question } from '#question';
|
|
5
5
|
import DatePeriodQuestion from './question.js';
|
|
6
6
|
|
|
7
7
|
describe('DatePeriodQuestion', () => {
|
|
@@ -52,18 +52,7 @@ export default class DateTimeQuestion extends Question {
|
|
|
52
52
|
return responseToSave;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
* gets the view model for this question
|
|
57
|
-
* @param {Section} section - the current section
|
|
58
|
-
* @param {Journey} journey - the journey we are in
|
|
59
|
-
* @param {Object|undefined} [customViewData] additional data to send to view
|
|
60
|
-
* @returns {QuestionViewModel & { answer: Record<string, unknown> }}
|
|
61
|
-
*/
|
|
62
|
-
prepQuestionForRendering(section, journey, customViewData, payload) {
|
|
63
|
-
let viewModel = super.prepQuestionForRendering(section, journey, customViewData);
|
|
64
|
-
|
|
65
|
-
/** @type {Record<string, unknown>} */
|
|
66
|
-
let answer = {};
|
|
55
|
+
answerForViewModel(answers, isPayload) {
|
|
67
56
|
let day;
|
|
68
57
|
let month;
|
|
69
58
|
let year;
|
|
@@ -71,15 +60,15 @@ export default class DateTimeQuestion extends Question {
|
|
|
71
60
|
let minutes;
|
|
72
61
|
let period;
|
|
73
62
|
|
|
74
|
-
if (
|
|
75
|
-
day =
|
|
76
|
-
month =
|
|
77
|
-
year =
|
|
78
|
-
hour =
|
|
79
|
-
minutes =
|
|
80
|
-
period =
|
|
63
|
+
if (isPayload) {
|
|
64
|
+
day = answers[`${this.fieldName}_day`];
|
|
65
|
+
month = answers[`${this.fieldName}_month`];
|
|
66
|
+
year = answers[`${this.fieldName}_year`];
|
|
67
|
+
hour = answers[`${this.fieldName}_hour`];
|
|
68
|
+
minutes = answers[`${this.fieldName}_minutes`];
|
|
69
|
+
period = answers[`${this.fieldName}_period`];
|
|
81
70
|
} else {
|
|
82
|
-
const answerDateString =
|
|
71
|
+
const answerDateString = answers[this.fieldName];
|
|
83
72
|
|
|
84
73
|
if (answerDateString && (typeof answerDateString === 'string' || answerDateString instanceof Date)) {
|
|
85
74
|
const answerDate = new Date(answerDateString);
|
|
@@ -92,7 +81,7 @@ export default class DateTimeQuestion extends Question {
|
|
|
92
81
|
}
|
|
93
82
|
}
|
|
94
83
|
|
|
95
|
-
|
|
84
|
+
return {
|
|
96
85
|
[`${this.fieldName}_day`]: day,
|
|
97
86
|
[`${this.fieldName}_month`]: month,
|
|
98
87
|
[`${this.fieldName}_year`]: year,
|
|
@@ -100,8 +89,6 @@ export default class DateTimeQuestion extends Question {
|
|
|
100
89
|
[`${this.fieldName}_minutes`]: minutes,
|
|
101
90
|
[`${this.fieldName}_period`]: period
|
|
102
91
|
};
|
|
103
|
-
|
|
104
|
-
return { ...viewModel, answer, question: { ...viewModel.question, value: answer } };
|
|
105
92
|
}
|
|
106
93
|
|
|
107
94
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Question } from '
|
|
1
|
+
import { Question } from '#question';
|
|
2
2
|
|
|
3
3
|
export default class IdentifierQuestion extends Question {
|
|
4
4
|
/** @type {string|undefined} page h1, optional, will default to use question's label */
|
|
@@ -22,13 +22,10 @@ export default class IdentifierQuestion extends Question {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* @param {import('#question').QuestionViewModel} viewModel
|
|
26
26
|
*/
|
|
27
|
-
|
|
28
|
-
let viewModel = super.prepQuestionForRendering(section, journey, customViewData);
|
|
27
|
+
addCustomDataToViewModel(viewModel) {
|
|
29
28
|
viewModel.question.inputClasses = this.inputClasses;
|
|
30
29
|
viewModel.question.label = this.label;
|
|
31
|
-
viewModel.question.value = payload ? payload[viewModel.question.fieldName] : viewModel.question.value;
|
|
32
|
-
return viewModel;
|
|
33
30
|
}
|
|
34
31
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { Question } from '
|
|
1
|
+
import { Question } from '#question';
|
|
2
2
|
import { Uuid } from '#src/lib/uuid.js';
|
|
3
|
-
import escape from 'escape-html';
|
|
4
3
|
import { nunjucksEnv } from '#src/components/utils/nunjucks.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -45,8 +44,10 @@ export default class ManageListQuestion extends Question {
|
|
|
45
44
|
return true;
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
/**
|
|
48
|
+
* @param {import('#question').QuestionViewModel} viewModel
|
|
49
|
+
*/
|
|
50
|
+
addCustomDataToViewModel(viewModel) {
|
|
50
51
|
viewModel.question.addAnotherLink = this.#addAnotherLink;
|
|
51
52
|
viewModel.question.firstQuestionUrl = this.#firstQuestionUrl;
|
|
52
53
|
viewModel.question.valueSummary = [];
|
|
@@ -58,7 +59,6 @@ export default class ManageListQuestion extends Question {
|
|
|
58
59
|
};
|
|
59
60
|
});
|
|
60
61
|
}
|
|
61
|
-
return viewModel;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
@@ -76,7 +76,7 @@ export default class ManageListQuestion extends Question {
|
|
|
76
76
|
return this.section.questions.map((q) => {
|
|
77
77
|
const formatted = q
|
|
78
78
|
.formatAnswerForSummary('', mockJourney, answer[q.fieldName])
|
|
79
|
-
.map((a) =>
|
|
79
|
+
.map((a) => a.value)
|
|
80
80
|
.join(', ');
|
|
81
81
|
|
|
82
82
|
return {
|
|
@@ -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
|
/**
|
|
@@ -18,7 +18,7 @@ export class JourneyResponse {
|
|
|
18
18
|
journeyId;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* @type {
|
|
21
|
+
* @type {import('./journey-types.d.ts').JourneyAnswers} - answers to the journey
|
|
22
22
|
*/
|
|
23
23
|
answers;
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@ export class JourneyResponse {
|
|
|
26
26
|
* creates an instance of a JourneyResponse
|
|
27
27
|
* @param {JourneyType} journeyId
|
|
28
28
|
* @param {string} referenceId
|
|
29
|
-
* @param {
|
|
29
|
+
* @param {import('./journey-types.d.ts').JourneyAnswers | null} answers
|
|
30
30
|
* @param {string} [lpaCode]
|
|
31
31
|
*/
|
|
32
32
|
constructor(journeyId, referenceId, answers, lpaCode) {
|
|
@@ -16,3 +16,13 @@ export type ManageListRouteParams =
|
|
|
16
16
|
manageListQuestion: string;
|
|
17
17
|
}
|
|
18
18
|
| {};
|
|
19
|
+
|
|
20
|
+
export interface JourneyAnswers {
|
|
21
|
+
// if the answer is for a manage list question, the answer will be an array of answer objects
|
|
22
|
+
[k: string]: unknown | ManageListAnswers[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ManageListAnswers {
|
|
26
|
+
id: string;
|
|
27
|
+
[k: string]: unknown;
|
|
28
|
+
}
|
|
@@ -69,12 +69,13 @@ export default class OptionsQuestion extends Question {
|
|
|
69
69
|
* @param {Journey} journey - the journey we are in
|
|
70
70
|
* @param {Record<string, unknown>} [customViewData] additional data to send to view
|
|
71
71
|
* @param {Record<string, unknown>} [payload]
|
|
72
|
+
* @param {import('#src/questions/question-types.d.ts').PrepQuestionForRenderingOptions} options
|
|
72
73
|
* @returns {QuestionViewModel}
|
|
73
74
|
*/
|
|
74
|
-
prepQuestionForRendering(section, journey, customViewData, payload) {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
const
|
|
75
|
+
prepQuestionForRendering(section, journey, customViewData, payload, options) {
|
|
76
|
+
const viewModel = super.prepQuestionForRendering(section, journey, customViewData, payload, options);
|
|
77
|
+
const answers = payload || this.answerObjectFromJourneyResponse(journey.response, options);
|
|
78
|
+
const answer = viewModel.question.value;
|
|
78
79
|
|
|
79
80
|
viewModel.question.options = [];
|
|
80
81
|
|
|
@@ -96,9 +97,7 @@ export default class OptionsQuestion extends Question {
|
|
|
96
97
|
let conditionalField = { ...optionData.conditional };
|
|
97
98
|
|
|
98
99
|
conditionalField.fieldName = getConditionalFieldName(this.fieldName, conditionalField.fieldName);
|
|
99
|
-
conditionalField.value =
|
|
100
|
-
? payload[conditionalField.fieldName]
|
|
101
|
-
: journey.response.answers[conditionalField.fieldName] || '';
|
|
100
|
+
conditionalField.value = answers[conditionalField.fieldName] || '';
|
|
102
101
|
|
|
103
102
|
optionData.conditional = {
|
|
104
103
|
html: nunjucks.render(`./components/conditional/${conditionalField.type}.njk`, {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import BaseValidator from '../validator/base-validator';
|
|
2
2
|
import { JourneyResponse } from '../journey/journey-response';
|
|
3
|
+
import { RouteParams } from '#src/journey/journey-types.d.ts';
|
|
4
|
+
import ManageListQuestion from '#src/components/manage-list/question.js';
|
|
3
5
|
|
|
4
6
|
export interface QuestionParameters {
|
|
5
7
|
title: string;
|
|
@@ -27,3 +29,27 @@ export interface ActionLink {
|
|
|
27
29
|
text: string;
|
|
28
30
|
href: string;
|
|
29
31
|
}
|
|
32
|
+
|
|
33
|
+
export interface QuestionViewModel {
|
|
34
|
+
question: {
|
|
35
|
+
value: string | number | Record<string, any>;
|
|
36
|
+
question: string;
|
|
37
|
+
fieldName: string;
|
|
38
|
+
pageTitle: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
html?: string;
|
|
41
|
+
};
|
|
42
|
+
layoutTemplate: string;
|
|
43
|
+
pageCaption: string;
|
|
44
|
+
continueButtonText: string;
|
|
45
|
+
backLink: string;
|
|
46
|
+
showBackToListLink: string;
|
|
47
|
+
listLink: string;
|
|
48
|
+
journeyTitle: string;
|
|
49
|
+
[k: string]: any;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface PrepQuestionForRenderingOptions {
|
|
53
|
+
params: RouteParams;
|
|
54
|
+
manageListQuestion?: ManageListQuestion;
|
|
55
|
+
}
|
|
@@ -25,7 +25,7 @@ import MultiFieldInputValidator from '../validator/multi-field-input-validator.j
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* @typedef {
|
|
28
|
+
* @typedef {Record<string, any>} QuestionViewModel
|
|
29
29
|
* @property {PreppedQuestion} question
|
|
30
30
|
* @property {string} layoutTemplate
|
|
31
31
|
* @property {string} pageCaption
|
|
@@ -186,14 +186,17 @@ export class Question {
|
|
|
186
186
|
* @param {Object} options
|
|
187
187
|
* @param {import('#src/journey/journey-types.d.ts').RouteParams} options.params
|
|
188
188
|
* @param {import('#src/components/manage-list/question.js')} [options.manageListQuestion]
|
|
189
|
-
* @param {Section} section - the current section
|
|
190
|
-
* @param {Journey} journey - the journey we are in
|
|
191
|
-
* @param {Record<string, unknown>} [customViewData] additional data to send to view
|
|
192
|
-
* @param {unknown} [payload]
|
|
189
|
+
* @param {Section} options.section - the current section
|
|
190
|
+
* @param {Journey} options.journey - the journey we are in
|
|
191
|
+
* @param {Record<string, unknown>} [options.customViewData] additional data to send to view
|
|
192
|
+
* @param {unknown} [options.payload]
|
|
193
193
|
* @returns {QuestionViewModel}
|
|
194
194
|
*/
|
|
195
195
|
toViewModel({ params, manageListQuestion, section, journey, customViewData, payload }) {
|
|
196
|
-
const viewModel = this.prepQuestionForRendering(section, journey, customViewData, payload
|
|
196
|
+
const viewModel = this.prepQuestionForRendering(section, journey, customViewData, payload, {
|
|
197
|
+
params,
|
|
198
|
+
manageListQuestion
|
|
199
|
+
});
|
|
197
200
|
viewModel.backLink = journey.getBackLink({ params, manageListQuestion });
|
|
198
201
|
return viewModel;
|
|
199
202
|
}
|
|
@@ -205,11 +208,14 @@ export class Question {
|
|
|
205
208
|
* @param {Journey} journey - the journey we are in
|
|
206
209
|
* @param {Record<string, unknown>} [customViewData] additional data to send to view
|
|
207
210
|
* @param {unknown} [payload]
|
|
211
|
+
* @param {import('./question-types.d.ts').PrepQuestionForRenderingOptions} [options] - required to support manage list question
|
|
208
212
|
* @returns {QuestionViewModel}
|
|
209
213
|
*/
|
|
210
|
-
prepQuestionForRendering(section, journey, customViewData, payload) {
|
|
211
|
-
const
|
|
212
|
-
|
|
214
|
+
prepQuestionForRendering(section, journey, customViewData, payload, options) {
|
|
215
|
+
const answers = payload || this.answerObjectFromJourneyResponse(journey.response, options);
|
|
216
|
+
const answer = this.answerForViewModel(answers, Boolean(payload));
|
|
217
|
+
|
|
218
|
+
const viewModel = {
|
|
213
219
|
question: {
|
|
214
220
|
value: answer,
|
|
215
221
|
question: this.question,
|
|
@@ -236,6 +242,56 @@ export class Question {
|
|
|
236
242
|
...customViewData,
|
|
237
243
|
...this.viewData
|
|
238
244
|
};
|
|
245
|
+
this.addCustomDataToViewModel(viewModel);
|
|
246
|
+
return viewModel;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The answer to this question for use in the viewModel
|
|
251
|
+
*
|
|
252
|
+
* Question implementations can override this for more complex answer types
|
|
253
|
+
*
|
|
254
|
+
* @param {Record<string, any>} answers - collection of answers to pull the answer from, may be from the response or the request/payload
|
|
255
|
+
* @param {Boolean} isPayload - whether the answers object is from the request/payload
|
|
256
|
+
* @returns {*|string}
|
|
257
|
+
*/ // eslint-disable-next-line no-unused-vars
|
|
258
|
+
answerForViewModel(answers, isPayload) {
|
|
259
|
+
return answers[this.fieldName] || '';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Question implementations can override this to add configuration or other values to the view model
|
|
264
|
+
*
|
|
265
|
+
* If possible override this method instead of prepQuestionForRendering for simple changes to the view model
|
|
266
|
+
*
|
|
267
|
+
* @param {QuestionViewModel} viewModel
|
|
268
|
+
*/ // eslint-disable-next-line no-unused-vars
|
|
269
|
+
addCustomDataToViewModel(viewModel) {}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get the answers object from the journey response, which may be nested in an array for manage list questions
|
|
273
|
+
*
|
|
274
|
+
* @param {JourneyResponse} response
|
|
275
|
+
* @param {import('./question-types.d.ts').PrepQuestionForRenderingOptions} [options]
|
|
276
|
+
* @returns {Record<string, any>}
|
|
277
|
+
*/
|
|
278
|
+
answerObjectFromJourneyResponse(response, { params, manageListQuestion } = {}) {
|
|
279
|
+
if (this.isInManageListSection) {
|
|
280
|
+
if (!params?.manageListItemId) {
|
|
281
|
+
throw new Error('no list item id for manage list question');
|
|
282
|
+
}
|
|
283
|
+
if (!manageListQuestion) {
|
|
284
|
+
throw new Error('no manageListQuestion for manage list question');
|
|
285
|
+
}
|
|
286
|
+
// if this is a manage list question, the response is within the 'parent' manage list answers array
|
|
287
|
+
const answers = response.answers[manageListQuestion.fieldName];
|
|
288
|
+
if (!Array.isArray(answers)) {
|
|
289
|
+
return {};
|
|
290
|
+
}
|
|
291
|
+
const itemId = params.manageListItemId;
|
|
292
|
+
return answers.find((a) => a.id === itemId) || {};
|
|
293
|
+
}
|
|
294
|
+
return response.answers;
|
|
239
295
|
}
|
|
240
296
|
|
|
241
297
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, mock } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
-
import { Question } from '
|
|
4
|
-
import { mockRes } from '
|
|
3
|
+
import { Question } from '#question';
|
|
4
|
+
import { mockRes } from '#test/utils/utils.js';
|
|
5
5
|
|
|
6
6
|
const res = mockRes();
|
|
7
7
|
|
|
@@ -208,6 +208,76 @@ describe('./src/dynamic-forms/question.js', () => {
|
|
|
208
208
|
});
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
+
describe('answerObjectFromJourney', () => {
|
|
212
|
+
it('should return journey response for regular questions', () => {
|
|
213
|
+
const question = getTestQuestion();
|
|
214
|
+
const answers = { myField: 'my-value' };
|
|
215
|
+
const journeyResponse = {
|
|
216
|
+
answers
|
|
217
|
+
};
|
|
218
|
+
const got = question.answerObjectFromJourneyResponse(journeyResponse);
|
|
219
|
+
assert.strictEqual(got, answers);
|
|
220
|
+
});
|
|
221
|
+
it('should return the manage list item object for manage list questions', () => {
|
|
222
|
+
const question = getTestQuestion();
|
|
223
|
+
question.isInManageListSection = true;
|
|
224
|
+
const manageListQuestion = {
|
|
225
|
+
fieldName: 'manageListQuestion'
|
|
226
|
+
};
|
|
227
|
+
const answers = {
|
|
228
|
+
id: '1',
|
|
229
|
+
myField: 'my-value'
|
|
230
|
+
};
|
|
231
|
+
const journeyResponse = {
|
|
232
|
+
answers: {
|
|
233
|
+
manageListQuestion: [answers]
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const params = { manageListItemId: '1' };
|
|
237
|
+
const got = question.answerObjectFromJourneyResponse(journeyResponse, { params, manageListQuestion });
|
|
238
|
+
assert.strictEqual(got, answers);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should fallback to {} if manage list item id not found', () => {
|
|
242
|
+
const question = getTestQuestion();
|
|
243
|
+
question.isInManageListSection = true;
|
|
244
|
+
const manageListQuestion = {
|
|
245
|
+
fieldName: 'manageListQuestion'
|
|
246
|
+
};
|
|
247
|
+
const answers = {
|
|
248
|
+
id: '2',
|
|
249
|
+
myField: 'my-value'
|
|
250
|
+
};
|
|
251
|
+
const journeyResponse = {
|
|
252
|
+
answers: {
|
|
253
|
+
manageListQuestion: [answers]
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const params = { manageListItemId: '1' };
|
|
257
|
+
let got = question.answerObjectFromJourneyResponse(journeyResponse, { params, manageListQuestion });
|
|
258
|
+
assert.deepStrictEqual(got, {});
|
|
259
|
+
// also fallback to {} if there is no manageListQuestion answer
|
|
260
|
+
delete journeyResponse.answers.manageListQuestion;
|
|
261
|
+
got = question.answerObjectFromJourneyResponse(journeyResponse, { params, manageListQuestion });
|
|
262
|
+
assert.deepStrictEqual(got, {});
|
|
263
|
+
});
|
|
264
|
+
it('should error if a manage list question is missing required parameters', () => {
|
|
265
|
+
const question = getTestQuestion();
|
|
266
|
+
question.isInManageListSection = true;
|
|
267
|
+
const answers = { myField: 'my-value' };
|
|
268
|
+
const journeyResponse = { answers };
|
|
269
|
+
assert.throws(
|
|
270
|
+
() => question.answerObjectFromJourneyResponse(journeyResponse),
|
|
271
|
+
/no list item id for manage list question/
|
|
272
|
+
);
|
|
273
|
+
const params = { manageListItemId: '1' };
|
|
274
|
+
assert.throws(
|
|
275
|
+
() => question.answerObjectFromJourneyResponse(journeyResponse, { params }),
|
|
276
|
+
/no manageListQuestion for manage list question/
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
211
281
|
describe('renderAction', () => {
|
|
212
282
|
it('should renderAction', () => {
|
|
213
283
|
const question = getTestQuestion();
|