-
Notifications
You must be signed in to change notification settings - Fork 85
/
Copy pathuseFormJson.ts
257 lines (228 loc) · 9.89 KB
/
useFormJson.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
import { useEffect, useState } from 'react';
import { type FormSchemaTransformer, type FormSchema, type FormSection, type ReferencedForm } from '../types';
import { isTrue } from '../utils/boolean-utils';
import { applyFormIntent } from '../utils/forms-loader';
import { fetchOpenMRSForm, fetchClobData } from '../api';
import { getRegisteredFormSchemaTransformers } from '../registry/registry';
import { moduleName } from '../globals';
export function useFormJson(formUuid: string, rawFormJson: any, encounterUuid: string, formSessionIntent: string) {
const [formJson, setFormJson] = useState<FormSchema>(null);
const [error, setError] = useState(validateFormsArgs(formUuid, rawFormJson));
useEffect(() => {
const abortController = new AbortController();
const setFormJsonWithTranslations = (formJson: FormSchema) => {
if (formJson?.translations) {
const language = window.i18next.language;
window.i18next.addResourceBundle(language, moduleName, formJson.translations, true, true);
}
setFormJson(formJson);
};
loadFormJson(formUuid, rawFormJson, formSessionIntent)
.then((formJson) => {
setFormJsonWithTranslations({ ...formJson, encounter: encounterUuid });
})
.catch((error) => {
if (error.name !== 'AbortError') {
console.error(error);
setError(new Error('Error loading form JSON: ' + error.message));
}
});
return () => {
abortController.abort();
};
}, [formSessionIntent, formUuid, rawFormJson, encounterUuid]);
return {
formJson,
isLoading: !formJson && !error,
formError: error,
};
}
/**
* Fetches a form JSON schema from OpenMRS and recursively fetches its subForms if they available.
*
* If `rawFormJson` is provided, it will be used as the raw form JSON object. Otherwise, the form JSON will be fetched from OpenMRS using the `formIdentifier` parameter.
*
* @param rawFormJson The raw form JSON object to be used if `formIdentifier` is not provided.
* @param formIdentifier The UUID or name of the form to be fetched from OpenMRS if `rawFormJson` is not provided.
* @param formSessionIntent An optional parameter that represents the current intent.
* @returns A well-built form object that might include subForms.
*/
export async function loadFormJson(
formIdentifier: string,
rawFormJson?: FormSchema,
formSessionIntent?: string,
): Promise<FormSchema> {
const openmrsFormResponse = await fetchOpenMRSForm(formIdentifier);
const clobDataResponse = await fetchClobData(openmrsFormResponse);
const transformers = await getRegisteredFormSchemaTransformers();
const formJson: FormSchema = clobDataResponse
? { ...clobDataResponse, uuid: openmrsFormResponse.uuid }
: parseFormJson(rawFormJson);
// Sub forms
const subFormRefs = extractSubFormRefs(formJson);
const subForms = await loadSubForms(subFormRefs, formSessionIntent);
updateFormJsonWithSubForms(formJson, subForms);
// Form components
const formComponentsRefs = getReferencedForms(formJson);
const resolvedFormComponents = await loadFormComponents(formComponentsRefs);
const formNameToAliasMap = formComponentsRefs.reduce((acc, form) => {
acc[form.formName] = form.alias;
return acc;
}, {});
const formComponents = mapFormComponents(resolvedFormComponents);
updateFormJsonWithComponents(formJson, formComponents, formNameToAliasMap);
return refineFormJson(formJson, transformers, formSessionIntent);
}
function extractSubFormRefs(formJson: FormSchema): string[] {
return formJson.pages
.filter((page) => page.isSubform && !page.subform.form && page.subform?.name)
.map((page) => page.subform?.name);
}
async function loadSubForms(subFormRefs: string[], formSessionIntent?: string): Promise<FormSchema[]> {
return Promise.all(subFormRefs.map((subForm) => loadFormJson(subForm, null, formSessionIntent)));
}
function updateFormJsonWithSubForms(formJson: FormSchema, subForms: FormSchema[]): void {
subForms.forEach((subForm) => {
const matchingPage = formJson.pages.find((page) => page.subform?.name === subForm.name);
if (matchingPage) {
matchingPage.subform.form = subForm;
}
});
}
function validateFormsArgs(formUuid: string, rawFormJson: any): Error {
if (!formUuid && !rawFormJson) {
return new Error('InvalidArgumentsErr: Neither formUuid nor formJson was provided');
}
if (formUuid && rawFormJson) {
return new Error('InvalidArgumentsErr: Both formUuid and formJson cannot be provided at the same time.');
}
}
/**
* Refines the input form JSON object by parsing it, removing inline sub forms, applying form schema transformers, setting the encounter type, and applying form intents if provided.
* @param {any} formJson - The input form JSON object or string.
* @param {string} [formSessionIntent] - The optional form session intent.
* @returns {FormSchema} - The refined form JSON object of type FormSchema.
*/
async function refineFormJson(
formJson: any,
schemaTransformers: FormSchemaTransformer[] = [],
formSessionIntent?: string,
): Promise<FormSchema> {
await removeInlineSubForms(formJson, formSessionIntent);
const transformedFormJson = await schemaTransformers.reduce(async (form, transformer) => {
const currentForm = await form;
if (isPromise(transformer.transform(currentForm))) {
return transformer.transform(currentForm);
} else {
return transformer.transform(currentForm);
}
}, Promise.resolve(formJson));
setEncounterType(transformedFormJson);
return applyFormIntent(formSessionIntent, transformedFormJson);
}
function isPromise(value: any): value is Promise<any> {
return value && typeof value.then === 'function';
}
/**
* Parses the input form JSON and returns a deep copy of the object.
* @param {any} formJson - The input form JSON object or string.
* @returns {FormSchema} - The parsed form JSON object of type FormSchema.
*/
function parseFormJson(formJson: any): FormSchema {
return typeof formJson === 'string' ? JSON.parse(formJson) : JSON.parse(JSON.stringify(formJson));
}
/**
* Removes inline sub forms from the form JSON and replaces them with their pages if the encounter type matches.
* @param {FormSchema} formJson - The input form JSON object of type FormSchema.
* @param {string} formSessionIntent - The form session intent.
*/
async function removeInlineSubForms(formJson: FormSchema, formSessionIntent: string): Promise<void> {
for (let i = formJson.pages.length - 1; i >= 0; i--) {
const page = formJson.pages[i];
if (
isTrue(page.isSubform) &&
!isTrue(page.isHidden) &&
page.subform?.form?.encounterType === formJson.encounterType
) {
const nonSubformPages = page.subform.form.pages.filter((page) => !isTrue(page.isSubform));
formJson.pages.splice(i, 1, ...(await refineFormJson(page.subform.form, [], formSessionIntent)).pages);
}
}
}
/**
* Sets the encounter type for the form JSON if it's provided through the `encounter` attribute.
* @param {FormSchema} formJson - The input form JSON object of type FormSchema.
*/
function setEncounterType(formJson: FormSchema): void {
if (formJson.encounter && typeof formJson.encounter === 'string' && !formJson.encounterType) {
formJson.encounterType = formJson.encounter;
delete formJson.encounter;
}
}
/**
* Functions to support reusable Form Components
*/
function getReferencedForms(formJson: FormSchema): Array<ReferencedForm> {
const referencedForms: Array<any> = formJson?.referencedForms;
if (!referencedForms) {
return [];
}
return referencedForms;
}
async function loadFormComponents(formComponentRefs: Array<ReferencedForm>): Promise<FormSchema[]> {
return Promise.all(formComponentRefs.map((formComponent) => loadFormJson(formComponent.formName, null, null)));
}
function mapFormComponents(formComponents: Array<FormSchema>): Map<string, FormSchema> {
const formComponentsMap: Map<string, FormSchema> = new Map();
formComponents.forEach((formComponent) => {
formComponentsMap.set(formComponent.name, formComponent);
});
return formComponentsMap;
}
function updateFormJsonWithComponents(
formJson: FormSchema,
formComponents: Map<string, FormSchema>,
formNameToAliasMap: Record<string, string>,
): void {
formComponents.forEach((component, targetFormName) => {
//loop through pages and search sections for reference key
formJson.pages.forEach((page) => {
if (page.sections) {
page.sections.forEach((section) => {
if (
section.reference &&
(section.reference.form === targetFormName || section.reference.form === formNameToAliasMap[targetFormName])
) {
// resolve referenced component section
let resolvedFormSection = getReferencedFormSection(section, component);
// add resulting referenced component section to section
Object.assign(section, resolvedFormSection);
}
});
}
});
});
}
function getReferencedFormSection(formSection: FormSection, formComponent: FormSchema): FormSection {
let referencedFormSection: FormSection;
// search for component page and section reference from component
let matchingComponentPage = formComponent.pages.filter((page) => page.label === formSection.reference.page);
if (matchingComponentPage.length > 0) {
let matchingComponentSection = matchingComponentPage[0].sections.filter(
(componentSection) => componentSection.label === formSection.reference.section,
);
if (matchingComponentSection.length > 0) {
referencedFormSection = matchingComponentSection[0];
}
}
return filterExcludedQuestions(referencedFormSection, formSection.reference);
}
function filterExcludedQuestions(formSection: FormSection, reference: any): FormSection {
if (reference?.excludeQuestions) {
const excludeQuestions = reference.excludeQuestions;
formSection.questions = formSection.questions.filter((question) => {
return !excludeQuestions.includes(question.id);
});
}
return formSection;
}