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
32 changes: 32 additions & 0 deletions .changeset/27053-page-slots-tabs-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
'@objectstack/spec': minor
'@objectstack/platform-objects': minor
'@objectstack/plugin-approvals': minor
'@object-ui/types': minor
'@object-ui/plugin-detail': minor
'@object-ui/app-shell': minor
'@objectstack/skills': minor
---

feat(spec,ui)!: the record page's `slots.tabs` is retired — a slotted page fills six slots, and a page that needs its own tab strip is `kind: 'full'`

Clause-②: no (narrowing)

Metadata-protocol: narrowed (`page.slots.tabs` is a tombstone that refuses every value; `slots.related` / `slots.relatedLists` are refused by name instead of being read as `tabs`)

<!-- adr-0087: registered page-slots-tabs-removed, page-slots-tabs-retired -->

**BREAKING** accept-set narrowing on the record page (`@objectstack/spec`), with the console following it in the same release.

The record page composes its tabs and columns from the object's roles and relationship flags. An authored `tabs` slot replaced that whole strip, so a page that used it lost every synthesized related list and its wide-screen columns. The slot is gone; the six live slots (`header`, `actions`, `alerts`, `highlights`, `details`, `discussion`) are unchanged.

- **FROM → TO.** `slots: { tabs: … }` on a `kind: 'slotted'` page → delete the key. To change which related lists appear or their order, set `relatedList` / `relatedList: 'primary'` on the child's reference field; to replace the Details body, author `slots.details`; a page that needs its own strip is `kind: 'full'` and authors its header, highlights, `page:tabs` strip and `record:discussion` in `regions`, drawn as authored.
- **The one-line fix.** `os migrate meta --from 17` lists the edit for existing sources, and `--write` applies it. A stored `sys_metadata` page is converted when it is read (the `page-slots-tabs-removed` conversion): the slot is dropped, and a page whose only slot it was becomes an empty slotted page, which renders the synthesized default. Whether such a page should become `kind: 'full'` is a judgement the conversion cannot make; the upgrade guide entry `page-slots-tabs-retired` lists it.
- **What the parse says.** `slots.tabs` is refused with its prescription ("`page.slots.tabs` was removed in @objectstack/spec 18.3.0 (ADR-0085) — …") at `defineStack` as `STACK_SCHEMA_INVALID` / 422, and at the metadata save door. `slots.related` and `slots.relatedLists` are refused by name with the same remedy. TypeScript rejects `tabs` in a `definePage` slot map.
- **The pair check.** `checkPageSlotPair` (the `details` + `tabs` refusal) is removed from `@objectstack/spec/ui` with the slot it paired; the `page-slots-details-beside-tabs-refused` upgrade entry is folded into `page-slots-tabs-retired`.

The four in-repo pages that authored `slots.tabs` are rebuilt as `kind: 'full'` pages that render the same nodes in the same order: `sys_user_detail` and `sys_organization_detail` (`@objectstack/platform-objects`), `sys_approval_request_detail` (`@objectstack/plugin-approvals`) and the showcase `showcase_project_detail`.

The console (`@object-ui/*`): `buildDefaultPageSchema` composes from the six slots only, and `RecordDetailView` lays every slotted page out by width (three columns from 1280 px), since none can opt out with an authored strip any more. `@object-ui/types` drops the pair check from its page schemas and types `PageSlotMap.tabs` as `never`. The Studio slot canvas lists six slots.

`@objectstack/skills`: the UI skill says a slotted page has no `tabs` slot and how to shape the strip instead.
12 changes: 12 additions & 0 deletions content/docs/references/ui/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,18 @@ View filter rule
| **repeatTableHeaders** | `boolean` | optional | Page-break hint: repeat a table's column headings at the top of every sheet the table runs onto (`thead { display: table-header-group }`). On when omitted. Applied by the console's browser print rendering. |
| **avoidBreakInside** | `boolean` | optional | Page-break hint: keep each block whole on one sheet when it fits, moving it to the next sheet instead of splitting it (`break-inside: avoid` on every block). Off when omitted. Applied by the console's browser print rendering. |

### Nested Shape: `Page.slots`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **header** | `{ type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record<string, string>; properties?: Record<string, any>; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record<string, string>; properties?: Record<string, any>; … }[]` | optional | |
| **actions** | `{ type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record<string, string>; properties?: Record<string, any>; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record<string, string>; properties?: Record<string, any>; … }[]` | optional | |
| **alerts** | `{ type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record<string, string>; properties?: Record<string, any>; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record<string, string>; properties?: Record<string, any>; … }[]` | optional | |
| **highlights** | `{ type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record<string, string>; properties?: Record<string, any>; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record<string, string>; properties?: Record<string, any>; … }[]` | optional | |
| **details** | `{ type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record<string, string>; properties?: Record<string, any>; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record<string, string>; properties?: Record<string, any>; … }[]` | optional | |
| **tabs** | `never` | optional | [REMOVED] `page.slots.tabs` was removed in @objectstack/spec 18.3.0 (ADR-0085) — an authored strip replaced the whole synthesized one, so the related lists, approvals, attachments and history the platform composes from the object were dropped unless re-added by hand, and the page stayed tabbed at every width instead of taking the record page's columns. Delete the key. The record page composes its tabs and columns from the object's roles and relationship flags: to change which related lists appear or their order, set `relatedList` / `relatedList: 'primary'` on the child's reference field; to replace the Details body, author `slots.details`; a page that needs its own strip is `kind: 'full'`, drawn as authored. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
| **discussion** | `{ type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record<string, string>; properties?: Record<string, any>; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record<string, string>; properties?: Record<string, any>; … }[]` | optional | |


---

Expand Down
9 changes: 5 additions & 4 deletions content/docs/ui/pages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -207,13 +207,14 @@ synthesizer fills in the rest. A slotted page authors `slots`, not `regions`:
| `alerts` | The alert banners above the body |
| `highlights` | The highlights strip |
| `details` | The body of the Details tab; the other tabs stay synthesized |
| `tabs` | The whole tab strip, to add, remove or reorder tabs |
| `discussion` | The discussion panel |

Each slot takes one component or an array of them, and replaces its part whole: there is no
deep merge. `details` and `tabs` are refused together, because the Details tab lives inside
the strip `tabs` replaces; to replace both, put `record:details` in the first `tabs` item
and author no `details` slot. A slotted `tabs` strip stays a strip at every width.
deep merge. There is no `tabs` slot: the tab strip is composed from the object's fields and
relationship flags. To change which related lists appear or their order, set `relatedList` /
`relatedList: 'primary'` on the child's reference field; a page that needs its own strip is
`kind: 'full'`, drawn as authored. `slots.tabs`, `slots.related` and `slots.relatedLists` are
refused by name, and `os migrate meta --from 17` drops a stored `slots.tabs`.

{/* os:check */}
```typescript
Expand Down
6 changes: 6 additions & 0 deletions docs/adr/0133-org-management-open-basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,12 @@ resolved here.
(`packages/plugins/plugin-auth/src/auth-plugin.ts#pages`). That file was
already in the tree at `77781151d`, the very commit cited above.

> **Amended (2026-10-28) — the mechanism moves from slotted to full; the
> strip does not.** The `tabs` slot retired (#27053), so
> `SysOrganizationDetailPage` is now a `kind: 'full'` page that authors the
> same Members, Invitations, Teams strip in its own `page:tabs` node, with
> the header, highlights and discussion the slotted page rendered.

⭐ **What the measurement missed, stated so it transfers.** The search was
exhaustive over the wrong space: scoped to `src/identity/` and to the
`relatedList` key, while the declaration lives one directory over, in
Expand Down
7 changes: 4 additions & 3 deletions docs/qa/platform-checklist/areas/identity-auth.json
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@
"title": "Setup Organization page resolves the active org and drives member/invitation/team management through the better-auth org endpoints — non-admins refused server-side",
"since": "v17",
"status": "active",
"revision": 5,
"revision": 6,
"priority": "P1",
"surface": "mixed",
"personas": ["tenant admin / org owner", "a target org member", "a non-admin org member (forger)"],
Expand Down Expand Up @@ -921,7 +921,7 @@
"traps": ["wrong-persona", "dispatcher-vs-hono-route", "hydration-race"],
"source": [
"packages/platform-objects/src/apps/setup-nav.contributions.ts#nav_organization (nav_organization recordId {current_org_id}, cloud ADR-0081 D3; Teams/Invitations always mounted per cloud ADR-0081 D1)",
"packages/platform-objects/src/pages/sys-organization.page.ts#SysOrganizationDetailPage (WHERE THE THREE TABS COME FROM: a kind: 'slotted' record page whose slots.tabs override replaces the synthesized tab strip outright, giving exactly Members / Invitations / Teams with Members at index 0. Handed to the runtime by plugin-auth's pages: [...]. NOT the Field.relatedList: 'primary' prominence key — no identity object declares that key, and it would not move this page while the tabs slot is present. A grader who finds a Details + stacked Related strip instead is looking at the synthesized default, i.e. this page failed to load)",
"packages/platform-objects/src/pages/sys-organization.page.ts#SysOrganizationDetailPage (WHERE THE THREE TABS COME FROM: a kind: 'full' record page that authors its own page:tabs strip, drawn as authored with no synthesized strip, giving exactly Members / Invitations / Teams with Members at index 0 (a kind: 'slotted' page whose slots.tabs override replaced the synthesized strip until that slot retired, #27053). Handed to the runtime by plugin-auth's pages: [...]. NOT the Field.relatedList: 'primary' prominence key — no identity object declares that key, and it would not move this full page. A grader who finds a Details + stacked Related strip instead is looking at the synthesized default, i.e. this page failed to load)",
"packages/plugins/plugin-auth/src/auth-route-ledger.ts (organization family: update-member-role, remove-member, update, create-team, add-team-member, list-members/teams/invitations, get-active-member, get-full-organization)",
"packages/spec/src/identity/membership-role.ts#BUILTIN_MEMBERSHIP_ROLES (BUILTIN_MEMBERSHIP_ROLES / BUILTIN_MEMBERSHIP_ROLE_OPTIONS — THE role vocabulary: owner/admin/delegated_admin/member, ADR-0108; 'nothing widens these at boot any more')",
"docs/adr/0108-membership-grade-is-not-a-capability-channel.md (why the list is closed: a grade decides what you can REACH, never a bundle of what you may do)",
Expand All @@ -939,7 +939,8 @@
"date": "2026-10-19",
"change": "steps[5] and acceptance[3].clause corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24538, identity-auth.org-membership-team-management; wave anchor #24579 §3): after a rename the step expected the nav label to follow, but nav_organization's label is the static 'Organization' (setup-nav.contributions.ts); what follows is the organization record page's title, which the run observed. Not a loosening: the persisted name and the page title must both show the rename",
"ref": "#24632"
}
},
{ "revision": 6, "date": "2026-10-28", "change": "PROVENANCE, no clause change: the record page's tabs slot retired (#27053), so SysOrganizationDetailPage moved from kind: 'slotted' with a slots.tabs override to kind: 'full', rebuilt as the header, highlights, the same Members / Invitations / Teams strip and the discussion the slotted page rendered. source[1] now names the full page; the tab order, steps, acceptance clauses, oracles and negatives are unchanged (ADR-0133 carries the matching amendment)", "ref": "#27053" }
]
},
{
Expand Down
Loading
Loading