@aehrc/smart-forms-renderer 0.35.8 → 0.36.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 (47) hide show
  1. package/README.md +1 -1
  2. package/lib/utils/calculatedExpression.js +15 -43
  3. package/lib/utils/calculatedExpression.js.map +1 -1
  4. package/lib/utils/manageForm.d.ts +7 -1
  5. package/lib/utils/manageForm.js +9 -0
  6. package/lib/utils/manageForm.js.map +1 -1
  7. package/lib/utils/questionnaireStoreUtils/resolveValueSets.js +8 -3
  8. package/lib/utils/questionnaireStoreUtils/resolveValueSets.js.map +1 -1
  9. package/lib/utils/removeEmptyAnswers.js +19 -6
  10. package/lib/utils/removeEmptyAnswers.js.map +1 -1
  11. package/lib/utils/repopulateIntoResponse.d.ts +1 -3
  12. package/lib/utils/repopulateIntoResponse.js +2 -31
  13. package/lib/utils/repopulateIntoResponse.js.map +1 -1
  14. package/lib/utils/tabs.js +6 -4
  15. package/lib/utils/tabs.js.map +1 -1
  16. package/lib/utils/updateQr.d.ts +9 -0
  17. package/lib/utils/updateQr.js +55 -0
  18. package/lib/utils/updateQr.js.map +1 -0
  19. package/lib/utils/valueSet.js +4 -2
  20. package/lib/utils/valueSet.js.map +1 -1
  21. package/package.json +5 -4
  22. package/src/utils/calculatedExpression.ts +26 -60
  23. package/src/utils/manageForm.ts +11 -1
  24. package/src/utils/questionnaireStoreUtils/resolveValueSets.ts +10 -6
  25. package/src/utils/removeEmptyAnswers.ts +22 -6
  26. package/src/utils/repopulateIntoResponse.ts +5 -58
  27. package/src/utils/tabs.ts +6 -4
  28. package/src/utils/updateQr.ts +88 -0
  29. package/src/utils/valueSet.ts +4 -2
  30. package/stats.html +4842 -0
  31. package/stats1.html +4842 -0
  32. package/stats3.html +4842 -0
  33. package/lib/utils/answerExpression.d.ts +0 -5
  34. package/lib/utils/answerExpression.js +0 -52
  35. package/lib/utils/answerExpression.js.map +0 -1
  36. package/lib/utils/dynamicValueSet.d.ts +0 -5
  37. package/lib/utils/dynamicValueSet.js +0 -96
  38. package/lib/utils/dynamicValueSet.js.map +0 -1
  39. package/lib/utils/fhirpathAsyncUtils/fhirpath-async.d.ts +0 -14
  40. package/lib/utils/fhirpathAsyncUtils/fhirpath-async.js +0 -431
  41. package/lib/utils/fhirpathAsyncUtils/fhirpath-async.js.map +0 -1
  42. package/lib/utils/fhirpathAsyncUtils/outcome-utils.d.ts +0 -3
  43. package/lib/utils/fhirpathAsyncUtils/outcome-utils.js +0 -41
  44. package/lib/utils/fhirpathAsyncUtils/outcome-utils.js.map +0 -1
  45. package/lib/utils/fhirpathAsyncUtils/test-questionnaire.d.ts +0 -1
  46. package/lib/utils/fhirpathAsyncUtils/test-questionnaire.js +0 -379
  47. package/lib/utils/fhirpathAsyncUtils/test-questionnaire.js.map +0 -1
@@ -34,6 +34,7 @@ import { getQrItemsIndex, mapQItemsIndex } from './mapItem';
34
34
  import { updateQrItemsInGroup } from './qrItem';
35
35
  import cloneDeep from 'lodash.clonedeep';
36
36
  import dayjs from 'dayjs';
37
+ import { updateQuestionnaireResponse } from './updateQr';
37
38
 
38
39
  interface EvaluateInitialCalculatedExpressionsParams {
39
40
  initialResponse: QuestionnaireResponse;
@@ -164,15 +165,6 @@ export function initialiseCalculatedExpressionValues(
164
165
  populatedResponse: QuestionnaireResponse,
165
166
  calculatedExpressions: Record<string, CalculatedExpression[]>
166
167
  ): QuestionnaireResponse {
167
- if (
168
- !questionnaire.item ||
169
- questionnaire.item.length === 0 ||
170
- !populatedResponse.item ||
171
- populatedResponse.item.length === 0
172
- ) {
173
- return populatedResponse;
174
- }
175
-
176
168
  // Filter calculated expressions, only preserve key-value pairs with values
177
169
  const calculatedExpressionsWithValues: Record<string, CalculatedExpression[]> = {};
178
170
  for (const linkId in calculatedExpressions) {
@@ -185,80 +177,54 @@ export function initialiseCalculatedExpressionValues(
185
177
  }
186
178
  }
187
179
 
188
- // Populate calculated expression values into QR
189
- const topLevelQrItems: QuestionnaireResponseItem[] = [];
190
- for (const [index, topLevelQItem] of questionnaire.item.entries()) {
191
- const populatedTopLevelQrItem = populatedResponse.item[index] ?? {
192
- linkId: topLevelQItem.linkId,
193
- text: topLevelQItem.text,
194
- item: []
195
- };
196
-
197
- const updatedTopLevelQRItem = initialiseItemCalculatedExpressionValueRecursive(
198
- topLevelQItem,
199
- populatedTopLevelQrItem,
200
- calculatedExpressionsWithValues
201
- );
202
-
203
- if (Array.isArray(updatedTopLevelQRItem)) {
204
- if (updatedTopLevelQRItem.length > 0) {
205
- topLevelQrItems.push(...updatedTopLevelQRItem);
206
- }
207
- continue;
208
- }
209
-
210
- if (updatedTopLevelQRItem) {
211
- topLevelQrItems.push(updatedTopLevelQRItem);
212
- }
213
- }
214
-
215
- return { ...populatedResponse, item: topLevelQrItems };
180
+ return updateQuestionnaireResponse(
181
+ questionnaire,
182
+ populatedResponse,
183
+ initialiseItemCalculatedExpressionValueRecursive,
184
+ calculatedExpressionsWithValues
185
+ );
216
186
  }
217
187
 
218
188
  function initialiseItemCalculatedExpressionValueRecursive(
219
189
  qItem: QuestionnaireItem,
220
- qrItem: QuestionnaireResponseItem | undefined,
190
+ qrItemOrItems: QuestionnaireResponseItem | QuestionnaireResponseItem[] | null,
221
191
  calculatedExpressions: Record<string, CalculatedExpression[]>
222
- ): QuestionnaireResponseItem[] | QuestionnaireResponseItem | null {
192
+ ): QuestionnaireResponseItem | QuestionnaireResponseItem[] | null {
193
+ // For repeat groups
194
+ const hasMultipleAnswers = Array.isArray(qrItemOrItems);
195
+ if (hasMultipleAnswers) {
196
+ return qrItemOrItems;
197
+ }
198
+
199
+ const qrItem = qrItemOrItems;
223
200
  const childQItems = qItem.item;
224
201
  if (childQItems && childQItems.length > 0) {
225
- // iterate through items of item recursively
226
202
  const childQrItems = qrItem?.item ?? [];
227
- // const updatedChildQrItems: QuestionnaireResponseItem[] = [];
228
-
229
- // FIXME Not implemented for repeat groups
230
- if (qItem.type === 'group' && qItem.repeats) {
231
- return qrItem ?? null;
232
- }
233
203
 
234
204
  const indexMap = mapQItemsIndex(qItem);
235
205
  const qrItemsByIndex = getQrItemsIndex(childQItems, childQrItems, indexMap);
236
206
 
237
207
  // Otherwise loop through qItem as usual
238
208
  for (const [index, childQItem] of childQItems.entries()) {
239
- const childQrItem = qrItemsByIndex[index];
240
-
241
- // FIXME Not implemented for repeat groups
242
- if (Array.isArray(childQrItem)) {
243
- continue;
244
- }
209
+ const childQRItemOrItems = qrItemsByIndex[index];
245
210
 
246
- const newQrItem = initialiseItemCalculatedExpressionValueRecursive(
211
+ const updatedChildQRItemOrItems = initialiseItemCalculatedExpressionValueRecursive(
247
212
  childQItem,
248
- childQrItem,
213
+ childQRItemOrItems ?? null,
249
214
  calculatedExpressions
250
215
  );
251
216
 
252
217
  // FIXME Not implemented for repeat groups
253
- if (Array.isArray(newQrItem)) {
218
+ if (Array.isArray(updatedChildQRItemOrItems)) {
254
219
  continue;
255
220
  }
256
221
 
257
- if (newQrItem) {
222
+ const updatedChildQRItem = updatedChildQRItemOrItems;
223
+ if (updatedChildQRItem) {
258
224
  updateQrItemsInGroup(
259
- newQrItem,
225
+ updatedChildQRItem,
260
226
  null,
261
- qrItem ?? { linkId: qItem.linkId, text: qItem.text, item: [] },
227
+ updatedChildQRItem ?? { linkId: qItem.linkId, text: qItem.text, item: [] },
262
228
  indexMap
263
229
  );
264
230
  }
@@ -287,7 +253,7 @@ function getCalculatedExpressionAnswer(
287
253
 
288
254
  function constructGroupItem(
289
255
  qItem: QuestionnaireItem,
290
- qrItem: QuestionnaireResponseItem | undefined,
256
+ qrItem: QuestionnaireResponseItem | null,
291
257
  calculatedExpressions: Record<string, CalculatedExpression[]>
292
258
  ): QuestionnaireResponseItem | null {
293
259
  const calculatedExpressionAnswer = getCalculatedExpressionAnswer(qItem, calculatedExpressions);
@@ -317,7 +283,7 @@ function constructGroupItem(
317
283
 
318
284
  function constructSingleItem(
319
285
  qItem: QuestionnaireItem,
320
- qrItem: QuestionnaireResponseItem | undefined,
286
+ qrItem: QuestionnaireResponseItem | null,
321
287
  calculatedExpressions: Record<string, CalculatedExpression[]>
322
288
  ): QuestionnaireResponseItem | null {
323
289
  const calculatedExpressionAnswer = getCalculatedExpressionAnswer(qItem, calculatedExpressions);
@@ -1,4 +1,4 @@
1
- import type { Questionnaire, QuestionnaireResponse } from 'fhir/r4';
1
+ import type { Questionnaire, QuestionnaireResponse, QuestionnaireResponseItem } from 'fhir/r4';
2
2
  import {
3
3
  questionnaireResponseStore,
4
4
  questionnaireStore,
@@ -30,6 +30,7 @@ export async function buildForm(
30
30
  terminologyServerUrl?: string,
31
31
  additionalVariables?: Record<string, object>
32
32
  ): Promise<void> {
33
+ // Reset terminology server
33
34
  if (terminologyServerUrl) {
34
35
  terminologyServerStore.getState().setUrl(terminologyServerUrl);
35
36
  } else {
@@ -120,3 +121,12 @@ export function removeEmptyAnswersFromResponse(
120
121
  enableWhenExpressions
121
122
  });
122
123
  }
124
+
125
+ /**
126
+ * Check if a QuestionnaireResponseItem has either an item or an answer property.
127
+ *
128
+ * @author Sean Fong
129
+ */
130
+ export function qrItemHasItemsOrAnswer(qrItem: QuestionnaireResponseItem): boolean {
131
+ return (!!qrItem.item && qrItem.item.length > 0) || (!!qrItem.answer && qrItem.answer.length > 0);
132
+ }
@@ -47,17 +47,21 @@ export async function resolveValueSets(
47
47
  const resolvedPromises = await resolveValueSetPromises(valueSetPromises);
48
48
 
49
49
  for (const valueSetUrl in resolvedPromises) {
50
- const valueSet = resolvedPromises[valueSetUrl].valueSet;
50
+ const valueSet = resolvedPromises[valueSetUrl]?.valueSet;
51
51
 
52
52
  if (valueSet) {
53
53
  if (valueSetToXFhirQueryVariableNameMap[valueSetUrl]) {
54
54
  // valueSetUrl is in x-fhir-query variables, save to variable
55
55
  const variableName = valueSetToXFhirQueryVariableNameMap[valueSetUrl];
56
- const variable = variables.xFhirQueryVariables[variableName];
57
- variables.xFhirQueryVariables[variableName] = {
58
- ...variable,
59
- result: resolvedPromises[valueSetUrl].valueSet
60
- };
56
+ if (variableName) {
57
+ const variable = variables.xFhirQueryVariables[variableName];
58
+ if (variable) {
59
+ variables.xFhirQueryVariables[variableName] = {
60
+ ...variable,
61
+ result: resolvedPromises[valueSetUrl]?.valueSet
62
+ };
63
+ }
64
+ }
61
65
  } else {
62
66
  // valueSetUrl is in x-fhir-query variables, save to preprocessedValueSetCodings
63
67
  processedValueSetCodings[valueSetUrl] = getValueSetCodings(valueSet);
@@ -24,6 +24,7 @@ import type {
24
24
  import type { EnableWhenExpressions, EnableWhenItems } from '../interfaces/enableWhen.interface';
25
25
  import { isHiddenByEnableWhen } from './qItem';
26
26
  import cloneDeep from 'lodash.clonedeep';
27
+ import { qrItemHasItemsOrAnswer } from './manageForm';
27
28
 
28
29
  interface removeEmptyAnswersParams {
29
30
  questionnaire: Questionnaire;
@@ -60,21 +61,31 @@ export function removeEmptyAnswers(params: removeEmptyAnswersParams): Questionna
60
61
  return updatedQuestionnaireResponse;
61
62
  }
62
63
 
63
- topLevelQRItems.forEach((qrItem, i) => {
64
+ const newQuestionnaireResponse: QuestionnaireResponse = { ...questionnaireResponse, item: [] };
65
+ for (const [i, topLevelQRItem] of topLevelQRItems.entries()) {
64
66
  const qItem = topLevelQItems[i];
67
+ if (!qItem) {
68
+ continue;
69
+ }
70
+
71
+ // If QR item don't have either item.item and item.answer, continue
72
+ if (!qrItemHasItemsOrAnswer(topLevelQRItem)) {
73
+ continue;
74
+ }
75
+
65
76
  const newTopLevelQRItem = removeEmptyAnswersFromItemRecursive({
66
77
  qItem,
67
- qrItem,
78
+ qrItem: topLevelQRItem,
68
79
  enableWhenIsActivated,
69
80
  enableWhenItems,
70
81
  enableWhenExpressions
71
82
  });
72
- if (newTopLevelQRItem && questionnaireResponse.item) {
73
- questionnaireResponse.item[i] = { ...newTopLevelQRItem };
83
+ if (newTopLevelQRItem && newQuestionnaireResponse.item) {
84
+ newQuestionnaireResponse.item.push(newTopLevelQRItem);
74
85
  }
75
- });
86
+ }
76
87
 
77
- return questionnaireResponse;
88
+ return newQuestionnaireResponse;
78
89
  }
79
90
 
80
91
  interface removeEmptyAnswersFromItemRecursiveParams {
@@ -90,6 +101,11 @@ function removeEmptyAnswersFromItemRecursive(
90
101
  ): QuestionnaireResponseItem | null {
91
102
  const { qItem, qrItem, enableWhenIsActivated, enableWhenItems, enableWhenExpressions } = params;
92
103
 
104
+ // If QR item don't have either item.item and item.answer, return null
105
+ if (!qrItemHasItemsOrAnswer(qrItem)) {
106
+ return null;
107
+ }
108
+
93
109
  const qItems = qItem.item;
94
110
  const qrItems = qrItem.item;
95
111
 
@@ -1,13 +1,9 @@
1
- import type {
2
- Questionnaire,
3
- QuestionnaireItem,
4
- QuestionnaireResponse,
5
- QuestionnaireResponseItem
6
- } from 'fhir/r4';
1
+ import type { QuestionnaireItem, QuestionnaireResponseItem } from 'fhir/r4';
7
2
  import type { ItemToRepopulate } from './repopulateItems';
8
3
  import { getQrItemsIndex, mapQItemsIndex } from './mapItem';
9
4
  import { isSpecificItemControl } from './itemControl';
10
5
  import { questionnaireResponseStore, questionnaireStore } from '../stores';
6
+ import { updateQuestionnaireResponse } from './updateQr';
11
7
 
12
8
  /**
13
9
  * Re-populate checked items in the re-population dialog into the current QuestionnaireResponse
@@ -18,68 +14,19 @@ export function repopulateResponse(checkedItemsToRepopulate: Record<string, Item
18
14
  const sourceQuestionnaire = questionnaireStore.getState().sourceQuestionnaire;
19
15
  const updatableResponse = questionnaireResponseStore.getState().updatableResponse;
20
16
 
21
- return repopulateItemsIntoResponse(
17
+ return updateQuestionnaireResponse(
22
18
  sourceQuestionnaire,
23
19
  updatableResponse,
20
+ repopulateItemRecursive,
24
21
  checkedItemsToRepopulate
25
22
  );
26
23
  }
27
24
 
28
- export function repopulateItemsIntoResponse(
29
- questionnaire: Questionnaire,
30
- updatableResponse: QuestionnaireResponse,
31
- checkedItemsToRepopulate: Record<string, ItemToRepopulate>
32
- ): QuestionnaireResponse {
33
- if (
34
- !questionnaire.item ||
35
- questionnaire.item.length === 0 ||
36
- !updatableResponse.item ||
37
- updatableResponse.item.length === 0
38
- ) {
39
- return updatableResponse;
40
- }
41
-
42
- const qItemsIndexMap = mapQItemsIndex(questionnaire);
43
- const topLevelQRItemsByIndex = getQrItemsIndex(
44
- questionnaire.item,
45
- updatableResponse.item,
46
- qItemsIndexMap
47
- );
48
-
49
- const topLevelQrItems: QuestionnaireResponseItem[] = [];
50
- for (const [index, topLevelQItem] of questionnaire.item.entries()) {
51
- const topLevelQRItemOrItems = topLevelQRItemsByIndex[index] ?? {
52
- linkId: topLevelQItem.linkId,
53
- text: topLevelQItem.text,
54
- item: []
55
- };
56
-
57
- const updatedTopLevelQRItem = repopulateItemRecursive(
58
- topLevelQItem,
59
- topLevelQRItemOrItems,
60
- checkedItemsToRepopulate
61
- );
62
-
63
- if (Array.isArray(updatedTopLevelQRItem)) {
64
- if (updatedTopLevelQRItem.length > 0) {
65
- topLevelQrItems.push(...updatedTopLevelQRItem);
66
- }
67
- continue;
68
- }
69
-
70
- if (updatedTopLevelQRItem) {
71
- topLevelQrItems.push(updatedTopLevelQRItem);
72
- }
73
- }
74
-
75
- return { ...updatableResponse, item: topLevelQrItems };
76
- }
77
-
78
25
  function repopulateItemRecursive(
79
26
  qItem: QuestionnaireItem,
80
27
  qrItemOrItems: QuestionnaireResponseItem | QuestionnaireResponseItem[] | null,
81
28
  checkedItemsToRepopulate: Record<string, ItemToRepopulate>
82
- ): QuestionnaireResponseItem[] | QuestionnaireResponseItem | null {
29
+ ): QuestionnaireResponseItem | QuestionnaireResponseItem[] | null {
83
30
  // For repeat groups
84
31
  const hasMultipleAnswers = Array.isArray(qrItemOrItems);
85
32
  if (hasMultipleAnswers) {
package/src/utils/tabs.ts CHANGED
@@ -38,12 +38,14 @@ export function getFirstVisibleTab(
38
38
  return false;
39
39
  }
40
40
 
41
- if (singleItems[tabLinkId]) {
42
- return singleItems[tabLinkId].isEnabled;
41
+ const singleItem = singleItems[tabLinkId];
42
+ if (singleItem) {
43
+ return singleItem.isEnabled;
43
44
  }
44
45
 
45
- if (singleExpressions[tabLinkId]) {
46
- return singleExpressions[tabLinkId].isEnabled;
46
+ const singleExpression = singleExpressions[tabLinkId];
47
+ if (singleExpression) {
48
+ return singleExpression.isEnabled;
47
49
  }
48
50
 
49
51
  return true;
@@ -0,0 +1,88 @@
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 {
19
+ Questionnaire,
20
+ QuestionnaireItem,
21
+ QuestionnaireResponse,
22
+ QuestionnaireResponseItem
23
+ } from 'fhir/r4';
24
+ import { getQrItemsIndex, mapQItemsIndex } from './mapItem';
25
+ import { qrItemHasItemsOrAnswer } from './manageForm';
26
+
27
+ export type RepopulateFunction<T> = (
28
+ qItem: QuestionnaireItem,
29
+ qrItemOrItems: QuestionnaireResponseItem | QuestionnaireResponseItem[] | null,
30
+ extraData: T
31
+ ) => QuestionnaireResponseItem | QuestionnaireResponseItem[] | null;
32
+
33
+ /**
34
+ * A generic (and safe) way to update a QuestionnaireResponse given a recursive function and a set of data i.e. Record<linkId, calculated expression values>, Record<linkId, re-populated values>
35
+ * This function relies heavily on mapQItemsIndex() and getQrItemsIndex() to accurately pinpoint the locations of QR items based on their positions in the Q, taking into account repeating group answers, non-filled questions, etc
36
+ *
37
+ * @author Sean Fong
38
+ */
39
+ export function updateQuestionnaireResponse<T>(
40
+ questionnaire: Questionnaire,
41
+ questionnaireResponse: QuestionnaireResponse,
42
+ recursiveUpdateFunction: RepopulateFunction<T>,
43
+ extraData: T
44
+ ) {
45
+ if (
46
+ !questionnaire.item ||
47
+ questionnaire.item.length === 0 ||
48
+ !questionnaireResponse.item ||
49
+ questionnaireResponse.item.length === 0
50
+ ) {
51
+ return questionnaireResponse;
52
+ }
53
+
54
+ const qItemsIndexMap = mapQItemsIndex(questionnaire);
55
+ const topLevelQRItemsByIndex = getQrItemsIndex(
56
+ questionnaire.item,
57
+ questionnaireResponse.item,
58
+ qItemsIndexMap
59
+ );
60
+
61
+ const topLevelQrItems = [];
62
+ for (const [index, topLevelQItem] of questionnaire.item.entries()) {
63
+ const topLevelQRItemOrItems = topLevelQRItemsByIndex[index] ?? {
64
+ linkId: topLevelQItem.linkId,
65
+ text: topLevelQItem.text,
66
+ item: []
67
+ };
68
+
69
+ const updatedTopLevelQRItem = recursiveUpdateFunction(
70
+ topLevelQItem,
71
+ topLevelQRItemOrItems,
72
+ extraData
73
+ );
74
+
75
+ if (Array.isArray(updatedTopLevelQRItem)) {
76
+ if (updatedTopLevelQRItem.length > 0) {
77
+ topLevelQrItems.push(...updatedTopLevelQRItem);
78
+ }
79
+ continue;
80
+ }
81
+
82
+ if (updatedTopLevelQRItem && qrItemHasItemsOrAnswer(updatedTopLevelQRItem)) {
83
+ topLevelQrItems.push(updatedTopLevelQRItem);
84
+ }
85
+ }
86
+
87
+ return { ...questionnaireResponse, item: topLevelQrItems };
88
+ }
@@ -52,8 +52,10 @@ export function getValueSetPromise(url: string, terminologyServerUrl: string): P
52
52
 
53
53
  if (url.includes('ValueSet/$expand?url=')) {
54
54
  const splitUrl = url.split('ValueSet/$expand?url=');
55
- terminologyServerUrl = splitUrl[0];
56
- valueSetUrl = splitUrl[1];
55
+ if (splitUrl[0] && splitUrl[1]) {
56
+ terminologyServerUrl = splitUrl[0];
57
+ valueSetUrl = splitUrl[1];
58
+ }
57
59
  }
58
60
 
59
61
  valueSetUrl = valueSetUrl.replace('|', '&version=');