@planning-inspectorate/dynamic-forms 2.4.0 → 3.0.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/package.json +1 -1
- package/src/components/manage-list/README.md +97 -0
- package/src/components/manage-list/answer-summary-list.njk +10 -0
- package/src/components/manage-list/index.njk +61 -0
- package/src/components/manage-list/manage-list-section.js +39 -0
- package/src/components/manage-list/manage-list-section.test.js +52 -0
- package/src/components/manage-list/question.js +147 -0
- package/src/components/manage-list/question.test.js +139 -0
- package/src/components/utils/nunjucks.js +20 -0
- package/src/controller.js +39 -10
- package/src/controller.test.js +90 -24
- package/src/index.js +3 -2
- package/src/journey/journey-types.d.ts +11 -1
- package/src/journey/journey.js +64 -18
- package/src/journey/journey.test.js +80 -15
- package/src/questions/question-props.d.ts +2 -1
- package/src/questions/question.js +60 -12
- package/src/questions/question.test.js +26 -3
- package/src/questions/questions.js +3 -1
- package/src/section-types.d.ts +18 -0
- package/src/section.js +40 -11
- package/src/section.test.js +89 -0
- package/test/journey.js +5 -0
- package/test/mock/journey.js +17 -0
- package/test/questions.js +30 -1
- package/test/questions.test.js +104 -40
- package/test/snapshots/check-your-answers-not-started.html +11 -0
- package/test/snapshots/check-your-answers-with-answers.html +11 -0
- package/test/snapshots/manage-list-answer-summary.html +14 -0
- package/test/snapshots/manage-list-render-questions.html +97 -0
- package/test/snapshots/manage-list-render.html +91 -0
- package/test/snapshots/travel-companions.html +19 -0
- package/test/snapshots/travel-companions_travel-companion-email.html +21 -0
- package/test/snapshots/travel-companions_travel-companion-name.html +21 -0
package/src/controller.test.js
CHANGED
|
@@ -178,6 +178,7 @@ const sampleQuestionObj = {
|
|
|
178
178
|
getDataToSave: mock.fn(),
|
|
179
179
|
checkForValidationErrors: mock.fn(),
|
|
180
180
|
checkForSavingErrors: mock.fn(),
|
|
181
|
+
toViewModel: mock.fn(),
|
|
181
182
|
prepQuestionForRendering: mock.fn(),
|
|
182
183
|
formatAnswerForSummary: mock.fn(() => [mockAnswer]),
|
|
183
184
|
viewFolder: 'sampleType'
|
|
@@ -325,13 +326,13 @@ describe('dynamic-form/controller', () => {
|
|
|
325
326
|
|
|
326
327
|
it('should render the question template', async () => {
|
|
327
328
|
sampleQuestionObj.renderAction.mock.resetCalls();
|
|
328
|
-
sampleQuestionObj.
|
|
329
|
+
sampleQuestionObj.toViewModel.mock.resetCalls();
|
|
329
330
|
req.params.referenceId = mockRef;
|
|
330
331
|
const mockAnswer = 'sampleAnswer';
|
|
331
332
|
const mockBackLink = 'back';
|
|
332
333
|
const mockQuestionRendering = 'test';
|
|
333
334
|
|
|
334
|
-
sampleQuestionObj.
|
|
335
|
+
sampleQuestionObj.toViewModel.mock.mockImplementationOnce(() => mockQuestionRendering);
|
|
335
336
|
|
|
336
337
|
mockJourney.getQuestionByParams = mock.fn();
|
|
337
338
|
mockJourney.getQuestionByParams.mock.mockImplementationOnce(() => sampleQuestionObj);
|
|
@@ -343,13 +344,11 @@ describe('dynamic-form/controller', () => {
|
|
|
343
344
|
|
|
344
345
|
assert.strictEqual(sampleQuestionObj.renderAction.mock.callCount(), 1);
|
|
345
346
|
assert.deepStrictEqual(sampleQuestionObj.renderAction.mock.calls[0].arguments, [res, mockQuestionRendering]);
|
|
346
|
-
assert.strictEqual(sampleQuestionObj.
|
|
347
|
-
|
|
348
|
-
assert.ok(
|
|
349
|
-
assert.
|
|
350
|
-
|
|
351
|
-
req.originalUrl
|
|
352
|
-
);
|
|
347
|
+
assert.strictEqual(sampleQuestionObj.toViewModel.mock.callCount(), 1);
|
|
348
|
+
const args = sampleQuestionObj.toViewModel.mock.calls[0].arguments[0];
|
|
349
|
+
assert.ok(args);
|
|
350
|
+
assert.ok(args?.customViewData?.originalUrl);
|
|
351
|
+
assert.strictEqual(args?.customViewData?.originalUrl, req.originalUrl);
|
|
353
352
|
});
|
|
354
353
|
});
|
|
355
354
|
|
|
@@ -394,7 +393,9 @@ describe('dynamic-form/controller', () => {
|
|
|
394
393
|
const mocks = [sampleQuestionObj.checkForValidationErrors.mock, sampleQuestionObj.checkForSavingErrors.mock];
|
|
395
394
|
for (const mockFn of mocks) {
|
|
396
395
|
assert.strictEqual(mockFn.callCount(), 1);
|
|
397
|
-
assert.deepStrictEqual(mockFn.calls[0].arguments
|
|
396
|
+
assert.deepStrictEqual(mockFn.calls[0].arguments[0], req);
|
|
397
|
+
assert.deepStrictEqual(mockFn.calls[0].arguments[1], sections[0]);
|
|
398
|
+
assert.deepStrictEqual(mockFn.calls[0].arguments[2], mockJourney);
|
|
398
399
|
}
|
|
399
400
|
});
|
|
400
401
|
|
|
@@ -404,7 +405,7 @@ describe('dynamic-form/controller', () => {
|
|
|
404
405
|
const sampleQuestionObjWithActions = {
|
|
405
406
|
...sampleQuestionObj,
|
|
406
407
|
saveAction: mock.fn(),
|
|
407
|
-
|
|
408
|
+
toViewModel: mock.fn(() => expectedViewModel),
|
|
408
409
|
renderAction: mock.fn()
|
|
409
410
|
};
|
|
410
411
|
|
|
@@ -434,20 +435,25 @@ describe('dynamic-form/controller', () => {
|
|
|
434
435
|
await buildSave(saveData)(req, res, journeyId);
|
|
435
436
|
|
|
436
437
|
assert.strictEqual(saveData.mock.callCount(), 1);
|
|
437
|
-
assert.deepStrictEqual(
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
438
|
+
assert.deepStrictEqual(
|
|
439
|
+
saveData.mock.calls[0].arguments,
|
|
440
|
+
[
|
|
441
|
+
{
|
|
442
|
+
req,
|
|
443
|
+
res,
|
|
444
|
+
journeyId: journeyParams.journeyId,
|
|
445
|
+
referenceId: journeyParams.referenceId,
|
|
446
|
+
data: undefined
|
|
447
|
+
}
|
|
448
|
+
],
|
|
449
|
+
'save data args'
|
|
450
|
+
);
|
|
446
451
|
|
|
447
|
-
assert.deepStrictEqual(
|
|
448
|
-
|
|
449
|
-
expectedViewModel
|
|
450
|
-
|
|
452
|
+
assert.deepStrictEqual(
|
|
453
|
+
sampleQuestionObjWithActions.renderAction.mock.calls[0].arguments,
|
|
454
|
+
[res, expectedViewModel],
|
|
455
|
+
'render args'
|
|
456
|
+
);
|
|
451
457
|
});
|
|
452
458
|
|
|
453
459
|
it('should handle validation errors', async () => {
|
|
@@ -546,6 +552,66 @@ describe('dynamic-form/controller', () => {
|
|
|
546
552
|
assert.match(res.redirect.mock.calls[0].arguments[0], /\/task-list$/);
|
|
547
553
|
assert.strictEqual(mockJourney.getNextQuestionUrl.mock.callCount(), 0);
|
|
548
554
|
});
|
|
555
|
+
|
|
556
|
+
describe('manageListQuestions', () => {
|
|
557
|
+
const setupJourney = () => {
|
|
558
|
+
const q = {
|
|
559
|
+
fieldName: 'question-2',
|
|
560
|
+
get isInManageListSection() {
|
|
561
|
+
return true;
|
|
562
|
+
},
|
|
563
|
+
checkForValidationErrors: mock.fn(),
|
|
564
|
+
getDataToSave: mock.fn(),
|
|
565
|
+
checkForSavingErrors: mock.fn()
|
|
566
|
+
};
|
|
567
|
+
const manageListQ = {
|
|
568
|
+
get isManageListQuestion() {
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
const req = {
|
|
573
|
+
params: {
|
|
574
|
+
section: 'section-1',
|
|
575
|
+
question: 'question-1',
|
|
576
|
+
manageListAction: 'add',
|
|
577
|
+
manageListItemId: 'item-id-1',
|
|
578
|
+
manageListQuestion: 'question-6'
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
const journey = {
|
|
582
|
+
getSection: mock.fn(() => ({ segment: 'section-1' })),
|
|
583
|
+
getQuestionByParams: mock.fn(),
|
|
584
|
+
redirectToNextQuestion: mock.fn()
|
|
585
|
+
};
|
|
586
|
+
const res = {
|
|
587
|
+
locals: {
|
|
588
|
+
journey,
|
|
589
|
+
journeyResponse: { answers: {} }
|
|
590
|
+
},
|
|
591
|
+
redirect: mock.fn()
|
|
592
|
+
};
|
|
593
|
+
journey.getQuestionByParams.mock.mockImplementationOnce(() => q, 0);
|
|
594
|
+
journey.getQuestionByParams.mock.mockImplementationOnce(() => manageListQ, 1);
|
|
595
|
+
return { journey, manageListQ, req, res, q };
|
|
596
|
+
};
|
|
597
|
+
it('should find the parent manage list question', async () => {
|
|
598
|
+
const { journey, manageListQ, req, res } = setupJourney();
|
|
599
|
+
const saveData = mock.fn();
|
|
600
|
+
await buildSave(saveData)(req, res, 'journey-1');
|
|
601
|
+
assert.strictEqual(journey.getSection.mock.callCount(), 1);
|
|
602
|
+
assert.strictEqual(journey.getQuestionByParams.mock.callCount(), 2);
|
|
603
|
+
assert.strictEqual(journey.redirectToNextQuestion.mock.callCount(), 1);
|
|
604
|
+
assert.strictEqual(journey.redirectToNextQuestion.mock.calls[0].arguments[2], manageListQ);
|
|
605
|
+
});
|
|
606
|
+
it('should preserve request route params', async () => {
|
|
607
|
+
const { journey, req, res } = setupJourney();
|
|
608
|
+
const saveData = mock.fn();
|
|
609
|
+
await buildSave(saveData)(req, res, 'journey-1');
|
|
610
|
+
assert.strictEqual(journey.redirectToNextQuestion.mock.callCount(), 1);
|
|
611
|
+
const params = journey.redirectToNextQuestion.mock.calls[0].arguments[1];
|
|
612
|
+
assert.deepStrictEqual(params, req.params);
|
|
613
|
+
});
|
|
614
|
+
});
|
|
549
615
|
});
|
|
550
616
|
});
|
|
551
617
|
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* @type {Readonly<{CHECKBOX: QuestionTypes, BOOLEAN: QuestionTypes, RADIO: QuestionTypes, DATE: QuestionTypes, DATE_PERIOD: QuestionTypes, TEXT_ENTRY: QuestionTypes, TEXT_ENTRY_REDACT: QuestionTypes, SELECT: QuestionTypes, SINGLE_LINE_INPUT: QuestionTypes, MULTI_FIELD_INPUT: QuestionTypes, NUMBER: QuestionTypes, ADDRESS: QuestionTypes, UNIT_OPTION: QuestionTypes, EMAIL: QuestionTypes}>}
|
|
6
|
+
* @type {Readonly<{CHECKBOX: QuestionTypes, BOOLEAN: QuestionTypes, RADIO: QuestionTypes, DATE: QuestionTypes, DATE_PERIOD: QuestionTypes, TEXT_ENTRY: QuestionTypes, TEXT_ENTRY_REDACT: QuestionTypes, SELECT: QuestionTypes, SINGLE_LINE_INPUT: QuestionTypes, MULTI_FIELD_INPUT: QuestionTypes, NUMBER: QuestionTypes, ADDRESS: QuestionTypes, UNIT_OPTION: QuestionTypes, EMAIL: QuestionTypes, MANAGE_LIST: QuestionTypes}>}
|
|
7
7
|
*/
|
|
8
8
|
export const COMPONENT_TYPES = Object.freeze({
|
|
9
9
|
CHECKBOX: 'checkbox',
|
|
@@ -20,5 +20,6 @@ export const COMPONENT_TYPES = Object.freeze({
|
|
|
20
20
|
NUMBER: 'number',
|
|
21
21
|
ADDRESS: 'site-address',
|
|
22
22
|
UNIT_OPTION: 'unit-option',
|
|
23
|
-
EMAIL: 'email'
|
|
23
|
+
EMAIL: 'email',
|
|
24
|
+
MANAGE_LIST: 'manage-list'
|
|
24
25
|
});
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Express route parameters used for journey routes
|
|
3
3
|
*/
|
|
4
|
-
export type RouteParams =
|
|
4
|
+
export type RouteParams = CoreRouteParams & ManageListRouteParams;
|
|
5
|
+
|
|
6
|
+
export type CoreRouteParams = {
|
|
5
7
|
section: string;
|
|
6
8
|
question: string;
|
|
7
9
|
[key: string]: string;
|
|
8
10
|
};
|
|
11
|
+
|
|
12
|
+
export type ManageListRouteParams =
|
|
13
|
+
| {
|
|
14
|
+
manageListAction: string;
|
|
15
|
+
manageListItemId: string;
|
|
16
|
+
manageListQuestion: string;
|
|
17
|
+
}
|
|
18
|
+
| {};
|
package/src/journey/journey.js
CHANGED
|
@@ -140,7 +140,11 @@ export class Journey {
|
|
|
140
140
|
* @returns {string} url for a question
|
|
141
141
|
*/
|
|
142
142
|
#buildQuestionUrl(params) {
|
|
143
|
-
|
|
143
|
+
const parts = [params.section, params.question];
|
|
144
|
+
if (params.manageListAction) {
|
|
145
|
+
parts.push(params.manageListAction, params.manageListItemId, params.manageListQuestion);
|
|
146
|
+
}
|
|
147
|
+
return this.#prependPathToUrl(this.baseUrl, parts.join('/'));
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
/**
|
|
@@ -158,12 +162,21 @@ export class Journey {
|
|
|
158
162
|
* Get question within a section
|
|
159
163
|
* @param {Section} section
|
|
160
164
|
* @param {string} questionSegment
|
|
165
|
+
* @param {{action: string, itemId: string, question: string}} [manageListParams]
|
|
161
166
|
* @returns {Question | undefined} question if it belongs in the given section
|
|
162
167
|
*/
|
|
163
|
-
#getQuestion(section, questionSegment) {
|
|
164
|
-
|
|
165
|
-
return q.fieldName ===
|
|
166
|
-
}
|
|
168
|
+
#getQuestion(section, questionSegment, manageListParams) {
|
|
169
|
+
const matchQuestion = (q, toMatch) => {
|
|
170
|
+
return q.fieldName === toMatch || q.url === toMatch;
|
|
171
|
+
};
|
|
172
|
+
const question = section?.questions.find((q) => matchQuestion(q, questionSegment));
|
|
173
|
+
if (!question) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
if (manageListParams && question.isManageListQuestion) {
|
|
177
|
+
return question.section.questions.find((q) => matchQuestion(q, manageListParams.question));
|
|
178
|
+
}
|
|
179
|
+
return question;
|
|
167
180
|
}
|
|
168
181
|
|
|
169
182
|
/**
|
|
@@ -177,19 +190,30 @@ export class Journey {
|
|
|
177
190
|
if (!section) {
|
|
178
191
|
return undefined;
|
|
179
192
|
}
|
|
180
|
-
|
|
181
|
-
|
|
193
|
+
let manageListParams;
|
|
194
|
+
if (params.manageListAction && params.manageListItemId && params.manageListQuestion) {
|
|
195
|
+
manageListParams = {
|
|
196
|
+
action: params.manageListAction,
|
|
197
|
+
itemId: params.manageListItemId,
|
|
198
|
+
question: params.manageListQuestion
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return this.#getQuestion(section, params.question, manageListParams);
|
|
182
202
|
}
|
|
183
203
|
|
|
184
204
|
/**
|
|
185
205
|
* Get the back link for the journey - e.g. the previous question
|
|
186
206
|
*
|
|
187
|
-
* @param {
|
|
188
|
-
* @param {
|
|
207
|
+
* @param {Object} options
|
|
208
|
+
* @param {import('./journey-types.d.ts').RouteParams} options.params
|
|
209
|
+
* @param {import('#src/components/manage-list/question.js')} [options.manageListQuestion]
|
|
189
210
|
* @returns {string|null} url for the next question, or null if unmatched
|
|
190
211
|
*/
|
|
191
|
-
getBackLink(
|
|
192
|
-
const previousQuestion = this.getNextQuestionUrl(
|
|
212
|
+
getBackLink({ params, manageListQuestion }) {
|
|
213
|
+
const previousQuestion = this.getNextQuestionUrl(params, {
|
|
214
|
+
manageListQuestion,
|
|
215
|
+
reverse: true
|
|
216
|
+
});
|
|
193
217
|
if (!previousQuestion) {
|
|
194
218
|
return this.initialBackLink;
|
|
195
219
|
}
|
|
@@ -202,10 +226,11 @@ export class Journey {
|
|
|
202
226
|
*
|
|
203
227
|
* @param {import('express').Response} res
|
|
204
228
|
* @param {import('./journey-types.d.ts').RouteParams} params
|
|
229
|
+
* @param {import('#src/components/manage-list/question.js')} [manageListQuestion]
|
|
205
230
|
* @returns {void}
|
|
206
231
|
*/
|
|
207
|
-
redirectToNextQuestion(res, params) {
|
|
208
|
-
const next = this.getNextQuestionUrl(params) ?? this.taskListUrl;
|
|
232
|
+
redirectToNextQuestion(res, params, manageListQuestion) {
|
|
233
|
+
const next = this.getNextQuestionUrl(params, { manageListQuestion }) ?? this.taskListUrl;
|
|
209
234
|
return res.redirect(next);
|
|
210
235
|
}
|
|
211
236
|
|
|
@@ -213,12 +238,15 @@ export class Journey {
|
|
|
213
238
|
* Get url for the next question in the journey
|
|
214
239
|
*
|
|
215
240
|
* @param {import('./journey-types.d.ts').RouteParams} params
|
|
216
|
-
* @param {
|
|
241
|
+
* @param {Object} options
|
|
242
|
+
* @param {boolean} [options.reverse] - if passed in this will get the previous question
|
|
243
|
+
* @param {import('#src/components/manage-list/question.js')} [options.manageListQuestion]
|
|
217
244
|
* @returns {string|null} url for the next question, or null if unmatched
|
|
218
245
|
*/
|
|
219
|
-
getNextQuestionUrl(params, reverse = false) {
|
|
246
|
+
getNextQuestionUrl(params, { reverse = false, manageListQuestion } = {}) {
|
|
220
247
|
const numberOfSections = this.sections.length;
|
|
221
248
|
const sectionsStart = reverse ? numberOfSections - 1 : 0;
|
|
249
|
+
const questionFieldName = manageListQuestion ? params.manageListQuestion : params.question;
|
|
222
250
|
|
|
223
251
|
let currentSectionIndex;
|
|
224
252
|
let foundSection = false;
|
|
@@ -237,19 +265,37 @@ export class Journey {
|
|
|
237
265
|
return null;
|
|
238
266
|
}
|
|
239
267
|
const question = currentSection.getNextQuestion({
|
|
240
|
-
questionFieldName
|
|
268
|
+
questionFieldName,
|
|
241
269
|
response: this.response,
|
|
270
|
+
manageListQuestion,
|
|
242
271
|
takeNextQuestion,
|
|
243
272
|
reverse
|
|
244
273
|
});
|
|
245
274
|
if (question === END_OF_SECTION) {
|
|
246
275
|
takeNextQuestion = true; // get the first question from the following section
|
|
247
276
|
} else if (question) {
|
|
248
|
-
|
|
249
|
-
|
|
277
|
+
/**
|
|
278
|
+
* if this is a regular question, then only section and question params are required
|
|
279
|
+
* don't include other params which may be set (e.g. manageList* params), as we may now be
|
|
280
|
+
* redirecting to a non-manage list question, having previously been on a manage list question
|
|
281
|
+
*
|
|
282
|
+
* @type {import('./journey-types.d.ts').RouteParams}
|
|
283
|
+
*/
|
|
284
|
+
let newParams = {
|
|
250
285
|
section: currentSection.segment,
|
|
251
286
|
question: question.url || question.fieldName
|
|
252
287
|
};
|
|
288
|
+
if (question.isInManageListSection && manageListQuestion) {
|
|
289
|
+
// if this is in a manage list section, then the manage list params will be set
|
|
290
|
+
// we need to retain the manageListAction and manageListItemId params
|
|
291
|
+
newParams = {
|
|
292
|
+
...params,
|
|
293
|
+
// the question param is for the 'parent' manageListQuestion
|
|
294
|
+
question: manageListQuestion.url,
|
|
295
|
+
// the manageListQuestion param is for the next question
|
|
296
|
+
manageListQuestion: question.url || question.fieldName
|
|
297
|
+
};
|
|
298
|
+
}
|
|
253
299
|
return this.#buildQuestionUrl(newParams);
|
|
254
300
|
}
|
|
255
301
|
}
|
|
@@ -241,7 +241,7 @@ describe('Journey class', () => {
|
|
|
241
241
|
journey.sections = mockSections;
|
|
242
242
|
journey.returnToListing = false;
|
|
243
243
|
|
|
244
|
-
const backLinks = journey.getBackLink(section.segment, name);
|
|
244
|
+
const backLinks = journey.getBackLink({ params: { section: section.segment, question: name } });
|
|
245
245
|
|
|
246
246
|
assert.strictEqual(backLinks, `${constructorArgs.makeBaseUrl()}/${section.segment}/${prevQuestionName}`);
|
|
247
247
|
});
|
|
@@ -257,7 +257,7 @@ describe('Journey class', () => {
|
|
|
257
257
|
journey.sections = mockSections;
|
|
258
258
|
journey.returnToListing = false;
|
|
259
259
|
|
|
260
|
-
const backLink = journey.getBackLink(section.segment, name
|
|
260
|
+
const backLink = journey.getBackLink({ params: { section: section.segment, question: name } });
|
|
261
261
|
|
|
262
262
|
assert.strictEqual(backLink, `${constructorArgs.makeBaseUrl()}/${prevSection.segment}/${prevQuestionName}`);
|
|
263
263
|
});
|
|
@@ -271,7 +271,7 @@ describe('Journey class', () => {
|
|
|
271
271
|
journey.sections = mockSections;
|
|
272
272
|
journey.returnToListing = false;
|
|
273
273
|
|
|
274
|
-
const backLink = journey.getBackLink(section.segment, name
|
|
274
|
+
const backLink = journey.getBackLink({ params: { section: section.segment, question: name } });
|
|
275
275
|
|
|
276
276
|
assert.strictEqual(backLink, null);
|
|
277
277
|
});
|
|
@@ -284,7 +284,7 @@ describe('Journey class', () => {
|
|
|
284
284
|
journey.sections = mockSections;
|
|
285
285
|
journey.returnToListing = false;
|
|
286
286
|
|
|
287
|
-
const backLink = journey.getBackLink(section.segment, name
|
|
287
|
+
const backLink = journey.getBackLink({ params: { section: section.segment, question: name } });
|
|
288
288
|
|
|
289
289
|
assert.strictEqual(backLink, '/some/back/link');
|
|
290
290
|
});
|
|
@@ -300,7 +300,7 @@ describe('Journey class', () => {
|
|
|
300
300
|
journey.sections = mockSections;
|
|
301
301
|
journey.returnToListing = returnToListing;
|
|
302
302
|
|
|
303
|
-
const nextQuestionUrl = journey.getNextQuestionUrl({ section: section, question: name }, false);
|
|
303
|
+
const nextQuestionUrl = journey.getNextQuestionUrl({ section: section, question: name }, { reverse: false });
|
|
304
304
|
|
|
305
305
|
assert.strictEqual(nextQuestionUrl, null);
|
|
306
306
|
});
|
|
@@ -315,7 +315,7 @@ describe('Journey class', () => {
|
|
|
315
315
|
journey.sections = mockSections;
|
|
316
316
|
journey.returnToListing = returnToListing;
|
|
317
317
|
|
|
318
|
-
const nextQuestionUrl = journey.getNextQuestionUrl({ section: section, question: name }, false);
|
|
318
|
+
const nextQuestionUrl = journey.getNextQuestionUrl({ section: section, question: name }, { reverse: false });
|
|
319
319
|
|
|
320
320
|
assert.strictEqual(nextQuestionUrl, null);
|
|
321
321
|
});
|
|
@@ -331,7 +331,10 @@ describe('Journey class', () => {
|
|
|
331
331
|
journey.sections = mockSections;
|
|
332
332
|
journey.returnToListing = returnToListing;
|
|
333
333
|
|
|
334
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
334
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
335
|
+
{ section: section.segment, question: name },
|
|
336
|
+
{ reverse: false }
|
|
337
|
+
);
|
|
335
338
|
|
|
336
339
|
assert.strictEqual(nextQuestionUrl, `${constructorArgs.makeBaseUrl()}/${section.segment}/${nextQuestionName}`);
|
|
337
340
|
});
|
|
@@ -356,7 +359,10 @@ describe('Journey class', () => {
|
|
|
356
359
|
journey.sections = mockSections;
|
|
357
360
|
journey.returnToListing = returnToListing;
|
|
358
361
|
|
|
359
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
362
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
363
|
+
{ section: section.segment, question: name },
|
|
364
|
+
{ reverse: false }
|
|
365
|
+
);
|
|
360
366
|
|
|
361
367
|
assert.strictEqual(nextQuestionUrl, `${constructorArgs.makeBaseUrl()}/${section.segment}/${nextQuestionName}`);
|
|
362
368
|
});
|
|
@@ -372,7 +378,10 @@ describe('Journey class', () => {
|
|
|
372
378
|
journey.sections = mockSections;
|
|
373
379
|
journey.returnToListing = returnToListing;
|
|
374
380
|
|
|
375
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
381
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
382
|
+
{ section: section.segment, question: name },
|
|
383
|
+
{ reverse: true }
|
|
384
|
+
);
|
|
376
385
|
|
|
377
386
|
assert.strictEqual(nextQuestionUrl, `${constructorArgs.makeBaseUrl()}/${section.segment}/${prevQuestionName}`);
|
|
378
387
|
});
|
|
@@ -387,7 +396,10 @@ describe('Journey class', () => {
|
|
|
387
396
|
journey.sections = mockSections;
|
|
388
397
|
journey.returnToListing = returnToListing;
|
|
389
398
|
|
|
390
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
399
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
400
|
+
{ section: section.segment, question: name },
|
|
401
|
+
{ reverse: false }
|
|
402
|
+
);
|
|
391
403
|
|
|
392
404
|
assert.strictEqual(nextQuestionUrl, null);
|
|
393
405
|
});
|
|
@@ -404,7 +416,10 @@ describe('Journey class', () => {
|
|
|
404
416
|
journey.sections = mockSections;
|
|
405
417
|
journey.returnToListing = false;
|
|
406
418
|
|
|
407
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
419
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
420
|
+
{ section: section.segment, question: name },
|
|
421
|
+
{ reverse: true }
|
|
422
|
+
);
|
|
408
423
|
|
|
409
424
|
assert.strictEqual(
|
|
410
425
|
nextQuestionUrl,
|
|
@@ -422,7 +437,10 @@ describe('Journey class', () => {
|
|
|
422
437
|
journey.sections = mockSections;
|
|
423
438
|
journey.returnToListing = returnToListing;
|
|
424
439
|
|
|
425
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
440
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
441
|
+
{ section: section.segment, question: name },
|
|
442
|
+
{ reverse: true }
|
|
443
|
+
);
|
|
426
444
|
|
|
427
445
|
assert.strictEqual(nextQuestionUrl, null);
|
|
428
446
|
});
|
|
@@ -437,7 +455,10 @@ describe('Journey class', () => {
|
|
|
437
455
|
journey.sections = mockSections;
|
|
438
456
|
journey.returnToListing = true;
|
|
439
457
|
|
|
440
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
458
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
459
|
+
{ section: section.segment, question: name },
|
|
460
|
+
{ reverse: false }
|
|
461
|
+
);
|
|
441
462
|
assert.strictEqual(nextQuestionUrl, null);
|
|
442
463
|
});
|
|
443
464
|
}
|
|
@@ -451,7 +472,10 @@ describe('Journey class', () => {
|
|
|
451
472
|
journey.sections = mockSections;
|
|
452
473
|
journey.returnToListing = true;
|
|
453
474
|
|
|
454
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
475
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
476
|
+
{ section: section.segment, question: name },
|
|
477
|
+
{ reverse: true }
|
|
478
|
+
);
|
|
455
479
|
|
|
456
480
|
assert.strictEqual(nextQuestionUrl, null);
|
|
457
481
|
});
|
|
@@ -466,10 +490,51 @@ describe('Journey class', () => {
|
|
|
466
490
|
const journey = new Journey(constructorArgs);
|
|
467
491
|
journey.sections = mockSections;
|
|
468
492
|
|
|
469
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
493
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
494
|
+
{ section: section.segment, question: name },
|
|
495
|
+
{ reverse: false }
|
|
496
|
+
);
|
|
470
497
|
|
|
471
498
|
assert.strictEqual(nextQuestionUrl, `base/${section.segment}/${nextQuestionName}?id=1`);
|
|
472
499
|
});
|
|
500
|
+
|
|
501
|
+
describe('manageListQuestions', () => {
|
|
502
|
+
it('should pass manageListQuestion to getNextQuestion', () => {
|
|
503
|
+
const getNextQuestion = mock.fn();
|
|
504
|
+
const journey = new Journey(constructorArgs);
|
|
505
|
+
journey.sections = [{ getNextQuestion }];
|
|
506
|
+
const manageListQuestion = { isManageListQuestion: true };
|
|
507
|
+
journey.getNextQuestionUrl({}, { manageListQuestion });
|
|
508
|
+
assert.strictEqual(getNextQuestion.mock.callCount(), 1);
|
|
509
|
+
assert.strictEqual(getNextQuestion.mock.calls[0].arguments[0].manageListQuestion, manageListQuestion);
|
|
510
|
+
});
|
|
511
|
+
it('should preserve params for manage list question', () => {
|
|
512
|
+
const question = {
|
|
513
|
+
url: 'question-3',
|
|
514
|
+
isInManageListSection: true
|
|
515
|
+
};
|
|
516
|
+
const manageListQuestion = {
|
|
517
|
+
url: 'manage-list-question',
|
|
518
|
+
isManageListQuestion: true
|
|
519
|
+
};
|
|
520
|
+
const params = {
|
|
521
|
+
section: 'section-1',
|
|
522
|
+
question: 'question-1,',
|
|
523
|
+
manageListAction: 'add',
|
|
524
|
+
manageListItemId: 'item-id-1',
|
|
525
|
+
manageListQuestion: 'question-2'
|
|
526
|
+
};
|
|
527
|
+
const getNextQuestion = mock.fn(() => question);
|
|
528
|
+
const section = {
|
|
529
|
+
segment: 'section-1',
|
|
530
|
+
getNextQuestion
|
|
531
|
+
};
|
|
532
|
+
const journey = new Journey(constructorArgs);
|
|
533
|
+
journey.sections = [section];
|
|
534
|
+
const url = journey.getNextQuestionUrl(params, { manageListQuestion });
|
|
535
|
+
assert.strictEqual(url, 'base/section-1/manage-list-question/add/item-id-1/question-3');
|
|
536
|
+
});
|
|
537
|
+
});
|
|
473
538
|
});
|
|
474
539
|
|
|
475
540
|
describe('getCurrentQuestionUrl', () => {
|
|
@@ -30,7 +30,6 @@ import MultiFieldInputValidator from '../validator/multi-field-input-validator.j
|
|
|
30
30
|
* @property {string} layoutTemplate
|
|
31
31
|
* @property {string} pageCaption
|
|
32
32
|
* @property {string} [continueButtonText]
|
|
33
|
-
* @property {Array.<string>} navigation
|
|
34
33
|
* @property {string} backLink
|
|
35
34
|
* @property {boolean} showBackToListLink
|
|
36
35
|
* @property {string} listLink
|
|
@@ -91,6 +90,7 @@ export class Question {
|
|
|
91
90
|
title: '',
|
|
92
91
|
text: ''
|
|
93
92
|
};
|
|
93
|
+
#isInManageListSection = false;
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
96
|
* @param {import('#question-types').QuestionParameters} params
|
|
@@ -150,8 +150,57 @@ export class Question {
|
|
|
150
150
|
});
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Is this question a manage list question?
|
|
155
|
+
* Implemented as a getter so manage list question implementations can override it,
|
|
156
|
+
* but it cannot be changed at runtime.
|
|
157
|
+
*
|
|
158
|
+
* @returns {boolean}
|
|
159
|
+
*/
|
|
160
|
+
get isManageListQuestion() {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Is this question added to a ManageListSection?
|
|
166
|
+
*
|
|
167
|
+
* @returns {boolean}
|
|
168
|
+
*/
|
|
169
|
+
get isInManageListSection() {
|
|
170
|
+
return this.#isInManageListSection;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
set isInManageListSection(value) {
|
|
174
|
+
if (!value) {
|
|
175
|
+
throw new Error('Question isInManageListSection is false by default');
|
|
176
|
+
}
|
|
177
|
+
this.#isInManageListSection = value;
|
|
178
|
+
}
|
|
179
|
+
|
|
153
180
|
/**
|
|
154
181
|
* gets the view model for this question
|
|
182
|
+
*
|
|
183
|
+
* Wraps prepQuestionForRendering to add the back link - which requires more parameters
|
|
184
|
+
* that prepQuestionForRendering doesn't need
|
|
185
|
+
*
|
|
186
|
+
* @param {Object} options
|
|
187
|
+
* @param {import('#src/journey/journey-types.d.ts').RouteParams} options.params
|
|
188
|
+
* @param {import('#src/components/manage-list/question.js')} [options.manageListQuestion]
|
|
189
|
+
* @param {Section} section - the current section
|
|
190
|
+
* @param {Journey} journey - the journey we are in
|
|
191
|
+
* @param {Record<string, unknown>} [customViewData] additional data to send to view
|
|
192
|
+
* @param {unknown} [payload]
|
|
193
|
+
* @returns {QuestionViewModel}
|
|
194
|
+
*/
|
|
195
|
+
toViewModel({ params, manageListQuestion, section, journey, customViewData, payload }) {
|
|
196
|
+
const viewModel = this.prepQuestionForRendering(section, journey, customViewData, payload);
|
|
197
|
+
viewModel.backLink = journey.getBackLink({ params, manageListQuestion });
|
|
198
|
+
return viewModel;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* gets the base view model for this question
|
|
203
|
+
*
|
|
155
204
|
* @param {Section} section - the current section
|
|
156
205
|
* @param {Journey} journey - the journey we are in
|
|
157
206
|
* @param {Record<string, unknown>} [customViewData] additional data to send to view
|
|
@@ -160,8 +209,6 @@ export class Question {
|
|
|
160
209
|
*/
|
|
161
210
|
prepQuestionForRendering(section, journey, customViewData, payload) {
|
|
162
211
|
const answer = journey.response.answers[this.fieldName] || '';
|
|
163
|
-
const backLink = journey.getBackLink(section.segment, this.fieldName);
|
|
164
|
-
|
|
165
212
|
return {
|
|
166
213
|
question: {
|
|
167
214
|
value: answer,
|
|
@@ -179,8 +226,6 @@ export class Question {
|
|
|
179
226
|
layoutTemplate: journey.journeyTemplate,
|
|
180
227
|
pageCaption: section?.name,
|
|
181
228
|
|
|
182
|
-
navigation: ['', backLink],
|
|
183
|
-
backLink,
|
|
184
229
|
showBackToListLink: this.showBackToListLink,
|
|
185
230
|
listLink: journey.taskListUrl,
|
|
186
231
|
journeyTitle: journey.journeyTitle,
|
|
@@ -212,23 +257,26 @@ export class Question {
|
|
|
212
257
|
* check for validation errors
|
|
213
258
|
* @param {import('express').Request} req
|
|
214
259
|
* @param {Journey} journey
|
|
215
|
-
* @param {Section}
|
|
260
|
+
* @param {Section} section
|
|
261
|
+
* @param {import('#src/components/manage-list/question.js')} [manageListQuestion]
|
|
216
262
|
* @returns {QuestionViewModel|undefined} returns the view model for displaying the error or undefined if there are no errors
|
|
217
263
|
*/
|
|
218
|
-
checkForValidationErrors(req,
|
|
264
|
+
checkForValidationErrors(req, section, journey, manageListQuestion) {
|
|
219
265
|
const { body = {} } = req;
|
|
220
266
|
const { errors = {}, errorSummary = [] } = body;
|
|
221
267
|
|
|
222
268
|
if (Object.keys(errors).length > 0) {
|
|
223
|
-
return this.
|
|
224
|
-
|
|
269
|
+
return this.toViewModel({
|
|
270
|
+
params: req.params,
|
|
271
|
+
section,
|
|
225
272
|
journey,
|
|
226
|
-
{
|
|
273
|
+
customViewData: {
|
|
227
274
|
errors,
|
|
228
275
|
errorSummary
|
|
229
276
|
},
|
|
230
|
-
body
|
|
231
|
-
|
|
277
|
+
payload: body,
|
|
278
|
+
manageListQuestion
|
|
279
|
+
});
|
|
232
280
|
}
|
|
233
281
|
}
|
|
234
282
|
|