@planning-inspectorate/dynamic-forms 3.16.0 → 3.17.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 +112 -1
- package/package.json +1 -1
- package/src/components/address/question.d.ts +1 -9
- package/src/components/address/question.d.ts.map +1 -1
- package/src/components/address/question.js +6 -17
- package/src/components/checkbox/question.d.ts +1 -21
- package/src/components/checkbox/question.d.ts.map +1 -1
- package/src/components/checkbox/question.js +36 -49
- package/src/components/checkbox/question.test.js +170 -27
- package/src/components/date/question.d.ts +0 -8
- package/src/components/date/question.d.ts.map +1 -1
- package/src/components/date/question.js +6 -14
- package/src/components/date-period/question.d.ts +1 -5
- package/src/components/date-period/question.d.ts.map +1 -1
- package/src/components/date-period/question.js +1 -1
- package/src/components/date-time/question.d.ts +0 -8
- package/src/components/date-time/question.d.ts.map +1 -1
- package/src/components/date-time/question.js +4 -12
- package/src/components/manage-list/answer-summary-list.njk +1 -1
- package/src/components/manage-list/question.d.ts +1 -5
- package/src/components/manage-list/question.d.ts.map +1 -1
- package/src/components/manage-list/question.js +1 -1
- package/src/components/number-entry/question.d.ts +0 -28
- package/src/components/number-entry/question.d.ts.map +1 -1
- package/src/components/number-entry/question.js +3 -21
- package/src/components/radio/question.d.ts +1 -9
- package/src/components/radio/question.d.ts.map +1 -1
- package/src/components/radio/question.js +29 -46
- package/src/components/radio/question.test.js +139 -60
- package/src/components/select/question.d.ts +1 -15
- package/src/components/select/question.d.ts.map +1 -1
- package/src/components/select/question.js +4 -47
- package/src/components/single-line-input/question.d.ts +0 -4
- package/src/components/single-line-input/question.d.ts.map +1 -1
- package/src/components/single-line-input/question.js +1 -1
- package/src/components/text-entry/question.d.ts +0 -4
- package/src/components/text-entry/question.d.ts.map +1 -1
- package/src/components/text-entry/question.js +1 -1
- package/src/components/text-entry-redact/question.d.ts +2 -6
- package/src/components/text-entry-redact/question.d.ts.map +1 -1
- package/src/components/text-entry-redact/question.js +1 -1
- package/src/components/utils/question-has-answer.js +1 -1
- package/src/components/utils/question-utils.d.ts +19 -1
- package/src/components/utils/question-utils.d.ts.map +1 -1
- package/src/components/utils/question-utils.js +31 -2
- package/src/components/utils/question-utils.test.js +89 -2
- package/src/questions/options-question.d.ts +4 -4
- package/src/questions/options-question.d.ts.map +1 -1
- package/src/questions/options-question.js +27 -2
- package/src/questions/options-question.test.js +106 -0
- package/src/questions/question.d.ts +16 -1
- package/src/questions/question.d.ts.map +1 -1
- package/src/questions/question.js +64 -9
- package/src/questions/question.test.js +86 -0
- package/test/journey.js +2 -0
- package/test/questions.js +50 -0
- package/test/snapshots/check-your-answers-not-started.html +22 -0
- package/test/snapshots/check-your-answers-with-answers.html +22 -0
- package/test/snapshots/favourite-activity-reason.html +54 -0
- package/test/snapshots/travel-requirements.html +48 -0
- package/test/utils/question-test-utils.js +36 -6
- package/typedefs/question-types.d.ts +27 -0
package/README.md
CHANGED
|
@@ -286,6 +286,117 @@ const questionProps = {
|
|
|
286
286
|
const questions = createQuestions(questionProps, questionClasses, {});
|
|
287
287
|
```
|
|
288
288
|
|
|
289
|
+
### Custom Summary Formatting
|
|
290
|
+
|
|
291
|
+
The `formatSummaryValue` option allows you to customise how answers are displayed on check-your-answers pages without needing to create a custom question class.
|
|
292
|
+
|
|
293
|
+
#### Basic Usage
|
|
294
|
+
|
|
295
|
+
Pass a `formatSummaryValue` function when creating a question:
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
import { RadioQuestion } from '@planning-inspectorate/dynamic-forms';
|
|
299
|
+
|
|
300
|
+
new RadioQuestion({
|
|
301
|
+
fieldName: 'status',
|
|
302
|
+
title: 'Application status',
|
|
303
|
+
question: 'What is the application status?',
|
|
304
|
+
options: [
|
|
305
|
+
{ text: 'Approved', value: 'approved' },
|
|
306
|
+
{ text: 'Rejected', value: 'rejected' }
|
|
307
|
+
],
|
|
308
|
+
formatSummaryValue: ({ formattedAnswer }) => `<strong>${formattedAnswer}</strong>`
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
#### Formatter Context
|
|
313
|
+
|
|
314
|
+
The formatter function receives a context object with the following properties:
|
|
315
|
+
|
|
316
|
+
| Property | Type | Description |
|
|
317
|
+
|----------|------|------------------------------------------------|
|
|
318
|
+
| `answer` | `unknown` | The raw answer value |
|
|
319
|
+
| `formattedAnswer` | `string` | The default formatted display value |
|
|
320
|
+
| `question` | `Question` | The question instance |
|
|
321
|
+
| `journey` | `Journey` | The journey instance |
|
|
322
|
+
| `sectionSegment` | `string` | The current section segment |
|
|
323
|
+
| `selectedOptions` | `Option[] \| undefined` | For OptionsQuestions (such as Checkbox or Radio), the matched option object(s) as an array |
|
|
324
|
+
|
|
325
|
+
#### Answer Formats
|
|
326
|
+
|
|
327
|
+
Different options-based questions store answers in different formats. The `selectedOptions` array normalises these formats for your formatter:
|
|
328
|
+
|
|
329
|
+
| Question Type | Answer Format | `selectedOptions` Result |
|
|
330
|
+
|---------------|---------------|--------------------------|
|
|
331
|
+
| **Radio / Select** | Single string value: `"approved"` | `[{ text: 'Approved', value: 'approved' }]` |
|
|
332
|
+
| **Radio / Select** | Conditional object: `{ value: "yes", conditional: "details" }` | `[{ text: 'Yes', value: 'yes', conditional: {...} }]` |
|
|
333
|
+
| **Checkbox** | Comma-separated string: `"option1,option2"` | `[{ text: 'Option 1', value: 'option1' }, { text: 'Option 2', value: 'option2' }]` |
|
|
334
|
+
| **Checkbox** | Single conditional object: `{ value: "other", conditional: "details" }` | `[{ text: 'Other', value: 'other', conditional: {...} }]` |
|
|
335
|
+
|
|
336
|
+
For single-select questions (Radio, Select), use `selectedOptions[0]` to access the selected option. For multi-select questions (Checkbox), iterate over the array.
|
|
337
|
+
|
|
338
|
+
#### Examples
|
|
339
|
+
|
|
340
|
+
**Bold formatting:**
|
|
341
|
+
|
|
342
|
+
```javascript
|
|
343
|
+
formatSummaryValue: ({ formattedAnswer }) => `<strong>${escapeHtml(formattedAnswer)}</strong>`
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Conditional styling based on answer:**
|
|
347
|
+
|
|
348
|
+
```javascript
|
|
349
|
+
formatSummaryValue: ({ selectedOptions }) => {
|
|
350
|
+
const selectedOption = selectedOptions[0];
|
|
351
|
+
const color = selectedOption?.value === 'approved' ? 'green' : 'red';
|
|
352
|
+
return `<span style="color: ${color}">${escapeHtml(selectedOption?.text ?? '-')}</span>`;
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Adding a suffix from another answer:**
|
|
357
|
+
|
|
358
|
+
```javascript
|
|
359
|
+
const suffixes = { solid: ' (tonnes)', liquid: ' (litres)' };
|
|
360
|
+
|
|
361
|
+
formatSummaryValue: ({ answer, formattedAnswer }) => {
|
|
362
|
+
const suffix = suffixes[answer] ?? '';
|
|
363
|
+
return `${formattedAnswer}${suffix}`;
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Accessing other journey answers:**
|
|
368
|
+
|
|
369
|
+
```javascript
|
|
370
|
+
formatSummaryValue: ({ formattedAnswer, journey }) => {
|
|
371
|
+
const capacity = journey.response.answers.wasteCapacity;
|
|
372
|
+
return capacity ? `${formattedAnswer}: ${capacity}` : formattedAnswer;
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Using selectedOptions for single-select questions (Radio/Select):**
|
|
377
|
+
|
|
378
|
+
```javascript
|
|
379
|
+
formatSummaryValue: ({ selectedOptions }) => {
|
|
380
|
+
const selectedOption = selectedOptions[0];
|
|
381
|
+
// Access the full option object including any custom properties
|
|
382
|
+
const warning = selectedOption?.warning;
|
|
383
|
+
return warning ? `${escapeHtml(selectedOption?.text ?? '-')} ⚠️` : escapeHtml(selectedOption?.text ?? '-');
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
**Using selectedOptions for multi-select questions (Checkbox):**
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
formatSummaryValue: ({ selectedOptions }) => {
|
|
391
|
+
// Join all selected option texts with custom formatting
|
|
392
|
+
return selectedOptions
|
|
393
|
+
.map(opt => `<span class="tag">${escapeHtml(opt.text)}</span>`)
|
|
394
|
+
.join(' ');
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
> **Note:** `formattedAnswer` is already escaped by dynamic-forms; you are responsible for escaping any additional user-controlled values you include (for example `answer`, option text from `selectedOptions`, or values read from `journey.response.answers`).
|
|
399
|
+
|
|
289
400
|
## Contributing
|
|
290
401
|
|
|
291
402
|
When contributing to this package, ensure changes are generic and not service-specific. Speak to the R&D devs if you are not sure. Prefer configuration over hardcoding values, and ensure the code is well documented.
|
|
@@ -321,7 +432,7 @@ To update any snapshots with rendering changes (or new questions), run `node --t
|
|
|
321
432
|
|
|
322
433
|
When implement a new component, extend the base `Question` class. Common methods to override are:
|
|
323
434
|
|
|
324
|
-
* `
|
|
435
|
+
* `formatAnswer` - used for check-your-answers display
|
|
325
436
|
* `getDataToSave` - answer data to save
|
|
326
437
|
* `addCustomDataToViewModel` - customise the view model with extra data/configuration
|
|
327
438
|
* `answerForViewModel` - customise the view model answer value
|
package/package.json
CHANGED
|
@@ -22,10 +22,6 @@ export class AddressQuestion extends Question {
|
|
|
22
22
|
* @returns {*|string}
|
|
23
23
|
*/
|
|
24
24
|
answerForViewModel(answers: Record<string, any>): any | string;
|
|
25
|
-
/**
|
|
26
|
-
* @param {import('#question').QuestionViewModel} viewModel
|
|
27
|
-
*/
|
|
28
|
-
addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
|
|
29
25
|
/**
|
|
30
26
|
* @param {Object<string, any>} answer
|
|
31
27
|
* @returns The formatted address to be presented in the UI
|
|
@@ -36,11 +32,7 @@ export class AddressQuestion extends Question {
|
|
|
36
32
|
/**
|
|
37
33
|
* returns the formatted answers values to be used to build task list elements
|
|
38
34
|
*/
|
|
39
|
-
|
|
40
|
-
key: string;
|
|
41
|
-
value: string;
|
|
42
|
-
action: import("../../index.js").ActionView | import("../../index.js").ActionView[] | undefined;
|
|
43
|
-
}[];
|
|
35
|
+
formatAnswer(answer: any): string;
|
|
44
36
|
formatLabelFromRequiredFields(fieldName: any): "" | " (optional)";
|
|
45
37
|
}
|
|
46
38
|
export default AddressQuestion;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAOA;IACC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,yBAAyB,EAqB3E;IAXE;;;;;;kBAA8C;IAIhD;;;;;;MAMC;IAiBF;;;OAGG;IACH,4BAHW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjB,GAAC,GAAC,MAAM,CAqBpB;
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAOA;IACC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,yBAAyB,EAqB3E;IAXE;;;;;;kBAA8C;IAIhD;;;;;;MAMC;IAiBF;;;OAGG;IACH,4BAHW,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjB,GAAC,GAAC,MAAM,CAqBpB;IAqCD;;;OAGG;IACH;;eAUC;IAED;;OAEG;IACH,kCAIC;IAED,kEAMC;CACD;;yBA1IwB,WAAW"}
|
|
@@ -70,7 +70,7 @@ export class AddressQuestion extends Question {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
* @param {import('#question').QuestionViewModel} viewModel
|
|
73
|
+
* @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
|
|
74
74
|
*/
|
|
75
75
|
addCustomDataToViewModel(viewModel) {
|
|
76
76
|
viewModel.question.labels = this.addressLabels;
|
|
@@ -123,23 +123,12 @@ export class AddressQuestion extends Question {
|
|
|
123
123
|
/**
|
|
124
124
|
* returns the formatted answers values to be used to build task list elements
|
|
125
125
|
*/
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
formattedAnswer = nl2br(escape(this.format(answer)));
|
|
131
|
-
} else if (answer === null) {
|
|
132
|
-
formattedAnswer = '';
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return [
|
|
136
|
-
{
|
|
137
|
-
key: `${this.title}`,
|
|
138
|
-
value: formattedAnswer,
|
|
139
|
-
action: this.getAction(sectionSegment, journey, answer)
|
|
140
|
-
}
|
|
141
|
-
];
|
|
126
|
+
formatAnswer(answer) {
|
|
127
|
+
if (answer === null) return '';
|
|
128
|
+
if (!answer) return this.notStartedText;
|
|
129
|
+
return nl2br(escape(this.format(answer)));
|
|
142
130
|
}
|
|
131
|
+
|
|
143
132
|
formatLabelFromRequiredFields(fieldName) {
|
|
144
133
|
if (this.requiredFields && this.requiredFields[fieldName]) {
|
|
145
134
|
return '';
|
|
@@ -1,29 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @typedef ConditionalAnswerObject
|
|
3
|
-
* @type {object}
|
|
4
|
-
* @property {string} value the checkbox answer
|
|
5
|
-
* @property {string} conditional the conditional text input
|
|
6
|
-
*/
|
|
7
1
|
export class CheckboxQuestion extends OptionsQuestion {
|
|
8
2
|
/**
|
|
9
3
|
* @param {import('#typedefs/question-props.d.ts').CheckboxQuestionParams} params
|
|
10
4
|
*/
|
|
11
|
-
constructor({
|
|
12
|
-
/**
|
|
13
|
-
* returns the formatted answers values to be used to build task list elements
|
|
14
|
-
*/
|
|
15
|
-
formatAnswerForSummary(sectionSegment: any, journey: any, answer: any): import("../../index.js").SummaryRow[];
|
|
5
|
+
constructor({ ...parentParams }: import("#typedefs/question-props.d.ts").CheckboxQuestionParams);
|
|
16
6
|
}
|
|
17
7
|
export default CheckboxQuestion;
|
|
18
|
-
export type ConditionalAnswerObject = {
|
|
19
|
-
/**
|
|
20
|
-
* the checkbox answer
|
|
21
|
-
*/
|
|
22
|
-
value: string;
|
|
23
|
-
/**
|
|
24
|
-
* the conditional text input
|
|
25
|
-
*/
|
|
26
|
-
conditional: string;
|
|
27
|
-
};
|
|
28
8
|
import OptionsQuestion from '../../questions/options-question.js';
|
|
29
9
|
//# sourceMappingURL=question.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAKA
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAKA;IACC;;OAEG;IACH,iCAFW,OAAO,+BAA+B,EAAE,sBAAsB,EAUxE;CA4CD;;4BA5D2B,qCAAqC"}
|
|
@@ -1,76 +1,63 @@
|
|
|
1
|
+
import escape from 'escape-html';
|
|
1
2
|
import OptionsQuestion from '../../questions/options-question.js';
|
|
2
|
-
import questionUtils from '../utils/question-utils.js';
|
|
3
3
|
|
|
4
4
|
const defaultOptionJoinString = ',';
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* @typedef ConditionalAnswerObject
|
|
8
|
-
* @type {object}
|
|
9
|
-
* @property {string} value the checkbox answer
|
|
10
|
-
* @property {string} conditional the conditional text input
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
6
|
export class CheckboxQuestion extends OptionsQuestion {
|
|
14
7
|
/**
|
|
15
8
|
* @param {import('#typedefs/question-props.d.ts').CheckboxQuestionParams} params
|
|
16
9
|
*/
|
|
17
|
-
constructor({
|
|
10
|
+
constructor({ ...parentParams }) {
|
|
18
11
|
super({
|
|
19
|
-
|
|
20
|
-
question,
|
|
12
|
+
...parentParams,
|
|
21
13
|
viewFolder: 'checkbox',
|
|
22
|
-
|
|
23
|
-
url,
|
|
24
|
-
pageTitle,
|
|
25
|
-
description,
|
|
26
|
-
options,
|
|
27
|
-
validators,
|
|
28
|
-
viewData
|
|
14
|
+
capitaliseAnswer: false
|
|
29
15
|
});
|
|
30
16
|
|
|
31
17
|
this.optionJoinString = defaultOptionJoinString;
|
|
32
18
|
}
|
|
33
19
|
|
|
34
20
|
/**
|
|
35
|
-
*
|
|
21
|
+
* Formats an answer value for display in the summary.
|
|
22
|
+
* Handles object answers with conditional fields.
|
|
23
|
+
* - Single value with conditional: { value: 'yes', conditional: { yes: 'details' } }
|
|
24
|
+
* - Multiple values with conditionals: { value: 'yes,no', conditional: { yes: 'details1', no: 'details2' } }
|
|
25
|
+
*
|
|
26
|
+
* @param {unknown} answer - the raw answer value
|
|
27
|
+
* @returns {string} the formatted answer for display
|
|
36
28
|
*/
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
return
|
|
29
|
+
formatAnswer(answer) {
|
|
30
|
+
if (answer === null || answer === undefined || answer === '') {
|
|
31
|
+
return this.notStartedText;
|
|
40
32
|
}
|
|
41
33
|
|
|
42
|
-
//
|
|
43
|
-
if (answer
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const conditionalAnswerText = selectedOption.conditional?.label
|
|
47
|
-
? `${selectedOption.conditional.label} ${answer.conditional}`
|
|
48
|
-
: answer.conditional;
|
|
49
|
-
|
|
50
|
-
const formattedConditionalText = [selectedOption.text, conditionalAnswerText].join('\n');
|
|
51
|
-
|
|
52
|
-
return super.formatAnswerForSummary(sectionSegment, journey, formattedConditionalText, false);
|
|
34
|
+
// Handle simple string answers (e.g. '1,2')
|
|
35
|
+
if (typeof answer !== 'object' || answer.value === undefined) {
|
|
36
|
+
return super.formatAnswer(answer);
|
|
53
37
|
}
|
|
54
38
|
|
|
55
|
-
//
|
|
56
|
-
|
|
39
|
+
// Handle object answers with conditional fields
|
|
40
|
+
// TODO DF-51 treat as an array
|
|
41
|
+
const answerValues = String(answer.value)
|
|
42
|
+
.split(this.optionJoinString)
|
|
43
|
+
.map((v) => v.trim());
|
|
44
|
+
const conditionals = answer.conditional || {};
|
|
45
|
+
|
|
46
|
+
const formattedParts = answerValues.map((value) => {
|
|
47
|
+
const option = this.options.find((opt) => opt.value === value);
|
|
48
|
+
const optionText = escape(option ? option.text : value);
|
|
57
49
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
questionUtils.getConditionalFieldName(this.fieldName, option.conditional.fieldName)
|
|
65
|
-
];
|
|
66
|
-
return [option.text, conditionalAnswer].join('\n');
|
|
67
|
-
}
|
|
50
|
+
// Check if this option has a conditional answer
|
|
51
|
+
const conditionalValue = conditionals[value];
|
|
52
|
+
if (conditionalValue) {
|
|
53
|
+
const label = option?.conditional?.label ? `${escape(option.conditional.label)} ` : '';
|
|
54
|
+
return `${optionText}<br>${label}${escape(conditionalValue)}`;
|
|
55
|
+
}
|
|
68
56
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
.join('\n');
|
|
57
|
+
return optionText;
|
|
58
|
+
});
|
|
72
59
|
|
|
73
|
-
return
|
|
60
|
+
return formattedParts.join('<br>');
|
|
74
61
|
}
|
|
75
62
|
}
|
|
76
63
|
|
|
@@ -54,35 +54,178 @@ describe('./src/dynamic-forms/components/checkbox/question.js', () => {
|
|
|
54
54
|
assert.deepStrictEqual(question.options, OPTIONS);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
describe('formatAnswer', () => {
|
|
58
|
+
it('should delegate simple string answers to parent class', () => {
|
|
59
|
+
const question = new CheckboxQuestion(CHECKBOX_PARAMS);
|
|
60
|
+
// Simple string should use parent's formatAnswer
|
|
61
|
+
const result = question.formatAnswer('1,2');
|
|
62
|
+
assert.strictEqual(result, 'a<br>b');
|
|
63
|
+
});
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
it('should format single answer with conditional', () => {
|
|
66
|
+
const question = new CheckboxQuestion(CHECKBOX_PARAMS);
|
|
67
|
+
const answer = {
|
|
68
|
+
value: '3',
|
|
69
|
+
conditional: {
|
|
70
|
+
3: 'conditional details'
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const result = question.formatAnswer(answer);
|
|
74
|
+
assert.strictEqual(result, 'c<br>conditional details');
|
|
75
|
+
});
|
|
70
76
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
it('should format single answer with conditional and label', () => {
|
|
78
|
+
const optionsWithLabel = [
|
|
79
|
+
{ text: 'Option A', value: 'a' },
|
|
80
|
+
{
|
|
81
|
+
text: 'Option B',
|
|
82
|
+
value: 'b',
|
|
83
|
+
conditional: {
|
|
84
|
+
fieldName: 'details',
|
|
85
|
+
label: 'Please provide details:'
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
const question = new CheckboxQuestion({
|
|
90
|
+
...CHECKBOX_PARAMS,
|
|
91
|
+
options: optionsWithLabel
|
|
92
|
+
});
|
|
93
|
+
const answer = {
|
|
94
|
+
value: 'b',
|
|
95
|
+
conditional: {
|
|
96
|
+
b: 'my details'
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const result = question.formatAnswer(answer);
|
|
100
|
+
assert.strictEqual(result, 'Option B<br>Please provide details: my details');
|
|
101
|
+
});
|
|
77
102
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
103
|
+
it('should format multiple answers where one has conditional', () => {
|
|
104
|
+
const question = new CheckboxQuestion(CHECKBOX_PARAMS);
|
|
105
|
+
const answer = {
|
|
106
|
+
value: '1,3',
|
|
107
|
+
conditional: {
|
|
108
|
+
3: 'conditional for c'
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const result = question.formatAnswer(answer);
|
|
112
|
+
assert.strictEqual(result, 'a<br>c<br>conditional for c');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should format multiple answers with multiple conditionals', () => {
|
|
116
|
+
const optionsWithMultipleConditionals = [
|
|
117
|
+
{
|
|
118
|
+
text: 'Option A',
|
|
119
|
+
value: 'a',
|
|
120
|
+
conditional: { fieldName: 'details-a' }
|
|
121
|
+
},
|
|
122
|
+
{ text: 'Option B', value: 'b' },
|
|
123
|
+
{
|
|
124
|
+
text: 'Option C',
|
|
125
|
+
value: 'c',
|
|
126
|
+
conditional: { fieldName: 'details-c' }
|
|
127
|
+
}
|
|
128
|
+
];
|
|
129
|
+
const question = new CheckboxQuestion({
|
|
130
|
+
...CHECKBOX_PARAMS,
|
|
131
|
+
options: optionsWithMultipleConditionals
|
|
132
|
+
});
|
|
133
|
+
const answer = {
|
|
134
|
+
value: 'a,c',
|
|
135
|
+
conditional: {
|
|
136
|
+
a: 'details for A',
|
|
137
|
+
c: 'details for C'
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const result = question.formatAnswer(answer);
|
|
141
|
+
assert.strictEqual(result, 'Option A<br>details for A<br>Option C<br>details for C');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should handle object answer with empty conditional', () => {
|
|
145
|
+
const question = new CheckboxQuestion(CHECKBOX_PARAMS);
|
|
146
|
+
const answer = {
|
|
147
|
+
value: '1,2',
|
|
148
|
+
conditional: {}
|
|
149
|
+
};
|
|
150
|
+
const result = question.formatAnswer(answer);
|
|
151
|
+
assert.strictEqual(result, 'a<br>b');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should handle object answer with null conditional', () => {
|
|
155
|
+
const question = new CheckboxQuestion(CHECKBOX_PARAMS);
|
|
156
|
+
const answer = {
|
|
157
|
+
value: '1,2',
|
|
158
|
+
conditional: null
|
|
159
|
+
};
|
|
160
|
+
const result = question.formatAnswer(answer);
|
|
161
|
+
assert.strictEqual(result, 'a<br>b');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should fall back to value when option not found in object answer', () => {
|
|
165
|
+
const question = new CheckboxQuestion(CHECKBOX_PARAMS);
|
|
166
|
+
const answer = {
|
|
167
|
+
value: 'unknown',
|
|
168
|
+
conditional: {}
|
|
169
|
+
};
|
|
170
|
+
const result = question.formatAnswer(answer);
|
|
171
|
+
assert.strictEqual(result, 'unknown');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should escape HTML in option text', () => {
|
|
175
|
+
const question = new CheckboxQuestion({
|
|
176
|
+
...CHECKBOX_PARAMS,
|
|
177
|
+
options: [{ text: '<script>alert("xss")</script>', value: '1' }]
|
|
178
|
+
});
|
|
179
|
+
const result = question.formatAnswer('1');
|
|
180
|
+
assert.strictEqual(result, '<script>alert("xss")</script>');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should escape HTML in conditional value', () => {
|
|
184
|
+
const question = new CheckboxQuestion({
|
|
185
|
+
...CHECKBOX_PARAMS,
|
|
186
|
+
options: [
|
|
187
|
+
{
|
|
188
|
+
text: 'Option',
|
|
189
|
+
value: '1',
|
|
190
|
+
conditional: { fieldName: 'details', type: 'text' }
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
});
|
|
194
|
+
const answer = {
|
|
195
|
+
value: '1',
|
|
196
|
+
conditional: { 1: '<img src=x onerror=alert(1)>' }
|
|
197
|
+
};
|
|
198
|
+
const result = question.formatAnswer(answer);
|
|
199
|
+
assert.strictEqual(result, 'Option<br><img src=x onerror=alert(1)>');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should escape HTML in conditional label', () => {
|
|
203
|
+
const question = new CheckboxQuestion({
|
|
204
|
+
...CHECKBOX_PARAMS,
|
|
205
|
+
options: [
|
|
206
|
+
{
|
|
207
|
+
text: 'Option',
|
|
208
|
+
value: '1',
|
|
209
|
+
conditional: { fieldName: 'details', type: 'text', label: '<b>Label</b>' }
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
});
|
|
213
|
+
const answer = {
|
|
214
|
+
value: '1',
|
|
215
|
+
conditional: { 1: 'safe value' }
|
|
216
|
+
};
|
|
217
|
+
const result = question.formatAnswer(answer);
|
|
218
|
+
assert.strictEqual(result, 'Option<br><b>Label</b> safe value');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should escape HTML in fallback value when option not found', () => {
|
|
222
|
+
const question = new CheckboxQuestion(CHECKBOX_PARAMS);
|
|
223
|
+
const answer = {
|
|
224
|
+
value: '<script>xss</script>',
|
|
225
|
+
conditional: {}
|
|
226
|
+
};
|
|
227
|
+
const result = question.formatAnswer(answer);
|
|
228
|
+
assert.strictEqual(result, '<script>xss</script>');
|
|
229
|
+
});
|
|
87
230
|
});
|
|
88
231
|
});
|
|
@@ -16,14 +16,6 @@ export class DateQuestion extends Question {
|
|
|
16
16
|
answerForViewModel(answers: any, isPayload: any): {
|
|
17
17
|
[x: string]: any;
|
|
18
18
|
};
|
|
19
|
-
/**
|
|
20
|
-
* returns the formatted answers values to be used to build task list elements
|
|
21
|
-
*/
|
|
22
|
-
formatAnswerForSummary(sectionSegment: any, journey: any, answer: any): {
|
|
23
|
-
key: string;
|
|
24
|
-
value: string;
|
|
25
|
-
action: import("../../index.js").ActionView | import("../../index.js").ActionView[] | undefined;
|
|
26
|
-
}[];
|
|
27
19
|
}
|
|
28
20
|
export default DateQuestion;
|
|
29
21
|
import { Question } from '../../questions/question.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAKA;;GAEG;AACH;IACC;;OAEG;IACH,6CAFW,OAAO,+BAA+B,EAAE,kBAAkB,EAQpE;IADA,mBAA4B;IAW7B;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,OAAO,mBAAmB,EAAE,eAAe,GACzC,OAAO,CAAE,MAAM,CAAC,CAY5B;IAED;;MAwBC;
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAKA;;GAEG;AACH;IACC;;OAEG;IACH,6CAFW,OAAO,+BAA+B,EAAE,kBAAkB,EAQpE;IADA,mBAA4B;IAW7B;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,OAAO,mBAAmB,EAAE,eAAe,GACzC,OAAO,CAAE,MAAM,CAAC,CAY5B;IAED;;MAwBC;CAWD;;yBAhFwB,6BAA6B"}
|
|
@@ -71,21 +71,13 @@ export class DateQuestion extends Question {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
*
|
|
74
|
+
* Formats a date answer for display in the summary.
|
|
75
|
+
* @param {unknown} answer - the date value
|
|
76
|
+
* @returns {string} the formatted date
|
|
75
77
|
*/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (answer) {
|
|
80
|
-
formattedAnswer = formatDateForDisplay(answer, { format: this.dateFormat });
|
|
81
|
-
} else {
|
|
82
|
-
formattedAnswer = this.notStartedText;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const action = this.getAction(sectionSegment, journey, answer);
|
|
86
|
-
const key = this.title ?? this.question;
|
|
87
|
-
|
|
88
|
-
return [{ key: key, value: formattedAnswer, action: action }];
|
|
78
|
+
formatAnswer(answer) {
|
|
79
|
+
if (!answer) return this.notStartedText;
|
|
80
|
+
return formatDateForDisplay(answer, { format: this.dateFormat });
|
|
89
81
|
}
|
|
90
82
|
}
|
|
91
83
|
|
|
@@ -25,17 +25,13 @@ export class DatePeriodQuestion extends Question {
|
|
|
25
25
|
answerForViewModel(answers: any, isPayload: any): {
|
|
26
26
|
[x: string]: any;
|
|
27
27
|
};
|
|
28
|
-
/**
|
|
29
|
-
* @param {import('#question').QuestionViewModel} viewModel
|
|
30
|
-
*/
|
|
31
|
-
addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
|
|
32
28
|
/**
|
|
33
29
|
* returns the formatted answers values to be used to build task list elements
|
|
34
30
|
*/
|
|
35
31
|
formatAnswerForSummary(sectionSegment: any, journey: any, answer: any): {
|
|
36
32
|
key: string;
|
|
37
33
|
value: string;
|
|
38
|
-
action: import("
|
|
34
|
+
action: import("#typedefs/question-types.d.ts").ActionView | import("#typedefs/question-types.d.ts").ActionView[] | undefined;
|
|
39
35
|
}[];
|
|
40
36
|
}
|
|
41
37
|
export default DatePeriodQuestion;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAOA;;;GAGG;AACH;IACC;;OAEG;IACH,mGAFW,OAAO,+BAA+B,EAAE,wBAAwB,EAuB1E;IAPA,mBAA4B;IAC5B;;;MAAsD;IAEtD,8BAA0B;IAC1B,4BAAsB;IACtB,kEAA+D;IAC/D,gEAA2D;IAkB5D;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,OAAO,mBAAmB,EAAE,eAAe,GACzC,OAAO,CAAE,MAAM,CAAC,CAgC5B;IAED;;MAuCC;
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAOA;;;GAGG;AACH;IACC;;OAEG;IACH,mGAFW,OAAO,+BAA+B,EAAE,wBAAwB,EAuB1E;IAPA,mBAA4B;IAC5B;;;MAAsD;IAEtD,8BAA0B;IAC1B,4BAAsB;IACtB,kEAA+D;IAC/D,gEAA2D;IAkB5D;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,OAAO,mBAAmB,EAAE,eAAe,GACzC,OAAO,CAAE,MAAM,CAAC,CAgC5B;IAED;;MAuCC;IAWD;;OAEG;IACH;;;;QAyBC;CACD;;yBAzKwB,WAAW"}
|
|
@@ -131,7 +131,7 @@ export class DatePeriodQuestion extends Question {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
|
-
* @param {import('#question').QuestionViewModel} viewModel
|
|
134
|
+
* @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
|
|
135
135
|
*/
|
|
136
136
|
addCustomDataToViewModel(viewModel) {
|
|
137
137
|
viewModel.labels = this.labels;
|
|
@@ -16,14 +16,6 @@ export class DateTimeQuestion extends Question {
|
|
|
16
16
|
answerForViewModel(answers: any, isPayload: any): {
|
|
17
17
|
[x: string]: any;
|
|
18
18
|
};
|
|
19
|
-
/**
|
|
20
|
-
* returns the formatted answers values to be used to build task list elements
|
|
21
|
-
*/
|
|
22
|
-
formatAnswerForSummary(sectionSegment: any, journey: any, answer: any): {
|
|
23
|
-
key: string;
|
|
24
|
-
value: string;
|
|
25
|
-
action: import("../../index.js").ActionView | import("../../index.js").ActionView[] | undefined;
|
|
26
|
-
}[];
|
|
27
19
|
#private;
|
|
28
20
|
}
|
|
29
21
|
export default DateTimeQuestion;
|