@openmrs/esm-form-builder-app 1.0.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.
- package/LICENSE +401 -0
- package/README.md +35 -0
- package/package.json +106 -0
- package/src/components/action-buttons/action-buttons.component.tsx +185 -0
- package/src/components/action-buttons/action-buttons.scss +16 -0
- package/src/components/dashboard/dashboard.component.tsx +309 -0
- package/src/components/dashboard/dashboard.scss +112 -0
- package/src/components/dashboard/dashboard.test.tsx +208 -0
- package/src/components/empty-state/empty-data-illustration.component.tsx +51 -0
- package/src/components/empty-state/empty-state.component.tsx +41 -0
- package/src/components/empty-state/empty-state.scss +55 -0
- package/src/components/error-state/error-state.component.tsx +37 -0
- package/src/components/error-state/error-state.scss +49 -0
- package/src/components/form-editor/form-editor.component.tsx +125 -0
- package/src/components/form-editor/form-editor.scss +33 -0
- package/src/components/form-renderer/form-renderer.component.tsx +123 -0
- package/src/components/form-renderer/form-renderer.scss +57 -0
- package/src/components/interactive-builder/add-question-modal.component.tsx +427 -0
- package/src/components/interactive-builder/delete-page-modal.component.tsx +89 -0
- package/src/components/interactive-builder/delete-question-modal.component.tsx +93 -0
- package/src/components/interactive-builder/delete-section-modal.component.tsx +91 -0
- package/src/components/interactive-builder/edit-question-modal.component.tsx +465 -0
- package/src/components/interactive-builder/editable-value.component.tsx +64 -0
- package/src/components/interactive-builder/editable-value.scss +23 -0
- package/src/components/interactive-builder/interactive-builder.component.tsx +569 -0
- package/src/components/interactive-builder/interactive-builder.scss +100 -0
- package/src/components/interactive-builder/new-form-modal.component.tsx +86 -0
- package/src/components/interactive-builder/page-modal.component.tsx +91 -0
- package/src/components/interactive-builder/question-modal.scss +35 -0
- package/src/components/interactive-builder/section-modal.component.tsx +94 -0
- package/src/components/interactive-builder/value-editor.component.tsx +55 -0
- package/src/components/interactive-builder/value-editor.scss +10 -0
- package/src/components/modals/save-form.component.tsx +310 -0
- package/src/components/modals/save-form.scss +5 -0
- package/src/components/schema-editor/schema-editor.component.tsx +191 -0
- package/src/components/schema-editor/schema-editor.scss +26 -0
- package/src/config-schema.ts +47 -0
- package/src/constants.ts +3 -0
- package/src/declarations.d.tsx +2 -0
- package/src/form-builder-app-menu-link.component.tsx +13 -0
- package/src/forms.resource.ts +178 -0
- package/src/hooks/useClobdata.ts +20 -0
- package/src/hooks/useConceptLookup.ts +18 -0
- package/src/hooks/useConceptName.ts +18 -0
- package/src/hooks/useEncounterTypes.ts +18 -0
- package/src/hooks/useForm.ts +18 -0
- package/src/hooks/useForms.ts +20 -0
- package/src/index.ts +70 -0
- package/src/root.component.tsx +19 -0
- package/src/setup-tests.ts +11 -0
- package/src/test-helpers.tsx +37 -0
- package/src/types.ts +132 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import React, { useCallback, useState } from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
import { useParams } from "react-router-dom";
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
ComposedModal,
|
|
7
|
+
Form,
|
|
8
|
+
FormGroup,
|
|
9
|
+
InlineLoading,
|
|
10
|
+
ModalBody,
|
|
11
|
+
ModalFooter,
|
|
12
|
+
ModalHeader,
|
|
13
|
+
Select,
|
|
14
|
+
SelectItem,
|
|
15
|
+
Stack,
|
|
16
|
+
TextArea,
|
|
17
|
+
TextInput,
|
|
18
|
+
} from "@carbon/react";
|
|
19
|
+
import { showNotification, showToast } from "@openmrs/esm-framework";
|
|
20
|
+
import {
|
|
21
|
+
uploadSchema,
|
|
22
|
+
saveNewForm,
|
|
23
|
+
updateName,
|
|
24
|
+
updateVersion,
|
|
25
|
+
updateDescription,
|
|
26
|
+
getResourceUUID,
|
|
27
|
+
deleteClobData,
|
|
28
|
+
deleteResource,
|
|
29
|
+
updateEncounterType,
|
|
30
|
+
} from "../../forms.resource";
|
|
31
|
+
import { EncounterType, Resource, RouteParams, Schema } from "../../types";
|
|
32
|
+
import { useEncounterTypes } from "../../hooks/useEncounterTypes";
|
|
33
|
+
import styles from "./save-form.scss";
|
|
34
|
+
|
|
35
|
+
type FormGroupData = {
|
|
36
|
+
name: string;
|
|
37
|
+
uuid: string;
|
|
38
|
+
version: string;
|
|
39
|
+
encounterType: EncounterType;
|
|
40
|
+
description: string;
|
|
41
|
+
resources: Array<Resource>;
|
|
42
|
+
};
|
|
43
|
+
type SaveFormModalProps = { form: FormGroupData; schema: Schema };
|
|
44
|
+
|
|
45
|
+
const SaveForm: React.FC<SaveFormModalProps> = ({ form, schema }) => {
|
|
46
|
+
const { t } = useTranslation();
|
|
47
|
+
const { formUuid } = useParams<RouteParams>();
|
|
48
|
+
const isSavingNewForm = !formUuid;
|
|
49
|
+
const { encounterTypes } = useEncounterTypes();
|
|
50
|
+
const [openSaveFormModal, setOpenSaveFormModal] = useState(false);
|
|
51
|
+
const [openConfirmSaveModal, setOpenConfirmSaveModal] = useState(false);
|
|
52
|
+
const [saveState, setSaveState] = useState("");
|
|
53
|
+
const [isSavingForm, setIsSavingForm] = useState(false);
|
|
54
|
+
|
|
55
|
+
const openModal = useCallback((option) => {
|
|
56
|
+
if (option === "newVersion") {
|
|
57
|
+
setSaveState("newVersion");
|
|
58
|
+
setOpenConfirmSaveModal(false);
|
|
59
|
+
setOpenSaveFormModal(true);
|
|
60
|
+
} else if (option === "new") {
|
|
61
|
+
setSaveState("newVersion");
|
|
62
|
+
setOpenSaveFormModal(true);
|
|
63
|
+
} else if (option === "update") {
|
|
64
|
+
setSaveState("update");
|
|
65
|
+
setOpenConfirmSaveModal(false);
|
|
66
|
+
setOpenSaveFormModal(true);
|
|
67
|
+
}
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const handleSubmit = async (event) => {
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
setIsSavingForm(true);
|
|
73
|
+
let name = event.target.name.value,
|
|
74
|
+
version = event.target.version.value,
|
|
75
|
+
encounterType = event.target.encounterType.value,
|
|
76
|
+
description = event.target.description.value,
|
|
77
|
+
encounterTypeUUID;
|
|
78
|
+
|
|
79
|
+
if (encounterType == "undefined") {
|
|
80
|
+
encounterTypeUUID = undefined;
|
|
81
|
+
} else {
|
|
82
|
+
encounterTypes.forEach((encType) => {
|
|
83
|
+
if (encounterType == encType.name) {
|
|
84
|
+
encounterTypeUUID = encType.uuid;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (saveState === "new" || saveState === "newVersion") {
|
|
90
|
+
try {
|
|
91
|
+
const newForm = await saveNewForm(
|
|
92
|
+
name,
|
|
93
|
+
version,
|
|
94
|
+
false,
|
|
95
|
+
description,
|
|
96
|
+
encounterTypeUUID
|
|
97
|
+
);
|
|
98
|
+
const newValueReference = await uploadSchema(schema);
|
|
99
|
+
await getResourceUUID(newForm.uuid, newValueReference.toString());
|
|
100
|
+
showToast({
|
|
101
|
+
title: t("formCreated", "New form created"),
|
|
102
|
+
kind: "success",
|
|
103
|
+
critical: true,
|
|
104
|
+
description:
|
|
105
|
+
name +
|
|
106
|
+
" " +
|
|
107
|
+
t(
|
|
108
|
+
"saveSuccessMessage",
|
|
109
|
+
"was created successfully. It is now visible on the Forms dashboard."
|
|
110
|
+
),
|
|
111
|
+
});
|
|
112
|
+
setOpenSaveFormModal(false);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
showNotification({
|
|
115
|
+
title: t("errorCreatingForm", "Error creating form"),
|
|
116
|
+
kind: "error",
|
|
117
|
+
critical: true,
|
|
118
|
+
description: error?.message,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
try {
|
|
123
|
+
if (form?.resources.length != 0) {
|
|
124
|
+
deleteClobData(form?.resources[0].valueReference);
|
|
125
|
+
deleteResource(form?.uuid, form?.resources[0].uuid);
|
|
126
|
+
}
|
|
127
|
+
const newValueReference = await uploadSchema(schema);
|
|
128
|
+
await getResourceUUID(form?.uuid, newValueReference.toString());
|
|
129
|
+
|
|
130
|
+
if (name !== form?.name) {
|
|
131
|
+
await updateName(name, form?.uuid);
|
|
132
|
+
}
|
|
133
|
+
if (version !== form?.version) {
|
|
134
|
+
await updateVersion(version, form?.uuid);
|
|
135
|
+
}
|
|
136
|
+
if (encounterTypeUUID !== form?.encounterType?.uuid) {
|
|
137
|
+
await updateEncounterType(encounterTypeUUID, form?.uuid);
|
|
138
|
+
}
|
|
139
|
+
if (description !== form?.description) {
|
|
140
|
+
await updateDescription(description, form?.uuid);
|
|
141
|
+
}
|
|
142
|
+
showToast({
|
|
143
|
+
title: t("success", "Success!"),
|
|
144
|
+
kind: "success",
|
|
145
|
+
critical: true,
|
|
146
|
+
description:
|
|
147
|
+
name + " " + t("saveSuccess", "was updated successfully"),
|
|
148
|
+
});
|
|
149
|
+
setOpenSaveFormModal(false);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
showNotification({
|
|
152
|
+
title: t("errorUpdatingForm", "Error updating form"),
|
|
153
|
+
kind: "error",
|
|
154
|
+
critical: true,
|
|
155
|
+
description: error?.message,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div>
|
|
163
|
+
{!isSavingNewForm ? (
|
|
164
|
+
<ComposedModal
|
|
165
|
+
open={openConfirmSaveModal}
|
|
166
|
+
onClose={() => setOpenConfirmSaveModal(false)}
|
|
167
|
+
>
|
|
168
|
+
<ModalHeader title={t("saveConfirmation", "Save or Update form")} />
|
|
169
|
+
<ModalBody>
|
|
170
|
+
<p>
|
|
171
|
+
{t(
|
|
172
|
+
"saveAsModal",
|
|
173
|
+
"A version of the form you're working on already exists on the server. Do you want to update the form or to save it as a new version?"
|
|
174
|
+
)}
|
|
175
|
+
</p>
|
|
176
|
+
</ModalBody>
|
|
177
|
+
<ModalFooter>
|
|
178
|
+
<Button kind={"tertiary"} onClick={() => openModal("update")}>
|
|
179
|
+
{t("updateExistingForm", "Update existing version")}
|
|
180
|
+
</Button>
|
|
181
|
+
<Button kind={"primary"} onClick={() => openModal("newVersion")}>
|
|
182
|
+
{t("saveAsNewForm", "Save as a new")}
|
|
183
|
+
</Button>
|
|
184
|
+
<Button
|
|
185
|
+
kind={"secondary"}
|
|
186
|
+
onClick={() => setOpenConfirmSaveModal(false)}
|
|
187
|
+
>
|
|
188
|
+
{t("close", "Close")}
|
|
189
|
+
</Button>
|
|
190
|
+
</ModalFooter>
|
|
191
|
+
</ComposedModal>
|
|
192
|
+
) : null}
|
|
193
|
+
|
|
194
|
+
<ComposedModal
|
|
195
|
+
open={openSaveFormModal}
|
|
196
|
+
onClose={() => setOpenSaveFormModal(false)}
|
|
197
|
+
>
|
|
198
|
+
<ModalHeader
|
|
199
|
+
title={t("saveFormToServer", "Save form to server")}
|
|
200
|
+
></ModalHeader>
|
|
201
|
+
<Form onSubmit={handleSubmit}>
|
|
202
|
+
<ModalBody>
|
|
203
|
+
<p>
|
|
204
|
+
{t(
|
|
205
|
+
"saveExplainerText",
|
|
206
|
+
"Clicking the Save button saves your form schema to the database. To see your form in your frontend, you first need to publish it. Click the Publish button to publish your form."
|
|
207
|
+
)}
|
|
208
|
+
</p>
|
|
209
|
+
<FormGroup legendText={""}>
|
|
210
|
+
<Stack gap={5}>
|
|
211
|
+
<TextInput
|
|
212
|
+
id="name"
|
|
213
|
+
labelText={t("formName", "Form name")}
|
|
214
|
+
defaultValue={saveState === "update" ? form?.name : ""}
|
|
215
|
+
placeholder="e.g. OHRI Express Care Patient Encounter Form"
|
|
216
|
+
required
|
|
217
|
+
/>
|
|
218
|
+
{saveState === "update" ? (
|
|
219
|
+
<TextInput
|
|
220
|
+
id="uuid"
|
|
221
|
+
labelText="UUID (auto-generated)"
|
|
222
|
+
disabled
|
|
223
|
+
defaultValue={saveState === "update" ? form?.uuid : ""}
|
|
224
|
+
/>
|
|
225
|
+
) : null}
|
|
226
|
+
<TextInput
|
|
227
|
+
id="version"
|
|
228
|
+
labelText="Version"
|
|
229
|
+
defaultValue={saveState === "update" ? form?.version : ""}
|
|
230
|
+
placeholder="e.g. 1.0"
|
|
231
|
+
required
|
|
232
|
+
/>
|
|
233
|
+
<Select
|
|
234
|
+
id="encounterType"
|
|
235
|
+
defaultValue={
|
|
236
|
+
form?.encounterType
|
|
237
|
+
? form?.encounterType?.name
|
|
238
|
+
: "undefined"
|
|
239
|
+
}
|
|
240
|
+
labelText={t("encounterType", "Encounter Type")}
|
|
241
|
+
required
|
|
242
|
+
>
|
|
243
|
+
{!form?.encounterType ? (
|
|
244
|
+
<SelectItem
|
|
245
|
+
text={t(
|
|
246
|
+
"chooseEncounterType",
|
|
247
|
+
"Choose an encounter type to link your form to"
|
|
248
|
+
)}
|
|
249
|
+
/>
|
|
250
|
+
) : null}
|
|
251
|
+
{encounterTypes?.map((encounterType, key) => (
|
|
252
|
+
<SelectItem
|
|
253
|
+
key={key}
|
|
254
|
+
value={encounterType.name}
|
|
255
|
+
text={encounterType.name}
|
|
256
|
+
/>
|
|
257
|
+
))}
|
|
258
|
+
</Select>
|
|
259
|
+
<TextArea
|
|
260
|
+
labelText={t("description", "Description")}
|
|
261
|
+
defaultValue={saveState === "update" ? form?.description : ""}
|
|
262
|
+
cols={6}
|
|
263
|
+
rows={3}
|
|
264
|
+
id="description"
|
|
265
|
+
placeholder={t(
|
|
266
|
+
"descriptionPlaceholderText",
|
|
267
|
+
"e.g. A form used to collect encounter data for clients in the Express Care program."
|
|
268
|
+
)}
|
|
269
|
+
required
|
|
270
|
+
/>
|
|
271
|
+
</Stack>
|
|
272
|
+
</FormGroup>
|
|
273
|
+
</ModalBody>
|
|
274
|
+
<ModalFooter>
|
|
275
|
+
<Button
|
|
276
|
+
kind={"secondary"}
|
|
277
|
+
onClick={() => setOpenSaveFormModal(false)}
|
|
278
|
+
>
|
|
279
|
+
{t("close", "Close")}
|
|
280
|
+
</Button>
|
|
281
|
+
<Button
|
|
282
|
+
disabled={isSavingForm}
|
|
283
|
+
className={styles.spinner}
|
|
284
|
+
type={"submit"}
|
|
285
|
+
kind={"primary"}
|
|
286
|
+
>
|
|
287
|
+
{isSavingForm ? (
|
|
288
|
+
<InlineLoading description={t("saving", "Saving") + "..."} />
|
|
289
|
+
) : (
|
|
290
|
+
<span>{t("save", "Save")}</span>
|
|
291
|
+
)}
|
|
292
|
+
</Button>
|
|
293
|
+
</ModalFooter>
|
|
294
|
+
</Form>
|
|
295
|
+
</ComposedModal>
|
|
296
|
+
|
|
297
|
+
<Button
|
|
298
|
+
disabled={!schema}
|
|
299
|
+
kind="primary"
|
|
300
|
+
onClick={() =>
|
|
301
|
+
isSavingNewForm ? openModal("new") : setOpenConfirmSaveModal(true)
|
|
302
|
+
}
|
|
303
|
+
>
|
|
304
|
+
{t("saveForm", "Save form")}
|
|
305
|
+
</Button>
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
export default SaveForm;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { useParams } from "react-router-dom";
|
|
3
|
+
import AceEditor from "react-ace";
|
|
4
|
+
import "ace-builds/webpack-resolver";
|
|
5
|
+
import "ace-builds/src-noconflict/ext-language_tools";
|
|
6
|
+
import { Button, InlineLoading } from "@carbon/react";
|
|
7
|
+
import { useTranslation } from "react-i18next";
|
|
8
|
+
import { OHRIFormSchema } from "@ohri/openmrs-ohri-form-engine-lib";
|
|
9
|
+
import { RouteParams, Schema } from "../../types";
|
|
10
|
+
import styles from "./schema-editor.scss";
|
|
11
|
+
|
|
12
|
+
type SchemaEditorProps = {
|
|
13
|
+
isLoading: boolean;
|
|
14
|
+
onSchemaChange: (schema: Schema) => void;
|
|
15
|
+
schema: Schema;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const SchemaEditor: React.FC<SchemaEditorProps> = ({
|
|
19
|
+
isLoading,
|
|
20
|
+
onSchemaChange,
|
|
21
|
+
schema,
|
|
22
|
+
}) => {
|
|
23
|
+
const { t } = useTranslation();
|
|
24
|
+
const { formUuid } = useParams<RouteParams>();
|
|
25
|
+
const isNewSchema = !formUuid;
|
|
26
|
+
|
|
27
|
+
const [stringifiedSchema, setStringifiedSchema] = useState(
|
|
28
|
+
schema ? JSON.stringify(schema, null, 2) : ""
|
|
29
|
+
);
|
|
30
|
+
const [invalidJsonErrorMessage, setInvalidJsonErrorMessage] = useState("");
|
|
31
|
+
const [isRendering, setIsRendering] = useState(false);
|
|
32
|
+
|
|
33
|
+
const resetErrorMessage = useCallback(() => {
|
|
34
|
+
setInvalidJsonErrorMessage("");
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
const handleSchemaChange = (updatedSchema: string) => {
|
|
38
|
+
setStringifiedSchema(updatedSchema);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const inputDummySchema = useCallback(() => {
|
|
42
|
+
const dummySchema: OHRIFormSchema = {
|
|
43
|
+
encounterType: "",
|
|
44
|
+
name: "Sample Form",
|
|
45
|
+
pages: [
|
|
46
|
+
{
|
|
47
|
+
label: "First Page",
|
|
48
|
+
sections: [
|
|
49
|
+
{
|
|
50
|
+
label: "A Section",
|
|
51
|
+
isExpanded: "true",
|
|
52
|
+
questions: [
|
|
53
|
+
{
|
|
54
|
+
label: "A Question of type obs that renders a text input",
|
|
55
|
+
type: "obs",
|
|
56
|
+
questionOptions: {
|
|
57
|
+
rendering: "text",
|
|
58
|
+
concept: "a-system-defined-concept-uuid",
|
|
59
|
+
},
|
|
60
|
+
id: "sampleQuestion",
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
label: "Another Section",
|
|
66
|
+
isExpanded: "true",
|
|
67
|
+
questions: [
|
|
68
|
+
{
|
|
69
|
+
label:
|
|
70
|
+
"Another Question of type obs whose answers get rendered as radio inputs",
|
|
71
|
+
type: "obs",
|
|
72
|
+
questionOptions: {
|
|
73
|
+
rendering: "radio",
|
|
74
|
+
concept: "system-defined-concept-uuid",
|
|
75
|
+
answers: [
|
|
76
|
+
{
|
|
77
|
+
concept: "another-system-defined-concept-uuid",
|
|
78
|
+
label: "Choice 1",
|
|
79
|
+
conceptMappings: [],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
concept: "yet-another-system-defined-concept-uuid",
|
|
83
|
+
label: "Choice 2",
|
|
84
|
+
conceptMappings: [],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
concept: "yet-one-more-system-defined-concept-uuid",
|
|
88
|
+
label: "Choice 3",
|
|
89
|
+
conceptMappings: [],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
id: "anotherSampleQuestion",
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
processor: "EncounterFormProcessor",
|
|
101
|
+
referencedForms: [],
|
|
102
|
+
uuid: "xxx",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
setStringifiedSchema(JSON.stringify(dummySchema, null, 2));
|
|
106
|
+
onSchemaChange({ ...dummySchema });
|
|
107
|
+
}, [onSchemaChange]);
|
|
108
|
+
|
|
109
|
+
const renderSchemaChanges = useCallback(() => {
|
|
110
|
+
setIsRendering(true);
|
|
111
|
+
resetErrorMessage();
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const parsedJson: Schema = JSON.parse(stringifiedSchema);
|
|
115
|
+
onSchemaChange(parsedJson);
|
|
116
|
+
setStringifiedSchema(JSON.stringify(parsedJson, null, 2));
|
|
117
|
+
} catch (error) {
|
|
118
|
+
setInvalidJsonErrorMessage(error.message);
|
|
119
|
+
}
|
|
120
|
+
setIsRendering(false);
|
|
121
|
+
}, [stringifiedSchema, onSchemaChange, resetErrorMessage]);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
setStringifiedSchema(JSON.stringify(schema, null, 2));
|
|
125
|
+
}, [schema]);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<>
|
|
129
|
+
<div className={styles.actionButtons}>
|
|
130
|
+
{isLoading ? (
|
|
131
|
+
<InlineLoading
|
|
132
|
+
description={t("loadingSchema", "Loading schema") + "..."}
|
|
133
|
+
/>
|
|
134
|
+
) : null}
|
|
135
|
+
|
|
136
|
+
{isNewSchema ? (
|
|
137
|
+
<Button kind="secondary" onClick={inputDummySchema}>
|
|
138
|
+
{t("inputDummySchema", "Input dummy schema")}
|
|
139
|
+
</Button>
|
|
140
|
+
) : null}
|
|
141
|
+
|
|
142
|
+
<Button
|
|
143
|
+
disabled={isRendering}
|
|
144
|
+
kind="primary"
|
|
145
|
+
onClick={renderSchemaChanges}
|
|
146
|
+
>
|
|
147
|
+
{isRendering ? (
|
|
148
|
+
<InlineLoading
|
|
149
|
+
className={styles.spinner}
|
|
150
|
+
description={t("rendering", "Rendering") + "..."}
|
|
151
|
+
/>
|
|
152
|
+
) : (
|
|
153
|
+
<span>{t("renderChanges", "Render changes")}</span>
|
|
154
|
+
)}
|
|
155
|
+
</Button>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{invalidJsonErrorMessage ? (
|
|
159
|
+
<div className={styles.errorContainer}>
|
|
160
|
+
<p className={styles.heading}>
|
|
161
|
+
{t("schemaError", "There's an error in your schema.")}
|
|
162
|
+
</p>
|
|
163
|
+
<p>{invalidJsonErrorMessage}</p>
|
|
164
|
+
</div>
|
|
165
|
+
) : null}
|
|
166
|
+
|
|
167
|
+
<AceEditor
|
|
168
|
+
style={{ height: "100vh", width: "100%" }}
|
|
169
|
+
mode="json"
|
|
170
|
+
theme="textmate"
|
|
171
|
+
name="schemaEditor"
|
|
172
|
+
onChange={handleSchemaChange}
|
|
173
|
+
fontSize={15}
|
|
174
|
+
showPrintMargin={false}
|
|
175
|
+
showGutter={true}
|
|
176
|
+
highlightActiveLine={true}
|
|
177
|
+
value={stringifiedSchema}
|
|
178
|
+
setOptions={{
|
|
179
|
+
enableBasicAutocompletion: false,
|
|
180
|
+
enableLiveAutocompletion: false,
|
|
181
|
+
displayIndentGuides: true,
|
|
182
|
+
enableSnippets: false,
|
|
183
|
+
showLineNumbers: true,
|
|
184
|
+
tabSize: 2,
|
|
185
|
+
}}
|
|
186
|
+
/>
|
|
187
|
+
</>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export default SchemaEditor;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/colors';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
|
|
4
|
+
.actionButtons {
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: flex-end;
|
|
8
|
+
margin: 1rem 0;
|
|
9
|
+
|
|
10
|
+
button {
|
|
11
|
+
margin-left: 1rem
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.errorContainer {
|
|
16
|
+
@include type.type-style("body-compact-02");
|
|
17
|
+
background-color: colors.$red-20;
|
|
18
|
+
color: colors.$red-70;
|
|
19
|
+
padding: 1.5rem;
|
|
20
|
+
margin: 1rem 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.heading {
|
|
24
|
+
@include type.type-style('heading-compact-02');
|
|
25
|
+
margin-bottom: 1rem;
|
|
26
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Type } from "@openmrs/esm-framework";
|
|
2
|
+
|
|
3
|
+
export const configSchema = {
|
|
4
|
+
questionTypes: {
|
|
5
|
+
_type: Type.Array,
|
|
6
|
+
_description:
|
|
7
|
+
"Provides information that the processor uses to render a field",
|
|
8
|
+
_default: [
|
|
9
|
+
"complex-obs",
|
|
10
|
+
"control",
|
|
11
|
+
"encounterDatetime",
|
|
12
|
+
"encounterLocation",
|
|
13
|
+
"encounterProvider",
|
|
14
|
+
"obs",
|
|
15
|
+
"obsGroup",
|
|
16
|
+
"personAttribute",
|
|
17
|
+
"testOrder",
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
fieldTypes: {
|
|
21
|
+
_type: Type.Array,
|
|
22
|
+
_description:
|
|
23
|
+
"An array of available field types. A question can have only one field type, and the field type determines how the question is rendered.",
|
|
24
|
+
_default: [
|
|
25
|
+
"date",
|
|
26
|
+
"drug",
|
|
27
|
+
"field-set",
|
|
28
|
+
"file",
|
|
29
|
+
"group",
|
|
30
|
+
"multiCheckbox",
|
|
31
|
+
"number",
|
|
32
|
+
"problem",
|
|
33
|
+
"radio",
|
|
34
|
+
"repeating",
|
|
35
|
+
"select",
|
|
36
|
+
"text",
|
|
37
|
+
"textarea",
|
|
38
|
+
"ui-select-extended",
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
patientUuid: {
|
|
42
|
+
_type: "String",
|
|
43
|
+
_default: "88f1032f-adae-4ef2-9025-2c40b71dd897",
|
|
44
|
+
_description:
|
|
45
|
+
"UUID of the test patient whose information gets rendered in a patient banner within the form renderer",
|
|
46
|
+
},
|
|
47
|
+
};
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ConfigurableLink } from "@openmrs/esm-framework";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
import { spaBasePath } from "./constants";
|
|
5
|
+
|
|
6
|
+
export default function FormBuilderAppMenuLink() {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
return (
|
|
9
|
+
<ConfigurableLink to={spaBasePath}>
|
|
10
|
+
{t("formBuilder", "Form Builder")}
|
|
11
|
+
</ConfigurableLink>
|
|
12
|
+
);
|
|
13
|
+
}
|