You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Cross-page select-all fan-out ignores the view's query under external pagination — lastFindParamsRef and the query-change reset are written only by ObjectGrid's own loader #4501
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:
constresolveBulkRows=async(rowsHint: any[])=>{if(!selectAllMatching)returnrowsHint;constbase={ ...(lastFindParamsRef.current??{})};// <-- null on the external pathdeletebase.$top;deletebase.$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-shellviews/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.
setError has 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.tsx on origin/main at c5756ff40: single writer of lastFindParamsRef at the loader, single reader in resolveBulkRows.
CLAIM — PRIORITY — session session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4501-bulk-fanout-params. PR #4503 (#4464's banner fix) is ACCEPTED but HELD IN DRAFT until this card's clause-1 guard lands — landing order is the safety mechanism.
Ruling (delegated authority; open veto window): both halves, per the filing dev's own analysis:
Host params prop (the contract fix): the external-pagination host hands its live query params down — ListView (which owns filter/search/sort on that path and already passes rowCount) passes the find-params it used, ObjectGrid's resolveBulkRows uses them for the fan-out. Measure the exact shape ListView holds (it issues the queries — the params exist) and mirror the internal loader's lastFindParamsRef content. NOT a grid-side ?? {} fallback — that is the tolerant-consumer shape AGENTS.md #0.1 rejects, and it is precisely what produces the unfiltered 5000-record collection.
Abstain floor (the permanent guard): when resolveBulkRows has NO params for the current data path — internal ref null AND no host prop — the select-all-matching AFFORDANCE is not offered (same answer as absent), so no future host wiring gap can reopen the unfiltered fan-out. The floor is what makes the hazard class structurally unreachable rather than currently-wired-correctly.
Clause 2 rides along: the escalation resets when the external host's query changes (ListView's filter/search/sort/page transitions) — mirror the internal loader's setSelectAllMatching(false) semantics at whatever seam the params prop creates (a changed params identity is the natural reset signal). Pin it: escalate → change filter → the escalation is gone, no stale "All N matching" against a new total.
Red-first: on current main the UI path is unreachable (that is WHY this lands first) — so the red is at the seam the filing measured: resolveBulkRows driven directly (or via the internal path with the ref nulled) fans out with {} params pre-fix (capture the find calls' params verbatim — $filter absent); post-fix it uses the host params / abstains per the floor. Plus a composition-level pin that the affordance is NOT offered when params are absent. After #4503 lands, its test file gains the full-path assertion — note that as the follow-through, don't block on it.
Grading: patch @object-ui/plugin-grid + patch @object-ui/plugin-list (the params prop producer); .d.ts measured both ways — if the prop is a new published member on ObjectGrid's props type, that is minor per precedent, say so and grade accordingly. Never major.
Surface (mutual exclusion): packages/plugin-grid/src/ObjectGrid.tsx (resolveBulkRows + props + the affordance gate), packages/plugin-list/src/ListView.tsx (params prop producer — NOW FREE, #4495 landed), tests in both packages, changesets. ⛔ NOT: BulkActionBar's file unless measurement demands (report first), app-shell ObjectView (its onBulkDelete wiring is downstream and unchanged), console/**, the sweep file. Coordinate note: PR #4503 (draft) edits ObjectGrid.tsx regions ~2301/2361/3104/3142 — your regions (~837/2042/props) are disjoint; branch from origin/main (NOT from #4503's branch) and expect #4503 to rebase over you.
Activity
yinlianghui commentedon Aug 12, 2026
CLAIM — PRIORITY — session
session_017Qqyix2QcnpUC9XeYVDzx3, branchclaude/issue-4501-bulk-fanout-params. PR #4503 (#4464's banner fix) is ACCEPTED but HELD IN DRAFT until this card's clause-1 guard lands — landing order is the safety mechanism.Ruling (delegated authority; open veto window): both halves, per the filing dev's own analysis:
rowCount) passes the find-params it used, ObjectGrid'sresolveBulkRowsuses them for the fan-out. Measure the exact shape ListView holds (it issues the queries — the params exist) and mirror the internal loader'slastFindParamsRefcontent. NOT a grid-side?? {}fallback — that is the tolerant-consumer shape AGENTS.md #0.1 rejects, and it is precisely what produces the unfiltered 5000-record collection.resolveBulkRowshas NO params for the current data path — internal ref null AND no host prop — the select-all-matching AFFORDANCE is not offered (same answer as absent), so no future host wiring gap can reopen the unfiltered fan-out. The floor is what makes the hazard class structurally unreachable rather than currently-wired-correctly.setSelectAllMatching(false)semantics at whatever seam the params prop creates (a changed params identity is the natural reset signal). Pin it: escalate → change filter → the escalation is gone, no stale "All N matching" against a new total.Red-first: on current main the UI path is unreachable (that is WHY this lands first) — so the red is at the seam the filing measured:
resolveBulkRowsdriven directly (or via the internal path with the ref nulled) fans out with{}params pre-fix (capture thefindcalls' params verbatim —$filterabsent); post-fix it uses the host params / abstains per the floor. Plus a composition-level pin that the affordance is NOT offered when params are absent. After #4503 lands, its test file gains the full-path assertion — note that as the follow-through, don't block on it.Grading: patch
@object-ui/plugin-grid+ patch@object-ui/plugin-list(the params prop producer);.d.tsmeasured both ways — if the prop is a new published member on ObjectGrid's props type, that is minor per precedent, say so and grade accordingly. Never major.Surface (mutual exclusion):
packages/plugin-grid/src/ObjectGrid.tsx(resolveBulkRows + props + the affordance gate),packages/plugin-list/src/ListView.tsx(params prop producer — NOW FREE, #4495 landed), tests in both packages, changesets. ⛔ NOT: BulkActionBar's file unless measurement demands (report first), app-shell ObjectView (its onBulkDelete wiring is downstream and unchanged), console/**, the sweep file. Coordinate note: PR #4503 (draft) edits ObjectGrid.tsx regions ~2301/2361/3104/3142 — your regions (~837/2042/props) are disjoint; branch from origin/main (NOT from #4503's branch) and expect #4503 to rebase over you.Generated by Claude Code
Generated by Claude Code
Merge origin/main into #4464 branch — the floor and the resolved tota…
fix(plugin-grid): select-all-matching replays the host's real query —…