From 5a44a9e495258f9384a404d37b80446512fe3dec Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Wed, 6 May 2026 21:29:14 +0200 Subject: [PATCH] fix(web): advance dashboard pagination on first Next click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driving loadMore() from the auto-load effect in page.tsx left a one-render gap where loadingMore was still false, so the snap-back effect in useDashboardFilters fired first and reverted currentPage from 2 back to totalPages=1 before the fetch even started. The fetch then ran (the auto-load effect saw the captured currentPage=2 from that render) and the list grew to totalPages=2, but currentPage had already been snapped to 1 — giving the visible "Page 1 of 2+" state and forcing the user to click Next a second time to actually see the new batch. Now handleNextPage calls loadMore() in the same click handler as setCurrentPage(prev+1), so React 18's automatic batching commits both state changes in the same render. Snap-back's `if (loadingMore) return` guard then correctly skips, currentPage stays at the overshoot, and once the fetch resolves totalPages catches up and the new batch renders. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../dashboard/hooks/useDashboardFilters.ts | 30 ++++++++++++------- .../app/dashboard/page.tsx | 1 + 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/Web/autopilot-monitor-web/app/dashboard/hooks/useDashboardFilters.ts b/src/Web/autopilot-monitor-web/app/dashboard/hooks/useDashboardFilters.ts index 4dbb2de2..25a51aa6 100644 --- a/src/Web/autopilot-monitor-web/app/dashboard/hooks/useDashboardFilters.ts +++ b/src/Web/autopilot-monitor-web/app/dashboard/hooks/useDashboardFilters.ts @@ -15,6 +15,7 @@ interface UseDashboardFiltersParams { tenantIdFilter: string; hasMore: boolean; loadingMore: boolean; + loadMore: () => void; } interface DashboardStats { @@ -61,6 +62,7 @@ export function useDashboardFilters({ tenantIdFilter, hasMore, loadingMore, + loadMore, }: UseDashboardFiltersParams): UseDashboardFiltersReturn { const [searchQuery, setSearchQuery] = useState(""); const [statusFilter, setStatusFilter] = useState(null); @@ -217,20 +219,28 @@ export function useDashboardFilters({ }, [sortedSessions, currentPage, sessionsPerPage]); const handlePreviousPage = () => setCurrentPage((prev) => Math.max(1, prev - 1)); - // Allow advancing past totalPages when the server has more pages; the auto-load - // effect in page.tsx will fetch the next batch and totalPages catches up. Without - // this overshoot the user could never trigger a load while staying inside a single - // Prev/Next control — they would have to click a separate "Load more" button. - const handleNextPage = () => setCurrentPage((prev) => { - if (prev < totalPages) return prev + 1; - if (hasMore && !loadingMore) return prev + 1; - return prev; - }); + // When advancing past locally-loaded pages, fire loadMore() in the SAME event + // handler so React batches setLoadingMore(true) with setCurrentPage(prev+1). + // If we relied on a follow-up effect to trigger the fetch, the snap-back effect + // below would race ahead (loadingMore still false at that point) and reset + // currentPage to totalPages — leaving the user on the previous page even though + // a load was in flight, forcing a second click to actually advance. + const handleNextPage = () => { + if (currentPage < totalPages) { + setCurrentPage((prev) => prev + 1); + return; + } + if (hasMore && !loadingMore) { + loadMore(); + setCurrentPage((prev) => prev + 1); + } + }; // Snap currentPage back to the last valid page once a load finishes that did NOT // grow totalPages (e.g. an active filter narrowed the new batch to zero matches). // Without this the user would be stuck on an empty paginated slice with no way - // to recover except clicking Prev. + // to recover except clicking Prev. Gated on !loadingMore so it never fires + // mid-fetch and reverts a legitimate overshoot. useEffect(() => { if (loadingMore) return; if (totalPages > 0 && currentPage > totalPages) setCurrentPage(totalPages); diff --git a/src/Web/autopilot-monitor-web/app/dashboard/page.tsx b/src/Web/autopilot-monitor-web/app/dashboard/page.tsx index 5c3c04c7..450ab56d 100644 --- a/src/Web/autopilot-monitor-web/app/dashboard/page.tsx +++ b/src/Web/autopilot-monitor-web/app/dashboard/page.tsx @@ -68,6 +68,7 @@ export default function Home() { tenantIdFilter, hasMore, loadingMore, + loadMore, }); // Redirect regular users (non-admin, non-operator) to progress portal – they must never see the session list