BulkActionDefSchema 是 .strict() 且没有 requiredPermissions,所以内联批量定义声明不了能力门:
bulkActionDefs: [
{ name: 'bulk_delete_plan', operation: 'delete', requiredPermissions: ['plan.delete'] },
]
Unrecognized key(s) on this bulk action definition: `requiredPermissions`.
而渲染侧早已在读这个键 —— objectui#3492 落地后 BulkActionBar 就是按它过滤的:
const permittedDefs = (actionDefs ?? []).filter(d => mayInvoke(d?.requiredPermissions));
也就是说这是「declared ≠ enforced」的镜像面:enforced ≠ declarable。运行时认这个键、按它隐藏按钮,spec 却不让作者写出来。
现状盘点(对着合并后的 main 实测,三条断言全过)
| 写法 |
能否拿到 requiredPermissions |
机制 |
bulkActions: ['my_action'](命名式) |
✅ |
resolveBulkActions 提升时转发动作上的声明(objectui#3492) |
bulkActionDefs: [{ name, operation:'custom', execution:'aggregate' }],name 命中已声明动作 |
✅ 继承 |
该形态会解析 objectDef.actions 并把命中动作合并进 def |
bulkActionDefs: [{ …, requiredPermissions: [...] }] 直写 |
❌ spec 拒绝 |
本单 |
bulkActionDefs: [{ operation:'update' | 'delete', patch }] |
❌ 无处继承 |
数据面 def 不解析对象动作 |
前两行意味着大多数场景今天就有出路(也是 schema 自己在 label 描述里推荐的那条:「declare a real action and name it in bulkActions to get localization」)。
真正表达不出来的是最后一行:内联的 update / delete 数据面批量定义。它按设计就不该去引用一个对象动作(它不派发动作,它是数据面 mass mutation),于是没有任何合法写法能给「批量删除」加上能力门 —— 而这恰恰是最需要门的一类按钮。
方案
给 BulkActionDefSchema(packages/spec/src/ui/bulk-action.zod.ts)加:
requiredPermissions: z.array(z.string()).optional().describe(
'ADR-0066 D4 capability gate, same semantics as `action.requiredPermissions`: ' +
'an empty/absent declaration always passes, several are AND-ed, and a client that ' +
'cannot resolve the caller’s capabilities fails OPEN (the server is the authority). ' +
'A def PROMOTED from `bulkActions: [name]` inherits the action’s declaration instead — ' +
'this key is for inline defs, notably the `update`/`delete` data-plane forms that ' +
'dispatch no action and therefore have nothing to inherit from.'
),
消费侧无需改动 —— objectui 的 BulkActionDef 类型与 BulkActionBar 的过滤都已就位,加上 spec 声明即打通。
配套:examples/app-showcase 里补一条内联 delete + requiredPermissions 的标本(#6157 的动作显隐矩阵已经把命名式的四种规格钉住了,缺的正是这一格)。
验收
bulkActionDefs: [{ operation: 'delete', requiredPermissions: ['x'] }] 能通过 objectstack validate,且无权用户在批量条上看不到该按钮 —— 与列表工具栏 / 行内 kebab / 记录页头三面结论一致。
BulkActionDefSchema是.strict()且没有requiredPermissions,所以内联批量定义声明不了能力门:而渲染侧早已在读这个键 —— objectui#3492 落地后
BulkActionBar就是按它过滤的:也就是说这是「declared ≠ enforced」的镜像面:enforced ≠ declarable。运行时认这个键、按它隐藏按钮,spec 却不让作者写出来。
现状盘点(对着合并后的 main 实测,三条断言全过)
requiredPermissionsbulkActions: ['my_action'](命名式)resolveBulkActions提升时转发动作上的声明(objectui#3492)bulkActionDefs: [{ name, operation:'custom', execution:'aggregate' }],name命中已声明动作objectDef.actions并把命中动作合并进 defbulkActionDefs: [{ …, requiredPermissions: [...] }]直写bulkActionDefs: [{ operation:'update' | 'delete', patch }]前两行意味着大多数场景今天就有出路(也是 schema 自己在
label描述里推荐的那条:「declare a real action and name it inbulkActionsto get localization」)。真正表达不出来的是最后一行:内联的
update/delete数据面批量定义。它按设计就不该去引用一个对象动作(它不派发动作,它是数据面 mass mutation),于是没有任何合法写法能给「批量删除」加上能力门 —— 而这恰恰是最需要门的一类按钮。方案
给
BulkActionDefSchema(packages/spec/src/ui/bulk-action.zod.ts)加:消费侧无需改动 —— objectui 的
BulkActionDef类型与BulkActionBar的过滤都已就位,加上 spec 声明即打通。配套:
examples/app-showcase里补一条内联delete+requiredPermissions的标本(#6157 的动作显隐矩阵已经把命名式的四种规格钉住了,缺的正是这一格)。验收
bulkActionDefs: [{ operation: 'delete', requiredPermissions: ['x'] }]能通过objectstack validate,且无权用户在批量条上看不到该按钮 —— 与列表工具栏 / 行内 kebab / 记录页头三面结论一致。