@openmrs/esm-form-builder-app 1.0.1-pre.126

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.
Files changed (60) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc +33 -0
  3. package/.husky/pre-commit +4 -0
  4. package/.husky/pre-push +6 -0
  5. package/.prettierignore +14 -0
  6. package/.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs +541 -0
  7. package/.yarn/plugins/@yarnpkg/plugin-version.cjs +550 -0
  8. package/.yarn/versions/7d315ef1.yml +0 -0
  9. package/LICENSE +401 -0
  10. package/README.md +14 -0
  11. package/__mocks__/react-i18next.js +56 -0
  12. package/dist/openmrs-esm-form-builder-app.js +1 -0
  13. package/i18next-parser.config.js +89 -0
  14. package/jest.config.json +19 -0
  15. package/package.json +102 -0
  16. package/src/components/dashboard/dashboard.component.tsx +310 -0
  17. package/src/components/dashboard/dashboard.scss +112 -0
  18. package/src/components/empty-state/empty-data-illustration.component.tsx +51 -0
  19. package/src/components/empty-state/empty-state.component.tsx +41 -0
  20. package/src/components/empty-state/empty-state.scss +55 -0
  21. package/src/components/error-state/error-state.component.tsx +37 -0
  22. package/src/components/error-state/error-state.scss +49 -0
  23. package/src/components/form-editor/form-editor.component.tsx +297 -0
  24. package/src/components/form-editor/form-editor.scss +50 -0
  25. package/src/components/form-renderer/form-renderer.component.tsx +82 -0
  26. package/src/components/form-renderer/form-renderer.scss +31 -0
  27. package/src/components/interactive-builder/add-question-modal.component.tsx +494 -0
  28. package/src/components/interactive-builder/edit-question-modal.component.tsx +447 -0
  29. package/src/components/interactive-builder/editable-value.component.tsx +60 -0
  30. package/src/components/interactive-builder/editable-value.scss +23 -0
  31. package/src/components/interactive-builder/interactive-builder.component.tsx +403 -0
  32. package/src/components/interactive-builder/interactive-builder.scss +83 -0
  33. package/src/components/interactive-builder/new-form-modal.component.tsx +86 -0
  34. package/src/components/interactive-builder/page-modal.component.tsx +91 -0
  35. package/src/components/interactive-builder/question-modal.scss +35 -0
  36. package/src/components/interactive-builder/section-modal.component.tsx +94 -0
  37. package/src/components/interactive-builder/value-editor.component.tsx +55 -0
  38. package/src/components/interactive-builder/value-editor.scss +10 -0
  39. package/src/components/modals/save-form.component.tsx +310 -0
  40. package/src/components/modals/save-form.scss +5 -0
  41. package/src/components/schema-editor/schema-editor.component.tsx +190 -0
  42. package/src/components/schema-editor/schema-editor.scss +30 -0
  43. package/src/config-schema.ts +47 -0
  44. package/src/constants.ts +3 -0
  45. package/src/declarations.d.tsx +2 -0
  46. package/src/form-builder-app-menu-link.component.tsx +13 -0
  47. package/src/forms.resource.ts +178 -0
  48. package/src/hooks/useClobdata.ts +20 -0
  49. package/src/hooks/useConceptLookup.ts +18 -0
  50. package/src/hooks/useEncounterTypes.ts +18 -0
  51. package/src/hooks/useForm.ts +18 -0
  52. package/src/hooks/useForms.ts +20 -0
  53. package/src/index.ts +70 -0
  54. package/src/root.component.tsx +19 -0
  55. package/src/setup-tests.ts +1 -0
  56. package/src/types.ts +132 -0
  57. package/translations/en.json +110 -0
  58. package/tsconfig.json +23 -0
  59. package/turbo.json +26 -0
  60. package/webpack.config.js +19 -0
@@ -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,297 @@
1
+ import React, { useCallback, useEffect, useState } from "react";
2
+ import {
3
+ Column,
4
+ ComposedModal,
5
+ InlineLoading,
6
+ InlineNotification,
7
+ Grid,
8
+ ModalBody,
9
+ ModalFooter,
10
+ ModalHeader,
11
+ Tabs,
12
+ Tab,
13
+ TabList,
14
+ TabPanels,
15
+ TabPanel,
16
+ Button,
17
+ } from "@carbon/react";
18
+ import { useParams } from "react-router-dom";
19
+ import { useSWRConfig } from "swr";
20
+ import { useTranslation } from "react-i18next";
21
+ import SaveForm from "../modals/save-form.component";
22
+ import {
23
+ showToast,
24
+ ExtensionSlot,
25
+ showNotification,
26
+ } from "@openmrs/esm-framework";
27
+ import { Schema, RouteParams } from "../../types";
28
+ import { useClobdata } from "../../hooks/useClobdata";
29
+ import { useForm } from "../../hooks/useForm";
30
+ import { publishForm, unpublishForm } from "../../forms.resource";
31
+ import FormRenderer from "../form-renderer/form-renderer.component";
32
+ import SchemaEditor from "../schema-editor/schema-editor.component";
33
+ import styles from "./form-editor.scss";
34
+ import InteractiveBuilder from "../interactive-builder/interactive-builder.component";
35
+
36
+ const Error = ({ error, title }) => {
37
+ return (
38
+ <InlineNotification
39
+ style={{
40
+ minWidth: "100%",
41
+ margin: "0rem",
42
+ padding: "0rem",
43
+ }}
44
+ kind={"error"}
45
+ lowContrast
46
+ subtitle={error?.message}
47
+ title={title}
48
+ />
49
+ );
50
+ };
51
+
52
+ const FormEditor: React.FC = () => {
53
+ const { t } = useTranslation();
54
+ const { formUuid } = useParams<RouteParams>();
55
+ const [schema, setSchema] = useState<Schema>(undefined);
56
+ const [isPublishing, setIsPublishing] = useState(false);
57
+ const [isUnpublishing, setIsUnpublishing] = useState(false);
58
+ const [showUnpublishModal, setShowUnpublishModal] = useState(false);
59
+ const { form, formError, isLoadingForm } = useForm(formUuid);
60
+ const { clobdata, clobdataError, isLoadingClobdata } = useClobdata(form);
61
+ const { cache, mutate }: { cache: any; mutate: Function } = useSWRConfig();
62
+
63
+ useEffect(() => {
64
+ if (clobdata) {
65
+ setSchema(clobdata);
66
+ }
67
+ }, [clobdata, setSchema]);
68
+
69
+ const updateSchema = useCallback((updatedSchema) => {
70
+ setSchema(updatedSchema);
71
+ }, []);
72
+
73
+ const revalidate = () => {
74
+ const apiUrlPattern = new RegExp("\\/ws\\/rest\\/v1\\/form");
75
+
76
+ // Find matching keys from SWR's cache and broadcast a revalidation message to their pre-bound SWR hooks
77
+ Array.from(cache.keys())
78
+ .filter((url: string) => apiUrlPattern.test(url))
79
+ .forEach((url: string) => mutate(url));
80
+ };
81
+
82
+ const launchUnpublishModal = () => {
83
+ setShowUnpublishModal(true);
84
+ };
85
+
86
+ async function handlePublish() {
87
+ setIsPublishing(true);
88
+ try {
89
+ await publishForm(form.uuid);
90
+
91
+ showToast({
92
+ title: t("formPublished", "Form published"),
93
+ kind: "success",
94
+ critical: true,
95
+ description:
96
+ `${form.name} ` +
97
+ t("formPublishedSuccessfully", "form was published successfully"),
98
+ });
99
+
100
+ revalidate();
101
+ } catch (error) {
102
+ showNotification({
103
+ title: t("errorPublishingForm", "Error publishing form"),
104
+ kind: "error",
105
+ critical: true,
106
+ description: error?.message,
107
+ });
108
+ }
109
+ setIsPublishing(false);
110
+ }
111
+
112
+ async function handleUnpublish() {
113
+ setIsUnpublishing(true);
114
+ try {
115
+ await unpublishForm(form.uuid);
116
+
117
+ showToast({
118
+ title: t("formUnpublished", "Form unpublished"),
119
+ kind: "success",
120
+ critical: true,
121
+ description:
122
+ `${form.name} ` +
123
+ t("formUnpublishedSuccessfully", "form was unpublished successfully"),
124
+ });
125
+
126
+ revalidate();
127
+ } catch (error) {
128
+ showNotification({
129
+ title: t("errorUnpublishingForm", "Error unpublishing form"),
130
+ kind: "error",
131
+ critical: true,
132
+ description: error?.message,
133
+ });
134
+ }
135
+ setIsUnpublishing(false);
136
+ setShowUnpublishModal(false);
137
+ }
138
+
139
+ useEffect(() => {
140
+ if (!isLoadingClobdata) {
141
+ setSchema(clobdata);
142
+ }
143
+ }, [clobdata, isLoadingClobdata]);
144
+
145
+ return (
146
+ <>
147
+ <div className={styles.breadcrumbsContainer}>
148
+ <ExtensionSlot extensionSlotName="breadcrumbs-slot" />
149
+ </div>
150
+ <div className={styles.container}>
151
+ <Grid className={styles.grid}>
152
+ <Column lg={8} md={8} className={styles.column}>
153
+ <Tabs>
154
+ <TabList>
155
+ <Tab>{t("schemaEditor", "Schema Editor")}</Tab>
156
+ <Tab>{t("interactiveBuilder", "Interactive Builder")}</Tab>
157
+ </TabList>
158
+ <TabPanels>
159
+ <TabPanel>
160
+ <>
161
+ {formError ? (
162
+ <Error
163
+ error={formError}
164
+ title={t("formError", "Error loading form metadata")}
165
+ />
166
+ ) : null}
167
+ {clobdataError ? (
168
+ <Error
169
+ error={clobdataError}
170
+ title={t("schemaLoadError", "Error loading schema")}
171
+ />
172
+ ) : null}
173
+ <SchemaEditor
174
+ schema={schema}
175
+ onSchemaChange={updateSchema}
176
+ isLoading={
177
+ formUuid && (isLoadingClobdata || isLoadingForm)
178
+ }
179
+ />
180
+ </>
181
+ </TabPanel>
182
+ <TabPanel>
183
+ <InteractiveBuilder
184
+ schema={schema}
185
+ onSchemaChange={updateSchema}
186
+ isLoading={formUuid && (isLoadingClobdata || isLoadingForm)}
187
+ />
188
+ </TabPanel>
189
+ </TabPanels>
190
+ </Tabs>
191
+ </Column>
192
+ <Column lg={8} md={8} className={styles.column}>
193
+ <Tabs>
194
+ <TabList>
195
+ <Tab>{t("preview", "Preview")}</Tab>
196
+ </TabList>
197
+ <TabPanels>
198
+ <TabPanel>
199
+ <>
200
+ <div className={styles.actionButtons}>
201
+ <SaveForm form={form} schema={schema} />
202
+
203
+ <>
204
+ {form && !form.published ? (
205
+ <Button
206
+ kind="secondary"
207
+ onClick={handlePublish}
208
+ disabled={isPublishing}
209
+ >
210
+ {isPublishing && !form?.published ? (
211
+ <InlineLoading
212
+ className={styles.spinner}
213
+ description={
214
+ t("publishing", "Publishing") + "..."
215
+ }
216
+ />
217
+ ) : (
218
+ <span>{t("publishForm", "Publish form")}</span>
219
+ )}
220
+ </Button>
221
+ ) : null}
222
+ {form && form.published ? (
223
+ <Button
224
+ kind="danger"
225
+ onClick={launchUnpublishModal}
226
+ disabled={isUnpublishing}
227
+ >
228
+ {t("unpublishForm", "Unpublish form")}
229
+ </Button>
230
+ ) : null}
231
+ {showUnpublishModal ? (
232
+ <ComposedModal
233
+ open={true}
234
+ onClose={() => setShowUnpublishModal(false)}
235
+ >
236
+ <ModalHeader
237
+ title={t(
238
+ "unpublishConfirmation",
239
+ "Are you sure you want to unpublish this form?"
240
+ )}
241
+ ></ModalHeader>
242
+ <ModalBody>
243
+ <p>
244
+ {t(
245
+ "unpublishExplainerText",
246
+ "Unpublishing a form means you can no longer access it from your frontend. Unpublishing forms does not delete their associated schemas, it only affects whether or not you can access them in your frontend."
247
+ )}
248
+ </p>
249
+ </ModalBody>
250
+ <ModalFooter>
251
+ <Button
252
+ kind="secondary"
253
+ onClick={() => setShowUnpublishModal(false)}
254
+ >
255
+ {t("cancel", "Cancel")}
256
+ </Button>
257
+ <Button
258
+ disabled={isUnpublishing}
259
+ kind={isUnpublishing ? "secondary" : "danger"}
260
+ onClick={handleUnpublish}
261
+ >
262
+ {isUnpublishing ? (
263
+ <InlineLoading
264
+ className={styles.spinner}
265
+ description={
266
+ t("unpublishing", "Unpublishing") + "..."
267
+ }
268
+ />
269
+ ) : (
270
+ <span>
271
+ {t("unpublishForm", "Unpublish form")}
272
+ </span>
273
+ )}
274
+ </Button>
275
+ </ModalFooter>
276
+ </ComposedModal>
277
+ ) : (
278
+ false
279
+ )}
280
+ </>
281
+ </div>
282
+ <FormRenderer
283
+ schema={schema}
284
+ onSchemaChange={updateSchema}
285
+ />
286
+ </>
287
+ </TabPanel>
288
+ </TabPanels>
289
+ </Tabs>
290
+ </Column>
291
+ </Grid>
292
+ </div>
293
+ </>
294
+ );
295
+ };
296
+
297
+ export default FormEditor;
@@ -0,0 +1,50 @@
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.breadcrumbs-container {
13
+ background-color: $ui-03;
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
+ }
34
+
35
+ .actionButtons {
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: flex-end;
39
+ margin: 1rem 0;
40
+
41
+ button {
42
+ margin-left: 1rem
43
+ }
44
+ }
45
+
46
+ .spinner {
47
+ &:global(.cds--inline-loading) {
48
+ min-height: 1rem;
49
+ }
50
+ }
@@ -0,0 +1,82 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { useTranslation } from "react-i18next";
3
+ import { OHRIFormSchema, OHRIForm } from "@ohri/openmrs-ohri-form-engine-lib";
4
+ import { Tile } from "@carbon/react";
5
+ import { useConfig } from "@openmrs/esm-framework";
6
+ import styles from "./form-renderer.scss";
7
+
8
+ type FormRendererProps = {
9
+ onSchemaChange?: (schema: OHRIFormSchema) => void;
10
+ schema: OHRIFormSchema;
11
+ };
12
+
13
+ const FormRenderer: React.FC<FormRendererProps> = ({ schema }) => {
14
+ const { t } = useTranslation();
15
+ const { patientUuid } = useConfig();
16
+
17
+ const dummySchema: OHRIFormSchema = {
18
+ encounterType: "",
19
+ name: "Test Form",
20
+ pages: [
21
+ {
22
+ label: "Test Page",
23
+ sections: [
24
+ {
25
+ label: "Test Section",
26
+ isExpanded: "true",
27
+ questions: [
28
+ {
29
+ label: "Test Question",
30
+ type: "obs",
31
+ questionOptions: {
32
+ rendering: "text",
33
+ concept: "xxxx",
34
+ },
35
+ id: "testQuestion",
36
+ },
37
+ ],
38
+ },
39
+ ],
40
+ },
41
+ ],
42
+ processor: "EncounterFormProcessor",
43
+ referencedForms: [],
44
+ uuid: "xxx",
45
+ };
46
+
47
+ const [schemaToRender, setSchemaToRender] =
48
+ useState<OHRIFormSchema>(dummySchema);
49
+
50
+ useEffect(() => {
51
+ if (schema) {
52
+ setSchemaToRender(schema);
53
+ }
54
+ }, [schema]);
55
+
56
+ return (
57
+ <div className={styles.container}>
58
+ {!schema && (
59
+ <Tile className={styles.emptyStateTile}>
60
+ <h4 className={styles.heading}>
61
+ {t("noSchemaLoaded", "No schema loaded")}
62
+ </h4>
63
+ <p className={styles.helperText}>
64
+ {t(
65
+ "formRendererHelperText",
66
+ "Load a form schema in the Schema Editor to the left to see it rendered here by the Form Engine."
67
+ )}
68
+ </p>
69
+ </Tile>
70
+ )}
71
+ {schema === schemaToRender && (
72
+ <OHRIForm
73
+ formJson={schemaToRender}
74
+ mode={"enter"}
75
+ patientUUID={patientUuid}
76
+ />
77
+ )}
78
+ </div>
79
+ );
80
+ };
81
+
82
+ export default FormRenderer;
@@ -0,0 +1,31 @@
1
+ @use '@carbon/styles/scss/colors';
2
+ @use '@carbon/styles/scss/spacing';
3
+ @use '@carbon/styles/scss/type';
4
+
5
+ .container {
6
+ background-color: white;
7
+ padding: 1rem;
8
+ overflow-y: scroll;
9
+ height: 100vh;
10
+ }
11
+
12
+ .emptyStateTile {
13
+ width: 80%;
14
+ margin: 1.25rem auto;
15
+ display: flex;
16
+ flex-flow: column wrap;
17
+ align-items: center;
18
+ justify-content: center;
19
+ padding: 1.5rem;
20
+ }
21
+
22
+ .heading {
23
+ @include type.type-style('heading-compact-02');
24
+ color: colors.$gray-100;
25
+ }
26
+
27
+ .helperText {
28
+ margin-top: 0.5rem;
29
+ @include type.type-style('body-01');
30
+ color: colors.$gray-90;
31
+ }