@openmrs/esm-fast-data-entry-app 1.0.1-pre.10 → 1.0.1-pre.101

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 (110) hide show
  1. package/README.md +21 -2
  2. package/dist/153.js +1 -0
  3. package/dist/153.js.map +1 -0
  4. package/dist/233.js +2 -0
  5. package/dist/233.js.LICENSE.txt +9 -0
  6. package/dist/233.js.map +1 -0
  7. package/dist/262.js +1 -0
  8. package/dist/262.js.map +1 -0
  9. package/dist/279.js +1 -0
  10. package/dist/279.js.map +1 -0
  11. package/dist/294.js +2 -0
  12. package/dist/294.js.LICENSE.txt +9 -0
  13. package/dist/294.js.map +1 -0
  14. package/dist/327.js +1 -0
  15. package/dist/327.js.map +1 -0
  16. package/dist/409.js +2 -0
  17. package/dist/409.js.LICENSE.txt +27 -0
  18. package/dist/409.js.map +1 -0
  19. package/dist/415.js +1 -0
  20. package/dist/415.js.map +1 -0
  21. package/dist/559.js +1 -0
  22. package/dist/559.js.map +1 -0
  23. package/dist/574.js +1 -0
  24. package/dist/651.js +1 -0
  25. package/dist/651.js.map +1 -0
  26. package/dist/706.js +1 -0
  27. package/dist/706.js.map +1 -0
  28. package/dist/757.js +1 -0
  29. package/dist/800.js +2 -0
  30. package/dist/800.js.LICENSE.txt +5 -0
  31. package/dist/800.js.map +1 -0
  32. package/dist/820.js +1 -0
  33. package/dist/820.js.map +1 -0
  34. package/dist/883.js +1 -0
  35. package/dist/883.js.map +1 -0
  36. package/dist/889.js +1 -0
  37. package/dist/889.js.map +1 -0
  38. package/dist/897.js +2 -0
  39. package/dist/897.js.LICENSE.txt +21 -0
  40. package/dist/897.js.map +1 -0
  41. package/dist/92.js +1 -0
  42. package/dist/92.js.map +1 -0
  43. package/dist/935.js +2 -0
  44. package/dist/935.js.LICENSE.txt +19 -0
  45. package/dist/935.js.map +1 -0
  46. package/dist/959.js +1 -0
  47. package/dist/959.js.map +1 -0
  48. package/dist/main.js +1 -0
  49. package/dist/main.js.map +1 -0
  50. package/dist/openmrs-esm-fast-data-entry-app.js +1 -1
  51. package/dist/openmrs-esm-fast-data-entry-app.js.buildmanifest.json +654 -0
  52. package/dist/openmrs-esm-fast-data-entry-app.js.map +1 -0
  53. package/dist/routes.json +1 -0
  54. package/jest.config.json +2 -1
  55. package/package.json +13 -10
  56. package/src/CancelModal.tsx +48 -0
  57. package/src/CompleteModal.tsx +46 -0
  58. package/src/FormBootstrap.tsx +18 -3
  59. package/src/add-group-modal/AddGroupModal.tsx +113 -34
  60. package/src/add-group-modal/styles.scss +14 -4
  61. package/src/config-schema.ts +22 -0
  62. package/src/context/FormWorkflowContext.tsx +13 -1
  63. package/src/context/FormWorkflowReducer.ts +13 -3
  64. package/src/context/GroupFormWorkflowContext.tsx +41 -6
  65. package/src/context/GroupFormWorkflowReducer.ts +170 -12
  66. package/src/form-entry-workflow/FormEntryWorkflow.tsx +67 -101
  67. package/src/form-entry-workflow/styles.scss +2 -1
  68. package/src/forms-page/FormsPage.tsx +8 -3
  69. package/src/forms-page/forms-table/FormsTable.tsx +11 -5
  70. package/src/group-form-entry-workflow/GroupFormEntryWorkflow.tsx +13 -400
  71. package/src/group-form-entry-workflow/GroupSessionWorkspace.tsx +247 -0
  72. package/src/group-form-entry-workflow/SessionDetailsForm.tsx +131 -0
  73. package/src/group-form-entry-workflow/SessionMetaWorkspace.tsx +107 -0
  74. package/src/group-form-entry-workflow/attendance-table/AttendanceTable.tsx +144 -0
  75. package/src/group-form-entry-workflow/attendance-table/index.ts +1 -0
  76. package/src/group-form-entry-workflow/{group-banner/GroupBanner.test.tsx → group-display-header/GroupDisplayHeader.test.tsx} +2 -2
  77. package/src/group-form-entry-workflow/{group-banner/GroupBanner.tsx → group-display-header/GroupDisplayHeader.tsx} +23 -5
  78. package/src/group-form-entry-workflow/group-display-header/index.ts +3 -0
  79. package/src/group-form-entry-workflow/group-search/CompactGroupResults.tsx +61 -28
  80. package/src/group-form-entry-workflow/group-search/CompactGroupSearch.tsx +5 -0
  81. package/src/group-form-entry-workflow/group-search/GroupSearch.tsx +65 -8
  82. package/src/group-form-entry-workflow/group-search/group-search.scss +8 -6
  83. package/src/group-form-entry-workflow/group-search-header/GroupSearchHeader.tsx +41 -10
  84. package/src/group-form-entry-workflow/styles.scss +12 -1
  85. package/src/hooks/index.ts +1 -0
  86. package/src/hooks/useGetPatient.ts +1 -1
  87. package/src/hooks/useGetPatients.ts +34 -0
  88. package/src/hooks/useGetSystemSetting.ts +38 -0
  89. package/src/hooks/usePostEndpoint.ts +76 -0
  90. package/src/hooks/useSearchEndpoint.ts +120 -0
  91. package/src/hooks/useStartVisit.ts +92 -0
  92. package/src/index.ts +13 -65
  93. package/src/patient-card/styles.scss +1 -0
  94. package/src/routes.json +24 -0
  95. package/tools/i18next-parser.config.js +93 -0
  96. package/translations/en.json +29 -9
  97. package/translations/fr.json +50 -0
  98. package/.editorconfig +0 -12
  99. package/.eslintignore +0 -2
  100. package/.eslintrc.js +0 -10
  101. package/.husky/pre-push +0 -1
  102. package/.prettierignore +0 -14
  103. package/.tx/config +0 -9
  104. package/.yarn/plugins/@yarnpkg/plugin-version.cjs +0 -550
  105. package/.yarn/versions/45b499b6.yml +0 -0
  106. package/src/group-form-entry-workflow/group-banner/index.ts +0 -3
  107. package/src/group-form-entry-workflow/group-search/mock-group-data.ts +0 -79
  108. package/src/group-form-entry-workflow/group-search/useGroupSearch.ts +0 -14
  109. package/src/hooks/usePostCohort.ts +0 -18
  110. /package/src/group-form-entry-workflow/{group-banner → group-display-header}/styles.scss +0 -0
@@ -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 CompactGroupResults = ({ groups, selectGroupAction }) => {
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}>&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
- const { t } = useTranslation();
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
- <div
52
- onClick={() => {
53
- dispatch({ type: "select", payload: index });
54
- selectGroupAction(group);
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}>&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 results = useGroupSearch(query);
23
- const error = false;
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 (query.length <= 2) return <SearchResultSkeleton />;
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.length === 0) {
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
- {results.length} {t("searchResultsText", "search result(s)")}
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
- .loadingIcon {
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,7 +1,6 @@
1
- import { Close } from "@carbon/react/icons";
1
+ import { Close, Add } from "@carbon/react/icons";
2
2
  import { Button } from "@carbon/react";
3
- import React, { useContext } from "react";
4
- import { Link } from "react-router-dom";
3
+ import React, { useCallback, useContext, useState } from "react";
5
4
  import GroupFormWorkflowContext from "../../context/GroupFormWorkflowContext";
6
5
  import styles from "./styles.scss";
7
6
  import { useTranslation } from "react-i18next";
@@ -10,11 +9,26 @@ import AddGroupModal from "../../add-group-modal/AddGroupModal";
10
9
 
11
10
  const GroupSearchHeader = () => {
12
11
  const { t } = useTranslation();
13
- const { activeGroupUuid, setGroup } = useContext(GroupFormWorkflowContext);
12
+ const { activeGroupUuid, setGroup, destroySession } = useContext(
13
+ GroupFormWorkflowContext
14
+ );
15
+ const [isOpen, setOpen] = useState(false);
14
16
  const handleSelectGroup = (group) => {
15
17
  setGroup(group);
16
18
  };
17
19
 
20
+ const handleCancel = useCallback(() => {
21
+ setOpen(false);
22
+ }, []);
23
+
24
+ const onPostSubmit = useCallback(() => {
25
+ setOpen(false);
26
+ }, []);
27
+
28
+ const handleOpenClick = useCallback(() => {
29
+ setOpen(true);
30
+ }, []);
31
+
18
32
  if (activeGroupUuid) return null;
19
33
 
20
34
  return (
@@ -25,15 +39,32 @@ const GroupSearchHeader = () => {
25
39
  </span>
26
40
  <span className={styles.padded}>{t("or", "or")}</span>
27
41
  <span>
28
- <AddGroupModal />
42
+ <Button
43
+ onClick={handleOpenClick}
44
+ renderIcon={Add}
45
+ iconDescription="Add"
46
+ >
47
+ {t("createNewGroup", "Create New Group")}
48
+ </Button>
49
+ <AddGroupModal
50
+ {...{
51
+ isCreate: true,
52
+ isOpen: isOpen,
53
+ handleCancel: handleCancel,
54
+ onPostSubmit: onPostSubmit,
55
+ }}
56
+ />
29
57
  </span>
30
58
  <span style={{ flexGrow: 1 }} />
31
59
  <span>
32
- <Link to="..">
33
- <Button kind="ghost">
34
- {t("cancel", "Cancel")} <Close size={20} />
35
- </Button>
36
- </Link>
60
+ <Button
61
+ kind="ghost"
62
+ onClick={() => {
63
+ destroySession();
64
+ }}
65
+ >
66
+ {t("cancel", "Cancel")} <Close size={20} />
67
+ </Button>
37
68
  </span>
38
69
  </div>
39
70
  );
@@ -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;
@@ -36,6 +46,7 @@
36
46
  flex-grow: 1;
37
47
  max-height: calc(100vh - 14rem);
38
48
  overflow-y: scroll;
49
+ text-align: left;
39
50
  }
40
51
 
41
52
  .formContainer :global(.cds--form-item) :global(.question-area) {
@@ -43,7 +54,7 @@
43
54
  }
44
55
 
45
56
  .rightPanel {
46
- width: 13rem;
57
+ min-width: 13rem;
47
58
  text-align: left;
48
59
  overflow-y: scroll;
49
60
  display: flex;
@@ -4,3 +4,4 @@ import useFormState from "./useFormState";
4
4
  import useGetEncounter from "./useGetEncounter";
5
5
 
6
6
  export { useGetAllForms, useGetPatient, useFormState, useGetEncounter };
7
+ export * from "./usePostEndpoint";
@@ -14,7 +14,7 @@ const useGetPatient = (patientUuid) => {
14
14
 
15
15
  const getPatient = async (uuid) => {
16
16
  const result = await fetchCurrentPatient(uuid);
17
- setPatient(result?.data);
17
+ setPatient(result);
18
18
  };
19
19
 
20
20
  return patient;
@@ -0,0 +1,34 @@
1
+ import { fetchCurrentPatient } from "@openmrs/esm-framework";
2
+ import { useEffect, useState } from "react";
3
+
4
+ const useGetPatients = (patientUuids) => {
5
+ const [patients, setPatients] = useState([]);
6
+ const [isLoading, setIsLoading] = useState(true);
7
+
8
+ useEffect(() => {
9
+ if (!patientUuids || patientUuids.length === 0) {
10
+ setPatients([]);
11
+ setIsLoading(false);
12
+ } else {
13
+ getPatients(patientUuids);
14
+ }
15
+ }, [patientUuids]);
16
+
17
+ const getPatients = async (uuids) => {
18
+ try {
19
+ setIsLoading(true);
20
+ const results = await Promise.all(
21
+ uuids.map(async (uuid) => await fetchCurrentPatient(uuid))
22
+ );
23
+ setPatients(results);
24
+ setIsLoading(false);
25
+ } catch (error) {
26
+ console.error("Error fetching patients:", error);
27
+ setIsLoading(false);
28
+ }
29
+ };
30
+
31
+ return { patients, isLoading };
32
+ };
33
+
34
+ export default useGetPatients;
@@ -0,0 +1,38 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { openmrsFetch } from "@openmrs/esm-framework";
3
+
4
+ const useGetSystemSetting = (settingId) => {
5
+ const [isSubmitting, setIsSubmitting] = useState(false);
6
+ const [result, setResult] = useState(null);
7
+ const [error, setError] = useState(null);
8
+
9
+ const onResult = useCallback((result) => {
10
+ setIsSubmitting(false);
11
+ setError(false);
12
+ setResult(result);
13
+ }, []);
14
+
15
+ const onError = useCallback((error) => {
16
+ setIsSubmitting(false);
17
+ setResult(null);
18
+ setError(error);
19
+ }, []);
20
+
21
+ const getSetting = useCallback(() => {
22
+ openmrsFetch(`/ws/rest/v1/systemsetting?q=${settingId}&v=default`)
23
+ .then(onResult)
24
+ .catch(onError);
25
+ }, [onError, onResult, settingId]);
26
+
27
+ useEffect(() => {
28
+ getSetting();
29
+ }, [getSetting]);
30
+
31
+ return {
32
+ result,
33
+ error,
34
+ isSubmitting,
35
+ };
36
+ };
37
+
38
+ export default useGetSystemSetting;
@@ -0,0 +1,76 @@
1
+ import { openmrsFetch } from "@openmrs/esm-framework";
2
+ import { useCallback, useState } from "react";
3
+
4
+ const usePostEndpoint = ({ endpointUrl }) => {
5
+ const [submissionInProgress, setSubmissionInProgress] = useState(null);
6
+ const [result, setResult] = useState(null);
7
+ const [error, setError] = useState(null);
8
+
9
+ const onFormPosted = useCallback(
10
+ (result) => {
11
+ setSubmissionInProgress(false);
12
+ if (error) {
13
+ setError(null);
14
+ }
15
+ setResult(result.data);
16
+ },
17
+ [error]
18
+ );
19
+
20
+ const onError = useCallback(
21
+ (error) => {
22
+ setSubmissionInProgress(false);
23
+ if (result) {
24
+ setResult(null);
25
+ }
26
+ setError(error?.responseBody?.error ?? error?.responseBody ?? error);
27
+ },
28
+ [result]
29
+ );
30
+
31
+ const post = useCallback(
32
+ async (data) => {
33
+ setSubmissionInProgress(true);
34
+
35
+ let path = endpointUrl;
36
+ if (data.uuid) {
37
+ path += "/" + data.uuid;
38
+ }
39
+
40
+ return openmrsFetch(path, {
41
+ method: "POST",
42
+ headers: {
43
+ "Content-Type": "application/json",
44
+ },
45
+ body: data,
46
+ })
47
+ .then(onFormPosted)
48
+ .catch(onError);
49
+ },
50
+ [endpointUrl, onError, onFormPosted]
51
+ );
52
+
53
+ const reset = () => {
54
+ setSubmissionInProgress(null);
55
+ setResult(null);
56
+ setError(null);
57
+ };
58
+
59
+ return {
60
+ post,
61
+ isPosting: submissionInProgress,
62
+ result,
63
+ error,
64
+ reset,
65
+ };
66
+ };
67
+
68
+ const usePostVisit = () => {
69
+ return usePostEndpoint({ endpointUrl: "/ws/rest/v1/visit" });
70
+ };
71
+
72
+ const usePostCohort = () => {
73
+ return usePostEndpoint({ endpointUrl: "/ws/rest/v1/cohortm/cohort" });
74
+ };
75
+
76
+ export { usePostEndpoint, usePostVisit, usePostCohort };
@@ -0,0 +1,120 @@
1
+ import { openmrsFetch, FetchResponse } from "@openmrs/esm-framework";
2
+ import { useCallback, useMemo } from "react";
3
+ import useSWRInfinite from "swr/infinite";
4
+
5
+ export interface SearchResponse {
6
+ data: Array<Record<string, unknown>> | null;
7
+ isLoading: boolean;
8
+ error: Error;
9
+ loadingNewData: boolean;
10
+ hasMore: boolean;
11
+ currentPage: number;
12
+ totalResults: number;
13
+ setPage: (size: number | ((_size: number) => number)) => Promise<
14
+ FetchResponse<{
15
+ results: Array<Record<string, unknown>>;
16
+ links: Array<{
17
+ rel: "prev" | "next";
18
+ }>;
19
+ }>[]
20
+ >;
21
+ }
22
+
23
+ interface SearchInfiniteProps {
24
+ baseUrl?: string;
25
+ searchTerm: string;
26
+ parameters?: Record<string, unknown> | undefined;
27
+ searching: boolean;
28
+ resultsToFetch?: number;
29
+ }
30
+
31
+ const useSearchEndpointInfinite = (
32
+ arg0: SearchInfiniteProps
33
+ ): SearchResponse => {
34
+ const {
35
+ baseUrl,
36
+ searchTerm,
37
+ parameters,
38
+ searching = true,
39
+ resultsToFetch = 10,
40
+ } = arg0;
41
+
42
+ const getUrl = useCallback(
43
+ (
44
+ page: number,
45
+ prevPageData: FetchResponse<{
46
+ results: Array<Record<string, unknown>>;
47
+ links: Array<{ rel: "prev" | "next" }>;
48
+ }>
49
+ ) => {
50
+ if (
51
+ prevPageData &&
52
+ !prevPageData?.data?.links.some((link) => link.rel === "next")
53
+ ) {
54
+ return null;
55
+ }
56
+ let url = `${baseUrl}?q=${searchTerm}`;
57
+ const params = {
58
+ // merge passed parameters and default parameters
59
+ // this way the defaults can be overriden if needed
60
+ totalCount: true,
61
+ limit: resultsToFetch,
62
+ ...parameters,
63
+ };
64
+ Object.entries(params).forEach(([key, value]) => {
65
+ // don't send null parmeters
66
+ if (value !== null && value !== undefined) {
67
+ url += `&${key}=${value}`;
68
+ }
69
+ });
70
+ if (page) {
71
+ url += `&startIndex=${page * resultsToFetch}`;
72
+ }
73
+ return url;
74
+ },
75
+ [baseUrl, searchTerm, parameters, resultsToFetch]
76
+ );
77
+
78
+ const { data, isValidating, setSize, error, size } = useSWRInfinite<
79
+ FetchResponse<{
80
+ results: Array<Record<string, unknown>>;
81
+ links: Array<{ rel: "prev" | "next" }>;
82
+ totalCount: number;
83
+ }>,
84
+ Error
85
+ >(searching ? getUrl : null, openmrsFetch);
86
+
87
+ const results = useMemo(
88
+ () => ({
89
+ data: data
90
+ ? [].concat(...(data?.map((resp) => resp?.data?.results) ?? []))
91
+ : null,
92
+ isLoading: !data && !error,
93
+ error,
94
+ hasMore: data?.length
95
+ ? !!data[data.length - 1].data?.links?.some(
96
+ (link) => link.rel === "next"
97
+ )
98
+ : false,
99
+ loadingNewData: isValidating,
100
+ setPage: setSize,
101
+ currentPage: size,
102
+ totalResults: data?.[0]?.data?.totalCount,
103
+ }),
104
+ [data, isValidating, error, setSize, size]
105
+ );
106
+
107
+ return results;
108
+ };
109
+
110
+ const useSearchCohortInfinite = ({
111
+ ...props
112
+ }: SearchInfiniteProps): SearchResponse => {
113
+ return useSearchEndpointInfinite({
114
+ baseUrl: "/ws/rest/v1/cohortm/cohort",
115
+ resultsToFetch: 10,
116
+ ...props,
117
+ });
118
+ };
119
+
120
+ export { useSearchEndpointInfinite, useSearchCohortInfinite };