Skip to content

Grid grouping collapses every row into one "(empty)" bucket when the grouping field is not also a displayed column — select is built from columns alone #7179

Description

@os-warren

Found by opening a grouped grid in a real ObjectStack application (objectstack-ai/duly) against published @objectstack/* 17.2.0, with 186 real records across 5 business units. Network payloads captured; root cause confirmed in this repo's source.

The claim

A grid view that declares grouping on a field absent from its columns renders one group labelled (empty) containing every row. No error, no warning, no empty state — a grouped grid that looks like it grouped, and did not.

The grouping field is fully populated on every record. The same field, grouped by a dashboard chart on the same data, resolves its labels correctly.

Measured

View declaration:

by_unit: {
  type: 'grid',
  columns,                                    // subject, status, due_date, period_key, owner, source
  grouping: { fields: [{ field: 'business_unit' }] },
}

Rendered: a single collapsible group, (empty) 100, holding every row.

Two payloads captured on that page load:

GET /api/v1/data/duly_task?
  keys: id,…,subject,duty,owner,business_unit,assignment,source,period_key,…
  business_unit: "Q2UJSlOy4ICnWmTg"          ← populated

GET /api/v1/data/duly_task?populate=owner&top=100&select=id,subject,status,due_date,period_key,…
  keys: id,subject,status,due_date,period_key,owner,source
  business_unit: undefined                    ← not requested

The grid's own request builds select from the view's columns and nothing else. business_unit is never asked for, so it is undefined on every row by the time grouping runs.

Storage confirms the data is not the problem — select business_unit, count(*) from duly_task group by 1 gives 5 distinct unit ids over 186 rows, 0 nulls.

Why it lands on (empty) rather than failing

packages/plugin-grid/src/useGroupedData.ts:119, first line of buildSegmentLabel:

if (value === undefined || value === null || value === '') return '(empty)';

That guard is right for a genuinely empty value and cannot distinguish it from a field that was never fetched. Everything below it handles lookups well — an expanded { id, name } resolves through name/label/title/…, and a bare FK string falls through to String(value), which would at least have shown five distinct id buckets. Neither path is reached, because the value is undefined before any of it runs.

So the defect is upstream of the label builder, in the projection: the query should include every field the view groups by.

Why this is worth prioritising

It is a silent wrong answer, not a missing feature, and it is invisible in exactly the conditions authors test under:

  • objectstack validate and build are green — business_unit is a real field on the object, so every binding check passes;
  • the view renders, the row count is right, the rows are right;
  • the only symptom is that the grouping did nothing, and (empty) reads as "these records have no unit" — a plausible, wrong, actionable conclusion. A manager looking at this concludes the org data is unpopulated.

An author whose grouping field happens to also be a displayed column never sees it, which is why this survives casual use.

Reproduce

// any object with a populated field `f`
{ type: 'grid',
  columns: [{ field: 'a' }, { field: 'b' }],   // note: no `f`
  grouping: { fields: [{ field: 'f' }] } }

Open the view. One group, (empty), all rows. Add { field: 'f' } to columns and it groups correctly.

Suggested direction

Union the grouping fields into the select projection where it is built, rather than requiring authors to mirror them in columns — grouping by a field you do not want as a visible column is an ordinary thing to want, and it is what the two config keys being separate implies is supported.

Worth checking the same builder for the neighbouring cases before closing: a sort on a non-column field, and the groupBy fields on kanban and gantt, all take a field name from a config key that is not columns.

If the projection is deliberately columns-only, then the author-time gate is the alternative — refuse a grouping.fields[] entry that names no column — but the projection fix is the one that matches what the schema already lets you write.

Application impact

In the app that found this, the affected view is the manager's org lens: "show me what is outstanding, by business unit". It shipped green, with a passing metadata-binding test that resolves business_unit against the object schema — which it does resolve, correctly, and which is exactly why the test could not catch this.

Activity

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

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfindingpriority:p1

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions