diff --git a/app/components/Sidebar/components/Starred.tsx b/app/components/Sidebar/components/Starred.tsx index f4c230fea..6ae3870df 100644 --- a/app/components/Sidebar/components/Starred.tsx +++ b/app/components/Sidebar/components/Starred.tsx @@ -3,9 +3,11 @@ import { observer } from "mobx-react"; import * as React from "react"; import { useDrop } from "react-dnd"; import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; import Star from "~/models/Star"; import DelayedMount from "~/components/DelayedMount"; import Flex from "~/components/Flex"; +import usePaginatedRequest from "~/hooks/usePaginatedRequest"; import useStores from "~/hooks/useStores"; import DropCursor from "./DropCursor"; import Header from "./Header"; @@ -18,36 +20,16 @@ import StarredLink from "./StarredLink"; const STARRED_PAGINATION_LIMIT = 10; function Starred() { - const [fetchError, setFetchError] = React.useState(); - const [displayedStarsCount, setDisplayedStarsCount] = React.useState( - STARRED_PAGINATION_LIMIT - ); const { stars } = useStores(); const { t } = useTranslation(); - const fetchResults = React.useCallback( - async (offset = 0) => { - try { - await stars.fetchPage({ - limit: STARRED_PAGINATION_LIMIT + 1, - offset, - }); - } catch (error) { - setFetchError(error); - } - }, - [stars] + const { loading, next, end, error, page } = usePaginatedRequest( + stars.fetchPage, + { + limit: STARRED_PAGINATION_LIMIT, + } ); - React.useEffect(() => { - void fetchResults(); - }, []); - - const handleShowMore = async () => { - await fetchResults(displayedStarsCount); - setDisplayedStarsCount((prev) => prev + STARRED_PAGINATION_LIMIT); - }; - // Drop to reorder document const [{ isOverReorder, isDraggingAnyStar }, dropToReorder] = useDrop({ accept: "star", @@ -62,6 +44,10 @@ function Starred() { }), }); + if (error) { + toast.error(t("Could not load starred documents")); + } + if (!stars.orderedData.length) { return null; } @@ -78,18 +64,20 @@ function Starred() { position="top" /> )} - {stars.orderedData.slice(0, displayedStarsCount).map((star) => ( - - ))} - {stars.orderedData.length > displayedStarsCount && ( + {stars.orderedData + .slice(0, page * STARRED_PAGINATION_LIMIT) + .map((star) => ( + + ))} + {!end && ( )} - {(stars.isFetching || fetchError) && !stars.orderedData.length && ( + {loading && ( diff --git a/app/hooks/usePaginatedRequest.ts b/app/hooks/usePaginatedRequest.ts new file mode 100644 index 000000000..c2e930d98 --- /dev/null +++ b/app/hooks/usePaginatedRequest.ts @@ -0,0 +1,91 @@ +import uniqBy from "lodash/uniqBy"; +import * as React from "react"; +import { PaginationParams } from "~/types"; +import useRequest from "./useRequest"; + +type RequestResponse = { + /** 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; + /** 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( + requestFn: (params?: PaginationParams | undefined) => Promise, + params: PaginationParams +): RequestResponse { + const [data, setData] = React.useState(); + const [offset, setOffset] = React.useState(INITIAL_OFFSET); + const [page, setPage] = React.useState(0); + const [end, setEnd] = React.useState(false); + const displayLimit = params.limit || DEFAULT_LIMIT; + const fetchLimit = displayLimit + 1; + const [paginatedReq, setPaginatedReq] = React.useState( + () => () => + requestFn({ + ...params, + offset: 0, + limit: fetchLimit, + }) + ); + + const { + data: response, + error, + loading, + request, + } = useRequest(paginatedReq); + + React.useEffect(() => { + void request(); + }, [request]); + + React.useEffect(() => { + if (response && !loading) { + setData((prev) => + uniqBy((prev ?? []).concat(response.slice(0, displayLimit)), "id") + ); + setPage((prev) => prev + 1); + if (response.length <= displayLimit) { + setEnd(true); + } + } + }, [response, displayLimit, loading]); + + React.useEffect(() => { + if (offset) { + setPaginatedReq( + () => () => + requestFn({ + ...params, + offset, + limit: fetchLimit, + }) + ); + } + }, [offset, fetchLimit, requestFn]); + + const next = React.useCallback(() => { + setOffset((prev) => prev + displayLimit); + }, [displayLimit]); + + return { data, next, loading, error, page, end }; +} diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index a95707afb..d334daa9b 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -243,6 +243,7 @@ "Empty": "Empty", "Go back": "Go back", "Go forward": "Go forward", + "Could not load starred documents": "Could not load starred documents", "Starred": "Starred", "Show more": "Show more", "Up to date": "Up to date",