sewt all limit #77

Merged
ahmed.mujtaba merged 2 commits from SQS_BROKER into main 2026-09-07 11:52:13 +00:00
2 changed files with 36 additions and 23 deletions

View File

@ -215,6 +215,18 @@ try {
hasSkeleton(view.html()) && rowCount(view.html()) === 0, hasSkeleton(view.html()) && rowCount(view.html()) === 0,
`skeleton=${hasSkeleton(view.html())} rows=${rowCount(view.html())}`, `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 ------------------------ // ---- frame 2: email home, sheet still travelling ------------------------
// THE REGRESSION: this frame used to render six placeholders on top of two // THE REGRESSION: this frame used to render six placeholders on top of two

View File

@ -49,10 +49,10 @@ const PAGE_SIZE_MAX = 500
* This list is a local table, not a live feed. Applications only appear when * This list is a local table, not a live feed. Applications only appear when
* the Sync worker writes them, and that path already invalidates * the Sync worker writes them, and that path already invalidates
* qk.mailbox.all() the moment a run completes so refetching on every visit * 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 * bought nothing and cost a full-width skeleton each time.
* 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 * List fetches always send the UI page size (10 / 50 / 100), including All
* render the same ten. * omitting limit used to dump the whole form_data table into the browser.
* *
* staleTime therefore covers a normal working stretch, and keepPreviousData * staleTime therefore covers a normal working stretch, and keepPreviousData
* means a tab switch, a page turn or a keystroke re-renders the rows already * 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]) }, [deepOpen, deepKind, setSearchParams])
const isForms = channel === 'forms' const isForms = channel === 'forms'
// Combined channel: both sources fetched UNPAGED (each endpoint reads a // Combined channel: each source is fetched with the UI page size, then
// missing top/limit as no LIMIT), merged by date, and paged client-side // merged newest-first. Per-source pages are not a perfect global timeline,
// per-source skip/top cannot compose into a correct global page. // but omitting limit dumped the whole form_data table on open.
const isAllChannel = channel === 'all' const isAllChannel = channel === 'all'
const channelTabs = isForms || isAllChannel ? FORM_TABS : TABS const channelTabs = isForms || isAllChannel ? FORM_TABS : TABS
const pageLimit = pageSize === 'all' ? undefined : pageSize
const tabFilter = TAB_FILTERS[tab] ?? {} const tabFilter = TAB_FILTERS[tab] ?? {}
const formTabFilter = FORM_TAB_FILTERS[tab] ?? {} const formTabFilter = FORM_TAB_FILTERS[tab] ?? {}
const listParams = useMemo(() => ({ const listParams = useMemo(() => ({
...tabFilter, ...tabFilter,
// 'all' page size and the All channel drop the param entirely the // Show-all omits top (no LIMIT). Otherwise send the pager size 10, 50, 100.
// endpoint reads a missing top as unpaged. top: pageLimit,
top: pageSize === 'all' || isAllChannel ? undefined : pageSize, skip: pageLimit == null ? 0 : skip,
skip: isAllChannel ? 0 : skip,
...(search ? { search } : {}), ...(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, * 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(() => ({ const formParams = useMemo(() => ({
// All channel spans every sheet tab, not just the selected one. // All channel spans every sheet tab, not just the selected one.
sheet: isAllChannel ? undefined : (formSheet || undefined), sheet: isAllChannel ? undefined : (formSheet || undefined),
offset: isAllChannel ? 0 : skip, offset: pageLimit == null ? 0 : skip,
limit: pageSize === 'all' || isAllChannel ? undefined : pageSize, limit: pageLimit,
...formTabFilter, ...formTabFilter,
...(search ? { search } : {}), ...(search ? { search } : {}),
...linkFilters, ...linkFilters,
}), [formSheet, skip, pageSize, search, formTabFilter, isAllChannel, linkFilters]) }), [formSheet, skip, pageLimit, search, formTabFilter, isAllChannel, linkFilters])
const applicationsQuery = useQuery({ const applicationsQuery = useQuery({
queryKey: qk.mailbox.applications(listParams), queryKey: qk.mailbox.applications(listParams),
@ -1135,8 +1135,7 @@ export default function Inbox() {
} }
}, [isForms, formSheetsQuery.data, formSheet]) }, [isForms, formSheetsQuery.data, formSheet])
// All channel: both sources arrive unpaged; merge newest-first and let the // All channel: one page from each source, merged newest-first.
// pager slice the merged array below.
const mergedRows = useMemo(() => { const mergedRows = useMemo(() => {
if (!isAllChannel) return null if (!isAllChannel) return null
const emails = Array.isArray(applicationsQuery.data?.rows) ? applicationsQuery.data.rows : [] const emails = Array.isArray(applicationsQuery.data?.rows) ? applicationsQuery.data.rows : []
@ -1198,15 +1197,17 @@ export default function Inbox() {
const countsReady = isAllChannel const countsReady = isAllChannel
? countsQuery.isSuccess && formCountsQuery.isSuccess ? countsQuery.isSuccess && formCountsQuery.isSuccess
: (isForms ? formCountsQuery.isSuccess : countsQuery.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 const total = search
? (activeQuery.data?.total ?? 0) ? searchTotal
: (countsReady ? tabTotal : (poolTotal || (activeQuery.data?.total ?? 0))) : (countsReady ? tabTotal : (poolTotal || (activeQuery.data?.total ?? 0)))
// 'all' = unpaged: the fetch omits top/limit (the endpoints treat a missing // 'all' = unpaged: the fetch omits top/limit (the endpoints treat a missing
// page size as no LIMIT), so the whole tab is one page. // page size as no LIMIT), so the whole tab is one page.
const showAll = pageSize === 'all' const showAll = pageSize === 'all'
const pages = showAll ? 1 : Math.max(1, Math.ceil(total / pageSize)) const pages = showAll ? 1 : Math.max(1, Math.ceil(total / pageSize))
const from = total ? skip + 1 : 0 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) const currentPage = showAll ? 1 : Math.min(Math.floor(skip / pageSize) + 1, pages)
useEffect(() => { useEffect(() => {
@ -1215,8 +1216,9 @@ export default function Inbox() {
setSkip(Math.max(0, Math.floor((total - 1) / pageSize) * pageSize)) setSkip(Math.max(0, Math.floor((total - 1) / pageSize) * pageSize))
}, [total, pageSize, skip, showAll]) }, [total, pageSize, skip, showAll])
// All channel pages the merged array client-side; single channels page on the server. // Server already applied skip/limit (or the whole tab when Show all).
const list = isAllChannel && !showAll ? inbox.slice(skip, skip + pageSize) : inbox 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. // Mixed rows: the row's own kind picks the detail endpoint, not the channel.
const selectedKind = inbox.find((i) => sameInboxId(i.id, selectedId))?.kind 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 /** 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. them when the list was fetched (channel, tab, search); no extra request.
Paged channels export the loaded page; the All channel and the "All" Paged channels export the loaded page; Show all exports the whole view. */
page size hold the whole view, so those export everything. */
async function exportRows() { async function exportRows() {
const rows = inbox const rows = inbox
if (!rows.length) { if (!rows.length) {