@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.
Files changed (44) hide show
  1. package/README.md +9 -9
  2. package/package.json +1 -1
  3. package/src/components/address/question.js +2 -4
  4. package/src/components/boolean/question.js +6 -11
  5. package/src/components/date/question.js +5 -11
  6. package/src/components/date-period/question.js +5 -9
  7. package/src/components/date-time/question.js +5 -8
  8. package/src/components/date-time/question.test.js +3 -24
  9. package/src/components/manage-list/README.md +21 -2
  10. package/src/components/manage-list/index.njk +3 -3
  11. package/src/components/manage-list/question.js +24 -15
  12. package/src/components/manage-list/question.test.js +38 -1
  13. package/src/components/manage-list/utils.js +35 -0
  14. package/src/components/manage-list/utils.test.js +80 -0
  15. package/src/components/multi-field-input/question.js +7 -11
  16. package/src/components/multi-field-input/question.test.js +29 -1
  17. package/src/components/text-entry-redact/question.js +3 -4
  18. package/src/components/unit-option-entry/question.js +6 -13
  19. package/src/components/utils/question-has-answer.js +45 -11
  20. package/src/components/utils/question-utils.js +3 -37
  21. package/src/controller.js +11 -0
  22. package/src/controller.test.js +13 -1
  23. package/src/journey/journey.js +1 -0
  24. package/src/lib/utils.js +11 -0
  25. package/src/lib/utils.test.js +19 -0
  26. package/src/lib/uuid.test.js +0 -12
  27. package/src/questions/options-question.js +6 -13
  28. package/src/questions/question.js +17 -31
  29. package/src/questions/question.test.js +5 -19
  30. package/src/section-types.d.ts +2 -0
  31. package/src/section.js +5 -1
  32. package/src/section.test.js +7 -3
  33. package/test/check-your-answers.test.js +1 -1
  34. package/test/mock/uuid.js +13 -0
  35. package/test/questions.test.js +3 -200
  36. package/test/utils/question-test-utils.js +200 -0
  37. package/src/components/macros/answer-list.njk +0 -29
  38. package/src/middleware/check-not-submitted.js +0 -13
  39. package/src/middleware/check-not-submitted.test.js +0 -118
  40. package/src/middleware/dynamic-req-files-to-req-body-files.js +0 -46
  41. package/src/middleware/dynamic-req-files-to-req-body-files.test.js +0 -221
  42. package/src/validator/required-file-upload-validator.js +0 -38
  43. package/src/validator/required-file-upload-validator.test.js +0 -141
  44. 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 { questionHasAnswer } from '@pins/dynamic-forms/src/components/utils/question-has-answer';
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((response) => questionHasAnswer(response, questions.q1, 'yes'))
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((response) => questionHasAnswer(response, questions.q1, 'yes'))
163
+ .withSectionCondition(whenQuestionHasAnswer(questions.q1, 'yes'))
164
164
  .addQuestion(questions.q3)
165
165
  .addQuestion(questions.q4)
166
- .withCondition((response) => questionHasAnswer(response, questions.q3, 'yes'))
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((response) => questionHasAnswer(response, questions.q1, 'yes'))
176
+ .withSectionCondition(whenQuestionHasAnswer(questions.q1, 'yes'))
177
177
  .addQuestion(questions.q2)
178
- .startMultiQuestionCondition('group-1', (response) => questionHasAnswer(response, questions.q2, 'valid'))
178
+ .startMultiQuestionCondition('group-1', whenQuestionHasAnswer(questions.q2, 'valid'))
179
179
  .addQuestion(questions.q3)
180
180
  .addQuestion(questions.q4)
181
- .withCondition((response) => questionHasAnswer(response, questions.q3, 'yes'))
181
+ .withCondition(whenQuestionHasAnswer(questions.q3, 'yes'))
182
182
  .endMultiQuestionCondition('group-1')
183
183
  .addQuestion(questions.q5)
184
- .withCondition((response) => questionHasAnswer(response, questions.q4, 'yes'))
185
- .startMultiQuestionCondition('group-2', (response) => questionHasAnswer(response, questions.q5, 90))
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planning-inspectorate/dynamic-forms",
3
- "version": "3.2.0",
3
+ "version": "3.3.1",
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",
@@ -72,12 +72,11 @@ export default class AddressQuestion extends Question {
72
72
  }
73
73
 
74
74
  /**
75
- * returns the data to send to the DB
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
- * returns the data to send to the DB
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
- // set answer on response
108
- let responseToSave = { answers: {} };
106
+ const answers = {};
109
107
  const fieldValue = req.body[this.fieldName]?.trim();
110
108
 
111
- responseToSave.answers[this.fieldName] = fieldValue === BOOLEAN_OPTIONS.YES;
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
- responseToSave.answers[propName] = req.body[propName]?.trim();
116
- journeyResponse.answers[propName] = req.body[propName]?.trim();
113
+ answers[propName] = req.body[propName]?.trim();
117
114
  }
118
115
  }
119
116
 
120
- journeyResponse.answers[this.fieldName] = fieldValue;
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
- * returns the data to send to the DB
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
- // set answer on response
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
- const dateToSave = parseDateInput({ day: dayInput, month: monthInput, year: yearInput });
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 responseToSave;
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
- * returns the data to send to the DB
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
- // set answer on response
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
- responseToSave.answers[this.fieldName] = { start: startDate, end: endDate };
74
-
75
- journeyResponse.answers[this.fieldName] = responseToSave.answers[this.fieldName];
71
+ answers[this.fieldName] = { start: startDate, end: endDate };
76
72
 
77
- return responseToSave;
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
- * returns the data to send to the DB
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
- let responseToSave = { answers: {} };
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
- responseToSave.answers[this.fieldName] = parseDateInput({
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
- journeyResponse.answers[this.fieldName] = responseToSave.answers[this.fieldName];
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 journeyResponse = { answers: {} };
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 journeyResponse = { answers: {} };
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 journeyResponse = { answers: {} };
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 as normal in the `ManageListSection`.
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 this.section.questions.map((q) => {
77
- const formatted = q
78
- .formatAnswerForSummary('', mockJourney, answer[q.fieldName])
79
- .map((a) => a.value)
80
- .join(', ');
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
- return {
83
- question: q.title,
84
- answer: formatted
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
- let responseToSave = { answers: {} };
91
- const data = journeyResponse.answers[this.fieldName];
92
- responseToSave.answers[this.fieldName] = data || [];
93
- return responseToSave;
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 '#src/lib/uuid.test.js';
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
- * returns the data to send to the DB
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
- responseToSave.answers[inputField.fieldName] = value;
95
- journeyResponse.answers[inputField.fieldName] = responseToSave.answers[inputField.fieldName];
90
+ answers[inputField.fieldName] = value;
96
91
  }
97
92
 
98
- return responseToSave;
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
- value: nl2br(escape(formattedAnswer)),
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/single-line-input/question.js', () => {
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 &amp; 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
  });