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.
@@ -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
@@ -0,0 +1,5 @@
1
+ import { createContext } from 'react';
2
+
3
+ import { QRFContextData } from './types';
4
+
5
+ export const QRFContext = createContext<QRFContextData>({} as any);
package/src/hooks.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { useContext } from 'react';
2
+
3
+ import { QRFContext } from './context';
4
+
5
+ export function useQuestionnaireResponseFormContext() {
6
+ return useContext(QRFContext);
7
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './types';
2
+ export * from './utils';
3
+ export * from './hooks';
4
+ export * from './components';
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
+ }