From 57fdbdda724c4681eebc3010b5937bb1f6ed2685 Mon Sep 17 00:00:00 2001 From: Jiang Bohan Date: Thu, 9 Apr 2026 14:09:06 +0800 Subject: [PATCH] fix(issues): add done issue pagination to list view List view only showed the first 50 done issues without a total count or load-more mechanism. Reuse the existing useLoadMoreDoneIssues hook and extract InfiniteScrollSentinel into a shared component so both board and list views paginate identically. --- .../features/issues/components/board-view.tsx | 27 ++---------------- .../components/infinite-scroll-sentinel.tsx | 28 +++++++++++++++++++ .../features/issues/components/list-view.tsx | 16 ++++++++--- 3 files changed, 42 insertions(+), 29 deletions(-) create mode 100644 apps/web/features/issues/components/infinite-scroll-sentinel.tsx diff --git a/apps/web/features/issues/components/board-view.tsx b/apps/web/features/issues/components/board-view.tsx index a33119a4d..ad74088ab 100644 --- a/apps/web/features/issues/components/board-view.tsx +++ b/apps/web/features/issues/components/board-view.tsx @@ -15,7 +15,7 @@ import { type DragOverEvent, } from "@dnd-kit/core"; import { arrayMove } from "@dnd-kit/sortable"; -import { Eye, Loader2, MoreHorizontal } from "lucide-react"; +import { Eye, MoreHorizontal } from "lucide-react"; import type { Issue, IssueStatus } from "@/shared/types"; import { Button } from "@/components/ui/button"; import { useLoadMoreDoneIssues } from "@core/issues/mutations"; @@ -32,30 +32,7 @@ import { sortIssues } from "@/features/issues/utils/sort"; import { StatusIcon } from "./status-icon"; import { BoardColumn } from "./board-column"; import { BoardCardContent } from "./board-card"; - -/** Sentinel that triggers `onVisible` when scrolled into view. */ -function InfiniteScrollSentinel({ onVisible, loading }: { onVisible: () => void; loading: boolean }) { - const sentinelRef = useRef(null); - const onVisibleRef = useRef(onVisible); - onVisibleRef.current = onVisible; - - useEffect(() => { - const node = sentinelRef.current; - if (!node) return; - const observer = new IntersectionObserver( - ([entry]) => { if (entry?.isIntersecting) onVisibleRef.current(); }, - { rootMargin: "100px" }, - ); - observer.observe(node); - return () => observer.disconnect(); - }, []); - - return ( -
- {loading && } -
- ); -} +import { InfiniteScrollSentinel } from "./infinite-scroll-sentinel"; const COLUMN_IDS = new Set(ALL_STATUSES); diff --git a/apps/web/features/issues/components/infinite-scroll-sentinel.tsx b/apps/web/features/issues/components/infinite-scroll-sentinel.tsx new file mode 100644 index 000000000..c0b77059b --- /dev/null +++ b/apps/web/features/issues/components/infinite-scroll-sentinel.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { Loader2 } from "lucide-react"; + +/** Sentinel that triggers `onVisible` when scrolled into view. */ +export function InfiniteScrollSentinel({ onVisible, loading }: { onVisible: () => void; loading: boolean }) { + const sentinelRef = useRef(null); + const onVisibleRef = useRef(onVisible); + onVisibleRef.current = onVisible; + + useEffect(() => { + const node = sentinelRef.current; + if (!node) return; + const observer = new IntersectionObserver( + ([entry]) => { if (entry?.isIntersecting) onVisibleRef.current(); }, + { rootMargin: "100px" }, + ); + observer.observe(node); + return () => observer.disconnect(); + }, []); + + return ( +
+ {loading && } +
+ ); +} diff --git a/apps/web/features/issues/components/list-view.tsx b/apps/web/features/issues/components/list-view.tsx index bc0ef4368..f4d563588 100644 --- a/apps/web/features/issues/components/list-view.tsx +++ b/apps/web/features/issues/components/list-view.tsx @@ -6,6 +6,7 @@ import { Accordion } from "@base-ui/react/accordion"; import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"; import { Button } from "@/components/ui/button"; import type { Issue, IssueStatus } from "@/shared/types"; +import { useLoadMoreDoneIssues } from "@core/issues/mutations"; import { STATUS_CONFIG } from "@/features/issues/config"; import { useModalStore } from "@/features/modals"; import { useViewStore } from "@/features/issues/stores/view-store-context"; @@ -13,6 +14,7 @@ import { useIssueSelectionStore } from "@/features/issues/stores/selection-store import { sortIssues } from "@/features/issues/utils/sort"; import { StatusIcon } from "./status-icon"; import { ListRow } from "./list-row"; +import { InfiniteScrollSentinel } from "./infinite-scroll-sentinel"; export function ListView({ issues, @@ -32,6 +34,7 @@ export function ListView({ const selectedIds = useIssueSelectionStore((s) => s.selectedIds); const select = useIssueSelectionStore((s) => s.select); const deselect = useIssueSelectionStore((s) => s.deselect); + const { loadMore, hasMore, isLoading: loadingMore, doneTotal } = useLoadMoreDoneIssues(); const issuesByStatus = useMemo(() => { const map = new Map(); @@ -101,7 +104,7 @@ export function ListView({ {cfg.label} - {statusIssues.length} + {status === "done" ? doneTotal : statusIssues.length}
@@ -128,9 +131,14 @@ export function ListView({ {statusIssues.length > 0 ? ( - statusIssues.map((issue) => ( - - )) + <> + {statusIssues.map((issue) => ( + + ))} + {status === "done" && hasMore && ( + + )} + ) : (

No issues