Uh oh!
There was an error while loading. Please reload this page.
fix(grid,list,core): union a grid's grouping fields into the query projection - #7214
Conversation
…ojection A grid view declaring `grouping` on a field absent from its `columns` rendered one group labelled `(empty)` holding every row. `$select` was built from the view's `columns` alone, so the grouping field was never requested and was `undefined` on every row by the time grouping ran. The fix is in the projection, at both sites it is built (`ObjectGrid` when it fetches for itself, `ListView` when it fetches for the grid), plus `$expand` so a lookup grouping field groups by name rather than by raw foreign key. The union is gated: grouping fields are intersected with the object's declared fields and pass the same FLS gate as columns, because an unknown `$select` key zeroes the whole list on backends that reject rather than ignore it.
✅ 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
|
os-warren
commented
Sep 1, 2026
Review — ACCEPT. Reviewer of record: |
Uh oh!
There was an error while loading. Please reload this page.
Textual conflict only, in the two adjacent import statements at the top of packages/plugin-list/src/ListView.tsx: - main (#7214) extended the `@object-ui/core` import with `collectGroupingFieldRefs` - this branch extended the `@object-ui/i18n` import with `pickLocalized` Git folded the two adjacent lines into one hunk and could not take both sides. Resolved by keeping BOTH: main's core import verbatim (so #7214's `collectGroupingFieldRefs` still resolves at both projection call sites) and this branch's i18n import (so `pickLocalized` still resolves at the description read site). Verified mechanically that each side differed from the other by exactly the one added symbol, so nothing else was hand-merged. Nothing #7214 added was touched: the diff of this file against origin/main is exactly this branch's own change — the i18n import line, the `viewDescription` const, and the description render. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
Fixes#7179
A grid view declaring
groupingon a field absent from itscolumnsrendered one grouplabelled
(empty)holding every row.$selectwas built fromcolumnsalone, so thegrouping field was never requested and was
undefinedon every row by the time groupingran. The grouping fields are now unioned into the projection — and into
$expand— atevery site the projection is built.
What the dispatch assumed, and what the code actually says
The projection is built at TWO sites, not one. Both are on the grid's path and they
are independent, so a fix landing on one leaves the other blind:
packages/plugin-grid/src/ObjectGrid.tsx—getSelectFields()packages/plugin-list/src/ListView.tsx— theselectFieldsIIFEListViewfetches and hands the rows down to the gridBoth are fixed, and each has its own regression file so neither can silently regress
back to the other's behaviour.
"Three sibling adapters each union their
groupByField" is not what the code does.plugin-kanban,plugin-ganttandplugin-timelinecontain no projection builder atall — a census of
$selectacross theirsrc/returns zero hits. All three are servedby one shared arm,
collectViewFieldsinListView.tsx, which readsgroupByField/groupField/groupByas plain strings. The grid was the only projection-blind path ofthe four not because it lacked an adapter, but because it spells the same intent
differently:
grouping.fields[], an array of objects, matching none of those candidatekeys. The correction stands — those three files are untouched here.
grouping.fields[]entries carry a plain string, confirmed against the spec.GroupingFieldSchemain@objectstack/spec@17.2.0is a$strictobject of exactly{ field: z.ZodString, order, collapsed }. No expression form, no nested-path form. Abare-string shorthand is refused, deliberately: the schema does not accept it, and
useGroupedDatareadsf.fieldoff each entry, so a bare string groups byundefinedno matter what the projection asks for. Reading it anyway would be the lenient
renderer-side alias AGENTS.md #0.1 forbids.
populateIS part of the fix. Aselectthat fetches a bare foreign key withoutexpanding it buckets by raw id instead of by name — a different wrong answer, not a fix.
The
expandFieldsmemo inListView.tsxalready records this exact failure for kanbanin its own comment ("list view shows 'Initech Solutions' but kanban used to show
'8UY9zHWBfjYjYor4'"), and a grid grouped by a lookup landed in it. Grouping fields are
now unioned into
$expandat both sites, pinned by its own test on each side.The hazard, and why the union is gated
An unguarded union would have been strictly worse than the bug. A
grouping.fields[]entry has never been through column validation — that is the card's whole premise — and
some backends answer an unknown
$selectkey with an empty result set rather thanignoring it. Unioned raw, a grouping field naming a field the object does not declare
turns one
(empty)group holding every row into zero rows, equally silently.So grouping fields land on the speculative, gated side of both builders, never
alongside the known-valid columns:
isProjectableFieldinObjectGrid,addSpeculativeinListView. A grouping field the object does not declare is dropped, and the rest ofthe projection survives intact (pinned in both directions: the unknown key is absent
and the columns are still there).
passesProjectionGateinObjectGrid, and a newaddGroupingFieldwrapper inListView. A grouping field can name a denied fieldexactly as a column can, and the projection is the half that goes on the wire, so
unioning after the FLS filter would have reopened objectui#6898 through a new door.
projectionFls-6898.test.tsxis run here as a regression control.$expandneeds no unknown-key gate and deliberately does not get one:buildExpandFieldsreturns a subset of the object's declared reference-bearingfields, so an unknown or non-relational grouping field is dropped structurally. Pinned
anyway, so a later refactor cannot make that accidental.
ObjectGrid's fetch effect gains agroupingProjectionKeydep — a content key over thegrouping field names only. Grouping is runtime-mutable (the toolbar popover rewrites
it), so without it, switching the grouping field would leave the query asking for the old
one and the new grouping would read
undefinedon every row — the same(empty)bucket,reachable a second way. Names only, because
orderandcollapsedare render-timeconcerns: collapsing a group must not cost a round trip.
Ablation
Direction and counts predicted before running; each leg's mutation proven on disk by
anchored marker count and blob hash; each restore proven by state (
git diff HEAD,git diff --cached,git status --shortall empty) usinggit checkout HEAD --with anabsolute path, never the bare form. The passing count is asserted alongside the red so a
collapsed population cannot pass for a clean run. Restore is trapped on
EXIT INT TERM.ObjectGridprojection union removedListViewprojection union removed$expandaugmentations removedAll three matched exactly. Leg A leaves the
ListViewfile fully green and leg B leavesthe
ObjectGridfile fully green — which is the measurement that shows the two sites aregenuinely independent, and that a one-site fix would have shipped looking correct.
Marker evidence per leg, e.g. leg A: removed-token
1 -> 0, injected-token0 -> 1,blob
0a3f9248to488b47ab. Restore confirmed both files byte-identical to their HEADblobs.
Verification
All runs below are on the final commit,
8006103ba.pnpm exec vitest run packages/plugin-grid/ packages/plugin-list/ packages/core/—272 files / 3846 tests passed, exit 0.
pnpm exec vitest runon the four suites that matter (the two new projection files,the new core unit file, and
projectionFls-6898.test.tsxas regression control) —4 files / 52 tests passed, exit 0. File count asserted as the control that the
right suites ran.
pnpm lint(the whole farm,eslint . --no-inline-config) — 47/47 taskssuccessful, exit 0. Not narrowed.
pnpm exec turbo run type-checkfor the three packages — 16/16 tasks successful,exit 0. Each package runs
tsc --noEmit && tsc -p tsconfig.test.json, and all threenew test files were confirmed present in the test program via
--listFiles, so thegreen covers the tests and not just the sources.
package.jsonand workflows (this repo has noscripts/pm/, and objectstack's dispatch-gates script answers only about its owntree):
check:control-bytes,check:self-import,check:esm-specifiers,check:phantom-deps,check:vi-mock-specifiers,check:vi-mock-inherit,check:side-effects-array,check:entry-guard,check-changeset-presence,check-changeset-no-major— all exit 0, each read from the gate's own printedverdict line rather than a bare exit status.
check:readme-exportsandcheck:eager-closureboth require a full monorepo build this worktree does not have.Each says so itself — readme-exports reports "the population COLLAPSED, this run
proves nothing" with 24 packages unbuilt, and eager-closure reports a missing
apps/console/dist/eager-closure.jsonand calls itself "a broken gauge, not asensitive gate". Neither reads any file in this diff (no README and no entry manifest
is touched). CI builds before it runs them.
Exit codes throughout were captured by redirecting to a file first, never read after a
pipe.
Scope
The
(empty)guard inuseGroupedData.tsis untouched — it is correct for agenuinely empty value and cannot distinguish it from a field that was never fetched. The
defect was upstream of it.
sortis untouched, per the reporter recording it asunmeasured rather than cleared. The kanban, gantt and timeline paths are untouched.
Out of scope, found while measuring
Not fixed here, and not yet filed — this seat's GitHub search returned
API rate limit already exceeded, so the mandatory duplicate check could not be run andfiling blind would risk duplicates. Handed to the PM in the structured report:
nullentry ingrouping.fields[]crashes the whole grid.ObjectGrid'sgroupValueFormattermemo dereferencesgf.fieldwith no null guard, throwing aTypeError before any projection is built. Confirmed pre-existing at this branch's
base commit
4f596e01a. A different defect class from this card's silent wronganswer, so it is filed rather than ridden. The new harvester handles
nullcorrectlyand that is pinned directly in the core unit test, which needs no grid to mount.
$expandcarries no FLS gate at either site.$selectis FLS-gated on bothpaths, but
buildExpandFieldsis handed a column list that was never filtered, so adenied lookup column is still expanded. Adjacent to objectui#6898 and affects columns
generally, not grouping, so fixing it here would widen the review surface past this
card.
ListView's speculative view bindings are not FLS-gated. The kanban / gantt /timeline / calendar / gallery fields routed through
addSpeculativeare checkedagainst the known-field set but never against
checkField. The grouping field addedby this PR is FLS-gated, so this PR does not extend the gap — it leaves an
asymmetry worth closing deliberately rather than as a rider.
Generated by Claude Code
Generated by Claude Code