Skip to content

A grouping field name with surrounding whitespace passes validation, is trimmed for $select, and then buckets every row into one (empty) group #7347

Description

@os-litant

Blocked-by: objectstack-ai/objectstack#17360

Surfaced while implementing objectui#7217 (the null-entry crash guard), by the measurement that defined that guard's admission rule. Filed unassigned, not fixed there — it is a different defect class from that card's loud crash, and it is deliberately left alone by objectui#7217's guard rather than ridden.

The claim

GroupingFieldSchema.field is a bare z.ZodString with no .trim() and no pattern, so grouping: { fields: [{ field: ' business_unit ' }] } is valid authored metadata. The two readers of that name then disagree:

  • collectGroupingFieldRefs (packages/core/src/utils/grouping-fields.ts:71-76) trims — it harvests business_unit, and that is what reaches $select.
  • ObjectGrid's groupValueFormatter memo and useGroupedData's buildSegmentKey bucket rows by the raw, untrimmed key, i.e. row[' business_unit '].

So the server is asked for business_unit and returns it under that name, while the grid looks up a key that no row carries. Every row reads undefined, buildSegmentLabel answers (empty) for all of them, and the view shows ONE group holding every record — no error, no warning, no empty state.

Measured, not inferred

Run in the objectui#7217 worktree against @objectstack/spec/ui and @object-ui/core's built dist:

spec accepts a padded field name: true {"fields":[{"field":"  business_unit  ","order":"asc","collapsed":false}]}
harvester (drives $select) yields: ["business_unit"]
grid would bucket rows by the RAW key: "  business_unit  "

Why this is worth a card of its own

It is the exact silent-wrong-answer failure objectui#7179 closed — one (empty) bucket holding every row, which reads as a true statement about the data rather than a bug in the view — but arriving by a different route, and it survives author-time validation. The null entry that objectui#7217 guards is refused by GroupingConfigSchema; a padded name is accepted by it. That makes this one strictly more reachable through the validated path, and quieter.

Why objectui#7217 did not fix it

That card's guard admits an entry when it is an object carrying a non-empty string field — the harvester's own admission rule, chosen so the grid can never group by an entry the projection ignored. A padded name satisfies it, so the entry is admitted and grouping proceeds on the raw key, exactly as it does on main today. Changing the value as well as the admission set would have been a behaviour change beyond that card's crash, on a hot file, so it was left for triage here.

Fix directions, not a ruling

  1. Normalize at the reader — trim in the grid's entry normalizer so both sides use the harvested name. Smallest change; makes the two readers agree by construction.
  2. Refuse at the producer — give GroupingFieldSchema.field a .trim() or a non-padded pattern in @objectstack/spec, so a padded name never becomes valid metadata. This is the contract-first direction (AGENTS.md #0.1: fix the producer, do not teach the renderer to tolerate), but it moves a published validator's accept set and is a spec change, so it needs the maintainer.
  3. Both: refuse new metadata at the producer, normalize for metadata already stored.

Worth checking whether the same trim asymmetry exists for the sibling view kinds' groupByField (kanban / gantt / timeline) before choosing, since they union their grouping field into the projection through their own path.

Related: objectui#7179 (the projection fix whose harvester does the trimming), objectui#7217 (the null-entry crash guard this came out of).


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:blockedpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions