@aehrc/smart-forms-renderer 0.25.2 → 0.26.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 (79) hide show
  1. package/lib/components/FormComponents/BooleanItem/BooleanItem.js +6 -1
  2. package/lib/components/FormComponents/BooleanItem/BooleanItem.js.map +1 -1
  3. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.js +5 -2
  4. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.js.map +1 -1
  5. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.js +5 -2
  6. package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.js.map +1 -1
  7. package/lib/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.js +5 -2
  8. package/lib/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.js.map +1 -1
  9. package/lib/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.js +5 -2
  10. package/lib/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.js.map +1 -1
  11. package/lib/components/FormComponents/DecimalItem/DecimalItem.js +9 -3
  12. package/lib/components/FormComponents/DecimalItem/DecimalItem.js.map +1 -1
  13. package/lib/components/FormComponents/IntegerItem/IntegerItem.js +7 -3
  14. package/lib/components/FormComponents/IntegerItem/IntegerItem.js.map +1 -1
  15. package/lib/components/FormComponents/StringItem/StringItem.js +7 -3
  16. package/lib/components/FormComponents/StringItem/StringItem.js.map +1 -1
  17. package/lib/components/FormComponents/TextItem/TextItem.js +7 -3
  18. package/lib/components/FormComponents/TextItem/TextItem.js.map +1 -1
  19. package/lib/hooks/useBooleanCalculatedExpression.d.ts +5 -4
  20. package/lib/hooks/useBooleanCalculatedExpression.js +12 -7
  21. package/lib/hooks/useBooleanCalculatedExpression.js.map +1 -1
  22. package/lib/hooks/useCodingCalculatedExpression.d.ts +4 -3
  23. package/lib/hooks/useCodingCalculatedExpression.js +14 -7
  24. package/lib/hooks/useCodingCalculatedExpression.js.map +1 -1
  25. package/lib/hooks/useDecimalCalculatedExpression.d.ts +3 -3
  26. package/lib/hooks/useDecimalCalculatedExpression.js +11 -7
  27. package/lib/hooks/useDecimalCalculatedExpression.js.map +1 -1
  28. package/lib/hooks/useDisplayCalculatedExpression.js +8 -1
  29. package/lib/hooks/useDisplayCalculatedExpression.js.map +1 -1
  30. package/lib/hooks/useIntegerCalculatedExpression.d.ts +3 -3
  31. package/lib/hooks/useIntegerCalculatedExpression.js +11 -7
  32. package/lib/hooks/useIntegerCalculatedExpression.js.map +1 -1
  33. package/lib/hooks/useStringCalculatedExpression.d.ts +3 -3
  34. package/lib/hooks/useStringCalculatedExpression.js +12 -7
  35. package/lib/hooks/useStringCalculatedExpression.js.map +1 -1
  36. package/lib/interfaces/calculatedExpression.interface.d.ts +1 -1
  37. package/lib/stores/questionnaireResponseStore.d.ts +3 -1
  38. package/lib/stores/questionnaireResponseStore.js +9 -1
  39. package/lib/stores/questionnaireResponseStore.js.map +1 -1
  40. package/lib/stores/questionnaireStore.js +6 -0
  41. package/lib/stores/questionnaireStore.js.map +1 -1
  42. package/lib/utils/calculatedExpression.d.ts +2 -1
  43. package/lib/utils/calculatedExpression.js +14 -8
  44. package/lib/utils/calculatedExpression.js.map +1 -1
  45. package/lib/utils/enableWhenExpression.d.ts +3 -2
  46. package/lib/utils/enableWhenExpression.js +15 -5
  47. package/lib/utils/enableWhenExpression.js.map +1 -1
  48. package/lib/utils/fhirpath.d.ts +5 -4
  49. package/lib/utils/fhirpath.js +45 -33
  50. package/lib/utils/fhirpath.js.map +1 -1
  51. package/lib/utils/initialise.js +5 -2
  52. package/lib/utils/initialise.js.map +1 -1
  53. package/lib/utils/parseInputs.js +1 -1
  54. package/lib/utils/parseInputs.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/components/FormComponents/BooleanItem/BooleanItem.tsx +9 -1
  57. package/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx +5 -2
  58. package/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.tsx +5 -2
  59. package/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx +5 -2
  60. package/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.tsx +5 -2
  61. package/src/components/FormComponents/DecimalItem/DecimalItem.tsx +14 -3
  62. package/src/components/FormComponents/IntegerItem/IntegerItem.tsx +10 -3
  63. package/src/components/FormComponents/StringItem/StringItem.tsx +10 -3
  64. package/src/components/FormComponents/TextItem/TextItem.tsx +10 -3
  65. package/src/hooks/useBooleanCalculatedExpression.ts +19 -12
  66. package/src/hooks/useCodingCalculatedExpression.ts +18 -8
  67. package/src/hooks/useDecimalCalculatedExpression.ts +26 -17
  68. package/src/hooks/useDisplayCalculatedExpression.ts +12 -1
  69. package/src/hooks/useIntegerCalculatedExpression.ts +18 -13
  70. package/src/hooks/useStringCalculatedExpression.ts +16 -13
  71. package/src/interfaces/calculatedExpression.interface.ts +1 -1
  72. package/src/stores/questionnaireResponseStore.ts +16 -2
  73. package/src/stores/questionnaireStore.ts +7 -0
  74. package/src/utils/calculatedExpression.ts +24 -10
  75. package/src/utils/enableWhenExpression.ts +22 -9
  76. package/src/utils/fhirpath.ts +62 -41
  77. package/src/utils/initialise.ts +5 -2
  78. package/src/utils/parseInputs.ts +1 -1
  79. package/src/utils/questionnaireResponseStoreUtils/updatableResponseItems.ts +62 -0
@@ -16,8 +16,7 @@
16
16
  */
17
17
 
18
18
  import { useEffect, useState } from 'react';
19
- import { createEmptyQrItem } from '../utils/qrItem';
20
- import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4';
19
+ import type { QuestionnaireItem } from 'fhir/r4';
21
20
  import { useQuestionnaireStore } from '../stores';
22
21
 
23
22
  interface UseStringCalculatedExpression {
@@ -27,14 +26,14 @@ interface UseStringCalculatedExpression {
27
26
  interface useStringCalculatedExpressionProps {
28
27
  qItem: QuestionnaireItem;
29
28
  inputValue: string;
30
- setInputValue: (value: string) => void;
31
- onQrItemChange: (qrItem: QuestionnaireResponseItem) => void;
29
+ onChangeByCalcExpressionString: (newValueString: string) => void;
30
+ onChangeByCalcExpressionNull: () => void;
32
31
  }
33
32
 
34
33
  function useStringCalculatedExpression(
35
34
  props: useStringCalculatedExpressionProps
36
35
  ): UseStringCalculatedExpression {
37
- const { qItem, inputValue, setInputValue, onQrItemChange } = props;
36
+ const { qItem, inputValue, onChangeByCalcExpressionString, onChangeByCalcExpressionNull } = props;
38
37
 
39
38
  const calculatedExpressions = useQuestionnaireStore.use.calculatedExpressions();
40
39
 
@@ -53,7 +52,9 @@ function useStringCalculatedExpression(
53
52
  // only update if calculated value is different from current value
54
53
  if (
55
54
  calcExpression.value !== inputValue &&
56
- (typeof calcExpression.value === 'string' || typeof calcExpression.value === 'number')
55
+ (typeof calcExpression.value === 'string' ||
56
+ typeof calcExpression.value === 'number' ||
57
+ calcExpression.value === null)
57
58
  ) {
58
59
  // update ui to show calculated value changes
59
60
  setCalcExpUpdated(true);
@@ -61,17 +62,19 @@ function useStringCalculatedExpression(
61
62
  setCalcExpUpdated(false);
62
63
  }, 500);
63
64
 
64
- const updatedInputValue =
65
+ // calculatedExpression value is null
66
+ if (calcExpression.value === null) {
67
+ onChangeByCalcExpressionNull();
68
+ return;
69
+ }
70
+
71
+ // calculatedExpression value is a string or number
72
+ const newInputValue =
65
73
  typeof calcExpression.value === 'string'
66
74
  ? calcExpression.value
67
75
  : calcExpression.value.toString();
68
76
 
69
- // update questionnaireResponse
70
- setInputValue(updatedInputValue);
71
- onQrItemChange({
72
- ...createEmptyQrItem(qItem),
73
- answer: [{ valueString: updatedInputValue }]
74
- });
77
+ onChangeByCalcExpressionString(newInputValue);
75
78
  }
76
79
  },
77
80
  // Only trigger this effect if calculatedExpression of item changes
@@ -1,5 +1,5 @@
1
1
  export interface CalculatedExpression {
2
2
  expression: string;
3
3
  from: 'item' | 'item._text';
4
- value?: number | string | boolean | object;
4
+ value?: number | string | boolean | object | null;
5
5
  }
@@ -16,7 +16,12 @@
16
16
  */
17
17
 
18
18
  import { createStore } from 'zustand/vanilla';
19
- import type { OperationOutcome, Questionnaire, QuestionnaireResponse } from 'fhir/r4';
19
+ import type {
20
+ OperationOutcome,
21
+ Questionnaire,
22
+ QuestionnaireResponse,
23
+ QuestionnaireResponseItem
24
+ } from 'fhir/r4';
20
25
  import { emptyResponse } from '../utils/emptyResource';
21
26
  import cloneDeep from 'lodash.clonedeep';
22
27
  import type { Diff } from 'deep-diff';
@@ -24,10 +29,12 @@ import { diff } from 'deep-diff';
24
29
  import { createSelectors } from './selector';
25
30
  import { validateQuestionnaire } from '../utils/validateQuestionnaire';
26
31
  import { questionnaireStore } from './questionnaireStore';
32
+ import { createQuestionnaireResponseItemMap } from '../utils/questionnaireResponseStoreUtils/updatableResponseItems';
27
33
 
28
34
  interface QuestionnaireResponseStoreType {
29
35
  sourceResponse: QuestionnaireResponse;
30
36
  updatableResponse: QuestionnaireResponse;
37
+ updatableResponseItems: Record<string, QuestionnaireResponseItem[]>;
31
38
  formChangesHistory: (Diff<QuestionnaireResponse, QuestionnaireResponse>[] | null)[];
32
39
  invalidItems: Record<string, OperationOutcome>;
33
40
  responseIsValid: boolean;
@@ -47,6 +54,7 @@ export const questionnaireResponseStore = createStore<QuestionnaireResponseStore
47
54
  (set, get) => ({
48
55
  sourceResponse: cloneDeep(emptyResponse),
49
56
  updatableResponse: cloneDeep(emptyResponse),
57
+ updatableResponseItems: {},
50
58
  formChangesHistory: [],
51
59
  invalidItems: {},
52
60
  responseIsValid: true,
@@ -74,13 +82,15 @@ export const questionnaireResponseStore = createStore<QuestionnaireResponseStore
74
82
  buildSourceResponse: (questionnaireResponse: QuestionnaireResponse) => {
75
83
  set(() => ({
76
84
  sourceResponse: questionnaireResponse,
77
- updatableResponse: questionnaireResponse
85
+ updatableResponse: questionnaireResponse,
86
+ updatableResponseItems: createQuestionnaireResponseItemMap(questionnaireResponse)
78
87
  }));
79
88
  },
80
89
  setUpdatableResponseAsPopulated: (populatedResponse: QuestionnaireResponse) => {
81
90
  const formChanges = diff(get().updatableResponse, populatedResponse) ?? null;
82
91
  set(() => ({
83
92
  updatableResponse: populatedResponse,
93
+ updatableResponseItems: createQuestionnaireResponseItemMap(populatedResponse),
84
94
  formChangesHistory: [...get().formChangesHistory, formChanges]
85
95
  }));
86
96
  },
@@ -88,6 +98,7 @@ export const questionnaireResponseStore = createStore<QuestionnaireResponseStore
88
98
  const formChanges = diff(get().updatableResponse, updatedResponse) ?? null;
89
99
  set(() => ({
90
100
  updatableResponse: updatedResponse,
101
+ updatableResponseItems: createQuestionnaireResponseItemMap(updatedResponse),
91
102
  formChangesHistory: [...get().formChangesHistory, formChanges]
92
103
  }));
93
104
  },
@@ -95,17 +106,20 @@ export const questionnaireResponseStore = createStore<QuestionnaireResponseStore
95
106
  set(() => ({
96
107
  sourceResponse: savedResponse,
97
108
  updatableResponse: savedResponse,
109
+ updatableResponseItems: createQuestionnaireResponseItemMap(savedResponse),
98
110
  formChangesHistory: []
99
111
  })),
100
112
  setUpdatableResponseAsEmpty: (clearedResponse: QuestionnaireResponse) =>
101
113
  set(() => ({
102
114
  updatableResponse: clearedResponse,
115
+ updatableResponseItems: createQuestionnaireResponseItemMap(clearedResponse),
103
116
  formChangesHistory: []
104
117
  })),
105
118
  destroySourceResponse: () =>
106
119
  set(() => ({
107
120
  sourceResponse: cloneDeep(emptyResponse),
108
121
  updatableResponse: cloneDeep(emptyResponse),
122
+ updatableResponseItems: createQuestionnaireResponseItemMap(cloneDeep(emptyResponse)),
109
123
  formChangesHistory: []
110
124
  }))
111
125
  })
@@ -45,6 +45,7 @@ import { terminologyServerStore } from './terminologyServerStore';
45
45
  import { createSelectors } from './selector';
46
46
  import { mutateRepeatEnableWhenExpressionInstances } from '../utils/enableWhenExpression';
47
47
  import { questionnaireResponseStore } from './questionnaireResponseStore';
48
+ import { createQuestionnaireResponseItemMap } from '../utils/questionnaireResponseStoreUtils/updatableResponseItems';
48
49
 
49
50
  interface QuestionnaireStoreType {
50
51
  sourceQuestionnaire: Questionnaire;
@@ -232,6 +233,7 @@ export const questionnaireStore = createStore<QuestionnaireStoreType>()((set, ge
232
233
 
233
234
  const { updatedEnableWhenExpressions, isUpdated } = mutateRepeatEnableWhenExpressionInstances({
234
235
  questionnaireResponse: questionnaireResponseStore.getState().updatableResponse,
236
+ questionnaireResponseItemMap: questionnaireResponseStore.getState().updatableResponseItems,
235
237
  variablesFhirPath: get().variables.fhirPathVariables,
236
238
  existingFhirPathContext: get().fhirPathContext,
237
239
  enableWhenExpressions: enableWhenExpressions,
@@ -250,6 +252,7 @@ export const questionnaireStore = createStore<QuestionnaireStoreType>()((set, ge
250
252
  toggleEnableWhenActivation: (isActivated: boolean) =>
251
253
  set(() => ({ enableWhenIsActivated: isActivated })),
252
254
  updateExpressions: (updatedResponse: QuestionnaireResponse) => {
255
+ const updatedResponseItemMap = createQuestionnaireResponseItemMap(updatedResponse);
253
256
  const {
254
257
  isUpdated,
255
258
  updatedEnableWhenExpressions,
@@ -257,6 +260,7 @@ export const questionnaireStore = createStore<QuestionnaireStoreType>()((set, ge
257
260
  updatedFhirPathContext
258
261
  } = evaluateUpdatedExpressions({
259
262
  updatedResponse,
263
+ updatedResponseItemMap,
260
264
  enableWhenExpressions: get().enableWhenExpressions,
261
265
  calculatedExpressions: get().calculatedExpressions,
262
266
  variablesFhirPath: get().variables.fhirPathVariables,
@@ -284,8 +288,11 @@ export const questionnaireStore = createStore<QuestionnaireStoreType>()((set, ge
284
288
  }
285
289
  })),
286
290
  updatePopulatedProperties: (populatedResponse: QuestionnaireResponse, persistTabIndex) => {
291
+ const initialResponseItemMap = createQuestionnaireResponseItemMap(populatedResponse);
292
+
287
293
  const evaluateInitialCalculatedExpressionsResult = evaluateInitialCalculatedExpressions({
288
294
  initialResponse: populatedResponse,
295
+ initialResponseItemMap: initialResponseItemMap,
289
296
  calculatedExpressions: get().calculatedExpressions,
290
297
  variablesFhirPath: get().variables.fhirPathVariables,
291
298
  existingFhirPathContext: get().fhirPathContext
@@ -37,6 +37,7 @@ import dayjs from 'dayjs';
37
37
 
38
38
  interface EvaluateInitialCalculatedExpressionsParams {
39
39
  initialResponse: QuestionnaireResponse;
40
+ initialResponseItemMap: Record<string, QuestionnaireResponseItem[]>;
40
41
  calculatedExpressions: Record<string, CalculatedExpression[]>;
41
42
  variablesFhirPath: Record<string, Expression[]>;
42
43
  existingFhirPathContext: Record<string, any>;
@@ -48,8 +49,13 @@ export function evaluateInitialCalculatedExpressions(
48
49
  initialCalculatedExpressions: Record<string, CalculatedExpression[]>;
49
50
  updatedFhirPathContext: Record<string, any>;
50
51
  } {
51
- const { initialResponse, calculatedExpressions, variablesFhirPath, existingFhirPathContext } =
52
- params;
52
+ const {
53
+ initialResponse,
54
+ initialResponseItemMap,
55
+ calculatedExpressions,
56
+ variablesFhirPath,
57
+ existingFhirPathContext
58
+ } = params;
53
59
 
54
60
  // Return early if initialResponse is empty or there are no calculated expressions to evaluate
55
61
  if (
@@ -65,10 +71,11 @@ export function evaluateInitialCalculatedExpressions(
65
71
  const initialCalculatedExpressions: Record<string, CalculatedExpression[]> = {
66
72
  ...calculatedExpressions
67
73
  };
74
+
68
75
  const updatedFhirPathContext = createFhirPathContext(
69
76
  initialResponse,
70
- variablesFhirPath,
71
- existingFhirPathContext
77
+ initialResponseItemMap,
78
+ variablesFhirPath
72
79
  );
73
80
 
74
81
  for (const linkId in initialCalculatedExpressions) {
@@ -83,7 +90,8 @@ export function evaluateInitialCalculatedExpressions(
83
90
  fhirpath_r4_model
84
91
  );
85
92
 
86
- if (!_isEqual(calcExpression.value, result[0])) {
93
+ // Only update calculatedExpressions if length of result array > 0
94
+ if (result.length > 0 && !_isEqual(calcExpression.value, result[0])) {
87
95
  calcExpression.value = result[0];
88
96
  }
89
97
  } catch (e) {
@@ -124,11 +132,17 @@ export function evaluateCalculatedExpressions(
124
132
  fhirpath_r4_model
125
133
  );
126
134
 
127
- if (result.length > 0) {
128
- if (!_isEqual(calcExpression.value, result[0])) {
129
- isUpdated = true;
130
- calcExpression.value = result[0];
131
- }
135
+ // Update calculatedExpressions if length of result array > 0
136
+ // Only update when current calcExpression value is different from the result, otherwise it will result in an infinite loop as per issue #733
137
+ if (result.length > 0 && !_isEqual(calcExpression.value, result[0])) {
138
+ isUpdated = true;
139
+ calcExpression.value = result[0];
140
+ }
141
+
142
+ // Update calculatedExpression value to null if no result is returned
143
+ if (result.length === 0 && calcExpression.value !== null) {
144
+ isUpdated = true;
145
+ calcExpression.value = null;
132
146
  }
133
147
  } catch (e) {
134
148
  console.warn(e.message, `LinkId: ${linkId}\nExpression: ${calcExpression.expression}`);
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */
17
17
 
18
- import type { Expression, QuestionnaireResponse } from 'fhir/r4';
18
+ import type { Expression, QuestionnaireResponse, QuestionnaireResponseItem } from 'fhir/r4';
19
19
  import { createFhirPathContext } from './fhirpath';
20
20
  import fhirpath from 'fhirpath';
21
21
  import fhirpath_r4_model from 'fhirpath/fhir-context/r4';
@@ -27,9 +27,9 @@ import type {
27
27
 
28
28
  interface EvaluateInitialEnableWhenExpressionsParams {
29
29
  initialResponse: QuestionnaireResponse;
30
+ initialResponseItemMap: Record<string, QuestionnaireResponseItem[]>;
30
31
  enableWhenExpressions: EnableWhenExpressions;
31
32
  variablesFhirPath: Record<string, Expression[]>;
32
- existingFhirPathContext: Record<string, any>;
33
33
  }
34
34
 
35
35
  export function evaluateInitialEnableWhenExpressions(
@@ -38,7 +38,7 @@ export function evaluateInitialEnableWhenExpressions(
38
38
  initialEnableWhenExpressions: EnableWhenExpressions;
39
39
  updatedFhirPathContext: Record<string, any>;
40
40
  } {
41
- const { initialResponse, enableWhenExpressions, variablesFhirPath, existingFhirPathContext } =
41
+ const { initialResponse, initialResponseItemMap, enableWhenExpressions, variablesFhirPath } =
42
42
  params;
43
43
 
44
44
  const initialEnableWhenExpressions: EnableWhenExpressions = {
@@ -46,8 +46,8 @@ export function evaluateInitialEnableWhenExpressions(
46
46
  };
47
47
  const updatedFhirPathContext = createFhirPathContext(
48
48
  initialResponse,
49
- variablesFhirPath,
50
- existingFhirPathContext
49
+ initialResponseItemMap,
50
+ variablesFhirPath
51
51
  );
52
52
 
53
53
  const initialEnableWhenSingleExpressions = evaluateEnableWhenSingleExpressions(
@@ -85,12 +85,18 @@ function evaluateEnableWhenSingleExpressions(
85
85
  const result = fhirpath.evaluate('', expression, updatedFhirPathContext, fhirpath_r4_model);
86
86
 
87
87
  // Update enableWhenExpressions if length of result array > 0
88
- // Only update when current isEnabled value is different from the result, otherwise it will result in am infinite loop as per #733
88
+ // Only update when current isEnabled value is different from the result, otherwise it will result in an infinite loop as per issue #733
89
89
  if (result.length > 0 && initialValue !== result[0] && typeof result[0] === 'boolean') {
90
90
  enableWhenSingleExpressions[linkId].isEnabled = result[0];
91
91
  isUpdated = true;
92
92
  }
93
93
 
94
+ // Update isEnabled value to false if no result is returned
95
+ if (result.length === 0 && initialValue !== false) {
96
+ enableWhenSingleExpressions[linkId].isEnabled = false;
97
+ isUpdated = true;
98
+ }
99
+
94
100
  // handle intersect edge case - evaluate() returns empty array if result is false
95
101
  if (
96
102
  enableWhenSingleExpressions[linkId].expression.includes('intersect') &&
@@ -198,6 +204,12 @@ export function evaluateEnableWhenRepeatExpressionInstance(
198
204
  isUpdated = true;
199
205
  }
200
206
 
207
+ // Update isEnabled value to false if no result is returned
208
+ if (result.length === 0 && initialValue !== false) {
209
+ isEnabled = false;
210
+ isUpdated = true;
211
+ }
212
+
201
213
  // handle intersect edge case - evaluate() returns empty array if result is false
202
214
  if (expression.includes('intersect') && result.length === 0 && initialValue !== result[0]) {
203
215
  isEnabled = false;
@@ -245,6 +257,7 @@ export function evaluateEnableWhenExpressions(
245
257
 
246
258
  interface MutateRepeatEnableWhenExpressionInstancesParams {
247
259
  questionnaireResponse: QuestionnaireResponse;
260
+ questionnaireResponseItemMap: Record<string, QuestionnaireResponseItem[]>;
248
261
  variablesFhirPath: Record<string, Expression[]>;
249
262
  existingFhirPathContext: Record<string, any>;
250
263
  enableWhenExpressions: EnableWhenExpressions;
@@ -258,8 +271,8 @@ export function mutateRepeatEnableWhenExpressionInstances(
258
271
  ): { updatedEnableWhenExpressions: EnableWhenExpressions; isUpdated: boolean } {
259
272
  const {
260
273
  questionnaireResponse,
274
+ questionnaireResponseItemMap,
261
275
  variablesFhirPath,
262
- existingFhirPathContext,
263
276
  enableWhenExpressions,
264
277
  parentRepeatGroupLinkId,
265
278
  parentRepeatGroupIndex,
@@ -270,8 +283,8 @@ export function mutateRepeatEnableWhenExpressionInstances(
270
283
 
271
284
  const updatedFhirPathContext = createFhirPathContext(
272
285
  questionnaireResponse,
273
- variablesFhirPath,
274
- existingFhirPathContext
286
+ questionnaireResponseItemMap,
287
+ variablesFhirPath
275
288
  );
276
289
 
277
290
  let isUpdated = false;
@@ -25,6 +25,7 @@ import { evaluateCalculatedExpressions } from './calculatedExpression';
25
25
 
26
26
  interface EvaluateUpdatedExpressionsParams {
27
27
  updatedResponse: QuestionnaireResponse;
28
+ updatedResponseItemMap: Record<string, QuestionnaireResponseItem[]>;
28
29
  calculatedExpressions: Record<string, CalculatedExpression[]>;
29
30
  enableWhenExpressions: EnableWhenExpressions;
30
31
  variablesFhirPath: Record<string, Expression[]>;
@@ -39,6 +40,7 @@ export function evaluateUpdatedExpressions(params: EvaluateUpdatedExpressionsPar
39
40
  } {
40
41
  const {
41
42
  updatedResponse,
43
+ updatedResponseItemMap,
42
44
  enableWhenExpressions,
43
45
  calculatedExpressions,
44
46
  variablesFhirPath,
@@ -59,8 +61,8 @@ export function evaluateUpdatedExpressions(params: EvaluateUpdatedExpressionsPar
59
61
 
60
62
  const updatedFhirPathContext = createFhirPathContext(
61
63
  updatedResponse,
62
- variablesFhirPath,
63
- existingFhirPathContext
64
+ updatedResponseItemMap,
65
+ variablesFhirPath
64
66
  );
65
67
 
66
68
  // Update enableWhenExpressions
@@ -87,58 +89,52 @@ export function evaluateUpdatedExpressions(params: EvaluateUpdatedExpressionsPar
87
89
 
88
90
  export function createFhirPathContext(
89
91
  questionnaireResponse: QuestionnaireResponse,
90
- variablesFhirPath: Record<string, Expression[]>,
91
- existingFhirPathContext: Record<string, any>
92
+ questionnaireResponseItemMap: Record<string, QuestionnaireResponseItem[]>,
93
+ variablesFhirPath: Record<string, Expression[]>
92
94
  ): Record<string, any> {
93
- let fhirPathContext = {};
94
- if (Object.keys(existingFhirPathContext).length > 0) {
95
- fhirPathContext = { ...fhirPathContext, ...existingFhirPathContext };
95
+ if (!questionnaireResponse.item) {
96
+ return {};
96
97
  }
97
98
 
98
99
  // Add latest resource to fhirPathContext
99
- fhirPathContext = { ...fhirPathContext, resource: questionnaireResponse };
100
-
101
- if (!questionnaireResponse.item || questionnaireResponse.item.length === 0) {
102
- return fhirPathContext;
103
- }
100
+ let fhirPathContext: Record<string, any> = { resource: questionnaireResponse };
104
101
 
105
- evaluateResourceLevelFhirPath(questionnaireResponse, variablesFhirPath, fhirPathContext);
102
+ // Evaluate resource-level variables
103
+ fhirPathContext = evaluateQuestionnaireLevelVariables(
104
+ questionnaireResponse,
105
+ variablesFhirPath,
106
+ fhirPathContext
107
+ );
106
108
 
107
- for (const topLevelItem of questionnaireResponse.item) {
108
- evaluateFhirPathRecursive(topLevelItem, variablesFhirPath, fhirPathContext);
109
+ // Add variables of items that exist in questionnaireResponseItemMap into fhirPathContext
110
+ for (const linkId in questionnaireResponseItemMap) {
111
+ // For non-repeat groups, the same linkId will have only one item
112
+ // For repeat groups, the same linkId will have multiple items
113
+ for (const qrItem of questionnaireResponseItemMap[linkId]) {
114
+ fhirPathContext = evaluateLinkIdVariables(qrItem, variablesFhirPath, fhirPathContext);
115
+ }
109
116
  }
110
117
 
111
- return fhirPathContext;
112
- }
113
-
114
- export function evaluateFhirPathRecursive(
115
- item: QuestionnaireResponseItem,
116
- variablesFhirPath: Record<string, Expression[]>,
117
- fhirPathContext: Record<string, any>
118
- ) {
119
- const items = item.item;
120
- if (items && items.length > 0) {
121
- // iterate through items of item recursively
122
- for (const childItem of items) {
123
- evaluateFhirPathRecursive(childItem, variablesFhirPath, fhirPathContext);
124
- }
118
+ // Items don't exist in questionnaireResponseItemMap, but we still have to add them into the fhirPathContext as empty arrays
119
+ for (const linkId in variablesFhirPath) {
120
+ fhirPathContext = addEmptyLinkIdVariables(linkId, variablesFhirPath, fhirPathContext);
125
121
  }
126
122
 
127
- evaluateItemFhirPath(item, variablesFhirPath, fhirPathContext);
123
+ return fhirPathContext;
128
124
  }
129
125
 
130
- export function evaluateItemFhirPath(
126
+ export function evaluateLinkIdVariables(
131
127
  item: QuestionnaireResponseItem,
132
128
  variablesFhirPath: Record<string, Expression[]>,
133
129
  fhirPathContext: Record<string, any>
134
130
  ) {
135
- const variablesOfItem = variablesFhirPath[item.linkId];
136
- if (!variablesOfItem || variablesOfItem.length === 0) {
137
- return;
131
+ const linkIdVariables = variablesFhirPath[item.linkId];
132
+ if (!linkIdVariables || linkIdVariables.length === 0) {
133
+ return fhirPathContext;
138
134
  }
139
135
 
140
- variablesOfItem.forEach((variable) => {
141
- if (variable.expression) {
136
+ for (const variable of linkIdVariables) {
137
+ if (variable.expression && variable.name) {
142
138
  try {
143
139
  fhirPathContext[`${variable.name}`] = fhirpath.evaluate(
144
140
  item,
@@ -153,20 +149,43 @@ export function evaluateItemFhirPath(
153
149
  console.warn(e.message, `LinkId: ${item.linkId}\nExpression: ${variable.expression}`);
154
150
  }
155
151
  }
156
- });
152
+ }
153
+
154
+ return fhirPathContext;
155
+ }
156
+
157
+ export function addEmptyLinkIdVariables(
158
+ linkId: string,
159
+ variablesFhirPath: Record<string, Expression[]>,
160
+ fhirPathContext: Record<string, any>
161
+ ) {
162
+ const linkIdVariables = variablesFhirPath[linkId];
163
+ if (!linkIdVariables || linkIdVariables.length === 0) {
164
+ return fhirPathContext;
165
+ }
166
+
167
+ for (const variable of linkIdVariables) {
168
+ if (variable.expression && variable.name) {
169
+ if (fhirPathContext[`${variable.name}`] === undefined) {
170
+ fhirPathContext[`${variable.name}`] = [];
171
+ }
172
+ }
173
+ }
174
+
175
+ return fhirPathContext;
157
176
  }
158
177
 
159
- export function evaluateResourceLevelFhirPath(
178
+ export function evaluateQuestionnaireLevelVariables(
160
179
  resource: QuestionnaireResponse,
161
180
  variablesFhirPath: Record<string, Expression[]>,
162
181
  fhirPathContext: Record<string, any>
163
182
  ) {
164
183
  const questionnaireLevelVariables = variablesFhirPath['QuestionnaireLevel'];
165
184
  if (!questionnaireLevelVariables || questionnaireLevelVariables.length === 0) {
166
- return;
185
+ return fhirPathContext;
167
186
  }
168
187
 
169
- questionnaireLevelVariables.forEach((variable) => {
188
+ for (const variable of questionnaireLevelVariables) {
170
189
  if (variable.expression) {
171
190
  try {
172
191
  fhirPathContext[`${variable.name}`] = fhirpath.evaluate(
@@ -182,5 +201,7 @@ export function evaluateResourceLevelFhirPath(
182
201
  console.warn(e.message, `Questionnaire-level\nExpression: ${variable.expression}`);
183
202
  }
184
203
  }
185
- });
204
+ }
205
+
206
+ return fhirPathContext;
186
207
  }
@@ -31,6 +31,7 @@ import type { Tabs } from '../interfaces/tab.interface';
31
31
  import { assignPopulatedAnswersToEnableWhen } from './enableWhen';
32
32
  import type { CalculatedExpression } from '../interfaces/calculatedExpression.interface';
33
33
  import { evaluateInitialCalculatedExpressions } from './calculatedExpression';
34
+ import { createQuestionnaireResponseItemMap } from './questionnaireResponseStoreUtils/updatableResponseItems';
34
35
 
35
36
  /**
36
37
  * Initialise a conformant questionnaireResponse from a given questionnaire
@@ -345,6 +346,7 @@ export function initialiseFormFromResponse(params: initialFormFromResponseParams
345
346
  variablesFhirPath,
346
347
  tabs
347
348
  } = params;
349
+ const initialResponseItemMap = createQuestionnaireResponseItemMap(questionnaireResponse);
348
350
  let updatedFhirPathContext = params.fhirPathContext;
349
351
 
350
352
  const { initialisedItems, linkedQuestions } = assignPopulatedAnswersToEnableWhen(
@@ -354,15 +356,16 @@ export function initialiseFormFromResponse(params: initialFormFromResponseParams
354
356
 
355
357
  const evaluateInitialEnableWhenExpressionsResult = evaluateInitialEnableWhenExpressions({
356
358
  initialResponse: questionnaireResponse,
359
+ initialResponseItemMap: initialResponseItemMap,
357
360
  enableWhenExpressions: enableWhenExpressions,
358
- variablesFhirPath: variablesFhirPath,
359
- existingFhirPathContext: updatedFhirPathContext
361
+ variablesFhirPath: variablesFhirPath
360
362
  });
361
363
  const { initialEnableWhenExpressions } = evaluateInitialEnableWhenExpressionsResult;
362
364
  updatedFhirPathContext = evaluateInitialEnableWhenExpressionsResult.updatedFhirPathContext;
363
365
 
364
366
  const evaluateInitialCalculatedExpressionsResult = evaluateInitialCalculatedExpressions({
365
367
  initialResponse: questionnaireResponse,
368
+ initialResponseItemMap: initialResponseItemMap,
366
369
  calculatedExpressions: calculatedExpressions,
367
370
  variablesFhirPath: variablesFhirPath,
368
371
  existingFhirPathContext: updatedFhirPathContext
@@ -59,7 +59,7 @@ export function parseDecimalStringWithPrecision(input: string, precision: number
59
59
 
60
60
  // truncate decimal digits based on precision
61
61
  const parsedDecimalPoint = input.indexOf('.');
62
- if (precision && parsedDecimalPoint !== -1) {
62
+ if (typeof precision === 'number' && parsedDecimalPoint !== -1) {
63
63
  parsedInput = parsedInput.substring(0, parsedDecimalPoint + precision + 1);
64
64
  }
65
65
 
@@ -0,0 +1,62 @@
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
+ import type { QuestionnaireResponse, QuestionnaireResponseItem } from 'fhir/r4';
19
+
20
+ export function createQuestionnaireResponseItemMap(
21
+ questionnaireResponse: QuestionnaireResponse
22
+ ): Record<string, QuestionnaireResponseItem[]> {
23
+ if (!questionnaireResponse.item) {
24
+ return {};
25
+ }
26
+
27
+ const questionnaireResponseItemMap: Record<string, QuestionnaireResponseItem[]> = {};
28
+ for (const topLevelQRItem of questionnaireResponse.item) {
29
+ fillQuestionnaireResponseItemMapRecursive(topLevelQRItem, questionnaireResponseItemMap);
30
+ }
31
+
32
+ return questionnaireResponseItemMap;
33
+ }
34
+
35
+ function fillQuestionnaireResponseItemMapRecursive(
36
+ qrItem: QuestionnaireResponseItem,
37
+ questionnaireResponseItemMap: Record<string, QuestionnaireResponseItem[]>
38
+ ) {
39
+ const qrItems = qrItem.item;
40
+ if (qrItems && qrItems.length > 0) {
41
+ // iterate through items of item recursively
42
+ for (const childQRItem of qrItems) {
43
+ fillQuestionnaireResponseItemMapRecursive(childQRItem, questionnaireResponseItemMap);
44
+ }
45
+ }
46
+
47
+ fillQuestionnaireResponseItemMap(qrItem, questionnaireResponseItemMap);
48
+ }
49
+
50
+ function fillQuestionnaireResponseItemMap(
51
+ qrItem: QuestionnaireResponseItem,
52
+ questionnaireResponseItemMap: Record<string, QuestionnaireResponseItem[]>
53
+ ) {
54
+ // linkId already exists in questionnaireResponseItemMap, it would be a repeat group
55
+ if (qrItem.linkId in questionnaireResponseItemMap) {
56
+ questionnaireResponseItemMap[qrItem.linkId].push(qrItem);
57
+ }
58
+ // Add item to questionnaireResponseItemMap
59
+ else {
60
+ questionnaireResponseItemMap[qrItem.linkId] = [qrItem];
61
+ }
62
+ }