@aehrc/smart-forms-renderer 0.17.0 → 0.19.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 (139) hide show
  1. package/lib/components/FormComponents/BooleanItem/BooleanField.d.ts +7 -3
  2. package/lib/components/FormComponents/BooleanItem/BooleanField.js +26 -4
  3. package/lib/components/FormComponents/BooleanItem/BooleanField.js.map +1 -1
  4. package/lib/components/FormComponents/BooleanItem/BooleanItem.js +19 -10
  5. package/lib/components/FormComponents/BooleanItem/BooleanItem.js.map +1 -1
  6. package/lib/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionFields.d.ts +0 -2
  7. package/lib/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionFields.js +5 -3
  8. package/lib/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionFields.js.map +1 -1
  9. package/lib/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionItem.d.ts +0 -2
  10. package/lib/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionItem.js +3 -3
  11. package/lib/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionItem.js.map +1 -1
  12. package/lib/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetFields.d.ts +2 -3
  13. package/lib/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetFields.js +4 -1
  14. package/lib/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetFields.js.map +1 -1
  15. package/lib/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetItem.d.ts +0 -2
  16. package/lib/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetItem.js +3 -3
  17. package/lib/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetItem.js.map +1 -1
  18. package/lib/components/FormComponents/ChoiceItems/ChoiceItemSwitcher.js +5 -6
  19. package/lib/components/FormComponents/ChoiceItems/ChoiceItemSwitcher.js.map +1 -1
  20. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.d.ts +0 -2
  21. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.js +4 -1
  22. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.js.map +1 -1
  23. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.d.ts +0 -2
  24. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.js +3 -3
  25. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.js.map +1 -1
  26. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.d.ts +0 -2
  27. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.js +4 -1
  28. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.js.map +1 -1
  29. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.d.ts +0 -2
  30. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.js +3 -3
  31. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.js.map +1 -1
  32. package/lib/components/FormComponents/GroupItem/GroupItem.d.ts +2 -1
  33. package/lib/components/FormComponents/GroupItem/GroupItem.js +3 -3
  34. package/lib/components/FormComponents/GroupItem/GroupItem.js.map +1 -1
  35. package/lib/components/FormComponents/GroupItem/GroupItemSwitcher.d.ts +2 -1
  36. package/lib/components/FormComponents/GroupItem/GroupItemSwitcher.js +4 -4
  37. package/lib/components/FormComponents/GroupItem/GroupItemSwitcher.js.map +1 -1
  38. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionFields.d.ts +0 -2
  39. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionFields.js +6 -4
  40. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionFields.js.map +1 -1
  41. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionItem.d.ts +0 -2
  42. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionItem.js +3 -3
  43. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionItem.js.map +1 -1
  44. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceItemSwitcher.js +2 -4
  45. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceItemSwitcher.js.map +1 -1
  46. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionFields.d.ts +0 -2
  47. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionFields.js +4 -1
  48. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionFields.js.map +1 -1
  49. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionItem.d.ts +0 -2
  50. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionItem.js +2 -2
  51. package/lib/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionItem.js.map +1 -1
  52. package/lib/components/FormComponents/RepeatGroup/RepeatGroup.d.ts +2 -2
  53. package/lib/components/FormComponents/RepeatGroup/RepeatGroup.js +8 -2
  54. package/lib/components/FormComponents/RepeatGroup/RepeatGroup.js.map +1 -1
  55. package/lib/components/FormComponents/RepeatGroup/RepeatGroupItem.d.ts +1 -0
  56. package/lib/components/FormComponents/RepeatGroup/RepeatGroupItem.js +2 -2
  57. package/lib/components/FormComponents/RepeatGroup/RepeatGroupItem.js.map +1 -1
  58. package/lib/components/FormComponents/SingleItem/SingleItem.d.ts +2 -2
  59. package/lib/components/FormComponents/SingleItem/SingleItem.js +11 -6
  60. package/lib/components/FormComponents/SingleItem/SingleItem.js.map +1 -1
  61. package/lib/hooks/useHidden.d.ts +1 -1
  62. package/lib/hooks/useHidden.js +3 -2
  63. package/lib/hooks/useHidden.js.map +1 -1
  64. package/lib/index.d.ts +1 -0
  65. package/lib/index.js +1 -0
  66. package/lib/index.js.map +1 -1
  67. package/lib/interfaces/enableWhen.interface.d.ts +20 -5
  68. package/lib/interfaces/index.d.ts +1 -0
  69. package/lib/interfaces/index.js +2 -0
  70. package/lib/interfaces/index.js.map +1 -0
  71. package/lib/interfaces/questionnaireStore.interface.d.ts +2 -2
  72. package/lib/interfaces/renderProps.interface.d.ts +4 -0
  73. package/lib/stores/questionnaireStore.d.ts +5 -3
  74. package/lib/stores/questionnaireStore.js +12 -5
  75. package/lib/stores/questionnaireStore.js.map +1 -1
  76. package/lib/utils/choice.d.ts +1 -1
  77. package/lib/utils/choice.js +1 -1
  78. package/lib/utils/choice.js.map +1 -1
  79. package/lib/utils/enableWhen.d.ts +11 -4
  80. package/lib/utils/enableWhen.js +130 -53
  81. package/lib/utils/enableWhen.js.map +1 -1
  82. package/lib/utils/enableWhenExpression.d.ts +1 -1
  83. package/lib/utils/enableWhenExpression.js +26 -7
  84. package/lib/utils/enableWhenExpression.js.map +1 -1
  85. package/lib/utils/index.d.ts +1 -0
  86. package/lib/utils/index.js +1 -0
  87. package/lib/utils/index.js.map +1 -1
  88. package/lib/utils/misc.d.ts +5 -0
  89. package/lib/utils/misc.js +116 -0
  90. package/lib/utils/misc.js.map +1 -0
  91. package/lib/utils/qItem.d.ts +2 -1
  92. package/lib/utils/qItem.js +19 -7
  93. package/lib/utils/qItem.js.map +1 -1
  94. package/lib/utils/questionnaireStoreUtils/createQuestionaireModel.js +1 -1
  95. package/lib/utils/questionnaireStoreUtils/createQuestionaireModel.js.map +1 -1
  96. package/lib/utils/questionnaireStoreUtils/extractOtherExtensions.d.ts +13 -3
  97. package/lib/utils/questionnaireStoreUtils/extractOtherExtensions.js +81 -18
  98. package/lib/utils/questionnaireStoreUtils/extractOtherExtensions.js.map +1 -1
  99. package/lib/utils/tabs.d.ts +1 -1
  100. package/lib/utils/tabs.js +5 -3
  101. package/lib/utils/tabs.js.map +1 -1
  102. package/package.json +1 -1
  103. package/src/components/FormComponents/BooleanItem/BooleanField.tsx +53 -17
  104. package/src/components/FormComponents/BooleanItem/BooleanItem.tsx +47 -13
  105. package/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionFields.tsx +4 -2
  106. package/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerOptionItem.tsx +0 -5
  107. package/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetFields.tsx +6 -3
  108. package/src/components/FormComponents/ChoiceItems/ChoiceCheckboxAnswerValueSetItem.tsx +2 -5
  109. package/src/components/FormComponents/ChoiceItems/ChoiceItemSwitcher.tsx +1 -6
  110. package/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionFields.tsx +4 -2
  111. package/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx +1 -5
  112. package/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetFields.tsx +4 -3
  113. package/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.tsx +1 -5
  114. package/src/components/FormComponents/GroupItem/GroupItem.tsx +8 -2
  115. package/src/components/FormComponents/GroupItem/GroupItemSwitcher.tsx +10 -2
  116. package/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionFields.tsx +3 -2
  117. package/src/components/FormComponents/OpenChoiceItems/OpenChoiceCheckboxAnswerOptionItem.tsx +0 -5
  118. package/src/components/FormComponents/OpenChoiceItems/OpenChoiceItemSwitcher.tsx +0 -5
  119. package/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionFields.tsx +3 -2
  120. package/src/components/FormComponents/OpenChoiceItems/OpenChoiceRadioAnswerOptionItem.tsx +1 -4
  121. package/src/components/FormComponents/RepeatGroup/RepeatGroup.tsx +13 -2
  122. package/src/components/FormComponents/RepeatGroup/RepeatGroupItem.tsx +4 -0
  123. package/src/components/FormComponents/SingleItem/SingleItem.tsx +19 -6
  124. package/src/hooks/useHidden.ts +3 -2
  125. package/src/index.ts +1 -0
  126. package/src/interfaces/enableWhen.interface.ts +24 -5
  127. package/src/interfaces/index.ts +8 -0
  128. package/src/interfaces/questionnaireStore.interface.ts +2 -2
  129. package/src/interfaces/renderProps.interface.ts +4 -0
  130. package/src/stores/questionnaireStore.ts +45 -8
  131. package/src/utils/choice.ts +4 -2
  132. package/src/utils/enableWhen.ts +194 -55
  133. package/src/utils/enableWhenExpression.ts +35 -8
  134. package/src/utils/index.ts +5 -0
  135. package/src/utils/misc.ts +160 -0
  136. package/src/utils/qItem.ts +31 -8
  137. package/src/utils/questionnaireStoreUtils/createQuestionaireModel.ts +1 -1
  138. package/src/utils/questionnaireStoreUtils/extractOtherExtensions.ts +122 -25
  139. package/src/utils/tabs.ts +7 -4
@@ -23,7 +23,11 @@ import type {
23
23
  QuestionnaireResponseItemAnswer
24
24
  } from 'fhir/r4';
25
25
  import cloneDeep from 'lodash.clonedeep';
26
- import type { EnableWhenItemProperties, EnableWhenItems } from '../interfaces/enableWhen.interface';
26
+ import type {
27
+ EnableWhenItems,
28
+ EnableWhenRepeatItemProperties,
29
+ EnableWhenSingleItemProperties
30
+ } from '../interfaces';
27
31
 
28
32
  /**
29
33
  * Create a linkedQuestionsMap that contains linked items of enableWhen items
@@ -35,18 +39,23 @@ import type { EnableWhenItemProperties, EnableWhenItems } from '../interfaces/en
35
39
  export function createEnableWhenLinkedQuestions(enableWhenItems: EnableWhenItems) {
36
40
  const linkedQuestionsMap: Record<string, string[]> = {};
37
41
 
38
- for (const linkId in enableWhenItems) {
39
- enableWhenItems[linkId].linked.forEach((linkedItem) => {
40
- const linkQId = linkedItem.enableWhen.question;
41
- if (!linkedQuestionsMap[linkQId]) {
42
- linkedQuestionsMap[linkQId] = [];
43
- }
42
+ const { singleItems, repeatItems } = enableWhenItems;
44
43
 
45
- if (!linkedQuestionsMap[linkQId].includes(linkId)) {
46
- linkedQuestionsMap[linkQId].push(linkId);
47
- }
48
- });
44
+ for (const items of [singleItems, repeatItems]) {
45
+ for (const linkId in items) {
46
+ items[linkId].linked.forEach((linkedItem) => {
47
+ const linkQId = linkedItem.enableWhen.question;
48
+ if (!linkedQuestionsMap[linkQId]) {
49
+ linkedQuestionsMap[linkQId] = [];
50
+ }
51
+
52
+ if (!linkedQuestionsMap[linkQId].includes(linkId)) {
53
+ linkedQuestionsMap[linkQId].push(linkId);
54
+ }
55
+ });
56
+ }
49
57
  }
58
+
50
59
  return linkedQuestionsMap;
51
60
  }
52
61
 
@@ -117,11 +126,11 @@ function answerOperatorSwitcher(
117
126
  value: boolean | string | number | Quantity | QuestionnaireResponseItemAnswer,
118
127
  operator: QuestionnaireItemEnableWhen['operator']
119
128
  ): boolean {
120
- // FIXME runs even when the linked textbox is not changed
121
129
  switch (operator) {
122
- case 'exists':
123
- // check if value is an object and contains any answerValues
124
- return typeof value === 'object' && Object.keys(value).length !== 0;
130
+ case 'exists': {
131
+ const answerExists = typeof value === 'object' && Object.keys(value).length !== 0;
132
+ return answerExists === expected;
133
+ }
125
134
  case '=':
126
135
  return value === expected;
127
136
  case '!=':
@@ -139,6 +148,36 @@ function answerOperatorSwitcher(
139
148
  }
140
149
  }
141
150
 
151
+ export function mutateRepeatEnableWhenItemInstances(
152
+ items: EnableWhenItems,
153
+ parentRepeatGroupLinkId: string,
154
+ parentRepeatGroupIndex: number,
155
+ actionType: 'add' | 'remove'
156
+ ): EnableWhenItems {
157
+ const { repeatItems } = items;
158
+
159
+ for (const linkId in repeatItems) {
160
+ for (const linkedItem of repeatItems[linkId].linked) {
161
+ if (linkedItem.parentLinkId !== parentRepeatGroupLinkId) {
162
+ continue;
163
+ }
164
+
165
+ if (actionType === 'add') {
166
+ linkedItem.answers.splice(parentRepeatGroupIndex, 0);
167
+ repeatItems[linkId].enabledIndexes[parentRepeatGroupIndex] = checkItemIsEnabledRepeat(
168
+ repeatItems[linkId],
169
+ parentRepeatGroupIndex
170
+ );
171
+ } else if (actionType === 'remove') {
172
+ linkedItem.answers.splice(parentRepeatGroupIndex, 1);
173
+ repeatItems[linkId].enabledIndexes.splice(parentRepeatGroupIndex, 1);
174
+ }
175
+ }
176
+ }
177
+
178
+ return items;
179
+ }
180
+
142
181
  /**
143
182
  * Read initial answer values in questionnaireResponse
144
183
  * return a map of initial values with key-value pair <linkedItemId, initial value>
@@ -203,7 +242,13 @@ export function setInitialAnswers(
203
242
  const linkedQuestions = linkedQuestionsMap[linkId];
204
243
  const newAnswer = initialAnswers[linkId];
205
244
 
206
- updatedItems = updateItemAnswer(updatedItems, linkedQuestions, linkId, newAnswer);
245
+ updatedItems = updateEnableWhenItemAnswer(
246
+ updatedItems,
247
+ linkedQuestions,
248
+ linkId,
249
+ newAnswer,
250
+ null
251
+ );
207
252
  }
208
253
  }
209
254
  return updatedItems;
@@ -215,35 +260,65 @@ export function setInitialAnswers(
215
260
  *
216
261
  * @author Sean Fong
217
262
  */
218
- export function updateItemAnswer(
263
+ export function updateEnableWhenItemAnswer(
219
264
  items: EnableWhenItems,
220
265
  linkedQuestions: string[],
221
266
  linkId: string,
222
- newAnswer: QuestionnaireResponseItemAnswer[]
267
+ newAnswer: QuestionnaireResponseItemAnswer[] | undefined,
268
+ parentRepeatGroupIndex: number | null
223
269
  ): EnableWhenItems {
224
- linkedQuestions.forEach((question) => {
225
- // Update modified linked answer
226
- items[question].linked.forEach((linkedItem) => {
227
- if (linkedItem.enableWhen.question === linkId) {
228
- linkedItem.answer = newAnswer ?? undefined;
229
- }
230
- });
270
+ const { singleItems, repeatItems } = items;
271
+
272
+ for (const linkedQuestion of linkedQuestions) {
273
+ // Linked question is in single items
274
+ if (singleItems[linkedQuestion]) {
275
+ // Update modified linked answer
276
+ singleItems[linkedQuestion].linked.forEach((linkedItem) => {
277
+ if (linkedItem.enableWhen.question === linkId) {
278
+ linkedItem.answer = newAnswer ?? undefined;
279
+ }
280
+ });
281
+
282
+ // Update enabled status of modified enableWhenItem
283
+ singleItems[linkedQuestion].isEnabled = checkItemIsEnabledSingle(singleItems[linkedQuestion]);
284
+ continue;
285
+ }
286
+
287
+ // Linked question is in repeat items
288
+ if (repeatItems[linkedQuestion] && parentRepeatGroupIndex !== null) {
289
+ // Update modified linked answer
290
+ repeatItems[linkedQuestion].linked.forEach((linkedItem) => {
291
+ if (linkedItem.enableWhen.question === linkId) {
292
+ if (newAnswer) {
293
+ linkedItem.answers[parentRepeatGroupIndex] = newAnswer[0] ?? undefined;
294
+ } else {
295
+ delete linkedItem.answers[parentRepeatGroupIndex];
296
+ }
297
+ }
298
+ });
299
+
300
+ // Update enabled status of modified enableWhenItem
301
+ repeatItems[linkedQuestion].enabledIndexes[parentRepeatGroupIndex] = checkItemIsEnabledRepeat(
302
+ repeatItems[linkedQuestion],
303
+ parentRepeatGroupIndex
304
+ );
305
+ }
306
+ }
231
307
 
232
- // Update enabled status of modified enableWhenItem
233
- items[question].isEnabled = checkItemIsEnabled(items[question]);
234
- });
235
308
  return items;
236
309
  }
237
310
 
238
311
  /**
239
- * Check if an enableWhenItem is enabled based on the answer of its linked items
312
+ * Check if a single enableWhenItem is enabled based on the answer of its linked items
240
313
  *
241
314
  * @author Sean Fong
242
315
  */
243
- export function checkItemIsEnabled(enableWhenItemProperties: EnableWhenItemProperties): boolean {
316
+ export function checkItemIsEnabledSingle(
317
+ enableWhenItemProperties: EnableWhenSingleItemProperties
318
+ ): boolean {
244
319
  const checkedIsEnabledItems: boolean[] = [];
245
320
 
246
- enableWhenItemProperties.linked.forEach((linkedItem) => {
321
+ for (const linkedItem of enableWhenItemProperties.linked) {
247
322
  if (linkedItem.answer && linkedItem.answer.length > 0) {
248
323
  for (const answer of linkedItem.answer) {
249
324
  const isEnabledForThisLinkedItem = isEnabledAnswerTypeSwitcher(
@@ -257,16 +332,60 @@ export function checkItemIsEnabled(enableWhenItemProperties: EnableWhenItemPrope
257
332
  break;
258
333
  }
259
334
  }
335
+ continue;
260
336
  }
261
- });
337
+
338
+ // Linked item doesn't have any answers, but we still have to check for unanswered booleans
339
+ if (evaluateNonExistentAnswers(linkedItem.enableWhen)) {
340
+ checkedIsEnabledItems.push(true);
341
+ }
342
+ }
343
+
344
+ if (checkedIsEnabledItems.length === 0) {
345
+ return false;
346
+ }
347
+
348
+ return evaluateEnableBehaviour(checkedIsEnabledItems, enableWhenItemProperties.enableBehavior);
349
+ }
350
+
351
+ /**
352
+ * Check if a repeat enableWhenItem is enabled based on the answer of its linked items
353
+ *
354
+ * @author Sean Fong
355
+ */
356
+ export function checkItemIsEnabledRepeat(
357
+ enableWhenItemProperties: EnableWhenRepeatItemProperties,
358
+ parentRepeatGroupIndex: number
359
+ ): boolean {
360
+ const checkedIsEnabledItems: boolean[] = [];
361
+
362
+ for (const linkedItem of enableWhenItemProperties.linked) {
363
+ const linkedAnswer = linkedItem.answers[parentRepeatGroupIndex];
364
+ if (linkedAnswer) {
365
+ const isEnabledForThisLinkedItem = isEnabledAnswerTypeSwitcher(
366
+ linkedItem.enableWhen,
367
+ linkedAnswer
368
+ );
369
+
370
+ if (isEnabledForThisLinkedItem) {
371
+ checkedIsEnabledItems.push(
372
+ isEnabledAnswerTypeSwitcher(linkedItem.enableWhen, linkedAnswer)
373
+ );
374
+ }
375
+ continue;
376
+ }
377
+
378
+ // Linked item doesn't have any answers, but we still have to check for unanswered booleans
379
+ if (evaluateNonExistentAnswers(linkedItem.enableWhen)) {
380
+ checkedIsEnabledItems.push(true);
381
+ }
382
+ }
262
383
 
263
384
  if (checkedIsEnabledItems.length === 0) {
264
385
  return false;
265
386
  }
266
387
 
267
- return enableWhenItemProperties.enableBehavior === 'any'
268
- ? checkedIsEnabledItems.some((isEnabled) => isEnabled)
269
- : checkedIsEnabledItems.every((isEnabled) => isEnabled);
388
+ return evaluateEnableBehaviour(checkedIsEnabledItems, enableWhenItemProperties.enableBehavior);
270
389
  }
271
390
 
272
391
  export function assignPopulatedAnswersToEnableWhen(
@@ -275,7 +394,7 @@ export function assignPopulatedAnswersToEnableWhen(
275
394
  ): { initialisedItems: EnableWhenItems; linkedQuestions: Record<string, string[]> } {
276
395
  const linkedQuestions = createEnableWhenLinkedQuestions(items);
277
396
  const initialAnswers = readInitialAnswers(questionnaireResponse, linkedQuestions);
278
- items = initialiseBooleanFalses(items);
397
+ items = initialiseUnansweredBooleans(items);
279
398
 
280
399
  const initialisedItems =
281
400
  Object.keys(initialAnswers).length > 0
@@ -285,30 +404,50 @@ export function assignPopulatedAnswersToEnableWhen(
285
404
  return { initialisedItems, linkedQuestions };
286
405
  }
287
406
 
288
- function initialiseBooleanFalses(items: EnableWhenItems): EnableWhenItems {
289
- for (const linkId in items) {
290
- const checkedIsEnabledItems: boolean[] = [];
291
- const enableWhenItemProperties = items[linkId];
407
+ function initialiseUnansweredBooleans(items: EnableWhenItems): EnableWhenItems {
408
+ const { singleItems, repeatItems } = items;
292
409
 
293
- for (const linkedItem of enableWhenItemProperties.linked) {
294
- if (linkedItem.enableWhen.answerBoolean === true && linkedItem.enableWhen.operator === '!=') {
295
- checkedIsEnabledItems.push(true);
296
- continue;
297
- }
410
+ // Initialise unanswered booleans for enableWhen single items
411
+ for (const linkId in singleItems) {
412
+ const checkedIsEnabledItems = singleItems[linkId].linked.map((linkedItem) =>
413
+ evaluateNonExistentAnswers(linkedItem.enableWhen)
414
+ );
298
415
 
299
- if (linkedItem.enableWhen.answerBoolean === false && linkedItem.enableWhen.operator === '=') {
300
- checkedIsEnabledItems.push(true);
301
- continue;
302
- }
416
+ singleItems[linkId].isEnabled = evaluateEnableBehaviour(
417
+ checkedIsEnabledItems,
418
+ singleItems[linkId].enableBehavior
419
+ );
420
+ }
303
421
 
304
- checkedIsEnabledItems.push(false);
305
- }
422
+ // Initialise unanswered booleans for enableWhen repeat items
423
+ for (const linkId in repeatItems) {
424
+ const checkedIsEnabledItems = repeatItems[linkId].linked.map((linkedItem) =>
425
+ evaluateNonExistentAnswers(linkedItem.enableWhen)
426
+ );
306
427
 
307
- enableWhenItemProperties.isEnabled =
308
- enableWhenItemProperties.enableBehavior === 'any'
309
- ? checkedIsEnabledItems.some((isEnabled) => isEnabled)
310
- : checkedIsEnabledItems.every((isEnabled) => isEnabled);
428
+ const isEnabled = evaluateEnableBehaviour(
429
+ checkedIsEnabledItems,
430
+ repeatItems[linkId].enableBehavior
431
+ );
432
+ repeatItems[linkId].enabledIndexes = repeatItems[linkId].enabledIndexes.map(() => isEnabled);
311
433
  }
312
434
 
313
435
  return items;
314
436
  }
437
+
438
+ // Internal functions
439
+ function evaluateNonExistentAnswers(enableWhen: QuestionnaireItemEnableWhen) {
440
+ const unansweredBoolean =
441
+ typeof enableWhen.answerBoolean === 'boolean' && enableWhen.operator === '!=';
442
+ const unExistingAnswer = enableWhen.answerBoolean === false && enableWhen.operator === 'exists';
443
+ return unansweredBoolean || unExistingAnswer;
444
+ }
445
+
446
+ function evaluateEnableBehaviour(
447
+ isEnabledArr: boolean[],
448
+ enableBehavior: 'all' | 'any' | undefined
449
+ ) {
450
+ return enableBehavior === 'any'
451
+ ? isEnabledArr.some((isEnabled) => isEnabled)
452
+ : isEnabledArr.every((isEnabled) => isEnabled);
453
+ }
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
 
18
- import type { EnableWhenExpression } from '../interfaces/enableWhen.interface';
18
+ import type { EnableWhenExpression } from '../interfaces';
19
19
  import type { Expression, QuestionnaireResponse } from 'fhir/r4';
20
20
  import { createFhirPathContext } from './fhirpath';
21
21
  import fhirpath from 'fhirpath';
@@ -69,13 +69,14 @@ export function evaluateInitialEnableWhenExpressions(
69
69
  fhirpath_r4_model
70
70
  );
71
71
 
72
+ // Update enableWhenExpressions if length of result array > 0
72
73
  if (result.length > 0) {
73
- initialEnableWhenExpressions[linkId].isEnabled = result[0];
74
+ updateEnableWhenExpressionStatus(initialEnableWhenExpressions, linkId, result);
74
75
  }
75
76
 
76
77
  // handle intersect edge case - evaluate() returns empty array if result is false
77
78
  if (enableWhenExpressions[linkId].expression.includes('intersect') && result.length === 0) {
78
- initialEnableWhenExpressions[linkId].isEnabled = false;
79
+ initialEnableWhenExpressions[linkId].isEnabledSingle = false;
79
80
  }
80
81
  } catch (e) {
81
82
  console.warn(
@@ -112,16 +113,14 @@ export function evaluateEnableWhenExpressions(
112
113
  fhirpath_r4_model
113
114
  );
114
115
 
116
+ // Update enableWhenExpressions if length of result array > 0
115
117
  if (result.length > 0) {
116
- if (enableWhenExpressions[linkId].isEnabled !== result[0]) {
117
- isUpdated = true;
118
- updatedEnableWhenExpressions[linkId].isEnabled = result[0];
119
- }
118
+ isUpdated = updateEnableWhenExpressionStatus(updatedEnableWhenExpressions, linkId, result);
120
119
  }
121
120
 
122
121
  // handle intersect edge case - evaluate() returns empty array if result is false
123
122
  if (enableWhenExpressions[linkId].expression.includes('intersect') && result.length === 0) {
124
- updatedEnableWhenExpressions[linkId].isEnabled = false;
123
+ updatedEnableWhenExpressions[linkId].isEnabledSingle = false;
125
124
  }
126
125
  } catch (e) {
127
126
  console.warn(
@@ -136,3 +135,31 @@ export function evaluateEnableWhenExpressions(
136
135
  updatedEnableWhenExpressions: updatedEnableWhenExpressions
137
136
  };
138
137
  }
138
+
139
+ function updateEnableWhenExpressionStatus(
140
+ enableWhenExpressions: Record<string, EnableWhenExpression>,
141
+ linkId: string,
142
+ result: any[]
143
+ ): boolean {
144
+ // Values are not fully boolean, expression is invalid
145
+ const everyResultIsBoolean = result.every((r) => typeof r === 'boolean');
146
+ if (!everyResultIsBoolean) {
147
+ return false;
148
+ }
149
+
150
+ // If result has multiple values
151
+ if (result.length > 1) {
152
+ if (enableWhenExpressions[linkId].isEnabledMultiple !== result) {
153
+ enableWhenExpressions[linkId].isEnabledMultiple = result;
154
+ return true;
155
+ }
156
+ }
157
+
158
+ // If result has only one value
159
+ if (enableWhenExpressions[linkId].isEnabledSingle !== result[0]) {
160
+ enableWhenExpressions[linkId].isEnabledSingle = result[0];
161
+ return true;
162
+ }
163
+
164
+ return false;
165
+ }
@@ -17,3 +17,8 @@
17
17
 
18
18
  export { isSpecificItemControl } from './itemControl';
19
19
  export { isRepeatItemAndNotCheckbox } from './qItem';
20
+ export {
21
+ createEnableWhenLinkedQuestions,
22
+ readInitialAnswers,
23
+ setInitialAnswers
24
+ } from './enableWhen';
@@ -0,0 +1,160 @@
1
+ /*
2
+ * Copyright 2024 Commonwealth Scientific and Industrial Research
3
+ * Organisation (CSIRO) ABN 41 687 119 230.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ // TODO to be imported into sdc-fhir-helpers
19
+
20
+ import type { Questionnaire, QuestionnaireItem } from 'fhir/r4';
21
+
22
+ export function getQuestionnaireItem(
23
+ questionnaire: Questionnaire,
24
+ targetLinkId: string
25
+ ): QuestionnaireItem | null {
26
+ // Search through the top level items recursively
27
+ const topLevelQItems = questionnaire.item;
28
+ if (topLevelQItems) {
29
+ for (const topLevelQItem of topLevelQItems) {
30
+ const foundQItem = getQuestionnaireItemRecursive(topLevelQItem, targetLinkId);
31
+ if (foundQItem) {
32
+ return foundQItem;
33
+ }
34
+ }
35
+ }
36
+
37
+ // No matching item found in the questionnaire, return null
38
+ return null;
39
+ }
40
+
41
+ export function getQuestionnaireItemRecursive(
42
+ qItem: QuestionnaireItem,
43
+ targetLinkId: string
44
+ ): QuestionnaireItem | null {
45
+ // Target linkId found in current item
46
+ if (qItem.linkId === targetLinkId) {
47
+ return qItem;
48
+ }
49
+
50
+ // Search through its child items recursively
51
+ const childQItems = qItem.item;
52
+ if (childQItems) {
53
+ for (const childQItem of childQItems) {
54
+ const foundQItem = getQuestionnaireItemRecursive(childQItem, targetLinkId);
55
+ if (foundQItem) {
56
+ return foundQItem;
57
+ }
58
+ }
59
+ }
60
+
61
+ // No matching item found in the current item or its child items, return null
62
+ return null;
63
+ }
64
+
65
+ ///
66
+
67
+ export function getParentItem(
68
+ questionnaire: Questionnaire,
69
+ targetLinkId: string
70
+ ): QuestionnaireItem | null {
71
+ // Search through the top level items recursively
72
+ const topLevelQItems = questionnaire.item;
73
+ if (topLevelQItems) {
74
+ for (const topLevelQItem of topLevelQItems) {
75
+ const foundParentQItem = getParentItemRecursive(topLevelQItem, targetLinkId);
76
+ if (foundParentQItem) {
77
+ return foundParentQItem;
78
+ }
79
+ }
80
+ }
81
+
82
+ // No matching parent item found in the questionnaire, return null
83
+ return null;
84
+ }
85
+
86
+ function getParentItemRecursive(
87
+ qItem: QuestionnaireItem,
88
+ targetLinkId: string,
89
+ parentQItem?: QuestionnaireItem
90
+ ): QuestionnaireItem | null {
91
+ // Current item has the target linkId, return the parent item if it exists
92
+ if (qItem.linkId === targetLinkId) {
93
+ return parentQItem ?? null;
94
+ }
95
+
96
+ // Search through its child items recursively
97
+ const childQItems = qItem.item;
98
+ if (childQItems) {
99
+ for (const childQItem of childQItems) {
100
+ const foundParentQItem = getParentItemRecursive(childQItem, targetLinkId, qItem);
101
+ if (foundParentQItem) {
102
+ return foundParentQItem;
103
+ }
104
+ }
105
+ }
106
+
107
+ // No matching parent item found in the current item or its child items, return null
108
+ return null;
109
+ }
110
+
111
+ export function getRepeatGroupParentItem(
112
+ questionnaire: Questionnaire,
113
+ targetLinkId: string
114
+ ): QuestionnaireItem | null {
115
+ // Search through the top level items recursively
116
+ const topLevelQItems = questionnaire.item;
117
+ if (topLevelQItems) {
118
+ for (const topLevelQItem of topLevelQItems) {
119
+ const foundParentQItem = getRepeatGroupParentItemRecursive(topLevelQItem, targetLinkId);
120
+ if (foundParentQItem) {
121
+ return foundParentQItem;
122
+ }
123
+ }
124
+ }
125
+
126
+ // No matching repeat group parent item found in the questionnaire, return null
127
+ return null;
128
+ }
129
+
130
+ function getRepeatGroupParentItemRecursive(
131
+ qItem: QuestionnaireItem,
132
+ targetLinkId: string,
133
+ repeatGroupParentQItem?: QuestionnaireItem
134
+ ): QuestionnaireItem | null {
135
+ // Current item has the target linkId, return the parent item if it exists
136
+ if (qItem.linkId === targetLinkId && repeatGroupParentQItem) {
137
+ return repeatGroupParentQItem ?? null;
138
+ }
139
+
140
+ // Check if the current item is a repeat group
141
+ const isRepeatGroup = qItem.repeats && qItem.type === 'group';
142
+
143
+ // Search through its child items recursively
144
+ const childQItems = qItem.item;
145
+ if (childQItems) {
146
+ for (const childQItem of childQItems) {
147
+ const foundParentQItem = getRepeatGroupParentItemRecursive(
148
+ childQItem,
149
+ targetLinkId,
150
+ isRepeatGroup ? qItem : repeatGroupParentQItem
151
+ );
152
+ if (foundParentQItem) {
153
+ return foundParentQItem;
154
+ }
155
+ }
156
+ }
157
+
158
+ // No matching repeat group parent item found in the current item or its child items, return null
159
+ return null;
160
+ }
@@ -19,28 +19,51 @@ import type { Extension, Questionnaire, QuestionnaireItem } from 'fhir/r4';
19
19
  import { getChoiceControlType } from './choice';
20
20
  import { ChoiceItemControl, OpenChoiceItemControl } from '../interfaces/choice.enum';
21
21
  import { getOpenChoiceControlType } from './openChoice';
22
- import type { EnableWhenExpression, EnableWhenItems } from '../interfaces/enableWhen.interface';
22
+ import type { EnableWhenExpression, EnableWhenItems } from '../interfaces';
23
23
 
24
24
  interface isHiddenByEnableWhensParams {
25
25
  linkId: string;
26
26
  enableWhenIsActivated: boolean;
27
27
  enableWhenItems: EnableWhenItems;
28
28
  enableWhenExpressions: Record<string, EnableWhenExpression>;
29
+ parentRepeatGroupIndex?: number;
29
30
  }
30
31
 
31
32
  export function isHiddenByEnableWhen(params: isHiddenByEnableWhensParams): boolean {
32
- const { linkId, enableWhenIsActivated, enableWhenItems, enableWhenExpressions } = params;
33
+ const {
34
+ linkId,
35
+ enableWhenIsActivated,
36
+ enableWhenItems,
37
+ enableWhenExpressions,
38
+ parentRepeatGroupIndex
39
+ } = params;
40
+
41
+ const { singleItems, repeatItems } = enableWhenItems;
42
+
43
+ // If enableWhen is not activated, items are not hidden by enableWhen
44
+ if (!enableWhenIsActivated) {
45
+ return false;
46
+ }
33
47
 
34
- if (enableWhenIsActivated) {
35
- if (enableWhenItems[linkId]) {
36
- return !enableWhenItems[linkId].isEnabled;
37
- }
48
+ if (singleItems[linkId]) {
49
+ return !singleItems[linkId].isEnabled;
50
+ }
38
51
 
39
- if (enableWhenExpressions[linkId]) {
40
- return !enableWhenExpressions[linkId].isEnabled;
52
+ if (repeatItems[linkId] && parentRepeatGroupIndex !== undefined) {
53
+ return !repeatItems[linkId].enabledIndexes[parentRepeatGroupIndex];
54
+ }
55
+
56
+ if (enableWhenExpressions[linkId] && parentRepeatGroupIndex !== undefined) {
57
+ const isEnabledMultiple = enableWhenExpressions[linkId].isEnabledMultiple;
58
+ if (isEnabledMultiple) {
59
+ return !isEnabledMultiple[parentRepeatGroupIndex];
41
60
  }
42
61
  }
43
62
 
63
+ if (enableWhenExpressions[linkId]) {
64
+ return !enableWhenExpressions[linkId].isEnabledSingle;
65
+ }
66
+
44
67
  return false;
45
68
  }
46
69
 
@@ -100,7 +100,7 @@ function createEmptyModel(): QuestionnaireModel {
100
100
  calculatedExpressions: {},
101
101
  enableWhenExpressions: {},
102
102
  answerExpressions: {},
103
- enableWhenItems: {},
103
+ enableWhenItems: { singleItems: {}, repeatItems: {} },
104
104
  processedValueSetCodings: {},
105
105
  processedValueSetUrls: {},
106
106
  fhirPathContext: {}