fix(app-shell,plugin-list): 关联列表「+ New」消费 create 谓词,行内编辑开关补 update 权限门 (#4646, #4647) - #5145
Merged
Merged
Conversation
#4646, #4647) 同一工具栏面上的两个「已声明未生效」缺口,合单处理。 #4646 —— `createPredicates` 有生产者、零消费者。 spec@17.0.0 把 `userActions.create` 放宽为 `z.union([z.boolean(), RowCrudActionOverrideSchema])`,`resolveCrudAffordances` 随之产出 `createPredicates`,而 objectui 侧无人读取(对照 `editPredicates`/`deletePredicates` 各约 15 处消费点)。症状:父记录进入冻结态 后,子表行内 Edit/Delete 已正确置灰,关联列表工具栏的「+ New」却依然可点,用户 填完整张子表单才拿到服务端 409。 现在关联列表工具栏按 spec docblock 对该键的绑定语义,**对宿主父记录求值一次** `visibleWhen` / `disabledWhen`,叠加在既有 `o.create ∧ can(child,'create')` 之上。 `visibleWhen` 隐藏按钮、fail-CLOSED;`disabledWhen` 置灰按钮、fail-SOFT —— 求值器、失败方向、隐藏 vs 置灰的分界,逐一镜像记录页页头 edit/delete 的既有消费形 (#4419 / PR #4515)。union 的 boolean 臂原样不动:无谓词即无求值。 #4647 —— 行内编辑开关是该工具栏上唯一无权限门的入口。 渲染条件曾是「grid 视图 ∧ 宿主传了 `onInlineEditChange` ∧ 非紧凑工具栏」,而宿主 无条件传该回调。同一工具栏上 New / Import 会按权限隐藏、批量删除入口 AND 了 `can(obj,'delete')`,唯独行内编辑对只读账号敞开:可开启、可改单元格、可点「全部 保存」,最后由服务端 403 拦下。 现按「对象已解析的 edit affordance ∧ `can(object,'update')`」设门,与同工具栏 批量删除的门形一致,并覆盖该功能的**全部三个渲染点**:宽工具栏开关、紧凑工具栏 设置浮层入口(此前完全无门,连回调都不要求)、以及交给 grid 的 `editable` 模式 本身 —— 否则存量视图上的 `inlineEdit: true` 仍会把只读账号直接丢进可编辑单元格, 连开关都不用按。 同时消费声明已久却无人读取的 `ListViewSchema.userActions.editInline`:显式 `false` 可整体关闭该功能,这是作者此前做不到的。 缺省语义如实说明:`editInline` 缺省时沿用宿主既有的 `inlineEdit` 通道,不强制 spec 的 `.default(false)`。强制该缺省会一次性抹掉所有存量 console 列表视图的开关 (没有任何 legacy 键折叠进 `editInline`,存量视图也都没声明它)。这正是相邻 `toolbarFlags` 块为自己写下的规则 —— 缺省取「这些开关一直以来的行为」—— 在「会删掉一个入口」方向上的同一应用。`InterfaceListPage` 作为该键的另一消费者, 把缺省读作 OFF,因为 ADR-0047 界面页没有可沿用的宿主通道。 行为变化(对只读用户):UI 从「能进编辑再 403」变为「不提供入口」。数据访问权限 未变 —— 服务端始终是执行边界,此处只是不再把用户领进注定失败的往返。有权限的账号 行为不变;未挂 `PermissionProvider` 的宿主(独立嵌入、Studio 设计器)保持现状, 因为 `can()` 在那里按设计答 true。 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Collaborator
Author
|
PM 验收:ACCEPT(session_01GTRjn8xBqp75dk7kFupVRt,objectui 分片,批次 23;Fixes #4646 + Fixes #4647 合单) 实物核验:merge-base 验收要点:
三件套照常:本评论 → undraft → auto-merge(SQUASH)。 Generated by Claude Code |
yinlianghui
marked this pull request as ready for review
August 18, 2026 05:20
This was referenced Aug 18, 2026
yinlianghui
pushed a commit
that referenced
this pull request
Aug 18, 2026
反向验证 (c) 查出一条非显然事实,值得留成钉子而不是只写进 PR 正文: 本门与 #4647(PR #5145)ListView 那道**并非字面同一谓词**。ObjectGrid 解析 `objectName = dataConfig.object ?? schema.objectName`,ListView 的 `inlineEditOffered` 只读 `schema.objectName`。因此当对象身份**仅**经 `data: { provider: 'object', object: … }` 传入、无顶层 `objectName` 时, ListView 的 `schema.objectName ? … : true` 分支会落开,该形态**只由本门** 判定 —— 本门不是 ListView 那道的冗余副本。 新增钉子覆盖该形态(无授权 ⇒ 不进编辑态;有授权 ⇒ 照常)。变异验证:把 `objectName` 收窄回 `schema.objectName`,预判「仅此一例红」,实测恰好 1 红 11 绿 —— 钉子既咬得住又不误伤。 同时订正上一次提交里一句过强的注释:原文写「两门是同一谓词」,与刚验证出 的差异相抵触。改为如实说明幂等只成立于 ListView 路径,以及差异朝安全方向 跑。「上游已经设过门了」正是当初留下这扇门的推理,不该由注释再复述一遍。 纯注释 + 测试,产物行为不变。 Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Aug 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4646
Fixes #4647
为什么合单
两卡共享同一张工具栏面(记录页关联列表 / 列表视图工具栏),分诊席在两卡各自的 triage 评论里都留了 serial note:「shares the RecordDetailView/list-toolbar surface with #464x — dispatch serially or to one dev」。两者又是同一个病:声明半已落、渲染半缺失。一席双卡,一个 PR。
前提复核(dist 级证据 → src 级读点)
两卡的证据都是 dist 级(console@17.0.0)。落地前逐条转成 src 级,其中两条需要修正,如实记录:
createPredicates零消费grep -rn "createPredicates" packages/ apps/→ 0 命中;生产者是 spec 的resolveCrudAffordances,经@object-ui/core的resolveEffectiveCrudAffordances转出。对照editPredicates在 5 个文件有消费/定义点RecordDetailView的关联列表工具栏仍是旧两轴」RecordDetailView.tsx,而在RelatedRecordActionsBridge.tsx(由 RecordDetailView 挂载):create: rawAff.create && can(objectName, 'create')。实质成立,位置更正parentId」resolve()只拿到parentId,拿不到父记录本身;record.frozen != true要的是字段不是主键 —— 所以本 PR 必须把parentRecord也穿进桥接层object.zod.ts:1528确为 union;实装@objectstack/spec@17.0.0;docblock 语义逐字核对ListView.tsx:2453、ObjectView.tsx:2013editInline全 dist 只作 i18n 标签键,零消费」InterfaceListPage.tsx:431就有一处消费:inlineEdit: userActions.editInline === true。dist 扫描看不见它,是因为该消费把键映射成了inlineEdit,键名没作为字符串留存。实质缺口仍在,但范围收窄为「对象列表工具栏路径零消费」两处修正都不推翻卡的结论,故照常实施;记录在此以免下一位读者重蹈。
#4646 半 —— 关联列表「+ New」消费 create 谓词
镜像模板 = 同面
editPredicates的既有消费形(RecordDetailView.tsx:1063-1128,即 #4419 / PR #4515 给记录页页头做的那一版),逐条对齐、不自创语义:useRowPredicate→evalRowPredicateevalRowPredicate(同一个,见下「为何无 hook」)visibleWhen缺省?? true,布尔短路visibleWhen失败方向fallback: false)visibleWhen声明判定!= null,非真值性(#3492 不变式)disabledWhen失败方向disabledWhen声明判定!= null门在求值之外fields传参visibleWhen折进 affordance 布尔;disabledWhen走独立disabled标志绑定域按 spec docblock:对宿主父记录求值一次(不是逐行)。独立对象列表没有 scope 记录 —— 它根本不经过这个桥接层。
为何 hook-free:
resolve要为可变数量的关联列表在一个useMemo内作答,每个子对象一次useRowPredicate会把 hook 数量绑到关联列表条数。这与plugin-grid的evalRowActionVisibility面对的是同一个约束(它的 row loop 逐 action 面对同一件事),故采同一解法;底层求值器两边都是 canonical 的evalRowPredicate。新增的 disabled 通道:
RelatedRecordHandlers原本只有「给 / 不给onCreate」二值,契约明说「省略 = 隐藏该 affordance」。disabledWhen需要第三态,故新增createDisabled→RelatedList的newDisabled→ 按钮disabled。把disabledWhen折进隐藏是错的:它会让同一个键在这个面上的行为与其他所有面不同。#4647 半
gap 1 挂点选择 —— 渲染侧,不是宿主侧。 理由:
ListView.tsx:1059的批量删除门resolveEffectiveCrudAffordances(objectDef, effectiveApiOps).delete && canDo(schema.objectName,'delete')就长在 ListView 自己身上,组件手里已经有usePermissions和objectDef。ObjectView一个入口;挂渲染侧连InterfaceListPage和任何嵌入宿主一起覆盖。门形与批量删除逐字同构,只把 operation 从
delete换成update;第一段写作isObjectInlineEditable(...),它就是resolveEffectiveCrudAffordances(...).edit,但用的是说清这个面在问什么的那个名字 —— 记录页正文的双击/铅笔也读同一个 helper,于是列表页和记录页不会对「这个对象的行能否就地编辑」给出两个答案。三个渲染点,不是一个。 按消费半径清点后发现该 affordance 有三个入口,只堵卡里点名的那个会留下两扇门:
ListView.tsx:2453)—— 卡里点名的ListView.tsx:2905,showInlineEdit={currentView === 'grid'})—— 此前完全无门,连onInlineEditChange都不要求editable模式本身(ListView.tsx:1885)—— 存量视图存着inlineEdit: true时,只读账号连开关都不用按就直接落进可编辑单元格,「改单元格 → 全部保存 → 403」这条卡里描述的往返原样可达只堵 1 会让卡自己描述的后果仍然可达,故三处同用一个
inlineEditOffered判定。gap 2 缺省语义 —— 如实写明,并未强制 spec 的
.default(false)。editInline显式false⇒ 不提供;显式true⇒ 提供;缺省 ⇒ 沿用宿主既有的inlineEdit通道。这是与 spec 声明缺省的一处有意分歧,理由:强制.default(false)会一次性抹掉所有存量 console 列表视图的行内编辑 —— 没有任何 legacy 键折叠进editInline(SHOW_FLAG_TO_USER_ACTION无此条目),存量视图也都没声明它。这正是相邻toolbarFlags块为自己写下的规则(缺省取「这些开关一直以来的行为」;hideFields/rowColor保持历史 OFF,因为翻转「会在每个存量视图上多长两个按钮」)在会删掉一个入口方向上的同一应用。该分歧连同「收敛到 spec 缺省」的两条路径,已另立 finding #5144 交维护者裁决,并在测试里钉了一条同名用例,使日后改动缺省是决策而非事故。
反向验证(先书面预判,后跑;commit 后变异、跑完还原)
预判写在
4646-reverse-predictions.md(scratch,已删),三条全部命中,其中 (c) 的细节值得单独说:(a) 摘掉 create 谓词消费 → 预判 RED,且恰是 6 条依赖谓词的用例;boolean 臂与对象级门用例须保持绿。
实测
6 failed | 19 passed,红的正是预判的 6 条,boolean 臂与对象级门全绿,兄弟文件effectiveOps全绿。(b) 摘掉
can(update)门 → 预判 RED 3 条。实测
3 failed | 14 passed,恰为预判三条(无权限失开关 / 存量视图仍进编辑模式 / 紧凑入口),gap-2 用例与 fail-open 用例保持绿。(c) 自选非显然方向 —— 隐藏 vs 置灰的分界实测。 变异:把
disabledWhen也折成隐藏(create: … && !createDisabled)。预判里明确写了一件事:这条变异只会被一条断言抓住,而且不是那条看起来最相关的 —— soft-fail 用例在两种实现下
createDisabled都是 false,不具区分力;真正把线守住的是 disabled 用例里的expect(h.onCreate).toBe(true)。实测
1 failed | 15 passed,失败信息AssertionError: expected false to be true,正是那一行。第三条如实记录而非套模板:这条边界目前由单一断言承载。若日后有人在 disabled 用例里只留
createDisabled的断言、删掉onCreate那行,折叠式回归会以「什么都没产出」的方式变绿(#5046 subtraction case 的同款陷阱)。验证
pnpm type-check:81/81 successfulplugin-list+plugin-detail:126 files / 1442 passedapp-shell/src/views+react+core:404 files / 5150 passed | 1 skippedplugin-grid+components+app-shell全量:658 files / 6203 passed | 1 skippeduserActions.delete.visibleWhen— the BOOLEAN form of the same key reaches all three delete surfaces, the predicate form only reaches the row kebab #4419/PR4515 及同族)7 文件:125/125 passedeslint:0 errors(413 warnings 全是既有no-explicit-any风格告警)check:control-bytesOK;改动文件自扫(含 ESC 位)grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f]'→ 0新增测试:
RelatedRecordActionsBridge.createPredicates.test.tsx(16 例)+ListView.permissions.test.tsx追加两个 describe(11 例)。一处测试写法值得记:紧凑工具栏那两例必须先点开 popover、且让
Section展开(defaultOpen={!!inlineEdit})才有意义 —— 对着关着的浮层断言「不存在」,对任何实现都会绿。正例对照组一开始就是这么红的,红得对。半径与刻意未动
packages/react/SchemaRendererDetailView.tsx:1538/1659的(related as any).onNew是 schema 作者自带回调通道,不经桥接层、不带对象元数据,正确地未受影响半径外发现(均已查重、未指派)
importPredicatesis produced by the spec resolver and consumed nowhere — the same inverse-symmetry gap as #4646, one key over #5142 ——importPredicates同样产出、零消费,与 Related-list toolbar “+ New” does not consumeuserActions.createpredicates — spec 17.0.0 shipped the declaration half, the renderer half is missing (createPredicatesproduced but never consumed) #4646 同族只差一个键。本 PR 刻意未放宽UserActionsOverride.import的类型:先放宽类型再补消费者,等于把刚修掉的病在隔壁键上重写一遍ObjectGrid'seditableschema key offers inline editing with nocan(object,'update')gate — the SDUI-authored grid path #4647 did not reach #5143 ——ObjectGrid的editableschema 键无can(object,'update')门(permissionUpdate在该文件已算出,却只喂给行 kebab)。Grid toolbar inline-edit toggle is not gated oncan(object, 'update');userActions.editInlineis declared in spec but has no consumer #4647 的 ListView 层已堵,SDUI 直写editable: true的 grid 路径未堵,属不同组件、不同声明通道userActions.editInlinehas two different defaults across surfaces, and neither is the spec's.default(false)#5144[finding]—— 上文那处editInline跨面缺省分歧家族关系
userActions.delete.visibleWhen— the BOOLEAN form of the same key reaches all three delete surfaces, the predicate form only reaches the row kebab #4419 / [console] 记录详情页头不消费 userActions.<action>.visibleWhen:同一谓词列表行已生效、记录页头照旧渲染按钮 #4213(edit/delete 到记录页页头)—— 本 PR Related-list toolbar “+ New” does not consumeuserActions.createpredicates — spec 17.0.0 shipped the declaration half, the renderer half is missing (createPredicatesproduced but never consumed) #4646 半的直接镜像模板,同为「谓词到了某些面没到另一些面」can(object, 'update');userActions.editInlineis declared in spec but has no consumer #4647 半的门形来源Generated by Claude Code