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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import fhirpath from 'fhirpath';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
import isEqual from 'lodash/isEqual';
|
|
5
|
+
import { ReactChild, useEffect, useContext, useMemo, useRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import { QuestionnaireItem } from 'shared/src/contrib/aidbox';
|
|
8
|
+
|
|
9
|
+
import { useQuestionnaireResponseFormContext } from '.';
|
|
10
|
+
import { QRFContext } from './context';
|
|
11
|
+
import { ItemContext, QRFContextData, QuestionItemProps, QuestionItemsProps } from './types';
|
|
12
|
+
import {
|
|
13
|
+
calcContext,
|
|
14
|
+
getBranchItems,
|
|
15
|
+
getEnabledQuestions,
|
|
16
|
+
wrapAnswerValue,
|
|
17
|
+
removeDisabledAnswers,
|
|
18
|
+
} from './utils';
|
|
19
|
+
|
|
20
|
+
export function usePreviousValue<T = any>(value: T) {
|
|
21
|
+
const prevValue = useRef<T>();
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
prevValue.current = value;
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
prevValue.current = undefined;
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return prevValue.current;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function QuestionItems(props: QuestionItemsProps) {
|
|
35
|
+
const { questionItems, parentPath, context } = props;
|
|
36
|
+
const { formValues } = useQuestionnaireResponseFormContext();
|
|
37
|
+
const cleanValues = removeDisabledAnswers(context.questionnaire.item!, formValues);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
{getEnabledQuestions(questionItems, parentPath, cleanValues).map((item, index) => {
|
|
42
|
+
return (
|
|
43
|
+
<div className={classNames('questionFormItem', item.linkId)}>
|
|
44
|
+
<QuestionItem
|
|
45
|
+
key={index}
|
|
46
|
+
questionItem={item}
|
|
47
|
+
context={context}
|
|
48
|
+
parentPath={parentPath}
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
})}
|
|
53
|
+
</>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function QuestionItem(props: QuestionItemProps) {
|
|
58
|
+
const { questionItem, context: initialContext, parentPath } = props;
|
|
59
|
+
const {
|
|
60
|
+
questionItemComponents,
|
|
61
|
+
customWidgets,
|
|
62
|
+
groupItemComponent,
|
|
63
|
+
itemControlQuestionItemComponents,
|
|
64
|
+
itemControlGroupItemComponents,
|
|
65
|
+
} = useContext(QRFContext);
|
|
66
|
+
const { formValues, setFormValues } = useQuestionnaireResponseFormContext();
|
|
67
|
+
|
|
68
|
+
const { type, linkId, calculatedExpression, variable, repeats, itemControl } = questionItem;
|
|
69
|
+
const fieldPath = useMemo(() => [...parentPath, linkId!], [parentPath, linkId]);
|
|
70
|
+
|
|
71
|
+
// TODO: how to do when item is not in QR (e.g. default element of repeatable group)
|
|
72
|
+
const branchItems = getBranchItems(
|
|
73
|
+
fieldPath,
|
|
74
|
+
initialContext.questionnaire,
|
|
75
|
+
initialContext.resource,
|
|
76
|
+
);
|
|
77
|
+
const context =
|
|
78
|
+
type === 'group'
|
|
79
|
+
? branchItems.qrItems.map((curQRItem) =>
|
|
80
|
+
calcContext(initialContext, variable, branchItems.qItem, curQRItem),
|
|
81
|
+
)
|
|
82
|
+
: calcContext(initialContext, variable, branchItems.qItem, branchItems.qrItems[0]!);
|
|
83
|
+
const prevAnswers = usePreviousValue(_.get(formValues, fieldPath));
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!isGroupItem(questionItem, context) && calculatedExpression) {
|
|
87
|
+
// TODO: Add support for x-fhir-query
|
|
88
|
+
if (calculatedExpression.language === 'text/fhirpath') {
|
|
89
|
+
const newValues = fhirpath.evaluate(
|
|
90
|
+
context.context || {},
|
|
91
|
+
calculatedExpression.expression!,
|
|
92
|
+
context as ItemContext,
|
|
93
|
+
);
|
|
94
|
+
const newAnswers = newValues.length
|
|
95
|
+
? repeats
|
|
96
|
+
? newValues.map((answer: any) => ({ value: wrapAnswerValue(type, answer) }))
|
|
97
|
+
: [{ value: wrapAnswerValue(type, newValues[0]) }]
|
|
98
|
+
: undefined;
|
|
99
|
+
|
|
100
|
+
if (!isEqual(newAnswers, prevAnswers)) {
|
|
101
|
+
setFormValues(_.set(_.cloneDeep(formValues), fieldPath, newAnswers));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}, [
|
|
106
|
+
setFormValues,
|
|
107
|
+
formValues,
|
|
108
|
+
calculatedExpression,
|
|
109
|
+
context,
|
|
110
|
+
parentPath,
|
|
111
|
+
repeats,
|
|
112
|
+
type,
|
|
113
|
+
questionItem,
|
|
114
|
+
prevAnswers,
|
|
115
|
+
fieldPath,
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
if (isGroupItem(questionItem, context)) {
|
|
119
|
+
if (itemControl) {
|
|
120
|
+
if (
|
|
121
|
+
!itemControlGroupItemComponents ||
|
|
122
|
+
!itemControlGroupItemComponents[itemControl?.coding?.[0]?.code!]
|
|
123
|
+
) {
|
|
124
|
+
console.warn(`QRF: Unsupported group itemControl '${itemControl?.coding?.[0]
|
|
125
|
+
?.code!}'.
|
|
126
|
+
Please define 'itemControlGroupWidgets' for '${itemControl?.coding?.[0]?.code!}'`);
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const Component = itemControlGroupItemComponents[itemControl?.coding?.[0]?.code!]!;
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<Component context={context} parentPath={parentPath} questionItem={questionItem} />
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (!groupItemComponent) {
|
|
138
|
+
console.warn(`QRF: groupWidget is not specified but used in questionnaire.`);
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const GroupWidgetComponent = groupItemComponent;
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<GroupWidgetComponent
|
|
147
|
+
context={context}
|
|
148
|
+
parentPath={parentPath}
|
|
149
|
+
questionItem={questionItem}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (itemControl) {
|
|
155
|
+
if (
|
|
156
|
+
!itemControlQuestionItemComponents ||
|
|
157
|
+
!itemControlQuestionItemComponents[itemControl.coding?.[0]?.code!]
|
|
158
|
+
) {
|
|
159
|
+
console.warn(
|
|
160
|
+
`QRF: Unsupported itemControl '${itemControl?.coding?.[0]?.code!}'.
|
|
161
|
+
Please define 'itemControlWidgets' for '${itemControl?.coding?.[0]?.code!}'`,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const Component = itemControlQuestionItemComponents[itemControl?.coding?.[0]?.code!]!;
|
|
168
|
+
|
|
169
|
+
return <Component context={context} parentPath={parentPath} questionItem={questionItem} />;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// TODO: deprecate!
|
|
173
|
+
if (customWidgets && linkId && linkId in customWidgets) {
|
|
174
|
+
console.warn(
|
|
175
|
+
`QRF: 'customWidgets' are deprecated, use 'Questionnaire.item.itemControl' instead`,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (type === 'group') {
|
|
179
|
+
console.error(`QRF: Use 'itemControl' for group custom widgets`);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const Component = customWidgets[linkId]!;
|
|
184
|
+
|
|
185
|
+
return <Component context={context} parentPath={parentPath} questionItem={questionItem} />;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (type in questionItemComponents) {
|
|
189
|
+
const Component = questionItemComponents[type]!;
|
|
190
|
+
|
|
191
|
+
return <Component context={context} parentPath={parentPath} questionItem={questionItem} />;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.error(`QRF: Unsupported item type '${type}'`);
|
|
195
|
+
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function QuestionnaireResponseFormProvider({
|
|
200
|
+
children,
|
|
201
|
+
...props
|
|
202
|
+
}: QRFContextData & { children: ReactChild }) {
|
|
203
|
+
return <QRFContext.Provider value={props}>{children}</QRFContext.Provider>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* Helper that resolves right context type */
|
|
207
|
+
function isGroupItem(
|
|
208
|
+
questionItem: QuestionnaireItem,
|
|
209
|
+
context: ItemContext | ItemContext[],
|
|
210
|
+
): context is ItemContext[] {
|
|
211
|
+
return questionItem.type === 'group';
|
|
212
|
+
}
|
package/src/context.ts
ADDED
package/src/hooks.ts
ADDED
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ComponentType } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Observation,
|
|
5
|
+
ParametersParameter,
|
|
6
|
+
Questionnaire,
|
|
7
|
+
QuestionnaireItem,
|
|
8
|
+
QuestionnaireResponse,
|
|
9
|
+
QuestionnaireResponseItem,
|
|
10
|
+
QuestionnaireResponseItemAnswer,
|
|
11
|
+
} from 'shared/src/contrib/aidbox';
|
|
12
|
+
|
|
13
|
+
export type GroupItemComponent = ComponentType<GroupItemProps>;
|
|
14
|
+
export type QuestionItemComponent = ComponentType<QuestionItemProps>;
|
|
15
|
+
|
|
16
|
+
export type CustomWidgetsMapping = {
|
|
17
|
+
// [linkId: QuestionnaireItem['linkId']]: QuestionItemComponent;
|
|
18
|
+
[linkId: string]: QuestionItemComponent;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type QuestionItemComponentMapping = {
|
|
22
|
+
// [type: QuestionnaireItem['type']]: QuestionItemComponent;
|
|
23
|
+
[type: string]: QuestionItemComponent;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ItemControlQuestionItemComponentMapping = {
|
|
27
|
+
[code: string]: QuestionItemComponent;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ItemControlGroupItemComponentMapping = {
|
|
31
|
+
[code: string]: GroupItemComponent;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ItemContext = {
|
|
35
|
+
resource: QuestionnaireResponse;
|
|
36
|
+
questionnaire: Questionnaire;
|
|
37
|
+
context: QuestionnaireResponseItem | QuestionnaireResponse;
|
|
38
|
+
qitem?: QuestionnaireItem;
|
|
39
|
+
[x: string]: any;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export interface QRFContextData {
|
|
43
|
+
questionItemComponents: QuestionItemComponentMapping;
|
|
44
|
+
groupItemComponent?: GroupItemComponent;
|
|
45
|
+
customWidgets?: CustomWidgetsMapping;
|
|
46
|
+
itemControlQuestionItemComponents?: ItemControlQuestionItemComponentMapping;
|
|
47
|
+
itemControlGroupItemComponents?: ItemControlGroupItemComponentMapping;
|
|
48
|
+
readOnly?: boolean;
|
|
49
|
+
|
|
50
|
+
formValues: FormItems;
|
|
51
|
+
setFormValues: (values: FormItems) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface QuestionItemsProps {
|
|
55
|
+
questionItems: QuestionnaireItem[];
|
|
56
|
+
|
|
57
|
+
context: ItemContext;
|
|
58
|
+
parentPath: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface QuestionItemProps {
|
|
62
|
+
questionItem: QuestionnaireItem;
|
|
63
|
+
|
|
64
|
+
context: ItemContext;
|
|
65
|
+
parentPath: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface GroupItemProps {
|
|
69
|
+
questionItem: QuestionnaireItem;
|
|
70
|
+
|
|
71
|
+
context: ItemContext[];
|
|
72
|
+
parentPath: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type AnswerValue = Required<QuestionnaireResponseItemAnswer>['value'] &
|
|
76
|
+
Required<Observation>['value'];
|
|
77
|
+
|
|
78
|
+
export interface RepeatableFormGroupItems {
|
|
79
|
+
question?: string;
|
|
80
|
+
items?: FormItems[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface NotRepeatableFormGroupItems {
|
|
84
|
+
question?: string;
|
|
85
|
+
items?: FormItems;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type FormGroupItems = RepeatableFormGroupItems | NotRepeatableFormGroupItems;
|
|
89
|
+
|
|
90
|
+
export interface FormAnswerItems<T = any> {
|
|
91
|
+
value: T;
|
|
92
|
+
question?: string;
|
|
93
|
+
items?: FormItems;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type FormItems = Record<string, FormGroupItems | FormAnswerItems[]>;
|
|
97
|
+
|
|
98
|
+
export interface QuestionnaireResponseFormData {
|
|
99
|
+
formValues: FormItems;
|
|
100
|
+
context: {
|
|
101
|
+
questionnaire: Questionnaire;
|
|
102
|
+
questionnaireResponse: QuestionnaireResponse;
|
|
103
|
+
launchContextParameters: ParametersParameter[];
|
|
104
|
+
};
|
|
105
|
+
}
|