Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .changeset/6677-grid-host-fed-default-columns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
'@object-ui/plugin-grid': patch
---

An object-bound grid whose rows arrive from a **host** now renders the object
schema's default-columns policy instead of the row payload's keys
(objectui#6677).

`ObjectGrid.generateColumns()` checks three default paths in order: authored
`columns` → the inline-data path → the object-schema path. The inline-data path
is gated on `hasInlineData` (`dataConfig.provider === 'value'`), and
`dataConfig` is built as `provider: 'value'` from the `data` **prop** before
anything else — so it is taken by every grid whose rows were handed down rather
than fetched. It returned unconditionally whenever rows were present, and its
projection is `schemaFields || Object.keys(inlineData[0])`, the first row's
keys. That made the object-schema path — the one carrying the documented policy
(`highlightFields` first; else drop `hidden`, drop readonly system-managed, push
the remaining system/ownership columns to the end) — **unreachable for every
object-bound grid reached through a fetching host** (`ListView`, `ObjectView`,
…). The branch that knows the object was the one that never ran.

Measured on the same page, source and object with one variable — who fetches:
`<object-grid objectName="opportunity" />` rendered the policy's **5** columns
(Opportunity Name / Stage / Amount / Close Date / Owner); the same object behind
`<list-view>` rendered **10**, adding `Id` (`hidden: true`) and the four audit
columns (`system`). Those are exactly what the policy exists to keep off a
default list, and the extra key set was whatever the query happened to return.

**The yield is as narrow as the defect, and the two boundaries are the change.**
Only the row-key *fallback* is wrong for an object-bound grid, so only that is
given up, and only once there is a policy to give it up to
(`!schemaFields && !!objectName && !!objectSchema`):

- **An authored `fields` projection still wins.** The schema path drops a name
the object does not declare (`if (!field) return;`), and a host may
legitimately join or derive keys, so an explicit projection is not overridden
— including when it names an audit column on purpose. `!schemaFields` is
exactly the condition under which the `||` reaches for the row keys, so the
gate cannot drift from the fallback it guards.
- **Gating on `objectName` alone would have been a worse defect.** The schema
arrives from an async fetch, so `objectSchema` is `null` on first paint; that
gate falls through to `if (!objectSchema) return []` and paints an empty
header row before flipping. Requiring the *loaded* schema keeps the row-key
columns on screen until the object is actually known, and is also the
graceful fallback when the schema fetch fails or the data source has no
`getObjectSchema` — the grid degrades to heuristic columns rather than going
blank.

Inline data with no object behind it is untouched: the "Legacy support" path is
reordered, never deleted, and is still the right answer there.

Scored **patch**, deliberately. No public API moves — no prop, type, export or
signature changes — and this restores the default-columns policy the component
already documents and already applied whenever the grid fetched its own rows;
the host-fed divergence was the defect, not a contract. `minor` was considered,
because the visible column set changes on existing screens, and rejected: the
lost columns were never *declared* by any author, only leaked by the branch
order, and this repo scores behaviour-correcting fixes as patch and reserves
`minor` for new capability (a `major` is never authored here — the fixed group
tracks `@objectstack`).
45 changes: 42 additions & 3 deletions packages/plugin-grid/src/ObjectGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2256,8 +2256,47 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
});
}

// Legacy support: use 'fields' if columns not provided
if (hasInlineData) {
// Legacy support: use 'fields' if columns not provided.
//
// ⭐ THE ORDER OF THIS PATH AND THE OBJECT-SCHEMA PATH BELOW IS
// LOAD-BEARING (objectui#6677).
//
// `hasInlineData` is `dataConfig.provider === 'value'`, and `dataConfig` is
// built as `provider: 'value'` from the `data` PROP before anything else.
// So this path is taken by EVERY grid whose rows were handed down instead
// of fetched — which is every object-bound grid reached through a fetching
// host (`ListView`, `ObjectView`, …). It used to return unconditionally
// whenever rows were present, which made the object-schema path below
// unreachable for all of them: the branch that knows the object was the one
// that never ran. Measured, same page / source / object, one variable:
// grid-fetches rendered the policy's 5 columns, host-fetches rendered 10 —
// the payload's keys, including `id` (`hidden: true`) and the four audit
// columns (`system`), exactly what the policy exists to exclude.
//
// The yield is as NARROW as the defect. Only the ROW-KEY FALLBACK
// (`Object.keys(inlineData[0])`) is wrong for an object-bound grid, so only
// that is given up, and only once there is a policy to give it up TO:
//
// - `schemaFields` present ⇒ this path keeps it. An authored projection
// is the author's contract, and the schema path would silently drop a
// name the object does not declare (`if (!field) return;`) — a host may
// legitimately join or derive keys. `!schemaFields` is exactly the
// condition under which the `||` below reaches for the row keys, so the
// gate and the fallback cannot drift apart.
// - `objectSchema` still `null` ⇒ this path keeps it. ⚠️ Gating on
// `objectName` ALONE is the trap: the schema arrives from an async
// fetch, so `objectSchema` is null on first paint and the grid would
// fall straight through to `if (!objectSchema) return []` and render an
// empty header row before flipping — a worse defect than this one. It
// is also the graceful fallback when the schema fetch fails or the data
// source has no `getObjectSchema`: the row keys stay the answer instead
// of the grid going blank.
//
// Both are pinned in `hostFetchedDefaultColumns-6677.test.tsx`, together
// with the case this file's own comment calls the right one for this path:
// inline data with no object behind it at all.
const rowKeysWouldOutrankSchemaPolicy = !schemaFields && !!objectName && !!objectSchema;
if (hasInlineData && !rowKeysWouldOutrankSchemaPolicy) {
const inlineData = dataConfig?.provider === 'value' ? dataConfig.items as any[] : [];
if (inlineData.length > 0) {
const fieldsToShow = schemaFields || Object.keys(inlineData[0]);
Expand Down Expand Up @@ -2381,7 +2420,7 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
});

return generatedColumns;
}, [objectSchema, schemaFields, schemaColumns, dataConfig, hasInlineData, navigation.handleClick, executeAction, data, resolveFieldLabel, translateOptions, schema.objectName, perms]);
}, [objectSchema, schemaFields, schemaColumns, dataConfig, hasInlineData, objectName, navigation.handleClick, executeAction, data, resolveFieldLabel, translateOptions, schema.objectName, perms]);

// Formats this grid can actually deliver (objectui#2942): the server stream
// handles csv/xlsx/json, the client fallback only csv/json. Declared-but-dead
Expand Down
Loading
Loading