@planning-inspectorate/dynamic-forms 2.3.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 +2 -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 +60 -20
- package/src/controller.test.js +110 -43
- package/src/index.js +3 -2
- package/src/journey/journey-types.d.ts +18 -0
- package/src/journey/journey.js +104 -40
- package/src/journey/journey.test.js +94 -23
- package/src/lib/uuid.js +10 -0
- package/src/lib/uuid.test.js +24 -0
- package/src/middleware/dynamic-req-files-to-req-body-files.js +1 -2
- package/src/middleware/dynamic-req-files-to-req-body-files.test.js +2 -2
- package/src/questions/question-props.d.ts +2 -1
- package/src/questions/question.js +67 -18
- 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 +67 -2
- package/src/section.test.js +176 -1
- package/src/validator/validator.js +1 -3
- package/src/validator/validator.test.js +7 -7
- package/test/journey.js +5 -0
- package/test/mock/journey.js +17 -0
- package/test/questions.js +30 -1
- package/test/questions.test.js +119 -48
- 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/test/utils/test-server.js +31 -9
- package/test/utils/utils.js +2 -1
- package/tsconfig.json +23 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, it, mock, beforeEach } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
3
|
import { Journey } from './journey.js';
|
|
4
|
+
import { Section } from '#src/section.js';
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
+
const mockSectionDetails = [
|
|
6
7
|
{
|
|
7
8
|
segment: 'section1',
|
|
8
9
|
questions: [
|
|
@@ -40,6 +41,11 @@ const mockSections = [
|
|
|
40
41
|
]
|
|
41
42
|
}
|
|
42
43
|
];
|
|
44
|
+
const mockSections = mockSectionDetails.map((data, index) => {
|
|
45
|
+
const section = new Section(`section ${index}`, data.segment);
|
|
46
|
+
section.questions = data.questions;
|
|
47
|
+
return section;
|
|
48
|
+
});
|
|
43
49
|
|
|
44
50
|
describe('Journey class', () => {
|
|
45
51
|
let constructorArgs;
|
|
@@ -193,15 +199,15 @@ describe('Journey class', () => {
|
|
|
193
199
|
});
|
|
194
200
|
});
|
|
195
201
|
|
|
196
|
-
describe('
|
|
202
|
+
describe('getQuestionByParams', () => {
|
|
197
203
|
it('should return the correct question by section and name', () => {
|
|
198
204
|
const journey = new Journey(constructorArgs);
|
|
199
205
|
journey.sections = mockSections;
|
|
200
206
|
|
|
201
|
-
const question = journey.
|
|
202
|
-
mockSections[0].segment,
|
|
203
|
-
mockSections[0].questions[1].fieldName
|
|
204
|
-
);
|
|
207
|
+
const question = journey.getQuestionByParams({
|
|
208
|
+
section: mockSections[0].segment,
|
|
209
|
+
question: mockSections[0].questions[1].fieldName
|
|
210
|
+
});
|
|
205
211
|
|
|
206
212
|
assert.strictEqual(question, mockSections[0].questions[1]);
|
|
207
213
|
});
|
|
@@ -210,7 +216,7 @@ describe('Journey class', () => {
|
|
|
210
216
|
const journey = new Journey(constructorArgs);
|
|
211
217
|
journey.sections = mockSections;
|
|
212
218
|
|
|
213
|
-
const question = journey.
|
|
219
|
+
const question = journey.getQuestionByParams({ section: 'a', question: 'b' });
|
|
214
220
|
|
|
215
221
|
assert.strictEqual(question, undefined);
|
|
216
222
|
});
|
|
@@ -219,7 +225,7 @@ describe('Journey class', () => {
|
|
|
219
225
|
const journey = new Journey(constructorArgs);
|
|
220
226
|
journey.sections = mockSections;
|
|
221
227
|
|
|
222
|
-
const question = journey.
|
|
228
|
+
const question = journey.getQuestionByParams({ section: mockSections[0].segment, question: 'nope' });
|
|
223
229
|
|
|
224
230
|
assert.strictEqual(question, undefined);
|
|
225
231
|
});
|
|
@@ -235,7 +241,7 @@ describe('Journey class', () => {
|
|
|
235
241
|
journey.sections = mockSections;
|
|
236
242
|
journey.returnToListing = false;
|
|
237
243
|
|
|
238
|
-
const backLinks = journey.getBackLink(section.segment, name);
|
|
244
|
+
const backLinks = journey.getBackLink({ params: { section: section.segment, question: name } });
|
|
239
245
|
|
|
240
246
|
assert.strictEqual(backLinks, `${constructorArgs.makeBaseUrl()}/${section.segment}/${prevQuestionName}`);
|
|
241
247
|
});
|
|
@@ -251,7 +257,7 @@ describe('Journey class', () => {
|
|
|
251
257
|
journey.sections = mockSections;
|
|
252
258
|
journey.returnToListing = false;
|
|
253
259
|
|
|
254
|
-
const backLink = journey.getBackLink(section.segment, name
|
|
260
|
+
const backLink = journey.getBackLink({ params: { section: section.segment, question: name } });
|
|
255
261
|
|
|
256
262
|
assert.strictEqual(backLink, `${constructorArgs.makeBaseUrl()}/${prevSection.segment}/${prevQuestionName}`);
|
|
257
263
|
});
|
|
@@ -265,7 +271,7 @@ describe('Journey class', () => {
|
|
|
265
271
|
journey.sections = mockSections;
|
|
266
272
|
journey.returnToListing = false;
|
|
267
273
|
|
|
268
|
-
const backLink = journey.getBackLink(section.segment, name
|
|
274
|
+
const backLink = journey.getBackLink({ params: { section: section.segment, question: name } });
|
|
269
275
|
|
|
270
276
|
assert.strictEqual(backLink, null);
|
|
271
277
|
});
|
|
@@ -278,7 +284,7 @@ describe('Journey class', () => {
|
|
|
278
284
|
journey.sections = mockSections;
|
|
279
285
|
journey.returnToListing = false;
|
|
280
286
|
|
|
281
|
-
const backLink = journey.getBackLink(section.segment, name
|
|
287
|
+
const backLink = journey.getBackLink({ params: { section: section.segment, question: name } });
|
|
282
288
|
|
|
283
289
|
assert.strictEqual(backLink, '/some/back/link');
|
|
284
290
|
});
|
|
@@ -294,7 +300,7 @@ describe('Journey class', () => {
|
|
|
294
300
|
journey.sections = mockSections;
|
|
295
301
|
journey.returnToListing = returnToListing;
|
|
296
302
|
|
|
297
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(section, name, false);
|
|
303
|
+
const nextQuestionUrl = journey.getNextQuestionUrl({ section: section, question: name }, { reverse: false });
|
|
298
304
|
|
|
299
305
|
assert.strictEqual(nextQuestionUrl, null);
|
|
300
306
|
});
|
|
@@ -309,7 +315,7 @@ describe('Journey class', () => {
|
|
|
309
315
|
journey.sections = mockSections;
|
|
310
316
|
journey.returnToListing = returnToListing;
|
|
311
317
|
|
|
312
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(section, name, false);
|
|
318
|
+
const nextQuestionUrl = journey.getNextQuestionUrl({ section: section, question: name }, { reverse: false });
|
|
313
319
|
|
|
314
320
|
assert.strictEqual(nextQuestionUrl, null);
|
|
315
321
|
});
|
|
@@ -325,7 +331,10 @@ describe('Journey class', () => {
|
|
|
325
331
|
journey.sections = mockSections;
|
|
326
332
|
journey.returnToListing = returnToListing;
|
|
327
333
|
|
|
328
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
334
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
335
|
+
{ section: section.segment, question: name },
|
|
336
|
+
{ reverse: false }
|
|
337
|
+
);
|
|
329
338
|
|
|
330
339
|
assert.strictEqual(nextQuestionUrl, `${constructorArgs.makeBaseUrl()}/${section.segment}/${nextQuestionName}`);
|
|
331
340
|
});
|
|
@@ -350,7 +359,10 @@ describe('Journey class', () => {
|
|
|
350
359
|
journey.sections = mockSections;
|
|
351
360
|
journey.returnToListing = returnToListing;
|
|
352
361
|
|
|
353
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
362
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
363
|
+
{ section: section.segment, question: name },
|
|
364
|
+
{ reverse: false }
|
|
365
|
+
);
|
|
354
366
|
|
|
355
367
|
assert.strictEqual(nextQuestionUrl, `${constructorArgs.makeBaseUrl()}/${section.segment}/${nextQuestionName}`);
|
|
356
368
|
});
|
|
@@ -366,7 +378,10 @@ describe('Journey class', () => {
|
|
|
366
378
|
journey.sections = mockSections;
|
|
367
379
|
journey.returnToListing = returnToListing;
|
|
368
380
|
|
|
369
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
381
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
382
|
+
{ section: section.segment, question: name },
|
|
383
|
+
{ reverse: true }
|
|
384
|
+
);
|
|
370
385
|
|
|
371
386
|
assert.strictEqual(nextQuestionUrl, `${constructorArgs.makeBaseUrl()}/${section.segment}/${prevQuestionName}`);
|
|
372
387
|
});
|
|
@@ -381,7 +396,10 @@ describe('Journey class', () => {
|
|
|
381
396
|
journey.sections = mockSections;
|
|
382
397
|
journey.returnToListing = returnToListing;
|
|
383
398
|
|
|
384
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
399
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
400
|
+
{ section: section.segment, question: name },
|
|
401
|
+
{ reverse: false }
|
|
402
|
+
);
|
|
385
403
|
|
|
386
404
|
assert.strictEqual(nextQuestionUrl, null);
|
|
387
405
|
});
|
|
@@ -398,7 +416,10 @@ describe('Journey class', () => {
|
|
|
398
416
|
journey.sections = mockSections;
|
|
399
417
|
journey.returnToListing = false;
|
|
400
418
|
|
|
401
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
419
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
420
|
+
{ section: section.segment, question: name },
|
|
421
|
+
{ reverse: true }
|
|
422
|
+
);
|
|
402
423
|
|
|
403
424
|
assert.strictEqual(
|
|
404
425
|
nextQuestionUrl,
|
|
@@ -416,7 +437,10 @@ describe('Journey class', () => {
|
|
|
416
437
|
journey.sections = mockSections;
|
|
417
438
|
journey.returnToListing = returnToListing;
|
|
418
439
|
|
|
419
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
440
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
441
|
+
{ section: section.segment, question: name },
|
|
442
|
+
{ reverse: true }
|
|
443
|
+
);
|
|
420
444
|
|
|
421
445
|
assert.strictEqual(nextQuestionUrl, null);
|
|
422
446
|
});
|
|
@@ -431,7 +455,10 @@ describe('Journey class', () => {
|
|
|
431
455
|
journey.sections = mockSections;
|
|
432
456
|
journey.returnToListing = true;
|
|
433
457
|
|
|
434
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
458
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
459
|
+
{ section: section.segment, question: name },
|
|
460
|
+
{ reverse: false }
|
|
461
|
+
);
|
|
435
462
|
assert.strictEqual(nextQuestionUrl, null);
|
|
436
463
|
});
|
|
437
464
|
}
|
|
@@ -445,7 +472,10 @@ describe('Journey class', () => {
|
|
|
445
472
|
journey.sections = mockSections;
|
|
446
473
|
journey.returnToListing = true;
|
|
447
474
|
|
|
448
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
475
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
476
|
+
{ section: section.segment, question: name },
|
|
477
|
+
{ reverse: true }
|
|
478
|
+
);
|
|
449
479
|
|
|
450
480
|
assert.strictEqual(nextQuestionUrl, null);
|
|
451
481
|
});
|
|
@@ -460,10 +490,51 @@ describe('Journey class', () => {
|
|
|
460
490
|
const journey = new Journey(constructorArgs);
|
|
461
491
|
journey.sections = mockSections;
|
|
462
492
|
|
|
463
|
-
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
493
|
+
const nextQuestionUrl = journey.getNextQuestionUrl(
|
|
494
|
+
{ section: section.segment, question: name },
|
|
495
|
+
{ reverse: false }
|
|
496
|
+
);
|
|
464
497
|
|
|
465
498
|
assert.strictEqual(nextQuestionUrl, `base/${section.segment}/${nextQuestionName}?id=1`);
|
|
466
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
|
+
});
|
|
467
538
|
});
|
|
468
539
|
|
|
469
540
|
describe('getCurrentQuestionUrl', () => {
|
package/src/lib/uuid.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Uuid } from '#src/lib/uuid.js';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import assert from 'node:assert';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
*
|
|
7
|
+
* @param {import('node:test').TestContext} ctx
|
|
8
|
+
* @param {string} [uuid]
|
|
9
|
+
*/
|
|
10
|
+
export function mockRandomUUID(ctx, uuid = '00000000-0000-0000-0000-000000000000') {
|
|
11
|
+
const original = Uuid.randomUUID;
|
|
12
|
+
Uuid.randomUUID = ctx.mock.fn(() => uuid);
|
|
13
|
+
// restore after the tests
|
|
14
|
+
ctx.after(() => (Uuid.randomUUID = original));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('uuid', () => {
|
|
18
|
+
it('should return a valid v4 UUID', () => {
|
|
19
|
+
assert.match(
|
|
20
|
+
Uuid.randomUUID(),
|
|
21
|
+
/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -16,10 +16,9 @@ export default () => (req, res, next) => {
|
|
|
16
16
|
return next();
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const { section, question } = req.params;
|
|
20
19
|
const { journey } = res.locals;
|
|
21
20
|
|
|
22
|
-
const questionObj = journey.
|
|
21
|
+
const questionObj = journey.getQuestionByParams(req.params);
|
|
23
22
|
const filesPropertyPath = questionObj.fieldName;
|
|
24
23
|
|
|
25
24
|
if (typeof req.files[filesPropertyPath] === 'undefined') {
|
|
@@ -8,7 +8,7 @@ describe('dynamic-forms/middleware/dynamic-req-files-to-req-body-files', () => {
|
|
|
8
8
|
let mockRes;
|
|
9
9
|
let filesPropertyPath = `example-files-property-path-${counter}`;
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const mockgetQuestionByParams = mock.fn(() => {
|
|
12
12
|
return { fieldName: filesPropertyPath };
|
|
13
13
|
});
|
|
14
14
|
|
|
@@ -24,7 +24,7 @@ describe('dynamic-forms/middleware/dynamic-req-files-to-req-body-files', () => {
|
|
|
24
24
|
cookie: mock.fn(),
|
|
25
25
|
locals: {
|
|
26
26
|
journeyResponse: mock.fn(),
|
|
27
|
-
journey: {
|
|
27
|
+
journey: { getQuestionByParams: mockgetQuestionByParams }
|
|
28
28
|
},
|
|
29
29
|
redirect: mock.fn(),
|
|
30
30
|
render: mock.fn(),
|
|
@@ -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
|
|
|
@@ -279,19 +327,20 @@ export class Question {
|
|
|
279
327
|
}
|
|
280
328
|
|
|
281
329
|
/**
|
|
282
|
-
* Handles redirect after saving
|
|
330
|
+
* Handles redirect after saving. Kept around for backwards compatibility.
|
|
331
|
+
*
|
|
283
332
|
* @param {import('express').Response} res
|
|
284
333
|
* @param {Journey} journey
|
|
285
334
|
* @param {string} sectionSegment
|
|
286
335
|
* @param {string} questionSegment
|
|
287
336
|
* @returns {void}
|
|
337
|
+
* @deprecated - use `journey.redirectToNextQuestion`
|
|
288
338
|
*/
|
|
289
339
|
handleNextQuestion(res, journey, sectionSegment, questionSegment) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
return res.redirect(next);
|
|
340
|
+
return journey.redirectToNextQuestion(res, {
|
|
341
|
+
section: sectionSegment,
|
|
342
|
+
question: questionSegment
|
|
343
|
+
});
|
|
295
344
|
}
|
|
296
345
|
|
|
297
346
|
/**
|
|
@@ -63,6 +63,7 @@ describe('./src/dynamic-forms/question.js', () => {
|
|
|
63
63
|
assert.strictEqual(question.validators, VALIDATORS);
|
|
64
64
|
assert.strictEqual(question.html, HTML);
|
|
65
65
|
assert.strictEqual(question.hint, HINT);
|
|
66
|
+
assert.strictEqual(question.isManageListQuestion, false);
|
|
66
67
|
});
|
|
67
68
|
it('should support viewData field', () => {
|
|
68
69
|
const question = getTestQuestion({ viewData: { test: 'test' } });
|
|
@@ -110,6 +111,30 @@ describe('./src/dynamic-forms/question.js', () => {
|
|
|
110
111
|
});
|
|
111
112
|
});
|
|
112
113
|
|
|
114
|
+
describe('isInManageListSection', () => {
|
|
115
|
+
it('should default to false', () => {
|
|
116
|
+
const question = getTestQuestion();
|
|
117
|
+
assert.strictEqual(question.isInManageListSection, false);
|
|
118
|
+
});
|
|
119
|
+
it('should support setting isInManageListSection', () => {
|
|
120
|
+
const question = getTestQuestion();
|
|
121
|
+
assert.strictEqual(question.isInManageListSection, false);
|
|
122
|
+
question.isInManageListSection = true;
|
|
123
|
+
assert.strictEqual(question.isInManageListSection, true);
|
|
124
|
+
});
|
|
125
|
+
it('should not support setting isInManageListSection false', () => {
|
|
126
|
+
const question = getTestQuestion();
|
|
127
|
+
assert.strictEqual(question.isInManageListSection, false);
|
|
128
|
+
assert.throws(
|
|
129
|
+
() => (question.isInManageListSection = false),
|
|
130
|
+
(error) => {
|
|
131
|
+
assert.strictEqual(error.message, 'Question isInManageListSection is false by default');
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
113
138
|
describe('prepQuestionForRendering', () => {
|
|
114
139
|
it('should prepQuestionForRendering', () => {
|
|
115
140
|
const question = getTestQuestion();
|
|
@@ -150,8 +175,6 @@ describe('./src/dynamic-forms/question.js', () => {
|
|
|
150
175
|
assert.deepStrictEqual(result.answer, journey.response.answers[question.fieldName]);
|
|
151
176
|
assert.deepStrictEqual(result.layoutTemplate, journey.journeyTemplate);
|
|
152
177
|
assert.deepStrictEqual(result.pageCaption, section.name);
|
|
153
|
-
assert.deepStrictEqual(result.navigation, ['', 'back']);
|
|
154
|
-
assert.deepStrictEqual(result.backLink, 'back');
|
|
155
178
|
assert.deepStrictEqual(result.showBackToListLink, question.showBackToListLink);
|
|
156
179
|
assert.deepStrictEqual(result.listLink, journey.taskListUrl);
|
|
157
180
|
assert.deepStrictEqual(result.journeyTitle, journey.journeyTitle);
|
|
@@ -205,7 +228,7 @@ describe('./src/dynamic-forms/question.js', () => {
|
|
|
205
228
|
const expectedResult = { a: 1 };
|
|
206
229
|
const req = { body: { errors: { error: 'we have an error' } } };
|
|
207
230
|
const question = getTestQuestion();
|
|
208
|
-
question.
|
|
231
|
+
question.toViewModel = mock.fn(() => expectedResult);
|
|
209
232
|
|
|
210
233
|
const result = question.checkForValidationErrors(req);
|
|
211
234
|
|
|
@@ -15,6 +15,7 @@ import EmailQuestion from '../components/email/question.js';
|
|
|
15
15
|
import { COMPONENT_TYPES } from '../index.js';
|
|
16
16
|
import TextEntryRedactQuestion from '../components/text-entry-redact/question.js';
|
|
17
17
|
import DateTimeQuestion from '../components/date-time/question.js';
|
|
18
|
+
import ManageListQuestion from '../components/manage-list/question.js';
|
|
18
19
|
|
|
19
20
|
// This looks a bit grim because so few of our
|
|
20
21
|
// Questions overlap with Question correctly.
|
|
@@ -35,5 +36,6 @@ export const questionClasses = Object.freeze({
|
|
|
35
36
|
[COMPONENT_TYPES.MULTI_FIELD_INPUT]: MultiFieldInputQuestion,
|
|
36
37
|
[COMPONENT_TYPES.NUMBER]: NumberEntryQuestion,
|
|
37
38
|
[COMPONENT_TYPES.UNIT_OPTION]: UnitOptionEntryQuestion,
|
|
38
|
-
[COMPONENT_TYPES.EMAIL]: EmailQuestion
|
|
39
|
+
[COMPONENT_TYPES.EMAIL]: EmailQuestion,
|
|
40
|
+
[COMPONENT_TYPES.MANAGE_LIST]: ManageListQuestion
|
|
39
41
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { JourneyResponse } from '#src/journey/journey-response.js';
|
|
2
|
+
import ManageListQuestion from '#src/components/manage-list/question.js';
|
|
3
|
+
import { Question } from '#src/questions/question.js';
|
|
4
|
+
|
|
5
|
+
export interface GetNextQuestionParams {
|
|
6
|
+
questionFieldName: string;
|
|
7
|
+
response: JourneyResponse;
|
|
8
|
+
// if this is part of a manage list section
|
|
9
|
+
manageListQuestion?: ManageListQuestion;
|
|
10
|
+
takeNextQuestion: boolean;
|
|
11
|
+
// to get previous question instead of next
|
|
12
|
+
reverse: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface StaticGetNextQuestionParams extends GetNextQuestionParams {
|
|
16
|
+
manageListQuestion: undefined;
|
|
17
|
+
questions: Question[];
|
|
18
|
+
}
|
package/src/section.js
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
import RequiredValidator from './validator/required-validator.js';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* A value indicating the final question of a section has been reached
|
|
11
|
+
*
|
|
12
|
+
* @type {Symbol}
|
|
13
|
+
*/
|
|
14
|
+
export const END_OF_SECTION = Symbol('END_OF_SECTION');
|
|
15
|
+
|
|
9
16
|
/**
|
|
10
17
|
* Defines a section for a questionnaire, a set of Questions
|
|
11
18
|
* @class
|
|
@@ -75,13 +82,20 @@ export class Section {
|
|
|
75
82
|
|
|
76
83
|
/**
|
|
77
84
|
* Fluent API method for adding questions
|
|
78
|
-
* @param {
|
|
85
|
+
* @param {import('#src/questions/question.js').Question} question
|
|
86
|
+
* @param {import('#src/components/manage-list/manage-list-section.js').ManageListSection} [manageListSection]
|
|
79
87
|
* @returns {Section}
|
|
80
88
|
*/
|
|
81
|
-
addQuestion(question) {
|
|
89
|
+
addQuestion(question, manageListSection) {
|
|
82
90
|
if (!question) {
|
|
83
91
|
throw new Error('question is required');
|
|
84
92
|
}
|
|
93
|
+
if (question.isManageListQuestion) {
|
|
94
|
+
if (!manageListSection || !manageListSection.isManageListSection) {
|
|
95
|
+
throw new Error('manage list questions require a ManageListSection');
|
|
96
|
+
}
|
|
97
|
+
question.section = manageListSection;
|
|
98
|
+
}
|
|
85
99
|
this.questions.push(question);
|
|
86
100
|
this.#conditionAdded = false; // reset condition flag
|
|
87
101
|
this.#applyConditions(question);
|
|
@@ -183,6 +197,57 @@ export class Section {
|
|
|
183
197
|
return this;
|
|
184
198
|
}
|
|
185
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Get the next question in this section given a questionParam (question fieldName)
|
|
202
|
+
* @param {import('./section-types.d.ts').GetNextQuestionParams} params
|
|
203
|
+
* @returns {Question|Symbol|null}
|
|
204
|
+
*/
|
|
205
|
+
getNextQuestion(params) {
|
|
206
|
+
const { manageListQuestion } = params;
|
|
207
|
+
if (manageListQuestion) {
|
|
208
|
+
// first check if the next question is within the manage list section
|
|
209
|
+
const next = Section.getNextQuestion({
|
|
210
|
+
...params,
|
|
211
|
+
questions: manageListQuestion.section.questions
|
|
212
|
+
});
|
|
213
|
+
if (next === END_OF_SECTION) {
|
|
214
|
+
// after the manage list section questions, go back to the manage list question
|
|
215
|
+
return manageListQuestion;
|
|
216
|
+
}
|
|
217
|
+
return next;
|
|
218
|
+
}
|
|
219
|
+
return Section.getNextQuestion({
|
|
220
|
+
...params,
|
|
221
|
+
questions: this.questions
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Implementation of getNextQuestion given a list of questions
|
|
227
|
+
*
|
|
228
|
+
* @param {import('./section-types.d.ts').StaticGetNextQuestionParams} params
|
|
229
|
+
* @returns {Question|Symbol|null}
|
|
230
|
+
*/
|
|
231
|
+
static getNextQuestion({ questions, questionFieldName, response, takeNextQuestion = false, reverse = false }) {
|
|
232
|
+
const numberOfQuestions = questions.length;
|
|
233
|
+
|
|
234
|
+
const questionsStart = reverse ? numberOfQuestions - 1 : 0;
|
|
235
|
+
for (let i = questionsStart; reverse ? i >= 0 : i < numberOfQuestions; reverse ? i-- : i++) {
|
|
236
|
+
const question = questions[i];
|
|
237
|
+
if (takeNextQuestion && question.shouldDisplay(response)) {
|
|
238
|
+
return question;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (question.fieldName === questionFieldName || question.url === questionFieldName) {
|
|
242
|
+
takeNextQuestion = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (takeNextQuestion) {
|
|
246
|
+
return END_OF_SECTION;
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
186
251
|
/**
|
|
187
252
|
* checks answers on response to ensure that a answer is provided for each required question in the section
|
|
188
253
|
* @param {JourneyResponse} journeyResponse
|