@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.
- package/.turbo/turbo-build.log +6 -6
- package/dist/openmrs-esm-react-utils.js +1 -1
- package/dist/openmrs-esm-react-utils.js.map +1 -1
- package/mock.tsx +12 -5
- package/package.json +10 -10
- package/src/index.ts +6 -2
- package/src/public.ts +6 -2
- package/src/useFhirFetchAll.ts +24 -0
- package/src/useFhirInfinite.ts +29 -0
- package/src/useFhirPagination.ts +66 -0
- package/src/useOpenmrsFetchAll.test.tsx +55 -0
- package/src/useOpenmrsFetchAll.ts +70 -0
- package/src/{useServerInfinite.test.tsx → useOpenmrsInfinite.test.tsx} +13 -12
- package/src/useOpenmrsInfinite.ts +152 -0
- package/src/{useServerPagination.test.tsx → useOpenmrsPagination.test.tsx} +24 -14
- package/src/useOpenmrsPagination.ts +173 -0
- package/src/useServerInfinite.ts +0 -112
- package/src/useServerPagination.ts +0 -100
|
@@ -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
|
+
}
|
package/src/useServerInfinite.ts
DELETED
|
@@ -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
|
-
}
|