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
38 changes: 38 additions & 0 deletions .changeset/objectview-table-columns-non-grid-5269.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
---
'@object-ui/plugin-view': patch
---

`ObjectView` now forwards the canonical `table.columns` on the non-grid paths, not only on the grid one.

`ObjectViewSchema.table` inherits from `ObjectGridSchema`, where `columns` is the
canonical spelling and `fields` carries `@deprecated Use columns instead`. Only
one of the file's three field-list read points consulted `table.columns` — the
grid one. `generateViewSchema`'s shared `baseProps` and the delegated
`renderListView` schema both read `table.fields` alone, so an author who wrote
`table: { columns: [...] }` on a non-grid view got an empty field list from a
schema that compiled and read correctly. Same silent-success shape as
objectui#5102, different mechanism: not a whitelist that knows only legacy
spellings, but one that disagreed with itself between two rendering paths.

Both sites now read the canonical key first and keep the deprecated one as a
working alias, exactly as objectui#5102 settled it for its four pairs. Nothing
is translated or reshaped on the way through, and precedence is unchanged: a
named view's `columns`, then the active view's, then the `table` segment.

Where this is observable, measured rather than assumed: `object-kanban` (the
card fields) and `object-tree` (its flat columns) consume the shared
`baseProps` field list, and the delegated `list-view` consumes `columns`.
`object-gallery`, `object-calendar`, `object-timeline`, `object-gantt` and
`object-map` read no field list off their schema at all, so the forwarded value
is inert there — before this change and after it.

One shape question the forwarding raised, answered at the boundary:
`table.columns` is `string[] | ListColumn[]`, and the non-grid slot is a
names slot (`ObjectKanban` indexes the record by each entry). The object form
is therefore resolved to field names there with `columnIdentity` — the same
fold `ObjectGrid` applies to this very value — so one authored `table.columns`
resolves identically on both paths, and a `ListColumn[]` cannot arrive as a
non-empty card field list naming nothing (which would suppress ObjectKanban's
`highlightFields` fallback and render emptier than the bug being fixed). The
delegated `list-view` slot declares the same union and keeps the value raw, so
an author's per-column `label` / `width` still reach the list renderer.
85 changes: 81 additions & 4 deletions packages/plugin-view/src/ObjectView.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,7 @@ import {
} from '@object-ui/components';
import { Plus } from 'lucide-react';
import { useObjectTranslation, createSafeTranslation } from '@object-ui/i18n';
import { buildExpandFields, normalizeListViewSchema, mergeFilterNodes } from '@object-ui/core';
import { buildExpandFields, normalizeListViewSchema, mergeFilterNodes, columnIdentity } from '@object-ui/core';
import { SchemaRenderer as ImportedSchemaRenderer } from '@object-ui/react';
import { ViewSwitcher } from './ViewSwitcher';
import { deriveRecordSurface } from './recordSurface';
Expand DownExpand Up@@ -114,6 +114,40 @@ function pickFlatMapConfig(mapConfig: unknown): Record<string, unknown> {
return Object.fromEntries(FLAT_MAP_CONFIG_KEYS.filter((key) => key in source).map((key) => [key, source[key]]));
}

/**
* `table.columns` as a FIELD-NAME list — the shape the non-grid field slot
* declares (objectui#5269).
*
* `ObjectGridSchema.columns` is `string[] | ListColumn[]`, but the slot the
* non-grid branch forwards into is a names slot: both segments ahead of the
* `table` one declare `string[]` (`NamedListView.columns`, the `views` prop),
* and its consumers treat every entry as a field name — `ObjectKanban` indexes
* the record by it (`resolveKanbanCardFields` casts straight to `string[]`).
* Handing a `ListColumn[]` down raw would therefore arrive as a non-empty card
* field list naming nothing, which renders WORSE than the empty one this card
* fixes: `ObjectKanban` skips its `highlightFields` fallback whenever the list
* is non-empty. That is the objectui#5270 failure again — a value forwarded
* into a slot whose declared shape it does not have — and it is answered the
* same way, at the boundary.
*
* `columnIdentity` is the repo's single converged reader for "which field does
* this column entry name" (objectui#3104), and it is what `ObjectGrid` already
* applies to the SAME `table.columns` value on the grid path (its `$select`
* derivation) and what `ObjectTree` applies downstream. So this narrows a
* declared union to the branch this slot can hold; it does not widen the set
* of accepted spellings, and it keeps the two paths resolving one value the
* same way.
*
* `undefined` — never `[]` — when nothing resolves, so the `||` chain falls
* through to the deprecated `table.fields` instead of stopping on a truthy
* empty array.
*/
function tableColumnFieldNames(columns: unknown): string[] | undefined {
if (!Array.isArray(columns) || columns.length === 0) return undefined;
const names = columns.map(columnIdentity).filter((n): n is string => !!n);
return names.length > 0 ? names : undefined;
}

/**
* Record-create verb, shared with the runtime object pages: both surfaces
* resolve `console.objectView.new` ("New" / 新建) so the Studio grid toolbar
Expand DownExpand Up@@ -839,7 +873,39 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
const generateViewSchema = useCallback((viewType: string): any => {
const baseProps: Record<string, any> = {
objectName: schema.objectName,
fields: currentNamedViewConfig?.columns || activeView?.columns || schema.table?.fields,
// objectui#5269 — the `table` segment reads the CANONICAL key first.
//
// `ObjectGridSchema.columns` is the canonical spelling and `fields` is
// its `@deprecated` alias ("@deprecated Use columns instead"), and
// `ObjectViewSchema.table` is `Partial< Omit< ObjectGridSchema, … > >`,
// so `table: { columns: [...] }` is the shape the type recommends. It
// reached the grid path (which forwards `table.columns` into its own
// `columns` slot) and stopped here: this line read the deprecated half
// alone, so an author who wrote the canonical key on a non-grid view got
// an empty field list from a compile-clean, semantically correct schema.
// Same user-visible shape as objectui#5102, different mechanism — not a
// whitelist that knows only legacy spellings, but one that disagreed
// with itself between two rendering paths.
//
// Forwarding, not translation, exactly as objectui#5102 settled it: the
// value is handed on unchanged and the two segments ahead of `table`
// keep their precedence. Both spellings stay working; only the ORDER
// between them is stated, canonical first.
//
// Reach, measured rather than assumed: of the surfaces this `baseProps`
// feeds, `object-kanban` consumes it (via `cardFields`, below) and
// `object-tree` consumes it (as its own `fields`). `object-gallery` /
// `object-calendar` / `object-timeline` / `object-gantt` / `object-map`
// read NO field list off their schema at all, so the value is inert
// there — before this change and after it.
//
// The `table` segment arrives through `tableColumnFieldNames` because
// THIS slot is a names slot and `table.columns` is a union — see that
// function for why raw forwarding would regress the `ListColumn[]` half.
// The delegated `list-view` slot below declares the same union, so it
// takes the value raw; each site gets the shape its slot declares.
fields: currentNamedViewConfig?.columns || activeView?.columns
|| tableColumnFieldNames(schema.table?.columns) || schema.table?.fields,
className: 'h-full w-full',
showSearch: activeView?.showSearch ?? schema.showSearch ?? false,
showSort: activeView?.showSort ?? schema.showSort ?? false,
Expand DownExpand Up@@ -1018,7 +1084,10 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
default:
return null;
}
}, [schema.objectName, schema.table?.fields, currentNamedViewConfig, activeView]);
// `schema.table?.columns` joins the list with the read added for
// objectui#5269: a memo that reads a key but does not depend on it keeps
// serving the field list the author has already replaced.
}, [schema.objectName, schema.table?.columns, schema.table?.fields, currentNamedViewConfig, activeView]);

// Build grid schema (default content renderer)
//
Expand DownExpand Up@@ -1281,7 +1350,15 @@ export const ObjectView: React.FC<ObjectViewProps> = ({
// Spec-canonical key (#2890) — the view configs this reads from are
// already `columns`-keyed, so emitting `fields` here was a pure
// canonical→legacy downgrade.
columns: currentNamedViewConfig?.columns || activeView?.columns || schema.table?.fields,
//
// objectui#5269: the `table` segment reads the canonical key first
// here too. This slot is `list-view`'s `columns`, declared
// `string[] | ListColumn[]` — the same union `table.columns` carries
// — so the canonical value arrives in a slot already shaped to hold
// it, and `ListView` reads it (`schema.columns`, its whole column
// set). The deprecated `table.fields` stays a working alias.
columns: currentNamedViewConfig?.columns || activeView?.columns
|| schema.table?.columns || schema.table?.fields,
filter: mergedFilters,
sort: mergedSort,
// Propagate appearance/view-config properties for live preview
Expand Down
Loading
Loading