From e378b323b92b50a4b367014284741d6a803d8e31 Mon Sep 17 00:00:00 2001 From: "ahmed.mujtaba" Date: Mon, 7 Sep 2026 16:47:44 +0500 Subject: [PATCH] sewt all limit --- frontend/inbox-loading.test.mjs | 12 +++++++++ frontend/src/screens/Inbox.jsx | 47 +++++++++++++++++---------------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/frontend/inbox-loading.test.mjs b/frontend/inbox-loading.test.mjs index e68b450..9a13f62 100644 --- a/frontend/inbox-loading.test.mjs +++ b/frontend/inbox-loading.test.mjs @@ -215,6 +215,18 @@ try { hasSkeleton(view.html()) && rowCount(view.html()) === 0, `skeleton=${hasSkeleton(view.html())} rows=${rowCount(view.html())}`, ) + const allFormFetch = requestsMatching('/sheet/form-data/fetch') + const allEmailFetch = requestsMatching('/inbox/all-applications').filter((u) => !u.includes('/count')) + check( + 'All channel sends the UI page size, not the whole form_data table', + allFormFetch.some((u) => u.includes('limit=10')), + allFormFetch.slice(-1)[0] || 'no form-data fetch on All', + ) + check( + 'All channel emails send the same page size', + allEmailFetch.some((u) => u.includes('top=10')), + allEmailFetch.slice(-1)[0] || 'no email fetch on All', + ) // ---- frame 2: email home, sheet still travelling ------------------------ // THE REGRESSION: this frame used to render six placeholders on top of two diff --git a/frontend/src/screens/Inbox.jsx b/frontend/src/screens/Inbox.jsx index 7074497..c2c3d2d 100644 --- a/frontend/src/screens/Inbox.jsx +++ b/frontend/src/screens/Inbox.jsx @@ -49,10 +49,10 @@ const PAGE_SIZE_MAX = 500 * This list is a local table, not a live feed. Applications only appear when * the Sync worker writes them, and that path already invalidates * qk.mailbox.all() the moment a run completes — so refetching on every visit - * bought nothing and cost a full-width skeleton each time. The All channel - * makes that worse: it fetches BOTH sources unpaged, so the "loading" state a - * recruiter saw on re-entry was thousands of rows being re-downloaded to - * render the same ten. + * bought nothing and cost a full-width skeleton each time. + * + * List fetches always send the UI page size (10 / 50 / 100), including All — + * omitting limit used to dump the whole form_data table into the browser. * * staleTime therefore covers a normal working stretch, and keepPreviousData * means a tab switch, a page turn or a keystroke re-renders the rows already @@ -1010,22 +1010,22 @@ export default function Inbox() { }, [deepOpen, deepKind, setSearchParams]) const isForms = channel === 'forms' - // Combined channel: both sources fetched UNPAGED (each endpoint reads a - // missing top/limit as no LIMIT), merged by date, and paged client-side — - // per-source skip/top cannot compose into a correct global page. + // Combined channel: each source is fetched with the UI page size, then + // merged newest-first. Per-source pages are not a perfect global timeline, + // but omitting limit dumped the whole form_data table on open. const isAllChannel = channel === 'all' const channelTabs = isForms || isAllChannel ? FORM_TABS : TABS + const pageLimit = pageSize === 'all' ? undefined : pageSize const tabFilter = TAB_FILTERS[tab] ?? {} const formTabFilter = FORM_TAB_FILTERS[tab] ?? {} const listParams = useMemo(() => ({ ...tabFilter, - // 'all' page size and the All channel drop the param entirely — the - // endpoint reads a missing top as unpaged. - top: pageSize === 'all' || isAllChannel ? undefined : pageSize, - skip: isAllChannel ? 0 : skip, + // Show-all omits top (no LIMIT). Otherwise send the pager size — 10, 50, 100. + top: pageLimit, + skip: pageLimit == null ? 0 : skip, ...(search ? { search } : {}), - }), [tabFilter, skip, pageSize, search, isAllChannel]) + }), [tabFilter, skip, pageLimit, search]) /** * Sheet Forms only. On the All channel these rows are merged with email ones, @@ -1042,12 +1042,12 @@ export default function Inbox() { const formParams = useMemo(() => ({ // All channel spans every sheet tab, not just the selected one. sheet: isAllChannel ? undefined : (formSheet || undefined), - offset: isAllChannel ? 0 : skip, - limit: pageSize === 'all' || isAllChannel ? undefined : pageSize, + offset: pageLimit == null ? 0 : skip, + limit: pageLimit, ...formTabFilter, ...(search ? { search } : {}), ...linkFilters, - }), [formSheet, skip, pageSize, search, formTabFilter, isAllChannel, linkFilters]) + }), [formSheet, skip, pageLimit, search, formTabFilter, isAllChannel, linkFilters]) const applicationsQuery = useQuery({ queryKey: qk.mailbox.applications(listParams), @@ -1135,8 +1135,7 @@ export default function Inbox() { } }, [isForms, formSheetsQuery.data, formSheet]) - // All channel: both sources arrive unpaged; merge newest-first and let the - // pager slice the merged array below. + // All channel: one page from each source, merged newest-first. const mergedRows = useMemo(() => { if (!isAllChannel) return null const emails = Array.isArray(applicationsQuery.data?.rows) ? applicationsQuery.data.rows : [] @@ -1198,15 +1197,17 @@ export default function Inbox() { const countsReady = isAllChannel ? countsQuery.isSuccess && formCountsQuery.isSuccess : (isForms ? formCountsQuery.isSuccess : countsQuery.isSuccess) + const searchTotal = isAllChannel + ? (Number(applicationsQuery.data?.total ?? 0) + Number(formQuery.data?.total ?? 0)) + : (activeQuery.data?.total ?? 0) const total = search - ? (activeQuery.data?.total ?? 0) + ? searchTotal : (countsReady ? tabTotal : (poolTotal || (activeQuery.data?.total ?? 0))) // 'all' = unpaged: the fetch omits top/limit (the endpoints treat a missing // page size as no LIMIT), so the whole tab is one page. const showAll = pageSize === 'all' const pages = showAll ? 1 : Math.max(1, Math.ceil(total / pageSize)) const from = total ? skip + 1 : 0 - const to = showAll ? total : Math.min(skip + pageSize, total) const currentPage = showAll ? 1 : Math.min(Math.floor(skip / pageSize) + 1, pages) useEffect(() => { @@ -1215,8 +1216,9 @@ export default function Inbox() { setSkip(Math.max(0, Math.floor((total - 1) / pageSize) * pageSize)) }, [total, pageSize, skip, showAll]) - // All channel pages the merged array client-side; single channels page on the server. - const list = isAllChannel && !showAll ? inbox.slice(skip, skip + pageSize) : inbox + // Server already applied skip/limit (or the whole tab when Show all). + const list = inbox + const to = showAll ? total : Math.min(skip + (isAllChannel ? list.length : pageSize), total) // Mixed rows: the row's own kind picks the detail endpoint, not the channel. const selectedKind = inbox.find((i) => sameInboxId(i.id, selectedId))?.kind @@ -1399,8 +1401,7 @@ export default function Inbox() { /** Styled XLSX of the rows already loaded in this view — the DB filtered them when the list was fetched (channel, tab, search); no extra request. - Paged channels export the loaded page; the All channel and the "All" - page size hold the whole view, so those export everything. */ + Paged channels export the loaded page; Show all exports the whole view. */ async function exportRows() { const rows = inbox if (!rows.length) { -- 2.40.1