From 81032b7ee2a04c7984800af5be2c312eb2a99c54 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Sun, 5 Jul 2026 01:31:32 +0800 Subject: [PATCH] docs(spec,skills,adr): correct related-list docs for relatedList 'primary' (#2579 follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - field.zod.ts: fix the `relatedList` JSDoc + describe — non-primary lists stack under a single shared "Related" tab; only 'primary' earns its own tab. Drops the stale "count-aware" wording that never shipped (we chose rule Z). - skills/objectstack-ui: the single "Related" tab is no longer the ONLY layout — document `relatedList: 'primary'` as the zero-page way to promote a child table to its own tab; columns auto-derive from highlightFields. - skills/objectstack-data: add the 'primary' tri-state; note columns are optional (derive from highlightFields), ONE related list PER FK (not one per child), and self-referential hierarchies. - docs/adr/0035: update the read-side-mirror section for the tri-state, optional columns, and one-per-FK derivation. Docs/description only; no behavior change. Co-Authored-By: Claude Opus 4.8 --- .changeset/relatedlist-describe-fix.md | 5 +++ docs/adr/0035-config-driven-master-detail.md | 16 +++++--- packages/spec/src/data/field.zod.ts | 8 ++-- .../objectstack-data/rules/relationships.md | 29 ++++++++++----- skills/objectstack-ui/SKILL.md | 37 +++++++++++-------- 5 files changed, 60 insertions(+), 35 deletions(-) create mode 100644 .changeset/relatedlist-describe-fix.md diff --git a/.changeset/relatedlist-describe-fix.md b/.changeset/relatedlist-describe-fix.md new file mode 100644 index 0000000000..5d2014088e --- /dev/null +++ b/.changeset/relatedlist-describe-fix.md @@ -0,0 +1,5 @@ +--- +"@objectstack/spec": patch +--- + +Docs: correct the `Field.relatedList` JSDoc + `.describe()` to match the shipped behavior (#2579 follow-up). Non-primary related lists stack under a single shared "Related" tab and only `'primary'` earns its own tab — there is no count-based auto-split (the "count-aware" wording was a stale draft). Comment/description only; no code or behavior change. diff --git a/docs/adr/0035-config-driven-master-detail.md b/docs/adr/0035-config-driven-master-detail.md index db8f205bc7..5cd6fdbf79 100644 --- a/docs/adr/0035-config-driven-master-detail.md +++ b/docs/adr/0035-config-driven-master-detail.md @@ -83,15 +83,19 @@ record detail page. Both intents live on the same relationship: | Side | Flag (on the child's FK field) | Effect | |---|---|---| | Write | `inlineEdit: true` | child rendered as an editable grid inside the parent's create/edit form (atomic save) | -| Read | `relatedList` (+ `relatedListTitle` / `relatedListColumns`) | child rendered as a related list on the parent's detail page | +| Read | `relatedList: boolean \| 'primary'` (+ `relatedListTitle` / `relatedListColumns`) | child rendered as a related list on the parent's detail page; `'primary'` promotes it to its own tab | Related lists are **shown by default** for every child relationship (`master_detail` and `lookup`) — owned (`master_detail`) children are ordered -first. The flag is opt-*out*: set `relatedList: false` to suppress a noisy -association/audit link from the detail page. `relatedListTitle` / -`relatedListColumns` override the derived title / columns. Audit FKs -(`created_by` / `updated_by` / `owner_id`) are skipped and children deduped to -one related list each (first eligible FK wins). +first. `relatedList` is tri-state (a *prominence* hint, not a layout switch — +see ADR-0085): `false` suppresses a noisy association/audit link; `true` / +omitted shows the child under a single shared **Related** tab; `'primary'` +promotes the relationship to its **own tab** on the detail page. `relatedListTitle` / +`relatedListColumns` override the derived title / columns — both optional, with +columns auto-deriving from the child object's `highlightFields` when omitted. +Audit FKs (`created_by` / `updated_by` / `owner_id`) are skipped; a child that +references the parent through several FKs yields **one related list per FK**, and +a self-referential FK surfaces a "children" list. The derivation is a pure function — `deriveRelatedLists(objectDef, objects)` in `@object-ui/app-shell` (`utils/deriveRelatedLists.ts`) — the read-side analogue diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index 98707d64fd..6b6381862c 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -450,9 +450,9 @@ export const FieldSchema = lazySchema(() => z.object({ * Tri-state (ADR-0085 semantic-role style — this is a PROMINENCE hint, NOT a * layout switch): * - `false` → suppress this child from the parent's detail page. - * - `true` / absent → shown; participates in the derived default layout - * (count-aware: few children → a tab each, many → the - * long tail collapses into a single "Related" tab). + * - `true` / absent → shown; stacks with the other non-primary children + * under a single shared "Related" tab. Only `'primary'` + * earns its own tab — there is no count-based auto-split. * - `'primary'` → CORE relationship: always surfaced prominently. The * detail renderer promotes it to its own tab regardless * of child count. This states business intent (true @@ -462,7 +462,7 @@ export const FieldSchema = lazySchema(() => z.object({ * (not a `relatedLayout` switch) is what admits it to the * object model under ADR-0085's admission test. */ - relatedList: z.union([z.boolean(), z.literal('primary')]).optional().describe('Show this child collection as a related list on the parent\'s detail page (read-side mirror of inlineEdit). false = suppress; true/absent = shown in the count-aware derived default; \'primary\' = core relationship, always promoted to its own tab. Prominence intent, not a layout switch (ADR-0085).'), + relatedList: z.union([z.boolean(), z.literal('primary')]).optional().describe('Show this child collection as a related list on the parent\'s detail page (read-side mirror of inlineEdit). false = suppress; true/absent = shown (stacked under the shared "Related" tab); \'primary\' = core relationship, promoted to its own tab. Prominence intent, not a layout switch (ADR-0085).'), /** Optional section title for the detail-page related list (defaults to the child object label). */ relatedListTitle: z.string().optional().describe('Title for the detail-page related list'), /** Optional explicit columns for the detail-page related list (derived from the child object when omitted). */ diff --git a/skills/objectstack-data/rules/relationships.md b/skills/objectstack-data/rules/relationships.md index beaf184aa4..369d08fc00 100644 --- a/skills/objectstack-data/rules/relationships.md +++ b/skills/objectstack-data/rules/relationships.md @@ -302,7 +302,8 @@ project: { type: 'master_detail', reference: 'project', inlineEdit: true, // write side: edit inline in the Project form - relatedListTitle: 'Tasks', // read side: title of the detail-page list + relatedList: 'primary', // read side: promote to its OWN detail-page tab + relatedListTitle: 'Tasks', // title of the detail-page list relatedListColumns: ['title', 'status', 'priority', 'due_date'], }, @@ -310,14 +311,24 @@ project: { audit_ref: { type: 'master_detail', reference: 'invoice', relatedList: false }, ``` -- `relatedList: false` — suppress this child from the parent's detail page - (use for chatty association/log children you don't want surfaced). -- `relatedListTitle` / `relatedListColumns` — override the derived title / - columns (columns are otherwise auto-derived from the child object's fields). - -Audit FKs (`created_by`/`updated_by`/`owner_id`) never become related lists, and -each child object yields at most one related list. See the objectstack-ui skill -for the rendering side. +`relatedList` is tri-state — a *prominence* hint, not a layout switch (ADR-0085): + +- `'primary'` — CORE relationship: the detail page gives it its **own tab**. + Every non-primary related list stacks under a single shared **Related** tab, + so `'primary'` is the one-word way to promote a child table to a first-class + tab without a custom page. +- `false` — suppress this child from the parent's detail page (chatty + association/log children you don't want surfaced). +- `true` / omitted — shown in the shared **Related** tab. + +`relatedListTitle` / `relatedListColumns` override the derived title / columns; +both are optional — when omitted, columns auto-derive from the child object's +`highlightFields` (the same source the lookup picker uses). Audit FKs +(`created_by`/`updated_by`/`owner_id`) never become related lists. A child that +references the parent through several FKs (e.g. `primary_account` + +`partner_account`) yields **one related list per FK**; a self-referential FK +(a hierarchy) surfaces a "children" list. See the objectstack-ui skill for the +rendering side. ## Incorrect vs Correct diff --git a/skills/objectstack-ui/SKILL.md b/skills/objectstack-ui/SKILL.md index 97acc01521..da94588258 100644 --- a/skills/objectstack-ui/SKILL.md +++ b/skills/objectstack-ui/SKILL.md @@ -145,22 +145,27 @@ inline grid, not just invoices: **Read side — detail-page related lists.** The mirror of `inlineEdit` is the related list on the parent's record DETAIL page. You don't author it: every child relationship is shown as a related list by default (owned `master_detail` -children first). Refine on the relationship — `relatedList: false` to suppress a -noisy child, `relatedListTitle` / `relatedListColumns` to override title / -columns (see objectstack-data → Relationships → Detail-page related lists). -Authored record pages can still place an explicit `record:related_list` (or -inline-editable `record:line_items`) when they need bespoke placement. - -**Related-list layout.** Every related list stacks under a single **Related** -tab on the synthesized record detail page — that is the only built-in layout. -The old `detail.relatedLayout` (`'stack'` | `'tabs'`) toggle — like the whole -object-level `detail: {...}` block — was **removed** (ADR-0085): an object -declares no per-surface layout hints. When a record's child tables deserve -first-class navigation (their own tabs, bespoke placement), assign the object a -**custom record Page** and lay it out explicitly with `record:related_list` -(or inline-editable `record:line_items`) blocks. For lighter tweaks stay at the -relationship layer: `relatedList: false` to suppress a noisy child, -`relatedListTitle` / `relatedListColumns` to override title / columns. +children first). Refine on the relationship: +- `relatedList: 'primary'` — mark a CORE relationship; the detail page promotes + it to its **own tab** (see layout below). A *prominence* intent, not a layout + switch (ADR-0085). +- `relatedList: false` — suppress a noisy child from the detail page. +- `relatedListTitle` / `relatedListColumns` — override the derived title / + columns (both optional; columns otherwise auto-derive from the child object's + `highlightFields`). See objectstack-data → Relationships → Detail-page related lists. + +**Related-list layout.** On the synthesized record detail page, each +`relatedList: 'primary'` child gets its **own tab**; every other related list +stacks under a single shared **Related** tab. Promoting a child table to a +first-class tab is therefore a one-word change on the relationship — no custom +page needed. The object still declares no per-surface *layout* hints: the old +`detail.relatedLayout` toggle and object-level `detail: {...}` block stay +**removed** (ADR-0085); `relatedLayout: 'tabs' | 'stack'` survives only as an +app-level default override, not an object key. For arrangements the +relationship layer can't express — filtered splits (e.g. Open vs Closed tabs), a +chart/report tab, exact tab ordering — assign the object a **custom record +Page** and lay it out explicitly with `record:related_list` (or inline-editable +`record:line_items`) blocks. ### Field Conditional Rules in Forms