mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 13:29:44 +02:00
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.
This commit is contained in:
@@ -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<HTMLDivElement>(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 (
|
||||
<div ref={sentinelRef} className="flex items-center justify-center py-2">
|
||||
{loading && <Loader2 className="size-3 animate-spin text-muted-foreground" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { InfiniteScrollSentinel } from "./infinite-scroll-sentinel";
|
||||
|
||||
const COLUMN_IDS = new Set<string>(ALL_STATUSES);
|
||||
|
||||
|
||||
@@ -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<HTMLDivElement>(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 (
|
||||
<div ref={sentinelRef} className="flex items-center justify-center py-2">
|
||||
{loading && <Loader2 className="size-3 animate-spin text-muted-foreground" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<IssueStatus, Issue[]>();
|
||||
@@ -101,7 +104,7 @@ export function ListView({
|
||||
{cfg.label}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{statusIssues.length}
|
||||
{status === "done" ? doneTotal : statusIssues.length}
|
||||
</span>
|
||||
</Accordion.Trigger>
|
||||
<div className="pr-2">
|
||||
@@ -128,9 +131,14 @@ export function ListView({
|
||||
</Accordion.Header>
|
||||
<Accordion.Panel className="pt-1">
|
||||
{statusIssues.length > 0 ? (
|
||||
statusIssues.map((issue) => (
|
||||
<ListRow key={issue.id} issue={issue} />
|
||||
))
|
||||
<>
|
||||
{statusIssues.map((issue) => (
|
||||
<ListRow key={issue.id} issue={issue} />
|
||||
))}
|
||||
{status === "done" && hasMore && (
|
||||
<InfiniteScrollSentinel onVisible={loadMore} loading={loadingMore} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p className="py-6 text-center text-xs text-muted-foreground">
|
||||
No issues
|
||||
|
||||
Reference in New Issue
Block a user