Compare commits

...

2 Commits

Author SHA1 Message Date
Jiang Bohan
febe267206 merge: resolve conflicts with monorepo restructuring on main
Update import paths from @/ and @core/ to @multica/core and @multica/ui
for board-view.tsx and list-view.tsx. Keep pagination changes (useLoadMoreDoneIssues
and InfiniteScrollSentinel) intact.
2026-04-09 14:17:31 +08:00
Jiang Bohan
57fdbdda72 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.
2026-04-09 14:09:06 +08:00
3 changed files with 42 additions and 29 deletions

View File

@@ -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 "@multica/core/types";
import { Button } from "@multica/ui/components/ui/button";
import { useLoadMoreDoneIssues } from "@multica/core/issues/mutations";
@@ -32,30 +32,7 @@ import { sortIssues } from "../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);

View File

@@ -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>
);
}

View File

@@ -6,6 +6,7 @@ import { Accordion } from "@base-ui/react/accordion";
import { Tooltip, TooltipTrigger, TooltipContent } from "@multica/ui/components/ui/tooltip";
import { Button } from "@multica/ui/components/ui/button";
import type { Issue, IssueStatus } from "@multica/core/types";
import { useLoadMoreDoneIssues } from "@multica/core/issues/mutations";
import { STATUS_CONFIG } from "@multica/core/issues/config";
import { useModalStore } from "@multica/core/modals";
import { useViewStore } from "@multica/core/issues/stores/view-store-context";
@@ -13,6 +14,7 @@ import { useIssueSelectionStore } from "@multica/core/issues/stores/selection-st
import { sortIssues } from "../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