Found by a retest of records-forms.bulk-select-all-matching (framework QA run objectstack-ai/objectstack#7439) against the console at pin 6314e87f2d49. The fix under retest — #4140 (the selectionResetKey bump) — is verified working; this is a separate, pre-existing gap that the retest was able to reach for the first time.
Symptom
Selecting an entire page in a genuinely server-paginated grid shows only N selected | Delete | Clear. The cross-page banner never renders: [data-testid=bulk-cross-page-banner] and [data-testid=bulk-select-all-matching] are both absent, for any match-set size. Reproduced on two fresh loads.
Reproduction
- As admin, author a paginated view:
PUT /api/v1/meta/view/showcase_contact.rt5_paged with
config {type:'grid', data:{provider:'object',object:'showcase_contact'}, filter:[{field:'account',operator:'equals',value:'<Northwind id>'}], pagination:{pageSize:10}}
- Open
/_console/apps/com.example.showcase/showcase_contact?view=showcase_contact.rt5_paged.
The grid pages server-side for real — GET /api/v1/data/showcase_contact?top=10&filter=… → total=26, 10 rows, count bar "26 records", pager "Page 1 of 3".
- Click the header select-all checkbox to select the whole visible page.
Expected: BulkActionBar shows "All 10 on this page are selected." plus a "Select all 26 matching" button.
Actual: no banner — only 10 selected | Delete | Clear.
Root cause
BulkActionBar gates the banner on:
const showCrossPageAffordance =
!allMatchingSelected &&
typeof totalMatching === 'number' &&
typeof pageSize === 'number' && pageSize > 0 &&
selectedRows.length >= pageSize &&
totalMatching > pageSize;
ObjectGrid passes totalMatching={singleSelection ? undefined : totalMatching}, and the only writer of that state is setTotalMatching(...) inside ObjectGrid's own data loader (ObjectGrid.tsx ~:710).
In the console the grid is hosted by ListView, which fetches rows itself and passes manualPagination:true, rowCount:serverTotal, page, pageSize, onPageChange down (plugin-list/src/ListView.tsx ~:2859-2864). So externalManualPagination is true, ObjectGrid's loader never runs, totalMatching stays undefined, and the banner condition is permanently false.
The tell that the data is right there: the pager shows the correct page count because it reads
manualRowCount = externalManualPagination ? rest.rowCount : totalMatching
— the grid has the real total on the external path; it just never reaches BulkActionBar.
Suggested fix: fall back to rest.rowCount when externalManualPagination is on.
This is the same shape as #4138 (fixed): a feature gated on internal-data state that the console's external-data path never populates. Worth a sweep for other totalMatching-style state with a single internal writer.
Consequence for the checklist item
With the affordance unreachable, three further clauses cannot run at all: the collected-id set can't be compared against the filtered API count, the aggregate-vs-per-record distinction over the matching set is undefined, and the HARD_CAP (5000) path is doubly out of reach. Only the clause #4140 addressed is green.
Two scope notes, stated plainly
Fixture correction worth recording
The previous run recorded these clauses blocked(fixture) believing no object exceeds the page window. Half of that is true — the stock console fetches top=100 and showcase_contact's 32 rows fit one page — but the blocker is not a hard 100-row limit: authoring a view with config.pagination.pageSize turns on real server-side paging, which is what made this defect reachable. (The probe view was deleted afterwards; no repo file was touched.)
Source
Retest of records-forms.bulk-select-all-matching at console pin 6314e87f2d49, framework b602d536.
Found by a retest of
records-forms.bulk-select-all-matching(framework QA run objectstack-ai/objectstack#7439) against the console at pin6314e87f2d49. The fix under retest — #4140 (theselectionResetKeybump) — is verified working; this is a separate, pre-existing gap that the retest was able to reach for the first time.Symptom
Selecting an entire page in a genuinely server-paginated grid shows only
N selected | Delete | Clear. The cross-page banner never renders:[data-testid=bulk-cross-page-banner]and[data-testid=bulk-select-all-matching]are both absent, for any match-set size. Reproduced on two fresh loads.Reproduction
PUT /api/v1/meta/view/showcase_contact.rt5_pagedwithconfig {type:'grid', data:{provider:'object',object:'showcase_contact'}, filter:[{field:'account',operator:'equals',value:'<Northwind id>'}], pagination:{pageSize:10}}/_console/apps/com.example.showcase/showcase_contact?view=showcase_contact.rt5_paged.The grid pages server-side for real —
GET /api/v1/data/showcase_contact?top=10&filter=…→total=26, 10 rows, count bar "26 records", pager "Page 1 of 3".Expected:
BulkActionBarshows "All 10 on this page are selected." plus a "Select all 26 matching" button.Actual: no banner — only
10 selected | Delete | Clear.Root cause
BulkActionBargates the banner on:ObjectGridpassestotalMatching={singleSelection ? undefined : totalMatching}, and the only writer of that state issetTotalMatching(...)inside ObjectGrid's own data loader (ObjectGrid.tsx~:710).In the console the grid is hosted by
ListView, which fetches rows itself and passesmanualPagination:true, rowCount:serverTotal, page, pageSize, onPageChangedown (plugin-list/src/ListView.tsx~:2859-2864). SoexternalManualPaginationis true, ObjectGrid's loader never runs,totalMatchingstaysundefined, and the banner condition is permanently false.The tell that the data is right there: the pager shows the correct page count because it reads
manualRowCount = externalManualPagination ? rest.rowCount : totalMatching— the grid has the real total on the external path; it just never reaches
BulkActionBar.Suggested fix: fall back to
rest.rowCountwhenexternalManualPaginationis on.This is the same shape as #4138 (fixed): a feature gated on internal-data state that the console's external-data path never populates. Worth a sweep for other
totalMatching-style state with a single internal writer.Consequence for the checklist item
With the affordance unreachable, three further clauses cannot run at all: the collected-id set can't be compared against the filtered API count, the aggregate-vs-per-record distinction over the matching set is undefined, and the HARD_CAP (5000) path is doubly out of reach. Only the clause #4140 addressed is green.
Two scope notes, stated plainly
resetSelection; this gap predates it. ObjectGrid bulk-bar Clear leaves row checkboxes ticked — onClearSelection omits the selectionResetKey bump that resetSelection() performs (#3056 drift) #4140 itself is verified: select-all-page →checkedBoxes 33 / "32 selected", then Clear →checkedBoxes 0and the bar gone, twice — the Bulk delete / by-name bulk action clears the toolbar but leaves the row checkboxes ticked #3056 drift where the toolbar emptied while row checkboxes stayed ticked is absent.automated.refexercisesObjectGridin isolation, where the internal loader does run — precisely the path the console does not take. A regression test for this needs the ListView-hosted composition.Fixture correction worth recording
The previous run recorded these clauses
blocked(fixture)believing no object exceeds the page window. Half of that is true — the stock console fetchestop=100andshowcase_contact's 32 rows fit one page — but the blocker is not a hard 100-row limit: authoring a view withconfig.pagination.pageSizeturns on real server-side paging, which is what made this defect reachable. (The probe view was deleted afterwards; no repo file was touched.)Source
Retest of
records-forms.bulk-select-all-matchingat console pin6314e87f2d49, frameworkb602d536.