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
57 changes: 57 additions & 0 deletions .changeset/react-list-view-binding-aliases-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
"@objectstack/spec": minor
"@objectstack/lint": minor
"@objectstack/example-showcase": patch
---

feat(spec)!: `<ListView objectName>` / `<ListView viewType>` are retired from the react-tier component contract — `data={{ provider: 'object', object }}` / `type` are the only spellings (#14791)

<!-- adr-0087: registered ui-react-list-view-binding-aliases-retired -->

**BREAKING** — an accept-set narrowing on a published contract. The `REACT_BLOCKS`
ListView entry no longer publishes the `objectName` and `viewType` overlay props that
#11284 had deprecated in favour of ListViewSchema's own `data` / `type`: the generated
contract (`skills/objectstack-ui/references/react-blocks.md`) drops both rows, and
`@objectstack/lint`'s `validate-react-page-props` now REFUSES either spelling on a
`kind:'react'` page with a new `react-prop-retired` error that carries the fix, where it
used to warn and accept. Shipped as `minor` under the repo's launch-window convention for
breaking changes; the hand-migration prescription is registered under protocol major 18
(`ui-react-list-view-binding-aliases-retired`). Maintainer ruling on #14791 (2026-09-07,
director seat summon #17, decision batch #1, option B — retire now, no deprecation window,
「同意」).

## FROM → TO

| you wrote | write instead |
|:--|:--|
| `<ListView objectName="account" … />` | `<ListView data={{ provider: 'object', object: 'account' }} … />` |
| `<ListView viewType="kanban" … />` | `<ListView type="kanban" … />` |
| `<ListView … />` with no binding at all | add `data={{ provider: 'object', object: '…' }}` — it is the required binding on a react page |

One-line fix: on every `<ListView>` in react page source replace `objectName="X"` with
`data={{ provider: 'object', object: 'X' }}` and `viewType="K"` with `type="K"`, then re-run
`objectstack validate` — a leftover alias is reported as `react-prop-retired` with this
same prescription, and a list with no data source as `react-prop-missing-required`.

## Why now, and why no window

The contract deprecated both aliases (#11284) while objectui's ListView still read only
`objectName`, so a page written the canonical way validated green and rendered an empty
list. That consumer half has landed and ships in the console this repo pins
(`normalizeListViewSchema` at `a472b071` folds `data.provider === 'object'` onto the key
the renderer reads and takes the author's `type` for the view kind), so both spellings
render today — and the maintainer's standing rule for a spelling with zero external
authors is to retire it at once rather than keep two vocabularies alive.

## What else moved

- `REACT_RETIRED_OVERLAY_PROPS` is a new export of `@objectstack/spec/ui`: the tombstone
ledger (prop → replacement + one-line fix) the lint quotes, the react-tier twin of a
metadata schema's `retiredKey()`.
- `data` is restated on the ListView overlay as its **required** binding (ledgered in
`REACT_OVERLAY_SHADOWS`), so the generated contract marks it ✓ and a `<ListView>` with no
data source is refused — the check the required `objectName` used to carry.
- `REACT_RECORD_BLOCK_ALTERNATIVES['record:related_list']` prescribes the canonical spelling.
- The showcase pages (`crm-workbench`, `renewals-pipeline`, `task-desk`), the published
`objectstack-ui` skill and the react-pages / validating-metadata guides write the
canonical spelling; `@objectstack/lint` exports `REACT_PROP_RETIRED`.
14 changes: 7 additions & 7 deletions content/docs/deployment/validating-metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,8 @@ name has the same failure: the block skips the name and renders one column, one
filter chip, or one form field short.

```jsx
<ListView objectName="crm_account" columns={['name', 'revenue']} />
// ↑ not a crm_account field → warning
<ListView data={{ provider: 'object', object: 'crm_account' }} columns={['name', 'revenue']} />
// ↑ not a crm_account field → warning
```

Checked on every injected block: `<ListView>`'s
Expand All @@ -336,9 +336,9 @@ They are withdrawn from the react tier, and using one is an **error**
(`react-block-needs-record-context`) — by tag, and through
`<Block type="record:…">` alike. On a react page the parent record is ordinary
React state, so bind it with a block that reads its own props: `<ListView
objectName="<child>" filters={['<lookup>', '=', parentId]}>` for a related list,
`<ObjectForm mode="view" recordId={…}>` for a field panel. To use the family
itself, author the page as `type:'record'`.
data={{ provider: 'object', object: '<child>' }} filters={['<lookup>', '=',
parentId]}>` for a related list, `<ObjectForm mode="view" recordId={…}>` for a
field panel. To use the family itself, author the page as `type:'record'`.

On a **record page**, where these blocks do work, §5 checks their field-bearing
props, and `<RecordRelatedList objectName>` is the **related (child)** object
Expand All @@ -349,8 +349,8 @@ there is the mistake that check was extended to catch.
A **filter position** is the exception that gates:

```jsx
<ListView objectName="crm_account" filters={['revenu', '=', stage]} />
// ↑ error, not a warning
<ListView data={{ provider: 'object', object: 'crm_account' }} filters={['revenu', '=', stage]} />
// ↑ error, not a warning
```

An unknown column in a predicate is not a skipped column — the predicate can
Expand Down
15 changes: 8 additions & 7 deletions content/docs/ui/react-pages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,14 @@ An injected block folds its JSX props into the block's schema, so you write flat
rather than a nested `schema` object:

```jsx
<ListView objectName="showcase_account" columns={['name', 'status']} navigation={{ mode: 'none' }} />
<ListView data={{ provider: 'object', object: 'showcase_account' }} columns={['name', 'status']} navigation={{ mode: 'none' }} />
```

Function props are passed through as real callbacks — that is how one block drives
another:

```jsx
<ListView objectName="showcase_account" onRowClick={(record) => setSelected(record.id)} />
<ListView data={{ provider: 'object', object: 'showcase_account' }} onRowClick={(record) => setSelected(record.id)} />
```

One collision is worth knowing. `type` is the SDUI envelope's component discriminator
Expand Down Expand Up @@ -155,7 +155,7 @@ Any registered component, including ones outside the curated contract:
```

A kanban, calendar, gantt, timeline or map of an object is also reachable without the
escape hatch — `<ListView viewType="kanban" …>` selects the visualization directly.
escape hatch — `<ListView type="kanban" …>` selects the visualization directly.

`Block` is **not** a way back to the `record:*` family; see
[below](#record-blocks-not-in-react).
Expand Down Expand Up @@ -358,8 +358,9 @@ by tag and through `<Block type="record:…">` alike:
The error names the replacement for the block you reached for, so read the hint rather
than a table here. In general: on a react page the parent record is React state, so bind
it with a block that reads its **own** props — `<ObjectForm mode="view" recordId={…}>`
for a field panel, `<ListView objectName="<child>" filters={['<lookup>', '=', parentId]}>`
for a related list — or read the record with `useAdapter().findOne` and lay it out in JSX.
for a field panel, `<ListView data={{ provider: 'object', object: '<child>' }}
filters={['<lookup>', '=', parentId]}>` for a related list — or read the record with
`useAdapter().findOne` and lay it out in JSX.
To use the family itself, author the page as `type:'record'` instead.

## How you check your work
Expand Down Expand Up @@ -434,7 +435,7 @@ function Page() {
<div style={{ display: 'flex', height: '100%', gap: 16, padding: 16 }}>
<div style={{ width: '50%' }}>
<ListView
objectName="showcase_account"
data={{ provider: 'object', object: 'showcase_account' }}
columns={['name', 'status']}
navigation={{ mode: 'none' }}
onRowClick={(record) => { setSel(record.id); setEditing(false); }}
Expand All @@ -457,7 +458,7 @@ function Page() {
yAxis={[{ field: 'total' }]}
title="Invoice value by status"
/>
<ListView objectName="showcase_invoice" filters={['account', '=', sel]} columns={['name', 'status', 'total']} navigation={{ mode: 'none' }} />
<ListView data={{ provider: 'object', object: 'showcase_invoice' }} filters={['account', '=', sel]} columns={['name', 'status', 'total']} navigation={{ mode: 'none' }} />
<button onClick={() => setEditing(true)} style={{ borderRadius: 'var(--radius)', border: '1px solid hsl(var(--border))', background: 'transparent', color: 'hsl(var(--foreground))', padding: '6px 12px', cursor: 'pointer' }}>
Edit account
</button>
Expand Down
2 changes: 1 addition & 1 deletion examples/app-showcase/src/ui/pages/crm-workbench.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function Page() {
<div style={{ display: 'grid', gridTemplateColumns: '3fr 2fr', gap: 24, alignItems: 'start' }}>
<section style={{ ...card, padding: 8 }}>
<ListView key={reloadKey} objectName="showcase_project"
<ListView key={reloadKey} data={{ provider: 'object', object: 'showcase_project' }}
fields={['name', 'status', 'health', 'budget', 'owner']}
navigation={{ mode: 'none' }} onRowClick={onRowClick} />
</section>
Expand Down
6 changes: 3 additions & 3 deletions examples/app-showcase/src/ui/pages/renewals-pipeline.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ function Page() {
))}
</div>
<ListView
objectName="showcase_account"
viewType="grid"
data={{ provider: 'object', object: 'showcase_account' }}
type="grid"
filters={['status', '=', stage]}
columns={['name', 'status']}
searchableFields={['name']}
Expand Down Expand Up @@ -166,7 +166,7 @@ function Page() {
<ObjectChart objectName="showcase_invoice" type="bar" aggregate={{ field: 'total', function: 'sum', groupBy: 'status' }} xAxis={{ field: 'status' }} yAxis={[{ field: 'total', format: '$0,0' }]} series={[{ name: 'total', label: 'Invoice value' }]} title="Invoice value by status" showLegend={true} />

<h3 style={{ margin: 0, fontSize: 14, fontWeight: 600, color: 'hsl(var(--foreground))' }}>Invoices</h3>
<ListView objectName="showcase_invoice" filters={['account', '=', sel]} columns={['name', 'status', 'total']} navigation={{ mode: 'none' }} />
<ListView data={{ provider: 'object', object: 'showcase_invoice' }} filters={['account', '=', sel]} columns={['name', 'status', 'total']} navigation={{ mode: 'none' }} />

{editing ? (
<ObjectForm objectName="showcase_account" mode="edit" recordId={sel}
Expand Down
2 changes: 1 addition & 1 deletion examples/app-showcase/src/ui/pages/task-desk.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function Page() {
</button>
</header>
<ListView key={reload} objectName="showcase_task"
<ListView key={reload} data={{ provider: 'object', object: 'showcase_task' }}
fields={['title', 'assignee', 'status', 'priority']}
navigation={{ mode: 'none' }} onRowClick={(r) => setEditId(r.id)} />
Expand Down
1 change: 1 addition & 0 deletions packages/lint/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ export {
REACT_BLOCK_NEEDS_RECORD_CONTEXT,
REACT_PAGE_SOURCE_UNPARSEABLE,
REACT_PROP_DEPRECATED,
REACT_PROP_RETIRED,
} from './validate-react-page-props.js';
export type { ReactPropFinding, ReactPropSeverity } from './validate-react-page-props.js';

Expand Down
Loading
Loading