@planning-inspectorate/dynamic-forms 3.15.6 → 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.
Files changed (264) hide show
  1. package/.gitattributes +3 -0
  2. package/.prettierrc +1 -0
  3. package/README.md +123 -4
  4. package/package.json +6 -4
  5. package/{types/src → src}/components/address/question.d.ts +6 -2
  6. package/src/components/address/question.d.ts.map +1 -0
  7. package/src/components/address/question.js +7 -19
  8. package/{types/src → src}/components/boolean/question.d.ts +2 -2
  9. package/src/components/boolean/question.d.ts.map +1 -0
  10. package/src/components/boolean/question.js +1 -1
  11. package/src/components/checkbox/question.d.ts +9 -0
  12. package/src/components/checkbox/question.d.ts.map +1 -0
  13. package/src/components/checkbox/question.js +37 -54
  14. package/src/components/checkbox/question.test.js +170 -27
  15. package/{types/src → src}/components/date/question.d.ts +2 -2
  16. package/src/components/date/question.d.ts.map +1 -0
  17. package/src/components/date/question.js +7 -16
  18. package/{types/src → src}/components/date-period/question.d.ts +12 -4
  19. package/src/components/date-period/question.d.ts.map +1 -0
  20. package/src/components/date-period/question.js +2 -3
  21. package/{types/src → src}/components/date-time/question.d.ts +2 -2
  22. package/src/components/date-time/question.d.ts.map +1 -0
  23. package/src/components/date-time/question.js +5 -14
  24. package/src/components/email/question.d.ts +23 -0
  25. package/src/components/email/question.d.ts.map +1 -0
  26. package/src/components/email/question.js +1 -9
  27. package/src/components/manage-list/answer-summary-list.njk +1 -1
  28. package/src/components/manage-list/manage-list-actions.d.ts.map +1 -0
  29. package/src/components/manage-list/manage-list-section.d.ts.map +1 -0
  30. package/{types/src → src}/components/manage-list/question.d.ts +6 -3
  31. package/src/components/manage-list/question.d.ts.map +1 -0
  32. package/src/components/manage-list/question.js +5 -2
  33. package/{types/src → src}/components/manage-list/utils.d.ts +3 -3
  34. package/src/components/manage-list/utils.d.ts.map +1 -0
  35. package/src/components/manage-list/utils.js +2 -2
  36. package/src/components/multi-field-input/question.d.ts +52 -0
  37. package/src/components/multi-field-input/question.d.ts.map +1 -0
  38. package/src/components/multi-field-input/question.js +5 -13
  39. package/src/components/number-entry/question.d.ts +12 -0
  40. package/src/components/number-entry/question.d.ts.map +1 -0
  41. package/src/components/number-entry/question.js +4 -22
  42. package/src/components/radio/question.d.ts +11 -0
  43. package/src/components/radio/question.d.ts.map +1 -0
  44. package/src/components/radio/question.js +30 -51
  45. package/src/components/radio/question.test.js +139 -60
  46. package/src/components/select/question.d.ts +12 -0
  47. package/src/components/select/question.d.ts.map +1 -0
  48. package/src/components/select/question.js +5 -48
  49. package/{types/src → src}/components/single-line-input/question.d.ts +2 -2
  50. package/src/components/single-line-input/question.d.ts.map +1 -0
  51. package/src/components/single-line-input/question.js +2 -2
  52. package/{types/src → src}/components/text-entry/question.d.ts +3 -3
  53. package/src/components/text-entry/question.d.ts.map +1 -0
  54. package/src/components/text-entry/question.js +2 -2
  55. package/{types/src → src}/components/text-entry-redact/question.d.ts +8 -9
  56. package/src/components/text-entry-redact/question.d.ts.map +1 -0
  57. package/src/components/text-entry-redact/question.js +5 -2
  58. package/{types/src → src}/components/unit-option-entry/question.d.ts +12 -8
  59. package/src/components/unit-option-entry/question.d.ts.map +1 -0
  60. package/src/components/unit-option-entry/question.js +5 -6
  61. package/src/components/utils/component-types.d.ts.map +1 -0
  62. package/src/components/utils/persisted-number-answer.d.ts.map +1 -0
  63. package/src/components/utils/question-has-answer.d.ts.map +1 -0
  64. package/src/components/utils/question-has-answer.js +1 -1
  65. package/src/components/utils/question-utils.d.ts +33 -0
  66. package/src/components/utils/question-utils.d.ts.map +1 -0
  67. package/src/components/utils/question-utils.js +31 -2
  68. package/src/components/utils/question-utils.test.js +89 -2
  69. package/{types/src → src}/controller.d.ts +2 -7
  70. package/src/controller.d.ts.map +1 -0
  71. package/src/controller.js +3 -10
  72. package/{types/src → src}/index.d.ts +4 -3
  73. package/src/index.d.ts.map +1 -0
  74. package/src/index.js +6 -4
  75. package/src/journey/journey-response.d.ts +28 -0
  76. package/src/journey/journey-response.d.ts.map +1 -0
  77. package/src/journey/journey-response.js +4 -8
  78. package/{types/src → src}/journey/journey.d.ts +8 -8
  79. package/src/journey/journey.d.ts.map +1 -0
  80. package/src/journey/journey.js +6 -6
  81. package/src/lib/address-utils.d.ts.map +1 -0
  82. package/src/lib/address.d.ts.map +1 -0
  83. package/src/lib/date-utils.d.ts.map +1 -0
  84. package/src/lib/session-answer-store.d.ts.map +1 -0
  85. package/src/lib/utils.d.ts.map +1 -0
  86. package/src/lib/uuid.d.ts.map +1 -0
  87. package/src/middleware/build-get-journey.d.ts.map +1 -0
  88. package/src/middleware/redirect-to-unanswered-question.d.ts +12 -0
  89. package/src/middleware/redirect-to-unanswered-question.d.ts.map +1 -0
  90. package/src/middleware/redirect-to-unanswered-question.js +1 -1
  91. package/src/questions/create-questions.d.ts +18 -0
  92. package/src/questions/create-questions.d.ts.map +1 -0
  93. package/src/questions/create-questions.js +3 -3
  94. package/{types/src → src}/questions/options-question.d.ts +7 -7
  95. package/src/questions/options-question.d.ts.map +1 -0
  96. package/src/questions/options-question.js +29 -4
  97. package/src/questions/options-question.test.js +106 -0
  98. package/{types/src → src}/questions/question.d.ts +48 -79
  99. package/src/questions/question.d.ts.map +1 -0
  100. package/src/questions/question.js +83 -60
  101. package/src/questions/question.test.js +86 -0
  102. package/src/questions/questions.d.ts +3 -0
  103. package/src/questions/questions.d.ts.map +1 -0
  104. package/src/questions/questions.js +1 -1
  105. package/{types/src → src}/section.d.ts +16 -15
  106. package/src/section.d.ts.map +1 -0
  107. package/src/section.js +12 -14
  108. package/src/validator/address-validator.d.ts.map +1 -0
  109. package/src/validator/base-validator.d.ts.map +1 -0
  110. package/src/validator/conditional-required-validator.d.ts.map +1 -0
  111. package/src/validator/confirmation-checkbox-validator.d.ts.map +1 -0
  112. package/src/validator/coordinates-validator.d.ts.map +1 -0
  113. package/src/validator/cross-question-validator.d.ts.map +1 -0
  114. package/src/validator/date-period-validator.d.ts.map +1 -0
  115. package/src/validator/date-time-validator.d.ts.map +1 -0
  116. package/src/validator/date-validator.d.ts.map +1 -0
  117. package/src/validator/document-upload-validator.d.ts.map +1 -0
  118. package/src/validator/email-validator.d.ts.map +1 -0
  119. package/{types/src → src}/validator/multi-field-input-validator.d.ts +3 -3
  120. package/src/validator/multi-field-input-validator.d.ts.map +1 -0
  121. package/src/validator/multi-field-input-validator.js +3 -3
  122. package/{types/src → src}/validator/numeric-validator.d.ts +2 -2
  123. package/src/validator/numeric-validator.d.ts.map +1 -0
  124. package/src/validator/numeric-validator.js +2 -2
  125. package/src/validator/required-validator.d.ts.map +1 -0
  126. package/src/validator/same-answer-validator.d.ts.map +1 -0
  127. package/{types/src → src}/validator/string-validator.d.ts +3 -3
  128. package/src/validator/string-validator.d.ts.map +1 -0
  129. package/src/validator/string-validator.js +3 -3
  130. package/src/validator/unit-option-entry-validator.d.ts.map +1 -0
  131. package/src/validator/valid-option-validator.d.ts.map +1 -0
  132. package/src/validator/validation-error-handler.d.ts.map +1 -0
  133. package/src/validator/validator.d.ts.map +1 -0
  134. package/test/journey.js +2 -0
  135. package/test/questions.js +51 -1
  136. package/test/secondary-actions.test.js +1 -1
  137. package/test/snapshots/check-your-answers-not-started.html +22 -0
  138. package/test/snapshots/check-your-answers-with-answers.html +22 -0
  139. package/test/snapshots/favourite-activity-reason.html +54 -0
  140. package/test/snapshots/travel-requirements.html +48 -0
  141. package/test/utils/question-test-utils.js +36 -6
  142. package/tsconfig.json +11 -4
  143. package/{types/src/questions → typedefs}/question-props.d.ts +28 -26
  144. package/typedefs/question-types.d.ts +141 -0
  145. package/{src → typedefs}/section-types.d.ts +1 -1
  146. package/typedefs/section-types.js +1 -0
  147. package/move-types.js +0 -12
  148. package/src/questions/question-props.d.ts +0 -295
  149. package/src/questions/question-types.d.ts +0 -86
  150. package/types/commitlint.config.d.ts +0 -7
  151. package/types/commitlint.config.d.ts.map +0 -1
  152. package/types/eslint.config.d.ts +0 -97
  153. package/types/eslint.config.d.ts.map +0 -1
  154. package/types/move-types.d.ts +0 -2
  155. package/types/move-types.d.ts.map +0 -1
  156. package/types/src/components/address/question.d.ts.map +0 -1
  157. package/types/src/components/boolean/question.d.ts.map +0 -1
  158. package/types/src/components/checkbox/question.d.ts +0 -33
  159. package/types/src/components/checkbox/question.d.ts.map +0 -1
  160. package/types/src/components/date/question.d.ts.map +0 -1
  161. package/types/src/components/date-period/question.d.ts.map +0 -1
  162. package/types/src/components/date-time/question.d.ts.map +0 -1
  163. package/types/src/components/email/question.d.ts +0 -35
  164. package/types/src/components/email/question.d.ts.map +0 -1
  165. package/types/src/components/manage-list/manage-list-actions.d.ts.map +0 -1
  166. package/types/src/components/manage-list/manage-list-section.d.ts.map +0 -1
  167. package/types/src/components/manage-list/question.d.ts.map +0 -1
  168. package/types/src/components/manage-list/utils.d.ts.map +0 -1
  169. package/types/src/components/multi-field-input/question.d.ts +0 -64
  170. package/types/src/components/multi-field-input/question.d.ts.map +0 -1
  171. package/types/src/components/number-entry/question.d.ts +0 -36
  172. package/types/src/components/number-entry/question.d.ts.map +0 -1
  173. package/types/src/components/radio/question.d.ts +0 -23
  174. package/types/src/components/radio/question.d.ts.map +0 -1
  175. package/types/src/components/select/question.d.ts +0 -22
  176. package/types/src/components/select/question.d.ts.map +0 -1
  177. package/types/src/components/single-line-input/question.d.ts.map +0 -1
  178. package/types/src/components/text-entry/question.d.ts.map +0 -1
  179. package/types/src/components/text-entry-redact/question.d.ts.map +0 -1
  180. package/types/src/components/unit-option-entry/question.d.ts.map +0 -1
  181. package/types/src/components/utils/component-types.d.ts.map +0 -1
  182. package/types/src/components/utils/persisted-number-answer.d.ts.map +0 -1
  183. package/types/src/components/utils/question-has-answer.d.ts.map +0 -1
  184. package/types/src/components/utils/question-utils.d.ts +0 -15
  185. package/types/src/components/utils/question-utils.d.ts.map +0 -1
  186. package/types/src/controller.d.ts.map +0 -1
  187. package/types/src/index.d.ts.map +0 -1
  188. package/types/src/journey/journey-response.d.ts +0 -32
  189. package/types/src/journey/journey-response.d.ts.map +0 -1
  190. package/types/src/journey/journey-types.d.ts +0 -28
  191. package/types/src/journey/journey-types.d.ts.map +0 -1
  192. package/types/src/journey/journey.d.ts.map +0 -1
  193. package/types/src/lib/address-utils.d.ts.map +0 -1
  194. package/types/src/lib/address.d.ts.map +0 -1
  195. package/types/src/lib/date-utils.d.ts.map +0 -1
  196. package/types/src/lib/session-answer-store.d.ts.map +0 -1
  197. package/types/src/lib/utils.d.ts.map +0 -1
  198. package/types/src/lib/uuid.d.ts.map +0 -1
  199. package/types/src/middleware/build-get-journey.d.ts.map +0 -1
  200. package/types/src/middleware/redirect-to-unanswered-question.d.ts +0 -12
  201. package/types/src/middleware/redirect-to-unanswered-question.d.ts.map +0 -1
  202. package/types/src/questions/create-questions.d.ts +0 -18
  203. package/types/src/questions/create-questions.d.ts.map +0 -1
  204. package/types/src/questions/options-question.d.ts.map +0 -1
  205. package/types/src/questions/question-props.d.ts.map +0 -1
  206. package/types/src/questions/question-types.d.ts +0 -86
  207. package/types/src/questions/question-types.d.ts.map +0 -1
  208. package/types/src/questions/question.d.ts.map +0 -1
  209. package/types/src/questions/questions.d.ts +0 -3
  210. package/types/src/questions/questions.d.ts.map +0 -1
  211. package/types/src/section.d.ts.map +0 -1
  212. package/types/src/validator/address-validator.d.ts.map +0 -1
  213. package/types/src/validator/base-validator.d.ts.map +0 -1
  214. package/types/src/validator/conditional-required-validator.d.ts.map +0 -1
  215. package/types/src/validator/confirmation-checkbox-validator.d.ts.map +0 -1
  216. package/types/src/validator/coordinates-validator.d.ts.map +0 -1
  217. package/types/src/validator/cross-question-validator.d.ts.map +0 -1
  218. package/types/src/validator/date-period-validator.d.ts.map +0 -1
  219. package/types/src/validator/date-time-validator.d.ts.map +0 -1
  220. package/types/src/validator/date-validator.d.ts.map +0 -1
  221. package/types/src/validator/document-upload-validator.d.ts.map +0 -1
  222. package/types/src/validator/email-validator.d.ts.map +0 -1
  223. package/types/src/validator/multi-field-input-validator.d.ts.map +0 -1
  224. package/types/src/validator/numeric-validator.d.ts.map +0 -1
  225. package/types/src/validator/required-validator.d.ts.map +0 -1
  226. package/types/src/validator/same-answer-validator.d.ts.map +0 -1
  227. package/types/src/validator/string-validator.d.ts.map +0 -1
  228. package/types/src/validator/unit-option-entry-validator.d.ts.map +0 -1
  229. package/types/src/validator/valid-option-validator.d.ts.map +0 -1
  230. package/types/src/validator/validation-error-handler.d.ts.map +0 -1
  231. package/types/src/validator/validator.d.ts.map +0 -1
  232. /package/{types/src → src}/components/manage-list/manage-list-actions.d.ts +0 -0
  233. /package/{types/src → src}/components/manage-list/manage-list-section.d.ts +0 -0
  234. /package/{types/src → src}/components/utils/component-types.d.ts +0 -0
  235. /package/{types/src → src}/components/utils/persisted-number-answer.d.ts +0 -0
  236. /package/{types/src → src}/components/utils/question-has-answer.d.ts +0 -0
  237. /package/{types/src → src}/lib/address-utils.d.ts +0 -0
  238. /package/{types/src → src}/lib/address.d.ts +0 -0
  239. /package/{types/src → src}/lib/date-utils.d.ts +0 -0
  240. /package/{types/src → src}/lib/session-answer-store.d.ts +0 -0
  241. /package/{types/src → src}/lib/utils.d.ts +0 -0
  242. /package/{types/src → src}/lib/uuid.d.ts +0 -0
  243. /package/{types/src → src}/middleware/build-get-journey.d.ts +0 -0
  244. /package/{types/src → src}/validator/address-validator.d.ts +0 -0
  245. /package/{types/src → src}/validator/base-validator.d.ts +0 -0
  246. /package/{types/src → src}/validator/conditional-required-validator.d.ts +0 -0
  247. /package/{types/src → src}/validator/confirmation-checkbox-validator.d.ts +0 -0
  248. /package/{types/src → src}/validator/coordinates-validator.d.ts +0 -0
  249. /package/{types/src → src}/validator/cross-question-validator.d.ts +0 -0
  250. /package/{types/src → src}/validator/date-period-validator.d.ts +0 -0
  251. /package/{types/src → src}/validator/date-time-validator.d.ts +0 -0
  252. /package/{types/src → src}/validator/date-validator.d.ts +0 -0
  253. /package/{types/src → src}/validator/document-upload-validator.d.ts +0 -0
  254. /package/{types/src → src}/validator/email-validator.d.ts +0 -0
  255. /package/{types/src → src}/validator/required-validator.d.ts +0 -0
  256. /package/{types/src → src}/validator/same-answer-validator.d.ts +0 -0
  257. /package/{types/src → src}/validator/unit-option-entry-validator.d.ts +0 -0
  258. /package/{types/src → src}/validator/valid-option-validator.d.ts +0 -0
  259. /package/{types/src → src}/validator/validation-error-handler.d.ts +0 -0
  260. /package/{types/src → src}/validator/validator.d.ts +0 -0
  261. /package/{src/journey → typedefs}/journey-types.d.ts +0 -0
  262. /package/{src/journey → typedefs}/journey-types.js +0 -0
  263. /package/{src/questions → typedefs}/question-props.js +0 -0
  264. /package/{src/questions → typedefs}/question-types.js +0 -0
package/.gitattributes CHANGED
@@ -1,2 +1,5 @@
1
+ # Ensure all text files use LF line endings
2
+ * text=auto eol=lf
3
+
1
4
  # snapshots always use \n
2
5
  test/snapshots/** text eol=lf
package/.prettierrc CHANGED
@@ -7,5 +7,6 @@
7
7
  "trailingComma": "none",
8
8
  "bracketSpacing": true,
9
9
  "arrowParens": "always",
10
+ "endOfLine": "lf",
10
11
  "plugins": []
11
12
  }
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.
@@ -295,9 +406,17 @@ Commits must follow conventional commits, and the commit types will be used by s
295
406
  The package will be released automatically using semantic-release, on merge to main. This will include a git tag for the release, and publishing to NPM.
296
407
 
297
408
  ### Type safety
298
- We use JSDocs to describe the types used. This is helpful for the JavaScript developers, and *crucial* for this project being compatible with TypeScript. Type declarations for TypeScript users are auto-generated by `tsc` to create type declarations. So please:
299
- 1. Add JSDocs annotations to your code, use `@import` or an inline import, i.e. `@param {import('#section').Section} section`. Avoid a top-level import, e.g. `@typedef {import('../validator/base-validator.js')} BaseValidator` - this seems to create confusion when the compiler auto-generates the type declarations.
300
- 2. Maintain `index.js`. If you're adding code that users of this module will import, please make it is being exported in `index.js`.
409
+ We use JSDocs to describe the types used. This is helpful for the JavaScript developers, and *crucial* for this project being compatible with TypeScript. Type declarations for TypeScript users are auto-generated by `tsc` inline alongside the source files.
410
+
411
+ **Type definitions:**
412
+ - Shared type definitions should be placed in the `typedefs/` folder (e.g. `typedefs/question-types.d.ts`)
413
+ - New type definition files need a corresponding empty `.js` file, which needs to be exported from `index.js`, in order to be importable from the package root.
414
+ - Types can be imported using path aliases, e.g. `@param {import('#typedefs/question-types.d.ts').SummaryRow[]} rows`
415
+
416
+ **Best practices:**
417
+ 1. Always use inline imports for JSDoc types, e.g. `@param {import('#section').Section} section`
418
+ 2. Avoid top-level `@typedef` imports like `@typedef {import('../validator/base-validator.js')} BaseValidator` - these can create confusion when the compiler auto-generates declarations
419
+ 3. Maintain `index.js` - if you're adding code that users of this module will import, ensure it is exported in `index.js`
301
420
 
302
421
  Thank you for your co-operation and contributions!
303
422
 
@@ -313,7 +432,7 @@ To update any snapshots with rendering changes (or new questions), run `node --t
313
432
 
314
433
  When implement a new component, extend the base `Question` class. Common methods to override are:
315
434
 
316
- * `formatAnswerForSummary` - used for check-your-answers display
435
+ * `formatAnswer` - used for check-your-answers display
317
436
  * `getDataToSave` - answer data to save
318
437
  * `addCustomDataToViewModel` - customise the view model with extra data/configuration
319
438
  * `answerForViewModel` - customise the view model answer value
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@planning-inspectorate/dynamic-forms",
3
- "version": "3.15.6",
3
+ "version": "3.17.0",
4
4
  "description": "This package is for building GDS forms using a configuration-based approach.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
7
- "types": "./types/src/index.d.ts",
7
+ "types": "./src/index.d.ts",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/Planning-Inspectorate/dynamic-forms.git"
@@ -17,7 +17,8 @@
17
17
  "lint": "npx eslint .",
18
18
  "format": "prettier --write .",
19
19
  "format-check": "prettier --check .",
20
- "generate-types": "npx tsc --project tsconfig.json && node move-types.js",
20
+ "generate-types": "npx tsc --project tsconfig.json",
21
+ "reset-generated-types": "find . -name \"*.d.ts\" -not -path \"./typedefs/*\" -not -path \"./node_modules/*\" -delete && find . -name \"*.d.ts.map\" -not -path \"./node_modules/*\" -delete",
21
22
  "prepare": "node .husky/install.mjs",
22
23
  "test": "node --test",
23
24
  "test-coverage": "node --test --experimental-test-coverage"
@@ -30,7 +31,8 @@
30
31
  "nunjucks": "^3.2.4"
31
32
  },
32
33
  "imports": {
33
- "#question-types": "./src/questions/question-types.d.ts",
34
+ "#question-types": "./typedefs/question-types.d.ts",
35
+ "#typedefs/*": "./typedefs/*",
34
36
  "#test/*": "./test/*",
35
37
  "#base-validator": "./src/validator/base-validator.js",
36
38
  "#journey": "./src/journey/journey.js",
@@ -1,8 +1,8 @@
1
1
  export class AddressQuestion extends Question {
2
2
  /**
3
- * @param {import('../../questions/question-props.d.ts').SiteAddressQuestionParams} params
3
+ * @param {import('#typedefs/question-props.d.ts').SiteAddressQuestionParams} params
4
4
  */
5
- constructor(params: import("../../questions/question-props.d.ts").SiteAddressQuestionParams);
5
+ constructor(params: import("#typedefs/question-props.d.ts").SiteAddressQuestionParams);
6
6
  requiredFields: {
7
7
  addressLine1: boolean;
8
8
  addressLine2: boolean;
@@ -29,6 +29,10 @@ export class AddressQuestion extends Question {
29
29
  format(answer: {
30
30
  [x: string]: any;
31
31
  }): string;
32
+ /**
33
+ * returns the formatted answers values to be used to build task list elements
34
+ */
35
+ formatAnswer(answer: any): string;
32
36
  formatLabelFromRequiredFields(fieldName: any): "" | " (optional)";
33
37
  }
34
38
  export default AddressQuestion;
@@ -0,0 +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;IAqCD;;;OAGG;IACH;;eAUC;IAED;;OAEG;IACH,kCAIC;IAED,kEAMC;CACD;;yBA1IwB,WAAW"}
@@ -7,7 +7,7 @@ import AddressValidator from '../../validator/address-validator.js';
7
7
 
8
8
  export class AddressQuestion extends Question {
9
9
  /**
10
- * @param {import('../../questions/question-props.d.ts').SiteAddressQuestionParams} params
10
+ * @param {import('#typedefs/question-props.d.ts').SiteAddressQuestionParams} params
11
11
  */
12
12
  constructor(params) {
13
13
  super({
@@ -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;
@@ -122,25 +122,13 @@ export class AddressQuestion extends Question {
122
122
 
123
123
  /**
124
124
  * returns the formatted answers values to be used to build task list elements
125
- * @type {Question['formatAnswerForSummary']}
126
125
  */
127
- formatAnswerForSummary(sectionSegment, journey, answer) {
128
- let formattedAnswer = this.notStartedText;
129
-
130
- if (answer) {
131
- formattedAnswer = nl2br(escape(this.format(answer)));
132
- } else if (answer === null) {
133
- formattedAnswer = '';
134
- }
135
-
136
- return [
137
- {
138
- key: `${this.title}`,
139
- value: formattedAnswer,
140
- action: this.getAction(sectionSegment, journey, answer)
141
- }
142
- ];
126
+ formatAnswer(answer) {
127
+ if (answer === null) return '';
128
+ if (!answer) return this.notStartedText;
129
+ return nl2br(escape(this.format(answer)));
143
130
  }
131
+
144
132
  formatLabelFromRequiredFields(fieldName) {
145
133
  if (this.requiredFields && this.requiredFields[fieldName]) {
146
134
  return '';
@@ -7,9 +7,9 @@ export function booleanToYesNoValue(value: any): "yes" | "no";
7
7
  export function booleanToYesNoOrNull(value: boolean | null): string | null;
8
8
  export class BooleanQuestion extends RadioQuestion {
9
9
  /**
10
- * @param {import('../../questions/question-props.d.ts').BooleanQuestionParams} params
10
+ * @param {import('#typedefs/question-props.d.ts').BooleanQuestionParams} params
11
11
  */
12
- constructor({ title, question, fieldName, url, hint, pageTitle, description, html, validators, interfaceType, options, editable, viewData }: import("../../questions/question-props.d.ts").BooleanQuestionParams);
12
+ constructor({ title, question, fieldName, url, hint, pageTitle, description, html, validators, interfaceType, options, editable, viewData }: import("#typedefs/question-props.d.ts").BooleanQuestionParams);
13
13
  interfaceType: "checkbox" | "radio";
14
14
  /**
15
15
  * Get the data to save from the request, returns an object of answers
@@ -0,0 +1 @@
1
+ {"version":3,"file":"question.d.ts","sourceRoot":"","sources":["question.js"],"names":[],"mappings":"AAEA;;;GAGG;AAMI,sCAHI,MAAM,GAAC,GAAC,GACN,OAAO,CAOnB;AAEM,8DAEN;AAMM,4CAHI,OAAO,GAAC,IAAI,GACV,MAAM,GAAC,IAAI,CAOvB;AAED;IACC;;OAEG;IACH,6IAFW,OAAO,+BAA+B,EAAE,qBAAqB,EAmDvE;IADA,oCAAkC;IAGnC;;;;;OAKG,CACH,mBAJW,OAAO,SAAS,EAAE,OAAO,mBACzB,eAAe,GACb,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAezD;CACD;;0BA5GyB,sBAAsB"}
@@ -33,7 +33,7 @@ export const booleanToYesNoOrNull = (value) => {
33
33
 
34
34
  export class BooleanQuestion extends RadioQuestion {
35
35
  /**
36
- * @param {import('../../questions/question-props.d.ts').BooleanQuestionParams} params
36
+ * @param {import('#typedefs/question-props.d.ts').BooleanQuestionParams} params
37
37
  */
38
38
  constructor({
39
39
  title,
@@ -0,0 +1,9 @@
1
+ export class CheckboxQuestion extends OptionsQuestion {
2
+ /**
3
+ * @param {import('#typedefs/question-props.d.ts').CheckboxQuestionParams} params
4
+ */
5
+ constructor({ ...parentParams }: import("#typedefs/question-props.d.ts").CheckboxQuestionParams);
6
+ }
7
+ export default CheckboxQuestion;
8
+ import OptionsQuestion from '../../questions/options-question.js';
9
+ //# sourceMappingURL=question.d.ts.map
@@ -0,0 +1 @@
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,80 +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
- * @param {import('../../questions/question-props.d.ts').CheckboxQuestionParams} params
8
+ * @param {import('#typedefs/question-props.d.ts').CheckboxQuestionParams} params
16
9
  */
17
- constructor({ title, question, fieldName, url, pageTitle, description, options, validators, viewData }) {
10
+ constructor({ ...parentParams }) {
18
11
  super({
19
- title,
20
- question,
12
+ ...parentParams,
21
13
  viewFolder: 'checkbox',
22
- fieldName,
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
- * returns the formatted answers values to be used to build task list elements
36
- * @param {string | ConditionalAnswerObject } answer will be a single value string, a comma-separated string representing multiple values (one of which may be a conditional) or a single ConditionalAnswerObject
37
- * @param {import('#journey').Journey} journey
38
- * @param {String} sectionSegment
39
- * @returns {Array.<Object>}
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
40
28
  */
41
- formatAnswerForSummary(sectionSegment, journey, answer) {
42
- if (!answer) {
43
- return super.formatAnswerForSummary(sectionSegment, journey, answer, false);
29
+ formatAnswer(answer) {
30
+ if (answer === null || answer === undefined || answer === '') {
31
+ return this.notStartedText;
44
32
  }
45
33
 
46
- // answer is single ConditionalAnswerObject
47
- if (answer?.conditional) {
48
- const selectedOption = this.options.find((option) => option.value === answer.value);
49
-
50
- const conditionalAnswerText = selectedOption.conditional?.label
51
- ? `${selectedOption.conditional.label} ${answer.conditional}`
52
- : answer.conditional;
53
-
54
- const formattedConditionalText = [selectedOption.text, conditionalAnswerText].join('\n');
55
-
56
- 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);
57
37
  }
58
38
 
59
- // answer is a string
60
- const answerArray = answer.split(this.optionJoinString);
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);
61
49
 
62
- const formattedAnswer = this.options
63
- .filter((option) => answerArray.includes(option.value))
64
- .map((option) => {
65
- if (option.conditional) {
66
- const conditionalAnswer =
67
- journey.response.answers[
68
- questionUtils.getConditionalFieldName(this.fieldName, option.conditional.fieldName)
69
- ];
70
- return [option.text, conditionalAnswer].join('\n');
71
- }
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
+ }
72
56
 
73
- return option.text;
74
- })
75
- .join('\n');
57
+ return optionText;
58
+ });
76
59
 
77
- return super.formatAnswerForSummary(sectionSegment, journey, formattedAnswer, false);
60
+ return formattedParts.join('<br>');
78
61
  }
79
62
  }
80
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
- it('should return option label when formatAnswerForSummary is called with one answer', () => {
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
- });
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
- it('should return formatted option labels when formatAnswerForSummary is called with a string representing several non-conditional answers', () => {
65
- const question = new CheckboxQuestion(CHECKBOX_PARAMS);
66
- question.getAction = () => {};
67
- const formattedAnswer = question.formatAnswerForSummary({}, JOURNEY, '1,2');
68
- assert.strictEqual(formattedAnswer[0].value, 'a<br>b');
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
- it('should return formatted option labels when formatAnswerForSummary is called with a string representing several answers including a conditional', () => {
72
- const question = new CheckboxQuestion(CHECKBOX_PARAMS);
73
- question.getAction = () => {};
74
- const formattedAnswer = question.formatAnswerForSummary({}, JOURNEY, '1,2,3');
75
- assert.strictEqual(formattedAnswer[0].value, `a<br>b<br>c<br>${CONDITIONAL_ANSWER_TEXT}`);
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
- it('should return a formatted option label when formatAnswerForSummary is called with a single conditional answer', () => {
79
- const question = new CheckboxQuestion(CHECKBOX_PARAMS);
80
- const conditionalAnswer = {
81
- value: '3',
82
- conditional: CONDITIONAL_ANSWER_TEXT
83
- };
84
- question.getAction = () => {};
85
- const formattedAnswer = question.formatAnswerForSummary({}, JOURNEY, conditionalAnswer);
86
- assert.strictEqual(formattedAnswer[0].value, `c<br>${CONDITIONAL_ANSWER_TEXT}`);
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, '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;');
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>&lt;img src=x onerror=alert(1)&gt;');
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>&lt;b&gt;Label&lt;/b&gt; 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, '&lt;script&gt;xss&lt;/script&gt;');
229
+ });
87
230
  });
88
231
  });
@@ -3,9 +3,9 @@
3
3
  */
4
4
  export class DateQuestion extends Question {
5
5
  /**
6
- * @param {import('../../questions/question-props.d.ts').DateQuestionParams} params
6
+ * @param {import('#typedefs/question-props.d.ts').DateQuestionParams} params
7
7
  */
8
- constructor({ dateFormat, ...parentParams }: import("../../questions/question-props.d.ts").DateQuestionParams);
8
+ constructor({ dateFormat, ...parentParams }: import("#typedefs/question-props.d.ts").DateQuestionParams);
9
9
  dateFormat: string;
10
10
  /**
11
11
  * Get the data to save from the request, returns an object of answers
@@ -0,0 +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;CAWD;;yBAhFwB,6BAA6B"}