Found by the #4464 census (the sweep that issue asks for: "worth a sweep for other totalMatching-style state with a single internal writer"). Two more members of the same #4138 / #4464 class, in the same feature. Filed unassigned for triage.
Reachability changed with #4464. Before that fix the cross-page banner could never appear under external pagination, so neither clause below could be reached from the console. #4464 makes the affordance appear — which is correct and is what the checklist item asks for — and in doing so it makes both of these live. They are pre-existing gaps, not regressions of that PR, but the first one should probably be graded against that PR's merge.
Clause 1 (the serious one) — the fan-out re-queries with NO filter, sort or search
ObjectGrid.tsx:
const resolveBulkRows = async (rowsHint: any[]) => {
if (!selectAllMatching) return rowsHint;
const base = { ...(lastFindParamsRef.current ?? {}) }; // <-- null on the external path
delete base.$top; delete base.$skip;
// ... pages through dataSource.find(objectName, { ...base, $top: 500, $skip }) up to HARD_CAP 5000
};
lastFindParamsRef.current has exactly ONE writer — inside ObjectGrid's own data loader, next to setTotalMatching. Under a host that fetches the rows itself (ListView passing manualPagination + rowCount, i.e. the console) that loader never runs, so the ref stays null, base is {}, and the fan-out asks the server for the whole object: the view's $filter, $orderby, $search and $select are all absent.
Consequences, in order of severity:
- The collected set is not the matching set. The bar says "All 26 matching records are selected"; the executor receives up to 5000 records drawn from the unfiltered object.
- That set is then handed to a destructive path.
apps/app-shell views/ObjectView.tsx wires onBulkDelete down to the grid, so select-all-matching then Delete confirms with the fanned-out count and deletes records the view never matched. The confirmation dialog does quote the count, so a user has one chance to notice that "26 matching" became a four-digit number — that is the only thing standing between this and data loss.
- It is also the reason the checklist clause "compare the collected-id set against the filtered API count" cannot pass even once the banner is reachable.
The fix direction is the same shape as #4464's: the query the fan-out replays must come from whichever side owns the fetch, not from a ref only one of them writes. A host-driven path has no way to hand its params down today, so this likely needs a prop (or the host performing its own fan-out) rather than a fallback inside the grid — worth a ruling rather than a guess.
Clause 2 (separable, lower severity) — the cross-page flag survives a query change
Same loader, three lines further on:
lastFindParamsRef.current = { ...params };
// Reset cross-page flag whenever the underlying query changes.
setSelectAllMatching(false);
That reset is the only query-driven writer of selectAllMatching (the others are resetSelection() and the bar's own button). On the external path it never fires, so a user who escalates to "all matching", then changes the filter, the search term or the page from ListView's toolbar, keeps selectAllMatching === true — and with #4464 landed the bar now re-renders "All N matching records are selected" against the NEW total without the user ever confirming that set.
Not filed, recorded here for the next reader
Source
Census performed while implementing #4464 (see that PR's "reachability note"). Verified by reading packages/plugin-grid/src/ObjectGrid.tsx on origin/main at c5756ff40: single writer of lastFindParamsRef at the loader, single reader in resolveBulkRows.
Generated by Claude Code
Found by the #4464 census (the sweep that issue asks for: "worth a sweep for other
totalMatching-style state with a single internal writer"). Two more members of the same #4138 / #4464 class, in the same feature. Filed unassigned for triage.Reachability changed with #4464. Before that fix the cross-page banner could never appear under external pagination, so neither clause below could be reached from the console. #4464 makes the affordance appear — which is correct and is what the checklist item asks for — and in doing so it makes both of these live. They are pre-existing gaps, not regressions of that PR, but the first one should probably be graded against that PR's merge.
Clause 1 (the serious one) — the fan-out re-queries with NO filter, sort or search
ObjectGrid.tsx:lastFindParamsRef.currenthas exactly ONE writer — inside ObjectGrid's own data loader, next tosetTotalMatching. Under a host that fetches the rows itself (ListView passingmanualPagination+rowCount, i.e. the console) that loader never runs, so the ref staysnull,baseis{}, and the fan-out asks the server for the whole object: the view's$filter,$orderby,$searchand$selectare all absent.Consequences, in order of severity:
apps/app-shellviews/ObjectView.tsxwiresonBulkDeletedown to the grid, so select-all-matching then Delete confirms with the fanned-out count and deletes records the view never matched. The confirmation dialog does quote the count, so a user has one chance to notice that "26 matching" became a four-digit number — that is the only thing standing between this and data loss.The fix direction is the same shape as #4464's: the query the fan-out replays must come from whichever side owns the fetch, not from a ref only one of them writes. A host-driven path has no way to hand its params down today, so this likely needs a prop (or the host performing its own fan-out) rather than a fallback inside the grid — worth a ruling rather than a guess.
Clause 2 (separable, lower severity) — the cross-page flag survives a query change
Same loader, three lines further on:
That reset is the only query-driven writer of
selectAllMatching(the others areresetSelection()and the bar's own button). On the external path it never fires, so a user who escalates to "all matching", then changes the filter, the search term or the page from ListView's toolbar, keepsselectAllMatching === true— and with #4464 landed the bar now re-renders "All N matching records are selected" against the NEW total without the user ever confirming that set.Not filed, recorded here for the next reader
hasInlineDatais true, sohandleExport'sserverEligiblebranch is skipped and the grid's own export falls back to client-side export of the current window while the count bar reports the full total. This is adjacent to the in-flight ListView export work (A list request that 404s withOBJECT_API_DISABLEDrenders as the generic empty state — "this page cannot work" reads as "you have no records" #4408) and may already be covered there, so no separate issue.setErrorhas only the loader's writer too, but ListView owns the error surface on that path, so nothing is lost.Source
Census performed while implementing #4464 (see that PR's "reachability note"). Verified by reading
packages/plugin-grid/src/ObjectGrid.tsxonorigin/mainatc5756ff40: single writer oflastFindParamsRefat the loader, single reader inresolveBulkRows.Generated by Claude Code