@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.
- package/README.md +52 -1
- package/docs/Dynamic Sections.md +47 -0
- package/package.json +1 -1
- package/src/components/manage-list/question.d.ts.map +1 -1
- package/src/components/manage-list/question.js +5 -0
- package/src/components/manage-list/question.test.js +3 -1
- package/src/components/multi-field-input/question.d.ts.map +1 -1
- package/src/components/multi-field-input/question.js +7 -5
- package/src/components/multi-field-input/question.test.js +126 -213
- package/src/components/text-entry-redact/question.d.ts.map +1 -1
- package/src/components/text-entry-redact/question.js +3 -1
- package/src/components/text-entry-redact/question.test.js +9 -0
- package/src/components/unit-option-entry/question.d.ts.map +1 -1
- package/src/components/unit-option-entry/question.js +3 -1
- package/src/components/unit-option-entry/question.test.js +9 -0
- package/src/controller.d.ts +15 -3
- package/src/controller.d.ts.map +1 -1
- package/src/controller.js +19 -11
- package/src/controller.test.js +182 -153
- package/src/dynamic-section.d.ts +40 -0
- package/src/dynamic-section.d.ts.map +1 -0
- package/src/dynamic-section.js +51 -0
- package/src/dynamic-section.test.js +113 -0
- package/src/index.d.ts +2 -1
- package/src/index.js +2 -1
- package/src/index.test.js +3 -0
- package/src/journey/journey.d.ts +10 -0
- package/src/journey/journey.d.ts.map +1 -1
- package/src/journey/journey.js +15 -0
- package/src/lib/answer-utils.d.ts +22 -0
- package/src/lib/answer-utils.d.ts.map +1 -0
- package/src/lib/answer-utils.js +34 -0
- package/src/{components/manage-list/utils.test.js → lib/answer-utils.test.js} +9 -11
- package/src/lib/session-answer-store.d.ts.map +1 -1
- package/src/lib/session-answer-store.js +14 -6
- package/src/lib/session-answer-store.test.js +43 -0
- package/src/middleware/redirect-to-unanswered-question.d.ts.map +1 -1
- package/src/middleware/redirect-to-unanswered-question.js +4 -3
- package/src/middleware/redirect-to-unanswered-question.test.js +32 -0
- package/src/questions/question.d.ts +8 -3
- package/src/questions/question.d.ts.map +1 -1
- package/src/questions/question.js +16 -6
- package/src/questions/question.test.js +19 -0
- package/src/section.d.ts +19 -0
- package/src/section.d.ts.map +1 -1
- package/src/section.js +50 -9
- package/src/section.test.js +9 -0
- package/test/dynamic-journey.test.js +372 -0
- package/typedefs/question-types.d.ts +3 -0
- package/src/components/manage-list/utils.d.ts +0 -18
- package/src/components/manage-list/utils.d.ts.map +0 -1
- 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
|
|
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
|
});
|
package/src/journey/journey.d.ts
CHANGED
|
@@ -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"}
|
package/src/journey/journey.js
CHANGED
|
@@ -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 {
|
|
3
|
+
import { answerObjectForListItem, answerObjectForListItemSaving } from '#src/lib/answer-utils.js';
|
|
4
4
|
|
|
5
|
-
describe('
|
|
6
|
-
describe('
|
|
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 =
|
|
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 =
|
|
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 =
|
|
43
|
+
got = answerObjectForListItem(journeyResponse, manageListQuestion, manageListItemId);
|
|
44
44
|
assert.deepStrictEqual(got, {});
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
describe('
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
69
|
-
|
|
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:
|
|
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,
|
|
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 =
|
|
18
|
+
const answer = response?.answers[question.fieldName];
|
|
18
19
|
|
|
19
|
-
const shouldSkip = conditions.some((condition) => condition(question,
|
|
20
|
-
const shouldDisplay = !question.shouldDisplay || question.shouldDisplay(
|
|
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":"AAKA;;;GAGG;AACH;IAiEC;;;OAGG;IACH,+NAHW,OAAO,+BAA+B,EAAE,kBAAkB,oBAC1D,MAAM,CAAC,MAAM,WAAW,EA0DlC;IA5HD,6EAA6E;IAC7E,WADW,MAAM,CACP;IACV,sDAAsD;IACtD,OADW,MAAM,CACX;IACN,+DAA+D;IAC/D,UADW,MAAM,CACR;IACT,mFAAmF;IACnF,aADW,MAAM,GAAC,SAAS,CACf;IACZ,iDAAiD;IACjD,YADW,MAAM,CACN;IACX,qHAAqH;IACrH,WADW,MAAM,CACP;IACV,6FAA6F;IAC7F,UADW,OAAO,CACF;IAChB,0IAA0I;IAC1I,YADW,KAAK,CAAE,OAAO,gCAAgC,EAAE,aAAa,CAAC,CACzD;IAChB,2DAA2D;IAC3D,MADW,MAAM,GAAC,SAAS,CACtB;IACL,sEAAsE;IACtE,oBADW,OAAO,CACQ;IAC1B,oDAAoD;IACpD,KADW,MAAM,GAAC,SAAS,CACvB;IACJ,qDAAqD;IACrD,MADW,MAAM,GAAC,SAAS,CACtB;IACL,sDAAsD;IACtD,eADW,MAAM,GAAC,SAAS,CACb;IACd,gGAAgG;IAChG,YADW,OAAO,+BAA+B,EAAE,UAAU,GAAC,SAAS,CAC5D;IAEX,iFAAiF;IACjF,gBADW,MAAM,CACc;IAC/B,sFAAsF;IACtF,kBADW,OAAO,CACM;IACxB,4CAA4C;IAC5C,oBADW,MAAM,CACe;IAChC,uDAAuD;IACvD,kBADW,MAAM,CACW;IAC5B,uDAAuD;IACvD,kBADW,MAAM,CACW;IAC5B,oDAAoD;IACpD,eADW,MAAM,CACK;IACtB,0IAA0I;IAC1I,oBADW,OAAO,+BAA+B,EAAE,qBAAqB,GAAC,SAAS,CAC/D;IAEnB;;;OAGG;IAEH,gBAAiB,WAJN,OAAO,gCAAgC,EAAE,eAI3B,KAHZ,OAAO,CAGe;IAEnC;;;MAGE;IACF;;;;;;OAMG;IACH,wBAFU,OAAO,CAEc;IA2C9B,iCAAgC;IAChC,kBAAwB;IAExB;;;;MAAwB;IAkBzB;;;;;;OAMG;IACH,4BAFa,OAAO,CAInB;IAWD,iCANa,OAAO,EAWnB;IAdD;;;;OAIG;IACH,6BAFa,OAAO,CAInB;IAqBD;;;;;;OAMG;IACH,sBAFa,MAAM,EAAE,CAIpB;IAED;;;;;;;;;;;;;;OAcG;IACH,uFARG;QAAoE,MAAM,EAAlE,OAAO,8BAA8B,EAAE,WAAW;QAC2B,kBAAkB;QACtD,OAAO,EAAhD,OAAO,eAAe,EAAE,OAAO;QACkB,OAAO,EAAxD,OAAO,uBAAuB,EAAE,OAAO;QACL,cAAc;QAC9B,OAAO,GAAzB,OAAO;KACf,GAAU,OAAO,+BAA+B,EAAE,iBAAiB,
|
|
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 {
|
|
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
|
|
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', () => {
|