@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
package/src/controller.d.ts
CHANGED
|
@@ -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
|
};
|
package/src/controller.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["controller.js"],"names":[],"mappings":"
|
|
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 {
|
|
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(
|
|
104
|
+
if (!question.shouldDisplay(response)) {
|
|
108
105
|
continue;
|
|
109
106
|
}
|
|
110
107
|
|
|
111
|
-
|
|
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 =
|
|
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') {
|
package/src/controller.test.js
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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:
|
|
178
|
+
value: 'Yes',
|
|
242
179
|
action: [
|
|
243
180
|
{
|
|
244
181
|
href: '/manage-appeals/questionnaire/123456/segment-1/title-1a',
|
|
245
|
-
text: '
|
|
246
|
-
visuallyHiddenText: '
|
|
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
|
-
[
|
|
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: '
|
|
838
|
-
visuallyHiddenText:
|
|
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: '
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
}
|