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
5 changes: 5 additions & 0 deletions .changeset/relatedlist-describe-fix.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 10 additions & 6 deletions docs/adr/0035-config-driven-master-detail.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions packages/spec/src/data/field.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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). */
Expand Down
29 changes: 20 additions & 9 deletions skills/objectstack-data/rules/relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,22 +302,33 @@ 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'],
},

// Suppress a noisy association from the detail page entirely:
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

Expand Down
37 changes: 21 additions & 16 deletions skills/objectstack-ui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down