sdc-qrf 0.0.1
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.d.ts +8 -0
- package/lib/components.js +129 -0
- package/lib/components.js.map +1 -0
- package/lib/context.d.ts +3 -0
- package/lib/context.js +6 -0
- package/lib/context.js.map +1 -0
- package/lib/hooks.d.ts +1 -0
- package/lib/hooks.js +10 -0
- package/lib/hooks.js.map +1 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +8 -0
- package/lib/index.js.map +1 -0
- package/lib/types.d.ts +73 -0
- package/lib/types.js +3 -0
- package/lib/types.js.map +1 -0
- package/lib/utils.d.ts +58 -0
- package/lib/utils.js +483 -0
- package/lib/utils.js.map +1 -0
- package/package.json +38 -0
- package/src/components.tsx +212 -0
- package/src/context.ts +5 -0
- package/src/hooks.ts +7 -0
- package/src/index.ts +4 -0
- package/src/types.ts +105 -0
- package/src/utils.ts +676 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import fhirpath from 'fhirpath';
|
|
2
|
+
import _ from 'lodash';
|
|
3
|
+
import isArray from 'lodash/isArray';
|
|
4
|
+
import isPlainObject from 'lodash/isPlainObject';
|
|
5
|
+
import queryString from 'query-string';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Questionnaire,
|
|
9
|
+
QuestionnaireItem,
|
|
10
|
+
QuestionnaireItemInitial,
|
|
11
|
+
QuestionnaireResponse,
|
|
12
|
+
QuestionnaireResponseItem,
|
|
13
|
+
QuestionnaireResponseItemAnswer,
|
|
14
|
+
} from 'shared/src/contrib/aidbox';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
AnswerValue,
|
|
18
|
+
FormAnswerItems,
|
|
19
|
+
FormGroupItems,
|
|
20
|
+
FormItems,
|
|
21
|
+
ItemContext,
|
|
22
|
+
QuestionnaireResponseFormData,
|
|
23
|
+
RepeatableFormGroupItems,
|
|
24
|
+
} from './types';
|
|
25
|
+
|
|
26
|
+
export function wrapAnswerValue(type: QuestionnaireItem['type'], answer: any) {
|
|
27
|
+
if (type === 'choice') {
|
|
28
|
+
if (isPlainObject(answer)) {
|
|
29
|
+
return { Coding: answer };
|
|
30
|
+
} else {
|
|
31
|
+
return { string: answer };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (type === 'open-choice') {
|
|
36
|
+
if (isPlainObject(answer)) {
|
|
37
|
+
return { Coding: answer };
|
|
38
|
+
} else {
|
|
39
|
+
return { string: answer };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (type === 'text') {
|
|
44
|
+
return { string: answer };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (type === 'attachment') {
|
|
48
|
+
return { Attachment: answer };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (type === 'reference') {
|
|
52
|
+
return { Reference: answer };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (type === 'quantity') {
|
|
56
|
+
return { Quantity: answer };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { [type]: answer };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getBranchItems(
|
|
63
|
+
fieldPath: string[],
|
|
64
|
+
questionnaire: Questionnaire,
|
|
65
|
+
questionnaireResponse: QuestionnaireResponse,
|
|
66
|
+
): { qItem: QuestionnaireItem; qrItems: QuestionnaireResponseItem[] } {
|
|
67
|
+
let qrItem: QuestionnaireResponseItem | QuestionnaireResponse | undefined =
|
|
68
|
+
questionnaireResponse;
|
|
69
|
+
let qItem: QuestionnaireItem | Questionnaire = questionnaire;
|
|
70
|
+
|
|
71
|
+
// TODO: check for question with sub items
|
|
72
|
+
// TODO: check for root
|
|
73
|
+
for (let i = 0; i < fieldPath.length; i++) {
|
|
74
|
+
qItem = qItem.item!.find((curItem: any) => curItem.linkId === fieldPath[i])!;
|
|
75
|
+
|
|
76
|
+
if (qrItem) {
|
|
77
|
+
const qrItems: QuestionnaireResponseItem[] =
|
|
78
|
+
qrItem.item?.filter((curItem: any) => curItem.linkId === fieldPath[i]) ?? [];
|
|
79
|
+
|
|
80
|
+
if (qItem.repeats) {
|
|
81
|
+
if (i + 2 < fieldPath.length) {
|
|
82
|
+
// In the middle
|
|
83
|
+
qrItem = qrItems[parseInt(fieldPath[i + 2]!, 10)];
|
|
84
|
+
} else {
|
|
85
|
+
// Leaf
|
|
86
|
+
return { qItem, qrItems };
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
qrItem = qrItems[0];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (qItem.repeats || qItem.type !== 'group') {
|
|
94
|
+
i += 2;
|
|
95
|
+
} else {
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { qItem, qrItems: [qrItem] } as {
|
|
101
|
+
qItem: QuestionnaireItem;
|
|
102
|
+
qrItems: QuestionnaireResponseItem[];
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function calcContext(
|
|
107
|
+
initialContext: ItemContext,
|
|
108
|
+
variables: QuestionnaireItem['variable'],
|
|
109
|
+
qItem: QuestionnaireItem,
|
|
110
|
+
qrItem: QuestionnaireResponseItem,
|
|
111
|
+
): ItemContext {
|
|
112
|
+
// TODO: add root variable support
|
|
113
|
+
return {
|
|
114
|
+
...(variables || []).reduce(
|
|
115
|
+
(acc, curVariable) => ({
|
|
116
|
+
...acc,
|
|
117
|
+
[curVariable.name!]: fhirpath.evaluate(qrItem || {}, curVariable.expression!, acc),
|
|
118
|
+
}),
|
|
119
|
+
{ ...initialContext, context: qrItem, qitem: qItem },
|
|
120
|
+
),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function compareValue(firstAnswerValue: AnswerValue, secondAnswerValue: AnswerValue) {
|
|
125
|
+
const firstValueType = _.keys(firstAnswerValue)[0] as keyof AnswerValue;
|
|
126
|
+
const secondValueType = _.keys(secondAnswerValue)[0] as keyof AnswerValue;
|
|
127
|
+
if (firstValueType !== secondValueType) {
|
|
128
|
+
throw new Error('Enable when must be used for the same type');
|
|
129
|
+
}
|
|
130
|
+
if (
|
|
131
|
+
!_.includes(
|
|
132
|
+
['string', 'date', 'dateTime', 'time', 'uri', 'boolean', 'integer', 'decimal'],
|
|
133
|
+
firstValueType,
|
|
134
|
+
)
|
|
135
|
+
) {
|
|
136
|
+
throw new Error('Impossible to compare non-primitive type');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (firstValueType === 'Quantity') {
|
|
140
|
+
throw new Error('Quantity type is not supported yet');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const firstValue = firstAnswerValue[firstValueType];
|
|
144
|
+
const secondValue = secondAnswerValue[secondValueType];
|
|
145
|
+
|
|
146
|
+
if (firstValue! < secondValue!) {
|
|
147
|
+
return -1;
|
|
148
|
+
}
|
|
149
|
+
if (firstValue! > secondValue!) {
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isGroup(question: QuestionnaireItem) {
|
|
156
|
+
return question.type === 'group';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isFormGroupItems(
|
|
160
|
+
question: QuestionnaireItem,
|
|
161
|
+
answers: FormGroupItems | FormAnswerItems[],
|
|
162
|
+
): answers is FormGroupItems {
|
|
163
|
+
return isGroup(question) && _.isPlainObject(answers);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isRepeatableFormGroupItems(
|
|
167
|
+
question: QuestionnaireItem,
|
|
168
|
+
answers: FormGroupItems,
|
|
169
|
+
): answers is RepeatableFormGroupItems {
|
|
170
|
+
return !!question.repeats && _.isArray(answers.items);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function hasSubAnswerItems(items?: FormItems): items is FormItems {
|
|
174
|
+
return !!items && _.some(items, (x) => !_.some(x, _.isEmpty));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function mapFormToResponseRecursive(
|
|
178
|
+
answersItems: FormItems,
|
|
179
|
+
questionnaireItems: QuestionnaireItem[],
|
|
180
|
+
): QuestionnaireResponseItem[] {
|
|
181
|
+
return Object.entries(answersItems).reduce((acc, [linkId, answers]) => {
|
|
182
|
+
if (!linkId) {
|
|
183
|
+
console.warn('The answer item has no linkId');
|
|
184
|
+
return acc;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const question = questionnaireItems.filter((qItem) => qItem.linkId === linkId)[0];
|
|
188
|
+
|
|
189
|
+
if (!question) {
|
|
190
|
+
return acc;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (isFormGroupItems(question, answers)) {
|
|
194
|
+
const groups = isRepeatableFormGroupItems(question, answers)
|
|
195
|
+
? answers.items || []
|
|
196
|
+
: answers.items
|
|
197
|
+
? [answers.items]
|
|
198
|
+
: [];
|
|
199
|
+
return groups.reduce((newAcc, group) => {
|
|
200
|
+
const items = mapFormToResponseRecursive(group, question.item ?? []);
|
|
201
|
+
|
|
202
|
+
return [
|
|
203
|
+
...newAcc,
|
|
204
|
+
{
|
|
205
|
+
linkId,
|
|
206
|
+
...(items.length ? { item: items } : {}),
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
}, acc);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return [
|
|
213
|
+
...acc,
|
|
214
|
+
{
|
|
215
|
+
linkId,
|
|
216
|
+
answer: answers.reduce((answersAcc, answer) => {
|
|
217
|
+
if (typeof answer === 'undefined') {
|
|
218
|
+
return answersAcc;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!answer.value) {
|
|
222
|
+
return answersAcc;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const items = hasSubAnswerItems(answer.items)
|
|
226
|
+
? mapFormToResponseRecursive(answer.items, question.item ?? [])
|
|
227
|
+
: [];
|
|
228
|
+
|
|
229
|
+
return [
|
|
230
|
+
...answersAcc,
|
|
231
|
+
{
|
|
232
|
+
value: answer.value,
|
|
233
|
+
...(items.length ? { item: items } : {}),
|
|
234
|
+
},
|
|
235
|
+
];
|
|
236
|
+
}, [] as QuestionnaireResponseItemAnswer[]),
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
}, [] as QuestionnaireResponseItem[]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function mapFormToResponse(
|
|
243
|
+
values: FormItems,
|
|
244
|
+
questionnaire: Questionnaire,
|
|
245
|
+
keepDisabledAnswers?: boolean,
|
|
246
|
+
): Pick<QuestionnaireResponse, 'item'> {
|
|
247
|
+
return {
|
|
248
|
+
item: mapFormToResponseRecursive(
|
|
249
|
+
keepDisabledAnswers ? values : removeDisabledAnswers(questionnaire.item ?? [], values),
|
|
250
|
+
questionnaire.item ?? [],
|
|
251
|
+
),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function mapResponseToFormRecursive(
|
|
256
|
+
questionnaireResponseItems: QuestionnaireResponseItem[],
|
|
257
|
+
questionnaireItems: QuestionnaireItem[],
|
|
258
|
+
): FormItems {
|
|
259
|
+
return questionnaireItems.reduce((acc, question) => {
|
|
260
|
+
const { linkId, initial, repeats, text } = question;
|
|
261
|
+
|
|
262
|
+
if (!linkId) {
|
|
263
|
+
console.warn('The question has no linkId');
|
|
264
|
+
return acc;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const qrItems =
|
|
268
|
+
questionnaireResponseItems.filter((qrItem) => qrItem.linkId === linkId) ?? [];
|
|
269
|
+
|
|
270
|
+
if (qrItems.length && isGroup(question)) {
|
|
271
|
+
if (repeats) {
|
|
272
|
+
return {
|
|
273
|
+
...acc,
|
|
274
|
+
[linkId]: {
|
|
275
|
+
question: text,
|
|
276
|
+
items: qrItems.map((qrItem) => {
|
|
277
|
+
return mapResponseToFormRecursive(
|
|
278
|
+
qrItem.item ?? [],
|
|
279
|
+
question.item ?? [],
|
|
280
|
+
);
|
|
281
|
+
}),
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
} else {
|
|
285
|
+
return {
|
|
286
|
+
...acc,
|
|
287
|
+
[linkId]: {
|
|
288
|
+
question: text,
|
|
289
|
+
items: mapResponseToFormRecursive(
|
|
290
|
+
qrItems[0]?.item ?? [],
|
|
291
|
+
question.item ?? [],
|
|
292
|
+
),
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const answers = qrItems?.[0]?.answer?.length
|
|
299
|
+
? qrItems[0].answer
|
|
300
|
+
: initialToQuestionnaireResponseItemAnswer(initial);
|
|
301
|
+
|
|
302
|
+
if (!answers.length) {
|
|
303
|
+
return acc;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
...acc,
|
|
308
|
+
[linkId]: answers.map((answer) => ({
|
|
309
|
+
question: text,
|
|
310
|
+
value: answer.value,
|
|
311
|
+
items: mapResponseToFormRecursive(answer.item ?? [], question.item ?? []),
|
|
312
|
+
})),
|
|
313
|
+
};
|
|
314
|
+
}, {});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function mapResponseToForm(resource: QuestionnaireResponse, questionnaire: Questionnaire) {
|
|
318
|
+
return mapResponseToFormRecursive(resource.item ?? [], questionnaire.item ?? []);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function initialToQuestionnaireResponseItemAnswer(initial: QuestionnaireItemInitial[] | undefined) {
|
|
322
|
+
return (initial ?? []).map(({ value }) => ({ value } as QuestionnaireResponseItemAnswer));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function findAnswersForQuestionsRecursive(linkId: string, values?: FormItems): any | null {
|
|
326
|
+
if (values && _.has(values, linkId)) {
|
|
327
|
+
return values[linkId];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return _.reduce(
|
|
331
|
+
values,
|
|
332
|
+
(acc, v) => {
|
|
333
|
+
if (acc) {
|
|
334
|
+
return acc;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (_.isArray(v)) {
|
|
338
|
+
return _.reduce(
|
|
339
|
+
v,
|
|
340
|
+
(acc2, v2) => {
|
|
341
|
+
if (acc2) {
|
|
342
|
+
return acc2;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return findAnswersForQuestionsRecursive(linkId, v2.items);
|
|
346
|
+
},
|
|
347
|
+
null,
|
|
348
|
+
);
|
|
349
|
+
} else if (_.isArray(v.items)) {
|
|
350
|
+
return _.reduce(
|
|
351
|
+
v.items,
|
|
352
|
+
(acc2, v2) => {
|
|
353
|
+
if (acc2) {
|
|
354
|
+
return acc2;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return findAnswersForQuestionsRecursive(linkId, v2);
|
|
358
|
+
},
|
|
359
|
+
null,
|
|
360
|
+
);
|
|
361
|
+
} else {
|
|
362
|
+
return findAnswersForQuestionsRecursive(linkId, v.items);
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
null,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function findAnswersForQuestion<T = any>(
|
|
370
|
+
linkId: string,
|
|
371
|
+
parentPath: string[],
|
|
372
|
+
values: FormItems,
|
|
373
|
+
): Array<FormAnswerItems<T>> {
|
|
374
|
+
const p = _.cloneDeep(parentPath);
|
|
375
|
+
|
|
376
|
+
// Go up
|
|
377
|
+
while (p.length) {
|
|
378
|
+
const part = p.pop()!;
|
|
379
|
+
|
|
380
|
+
// Find answers in parent groups (including repeatable)
|
|
381
|
+
// They might have either 'items' of the group or number of the repeatable group in path
|
|
382
|
+
if (part === 'items' || !isNaN(part as any)) {
|
|
383
|
+
const parentGroup = _.get(values, [...p, part]);
|
|
384
|
+
|
|
385
|
+
if (typeof parentGroup === 'object' && linkId in parentGroup) {
|
|
386
|
+
return parentGroup[linkId];
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Go down
|
|
392
|
+
const answers = findAnswersForQuestionsRecursive(linkId, values);
|
|
393
|
+
|
|
394
|
+
return answers ? answers : [];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function isValueEqual(firstValue: AnswerValue, secondValue: AnswerValue) {
|
|
398
|
+
const firstValueType = _.keys(firstValue)[0];
|
|
399
|
+
const secondValueType = _.keys(secondValue)[0];
|
|
400
|
+
|
|
401
|
+
if (firstValueType !== secondValueType) {
|
|
402
|
+
console.error('Enable when must be used for the same type');
|
|
403
|
+
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (firstValueType === 'Coding') {
|
|
408
|
+
// NOTE: what if undefined === undefined
|
|
409
|
+
return firstValue.Coding?.code === secondValue.Coding?.code;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return _.isEqual(firstValue, secondValue);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function getChecker(
|
|
416
|
+
operator: string,
|
|
417
|
+
): (values: Array<{ value: any }>, answerValue: any) => boolean {
|
|
418
|
+
if (operator === '=') {
|
|
419
|
+
return (values, answerValue) =>
|
|
420
|
+
_.findIndex(values, ({ value }) => isValueEqual(value, answerValue)) !== -1;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (operator === '!=') {
|
|
424
|
+
return (values, answerValue) =>
|
|
425
|
+
_.findIndex(values, ({ value }) => isValueEqual(value, answerValue)) === -1;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (operator === 'exists') {
|
|
429
|
+
return (values, answerValue) => {
|
|
430
|
+
const answersLength = _.reject(
|
|
431
|
+
values,
|
|
432
|
+
(value) => _.isEmpty(value.value) || _.every(_.mapValues(value.value, _.isEmpty)),
|
|
433
|
+
).length;
|
|
434
|
+
const answer = answerValue?.boolean ?? true;
|
|
435
|
+
return answersLength > 0 === answer;
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (operator === '>=') {
|
|
440
|
+
return (values, answerValue) =>
|
|
441
|
+
_.findIndex(
|
|
442
|
+
_.reject(values, (value) => _.isEmpty(value.value)),
|
|
443
|
+
({ value }) => compareValue(value, answerValue) >= 0,
|
|
444
|
+
) !== -1;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (operator === '>') {
|
|
448
|
+
return (values, answerValue) =>
|
|
449
|
+
_.findIndex(
|
|
450
|
+
_.reject(values, (value) => _.isEmpty(value.value)),
|
|
451
|
+
({ value }) => compareValue(value, answerValue) > 0,
|
|
452
|
+
) !== -1;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (operator === '<=') {
|
|
456
|
+
return (values, answerValue) =>
|
|
457
|
+
_.findIndex(
|
|
458
|
+
_.reject(values, (value) => _.isEmpty(value.value)),
|
|
459
|
+
({ value }) => compareValue(value, answerValue) <= 0,
|
|
460
|
+
) !== -1;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (operator === '<') {
|
|
464
|
+
return (values, answerValue) =>
|
|
465
|
+
_.findIndex(
|
|
466
|
+
_.reject(values, (value) => _.isEmpty(value.value)),
|
|
467
|
+
({ value }) => compareValue(value, answerValue) < 0,
|
|
468
|
+
) !== -1;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
console.error(`Unsupported enableWhen.operator ${operator}`);
|
|
472
|
+
|
|
473
|
+
return _.constant(true);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function isQuestionEnabled(qItem: QuestionnaireItem, parentPath: string[], values: FormItems) {
|
|
477
|
+
const { enableWhen, enableBehavior } = qItem;
|
|
478
|
+
|
|
479
|
+
if (!enableWhen) {
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const iterFn = enableBehavior === 'any' ? _.some : _.every;
|
|
484
|
+
|
|
485
|
+
return iterFn(enableWhen, ({ question, answer, operator }) => {
|
|
486
|
+
const check = getChecker(operator);
|
|
487
|
+
|
|
488
|
+
if (_.includes(parentPath, question)) {
|
|
489
|
+
// TODO: handle double-nested values
|
|
490
|
+
const parentAnswerPath = _.slice(parentPath, 0, parentPath.length - 1);
|
|
491
|
+
const parentAnswer = _.get(values, parentAnswerPath);
|
|
492
|
+
|
|
493
|
+
return check(parentAnswer ? [parentAnswer] : [], answer);
|
|
494
|
+
}
|
|
495
|
+
const answers = findAnswersForQuestion(question, parentPath, values);
|
|
496
|
+
|
|
497
|
+
return check(_.compact(answers), answer);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export function removeDisabledAnswers(
|
|
502
|
+
questionnaireItems: QuestionnaireItem[],
|
|
503
|
+
values: FormItems,
|
|
504
|
+
): FormItems {
|
|
505
|
+
return removeDisabledAnswersRecursive(questionnaireItems, [], values, {});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function removeDisabledAnswersRecursive(
|
|
509
|
+
questionnaireItems: QuestionnaireItem[],
|
|
510
|
+
parentPath: string[],
|
|
511
|
+
answersItems: FormItems,
|
|
512
|
+
initialValues: FormItems,
|
|
513
|
+
): FormItems {
|
|
514
|
+
return questionnaireItems.reduce((acc, questionnaireItem) => {
|
|
515
|
+
const values = parentPath.length ? _.set(_.cloneDeep(initialValues), parentPath, acc) : acc;
|
|
516
|
+
|
|
517
|
+
const { linkId } = questionnaireItem;
|
|
518
|
+
const answers = answersItems[linkId!];
|
|
519
|
+
|
|
520
|
+
if (!answers) {
|
|
521
|
+
return acc;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (!isQuestionEnabled(questionnaireItem, parentPath, values)) {
|
|
525
|
+
return acc;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (isFormGroupItems(questionnaireItem, answers)) {
|
|
529
|
+
if (!answers.items) {
|
|
530
|
+
return acc;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (isRepeatableFormGroupItems(questionnaireItem, answers)) {
|
|
534
|
+
return {
|
|
535
|
+
...acc,
|
|
536
|
+
[linkId!]: {
|
|
537
|
+
...answers,
|
|
538
|
+
items: answers.items.map((group, index) =>
|
|
539
|
+
removeDisabledAnswersRecursive(
|
|
540
|
+
questionnaireItem.item ?? [],
|
|
541
|
+
[...parentPath, linkId!, 'items', index.toString()],
|
|
542
|
+
group,
|
|
543
|
+
values,
|
|
544
|
+
),
|
|
545
|
+
),
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
} else {
|
|
549
|
+
return {
|
|
550
|
+
...acc,
|
|
551
|
+
[linkId!]: {
|
|
552
|
+
...answers,
|
|
553
|
+
items: removeDisabledAnswersRecursive(
|
|
554
|
+
questionnaireItem.item ?? [],
|
|
555
|
+
[...parentPath, linkId!, 'items'],
|
|
556
|
+
answers.items,
|
|
557
|
+
values,
|
|
558
|
+
),
|
|
559
|
+
},
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
...acc,
|
|
566
|
+
[linkId!]: answers.reduce((answersAcc, answer, index) => {
|
|
567
|
+
if (typeof answer === 'undefined') {
|
|
568
|
+
return answersAcc;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (!answer.value) {
|
|
572
|
+
return answersAcc;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const items = hasSubAnswerItems(answer.items)
|
|
576
|
+
? removeDisabledAnswersRecursive(
|
|
577
|
+
questionnaireItem.item ?? [],
|
|
578
|
+
[...parentPath, linkId!, index.toString(), 'items'],
|
|
579
|
+
answer.items,
|
|
580
|
+
values,
|
|
581
|
+
)
|
|
582
|
+
: {};
|
|
583
|
+
|
|
584
|
+
return [...answersAcc, { ...answer, items }];
|
|
585
|
+
}, [] as any),
|
|
586
|
+
};
|
|
587
|
+
}, {} as any);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export function getEnabledQuestions(
|
|
591
|
+
questionnaireItems: QuestionnaireItem[],
|
|
592
|
+
parentPath: string[],
|
|
593
|
+
values: FormItems,
|
|
594
|
+
) {
|
|
595
|
+
return _.filter(questionnaireItems, (qItem) => {
|
|
596
|
+
const { linkId } = qItem;
|
|
597
|
+
|
|
598
|
+
if (!linkId) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return isQuestionEnabled(qItem, parentPath, values);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export function calcInitialContext(
|
|
607
|
+
qrfDataContext: QuestionnaireResponseFormData['context'],
|
|
608
|
+
values: FormItems,
|
|
609
|
+
): ItemContext {
|
|
610
|
+
const questionnaireResponse = {
|
|
611
|
+
...qrfDataContext.questionnaireResponse,
|
|
612
|
+
...mapFormToResponse(values, qrfDataContext.questionnaire),
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
return {
|
|
616
|
+
...qrfDataContext.launchContextParameters.reduce(
|
|
617
|
+
(acc, { name, value, resource }) => ({
|
|
618
|
+
...acc,
|
|
619
|
+
[name]:
|
|
620
|
+
value && isPlainObject(value)
|
|
621
|
+
? value[Object.keys(value)[0] as keyof AnswerValue]
|
|
622
|
+
: resource,
|
|
623
|
+
}),
|
|
624
|
+
{},
|
|
625
|
+
),
|
|
626
|
+
|
|
627
|
+
// Vars defined in IG
|
|
628
|
+
questionnaire: qrfDataContext.questionnaire,
|
|
629
|
+
resource: questionnaireResponse,
|
|
630
|
+
context: questionnaireResponse,
|
|
631
|
+
|
|
632
|
+
// Vars we use for backward compatibility
|
|
633
|
+
Questionnaire: qrfDataContext.questionnaire,
|
|
634
|
+
QuestionnaireResponse: questionnaireResponse,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function resolveTemplateExpr(str: string, context: ItemContext) {
|
|
639
|
+
const matches = str.match(/{{[^}]+}}/g);
|
|
640
|
+
|
|
641
|
+
if (matches) {
|
|
642
|
+
return matches.reduce((result, match) => {
|
|
643
|
+
const expr = match.replace(/[{}]/g, '');
|
|
644
|
+
|
|
645
|
+
const resolvedVar = fhirpath.evaluate(context.context || {}, expr, context);
|
|
646
|
+
|
|
647
|
+
if (resolvedVar?.length) {
|
|
648
|
+
return result.replace(match, resolvedVar.join(','));
|
|
649
|
+
} else {
|
|
650
|
+
return result.replace(match, '');
|
|
651
|
+
}
|
|
652
|
+
}, str);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return str;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function parseFhirQueryExpression(expression: string, context: ItemContext) {
|
|
659
|
+
const [resourceType, paramsQS] = expression.split('?', 2);
|
|
660
|
+
const searchParams = Object.fromEntries(
|
|
661
|
+
Object.entries(queryString.parse(paramsQS ?? '')).map(([key, value]) => {
|
|
662
|
+
if (!value) {
|
|
663
|
+
return [key, value];
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return [
|
|
667
|
+
key,
|
|
668
|
+
isArray(value)
|
|
669
|
+
? value.map((arrValue) => resolveTemplateExpr(arrValue!, context))
|
|
670
|
+
: resolveTemplateExpr(value, context),
|
|
671
|
+
];
|
|
672
|
+
}),
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
return [resourceType, searchParams];
|
|
676
|
+
}
|