mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
0139b91b5d
* chore: Replace lodash with es-toolkit Migrate all direct lodash imports to es-toolkit/compat for a smaller, faster, lodash-compatible utility library. Transitive lodash usage from other packages remains unchanged. * fix: Restore isPlainObject semantics in CanCan policy The lodash migration aliased `isObject` to `lodash/isPlainObject` and the codemod incorrectly mapped the local name to es-toolkit's `isObject`, which also returns true for arrays and functions. This caused condition objects in policy definitions to be skipped, breaking authorization checks across the codebase. * fix: Restore unicode-aware length counting in validators es-toolkit/compat's size() returns string.length, while lodash's _.size() counts unicode code points. Switch to [...value].length to preserve the previous behavior so multi-byte characters like emoji count as one.
107 lines
2.6 KiB
TypeScript
107 lines
2.6 KiB
TypeScript
import { uniqBy } from "es-toolkit/compat";
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import type { PaginationParams } from "~/types";
|
|
import useRequest from "./useRequest";
|
|
|
|
type RequestResponse<T> = {
|
|
/** The return value of the paginated request function. */
|
|
data: T[] | undefined;
|
|
/** The request error, if any. */
|
|
error: unknown;
|
|
/** Whether the request is currently in progress. */
|
|
loading: boolean;
|
|
/** Function to trigger next page request. */
|
|
next: () => void;
|
|
/** Page number */
|
|
page: number;
|
|
/** Offset */
|
|
offset: number;
|
|
/** Marks the end of pagination */
|
|
end: boolean;
|
|
};
|
|
|
|
const INITIAL_OFFSET = 0;
|
|
const DEFAULT_LIMIT = 10;
|
|
|
|
/**
|
|
* A hook to make paginated API request and track its state within a component.
|
|
*
|
|
* @param requestFn The function to call to make the request, it should return a promise.
|
|
* @param params Pagination params(limit, offset etc) to be passed to requestFn.
|
|
* @returns
|
|
*/
|
|
export default function usePaginatedRequest<T = unknown>(
|
|
requestFn: (params?: PaginationParams) => Promise<T[]>,
|
|
params: PaginationParams = {}
|
|
): RequestResponse<T> {
|
|
const [data, setData] = useState<T[]>();
|
|
const [offset, setOffset] = useState(INITIAL_OFFSET);
|
|
const [page, setPage] = useState(0);
|
|
const [end, setEnd] = useState(false);
|
|
const displayLimit = params.limit || DEFAULT_LIMIT;
|
|
const fetchLimit = displayLimit + 1;
|
|
const [paginatedReq, setPaginatedReq] = useState(
|
|
() => () =>
|
|
requestFn({
|
|
...params,
|
|
offset: 0,
|
|
limit: fetchLimit,
|
|
})
|
|
);
|
|
|
|
const {
|
|
data: response,
|
|
error,
|
|
loading,
|
|
request,
|
|
} = useRequest<T[]>(paginatedReq);
|
|
|
|
useEffect(() => {
|
|
void request();
|
|
}, [request]);
|
|
|
|
useEffect(() => {
|
|
if (response && !loading) {
|
|
setData((prev) =>
|
|
uniqBy((prev ?? []).concat(response.slice(0, displayLimit)), "id")
|
|
);
|
|
setPage((prev) => prev + 1);
|
|
setEnd(response.length <= displayLimit);
|
|
}
|
|
}, [response, displayLimit, loading]);
|
|
|
|
useEffect(() => {
|
|
if (offset) {
|
|
setPaginatedReq(
|
|
() => () =>
|
|
requestFn({
|
|
...params,
|
|
offset,
|
|
limit: fetchLimit,
|
|
})
|
|
);
|
|
}
|
|
}, [offset, fetchLimit, requestFn]);
|
|
|
|
const next = useCallback(() => {
|
|
setOffset((prev) => prev + displayLimit);
|
|
}, [displayLimit]);
|
|
|
|
useEffect(() => {
|
|
setEnd(false);
|
|
setData(undefined);
|
|
setPage(0);
|
|
setOffset(0);
|
|
setPaginatedReq(
|
|
() => () =>
|
|
requestFn({
|
|
...params,
|
|
offset: 0,
|
|
limit: fetchLimit,
|
|
})
|
|
);
|
|
}, [requestFn]);
|
|
|
|
return { data, next, loading, error, page, offset, end };
|
|
}
|