Skip to content

Commit 6ae818e

Browse files
os-zhuangclaude
andauthored
fix(core,list): 列身份归一到 ingestion chokepoint — 一列一个身份 (#3104 PR1) (#3119)
Column 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` / `$select` builders and ObjectGrid.getSelectFields read `f?.field` and ONLY that. 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. 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 boundary that already folds this view's vocabulary: `normalizeListViewSchema` now canonicalizes each column's identity too, running after the `fields` -> `columns` rename so a doubly-legacy view is fixed on both axes in one pass. New in @object-ui/core (`utils/column-identity.ts`): columnIdentity() the single reader, canonical-first normalizeColumnIdentity(-ies)() the fold hasConflictingColumnIdentity() true when a column's keys disagree The fold MIRRORS rather than deletes the legacy key, unlike the other folds in normalizeListViewSchema. Deleting works in-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. An absent legacy key is never invented, and an already-canonical column is returned by reference so ListView's useMemo deps stay stable. `accessorKey` is deliberately untouched: it is TanStack Table's column key (`TableColumn.accessorKey`), not ObjectStack metadata identity. Tests: - ListView.columnIdentity.test.tsx — the repro through the real ListView. `checkField` is the observable: BOTH paths call it, so a column whose keys disagree checks two different field names. Now exactly one. - column-identity.ratchet.test.ts — the 34-site inventory with a per-site precedence and verdict; a new dual read, or an extra one in a listed file, fails. Shrinking fails too, so the tree and the record cannot drift. The inventory corrects the issue's framing in two places: resolveActionParams (3) and dashboard-filters (1) carry BOTH orders legitimately — `name` and `field` are separately declared there and mean different things, so those are two layers, not two spellings. The real pollution is worse than "two orders coexist": ListView and ObjectGrid each disagree with themselves. Behaviour is unchanged for any column carrying a single identity key. The entries whose resolution moves are exactly the ones where two sites already disagreed. Refs: objectstack#4115, #3090 (playbook), #2598 (chokepoint precedent) Claude-Session: https://claude.ai/code/session_01C2pdPmf2yZSd4wFDs1NHY5 Co-authored-by: Claude <noreply@anthropic.com>
1 parent bac266c commit 6ae818e

8 files changed

Lines changed: 970 additions & 1 deletion

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
"@object-ui/core": minor
3+
---
4+
5+
feat(core): one column identity per column — `field` stamped at ingestion (#3104)
6+
7+
A column's field identity was resolved twice, with two different precedences
8+
over the same `schema.columns` array, and the two halves disagreed:
9+
10+
- **request path**`ListView`'s `$expand` and `$select` builders, and
11+
`ObjectGrid.getSelectFields`, read `f?.field` and only `f?.field`.
12+
- **render path** — the FLS gate, the hidden-field filter, `fieldOrder`, both
13+
export branches and the hide-fields popover read
14+
`f.name || f.fieldName || f.field` — name FIRST.
15+
16+
So `{ field: 'account', name: 'account_name' }` fetched `account` while the
17+
renderer keyed off `account_name`, and `{ name: 'account' }` rendered a column
18+
the request dropped entirely — neither `$select` nor `$expand` carried it. That
19+
is the mechanism behind the "relation column shows a bare id / column is empty
20+
/ sort does nothing / export is missing a column" defect class.
21+
22+
Per AGENTS.md #0.1 the fix is not another `?? name` at the read sites. Legacy
23+
acceptance moves to the one boundary that already folds this view's vocabulary,
24+
`normalizeListViewSchema`, which now also canonicalizes each column's identity.
25+
26+
New in `@object-ui/core`:
27+
28+
- `columnIdentity(entry)` — the single reader. Resolves `field``name`
29+
`fieldName`, canonical-first, so it agrees with `buildExpandFields` instead
30+
of racing it. Handles bare-string columns.
31+
- `normalizeColumnIdentity(entry)` / `normalizeColumnIdentities(columns)` — the
32+
fold. Stamps `field`; a legacy key that is **already present** is mirrored
33+
onto the same identity so name-first readers resolve what the request asked
34+
for; a legacy key that is **absent is never invented**, and an
35+
already-canonical column is returned by reference.
36+
- `hasConflictingColumnIdentity(entry)` — true when a column's keys disagree.
37+
- `CANONICAL_COLUMN_IDENTITY_KEY`, `LEGACY_COLUMN_IDENTITY_KEYS`,
38+
`TABLE_ADAPTER_COLUMN_KEY`.
39+
40+
The fold **mirrors** rather than deleting the legacy key, unlike the other
41+
folds in `normalizeListViewSchema`. Deleting would work inside this repo (every
42+
name-first read falls through to `field`), but `columns` entries cross the
43+
package boundary into host renderers and dropping `name` from under them is a
44+
breaking change with no inventory. Deletion is a later call, once the in-repo
45+
consumers read `columnIdentity()`.
46+
47+
Behaviour is unchanged for any column carrying a single identity key — every
48+
read site resolves the same string it did before. The entries whose resolution
49+
moves are exactly the ones where two sites already disagreed.
50+
51+
`accessorKey` is deliberately untouched: it is TanStack Table's own column key
52+
(`TableColumn.accessorKey`), not ObjectStack metadata identity, and folding
53+
across that boundary would fossilize the merge.

content/docs/guide/troubleshooting.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,50 @@ This reports duplicate registrations and namespace collisions.
282282

283283
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.
284284

285+
## 11. A list column is empty, or a relation column shows a raw id
286+
287+
**Symptom:** A column appears in a list/grid but every cell is blank, or a
288+
`lookup` / `master_detail` / `user` column shows a record id (`8UY9zHWBfjYjYor4`)
289+
instead of the related record's name. Sorting by that column does nothing, and
290+
exports come out missing it.
291+
292+
**Cause:** The column object names its field with more than one key. The
293+
canonical key is `field` — the only identity key `@objectstack/spec`'s
294+
`ListColumnSchema` declares — but stored objectui metadata also carries the
295+
legacy `name` (and, in older imports, `fieldName`). When a column carries two
296+
of them with different values, the renderer and the data request can resolve
297+
two different fields: the row fetch asks the server for one field while the
298+
grid renders another, so the cell has nothing behind it and the relation is
299+
never expanded.
300+
301+
```jsonc
302+
// ✗ two identities on one column
303+
{ "field": "account", "name": "account_name" }
304+
305+
// ✗ legacy-only: the renderer shows it, the request used to drop it
306+
{ "name": "account" }
307+
308+
// ✓ canonical
309+
{ "field": "account" }
310+
```
311+
312+
**Fix:** Author columns with `field`. Metadata reaching a `list-view` is
313+
canonicalized at the component boundary by `normalizeListViewSchema`, which
314+
stamps `field` from whichever spelling is present and makes any legacy key it
315+
already carries agree — so mixed metadata resolves to one field everywhere
316+
instead of two. Legacy keys are still accepted, but they are a migration
317+
bridge, not a second contract: fix the producer.
318+
319+
If you read column identity in your own code, use the one reader rather than
320+
spelling out a fallback chain — it resolves canonical-first, so it agrees with
321+
what the data layer requested:
322+
323+
```typescript
324+
import { columnIdentity } from '@object-ui/core';
325+
326+
const fieldName = columnIdentity(column); // string | undefined
327+
```
328+
285329
## Getting Help
286330

287331
If none of the above resolves your issue:

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export * from './utils/filter-converter.js';
1818
export * from './utils/managedBy.js';
1919
export * from './utils/extract-records.js';
2020
export * from './utils/expand-fields.js';
21+
export * from './utils/column-identity.js';
2122
export * from './utils/sort-values.js';
2223
export * from './evaluator/index.js';
2324
export * from './actions/index.js';

0 commit comments

Comments
 (0)