@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/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
- useServerPagination as realUseServerPagination,
10
- useServerInfinite as realUseServerInfinite,
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 useServerPagination = jest.fn(realUseServerPagination);
75
-
76
- export const useServerInfinite = jest.fn(realUseServerInfinite);
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.2249",
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.2249",
65
- "@openmrs/esm-config": "5.8.1-pre.2249",
66
- "@openmrs/esm-context": "5.8.1-pre.2249",
67
- "@openmrs/esm-error-handling": "5.8.1-pre.2249",
68
- "@openmrs/esm-extensions": "5.8.1-pre.2249",
69
- "@openmrs/esm-feature-flags": "5.8.1-pre.2249",
70
- "@openmrs/esm-globals": "5.8.1-pre.2249",
71
- "@openmrs/esm-navigation": "5.8.1-pre.2249",
72
- "@openmrs/esm-utils": "5.8.1-pre.2249",
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 './useServerPagination';
39
- export * from './useServerInfinite';
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 './useServerPagination';
35
- export * from './useServerInfinite';
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 { useServerInfinite } from './useServerInfinite';
4
- import { getIntArray, getTestData } from './useServerPagination.test';
3
+ import { useOpenmrsInfinite } from './useOpenmrsInfinite';
4
+ import { getIntArray, getTestData } from './useOpenmrsPagination.test';
5
5
 
6
- describe('useServerInfinite', () => {
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
- useServerInfinite(`/1?limit=${pageSize}`, (url) =>
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
- useServerInfinite(`/2?limit=${pageSize}`, (url) =>
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
- useServerInfinite(`/3?limit=${pageSize}`, (url) =>
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 { useServerPagination, type OpenMRSPaginatedResponse } from './useServerPagination';
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('useServerPagination', () => {
28
+ describe('useOpenmrsPagination', () => {
29
29
  afterEach(cleanup);
30
30
 
31
- it('should render all rows on 1 page if number of rows < pageSize', async () => {
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
- useServerPagination('/1', pageSize, (url) =>
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 render 2 pages if pageSize < number of rows <= 2 * pageSize', async () => {
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
- useServerPagination('/2', pageSize, (url) =>
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 render n pages for n >> 1', async () => {
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
- useServerPagination('/3', pageSize, (url) =>
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());