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
53 changes: 53 additions & 0 deletions .changeset/column-identity-single-key-at-ingestion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
"@object-ui/core": minor
---

feat(core): one column identity per column — `field` stamped at ingestion (#3104)

A column's field identity was resolved twice, with two different precedences
over the same `schema.columns` array, and the two halves disagreed:

- **request path** — `ListView`'s `$expand` and `$select` builders, and
`ObjectGrid.getSelectFields`, read `f?.field` and only `f?.field`.
- **render path** — the FLS gate, the hidden-field filter, `fieldOrder`, both
export branches and the hide-fields popover read
`f.name || f.fieldName || f.field` — name FIRST.

So `{ field: 'account', name: 'account_name' }` fetched `account` while the
renderer keyed off `account_name`, and `{ name: 'account' }` rendered a column
the request dropped entirely — neither `$select` nor `$expand` carried it. That
is the mechanism behind the "relation column shows a bare id / column is empty
/ sort does nothing / export is missing a column" defect class.

Per AGENTS.md #0.1 the fix is not another `?? name` at the read sites. Legacy
acceptance moves to the one boundary that already folds this view's vocabulary,
`normalizeListViewSchema`, which now also canonicalizes each column's identity.

New in `@object-ui/core`:

- `columnIdentity(entry)` — the single reader. Resolves `field` → `name` →
`fieldName`, canonical-first, so it agrees with `buildExpandFields` instead
of racing it. Handles bare-string columns.
- `normalizeColumnIdentity(entry)` / `normalizeColumnIdentities(columns)` — the
fold. Stamps `field`; a legacy key that is **already present** is mirrored
onto the same identity so name-first readers resolve what the request asked
for; a legacy key that is **absent is never invented**, and an
already-canonical column is returned by reference.
- `hasConflictingColumnIdentity(entry)` — true when a column's keys disagree.
- `CANONICAL_COLUMN_IDENTITY_KEY`, `LEGACY_COLUMN_IDENTITY_KEYS`,
`TABLE_ADAPTER_COLUMN_KEY`.

The fold **mirrors** rather than deleting the legacy key, unlike the other
folds in `normalizeListViewSchema`. Deleting would work inside this repo (every
name-first read falls through to `field`), but `columns` entries cross the
package boundary into host renderers and dropping `name` from under them is a
breaking change with no inventory. Deletion is a later call, once the in-repo
consumers read `columnIdentity()`.

Behaviour is unchanged for any column carrying a single identity key — every
read site resolves the same string it did before. The entries whose resolution
moves are exactly the ones where two sites already disagreed.

`accessorKey` is deliberately untouched: it is TanStack Table's own column key
(`TableColumn.accessorKey`), not ObjectStack metadata identity, and folding
across that boundary would fossilize the merge.
44 changes: 44 additions & 0 deletions content/docs/guide/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,50 @@ This reports duplicate registrations and namespace collisions.

The components shipped in `@object-ui/components` and `@object-ui/plugin-calendar` already use the v9 API; this note exists so downstream apps with their own `<Calendar>` wrappers can apply the same migration.

## 11. A list column is empty, or a relation column shows a raw id

**Symptom:** A column appears in a list/grid but every cell is blank, or a
`lookup` / `master_detail` / `user` column shows a record id (`8UY9zHWBfjYjYor4`)
instead of the related record's name. Sorting by that column does nothing, and
exports come out missing it.

**Cause:** The column object names its field with more than one key. The
canonical key is `field` — the only identity key `@objectstack/spec`'s
`ListColumnSchema` declares — but stored objectui metadata also carries the
legacy `name` (and, in older imports, `fieldName`). When a column carries two
of them with different values, the renderer and the data request can resolve
two different fields: the row fetch asks the server for one field while the
grid renders another, so the cell has nothing behind it and the relation is
never expanded.

```jsonc
// ✗ two identities on one column
{ "field": "account", "name": "account_name" }

// ✗ legacy-only: the renderer shows it, the request used to drop it
{ "name": "account" }

// ✓ canonical
{ "field": "account" }
```

**Fix:** Author columns with `field`. Metadata reaching a `list-view` is
canonicalized at the component boundary by `normalizeListViewSchema`, which
stamps `field` from whichever spelling is present and makes any legacy key it
already carries agree — so mixed metadata resolves to one field everywhere
instead of two. Legacy keys are still accepted, but they are a migration
bridge, not a second contract: fix the producer.

If you read column identity in your own code, use the one reader rather than
spelling out a fallback chain — it resolves canonical-first, so it agrees with
what the data layer requested:

```typescript
import { columnIdentity } from '@object-ui/core';

const fieldName = columnIdentity(column); // string | undefined
```

## Getting Help

If none of the above resolves your issue:
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from './utils/filter-converter.js';
export * from './utils/managedBy.js';
export * from './utils/extract-records.js';
export * from './utils/expand-fields.js';
export * from './utils/column-identity.js';
export * from './utils/sort-values.js';
export * from './evaluator/index.js';
export * from './actions/index.js';
Expand Down
Loading
Loading