fix(board): show total count in Done column and infinite scroll (#498)

* fix(board): show total count in Done column header and auto-load on scroll

- Column header now shows server-side doneTotal instead of loaded count
- Replace "Load more" button with IntersectionObserver sentinel for
  infinite scroll in the Done column

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(board): move sentinel below imports and stabilize observer

- Move InfiniteScrollSentinel after all import statements
- Use callback ref to avoid recreating IntersectionObserver on every render

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
LinYushen 2026-04-08 16:15:15 +08:00 committed by GitHub
parent 4bdb86057e
commit 76354cd968
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 31 additions and 14 deletions

View file

@ -67,7 +67,7 @@ export function useLoadMoreDoneIssues() {
}
}, [qc, wsId, doneLoaded, hasMore, isLoading]);
return { loadMore, hasMore, isLoading };
return { loadMore, hasMore, isLoading, doneTotal };
}
// ---------------------------------------------------------------------------

View file

@ -23,11 +23,13 @@ export function BoardColumn({
status,
issueIds,
issueMap,
totalCount,
footer,
}: {
status: IssueStatus;
issueIds: string[];
issueMap: Map<string, Issue>;
totalCount?: number;
footer?: ReactNode;
}) {
const cfg = STATUS_CONFIG[status];
@ -54,7 +56,7 @@ export function BoardColumn({
{cfg.label}
</span>
<span className="text-xs text-muted-foreground">
{issueIds.length}
{totalCount ?? issueIds.length}
</span>
</div>

View file

@ -33,6 +33,30 @@ 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>
);
}
const COLUMN_IDS = new Set<string>(ALL_STATUSES);
const kanbanCollision: CollisionDetection = (args) => {
@ -111,7 +135,7 @@ export function BoardView({
}) {
const sortBy = useViewStore((s) => s.sortBy);
const sortDirection = useViewStore((s) => s.sortDirection);
const { loadMore, hasMore, isLoading: loadingMore } = useLoadMoreDoneIssues();
const { loadMore, hasMore, isLoading: loadingMore, doneTotal } = useLoadMoreDoneIssues();
// --- Drag state ---
const [activeIssue, setActiveIssue] = useState<Issue | null>(null);
@ -274,19 +298,10 @@ export function BoardView({
status={status}
issueIds={columns[status] ?? []}
issueMap={issueMapRef.current}
totalCount={status === "done" ? doneTotal : undefined}
footer={
status === "done" && hasMore ? (
<button
type="button"
className="mt-1 flex w-full items-center justify-center gap-1.5 rounded-md py-2 text-xs text-muted-foreground hover:bg-accent/60 transition-colors disabled:opacity-50"
onClick={loadMore}
disabled={loadingMore}
>
{loadingMore ? (
<Loader2 className="size-3 animate-spin" />
) : null}
{loadingMore ? "Loading..." : "Load more"}
</button>
<InfiniteScrollSentinel onVisible={loadMore} loading={loadingMore} />
) : undefined
}
/>