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
74 changes: 74 additions & 0 deletions .changeset/7997-detail-view-related-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
'@object-ui/types': minor
'@object-ui/plugin-detail': minor
---

`DetailViewSchema.related` is retired — author a `record:related_list` block
(objectui#7997, ADR-0049 enforce-or-remove; maintainer ruling 2026-09-10).

**Breaking, and graded `minor` by this repo's convention** — a `major` would drag
the whole 39-package fixed group off `@objectstack`'s cadence. A `detail-view`
node authoring `related` used to parse **green** and render a Related section; it
now reds at that key on both faces, and the renderer draws nothing from it.

**What retired is a DOOR, not the capability.** `record:related_list` is
unchanged and is now the only **declared, protocol-governed** entry
(`@objectstack/spec` `RecordRelatedListProps`); it has always rendered through
the same `RelatedList` component the retired array fed, so nothing about the
rendered result is lost. ⚠️ Not the only entry full stop — `plugin-detail`
still registers a bare `related-list` node against the same component with
untyped `columns`, and that registration is out of this card's scope and
untouched.

| before, on a `detail-view` node | after |
| --- | --- |
| `related: [{ title, type, api, columns: [{ accessorKey, header }] }]` | a `record:related_list` node: `{ objectName, relationshipField, title, columns: ['name', 'email'] }` |

⚠️ `columns` on the surviving entry is an array of **field-name strings**, which
is what the protocol declares. The header is derived from the related object's
field `label` and the cell from the field's type, so a label rename reaches the
list for free — the hand-spelled `{ accessorKey, header }` form froze both.
`relationshipField` names the field on the related object that points back at
this record, and replaces the retired form's `api` endpoint.

**Why it retired.** `@objectstack/spec` declares no `DetailView` schema at all —
every `DetailView` occurrence in `packages/spec/src` is prose about this repo's
own `RecordDetailView.tsx` — so this array mirrored no protocol schema and
drifted freely: it declared `columns` as `TableColumn[]` while the renderer it
fed also accepted bare field names, `{ field, label }` and legacy
`{ name, label }` spellings. The axis that carried the ruling was measured **zero
pull**: no application code authored the member, both internal producers of a
`detail-view` node (`RecordDetailDrawer`, `renderers/record-details.tsx`)
synthesize it without `related`, and the only in-tree authorings carrying real
columns were two documents — both rewritten here.

**A named refusal, not a deletion.** `BaseSchemaCore` ends `.passthrough()` and
the TypeScript `BaseSchema` closes with an any-valued index signature, so a
*dropped* member key is kept, not refused — deleting the declaration would have
left the silent accept exactly as it was. The key stays declared and unwritable:
`retirementTombstone()` on the Zod face, `?: never` on the TypeScript face, one
guidance string feeding both the parse-time message and `.describe()`. A pin
authors an undeclared sibling key through the same parse and watches it survive,
so "a bare delete would not have refused it" is a reading rather than a claim.

**What moved in `@object-ui/plugin-detail`.** `DetailView` no longer reads
`schema.related`: the flat Related section, the `autoTabs` Related tab, its
trigger and its count badge are gone, and `related` is off the `detail-view`
registry's `inputs` and `defaultProps`. `RelatedList` itself, the
`related-list` / `related_list` registrations and `record:related_list` are
untouched.

**Documentation.** `packages/plugin-detail/README.md` and
`content/docs/api/schema-reference.md` stop teaching the retired array and gain a
migration block each.

⚠️ The docs page had been teaching `{ name, label }` columns, and the two faces
disagreed about that shape: the retired **TypeScript** declaration never
admitted it (`TableColumn` requires `header` **and** `accessorKey`), while the
retired **zod mirror** did — it spelled the member `z.array(z.any())` — so the
JSON document that page taught parsed green and rendered. The page was wrong for
a **typed** author and right for a **JSON** author, which is a sharper defect
than a single wrong example: the two authoring faces of one member disagreed
about what a column is. Retiring the member closes that split at the source, and
the page now teaches `record:related_list`, whose `columns` is
`z.array(z.string())` on both faces.
49 changes: 35 additions & 14 deletions content/docs/api/schema-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1083,7 +1083,7 @@ objectui#5667: nothing read them on the authored-node path.

### DetailViewSchema

An enhanced detail view for a single record with sections, tabs, related records, and navigation.
An enhanced detail view for a single record with sections, tabs and navigation.

```json
{
Expand Down Expand Up @@ -1122,18 +1122,6 @@ An enhanced detail view for a single record with sections, tabs, related records
"content": { "type": "timeline", "events": [] }
}
],
"related": [
{
"title": "Recent Orders",
"type": "table",
"api": "/api/contacts/contact-123/orders",
"columns": [
{ "name": "id", "label": "Order #" },
{ "name": "total", "label": "Total" },
{ "name": "status", "label": "Status" }
]
}
],
"actions": [
{ "type": "action", "label": "Send Email", "icon": "Mail", "level": "primary" }
]
Expand All @@ -1152,13 +1140,46 @@ An enhanced detail view for a single record with sections, tabs, related records
| `sections` | `DetailViewSection[]` | Field groups with `title`, `icon`, `fields`, `collapsible`. |
| `fields` | `DetailViewField[]` | Direct fields (without sections). |
| `tabs` | `DetailViewTab[]` | Tabbed content with `key`, `label`, `icon`, `badge`, `content`. |
| `related` | `array` | Related record sections with `title`, `type`, `api`, `columns`. |
| `related` | ⛔ **RETIRED** | Retired in objectui#7997 (ADR-0049 enforce-or-remove). Authoring it is now refused by name on both faces. Author a `record:related_list` block instead — see below. |
| `actions` | `ActionSchema[]` | Available actions. |
| `showBack` / `backUrl` | `boolean` / `string` | Back navigation. |
| `showEdit` / `editUrl` | `boolean` / `string` | Edit navigation. |
| `showDelete` / `deleteConfirmation` | `boolean` / `string` | Delete with confirmation message. |
| `header` / `footer` | `SchemaNode` | Custom header/footer content. |

> **Retired: `related`** (objectui#7997, ADR-0049 enforce-or-remove).
> Author a `record:related_list` block instead.
>
> Until objectui#7997 this block carried its own `related` array, and this page
> taught it with `{ "name": ..., "label": ... }` columns. That array is retired
> under ADR-0049 enforce-or-remove: it was a second entry to a capability
> `@objectstack/spec` already governs, it mirrored no protocol schema, and it
> drifted from the renderer it fed. Authoring it is now **refused by name** on
> both the TypeScript and the JSON face — it is not silently ignored.
>
> Related lists have one declared entry now, and it renders through the same component:
>
> ```json
> {
> "type": "record:related_list",
> "objectName": "order",
> "relationshipField": "contact_id",
> "title": "Recent Orders",
> "columns": ["id", "total", "status"]
> }
> ```
>
> ⚠️ `columns` here is an array of **field-name strings**, not column objects —
> that is what the protocol declares (`RecordRelatedListProps.columns`), and the
> header and cell formatting are derived from the related object's schema, so a
> field label rename reaches the list for free. `relationshipField` names the
> field on the RELATED object that points back at this record, and replaces the
> retired form's `api` endpoint.
>
> ⚠️ The block reads the parent record from the record page's `RecordContext`,
> so author it on a record page. Placed anywhere it cannot resolve a parent id
> it scopes to nothing and renders an empty list.

**Related:** [DetailSchema](#detailschema), [ObjectViewSchema](#objectviewschema)

---
Expand Down
5 changes: 4 additions & 1 deletion packages/app-shell/src/utils/deriveRelatedLists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
* This helper scans every object for fields whose `reference`/`reference_to`
* points back at the parent object and produces one related-list descriptor per
* eligible FK. The detail page (`RecordDetailView`) feeds these into the
* `record:related_list` renderers (and the legacy `DetailView.related`).
* `record:related_list` renderers. (It also fed `DetailView.related`, which is
* RETIRED as of objectui#7997 — that entry is a `?: never` tombstone on both
* faces now, and this helper's output reaches the page only as
* `record:related_list` nodes.)
*
* Rules (kept in lockstep with the relationship-level `relatedList` spec flag):
* - Owned children (`master_detail`) and `lookup` children are SHOWN by
Expand Down
79 changes: 51 additions & 28 deletions packages/plugin-detail/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ DetailView plugin for ObjectUI - A comprehensive detail page component with fiel
- **Field Grouping/Sections**: Organize fields into logical sections with titles
- **Collapsible Sections**: Make sections collapsible to save space
- **Tab Navigation**: Organize content into tabs for better UX
- **Related Lists**: Display related records (e.g., contacts for an account)
- **Related Lists**: Display related records (e.g., contacts for an account) —
authored as a `record:related_list` block, ⛔ not as a `detail-view` `related`
array, which is retired (objectui#7997; see **RelatedList** below)
- **Action Buttons**: Edit, Delete, and custom action buttons
- **Readonly/Edit Mode**: Toggle between view and edit modes
- **Back Navigation**: Built-in back button with customizable behavior
Expand Down Expand Up @@ -128,7 +130,7 @@ const accountDetail = <DetailView
/>;
```

### With Tabs and Related Lists
### With Tabs

```tsx
import { DetailView } from '@object-ui/plugin-detail';
Expand Down Expand Up @@ -170,30 +172,6 @@ const accountDetail = <DetailView
},
},
],
related: [
{
title: 'Contacts',
type: 'table',
api: '/api/accounts/12345/contacts',
columns: [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
{ accessorKey: 'phone', header: 'Phone' },
{ accessorKey: 'title', header: 'Title' },
],
},
{
title: 'Opportunities',
type: 'table',
api: '/api/accounts/12345/opportunities',
columns: [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'amount', header: 'Amount' },
{ accessorKey: 'stage', header: 'Stage' },
{ accessorKey: 'close_date', header: 'Close Date' },
],
},
],
showEdit: true,
showDelete: true,
}}
Expand Down Expand Up @@ -225,11 +203,10 @@ const schema: DetailViewSchema = {
summaryFields: ['industry', 'website'],
layout: 'vertical',
columns: 2,
// See the examples above for the shapes these four carry.
// See the examples above for the shapes these three carry.
sections: [],
fields: [],
tabs: [],
related: [],
actions: [],
showBack: true,
backUrl: '/accounts',
Expand Down Expand Up @@ -278,6 +255,52 @@ Tab navigation for organizing content into different views.

Displays related records in list, grid, or table format.

> **Retired: `DetailViewSchema.related`** (objectui#7997, ADR-0049
> enforce-or-remove). Author a `record:related_list` block instead.
>
> Until objectui#7997 a `detail-view` node could carry its own `related` array,
> and this README taught it. That array is retired under ADR-0049
> enforce-or-remove (maintainer ruling 2026-09-10). It was a second entry to a
> capability `@objectstack/spec` already governs — the protocol declares no
> `DetailView` schema at all — so it mirrored nothing and drifted from the
> renderer it fed: it typed `columns` as `TableColumn` objects while the
> renderer also accepted bare field names. Authoring it is now **refused by
> name** on both the TypeScript and the JSON face, ⛔ not silently ignored.
>
> Nothing about the rendered result changed: both entries always went through
> the `RelatedList` component documented here. Only the second door closed.
>
> ```tsx
> import { SchemaRenderer } from '@object-ui/react';
>
> const contacts = (
> <SchemaRenderer
> schema={{
> type: 'record:related_list',
> objectName: 'contact',
> relationshipField: 'account_id',
> title: 'Contacts',
> columns: ['name', 'email', 'phone'],
> }}
> />
> );
> ```
>
> ⚠️ `columns` is an array of **field-name strings**, which is what the protocol
> declares (`RecordRelatedListProps.columns`). The header comes from the related
> object's field `label` and the cell from the field's type, so a label rename
> reaches the list for free — the hand-spelled `{ accessorKey, header }` form the
> retired array taught froze both. `relationshipField` names the field on the
> related object pointing back at this record, and replaces the retired form's
> `api` endpoint.
>
> ⚠️ The block reads the parent record from the record page's `RecordContext`,
> so author it on a record page. Placed anywhere it cannot resolve a parent id
> it scopes to nothing and renders an empty list — which is also what the
> retired `related` form did whenever it declared `api` without a
> `referenceField`.


Related lists are **paged by default**: the `record:related_list` renderer
applies the spec default `limit` of **5** when the node doesn't declare one
(`@objectstack/spec` `RecordRelatedListProps.limit`, "Number of records to
Expand Down
Loading
Loading