@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,100 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/type';
|
|
2
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
3
|
+
|
|
4
|
+
.container {
|
|
5
|
+
:global(.cds--modal-content:focus) {
|
|
6
|
+
outline: none;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
:global(.cds--tooltip) {
|
|
10
|
+
z-index: 1;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.helperText {
|
|
15
|
+
@include type.type-style('body-compact-01');
|
|
16
|
+
margin: 1rem 0rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.heading {
|
|
20
|
+
@include type.type-style('heading-03');
|
|
21
|
+
margin: 1rem 0rem 0.5rem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.styledHeading {
|
|
25
|
+
@include type.type-style('heading-02');
|
|
26
|
+
margin: 0.5rem 0rem;
|
|
27
|
+
|
|
28
|
+
&:after {
|
|
29
|
+
content: "";
|
|
30
|
+
display: block;
|
|
31
|
+
width: 2rem;
|
|
32
|
+
padding-top: 0.188rem;
|
|
33
|
+
border-bottom: 0.375rem solid var(--brand-03);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.subheading {
|
|
38
|
+
@extend .heading;
|
|
39
|
+
@include type.type-style('heading-02');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.name {
|
|
43
|
+
@extend .heading;
|
|
44
|
+
@include type.type-style('heading-02');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.editableFieldsContainer {
|
|
48
|
+
margin: 1rem 0;
|
|
49
|
+
border-bottom: 1px solid $ui-03;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.addQuestionButton {
|
|
53
|
+
margin: 0.75rem 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.header {
|
|
57
|
+
margin: 2rem 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.explainer {
|
|
61
|
+
margin-bottom: 2rem;
|
|
62
|
+
|
|
63
|
+
p {
|
|
64
|
+
margin: 1.5rem 0;
|
|
65
|
+
|
|
66
|
+
&:first-of-type {
|
|
67
|
+
@include type.type-style('heading-04');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.sectionExplainer {
|
|
73
|
+
margin: 1.5rem 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.startButton {
|
|
77
|
+
margin: 1rem 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.editorContainer {
|
|
81
|
+
display: flex;
|
|
82
|
+
justify-content: space-between;
|
|
83
|
+
margin: 0.25rem 0.5rem;
|
|
84
|
+
align-items: center;
|
|
85
|
+
width: 100%;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.buttonContainer {
|
|
89
|
+
span {
|
|
90
|
+
margin: 0rem 0.5rem;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.questionLabel {
|
|
95
|
+
@include type.type-style('body-01');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.addSectionButton {
|
|
99
|
+
margin: 1.5rem 0;
|
|
100
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
ComposedModal,
|
|
6
|
+
Form,
|
|
7
|
+
FormGroup,
|
|
8
|
+
ModalBody,
|
|
9
|
+
ModalFooter,
|
|
10
|
+
ModalHeader,
|
|
11
|
+
TextInput,
|
|
12
|
+
} from "@carbon/react";
|
|
13
|
+
import { showToast, showNotification } from "@openmrs/esm-framework";
|
|
14
|
+
import { Schema } from "../../types";
|
|
15
|
+
|
|
16
|
+
type NewFormModalProps = {
|
|
17
|
+
schema: Schema;
|
|
18
|
+
onSchemaChange: (schema: Schema) => void;
|
|
19
|
+
showModal: boolean;
|
|
20
|
+
onModalChange: (showModal: boolean) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const NewFormModal: React.FC<NewFormModalProps> = ({
|
|
24
|
+
schema,
|
|
25
|
+
onSchemaChange,
|
|
26
|
+
showModal,
|
|
27
|
+
onModalChange,
|
|
28
|
+
}) => {
|
|
29
|
+
const { t } = useTranslation();
|
|
30
|
+
const [formName, setFormName] = useState("");
|
|
31
|
+
|
|
32
|
+
const updateFormName = () => {
|
|
33
|
+
try {
|
|
34
|
+
schema.name = formName;
|
|
35
|
+
onSchemaChange({ ...schema });
|
|
36
|
+
|
|
37
|
+
showToast({
|
|
38
|
+
title: t("success", "Success!"),
|
|
39
|
+
kind: "success",
|
|
40
|
+
critical: true,
|
|
41
|
+
description: t("formCreated", "New form created"),
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
showNotification({
|
|
45
|
+
title: t("errorCreatingForm", "Error creating form"),
|
|
46
|
+
kind: "error",
|
|
47
|
+
critical: true,
|
|
48
|
+
description: error?.message,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<ComposedModal open={showModal} onClose={() => onModalChange(false)}>
|
|
55
|
+
<ModalHeader title={t("createNewForm", "Create a new form")} />
|
|
56
|
+
<Form onSubmit={(event) => event.preventDefault()}>
|
|
57
|
+
<ModalBody>
|
|
58
|
+
<FormGroup legendText={""}>
|
|
59
|
+
<TextInput
|
|
60
|
+
id="formName"
|
|
61
|
+
labelText={t("formName", "Form name")}
|
|
62
|
+
value={formName}
|
|
63
|
+
onChange={(event) => setFormName(event.target.value)}
|
|
64
|
+
/>
|
|
65
|
+
</FormGroup>
|
|
66
|
+
</ModalBody>
|
|
67
|
+
</Form>
|
|
68
|
+
<ModalFooter>
|
|
69
|
+
<Button kind="secondary" onClick={() => onModalChange(false)}>
|
|
70
|
+
{t("cancel", "Cancel")}
|
|
71
|
+
</Button>
|
|
72
|
+
<Button
|
|
73
|
+
disabled={!formName}
|
|
74
|
+
onClick={() => {
|
|
75
|
+
updateFormName();
|
|
76
|
+
onModalChange(false);
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
<span>{t("createForm", "Create Form")}</span>
|
|
80
|
+
</Button>
|
|
81
|
+
</ModalFooter>
|
|
82
|
+
</ComposedModal>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default NewFormModal;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
ComposedModal,
|
|
6
|
+
Form,
|
|
7
|
+
FormGroup,
|
|
8
|
+
ModalBody,
|
|
9
|
+
ModalFooter,
|
|
10
|
+
ModalHeader,
|
|
11
|
+
TextInput,
|
|
12
|
+
} from "@carbon/react";
|
|
13
|
+
import { showToast, showNotification } from "@openmrs/esm-framework";
|
|
14
|
+
import { Schema } from "../../types";
|
|
15
|
+
|
|
16
|
+
type PageModalProps = {
|
|
17
|
+
schema: Schema;
|
|
18
|
+
onSchemaChange: (schema: Schema) => void;
|
|
19
|
+
showModal: boolean;
|
|
20
|
+
onModalChange: (showModal: boolean) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const PageModal: React.FC<PageModalProps> = ({
|
|
24
|
+
schema,
|
|
25
|
+
onSchemaChange,
|
|
26
|
+
showModal,
|
|
27
|
+
onModalChange,
|
|
28
|
+
}) => {
|
|
29
|
+
const { t } = useTranslation();
|
|
30
|
+
const [pageTitle, setPageTitle] = useState("");
|
|
31
|
+
|
|
32
|
+
const handleUpdatePageTitle = () => {
|
|
33
|
+
updatePages();
|
|
34
|
+
onModalChange(false);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const updatePages = () => {
|
|
38
|
+
try {
|
|
39
|
+
if (pageTitle) {
|
|
40
|
+
schema.pages.push({
|
|
41
|
+
label: pageTitle,
|
|
42
|
+
sections: [],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
onSchemaChange({ ...schema });
|
|
46
|
+
setPageTitle("");
|
|
47
|
+
}
|
|
48
|
+
showToast({
|
|
49
|
+
title: t("success", "Success!"),
|
|
50
|
+
kind: "success",
|
|
51
|
+
critical: true,
|
|
52
|
+
description: t("pageCreated", "New page created"),
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
showNotification({
|
|
56
|
+
title: t("errorCreatingPage", "Error creating page"),
|
|
57
|
+
kind: "error",
|
|
58
|
+
critical: true,
|
|
59
|
+
description: error?.message,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<ComposedModal open={showModal} onClose={() => onModalChange(false)}>
|
|
66
|
+
<ModalHeader title={t("createNewPage", "Create a new page")} />
|
|
67
|
+
<Form onSubmit={(event) => event.preventDefault()}>
|
|
68
|
+
<ModalBody>
|
|
69
|
+
<FormGroup legendText={""}>
|
|
70
|
+
<TextInput
|
|
71
|
+
id="pageTitle"
|
|
72
|
+
labelText={t("enterPageTitle", "Enter a title for your new page")}
|
|
73
|
+
value={pageTitle}
|
|
74
|
+
onChange={(event) => setPageTitle(event.target.value)}
|
|
75
|
+
/>
|
|
76
|
+
</FormGroup>
|
|
77
|
+
</ModalBody>
|
|
78
|
+
</Form>
|
|
79
|
+
<ModalFooter>
|
|
80
|
+
<Button onClick={() => onModalChange(false)} kind="secondary">
|
|
81
|
+
{t("cancel", "Cancel")}
|
|
82
|
+
</Button>
|
|
83
|
+
<Button disabled={!pageTitle} onClick={handleUpdatePageTitle}>
|
|
84
|
+
<span>{t("save", "Save")}</span>
|
|
85
|
+
</Button>
|
|
86
|
+
</ModalFooter>
|
|
87
|
+
</ComposedModal>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default PageModal;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/type';
|
|
2
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
3
|
+
|
|
4
|
+
.label {
|
|
5
|
+
margin-bottom: 0.5rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.loader {
|
|
9
|
+
padding: 1rem 0.5rem;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.tag {
|
|
13
|
+
margin-right: 0.5rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.conceptList {
|
|
17
|
+
max-height: 14rem;
|
|
18
|
+
overflow-y: auto;
|
|
19
|
+
border: 1px solid $ui-03;
|
|
20
|
+
border-top: none;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.conceptList li:hover {
|
|
24
|
+
background-color: $ui-03;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.concept {
|
|
28
|
+
padding: 0.75rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.emptyResults {
|
|
32
|
+
@include type.type-style("body-compact-01");
|
|
33
|
+
color: $text-02;
|
|
34
|
+
min-height: 1rem;
|
|
35
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
ComposedModal,
|
|
6
|
+
Form,
|
|
7
|
+
FormGroup,
|
|
8
|
+
ModalBody,
|
|
9
|
+
ModalFooter,
|
|
10
|
+
ModalHeader,
|
|
11
|
+
TextInput,
|
|
12
|
+
} from "@carbon/react";
|
|
13
|
+
import { showToast, showNotification } from "@openmrs/esm-framework";
|
|
14
|
+
import { Schema } from "../../types";
|
|
15
|
+
|
|
16
|
+
type SectionModalProps = {
|
|
17
|
+
schema: Schema;
|
|
18
|
+
onSchemaChange: (schema: Schema) => void;
|
|
19
|
+
pageIndex: number;
|
|
20
|
+
resetIndices: () => void;
|
|
21
|
+
showModal: boolean;
|
|
22
|
+
onModalChange: (showModal: boolean) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const SectionModal: React.FC<SectionModalProps> = ({
|
|
26
|
+
schema,
|
|
27
|
+
onSchemaChange,
|
|
28
|
+
pageIndex,
|
|
29
|
+
resetIndices,
|
|
30
|
+
showModal,
|
|
31
|
+
onModalChange,
|
|
32
|
+
}) => {
|
|
33
|
+
const { t } = useTranslation();
|
|
34
|
+
const [sectionTitle, setSectionTitle] = useState("");
|
|
35
|
+
|
|
36
|
+
const handleUpdatePageSections = () => {
|
|
37
|
+
updateSections();
|
|
38
|
+
onModalChange(false);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const updateSections = () => {
|
|
42
|
+
try {
|
|
43
|
+
schema.pages[pageIndex]?.sections?.push({
|
|
44
|
+
label: sectionTitle,
|
|
45
|
+
isExpanded: "true",
|
|
46
|
+
questions: [],
|
|
47
|
+
});
|
|
48
|
+
onSchemaChange({ ...schema });
|
|
49
|
+
setSectionTitle("");
|
|
50
|
+
resetIndices();
|
|
51
|
+
|
|
52
|
+
showToast({
|
|
53
|
+
title: t("success", "Success!"),
|
|
54
|
+
kind: "success",
|
|
55
|
+
critical: true,
|
|
56
|
+
description: t("sectionCreated", "New section created"),
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
showNotification({
|
|
60
|
+
title: t("errorCreatingSection", "Error creating section"),
|
|
61
|
+
kind: "error",
|
|
62
|
+
critical: true,
|
|
63
|
+
description: error?.message,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<ComposedModal open={showModal} onClose={() => onModalChange(false)}>
|
|
70
|
+
<ModalHeader title={t("createNewSection", "Create a new section")} />
|
|
71
|
+
<Form onSubmit={(event) => event.preventDefault()}>
|
|
72
|
+
<ModalBody>
|
|
73
|
+
<FormGroup legendText={""}>
|
|
74
|
+
<TextInput
|
|
75
|
+
id="sectionTitle"
|
|
76
|
+
labelText={t("enterSectionTitle", "Enter a section title")}
|
|
77
|
+
value={sectionTitle}
|
|
78
|
+
onChange={(event) => setSectionTitle(event.target.value)}
|
|
79
|
+
/>
|
|
80
|
+
</FormGroup>
|
|
81
|
+
</ModalBody>
|
|
82
|
+
</Form>
|
|
83
|
+
<ModalFooter>
|
|
84
|
+
<Button onClick={() => onModalChange(false)} kind="secondary">
|
|
85
|
+
{t("cancel", "Cancel")}
|
|
86
|
+
</Button>
|
|
87
|
+
<Button disabled={!sectionTitle} onClick={handleUpdatePageSections}>
|
|
88
|
+
<span>{t("save", "Save")}</span>
|
|
89
|
+
</Button>
|
|
90
|
+
</ModalFooter>
|
|
91
|
+
</ComposedModal>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
export default SectionModal;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
import { Button, TextInput } from "@carbon/react";
|
|
4
|
+
import { Close, Save } from "@carbon/react/icons";
|
|
5
|
+
import styles from "./value-editor.scss";
|
|
6
|
+
|
|
7
|
+
type ValueEditorProps = {
|
|
8
|
+
id: string;
|
|
9
|
+
handleCancel: () => void;
|
|
10
|
+
handleSave: (value: string) => void;
|
|
11
|
+
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
12
|
+
value: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const ValueEditor: React.FC<ValueEditorProps> = ({
|
|
16
|
+
id,
|
|
17
|
+
handleCancel,
|
|
18
|
+
handleSave,
|
|
19
|
+
onChange,
|
|
20
|
+
value,
|
|
21
|
+
}) => {
|
|
22
|
+
const { t } = useTranslation();
|
|
23
|
+
const [tmpValue, setTmpValue] = useState(value);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<TextInput
|
|
28
|
+
id={id}
|
|
29
|
+
labelText=""
|
|
30
|
+
value={tmpValue}
|
|
31
|
+
onChange={(event) => setTmpValue(event.target.value)}
|
|
32
|
+
/>
|
|
33
|
+
<div className={styles.actionButtons}>
|
|
34
|
+
<Button
|
|
35
|
+
size="md"
|
|
36
|
+
renderIcon={(props) => <Save {...props} size={16} />}
|
|
37
|
+
kind="primary"
|
|
38
|
+
onClick={() => handleSave(tmpValue)}
|
|
39
|
+
>
|
|
40
|
+
{t("saveButtonText", "Save")}
|
|
41
|
+
</Button>
|
|
42
|
+
<Button
|
|
43
|
+
size="md"
|
|
44
|
+
renderIcon={(props) => <Close {...props} size={16} />}
|
|
45
|
+
kind="secondary"
|
|
46
|
+
onClick={handleCancel}
|
|
47
|
+
>
|
|
48
|
+
{t("cancelButtonText", "Cancel")}
|
|
49
|
+
</Button>
|
|
50
|
+
</div>
|
|
51
|
+
</>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default ValueEditor;
|