@openmrs/esm-react-utils 5.8.1-pre.2249 → 5.8.1-pre.2255

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.
@@ -0,0 +1,173 @@
1
+ /** @module @category UI */
2
+ import { type FetchResponse, openmrsFetch } from '@openmrs/esm-api';
3
+ import { useCallback, useMemo, useRef, useState } from 'react';
4
+ import useSWR, { type SWRConfiguration } from 'swr';
5
+ import useSWRImmutable from 'swr/immutable';
6
+
7
+ export interface OpenMRSPaginatedResponse<T> {
8
+ results: Array<T>;
9
+ links: Array<{ rel: 'prev' | 'next'; uri: string }>;
10
+ totalCount: number;
11
+ }
12
+
13
+ export interface UseServerPaginationOptions<R> {
14
+ /**
15
+ * Whether to use useSWR or useSWRInfinite to fetch data
16
+ */
17
+ immutable?: boolean;
18
+
19
+ /**
20
+ * The fetcher to use. Defaults to openmrsFetch
21
+ */
22
+ fetcher?: (key: string) => Promise<FetchResponse<R>>;
23
+
24
+ /**
25
+ * The configuration object for useSWR or useSWRInfinite
26
+ */
27
+ swrConfig?: SWRConfiguration;
28
+ }
29
+
30
+ /**
31
+ * Most OpenMRS REST endpoints that return a list of objects, such as getAll or search, are server-side paginated.
32
+ * The server limits the max number of results being returned, and multiple requests are needed to get the full data set
33
+ * if its size exceeds this limit.
34
+ * The max number of results per request is configurable server-side
35
+ * with the key "webservices.rest.maxResultsDefault". See: https://openmrs.atlassian.net/wiki/spaces/docs/pages/25469882/REST+Module
36
+ *
37
+ * For any UI that displays a paginated view of the full data set, we MUST handle the server-side pagination properly,
38
+ * or else the UI does not correctly display the full data set.
39
+ * This hook does that by providing callback functions for navigating to different pages of the results, and
40
+ * lazy-loads the data on each page as needed.
41
+ *
42
+ * Note that this hook is not suitable for use for situations that require client-side sorting or filtering
43
+ * of the data set. In that case, all data must be loaded onto client-side first.
44
+ *
45
+ * @see `useOpenmrsInfinite`
46
+ * @see `useOpenmrsFetchAll`
47
+ * @see `usePagination` for pagination of client-side data`
48
+ * @see `useFhirPagination``
49
+ *
50
+ * @param url The URL of the paginated rest endpoint. \
51
+ * It should be populated with any needed GET params, except `limit`, `startIndex` or `totalCount`,
52
+ * which will be overridden and manipulated by the `goTo*` callbacks.
53
+ * Similar to useSWR, this param can be null to disable fetching.
54
+ * @param pageSize The number of results to return per page / fetch. Note that this value MUST NOT exceed
55
+ * "webservices.rest.maxResultsAbsolute", which should be reasonably high by default (1000).
56
+ * @param options The options object
57
+ * @returns
58
+ */
59
+ export function useOpenmrsPagination<T>(
60
+ url: string | URL,
61
+ pageSize: number,
62
+ options: UseServerPaginationOptions<OpenMRSPaginatedResponse<T>> = {},
63
+ ) {
64
+ return useServerPagination<T, OpenMRSPaginatedResponse<T>>(url, pageSize, openmrsServerPaginationHandlers, options);
65
+ }
66
+
67
+ type OpenmrsServerPaginationHandlers<T> = ServerPaginationHandlers<T, OpenMRSPaginatedResponse<T>>;
68
+ export const openmrsServerPaginationHandlers: OpenmrsServerPaginationHandlers<any> = {
69
+ getPaginatedUrl: (url: string | URL, limit: number, startIndex: number) => {
70
+ if (url) {
71
+ const urlUrl = new URL(url.toString());
72
+ urlUrl.searchParams.set('limit', '' + limit);
73
+ urlUrl.searchParams.set('startIndex', '' + startIndex);
74
+ urlUrl.searchParams.set('totalCount', 'true');
75
+ return urlUrl.toString();
76
+ } else {
77
+ return null;
78
+ }
79
+ },
80
+ getNextUrl: (response) => {
81
+ const uri = response?.links?.find((link) => link.rel == 'next')?.uri;
82
+ if (uri) {
83
+ const url = new URL(uri);
84
+
85
+ // allows frontend proxies to work
86
+ url.host = window.location.host;
87
+ url.protocol = window.location.protocol;
88
+
89
+ return url.toString();
90
+ } else {
91
+ return null;
92
+ }
93
+ },
94
+ getTotalCount: (response) => response?.totalCount ?? Number.NaN,
95
+ getCurrentPageSize: (response) => response?.results?.length ?? Number.NaN,
96
+ getData: (response) => response?.results,
97
+ };
98
+
99
+ export interface ServerPaginationHandlers<T, R> {
100
+ getPaginatedUrl: (url: string | URL, limit: number, startIndex: number) => string | null;
101
+ getNextUrl: (response: R | undefined) => string | null;
102
+ getTotalCount: (response: R | undefined) => number;
103
+ getCurrentPageSize: (response: R | undefined) => number;
104
+ getData: (response: R | undefined) => Array<T> | undefined;
105
+ }
106
+
107
+ export function useServerPagination<T, R>(
108
+ url: string | URL,
109
+ pageSize: number,
110
+ serverPaginationHandlers: ServerPaginationHandlers<T, R>,
111
+ options: UseServerPaginationOptions<R> = {},
112
+ ) {
113
+ const { getPaginatedUrl, getTotalCount, getCurrentPageSize, getData } = serverPaginationHandlers;
114
+ const { immutable, swrConfig } = options;
115
+ const fetcher: (key: string) => Promise<FetchResponse<R>> = options.fetcher ?? openmrsFetch;
116
+ const [currentPage, setCurrentPage] = useState<number>(1); // 1-indexing instead of 0-indexing, to keep consistency with `usePagination`
117
+
118
+ // Cache the totalCount and currentPageSize so we don't lose them
119
+ // as we wait for next page's result to load while navigating to a different page.
120
+ // This can be used to prevent jarring UI changes while loading
121
+ const totalCount = useRef<number>(Number.NaN);
122
+ const currentPageSize = useRef<number>(Number.NaN); // this value is usually same as pageSize, except when currentPage is last page.
123
+
124
+ const limit = pageSize;
125
+ const startIndex = (currentPage - 1) * pageSize;
126
+
127
+ const urlUrl = useMemo(() => {
128
+ return getPaginatedUrl(url, limit, startIndex);
129
+ }, [url, limit, startIndex]);
130
+
131
+ const swr = immutable ? useSWRImmutable : useSWR;
132
+ const { data, ...rest } = swr(urlUrl, fetcher, swrConfig);
133
+
134
+ if (data?.data) {
135
+ totalCount.current = getTotalCount(data?.data);
136
+ currentPageSize.current = getCurrentPageSize(data?.data);
137
+ }
138
+
139
+ const totalPages = Math.ceil(totalCount.current / pageSize);
140
+
141
+ const goTo = useCallback(
142
+ (page: number) => {
143
+ if (0 < page && page <= totalPages) {
144
+ setCurrentPage(page);
145
+ } else {
146
+ console.warn('Invalid attempt to go to out of bounds page: ' + page);
147
+ }
148
+ },
149
+ [url, currentPage, totalPages],
150
+ );
151
+ const goToNext = useCallback(() => {
152
+ goTo(currentPage + 1);
153
+ }, [url, currentPage, totalPages]);
154
+
155
+ const goToPrevious = useCallback(() => {
156
+ goTo(currentPage - 1);
157
+ }, [url, currentPage, totalPages]);
158
+
159
+ return {
160
+ data: getData(data?.data),
161
+ totalPages,
162
+ totalCount: totalCount.current,
163
+ currentPage,
164
+ currentPageSize,
165
+ paginated: totalPages > 1,
166
+ showNextButton: currentPage < totalPages,
167
+ showPreviousButton: currentPage > 1,
168
+ goTo,
169
+ goToNext,
170
+ goToPrevious,
171
+ ...rest,
172
+ };
173
+ }
@@ -1,112 +0,0 @@
1
- /** @module @category UI */
2
- import { useCallback } from 'react';
3
- import useSWRInfinite, { type SWRInfiniteResponse } from 'swr/infinite';
4
- import { type FetchResponse, openmrsFetch } from '@openmrs/esm-api';
5
- import { type OpenMRSPaginatedResponse } from './useServerPagination';
6
-
7
- // "swr/infinite" doesn't export InfiniteKeyedMutator directly
8
- type InfiniteKeyedMutator<T> = SWRInfiniteResponse<T extends (infer I)[] ? I : T>['mutate'];
9
-
10
- export interface UseServerInfiniteReturnObject<T> {
11
- /**
12
- * The data fetched from the server so far. Note that this array contains
13
- * the aggregate of data from all fetched pages. Unless hasMore == false,
14
- * this array does not contain the complete data set.
15
- */
16
- data: T[] | undefined;
17
-
18
- /**
19
- * The total number of rows in the data set.
20
- */
21
- totalCount: number | undefined;
22
-
23
- /**
24
- * Whether there are more results in the data set that have not been fetched yet.
25
- */
26
- hasMore: boolean;
27
-
28
- /**
29
- * callback function to make another fetch of next page's data set.
30
- */
31
- loadMore: () => void;
32
-
33
- /**
34
- * from useSWRInfinite
35
- */
36
- error: any;
37
-
38
- /**
39
- * from useSWRInfinite
40
- */
41
- mutate: InfiniteKeyedMutator<FetchResponse<OpenMRSPaginatedResponse<T>>[]>;
42
-
43
- /**
44
- * from useSWRInfinite
45
- */
46
- isValidating: boolean;
47
-
48
- /**
49
- * from useSWRInfinite
50
- */
51
- isLoading: boolean;
52
- }
53
-
54
- /**
55
- * Most REST endpoints that return a list of objects, such as getAll or search, are server-side paginated.
56
- * The server limits the max number of results being returned, and multiple requests are needed to get the full data set
57
- * if its size exceeds this limit.
58
- * The max number of results per request is configurable server-side
59
- * with the key "webservices.rest.maxResultsDefault". See: https://openmrs.atlassian.net/wiki/spaces/docs/pages/25469882/REST+Module
60
- *
61
- * This hook fetches data from a paginated rest endpoint, initially by fetching the first page of the results.
62
- * It provides a callback to load data from subsequent pages as needed. This hook is intended to serve UIs that
63
- * provide infinite loading / scrolling of results.
64
- *
65
- * While not ideal, this hook can be used to fetch the complete data set of results (from all pages) as follows:
66
- *
67
- * useEffect(() => hasMore && loadMore(), [hasMore])
68
- *
69
- * The above should only be used when there is a need to fetch the complete data set onto the client side (ex:
70
- * need to support client-side sorting or filtering of data).
71
- *
72
- * @see `useServerPagination` for lazy-loading paginated data`
73
- *
74
- * @param url The URL of the paginated rest endpoint. Note that the `limit` GET param can be set to specify
75
- * the page size; if not set, the page size defaults to the `webservices.rest.maxResultsDefault` value defined
76
- * server-side.
77
- * @param fetcher The fetcher to use. Defaults to openmrsFetch
78
- * @returns a UseServerInfiniteReturnObject object
79
- */
80
- export function useServerInfinite<T>(
81
- url: string | URL,
82
- fetcher: (key: string) => Promise<FetchResponse<OpenMRSPaginatedResponse<T>>> = openmrsFetch,
83
- ): UseServerInfiniteReturnObject<T> {
84
- const getNextUri = useCallback((data: FetchResponse<OpenMRSPaginatedResponse<T>>) => {
85
- return data?.data?.links?.find((link) => link.rel == 'next');
86
- }, []);
87
-
88
- const getKey = useCallback((pageIndex: number, previousPageData: FetchResponse<OpenMRSPaginatedResponse<T>>) => {
89
- if (pageIndex == 0) {
90
- return url;
91
- } else {
92
- return getNextUri(previousPageData)?.uri ?? null;
93
- }
94
- }, []);
95
-
96
- const { data, size, setSize, ...rest } = useSWRInfinite<FetchResponse<OpenMRSPaginatedResponse<T>>>(getKey, fetcher);
97
- const nextUri = data?.[data.length - 1].data?.links?.find((link) => link.rel == 'next');
98
-
99
- const hasMore = nextUri != null;
100
- const loadMore = () => {
101
- setSize(size + 1);
102
- };
103
- const totalCount = data?.[0]?.data?.totalCount;
104
-
105
- return {
106
- data: data?.flatMap((d) => d.data.results),
107
- totalCount,
108
- hasMore,
109
- loadMore,
110
- ...rest,
111
- };
112
- }
@@ -1,100 +0,0 @@
1
- /** @module @category UI */
2
- import { type FetchResponse, openmrsFetch } from '@openmrs/esm-api';
3
- import { useCallback, useRef, useState } from 'react';
4
- import useSWR from 'swr';
5
-
6
- export interface OpenMRSPaginatedResponse<T> {
7
- results: Array<T>;
8
- links: Array<{ rel: 'prev' | 'next'; uri: string }>;
9
- totalCount: number;
10
- }
11
-
12
- /**
13
- * Most REST endpoints that return a list of objects, such as getAll or search, are server-side paginated.
14
- * The server limits the max number of results being returned, and multiple requests are needed to get the full data set
15
- * if its size exceeds this limit.
16
- * The max number of results per request is configurable server-side
17
- * with the key "webservices.rest.maxResultsDefault". See: https://openmrs.atlassian.net/wiki/spaces/docs/pages/25469882/REST+Module
18
- *
19
- * For any UI that displays a paginated view of the full data set, we MUST handle the server-side pagination properly,
20
- * or else the UI does not correctly display the full data set.
21
- * This hook does that by providing callback functions for navigating to different pages of the results, and
22
- * lazy-loads the data on each page as needed.
23
- *
24
- * Note that this hook is not suitable for use for situations that require client-side sorting or filtering
25
- * of the data set. In that case, all data must be loaded onto client-side first.
26
- *
27
- * @see `useServerInfinite` for completely loading data (from all pages) onto client side
28
- * @see `usePagination` for pagination of client-side data`
29
- *
30
- * @param url The URL of the paginated rest endpoint.
31
- * It should be populated with any needed GET params, except `limit`, `startIndex` or `totalCount`,
32
- * which will be overridden and manipulated by the `goTo*` callbacks
33
- * @param pageSize The number of results to return per page / fetch. Note that this value MUST NOT exceed
34
- * "webservices.rest.maxResultsAbsolute", which should be reasonably high by default (1000).
35
- * @param fetcher The fetcher to use. Defaults to openmrsFetch
36
- * @returns
37
- */
38
- export function useServerPagination<T>(
39
- url: string | URL,
40
- pageSize: number,
41
- fetcher: (key: string) => Promise<FetchResponse<OpenMRSPaginatedResponse<T>>> = openmrsFetch,
42
- ) {
43
- const [currentPage, setCurrentPage] = useState<number>(1); // 1-indexing instead of 0-indexing, to keep consistency with `usePagination`
44
-
45
- // Cache the totalCount and currentPageSize so we don't lose them
46
- // as we wait for next page's result to load while navigating to a different page.
47
- // This can be used to prevent jarring UI changes while loading
48
- const totalCount = useRef<number>(Number.NaN);
49
- const currentPageSize = useRef<number>(Number.NaN); // this value is usually same as pageSize, except when currentPage is last page.
50
-
51
- const limit = pageSize;
52
- const startIndex = (currentPage - 1) * pageSize;
53
-
54
- const urlUrl = new URL(url, window.location.toString());
55
- urlUrl.searchParams.set('limit', '' + limit);
56
- urlUrl.searchParams.set('startIndex', '' + startIndex);
57
- urlUrl.searchParams.set('totalCount', 'true');
58
-
59
- const { data, ...rest } = useSWR(urlUrl.toString(), fetcher);
60
-
61
- if (data?.data) {
62
- totalCount.current = data.data?.totalCount;
63
- currentPageSize.current = data.data?.results.length;
64
- }
65
-
66
- const totalPages = Math.ceil(totalCount.current / pageSize);
67
-
68
- const goTo = useCallback(
69
- (page: number) => {
70
- if (0 < page && page <= totalPages) {
71
- setCurrentPage(page);
72
- } else {
73
- console.warn('Invalid attempt to go to out of bounds page: ' + page);
74
- }
75
- },
76
- [url, currentPage, totalPages],
77
- );
78
- const goToNext = useCallback(() => {
79
- goTo(currentPage + 1);
80
- }, [url, currentPage, totalPages]);
81
-
82
- const goToPrevious = useCallback(() => {
83
- goTo(currentPage - 1);
84
- }, [url, currentPage, totalPages]);
85
-
86
- return {
87
- data: data?.data?.results,
88
- totalPages,
89
- totalCount: totalCount.current,
90
- currentPage,
91
- currentPageSize,
92
- paginated: totalPages > 1,
93
- showNextButton: currentPage < totalPages,
94
- showPreviousButton: currentPage > 1,
95
- goTo,
96
- goToNext,
97
- goToPrevious,
98
- ...rest,
99
- };
100
- }