fix(components): 记录页头的动作谓词改走 CEL,与行菜单/批量栏同一入口 (#3521) - #3791
Conversation
`page:header` 的 `visible` / `hidden` / `disabled` 一直交给 objectui 的遗留 JS 求值器(`new ExpressionEvaluator(evalCtx)` + `evaluateExpression(src)` 两处), 而行 kebab、批量栏、条件格式自 #1584 / ADR-0058 起统一走 `evalRowPredicate` (裸字符串即 CEL —— 这正是 `@objectstack/spec` 给 `ActionSchema.visible` 的类型, 也是服务端执行的方言)。于是每一个 CEL 独有构造在列表里对、在详情页头抛错, 而抛错是 fail-closed:按钮直接消失,页面上没有任何痕迹。 - 方法调用:`record.f_tags.size() > 0`、`record.f_textarea.contains("x")` - `in` 运算符:`'"red" in record.f_multiselect'`(解析失败) - stdlib 函数:`record.f_date < today()`(`today is not a function`) 两处求值改走 `evalRowPredicate`:同一入口、同一套绑定(`record.*` + 裸字段 + `data.*` + 宿主 scope,关系字段按 #3501 折回外键)、同一套 fail-closed + warn-once。谓词整体传入(不再拆出 `.source`),所以 `{ dialect: 'cel' }` 信封 保持权威 CEL,与行 kebab 传 `def.visible` 的规则一致。 遗留方言不回归:`${…}`、`===`/`!==`、`?.`、`??`、`.includes()` 等仍由 `isLegacyDialectSource` 兜到遗留引擎(保留既有 deprecation 警告)。`${…}` 模板 此前在页头根本求不出值(`evaluateExpression` 拿不到模板)、按钮恒隐藏,现在经 回退正确求值。 诊断:故障报告改由 `evalRowPredicate` 的 warn-once 机制写出,label 带上 `page:header action "…" visible`,与行 kebab/批量栏逐字同一措辞;页头本地只保留 「谓词引用了 payload 里不存在的字段」这一条(引擎看不到的成因)。 测试: - `page-header-predicate-dialect.test.tsx` —— 三类 CEL 构造双向求值、信封、 `hidden`/`disabled`、裸字段/`data.*`/`os.user.*` 绑定、#3501 关系绑定、 遗留方言回退、fail-closed + warn-once。 - `plugin-grid/__tests__/predicate-surface-parity.test.tsx` —— 同一谓词 + 同一 记录,在行 kebab(`isCustomRowActionVisible`)、批量栏(`partitionBulkRows`)、 记录页头三处给出同一结论(17 例)。
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
|
勘误(不值得为它 force-push,commit message 留原样):提交正文末尾写的 parity 用例数「17 例」是笔误,实际是 16 例( 另外两条在实施本单时发现、与本 PR 改动无关的观察已单独归档(
Generated by Claude Code Generated by Claude Code |
issue 表格里点名的第三个方法调用。它也是离遗留标记最近的一个:`.match(` 是 legacy marker,`.matches(` 故意不是(marker 要求 `match` 后紧跟 `(`),所以必须 落到 CEL 引擎 —— 正反两向各钉一条,免得「不再隐藏」被当成「求值正确」。 夹具补 `f_email`(showcase field-zoo 里同名字段的形状)。 pnpm vitest run packages/components/src/__tests__/page-header-predicate-dialect.test.tsx → Tests 28 passed (28)
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
|
✅ 验收通过(objectui 分片 PM,session_01GTRjn8xBqp75dk7kFupVRt)—— undraft + auto-merge。 git 实物核验( 验证质量:28 条方言单测(CEL 构造双向 + 信封 + 绑定)+ 16 条三面 parity(行菜单/批量栏/页头逐字相等),反向验证预判「改动前红」实测 20 条红且全在页头列;两处诚实记录尤佳 —— CEL-假用例改动前也绿(抛错隐藏与求值假隐藏外观同形,故每类钉双向)、 风险裁定(四条均采纳 dev 处置):诊断措辞变化已归档 #3792;非布尔真值收紧与非 cel 信封按 CEL 求值均为向行面既有规则收敛,仓内无受影响夹具;宿主 scope Generated by Claude Code |
Fixes #3521
问题:同一条元数据,两种方言
记录页头(
page:header)的动作谓词一直交给 objectui 的遗留 JS 求值器 ——packages/components/src/renderers/layout/containers.tsx里new ExpressionEvaluator(evalCtx)+evaluator.evaluateExpression(src)的两处(visible/hidden的evalExpr,以及disabled的dEvaluator)。而行 kebab、批量选择栏、条件格式自 #1584 / ADR-0058 起统一走evalRowPredicate:裸字符串即 CEL —— 这正是@objectstack/spec给ActionSchema.visible的类型,也是服务端执行的方言。于是每一个 CEL 独有构造在列表里对、在详情页头抛错,而抛错是 fail-closed:按钮直接消失,页面上没有任何痕迹。
前后对照
record.f_tags.size() > 0record.f_textarea.contains("x")'"red" in record.f_multiselect'record.f_date < today()today is not a function→ 隐藏{ dialect: 'cel', source: '…' }信封.source后仍按 JS 求值${record.f_status === "open"}evaluateExpression求不出模板 → 恒隐藏record.f_status === "open"改动落在同一处的方言这一半 —— #3501 刚落的绑定那一半(
toPredicateRecord)完整保留:关系字段仍按服务端存的外键绑定,只是现在由evalRowPredicate的fields选项统一完成(record.*/ 裸字段 /data.*三路绑定都过同一次归一化),页头本地那份只留给引擎不构造的ctx.*命名空间。展示不受影响(标题仍读原始ctx.data,关系字段照常显示名称)。谓词现在整体传入
evalRowPredicate(不再拆出.source),所以{ dialect: 'cel' }信封不会被拍平成方言猜测 —— 与行 kebab 直接传def.visible的规则逐字一致。三面 parity 证据
packages/plugin-grid/src/__tests__/predicate-surface-parity.test.tsx(plugin-grid 是唯一能同时看见三面的包:它拥有行 kebab 的isCustomRowActionVisible与批量栏的partitionBulkRows,并依赖@object-ui/components拿到page:header)。16 个 case(16 passed),每个 case 断言三面结论逐字相等:反向验证(方向预先声明为「改动前红」):只回退
containers.tsx、保留两个新测试文件后重跑,20 个断言转红,红的正是页头列:两处如实记录,免得后人误读:
${…}模板在改动前也是红的:老页头调的是evaluateExpression,它拿不到${…}模板,按钮恒隐藏。也就是说遗留方言这条路不但没回归,${…}反而从「求不出」变成「经回退正确求值」。遗留方言不回归
packages/components/src/__tests__/page-header-predicate-dialect.test.tsx里单列一组:${…}模板、裸===、JS-only 方法.includes()、遗留disabled,全部仍走遗留引擎并保留既有 deprecation 警告(isLegacyDialectSource兜底,未动 spec)。fail-closed 与 warn-once 也逐条钉住:故障
visible仍隐藏、故障disabled仍保持可用、故障hidden仍渲染(都是历史方向,由fallback: false保住)。诊断措辞的一处变化(有意)
故障报告改由
evalRowPredicate自己的 warn-once 机制写出,label 带page:header action "…" visible,与行 kebab / 批量栏逐字同一措辞 —— 页头原来那条[page:header] … its predicate threw的本地实现随之删除。#2358 要的事实一件没少(隐藏的按钮报自己的名字、引用自己的谓词、只报一次),但引擎给的 reason 文本不再回显。页头本地只保留「谓词引用了 payload 里不存在的字段」这一条 —— 那是引擎看不到的成因(服务端会把hidden: true字段从详情 payload 里剥掉)。因此
page-header-actions.test.tsx里两条断言按新事实更新:措辞从predicate threw改为failed to evaluate(仍断言含page:header、动作名、谓词原文、去重一次);trap 3 从「一共一条警告」改为「成因一条 + 判决一条,各一次」—— 遗留引擎对缺席键返回undefined不抛错,所以以前只有第一条,CEL 对缺席键是 fault,判决那条是新增的信号。验证
消费半径已按调用方清扫:
page:header的渲染方(components 自身测试、app-shell 的RecordDetailView.*、plugin-detail 的 header 合成)全跑过;仓内没有别的record_header动作谓词夹具使用遗留语法(已 grep 确认)。两个新测试文件按约定登记进
vitest.config.mts的heavyDomTests(都经 ComponentRegistry 渲染)。⛔ 未扩进 objectstack#5970(spec 侧
ActionSchema.visible统一):isLegacyDialectSource回退在不动 spec 的前提下完整保住了,本单不需要 spec 变更。Generated by Claude Code