@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.
- package/lib/components/FormComponents/BooleanItem/BooleanItem.js +6 -1
- package/lib/components/FormComponents/BooleanItem/BooleanItem.js.map +1 -1
- package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.js +5 -2
- package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.js.map +1 -1
- package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.js +5 -2
- package/lib/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.js.map +1 -1
- package/lib/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.js +5 -2
- package/lib/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.js.map +1 -1
- package/lib/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.js +5 -2
- package/lib/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.js.map +1 -1
- package/lib/components/FormComponents/DecimalItem/DecimalItem.js +9 -3
- package/lib/components/FormComponents/DecimalItem/DecimalItem.js.map +1 -1
- package/lib/components/FormComponents/IntegerItem/IntegerItem.js +7 -3
- package/lib/components/FormComponents/IntegerItem/IntegerItem.js.map +1 -1
- package/lib/components/FormComponents/StringItem/StringItem.js +7 -3
- package/lib/components/FormComponents/StringItem/StringItem.js.map +1 -1
- package/lib/components/FormComponents/TextItem/TextItem.js +7 -3
- package/lib/components/FormComponents/TextItem/TextItem.js.map +1 -1
- package/lib/hooks/useBooleanCalculatedExpression.d.ts +5 -4
- package/lib/hooks/useBooleanCalculatedExpression.js +12 -7
- package/lib/hooks/useBooleanCalculatedExpression.js.map +1 -1
- package/lib/hooks/useCodingCalculatedExpression.d.ts +4 -3
- package/lib/hooks/useCodingCalculatedExpression.js +14 -7
- package/lib/hooks/useCodingCalculatedExpression.js.map +1 -1
- package/lib/hooks/useDecimalCalculatedExpression.d.ts +3 -3
- package/lib/hooks/useDecimalCalculatedExpression.js +11 -7
- package/lib/hooks/useDecimalCalculatedExpression.js.map +1 -1
- package/lib/hooks/useDisplayCalculatedExpression.js +8 -1
- package/lib/hooks/useDisplayCalculatedExpression.js.map +1 -1
- package/lib/hooks/useIntegerCalculatedExpression.d.ts +3 -3
- package/lib/hooks/useIntegerCalculatedExpression.js +11 -7
- package/lib/hooks/useIntegerCalculatedExpression.js.map +1 -1
- package/lib/hooks/useStringCalculatedExpression.d.ts +3 -3
- package/lib/hooks/useStringCalculatedExpression.js +12 -7
- package/lib/hooks/useStringCalculatedExpression.js.map +1 -1
- package/lib/interfaces/calculatedExpression.interface.d.ts +1 -1
- package/lib/stores/questionnaireResponseStore.d.ts +3 -1
- package/lib/stores/questionnaireResponseStore.js +9 -1
- package/lib/stores/questionnaireResponseStore.js.map +1 -1
- package/lib/stores/questionnaireStore.js +6 -0
- package/lib/stores/questionnaireStore.js.map +1 -1
- package/lib/utils/calculatedExpression.d.ts +2 -1
- package/lib/utils/calculatedExpression.js +14 -8
- package/lib/utils/calculatedExpression.js.map +1 -1
- package/lib/utils/enableWhenExpression.d.ts +3 -2
- package/lib/utils/enableWhenExpression.js +15 -5
- package/lib/utils/enableWhenExpression.js.map +1 -1
- package/lib/utils/fhirpath.d.ts +5 -4
- package/lib/utils/fhirpath.js +45 -33
- package/lib/utils/fhirpath.js.map +1 -1
- package/lib/utils/initialise.js +5 -2
- package/lib/utils/initialise.js.map +1 -1
- package/lib/utils/parseInputs.js +1 -1
- package/lib/utils/parseInputs.js.map +1 -1
- package/package.json +1 -1
- package/src/components/FormComponents/BooleanItem/BooleanItem.tsx +9 -1
- package/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerOptionItem.tsx +5 -2
- package/src/components/FormComponents/ChoiceItems/ChoiceRadioAnswerValueSetItem.tsx +5 -2
- package/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerOptionItem.tsx +5 -2
- package/src/components/FormComponents/ChoiceItems/ChoiceSelectAnswerValueSetItem.tsx +5 -2
- package/src/components/FormComponents/DecimalItem/DecimalItem.tsx +14 -3
- package/src/components/FormComponents/IntegerItem/IntegerItem.tsx +10 -3
- package/src/components/FormComponents/StringItem/StringItem.tsx +10 -3
- package/src/components/FormComponents/TextItem/TextItem.tsx +10 -3
- package/src/hooks/useBooleanCalculatedExpression.ts +19 -12
- package/src/hooks/useCodingCalculatedExpression.ts +18 -8
- package/src/hooks/useDecimalCalculatedExpression.ts +26 -17
- package/src/hooks/useDisplayCalculatedExpression.ts +12 -1
- package/src/hooks/useIntegerCalculatedExpression.ts +18 -13
- package/src/hooks/useStringCalculatedExpression.ts +16 -13
- package/src/interfaces/calculatedExpression.interface.ts +1 -1
- package/src/stores/questionnaireResponseStore.ts +16 -2
- package/src/stores/questionnaireStore.ts +7 -0
- package/src/utils/calculatedExpression.ts +24 -10
- package/src/utils/enableWhenExpression.ts +22 -9
- package/src/utils/fhirpath.ts +62 -41
- package/src/utils/initialise.ts +5 -2
- package/src/utils/parseInputs.ts +1 -1
- 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 {
|
|
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
|
-
|
|
31
|
-
|
|
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,
|
|
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' ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { createStore } from 'zustand/vanilla';
|
|
19
|
-
import type {
|
|
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 {
|
|
52
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
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
|
|
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
|
-
|
|
274
|
-
|
|
286
|
+
questionnaireResponseItemMap,
|
|
287
|
+
variablesFhirPath
|
|
275
288
|
);
|
|
276
289
|
|
|
277
290
|
let isUpdated = false;
|
package/src/utils/fhirpath.ts
CHANGED
|
@@ -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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
+
questionnaireResponseItemMap: Record<string, QuestionnaireResponseItem[]>,
|
|
93
|
+
variablesFhirPath: Record<string, Expression[]>
|
|
92
94
|
): Record<string, any> {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
fhirPathContext = { ...fhirPathContext, ...existingFhirPathContext };
|
|
95
|
+
if (!questionnaireResponse.item) {
|
|
96
|
+
return {};
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
// Add latest resource to fhirPathContext
|
|
99
|
-
fhirPathContext = {
|
|
100
|
-
|
|
101
|
-
if (!questionnaireResponse.item || questionnaireResponse.item.length === 0) {
|
|
102
|
-
return fhirPathContext;
|
|
103
|
-
}
|
|
100
|
+
let fhirPathContext: Record<string, any> = { resource: questionnaireResponse };
|
|
104
101
|
|
|
105
|
-
|
|
102
|
+
// Evaluate resource-level variables
|
|
103
|
+
fhirPathContext = evaluateQuestionnaireLevelVariables(
|
|
104
|
+
questionnaireResponse,
|
|
105
|
+
variablesFhirPath,
|
|
106
|
+
fhirPathContext
|
|
107
|
+
);
|
|
106
108
|
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
+
return fhirPathContext;
|
|
128
124
|
}
|
|
129
125
|
|
|
130
|
-
export function
|
|
126
|
+
export function evaluateLinkIdVariables(
|
|
131
127
|
item: QuestionnaireResponseItem,
|
|
132
128
|
variablesFhirPath: Record<string, Expression[]>,
|
|
133
129
|
fhirPathContext: Record<string, any>
|
|
134
130
|
) {
|
|
135
|
-
const
|
|
136
|
-
if (!
|
|
137
|
-
return;
|
|
131
|
+
const linkIdVariables = variablesFhirPath[item.linkId];
|
|
132
|
+
if (!linkIdVariables || linkIdVariables.length === 0) {
|
|
133
|
+
return fhirPathContext;
|
|
138
134
|
}
|
|
139
135
|
|
|
140
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/src/utils/initialise.ts
CHANGED
|
@@ -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
|
package/src/utils/parseInputs.ts
CHANGED
|
@@ -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
|
+
}
|