Repository navigation
[console] 记录详情页头不消费 userActions.<action>.visibleWhen:同一谓词列表行已生效、记录页头照旧渲染按钮 #4213
Description
Activity
Triage:
pm:queue— concrete defect, anchored landing, repro on the card. Whole-repo seat: nodomain:*/target:*labels here by protocol.Landing verified on
origin/main@e1ade8f(not just from the card's prose): the record-detail header's Edit gate isresolveRecordHeaderActionGates(packages/app-shell/src/views/RecordDetailView.tsx:194-200), which resolves only the static CRUD affordances intersected with effective API operations, then folds in the record-level write gate (:2069) — no per-recorduserActions.<action>.visibleWhenevaluation anywhere on that path. The list side does evaluate it:packages/components/src/renderers/complex/data-table.tsx:235-246(evalRowActionVisibilityon builtin row actions). That asymmetry is exactly the card's observed behaviour, and also explains the boolean-switch clue (the staticuserActionsswitch flows through the affordances resolver; the predicate never does).Dup check: no open shadow in any of the three repos (probes:
visibleWhen,RecordDetailView, record header). QA umbrella #7439's records-forms run passed its conditional-rules items — different surface (field rules, not header actions), no overlap. ui#2614 (capability) and ui#4096 (permission gate on list inline actions) are both closed neighbours, correctly cross-referenced by the card. Open PRs: none touch this path.The author's offer of a public showcase repro is welcome but not blocking — the code-level gap is verifiable as above.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Measured while implementing #4419 — the console record header is a SECOND site, and #4419's fix does not reach it. Commenting here rather than filing a twin, since this issue already owns that surface.
#4419 anchors the defect at
packages/plugin-detail/src/DetailView.tsx:306-307and is being fixed there. That is a real site, but it is not the one this issue's console repro screenshots. Measured onorigin/main@7ffd61658:packages/plugin-detail/src/renderers/record-details.tsx:257synthesizes the innerdetail-viewwithshowHeader: schema.showHeader ?? false, and never setsshowEdit/showDelete. Composed under a Lightningpage:header,DetailView's own header — and therefore itsshowEdit/showDeleteconjunction — does not render at all.packages/app-shell/src/views/RecordDetailView.tsxcontains zero< DetailViewmounts (the ADR-0085 PR4 note at line 355 records the monolith's removal). The console record header's Edit / Share / Delete are synthesized bysynthSystemActionsatRecordDetailView.tsx:1911:
1912 const objectAffordances = resolveRecordHeaderActionGates(objectDef, effectiveApiOperations); 1915 edit: objectAffordances.edit && recordWriteAllowed, 1916 delete: objectAffordances.delete && recordDeleteAllowed,resolveRecordHeaderActionGatesreturnsresolveEffectiveCrudAffordances(...).edit / .delete— the boolean/affordance channel only. That is precisely the "定位线索" in this issue's body, now with a file:line: the static switch is consumed (via the affordance resolver), the per-record predicate is never parsed or evaluated on this path. NouserActionPredicates/useRowPredicatecall exists anywhere inRecordDetailView.tsx.So the two sites are:
site header served by predicate today console record page (this issue's repro) app-shellRecordDetailView.synthSystemActionsnot consumed RecordDetailDrawer, and any host mountingdetail-view/detailwithshowEdit/showDeleteplugin-detailDetailViewheaderfixed by #4419 #4419's PR is deliberately scoped to
DetailView.tsxby its ruling, so it closes the second row only. Closing this issue needs theapp-shellhalf — same helper family (userActionPredicatesfrom@object-ui/core+useRowPredicatefrom@object-ui/react), ANDed as a fourth conjunct besideobjectAffordancesandrecordWriteAllowed/recordDeleteAllowed, withdisabledWhenmapping onto thedisabledkeysys_editalready uses forapprovalLocked.Not filed as a new issue: this is that half of the work, and it belongs to this card.
Generated by Claude Code
Generated by Claude Code
CLAIM — session
session_017Qqyix2QcnpUC9XeYVDzx3, branchclaude/issue-4213-record-header-predicates. This card is the CONSOLE half of the asymmetry PR #4515 (#4419) just fixed for the DetailView surface — the file:line evidence in the comment above (from that PR's dev) is the location:RecordDetailView.tsx:1911'ssynthSystemActionsgates onobjectAffordances.edit && recordWriteAllowed/.delete && recordDeleteAllowed— the boolean affordance channel only, zero predicate calls in the file.Ruling (delegated authority; open veto window): fold the SAME helper family —
userActionPredicates(from@object-ui/core) parsed once, evaluated per the open record with the same evaluator semantics (useRowPredicatefrom@object-ui/react,{fallback:false, warnOnError:true, fields}— the shape PR #4515 established and RowActionMenu/RelatedList already share) — as a FOURTH conjunct beside the existing gates.visibleWhenfalse ⇒ the synth action is not emitted;disabledWhentrue ⇒ map onto thedisabledkeysys_editalready uses (the approvalLocked precedent at the same site). One evaluator, one answer, now on all three surfaces (row kebab, DetailView header, console record header). The #4419-established extra pins carry over:visibleWhen: falseis a DECLARED gate (#3492 invariant), and the canonical{dialect, source}envelope is accepted.Red-first (the card's own repro): record whose
edit.visibleWhenjudges false (status: 'reported') → pre-fix the console record header renders 【编辑】 (capture); post-fix absent. Must-not-change (green both sides): booleandelete: falsestill hides (the card's own 定位线索 — both surfaces already consume it); predicate-true records keep their buttons; the permission/writability gates still win independently;sys_shareand non-CRUD synth actions untouched; approvalLocked's existing disabled behavior unchanged.Grading: patch
@object-ui/app-shell(.d.tsmeasured both ways). Never major.Surface (mutual exclusion):
packages/app-shell/src/views/RecordDetailView.tsx(+resolveRecordHeaderActionGates's home if the fold naturally lands beside it — measure, name it) + tests + one changeset. ⛔ NOT: selection-bar (#4420, undecided semantics), DetailView/RelatedList (landed #4515's surface),console/AppContent.tsx(in-flight #4252), metadata-admin (in-flight #4446),content/docs/releases/.
Generated by Claude Code
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026
现象
对象上声明的
userActions.edit.visibleWhen(逐记录谓词,specRowCrudActionOverrideSchema)只在列表「操作」列被消费,记录详情页头不消费——详情页头的【编辑】按钮无视谓词照旧渲染。版本:
@objectstack/*@17.0.0-rc.6(rc.5 同样表现)。复现
对象声明(节选):
编译产物里已确认该谓词写入元数据。实测同一账号、同一条记录(
status为reported,谓词应判 false):一条定位线索:同一结构里的布尔开关两处都生效
同为
userActions下的声明,对象级布尔开关userActions: { delete: false }是两处都消费的——列表行操作菜单与记录详情页头的删除入口会一起消失(我们在另一个项目工作项里验证过)。而逐记录谓词
visibleWhen只有列表消费。两者对比指向:record header 的动作渲染路径读到了userActions的静态开关,但没有接谓词求值这一段。期望
记录详情页头与列表行一致地消费
userActions.<action>.visibleWhen,谓词判 false 时不渲染该内建动作入口。影响
下游应用要做「按记录状态收敛编辑入口」时,只能收敛列表侧,记录页仍能点进编辑表单。功能安全可由服务端守卫兜住(提交被拒),但入口不干净,用户会先看到按钮、点开表单、填完才被拒。
关联
备注
发现于下游应用项目的真机 UI 实测(私有仓,截图不便直接引用)。如需公开可复现的最小样例,我可以在 showcase 类示例应用上构造一份,告诉我需要即可。