@openmrs/esm-fast-data-entry-app 1.0.1-pre.10 → 1.0.1-pre.17
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/dist/132.js +1 -0
- package/dist/247.js +1 -0
- package/dist/255.js +1 -0
- package/dist/294.js +2 -0
- package/dist/294.js.LICENSE.txt +9 -0
- package/dist/32.js +1 -0
- package/dist/327.js +1 -0
- package/dist/403.js +2 -0
- package/dist/403.js.LICENSE.txt +14 -0
- package/dist/553.js +2 -0
- package/dist/553.js.LICENSE.txt +14 -0
- package/dist/569.js +2 -0
- package/dist/569.js.LICENSE.txt +27 -0
- package/dist/574.js +1 -0
- package/dist/595.js +2 -0
- package/dist/595.js.LICENSE.txt +1 -0
- package/dist/617.js +1 -0
- package/dist/68.js +2 -0
- package/dist/68.js.LICENSE.txt +21 -0
- package/dist/776.js +1 -0
- package/dist/804.js +1 -0
- package/dist/820.js +1 -0
- package/dist/906.js +1 -0
- package/dist/935.js +2 -0
- package/dist/935.js.LICENSE.txt +19 -0
- package/dist/main.js +1 -0
- package/dist/openmrs-esm-fast-data-entry-app.js +1 -1
- package/dist/openmrs-esm-fast-data-entry-app.js.buildmanifest.json +544 -0
- package/dist/openmrs-esm-fast-data-entry-app.old +1 -0
- package/package.json +5 -3
- package/src/add-group-modal/AddGroupModal.tsx +80 -27
- package/src/add-group-modal/styles.scss +14 -4
- package/src/context/GroupFormWorkflowContext.tsx +2 -0
- package/src/context/GroupFormWorkflowReducer.ts +26 -2
- package/src/group-form-entry-workflow/GroupFormEntryWorkflow.tsx +2 -2
- package/src/group-form-entry-workflow/{group-banner/GroupBanner.test.tsx → group-display-header/GroupDisplayHeader.test.tsx} +2 -2
- package/src/group-form-entry-workflow/{group-banner/GroupBanner.tsx → group-display-header/GroupDisplayHeader.tsx} +31 -5
- package/src/group-form-entry-workflow/group-display-header/index.ts +3 -0
- package/src/group-form-entry-workflow/{group-banner → group-display-header}/styles.scss +0 -0
- package/src/group-form-entry-workflow/group-search/CompactGroupResults.tsx +61 -28
- package/src/group-form-entry-workflow/group-search/CompactGroupSearch.tsx +5 -0
- package/src/group-form-entry-workflow/group-search/GroupSearch.tsx +65 -8
- package/src/group-form-entry-workflow/group-search/group-search.scss +8 -6
- package/src/group-form-entry-workflow/group-search-header/GroupSearchHeader.tsx +14 -7
- package/src/group-form-entry-workflow/styles.scss +10 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/usePostEndpoint.ts +70 -0
- package/src/hooks/useSearchEndpoint.ts +120 -0
- package/translations/en.json +6 -1
- package/.editorconfig +0 -12
- package/.eslintignore +0 -2
- package/.eslintrc.js +0 -10
- package/.husky/pre-push +0 -1
- package/.prettierignore +0 -14
- package/.tx/config +0 -9
- package/.yarn/plugins/@yarnpkg/plugin-version.cjs +0 -550
- package/.yarn/versions/45b499b6.yml +0 -0
- package/src/group-form-entry-workflow/group-banner/index.ts +0 -3
- package/src/group-form-entry-workflow/group-search/mock-group-data.ts +0 -79
- package/src/group-form-entry-workflow/group-search/useGroupSearch.ts +0 -14
- package/src/hooks/usePostCohort.ts +0 -18
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useContext, useState } from "react";
|
|
1
|
+
import React, { useCallback, useContext, useEffect, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
ComposedModal,
|
|
4
4
|
Button,
|
|
@@ -7,12 +7,14 @@ import {
|
|
|
7
7
|
ModalBody,
|
|
8
8
|
TextInput,
|
|
9
9
|
FormLabel,
|
|
10
|
+
Loading,
|
|
10
11
|
} from "@carbon/react";
|
|
11
|
-
import { Add,
|
|
12
|
+
import { Add, TrashCan } from "@carbon/react/icons";
|
|
12
13
|
import { useTranslation } from "react-i18next";
|
|
13
|
-
import { ExtensionSlot } from "@openmrs/esm-framework";
|
|
14
|
+
import { ExtensionSlot, showToast } from "@openmrs/esm-framework";
|
|
14
15
|
import styles from "./styles.scss";
|
|
15
16
|
import GroupFormWorkflowContext from "../context/GroupFormWorkflowContext";
|
|
17
|
+
import { usePostCohort } from "../hooks";
|
|
16
18
|
|
|
17
19
|
const MemExtension = React.memo(ExtensionSlot);
|
|
18
20
|
|
|
@@ -20,18 +22,19 @@ const PatientRow = ({ patient, removePatient }) => {
|
|
|
20
22
|
const { t } = useTranslation();
|
|
21
23
|
return (
|
|
22
24
|
<li key={patient.uuid} className={styles.patientRow}>
|
|
23
|
-
<span className={styles.patientName}>{patient?.display}</span>
|
|
24
25
|
<span>
|
|
25
26
|
<Button
|
|
26
27
|
kind="tertiary"
|
|
27
28
|
size="sm"
|
|
29
|
+
hasIconOnly
|
|
28
30
|
onClick={() => removePatient(patient.uuid)}
|
|
29
|
-
renderIcon={
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{t("remove", "Remove")}
|
|
33
|
-
|
|
31
|
+
renderIcon={TrashCan}
|
|
32
|
+
tooltipAlignment="start"
|
|
33
|
+
tooltipPosition="top"
|
|
34
|
+
iconDescription={t("remove", "Remove")}
|
|
35
|
+
/>
|
|
34
36
|
</span>
|
|
37
|
+
<span className={styles.patientName}>{patient?.display}</span>
|
|
35
38
|
</li>
|
|
36
39
|
);
|
|
37
40
|
};
|
|
@@ -64,17 +67,21 @@ const NewGroupForm = (props) => {
|
|
|
64
67
|
/>
|
|
65
68
|
{errors?.name && (
|
|
66
69
|
<p className={styles.formError}>
|
|
67
|
-
{
|
|
70
|
+
{errors.name === "required"
|
|
71
|
+
? t("groupNameError", "Please enter a group name.")
|
|
72
|
+
: errors.name}
|
|
68
73
|
</p>
|
|
69
74
|
)}
|
|
70
|
-
<FormLabel>
|
|
75
|
+
<FormLabel>
|
|
76
|
+
{patientList.length} {t("patientsInGroup", "Patients in group")}
|
|
77
|
+
</FormLabel>
|
|
71
78
|
{errors?.patientList && (
|
|
72
79
|
<p className={styles.formError}>
|
|
73
80
|
{t("noPatientError", "Please enter at least one patient.")}
|
|
74
81
|
</p>
|
|
75
82
|
)}
|
|
76
83
|
{!errors?.patientList && (
|
|
77
|
-
<ul>
|
|
84
|
+
<ul className={styles.patientList}>
|
|
78
85
|
{patientList?.map((patient, index) => (
|
|
79
86
|
<PatientRow
|
|
80
87
|
patient={patient}
|
|
@@ -92,7 +99,7 @@ const NewGroupForm = (props) => {
|
|
|
92
99
|
state={{
|
|
93
100
|
selectPatientAction: updatePatientList,
|
|
94
101
|
buttonProps: {
|
|
95
|
-
kind: "
|
|
102
|
+
kind: "secondary",
|
|
96
103
|
},
|
|
97
104
|
}}
|
|
98
105
|
/>
|
|
@@ -108,6 +115,7 @@ const AddGroupModal = () => {
|
|
|
108
115
|
const [errors, setErrors] = useState({});
|
|
109
116
|
const [name, setName] = useState("");
|
|
110
117
|
const [patientList, setPatientList] = useState([]);
|
|
118
|
+
const { post, result, isPosting, error } = usePostCohort();
|
|
111
119
|
|
|
112
120
|
const handleCancel = () => {
|
|
113
121
|
setOpen(false);
|
|
@@ -165,10 +173,46 @@ const AddGroupModal = () => {
|
|
|
165
173
|
|
|
166
174
|
const handleSubmit = () => {
|
|
167
175
|
if (validate()) {
|
|
168
|
-
|
|
176
|
+
post({
|
|
177
|
+
name: name,
|
|
178
|
+
cohortMembers: patientList.map((p) => ({ patient: p.uuid })),
|
|
179
|
+
});
|
|
169
180
|
}
|
|
170
181
|
};
|
|
171
182
|
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (result) {
|
|
185
|
+
setGroup({
|
|
186
|
+
...result,
|
|
187
|
+
// the result doesn't come with cohortMembers.
|
|
188
|
+
// need to add it in based on our local state
|
|
189
|
+
cohortMembers: patientList.map((p) => ({ patient: { uuid: p.uuid } })),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}, [result, setGroup, patientList]);
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (error) {
|
|
196
|
+
showToast({
|
|
197
|
+
kind: "error",
|
|
198
|
+
title: t("postError", "POST Error"),
|
|
199
|
+
description:
|
|
200
|
+
error.message ??
|
|
201
|
+
t("unknownPostError", "An unknown error occured while saving data"),
|
|
202
|
+
});
|
|
203
|
+
if (error.fieldErrors) {
|
|
204
|
+
setErrors(
|
|
205
|
+
Object.fromEntries(
|
|
206
|
+
Object.entries(error.fieldErrors).map(([key, value]) => [
|
|
207
|
+
key,
|
|
208
|
+
value?.[0]?.message,
|
|
209
|
+
])
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}, [error, t]);
|
|
215
|
+
|
|
172
216
|
return (
|
|
173
217
|
<div className={styles.modal}>
|
|
174
218
|
<Button
|
|
@@ -181,23 +225,32 @@ const AddGroupModal = () => {
|
|
|
181
225
|
<ComposedModal open={open} onClose={() => setOpen(false)}>
|
|
182
226
|
<ModalHeader>{t("createNewGroup", "Create New Group")}</ModalHeader>
|
|
183
227
|
<ModalBody>
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
228
|
+
{result ? (
|
|
229
|
+
<p>Group saved succesfully</p>
|
|
230
|
+
) : isPosting ? (
|
|
231
|
+
<div className={styles.loading}>
|
|
232
|
+
<Loading withOverlay={false} />
|
|
233
|
+
<span>Saving new group...</span>
|
|
234
|
+
</div>
|
|
235
|
+
) : (
|
|
236
|
+
<NewGroupForm
|
|
237
|
+
{...{
|
|
238
|
+
name,
|
|
239
|
+
setName,
|
|
240
|
+
patientList,
|
|
241
|
+
updatePatientList,
|
|
242
|
+
errors,
|
|
243
|
+
validate,
|
|
244
|
+
removePatient,
|
|
245
|
+
}}
|
|
246
|
+
/>
|
|
247
|
+
)}
|
|
195
248
|
</ModalBody>
|
|
196
249
|
<ModalFooter>
|
|
197
|
-
<Button kind="secondary" onClick={handleCancel}>
|
|
250
|
+
<Button kind="secondary" onClick={handleCancel} disabled={isPosting}>
|
|
198
251
|
{t("cancel", "Cancel")}
|
|
199
252
|
</Button>
|
|
200
|
-
<Button kind="primary" onClick={handleSubmit}>
|
|
253
|
+
<Button kind="primary" onClick={handleSubmit} disabled={isPosting}>
|
|
201
254
|
{t("createGroup", "Create Group")}
|
|
202
255
|
</Button>
|
|
203
256
|
</ModalFooter>
|
|
@@ -23,13 +23,23 @@
|
|
|
23
23
|
|
|
24
24
|
.patientRow {
|
|
25
25
|
display: flex;
|
|
26
|
+
align-items: center;
|
|
26
27
|
width: "100%";
|
|
28
|
+
&:nth-child(odd) {
|
|
29
|
+
background-color: colors.$gray-20;
|
|
30
|
+
}
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
.patientName {
|
|
30
34
|
flex-grow: 1;
|
|
31
|
-
padding: spacing.$spacing-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
padding-left: spacing.$spacing-05;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.loading {
|
|
39
|
+
display: flex;
|
|
40
|
+
height: 100%;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
align-items: center;
|
|
44
|
+
row-gap: spacing.$spacing-05;
|
|
35
45
|
}
|
|
@@ -20,6 +20,7 @@ export interface MetaType {
|
|
|
20
20
|
|
|
21
21
|
const initialActions = {
|
|
22
22
|
setGroup: (group: GroupType) => undefined,
|
|
23
|
+
unsetGroup: () => undefined,
|
|
23
24
|
setSessionMeta: (meta: MetaType) => undefined,
|
|
24
25
|
openPatientSearch: () => undefined,
|
|
25
26
|
saveEncounter: (encounterUuid: string | number) => undefined,
|
|
@@ -75,6 +76,7 @@ const GroupFormWorkflowProvider = ({ children }) => {
|
|
|
75
76
|
activeFormUuid,
|
|
76
77
|
}),
|
|
77
78
|
setGroup: (group) => dispatch({ type: "SET_GROUP", group }),
|
|
79
|
+
unsetGroup: () => dispatch({ type: "UNSET_GROUP" }),
|
|
78
80
|
setSessionMeta: (meta) => dispatch({ type: "SET_SESSION_META", meta }),
|
|
79
81
|
openPatientSearch: () => dispatch({ type: "OPEN_PATIENT_SEARCH" }),
|
|
80
82
|
saveEncounter: (encounterUuid) =>
|
|
@@ -75,9 +75,33 @@ const reducer = (state, action) => {
|
|
|
75
75
|
...state.forms,
|
|
76
76
|
[state.activeFormUuid]: {
|
|
77
77
|
...state.forms[state.activeFormUuid],
|
|
78
|
-
groupUuid: action.group.
|
|
78
|
+
groupUuid: action.group.uuid,
|
|
79
79
|
groupName: action.group.name,
|
|
80
|
-
patientUuids:
|
|
80
|
+
patientUuids:
|
|
81
|
+
// this translation is not preferred
|
|
82
|
+
// the only reason we tollerate it here is beause it should be the only time
|
|
83
|
+
// we add cohort information to state
|
|
84
|
+
action.group.cohortMembers?.map(
|
|
85
|
+
(member) => member?.patient?.uuid
|
|
86
|
+
) ?? [],
|
|
87
|
+
activePatientUuid: null,
|
|
88
|
+
activeEncounterUuid: null,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
persistData(newState);
|
|
93
|
+
return newState;
|
|
94
|
+
}
|
|
95
|
+
case "UNSET_GROUP": {
|
|
96
|
+
const newState = {
|
|
97
|
+
...state,
|
|
98
|
+
forms: {
|
|
99
|
+
...state.forms,
|
|
100
|
+
[state.activeFormUuid]: {
|
|
101
|
+
...state.forms[state.activeFormUuid],
|
|
102
|
+
groupUuid: null,
|
|
103
|
+
groupName: null,
|
|
104
|
+
patientUuids: [],
|
|
81
105
|
activePatientUuid: null,
|
|
82
106
|
activeEncounterUuid: null,
|
|
83
107
|
},
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import React, { useContext, useEffect, useState } from "react";
|
|
20
20
|
import { useNavigate } from "react-router-dom";
|
|
21
21
|
import PatientCard from "../patient-card/PatientCard";
|
|
22
|
-
import
|
|
22
|
+
import GroupDisplayHeader from "./group-display-header";
|
|
23
23
|
import styles from "./styles.scss";
|
|
24
24
|
import { useTranslation } from "react-i18next";
|
|
25
25
|
import GroupFormWorkflowContext, {
|
|
@@ -387,7 +387,7 @@ const GroupFormEntryWorkflow = () => {
|
|
|
387
387
|
<ExtensionSlot extensionSlotName="breadcrumbs-slot" />
|
|
388
388
|
</div>
|
|
389
389
|
<GroupSearchHeader />
|
|
390
|
-
<
|
|
390
|
+
<GroupDisplayHeader />
|
|
391
391
|
{workflowState === "NEW_GROUP_SESSION" && (
|
|
392
392
|
<div className={styles.workspaceWrapper}>
|
|
393
393
|
<SessionMetaWorkspace />
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { render } from "@testing-library/react";
|
|
3
|
-
import
|
|
3
|
+
import GroupDisplayHeader from "./GroupDisplayHeader";
|
|
4
4
|
|
|
5
5
|
describe("PatientBanner", () => {
|
|
6
6
|
it("renders placeholder information when no data is present", () => {
|
|
7
|
-
render(<
|
|
7
|
+
render(<GroupDisplayHeader />);
|
|
8
8
|
});
|
|
9
9
|
});
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import React, { useContext } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { Button } from "@carbon/react";
|
|
3
|
+
import { Events, Close } from "@carbon/react/icons";
|
|
3
4
|
import styles from "./styles.scss";
|
|
4
5
|
import { useTranslation } from "react-i18next";
|
|
5
6
|
import GroupFormWorkflowContext from "../../context/GroupFormWorkflowContext";
|
|
7
|
+
import { navigate } from "@openmrs/esm-framework";
|
|
6
8
|
|
|
7
|
-
const
|
|
8
|
-
const {
|
|
9
|
-
|
|
9
|
+
const GroupDisplayHeader = () => {
|
|
10
|
+
const {
|
|
11
|
+
activeGroupName,
|
|
12
|
+
activeGroupUuid,
|
|
13
|
+
patientUuids,
|
|
14
|
+
activeSessionMeta,
|
|
15
|
+
unsetGroup,
|
|
16
|
+
destroySession,
|
|
17
|
+
} = useContext(GroupFormWorkflowContext);
|
|
10
18
|
const { t } = useTranslation();
|
|
11
19
|
|
|
12
20
|
if (!activeGroupUuid) {
|
|
@@ -38,8 +46,26 @@ const GroupBanner = () => {
|
|
|
38
46
|
</div>
|
|
39
47
|
</div>
|
|
40
48
|
)}
|
|
49
|
+
<span style={{ flexGrow: 1 }} />
|
|
50
|
+
<span>
|
|
51
|
+
<Button kind="ghost" onClick={() => unsetGroup()}>
|
|
52
|
+
{t("changeGroup", "Choose a different group")} <Close size={20} />
|
|
53
|
+
</Button>
|
|
54
|
+
</span>
|
|
55
|
+
<span>
|
|
56
|
+
<Button
|
|
57
|
+
kind="ghost"
|
|
58
|
+
onClick={() => {
|
|
59
|
+
destroySession();
|
|
60
|
+
// eslint-disable-next-line
|
|
61
|
+
navigate({ to: "${openmrsSpaBase}/forms" });
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
{t("cancel", "Cancel")} <Close size={20} />
|
|
65
|
+
</Button>
|
|
66
|
+
</span>
|
|
41
67
|
</div>
|
|
42
68
|
);
|
|
43
69
|
};
|
|
44
70
|
|
|
45
|
-
export default
|
|
71
|
+
export default GroupDisplayHeader;
|
|
File without changes
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useReducer } from "react";
|
|
1
|
+
import React, { useEffect, useReducer, useRef } from "react";
|
|
2
2
|
import { SkeletonIcon, SkeletonText } from "@carbon/react";
|
|
3
3
|
import { Events } from "@carbon/react/icons";
|
|
4
4
|
import styles from "./compact-group-result.scss";
|
|
@@ -20,11 +20,64 @@ const reducer = (state, action) => {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
const
|
|
23
|
+
const scrollingOptions = {
|
|
24
|
+
behavior: "smooth",
|
|
25
|
+
block: "nearest",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ResultItem = ({
|
|
29
|
+
index,
|
|
30
|
+
selectGroupAction,
|
|
31
|
+
group,
|
|
32
|
+
dispatch,
|
|
33
|
+
state,
|
|
34
|
+
totalGroups,
|
|
35
|
+
lastRef,
|
|
36
|
+
}) => {
|
|
37
|
+
const ref = useRef(null);
|
|
38
|
+
const { t } = useTranslation();
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (state.selectedIndex === totalGroups - 1) {
|
|
42
|
+
lastRef.current.scrollIntoView(scrollingOptions);
|
|
43
|
+
} else if (state.selectedIndex === index) {
|
|
44
|
+
ref.current.scrollIntoView(scrollingOptions);
|
|
45
|
+
}
|
|
46
|
+
}, [state, index, totalGroups, lastRef]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
onClick={() => {
|
|
51
|
+
dispatch({ type: "select", payload: index });
|
|
52
|
+
selectGroupAction(group);
|
|
53
|
+
}}
|
|
54
|
+
className={`${styles.patientSearchResult} ${
|
|
55
|
+
index === state.selectedIndex && styles.patientSearchResultSelected
|
|
56
|
+
}`}
|
|
57
|
+
role="button"
|
|
58
|
+
aria-pressed={index === state.selectedIndex}
|
|
59
|
+
tabIndex={0}
|
|
60
|
+
ref={ref}
|
|
61
|
+
>
|
|
62
|
+
<div className={styles.patientAvatar} role="img">
|
|
63
|
+
<Events size={24} />
|
|
64
|
+
</div>
|
|
65
|
+
<div>
|
|
66
|
+
<h2 className={styles.patientName}>{group.name}</h2>
|
|
67
|
+
<p className={styles.demographics}>
|
|
68
|
+
{group.cohortMembers?.length ?? 0} {t("members", "members")}
|
|
69
|
+
<span className={styles.middot}>·</span> {group.description}
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const CompactGroupResults = ({ groups, selectGroupAction, lastRef }) => {
|
|
24
77
|
const arrowUpPressed = useKeyPress("ArrowUp");
|
|
25
78
|
const arrowDownPressed = useKeyPress("ArrowDown");
|
|
26
79
|
const enterPressed = useKeyPress("Enter");
|
|
27
|
-
|
|
80
|
+
|
|
28
81
|
const [state, dispatch] = useReducer(reducer, { selectedIndex: 0 });
|
|
29
82
|
|
|
30
83
|
useEffect(() => {
|
|
@@ -48,31 +101,11 @@ const CompactGroupResults = ({ groups, selectGroupAction }) => {
|
|
|
48
101
|
return (
|
|
49
102
|
<>
|
|
50
103
|
{groups.map((group, index) => (
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
key={group.id}
|
|
57
|
-
className={`${styles.patientSearchResult} ${
|
|
58
|
-
index === state.selectedIndex && styles.patientSearchResultSelected
|
|
59
|
-
}`}
|
|
60
|
-
role="button"
|
|
61
|
-
aria-pressed={index === state.selectedIndex}
|
|
62
|
-
tabIndex={0}
|
|
63
|
-
>
|
|
64
|
-
<div className={styles.patientAvatar} role="img">
|
|
65
|
-
<Events size={24} />
|
|
66
|
-
</div>
|
|
67
|
-
<div>
|
|
68
|
-
<h2 className={styles.patientName}>{group.name}</h2>
|
|
69
|
-
<p className={styles.demographics}>
|
|
70
|
-
{group.members.length} {t("members", "members")}
|
|
71
|
-
<span className={styles.middot}>·</span>{" "}
|
|
72
|
-
{group.description}
|
|
73
|
-
</p>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
104
|
+
<ResultItem
|
|
105
|
+
key={index}
|
|
106
|
+
totalGroups={groups.length}
|
|
107
|
+
{...{ lastRef, index, selectGroupAction, group, dispatch, state }}
|
|
108
|
+
/>
|
|
76
109
|
))}
|
|
77
110
|
</>
|
|
78
111
|
);
|
|
@@ -4,6 +4,7 @@ import styles from "./compact-group-search.scss";
|
|
|
4
4
|
import GroupSearch from "./GroupSearch";
|
|
5
5
|
import { Button, Search } from "@carbon/react";
|
|
6
6
|
import { useTranslation } from "react-i18next";
|
|
7
|
+
import debounce from "lodash-es/debounce";
|
|
7
8
|
|
|
8
9
|
interface CompactGroupSearchProps {
|
|
9
10
|
selectGroupAction?: (group: GroupType) => void;
|
|
@@ -23,6 +24,10 @@ const CompactGroupSearch: React.FC<CompactGroupSearchProps> = ({
|
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
const handleSearchChange = (e) => {
|
|
27
|
+
debounce((q) => {
|
|
28
|
+
setDropdownShown(!!e.length);
|
|
29
|
+
setQuery(q);
|
|
30
|
+
}, 300);
|
|
26
31
|
setQuery(e);
|
|
27
32
|
if (e.length) {
|
|
28
33
|
setDropdownShown(true);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useCallback, useRef } from "react";
|
|
2
2
|
import { useTranslation } from "react-i18next";
|
|
3
|
-
import { Layer, Tile } from "@carbon/react";
|
|
3
|
+
import { Layer, Tile, Loading } from "@carbon/react";
|
|
4
4
|
import styles from "./group-search.scss";
|
|
5
5
|
import { EmptyDataIllustration } from "../../empty-state/EmptyDataIllustration";
|
|
6
|
-
import { useGroupSearch } from "./useGroupSearch";
|
|
7
6
|
import CompactGroupResults, {
|
|
8
7
|
SearchResultSkeleton,
|
|
9
8
|
} from "./CompactGroupResults";
|
|
10
9
|
import { GroupType } from "../../context/GroupFormWorkflowContext";
|
|
10
|
+
import { useSearchCohortInfinite } from "../../hooks/useSearchEndpoint";
|
|
11
11
|
|
|
12
12
|
interface GroupSearchProps {
|
|
13
13
|
query: string;
|
|
@@ -19,8 +19,48 @@ const GroupSearch: React.FC<GroupSearchProps> = ({
|
|
|
19
19
|
selectGroupAction,
|
|
20
20
|
}) => {
|
|
21
21
|
const { t } = useTranslation();
|
|
22
|
-
const
|
|
23
|
-
|
|
22
|
+
const {
|
|
23
|
+
isLoading,
|
|
24
|
+
data: results,
|
|
25
|
+
error,
|
|
26
|
+
loadingNewData,
|
|
27
|
+
setPage,
|
|
28
|
+
hasMore,
|
|
29
|
+
totalResults,
|
|
30
|
+
} = useSearchCohortInfinite({
|
|
31
|
+
searchTerm: query,
|
|
32
|
+
searching: !!query,
|
|
33
|
+
parameters: {
|
|
34
|
+
v: "full",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const lastItem = useRef(null);
|
|
39
|
+
const observer = useRef(null);
|
|
40
|
+
const loadingRef = useCallback(
|
|
41
|
+
(node) => {
|
|
42
|
+
if (loadingNewData) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (observer.current) {
|
|
46
|
+
observer.current.disconnect();
|
|
47
|
+
}
|
|
48
|
+
observer.current = new IntersectionObserver(
|
|
49
|
+
(entries) => {
|
|
50
|
+
if (entries[0].isIntersecting && hasMore) {
|
|
51
|
+
setPage((page) => page + 1);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
threshold: 0.75,
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
if (node) {
|
|
59
|
+
observer.current.observe(node);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
[loadingNewData, hasMore, setPage]
|
|
63
|
+
);
|
|
24
64
|
|
|
25
65
|
if (error) {
|
|
26
66
|
return (
|
|
@@ -43,9 +83,19 @@ const GroupSearch: React.FC<GroupSearchProps> = ({
|
|
|
43
83
|
);
|
|
44
84
|
}
|
|
45
85
|
|
|
46
|
-
if (
|
|
86
|
+
if (isLoading) {
|
|
87
|
+
return (
|
|
88
|
+
<div className={styles.searchResultsContainer}>
|
|
89
|
+
<SearchResultSkeleton />
|
|
90
|
+
<SearchResultSkeleton />
|
|
91
|
+
<SearchResultSkeleton />
|
|
92
|
+
<SearchResultSkeleton />
|
|
93
|
+
<SearchResultSkeleton />
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
47
97
|
|
|
48
|
-
if (results
|
|
98
|
+
if (results?.length === 0) {
|
|
49
99
|
return (
|
|
50
100
|
<div className={styles.searchResults}>
|
|
51
101
|
<Layer>
|
|
@@ -79,12 +129,19 @@ const GroupSearch: React.FC<GroupSearchProps> = ({
|
|
|
79
129
|
}}
|
|
80
130
|
>
|
|
81
131
|
<p className={styles.resultsText}>
|
|
82
|
-
{
|
|
132
|
+
{totalResults} {t("searchResultsText", "search result(s)")}
|
|
83
133
|
</p>
|
|
84
134
|
<CompactGroupResults
|
|
85
135
|
groups={results}
|
|
86
136
|
selectGroupAction={selectGroupAction}
|
|
137
|
+
lastRef={lastItem}
|
|
87
138
|
/>
|
|
139
|
+
<div ref={lastItem}>
|
|
140
|
+
<div className={styles.lastItem} ref={loadingRef}>
|
|
141
|
+
{hasMore && <Loading withOverlay={false} small />}
|
|
142
|
+
{!hasMore && <p>{t("noMoreResults", "End of search results")}</p>}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
88
145
|
</div>
|
|
89
146
|
</div>
|
|
90
147
|
);
|
|
@@ -29,12 +29,7 @@
|
|
|
29
29
|
width: 100%;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
padding: spacing.$spacing-05 0;
|
|
34
|
-
display: flex;
|
|
35
|
-
justify-content: center;
|
|
36
|
-
align-items: center;
|
|
37
|
-
}
|
|
32
|
+
|
|
38
33
|
|
|
39
34
|
.searchTerm {
|
|
40
35
|
@include type.type-style('heading-03');
|
|
@@ -92,3 +87,10 @@
|
|
|
92
87
|
@include type.type-style('body-01');
|
|
93
88
|
color: $text-02;
|
|
94
89
|
}
|
|
90
|
+
|
|
91
|
+
.lastItem {
|
|
92
|
+
padding: spacing.$spacing-05;
|
|
93
|
+
display: flex;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
align-items: center;
|
|
96
|
+
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { Close } from "@carbon/react/icons";
|
|
2
2
|
import { Button } from "@carbon/react";
|
|
3
3
|
import React, { useContext } from "react";
|
|
4
|
-
import { Link } from "react-router-dom";
|
|
5
4
|
import GroupFormWorkflowContext from "../../context/GroupFormWorkflowContext";
|
|
6
5
|
import styles from "./styles.scss";
|
|
7
6
|
import { useTranslation } from "react-i18next";
|
|
8
7
|
import CompactGroupSearch from "../group-search/CompactGroupSearch";
|
|
9
8
|
import AddGroupModal from "../../add-group-modal/AddGroupModal";
|
|
9
|
+
import { navigate } from "@openmrs/esm-framework";
|
|
10
10
|
|
|
11
11
|
const GroupSearchHeader = () => {
|
|
12
12
|
const { t } = useTranslation();
|
|
13
|
-
const { activeGroupUuid, setGroup } = useContext(
|
|
13
|
+
const { activeGroupUuid, setGroup, destroySession } = useContext(
|
|
14
|
+
GroupFormWorkflowContext
|
|
15
|
+
);
|
|
14
16
|
const handleSelectGroup = (group) => {
|
|
15
17
|
setGroup(group);
|
|
16
18
|
};
|
|
@@ -29,11 +31,16 @@ const GroupSearchHeader = () => {
|
|
|
29
31
|
</span>
|
|
30
32
|
<span style={{ flexGrow: 1 }} />
|
|
31
33
|
<span>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
<Button
|
|
35
|
+
kind="ghost"
|
|
36
|
+
onClick={() => {
|
|
37
|
+
destroySession();
|
|
38
|
+
// eslint-disable-next-line
|
|
39
|
+
navigate({ to: "${openmrsSpaBase}/forms" });
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
{t("cancel", "Cancel")} <Close size={20} />
|
|
43
|
+
</Button>
|
|
37
44
|
</span>
|
|
38
45
|
</div>
|
|
39
46
|
);
|
|
@@ -19,6 +19,16 @@
|
|
|
19
19
|
width: 1100px;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
:global(.omrs-breakpoint-lt-large-desktop) .workspace {
|
|
23
|
+
width: 1000px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
:global(.omrs-breakpoint-lt-small-desktop) .workspace {
|
|
27
|
+
// there's only so much we can do here. Currenlty the design does not support tablet
|
|
28
|
+
width: 100vw;
|
|
29
|
+
padding: 0 spacing.$spacing-04;
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
.selectPatientMessage {
|
|
23
33
|
@include type.type-style('productive-heading-03');
|
|
24
34
|
margin: spacing.$spacing-07;
|