@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,208 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { navigate, openmrsFetch } from "@openmrs/esm-framework";
|
|
5
|
+
import { renderWithSwr, waitForLoadingToFinish } from "../../test-helpers";
|
|
6
|
+
import Dashboard from "./dashboard.component";
|
|
7
|
+
|
|
8
|
+
const mockedOpenmrsFetch = openmrsFetch as jest.Mock;
|
|
9
|
+
|
|
10
|
+
const formsResponse = [
|
|
11
|
+
{
|
|
12
|
+
uuid: "2ddde996-b1c3-37f1-a53e-378dd1a4f6b5",
|
|
13
|
+
name: "Test Form 1",
|
|
14
|
+
encounterType: {
|
|
15
|
+
uuid: "dd528487-82a5-4082-9c72-ed246bd49591",
|
|
16
|
+
name: "Consultation",
|
|
17
|
+
},
|
|
18
|
+
version: "1",
|
|
19
|
+
published: true,
|
|
20
|
+
retired: false,
|
|
21
|
+
resources: [
|
|
22
|
+
{
|
|
23
|
+
uuid: "ea27fd4f-7a4d-4869-8855-5b890c8fed56",
|
|
24
|
+
name: "JSON schema",
|
|
25
|
+
dataType: "AmpathJsonSchema",
|
|
26
|
+
valueReference: "511efba8-f08f-4544-a6da-6a6fa2497b9e",
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
describe("Dashboard", () => {
|
|
33
|
+
it("renders an empty state view if no forms are available", async () => {
|
|
34
|
+
mockedOpenmrsFetch.mockReturnValueOnce({ data: { results: [] } });
|
|
35
|
+
|
|
36
|
+
renderDashboard();
|
|
37
|
+
|
|
38
|
+
await waitForLoadingToFinish();
|
|
39
|
+
|
|
40
|
+
expect(
|
|
41
|
+
screen.getByRole("heading", { name: /form builder/i })
|
|
42
|
+
).toBeInTheDocument();
|
|
43
|
+
expect(screen.getByRole("heading", { name: /forms/i })).toBeInTheDocument();
|
|
44
|
+
expect(screen.getByTitle(/empty data illustration/i)).toBeInTheDocument();
|
|
45
|
+
expect(
|
|
46
|
+
screen.getByText(/there are no forms to display/i)
|
|
47
|
+
).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByText(/create a new form/i)).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("renders a list of forms fetched from the server", async () => {
|
|
52
|
+
mockedOpenmrsFetch.mockReturnValueOnce({
|
|
53
|
+
data: {
|
|
54
|
+
results: formsResponse,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
renderDashboard();
|
|
59
|
+
|
|
60
|
+
await waitForLoadingToFinish();
|
|
61
|
+
|
|
62
|
+
expect(
|
|
63
|
+
screen.getByRole("heading", { name: /form builder/i })
|
|
64
|
+
).toBeInTheDocument();
|
|
65
|
+
expect(
|
|
66
|
+
screen.getByRole("button", { name: /filter by publish status/i })
|
|
67
|
+
).toBeInTheDocument();
|
|
68
|
+
expect(
|
|
69
|
+
screen.getByRole("button", { name: /create a new form/i })
|
|
70
|
+
).toBeInTheDocument();
|
|
71
|
+
expect(
|
|
72
|
+
screen.getByRole("button", { name: /edit schema/i })
|
|
73
|
+
).toBeInTheDocument();
|
|
74
|
+
expect(
|
|
75
|
+
screen.getByRole("button", { name: /download schema/i })
|
|
76
|
+
).toBeInTheDocument();
|
|
77
|
+
expect(
|
|
78
|
+
screen.getByRole("search", { name: /filter table/i })
|
|
79
|
+
).toBeInTheDocument();
|
|
80
|
+
expect(screen.getByRole("table")).toBeInTheDocument();
|
|
81
|
+
expect(screen.getByText(/Test Form 1/i)).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("searching for a form by name filters the list of forms", async () => {
|
|
85
|
+
const user = userEvent.setup();
|
|
86
|
+
|
|
87
|
+
mockedOpenmrsFetch.mockReturnValueOnce({
|
|
88
|
+
data: {
|
|
89
|
+
results: formsResponse,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
renderDashboard();
|
|
94
|
+
|
|
95
|
+
await waitForLoadingToFinish();
|
|
96
|
+
|
|
97
|
+
expect(screen.getByText(/Test Form 1/i)).toBeInTheDocument();
|
|
98
|
+
|
|
99
|
+
const searchbox = screen.getByRole("searchbox");
|
|
100
|
+
|
|
101
|
+
await user.type(searchbox, "COVID");
|
|
102
|
+
|
|
103
|
+
expect(screen.queryByText(/Test Form 1/i)).not.toBeInTheDocument();
|
|
104
|
+
expect(
|
|
105
|
+
screen.getByText(/no matching forms to display/i)
|
|
106
|
+
).toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('filters the list of forms by "published" status', async () => {
|
|
110
|
+
const user = userEvent.setup();
|
|
111
|
+
|
|
112
|
+
mockedOpenmrsFetch.mockReturnValueOnce({
|
|
113
|
+
data: {
|
|
114
|
+
results: formsResponse,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
renderDashboard();
|
|
119
|
+
|
|
120
|
+
await waitForLoadingToFinish();
|
|
121
|
+
|
|
122
|
+
const publishStatusFilter = screen.getByRole("button", {
|
|
123
|
+
name: /filter by publish status/i,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await user.click(publishStatusFilter);
|
|
127
|
+
await user.click(screen.getByRole("option", { name: /unpublished/i }));
|
|
128
|
+
|
|
129
|
+
expect(screen.queryByText(/Test Form 1/i)).not.toBeInTheDocument();
|
|
130
|
+
expect(
|
|
131
|
+
screen.getByText(/no matching forms to display/i)
|
|
132
|
+
).toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('clicking on "create a new form" button navigates to the "create form" page', async () => {
|
|
136
|
+
const user = userEvent.setup();
|
|
137
|
+
|
|
138
|
+
mockedOpenmrsFetch.mockReturnValueOnce({
|
|
139
|
+
data: {
|
|
140
|
+
results: formsResponse,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
renderDashboard();
|
|
145
|
+
|
|
146
|
+
await waitForLoadingToFinish();
|
|
147
|
+
|
|
148
|
+
const createFormButton = screen.getByRole("button", {
|
|
149
|
+
name: /create a new form/i,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await user.click(createFormButton);
|
|
153
|
+
|
|
154
|
+
expect(navigate).toHaveBeenCalledWith({
|
|
155
|
+
to: expect.stringMatching(/form\-builder\/new/),
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('clicking on "edit schema" button navigates to the "edit schema" page', async () => {
|
|
160
|
+
const user = userEvent.setup();
|
|
161
|
+
|
|
162
|
+
mockedOpenmrsFetch.mockReturnValueOnce({
|
|
163
|
+
data: {
|
|
164
|
+
results: formsResponse,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
renderDashboard();
|
|
169
|
+
|
|
170
|
+
await waitForLoadingToFinish();
|
|
171
|
+
|
|
172
|
+
const editSchemaButton = screen.getByRole("button", {
|
|
173
|
+
name: /edit schema/i,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await user.click(editSchemaButton);
|
|
177
|
+
|
|
178
|
+
expect(navigate).toHaveBeenCalledWith({
|
|
179
|
+
to: expect.stringMatching(/form\-builder\/edit/),
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('clicking on "download schema" button downloads the schema', async () => {
|
|
184
|
+
const user = userEvent.setup();
|
|
185
|
+
|
|
186
|
+
mockedOpenmrsFetch.mockReturnValueOnce({
|
|
187
|
+
data: {
|
|
188
|
+
results: formsResponse,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
renderDashboard();
|
|
193
|
+
|
|
194
|
+
await waitForLoadingToFinish();
|
|
195
|
+
|
|
196
|
+
const downloadSchemaButton = screen.getByRole("button", {
|
|
197
|
+
name: /download schema/i,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await user.click(downloadSchemaButton);
|
|
201
|
+
|
|
202
|
+
expect(window.URL.createObjectURL).toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
function renderDashboard() {
|
|
207
|
+
renderWithSwr(<Dashboard />);
|
|
208
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export const EmptyDataIllustration = ({ width = "64", height = "64" }) => {
|
|
4
|
+
return (
|
|
5
|
+
<svg width={width} height={height} viewBox="0 0 64 64">
|
|
6
|
+
<title>Empty data illustration</title>
|
|
7
|
+
<g fill="none" fillRule="evenodd">
|
|
8
|
+
<path
|
|
9
|
+
d="M38.133 13.186H21.947c-.768.001-1.39.623-1.39 1.391V50.55l-.186.057-3.97 1.216a.743.743 0 01-.927-.493L3.664 12.751a.742.742 0 01.492-.926l6.118-1.874 17.738-5.43 6.119-1.873a.741.741 0 01.926.492L38.076 13l.057.186z"
|
|
10
|
+
fill="#F4F4F4"
|
|
11
|
+
/>
|
|
12
|
+
<path
|
|
13
|
+
d="M41.664 13L38.026 1.117A1.576 1.576 0 0036.056.07l-8.601 2.633-17.737 5.43-8.603 2.634a1.578 1.578 0 00-1.046 1.97l12.436 40.616a1.58 1.58 0 001.969 1.046l5.897-1.805.185-.057v-.194l-.185.057-5.952 1.822a1.393 1.393 0 01-1.737-.923L.247 12.682a1.39 1.39 0 01.923-1.738L9.772 8.31 27.51 2.881 36.112.247a1.393 1.393 0 011.737.923L41.47 13l.057.186h.193l-.057-.185z"
|
|
14
|
+
fill="#8D8D8D"
|
|
15
|
+
/>
|
|
16
|
+
<path
|
|
17
|
+
d="M11.378 11.855a.836.836 0 01-.798-.59L9.385 7.361a.835.835 0 01.554-1.042l16.318-4.996a.836.836 0 011.042.554l1.195 3.902a.836.836 0 01-.554 1.043l-16.318 4.995a.831.831 0 01-.244.037z"
|
|
18
|
+
fill="#C6C6C6"
|
|
19
|
+
/>
|
|
20
|
+
<circle fill="#C6C6C6" cx={17.636} cy={2.314} r={1.855} />
|
|
21
|
+
<circle
|
|
22
|
+
fill="#FFF"
|
|
23
|
+
fillRule="nonzero"
|
|
24
|
+
cx={17.636}
|
|
25
|
+
cy={2.314}
|
|
26
|
+
r={1.175}
|
|
27
|
+
/>
|
|
28
|
+
<path
|
|
29
|
+
d="M55.893 53.995H24.544a.79.79 0 01-.788-.789V15.644a.79.79 0 01.788-.788h31.349a.79.79 0 01.788.788v37.562a.79.79 0 01-.788.789z"
|
|
30
|
+
fill="#F4F4F4"
|
|
31
|
+
/>
|
|
32
|
+
<path
|
|
33
|
+
d="M41.47 13H21.948a1.579 1.579 0 00-1.576 1.577V52.4l.185-.057V14.577c.001-.768.623-1.39 1.391-1.39h19.581L41.471 13zm17.02 0H21.947a1.579 1.579 0 00-1.576 1.577v42.478c0 .87.706 1.576 1.576 1.577H58.49a1.579 1.579 0 001.576-1.577V14.577a1.579 1.579 0 00-1.576-1.576zm1.39 44.055c0 .768-.622 1.39-1.39 1.392H21.947c-.768-.001-1.39-.624-1.39-1.392V14.577c0-.768.622-1.39 1.39-1.39H58.49c.768 0 1.39.622 1.39 1.39v42.478z"
|
|
34
|
+
fill="#8D8D8D"
|
|
35
|
+
/>
|
|
36
|
+
<path
|
|
37
|
+
d="M48.751 17.082H31.686a.836.836 0 01-.835-.835v-4.081c0-.46.374-.834.835-.835H48.75c.461 0 .834.374.835.835v4.08c0 .462-.374.835-.835.836z"
|
|
38
|
+
fill="#C6C6C6"
|
|
39
|
+
/>
|
|
40
|
+
<circle fill="#C6C6C6" cx={40.218} cy={9.755} r={1.855} />
|
|
41
|
+
<circle
|
|
42
|
+
fill="#FFF"
|
|
43
|
+
fillRule="nonzero"
|
|
44
|
+
cx={40.218}
|
|
45
|
+
cy={9.755}
|
|
46
|
+
r={1.13}
|
|
47
|
+
/>
|
|
48
|
+
</g>
|
|
49
|
+
</svg>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Layer, Link, Tile } from "@carbon/react";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
import { navigate, useLayoutType } from "@openmrs/esm-framework";
|
|
5
|
+
|
|
6
|
+
import styles from "./empty-state.scss";
|
|
7
|
+
import { EmptyDataIllustration } from "./empty-data-illustration.component";
|
|
8
|
+
|
|
9
|
+
function EmptyState() {
|
|
10
|
+
const { t } = useTranslation();
|
|
11
|
+
const isTablet = useLayoutType() === "tablet";
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Layer>
|
|
15
|
+
<Tile className={styles.tile}>
|
|
16
|
+
<div
|
|
17
|
+
className={isTablet ? styles.tabletHeading : styles.desktopHeading}
|
|
18
|
+
>
|
|
19
|
+
<h4>{t("forms", "Forms")}</h4>
|
|
20
|
+
</div>
|
|
21
|
+
<EmptyDataIllustration />
|
|
22
|
+
<p className={styles.content}>
|
|
23
|
+
{t("noFormsToDisplay", "There are no forms to display.")}
|
|
24
|
+
</p>
|
|
25
|
+
<p className={styles.action}>
|
|
26
|
+
<Link
|
|
27
|
+
onClick={() =>
|
|
28
|
+
navigate({
|
|
29
|
+
to: `${window.spaBase}/form-builder/new`,
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
>
|
|
33
|
+
{t("createNewForm", "Create a new form")}
|
|
34
|
+
</Link>
|
|
35
|
+
</p>
|
|
36
|
+
</Tile>
|
|
37
|
+
</Layer>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default EmptyState;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
4
|
+
|
|
5
|
+
.action {
|
|
6
|
+
margin-bottom: spacing.$spacing-03;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.content {
|
|
10
|
+
@include type.type-style("heading-compact-01");
|
|
11
|
+
color: $text-02;
|
|
12
|
+
margin-top: spacing.$spacing-05;
|
|
13
|
+
margin-bottom: spacing.$spacing-03;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.desktopHeading {
|
|
17
|
+
h4 {
|
|
18
|
+
@include type.type-style('heading-compact-02');
|
|
19
|
+
color: $text-02;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.tabletHeading {
|
|
24
|
+
h4 {
|
|
25
|
+
@include type.type-style('heading-03');
|
|
26
|
+
color: $text-02;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.desktopHeading, .tabletHeading {
|
|
31
|
+
text-align: left;
|
|
32
|
+
text-transform: capitalize;
|
|
33
|
+
margin-bottom: spacing.$spacing-05;
|
|
34
|
+
|
|
35
|
+
h4:after {
|
|
36
|
+
content: "";
|
|
37
|
+
display: block;
|
|
38
|
+
width: 2rem;
|
|
39
|
+
padding-top: 0.188rem;
|
|
40
|
+
border-bottom: 0.375rem solid var(--brand-03);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.heading:after {
|
|
45
|
+
content: "";
|
|
46
|
+
display: block;
|
|
47
|
+
width: 2rem;
|
|
48
|
+
padding-top: 0.188rem;
|
|
49
|
+
border-bottom: 0.375rem solid var(--brand-03);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.tile {
|
|
53
|
+
text-align: center;
|
|
54
|
+
border: 1px solid $ui-03;
|
|
55
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Layer, Tile } from "@carbon/react";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
import { useLayoutType } from "@openmrs/esm-framework";
|
|
5
|
+
import styles from "./error-state.scss";
|
|
6
|
+
|
|
7
|
+
interface ErrorStateProps {
|
|
8
|
+
error: Error;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ErrorState: React.FC<ErrorStateProps> = ({ error }) => {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const isTablet = useLayoutType() === "tablet";
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Layer>
|
|
17
|
+
<Tile className={styles.tile}>
|
|
18
|
+
<div
|
|
19
|
+
className={isTablet ? styles.tabletHeading : styles.desktopHeading}
|
|
20
|
+
>
|
|
21
|
+
<h4>{t("forms", "Forms")}</h4>
|
|
22
|
+
</div>
|
|
23
|
+
<p className={styles.errorMessage}>
|
|
24
|
+
{t("error", "Error")}: {error?.message}
|
|
25
|
+
</p>
|
|
26
|
+
<p className={styles.errorCopy}>
|
|
27
|
+
{t(
|
|
28
|
+
"errorCopy",
|
|
29
|
+
"Sorry, there was a problem displaying this information. You can try to reload this page, or contact the site administrator and quote the error code above."
|
|
30
|
+
)}
|
|
31
|
+
</p>
|
|
32
|
+
</Tile>
|
|
33
|
+
</Layer>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default ErrorState;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
4
|
+
|
|
5
|
+
.errorMessage {
|
|
6
|
+
@include type.type-style("heading-compact-02");
|
|
7
|
+
|
|
8
|
+
margin-top: 2.25rem;
|
|
9
|
+
margin-bottom: spacing.$spacing-03;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.errorCopy {
|
|
13
|
+
margin-bottom: spacing.$spacing-03;
|
|
14
|
+
@include type.type-style("body-01");
|
|
15
|
+
color: $text-02;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.desktopHeading {
|
|
19
|
+
h4 {
|
|
20
|
+
@include type.type-style('heading-compact-02');
|
|
21
|
+
color: $text-02;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.tabletHeading {
|
|
26
|
+
h4 {
|
|
27
|
+
@include type.type-style('heading-03');
|
|
28
|
+
color: $text-02;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.desktopHeading, .tabletHeading {
|
|
33
|
+
text-align: left;
|
|
34
|
+
text-transform: capitalize;
|
|
35
|
+
margin-bottom: spacing.$spacing-05;
|
|
36
|
+
|
|
37
|
+
h4:after {
|
|
38
|
+
content: "";
|
|
39
|
+
display: block;
|
|
40
|
+
width: 2rem;
|
|
41
|
+
padding-top: 0.188rem;
|
|
42
|
+
border-bottom: 0.375rem solid var(--brand-03);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.tile {
|
|
47
|
+
text-align: center;
|
|
48
|
+
border: 1px solid $ui-03;
|
|
49
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Column,
|
|
4
|
+
InlineNotification,
|
|
5
|
+
Grid,
|
|
6
|
+
Tabs,
|
|
7
|
+
Tab,
|
|
8
|
+
TabList,
|
|
9
|
+
TabPanels,
|
|
10
|
+
TabPanel,
|
|
11
|
+
} from "@carbon/react";
|
|
12
|
+
import { useParams } from "react-router-dom";
|
|
13
|
+
import { useTranslation } from "react-i18next";
|
|
14
|
+
import { ExtensionSlot } from "@openmrs/esm-framework";
|
|
15
|
+
import { Schema, RouteParams } from "../../types";
|
|
16
|
+
import { useClobdata } from "../../hooks/useClobdata";
|
|
17
|
+
import { useForm } from "../../hooks/useForm";
|
|
18
|
+
import FormRenderer from "../form-renderer/form-renderer.component";
|
|
19
|
+
import InteractiveBuilder from "../interactive-builder/interactive-builder.component";
|
|
20
|
+
import SchemaEditor from "../schema-editor/schema-editor.component";
|
|
21
|
+
import styles from "./form-editor.scss";
|
|
22
|
+
|
|
23
|
+
const Error = ({ error, title }) => {
|
|
24
|
+
return (
|
|
25
|
+
<InlineNotification
|
|
26
|
+
style={{
|
|
27
|
+
minWidth: "100%",
|
|
28
|
+
margin: "0rem",
|
|
29
|
+
padding: "0rem",
|
|
30
|
+
}}
|
|
31
|
+
kind={"error"}
|
|
32
|
+
lowContrast
|
|
33
|
+
subtitle={error?.message}
|
|
34
|
+
title={title}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const FormEditor: React.FC = () => {
|
|
40
|
+
const { t } = useTranslation();
|
|
41
|
+
const { formUuid } = useParams<RouteParams>();
|
|
42
|
+
const [schema, setSchema] = useState<Schema>();
|
|
43
|
+
const { form, formError, isLoadingForm } = useForm(formUuid);
|
|
44
|
+
const { clobdata, clobdataError, isLoadingClobdata } = useClobdata(form);
|
|
45
|
+
const isLoadingFormOrSchema =
|
|
46
|
+
formUuid && (isLoadingClobdata || isLoadingForm);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!isLoadingClobdata && clobdata) {
|
|
50
|
+
setSchema(clobdata);
|
|
51
|
+
}
|
|
52
|
+
}, [clobdata, isLoadingClobdata, setSchema]);
|
|
53
|
+
|
|
54
|
+
const updateSchema = useCallback((updatedSchema) => {
|
|
55
|
+
setSchema(updatedSchema);
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
<div className={styles.breadcrumbsContainer}>
|
|
61
|
+
<ExtensionSlot extensionSlotName="breadcrumbs-slot" />
|
|
62
|
+
</div>
|
|
63
|
+
<div className={styles.container}>
|
|
64
|
+
<Grid className={styles.grid}>
|
|
65
|
+
<Column lg={8} md={8} className={styles.column}>
|
|
66
|
+
<Tabs>
|
|
67
|
+
<TabList>
|
|
68
|
+
<Tab>{t("schemaEditor", "Schema Editor")}</Tab>
|
|
69
|
+
</TabList>
|
|
70
|
+
<TabPanels>
|
|
71
|
+
<TabPanel>
|
|
72
|
+
<>
|
|
73
|
+
{formError ? (
|
|
74
|
+
<Error
|
|
75
|
+
error={formError}
|
|
76
|
+
title={t("formError", "Error loading form metadata")}
|
|
77
|
+
/>
|
|
78
|
+
) : null}
|
|
79
|
+
{clobdataError ? (
|
|
80
|
+
<Error
|
|
81
|
+
error={clobdataError}
|
|
82
|
+
title={t("schemaLoadError", "Error loading schema")}
|
|
83
|
+
/>
|
|
84
|
+
) : null}
|
|
85
|
+
<SchemaEditor
|
|
86
|
+
schema={schema}
|
|
87
|
+
onSchemaChange={updateSchema}
|
|
88
|
+
isLoading={isLoadingFormOrSchema}
|
|
89
|
+
/>
|
|
90
|
+
</>
|
|
91
|
+
</TabPanel>
|
|
92
|
+
</TabPanels>
|
|
93
|
+
</Tabs>
|
|
94
|
+
</Column>
|
|
95
|
+
<Column lg={8} md={8} className={styles.column}>
|
|
96
|
+
<Tabs>
|
|
97
|
+
<TabList>
|
|
98
|
+
<Tab>{t("preview", "Preview")}</Tab>
|
|
99
|
+
<Tab>{t("interactiveBuilder", "Interactive Builder")}</Tab>
|
|
100
|
+
</TabList>
|
|
101
|
+
<TabPanels>
|
|
102
|
+
<TabPanel>
|
|
103
|
+
<FormRenderer
|
|
104
|
+
schema={schema}
|
|
105
|
+
onSchemaChange={updateSchema}
|
|
106
|
+
isLoading={isLoadingFormOrSchema}
|
|
107
|
+
/>
|
|
108
|
+
</TabPanel>
|
|
109
|
+
<TabPanel>
|
|
110
|
+
<InteractiveBuilder
|
|
111
|
+
schema={schema}
|
|
112
|
+
onSchemaChange={updateSchema}
|
|
113
|
+
isLoading={isLoadingFormOrSchema}
|
|
114
|
+
/>
|
|
115
|
+
</TabPanel>
|
|
116
|
+
</TabPanels>
|
|
117
|
+
</Tabs>
|
|
118
|
+
</Column>
|
|
119
|
+
</Grid>
|
|
120
|
+
</div>
|
|
121
|
+
</>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export default FormEditor;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
@use "@carbon/styles/scss/spacing";
|
|
2
|
+
@use "@carbon/styles/scss/type";
|
|
3
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
4
|
+
|
|
5
|
+
.container {
|
|
6
|
+
padding: 2rem;
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.breadcrumbsContainer {
|
|
12
|
+
nav {
|
|
13
|
+
background-color: $ui-02;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.grid {
|
|
18
|
+
margin-left: 0;
|
|
19
|
+
margin-right: 0;
|
|
20
|
+
padding-left: 0;
|
|
21
|
+
padding-right: 0;
|
|
22
|
+
max-width: 100%;
|
|
23
|
+
|
|
24
|
+
:global(.cds--tabs__nav-item--selected) {
|
|
25
|
+
border-bottom: 2px solid var(--cds-border-interactive, #005d5d);
|
|
26
|
+
outline: none !important;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.column {
|
|
31
|
+
margin-left: 0;
|
|
32
|
+
margin-right: 0;
|
|
33
|
+
}
|