@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
@@ -0,0 +1,113 @@
1
+ import { describe, it, mock } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { DynamicSection } from '#src/dynamic-section.js';
4
+ import { SECTION_STATUS } from '#section';
5
+ import { Question } from '#question';
6
+
7
+ // use a real question to avoid mocking lots of logic
8
+ const mockQuestion = (fieldName = 'fieldName', required = true) =>
9
+ new Question({
10
+ fieldName: fieldName,
11
+ title: 'question',
12
+ question: 'question',
13
+ viewFolder: 'folder',
14
+ validators: [
15
+ {
16
+ isRequired() {
17
+ return required;
18
+ }
19
+ }
20
+ ]
21
+ });
22
+
23
+ describe('dynamic-section', () => {
24
+ describe('responseForSection', () => {
25
+ it('should return inner object for dynamic section', () => {
26
+ const section = new DynamicSection('s1', 'id-1', 'myList');
27
+ const answers = { id: 'id-1', myField: 'answer' };
28
+ const res = {
29
+ answers: {
30
+ myList: [{ id: 'id-2' }, { id: 'id-3' }, answers]
31
+ }
32
+ };
33
+ const got = section.getResponse(res);
34
+ assert.strictEqual(got?.answers, answers);
35
+ });
36
+ });
37
+
38
+ describe('getStatus', () => {
39
+ it('should use section answers for status', () => {
40
+ const s1Answers = { id: 'S', fieldOne: 'answer' };
41
+ const mockJourneyResponse = {
42
+ answers: {
43
+ visitFrequently: 'Answer 1',
44
+ anotherFieldName: 'Answer 2',
45
+ myList: [s1Answers]
46
+ }
47
+ };
48
+
49
+ const requiredQuestion = mockQuestion('visitFrequently');
50
+
51
+ const section = new DynamicSection('s1', 'S', 'myList');
52
+ section.addQuestion(requiredQuestion);
53
+
54
+ mock.method(requiredQuestion, 'shouldDisplay');
55
+ mock.method(requiredQuestion, 'isAnswered');
56
+
57
+ section.getStatus(mockJourneyResponse);
58
+
59
+ assert.strictEqual(requiredQuestion.shouldDisplay.mock.callCount(), 1);
60
+ assert.strictEqual(requiredQuestion.isAnswered.mock.callCount(), 2);
61
+ assert.strictEqual(requiredQuestion.shouldDisplay.mock.calls[0].arguments[0].answers, s1Answers);
62
+ assert.strictEqual(requiredQuestion.isAnswered.mock.calls[0].arguments[0].answers, s1Answers);
63
+ });
64
+
65
+ it('should return NOT_STARTED when no answers are given', () => {
66
+ const mockJourneyResponse = {
67
+ answers: {}
68
+ };
69
+ const section = new DynamicSection('s1', 'S', 'myList');
70
+ section.addQuestion(mockQuestion());
71
+ const result = section.getStatus(mockJourneyResponse);
72
+ assert.strictEqual(result, SECTION_STATUS.NOT_STARTED);
73
+ const isComplete = section.isComplete(mockJourneyResponse);
74
+ assert.strictEqual(isComplete, false);
75
+ });
76
+
77
+ it('should return IN_PROGRESS when at least one answer is given', () => {
78
+ const mockJourneyResponse = {
79
+ answers: {
80
+ myList: [{ id: 'S', visitFrequently: 'Answer 1' }]
81
+ }
82
+ };
83
+
84
+ const section = new DynamicSection('s1', 'S', 'myList');
85
+ section.addQuestion(mockQuestion());
86
+ section.addQuestion(mockQuestion('visitFrequently'));
87
+ const result = section.getStatus(mockJourneyResponse);
88
+ assert.strictEqual(result, SECTION_STATUS.IN_PROGRESS);
89
+ const isComplete = section.isComplete(mockJourneyResponse);
90
+ assert.strictEqual(isComplete, false);
91
+ });
92
+
93
+ it('should return COMPLETE when all required answers are given', () => {
94
+ const s1Answers = { id: 'S', visitFrequently: 'answer', anotherFieldName: true };
95
+ const mockJourneyResponse = {
96
+ answers: {
97
+ myList: [s1Answers]
98
+ }
99
+ };
100
+
101
+ const section = new DynamicSection('s1', 'S', 'myList');
102
+ section.addQuestion(mockQuestion('visitFrequently'));
103
+ section.addQuestion(mockQuestion('anotherFieldName'));
104
+ section.addQuestion(mockQuestion('someQuestion', false));
105
+
106
+ const result = section.getStatus(mockJourneyResponse);
107
+ assert.strictEqual(result, SECTION_STATUS.COMPLETE);
108
+
109
+ const isComplete = section.isComplete(mockJourneyResponse);
110
+ assert.strictEqual(isComplete, true);
111
+ });
112
+ });
113
+ });
package/src/index.d.ts CHANGED
@@ -32,6 +32,8 @@ export * from "./lib/utils.js";
32
32
  export * from "./middleware/build-get-journey.js";
33
33
  export * from "./middleware/redirect-to-unanswered-question.js";
34
34
  export * from "./questions/options-question.js";
35
+ export * from "./dynamic-section.js";
36
+ export * from "./section.js";
35
37
  export * from "./validator/address-validator.js";
36
38
  export * from "./validator/base-validator.js";
37
39
  export * from "./validator/conditional-required-validator.js";
@@ -58,6 +60,5 @@ export { JourneyResponse } from "./journey/journey-response.js";
58
60
  export { Question } from "./questions/question.js";
59
61
  export { createQuestions } from "./questions/create-questions.js";
60
62
  export { questionClasses } from "./questions/questions.js";
61
- export { Section } from "./section.js";
62
63
  export { default as validate } from "./validator/validator.js";
63
64
  //# sourceMappingURL=index.d.ts.map
package/src/index.js CHANGED
@@ -55,7 +55,8 @@ export * from './questions/options-question.js';
55
55
  export { questionClasses } from './questions/questions.js';
56
56
 
57
57
  // Section
58
- export { Section } from './section.js';
58
+ export * from './dynamic-section.js';
59
+ export * from './section.js';
59
60
 
60
61
  // Validators
61
62
  export * from './validator/address-validator.js';
package/src/index.test.js CHANGED
@@ -52,6 +52,7 @@ describe('public API', () => {
52
52
  assert.equal(typeof api.Question, 'function');
53
53
  assert.equal(typeof api.createQuestions, 'function');
54
54
  assert.equal(typeof api.questionClasses, 'object');
55
+ assert.equal(typeof api.DynamicSection, 'function');
55
56
  assert.equal(typeof api.Section, 'function');
56
57
  assert.equal(typeof api.Journey, 'function');
57
58
  assert.equal(typeof api.JourneyResponse, 'function');
@@ -84,5 +85,7 @@ describe('public API', () => {
84
85
  assert.equal(typeof api.booleanToYesNoOrNull, 'function');
85
86
  assert.equal(typeof api.REDACT_CHARACTER, 'string');
86
87
  assert.equal(typeof api.TRUNCATED_MAX_LENGTH, 'number');
88
+ assert.equal(typeof api.END_OF_SECTION, 'symbol');
89
+ assert.equal(typeof api.SECTION_STATUS, 'object');
87
90
  });
88
91
  });
@@ -60,6 +60,16 @@ export class Journey {
60
60
  * @returns {import('../section.js').Section | undefined}
61
61
  */
62
62
  getSection(sectionSegment: string): import("../section.js").Section | undefined;
63
+ /**
64
+ * Get the response for the given section, required to support dynamic sections
65
+ *
66
+ * Often used in formatAnswerForSummary to get other answer fields.
67
+ *
68
+ * @param {string} sectionSegment
69
+ */
70
+ responseForSection(sectionSegment: string): import("./journey-response.js").JourneyResponse<import("#typedefs/journey-types.d.ts").JourneyAnswers> | {
71
+ answers: Record<string, unknown>;
72
+ };
63
73
  /**
64
74
  * gets a question from the object's sections based on a section + question names
65
75
  * @param {import('#typedefs/journey-types.d.ts').RouteParams} params
@@ -1 +1 @@
1
- {"version":3,"file":"journey.d.ts","sourceRoot":"","sources":["journey.js"],"names":[],"mappings":"AAQA;;;GAGG;AACH;IAwBC;;;;;;;;;;;;;;OAcG;IACH,qLAZG;QAAwB,SAAS,EAAzB,MAAM;QACyE,WAAW,EAA1F,CAAC,QAAQ,EAAE,OAAO,uBAAuB,EAAE,eAAe,KAAK,MAAM;QACpD,WAAW;QAC6B,QAAQ,EAAjE,OAAO,uBAAuB,EAAE,eAAe;QAC/B,eAAe,EAA/B,MAAM;QACU,gBAAgB,EAAhC,MAAM;QACW,uBAAuB;QACxB,YAAY,EAA5B,MAAM;QACY,eAAe;QACU,QAAQ,EAAnD,OAAO,eAAe,EAAE,OAAO,EAAE;QAChB,eAAe;KAC1C,EAoDA;IAzFD,8EAA8E;IAC9E,WADW,MAAM,CACP;IACV,6FAA6F;IAC7F,UADW,KAAK,CAAE,OAAO,eAAe,EAAE,OAAO,CAAC,CACpC;IACd,mHAAmH;IACnH,UADW,OAAO,uBAAuB,EAAE,eAAe,CACjD;IACT,wFAAwF;IACxF,SADW,MAAM,CACJ;IACb,sJAAsJ;IACtJ,aADW,CAAC,eAAe,EAAE,OAAO,uBAAuB,EAAE,eAAe,KAAK,MAAM,CAChE;IACvB,kEAAkE;IAClE,aADW,MAAM,CACA;IACjB,uEAAuE;IACvE,iBADW,MAAM,CACI;IACrB,qFAAqF;IACrF,kBADW,MAAM,CACK;IACtB,4GAA4G;IAC5G,yBADW,MAAM,CACY;IAC7B,qFAAqF;IACrF,iBADW,OAAO,CACM;IACxB,2DAA2D;IAC3D,cADU,MAAM,CACH;IAmEZ,+BAA8C;IA2C/C;;;;OAIG;IACH,2BAHW,MAAM,GACJ,OAAO,eAAe,EAAE,OAAO,GAAG,SAAS,CAMvD;IA8BD;;;;OAIG;IACH,4BAHW,OAAO,8BAA8B,EAAE,WAAW,GAChD,OAAO,0BAA0B,EAAE,QAAQ,GAAG,SAAS,CAiBnE;IAED;;;;;;;OAOG;IACH,4CAJG;QAAoE,MAAM,EAAlE,OAAO,8BAA8B,EAAE,WAAW;QACU,kBAAkB;KACtF,GAAU,MAAM,GAAC,IAAI,CAWvB;IAED;;;;;;;;OAQG;IACH,4BALW,OAAO,SAAS,EAAE,QAAQ,UAC1B,OAAO,8BAA8B,EAAE,WAAW,kFAEhD,IAAI,CAKhB;IAED;;;;;;;;OAQG;IACH,2BANW,OAAO,8BAA8B,EAAE,WAAW,oCAE1D;QAA0B,OAAO;QACmC,kBAAkB;KACtF,GAAU,MAAM,GAAC,IAAI,CAyEvB;IAED;;;;;OAKG;IACH,wBAAyB,gBAJd,MAI4B,EAAE,iBAH9B,MAG6C,KAF3C,MAAM,CAqBjB;IAEF;;;;OAIG;IACH,sCAAuC,iBAH5B,MAG2C,KAFzC,MAAM,CAIjB;IAEF;;;;;;OAMG;IACH,0BAA2B,gBALhB,MAK8B,EAAE,iBAJhC,MAI+C,EAAE,UAHjD,MAGyD,KAFvD,MAAM,CAuBjB;IAEF;;;OAGG;IACH,cAFa,OAAO,CAInB;IAED;;OAEG;IACH,6BAFW,OAAO,uBAAuB,EAAE,eAAe,QAKzD;;CACD"}
1
+ {"version":3,"file":"journey.d.ts","sourceRoot":"","sources":["journey.js"],"names":[],"mappings":"AAQA;;;GAGG;AACH;IAwBC;;;;;;;;;;;;;;OAcG;IACH,qLAZG;QAAwB,SAAS,EAAzB,MAAM;QACyE,WAAW,EAA1F,CAAC,QAAQ,EAAE,OAAO,uBAAuB,EAAE,eAAe,KAAK,MAAM;QACpD,WAAW;QAC6B,QAAQ,EAAjE,OAAO,uBAAuB,EAAE,eAAe;QAC/B,eAAe,EAA/B,MAAM;QACU,gBAAgB,EAAhC,MAAM;QACW,uBAAuB;QACxB,YAAY,EAA5B,MAAM;QACY,eAAe;QACU,QAAQ,EAAnD,OAAO,eAAe,EAAE,OAAO,EAAE;QAChB,eAAe;KAC1C,EAoDA;IAzFD,8EAA8E;IAC9E,WADW,MAAM,CACP;IACV,6FAA6F;IAC7F,UADW,KAAK,CAAE,OAAO,eAAe,EAAE,OAAO,CAAC,CACpC;IACd,mHAAmH;IACnH,UADW,OAAO,uBAAuB,EAAE,eAAe,CACjD;IACT,wFAAwF;IACxF,SADW,MAAM,CACJ;IACb,sJAAsJ;IACtJ,aADW,CAAC,eAAe,EAAE,OAAO,uBAAuB,EAAE,eAAe,KAAK,MAAM,CAChE;IACvB,kEAAkE;IAClE,aADW,MAAM,CACA;IACjB,uEAAuE;IACvE,iBADW,MAAM,CACI;IACrB,qFAAqF;IACrF,kBADW,MAAM,CACK;IACtB,4GAA4G;IAC5G,yBADW,MAAM,CACY;IAC7B,qFAAqF;IACrF,iBADW,OAAO,CACM;IACxB,2DAA2D;IAC3D,cADU,MAAM,CACH;IAmEZ,+BAA8C;IA2C/C;;;;OAIG;IACH,2BAHW,MAAM,GACJ,OAAO,eAAe,EAAE,OAAO,GAAG,SAAS,CAMvD;IAED;;;;;;OAMG;IACH,mCAFW,MAAM;;MAQhB;IA8BD;;;;OAIG;IACH,4BAHW,OAAO,8BAA8B,EAAE,WAAW,GAChD,OAAO,0BAA0B,EAAE,QAAQ,GAAG,SAAS,CAiBnE;IAED;;;;;;;OAOG;IACH,4CAJG;QAAoE,MAAM,EAAlE,OAAO,8BAA8B,EAAE,WAAW;QACU,kBAAkB;KACtF,GAAU,MAAM,GAAC,IAAI,CAWvB;IAED;;;;;;;;OAQG;IACH,4BALW,OAAO,SAAS,EAAE,QAAQ,UAC1B,OAAO,8BAA8B,EAAE,WAAW,kFAEhD,IAAI,CAKhB;IAED;;;;;;;;OAQG;IACH,2BANW,OAAO,8BAA8B,EAAE,WAAW,oCAE1D;QAA0B,OAAO;QACmC,kBAAkB;KACtF,GAAU,MAAM,GAAC,IAAI,CAyEvB;IAED;;;;;OAKG;IACH,wBAAyB,gBAJd,MAI4B,EAAE,iBAH9B,MAG6C,KAF3C,MAAM,CAqBjB;IAEF;;;;OAIG;IACH,sCAAuC,iBAH5B,MAG2C,KAFzC,MAAM,CAIjB;IAEF;;;;;;OAMG;IACH,0BAA2B,gBALhB,MAK8B,EAAE,iBAJhC,MAI+C,EAAE,UAHjD,MAGyD,KAFvD,MAAM,CAuBjB;IAEF;;;OAGG;IACH,cAFa,OAAO,CAInB;IAED;;OAEG;IACH,6BAFW,OAAO,uBAAuB,EAAE,eAAe,QAKzD;;CACD"}
@@ -153,6 +153,21 @@ export class Journey {
153
153
  });
154
154
  }
155
155
 
156
+ /**
157
+ * Get the response for the given section, required to support dynamic sections
158
+ *
159
+ * Often used in formatAnswerForSummary to get other answer fields.
160
+ *
161
+ * @param {string} sectionSegment
162
+ */
163
+ responseForSection(sectionSegment) {
164
+ const section = this.getSection(sectionSegment);
165
+ if (!section) {
166
+ throw new Error(`No section found for section segment: '${sectionSegment}'`);
167
+ }
168
+ return section.getResponse(this.response);
169
+ }
170
+
156
171
  /**
157
172
  * Get question within a section
158
173
  * @param {import('../section.js').Section} section
@@ -0,0 +1,22 @@
1
+ /**
2
+ *
3
+ * @param {import('#journey-response').JourneyResponse} response
4
+ * @param {{fieldName: string}} withFieldName
5
+ * @param {string} itemId
6
+ * @returns {Record<string, unknown>}
7
+ */
8
+ export function answerObjectForListItem(response: import("#journey-response").JourneyResponse, withFieldName: {
9
+ fieldName: string;
10
+ }, itemId: string): Record<string, unknown>;
11
+ /**
12
+ * Similar to answerObjectForListItem but will edit response and add a new array entry if not found
13
+ *
14
+ * @param {import('#journey-response').JourneyResponse} response
15
+ * @param {{fieldName: string}} withFieldName
16
+ * @param {string} itemId
17
+ * @returns {import('#typedefs/journey-types.d.ts').ManageListAnswers}
18
+ */
19
+ export function answerObjectForListItemSaving(response: import("#journey-response").JourneyResponse, withFieldName: {
20
+ fieldName: string;
21
+ }, itemId: string): import("#typedefs/journey-types.d.ts").ManageListAnswers;
22
+ //# sourceMappingURL=answer-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"answer-utils.d.ts","sourceRoot":"","sources":["answer-utils.js"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,kDALW,OAAO,mBAAmB,EAAE,eAAe,iBAC3C;IAAC,SAAS,EAAE,MAAM,CAAA;CAAC,UACnB,MAAM,GACJ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQnC;AAED;;;;;;;GAOG;AACH,wDALW,OAAO,mBAAmB,EAAE,eAAe,iBAC3C;IAAC,SAAS,EAAE,MAAM,CAAA;CAAC,UACnB,MAAM,GACJ,OAAO,8BAA8B,EAAE,iBAAiB,CAYpE"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ *
3
+ * @param {import('#journey-response').JourneyResponse} response
4
+ * @param {{fieldName: string}} withFieldName
5
+ * @param {string} itemId
6
+ * @returns {Record<string, unknown>}
7
+ */
8
+ export function answerObjectForListItem(response, withFieldName, itemId) {
9
+ const answers = response.answers[withFieldName.fieldName];
10
+ if (!Array.isArray(answers)) {
11
+ return {};
12
+ }
13
+ return answers.find((a) => a.id === itemId) || {};
14
+ }
15
+
16
+ /**
17
+ * Similar to answerObjectForListItem but will edit response and add a new array entry if not found
18
+ *
19
+ * @param {import('#journey-response').JourneyResponse} response
20
+ * @param {{fieldName: string}} withFieldName
21
+ * @param {string} itemId
22
+ * @returns {import('#typedefs/journey-types.d.ts').ManageListAnswers}
23
+ */
24
+ export function answerObjectForListItemSaving(response, withFieldName, itemId) {
25
+ const answersList = response.answers[withFieldName?.fieldName] || (response.answers[withFieldName?.fieldName] = []);
26
+ let answers = answersList.find((item) => item.id === itemId);
27
+ if (!answers) {
28
+ answers = {
29
+ id: itemId
30
+ };
31
+ answersList.push(answers);
32
+ }
33
+ return answers;
34
+ }
@@ -1,9 +1,9 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert';
3
- import { answerObjectForManageList, answerObjectForManageListSaving } from '#src/components/manage-list/utils.js';
3
+ import { answerObjectForListItem, answerObjectForListItemSaving } from '#src/lib/answer-utils.js';
4
4
 
5
- describe('manage-list-utils', () => {
6
- describe('answerObjectForManageList', () => {
5
+ describe('answer-utils', () => {
6
+ describe('answerObjectForListItem', () => {
7
7
  it('should return the manage list item object for manage list questions', () => {
8
8
  const manageListQuestion = {
9
9
  fieldName: 'manageListQuestion'
@@ -18,7 +18,7 @@ describe('manage-list-utils', () => {
18
18
  }
19
19
  };
20
20
  const manageListItemId = '1';
21
- const got = answerObjectForManageList(journeyResponse, manageListQuestion, manageListItemId);
21
+ const got = answerObjectForListItem(journeyResponse, manageListQuestion, manageListItemId);
22
22
  assert.strictEqual(got, answers);
23
23
  });
24
24
 
@@ -36,16 +36,16 @@ describe('manage-list-utils', () => {
36
36
  }
37
37
  };
38
38
  const manageListItemId = '1';
39
- let got = answerObjectForManageList(journeyResponse, manageListQuestion, manageListItemId);
39
+ let got = answerObjectForListItem(journeyResponse, manageListQuestion, manageListItemId);
40
40
  assert.deepStrictEqual(got, {});
41
41
  // also fallback to {} if there is no manageListQuestion answer
42
42
  delete journeyResponse.answers.manageListQuestion;
43
- got = answerObjectForManageList(journeyResponse, manageListQuestion, manageListItemId);
43
+ got = answerObjectForListItem(journeyResponse, manageListQuestion, manageListItemId);
44
44
  assert.deepStrictEqual(got, {});
45
45
  });
46
46
  });
47
47
 
48
- describe('answerObjectForManageListSaving', () => {
48
+ describe('answerObjectForListItemSaving', () => {
49
49
  it('should add an array with a new object if not found', () => {
50
50
  const manageListQuestion = {
51
51
  fieldName: 'manageListQuestion'
@@ -53,8 +53,7 @@ describe('manage-list-utils', () => {
53
53
  const journeyResponse = {
54
54
  answers: {}
55
55
  };
56
- const params = { manageListItemId: '1' };
57
- const got = answerObjectForManageListSaving(journeyResponse, manageListQuestion, params);
56
+ const got = answerObjectForListItemSaving(journeyResponse, manageListQuestion, '1');
58
57
  assert.deepStrictEqual(got, { id: '1' });
59
58
  assert.ok(Array.isArray(journeyResponse.answers.manageListQuestion));
60
59
  assert.ok(journeyResponse.answers.manageListQuestion[0]?.id, '1');
@@ -72,8 +71,7 @@ describe('manage-list-utils', () => {
72
71
  manageListQuestion: [answers]
73
72
  }
74
73
  };
75
- const params = { manageListItemId: '1' };
76
- const got = answerObjectForManageListSaving(journeyResponse, manageListQuestion, params);
74
+ const got = answerObjectForListItemSaving(journeyResponse, manageListQuestion, '1');
77
75
  assert.strictEqual(got, answers);
78
76
  });
79
77
  });
@@ -1 +1 @@
1
- {"version":3,"file":"session-answer-store.d.ts","sourceRoot":"","sources":["session-answer-store.js"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,sDAJG;IAAsB,QAAQ;CAE9B,GAAU,OAAO,kBAAkB,EAAE,UAAU,CA8CjD;AASD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,gFANG;IAA0C,GAAG,EAArC,OAAO,SAAS,EAAE,OAAO;IACV,SAAS,EAAxB,MAAM;IACuB,WAAW;;;IACxB,QAAQ;CAChC,GAAU,IAAI,CAkBhB;AAED;;;;;;GAMG;AACH,8DAJW,MAAM,aACN,MAAM,GACJ,OAAO,SAAS,EAAE,OAAO,CA2BrC;AA/ED;;;;GAIG;AACH,gCAFU,OAAO,kBAAkB,EAAE,UAAU,CAEW"}
1
+ {"version":3,"file":"session-answer-store.d.ts","sourceRoot":"","sources":["session-answer-store.js"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,sDAJG;IAAsB,QAAQ;CAE9B,GAAU,OAAO,kBAAkB,EAAE,UAAU,CAsDjD;AASD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,gFANG;IAA0C,GAAG,EAArC,OAAO,SAAS,EAAE,OAAO;IACV,SAAS,EAAxB,MAAM;IACuB,WAAW;;;IACxB,QAAQ;CAChC,GAAU,IAAI,CAkBhB;AAED;;;;;;GAMG;AACH,8DAJW,MAAM,aACN,MAAM,GACJ,OAAO,SAAS,EAAE,OAAO,CA2BrC;AA/ED;;;;GAIG;AACH,gCAFU,OAAO,kBAAkB,EAAE,UAAU,CAEW"}
@@ -50,7 +50,10 @@ export function buildSaveDataToSession({ reqParam } = {}) {
50
50
  data,
51
51
  isManageListItem,
52
52
  manageListQuestionFieldName,
53
- manageListItemRemove = false
53
+ manageListItemRemove = false,
54
+ isDynamicSection,
55
+ dynamicSectionFieldName,
56
+ dynamicSectionId
54
57
  }) => {
55
58
  if (!req.session) {
56
59
  throw new Error('request session required');
@@ -64,12 +67,17 @@ export function buildSaveDataToSession({ reqParam } = {}) {
64
67
  }
65
68
  let answers = forms[journeyId] || (forms[journeyId] = {});
66
69
 
67
- if (isManageListItem) {
68
- const answersList = answers[manageListQuestionFieldName] || (answers[manageListQuestionFieldName] = []);
69
- answers = answersList.find((item) => item.id === req.params.manageListItemId);
70
+ if (isManageListItem || isDynamicSection) {
71
+ // manage list and dynamic sections can be handled the same, just with different properties
72
+ // for the fieldName and item id
73
+ const fieldName = isManageListItem ? manageListQuestionFieldName : dynamicSectionFieldName;
74
+ const itemId = isManageListItem ? req.params.manageListItemId : dynamicSectionId;
75
+
76
+ const answersList = answers[fieldName] || (answers[fieldName] = []);
77
+ answers = answersList.find((item) => item.id === itemId);
70
78
  if (!answers) {
71
- answers = { id: req.params.manageListItemId };
72
- answersList.push(answers);
79
+ answers = { id: itemId }; // answers object to manipulate and add other answers to
80
+ answersList.push(answers); // add the answers object to the array
73
81
  }
74
82
  } else if (manageListItemRemove && manageListQuestionFieldName && req.params.manageListItemId) {
75
83
  const answersList = answers[manageListQuestionFieldName];
@@ -123,6 +123,49 @@ describe('session-answer-store', () => {
123
123
  });
124
124
  });
125
125
 
126
+ it('should add a new dynamic section when not present in the session', async () => {
127
+ const req = mockReq();
128
+ req.session = {};
129
+ const journeyId = 'j-1';
130
+ const data = { answers: { q1: 'a1' } };
131
+ const isDynamicSection = true;
132
+ const dynamicSectionFieldName = 'myList';
133
+ const dynamicSectionId = 'section-1';
134
+ const saveDataToSession = buildSaveDataToSession();
135
+ await saveDataToSession({ req, journeyId, data, isDynamicSection, dynamicSectionFieldName, dynamicSectionId });
136
+ assert.deepStrictEqual(req.session, {
137
+ forms: {
138
+ 'j-1': {
139
+ myList: [{ id: 'section-1', q1: 'a1' }]
140
+ }
141
+ }
142
+ });
143
+ });
144
+ it('should add to an existing dynamic section using the id when present in the session', async () => {
145
+ const req = mockReq();
146
+ req.session = {
147
+ forms: {
148
+ 'j-1': {
149
+ myList: [{ id: 'section-1', q1: 'old value' }]
150
+ }
151
+ }
152
+ };
153
+ const journeyId = 'j-1';
154
+ const data = { answers: { q2: 'a1' } };
155
+ const isDynamicSection = true;
156
+ const dynamicSectionFieldName = 'myList';
157
+ const dynamicSectionId = 'section-1';
158
+ const saveDataToSession = buildSaveDataToSession();
159
+ await saveDataToSession({ req, journeyId, data, isDynamicSection, dynamicSectionFieldName, dynamicSectionId });
160
+ assert.deepStrictEqual(req.session, {
161
+ forms: {
162
+ 'j-1': {
163
+ myList: [{ id: 'section-1', q1: 'old value', q2: 'a1' }]
164
+ }
165
+ }
166
+ });
167
+ });
168
+
126
169
  it('should remove a managed list item using the id when present in the session', async () => {
127
170
  const req = mockReq();
128
171
  req.params = { manageListItemId: '56789' };
@@ -1 +1 @@
1
- {"version":3,"file":"redirect-to-unanswered-question.d.ts","sourceRoot":"","sources":["redirect-to-unanswered-question.js"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;GAKG;AACH,0DAHW,sBAAsB,EAAE,GACtB,OAAO,SAAS,EAAE,OAAO,CA8BrC;qCArCY,CAAS,IAA2C,EAA3C,OAAO,0BAA0B,EAAE,QAAQ,EAAE,IAA2C,EAA3C,OAAO,mBAAmB,EAAE,eAAe,KAAG,OAAO"}
1
+ {"version":3,"file":"redirect-to-unanswered-question.d.ts","sourceRoot":"","sources":["redirect-to-unanswered-question.js"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;GAKG;AACH,0DAHW,sBAAsB,EAAE,GACtB,OAAO,SAAS,EAAE,OAAO,CA+BrC;qCAtCY,CAAS,IAA2C,EAA3C,OAAO,0BAA0B,EAAE,QAAQ,EAAE,IAA2C,EAA3C,OAAO,mBAAmB,EAAE,eAAe,KAAG,OAAO"}
@@ -13,11 +13,12 @@ export function redirectToUnansweredQuestion(conditions = []) {
13
13
  const { journeyResponse, journey } = res.locals;
14
14
 
15
15
  for (const section of journey.sections) {
16
+ const response = section.getResponse(journeyResponse);
16
17
  for (const question of section.questions) {
17
- const answer = journey.response?.answers[question.fieldName];
18
+ const answer = response?.answers[question.fieldName];
18
19
 
19
- const shouldSkip = conditions.some((condition) => condition(question, journeyResponse));
20
- const shouldDisplay = !question.shouldDisplay || question.shouldDisplay(journeyResponse);
20
+ const shouldSkip = conditions.some((condition) => condition(question, response));
21
+ const shouldDisplay = !question.shouldDisplay || question.shouldDisplay(response);
21
22
 
22
23
  if (shouldSkip || !shouldDisplay) {
23
24
  continue;
@@ -7,6 +7,7 @@ import { Journey } from '../journey/journey.js';
7
7
  import { Section } from '../section.js';
8
8
  import { COMPONENT_TYPES } from '../components/utils/component-types.js';
9
9
  import { questionHasAnswer } from '../components/utils/question-has-answer.js';
10
+ import { DynamicSection } from '#src/dynamic-section.js';
10
11
 
11
12
  describe('redirectToUnansweredQuestion Middleware', () => {
12
13
  const questions = {
@@ -145,4 +146,35 @@ describe('redirectToUnansweredQuestion Middleware', () => {
145
146
  assert.deepStrictEqual(res.redirect.mock.calls[0].arguments, ['base/url/task-list/page']);
146
147
  assert.strictEqual(next.mock.callCount(), 0);
147
148
  });
149
+
150
+ it('should support dynamic sections', () => {
151
+ const journeyResponse = new JourneyResponse(
152
+ 'id-1',
153
+ '0000003',
154
+ {
155
+ questionOne: true,
156
+ questionTwo: false,
157
+ items: [
158
+ { id: 'item-1', questionThree: true, questionFour: true },
159
+ { id: 'item-2', questionThree: true }
160
+ ]
161
+ },
162
+ 'Q9999'
163
+ );
164
+ res.locals.journeyResponse = journeyResponse;
165
+ const sections = [
166
+ new Section('S1', 's1').addQuestion(questions.q1).addQuestion(questions.q2),
167
+ new DynamicSection('S2', 'item-1', 'items').addQuestion(questions.q3).addQuestion(questions.q4),
168
+ new DynamicSection('S3', 'item-2', 'items').addQuestion(questions.q3).addQuestion(questions.q4)
169
+ ];
170
+ const journey = new Journey({ response: journeyResponse, ...params, sections });
171
+ res.locals.journey = journey;
172
+
173
+ redirectToUnansweredQuestion([])(req, res, next);
174
+
175
+ assert.strictEqual(res.redirect.mock.callCount(), 1);
176
+ // all questions answered apart from the last dynamic section question
177
+ assert.deepStrictEqual(res.redirect.mock.calls[0].arguments, ['base/url/item-2/question-4']);
178
+ assert.strictEqual(next.mock.callCount(), 0);
179
+ });
148
180
  });
@@ -127,7 +127,7 @@ export class Question {
127
127
  * @param {import('../journey/journey.js').Journey} journey - the journey we are in
128
128
  * @param {Record<string, unknown>} [customViewData] additional data to send to view
129
129
  * @param {unknown} [payload]
130
- * @param {import('#typedefs/question-types.d.ts').PrepQuestionForRenderingOptions} [options] - required to support manage list question
130
+ * @param {import('#typedefs/question-types.d.ts').PrepQuestionForRenderingOptions} [options] - required to support manage list question and dynamic sections
131
131
  * @returns {import('#typedefs/question-types.d.ts').QuestionViewModel}
132
132
  */
133
133
  prepQuestionForRendering(section: import("../section.js").Section, journey: import("../journey/journey.js").Journey, customViewData?: Record<string, unknown>, payload?: unknown, options?: import("#typedefs/question-types.d.ts").PrepQuestionForRenderingOptions): import("#typedefs/question-types.d.ts").QuestionViewModel;
@@ -148,13 +148,13 @@ export class Question {
148
148
  * @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
149
149
  */ addCustomDataToViewModel(viewModel: import("#typedefs/question-types.d.ts").QuestionViewModel): void;
150
150
  /**
151
- * Get the answers object from the journey response, which may be nested in an array for manage list questions
151
+ * Get the answers object from the journey response, which may be nested in an array for manage list questions or dynamic sections
152
152
  *
153
153
  * @param {import('../journey/journey-response.js').JourneyResponse} response
154
154
  * @param {import('#typedefs/question-types.d.ts').PrepQuestionForRenderingOptions} [options]
155
155
  * @returns {Record<string, any>}
156
156
  */
157
- answerObjectFromJourneyResponse(response: import("../journey/journey-response.js").JourneyResponse, { params, manageListQuestion }?: import("#typedefs/question-types.d.ts").PrepQuestionForRenderingOptions): Record<string, any>;
157
+ answerObjectFromJourneyResponse(response: import("../journey/journey-response.js").JourneyResponse, { params, manageListQuestion, dynamicSection }?: import("#typedefs/question-types.d.ts").PrepQuestionForRenderingOptions): Record<string, any>;
158
158
  /**
159
159
  * renders the question
160
160
  * @param {import('express').Response} res - the express response
@@ -200,6 +200,11 @@ export class Question {
200
200
  handleNextQuestion(res: import("express").Response, journey: import("../journey/journey.js").Journey, sectionSegment: string, questionSegment: string): void;
201
201
  /**
202
202
  * returns the formatted answers values to be used to build task list elements
203
+ *
204
+ * If overriding this method and access answers on the journey, use journey.responseForSection
205
+ * to get the answers, instead of `journey.response` directly. This is to support DynamicSections
206
+ * where answers are in an array. See `MultiFieldInputQuestion` for an example.
207
+ *
203
208
  * @param {string} sectionSegment
204
209
  * @param {import('../journey/journey.js').Journey} journey
205
210
  * @param {unknown} answer
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import escape from 'escape-html';
2
2
  import { capitalize, nl2br, trimTrailingSlash } from '../lib/utils.js';
3
3
  import MultiFieldInputValidator from '../validator/multi-field-input-validator.js';
4
- import { answerObjectForManageList } from '#src/components/manage-list/utils.js';
4
+ import { answerObjectForListItem } from '#src/lib/answer-utils.js';
5
5
 
6
6
  /**
7
7
  * A specific question within a journey which is made up of one (usually) or many (sometimes) components and their required content.
@@ -202,7 +202,8 @@ export class Question {
202
202
  toViewModel({ params, manageListQuestion, section, journey, customViewData, payload }) {
203
203
  const viewModel = this.prepQuestionForRendering(section, journey, customViewData, payload, {
204
204
  params,
205
- manageListQuestion
205
+ manageListQuestion,
206
+ dynamicSection: section.isDynamicSection ? section : undefined
206
207
  });
207
208
  viewModel.backLink = journey.getBackLink({ params, manageListQuestion });
208
209
  return viewModel;
@@ -215,7 +216,7 @@ export class Question {
215
216
  * @param {import('../journey/journey.js').Journey} journey - the journey we are in
216
217
  * @param {Record<string, unknown>} [customViewData] additional data to send to view
217
218
  * @param {unknown} [payload]
218
- * @param {import('#typedefs/question-types.d.ts').PrepQuestionForRenderingOptions} [options] - required to support manage list question
219
+ * @param {import('#typedefs/question-types.d.ts').PrepQuestionForRenderingOptions} [options] - required to support manage list question and dynamic sections
219
220
  * @returns {import('#typedefs/question-types.d.ts').QuestionViewModel}
220
221
  */
221
222
  prepQuestionForRendering(section, journey, customViewData, payload, options) {
@@ -280,13 +281,13 @@ export class Question {
280
281
  addCustomDataToViewModel(viewModel) {}
281
282
 
282
283
  /**
283
- * Get the answers object from the journey response, which may be nested in an array for manage list questions
284
+ * Get the answers object from the journey response, which may be nested in an array for manage list questions or dynamic sections
284
285
  *
285
286
  * @param {import('../journey/journey-response.js').JourneyResponse} response
286
287
  * @param {import('#typedefs/question-types.d.ts').PrepQuestionForRenderingOptions} [options]
287
288
  * @returns {Record<string, any>}
288
289
  */
289
- answerObjectFromJourneyResponse(response, { params, manageListQuestion } = {}) {
290
+ answerObjectFromJourneyResponse(response, { params, manageListQuestion, dynamicSection } = {}) {
290
291
  if (this.isInManageListSection) {
291
292
  if (!params?.manageListItemId) {
292
293
  throw new Error('no list item id for manage list question');
@@ -295,7 +296,11 @@ export class Question {
295
296
  throw new Error('no manageListQuestion for manage list question');
296
297
  }
297
298
  // if this is a manage list question, the response is within the 'parent' manage list answers array
298
- return answerObjectForManageList(response, manageListQuestion, params.manageListItemId);
299
+ return answerObjectForListItem(response, manageListQuestion, params.manageListItemId);
300
+ }
301
+ if (dynamicSection) {
302
+ // if this is a question in a dynamic section, the response is within an answers array, key by section
303
+ return answerObjectForListItem(response, dynamicSection, params.section);
299
304
  }
300
305
  return response.answers;
301
306
  }
@@ -393,6 +398,11 @@ export class Question {
393
398
 
394
399
  /**
395
400
  * returns the formatted answers values to be used to build task list elements
401
+ *
402
+ * If overriding this method and access answers on the journey, use journey.responseForSection
403
+ * to get the answers, instead of `journey.response` directly. This is to support DynamicSections
404
+ * where answers are in an array. See `MultiFieldInputQuestion` for an example.
405
+ *
396
406
  * @param {string} sectionSegment
397
407
  * @param {import('../journey/journey.js').Journey} journey
398
408
  * @param {unknown} answer
@@ -285,6 +285,25 @@ describe('./src/dynamic-forms/question.js', () => {
285
285
  /no manageListQuestion for manage list question/
286
286
  );
287
287
  });
288
+
289
+ it('should return dynamic section object', () => {
290
+ const question = getTestQuestion();
291
+ const answers = {
292
+ id: 'section-1',
293
+ myField: 'my-value'
294
+ };
295
+ const journeyResponse = {
296
+ answers: {
297
+ myList: [answers]
298
+ }
299
+ };
300
+ const params = { section: 'section-1' };
301
+ const got = question.answerObjectFromJourneyResponse(journeyResponse, {
302
+ params,
303
+ dynamicSection: { fieldName: 'myList' }
304
+ });
305
+ assert.strictEqual(got, answers);
306
+ });
288
307
  });
289
308
 
290
309
  describe('renderAction', () => {