目标
如今要让记录详情页把子表显示成独立 tab(如 showcase_account 的 Projects / Invoices),必须手写一整张自定义 record page(examples/app-showcase/src/pages/account-detail.page.ts)。零配置派生出的详情页太弱,不足以作默认:
所有相关列表被堆进一个 "Related" tab;
每个相关列表都得手写列;
没有办法标记某个子表是「核心」;
每个子对象只派生一条相关列表(同一子对象的多个外键会丢关系)。
本 issue 让**「不写 page / 轻量标注」这条路足够好用,使自定义 page 退回为 逃生舱(Tier 2)**——只服务真正复杂/定制的布局,而不是默认路径。下面每一项改动,都是把对象从「必须写 page」往下拉到「零配置或一句字段标注」。
延续 ADR-0085 (语义角色优先于表面提示块)。按仓库惯例不新开 ADR(这是在执行 ADR-0085 的既定方向);是否为 relatedList: 'primary' 补一条 ADR-0085 addendum 见文末备注。
三层模型
层
作者要做什么
用 page 吗
0 · 零配置
什么都不做;从关系图自动派生,列取自子对象 highlightFields/默认列,按确定性规则排布
❌
1 · 字段级意图
在子表外键上标注:relatedList: false | true | 'primary'、relatedListTitle、relatedListColumns
❌
2 · 自定义 page
扇出(Open/Closed 分裂)、条件 tab、一个 tab 放多个列表、非关系 tab(图表/报表/嵌入)、精确排序
✅
每项改动都在把对象从 Tier 2 往 Tier 0/1 拉。
默认布局规则(修正:删去「数量自适应」)
只有两件事让一个子表获得独立 tab:
在它的关系字段上标 relatedList: 'primary';或
你在自定义 page 里显式排了它。
其余所有子表统一进一个聚合的 "Related" tab(纵向堆叠);relatedList: false 则完全不显示。
⚠️ 这修正了本 issue 早期草案里的「数量自适应」 (≤3 个各占一 tab、超过则折叠进 Related)。删除它,因为:① 与 primary 冗余——已有显式的核心信号,就不该再靠数量去猜;② 阈值有「悬崖」(加第 4 个子表会突然从 4 个 tab 塌成 1 个 Related);③ 同一 app 内不一致、难解释;④ 对 AI 授权不友好(结果由 AI 无法预判的计数决定)。改后:完全可预测、无悬崖、对齐主流平台、AI 友好。代价:未标注的简单对象会显示成一个 Related tab 而非多个 tab——但标一个词即可,且更一致。
详情页固定框架:顶部 highlights 条 →Details→ 各 primary tab →Related→Activity/History。
相关子表的排序(tab 顺序 与 Related 内堆叠顺序)
排序对是否 tab 都适用 :既决定 tab 条里各 primary tab 的先后,也决定 Related tab 内堆叠列表的先后。
决策:排序沿用「确定性默认 + 需要精确顺序就写 page」,不在对象层加排序键。
派生默认顺序(Tier 0/1,确定性、可预测) :master_detail(拥有/组合型,如行项)在前 → lookup 在后;组内用稳定次序兜底(对象注册顺序 / 标题)。primary tab 之间、Related 堆叠之间都用这条规则。
精确顺序 = 自定义 page(Tier 2) :page:tabs.items[] 数组本身就是那份「有序列表」,而顺序本质是排布(ADR-0085 准入测试判它归 page);主流平台(Salesforce)也把相关列表顺序放在页面布局属性上,不放在字段上。
为什么不在字段上加数字 relatedListOrder?任何精确全序都需要一份「有序列表」;放在分散于各子对象的外键上,只能用数字 (有间隙/并列/重排的坑,AI 易错)或父对象上的名单 (= 已否决的「page 换皮」)。唯一自然的有序列表就是 page 的 tabs.items[]——所以精确排序天然归 page。若实测证明「仅为重排就得写 page」太重,退路是字段上的可选数字 relatedListOrder(带上述代价),但不作首选。
场景 cookbook(不同业务需求 → 写什么元数据 → 详情页长什么样)
记号:【X】=一个 tab;★=独立 tab;▾Related=聚合 tab 内堆叠。
S1 · 零配置(最常见的默认)
account 下挂 contact / opportunity / case,三个普通 lookup,什么都不额外标:
// contact / opportunity / case 各自:
account : Field . lookup ( 'account' , { label : 'Account' } ) ,
→ 【Details】【▾Related(Contacts、Opportunities、Cases 堆叠)】【Activity】(可预测兜底,对齐 Salesforce 默认)。
S2 · 标出核心子表(Tier 1 主力用法)
opportunity、case 是命脉要各自独立 tab;contact 留在 Related:
// opportunity.object.ts
account : Field . lookup ( 'account' , { label : 'Account' , relatedList : 'primary' } ) ,
// case.object.ts
account : Field . lookup ( 'account' , { label : 'Account' , relatedList : 'primary' } ) ,
// contact 保持普通 lookup(不标)
→ 【Details】【Opportunities ★】【Cases ★】【▾Related(Contacts)】【Activity】
S3 · 主子表 / 行项(master_detail,写侧 + 读侧)
invoice 下有 invoice_line,级联删除,在发票表单里内联编辑,并在发票详情页给独立 tab:
// invoice_line.object.ts —— 父是 invoice
invoice : Field . masterDetail ( 'invoice' , {
label : 'Invoice' ,
deleteBehavior : 'cascade' ,
inlineEdit : 'grid' , // 写侧:invoice 表单里内联行项网格
inlineTitle : 'Line Items' ,
relatedList : 'primary' , // 读侧:invoice 详情页给独立 tab
} ) ,
→ 发票编辑表单 :可编辑 Line Items 行项网格(父子原子保存)。
→ 发票详情页 :【Details】【Line Items ★(可编辑行项网格)】【▾Related(…)】【Activity】
S4 · 隐藏噪音子表
// account_sync_log.object.ts
account : Field . lookup ( 'account' , { relatedList : false } ) ,
→ 该列表完全不出现(连 Related 里也没有)。(created_by/updated_by/owner 等审计外键本就自动跳过;false 用于业务噪音表。)
S5 · 自定义列 / 标题
account 的发票 tab 标题叫 "Billing",列要 name/total/status/issued_on:
// invoice.object.ts
account : Field . lookup ( 'account' , {
label : 'Account' ,
relatedList : 'primary' ,
relatedListTitle : 'Billing' ,
relatedListColumns : [ 'name' , 'total' , 'status' , 'issued_on' ] ,
} ) ,
→ 【…【Billing ★(列:name/total/status/issued_on)】…】
→ 不写 relatedListColumns 时,列自动派生自 invoice 的 highlightFields (「columns 变可选」那条改动的效果)。
S6 · 同一子对象多条关系(multi-FK)
opportunity 有 primary_account 和 partner_account 两个字段指向 account:
// opportunity.object.ts
primary_account : Field . lookup ( 'account' , { label : 'Primary Account' ,
relatedList : 'primary' , relatedListTitle : 'Primary Opportunities' } ) ,
partner_account : Field . lookup ( 'account' , { label : 'Partner Account' ,
relatedListTitle : 'Partner Opportunities' } ) , // 不标 primary → 进 Related
→ 【…【Primary Opportunities ★】…【▾Related(Partner Opportunities、…)】】
→ 修复前 :「第一个外键胜出」,partner 那条整个消失。
S7 · 自引用层级
// account.object.ts
parent_account : Field . lookup ( 'account' , { label : 'Parent Account' ,
relatedList : 'primary' , relatedListTitle : 'Child Accounts' } ) ,
→ 【…【Child Accounts ★(parent_account = 当前账户 的那些账户)】…】
→ 修复前 :自引用(child===parent)被整体跳过,没有这个列表。
S8 · 复杂需求 = 自定义 page(Tier 2 逃生舱)
account 的发票拆 Unpaid/Paid 两 tab + 一个 Revenue 图表 tab + 只对 status=customer 显示的 Contracts tab —— 字段标注做不到(扇出/非关系/条件),写 page:
definePage ( { type :'record' , object :'account' , kind :'slotted' , slots :{ tabs :{ type :'page:tabs' , properties :{ items :[
{ key :'details' , label :'Details' , children :[ { type :'record:details' } ] } ,
{ key :'unpaid' , label :'Unpaid' , children :[ { type :'record:related_list' ,
properties :{ objectName :'invoice' , relationshipField :'account' ,
filter :[ { field :'status' , operator :'ne' , value :'paid' } ] } } ] } ,
{ key :'paid' , label :'Paid' , children :[ { type :'record:related_list' ,
properties :{ objectName :'invoice' , relationshipField :'account' ,
filter :[ { field :'status' , operator :'eq' , value :'paid' } ] } } ] } ,
{ key :'revenue' , label :'Revenue' , children :[ { type :'record:chart' , /* 非关系 tab */ } ] } ,
] } } } } )
→ 完全按你排的 tab。条件 tab(Contracts 仅 customer)目前靠组件级 visibility (内容隐藏、tab 头暂留)——tab 头级 visibility 是单列的 follow-up。
改动清单
1. relatedList 三态提示 — protocol:data(已实现)
packages/spec/src/data/field.zod.ts:relatedList 从 z.boolean() 升为 z.union([z.boolean(), z.literal('primary')])。false=隐藏;true/缺省=进 Related;'primary'=核心,独立 tab。这是显著度意图 (跨表面成立),不是布局开关——「primary→独立 tab」只是详情页渲染器的解读,这也是它能通过 ADR-0085 准入测试的原因(对比被否决的 relatedLayout)。加性、向后兼容 → minor (string|false 已有先例 stageField)。
2. record:related_list 的 columns 改为可选 + 子对象派生 — protocol:ui(spec 侧已实现,objectui 派生待做)
packages/spec/src/ui/component.zod.ts:RecordRelatedListProps.columns 当前必填,改为可选;省略时列取自子对象 highlightFields/默认列。覆盖链(全是本地值,无跨引用 ):子对象 highlightFields → 字段级 relatedListColumns → 每 tab 内联 columns。放宽(必填→可选)→ 向后兼容。
3. 默认布局与排序 — objectui
plugin-detail/src/synth/buildDefaultPageSchema.ts + app-shell/src/utils/deriveRelatedLists.ts:实现上文「primary/page→tab,其余→Related」规则 + 确定性排序(master_detail 在前,lookup 在后,稳定次序兜底);删去 count-aware 阈值逻辑 。列省略时从子对象 highlightFields 派生。可保留一个应用级 全局开关(全 tab / 全 Related),但不设对象级开关 (ADR-0085)。
4. 多外键派生 — objectui
deriveRelatedLists.ts:当前「第一个外键胜出」(seenChild 去重 + break)→ 同一子对象两外键指向同一父只出一条。改为每个合格外键出一条 ;标题默认 relatedListTitle,否则 ${childLabel} · ${fkFieldLabel}。
5. 自引用相关列表 — objectui
deriveRelatedLists.ts 当前整体跳过 child.name === parentName → 自引用父外键无自动「下级」列表。改为:非审计外键且 relatedList !== false 时允许自引用;按 where <selfFk> == 本记录id 过滤。
6. lookup 弹出框(选择器)的列 — 澄清 + 一处一致性 — protocol:data / objectui
不复用 relatedList* :选择器是反方向 (子选父),展示的是目标对象 的候选行(不是子记录),列属于不同对象,不能与相关列表共用键。
选择器保留自己的键:displayField / descriptionField / lookupColumns(富列 {field,label,type})/ lookupFilters / lookupPageSize。
一致性(小改动) :选择器省略 lookupColumns 时的默认列,统一到目标对象的 highlightFields (与相关列表同源)——「如何列出对象 X」只有一处真源(X 的 highlightFields),相关列表与选择器都从它派生;两侧各自的覆盖键(relatedListColumns vs lookupColumns)保持分离,因为描述不同对象/方向。
明确不做 / 已否决
❌ record:related_list 加 view 引用 —— AI 易错面(悬空引用、引错对象、与内联 filter 双写打架、build 绿运行期静默空)。列从子对象派生(✨ Set up Copilot instructions #2 );相关列表的过滤本就是结构性外键,不是作者写的业务逻辑。AI 授权平台里「能内联就别引用」。
❌ 对象层 relatedLists: [...] 数组 —— page 换皮;ADR-0085 否决对象层信息重排。
❌ 对象层 relatedLayout 开关 —— ADR-0085 已否决。
❌ 字段上数字 relatedListOrder(暂不做) —— 见「排序」一节;精确顺序归 page。若实测嫌重再议。
业务子过滤分裂(Open/Closed tab) —— 留在 Tier 2(page 内联 filter)。设计如此,对象层不设键。
相关 follow-up(单独,不阻塞本 issue)
page:tabs 的 item 级 visibility(CEL) 做条件 tab。tab item 形状({label,icon,children})缺 visibility,而 PageComponentSchema 已有。属 Tier 2 页面能力,与本 issue「降低对 page 的依赖」正交,单独跟踪。
验收
examples/app-showcase:删除 account-detail.page.ts ,仅靠在 showcase_project.account、showcase_invoice.account 上标 relatedList: 'primary' + 派生默认,复现等价的 Projects/Invoices 独立子表 tab。这是「常见情况不再需要自定义 page」的 dogfood 证明。
field-zoo / dogfood 回归覆盖:primary 提级、列派生、多外键、自引用、确定性排序。
CI 绿:spec api-surface 保持 minor (无删除导出);example-apps typecheck;dogfood gate。
备注
跨 framework (spec)+ objectui (渲染/派生),配套 objectui PR。
按仓库惯例(bugfix/follow-up 不新开 ADR):理由写进 changeset + 代码注释。仅当要把「显著度=意图 / 布局=排布」这条线固化时,才补一条 ADR-0085 addendum ——由 owner 定。
目标
如今要让记录详情页把子表显示成独立 tab(如
showcase_account的 Projects / Invoices),必须手写一整张自定义recordpage(examples/app-showcase/src/pages/account-detail.page.ts)。零配置派生出的详情页太弱,不足以作默认:本 issue 让**「不写 page / 轻量标注」这条路足够好用,使自定义 page 退回为逃生舱(Tier 2)**——只服务真正复杂/定制的布局,而不是默认路径。下面每一项改动,都是把对象从「必须写 page」往下拉到「零配置或一句字段标注」。
延续 ADR-0085(语义角色优先于表面提示块)。按仓库惯例不新开 ADR(这是在执行 ADR-0085 的既定方向);是否为
relatedList: 'primary'补一条 ADR-0085 addendum 见文末备注。三层模型
highlightFields/默认列,按确定性规则排布relatedList: false | true | 'primary'、relatedListTitle、relatedListColumns每项改动都在把对象从 Tier 2 往 Tier 0/1 拉。
默认布局规则(修正:删去「数量自适应」)
只有两件事让一个子表获得独立 tab:
relatedList: 'primary';或其余所有子表统一进一个聚合的 "Related" tab(纵向堆叠);
relatedList: false则完全不显示。详情页固定框架:顶部 highlights 条 →
Details→ 各 primary tab →Related→Activity/History。相关子表的排序(tab 顺序 与 Related 内堆叠顺序)
排序对是否 tab 都适用:既决定 tab 条里各 primary tab 的先后,也决定 Related tab 内堆叠列表的先后。
决策:排序沿用「确定性默认 + 需要精确顺序就写 page」,不在对象层加排序键。
master_detail(拥有/组合型,如行项)在前 →lookup在后;组内用稳定次序兜底(对象注册顺序 / 标题)。primary tab 之间、Related 堆叠之间都用这条规则。page:tabs.items[]数组本身就是那份「有序列表」,而顺序本质是排布(ADR-0085 准入测试判它归 page);主流平台(Salesforce)也把相关列表顺序放在页面布局属性上,不放在字段上。场景 cookbook(不同业务需求 → 写什么元数据 → 详情页长什么样)
记号:
【X】=一个 tab;★=独立 tab;▾Related=聚合 tab 内堆叠。S1 · 零配置(最常见的默认)
account 下挂 contact / opportunity / case,三个普通
lookup,什么都不额外标:→
【Details】【▾Related(Contacts、Opportunities、Cases 堆叠)】【Activity】(可预测兜底,对齐 Salesforce 默认)。S2 · 标出核心子表(Tier 1 主力用法)
opportunity、case 是命脉要各自独立 tab;contact 留在 Related:
→
【Details】【Opportunities ★】【Cases ★】【▾Related(Contacts)】【Activity】S3 · 主子表 / 行项(master_detail,写侧 + 读侧)
invoice 下有 invoice_line,级联删除,在发票表单里内联编辑,并在发票详情页给独立 tab:
→ 发票编辑表单:可编辑 Line Items 行项网格(父子原子保存)。
→ 发票详情页:
【Details】【Line Items ★(可编辑行项网格)】【▾Related(…)】【Activity】S4 · 隐藏噪音子表
→ 该列表完全不出现(连 Related 里也没有)。(
created_by/updated_by/owner等审计外键本就自动跳过;false用于业务噪音表。)S5 · 自定义列 / 标题
account 的发票 tab 标题叫 "Billing",列要 name/total/status/issued_on:
→
【…【Billing ★(列:name/total/status/issued_on)】…】→ 不写
relatedListColumns时,列自动派生自 invoice 的highlightFields(「columns 变可选」那条改动的效果)。S6 · 同一子对象多条关系(multi-FK)
opportunity 有
primary_account和partner_account两个字段指向 account:→
【…【Primary Opportunities ★】…【▾Related(Partner Opportunities、…)】】→ 修复前:「第一个外键胜出」,partner 那条整个消失。
S7 · 自引用层级
→
【…【Child Accounts ★(parent_account = 当前账户 的那些账户)】…】→ 修复前:自引用(child===parent)被整体跳过,没有这个列表。
S8 · 复杂需求 = 自定义 page(Tier 2 逃生舱)
account 的发票拆 Unpaid/Paid 两 tab + 一个 Revenue 图表 tab + 只对
status=customer显示的 Contracts tab —— 字段标注做不到(扇出/非关系/条件),写 page:→ 完全按你排的 tab。条件 tab(Contracts 仅 customer)目前靠组件级
visibility(内容隐藏、tab 头暂留)——tab 头级visibility是单列的 follow-up。改动清单
1.
relatedList三态提示 —protocol:data(已实现)packages/spec/src/data/field.zod.ts:relatedList从z.boolean()升为z.union([z.boolean(), z.literal('primary')])。false=隐藏;true/缺省=进 Related;'primary'=核心,独立 tab。这是显著度意图(跨表面成立),不是布局开关——「primary→独立 tab」只是详情页渲染器的解读,这也是它能通过 ADR-0085 准入测试的原因(对比被否决的relatedLayout)。加性、向后兼容 → minor(string|false已有先例stageField)。2.
record:related_list的columns改为可选 + 子对象派生 —protocol:ui(spec 侧已实现,objectui 派生待做)packages/spec/src/ui/component.zod.ts:RecordRelatedListProps.columns当前必填,改为可选;省略时列取自子对象highlightFields/默认列。覆盖链(全是本地值,无跨引用):子对象highlightFields→ 字段级relatedListColumns→ 每 tab 内联columns。放宽(必填→可选)→ 向后兼容。3. 默认布局与排序 —
objectuiplugin-detail/src/synth/buildDefaultPageSchema.ts+app-shell/src/utils/deriveRelatedLists.ts:实现上文「primary/page→tab,其余→Related」规则 + 确定性排序(master_detail在前,lookup在后,稳定次序兜底);删去 count-aware 阈值逻辑。列省略时从子对象highlightFields派生。可保留一个应用级全局开关(全 tab / 全 Related),但不设对象级开关(ADR-0085)。4. 多外键派生 —
objectuideriveRelatedLists.ts:当前「第一个外键胜出」(seenChild去重 +break)→ 同一子对象两外键指向同一父只出一条。改为每个合格外键出一条;标题默认relatedListTitle,否则${childLabel} · ${fkFieldLabel}。5. 自引用相关列表 —
objectuideriveRelatedLists.ts当前整体跳过child.name === parentName→ 自引用父外键无自动「下级」列表。改为:非审计外键且relatedList !== false时允许自引用;按where <selfFk> == 本记录id过滤。6. lookup 弹出框(选择器)的列 — 澄清 + 一处一致性 —
protocol:data/objectuirelatedList*:选择器是反方向(子选父),展示的是目标对象的候选行(不是子记录),列属于不同对象,不能与相关列表共用键。displayField/descriptionField/lookupColumns(富列{field,label,type})/lookupFilters/lookupPageSize。lookupColumns时的默认列,统一到目标对象的highlightFields(与相关列表同源)——「如何列出对象 X」只有一处真源(X 的highlightFields),相关列表与选择器都从它派生;两侧各自的覆盖键(relatedListColumnsvslookupColumns)保持分离,因为描述不同对象/方向。明确不做 / 已否决
record:related_list加view引用 —— AI 易错面(悬空引用、引错对象、与内联 filter 双写打架、build 绿运行期静默空)。列从子对象派生(✨ Set up Copilot instructions #2);相关列表的过滤本就是结构性外键,不是作者写的业务逻辑。AI 授权平台里「能内联就别引用」。relatedLists: [...]数组 —— page 换皮;ADR-0085 否决对象层信息重排。relatedLayout开关 —— ADR-0085 已否决。relatedListOrder(暂不做) —— 见「排序」一节;精确顺序归 page。若实测嫌重再议。filter)。设计如此,对象层不设键。相关 follow-up(单独,不阻塞本 issue)
page:tabs的 item 级visibility(CEL) 做条件 tab。tab item 形状({label,icon,children})缺visibility,而PageComponentSchema已有。属 Tier 2 页面能力,与本 issue「降低对 page 的依赖」正交,单独跟踪。验收
examples/app-showcase:删除account-detail.page.ts,仅靠在showcase_project.account、showcase_invoice.account上标relatedList: 'primary'+ 派生默认,复现等价的 Projects/Invoices 独立子表 tab。这是「常见情况不再需要自定义 page」的 dogfood 证明。field-zoo/ dogfood 回归覆盖:primary 提级、列派生、多外键、自引用、确定性排序。备注