@planning-inspectorate/dynamic-forms 3.21.0 → 3.22.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.
Files changed (52) hide show
  1. package/README.md +52 -1
  2. package/docs/Dynamic Sections.md +47 -0
  3. package/package.json +1 -1
  4. package/src/components/manage-list/question.d.ts.map +1 -1
  5. package/src/components/manage-list/question.js +5 -0
  6. package/src/components/manage-list/question.test.js +3 -1
  7. package/src/components/multi-field-input/question.d.ts.map +1 -1
  8. package/src/components/multi-field-input/question.js +7 -5
  9. package/src/components/multi-field-input/question.test.js +126 -213
  10. package/src/components/text-entry-redact/question.d.ts.map +1 -1
  11. package/src/components/text-entry-redact/question.js +3 -1
  12. package/src/components/text-entry-redact/question.test.js +9 -0
  13. package/src/components/unit-option-entry/question.d.ts.map +1 -1
  14. package/src/components/unit-option-entry/question.js +3 -1
  15. package/src/components/unit-option-entry/question.test.js +9 -0
  16. package/src/controller.d.ts +15 -3
  17. package/src/controller.d.ts.map +1 -1
  18. package/src/controller.js +19 -11
  19. package/src/controller.test.js +182 -153
  20. package/src/dynamic-section.d.ts +40 -0
  21. package/src/dynamic-section.d.ts.map +1 -0
  22. package/src/dynamic-section.js +51 -0
  23. package/src/dynamic-section.test.js +113 -0
  24. package/src/index.d.ts +2 -1
  25. package/src/index.js +2 -1
  26. package/src/index.test.js +3 -0
  27. package/src/journey/journey.d.ts +10 -0
  28. package/src/journey/journey.d.ts.map +1 -1
  29. package/src/journey/journey.js +15 -0
  30. package/src/lib/answer-utils.d.ts +22 -0
  31. package/src/lib/answer-utils.d.ts.map +1 -0
  32. package/src/lib/answer-utils.js +34 -0
  33. package/src/{components/manage-list/utils.test.js → lib/answer-utils.test.js} +9 -11
  34. package/src/lib/session-answer-store.d.ts.map +1 -1
  35. package/src/lib/session-answer-store.js +14 -6
  36. package/src/lib/session-answer-store.test.js +43 -0
  37. package/src/middleware/redirect-to-unanswered-question.d.ts.map +1 -1
  38. package/src/middleware/redirect-to-unanswered-question.js +4 -3
  39. package/src/middleware/redirect-to-unanswered-question.test.js +32 -0
  40. package/src/questions/question.d.ts +8 -3
  41. package/src/questions/question.d.ts.map +1 -1
  42. package/src/questions/question.js +16 -6
  43. package/src/questions/question.test.js +19 -0
  44. package/src/section.d.ts +19 -0
  45. package/src/section.d.ts.map +1 -1
  46. package/src/section.js +50 -9
  47. package/src/section.test.js +9 -0
  48. package/test/dynamic-journey.test.js +372 -0
  49. package/typedefs/question-types.d.ts +3 -0
  50. package/src/components/manage-list/utils.d.ts +0 -18
  51. package/src/components/manage-list/utils.d.ts.map +0 -1
  52. package/src/components/manage-list/utils.js +0 -35
@@ -20,6 +20,9 @@ export function question(req: import("express-serve-static-core").Request<import
20
20
  * @property {boolean} isManageListItem
21
21
  * @property {string} [manageListQuestionFieldName]
22
22
  * @property {boolean} [manageListItemRemove]
23
+ * @property {boolean} isDynamicSection is this save action for a question within a DynamicSection?
24
+ * @property {string} [dynamicSectionId] if this is within a DynamicSection, what is the section ID (segment)
25
+ * @property {string} [dynamicSectionFieldName] if this is within a DynamicSection, what is the fieldName for the array?
23
26
  * @property {Object<string, any>} data
24
27
  */
25
28
  /**
@@ -31,9 +34,6 @@ export function question(req: import("express-serve-static-core").Request<import
31
34
  * @returns {import('express').Handler}
32
35
  */
33
36
  export function buildSave(saveData: SaveDataFn, redirectToTaskListOnSuccess?: boolean): import("express").Handler;
34
- export type Journey = import("./journey/journey.js").Journey;
35
- export type Question = import("./questions/question.js").Question;
36
- export type Section = import("./section.js").Section;
37
37
  export type SectionView = {
38
38
  heading: string;
39
39
  status: string;
@@ -62,6 +62,18 @@ export type SaveParams = {
62
62
  isManageListItem: boolean;
63
63
  manageListQuestionFieldName?: string | undefined;
64
64
  manageListItemRemove?: boolean | undefined;
65
+ /**
66
+ * is this save action for a question within a DynamicSection?
67
+ */
68
+ isDynamicSection: boolean;
69
+ /**
70
+ * if this is within a DynamicSection, what is the section ID (segment)
71
+ */
72
+ dynamicSectionId?: string | undefined;
73
+ /**
74
+ * if this is within a DynamicSection, what is the fieldName for the array?
75
+ */
76
+ dynamicSectionFieldName?: string | undefined;
65
77
  data: {
66
78
  [x: string]: any;
67
79
  };
@@ -1 +1 @@
1
- {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["controller.js"],"names":[],"mappings":"AA+DA;;;GAGG;AACH,qCAHW,MAAM,GACJ,OAAO,SAAS,EAAE,OAAO,CAIrC;AAED;;;;;GAKG;AACH,0BALW,OAAO,SAAS,EAAE,OAAO,OACzB,OAAO,SAAS,EAAE,QAAQ,eAC1B,MAAM,YACN,MAAM,iBA8DhB;;AAmDD;;;;;;;;;;GAUG;AAEH;;GAEG;AAEH;;;;GAIG;AACH,oCAJW,UAAU,gCACV,OAAO,GACL,OAAO,SAAS,EAAE,OAAO,CAsFrC;sBA7RY,OAAO,sBAAsB,EAAE,OAAO;uBACtC,OAAO,yBAAyB,EAAE,QAAQ;sBAC1C,OAAO,cAAc,EAAE,OAAO;;aAK7B,MAAM;YACN,MAAM;UAEjB;QAAiC,IAAI,EAA1B,KAAK,CAAE,OAAO,CAAC;KAC5B;;;SAIa;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;WAChB;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;;eAC1B,OAAO,+BAA+B,EAAE,UAAU,EAAE;;;;SAqK7D,OAAO,SAAS,EAAE,OAAO;SACzB,OAAO,SAAS,EAAE,QAAQ;eAC1B,MAAM;iBACN,MAAM;sBACN,OAAO;;;;;;;yBAOR,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC"}
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["controller.js"],"names":[],"mappings":"AAyDA;;;GAGG;AACH,qCAHW,MAAM,GACJ,OAAO,SAAS,EAAE,OAAO,CAIrC;AAED;;;;;GAKG;AACH,0BALW,OAAO,SAAS,EAAE,OAAO,OACzB,OAAO,SAAS,EAAE,QAAQ,eAC1B,MAAM,YACN,MAAM,iBAmEhB;;AAmDD;;;;;;;;;;;;;GAaG;AAEH;;GAEG;AAEH;;;;GAIG;AACH,oCAJW,UAAU,gCACV,OAAO,GACL,OAAO,SAAS,EAAE,OAAO,CA4FrC;;aApSa,MAAM;YACN,MAAM;UAEjB;QAAiC,IAAI,EAA1B,KAAK,CAAE,OAAO,CAAC;KAC5B;;;SAIa;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;WAChB;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;;eAC1B,OAAO,+BAA+B,EAAE,UAAU,EAAE;;;;SA0K7D,OAAO,SAAS,EAAE,OAAO;SACzB,OAAO,SAAS,EAAE,QAAQ;eAC1B,MAAM;iBACN,MAAM;sBACN,OAAO;;;;;;sBAGP,OAAO;;;;;;;;;;;;;yBAOR,CAAC,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC"}
package/src/controller.js CHANGED
@@ -1,16 +1,10 @@
1
1
  import { SECTION_STATUS } from './section.js';
2
2
  import questionUtils from './components/utils/question-utils.js';
3
- import { answerObjectForManageListSaving } from '#src/components/manage-list/utils.js';
3
+ import { answerObjectForListItemSaving } from '#src/lib/answer-utils.js';
4
4
  import { booleanToYesNoValue } from '#src/components/boolean/question.js';
5
5
  import { MANAGE_LIST_ACTIONS } from '#src/components/manage-list/manage-list-actions.js';
6
6
  import { toArray } from '#src/lib/utils.js';
7
7
 
8
- /**
9
- * @typedef {import('./journey/journey.js').Journey} Journey
10
- * @typedef {import('./questions/question.js').Question} Question
11
- * @typedef {import('./section.js').Section} Section
12
- */
13
-
14
8
  /**
15
9
  * @typedef {Object} SectionView
16
10
  * @property {string} heading
@@ -77,7 +71,7 @@ export function buildList(viewData = {}) {
77
71
  */
78
72
  export async function list(req, res, pageCaption, viewData) {
79
73
  //render check your answers view
80
- /** @type {Journey} */
74
+ /** @type {import('./journey/journey.js').Journey} */
81
75
  const journey = res.locals.journey;
82
76
  /** @type {import("./journey/journey-response.js").JourneyResponse} */
83
77
  const journeyResponse = res.locals.journeyResponse;
@@ -92,6 +86,9 @@ export async function list(req, res, pageCaption, viewData) {
92
86
  const status = section.getStatus(journeyResponse);
93
87
  const sectionView = buildSectionViewModel(section.name, status);
94
88
 
89
+ // answers for this section may be within an array, for example dynamic sections
90
+ const response = section.getResponse(journeyResponse);
91
+
95
92
  // update completed count
96
93
  if (status === SECTION_STATUS.COMPLETE) {
97
94
  summaryListData.completedSectionCount++;
@@ -104,11 +101,13 @@ export async function list(req, res, pageCaption, viewData) {
104
101
  continue;
105
102
  }
106
103
 
107
- if (!question.shouldDisplay(journeyResponse)) {
104
+ if (!question.shouldDisplay(response)) {
108
105
  continue;
109
106
  }
110
107
 
111
- const answers = journey.response?.answers;
108
+ // answers here may be from an array for dynamic sections
109
+ // this is handled by section.responseForSection above
110
+ const answers = response.answers;
112
111
  let answer = answers[question.fieldName];
113
112
  const conditionalAnswer = questionUtils.getConditionalAnswer(answers, question, answer);
114
113
  if (conditionalAnswer) {
@@ -195,6 +194,9 @@ export async function question(req, res) {
195
194
  * @property {boolean} isManageListItem
196
195
  * @property {string} [manageListQuestionFieldName]
197
196
  * @property {boolean} [manageListItemRemove]
197
+ * @property {boolean} isDynamicSection is this save action for a question within a DynamicSection?
198
+ * @property {string} [dynamicSectionId] if this is within a DynamicSection, what is the section ID (segment)
199
+ * @property {string} [dynamicSectionFieldName] if this is within a DynamicSection, what is the fieldName for the array?
198
200
  * @property {Object<string, any>} data
199
201
  */
200
202
 
@@ -229,6 +231,7 @@ export function buildSave(saveData, redirectToTaskListOnSuccess) {
229
231
  return res.redirect(journey.taskListUrl);
230
232
  }
231
233
  }
234
+ const isDynamicSection = section.isDynamicSection;
232
235
 
233
236
  try {
234
237
  // check for validation errors
@@ -248,6 +251,9 @@ export function buildSave(saveData, redirectToTaskListOnSuccess) {
248
251
  isManageListItem: question.isInManageListSection,
249
252
  manageListQuestionFieldName: manageListQuestion?.fieldName,
250
253
  manageListItemRemove: req.params?.manageListAction === MANAGE_LIST_ACTIONS.REMOVE,
254
+ isDynamicSection,
255
+ dynamicSectionId: isDynamicSection ? section.segment : undefined,
256
+ dynamicSectionFieldName: isDynamicSection ? section.fieldName : undefined,
251
257
  data
252
258
  });
253
259
 
@@ -264,7 +270,9 @@ export function buildSave(saveData, redirectToTaskListOnSuccess) {
264
270
  // as question.shouldDisplay checks the response and is used to determine the next question
265
271
  let answers = journeyResponse.answers;
266
272
  if (question.isInManageListSection) {
267
- answers = answerObjectForManageListSaving(journeyResponse, manageListQuestion, req.params);
273
+ answers = answerObjectForListItemSaving(journeyResponse, manageListQuestion, req.params.manageListItemId);
274
+ } else if (isDynamicSection) {
275
+ answers = answerObjectForListItemSaving(journeyResponse, section, section.segment);
268
276
  }
269
277
  for (const [k, v] of Object.entries(data?.answers || {})) {
270
278
  if (typeof v === 'boolean') {
@@ -3,11 +3,13 @@ import assert from 'node:assert';
3
3
  import { list, question, buildSave } from './controller.js';
4
4
 
5
5
  import { Journey } from './journey/journey.js';
6
- import { SECTION_STATUS } from './section.js';
6
+ import { Section, SECTION_STATUS } from './section.js';
7
7
 
8
8
  import { mockReq, mockRes } from '#test/utils/utils.js';
9
9
  import { BOOLEAN_OPTIONS } from '#src/components/boolean/question.js';
10
10
  import { MANAGE_LIST_ACTIONS } from '#src/components/manage-list/manage-list-actions.js';
11
+ import { DynamicSection } from '#src/dynamic-section.js';
12
+ import { Question } from '#question';
11
13
 
12
14
  const mockBaseUrl = '/manage-appeals/questionnaire';
13
15
  const mockRef = '123456';
@@ -18,148 +20,83 @@ const mockAnswer = 'Not started';
18
20
 
19
21
  function testSetup() {
20
22
  const sections = [
21
- {
22
- name: 'Section 1',
23
- segment: 'segment-1',
24
- getStatus: () => {
25
- return SECTION_STATUS.COMPLETE;
26
- },
27
- isComplete: () => {
28
- return true;
29
- },
30
- questions: [
31
- {
32
- title: 'Title 1a',
33
- question: 'Why?',
34
- taskList: true,
35
- fieldName: 'title-1a',
36
- shouldDisplay: () => true,
37
- formatAnswerForSummary: mock.fn(() => [
38
- {
39
- key: 'Title 1a',
40
- value: mockAnswer,
41
- action: {
42
- href: '/manage-appeals/questionnaire/123456/segment-1/title-1a',
43
- text: 'Answer',
44
- visuallyHiddenText: 'Answer'
45
- }
46
- }
47
- ])
48
- },
49
- {
50
- title: 'Title 1b',
51
- question: 'Who?',
52
- taskList: false,
53
- fieldName: 'title-1b',
54
- shouldDisplay: () => true,
55
- formatAnswerForSummary: mock.fn(() => [
56
- {
57
- key: 'Title 1b',
58
- value: mockAnswer,
59
- action: {
60
- href: '/manage-appeals/questionnaire/123456/segment-1/title-1b',
61
- text: 'Answer',
62
- visuallyHiddenText: 'Answer'
63
- }
64
- }
65
- ])
66
- }
67
- ]
68
- },
69
- {
70
- name: 'Section 2',
71
- segment: 'segment-2',
72
- getStatus: () => {
73
- return SECTION_STATUS.IN_PROGRESS;
74
- },
75
- isComplete: () => {
76
- return true;
23
+ new Section('Section 1', 'segment-1'),
24
+ new Section('Section 2', 'segment-2'),
25
+ new DynamicSection('Section 3', 'segment-3', 'arrayOfAnswers')
26
+ ];
27
+ /**
28
+ * Create a new question and set taskList if needed
29
+ * Also mocks formatAnswerForSummary so it can be overridden later.
30
+ * Default mock uses the original implementation
31
+ *
32
+ * @param {import('#typedefs/question-types.d.ts').QuestionParameters} params
33
+ * @param {boolean} [taskList]
34
+ * @returns {Question}
35
+ */
36
+ const newQuestion = (params, taskList) => {
37
+ const q = new Question(params);
38
+ if (typeof taskList === 'boolean') {
39
+ q.taskList = taskList;
40
+ }
41
+ mock.method(q, 'formatAnswerForSummary');
42
+ return q;
43
+ };
44
+ // mock/override what we need
45
+ mock.method(sections[0], 'getStatus', () => SECTION_STATUS.COMPLETE);
46
+ mock.method(sections[0], 'isComplete', () => true);
47
+ sections[0].questions = [
48
+ newQuestion({
49
+ title: 'Title 1a',
50
+ question: 'Why?',
51
+ fieldName: 'title-1a',
52
+ viewFolder: 'mock'
53
+ }),
54
+ newQuestion(
55
+ {
56
+ title: 'Title 1b',
57
+ question: 'Who?',
58
+ fieldName: 'title-1b',
59
+ viewFolder: 'mock'
77
60
  },
78
- questions: [
79
- {
80
- title: 'Title 2a',
81
- question: 'How?',
82
- taskList: true,
83
- fieldName: 'title-2a',
84
- shouldDisplay: () => true,
85
- formatAnswerForSummary: () => [
86
- {
87
- key: 'Title 2a',
88
- value: mockAnswer,
89
- action: {
90
- href: '/manage-appeals/questionnaire/123456/segment-2/title-2a',
91
- text: 'Answer',
92
- visuallyHiddenText: 'Answer'
93
- }
94
- }
95
- ]
96
- },
97
- {
98
- title: 'Title 2b',
99
- question: 'What?',
100
- taskList: true,
101
- fieldName: 'title-2b',
102
- shouldDisplay: () => true,
103
- formatAnswerForSummary: () => [
104
- {
105
- key: 'Title 2b',
106
- value: mockAnswer,
107
- action: {
108
- href: '/manage-appeals/questionnaire/123456/segment-2/title-2b',
109
- text: 'Answer',
110
- visuallyHiddenText: 'Answer'
111
- }
112
- }
113
- ]
114
- }
115
- ]
116
- },
117
- {
118
- name: 'Section 3',
119
- segment: 'segment-3',
120
- getStatus: () => {
121
- return SECTION_STATUS.NOT_STARTED;
61
+ false
62
+ )
63
+ ];
64
+
65
+ mock.method(sections[1], 'getStatus', () => SECTION_STATUS.IN_PROGRESS);
66
+ mock.method(sections[1], 'isComplete', () => true);
67
+ sections[1].questions = [
68
+ newQuestion({
69
+ title: 'Title 2a',
70
+ question: 'How?',
71
+ fieldName: 'title-2a',
72
+ viewFolder: 'mock'
73
+ }),
74
+ newQuestion({
75
+ title: 'Title 2b',
76
+ question: 'What?',
77
+ fieldName: 'title-2b',
78
+ viewFolder: 'mock'
79
+ })
80
+ ];
81
+
82
+ mock.method(sections[2], 'getStatus', () => SECTION_STATUS.NOT_STARTED);
83
+ mock.method(sections[2], 'isComplete', () => false);
84
+ sections[2].questions = [
85
+ newQuestion(
86
+ {
87
+ title: 'Title 3a',
88
+ question: 'When?',
89
+ fieldName: 'title-3a',
90
+ viewFolder: 'mock'
122
91
  },
123
- isComplete: mock.fn(),
124
- questions: [
125
- {
126
- title: 'Title 3a',
127
- question: 'When?',
128
- taskList: false,
129
- fieldName: 'title-3a',
130
- shouldDisplay: () => true,
131
- formatAnswerForSummary: () => [
132
- {
133
- key: 'Title 3a',
134
- value: mockAnswer,
135
- action: {
136
- href: '/manage-appeals/questionnaire/123456/segment-3/title-3a',
137
- text: 'Answer',
138
- visuallyHiddenText: 'Answer'
139
- }
140
- }
141
- ]
142
- },
143
- {
144
- title: 'Title 3b',
145
- question: 'Really?',
146
- taskList: true,
147
- fieldName: 'title-3b',
148
- shouldDisplay: () => true,
149
- formatAnswerForSummary: () => [
150
- {
151
- key: 'Title 3b',
152
- value: mockAnswer,
153
- action: {
154
- href: '/manage-appeals/questionnaire/123456/segment-3/title-3b',
155
- text: 'Answer',
156
- visuallyHiddenText: 'Answer'
157
- }
158
- }
159
- ]
160
- }
161
- ]
162
- }
92
+ false
93
+ ),
94
+ newQuestion({
95
+ title: 'Title 3b',
96
+ question: 'Really?',
97
+ fieldName: 'title-3b',
98
+ viewFolder: 'mock'
99
+ })
163
100
  ];
164
101
 
165
102
  const journeyParams = {
@@ -186,7 +123,7 @@ function testSetup() {
186
123
 
187
124
  const mockJourney = new Journey({ response: mockResponse, ...journeyParams });
188
125
 
189
- res.locals.journeyResponse = {};
126
+ res.locals.journeyResponse = mockResponse;
190
127
  res.locals.journey = mockJourney;
191
128
 
192
129
  const sampleQuestionObj = {
@@ -238,12 +175,12 @@ describe('dynamic-form/controller', () => {
238
175
  mockJourney.sections[0].questions[0].formatAnswerForSummary.mock.mockImplementation(() => [
239
176
  {
240
177
  key: 'Title 1a',
241
- value: mockAnswer,
178
+ value: 'Yes',
242
179
  action: [
243
180
  {
244
181
  href: '/manage-appeals/questionnaire/123456/segment-1/title-1a',
245
- text: 'Answer',
246
- visuallyHiddenText: 'Answer'
182
+ text: 'Change',
183
+ visuallyHiddenText: 'Why?'
247
184
  },
248
185
  {
249
186
  href: '/manage-appeals/questionnaire/123456/segment-1/title-1a/edit',
@@ -344,6 +281,35 @@ describe('dynamic-form/controller', () => {
344
281
  visuallyHiddenText: 'Answer'
345
282
  });
346
283
  });
284
+
285
+ it('should render dynamic sections answers', async () => {
286
+ const { req, res, mockJourney } = testSetup();
287
+ const dynamicSection = mockJourney.sections[2];
288
+
289
+ // show all questions
290
+ dynamicSection.questions[0].taskList = true;
291
+ mockJourney.response.answers = {
292
+ arrayOfAnswers: [
293
+ { id: 'id-1', 'title-3a': 'First answer', 'title-3b': 'Second answer' },
294
+ // should match these answers - based on section segment
295
+ { id: 'segment-3', 'title-3a': 'Another answer', 'title-3b': 'A fourth answer' }
296
+ ]
297
+ };
298
+
299
+ await list(req, res, '', {});
300
+
301
+ const mockFn = res.render.mock;
302
+ assert.strictEqual(mockFn.callCount(), 1);
303
+ const args = mockFn.calls[0].arguments[1];
304
+ assert.ok(args.summaryListData);
305
+ const section = args.summaryListData.sections[2];
306
+ assert.strictEqual(section.heading, 'Section 3');
307
+ const rows = section.list.rows;
308
+ assert.strictEqual(rows.length, 2);
309
+ // answers extracted from the array
310
+ assert.strictEqual(rows[0].value.html, 'Another answer');
311
+ assert.strictEqual(rows[1].value.html, 'A fourth answer');
312
+ });
347
313
  });
348
314
 
349
315
  describe('question', () => {
@@ -562,6 +528,9 @@ describe('dynamic-form/controller', () => {
562
528
  isManageListItem: undefined,
563
529
  manageListItemRemove: false,
564
530
  manageListQuestionFieldName: undefined,
531
+ isDynamicSection: false,
532
+ dynamicSectionId: undefined,
533
+ dynamicSectionFieldName: undefined,
565
534
  data: undefined
566
535
  }
567
536
  ]);
@@ -623,7 +592,10 @@ describe('dynamic-form/controller', () => {
623
592
  data: undefined,
624
593
  isManageListItem: undefined,
625
594
  manageListItemRemove: false,
626
- manageListQuestionFieldName: undefined
595
+ manageListQuestionFieldName: undefined,
596
+ isDynamicSection: false,
597
+ dynamicSectionId: undefined,
598
+ dynamicSectionFieldName: undefined
627
599
  }
628
600
  ],
629
601
  'save data args'
@@ -678,7 +650,7 @@ describe('dynamic-form/controller', () => {
678
650
  };
679
651
  sampleQuestionObjWithActions.getDataToSave.mock.mockImplementationOnce(() => ({
680
652
  answers: {
681
- [sampleQuestionObj.fieldName]: 'my-answer',
653
+ [sampleQuestionObjWithActions.fieldName]: 'my-answer',
682
654
  aBooleanToTest: true
683
655
  }
684
656
  }));
@@ -749,6 +721,63 @@ describe('dynamic-form/controller', () => {
749
721
  assert.strictEqual(Object.keys(res.locals.journeyResponse.answers).length, 0);
750
722
  });
751
723
 
724
+ it('should call saveData with dynamic section properties', async () => {
725
+ const { req, res, mockJourney, sampleQuestionObj, journeyParams } = testSetup();
726
+ const journeyId = 'has-questionnaire';
727
+ const dynamicSection = mockJourney.sections[2];
728
+
729
+ sampleQuestionObj.getDataToSave.mock.mockImplementationOnce(() => ({
730
+ answers: {
731
+ [sampleQuestionObj.fieldName]: 'my-answer'
732
+ }
733
+ }));
734
+
735
+ req.params = {
736
+ referenceId: mockRef,
737
+ section: dynamicSection.segment,
738
+ question: dynamicSection.questions[0].fieldName
739
+ };
740
+
741
+ res.locals.journeyResponse = {
742
+ ...journeyParams,
743
+ answers: {}
744
+ };
745
+
746
+ req.body = {
747
+ sampleFieldName: true,
748
+ sampleFieldName_sub: 'send this',
749
+ notSampleFieldName: 'do not send this'
750
+ };
751
+
752
+ mockJourney.getQuestionByParams = mock.fn();
753
+ mockJourney.getQuestionByParams.mock.mockImplementationOnce(() => sampleQuestionObj);
754
+
755
+ const saveData = mock.fn();
756
+ await buildSave(saveData)(req, res, journeyId);
757
+
758
+ assert.strictEqual(sampleQuestionObj.getDataToSave.mock.callCount(), 1);
759
+ assert.strictEqual(saveData.mock.callCount(), 1);
760
+ assert.deepStrictEqual(saveData.mock.calls[0].arguments, [
761
+ {
762
+ req,
763
+ res,
764
+ journeyId: journeyParams.journeyId,
765
+ referenceId: journeyParams.referenceId,
766
+ isManageListItem: undefined,
767
+ manageListItemRemove: false,
768
+ manageListQuestionFieldName: undefined,
769
+ isDynamicSection: true,
770
+ dynamicSectionId: 'segment-3',
771
+ dynamicSectionFieldName: 'arrayOfAnswers',
772
+ data: { answers: { [sampleQuestionObj.fieldName]: 'my-answer' } }
773
+ }
774
+ ]);
775
+ // journey response should be edited if redirecting to next question
776
+ assert.deepStrictEqual(res.locals.journeyResponse.answers?.arrayOfAnswers, [
777
+ { id: 'segment-3', [sampleQuestionObj.fieldName]: 'my-answer' }
778
+ ]);
779
+ });
780
+
752
781
  describe('manageListQuestions', () => {
753
782
  const setupJourney = () => {
754
783
  const q = {
@@ -834,13 +863,13 @@ const _getmockSummaryListData = (mockJourney) => {
834
863
  items: [
835
864
  {
836
865
  href: `${mockBaseUrl}/${mockRef}/${mockJourney.sections[0].segment}/${mockJourney.sections[0].questions[0].fieldName}`,
837
- text: 'Answer',
838
- visuallyHiddenText: 'Answer'
866
+ text: 'Change',
867
+ visuallyHiddenText: mockJourney.sections[0].questions[0].question
839
868
  }
840
869
  ]
841
870
  },
842
871
  key: { text: mockJourney.sections[0].questions[0].title },
843
- value: { html: 'Not started' }
872
+ value: { html: 'Yes' }
844
873
  }
845
874
  ]
846
875
  }
@@ -856,7 +885,7 @@ const _getmockSummaryListData = (mockJourney) => {
856
885
  {
857
886
  href: `${mockBaseUrl}/${mockRef}/${mockJourney.sections[1].segment}/${mockJourney.sections[1].questions[0].fieldName}`,
858
887
  text: 'Answer',
859
- visuallyHiddenText: 'Answer'
888
+ visuallyHiddenText: mockJourney.sections[1].questions[0].question
860
889
  }
861
890
  ]
862
891
  },
@@ -869,7 +898,7 @@ const _getmockSummaryListData = (mockJourney) => {
869
898
  {
870
899
  href: `${mockBaseUrl}/${mockRef}/${mockJourney.sections[1].segment}/${mockJourney.sections[1].questions[1].fieldName}`,
871
900
  text: 'Answer',
872
- visuallyHiddenText: 'Answer'
901
+ visuallyHiddenText: mockJourney.sections[1].questions[1].question
873
902
  }
874
903
  ]
875
904
  },
@@ -890,7 +919,7 @@ const _getmockSummaryListData = (mockJourney) => {
890
919
  {
891
920
  href: `${mockBaseUrl}/${mockRef}/${mockJourney.sections[2].segment}/${mockJourney.sections[2].questions[1].fieldName}`,
892
921
  text: 'Answer',
893
- visuallyHiddenText: 'Answer'
922
+ visuallyHiddenText: mockJourney.sections[2].questions[1].question
894
923
  }
895
924
  ]
896
925
  },
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Use a dynamic section for sections which are generated from an array of objects
3
+ *
4
+ * This can be useful for adding a section for each entry in a ManageList, or each row in a database
5
+ * The controller logic, when encountering a DynamicSection, will check for an answer object within
6
+ * `journeyResponse.answers[fieldName]`, matching by the `id` property
7
+ *
8
+ * ```
9
+ * answers = {
10
+ * myField: 'some answer',
11
+ * list: [
12
+ * { id: 'person-id-1', name: 'Grogu' },
13
+ * { id: 'person-id-2', name: 'Mando' }
14
+ * ]
15
+ * }
16
+ * ```
17
+ *
18
+ * In this example, create a DynamicSection for each list entry:
19
+ *
20
+ * ```
21
+ * function createPersonSection(person) {
22
+ * return new DynamicSection(person.name, person.id, 'list')
23
+ * .addQuestion(questions.name)
24
+ * // ... add questions
25
+ * }
26
+ * ```
27
+ *
28
+ * When using with buildSave, be sure to handle the parameters: `isDynamicSection`, `dynamicSectionId`,`dynamicSectionFieldName`
29
+ */
30
+ export class DynamicSection extends Section {
31
+ /**
32
+ * @param name
33
+ * @param segment
34
+ * @param fieldName the key used in the answers object, which will be an array of answers for this section
35
+ */
36
+ constructor(name: any, segment: any, fieldName: any);
37
+ fieldName: any;
38
+ }
39
+ import { Section } from '#section';
40
+ //# sourceMappingURL=dynamic-section.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dynamic-section.d.ts","sourceRoot":"","sources":["dynamic-section.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH;IACC;;;;OAIG;IACH,qDAIC;IADA,eAA0B;CAU3B;wBAlDuB,UAAU"}
@@ -0,0 +1,51 @@
1
+ import { Section } from '#section';
2
+
3
+ /**
4
+ * Use a dynamic section for sections which are generated from an array of objects
5
+ *
6
+ * This can be useful for adding a section for each entry in a ManageList, or each row in a database
7
+ * The controller logic, when encountering a DynamicSection, will check for an answer object within
8
+ * `journeyResponse.answers[fieldName]`, matching by the `id` property
9
+ *
10
+ * ```
11
+ * answers = {
12
+ * myField: 'some answer',
13
+ * list: [
14
+ * { id: 'person-id-1', name: 'Grogu' },
15
+ * { id: 'person-id-2', name: 'Mando' }
16
+ * ]
17
+ * }
18
+ * ```
19
+ *
20
+ * In this example, create a DynamicSection for each list entry:
21
+ *
22
+ * ```
23
+ * function createPersonSection(person) {
24
+ * return new DynamicSection(person.name, person.id, 'list')
25
+ * .addQuestion(questions.name)
26
+ * // ... add questions
27
+ * }
28
+ * ```
29
+ *
30
+ * When using with buildSave, be sure to handle the parameters: `isDynamicSection`, `dynamicSectionId`,`dynamicSectionFieldName`
31
+ */
32
+ export class DynamicSection extends Section {
33
+ /**
34
+ * @param name
35
+ * @param segment
36
+ * @param fieldName the key used in the answers object, which will be an array of answers for this section
37
+ */
38
+ constructor(name, segment, fieldName) {
39
+ super(name, segment);
40
+
41
+ this.fieldName = fieldName;
42
+ }
43
+
44
+ /**
45
+ * Indicate that this section is dynamic, and answers should be pulled from an array
46
+ * @returns {boolean}
47
+ */
48
+ get isDynamicSection() {
49
+ return true;
50
+ }
51
+ }