@planning-inspectorate/dynamic-forms 3.2.0 → 3.3.1
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 +9 -9
- package/package.json +1 -1
- package/src/components/address/question.js +2 -4
- package/src/components/boolean/question.js +6 -11
- package/src/components/date/question.js +5 -11
- package/src/components/date-period/question.js +5 -9
- package/src/components/date-time/question.js +5 -8
- package/src/components/date-time/question.test.js +3 -24
- package/src/components/manage-list/README.md +21 -2
- package/src/components/manage-list/index.njk +3 -3
- package/src/components/manage-list/question.js +24 -15
- package/src/components/manage-list/question.test.js +38 -1
- package/src/components/manage-list/utils.js +35 -0
- package/src/components/manage-list/utils.test.js +80 -0
- package/src/components/multi-field-input/question.js +7 -11
- package/src/components/multi-field-input/question.test.js +29 -1
- package/src/components/text-entry-redact/question.js +3 -4
- package/src/components/unit-option-entry/question.js +6 -13
- package/src/components/utils/question-has-answer.js +45 -11
- package/src/components/utils/question-utils.js +3 -37
- package/src/controller.js +11 -0
- package/src/controller.test.js +13 -1
- package/src/journey/journey.js +1 -0
- package/src/lib/utils.js +11 -0
- package/src/lib/utils.test.js +19 -0
- package/src/lib/uuid.test.js +0 -12
- package/src/questions/options-question.js +6 -13
- package/src/questions/question.js +17 -31
- package/src/questions/question.test.js +5 -19
- package/src/section-types.d.ts +2 -0
- package/src/section.js +5 -1
- package/src/section.test.js +7 -3
- package/test/check-your-answers.test.js +1 -1
- package/test/mock/uuid.js +13 -0
- package/test/questions.test.js +3 -200
- package/test/utils/question-test-utils.js +200 -0
- package/src/components/macros/answer-list.njk +0 -29
- package/src/middleware/check-not-submitted.js +0 -13
- package/src/middleware/check-not-submitted.test.js +0 -118
- package/src/middleware/dynamic-req-files-to-req-body-files.js +0 -46
- package/src/middleware/dynamic-req-files-to-req-body-files.test.js +0 -221
- package/src/validator/required-file-upload-validator.js +0 -38
- package/src/validator/required-file-upload-validator.test.js +0 -141
- package/src/validator/schemas/required-file-upload-schema.js +0 -35
package/README.md
CHANGED
|
@@ -146,12 +146,12 @@ to add conditions to questions in multiple ways.
|
|
|
146
146
|
The simplest condition applies to just one question:
|
|
147
147
|
|
|
148
148
|
```javascript
|
|
149
|
-
import {
|
|
149
|
+
import { whenQuestionHasAnswer } from '@pins/dynamic-forms/src/components/utils/question-has-answer';
|
|
150
150
|
|
|
151
151
|
new Section('section-key', 'Section title')
|
|
152
152
|
.addQuestion(questions.q1)
|
|
153
153
|
.addQuestion(questions.q2)
|
|
154
|
-
.withCondition((
|
|
154
|
+
.withCondition(whenQuestionHasAnswer(questions.q1, 'yes'))
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
In this scenario question two will only be shown if question one has an answer of 'yes'.
|
|
@@ -160,10 +160,10 @@ Conditions can also be added to an entire section:
|
|
|
160
160
|
|
|
161
161
|
```javascript
|
|
162
162
|
new Section('section-key', 'Section title')
|
|
163
|
-
.withSectionCondition((
|
|
163
|
+
.withSectionCondition(whenQuestionHasAnswer(questions.q1, 'yes'))
|
|
164
164
|
.addQuestion(questions.q3)
|
|
165
165
|
.addQuestion(questions.q4)
|
|
166
|
-
.withCondition((
|
|
166
|
+
.withCondition(whenQuestionHasAnswer(questions.q3, 'yes'))
|
|
167
167
|
```
|
|
168
168
|
|
|
169
169
|
In this case q3 and q4 will only be shown if q1 (from a previous section) is answered 'yes'. Q4 will only show if q1 is
|
|
@@ -173,16 +173,16 @@ Finally, conditions can be added to multiple questions - these can overlap:
|
|
|
173
173
|
|
|
174
174
|
```javascript
|
|
175
175
|
new Section('section-key', 'Section title')
|
|
176
|
-
.withSectionCondition((
|
|
176
|
+
.withSectionCondition(whenQuestionHasAnswer(questions.q1, 'yes'))
|
|
177
177
|
.addQuestion(questions.q2)
|
|
178
|
-
.startMultiQuestionCondition('group-1', (
|
|
178
|
+
.startMultiQuestionCondition('group-1', whenQuestionHasAnswer(questions.q2, 'valid'))
|
|
179
179
|
.addQuestion(questions.q3)
|
|
180
180
|
.addQuestion(questions.q4)
|
|
181
|
-
.withCondition((
|
|
181
|
+
.withCondition(whenQuestionHasAnswer(questions.q3, 'yes'))
|
|
182
182
|
.endMultiQuestionCondition('group-1')
|
|
183
183
|
.addQuestion(questions.q5)
|
|
184
|
-
.withCondition((
|
|
185
|
-
.startMultiQuestionCondition('group-2', (
|
|
184
|
+
.withCondition(whenQuestionHasAnswer(questions.q4, 'yes'))
|
|
185
|
+
.startMultiQuestionCondition('group-2', whenQuestionHasAnswer(questions.q5, 90))
|
|
186
186
|
.addQuestion(questions.q6)
|
|
187
187
|
.addQuestion(questions.q7)
|
|
188
188
|
.endMultiQuestionCondition('group-2')
|
package/package.json
CHANGED
|
@@ -72,12 +72,11 @@ export default class AddressQuestion extends Question {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
76
|
-
* side effect: modifies journeyResponse with the new answers
|
|
75
|
+
* Get the data to save from the request, returns an object of answers
|
|
77
76
|
* @param {import('express').Request} req
|
|
78
77
|
* @param {JourneyResponse} journeyResponse
|
|
79
78
|
* @returns {Promise<{answers: Record<string, unknown>}>}
|
|
80
|
-
*/
|
|
79
|
+
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
81
80
|
async getDataToSave(req, journeyResponse) {
|
|
82
81
|
const data = {
|
|
83
82
|
addressLine1: req.body[this.fieldName + '_addressLine1'],
|
|
@@ -94,7 +93,6 @@ export default class AddressQuestion extends Question {
|
|
|
94
93
|
const answers = {
|
|
95
94
|
[this.fieldName]: address
|
|
96
95
|
};
|
|
97
|
-
journeyResponse.answers[this.fieldName] = address;
|
|
98
96
|
|
|
99
97
|
return {
|
|
100
98
|
answers
|
|
@@ -97,28 +97,23 @@ export default class BooleanQuestion extends RadioQuestion {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
*
|
|
101
|
-
* side effect: modifies journeyResponse with the new answers
|
|
100
|
+
* Get the data to save from the request, returns an object of answers
|
|
102
101
|
* @param {import('express').Request} req
|
|
103
102
|
* @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
|
|
104
103
|
* @returns {Promise.<Object>}
|
|
105
|
-
*/
|
|
104
|
+
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
106
105
|
async getDataToSave(req, journeyResponse) {
|
|
107
|
-
|
|
108
|
-
let responseToSave = { answers: {} };
|
|
106
|
+
const answers = {};
|
|
109
107
|
const fieldValue = req.body[this.fieldName]?.trim();
|
|
110
108
|
|
|
111
|
-
|
|
109
|
+
answers[this.fieldName] = fieldValue === BOOLEAN_OPTIONS.YES;
|
|
112
110
|
|
|
113
111
|
for (const propName in req.body) {
|
|
114
112
|
if (propName.startsWith(this.fieldName + '_')) {
|
|
115
|
-
|
|
116
|
-
journeyResponse.answers[propName] = req.body[propName]?.trim();
|
|
113
|
+
answers[propName] = req.body[propName]?.trim();
|
|
117
114
|
}
|
|
118
115
|
}
|
|
119
116
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return responseToSave;
|
|
117
|
+
return { answers };
|
|
123
118
|
}
|
|
124
119
|
}
|
|
@@ -27,27 +27,21 @@ export default class DateQuestion extends Question {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
* side effect: modifies journeyResponse with the new answers
|
|
30
|
+
* Get the data to save from the request, returns an object of answers
|
|
32
31
|
* @param {import('express').Request} req
|
|
33
32
|
* @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
|
|
34
33
|
* @returns {Promise.<Object>}
|
|
35
|
-
*/
|
|
34
|
+
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
36
35
|
async getDataToSave(req, journeyResponse) {
|
|
37
|
-
|
|
38
|
-
let responseToSave = { answers: {} };
|
|
36
|
+
const answers = {};
|
|
39
37
|
|
|
40
38
|
const dayInput = req.body[`${this.fieldName}_day`];
|
|
41
39
|
const monthInput = req.body[`${this.fieldName}_month`];
|
|
42
40
|
const yearInput = req.body[`${this.fieldName}_year`];
|
|
43
41
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
responseToSave.answers[this.fieldName] = dateToSave;
|
|
47
|
-
|
|
48
|
-
journeyResponse.answers[this.fieldName] = responseToSave.answers[this.fieldName];
|
|
42
|
+
answers[this.fieldName] = parseDateInput({ day: dayInput, month: monthInput, year: yearInput });
|
|
49
43
|
|
|
50
|
-
return
|
|
44
|
+
return { answers };
|
|
51
45
|
}
|
|
52
46
|
|
|
53
47
|
answerForViewModel(answers, isPayload) {
|
|
@@ -36,15 +36,13 @@ export default class DatePeriodQuestion extends Question {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
* side effect: modifies journeyResponse with the new answers
|
|
39
|
+
* Get the data to save from the request, returns an object of answers
|
|
41
40
|
* @param {import('express').Request} req
|
|
42
41
|
* @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
|
|
43
42
|
* @returns {Promise.<Object>}
|
|
44
|
-
*/
|
|
43
|
+
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
45
44
|
async getDataToSave(req, journeyResponse) {
|
|
46
|
-
|
|
47
|
-
let responseToSave = { answers: {} };
|
|
45
|
+
const answers = {};
|
|
48
46
|
|
|
49
47
|
const startDayInput = req.body[`${this.fieldName}_start_day`];
|
|
50
48
|
const startMonthInput = req.body[`${this.fieldName}_start_month`];
|
|
@@ -70,11 +68,9 @@ export default class DatePeriodQuestion extends Question {
|
|
|
70
68
|
year: endYearInput
|
|
71
69
|
});
|
|
72
70
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
journeyResponse.answers[this.fieldName] = responseToSave.answers[this.fieldName];
|
|
71
|
+
answers[this.fieldName] = { start: startDate, end: endDate };
|
|
76
72
|
|
|
77
|
-
return
|
|
73
|
+
return { answers };
|
|
78
74
|
}
|
|
79
75
|
|
|
80
76
|
answerForViewModel(answers, isPayload) {
|
|
@@ -23,14 +23,13 @@ export default class DateTimeQuestion extends Question {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
* side effect: modifies journeyResponse with the new answers
|
|
26
|
+
* Get the data to save from the request, returns an object of answers
|
|
28
27
|
* @param {import('express').Request} req
|
|
29
28
|
* @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
|
|
30
29
|
* @returns {Promise.<Object>}
|
|
31
|
-
*/
|
|
30
|
+
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
32
31
|
async getDataToSave(req, journeyResponse) {
|
|
33
|
-
|
|
32
|
+
const answers = {};
|
|
34
33
|
|
|
35
34
|
const dayInput = req.body[`${this.fieldName}_day`];
|
|
36
35
|
const monthInput = req.body[`${this.fieldName}_month`];
|
|
@@ -39,7 +38,7 @@ export default class DateTimeQuestion extends Question {
|
|
|
39
38
|
const minutesInput = req.body[`${this.fieldName}_minutes`];
|
|
40
39
|
const periodInput = req.body[`${this.fieldName}_period`];
|
|
41
40
|
|
|
42
|
-
|
|
41
|
+
answers[this.fieldName] = parseDateInput({
|
|
43
42
|
day: dayInput,
|
|
44
43
|
month: monthInput,
|
|
45
44
|
year: yearInput,
|
|
@@ -47,9 +46,7 @@ export default class DateTimeQuestion extends Question {
|
|
|
47
46
|
minute: minutesInput
|
|
48
47
|
});
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return responseToSave;
|
|
49
|
+
return { answers };
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
answerForViewModel(answers, isPayload) {
|
|
@@ -131,20 +131,13 @@ describe('./lib/forms/custom-components/date-time/question.js', () => {
|
|
|
131
131
|
siteVisitDate_period: 'am'
|
|
132
132
|
}
|
|
133
133
|
};
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
const responseToSave = await question.getDataToSave(req, journeyResponse);
|
|
134
|
+
const responseToSave = await question.getDataToSave(req);
|
|
137
135
|
|
|
138
136
|
assert.deepStrictEqual(responseToSave, {
|
|
139
137
|
answers: {
|
|
140
138
|
siteVisitDate: new Date('2025-02-21T00:12:00.000Z')
|
|
141
139
|
}
|
|
142
140
|
});
|
|
143
|
-
assert.deepStrictEqual(journeyResponse, {
|
|
144
|
-
answers: {
|
|
145
|
-
siteVisitDate: new Date('2025-02-21T00:12:00.000Z')
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
141
|
});
|
|
149
142
|
it('should get data to save when time period is pm', async () => {
|
|
150
143
|
const req = {
|
|
@@ -157,20 +150,13 @@ describe('./lib/forms/custom-components/date-time/question.js', () => {
|
|
|
157
150
|
siteVisitDate_period: 'pm'
|
|
158
151
|
}
|
|
159
152
|
};
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
const responseToSave = await question.getDataToSave(req, journeyResponse);
|
|
153
|
+
const responseToSave = await question.getDataToSave(req);
|
|
163
154
|
|
|
164
155
|
assert.deepStrictEqual(responseToSave, {
|
|
165
156
|
answers: {
|
|
166
157
|
siteVisitDate: new Date('2025-02-21T20:12:00.000Z')
|
|
167
158
|
}
|
|
168
159
|
});
|
|
169
|
-
assert.deepStrictEqual(journeyResponse, {
|
|
170
|
-
answers: {
|
|
171
|
-
siteVisitDate: new Date('2025-02-21T20:12:00.000Z')
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
160
|
});
|
|
175
161
|
it('should get data to save when bst date', async () => {
|
|
176
162
|
const req = {
|
|
@@ -183,20 +169,13 @@ describe('./lib/forms/custom-components/date-time/question.js', () => {
|
|
|
183
169
|
siteVisitDate_period: 'am'
|
|
184
170
|
}
|
|
185
171
|
};
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
const responseToSave = await question.getDataToSave(req, journeyResponse);
|
|
172
|
+
const responseToSave = await question.getDataToSave(req);
|
|
189
173
|
|
|
190
174
|
assert.deepStrictEqual(responseToSave, {
|
|
191
175
|
answers: {
|
|
192
176
|
siteVisitDate: new Date('2025-05-20T23:12:00.000Z')
|
|
193
177
|
}
|
|
194
178
|
});
|
|
195
|
-
assert.deepStrictEqual(journeyResponse, {
|
|
196
|
-
answers: {
|
|
197
|
-
siteVisitDate: new Date('2025-05-20T23:12:00.000Z')
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
179
|
});
|
|
201
180
|
});
|
|
202
181
|
describe('formatAnswerForSummary', () => {
|
|
@@ -59,7 +59,7 @@ const questionProps = {
|
|
|
59
59
|
|
|
60
60
|
### 2. Set up the section within the journey
|
|
61
61
|
|
|
62
|
-
You can use conditional logic
|
|
62
|
+
You can use conditional logic in the `ManageListSection` (see [Conditions](#conditions))
|
|
63
63
|
|
|
64
64
|
```js
|
|
65
65
|
new Section('New Holiday', 'holiday')
|
|
@@ -94,4 +94,23 @@ router.post(
|
|
|
94
94
|
validationErrorHandler,
|
|
95
95
|
buildSave(saveDataToSession)
|
|
96
96
|
);
|
|
97
|
-
```
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Conditions
|
|
100
|
+
|
|
101
|
+
The usual conditions can be used in a `ManageListSection`. However, only answers in the `ManageListSection` will be available to check, not answers for main the journey. For example:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
new Section('New Holiday', 'holiday')
|
|
105
|
+
.addQuestion(questions.holidayDestination)
|
|
106
|
+
// add a manage list question like any other, but include the `ManageListSection` as well
|
|
107
|
+
.addQuestion(
|
|
108
|
+
questions.holidayActivitiesList,
|
|
109
|
+
new ManageListSection()
|
|
110
|
+
.addQuestion(questions.holidayActivityType)
|
|
111
|
+
.addQuestion(questions.holidayActivityDate)
|
|
112
|
+
.withCondition(whenQuestionHasAnswer(questions.holidayActivityType, 'swimming'))
|
|
113
|
+
)
|
|
114
|
+
.addQuestion(questions.departureDate)
|
|
115
|
+
.addQuestion(questions.holidayPeriod)
|
|
116
|
+
```
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
<dd class="govuk-summary-list__actions">
|
|
32
32
|
<ul class="govuk-summary-list__actions-list">
|
|
33
33
|
<li class="govuk-summary-list__actions-list-item">
|
|
34
|
-
<a class="govuk-link" href="{{ originalUrl }}/edit/{{ item.id }}/{{ question.firstQuestionUrl }}">
|
|
34
|
+
<a class="govuk-link" href="{{ util.trimTrailingSlash(originalUrl) }}/edit/{{ item.id }}/{{ question.firstQuestionUrl }}">
|
|
35
35
|
Change <span class="govuk-visually-hidden">{{ titleSingular }} {{ loop.index }}</span>
|
|
36
36
|
</a>
|
|
37
37
|
</li>
|
|
38
38
|
<li class="govuk-summary-list__actions-list-item">
|
|
39
|
-
<a class="govuk-link" href="{{ originalUrl }}/remove/{{ item.id }}/confirm">
|
|
39
|
+
<a class="govuk-link" href="{{ util.trimTrailingSlash(originalUrl) }}/remove/{{ item.id }}/confirm">
|
|
40
40
|
Remove <span class="govuk-visually-hidden">{{ titleSingular }} {{ loop.index }}</span>
|
|
41
41
|
</a>
|
|
42
42
|
</li>
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
</dl>
|
|
48
48
|
|
|
49
49
|
<p class="govuk-body">
|
|
50
|
-
<a href="{{ originalUrl }}/{{ question.addAnotherLink }}" class="govuk-link">Add another</a>
|
|
50
|
+
<a href="{{ util.trimTrailingSlash(originalUrl) }}/{{ question.addAnotherLink }}" class="govuk-link">Add another</a>
|
|
51
51
|
</p>
|
|
52
52
|
|
|
53
53
|
{{ govukButton({
|
|
@@ -71,26 +71,35 @@ export default class ManageListQuestion extends Question {
|
|
|
71
71
|
return [];
|
|
72
72
|
}
|
|
73
73
|
const mockJourney = {
|
|
74
|
-
getCurrentQuestionUrl() {}
|
|
74
|
+
getCurrentQuestionUrl() {},
|
|
75
|
+
response: {
|
|
76
|
+
answers: answer
|
|
77
|
+
}
|
|
75
78
|
};
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.
|
|
80
|
-
.
|
|
79
|
+
return (
|
|
80
|
+
this.section.questions
|
|
81
|
+
// only show questions which should be displayed based on any conditional logic
|
|
82
|
+
.filter((q) => q.shouldDisplay({ answers: answer }))
|
|
83
|
+
.map((q) => {
|
|
84
|
+
const formatted = q
|
|
85
|
+
.formatAnswerForSummary('', mockJourney, answer[q.fieldName])
|
|
86
|
+
.map((a) => a.value)
|
|
87
|
+
.join(', ');
|
|
81
88
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
return {
|
|
90
|
+
question: q.title,
|
|
91
|
+
answer: formatted
|
|
92
|
+
};
|
|
93
|
+
})
|
|
94
|
+
);
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
async getDataToSave(req, journeyResponse) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
return {
|
|
99
|
+
answers: {
|
|
100
|
+
[this.fieldName]: journeyResponse.answers[this.fieldName] || []
|
|
101
|
+
}
|
|
102
|
+
};
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
formatAnswerForSummary(sectionSegment, journey, answer) {
|
|
@@ -3,7 +3,7 @@ import assert from 'node:assert';
|
|
|
3
3
|
import ManageListQuestion from '#src/components/manage-list/question.js';
|
|
4
4
|
import { ManageListSection } from '#src/components/manage-list/manage-list-section.js';
|
|
5
5
|
import { mockJourney } from '#test/mock/journey.js';
|
|
6
|
-
import { mockRandomUUID } from '#
|
|
6
|
+
import { mockRandomUUID } from '#test/mock/uuid.js';
|
|
7
7
|
import { configureNunjucksTestEnv } from '#test/utils/nunjucks.js';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { snapshotsDir } from '#test/utils/utils.js';
|
|
@@ -136,4 +136,41 @@ describe('components/manage-list/question', () => {
|
|
|
136
136
|
}
|
|
137
137
|
);
|
|
138
138
|
});
|
|
139
|
+
|
|
140
|
+
describe('getDataToSave', () => {
|
|
141
|
+
it('should return existing journey answers if present', async () => {
|
|
142
|
+
const q = newQuestion();
|
|
143
|
+
const existingData = [{ id: '1', name: 'Test' }];
|
|
144
|
+
const req = { body: {} };
|
|
145
|
+
const journeyResponse = {
|
|
146
|
+
answers: {
|
|
147
|
+
[FIELDNAME]: existingData
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const result = await q.getDataToSave(req, journeyResponse);
|
|
152
|
+
|
|
153
|
+
assert.deepStrictEqual(result, {
|
|
154
|
+
answers: {
|
|
155
|
+
[FIELDNAME]: existingData
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should return an empty array if no journey answers are present', async () => {
|
|
161
|
+
const q = newQuestion();
|
|
162
|
+
const req = { body: {} };
|
|
163
|
+
const journeyResponse = {
|
|
164
|
+
answers: {}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const result = await q.getDataToSave(req, journeyResponse);
|
|
168
|
+
|
|
169
|
+
assert.deepStrictEqual(result, {
|
|
170
|
+
answers: {
|
|
171
|
+
[FIELDNAME]: []
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
139
176
|
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* @param {import('#src/journey/journey-response.js').JourneyResponse} response
|
|
4
|
+
* @param {import('./question.js')} manageListQuestion
|
|
5
|
+
* @param {string} manageListItemId
|
|
6
|
+
* @returns {Record<string, unknown>}
|
|
7
|
+
*/
|
|
8
|
+
export function answerObjectForManageList(response, manageListQuestion, manageListItemId) {
|
|
9
|
+
const answers = response.answers[manageListQuestion.fieldName];
|
|
10
|
+
if (!Array.isArray(answers)) {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
return answers.find((a) => a.id === manageListItemId) || {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Similar to answerObjectForManageList but will edit response and add a new array entry if not found
|
|
18
|
+
*
|
|
19
|
+
* @param {import('#src/journey/journey-response.js').JourneyResponse} response
|
|
20
|
+
* @param {import('./question.js')} manageListQuestion
|
|
21
|
+
* @param {import('#src/journey/journey-types.d.ts').RouteParams} params
|
|
22
|
+
* @returns {import('#src/journey/journey-types.d.ts').ManageListAnswers}
|
|
23
|
+
*/
|
|
24
|
+
export function answerObjectForManageListSaving(response, manageListQuestion, params) {
|
|
25
|
+
const answersList =
|
|
26
|
+
response.answers[manageListQuestion?.fieldName] || (response.answers[manageListQuestion?.fieldName] = []);
|
|
27
|
+
let answers = answersList.find((item) => item.id === params.manageListItemId);
|
|
28
|
+
if (!answers) {
|
|
29
|
+
answers = {
|
|
30
|
+
id: params.manageListItemId
|
|
31
|
+
};
|
|
32
|
+
answersList.push(answers);
|
|
33
|
+
}
|
|
34
|
+
return answers;
|
|
35
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { answerObjectForManageList, answerObjectForManageListSaving } from '#src/components/manage-list/utils.js';
|
|
4
|
+
|
|
5
|
+
describe('manage-list-utils', () => {
|
|
6
|
+
describe('answerObjectForManageList', () => {
|
|
7
|
+
it('should return the manage list item object for manage list questions', () => {
|
|
8
|
+
const manageListQuestion = {
|
|
9
|
+
fieldName: 'manageListQuestion'
|
|
10
|
+
};
|
|
11
|
+
const answers = {
|
|
12
|
+
id: '1',
|
|
13
|
+
myField: 'my-value'
|
|
14
|
+
};
|
|
15
|
+
const journeyResponse = {
|
|
16
|
+
answers: {
|
|
17
|
+
manageListQuestion: [answers]
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const manageListItemId = '1';
|
|
21
|
+
const got = answerObjectForManageList(journeyResponse, manageListQuestion, manageListItemId);
|
|
22
|
+
assert.strictEqual(got, answers);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should fallback to {} if manage list item id not found', () => {
|
|
26
|
+
const manageListQuestion = {
|
|
27
|
+
fieldName: 'manageListQuestion'
|
|
28
|
+
};
|
|
29
|
+
const answers = {
|
|
30
|
+
id: '2',
|
|
31
|
+
myField: 'my-value'
|
|
32
|
+
};
|
|
33
|
+
const journeyResponse = {
|
|
34
|
+
answers: {
|
|
35
|
+
manageListQuestion: [answers]
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const manageListItemId = '1';
|
|
39
|
+
let got = answerObjectForManageList(journeyResponse, manageListQuestion, manageListItemId);
|
|
40
|
+
assert.deepStrictEqual(got, {});
|
|
41
|
+
// also fallback to {} if there is no manageListQuestion answer
|
|
42
|
+
delete journeyResponse.answers.manageListQuestion;
|
|
43
|
+
got = answerObjectForManageList(journeyResponse, manageListQuestion, manageListItemId);
|
|
44
|
+
assert.deepStrictEqual(got, {});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('answerObjectForManageListSaving', () => {
|
|
49
|
+
it('should add an array with a new object if not found', () => {
|
|
50
|
+
const manageListQuestion = {
|
|
51
|
+
fieldName: 'manageListQuestion'
|
|
52
|
+
};
|
|
53
|
+
const journeyResponse = {
|
|
54
|
+
answers: {}
|
|
55
|
+
};
|
|
56
|
+
const params = { manageListItemId: '1' };
|
|
57
|
+
const got = answerObjectForManageListSaving(journeyResponse, manageListQuestion, params);
|
|
58
|
+
assert.deepStrictEqual(got, { id: '1' });
|
|
59
|
+
assert.ok(Array.isArray(journeyResponse.answers.manageListQuestion));
|
|
60
|
+
assert.ok(journeyResponse.answers.manageListQuestion[0]?.id, '1');
|
|
61
|
+
});
|
|
62
|
+
it('should return the manage list item object for manage list questions', () => {
|
|
63
|
+
const manageListQuestion = {
|
|
64
|
+
fieldName: 'manageListQuestion'
|
|
65
|
+
};
|
|
66
|
+
const answers = {
|
|
67
|
+
id: '1',
|
|
68
|
+
myField: 'my-value'
|
|
69
|
+
};
|
|
70
|
+
const journeyResponse = {
|
|
71
|
+
answers: {
|
|
72
|
+
manageListQuestion: [answers]
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const params = { manageListItemId: '1' };
|
|
76
|
+
const got = answerObjectForManageListSaving(journeyResponse, manageListQuestion, params);
|
|
77
|
+
assert.strictEqual(got, answers);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -74,28 +74,23 @@ export default class MultiFieldInputQuestion extends Question {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
78
|
-
* side effect: modifies journeyResponse with the new answers
|
|
77
|
+
* Get the data to save from the request, returns an object of answers
|
|
79
78
|
* @param {import('express').Request} req
|
|
80
79
|
* @param {JourneyResponse} journeyResponse - current journey response, modified with the new answers
|
|
81
80
|
* @returns {Promise<{ answers: Record<string, unknown> }>}
|
|
82
|
-
*/
|
|
81
|
+
*/ //eslint-disable-next-line no-unused-vars -- journeyResponse kept for other questions to use
|
|
83
82
|
async getDataToSave(req, journeyResponse) {
|
|
84
|
-
|
|
85
|
-
* @type {{ answers: Record<string, unknown> }}
|
|
86
|
-
*/
|
|
87
|
-
let responseToSave = { answers: {} };
|
|
83
|
+
const answers = {};
|
|
88
84
|
|
|
89
85
|
for (const inputField of this.inputFields) {
|
|
90
86
|
let value = req.body[inputField.fieldName];
|
|
91
87
|
if (typeof value === 'string') {
|
|
92
88
|
value = value.trim();
|
|
93
89
|
}
|
|
94
|
-
|
|
95
|
-
journeyResponse.answers[inputField.fieldName] = responseToSave.answers[inputField.fieldName];
|
|
90
|
+
answers[inputField.fieldName] = value;
|
|
96
91
|
}
|
|
97
92
|
|
|
98
|
-
return
|
|
93
|
+
return { answers };
|
|
99
94
|
}
|
|
100
95
|
|
|
101
96
|
/**
|
|
@@ -123,7 +118,8 @@ export default class MultiFieldInputQuestion extends Question {
|
|
|
123
118
|
return [
|
|
124
119
|
{
|
|
125
120
|
key: `${this.title}`,
|
|
126
|
-
|
|
121
|
+
// Do not convert new lines to breaks in ManageListSection, as this causes a "doubled up" <br>
|
|
122
|
+
value: this.isInManageListSection ? escape(formattedAnswer) : nl2br(escape(formattedAnswer)),
|
|
127
123
|
action: this.getAction(sectionSegment, journey, summaryDetails)
|
|
128
124
|
}
|
|
129
125
|
];
|
|
@@ -34,7 +34,7 @@ function createMultiFieldInputQuestion(
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
describe('./src/dynamic-forms/components/
|
|
37
|
+
describe('./src/dynamic-forms/components/multi-field-input/question.js', () => {
|
|
38
38
|
it('should create', () => {
|
|
39
39
|
const testQuestion = createMultiFieldInputQuestion();
|
|
40
40
|
|
|
@@ -320,5 +320,33 @@ describe('./src/dynamic-forms/components/single-line-input/question.js', () => {
|
|
|
320
320
|
|
|
321
321
|
assert.deepStrictEqual(result, expectedResult);
|
|
322
322
|
});
|
|
323
|
+
it('should return escaped text with newlines (no <br>) when isInManageListSection is true', () => {
|
|
324
|
+
const question = createMultiFieldInputQuestion();
|
|
325
|
+
|
|
326
|
+
question.isInManageListSection = true;
|
|
327
|
+
|
|
328
|
+
const journey = new Journey({
|
|
329
|
+
journeyId: 'TEST',
|
|
330
|
+
makeBaseUrl: () => 'base',
|
|
331
|
+
taskListUrl: 'cases/create-a-case/check-your-answers',
|
|
332
|
+
response: {
|
|
333
|
+
answers: {
|
|
334
|
+
testField1: 'Line 1',
|
|
335
|
+
testField2: 'Line & 2'
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
journeyTemplate: 'mock template',
|
|
339
|
+
taskListTemplate: 'mock path',
|
|
340
|
+
journeyTitle: 'mock title',
|
|
341
|
+
sections: []
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const expectedValue = 'Line 1\nLine & 2\n';
|
|
345
|
+
|
|
346
|
+
const result = question.formatAnswerForSummary('questions', journey);
|
|
347
|
+
|
|
348
|
+
assert.strictEqual(result[0].value, expectedValue);
|
|
349
|
+
assert.strictEqual(result[0].value.includes('<br>'), false);
|
|
350
|
+
});
|
|
323
351
|
});
|
|
324
352
|
});
|