Skip to content

[console] 记录详情页头不消费 userActions.<action>.visibleWhen:同一谓词列表行已生效、记录页头照旧渲染按钮 #4213

Description

@baozhoutao

现象

对象上声明的 userActions.edit.visibleWhen(逐记录谓词,spec RowCrudActionOverrideSchema)只在列表「操作」列被消费,记录详情页头不消费——详情页头的【编辑】按钮无视谓词照旧渲染。

版本:@objectstack/*@17.0.0-rc.6(rc.5 同样表现)。

复现

对象声明(节选):

userActions: {
  edit: { visibleWhen: 'record.status == "pending" || record.status == "in_progress"' },
},

编译产物里已确认该谓词写入元数据。实测同一账号、同一条记录(status 为 reported,谓词应判 false):

入口 结果
列表视图「操作」列的行内【编辑】 ✅ 谓词生效,按钮按状态收敛,不出现
记录详情页头的【编辑】 ❌ 谓词未被消费,按钮照旧渲染,可点开编辑弹窗

一条定位线索:同一结构里的布尔开关两处都生效

同为 userActions 下的声明,对象级布尔开关 userActions: { delete: false } 是两处都消费的——列表行操作菜单与记录详情页头的删除入口会一起消失(我们在另一个项目工作项里验证过)。

而逐记录谓词 visibleWhen 只有列表消费。两者对比指向:record header 的动作渲染路径读到了 userActions 的静态开关,但没有接谓词求值这一段。

期望

记录详情页头与列表行一致地消费 userActions.<action>.visibleWhen,谓词判 false 时不渲染该内建动作入口。

影响

下游应用要做「按记录状态收敛编辑入口」时,只能收敛列表侧,记录页仍能点进编辑表单。功能安全可由服务端守卫兜住(提交被拒),但入口不干净,用户会先看到按钮、点开表单、填完才被拒。

关联

备注

发现于下游应用项目的真机 UI 实测(私有仓,截图不便直接引用)。如需公开可复现的最小样例,我可以在 showcase 类示例应用上构造一份,告诉我需要即可。

Activity

  1. claude commented on Aug 11, 2026

    @claude
    Contributor

    Triage: pm:queue — concrete defect, anchored landing, repro on the card. Whole-repo seat: no domain:*/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 is resolveRecordHeaderActionGates (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-record userActions.&lt;action&gt;.visibleWhen evaluation anywhere on that path. The list side does evaluate it: packages/components/src/renderers/complex/data-table.tsx:235-246 (evalRowActionVisibility on builtin row actions). That asymmetry is exactly the card's observed behaviour, and also explains the boolean-switch clue (the static userActions switch 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

  2. yinlianghui commented on Aug 13, 2026

    @yinlianghui
    Collaborator

    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-307 and is being fixed there. That is a real site, but it is not the one this issue's console repro screenshots. Measured on origin/main @ 7ffd61658:

    • packages/plugin-detail/src/renderers/record-details.tsx:257 synthesizes the inner detail-view with showHeader: schema.showHeader ?? false, and never sets showEdit / showDelete. Composed under a Lightning page:header, DetailView's own header — and therefore its showEdit/showDelete conjunction — does not render at all.
    • packages/app-shell/src/views/RecordDetailView.tsx contains zero < DetailView mounts (the ADR-0085 PR4 note at line 355 records the monolith's removal). The console record header's Edit / Share / Delete are synthesized by synthSystemActions at RecordDetailView.tsx:1911:
    1912    const objectAffordances = resolveRecordHeaderActionGates(objectDef, effectiveApiOperations);
    1915      edit: objectAffordances.edit && recordWriteAllowed,
    1916      delete: objectAffordances.delete && recordDeleteAllowed,
    

    resolveRecordHeaderActionGates returns resolveEffectiveCrudAffordances(...).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. No userActionPredicates / useRowPredicate call exists anywhere in RecordDetailView.tsx.

    So the two sites are:

    site header served by predicate today
    console record page (this issue's repro) app-shell RecordDetailView.synthSystemActions not consumed
    RecordDetailDrawer, and any host mounting detail-view / detail with showEdit/showDelete plugin-detail DetailView header fixed by #4419

    #4419's PR is deliberately scoped to DetailView.tsx by its ruling, so it closes the second row only. Closing this issue needs the app-shell half — same helper family (userActionPredicates from @object-ui/core + useRowPredicate from @object-ui/react), ANDed as a fourth conjunct beside objectAffordances and recordWriteAllowed / recordDeleteAllowed, with disabledWhen mapping onto the disabled key sys_edit already uses for approvalLocked.

    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

  3. self-assigned this
    on Aug 13, 2026
  4. yinlianghui commented on Aug 13, 2026

    @yinlianghui
    Collaborator

    CLAIM — session session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/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's synthSystemActions gates on objectAffordances.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 (useRowPredicate from @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. visibleWhen false ⇒ the synth action is not emitted; disabledWhen true ⇒ map onto the disabled key sys_edit already 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: false is a DECLARED gate (#3492 invariant), and the canonical {dialect, source} envelope is accepted.

    Red-first (the card's own repro): record whose edit.visibleWhen judges false (status: 'reported') → pre-fix the console record header renders 【编辑】 (capture); post-fix absent. Must-not-change (green both sides): boolean delete: false still hides (the card's own 定位线索 — both surfaces already consume it); predicate-true records keep their buttons; the permission/writability gates still win independently; sys_share and non-CRUD synth actions untouched; approvalLocked's existing disabled behavior unchanged.

    Grading: patch @object-ui/app-shell (.d.ts measured 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions