@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
package/mock.tsx
CHANGED
|
@@ -6,8 +6,12 @@ import { createGlobalStore } from '@openmrs/esm-state/mock';
|
|
|
6
6
|
import {
|
|
7
7
|
isDesktop as realIsDesktop,
|
|
8
8
|
usePagination as realUsePagination,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
useOpenmrsPagination as realUseOpenmrsrPagination,
|
|
10
|
+
useOpenmrsInfinite as realUseOpenmrsInfinite,
|
|
11
|
+
useOpenmrsFetchAll as realUseOpenmrsFetchAll,
|
|
12
|
+
useFhirPagination as realUseFhirPagination,
|
|
13
|
+
useFhirInfinite as realUseFhirInfinite,
|
|
14
|
+
useFhirFetchAll as realUseFhirFetchAll,
|
|
11
15
|
} from './src/index';
|
|
12
16
|
export { ConfigurableLink, useStore, useStoreWithActions, createUseStore } from './src/index';
|
|
13
17
|
import * as utils from '@openmrs/esm-utils';
|
|
@@ -71,9 +75,12 @@ export const useFeatureFlag = jest.fn().mockReturnValue(true);
|
|
|
71
75
|
|
|
72
76
|
export const usePagination = jest.fn(realUsePagination);
|
|
73
77
|
|
|
74
|
-
export const
|
|
75
|
-
|
|
76
|
-
export const
|
|
78
|
+
export const useOpenmrsPagination = jest.fn(realUseOpenmrsrPagination);
|
|
79
|
+
export const useOpenmrsInfinite = jest.fn(realUseOpenmrsInfinite);
|
|
80
|
+
export const useOpenmrsFetchAll = jest.fn(realUseOpenmrsFetchAll);
|
|
81
|
+
export const useFhirPagination = jest.fn(realUseFhirPagination);
|
|
82
|
+
export const useFhirInfinite = jest.fn(realUseFhirInfinite);
|
|
83
|
+
export const useFhirFetchAll = jest.fn(realUseFhirFetchAll);
|
|
77
84
|
|
|
78
85
|
export const useVisit = jest.fn().mockReturnValue({
|
|
79
86
|
error: null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-react-utils",
|
|
3
|
-
"version": "5.8.1-pre.
|
|
3
|
+
"version": "5.8.1-pre.2255",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "React utilities for OpenMRS.",
|
|
6
6
|
"browser": "dist/openmrs-esm-react-utils.js",
|
|
@@ -61,15 +61,15 @@
|
|
|
61
61
|
"swr": "2.x"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@openmrs/esm-api": "5.8.1-pre.
|
|
65
|
-
"@openmrs/esm-config": "5.8.1-pre.
|
|
66
|
-
"@openmrs/esm-context": "5.8.1-pre.
|
|
67
|
-
"@openmrs/esm-error-handling": "5.8.1-pre.
|
|
68
|
-
"@openmrs/esm-extensions": "5.8.1-pre.
|
|
69
|
-
"@openmrs/esm-feature-flags": "5.8.1-pre.
|
|
70
|
-
"@openmrs/esm-globals": "5.8.1-pre.
|
|
71
|
-
"@openmrs/esm-navigation": "5.8.1-pre.
|
|
72
|
-
"@openmrs/esm-utils": "5.8.1-pre.
|
|
64
|
+
"@openmrs/esm-api": "5.8.1-pre.2255",
|
|
65
|
+
"@openmrs/esm-config": "5.8.1-pre.2255",
|
|
66
|
+
"@openmrs/esm-context": "5.8.1-pre.2255",
|
|
67
|
+
"@openmrs/esm-error-handling": "5.8.1-pre.2255",
|
|
68
|
+
"@openmrs/esm-extensions": "5.8.1-pre.2255",
|
|
69
|
+
"@openmrs/esm-feature-flags": "5.8.1-pre.2255",
|
|
70
|
+
"@openmrs/esm-globals": "5.8.1-pre.2255",
|
|
71
|
+
"@openmrs/esm-navigation": "5.8.1-pre.2255",
|
|
72
|
+
"@openmrs/esm-utils": "5.8.1-pre.2255",
|
|
73
73
|
"dayjs": "^1.10.8",
|
|
74
74
|
"i18next": "^21.10.0",
|
|
75
75
|
"react": "^18.1.0",
|
package/src/index.ts
CHANGED
|
@@ -35,5 +35,9 @@ export * from './useVisit';
|
|
|
35
35
|
export * from './useVisitTypes';
|
|
36
36
|
export * from './usePagination';
|
|
37
37
|
export * from './usePrimaryIdentifierResource';
|
|
38
|
-
export * from './
|
|
39
|
-
export * from './
|
|
38
|
+
export * from './useFhirPagination';
|
|
39
|
+
export * from './useFhirInfinite';
|
|
40
|
+
export * from './useFhirFetchAll';
|
|
41
|
+
export { useOpenmrsPagination, type UseServerPaginationOptions } from './useOpenmrsPagination';
|
|
42
|
+
export { useOpenmrsInfinite, type UseServerInfiniteOptions } from './useOpenmrsInfinite';
|
|
43
|
+
export { useOpenmrsFetchAll, type UseServerFetchAllOptions } from './useOpenmrsFetchAll';
|
package/src/public.ts
CHANGED
|
@@ -31,5 +31,9 @@ export * from './useVisit';
|
|
|
31
31
|
export * from './useVisitTypes';
|
|
32
32
|
export * from './usePagination';
|
|
33
33
|
export * from './usePrimaryIdentifierResource';
|
|
34
|
-
export * from './
|
|
35
|
-
export * from './
|
|
34
|
+
export * from './useFhirPagination';
|
|
35
|
+
export * from './useFhirInfinite';
|
|
36
|
+
export * from './useFhirFetchAll';
|
|
37
|
+
export { useOpenmrsPagination, type UseServerPaginationOptions } from './useOpenmrsPagination';
|
|
38
|
+
export { useOpenmrsInfinite, type UseServerInfiniteOptions } from './useOpenmrsInfinite';
|
|
39
|
+
export { useOpenmrsFetchAll, type UseServerFetchAllOptions } from './useOpenmrsFetchAll';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type UseServerInfiniteReturnObject } from './useOpenmrsInfinite';
|
|
2
|
+
import { getFhirServerPaginationHandlers } from './useFhirPagination';
|
|
3
|
+
import { useServerFetchAll, type UseServerFetchAllOptions } from './useOpenmrsFetchAll';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This hook handles fetching results from *all* pages of a paginated FHIR REST endpoint, making multiple requests
|
|
7
|
+
* as needed.
|
|
8
|
+
* This function is the FHIR counterpart of `useOpenmrsPagination`.
|
|
9
|
+
*
|
|
10
|
+
* @see `useFhirPagination`
|
|
11
|
+
* @see `useFhirInfinite`
|
|
12
|
+
* @see `useOpenmrsFetchAll``
|
|
13
|
+
*
|
|
14
|
+
* @param url The URL of the paginated rest endpoint.
|
|
15
|
+
* Similar to useSWRInfinite, this param can be null to disable fetching.
|
|
16
|
+
* @param options The options object
|
|
17
|
+
* @returns a UseFhirInfiniteReturnObject object
|
|
18
|
+
*/
|
|
19
|
+
export function useFhirFetchAll<T extends fhir.ResourceBase>(
|
|
20
|
+
url,
|
|
21
|
+
options: UseServerFetchAllOptions<fhir.Bundle> = {},
|
|
22
|
+
): UseServerInfiniteReturnObject<T, fhir.Bundle> {
|
|
23
|
+
return useServerFetchAll<T, fhir.Bundle>(url, getFhirServerPaginationHandlers(), options);
|
|
24
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { getFhirServerPaginationHandlers } from './useFhirPagination';
|
|
2
|
+
import {
|
|
3
|
+
useServerInfinite,
|
|
4
|
+
type UseServerInfiniteOptions,
|
|
5
|
+
type UseServerInfiniteReturnObject,
|
|
6
|
+
} from './useOpenmrsInfinite';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fhir REST endpoints that return a list of objects, are server-side paginated.
|
|
10
|
+
* The server limits the max number of results being returned, and multiple requests are needed to get the full data set
|
|
11
|
+
* if its size exceeds this limit.
|
|
12
|
+
*
|
|
13
|
+
* This function is the FHIR counterpart of `useOpenmrsInfinite`.
|
|
14
|
+
*
|
|
15
|
+
* @see `useFhirPagination`
|
|
16
|
+
* @see `useFhirFetchAll`
|
|
17
|
+
* @see `useOpenmrsInfinite`
|
|
18
|
+
*
|
|
19
|
+
* @param url The URL of the paginated rest endpoint.
|
|
20
|
+
* Similar to useSWRInfinite, this param can be null to disable fetching.
|
|
21
|
+
* @param options The options object
|
|
22
|
+
* @returns a UseServerInfiniteReturnObject object
|
|
23
|
+
*/
|
|
24
|
+
export function useFhirInfinite<T extends fhir.ResourceBase>(
|
|
25
|
+
url: string | URL,
|
|
26
|
+
options: UseServerInfiniteOptions<fhir.Bundle> = {},
|
|
27
|
+
): UseServerInfiniteReturnObject<T, fhir.Bundle> {
|
|
28
|
+
return useServerInfinite<T, fhir.Bundle>(url, getFhirServerPaginationHandlers(), options);
|
|
29
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** @module @category UI */
|
|
2
|
+
import { type FetchResponse, makeUrl, openmrsFetch } from '@openmrs/esm-api';
|
|
3
|
+
import {
|
|
4
|
+
type ServerPaginationHandlers,
|
|
5
|
+
useServerPagination,
|
|
6
|
+
type UseServerPaginationOptions,
|
|
7
|
+
} from './useOpenmrsPagination';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fhir REST endpoints that return a list of objects, are server-side paginated.
|
|
11
|
+
* The server limits the max number of results being returned, and multiple requests are needed to get the full data set
|
|
12
|
+
* if its size exceeds this limit.
|
|
13
|
+
*
|
|
14
|
+
* This function is the FHIR counterpart of `useOpenmrsPagination`.
|
|
15
|
+
*
|
|
16
|
+
* @see `useOpenmrsPagination
|
|
17
|
+
* @see `useFhirInfinite`
|
|
18
|
+
* @see `useFhirFetchAll`
|
|
19
|
+
* @see `usePagination` for pagination of client-side data`
|
|
20
|
+
*
|
|
21
|
+
* @param url The URL of the paginated rest endpoint.
|
|
22
|
+
* which will be overridden and manipulated by the `goTo*` callbacks
|
|
23
|
+
* @param pageSize The number of results to return per page / fetch.
|
|
24
|
+
* @param options The options object
|
|
25
|
+
* @returns
|
|
26
|
+
*/
|
|
27
|
+
export function useFhirPagination<T extends fhir.ResourceBase>(
|
|
28
|
+
url: string | URL,
|
|
29
|
+
pageSize: number,
|
|
30
|
+
options: UseServerPaginationOptions<fhir.Bundle> = {},
|
|
31
|
+
) {
|
|
32
|
+
return useServerPagination<T, fhir.Bundle>(url, pageSize, getFhirServerPaginationHandlers<T>(), options);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type FhirServerPaginationHandlers<T> = ServerPaginationHandlers<T, fhir.Bundle>;
|
|
36
|
+
export function getFhirServerPaginationHandlers<T>(): FhirServerPaginationHandlers<T> {
|
|
37
|
+
return {
|
|
38
|
+
getPaginatedUrl: (url: string | URL, limit: number, startIndex: number) => {
|
|
39
|
+
if (url) {
|
|
40
|
+
const urlUrl = new URL(makeUrl(url.toString()), window.location.toString());
|
|
41
|
+
urlUrl.searchParams.set('_count', '' + limit);
|
|
42
|
+
urlUrl.searchParams.set('_getpagesoffset', '' + startIndex);
|
|
43
|
+
return urlUrl.toString();
|
|
44
|
+
} else {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
getNextUrl: (response) => {
|
|
49
|
+
const uri = response?.link?.find((link) => link.relation == 'next')?.url;
|
|
50
|
+
if (uri) {
|
|
51
|
+
const url = new URL(uri);
|
|
52
|
+
|
|
53
|
+
// allows frontend proxies to work
|
|
54
|
+
url.host = window.location.host;
|
|
55
|
+
url.protocol = window.location.protocol;
|
|
56
|
+
|
|
57
|
+
return url.toString();
|
|
58
|
+
} else {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
getTotalCount: (response) => response?.total ?? Number.NaN,
|
|
63
|
+
getCurrentPageSize: (response) => response?.entry?.length ?? Number.NaN,
|
|
64
|
+
getData: (response) => response?.entry?.map((entry) => entry.resource) as T[],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { renderHook, cleanup, waitFor, act } from '@testing-library/react';
|
|
2
|
+
import '@testing-library/jest-dom';
|
|
3
|
+
import { useOpenmrsFetchAll } from './useOpenmrsFetchAll';
|
|
4
|
+
import { type OpenMRSPaginatedResponse } from './useOpenmrsPagination';
|
|
5
|
+
|
|
6
|
+
// returns an sequentially increasing int array of specified length starting at the specified start integer.
|
|
7
|
+
export function getIntArray(start: number, length: number) {
|
|
8
|
+
return new Array(length).fill(0).map((_, i) => start + i);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// This function mocks the return value of a server-side paginated API.
|
|
12
|
+
// It returns a slice (page) of the array of integers [0...totalCount-1],
|
|
13
|
+
// with the page defined by the limit and startIndex in the url params.
|
|
14
|
+
export async function getTestData(url: string, totalCount: number): Promise<OpenMRSPaginatedResponse<number>> {
|
|
15
|
+
const urlUrl = new URL(url, window.location.toString());
|
|
16
|
+
const limit = Number.parseInt(urlUrl.searchParams.get('limit') ?? '50');
|
|
17
|
+
const startIndex = Number.parseInt(urlUrl.searchParams.get('startIndex') ?? '0');
|
|
18
|
+
|
|
19
|
+
const length = Math.max(0, Math.min(totalCount - startIndex, limit));
|
|
20
|
+
const results = new Array(length).fill(0).map((_, i) => i + startIndex);
|
|
21
|
+
const hasNext = startIndex + limit < totalCount;
|
|
22
|
+
if (hasNext) {
|
|
23
|
+
urlUrl.searchParams.set('startIndex', startIndex + limit + '');
|
|
24
|
+
}
|
|
25
|
+
const links = hasNext ? [{ rel: 'next', uri: urlUrl.toString() }] : [];
|
|
26
|
+
return { results, links, totalCount } as OpenMRSPaginatedResponse<number>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('useOpenmrsFetchAll', () => {
|
|
30
|
+
afterEach(cleanup);
|
|
31
|
+
|
|
32
|
+
it('should render all rows on if number of rows < pageSize', async () => {
|
|
33
|
+
const expectedRowCount = 17;
|
|
34
|
+
const { result } = renderHook(() =>
|
|
35
|
+
useOpenmrsFetchAll(`http://localhost/1`, {
|
|
36
|
+
fetcher: (url) => getTestData(url, expectedRowCount).then((data) => ({ data }) as any),
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
await waitFor(() => expect(result.current.isLoading).toBeFalsy());
|
|
40
|
+
expect(result.current.totalCount).toEqual(expectedRowCount);
|
|
41
|
+
expect(result.current.data).toEqual(getIntArray(0, 17));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should render all rows on if number of rows > pageSize with no partialData', async () => {
|
|
45
|
+
const expectedRowCount = 75;
|
|
46
|
+
const { result } = renderHook(() =>
|
|
47
|
+
useOpenmrsFetchAll(`http://localhost/2`, {
|
|
48
|
+
fetcher: (url) => getTestData(url, expectedRowCount).then((data) => ({ data }) as any),
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
await waitFor(() => expect(result.current.isLoading).toBeFalsy());
|
|
52
|
+
expect(result.current.totalCount).toEqual(expectedRowCount);
|
|
53
|
+
expect(result.current.data).toEqual(getIntArray(0, 75));
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** @module @category UI */
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
useServerInfinite,
|
|
5
|
+
type UseServerInfiniteOptions,
|
|
6
|
+
type UseServerInfiniteReturnObject,
|
|
7
|
+
} from './useOpenmrsInfinite';
|
|
8
|
+
import {
|
|
9
|
+
type OpenMRSPaginatedResponse,
|
|
10
|
+
openmrsServerPaginationHandlers,
|
|
11
|
+
type ServerPaginationHandlers,
|
|
12
|
+
} from './useOpenmrsPagination';
|
|
13
|
+
|
|
14
|
+
export interface UseServerFetchAllOptions<R> extends UseServerInfiniteOptions<R> {
|
|
15
|
+
/**
|
|
16
|
+
* If true, the data of any page is returned as soon as they are fetched.
|
|
17
|
+
* This is useful when you want to display data as soon as possible, even if not all pages are fetched.
|
|
18
|
+
* If false, the returned data will be undefined until all pages are fetched. This is useful when you want to
|
|
19
|
+
* display all data at once or reduce the number of re-renders (to avoid confusing users).
|
|
20
|
+
*/
|
|
21
|
+
partialData?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Most OpenMRS REST endpoints that return a list of objects, such as getAll or search, are server-side paginated.
|
|
26
|
+
* This hook handles fetching results from *all* pages of a paginated OpenMRS REST endpoint, making multiple requests
|
|
27
|
+
* as needed.
|
|
28
|
+
*
|
|
29
|
+
* @see `useOpenmrsPagination`
|
|
30
|
+
* @see `useOpenmrsInfinite`
|
|
31
|
+
* @see `useFhirFetchAll`
|
|
32
|
+
*
|
|
33
|
+
* @param url The URL of the paginated OpenMRS REST endpoint. Note that the `limit` GET param can be set to specify
|
|
34
|
+
* the page size; if not set, the page size defaults to the `webservices.rest.maxResultsDefault` value defined
|
|
35
|
+
* server-side.
|
|
36
|
+
* Similar to useSWRInfinite, this param can be null to disable fetching.
|
|
37
|
+
* @param options The options object
|
|
38
|
+
* @returns a UseOpenmrsInfiniteReturnObject object
|
|
39
|
+
*/
|
|
40
|
+
export function useOpenmrsFetchAll<T>(
|
|
41
|
+
url: string | URL,
|
|
42
|
+
options: UseServerFetchAllOptions<OpenMRSPaginatedResponse<T>> = {},
|
|
43
|
+
): UseServerInfiniteReturnObject<T, OpenMRSPaginatedResponse<T>> {
|
|
44
|
+
return useServerFetchAll<T, OpenMRSPaginatedResponse<T>>(url, openmrsServerPaginationHandlers, options);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useServerFetchAll<T, R>(
|
|
48
|
+
url: string | URL,
|
|
49
|
+
serverPaginationHandlers: ServerPaginationHandlers<T, R>,
|
|
50
|
+
options: UseServerFetchAllOptions<R> = {},
|
|
51
|
+
): UseServerInfiniteReturnObject<T, R> {
|
|
52
|
+
const response = useServerInfinite<T, R>(url, serverPaginationHandlers, options);
|
|
53
|
+
const { hasMore, error, data, loadMore, isLoading } = response;
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (hasMore && !error) {
|
|
57
|
+
loadMore();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (options.partialData) {
|
|
62
|
+
return response;
|
|
63
|
+
} else {
|
|
64
|
+
return {
|
|
65
|
+
...response,
|
|
66
|
+
data: hasMore || error ? undefined : data,
|
|
67
|
+
isLoading: isLoading || hasMore,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import '@testing-library/jest-dom';
|
|
2
2
|
import { act, cleanup, renderHook, waitFor } from '@testing-library/react';
|
|
3
|
-
import {
|
|
4
|
-
import { getIntArray, getTestData } from './
|
|
3
|
+
import { useOpenmrsInfinite } from './useOpenmrsInfinite';
|
|
4
|
+
import { getIntArray, getTestData } from './useOpenmrsPagination.test';
|
|
5
5
|
|
|
6
|
-
describe('
|
|
6
|
+
describe('useOpenmrsInfinite', () => {
|
|
7
7
|
afterEach(cleanup);
|
|
8
8
|
|
|
9
9
|
it('should load all rows with 1 fetch if number of rows < pageSize', async () => {
|
|
10
10
|
const pageSize = 20;
|
|
11
11
|
const expectedRowCount = 17;
|
|
12
12
|
const { result } = renderHook(() =>
|
|
13
|
-
|
|
14
|
-
getTestData(url, expectedRowCount).then((data) => ({ data }) as any),
|
|
15
|
-
),
|
|
13
|
+
useOpenmrsInfinite(`http://localhost/1?limit=${pageSize}`, {
|
|
14
|
+
fetcher: (url) => getTestData(url, expectedRowCount).then((data) => ({ data }) as any),
|
|
15
|
+
}),
|
|
16
16
|
);
|
|
17
|
+
|
|
17
18
|
await waitFor(() => expect(result.current.isLoading).toBeFalsy());
|
|
18
19
|
|
|
19
20
|
expect(result.current.data?.length).toBe(expectedRowCount);
|
|
@@ -26,9 +27,9 @@ describe('useServerInfinite', () => {
|
|
|
26
27
|
const pageSize = 20;
|
|
27
28
|
const expectedRowCount = 40;
|
|
28
29
|
const { result } = renderHook(() =>
|
|
29
|
-
|
|
30
|
-
getTestData(url, expectedRowCount).then((data) => ({ data }) as any),
|
|
31
|
-
),
|
|
30
|
+
useOpenmrsInfinite(`http://localhost/2?limit=${pageSize}`, {
|
|
31
|
+
fetcher: (url) => getTestData(url, expectedRowCount).then((data) => ({ data }) as any),
|
|
32
|
+
}),
|
|
32
33
|
);
|
|
33
34
|
|
|
34
35
|
await waitFor(() => expect(result.current.isLoading).toBeFalsy());
|
|
@@ -51,9 +52,9 @@ describe('useServerInfinite', () => {
|
|
|
51
52
|
const pageSize = 100;
|
|
52
53
|
const expectedRowCount = 1337;
|
|
53
54
|
const { result } = renderHook(() =>
|
|
54
|
-
|
|
55
|
-
getTestData(url, expectedRowCount).then((data) => ({ data }) as any),
|
|
56
|
-
),
|
|
55
|
+
useOpenmrsInfinite(`http://localhost/3?limit=${pageSize}`, {
|
|
56
|
+
fetcher: (url) => getTestData(url, expectedRowCount).then((data) => ({ data }) as any),
|
|
57
|
+
}),
|
|
57
58
|
);
|
|
58
59
|
await waitFor(() => expect(result.current.isLoading).toBeFalsy());
|
|
59
60
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/** @module @category UI */
|
|
2
|
+
import { type FetchResponse, openmrsFetch } from '@openmrs/esm-api';
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
import { type KeyedMutator } from 'swr';
|
|
5
|
+
import useSWRInfinite, { type SWRInfiniteConfiguration, type SWRInfiniteResponse } from 'swr/infinite';
|
|
6
|
+
import {
|
|
7
|
+
openmrsServerPaginationHandlers,
|
|
8
|
+
type ServerPaginationHandlers,
|
|
9
|
+
type OpenMRSPaginatedResponse,
|
|
10
|
+
} from './useOpenmrsPagination';
|
|
11
|
+
|
|
12
|
+
// "swr/infinite" doesn't export InfiniteKeyedMutator directly
|
|
13
|
+
type InfiniteKeyedMutator<T> = SWRInfiniteResponse<T extends (infer I)[] ? I : T>['mutate'];
|
|
14
|
+
|
|
15
|
+
export interface UseServerInfiniteOptions<R> {
|
|
16
|
+
/**
|
|
17
|
+
* The fetcher to use. Defaults to openmrsFetch
|
|
18
|
+
*/
|
|
19
|
+
fetcher?: (key: string) => Promise<FetchResponse<R>>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* If true, sets these options in swrInfintieConfig to false:
|
|
23
|
+
* revalidateIfStale, revalidateOnFocus, revalidateOnReconnect
|
|
24
|
+
* This should be the counterpart of using useSWRImmutable` for `useSWRInfinite`
|
|
25
|
+
*/
|
|
26
|
+
immutable?: boolean;
|
|
27
|
+
|
|
28
|
+
swrInfiniteConfig?: SWRInfiniteConfiguration;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface UseServerInfiniteReturnObject<T, R> {
|
|
32
|
+
/**
|
|
33
|
+
* The data fetched from the server so far. Note that this array contains
|
|
34
|
+
* the aggregate of data from all fetched pages. Unless hasMore == false,
|
|
35
|
+
* this array does not contain the complete data set.
|
|
36
|
+
*/
|
|
37
|
+
data: T[] | undefined;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The total number of rows in the data set.
|
|
41
|
+
*/
|
|
42
|
+
totalCount: number | undefined;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Whether there are more results in the data set that have not been fetched yet.
|
|
46
|
+
*/
|
|
47
|
+
hasMore: boolean;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* callback function to make another fetch of next page's data set.
|
|
51
|
+
*/
|
|
52
|
+
loadMore: () => void;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* from useSWRInfinite
|
|
56
|
+
*/
|
|
57
|
+
error: any;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* from useSWRInfinite
|
|
61
|
+
*/
|
|
62
|
+
mutate: InfiniteKeyedMutator<FetchResponse<R>[]>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* from useSWRInfinite
|
|
66
|
+
*/
|
|
67
|
+
isValidating: boolean;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* from useSWRInfinite
|
|
71
|
+
*/
|
|
72
|
+
isLoading: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Most REST endpoints that return a list of objects, such as getAll or search, are server-side paginated.
|
|
77
|
+
* The server limits the max number of results being returned, and multiple requests are needed to get the full data set
|
|
78
|
+
* if its size exceeds this limit.
|
|
79
|
+
* The max number of results per request is configurable server-side
|
|
80
|
+
* with the key "webservices.rest.maxResultsDefault". See: https://openmrs.atlassian.net/wiki/spaces/docs/pages/25469882/REST+Module
|
|
81
|
+
*
|
|
82
|
+
* This hook fetches data from a paginated rest endpoint, initially by fetching the first page of the results.
|
|
83
|
+
* It provides a callback to load data from subsequent pages as needed. This hook is intended to serve UIs that
|
|
84
|
+
* provide infinite loading / scrolling of results. Unlike `useOpenmrsPagination`, this hook does not allow random access
|
|
85
|
+
* (and lazy-loading) of any arbitrary page; rather, it fetches pages sequentially starting form the initial page, and the next page
|
|
86
|
+
* is fetched by calling `loadMore`. See: https://swr.vercel.app/docs/pagination#useswrinfinite
|
|
87
|
+
*
|
|
88
|
+
* @see `useOpenmrsPagination`
|
|
89
|
+
* @see `useOpenmrsFetchAll`
|
|
90
|
+
* @see `useFhirInfinite`
|
|
91
|
+
*
|
|
92
|
+
* @param url The URL of the paginated rest endpoint. Note that the `limit` GET param can be set to specify
|
|
93
|
+
* the page size; if not set, the page size defaults to the `webservices.rest.maxResultsDefault` value defined
|
|
94
|
+
* server-side.
|
|
95
|
+
* Similar to useSWRInfinite, this param can be null to disable fetching.
|
|
96
|
+
* @param options The options object
|
|
97
|
+
* @returns a UseServerInfiniteReturnObject object
|
|
98
|
+
*/
|
|
99
|
+
export function useOpenmrsInfinite<T>(
|
|
100
|
+
url: string | URL,
|
|
101
|
+
options: UseServerInfiniteOptions<OpenMRSPaginatedResponse<T>> = {},
|
|
102
|
+
): UseServerInfiniteReturnObject<T, OpenMRSPaginatedResponse<T>> {
|
|
103
|
+
return useServerInfinite<T, OpenMRSPaginatedResponse<T>>(url, openmrsServerPaginationHandlers, options);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function useServerInfinite<T, R>(
|
|
107
|
+
url: string | URL,
|
|
108
|
+
serverPaginationHandlers: ServerPaginationHandlers<T, R>,
|
|
109
|
+
options: UseServerInfiniteOptions<R> = {},
|
|
110
|
+
): UseServerInfiniteReturnObject<T, R> {
|
|
111
|
+
const { swrInfiniteConfig, immutable } = options;
|
|
112
|
+
const { getNextUrl, getTotalCount, getData } = serverPaginationHandlers;
|
|
113
|
+
const fetcher: (key: string) => Promise<FetchResponse<R>> = options.fetcher ?? openmrsFetch;
|
|
114
|
+
const getKey = useCallback(
|
|
115
|
+
(pageIndex: number, previousPageData: FetchResponse<R>) => {
|
|
116
|
+
if (pageIndex == 0) {
|
|
117
|
+
return url;
|
|
118
|
+
} else {
|
|
119
|
+
return serverPaginationHandlers.getNextUrl(previousPageData.data);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
[url],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const { data, size, setSize, ...rest } = useSWRInfinite<FetchResponse<R>>(getKey, fetcher, {
|
|
126
|
+
...swrInfiniteConfig,
|
|
127
|
+
// `useSWR` has a useSWRImmutable counterpart, but `useSWRInfinite` does not seem to.
|
|
128
|
+
// Setting the revalidate params manually if immutable is true, see: https://swr.vercel.app/docs/revalidation
|
|
129
|
+
...(immutable
|
|
130
|
+
? {
|
|
131
|
+
revalidateIfStale: false,
|
|
132
|
+
revalidateOnFocus: false,
|
|
133
|
+
revalidateOnReconnect: false,
|
|
134
|
+
}
|
|
135
|
+
: {}),
|
|
136
|
+
});
|
|
137
|
+
const nextUri = getNextUrl(data?.[data.length - 1].data);
|
|
138
|
+
|
|
139
|
+
const hasMore = nextUri != null;
|
|
140
|
+
const loadMore = () => {
|
|
141
|
+
setSize((data?.length ?? 0) + 1);
|
|
142
|
+
};
|
|
143
|
+
const totalCount = getTotalCount(data?.[0]?.data);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
data: data?.flatMap((d) => getData(d.data) as T[]),
|
|
147
|
+
totalCount,
|
|
148
|
+
hasMore,
|
|
149
|
+
loadMore,
|
|
150
|
+
...rest,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { renderHook, cleanup, waitFor, act } from '@testing-library/react';
|
|
2
2
|
import '@testing-library/jest-dom';
|
|
3
|
-
import {
|
|
3
|
+
import { useOpenmrsPagination, type OpenMRSPaginatedResponse } from './useOpenmrsPagination';
|
|
4
4
|
|
|
5
5
|
// returns an sequentially increasing int array of specified length starting at the specified start integer.
|
|
6
6
|
export function getIntArray(start: number, length: number) {
|
|
@@ -25,16 +25,26 @@ export async function getTestData(url: string, totalCount: number): Promise<Open
|
|
|
25
25
|
return { results, links, totalCount } as OpenMRSPaginatedResponse<number>;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
describe('
|
|
28
|
+
describe('useOpenmrsPagination', () => {
|
|
29
29
|
afterEach(cleanup);
|
|
30
30
|
|
|
31
|
-
it('should
|
|
31
|
+
it('should not fetch anything if url is null', async () => {
|
|
32
|
+
const { result } = renderHook(() =>
|
|
33
|
+
useOpenmrsPagination(null as any, 50, {
|
|
34
|
+
fetcher: (url) => getTestData(url, 100).then((data) => ({ data }) as any),
|
|
35
|
+
}),
|
|
36
|
+
);
|
|
37
|
+
expect(result.current.isLoading).toBeFalsy();
|
|
38
|
+
expect(result.current.data).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should fetch all rows on 1 page if number of rows < pageSize', async () => {
|
|
32
42
|
const pageSize = 20;
|
|
33
43
|
const expectedRowCount = 17;
|
|
34
44
|
const { result } = renderHook(() =>
|
|
35
|
-
|
|
36
|
-
getTestData(url, expectedRowCount).then((data) => ({ data }) as any),
|
|
37
|
-
),
|
|
45
|
+
useOpenmrsPagination('http://localhost/1', pageSize, {
|
|
46
|
+
fetcher: (url) => getTestData(url, expectedRowCount).then((data) => ({ data }) as any),
|
|
47
|
+
}),
|
|
38
48
|
);
|
|
39
49
|
await waitFor(() => expect(result.current.isLoading).toBeFalsy());
|
|
40
50
|
expect(result.current.totalPages).toEqual(1);
|
|
@@ -45,13 +55,13 @@ describe('useServerPagination', () => {
|
|
|
45
55
|
expect(result.current.data).toEqual(getIntArray(0, 17));
|
|
46
56
|
});
|
|
47
57
|
|
|
48
|
-
it('should
|
|
58
|
+
it('should fetch 2 pages if pageSize < number of rows <= 2 * pageSize', async () => {
|
|
49
59
|
const pageSize = 20;
|
|
50
60
|
const expectedRowCount = 40;
|
|
51
61
|
const { result } = renderHook(() =>
|
|
52
|
-
|
|
53
|
-
getTestData(url, expectedRowCount).then((data) => ({ data }) as any),
|
|
54
|
-
),
|
|
62
|
+
useOpenmrsPagination('http://localhost/2', pageSize, {
|
|
63
|
+
fetcher: (url) => getTestData(url, expectedRowCount).then((data) => ({ data }) as any),
|
|
64
|
+
}),
|
|
55
65
|
);
|
|
56
66
|
|
|
57
67
|
await waitFor(() => expect(result.current.isLoading).toBeFalsy());
|
|
@@ -81,14 +91,14 @@ describe('useServerPagination', () => {
|
|
|
81
91
|
expect(result.current.data).toEqual(getIntArray(0, 20));
|
|
82
92
|
});
|
|
83
93
|
|
|
84
|
-
it('should
|
|
94
|
+
it('should fetch n pages for n >> 1', async () => {
|
|
85
95
|
const pageSize = 20;
|
|
86
96
|
const expectedRowCount = 1337;
|
|
87
97
|
const expectedTotalPages = Math.ceil(expectedRowCount / pageSize);
|
|
88
98
|
const { result } = renderHook(() =>
|
|
89
|
-
|
|
90
|
-
getTestData(url, expectedRowCount).then((data) => ({ data }) as any),
|
|
91
|
-
),
|
|
99
|
+
useOpenmrsPagination('http://localhost/3', pageSize, {
|
|
100
|
+
fetcher: (url) => getTestData(url, expectedRowCount).then((data) => ({ data }) as any),
|
|
101
|
+
}),
|
|
92
102
|
);
|
|
93
103
|
|
|
94
104
|
await waitFor(() => expect(result.current.isLoading).toBeFalsy());
|