@planning-inspectorate/dynamic-forms 3.16.0 → 3.18.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 +124 -189
- package/docs/Email Validation.md +86 -0
- package/docs/Getting Started.md +299 -0
- 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 +38 -50
- package/src/components/checkbox/question.test.js +177 -26
- 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/email/question.d.ts +0 -9
- package/src/components/email/question.d.ts.map +1 -1
- package/src/components/email/question.js +2 -13
- package/src/components/email/question.test.js +19 -0
- 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/multi-field-input/question.d.ts +4 -19
- package/src/components/multi-field-input/question.d.ts.map +1 -1
- package/src/components/multi-field-input/question.js +89 -23
- package/src/components/multi-field-input/question.test.js +441 -3
- 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 -20
- package/src/components/number-entry/question.test.js +17 -0
- 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/unit-option-entry/question.d.ts.map +1 -1
- package/src/components/unit-option-entry/question.js +3 -1
- package/src/components/unit-option-entry/question.test.js +15 -0
- 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 +23 -1
- package/test/snapshots/favourite-activity-reason.html +54 -0
- package/test/snapshots/travel-requirements.html +48 -0
- package/test/utils/question-test-utils.js +38 -7
- package/typedefs/question-props.d.ts +33 -0
- package/typedefs/question-types.d.ts +4 -0
|
@@ -53,36 +53,187 @@ describe('./src/dynamic-forms/components/checkbox/question.js', () => {
|
|
|
53
53
|
assert.deepStrictEqual(question.validators, [...VALIDATORS, new ValidOptionValidator()]);
|
|
54
54
|
assert.deepStrictEqual(question.options, OPTIONS);
|
|
55
55
|
});
|
|
56
|
+
it('should allow capitaliseAnswer override', () => {
|
|
57
|
+
const question = new CheckboxQuestion({
|
|
58
|
+
...CHECKBOX_PARAMS,
|
|
59
|
+
capitaliseAnswer: true
|
|
60
|
+
});
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
const question = new CheckboxQuestion(CHECKBOX_PARAMS);
|
|
59
|
-
question.getAction = () => {};
|
|
60
|
-
const formattedAnswer = question.formatAnswerForSummary({}, JOURNEY, '1');
|
|
61
|
-
assert.strictEqual(formattedAnswer[0].value, 'a');
|
|
62
|
+
assert.strictEqual(question.capitaliseAnswer, true);
|
|
62
63
|
});
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
describe('formatAnswer', () => {
|
|
66
|
+
it('should delegate simple string answers to parent class', () => {
|
|
67
|
+
const question = new CheckboxQuestion(CHECKBOX_PARAMS);
|
|
68
|
+
// Simple string should use parent's formatAnswer
|
|
69
|
+
const result = question.formatAnswer('1,2');
|
|
70
|
+
assert.strictEqual(result, 'a<br>b');
|
|
71
|
+
});
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
it('should format single answer with conditional', () => {
|
|
74
|
+
const question = new CheckboxQuestion(CHECKBOX_PARAMS);
|
|
75
|
+
const answer = {
|
|
76
|
+
value: '3',
|
|
77
|
+
conditional: {
|
|
78
|
+
3: 'conditional details'
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const result = question.formatAnswer(answer);
|
|
82
|
+
assert.strictEqual(result, 'c<br>conditional details');
|
|
83
|
+
});
|
|
77
84
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
it('should format single answer with conditional and label', () => {
|
|
86
|
+
const optionsWithLabel = [
|
|
87
|
+
{ text: 'Option A', value: 'a' },
|
|
88
|
+
{
|
|
89
|
+
text: 'Option B',
|
|
90
|
+
value: 'b',
|
|
91
|
+
conditional: {
|
|
92
|
+
fieldName: 'details',
|
|
93
|
+
label: 'Please provide details:'
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
];
|
|
97
|
+
const question = new CheckboxQuestion({
|
|
98
|
+
...CHECKBOX_PARAMS,
|
|
99
|
+
options: optionsWithLabel
|
|
100
|
+
});
|
|
101
|
+
const answer = {
|
|
102
|
+
value: 'b',
|
|
103
|
+
conditional: {
|
|
104
|
+
b: 'my details'
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const result = question.formatAnswer(answer);
|
|
108
|
+
assert.strictEqual(result, 'Option B<br>Please provide details: my details');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should format multiple answers where one has conditional', () => {
|
|
112
|
+
const question = new CheckboxQuestion(CHECKBOX_PARAMS);
|
|
113
|
+
const answer = {
|
|
114
|
+
value: '1,3',
|
|
115
|
+
conditional: {
|
|
116
|
+
3: 'conditional for c'
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const result = question.formatAnswer(answer);
|
|
120
|
+
assert.strictEqual(result, 'a<br>c<br>conditional for c');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should format multiple answers with multiple conditionals', () => {
|
|
124
|
+
const optionsWithMultipleConditionals = [
|
|
125
|
+
{
|
|
126
|
+
text: 'Option A',
|
|
127
|
+
value: 'a',
|
|
128
|
+
conditional: { fieldName: 'details-a' }
|
|
129
|
+
},
|
|
130
|
+
{ text: 'Option B', value: 'b' },
|
|
131
|
+
{
|
|
132
|
+
text: 'Option C',
|
|
133
|
+
value: 'c',
|
|
134
|
+
conditional: { fieldName: 'details-c' }
|
|
135
|
+
}
|
|
136
|
+
];
|
|
137
|
+
const question = new CheckboxQuestion({
|
|
138
|
+
...CHECKBOX_PARAMS,
|
|
139
|
+
options: optionsWithMultipleConditionals
|
|
140
|
+
});
|
|
141
|
+
const answer = {
|
|
142
|
+
value: 'a,c',
|
|
143
|
+
conditional: {
|
|
144
|
+
a: 'details for A',
|
|
145
|
+
c: 'details for C'
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const result = question.formatAnswer(answer);
|
|
149
|
+
assert.strictEqual(result, 'Option A<br>details for A<br>Option C<br>details for C');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should handle object answer with empty conditional', () => {
|
|
153
|
+
const question = new CheckboxQuestion(CHECKBOX_PARAMS);
|
|
154
|
+
const answer = {
|
|
155
|
+
value: '1,2',
|
|
156
|
+
conditional: {}
|
|
157
|
+
};
|
|
158
|
+
const result = question.formatAnswer(answer);
|
|
159
|
+
assert.strictEqual(result, 'a<br>b');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should handle object answer with null conditional', () => {
|
|
163
|
+
const question = new CheckboxQuestion(CHECKBOX_PARAMS);
|
|
164
|
+
const answer = {
|
|
165
|
+
value: '1,2',
|
|
166
|
+
conditional: null
|
|
167
|
+
};
|
|
168
|
+
const result = question.formatAnswer(answer);
|
|
169
|
+
assert.strictEqual(result, 'a<br>b');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should fall back to value when option not found in object answer', () => {
|
|
173
|
+
const question = new CheckboxQuestion(CHECKBOX_PARAMS);
|
|
174
|
+
const answer = {
|
|
175
|
+
value: 'unknown',
|
|
176
|
+
conditional: {}
|
|
177
|
+
};
|
|
178
|
+
const result = question.formatAnswer(answer);
|
|
179
|
+
assert.strictEqual(result, 'unknown');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should escape HTML in option text', () => {
|
|
183
|
+
const question = new CheckboxQuestion({
|
|
184
|
+
...CHECKBOX_PARAMS,
|
|
185
|
+
options: [{ text: '<script>alert("xss")</script>', value: '1' }]
|
|
186
|
+
});
|
|
187
|
+
const result = question.formatAnswer('1');
|
|
188
|
+
assert.strictEqual(result, '<script>alert("xss")</script>');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should escape HTML in conditional value', () => {
|
|
192
|
+
const question = new CheckboxQuestion({
|
|
193
|
+
...CHECKBOX_PARAMS,
|
|
194
|
+
options: [
|
|
195
|
+
{
|
|
196
|
+
text: 'Option',
|
|
197
|
+
value: '1',
|
|
198
|
+
conditional: { fieldName: 'details', type: 'text' }
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
});
|
|
202
|
+
const answer = {
|
|
203
|
+
value: '1',
|
|
204
|
+
conditional: { 1: '<img src=x onerror=alert(1)>' }
|
|
205
|
+
};
|
|
206
|
+
const result = question.formatAnswer(answer);
|
|
207
|
+
assert.strictEqual(result, 'Option<br><img src=x onerror=alert(1)>');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should escape HTML in conditional label', () => {
|
|
211
|
+
const question = new CheckboxQuestion({
|
|
212
|
+
...CHECKBOX_PARAMS,
|
|
213
|
+
options: [
|
|
214
|
+
{
|
|
215
|
+
text: 'Option',
|
|
216
|
+
value: '1',
|
|
217
|
+
conditional: { fieldName: 'details', type: 'text', label: '<b>Label</b>' }
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
});
|
|
221
|
+
const answer = {
|
|
222
|
+
value: '1',
|
|
223
|
+
conditional: { 1: 'safe value' }
|
|
224
|
+
};
|
|
225
|
+
const result = question.formatAnswer(answer);
|
|
226
|
+
assert.strictEqual(result, 'Option<br><b>Label</b> safe value');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should escape HTML in fallback value when option not found', () => {
|
|
230
|
+
const question = new CheckboxQuestion(CHECKBOX_PARAMS);
|
|
231
|
+
const answer = {
|
|
232
|
+
value: '<script>xss</script>',
|
|
233
|
+
conditional: {}
|
|
234
|
+
};
|
|
235
|
+
const result = question.formatAnswer(answer);
|
|
236
|
+
assert.strictEqual(result, '<script>xss</script>');
|
|
237
|
+
});
|
|
87
238
|
});
|
|
88
239
|
});
|
|
@@ -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;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAMA;IACC,kBAAiB;IACjB,kBAAiB;IAEjB;;OAEG;IACH,yDAFW,OAAO,+BAA+B,EAAE,sBAAsB,EASxE;IAFA,mBAA4B;IAC5B,mBAA4B;IAkB7B;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,eAAe,GACb,OAAO,CAAE,MAAM,CAAC,CAqB5B;IAED;;MAqCC
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAMA;IACC,kBAAiB;IACjB,kBAAiB;IAEjB;;OAEG;IACH,yDAFW,OAAO,+BAA+B,EAAE,sBAAsB,EASxE;IAFA,mBAA4B;IAC5B,mBAA4B;IAkB7B;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,eAAe,GACb,OAAO,CAAE,MAAM,CAAC,CAqB5B;IAED;;MAqCC;;CA2BD;;yBAhIwB,6BAA6B"}
|
|
@@ -102,19 +102,11 @@ export class DateTimeQuestion extends Question {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
*
|
|
105
|
+
* Formats a date-time answer for display in the summary.
|
|
106
|
+
* @param {unknown} answer - the date value
|
|
107
|
+
* @returns {string} the formatted date and time
|
|
106
108
|
*/
|
|
107
|
-
|
|
108
|
-
return [
|
|
109
|
-
{
|
|
110
|
-
key: this.title,
|
|
111
|
-
value: this.#formatDateTimeValue(answer),
|
|
112
|
-
action: this.getAction(sectionSegment, journey, answer)
|
|
113
|
-
}
|
|
114
|
-
];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
#formatDateTimeValue(answer) {
|
|
109
|
+
formatAnswer(answer) {
|
|
118
110
|
if (!answer) return this.notStartedText;
|
|
119
111
|
|
|
120
112
|
const formattedDate = formatDateForDisplay(answer, { format: this.dateFormat });
|
|
@@ -8,15 +8,6 @@ export class EmailQuestion extends SingleLineInputQuestion {
|
|
|
8
8
|
* @param {import('#typedefs/question-props.d.ts').EmailQuestionParams} params
|
|
9
9
|
*/
|
|
10
10
|
constructor(params: import("#typedefs/question-props.d.ts").EmailQuestionParams);
|
|
11
|
-
/**
|
|
12
|
-
* Override formatAnswerForSummary to prevent capitalization of email addresses
|
|
13
|
-
* Email addresses should remain in their original case (typically lowercase)
|
|
14
|
-
* @param {string} sectionSegment
|
|
15
|
-
* @param {import('#journey').Journey} journey
|
|
16
|
-
* @param {string} answer
|
|
17
|
-
* @returns {Array}
|
|
18
|
-
*/
|
|
19
|
-
formatAnswerForSummary(sectionSegment: string, journey: import("#journey").Journey, answer: string): any[];
|
|
20
11
|
}
|
|
21
12
|
export default EmailQuestion;
|
|
22
13
|
import SingleLineInputQuestion from '../single-line-input/question.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;;;;GAIG;AACH;IACC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,mBAAmB,
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;;;;GAIG;AACH;IACC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,mBAAmB,EAqBrE;CACD;;oCA/BmC,kCAAkC"}
|
|
@@ -21,25 +21,14 @@ export class EmailQuestion extends SingleLineInputQuestion {
|
|
|
21
21
|
const autocomplete = params.autocomplete || 'email';
|
|
22
22
|
|
|
23
23
|
super({
|
|
24
|
+
// Prevent capitalisation of email answers by default, but allow override
|
|
25
|
+
capitaliseAnswer: false,
|
|
24
26
|
...params,
|
|
25
27
|
viewFolder: 'single-line-input', // Reuse single-line-input template
|
|
26
28
|
inputAttributes: emailInputAttributes,
|
|
27
29
|
autocomplete: autocomplete
|
|
28
30
|
});
|
|
29
31
|
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Override formatAnswerForSummary to prevent capitalization of email addresses
|
|
33
|
-
* Email addresses should remain in their original case (typically lowercase)
|
|
34
|
-
* @param {string} sectionSegment
|
|
35
|
-
* @param {import('#journey').Journey} journey
|
|
36
|
-
* @param {string} answer
|
|
37
|
-
* @returns {Array}
|
|
38
|
-
*/
|
|
39
|
-
formatAnswerForSummary(sectionSegment, journey, answer) {
|
|
40
|
-
// Call parent method with capitals=false to prevent email capitalization
|
|
41
|
-
return super.formatAnswerForSummary(sectionSegment, journey, answer, false);
|
|
42
|
-
}
|
|
43
32
|
}
|
|
44
33
|
|
|
45
34
|
export default EmailQuestion;
|
|
@@ -16,6 +16,25 @@ describe('src/dynamic-forms/components/email/question.js', () => {
|
|
|
16
16
|
assert.strictEqual(emailQuestion.autocomplete, 'email');
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
it('should default capitaliseAnswer to false', () => {
|
|
20
|
+
const emailQuestion = new EmailQuestion({
|
|
21
|
+
title: 'Email Address',
|
|
22
|
+
question: 'What is your email address?',
|
|
23
|
+
fieldName: 'email'
|
|
24
|
+
});
|
|
25
|
+
assert.strictEqual(emailQuestion.capitaliseAnswer, false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should allow capitaliseAnswer to be set via constructor', () => {
|
|
29
|
+
const emailQuestion = new EmailQuestion({
|
|
30
|
+
title: 'Email Address',
|
|
31
|
+
question: 'What is your email address?',
|
|
32
|
+
fieldName: 'email',
|
|
33
|
+
capitaliseAnswer: true
|
|
34
|
+
});
|
|
35
|
+
assert.strictEqual(emailQuestion.capitaliseAnswer, true);
|
|
36
|
+
});
|
|
37
|
+
|
|
19
38
|
it('should allow custom input attributes while preserving email defaults', () => {
|
|
20
39
|
const customAttributes = {
|
|
21
40
|
placeholder: 'Enter your email',
|
|
@@ -11,10 +11,6 @@ export class ManageListQuestion extends Question {
|
|
|
11
11
|
*/
|
|
12
12
|
constructor(params: import("#typedefs/question-props.d.ts").ManageListQuestionParams);
|
|
13
13
|
get confirmationQuestionParam(): string;
|
|
14
|
-
/**
|
|
15
|
-
* @param {import('#question').QuestionViewModel} viewModel
|
|
16
|
-
*/
|
|
17
|
-
addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
|
|
18
14
|
getDataToSave(req: any, journeyResponse: any): Promise<{
|
|
19
15
|
answers: {
|
|
20
16
|
[x: string]: any;
|
|
@@ -26,7 +22,7 @@ export class ManageListQuestion extends Question {
|
|
|
26
22
|
formatAnswerForSummary(sectionSegment: any, journey: any, answer: any): {
|
|
27
23
|
key: string;
|
|
28
24
|
value: string;
|
|
29
|
-
action: import("
|
|
25
|
+
action: import("#typedefs/question-types.d.ts").ActionView | import("#typedefs/question-types.d.ts").ActionView[] | undefined;
|
|
30
26
|
}[];
|
|
31
27
|
renderConfirmationAction(res: any, itemToRemove: any, viewModel: any): void;
|
|
32
28
|
set section(section: import("../../section.js").Section);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAKA;;;;;;GAMG;AAEH;IAOC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,wBAAwB,EAe1E;IAcD,wCAEC;
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAKA;;;;;;GAMG;AAEH;IAOC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,wBAAwB,EAe1E;IAcD,wCAEC;IAyDD;;;;OAMC;IAED;;OAEG;IACH;;;;QAsBC;IAED,4EAQC;IAoCD,yDAEC;IATD,kDAKC;;CAKD;;;;;;mBAxLa,MAAM;;;;;;;;;;;;;;yBAPK,WAAW"}
|
|
@@ -53,7 +53,7 @@ export class ManageListQuestion extends Question {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* @param {import('#question').QuestionViewModel} viewModel
|
|
56
|
+
* @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
|
|
57
57
|
*/
|
|
58
58
|
addCustomDataToViewModel(viewModel) {
|
|
59
59
|
viewModel.question.addAnotherLink = this.#addAnotherLink;
|
|
@@ -14,6 +14,7 @@ export class MultiFieldInputQuestion extends Question {
|
|
|
14
14
|
formatJoinString?: string;
|
|
15
15
|
formatPrefix?: string;
|
|
16
16
|
formatTextFunction?: (text: string) => string;
|
|
17
|
+
formatSummaryValue?: (context: import("#typedefs/question-props.d.ts").InputFieldSummaryContext) => string;
|
|
17
18
|
attributes?: Record<string, string>;
|
|
18
19
|
suffix?: import("#typedefs/question-props.d.ts").Affix;
|
|
19
20
|
prefix?: import("#typedefs/question-props.d.ts").Affix;
|
|
@@ -24,27 +25,11 @@ export class MultiFieldInputQuestion extends Question {
|
|
|
24
25
|
}[];
|
|
25
26
|
/**
|
|
26
27
|
* returns the formatted answers values to be used to build task list elements
|
|
27
|
-
* @param {import('#journey').Journey} journey
|
|
28
28
|
* @param {string} sectionSegment
|
|
29
|
-
* @
|
|
30
|
-
*
|
|
31
|
-
* value: string | Object;
|
|
32
|
-
* action: {
|
|
33
|
-
* href: string;
|
|
34
|
-
* text: string;
|
|
35
|
-
* visuallyHiddenText: string;
|
|
36
|
-
* };
|
|
37
|
-
* }>}
|
|
29
|
+
* @param {import('#journey').Journey} journey
|
|
30
|
+
* @returns {import('#typedefs/question-types.d.ts').SummaryRow[]}
|
|
38
31
|
*/
|
|
39
|
-
formatAnswerForSummary(sectionSegment: string, journey: import("#journey").Journey):
|
|
40
|
-
key: string;
|
|
41
|
-
value: string | Object;
|
|
42
|
-
action: {
|
|
43
|
-
href: string;
|
|
44
|
-
text: string;
|
|
45
|
-
visuallyHiddenText: string;
|
|
46
|
-
};
|
|
47
|
-
}>;
|
|
32
|
+
formatAnswerForSummary(sectionSegment: string, journey: import("#journey").Journey): import("#typedefs/question-types.d.ts").SummaryRow[];
|
|
48
33
|
#private;
|
|
49
34
|
}
|
|
50
35
|
export default MultiFieldInputQuestion;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAIA;;GAEG;AACH;IACC;;OAEG;IACH,8CAFW,OAAO,+BAA+B,EAAE,6BAA6B,
|
|
1
|
+
{"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAIA;;GAEG;AACH;IACC;;OAEG;IACH,8CAFW,OAAO,+BAA+B,EAAE,6BAA6B,EAe/E;IAJC,kEAA8B;IAchC;;;;;;;;;;;;;;;QAOC;IA4CD;;;;;OAKG;IACH,uCAJW,MAAM,WACN,OAAO,UAAU,EAAE,OAAO,GACxB,OAAO,+BAA+B,EAAE,UAAU,EAAE,CAuChE;;CAsDD;;yBAtLwB,WAAW"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Question } from '#question';
|
|
2
2
|
import escape from 'escape-html';
|
|
3
|
-
import { nl2br } from '../../lib/utils.js';
|
|
3
|
+
import { capitalize, nl2br } from '../../lib/utils.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @class
|
|
@@ -11,6 +11,8 @@ export class MultiFieldInputQuestion extends Question {
|
|
|
11
11
|
*/
|
|
12
12
|
constructor({ inputFields, ...parentParams }) {
|
|
13
13
|
super({
|
|
14
|
+
// default but allow overrides
|
|
15
|
+
capitaliseAnswer: false,
|
|
14
16
|
...parentParams,
|
|
15
17
|
viewFolder: 'multi-field-input'
|
|
16
18
|
});
|
|
@@ -59,36 +61,100 @@ export class MultiFieldInputQuestion extends Question {
|
|
|
59
61
|
return { answers };
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Formats an answer value for display in the summary.
|
|
66
|
+
* Handles the ManageListSection edge case where nl2br should not be applied.
|
|
67
|
+
*
|
|
68
|
+
* @param {unknown} answer - the raw answer value (composed from multiple fields)
|
|
69
|
+
* @returns {string} the formatted answer for display
|
|
70
|
+
*/
|
|
71
|
+
formatAnswer(answer) {
|
|
72
|
+
// Only show notStartedText for null/undefined, not for empty string
|
|
73
|
+
if (answer === null || answer === undefined) return this.notStartedText;
|
|
74
|
+
if (answer === '') return '';
|
|
75
|
+
|
|
76
|
+
// Coerce to string and apply optional capitalisation
|
|
77
|
+
let formatted = String(answer);
|
|
78
|
+
if (this.capitaliseAnswer) {
|
|
79
|
+
formatted = capitalize(formatted);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Do not convert new lines to breaks in ManageListSection, as this causes a "doubled up" <br>
|
|
83
|
+
return this.isInManageListSection ? escape(formatted) : nl2br(escape(formatted));
|
|
84
|
+
}
|
|
85
|
+
|
|
62
86
|
/**
|
|
63
87
|
* returns the formatted answers values to be used to build task list elements
|
|
64
|
-
* @param {import('#journey').Journey} journey
|
|
65
88
|
* @param {string} sectionSegment
|
|
66
|
-
* @
|
|
67
|
-
*
|
|
68
|
-
* value: string | Object;
|
|
69
|
-
* action: {
|
|
70
|
-
* href: string;
|
|
71
|
-
* text: string;
|
|
72
|
-
* visuallyHiddenText: string;
|
|
73
|
-
* };
|
|
74
|
-
* }>}
|
|
89
|
+
* @param {import('#journey').Journey} journey
|
|
90
|
+
* @returns {import('#typedefs/question-types.d.ts').SummaryRow[]}
|
|
75
91
|
*/
|
|
76
92
|
formatAnswerForSummary(sectionSegment, journey) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return
|
|
93
|
+
// Handle unanswered case - delegate to parent for notStartedText
|
|
94
|
+
if (this.#allQuestionsUnanswered(journey)) {
|
|
95
|
+
return super.formatAnswerForSummary(sectionSegment, journey, null);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Default join string depends on context
|
|
99
|
+
const defaultJoinString = this.isInManageListSection ? '\n' : '<br>';
|
|
100
|
+
|
|
101
|
+
let summaryDetails = this.inputFields.reduce((accumulator, field) => {
|
|
102
|
+
const rawAnswer = journey.response.answers[field.fieldName];
|
|
103
|
+
if (rawAnswer === undefined || rawAnswer === null || rawAnswer === '') return accumulator;
|
|
104
|
+
|
|
105
|
+
const formatted = this.#formatFieldForSummary(rawAnswer, field, journey, sectionSegment);
|
|
106
|
+
return accumulator + (field.formatPrefix || '') + formatted + (field.formatJoinString ?? defaultJoinString);
|
|
80
107
|
}, '');
|
|
81
108
|
|
|
82
|
-
|
|
109
|
+
// Remove trailing join string
|
|
110
|
+
if (summaryDetails.endsWith(defaultJoinString)) {
|
|
111
|
+
summaryDetails = summaryDetails.slice(0, -defaultJoinString.length);
|
|
112
|
+
}
|
|
83
113
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
114
|
+
// Apply question-level formatSummaryValue if provided
|
|
115
|
+
const displayValue = this.formatSummaryValue
|
|
116
|
+
? this.formatSummaryValue({
|
|
117
|
+
answer: summaryDetails,
|
|
118
|
+
formattedAnswer: summaryDetails,
|
|
119
|
+
question: this,
|
|
120
|
+
journey,
|
|
121
|
+
sectionSegment
|
|
122
|
+
})
|
|
123
|
+
: summaryDetails;
|
|
124
|
+
|
|
125
|
+
// Build result directly - escaping already handled per-field
|
|
126
|
+
const action = this.getAction(sectionSegment, journey, summaryDetails);
|
|
127
|
+
const key = this.title ?? this.question;
|
|
128
|
+
return [{ key, value: displayValue || '', action }];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Formats a single field value for summary display
|
|
133
|
+
* @param {unknown} answer - the raw answer value
|
|
134
|
+
* @param {import('#typedefs/question-props.d.ts').InputField} field - the input field config
|
|
135
|
+
* @param {import('#journey').Journey} journey - the journey instance
|
|
136
|
+
* @param {string} sectionSegment - the section segment
|
|
137
|
+
* @returns {string}
|
|
138
|
+
*/
|
|
139
|
+
#formatFieldForSummary(answer, field, journey, sectionSegment) {
|
|
140
|
+
// Apply formatTextFunction (if provided) before escaping, same as answerForViewModel
|
|
141
|
+
const textFormattedAnswer = this.#formatValue(answer, field.formatTextFunction);
|
|
142
|
+
const formattedAnswer = escape(String(textFormattedAnswer));
|
|
143
|
+
|
|
144
|
+
// If formatSummaryValue is provided, use it with full context (output is not escaped)
|
|
145
|
+
if (typeof field.formatSummaryValue === 'function') {
|
|
146
|
+
return field.formatSummaryValue({
|
|
147
|
+
answer,
|
|
148
|
+
formattedAnswer,
|
|
149
|
+
question: this,
|
|
150
|
+
journey,
|
|
151
|
+
sectionSegment,
|
|
152
|
+
field
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Default: escaped, formatTextFunction-applied value
|
|
157
|
+
return formattedAnswer;
|
|
92
158
|
}
|
|
93
159
|
|
|
94
160
|
/**
|