From 28cd2efad0aea2ecdefbceb383c1738efb906ca0 Mon Sep 17 00:00:00 2001 From: VipinDevelops Date: Mon, 29 Dec 2025 17:06:08 +0530 Subject: [PATCH] chore: pagination components --- .../web/core/components/pages/list/loader.tsx | 15 +++++ apps/web/core/components/pages/list/root.tsx | 59 +++++++++++++++++-- .../pages/pages-list-main-content.tsx | 5 ++ .../core/components/pages/pages-list-view.tsx | 17 ++++-- 4 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 apps/web/core/components/pages/list/loader.tsx diff --git a/apps/web/core/components/pages/list/loader.tsx b/apps/web/core/components/pages/list/loader.tsx new file mode 100644 index 0000000000..6c504d9b3e --- /dev/null +++ b/apps/web/core/components/pages/list/loader.tsx @@ -0,0 +1,15 @@ +import { Loader } from "@plane/ui"; + +export function PageListLoader() { + return ( + + +
+ + + + +
+
+ ); +} diff --git a/apps/web/core/components/pages/list/root.tsx b/apps/web/core/components/pages/list/root.tsx index 0d477745fe..175d4b3412 100644 --- a/apps/web/core/components/pages/list/root.tsx +++ b/apps/web/core/components/pages/list/root.tsx @@ -1,13 +1,19 @@ +import { useCallback, useRef, useState } from "react"; import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; // types import type { TPageNavigationTabs } from "@plane/types"; +import { Loader } from "@plane/ui"; // components import { ListLayout } from "@/components/core/list"; +// hooks +import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; // plane web hooks import type { EPageStoreType } from "@/plane-web/hooks/store"; import { usePageStore } from "@/plane-web/hooks/store"; // local imports import { PageListBlock } from "./block"; +import { PageListLoader } from "./loader"; type TPagesListRoot = { pageType: TPageNavigationTabs; @@ -16,17 +22,58 @@ type TPagesListRoot = { export const PagesListRoot = observer(function PagesListRoot(props: TPagesListRoot) { const { pageType, storeType } = props; + // params + const { workspaceSlug, projectId } = useParams(); // store hooks - const { getCurrentProjectFilteredPageIdsByTab } = usePageStore(storeType); + const { getCurrentProjectFilteredPageIdsByTab, fetchPagesList, getPaginationInfo, getPaginationLoader } = + usePageStore(storeType); + + // pagination hooks + const paginationInfo = getPaginationInfo(pageType); + const paginationLoader = getPaginationLoader(pageType); + const hasNextPage = paginationInfo.hasNextPage; + const isFetchingNextPage = paginationLoader === "pagination"; + + // state for intersection observer element + const [intersectionElement, setIntersectionElement] = useState(null); + // ref for container - we'll wrap ListLayout + const containerRef = useRef(null); + // derived values const filteredPageIds = getCurrentProjectFilteredPageIdsByTab(pageType); + // Function to fetch next page + const fetchNextPage = useCallback(() => { + if (!workspaceSlug || !projectId || !hasNextPage || isFetchingNextPage) { + return; + } + // Use fetchPagesList with cursor from pagination info + void fetchPagesList( + workspaceSlug.toString(), + projectId.toString(), + pageType, + paginationInfo.nextCursor ?? undefined + ); + }, [workspaceSlug, projectId, hasNextPage, isFetchingNextPage, fetchPagesList, pageType, paginationInfo.nextCursor]); + + // Set up intersection observer to trigger loading more pages + useIntersectionObserver( + containerRef, + isFetchingNextPage ? null : intersectionElement, + fetchNextPage, + `100% 0% 100% 0%` + ); + if (!filteredPageIds) return <>; + return ( - - {filteredPageIds.map((pageId) => ( - - ))} - +
+ + {filteredPageIds.map((pageId) => ( + + ))} + {hasNextPage &&
{isFetchingNextPage && }
} +
+
); }); diff --git a/apps/web/core/components/pages/pages-list-main-content.tsx b/apps/web/core/components/pages/pages-list-main-content.tsx index 58e239140b..8396e53c78 100644 --- a/apps/web/core/components/pages/pages-list-main-content.tsx +++ b/apps/web/core/components/pages/pages-list-main-content.tsx @@ -156,6 +156,11 @@ export const PagesListMainContent = observer(function PagesListMainContent(props /> ); } + + if (loader === "mutation-loader" && (!filteredPageIds || filteredPageIds.length === 0)) { + return ; + } + // if no pages match the filter criteria if (filteredPageIds?.length === 0) return ( diff --git a/apps/web/core/components/pages/pages-list-view.tsx b/apps/web/core/components/pages/pages-list-view.tsx index 15f114f46e..86e3e25dda 100644 --- a/apps/web/core/components/pages/pages-list-view.tsx +++ b/apps/web/core/components/pages/pages-list-view.tsx @@ -19,11 +19,20 @@ type TPageView = { export const PagesListView = observer(function PagesListView(props: TPageView) { const { children, pageType, projectId, storeType, workspaceSlug } = props; // store hooks - const { isAnyPageAvailable, fetchPagesList } = usePageStore(storeType); - // fetching pages list + const { isAnyPageAvailable, fetchPagesList, filters } = usePageStore(storeType); + + // fetching pages list - include filters and sorting in SWR key to refetch when they change useSWR( - workspaceSlug && projectId && pageType ? `PROJECT_PAGES_${projectId}` : null, - workspaceSlug && projectId && pageType ? () => fetchPagesList(workspaceSlug, projectId, pageType) : null + workspaceSlug && projectId && pageType + ? `PROJECT_PAGES_${projectId}_${pageType}_${filters.searchQuery || ""}_${filters.sortKey || ""}_${ + filters.sortBy || "" + }_${JSON.stringify(filters.filters || {})}` + : null, + workspaceSlug && projectId && pageType ? () => fetchPagesList(workspaceSlug, projectId, pageType) : null, + { + revalidateOnFocus: true, + revalidateIfStale: true, + } ); // pages loader