Repository navigation
fix(plugin-grid,i18n): a paged grid's footer reads the whole matched set, or says it reads the page (objectui#12081 item 3) - #12123
Conversation
…n main (objectui#12081 item 3)
The footer sums the rows the grid holds, which under server paging are one
page, and draws the figure beside the count of the whole set. Eleven pins
fail on main ("Amount: Sum: 32,500" where the 300-row sum is 4,515,000);
the two CONTROL pins (rows handed in whole, a one-page set) pass.
Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
Co-authored-by: Claude <noreply@anthropic.com>
…set, or says it reads the page (objectui#12081 item 3)
The footer summed the rows the grid holds, which under server paging are one
page, and drew the figure beside the count of the whole set ("Sum:
3,685,900.00 · 300 records" where the 300 rows sum to 40,607,000).
When the rows are one page of more, the grid now asks the whole set's
figures as ONE aggregate query through DataSource.queryGroupHeaders with no
groupBy: the query behind the rows (filter lowered through parseFilterAST,
search and searchFields) less its window, with the node set compiled from
the spec's COLUMN_SUMMARY_AGGREGATION and read back through
deriveColumnSummary. A page turn or a sort asks nothing; a refresh or a write
re-asks. The answer is used only when its count equals the count the grid
shows. Otherwise (no door, a refusal, an unaskable query, a count that
differs) the figure is the page's and the label says so: new key
grid.summary.pagePattern in all ten packs.
useColumnSummary's public signature is unchanged; the grid reads the new
module-internal useScopedColumnSummary.
Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
Co-authored-by: Claude <noreply@anthropic.com>
…queryGroupHeaders answers with no groupBy (objectui#12081 item 3) Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz Co-authored-by: Claude <noreply@anthropic.com>
…s effect, and a typed test double (objectui#12081 item 3) The pending state needs no write: the read compares the held question with the render's. The new test file and hook carry no explicit any. Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Brings in objectui#12119 (ObjectGrid.tsx bulk dispatch), objectui#12120 (locale packs) and objectui#12122 before the rework round. Merged without conflicts. Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz Co-authored-by: Claude <noreply@anthropic.com>
…regate (objectui#12081 item 3, rework round 1) summaryColumns is every column object, and the whole-set query always seeded the count node, so every paged grid with object-form columns and no declared summary (every console list view ListView hosts) sent one queryGroupHeaders request per question for a footer it never draws. wholeSetSummaryNodes now answers null when no column declares a summary that maps to an aggregate (none at all, only none, only unknown members); compileWholeSetSummaryQuery answers null for it, and useWholeSetSummary is off: no request, no invalidation subscription. Pinned through the real ObjectGrid with a control leg; changeset and docs name the case. Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Reviewed, and nothing else: card objectui#12081 (body and all 17 comments: triage ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named right or wrong:
Noted, not wrong: (i) whole-set ② Semver level
③ Boundary flagsRound-0 report
REWORK Round-1 report
Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #12081 (item 3)
Clause-②: yes
Items 4 and 7 of objectui#12081 remain open on the card; this pull request carries item 3 only.
What was wrong (reproduced red on
main)Under server paging a grid holds one page of the matched set.
useColumnSummarysummed that page, andObjectGriddrew the figure beside the count of the whole set. The card's reading on objectstack-ai/hotclm#87 (17.7.0): "Planned Amount: Sum: 3,685,900.00 · 300 records", where the 300 rows sum to 40,607,000.The new suite
footerSummaryScope-12081.test.tsxwas committed alone first (95e2271, atopmain206505c): 11 failed, 2 passed (13). Every defect pin failed, for exampleexpected 'Amount: Sum: 32,500' to be 'Amount: Sum: 4,515,000'. The two CONTROL pins passed. Every footer aggregate had the same fault:Name: Count: 25beside 300 rows.What changed
DataSource.queryGroupHeaders, with nogroupBy(newuseWholeSetSummary.ts). The query is the one behind the rows on screen, the same single value the cross-page fan-out replays (bulkFanoutParams, hoisted above the hook):$filterlowered through the spec'sparseFilterAST(the same lowering the grouped header query uses),$searchand$searchFields. Its window, order and projection are not part of the query, so turning the page or sorting asks nothing. A refresh or a write to the object (the data-invalidation bus) asks again and keeps the answer on screen until the new one arrives. The aggregation nodes are built from the spec'sCOLUMN_SUMMARY_AGGREGATIONandcolumnSummaryAlias, and the answer is read back throughderiveColumnSummary, so the mapping is the spec's in both directions.compileListViewGroupQueryitself refuses an empty grouping.countequals the count the grid shows (resolvedTotalMatching, hoisted). It is not used when the data source has no door, refuses, or answers with anything but one row. It is not used when the query carries a key outside the window keys and the three it repeats. In each of those cases the footer shows the page's figure under a label that says so. Inenthat readsAmount: Sum (this page): 32,500, from the newgrid.summary.pagePatternin all ten packs. The answer is also not used while it is pending.useColumnSummarykeeps its public signature and behaviour. The grid reads the new module-internaluseScopedColumnSummary.Where the fix lands, and why not
aggregate()The claim's file list names
useColumnSummary.tsand the footer inplugin-grid. That is where the fix lands, plus the newplugin-grid/src/useWholeSetSummary.ts.data-objectstackneeds no change. I measured the PM's door assumption:DataSource.aggregate()takes the analytics-shapedAggregateParams(one field, one function). Neither of its branches carries a search: the spec-shape branch copiesgroupBy/aggregations/where/limitonly, and the analytics payload carriescube/measures/dimensions/whereonly. The analytics branch can also fall back to a client-side reduction over afind(). So it cannot answer "these rows' figures" under a search.queryGroupHeaderssendsEngineAggregateOptionsas-is toPOST /data/:object/queryand has no fallback.Measured on the platform (17.7.0, in process)
@objectstack/objectql+driver-memory+metadata-protocol+rest17.7.0, through thePOST /api/v1/data/:object/queryroute handler, with 300 rows:total(limit 25)count/sumwhere: { stage: 'won' }wherematching nothingavg/min/maxnullsearch: '1',searchFields: ['name']The aggregate door answers ONE row over the same
whereand the same search expansion as the flatfind. Under a search, though, the paged flattotalisoffset + page + 1(engine.counttakes no search), so the count the console shows there is an estimate. The count check sends that case to the labelled page figure. Pinned by(b) the answer counts other rows than the count shown.Tests and proofs
packages/plugin-grid/src/__tests__/footerSummaryScope-12081.test.tsx, 18 passed. It covers: (a) the grid's own fetch and a host-driven page both read the whole set, and a page turn asks nothing; (c) a filter plus a search narrow the count and the footer alike, asserted on the exact query sent; every footer aggregate; a currency column (the card's shape); string figures from a driver; a write asks again; (b) no door, a refusal, a non-one-row answer, a count that differs, a host with no query, an unrepeatable key; the zh pack's own placement; CONTROL ×2.scripts/ablation-replace.mjs(anchor hit 1→0, blob0e8b5d84fea5→75859734926dand back,git diff HEADempty after restore). Removing the count check turns exactly(b) the answer counts other rows than the count shownred: 1 failed, 17 passed.Gate readings at head
d61feee(run after the last commit; the box is shared, so durations are not idle-box figures)pnpm exec vitest run packages/plugin-grid/: 199 files, 1874 tests passed.pnpm exec vitest run packages/i18n/: 82 files, 1316 passed, 13 skipped (thedescribe.runIf(DRIVEN)timezone legs).pnpm --filter @object-ui/plugin-grid type-check,@object-ui/i18nand@object-ui/types: exit 0, each echoingtype-check.tsc -p tsconfig.test.json --listFilesreads the new test and the new hook.check:phantom-deps,check:self-import,check:esm-specifiers,check:new-line-citations(0 new),check:control-bytes,check-changeset-presence(15 source files of 3 released packages, 1 changeset),check-changeset-no-major,check:changeset-claims,check:pending-changeset-literals,check:test-path-roots, the threecheck:vi-mock-*, the four i18n checks (i18n-keys·i18n-drift·i18n-dead-keys, which does not list the new key ·i18n-designer-parity),check:readme-exportsandcheck:unreferenced-sources.206505c→ headd61feee(two console builds,check:eager-closuregreen on both): +965 bytes gzipped (3,247,241 → 3,248,206). Of that, +949 is theplugin-gridchunk, +21i18n-locale-enand +11vendor-objectstack; the rest is hash churn of a few bytes. The chunk count is unchanged at 290. Headroom went from 33.4 KB to 32.5 KB.check:eager-locale-catalogues:enalone is eager.eslint --print-configresolves a config for each of the 15 touched.ts/.tsxfiles, and none is ignored; (2) the count comes from--format json: 15 entries, 0 errors; (3) invariance:eslint.config.jsdeclares noparserOptions.project/projectService(no type-aware linting), and no customobject-uirule reads the filesystem, so this diff cannot move the verdict of a file it does not touch. Base vs head per file:ObjectGrid.tsx205 → 207 warnings, the +2 beingreact-hooks/refson the hoistedbulkFanoutParamsline. That is the same ref read as before, now with two more readers.useColumnSummary.ts1 → 1,types/src/data.ts27 → 27, and the two new files and the ten packs 0.Clause-② inventory (what this diff adds to a published surface)
grid.summary.pagePattern, in all ten packs (@object-ui/i18nminor).DataSource.queryGroupHeadersJSDoc in@object-ui/typesnow says that the same query with nogroupByanswers the whole filtered set as one header row, and that a source may refuse it. The member's type is unchanged.useScopedColumnSummary,ColumnSummaryScope,useWholeSetSummary,WholeSetSummary,WholeSetSummaryInputandcompileWholeSetSummaryQueryare module exports.plugin-grid'sexportsis.alone (plus./style.css), andsrc/index.tsxexports none of them.useColumnSummary's signature is byte-identical.Overlap
objectui#12119 (in review) also edits
ObjectGrid.tsx: its imports block and the bulk-dispatch region. This diff touches the summary-hook region, one import line, and two constant blocks moved up unchanged except for comments (bulkFanoutParams,resolvedTotalMatching). The hunks do not overlap. Whichever pull request lands second mergesmain.Acceptance notes
plugin-gridREADME,content/docs/plugins/plugin-grid.mdx(AGENTS.md rule Add automated testing infrastructure and CI/CD workflows #2: docs follow the code), and thequeryGroupHeadersJSDoc inpackages/types/src/data.ts. That JSDoc edit is a comment, and the contract doc should name the query this diff now sends. Each is a separate hunk and easy to drop if the seat prefers.count_emptyorpercent_*uses the server's meaning (a storednull). The client footer also counts''and[]as empty. The spec'sColumnSummarySchematext names this "the renderer's to converge". This pull request does not converge it: that would change the one-page footer, which the CONTROL pins keep as it was.numberfigure with no declaredscale. It rounds to the widest decimal count among the page's values (plus the figure itself formin/max). The other pages' values are not in hand.ListView.findParamsHandoff.test.tsxpins that the host hands down exactly the params it fetched with, plusmanualPaginationandrowCount, and this suite's host-driven pins render that handoff.Session:
https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz(domain:uiseat 1 dispatch,os-dev).Generated by Claude Code