@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.
- package/.editorconfig +12 -0
- package/.eslintignore +1 -0
- package/.eslintrc +28 -0
- package/.github/workflows/codeql-analysis.yml +72 -0
- package/.github/workflows/node.js.yml +117 -0
- package/.husky/pre-commit +6 -0
- package/.husky/pre-push +6 -0
- package/.prettierignore +14 -0
- package/LICENSE +401 -0
- package/README.md +16 -0
- package/__mocks__/react-i18next.js +57 -0
- package/babel.config.json +8 -0
- package/jest.config.json +21 -0
- package/package.json +102 -0
- package/src/cohort-builder.resources.ts +84 -0
- package/src/cohort-builder.scss +126 -0
- package/src/cohort-builder.test.tsx +12 -0
- package/src/cohort-builder.tsx +213 -0
- package/src/cohort-builder.utils.ts +197 -0
- package/src/components/composition/composition.component.tsx +109 -0
- package/src/components/composition/composition.style.css +3 -0
- package/src/components/composition/composition.test.tsx +112 -0
- package/src/components/composition/composition.utils.ts +53 -0
- package/src/components/empty-data/empty-data.component.tsx +23 -0
- package/src/components/empty-data/empty-data.style.scss +20 -0
- package/src/components/saved-cohorts/saved-cohorts-options/saved-cohort-options.test.tsx +49 -0
- package/src/components/saved-cohorts/saved-cohorts-options/saved-cohorts-options.component.tsx +112 -0
- package/src/components/saved-cohorts/saved-cohorts.component.tsx +139 -0
- package/src/components/saved-cohorts/saved-cohorts.resources.ts +39 -0
- package/src/components/saved-cohorts/saved-cohorts.scss +15 -0
- package/src/components/saved-cohorts/saved-cohorts.test.tsx +51 -0
- package/src/components/saved-queries/saved-queries-options/saved-queries-options.component.tsx +112 -0
- package/src/components/saved-queries/saved-queries-options/saved-queries-options.test.tsx +47 -0
- package/src/components/saved-queries/saved-queries.component.tsx +139 -0
- package/src/components/saved-queries/saved-queries.resources.ts +39 -0
- package/src/components/saved-queries/saved-queries.scss +23 -0
- package/src/components/saved-queries/saved-queries.test.tsx +50 -0
- package/src/components/search-button-set/search-button-set.css +9 -0
- package/src/components/search-button-set/search-button-set.test.tsx +26 -0
- package/src/components/search-button-set/search-button-set.tsx +47 -0
- package/src/components/search-by-concepts/search-by-concepts.component.tsx +344 -0
- package/src/components/search-by-concepts/search-by-concepts.style.scss +48 -0
- package/src/components/search-by-concepts/search-by-concepts.test.tsx +129 -0
- package/src/components/search-by-concepts/search-concept/search-concept.component.tsx +130 -0
- package/src/components/search-by-concepts/search-concept/search-concept.resource.ts +53 -0
- package/src/components/search-by-concepts/search-concept/search-concept.style.css +25 -0
- package/src/components/search-by-concepts/search-concept/search-concept.test.tsx +89 -0
- package/src/components/search-by-demographics/search-by-demographics.component.tsx +209 -0
- package/src/components/search-by-demographics/search-by-demographics.style.scss +42 -0
- package/src/components/search-by-demographics/search-by-demographics.test.tsx +92 -0
- package/src/components/search-by-demographics/search-by-demographics.utils.ts +129 -0
- package/src/components/search-by-drug-orders/search-by-drug-orders.component.tsx +185 -0
- package/src/components/search-by-drug-orders/search-by-drug-orders.resources.ts +60 -0
- package/src/components/search-by-drug-orders/search-by-drug-orders.style.scss +4 -0
- package/src/components/search-by-drug-orders/search-by-drug-orders.test.tsx +159 -0
- package/src/components/search-by-drug-orders/search-by-drug-orders.utils.ts +118 -0
- package/src/components/search-by-encounters/search-by-encounters.component.tsx +188 -0
- package/src/components/search-by-encounters/search-by-encounters.resources.ts +51 -0
- package/src/components/search-by-encounters/search-by-encounters.style.scss +12 -0
- package/src/components/search-by-encounters/search-by-encounters.test.tsx +202 -0
- package/src/components/search-by-encounters/search-by-encounters.utils.ts +113 -0
- package/src/components/search-by-enrollments/search-by-enrollments.component.tsx +183 -0
- package/src/components/search-by-enrollments/search-by-enrollments.resources.ts +31 -0
- package/src/components/search-by-enrollments/search-by-enrollments.style.scss +4 -0
- package/src/components/search-by-enrollments/search-by-enrollments.test.tsx +158 -0
- package/src/components/search-by-enrollments/search-by-enrollments.utils.ts +69 -0
- package/src/components/search-by-location/search-by-location.component.tsx +97 -0
- package/src/components/search-by-location/search-by-location.style.scss +4 -0
- package/src/components/search-by-location/search-by-location.test.tsx +110 -0
- package/src/components/search-by-location/search-by-location.utils.ts +40 -0
- package/src/components/search-by-person-attributes/search-by-person-attributes.component.tsx +90 -0
- package/src/components/search-by-person-attributes/search-by-person-attributes.resource.ts +27 -0
- package/src/components/search-by-person-attributes/search-by-person-attributes.style.scss +4 -0
- package/src/components/search-by-person-attributes/search-by-person-attributes.test.tsx +133 -0
- package/src/components/search-by-person-attributes/search-by-person-attributes.utils.ts +42 -0
- package/src/components/search-history/search-history-options/search-history-options.component.tsx +290 -0
- package/src/components/search-history/search-history-options/search-history-options.resources.ts +29 -0
- package/src/components/search-history/search-history-options/search-history-options.test.tsx +144 -0
- package/src/components/search-history/search-history.component.tsx +174 -0
- package/src/components/search-history/search-history.style.scss +12 -0
- package/src/components/search-history/search-history.test.tsx +106 -0
- package/src/components/search-history/search-history.utils.ts +14 -0
- package/src/components/search-results-table/search-results-table.component.tsx +105 -0
- package/src/components/search-results-table/search-results-table.scss +4 -0
- package/src/components/search-results-table/search-results-table.test.tsx +47 -0
- package/src/declarations.d.tsx +2 -0
- package/src/index.ts +36 -0
- package/src/setup-tests.ts +1 -0
- package/src/types/index.ts +120 -0
- package/translations/en.json +119 -0
- package/tsconfig.json +23 -0
- 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,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
|
+
});
|
package/src/components/saved-cohorts/saved-cohorts-options/saved-cohorts-options.component.tsx
ADDED
|
@@ -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;
|