@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.
Files changed (77) hide show
  1. package/README.md +124 -189
  2. package/docs/Email Validation.md +86 -0
  3. package/docs/Getting Started.md +299 -0
  4. package/package.json +1 -1
  5. package/src/components/address/question.d.ts +1 -9
  6. package/src/components/address/question.d.ts.map +1 -1
  7. package/src/components/address/question.js +6 -17
  8. package/src/components/checkbox/question.d.ts +1 -21
  9. package/src/components/checkbox/question.d.ts.map +1 -1
  10. package/src/components/checkbox/question.js +38 -50
  11. package/src/components/checkbox/question.test.js +177 -26
  12. package/src/components/date/question.d.ts +0 -8
  13. package/src/components/date/question.d.ts.map +1 -1
  14. package/src/components/date/question.js +6 -14
  15. package/src/components/date-period/question.d.ts +1 -5
  16. package/src/components/date-period/question.d.ts.map +1 -1
  17. package/src/components/date-period/question.js +1 -1
  18. package/src/components/date-time/question.d.ts +0 -8
  19. package/src/components/date-time/question.d.ts.map +1 -1
  20. package/src/components/date-time/question.js +4 -12
  21. package/src/components/email/question.d.ts +0 -9
  22. package/src/components/email/question.d.ts.map +1 -1
  23. package/src/components/email/question.js +2 -13
  24. package/src/components/email/question.test.js +19 -0
  25. package/src/components/manage-list/answer-summary-list.njk +1 -1
  26. package/src/components/manage-list/question.d.ts +1 -5
  27. package/src/components/manage-list/question.d.ts.map +1 -1
  28. package/src/components/manage-list/question.js +1 -1
  29. package/src/components/multi-field-input/question.d.ts +4 -19
  30. package/src/components/multi-field-input/question.d.ts.map +1 -1
  31. package/src/components/multi-field-input/question.js +89 -23
  32. package/src/components/multi-field-input/question.test.js +441 -3
  33. package/src/components/number-entry/question.d.ts +0 -28
  34. package/src/components/number-entry/question.d.ts.map +1 -1
  35. package/src/components/number-entry/question.js +3 -20
  36. package/src/components/number-entry/question.test.js +17 -0
  37. package/src/components/radio/question.d.ts +1 -9
  38. package/src/components/radio/question.d.ts.map +1 -1
  39. package/src/components/radio/question.js +29 -46
  40. package/src/components/radio/question.test.js +139 -60
  41. package/src/components/select/question.d.ts +1 -15
  42. package/src/components/select/question.d.ts.map +1 -1
  43. package/src/components/select/question.js +4 -47
  44. package/src/components/single-line-input/question.d.ts +0 -4
  45. package/src/components/single-line-input/question.d.ts.map +1 -1
  46. package/src/components/single-line-input/question.js +1 -1
  47. package/src/components/text-entry/question.d.ts +0 -4
  48. package/src/components/text-entry/question.d.ts.map +1 -1
  49. package/src/components/text-entry/question.js +1 -1
  50. package/src/components/text-entry-redact/question.d.ts +2 -6
  51. package/src/components/text-entry-redact/question.d.ts.map +1 -1
  52. package/src/components/text-entry-redact/question.js +1 -1
  53. package/src/components/unit-option-entry/question.d.ts.map +1 -1
  54. package/src/components/unit-option-entry/question.js +3 -1
  55. package/src/components/unit-option-entry/question.test.js +15 -0
  56. package/src/components/utils/question-has-answer.js +1 -1
  57. package/src/components/utils/question-utils.d.ts +19 -1
  58. package/src/components/utils/question-utils.d.ts.map +1 -1
  59. package/src/components/utils/question-utils.js +31 -2
  60. package/src/components/utils/question-utils.test.js +89 -2
  61. package/src/questions/options-question.d.ts +4 -4
  62. package/src/questions/options-question.d.ts.map +1 -1
  63. package/src/questions/options-question.js +27 -2
  64. package/src/questions/options-question.test.js +106 -0
  65. package/src/questions/question.d.ts +16 -1
  66. package/src/questions/question.d.ts.map +1 -1
  67. package/src/questions/question.js +64 -9
  68. package/src/questions/question.test.js +86 -0
  69. package/test/journey.js +2 -0
  70. package/test/questions.js +50 -0
  71. package/test/snapshots/check-your-answers-not-started.html +22 -0
  72. package/test/snapshots/check-your-answers-with-answers.html +23 -1
  73. package/test/snapshots/favourite-activity-reason.html +54 -0
  74. package/test/snapshots/travel-requirements.html +48 -0
  75. package/test/utils/question-test-utils.js +38 -7
  76. package/typedefs/question-props.d.ts +33 -0
  77. package/typedefs/question-types.d.ts +4 -0
@@ -1,41 +1,14 @@
1
+ import escape from 'escape-html';
1
2
  import OptionsQuestion from '../../questions/options-question.js';
2
3
 
3
4
  export class RadioQuestion extends OptionsQuestion {
4
5
  /**
5
6
  * @param {import('#typedefs/question-props.d.ts').RadioQuestionParams} params
6
7
  */
7
- constructor({
8
- title,
9
- question,
10
- fieldName,
11
- viewFolder,
12
- url,
13
- hint,
14
- pageTitle,
15
- description,
16
- label,
17
- html,
18
- legend,
19
- options,
20
- validators,
21
- actionLink,
22
- editable,
23
- viewData
24
- }) {
8
+ constructor({ label, html, legend, viewFolder, ...parentParams }) {
25
9
  super({
26
- title,
27
- question,
28
- viewFolder: !viewFolder ? 'radio' : viewFolder,
29
- fieldName,
30
- url,
31
- hint,
32
- pageTitle,
33
- description,
34
- options,
35
- validators,
36
- actionLink,
37
- editable,
38
- viewData
10
+ ...parentParams,
11
+ viewFolder: viewFolder || 'radio'
39
12
  });
40
13
 
41
14
  this.html = html;
@@ -44,7 +17,7 @@ export class RadioQuestion extends OptionsQuestion {
44
17
  }
45
18
 
46
19
  /**
47
- * @param {import('#question').QuestionViewModel} viewModel
20
+ * @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
48
21
  */
49
22
  addCustomDataToViewModel(viewModel) {
50
23
  viewModel.question.label = this.label;
@@ -52,22 +25,32 @@ export class RadioQuestion extends OptionsQuestion {
52
25
  }
53
26
 
54
27
  /**
55
- * returns the formatted answers values to be used to build task list elements
28
+ * Formats an answer value for display in the summary.
29
+ * Handles object answers with conditional fields (e.g. { value: 'yes', conditional: { yes: 'details' } })
30
+ *
31
+ * @param {unknown} answer - the raw answer value
32
+ * @returns {string} the formatted answer for display
56
33
  */
57
- formatAnswerForSummary(sectionSegment, journey, answer) {
58
- if (answer?.conditional) {
59
- const selectedOption = this.options.find((option) => option.value === answer.value);
60
- const conditionalAnswerText = selectedOption.conditional?.label
61
- ? `${selectedOption.conditional.label} ${answer.conditional}`
62
- : answer.conditional;
63
- const formattedAnswer = [selectedOption.text, conditionalAnswerText].join('\n');
64
- return super.formatAnswerForSummary(sectionSegment, journey, formattedAnswer, false);
65
- } else if (answer) {
66
- const selectedOption = this.options.find((option) => option.value === answer);
67
- const selectedText = selectedOption?.text || '';
68
- return super.formatAnswerForSummary(sectionSegment, journey, selectedText, false);
34
+ formatAnswer(answer) {
35
+ if (answer === null || answer === undefined || answer === '') {
36
+ return this.notStartedText;
69
37
  }
70
- return super.formatAnswerForSummary(sectionSegment, journey, answer);
38
+
39
+ // Handle simple string answers
40
+ if (typeof answer !== 'object' || answer.value === undefined) {
41
+ return super.formatAnswer(answer);
42
+ }
43
+
44
+ // Handle object answers with conditional fields
45
+ const option = this.options.find((opt) => opt.value === answer.value);
46
+ const optionText = escape(option ? option.text : answer.value);
47
+
48
+ const conditionalValue = answer.conditional?.[answer.value];
49
+ if (conditionalValue) {
50
+ const label = option?.conditional?.label ? `${escape(option.conditional.label)} ` : '';
51
+ return `${optionText}<br>${label}${escape(conditionalValue)}`;
52
+ }
53
+ return optionText;
71
54
  }
72
55
  }
73
56
 
@@ -96,78 +96,157 @@ describe('./src/dynamic-forms/components/radio/question.js', () => {
96
96
  assert.strictEqual(preppedQuestion.question.label, LABEL);
97
97
  });
98
98
 
99
- it('should customise answer for summary when question has a conditional field filled in', () => {
100
- const radioQuestion = new RadioQuestion({
101
- title: TITLE,
102
- question: QUESTION,
103
- description: DESCRIPTION,
104
- fieldName: FIELDNAME,
105
- viewFolder: VIEWFOLDER,
106
- html: HTML,
107
- label: LABEL,
108
- options: OPTIONS
99
+ describe('formatAnswer', () => {
100
+ it('should delegate simple string answers to parent class', () => {
101
+ const radioQuestion = new RadioQuestion({
102
+ title: TITLE,
103
+ question: QUESTION,
104
+ description: DESCRIPTION,
105
+ fieldName: FIELDNAME,
106
+ options: [
107
+ { text: 'Option 1', value: 'op-1' },
108
+ { text: 'Option 2', value: 'op-2' }
109
+ ]
110
+ });
111
+
112
+ const result = radioQuestion.formatAnswer('op-1');
113
+ assert.strictEqual(result, 'Option 1');
109
114
  });
110
115
 
111
- const rowParams = radioQuestion.formatAnswerForSummary(SECTION, JOURNEY, {
112
- value: 'yes',
113
- conditional: 'test'
116
+ it('should format object answer with value only (no conditional)', () => {
117
+ const radioQuestion = new RadioQuestion({
118
+ title: TITLE,
119
+ question: QUESTION,
120
+ description: DESCRIPTION,
121
+ fieldName: FIELDNAME,
122
+ options: OPTIONS
123
+ });
124
+
125
+ const result = radioQuestion.formatAnswer({ value: 'no' });
126
+ assert.strictEqual(result, 'No');
114
127
  });
115
- assert.strictEqual(rowParams[0].value, 'Yes<br>test');
116
- });
117
128
 
118
- it('should use option text (not value) for display', () => {
119
- const radioQuestion = new RadioQuestion({
120
- title: TITLE,
121
- question: QUESTION,
122
- description: DESCRIPTION,
123
- fieldName: FIELDNAME,
124
- viewFolder: VIEWFOLDER,
125
- html: HTML,
126
- label: LABEL,
127
- options: [
128
- { text: 'Option 1', value: 'op-1' },
129
- { text: 'Option 2', value: 'op-2' }
130
- ]
129
+ it('should format object answer with conditional', () => {
130
+ const radioQuestion = new RadioQuestion({
131
+ title: TITLE,
132
+ question: QUESTION,
133
+ description: DESCRIPTION,
134
+ fieldName: FIELDNAME,
135
+ options: OPTIONS
136
+ });
137
+
138
+ const result = radioQuestion.formatAnswer({
139
+ value: 'yes',
140
+ conditional: { yes: 'test details' }
141
+ });
142
+ assert.strictEqual(result, 'Yes<br>test details');
131
143
  });
132
144
 
133
- const rowParams = radioQuestion.formatAnswerForSummary(SECTION, JOURNEY, 'op-1');
134
- assert.strictEqual(rowParams[0].value, 'Option 1');
135
- });
145
+ it('should format object answer with conditional and label', () => {
146
+ const optionsWithLabel = [...OPTIONS];
147
+ optionsWithLabel[0] = {
148
+ ...optionsWithLabel[0],
149
+ conditional: {
150
+ ...optionsWithLabel[0].conditional,
151
+ label: 'label:'
152
+ }
153
+ };
154
+ const radioQuestion = new RadioQuestion({
155
+ title: TITLE,
156
+ question: QUESTION,
157
+ description: DESCRIPTION,
158
+ fieldName: FIELDNAME,
159
+ options: optionsWithLabel
160
+ });
161
+
162
+ const result = radioQuestion.formatAnswer({
163
+ value: 'yes',
164
+ conditional: { yes: 'test details' }
165
+ });
166
+ assert.strictEqual(result, 'Yes<br>label: test details');
167
+ });
136
168
 
137
- it('should customise answer for summary and add label when question has a conditional field filled in and conditional field has a label', () => {
138
- const optionsWithLabel = [...OPTIONS];
139
- optionsWithLabel[0].conditional.label = 'label:';
140
- const radioQuestion = new RadioQuestion({
141
- title: TITLE,
142
- question: QUESTION,
143
- description: DESCRIPTION,
144
- fieldName: FIELDNAME,
145
- viewFolder: VIEWFOLDER,
146
- html: HTML,
147
- label: LABEL,
148
- options: optionsWithLabel
169
+ it('should fall back to value when option not found in object answer', () => {
170
+ const radioQuestion = new RadioQuestion({
171
+ title: TITLE,
172
+ question: QUESTION,
173
+ description: DESCRIPTION,
174
+ fieldName: FIELDNAME,
175
+ options: OPTIONS
176
+ });
177
+
178
+ const result = radioQuestion.formatAnswer({ value: 'unknown' });
179
+ assert.strictEqual(result, 'unknown');
149
180
  });
150
181
 
151
- const rowParams = radioQuestion.formatAnswerForSummary(SECTION, JOURNEY, {
152
- value: 'yes',
153
- conditional: 'test'
182
+ it('should escape HTML in option text', () => {
183
+ const radioQuestion = new RadioQuestion({
184
+ title: TITLE,
185
+ question: QUESTION,
186
+ description: DESCRIPTION,
187
+ fieldName: FIELDNAME,
188
+ options: [{ text: '<script>alert("xss")</script>', value: 'xss' }]
189
+ });
190
+
191
+ const result = radioQuestion.formatAnswer('xss');
192
+ assert.strictEqual(result, '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;');
154
193
  });
155
- assert.strictEqual(rowParams[0].value, 'Yes<br>label: test');
156
- });
157
194
 
158
- it('should not customise answer for summary when question does not have a conditional field filled in', () => {
159
- const radioQuestion = new RadioQuestion({
160
- title: TITLE,
161
- question: QUESTION,
162
- description: DESCRIPTION,
163
- fieldName: FIELDNAME,
164
- viewFolder: VIEWFOLDER,
165
- html: HTML,
166
- label: LABEL,
167
- options: OPTIONS
195
+ it('should escape HTML in conditional value', () => {
196
+ const radioQuestion = new RadioQuestion({
197
+ title: TITLE,
198
+ question: QUESTION,
199
+ description: DESCRIPTION,
200
+ fieldName: FIELDNAME,
201
+ options: [
202
+ {
203
+ text: 'Yes',
204
+ value: 'yes',
205
+ conditional: { fieldName: 'details', type: 'text' }
206
+ }
207
+ ]
208
+ });
209
+
210
+ const result = radioQuestion.formatAnswer({
211
+ value: 'yes',
212
+ conditional: { yes: '<img src=x onerror=alert(1)>' }
213
+ });
214
+ assert.strictEqual(result, 'Yes<br>&lt;img src=x onerror=alert(1)&gt;');
168
215
  });
169
216
 
170
- const rowParams = radioQuestion.formatAnswerForSummary(SECTION, JOURNEY, 'no');
171
- assert.strictEqual(rowParams[0].value, 'No');
217
+ it('should escape HTML in conditional label', () => {
218
+ const radioQuestion = new RadioQuestion({
219
+ title: TITLE,
220
+ question: QUESTION,
221
+ description: DESCRIPTION,
222
+ fieldName: FIELDNAME,
223
+ options: [
224
+ {
225
+ text: 'Yes',
226
+ value: 'yes',
227
+ conditional: { fieldName: 'details', type: 'text', label: '<b>Label</b>' }
228
+ }
229
+ ]
230
+ });
231
+
232
+ const result = radioQuestion.formatAnswer({
233
+ value: 'yes',
234
+ conditional: { yes: 'safe value' }
235
+ });
236
+ assert.strictEqual(result, 'Yes<br>&lt;b&gt;Label&lt;/b&gt; safe value');
237
+ });
238
+
239
+ it('should escape HTML in fallback value when option not found', () => {
240
+ const radioQuestion = new RadioQuestion({
241
+ title: TITLE,
242
+ question: QUESTION,
243
+ description: DESCRIPTION,
244
+ fieldName: FIELDNAME,
245
+ options: OPTIONS
246
+ });
247
+
248
+ const result = radioQuestion.formatAnswer({ value: '<script>xss</script>' });
249
+ assert.strictEqual(result, '&lt;script&gt;xss&lt;/script&gt;');
250
+ });
172
251
  });
173
252
  });
@@ -2,23 +2,9 @@ export class SelectQuestion extends OptionsQuestion {
2
2
  /**
3
3
  * @param {import('#typedefs/question-props.d.ts').SelectQuestionParams} params
4
4
  */
5
- constructor({ title, question, fieldName, viewFolder, url, hint, pageTitle, description, label, html, legend, disableAccessibleAutocomplete, options, validators, viewData }: import("#typedefs/question-props.d.ts").SelectQuestionParams);
5
+ constructor({ label, html, legend, disableAccessibleAutocomplete, viewFolder, ...parentParams }: import("#typedefs/question-props.d.ts").SelectQuestionParams);
6
6
  label: string | undefined;
7
7
  legend: string | undefined;
8
- /**
9
- * @param {import('#question').QuestionViewModel} viewModel
10
- */
11
- addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
12
- /**
13
- * returns the formatted answers values to be used to build task list elements
14
- * note: only supports a single answer
15
- *
16
- * @param {unknown} answer
17
- * @param {Journey} journey
18
- * @param {string} sectionSegment
19
- * @returns {Array.<Object>}
20
- */
21
- formatAnswerForSummary(sectionSegment: string, journey: Journey, answer: unknown): Array<Object>;
22
8
  #private;
23
9
  }
24
10
  export default SelectQuestion;
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;IAEC;;OAEG;IACH,8KAFW,OAAO,+BAA+B,EAAE,oBAAoB,EAqCtE;IAHA,0BAAkB;IAClB,2BAAoB;IAIrB;;OAEG;IACH,oCAFW,OAAO,WAAW,EAAE,iBAAiB,QAM/C;IAED;;;;;;;;OAQG;IACH,uCAHW,MAAM,WADN,OAAO,UADP,OAAO,GAGL,KAAK,CAAE,MAAM,CAAC,CAS1B;;CACD;;4BAtE2B,qCAAqC"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;IAEC;;OAEG;IACH,iGAFW,OAAO,+BAA+B,EAAE,oBAAoB,EAYtE;IAHA,0BAAkB;IAClB,2BAAoB;;CAYrB;;4BA3B2B,qCAAqC"}
@@ -5,35 +5,10 @@ export class SelectQuestion extends OptionsQuestion {
5
5
  /**
6
6
  * @param {import('#typedefs/question-props.d.ts').SelectQuestionParams} params
7
7
  */
8
- constructor({
9
- title,
10
- question,
11
- fieldName,
12
- viewFolder,
13
- url,
14
- hint,
15
- pageTitle,
16
- description,
17
- label,
18
- html,
19
- legend,
20
- disableAccessibleAutocomplete,
21
- options,
22
- validators,
23
- viewData
24
- }) {
8
+ constructor({ label, html, legend, disableAccessibleAutocomplete, viewFolder, ...parentParams }) {
25
9
  super({
26
- title,
27
- question,
28
- viewFolder: !viewFolder ? 'select' : viewFolder,
29
- fieldName,
30
- url,
31
- hint,
32
- pageTitle,
33
- description,
34
- options,
35
- validators,
36
- viewData
10
+ ...parentParams,
11
+ viewFolder: viewFolder || 'select'
37
12
  });
38
13
 
39
14
  this.html = html;
@@ -43,31 +18,13 @@ export class SelectQuestion extends OptionsQuestion {
43
18
  }
44
19
 
45
20
  /**
46
- * @param {import('#question').QuestionViewModel} viewModel
21
+ * @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
47
22
  */
48
23
  addCustomDataToViewModel(viewModel) {
49
24
  viewModel.question.label = this.label;
50
25
  viewModel.question.legend = this.legend;
51
26
  viewModel.question.disableAccessibleAutocomplete = this.#disableAccessibleAutocomplete;
52
27
  }
53
-
54
- /**
55
- * returns the formatted answers values to be used to build task list elements
56
- * note: only supports a single answer
57
- *
58
- * @param {unknown} answer
59
- * @param {Journey} journey
60
- * @param {string} sectionSegment
61
- * @returns {Array.<Object>}
62
- */
63
- formatAnswerForSummary(sectionSegment, journey, answer) {
64
- if (answer) {
65
- const selectedOption = this.options.find((option) => option.value === answer);
66
- const selectedText = selectedOption?.text || '';
67
- return super.formatAnswerForSummary(sectionSegment, journey, selectedText, false);
68
- }
69
- return super.formatAnswerForSummary(sectionSegment, journey, answer);
70
- }
71
28
  }
72
29
 
73
30
  export default SelectQuestion;
@@ -10,10 +10,6 @@ export class SingleLineInputQuestion extends Question {
10
10
  inputAttributes: Record<string, string>;
11
11
  label: string | undefined;
12
12
  classes: string;
13
- /**
14
- * @param {import('#question').QuestionViewModel} viewModel
15
- */
16
- addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
17
13
  }
18
14
  export default SingleLineInputQuestion;
19
15
  import { Question } from '#question';
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;;GAEG;AACH;IAIC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,6BAA6B,EAW/E;IAfD,qCAAqC;IACrC,iBADW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CACjB;IAWf,0BAAyB;IAEzB,gBAAmC;IAGpC;;OAEG;IACH,oCAFW,OAAO,WAAW,EAAE,iBAAiB,QAU/C;CACD;;yBAnCwB,WAAW"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;;GAEG;AACH;IAIC;;OAEG;IACH,oBAFW,OAAO,+BAA+B,EAAE,6BAA6B,EAW/E;IAfD,qCAAqC;IACrC,iBADW,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CACjB;IAWf,0BAAyB;IAEzB,gBAAmC;CAepC;;yBAnCwB,WAAW"}
@@ -22,7 +22,7 @@ export class SingleLineInputQuestion extends Question {
22
22
  }
23
23
 
24
24
  /**
25
- * @param {import('#question').QuestionViewModel} viewModel
25
+ * @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
26
26
  */
27
27
  addCustomDataToViewModel(viewModel) {
28
28
  viewModel.question.label = this.label;
@@ -21,10 +21,6 @@ export class TextEntryQuestion extends Question {
21
21
  constructor({ textEntryCheckbox, label, ...parentParams }: import("#typedefs/question-props.d.ts").TextEntryQuestionParams);
22
22
  textEntryCheckbox: import("#typedefs/question-props.d.ts").TextEntryCheckbox | undefined;
23
23
  label: string | undefined;
24
- /**
25
- * @param {import('#question').QuestionViewModel} viewModel
26
- */
27
- addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
28
24
  }
29
25
  export default TextEntryQuestion;
30
26
  export type TextEntryCheckbox = {
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AACA;;;;;GAKG;AAEH;;;;;;GAMG;AAEH;;GAEG;AACH;IACC;;OAEG;IACH,2DAFW,OAAO,+BAA+B,EAAE,uBAAuB,EAUzE;IAFA,yFAA0C;IAC1C,0BAAkB;IAGnB;;OAEG;IACH,oCAFW,OAAO,WAAW,EAAE,iBAAiB,QAK/C;CACD;;;YA9Ba,MAAM;UACN,MAAM;UACN,MAAM;;;yBAZK,WAAW"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AACA;;;;;GAKG;AAEH;;;;;;GAMG;AAEH;;GAEG;AACH;IACC;;OAEG;IACH,2DAFW,OAAO,+BAA+B,EAAE,uBAAuB,EAUzE;IAFA,yFAA0C;IAC1C,0BAAkB;CAUnB;;;YA9Ba,MAAM;UACN,MAAM;UACN,MAAM;;;yBAZK,WAAW"}
@@ -32,7 +32,7 @@ export class TextEntryQuestion extends Question {
32
32
  }
33
33
 
34
34
  /**
35
- * @param {import('#question').QuestionViewModel} viewModel
35
+ * @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
36
36
  */
37
37
  addCustomDataToViewModel(viewModel) {
38
38
  viewModel.question.label = this.label;
@@ -15,16 +15,12 @@ export class TextEntryRedactQuestion extends Question {
15
15
  showSuggestionsUi: boolean | undefined;
16
16
  summaryText: string | undefined;
17
17
  shouldTruncateSummary: boolean | undefined;
18
- prepQuestionForRendering(section: any, journey: any, customViewData: any, payload: any, options: any): import("../../index.js").QuestionViewModel<import("../../index.js").BaseQuestionViewData>;
18
+ prepQuestionForRendering(section: any, journey: any, customViewData: any, payload: any, options: any): import("#typedefs/question-types.d.ts").QuestionViewModel<import("#typedefs/question-types.d.ts").BaseQuestionViewData>;
19
19
  answerForViewModel(answers: any): string;
20
- /**
21
- * @param {import('#question').QuestionViewModel} viewModel
22
- */
23
- addCustomDataToViewModel(viewModel: import("#question").QuestionViewModel): void;
24
20
  /**
25
21
  * returns the formatted answers values to be used to build task list elements
26
22
  */
27
- formatAnswerForSummary(sectionSegment: any, journey: any, answer: any, capitals?: boolean): import("../../index.js").SummaryRow[];
23
+ formatAnswerForSummary(sectionSegment: any, journey: any, answer: any, capitals?: boolean): import("#typedefs/question-types.d.ts").SummaryRow[];
28
24
  }
29
25
  export default TextEntryRedactQuestion;
30
26
  import { Question } from '#question';
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAGA,+BAAgC,QAAG,CAAC;AACpC,mCAAoC,GAAG,CAAC;AAExC;;GAEG;AACH;IACC;;OAEG;IACH,gLAFW,OAAO,+BAA+B,EAAE,6BAA6B,EAwB/E;IAPA,yFAA0C;IAC1C,0BAAkB;IAClB,qDAAsE;IACtE,iDAA8D;IAC9D,uCAA0C;IAC1C,gCAA8B;IAC9B,2CAAkD;IAkBnD,iMAOC;IAED,yCAEC;IAED;;OAEG;IACH,oCAFW,OAAO,WAAW,EAAE,iBAAiB,QAO/C;IAED;;OAEG;IACH,kIAuBC;CACD;;yBAtGwB,WAAW"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAGA,+BAAgC,QAAG,CAAC;AACpC,mCAAoC,GAAG,CAAC;AAExC;;GAEG;AACH;IACC;;OAEG;IACH,gLAFW,OAAO,+BAA+B,EAAE,6BAA6B,EAwB/E;IAPA,yFAA0C;IAC1C,0BAAkB;IAClB,qDAAsE;IACtE,iDAA8D;IAC9D,uCAA0C;IAC1C,gCAA8B;IAC9B,2CAAkD;IAkBnD,+NAOC;IAED,yCAEC;IAYD;;OAEG;IACH,iJAuBC;CACD;;yBAtGwB,WAAW"}
@@ -64,7 +64,7 @@ export class TextEntryRedactQuestion extends Question {
64
64
  }
65
65
 
66
66
  /**
67
- * @param {import('#question').QuestionViewModel} viewModel
67
+ * @param {import('#typedefs/question-types.d.ts').QuestionViewModel} viewModel
68
68
  */
69
69
  addCustomDataToViewModel(viewModel) {
70
70
  viewModel.question.label = this.label;
@@ -1 +1 @@
1
- {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;IAIC;;;OAGG;IACH,uEAHW,OAAO,+BAA+B,EAAE,6BAA6B,oBACrE,MAAM,CAAC,MAAM,WAAW,EAkBlC;IAvBD,gCAAgC;IAChC,SADW,KAAK,CAAC,UAAU,CAAC,CACpB;IAkBP,6BAAgD;IAEhD,0BAAkB;IAClB,yBAA+C;IAGhD;;;;;;;;;;;;OAYG;IACH,kCAXW,OAAO,kBAAkB,EAAE,OAAO,WAClC,OAAO,UAAU,EAAE,OAAO,mBAC1B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YACvB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACvB,OAAO,+BAA+B,EAAE,+BAA+B,GACrE,OAAO,+BAA+B,EAAE,iBAAiB,GAAG;QACpE,QAAQ,EAAE,OAAO,+BAA+B,EAAE,iBAAiB,CAAC,UAAU,CAAC,GAAG;YAChF,OAAO,EAAC,UAAU,EAAE,CAAA;SACrB,CAAA;KACF,CAmDH;IAED;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,eAAe,GACb,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAgCzD;IAED;;OAEG;IACH,6HAaC;CACD;;;;;;;;;;yBAnKY;IACR,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE;QACX,SAAS,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,OAAO,CAAC;QACZ,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAA;CACH;yBA5BsB,6BAA6B"}
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;IAIC;;;OAGG;IACH,uEAHW,OAAO,+BAA+B,EAAE,6BAA6B,oBACrE,MAAM,CAAC,MAAM,WAAW,EAoBlC;IAzBD,gCAAgC;IAChC,SADW,KAAK,CAAC,UAAU,CAAC,CACpB;IAoBP,6BAAgD;IAEhD,0BAAkB;IAClB,yBAA+C;IAGhD;;;;;;;;;;;;OAYG;IACH,kCAXW,OAAO,kBAAkB,EAAE,OAAO,WAClC,OAAO,UAAU,EAAE,OAAO,mBAC1B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YACvB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACvB,OAAO,+BAA+B,EAAE,+BAA+B,GACrE,OAAO,+BAA+B,EAAE,iBAAiB,GAAG;QACpE,QAAQ,EAAE,OAAO,+BAA+B,EAAE,iBAAiB,CAAC,UAAU,CAAC,GAAG;YAChF,OAAO,EAAC,UAAU,EAAE,CAAA;SACrB,CAAA;KACF,CAmDH;IAED;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,eAAe,GACb,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAgCzD;IAED;;OAEG;IACH,6HAaC;CACD;;;;;;;;;;yBArKY;IACR,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE;QACX,SAAS,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,OAAO,CAAC;QACZ,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAA;CACH;yBA5BsB,6BAA6B"}
@@ -41,6 +41,8 @@ export class UnitOptionEntryQuestion extends Question {
41
41
  constructor({ conditionalFieldName, options, label, ...parentParams }, methodOverrides) {
42
42
  super(
43
43
  {
44
+ // Prevent capitalisation of answers, but allow override
45
+ capitaliseAnswer: false,
44
46
  ...parentParams,
45
47
  viewFolder: 'unit-option-entry'
46
48
  },
@@ -173,7 +175,7 @@ export class UnitOptionEntryQuestion extends Question {
173
175
  if (isNaN(answerQuantity)) throw new Error('Conditional answer had an unexpected type');
174
176
 
175
177
  const formattedAnswer = `${answerQuantity} ${answer}`;
176
- return super.formatAnswerForSummary(sectionSegment, journey, formattedAnswer, false);
178
+ return super.formatAnswerForSummary(sectionSegment, journey, formattedAnswer);
177
179
  }
178
180
  }
179
181
 
@@ -53,6 +53,21 @@ describe('./src/dynamic-forms/components/unit-option-entry/question.js', () => {
53
53
  assert.strictEqual(unitOptionEntryQuestion.options, OPTIONS);
54
54
  });
55
55
 
56
+ it('should default capitaliseAnswer to false', () => {
57
+ assert.strictEqual(unitOptionEntryQuestion.capitaliseAnswer, false);
58
+ });
59
+
60
+ it('should allow capitaliseAnswer to be set via constructor', () => {
61
+ const customQuestion = new UnitOptionEntryQuestion({
62
+ title: TITLE,
63
+ question: QUESTION,
64
+ fieldName: FIELDNAME,
65
+ conditionalFieldName: CONDITIONAL_FIELDNAME,
66
+ options: OPTIONS,
67
+ capitaliseAnswer: true
68
+ });
69
+ assert.strictEqual(customQuestion.capitaliseAnswer, true);
70
+ });
56
71
  it('should handle decimal string formatting conversion', () => {
57
72
  const journey = {
58
73
  response: {
@@ -41,7 +41,7 @@ export const questionHasAnswer = (response, question, expectedValue) => {
41
41
  if (Array.isArray(answerField)) {
42
42
  return answerField.includes(expectedValue);
43
43
  } else if (question.optionJoinString && typeof answerField === 'string') {
44
- // todo: answers from options questions sometimes are array sometimes string, why?
44
+ // todo: DF-51 answers from options questions sometimes are array sometimes string, why?
45
45
  if (!answerField) return false;
46
46
  const answers = answerField.split(question.optionJoinString);
47
47
  return answers.includes(expectedValue);
@@ -1,5 +1,23 @@
1
1
  export function getConditionalFieldName(parentField: any, conditionalField: any): string;
2
- export function getConditionalAnswer(answers: any, question: any, answer: any): any;
2
+ /**
3
+ * Get conditional answer(s) for a question.
4
+ * Returns an object keyed by option value with conditional answers, or null if no conditionals exist.
5
+ *
6
+ * @param {Record<string, unknown>} answers - all answers
7
+ * @param {{options?: Array<{value: string, conditional?: {fieldName: string}}>, fieldName: string, optionJoinString?: string}} question - question with options
8
+ * @param {string} answer - the answer value (may be separated by optionJoinString for checkboxes)
9
+ * @returns {Record<string, string> | null} conditional answer(s) keyed by option value
10
+ */
11
+ export function getConditionalAnswer(answers: Record<string, unknown>, question: {
12
+ options?: Array<{
13
+ value: string;
14
+ conditional?: {
15
+ fieldName: string;
16
+ };
17
+ }>;
18
+ fieldName: string;
19
+ optionJoinString?: string;
20
+ }, answer: string): Record<string, string> | null;
3
21
  /**
4
22
  * @param {unknown} conditional
5
23
  * @returns {conditional is {html: string}}
@@ -1 +1 @@
1
- {"version":3,"file":"question-utils.d.ts","sourceRoot":"","sources":["question-utils.js"],"names":[],"mappings":"AAAA,yFAEC;AAED,oFAGC;AAED;;;GAGG;AACH,mDAHW,OAAO,GACL,WAAW,IAAI;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAIzC"}
1
+ {"version":3,"file":"question-utils.d.ts","sourceRoot":"","sources":["question-utils.js"],"names":[],"mappings":"AAAA,yFAEC;AAED;;;;;;;;GAQG;AACH,8CALW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,YACvB;IAAC,OAAO,CAAC,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE;YAAC,SAAS,EAAE,MAAM,CAAA;SAAC,CAAA;KAAC,CAAC,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAC,UACnH,MAAM,GACJ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAyBzC;AAED;;;GAGG;AACH,mDAHW,OAAO,GACL,WAAW,IAAI;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAIzC"}
@@ -2,9 +2,38 @@ export function getConditionalFieldName(parentField, conditionalField) {
2
2
  return `${parentField}_${conditionalField}`;
3
3
  }
4
4
 
5
+ /**
6
+ * Get conditional answer(s) for a question.
7
+ * Returns an object keyed by option value with conditional answers, or null if no conditionals exist.
8
+ *
9
+ * @param {Record<string, unknown>} answers - all answers
10
+ * @param {{options?: Array<{value: string, conditional?: {fieldName: string}}>, fieldName: string, optionJoinString?: string}} question - question with options
11
+ * @param {string} answer - the answer value (may be separated by optionJoinString for checkboxes)
12
+ * @returns {Record<string, string> | null} conditional answer(s) keyed by option value
13
+ */
5
14
  export function getConditionalAnswer(answers, question, answer) {
6
- const conditionalFieldName = question.options?.find((option) => option.value === answer)?.conditional?.fieldName;
7
- return conditionalFieldName ? answers[getConditionalFieldName(question.fieldName, conditionalFieldName)] : null;
15
+ if (!answer || !question.options) {
16
+ return null;
17
+ }
18
+
19
+ // TODO DF-51 treat as an array
20
+ const joinString = question.optionJoinString || ',';
21
+ const answerValues = String(answer)
22
+ .split(joinString)
23
+ .map((v) => v.trim());
24
+
25
+ const conditionals = {};
26
+ for (const value of answerValues) {
27
+ const option = question.options.find((opt) => opt.value === value);
28
+ if (!option?.conditional?.fieldName) {
29
+ continue;
30
+ }
31
+ const conditionalValue = answers[getConditionalFieldName(question.fieldName, option.conditional.fieldName)];
32
+ if (conditionalValue) {
33
+ conditionals[value] = conditionalValue;
34
+ }
35
+ }
36
+ return Object.keys(conditionals).length > 0 ? conditionals : null;
8
37
  }
9
38
 
10
39
  /**