Skip to content

Commit 93e6d02

Browse files
os-zhuangclaude
andauthored
docs(spec,skills,adr): correct related-list docs for relatedList 'primary' (#2579 follow-up) (#2600)
- 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 <noreply@anthropic.com>
1 parent a98bd13 commit 93e6d02

5 files changed

Lines changed: 60 additions & 35 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
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.

docs/adr/0035-config-driven-master-detail.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,19 @@ record detail page. Both intents live on the same relationship:
8383
| Side | Flag (on the child's FK field) | Effect |
8484
|---|---|---|
8585
| Write | `inlineEdit: true` | child rendered as an editable grid inside the parent's create/edit form (atomic save) |
86-
| Read | `relatedList` (+ `relatedListTitle` / `relatedListColumns`) | child rendered as a related list on the parent's detail page |
86+
| 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 |
8787

8888
Related lists are **shown by default** for every child relationship
8989
(`master_detail` and `lookup`) — owned (`master_detail`) children are ordered
90-
first. The flag is opt-*out*: set `relatedList: false` to suppress a noisy
91-
association/audit link from the detail page. `relatedListTitle` /
92-
`relatedListColumns` override the derived title / columns. Audit FKs
93-
(`created_by` / `updated_by` / `owner_id`) are skipped and children deduped to
94-
one related list each (first eligible FK wins).
90+
first. `relatedList` is tri-state (a *prominence* hint, not a layout switch —
91+
see ADR-0085): `false` suppresses a noisy association/audit link; `true` /
92+
omitted shows the child under a single shared **Related** tab; `'primary'`
93+
promotes the relationship to its **own tab** on the detail page. `relatedListTitle` /
94+
`relatedListColumns` override the derived title / columns — both optional, with
95+
columns auto-deriving from the child object's `highlightFields` when omitted.
96+
Audit FKs (`created_by` / `updated_by` / `owner_id`) are skipped; a child that
97+
references the parent through several FKs yields **one related list per FK**, and
98+
a self-referential FK surfaces a "children" list.
9599

96100
The derivation is a pure function — `deriveRelatedLists(objectDef, objects)` in
97101
`@object-ui/app-shell` (`utils/deriveRelatedLists.ts`) — the read-side analogue

packages/spec/src/data/field.zod.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -450,9 +450,9 @@ export const FieldSchema = lazySchema(() => z.object({
450450
* Tri-state (ADR-0085 semantic-role style — this is a PROMINENCE hint, NOT a
451451
* layout switch):
452452
* - `false` → suppress this child from the parent's detail page.
453-
* - `true` / absent → shown; participates in the derived default layout
454-
* (count-aware: few children → a tab each, many → the
455-
* long tail collapses into a single "Related" tab).
453+
* - `true` / absent → shown; stacks with the other non-primary children
454+
* under a single shared "Related" tab. Only `'primary'`
455+
* earns its own tab — there is no count-based auto-split.
456456
* - `'primary'` → CORE relationship: always surfaced prominently. The
457457
* detail renderer promotes it to its own tab regardless
458458
* of child count. This states business intent (true
@@ -462,7 +462,7 @@ export const FieldSchema = lazySchema(() => z.object({
462462
* (not a `relatedLayout` switch) is what admits it to the
463463
* object model under ADR-0085's admission test.
464464
*/
465-
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).'),
465+
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).'),
466466
/** Optional section title for the detail-page related list (defaults to the child object label). */
467467
relatedListTitle: z.string().optional().describe('Title for the detail-page related list'),
468468
/** Optional explicit columns for the detail-page related list (derived from the child object when omitted). */

skills/objectstack-data/rules/relationships.md

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -302,22 +302,33 @@ project: {
302302
type: 'master_detail',
303303
reference: 'project',
304304
inlineEdit: true, // write side: edit inline in the Project form
305-
relatedListTitle: 'Tasks', // read side: title of the detail-page list
305+
relatedList: 'primary', // read side: promote to its OWN detail-page tab
306+
relatedListTitle: 'Tasks', // title of the detail-page list
306307
relatedListColumns: ['title', 'status', 'priority', 'due_date'],
307308
},
308309

309310
// Suppress a noisy association from the detail page entirely:
310311
audit_ref: { type: 'master_detail', reference: 'invoice', relatedList: false },
311312
```
312313
313-
- `relatedList: false` — suppress this child from the parent's detail page
314-
(use for chatty association/log children you don't want surfaced).
315-
- `relatedListTitle` / `relatedListColumns` — override the derived title /
316-
columns (columns are otherwise auto-derived from the child object's fields).
317-
318-
Audit FKs (`created_by`/`updated_by`/`owner_id`) never become related lists, and
319-
each child object yields at most one related list. See the objectstack-ui skill
320-
for the rendering side.
314+
`relatedList` is tri-state — a *prominence* hint, not a layout switch (ADR-0085):
315+
316+
- `'primary'` — CORE relationship: the detail page gives it its **own tab**.
317+
Every non-primary related list stacks under a single shared **Related** tab,
318+
so `'primary'` is the one-word way to promote a child table to a first-class
319+
tab without a custom page.
320+
- `false` — suppress this child from the parent's detail page (chatty
321+
association/log children you don't want surfaced).
322+
- `true` / omitted — shown in the shared **Related** tab.
323+
324+
`relatedListTitle` / `relatedListColumns` override the derived title / columns;
325+
both are optional — when omitted, columns auto-derive from the child object's
326+
`highlightFields` (the same source the lookup picker uses). Audit FKs
327+
(`created_by`/`updated_by`/`owner_id`) never become related lists. A child that
328+
references the parent through several FKs (e.g. `primary_account` +
329+
`partner_account`) yields **one related list per FK**; a self-referential FK
330+
(a hierarchy) surfaces a "children" list. See the objectstack-ui skill for the
331+
rendering side.
321332
322333
## Incorrect vs Correct
323334

skills/objectstack-ui/SKILL.md

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -145,22 +145,27 @@ inline grid, not just invoices:
145145
**Read side — detail-page related lists.** The mirror of `inlineEdit` is the
146146
related list on the parent's record DETAIL page. You don't author it: every
147147
child relationship is shown as a related list by default (owned `master_detail`
148-
children first). Refine on the relationship — `relatedList: false` to suppress a
149-
noisy child, `relatedListTitle` / `relatedListColumns` to override title /
150-
columns (see objectstack-data → Relationships → Detail-page related lists).
151-
Authored record pages can still place an explicit `record:related_list` (or
152-
inline-editable `record:line_items`) when they need bespoke placement.
153-
154-
**Related-list layout.** Every related list stacks under a single **Related**
155-
tab on the synthesized record detail page — that is the only built-in layout.
156-
The old `detail.relatedLayout` (`'stack'` | `'tabs'`) toggle — like the whole
157-
object-level `detail: {...}` block — was **removed** (ADR-0085): an object
158-
declares no per-surface layout hints. When a record's child tables deserve
159-
first-class navigation (their own tabs, bespoke placement), assign the object a
160-
**custom record Page** and lay it out explicitly with `record:related_list`
161-
(or inline-editable `record:line_items`) blocks. For lighter tweaks stay at the
162-
relationship layer: `relatedList: false` to suppress a noisy child,
163-
`relatedListTitle` / `relatedListColumns` to override title / columns.
148+
children first). Refine on the relationship:
149+
- `relatedList: 'primary'` — mark a CORE relationship; the detail page promotes
150+
it to its **own tab** (see layout below). A *prominence* intent, not a layout
151+
switch (ADR-0085).
152+
- `relatedList: false` — suppress a noisy child from the detail page.
153+
- `relatedListTitle` / `relatedListColumns` — override the derived title /
154+
columns (both optional; columns otherwise auto-derive from the child object's
155+
`highlightFields`). See objectstack-data → Relationships → Detail-page related lists.
156+
157+
**Related-list layout.** On the synthesized record detail page, each
158+
`relatedList: 'primary'` child gets its **own tab**; every other related list
159+
stacks under a single shared **Related** tab. Promoting a child table to a
160+
first-class tab is therefore a one-word change on the relationship — no custom
161+
page needed. The object still declares no per-surface *layout* hints: the old
162+
`detail.relatedLayout` toggle and object-level `detail: {...}` block stay
163+
**removed** (ADR-0085); `relatedLayout: 'tabs' | 'stack'` survives only as an
164+
app-level default override, not an object key. For arrangements the
165+
relationship layer can't express — filtered splits (e.g. Open vs Closed tabs), a
166+
chart/report tab, exact tab ordering — assign the object a **custom record
167+
Page** and lay it out explicitly with `record:related_list` (or inline-editable
168+
`record:line_items`) blocks.
164169

165170
### Field Conditional Rules in Forms
166171

0 commit comments

Comments
 (0)