@openmrs/esm-fast-data-entry-app 1.0.1-pre.8 → 1.0.1-pre.82
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/README.md +21 -2
- package/dist/132.js +1 -0
- package/dist/168.js +1 -0
- package/dist/229.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/74.js +1 -0
- package/dist/757.js +1 -0
- package/dist/776.js +1 -0
- package/dist/804.js +1 -0
- package/dist/820.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 +612 -0
- package/dist/openmrs-esm-fast-data-entry-app.old +1 -0
- package/package.json +9 -9
- package/src/CancelModal.tsx +48 -0
- package/src/CompleteModal.tsx +46 -0
- package/src/FormBootstrap.tsx +18 -3
- package/src/add-group-modal/AddGroupModal.tsx +80 -27
- package/src/add-group-modal/styles.scss +14 -4
- package/src/config-schema.ts +22 -0
- package/src/context/FormWorkflowContext.tsx +13 -1
- package/src/context/FormWorkflowReducer.ts +13 -3
- package/src/context/GroupFormWorkflowContext.tsx +41 -6
- package/src/context/GroupFormWorkflowReducer.ts +170 -12
- package/src/form-entry-workflow/FormEntryWorkflow.tsx +67 -101
- package/src/form-entry-workflow/styles.scss +2 -1
- package/src/forms-page/FormsPage.tsx +8 -3
- package/src/forms-page/forms-table/FormsTable.tsx +11 -5
- package/src/group-form-entry-workflow/GroupFormEntryWorkflow.tsx +13 -400
- package/src/group-form-entry-workflow/GroupSessionWorkspace.tsx +247 -0
- package/src/group-form-entry-workflow/SessionDetailsForm.tsx +122 -0
- package/src/group-form-entry-workflow/SessionMetaWorkspace.tsx +107 -0
- package/src/group-form-entry-workflow/attendance-table/AttendanceTable.tsx +105 -0
- package/src/group-form-entry-workflow/attendance-table/index.ts +1 -0
- 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} +23 -5
- package/src/group-form-entry-workflow/group-display-header/index.ts +3 -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 +11 -7
- package/src/group-form-entry-workflow/styles.scss +12 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useGetSystemSetting.ts +38 -0
- package/src/hooks/usePostEndpoint.ts +70 -0
- package/src/hooks/useSearchEndpoint.ts +120 -0
- package/src/hooks/useStartVisit.ts +92 -0
- package/src/patient-card/styles.scss +1 -0
- package/tools/i18next-parser.config.js +93 -0
- package/translations/en.json +27 -9
- package/translations/fr.json +50 -0
- package/.editorconfig +0 -12
- package/.eslintignore +0 -2
- package/.eslintrc.js +0 -10
- package/.husky/pre-push +0 -1
- package/.prettierignore +0 -14
- package/.yarn/plugins/@yarnpkg/plugin-version.cjs +0 -550
- package/.yarn/versions/7ee3eceb.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
- /package/src/group-form-entry-workflow/{group-banner → group-display-header}/styles.scss +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Layer,
|
|
3
|
+
Tile,
|
|
4
|
+
TextInput,
|
|
5
|
+
TextArea,
|
|
6
|
+
DatePicker,
|
|
7
|
+
DatePickerInput,
|
|
8
|
+
} from "@carbon/react";
|
|
9
|
+
import React from "react";
|
|
10
|
+
import styles from "./styles.scss";
|
|
11
|
+
import { useTranslation } from "react-i18next";
|
|
12
|
+
import { Controller, useFormContext } from "react-hook-form";
|
|
13
|
+
import { AttendanceTable } from "./attendance-table";
|
|
14
|
+
|
|
15
|
+
const SessionDetailsForm = () => {
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const {
|
|
18
|
+
register,
|
|
19
|
+
formState: { errors },
|
|
20
|
+
control,
|
|
21
|
+
} = useFormContext();
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className={styles.formSection}>
|
|
25
|
+
<h4>{t("sessionDetails", "1. Session details")}</h4>
|
|
26
|
+
<div>
|
|
27
|
+
<p>
|
|
28
|
+
{t(
|
|
29
|
+
"allFieldsRequired",
|
|
30
|
+
"All fields are required unless marked optional"
|
|
31
|
+
)}
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
<Layer>
|
|
35
|
+
<Tile className={styles.formSectionTile}>
|
|
36
|
+
<Layer>
|
|
37
|
+
<div
|
|
38
|
+
style={{
|
|
39
|
+
display: "flex",
|
|
40
|
+
flexDirection: "column",
|
|
41
|
+
rowGap: "1.5rem",
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<TextInput
|
|
45
|
+
id="text"
|
|
46
|
+
type="text"
|
|
47
|
+
labelText={t("sessionName", "Session Name")}
|
|
48
|
+
{...register("sessionName", { required: true })}
|
|
49
|
+
invalid={errors.sessionName}
|
|
50
|
+
invalidText={"This field is required"}
|
|
51
|
+
/>
|
|
52
|
+
<TextInput
|
|
53
|
+
id="text"
|
|
54
|
+
type="text"
|
|
55
|
+
labelText={t("practitionerName", "Practitioner Name")}
|
|
56
|
+
{...register("practitionerName", { required: true })}
|
|
57
|
+
invalid={errors.practitionerName}
|
|
58
|
+
invalidText={"This field is required"}
|
|
59
|
+
/>
|
|
60
|
+
<Controller
|
|
61
|
+
name="sessionDate"
|
|
62
|
+
control={control}
|
|
63
|
+
rules={{ required: true }}
|
|
64
|
+
render={({ field }) => (
|
|
65
|
+
<DatePicker
|
|
66
|
+
datePickerType="single"
|
|
67
|
+
size="md"
|
|
68
|
+
maxDate={new Date()}
|
|
69
|
+
{...field}
|
|
70
|
+
>
|
|
71
|
+
<DatePickerInput
|
|
72
|
+
id="session-date"
|
|
73
|
+
labelText={t("sessionDate", "Session Date")}
|
|
74
|
+
placeholder="mm/dd/yyyy"
|
|
75
|
+
size="md"
|
|
76
|
+
invalid={errors.sessionDate}
|
|
77
|
+
invalidText={"This field is required"}
|
|
78
|
+
/>
|
|
79
|
+
</DatePicker>
|
|
80
|
+
)}
|
|
81
|
+
/>
|
|
82
|
+
<TextArea
|
|
83
|
+
id="text"
|
|
84
|
+
type="text"
|
|
85
|
+
labelText={t("sessionNotes", "Session Notes")}
|
|
86
|
+
{...register("sessionNotes", { required: true })}
|
|
87
|
+
invalid={errors.sessionNotes}
|
|
88
|
+
invalidText={"This field is required"}
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
</Layer>
|
|
92
|
+
</Tile>
|
|
93
|
+
</Layer>
|
|
94
|
+
<h4>{t("sessionParticipants", "2. Session participants")}</h4>
|
|
95
|
+
<div>
|
|
96
|
+
<p>
|
|
97
|
+
{t(
|
|
98
|
+
"markAbsentPatients",
|
|
99
|
+
"The patients in this group. Patients that are not present in the session should be marked as absent."
|
|
100
|
+
)}
|
|
101
|
+
</p>
|
|
102
|
+
</div>
|
|
103
|
+
<Layer>
|
|
104
|
+
<Tile className={styles.formSectionTile}>
|
|
105
|
+
<Layer>
|
|
106
|
+
<div
|
|
107
|
+
style={{
|
|
108
|
+
display: "flex",
|
|
109
|
+
flexDirection: "column",
|
|
110
|
+
rowGap: "1.5rem",
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<AttendanceTable />
|
|
114
|
+
</div>
|
|
115
|
+
</Layer>
|
|
116
|
+
</Tile>
|
|
117
|
+
</Layer>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export default SessionDetailsForm;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Button } from "@carbon/react";
|
|
2
|
+
import React, { useContext, useEffect, useState } from "react";
|
|
3
|
+
import styles from "./styles.scss";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import GroupFormWorkflowContext from "../context/GroupFormWorkflowContext";
|
|
6
|
+
import { FormProvider, useForm, useFormContext } from "react-hook-form";
|
|
7
|
+
import CancelModal from "../CancelModal";
|
|
8
|
+
import SessionDetailsForm from "./SessionDetailsForm";
|
|
9
|
+
|
|
10
|
+
const NewGroupWorkflowButtons = () => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const context = useContext(GroupFormWorkflowContext);
|
|
13
|
+
const { workflowState, patientUuids } = context;
|
|
14
|
+
const [cancelModalOpen, setCancelModalOpen] = useState(false);
|
|
15
|
+
if (workflowState !== "NEW_GROUP_SESSION") return null;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<div className={styles.rightPanelActionButtons}>
|
|
20
|
+
<Button kind="secondary" type="submit" disabled={!patientUuids.length}>
|
|
21
|
+
{t("createNewSession", "Create New Session")}
|
|
22
|
+
</Button>
|
|
23
|
+
<Button
|
|
24
|
+
kind="tertiary"
|
|
25
|
+
onClick={() => {
|
|
26
|
+
setCancelModalOpen(true);
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
{t("cancel", "Cancel")}
|
|
30
|
+
</Button>
|
|
31
|
+
</div>
|
|
32
|
+
<CancelModal
|
|
33
|
+
open={cancelModalOpen}
|
|
34
|
+
setOpen={setCancelModalOpen}
|
|
35
|
+
context={context}
|
|
36
|
+
/>
|
|
37
|
+
</>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const GroupIdField = () => {
|
|
42
|
+
const { t } = useTranslation();
|
|
43
|
+
const {
|
|
44
|
+
register,
|
|
45
|
+
formState: { errors },
|
|
46
|
+
setValue,
|
|
47
|
+
} = useFormContext();
|
|
48
|
+
const { activeGroupUuid } = useContext(GroupFormWorkflowContext);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (activeGroupUuid) setValue("groupUuid", activeGroupUuid);
|
|
52
|
+
}, [activeGroupUuid, setValue]);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
<input
|
|
57
|
+
hidden
|
|
58
|
+
{...register("groupUuid", {
|
|
59
|
+
value: activeGroupUuid,
|
|
60
|
+
required: t("chooseGroupError", "Please choose a group."),
|
|
61
|
+
})}
|
|
62
|
+
/>
|
|
63
|
+
{errors.groupUuid && !activeGroupUuid && (
|
|
64
|
+
<div className={styles.formError}>
|
|
65
|
+
{errors.groupUuid.message as string}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const SessionMetaWorkspace = () => {
|
|
73
|
+
const { t } = useTranslation();
|
|
74
|
+
const { setSessionMeta, workflowState } = useContext(
|
|
75
|
+
GroupFormWorkflowContext
|
|
76
|
+
);
|
|
77
|
+
const methods = useForm();
|
|
78
|
+
|
|
79
|
+
const onSubmit = (data) => {
|
|
80
|
+
const { sessionDate, ...rest } = data;
|
|
81
|
+
setSessionMeta({ ...rest, sessionDate: sessionDate[0] });
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (workflowState !== "NEW_GROUP_SESSION") return null;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<FormProvider {...methods}>
|
|
88
|
+
<form onSubmit={methods.handleSubmit(onSubmit)}>
|
|
89
|
+
<div className={styles.workspace}>
|
|
90
|
+
<div className={styles.formMainContent}>
|
|
91
|
+
<div className={styles.formContainer}>
|
|
92
|
+
<SessionDetailsForm />
|
|
93
|
+
</div>
|
|
94
|
+
<div className={styles.rightPanel}>
|
|
95
|
+
<h4>{t("newGroupSession", "New Group Session")}</h4>
|
|
96
|
+
<GroupIdField />
|
|
97
|
+
<hr style={{ width: "100%" }} />
|
|
98
|
+
<NewGroupWorkflowButtons />
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</form>
|
|
103
|
+
</FormProvider>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export default SessionMetaWorkspace;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React, { useContext } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Checkbox,
|
|
5
|
+
SkeletonText,
|
|
6
|
+
Table,
|
|
7
|
+
TableHead,
|
|
8
|
+
TableRow,
|
|
9
|
+
TableHeader,
|
|
10
|
+
TableBody,
|
|
11
|
+
TableCell,
|
|
12
|
+
} from "@carbon/react";
|
|
13
|
+
import { useTranslation } from "react-i18next";
|
|
14
|
+
import GroupFormWorkflowContext from "../../context/GroupFormWorkflowContext";
|
|
15
|
+
import { useGetPatient } from "../../hooks";
|
|
16
|
+
|
|
17
|
+
const PatientRow = ({ patientUuid }) => {
|
|
18
|
+
const patient = useGetPatient(patientUuid);
|
|
19
|
+
const { patientUuids, addPatientUuid, removePatientUuid } = useContext(
|
|
20
|
+
GroupFormWorkflowContext
|
|
21
|
+
);
|
|
22
|
+
const givenName = patient?.name?.[0]?.given?.[0];
|
|
23
|
+
const familyName = patient?.name?.[0]?.family;
|
|
24
|
+
const identifier = patient?.identifier?.[0]?.value;
|
|
25
|
+
|
|
26
|
+
const handleOnChange = (e, { checked }) => {
|
|
27
|
+
if (checked) {
|
|
28
|
+
addPatientUuid(patientUuid);
|
|
29
|
+
} else {
|
|
30
|
+
removePatientUuid(patientUuid);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (!patient) {
|
|
35
|
+
return (
|
|
36
|
+
<TableRow>
|
|
37
|
+
<TableCell>
|
|
38
|
+
<SkeletonText />
|
|
39
|
+
</TableCell>
|
|
40
|
+
<TableCell>
|
|
41
|
+
<SkeletonText />
|
|
42
|
+
</TableCell>
|
|
43
|
+
<TableCell>
|
|
44
|
+
<Checkbox diabled />
|
|
45
|
+
</TableCell>
|
|
46
|
+
</TableRow>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<TableRow>
|
|
52
|
+
<TableCell>
|
|
53
|
+
{patient.display ||
|
|
54
|
+
patient.displayName ||
|
|
55
|
+
[givenName, familyName].join(" ")}
|
|
56
|
+
</TableCell>
|
|
57
|
+
<TableCell>{identifier}</TableCell>
|
|
58
|
+
<TableCell>
|
|
59
|
+
<Checkbox
|
|
60
|
+
checked={patientUuids.includes(patientUuid)}
|
|
61
|
+
labelText={patientUuid}
|
|
62
|
+
hideLabel
|
|
63
|
+
id={`${identifier}-attendance-checkbox`}
|
|
64
|
+
onChange={handleOnChange}
|
|
65
|
+
/>
|
|
66
|
+
</TableCell>
|
|
67
|
+
</TableRow>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const AttendanceTable = () => {
|
|
72
|
+
const { t } = useTranslation();
|
|
73
|
+
const { activeGroupUuid, activeGroupMembers } = useContext(
|
|
74
|
+
GroupFormWorkflowContext
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const headers = [
|
|
78
|
+
t("name", "Name"),
|
|
79
|
+
t("identifier", "Patient ID"),
|
|
80
|
+
t("patientIsPresent", "Patient is present"),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
if (!activeGroupUuid) {
|
|
84
|
+
return <div>{t("selectGroupFirst", "Please select a group first")}</div>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<Table>
|
|
89
|
+
<TableHead>
|
|
90
|
+
<TableRow>
|
|
91
|
+
{headers.map((header, index) => (
|
|
92
|
+
<TableHeader key={index}>{header}</TableHeader>
|
|
93
|
+
))}
|
|
94
|
+
</TableRow>
|
|
95
|
+
</TableHead>
|
|
96
|
+
<TableBody>
|
|
97
|
+
{activeGroupMembers.map((patientUuid, index) => (
|
|
98
|
+
<PatientRow {...{ patientUuid }} key={index} />
|
|
99
|
+
))}
|
|
100
|
+
</TableBody>
|
|
101
|
+
</Table>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export default AttendanceTable;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as AttendanceTable } from "./AttendanceTable";
|
|
@@ -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,19 @@
|
|
|
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";
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
-
const {
|
|
9
|
-
|
|
8
|
+
const GroupDisplayHeader = () => {
|
|
9
|
+
const {
|
|
10
|
+
activeGroupName,
|
|
11
|
+
activeGroupUuid,
|
|
12
|
+
patientUuids,
|
|
13
|
+
activeSessionMeta,
|
|
14
|
+
unsetGroup,
|
|
15
|
+
destroySession,
|
|
16
|
+
} = useContext(GroupFormWorkflowContext);
|
|
10
17
|
const { t } = useTranslation();
|
|
11
18
|
|
|
12
19
|
if (!activeGroupUuid) {
|
|
@@ -38,8 +45,19 @@ const GroupBanner = () => {
|
|
|
38
45
|
</div>
|
|
39
46
|
</div>
|
|
40
47
|
)}
|
|
48
|
+
<span style={{ flexGrow: 1 }} />
|
|
49
|
+
<span>
|
|
50
|
+
<Button kind="ghost" onClick={() => unsetGroup()}>
|
|
51
|
+
{t("changeGroup", "Choose a different group")} <Close size={20} />
|
|
52
|
+
</Button>
|
|
53
|
+
</span>
|
|
54
|
+
<span>
|
|
55
|
+
<Button kind="ghost" onClick={() => destroySession()}>
|
|
56
|
+
{t("cancel", "Cancel")} <Close size={20} />
|
|
57
|
+
</Button>
|
|
58
|
+
</span>
|
|
41
59
|
</div>
|
|
42
60
|
);
|
|
43
61
|
};
|
|
44
62
|
|
|
45
|
-
export default
|
|
63
|
+
export default GroupDisplayHeader;
|
|
@@ -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
|
+
}
|