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
54 changes: 54 additions & 0 deletions .changeset/8653-listview-title-retired-rowactiondefs-pinned.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
'@object-ui/plugin-list': minor
---

`list-view`: retire the legacy `title` alias from the export-filename read, and pin the
`rowActionDefs` exemption at both of `ListView`'s read sites (objectui#8653, the
objectui#8327 family's `plugin-list` card).

**Two keys, two opposite exits — and the contract chose each one.**

`title` — **retired.** `ListView` resolved its export filename through
`schema.label || (schema as any).title`. `@objectstack/spec/ui`'s `ListViewSchema`
refuses `title` **by name** (`unrecognized_keys: ['title']`) while
`ObjectGridPropsSchema` **accepts** it; `packages/types` mirrors the platform contract
rather than ruling over it, so declaring `title` on `ListViewSchema` would have made
this repo accept what the platform save gate rejects. That asymmetry is also why
objectui#6639 could take the *declare* branch for `ObjectGridSchema.title` one package
over and this site could not. A parse-based census of `apps/ examples/ content/` and
`packages/` found **zero** `list-view` nodes authoring `title`, so the retirement costs
no author a filename. Over that same corpus the instrument reports **three**
`object-grid` nodes carrying the key: **two authored** ones, both in
`content/docs/api/schema-reference.md`, plus one that is not authored at all —
`packages/plugin-view/src/ObjectView.tsx` composes `title: schema.table?.title` onto a
grid node it builds, so it is a producer writing the key rather than an author
declaring it. `ObjectGrid`'s own `title` reads are untouched — they remain declared,
ruled and read.

**Behaviour change, deliberate.** A list view authoring only `title` (no `label`) no
longer contributes a view segment to the export filename; it exports as
`<objectLabel|objectName>-<timestamp>.<ext>`. Migration: write `label`, which is the
declared slot on both this repo's `ListViewSchema` and the platform's.

**Bug fixed in the same expression.** The `as any` was laundering a second defect:
`X || any` collapses the whole expression to `any`, so an inline locale-map `label`
(`{ en: 'Quarterly Review', zh: '季度复盘' }` — the shape `BaseSchema.label` has
published since objectui#4580's revised Q1) reached `sanitizeFileNameBase` **unresolved**
and exported as `account-[object Object]-….csv`. The read now goes through the spec's
own `resolveI18nLabel` against the display locale, the same resolver and argument order
`ObjectGrid` already used at its twin site.

`rowActionDefs` — **exempt, still read, and now pinned.** objectui#5091 ruled this
producer-derived key NON-AUTHOR SURFACE for `object-grid` on 2026-08-19. The exemption
extends to `ListView`: the platform refuses the key by name on **both** surfaces,
`@object-ui/types` declares it on neither mirror, and the producer is the same one
(`app-shell`'s `ObjectView` derives it from `objectDef.actions` filtered by
`locations.includes('list_item')`; `plugin-view`'s composes the same key onto a
`list-view` node). Both read sites now carry the ruling in a docblock, and a new pin
asserts — at runtime, never by source grep — that the defs still reach the child
`object-grid` node **and** that a field named only by a row action's `visible` CEL still
reaches `$select`. Without that second half, deleting the read would have returned rows
whose predicate operand was never selected: objectui#3501's fail-closed CEL fault
arriving with a success receipt.

No published type changed: neither key is declared on any face by this change.
76 changes: 75 additions & 1 deletion packages/plugin-list/src/ListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2080,6 +2080,29 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({

for (const f of collectPredicateFieldRefs(listViewPredicates({
conditionalFormatting: schema.conditionalFormatting as unknown[] | undefined,
/**
* NON-AUTHOR SURFACE, cast on purpose — objectui#5091's 2026-08-19
* ruling, extended to this reader by objectui#8653 item 2.
*
* `rowActionDefs` is PRODUCER-DERIVED, not authored: `app-shell`'s
* `ObjectView` builds it from `objectDef.actions` filtered by
* `locations.includes('list_item')` and writes it onto a
* `fullSchema: ListViewSchema`; `plugin-view`'s `ObjectView`
* composes the same key onto a `list-view` node. `@objectstack/spec/ui`
* REFUSES it by name on both the list and the grid surface, and
* `@object-ui/types` declares it on neither mirror — which is why
* the read is a cast and ⛔ why declaring it would publish a key
* the save gate rejects.
*
* ⛔ DO NOT delete this read to "tidy up" an unexplained cast. The
* defs carry `visible` / `disabled` CELs and `recordIdField`, and
* this is the harvest that puts their operands into `$select`.
* Dropping it returns a row whose predicate operand was never
* selected — objectui#3501's fail-closed CEL fault (`No such key`)
* arriving with a success receipt. Pinned by name in
* `__tests__/listViewNonAuthorKeys-8653.test.tsx` §4 SITE 2; the
* declared sibling `bulkActionDefs` is that pin's control.
*/
rowActionDefs: (schema as any).rowActionDefs,
bulkActionDefs: (schema as any).bulkActionDefs,
objectActions: (objectDef as any)?.actions,
Expand Down Expand Up @@ -2637,6 +2660,21 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
...(groupingConfig ? { grouping: groupingConfig } : {}),
...(rowColorConfig ? { rowColor: rowColorConfig } : {}),
...(schema.rowActions ? { rowActions: schema.rowActions } : {}),
/**
* The RELAY half of the same non-author surface documented at the
* `listViewPredicates` call above (objectui#5091, extended here by
* objectui#8653 item 2). Undeclared on `ListViewSchema` — hence the
* cast — but DECLARED one node down: `DataTableSchema.rowActionDefs`
* is what `object-grid` hands `RowActionMenu`.
*
* ⛔ DO NOT delete this read. `baseProps` above is an explicit
* picklist, so this line is the ONLY way the host's composed row
* actions reach the row menu; dropping it removes the menu with no
* type error, no lint finding and no test failure anywhere else in
* the tree. Pinned by name in
* `__tests__/listViewNonAuthorKeys-8653.test.tsx` §4 SITE 1, with
* the declared `bulkActionDefs` below as that pin's control.
*/
...((schema as any).rowActionDefs ? { rowActionDefs: (schema as any).rowActionDefs } : {}),
...(schema.bulkActions ? { batchActions: schema.bulkActions } : {}),
...((schema as any).bulkActionDefs ? { bulkActionDefs: (schema as any).bulkActionDefs } : {}),
Expand Down Expand Up @@ -3329,7 +3367,43 @@ export const ListView = React.forwardRef<ListViewHandle, ListViewProps>(({
prefix: exportConfig?.fileNamePrefix,
label: translatedLabel,
objectName: schema.objectName,
viewLabel: schema.label || (schema as any).title,
/**
* The view's own label, resolved against the display locale — `label` is
* `string | I18nLabel` (the spec's INLINE locale map) and `viewLabel` is
* `string`, so the map has to be resolved before it gets here. Same
* resolver, same argument order as `ObjectGrid`'s twin site.
*
* ⛔ NO legacy `title` arm. This read used to be
* `schema.label || (schema as any).title`, and objectui#8653 item 1
* retired the second operand — the objectui#7129 route, as taken for
* `DashboardComponentSchema.title` at objectui#7623 — on two
* measurements:
*
* - `@objectstack/spec/ui`'s `ListViewSchema` REFUSES `title` BY NAME
* (`unrecognized_keys: ['title']`) while `ObjectGridPropsSchema`
* ACCEPTS it. `packages/types` mirrors the spec rather than ruling
* over it, so declaring `title` on `ListViewSchema` would make this
* repo accept what the platform save gate rejects. That asymmetry is
* also why objectui#6639 could take the DECLARE branch one package
* over and this site could not.
* - a parse-based census of `apps/ examples/ content/ packages/` found
* ZERO `list-view` nodes authoring `title`, so no author loses a
* filename to the retirement. Over that same corpus the instrument
* reports THREE `object-grid` nodes carrying the key: TWO AUTHORED,
* both in `content/docs/api/schema-reference.md`, plus one that is
* not authored at all — `plugin-view`'s `ObjectView` composes
* `title: schema.table?.title` onto a grid node it builds, so that
* third hit is a producer writing the key, not an author declaring
* it. Say "authored" or the two numbers disagree.
*
* ⭐ The `as any` was also laundering a defect of its own: `X || any`
* collapses the whole expression to `any`, so a locale-map `label`
* reached `sanitizeFileNameBase` UNRESOLVED and exported as
* `[object Object]`. Both halves are pinned in
* `__tests__/listViewNonAuthorKeys-8653.test.tsx` §3, which reads the
* download anchor rather than re-implementing the filename.
*/
viewLabel: resolveInlineI18nLabel(schema.label, displayLocale),
});

// Server-streamed path: csv / xlsx / json via dataSource.exportDownload.
Expand Down
Loading
Loading