@openmrs/esm-cohort-builder-app 3.0.1-pre.80

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 (92) hide show
  1. package/.editorconfig +12 -0
  2. package/.eslintignore +1 -0
  3. package/.eslintrc +28 -0
  4. package/.github/workflows/codeql-analysis.yml +72 -0
  5. package/.github/workflows/node.js.yml +117 -0
  6. package/.husky/pre-commit +6 -0
  7. package/.husky/pre-push +6 -0
  8. package/.prettierignore +14 -0
  9. package/LICENSE +401 -0
  10. package/README.md +16 -0
  11. package/__mocks__/react-i18next.js +57 -0
  12. package/babel.config.json +8 -0
  13. package/jest.config.json +21 -0
  14. package/package.json +102 -0
  15. package/src/cohort-builder.resources.ts +84 -0
  16. package/src/cohort-builder.scss +126 -0
  17. package/src/cohort-builder.test.tsx +12 -0
  18. package/src/cohort-builder.tsx +213 -0
  19. package/src/cohort-builder.utils.ts +197 -0
  20. package/src/components/composition/composition.component.tsx +109 -0
  21. package/src/components/composition/composition.style.css +3 -0
  22. package/src/components/composition/composition.test.tsx +112 -0
  23. package/src/components/composition/composition.utils.ts +53 -0
  24. package/src/components/empty-data/empty-data.component.tsx +23 -0
  25. package/src/components/empty-data/empty-data.style.scss +20 -0
  26. package/src/components/saved-cohorts/saved-cohorts-options/saved-cohort-options.test.tsx +49 -0
  27. package/src/components/saved-cohorts/saved-cohorts-options/saved-cohorts-options.component.tsx +112 -0
  28. package/src/components/saved-cohorts/saved-cohorts.component.tsx +139 -0
  29. package/src/components/saved-cohorts/saved-cohorts.resources.ts +39 -0
  30. package/src/components/saved-cohorts/saved-cohorts.scss +15 -0
  31. package/src/components/saved-cohorts/saved-cohorts.test.tsx +51 -0
  32. package/src/components/saved-queries/saved-queries-options/saved-queries-options.component.tsx +112 -0
  33. package/src/components/saved-queries/saved-queries-options/saved-queries-options.test.tsx +47 -0
  34. package/src/components/saved-queries/saved-queries.component.tsx +139 -0
  35. package/src/components/saved-queries/saved-queries.resources.ts +39 -0
  36. package/src/components/saved-queries/saved-queries.scss +23 -0
  37. package/src/components/saved-queries/saved-queries.test.tsx +50 -0
  38. package/src/components/search-button-set/search-button-set.css +9 -0
  39. package/src/components/search-button-set/search-button-set.test.tsx +26 -0
  40. package/src/components/search-button-set/search-button-set.tsx +47 -0
  41. package/src/components/search-by-concepts/search-by-concepts.component.tsx +344 -0
  42. package/src/components/search-by-concepts/search-by-concepts.style.scss +48 -0
  43. package/src/components/search-by-concepts/search-by-concepts.test.tsx +129 -0
  44. package/src/components/search-by-concepts/search-concept/search-concept.component.tsx +130 -0
  45. package/src/components/search-by-concepts/search-concept/search-concept.resource.ts +53 -0
  46. package/src/components/search-by-concepts/search-concept/search-concept.style.css +25 -0
  47. package/src/components/search-by-concepts/search-concept/search-concept.test.tsx +89 -0
  48. package/src/components/search-by-demographics/search-by-demographics.component.tsx +209 -0
  49. package/src/components/search-by-demographics/search-by-demographics.style.scss +42 -0
  50. package/src/components/search-by-demographics/search-by-demographics.test.tsx +92 -0
  51. package/src/components/search-by-demographics/search-by-demographics.utils.ts +129 -0
  52. package/src/components/search-by-drug-orders/search-by-drug-orders.component.tsx +185 -0
  53. package/src/components/search-by-drug-orders/search-by-drug-orders.resources.ts +60 -0
  54. package/src/components/search-by-drug-orders/search-by-drug-orders.style.scss +4 -0
  55. package/src/components/search-by-drug-orders/search-by-drug-orders.test.tsx +159 -0
  56. package/src/components/search-by-drug-orders/search-by-drug-orders.utils.ts +118 -0
  57. package/src/components/search-by-encounters/search-by-encounters.component.tsx +188 -0
  58. package/src/components/search-by-encounters/search-by-encounters.resources.ts +51 -0
  59. package/src/components/search-by-encounters/search-by-encounters.style.scss +12 -0
  60. package/src/components/search-by-encounters/search-by-encounters.test.tsx +202 -0
  61. package/src/components/search-by-encounters/search-by-encounters.utils.ts +113 -0
  62. package/src/components/search-by-enrollments/search-by-enrollments.component.tsx +183 -0
  63. package/src/components/search-by-enrollments/search-by-enrollments.resources.ts +31 -0
  64. package/src/components/search-by-enrollments/search-by-enrollments.style.scss +4 -0
  65. package/src/components/search-by-enrollments/search-by-enrollments.test.tsx +158 -0
  66. package/src/components/search-by-enrollments/search-by-enrollments.utils.ts +69 -0
  67. package/src/components/search-by-location/search-by-location.component.tsx +97 -0
  68. package/src/components/search-by-location/search-by-location.style.scss +4 -0
  69. package/src/components/search-by-location/search-by-location.test.tsx +110 -0
  70. package/src/components/search-by-location/search-by-location.utils.ts +40 -0
  71. package/src/components/search-by-person-attributes/search-by-person-attributes.component.tsx +90 -0
  72. package/src/components/search-by-person-attributes/search-by-person-attributes.resource.ts +27 -0
  73. package/src/components/search-by-person-attributes/search-by-person-attributes.style.scss +4 -0
  74. package/src/components/search-by-person-attributes/search-by-person-attributes.test.tsx +133 -0
  75. package/src/components/search-by-person-attributes/search-by-person-attributes.utils.ts +42 -0
  76. package/src/components/search-history/search-history-options/search-history-options.component.tsx +290 -0
  77. package/src/components/search-history/search-history-options/search-history-options.resources.ts +29 -0
  78. package/src/components/search-history/search-history-options/search-history-options.test.tsx +144 -0
  79. package/src/components/search-history/search-history.component.tsx +174 -0
  80. package/src/components/search-history/search-history.style.scss +12 -0
  81. package/src/components/search-history/search-history.test.tsx +106 -0
  82. package/src/components/search-history/search-history.utils.ts +14 -0
  83. package/src/components/search-results-table/search-results-table.component.tsx +105 -0
  84. package/src/components/search-results-table/search-results-table.scss +4 -0
  85. package/src/components/search-results-table/search-results-table.test.tsx +47 -0
  86. package/src/declarations.d.tsx +2 -0
  87. package/src/index.ts +36 -0
  88. package/src/setup-tests.ts +1 -0
  89. package/src/types/index.ts +120 -0
  90. package/translations/en.json +119 -0
  91. package/tsconfig.json +23 -0
  92. package/webpack.config.js +3 -0
@@ -0,0 +1,197 @@
1
+ import { Column, Patient, Query } from "./types";
2
+
3
+ export const composeJson = (searchParameters) => {
4
+ const query: Query = {
5
+ type: "org.openmrs.module.reporting.dataset.definition.PatientDataSetDefinition",
6
+ columns: [],
7
+ rowFilters: [],
8
+ customRowFilterCombination: "",
9
+ };
10
+ query.columns = addColumnsToDisplay();
11
+ let counter = 0;
12
+ query.rowFilters = [];
13
+ for (let field in searchParameters) {
14
+ if (isNullValues(searchParameters[field])) {
15
+ delete searchParameters[field];
16
+ continue;
17
+ }
18
+ if (searchParameters[field] != "all" && searchParameters != "") {
19
+ query.rowFilters[counter] = {};
20
+ query.rowFilters[counter].key = getDefinitionLibraryKey(
21
+ field,
22
+ searchParameters[field]
23
+ );
24
+ }
25
+ if (Array.isArray(searchParameters[field])) {
26
+ query.rowFilters[counter].parameterValues = getParameterValues(
27
+ searchParameters[field]
28
+ );
29
+ }
30
+ if (
31
+ searchParameters[field].length >= 1 &&
32
+ searchParameters[field][0].livingStatus === "alive"
33
+ ) {
34
+ query.rowFilters[counter].livingStatus = "alive";
35
+ }
36
+ query.rowFilters[counter].type =
37
+ "org.openmrs.module.reporting.dataset.definition.PatientDataSetDefinition";
38
+ counter += 1;
39
+ }
40
+ query.customRowFilterCombination = composeFilterCombination(query.rowFilters);
41
+ return { query };
42
+ };
43
+
44
+ export const isNullValues = (fieldValues) => {
45
+ if (Array.isArray(fieldValues) && fieldValues.length >= 1) {
46
+ return !fieldValues[0].value;
47
+ }
48
+ return fieldValues === "all" || !fieldValues;
49
+ };
50
+
51
+ export const getDefinitionLibraryKey = (field: string, value: string) => {
52
+ let definitionLibraryKey = "reporting.library.cohortDefinition.builtIn";
53
+ switch (field) {
54
+ case "gender":
55
+ definitionLibraryKey += `.${value}`;
56
+ break;
57
+ default:
58
+ definitionLibraryKey += `.${field}`;
59
+ }
60
+ return definitionLibraryKey;
61
+ };
62
+
63
+ export const getParameterValues = (parameterFields) => {
64
+ const parameter = {};
65
+ parameterFields.forEach((eachParam) => {
66
+ parameter[eachParam.name] = eachParam.value;
67
+ });
68
+ return parameter;
69
+ };
70
+
71
+ export const composeFilterCombination = (filterColumns) => {
72
+ let compositionTitle = "";
73
+ const totalNumber = filterColumns.length;
74
+ for (let index = 1; index <= totalNumber; index++) {
75
+ if (filterColumns[index - 1].livingStatus === "alive") {
76
+ compositionTitle += `NOT ${index}`;
77
+ delete filterColumns[index - 1].livingStatus;
78
+ } else {
79
+ compositionTitle += `${index}`;
80
+ }
81
+ compositionTitle += index < totalNumber ? " AND " : "";
82
+ }
83
+ return compositionTitle;
84
+ };
85
+
86
+ export const addColumnsToDisplay = () => {
87
+ const columns = [
88
+ {
89
+ name: "firstname",
90
+ key: "preferredName.givenName",
91
+ },
92
+ {
93
+ name: "lastname",
94
+ key: "preferredName.familyName",
95
+ },
96
+ {
97
+ name: "gender",
98
+ key: "gender",
99
+ },
100
+ {
101
+ name: "age",
102
+ key: "ageOnDate.fullYears",
103
+ },
104
+ {
105
+ name: "patientId",
106
+ key: "patientId",
107
+ },
108
+ ];
109
+
110
+ const columnValues = columns.map((aColumn: Column) => {
111
+ aColumn.type =
112
+ "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition";
113
+ aColumn.key = `reporting.library.patientDataDefinition.builtIn.${aColumn.key}`;
114
+ return aColumn;
115
+ });
116
+ return columnValues;
117
+ };
118
+
119
+ export const addToHistory = (
120
+ description: string,
121
+ patients: Patient[],
122
+ parameters: {}
123
+ ) => {
124
+ const oldHistory = JSON.parse(
125
+ window.sessionStorage.getItem("openmrsHistory")
126
+ );
127
+ let newHistory = [];
128
+
129
+ if (oldHistory) {
130
+ newHistory = [...oldHistory, { description, patients, parameters }];
131
+ } else {
132
+ newHistory = [{ description, patients, parameters }];
133
+ }
134
+ window.sessionStorage.setItem("openmrsHistory", JSON.stringify(newHistory));
135
+ };
136
+
137
+ export const formatDate = (dateString: string) => {
138
+ const date = new Date(dateString);
139
+ const day = date.getDate();
140
+ const month = date.getMonth() + 1;
141
+ const year = date.getFullYear();
142
+ return `${day}/${month}/${year}`;
143
+ };
144
+
145
+ /**
146
+ * builds a query description based on query input
147
+ * @param {object} state the current state
148
+ * @param {string} conceptName the concept name
149
+ * @returns {string} date in the required format
150
+ */
151
+ export const queryDescriptionBuilder = (state, conceptName: string) => {
152
+ const { timeModifier, onOrAfter, onOrBefore } = state;
153
+
154
+ const onOrAfterDescription = onOrAfter
155
+ ? `since ${formatDate(onOrAfter)}`
156
+ : "";
157
+ const onOrBeforeDescription = onOrBefore
158
+ ? `until ${formatDate(onOrBefore)}`
159
+ : "";
160
+
161
+ return `Patients with ${timeModifier} ${conceptName} ${onOrAfterDescription} ${onOrBeforeDescription}`.trim();
162
+ };
163
+
164
+ const convertToCSV = (patients: Patient[]) => {
165
+ const csv =
166
+ "patient_id, full_name, age, gender\n" +
167
+ patients
168
+ .map((patient) => {
169
+ const orderedPatient = {
170
+ patientId: patient.patientId,
171
+ name: patient.name,
172
+ age: patient.age,
173
+ gender: patient.gender,
174
+ };
175
+
176
+ return Object.keys(orderedPatient)
177
+ .map((key) => {
178
+ return `"${patient[key]}"`;
179
+ })
180
+ .join(",");
181
+ })
182
+ .join("\n");
183
+
184
+ return csv;
185
+ };
186
+
187
+ export const downloadCSV = (data, filename) => {
188
+ const blob = new Blob([convertToCSV(data)], {
189
+ type: "text/csv;charset=utf-8;",
190
+ });
191
+ const url = URL.createObjectURL(blob);
192
+
193
+ const pom = document.createElement("a");
194
+ pom.href = url;
195
+ pom.setAttribute("download", filename);
196
+ pom.click();
197
+ };
@@ -0,0 +1,109 @@
1
+ import React, { useState } from "react";
2
+
3
+ import { TextInput } from "@carbon/react";
4
+ import { showNotification } from "@openmrs/esm-framework";
5
+ import { useTranslation } from "react-i18next";
6
+
7
+ import { SearchByProps } from "../../types";
8
+ import SearchButtonSet from "../search-button-set/search-button-set";
9
+ import styles from "./composition.style.css";
10
+ import {
11
+ createCompositionQuery,
12
+ isCompositionValid,
13
+ } from "./composition.utils";
14
+
15
+ const Composition: React.FC<SearchByProps> = ({ onSubmit }) => {
16
+ const [isLoading, setIsLoading] = useState(false);
17
+ const [compositionQuery, setCompositionQuery] = useState("");
18
+ const [description, setDescription] = useState("");
19
+ const { t } = useTranslation();
20
+
21
+ const handleResetInputs = () => {
22
+ setDescription("");
23
+ setCompositionQuery("");
24
+ };
25
+
26
+ const handleCompositionQuery = (composition: string) => {
27
+ setCompositionQuery(composition);
28
+ setDescription("Composition of " + composition);
29
+ };
30
+
31
+ const submit = async () => {
32
+ setIsLoading(true);
33
+ try {
34
+ if (isCompositionValid(compositionQuery)) {
35
+ const searchParams = createCompositionQuery(compositionQuery);
36
+ await onSubmit(searchParams, description);
37
+ } else {
38
+ showNotification({
39
+ title: t("error", "Error!"),
40
+ kind: "error",
41
+ critical: true,
42
+ description: t("invalidComposition", "Composition is not valid"),
43
+ });
44
+ }
45
+ setIsLoading(false);
46
+ } catch (error) {
47
+ setIsLoading(false);
48
+ showNotification({
49
+ title: t("error", "Error!"),
50
+ kind: "error",
51
+ critical: true,
52
+ description: t("invalidComposition", "Composition is not valid"),
53
+ });
54
+ }
55
+ };
56
+
57
+ return (
58
+ <>
59
+ <TextInput
60
+ data-modal-primary-focus
61
+ required
62
+ labelText={t("composition", "Composition")}
63
+ data-testid="composition-query"
64
+ id="composition-query"
65
+ onChange={(e) => handleCompositionQuery(e.target.value)}
66
+ value={compositionQuery}
67
+ />
68
+ <br />
69
+ <TextInput
70
+ data-modal-primary-focus
71
+ required
72
+ labelText={t("description", "Description")}
73
+ data-testid="composition-description"
74
+ id="composition-description"
75
+ onChange={(e) => setDescription(e.target.value)}
76
+ value={description}
77
+ />
78
+ <br />
79
+ <p className={styles.text}>
80
+ {t(
81
+ "compositionExplanationOne",
82
+ "A composition query combines together the results of multiple cohorts using the logical operators: AND, OR and NOT."
83
+ )}
84
+ </p>
85
+ <br />
86
+ <p className={styles.text}>
87
+ {t(
88
+ "compositionExplanationTwo",
89
+ "To use this query you need to already have query results in your search history. Those existing query results can then be combined to yield the results of the composition query."
90
+ )}
91
+ </p>
92
+ <br />
93
+ <p className={styles.text}>
94
+ {t(
95
+ "compositionExplanationThree",
96
+ "Example: if the search history #1 is a cohort of patients who are males, and if the search history #2 is a cohort of patients with ages between 23 and 35 years; then '1 AND 2' will result in a cohort of patients who are males with ages between 23 and 35 years."
97
+ )}
98
+ </p>
99
+ <br />
100
+ <SearchButtonSet
101
+ onHandleReset={handleResetInputs}
102
+ onHandleSubmit={submit}
103
+ isLoading={isLoading}
104
+ />
105
+ </>
106
+ );
107
+ };
108
+
109
+ export default Composition;
@@ -0,0 +1,3 @@
1
+ .text {
2
+ font-size: 0.85rem;
3
+ }
@@ -0,0 +1,112 @@
1
+ import React from "react";
2
+
3
+ import { render, cleanup, screen, waitFor } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+
6
+ import Composition from "./composition.component";
7
+
8
+ const mockCompositionQuery = {
9
+ query: {
10
+ type: "org.openmrs.module.reporting.dataset.definition.PatientDataSetDefinition",
11
+ columns: [
12
+ {
13
+ name: "firstname",
14
+ key: "reporting.library.patientDataDefinition.builtIn.preferredName.givenName",
15
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
16
+ },
17
+ {
18
+ name: "lastname",
19
+ key: "reporting.library.patientDataDefinition.builtIn.preferredName.familyName",
20
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
21
+ },
22
+ {
23
+ name: "gender",
24
+ key: "reporting.library.patientDataDefinition.builtIn.gender",
25
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
26
+ },
27
+ {
28
+ name: "age",
29
+ key: "reporting.library.patientDataDefinition.builtIn.ageOnDate.fullYears",
30
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
31
+ },
32
+ {
33
+ name: "patientId",
34
+ key: "reporting.library.patientDataDefinition.builtIn.patientId",
35
+ type: "org.openmrs.module.reporting.data.patient.definition.PatientDataDefinition",
36
+ },
37
+ ],
38
+ rowFilters: [
39
+ {
40
+ key: "reporting.library.cohortDefinition.builtIn.codedObsSearchAdvanced",
41
+ parameterValues: {
42
+ operator1: "LESS_THAN",
43
+ question: "163126AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
44
+ timeModifier: "NO",
45
+ },
46
+ type: "org.openmrs.module.reporting.dataset.definition.PatientDataSetDefinition",
47
+ },
48
+ {
49
+ key: "reporting.library.cohortDefinition.builtIn.encounterSearchAdvanced",
50
+ parameterValues: {
51
+ locationList: [
52
+ "1ce1b7d4-c865-4178-82b0-5932e51503d6",
53
+ "ba685651-ed3b-4e63-9b35-78893060758a",
54
+ "44c3efb0-2583-4c80-a79e-1f756a03c0a1",
55
+ ],
56
+ timeQualifier: "ANY",
57
+ },
58
+ type: "org.openmrs.module.reporting.dataset.definition.PatientDataSetDefinition",
59
+ },
60
+ ],
61
+ customRowFilterCombination: "(1) and (2)",
62
+ },
63
+ };
64
+
65
+ jest.mock("./composition.utils", () => {
66
+ const original = jest.requireActual("./composition.utils");
67
+ return {
68
+ ...original,
69
+ createCompositionQuery: jest
70
+ .fn()
71
+ .mockImplementation(() => mockCompositionQuery),
72
+ };
73
+ });
74
+
75
+ describe("Test the composition component", () => {
76
+ afterEach(() => {
77
+ cleanup();
78
+ jest.restoreAllMocks();
79
+ });
80
+
81
+ it("should be throw an error when an invalid composition query is entered", async () => {
82
+ const user = userEvent.setup();
83
+ const submit = jest.fn();
84
+ render(<Composition onSubmit={submit} />);
85
+
86
+ const compositionInput = screen.getByTestId("composition-query");
87
+ await user.click(compositionInput);
88
+ await waitFor(() => user.type(compositionInput, "random text"));
89
+
90
+ await waitFor(() => user.click(screen.getByTestId("search-btn")));
91
+ await waitFor(() => expect(submit).not.toBeCalled());
92
+ });
93
+
94
+ it("should be to search a composition query", async () => {
95
+ const compositionQuery = "1 and 2";
96
+
97
+ const user = userEvent.setup();
98
+ const submit = jest.fn();
99
+ render(<Composition onSubmit={submit} />);
100
+ const compositionInput = screen.getByTestId("composition-query");
101
+ await user.click(compositionInput);
102
+ await waitFor(() => user.type(compositionInput, compositionQuery));
103
+
104
+ await waitFor(() => user.click(screen.getByTestId("search-btn")));
105
+ await waitFor(() =>
106
+ expect(submit).toBeCalledWith(
107
+ mockCompositionQuery,
108
+ `Composition of ${compositionQuery}`
109
+ )
110
+ );
111
+ });
112
+ });
@@ -0,0 +1,53 @@
1
+ import { addColumnsToDisplay } from "../../cohort-builder.utils";
2
+ import { Query } from "../../types";
3
+
4
+ export const isCompositionValid = (search: string) => {
5
+ return (
6
+ search.match(/and|or|not|\d+|\)|\(|union|intersection|\!|\+/gi).length ===
7
+ search.split(/\s+/g).length
8
+ );
9
+ };
10
+
11
+ const formatFilterCombination = (
12
+ filterText: string,
13
+ numberOfSearches: number
14
+ ) => {
15
+ return filterText.replace(/\d/, (theDigit) =>
16
+ (parseInt(theDigit) + numberOfSearches).toString()
17
+ );
18
+ };
19
+
20
+ export const createCompositionQuery = (compositionQuery: string) => {
21
+ const search = compositionQuery.replace(/(\(|\))+/g, (char) =>
22
+ char === "(" ? "( " : " )"
23
+ );
24
+ const query: Query = {
25
+ type: "org.openmrs.module.reporting.dataset.definition.PatientDataSetDefinition",
26
+ columns: addColumnsToDisplay(),
27
+ rowFilters: [],
28
+ customRowFilterCombination: "",
29
+ };
30
+
31
+ const searchTokens = search.split(/\s+/);
32
+
33
+ searchTokens.forEach((eachToken) => {
34
+ if (eachToken.match(/\d/)) {
35
+ const history = JSON.parse(
36
+ window.sessionStorage.getItem("openmrsHistory")
37
+ );
38
+ const operandQuery = history[parseInt(eachToken) - 1];
39
+
40
+ const jsonRequestObject = operandQuery.parameters;
41
+ jsonRequestObject.customRowFilterCombination = formatFilterCombination(
42
+ jsonRequestObject.customRowFilterCombination,
43
+ query.rowFilters.length
44
+ );
45
+ query.customRowFilterCombination += `(${jsonRequestObject.customRowFilterCombination})`;
46
+ query.rowFilters = query.rowFilters.concat(jsonRequestObject.rowFilters);
47
+ } else {
48
+ query.customRowFilterCombination += ` ${eachToken} `;
49
+ }
50
+ });
51
+
52
+ return { query };
53
+ };
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+
3
+ import { Tile } from "@carbon/react";
4
+ import { EmptyDataIllustration } from "@openmrs/esm-patient-common-lib/src/empty-state/index";
5
+
6
+ import styles from "./empty-data.style.scss";
7
+
8
+ export interface EmptyDataProps {
9
+ displayText: string;
10
+ }
11
+
12
+ const EmptyData: React.FC<EmptyDataProps> = (props) => {
13
+ return (
14
+ <Tile className={styles.tile}>
15
+ <EmptyDataIllustration />
16
+ <p className={styles.content}>
17
+ There are no {props.displayText.toLowerCase()} to display
18
+ </p>
19
+ </Tile>
20
+ );
21
+ };
22
+
23
+ export default EmptyData;
@@ -0,0 +1,20 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ // @use '@carbon/colors';
3
+ @use '@carbon/styles/scss/type';
4
+ @import '~@openmrs/esm-styleguide/src/vars';
5
+
6
+ .action {
7
+ margin-bottom: spacing.$spacing-03;
8
+ }
9
+
10
+ .content {
11
+ @include type.type-style("productive-heading-01");
12
+ color: $text-02;
13
+ margin-top: spacing.$spacing-05;
14
+ margin-bottom: spacing.$spacing-03;
15
+ }
16
+
17
+ .tile {
18
+ text-align: center;
19
+ border: 1px solid $ui-03;
20
+ }
@@ -0,0 +1,49 @@
1
+ import React from "react";
2
+
3
+ import { screen, render, cleanup, waitFor } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+
6
+ import { DefinitionDataRow } from "../../../types";
7
+ import SavedCohortsOptions from "./saved-cohorts-options.component";
8
+
9
+ const cohort: DefinitionDataRow = {
10
+ id: "1",
11
+ name: "Female Patients",
12
+ description: "Female Patients that are alive",
13
+ };
14
+
15
+ describe("Test the saved cohorts options", () => {
16
+ afterEach(cleanup);
17
+ it("should be able to view the saved cohort", async () => {
18
+ const user = userEvent.setup();
19
+ const onViewCohort = jest.fn();
20
+ render(
21
+ <SavedCohortsOptions
22
+ cohort={cohort}
23
+ onViewCohort={onViewCohort}
24
+ onDeleteCohort={jest.fn()}
25
+ />
26
+ );
27
+
28
+ await waitFor(() => user.click(screen.getByTestId("options")));
29
+ await waitFor(() => user.click(screen.getByTestId("view")));
30
+ expect(onViewCohort).toBeCalledWith(cohort.id);
31
+ });
32
+
33
+ it("should be able delete a cohort", async () => {
34
+ const user = userEvent.setup();
35
+ const onDeleteCohort = jest.fn();
36
+ render(
37
+ <SavedCohortsOptions
38
+ cohort={cohort}
39
+ onViewCohort={jest.fn()}
40
+ onDeleteCohort={onDeleteCohort}
41
+ />
42
+ );
43
+
44
+ await waitFor(() => user.click(screen.getByTestId("options")));
45
+ await waitFor(() => user.click(screen.getByTestId("delete")));
46
+ await waitFor(() => user.click(screen.getByText("Delete")));
47
+ expect(onDeleteCohort).toBeCalledWith(cohort.id);
48
+ });
49
+ });
@@ -0,0 +1,112 @@
1
+ import React, { useState } from "react";
2
+
3
+ import {
4
+ ComposedModal,
5
+ ModalFooter,
6
+ ModalHeader,
7
+ OverflowMenu,
8
+ OverflowMenuItem,
9
+ } from "@carbon/react";
10
+ import { showToast } from "@openmrs/esm-framework";
11
+ import { useTranslation } from "react-i18next";
12
+
13
+ import { DefinitionDataRow } from "../../../types";
14
+
15
+ enum Options {
16
+ VIEW,
17
+ DELETE,
18
+ }
19
+
20
+ interface SavedCohortsOptionsProps {
21
+ cohort: DefinitionDataRow;
22
+ onViewCohort: (cohortId: string) => Promise<void>;
23
+ onDeleteCohort: (cohortId: string) => Promise<void>;
24
+ }
25
+
26
+ const SavedCohortsOptions: React.FC<SavedCohortsOptionsProps> = ({
27
+ cohort,
28
+ onViewCohort,
29
+ onDeleteCohort,
30
+ }) => {
31
+ const { t } = useTranslation();
32
+ const [isDeleteCohortModalVisible, setIsDeleteCohortModalVisible] =
33
+ useState(false);
34
+
35
+ const handleViewCohort = async () => {
36
+ try {
37
+ await onViewCohort(cohort.id);
38
+ } catch (error) {
39
+ showToast({
40
+ title: t("cohortViewError", "Error viewing the cohort"),
41
+ kind: "error",
42
+ critical: true,
43
+ description: error?.message,
44
+ });
45
+ }
46
+ };
47
+
48
+ const handleOption = async (option: Options) => {
49
+ switch (option) {
50
+ case Options.VIEW:
51
+ handleViewCohort();
52
+ break;
53
+ case Options.DELETE:
54
+ setIsDeleteCohortModalVisible(true);
55
+ break;
56
+ }
57
+ };
58
+
59
+ const handleDeleteCohort = async () => {
60
+ await onDeleteCohort(cohort.id);
61
+ setIsDeleteCohortModalVisible(false);
62
+ };
63
+
64
+ return (
65
+ <>
66
+ <OverflowMenu
67
+ ariaLabel="overflow-menu"
68
+ size="md"
69
+ flipped
70
+ direction="bottom"
71
+ data-testid="options"
72
+ >
73
+ <OverflowMenuItem
74
+ data-testid="view"
75
+ itemText={t("view", "View")}
76
+ onClick={() => handleOption(Options.VIEW)}
77
+ />
78
+ <OverflowMenuItem
79
+ data-testid="delete"
80
+ itemText={t("delete", "Delete")}
81
+ onClick={() => handleOption(Options.DELETE)}
82
+ />
83
+ </OverflowMenu>
84
+
85
+ <ComposedModal
86
+ size={"sm"}
87
+ open={isDeleteCohortModalVisible}
88
+ onClose={() => setIsDeleteCohortModalVisible(false)}
89
+ >
90
+ <ModalHeader>
91
+ <p>
92
+ {t(
93
+ "deleteItem",
94
+ `Are you sure you want to delete ${cohort?.name}?`,
95
+ {
96
+ itemName: cohort?.name,
97
+ }
98
+ )}
99
+ </p>
100
+ </ModalHeader>
101
+ <ModalFooter
102
+ danger
103
+ onRequestSubmit={handleDeleteCohort}
104
+ primaryButtonText={t("delete", "Delete")}
105
+ secondaryButtonText={t("cancel", "Cancel")}
106
+ />
107
+ </ComposedModal>
108
+ </>
109
+ );
110
+ };
111
+
112
+ export default SavedCohortsOptions;