@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.
Files changed (52) hide show
  1. package/LICENSE +401 -0
  2. package/README.md +35 -0
  3. package/package.json +106 -0
  4. package/src/components/action-buttons/action-buttons.component.tsx +185 -0
  5. package/src/components/action-buttons/action-buttons.scss +16 -0
  6. package/src/components/dashboard/dashboard.component.tsx +309 -0
  7. package/src/components/dashboard/dashboard.scss +112 -0
  8. package/src/components/dashboard/dashboard.test.tsx +208 -0
  9. package/src/components/empty-state/empty-data-illustration.component.tsx +51 -0
  10. package/src/components/empty-state/empty-state.component.tsx +41 -0
  11. package/src/components/empty-state/empty-state.scss +55 -0
  12. package/src/components/error-state/error-state.component.tsx +37 -0
  13. package/src/components/error-state/error-state.scss +49 -0
  14. package/src/components/form-editor/form-editor.component.tsx +125 -0
  15. package/src/components/form-editor/form-editor.scss +33 -0
  16. package/src/components/form-renderer/form-renderer.component.tsx +123 -0
  17. package/src/components/form-renderer/form-renderer.scss +57 -0
  18. package/src/components/interactive-builder/add-question-modal.component.tsx +427 -0
  19. package/src/components/interactive-builder/delete-page-modal.component.tsx +89 -0
  20. package/src/components/interactive-builder/delete-question-modal.component.tsx +93 -0
  21. package/src/components/interactive-builder/delete-section-modal.component.tsx +91 -0
  22. package/src/components/interactive-builder/edit-question-modal.component.tsx +465 -0
  23. package/src/components/interactive-builder/editable-value.component.tsx +64 -0
  24. package/src/components/interactive-builder/editable-value.scss +23 -0
  25. package/src/components/interactive-builder/interactive-builder.component.tsx +569 -0
  26. package/src/components/interactive-builder/interactive-builder.scss +100 -0
  27. package/src/components/interactive-builder/new-form-modal.component.tsx +86 -0
  28. package/src/components/interactive-builder/page-modal.component.tsx +91 -0
  29. package/src/components/interactive-builder/question-modal.scss +35 -0
  30. package/src/components/interactive-builder/section-modal.component.tsx +94 -0
  31. package/src/components/interactive-builder/value-editor.component.tsx +55 -0
  32. package/src/components/interactive-builder/value-editor.scss +10 -0
  33. package/src/components/modals/save-form.component.tsx +310 -0
  34. package/src/components/modals/save-form.scss +5 -0
  35. package/src/components/schema-editor/schema-editor.component.tsx +191 -0
  36. package/src/components/schema-editor/schema-editor.scss +26 -0
  37. package/src/config-schema.ts +47 -0
  38. package/src/constants.ts +3 -0
  39. package/src/declarations.d.tsx +2 -0
  40. package/src/form-builder-app-menu-link.component.tsx +13 -0
  41. package/src/forms.resource.ts +178 -0
  42. package/src/hooks/useClobdata.ts +20 -0
  43. package/src/hooks/useConceptLookup.ts +18 -0
  44. package/src/hooks/useConceptName.ts +18 -0
  45. package/src/hooks/useEncounterTypes.ts +18 -0
  46. package/src/hooks/useForm.ts +18 -0
  47. package/src/hooks/useForms.ts +20 -0
  48. package/src/index.ts +70 -0
  49. package/src/root.component.tsx +19 -0
  50. package/src/setup-tests.ts +11 -0
  51. package/src/test-helpers.tsx +37 -0
  52. package/src/types.ts +132 -0
@@ -0,0 +1,178 @@
1
+ import { openmrsFetch, FetchResponse } from "@openmrs/esm-framework";
2
+ import axios from "axios";
3
+ import { Schema } from "./types";
4
+
5
+ export const deleteClobData = async (valueReference: string) => {
6
+ const request: FetchResponse = await openmrsFetch(
7
+ `/ws/rest/v1/clobdata/${valueReference}`,
8
+ {
9
+ method: "DELETE",
10
+ headers: { "Content-Type": "application/json" },
11
+ }
12
+ );
13
+ return request;
14
+ };
15
+
16
+ export const deleteResource = async (
17
+ formUUID: string,
18
+ resourceUUID: string
19
+ ) => {
20
+ const request: FetchResponse = await openmrsFetch(
21
+ `/ws/rest/v1/form/${formUUID}/resource/${resourceUUID}`,
22
+ {
23
+ method: "DELETE",
24
+ headers: { "Content-Type": "application/json" },
25
+ }
26
+ );
27
+ return request;
28
+ };
29
+
30
+ export const uploadSchema = async (schema: Schema) => {
31
+ const schemaBlob = new Blob([JSON.stringify(schema)], {
32
+ type: "application/json",
33
+ });
34
+ const body = new FormData();
35
+ body.append("file", schemaBlob);
36
+ const headers = {
37
+ Accept: "application/json",
38
+ "Content-Type": undefined,
39
+ };
40
+ const request = axios
41
+ .post(`${window.origin}/openmrs/ws/rest/v1/clobdata`, body, {
42
+ headers: headers,
43
+ })
44
+ .then((response) => {
45
+ return response.data;
46
+ });
47
+ return request;
48
+ };
49
+
50
+ export const getResourceUUID = async (
51
+ formUUID: string,
52
+ valueReference: any
53
+ ) => {
54
+ const body = {
55
+ name: "JSON schema",
56
+ dataType: "AmpathJsonSchema",
57
+ valueReference: valueReference,
58
+ };
59
+ const request: FetchResponse = await openmrsFetch(
60
+ `/ws/rest/v1/form/${formUUID}/resource`,
61
+ {
62
+ method: "POST",
63
+ headers: { "Content-Type": "application/json" },
64
+ body: body,
65
+ }
66
+ );
67
+ return request;
68
+ };
69
+
70
+ export const saveNewForm = async (
71
+ name: any,
72
+ version: any,
73
+ published?: any,
74
+ description?: any,
75
+ encounterType?: any
76
+ ) => {
77
+ const abortController = new AbortController();
78
+
79
+ const body = {
80
+ name: name,
81
+ version: version,
82
+ published: published || false,
83
+ description: description || "",
84
+ };
85
+ if (encounterType) {
86
+ body["encounterType"] = encounterType;
87
+ }
88
+ const headers = {
89
+ "Content-Type": "application/json",
90
+ };
91
+
92
+ const response: FetchResponse = await openmrsFetch(`/ws/rest/v1/form`, {
93
+ method: "POST",
94
+ headers: headers,
95
+ body: body,
96
+ signal: abortController.signal,
97
+ });
98
+
99
+ return response.data;
100
+ };
101
+
102
+ export const publishForm = async (uuid) => {
103
+ const body = { published: true };
104
+ const request: FetchResponse = await openmrsFetch(
105
+ `/ws/rest/v1/form/${uuid}`,
106
+ {
107
+ method: "POST",
108
+ headers: { "Content-Type": "application/json" },
109
+ body: body,
110
+ }
111
+ );
112
+ return request;
113
+ };
114
+
115
+ export const unpublishForm = async (uuid) => {
116
+ const body = { published: false };
117
+ const request: FetchResponse = await openmrsFetch(
118
+ `/ws/rest/v1/form/${uuid}`,
119
+ {
120
+ method: "POST",
121
+ headers: { "Content-Type": "application/json" },
122
+ body: body,
123
+ }
124
+ );
125
+ return request;
126
+ };
127
+
128
+ export const updateName = async (name: string, uuid) => {
129
+ const body = { name: name };
130
+ const request: FetchResponse = await openmrsFetch(
131
+ `/ws/rest/v1/form/${uuid}`,
132
+ {
133
+ method: "POST",
134
+ headers: { "Content-Type": "application/json" },
135
+ body: body,
136
+ }
137
+ );
138
+ return request;
139
+ };
140
+
141
+ export const updateVersion = async (version: string, uuid) => {
142
+ const body = { version: version };
143
+ const request: FetchResponse = await openmrsFetch(
144
+ `/ws/rest/v1/form/${uuid}`,
145
+ {
146
+ method: "POST",
147
+ headers: { "Content-Type": "application/json" },
148
+ body: body,
149
+ }
150
+ );
151
+ return request;
152
+ };
153
+
154
+ export const updateDescription = async (description: string, uuid) => {
155
+ const body = { description: description };
156
+ const request: FetchResponse = await openmrsFetch(
157
+ `/ws/rest/v1/form/${uuid}`,
158
+ {
159
+ method: "POST",
160
+ headers: { "Content-Type": "application/json" },
161
+ body: body,
162
+ }
163
+ );
164
+ return request;
165
+ };
166
+
167
+ export const updateEncounterType = async (encounterTypeUUID: any, uuid) => {
168
+ const body = { encounterType: encounterTypeUUID };
169
+ const request: FetchResponse = await openmrsFetch(
170
+ `/ws/rest/v1/form/${uuid}`,
171
+ {
172
+ method: "POST",
173
+ headers: { "Content-Type": "application/json" },
174
+ body: body,
175
+ }
176
+ );
177
+ return request;
178
+ };
@@ -0,0 +1,20 @@
1
+ import useSWRImmutable from "swr/immutable";
2
+ import { openmrsFetch } from "@openmrs/esm-framework";
3
+ import { Form, Schema } from "../types";
4
+
5
+ export const useClobdata = (form?: Form) => {
6
+ const valueReference = form?.resources?.[0].valueReference;
7
+ const formHasResources = form?.resources.length > 0 && valueReference;
8
+ const CLOBDATA_URL = `/ws/rest/v1/clobdata/${valueReference}`;
9
+
10
+ const { data, error } = useSWRImmutable<{ data: Schema }, Error>(
11
+ formHasResources ? CLOBDATA_URL : null,
12
+ openmrsFetch
13
+ );
14
+
15
+ return {
16
+ clobdata: data?.data,
17
+ clobdataError: error || null,
18
+ isLoadingClobdata: (!data && !error) || false,
19
+ };
20
+ };
@@ -0,0 +1,18 @@
1
+ import useSWR from "swr";
2
+ import { openmrsFetch } from "@openmrs/esm-framework";
3
+ import { Concept } from "../types";
4
+
5
+ export function useConceptLookup(conceptId: string) {
6
+ const CONCEPT_LOOKUP_URL = `/ws/rest/v1/concept?q=${conceptId}&v=full`;
7
+
8
+ const { data, error } = useSWR<{ data: { results: Array<Concept> } }, Error>(
9
+ conceptId ? CONCEPT_LOOKUP_URL : null,
10
+ openmrsFetch
11
+ );
12
+
13
+ return {
14
+ concepts: data?.data?.results ?? [],
15
+ conceptLookupError: error || null,
16
+ isLoadingConcepts: (!data && !error) || false,
17
+ };
18
+ }
@@ -0,0 +1,18 @@
1
+ import useSWR from "swr";
2
+ import { openmrsFetch } from "@openmrs/esm-framework";
3
+
4
+ export function useConceptName(conceptId: string) {
5
+ const customRepresentation = "custom:(name:(display))";
6
+ const CONCEPT_LOOKUP_URL = `/ws/rest/v1/concept/${conceptId}?v=${customRepresentation}`;
7
+
8
+ const { data, error } = useSWR<
9
+ { data: { name: { display: string } } },
10
+ Error
11
+ >(conceptId ? CONCEPT_LOOKUP_URL : null, openmrsFetch);
12
+
13
+ return {
14
+ conceptName: data?.data?.name?.display ?? null,
15
+ conceptNameLookupError: error || null,
16
+ isLoadingConceptName: (!data && !error) || false,
17
+ };
18
+ }
@@ -0,0 +1,18 @@
1
+ import useSWRImmutable from "swr/immutable";
2
+ import { openmrsFetch } from "@openmrs/esm-framework";
3
+ import { EncounterType } from "../types";
4
+
5
+ export const useEncounterTypes = () => {
6
+ const ENCOUNTER_TYPES_URL = `/ws/rest/v1/encountertype?v=custom:(uuid,name)`;
7
+
8
+ const { data, error } = useSWRImmutable<
9
+ { data: { results: Array<EncounterType> } },
10
+ Error
11
+ >(ENCOUNTER_TYPES_URL, openmrsFetch);
12
+
13
+ return {
14
+ encounterTypes: data?.data?.results ?? [],
15
+ encounterTypesError: error || null,
16
+ isEncounterTypesLoading: (!data && !error) || false,
17
+ };
18
+ };
@@ -0,0 +1,18 @@
1
+ import useSWR from "swr/immutable";
2
+ import { openmrsFetch } from "@openmrs/esm-framework";
3
+ import { Form } from "../types";
4
+
5
+ export const useForm = (uuid: string) => {
6
+ const FORM_URL = `/ws/rest/v1/form/${uuid}?v=full`;
7
+
8
+ const { data, error } = useSWR<{ data: Form }, Error>(
9
+ uuid ? FORM_URL : null,
10
+ openmrsFetch
11
+ );
12
+
13
+ return {
14
+ form: data?.data,
15
+ formError: error || null,
16
+ isLoadingForm: (!data && !error) || false,
17
+ };
18
+ };
@@ -0,0 +1,20 @@
1
+ import useSWR from "swr";
2
+ import { openmrsFetch } from "@openmrs/esm-framework";
3
+ import { Form } from "../types";
4
+
5
+ export function useForms() {
6
+ const FORMS_URL =
7
+ "/ws/rest/v1/form?v=custom:(uuid,name,encounterType:(uuid,name),version,published,retired,resources:(uuid,name,dataType,valueReference))";
8
+
9
+ const { data, error, isValidating } = useSWR<
10
+ { data: { results: Array<Form> } },
11
+ Error
12
+ >(FORMS_URL, openmrsFetch);
13
+
14
+ return {
15
+ forms: data?.data?.results ?? [],
16
+ error: error,
17
+ isLoading: (!data && !error) || false,
18
+ isValidating,
19
+ };
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1,70 @@
1
+ import {
2
+ getAsyncLifecycle,
3
+ defineConfigSchema,
4
+ registerBreadcrumbs,
5
+ } from "@openmrs/esm-framework";
6
+ import { configSchema } from "./config-schema";
7
+
8
+ const importTranslation = require.context(
9
+ "../translations",
10
+ false,
11
+ /.json$/,
12
+ "lazy"
13
+ );
14
+
15
+ const backendDependencies = {
16
+ fhir2: "^1.2.0",
17
+ "webservices.rest": "^2.2.0",
18
+ };
19
+
20
+ function setupOpenMRS() {
21
+ const moduleName = "@openmrs/esm-form-builder-app";
22
+
23
+ const options = {
24
+ featureName: "form-builder",
25
+ moduleName,
26
+ };
27
+
28
+ defineConfigSchema(moduleName, configSchema);
29
+
30
+ registerBreadcrumbs([
31
+ {
32
+ path: `${window.spaBase}/form-builder`,
33
+ title: "Form Builder",
34
+ parent: `${window.spaBase}/home`,
35
+ },
36
+ {
37
+ path: `${window.spaBase}/form-builder/new`,
38
+ title: "Form Editor",
39
+ parent: `${window.spaBase}/form-builder`,
40
+ },
41
+ {
42
+ path: `${window.spaBase}/form-builder/edit/:uuid`,
43
+ title: "Form Editor",
44
+ parent: `${window.spaBase}/form-builder`,
45
+ },
46
+ ]);
47
+
48
+ return {
49
+ pages: [
50
+ {
51
+ load: getAsyncLifecycle(() => import("./root.component"), options),
52
+ route: "form-builder",
53
+ },
54
+ ],
55
+ extensions: [
56
+ {
57
+ id: "form-builder-app-menu-link",
58
+ slot: "app-menu-slot",
59
+ load: getAsyncLifecycle(
60
+ () => import("./form-builder-app-menu-link.component"),
61
+ options
62
+ ),
63
+ online: true,
64
+ offline: true,
65
+ },
66
+ ],
67
+ };
68
+ }
69
+
70
+ export { backendDependencies, importTranslation, setupOpenMRS };
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import { BrowserRouter, Route, Routes } from "react-router-dom";
3
+
4
+ import Dashboard from "./components/dashboard/dashboard.component";
5
+ import FormEditor from "./components/form-editor/form-editor.component";
6
+
7
+ const RootComponent: React.FC = () => {
8
+ return (
9
+ <BrowserRouter basename={`${window.spaBase}/form-builder`}>
10
+ <Routes>
11
+ <Route path="/" element={<Dashboard />} />
12
+ <Route path="/new" element={<FormEditor />} />
13
+ <Route path="/edit/:formUuid" element={<FormEditor />} />
14
+ </Routes>
15
+ </BrowserRouter>
16
+ );
17
+ };
18
+
19
+ export default RootComponent;
@@ -0,0 +1,11 @@
1
+ import "@testing-library/jest-dom/extend-expect";
2
+
3
+ declare global {
4
+ interface Window {
5
+ URL: {
6
+ createObjectURL: (blob: Blob) => string;
7
+ };
8
+ }
9
+ }
10
+
11
+ window.URL.createObjectURL = jest.fn();
@@ -0,0 +1,37 @@
1
+ import React from "react";
2
+ import { SWRConfig } from "swr";
3
+ import {
4
+ render,
5
+ screen,
6
+ waitForElementToBeRemoved,
7
+ } from "@testing-library/react";
8
+
9
+ // This component wraps whatever component is passed to it with an SWRConfig context which provides a global configuration for all SWR hooks.
10
+ const swrWrapper = ({ children }) => {
11
+ return (
12
+ <SWRConfig
13
+ value={{
14
+ // Sets the `dedupingInterval` to 0 - we don't need to dedupe requests in our test environment.
15
+ dedupingInterval: 0,
16
+ // Returns a new Map object, effectively wrapping our application with an empty cache provider. This is useful for resetting the SWR cache between test cases.
17
+ provider: () => new Map(),
18
+ }}
19
+ >
20
+ {children}
21
+ </SWRConfig>
22
+ );
23
+ };
24
+
25
+ // Render the provided component within the wrapper we created above
26
+ export const renderWithSwr = (ui, options?) =>
27
+ render(ui, { wrapper: swrWrapper, ...options });
28
+
29
+ // Helper function that waits for a loading state to disappear from the screen
30
+ export function waitForLoadingToFinish() {
31
+ return waitForElementToBeRemoved(
32
+ () => [...screen.queryAllByRole(/progressbar/i)],
33
+ {
34
+ timeout: 4000,
35
+ }
36
+ );
37
+ }
package/src/types.ts ADDED
@@ -0,0 +1,132 @@
1
+ export interface Form {
2
+ uuid: string;
3
+ name: string;
4
+ encounterType: EncounterType;
5
+ version: string;
6
+ description: string;
7
+ published: boolean;
8
+ retired: boolean;
9
+ resources: Array<Resource>;
10
+ }
11
+
12
+ export type RouteParams = { formUuid: string };
13
+
14
+ export interface FilterProps {
15
+ rowIds: Array<string>;
16
+ headers: Array<Record<string, string>>;
17
+ cellsById: any;
18
+ inputValue: string;
19
+ getCellId: (row, key) => string;
20
+ }
21
+
22
+ export interface EncounterType {
23
+ uuid: string;
24
+ name: string;
25
+ }
26
+
27
+ export interface Resource {
28
+ uuid: string;
29
+ name: string;
30
+ dataType: string;
31
+ valueReference: string;
32
+ }
33
+
34
+ export interface Schema {
35
+ name: string;
36
+ pages: any;
37
+ processor: string;
38
+ uuid: string;
39
+ encounterType: string;
40
+ referencedForms: any;
41
+ }
42
+
43
+ export interface ClobResponse {
44
+ body: any;
45
+ data: any;
46
+ headers: any;
47
+ statusText: any;
48
+ status: any;
49
+ }
50
+
51
+ export interface SchemaContextType {
52
+ schema: Schema;
53
+ setSchema: any;
54
+ }
55
+
56
+ export interface Page {
57
+ label: string;
58
+ sections: Array<Section>;
59
+ }
60
+
61
+ export interface Section {
62
+ label: string;
63
+ questions: Array<Question>;
64
+ isExpanded: string | boolean;
65
+ }
66
+
67
+ export interface Question {
68
+ label: string;
69
+ id: string;
70
+ type: string;
71
+ questionOptions: QuestionOptions;
72
+ required: string;
73
+ }
74
+
75
+ export interface QuestionOptions {
76
+ rendering: string;
77
+ answers: Array<Answer>;
78
+ max: string;
79
+ min: string;
80
+ concept: string;
81
+ conceptMappings: Array<ConceptMapping>;
82
+ weekList: [];
83
+ attributeType: string;
84
+ calculate: any;
85
+ rows: string;
86
+ orderSettingUuid: string;
87
+ orderType: string;
88
+ selectableOrders: Array<Answer>;
89
+ }
90
+
91
+ export interface Answer {
92
+ concept: string;
93
+ label: string;
94
+ }
95
+
96
+ export interface ConceptMapping {
97
+ type: string;
98
+ value: string;
99
+ }
100
+
101
+ export interface Concept {
102
+ uuid: string;
103
+ display: string;
104
+ mappings: Array<Mappings>;
105
+ answers: Array<ConceptAnswer>;
106
+ }
107
+
108
+ export interface ConceptAnswer {
109
+ uuid: string;
110
+ display: string;
111
+ }
112
+
113
+ export interface Mappings {
114
+ display: string;
115
+ }
116
+
117
+ export enum FieldTypes {
118
+ Date = "date",
119
+ Drug = "drug",
120
+ FieldSet = "field-set",
121
+ File = "file",
122
+ Group = "group",
123
+ MultiCheckbox = "multiCheckbox",
124
+ Number = "number",
125
+ Problem = "problem",
126
+ Radio = "radio",
127
+ Repeating = "repeating",
128
+ Select = "select",
129
+ Text = "text",
130
+ TextArea = "textarea",
131
+ UiSelectExtended = "ui-select-extended",
132
+ }