Skip to content

userActions.edit.visibleWhen 对详情页页头内建【编辑】不生效 —— 同一份声明里 delete 生效、edit 不生效(17.0.0-rc.6) #8499

Description

@baozhoutao

现象

平台版本 @objectstack/*@17.0.0-rc.6。

同一份对象级 userActions 声明中,两个动作用完全同形的谓词,收敛结果却不一致:delete.visibleWhen 生效,edit.visibleWhen 在记录详情页页头的内建【编辑】按钮上不生效。

逐字的对象声明:

userActions: {
  edit:   { visibleWhen: P`os.user.id != record.executor` },
  delete: { visibleWhen: P`os.user.id != record.executor` },
},

复现与实测

以目标角色账号(即该记录 executor 本人)登录 Console,打开该记录的详情页:

入口 实测 是否符合谓词
列表行入口的【删除】 已按谓词隐藏 ✅
页头「⋯」溢出菜单里的【删除】 已按谓词隐藏 ✅
页头内建【编辑】 仍然显示 ❌

点开页头【编辑】得到的是可编辑表单;提交时被服务端权限 / RLS 拒绝,数据未发生变更 —— 所以这不是数据完整性问题,是纯粹的 UI 入口收敛问题。

A/B 对照(用于排除项目侧写法问题):同一构建、同一账号、同一会话下,两个不同业务对象使用同一形态的 userActions 声明,表现完全一致 —— 均为「delete 生效、页头 edit 不生效」。因此不是单个对象的声明写错。

影响

业务上「某角色对该记录只读」这类需求,UI 入口收不干净:只能依赖服务端拦截,用户会先看到入口、点进去、填完再被拒。体验之外,合规审计也会把「能看到编辑入口」记为缺陷 —— 每轮 QA 都会重复报同一条。

期望

  1. userActions.edit.visibleWhen 对详情页头的内建【编辑】按钮同样生效,与 delete(以及列表行入口)行为一致;
  2. 若设计上「页头内建入口不受该谓词约束」是有意为之,请在文档中明示这条边界,并提供一个可用的收敛方式 —— 目前没有任何其他杠杆可以隐藏这颗按钮。

相邻单(非重复)

Activity

  1. added theissue type on Aug 13, 2026
  2. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Triage: lands in objectui's detail-page header (the built-in Edit button's visibility wiring) ⇒ seam card stays here with repo:objectui, for the objectui whole-repo seat. Queued as Bug: same-shape predicate honoured by delete (list row + overflow menu) and ignored by the built-in header edit — an A/B-verified UI-entry convergence defect on rc.6. Not p0: the server-side permission/RLS backstop holds (submit is rejected, no data-integrity exposure); the cost is UX + repeated QA findings.

    Adjacent, not duplicate: #3076 (the per-record CEL capability this is a gap in), #7692/#7758 (the create face, resolved). Sibling of #8500 — same page surface, different mechanism (declared visibleWhen not consumed vs CEL evaluation fail-open); serial-check the file surface if both are in flight at once.

    Size/model suggestion: S–M.

    本评论来自分诊座位 Routine。


    Generated by Claude Code

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

    @yinlianghui
    Collaborator

    Claim: PM loop round 4
    Session: session_01RnQd8iMMUwXQEV1crFmQiQ
    Branch: claude/issue-8499-builtin-edit-visiblewhen
    Worktree: objectui-issue-8499
    Domain: repo:objectui
    File surface: packages/app-shell/src/views/RecordDetailView.tsx (built-in edit/delete predicate wiring region ~1040-1070 + the header button consumption site) + its tests (stop on breach; explain in the report)
    Container & model: S–M, mode:subagent, model: opus
    Serial constraints cleared: sibling #8500 is CLOSED (its PR #4641 added tests for DECLARED header actions only — the built-in path this card is about was deliberately untouched there); in-flight #8018 declares plugin-grid + useConsoleActionRuntime + projection builders (disjoint files); no open dev PR touches RecordDetailView (checked this round). Premise caution in the dispatch: current main already carries editVisible = useRowPredicate(editPredicates?.visibleWhen ?? true, …) — the card was measured on rc.6 and may be already fixed; closed-PR search mandatory before writing.


    Generated by Claude Code

  5. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    Collaborator
    {
      "issue": 8499,
      "status": "done",
      "branch": "claude/issue-8499-builtin-edit-visiblewhen",
      "pr": "https://github.com/objectstack-ai/objectui/pull/4666",
      "premise_still_valid": false,
      "summary": "The defect is NOT live on main. The card was measured on 17.0.0-rc.6; the wiring landed between rc.6 and 17.5.0 on both detail surfaces — objectui PR #4524 (objectui#4213) for the console record page (RecordDetailView.tsx) and PR #4515 (objectui#4419) for plugin-detail's DetailView — and current main computes editVisible AND consumes it as a fourth conjunct of the sys_edit affordance gate. The PM's fallback hypothesis (the consumption seam is unwired) is falsified too: the seam reads editVisible correctly, and the mobile twin sys_edit_mobile converges via the same schema.showEdit. What WAS missing is coverage of the card's own predicate shape: 'os.user.id != record.executor' reads the SESSION USER against a RELATION field, and the two existing pins bracket that shape from opposite sides without covering it — the #4213 built-in pin uses only self-contained record.status compares and mounts no ExpressionProvider (so os.user was bound nowhere on the built-in path), while the #4641 lookup pin drives os.user.id against a relation but only through DECLARED record_header actions. Delivered that missing pin: 8 cases, both relation payload shapes (bare FK and $expand'd), each as a discriminating PAIR because visibleWhen fails CLOSED — the executor gets no Edit, any other session user keeps it, so an absence-only assertion could not pass for the wrong reason.",
      "tests": "All at final head 3fcc3311f (quoted from `git rev-parse --short HEAD` after the last commit; main moved to 6d4f5bd59 during the run but only touched content/docs/components/disclosure/toggle-group.mdx, so the run still covers the tree). Build closure first: `pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build` => EXIT=0. New pin alone: 8 passed. Dispatch-named gate `pnpm exec vitest run packages/app-shell/src/views` => 243 files, 2311 passed | 1 skipped, EXIT=0. `pnpm --filter @object-ui/app-shell type-check` => EXIT=0, and it genuinely RAN (banner '> @object-ui/app-shell@17.5.0 type-check' + 'tsc --noEmit && tsc -p tsconfig.test.json', so the new test file is type-checked — not a zero-match silent pass). REVERSE VERIFICATION, direction predicted before running: ablating the `&& editVisible` conjunct (the rc.6 state) was predicted to redden the five 'Edit absent' assertions and leave the three 'Edit present' controls green; observed exactly that — 'Tests 5 failed | 3 passed (8)'. Restored via `git checkout <branch> -- <path>` from the branch's own commit (the pin was committed BEFORE the ablation); tree clean afterwards, RecordDetailView.tsx byte-unchanged vs main. Changeset: tests-only, so the gate's own prescribed exemption — check-changeset-presence.mjs first ran RED (exit 1, 'no changeset'), and after adding an EMPTY-frontmatter changeset it returned exit 0 with 'Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate.' `pnpm changeset:check` => exit 0 (fixed group, no major). `pnpm check:control-bytes` => OK (4192 tracked text files); targeted control-byte self-scan of the new file => no hits. PR labels read back after bots settled: ['tests'].",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Plain-language outcome: the reported behaviour already works on main — this is a premise falsification, not a re-implementation. The card still closes because the one thing genuinely missing (a regression pin for the exact os.user + relation predicate shape on the built-in Edit) is delivered in objectui#4666, verified to go red without the fix.

    No file-surface breach: only the new test file and its changeset were added; RecordDetailView.tsx is unchanged. The sibling in flight (#8018, plugin-grid + useConsoleActionRuntime) stayed disjoint.


    Generated by Claude Code


    Generated by Claude Code

  6. yinlianghui commented on Aug 15, 2026

    @yinlianghui
    Collaborator

    ACCEPT (premise-falsified + coverage delivered) — PR objectstack-ai/objectui#4666 (reviewer of record: seat session session_01RnQd8iMMUwFupVRt… correction: session_01RnQd8iMMUwXQEV1crFmQiQ).

    The defect is not live on objectui main: the wiring landed between rc.6 and 17.5.0 on both detail surfaces (PR #4524 for the console record page, PR #4515 for plugin-detail), and the dispatch's fallback hypothesis (unwired consumption seam) was also falsified — editVisible is consumed as a conjunct of the sys_edit gate. What the card actually exposed is a coverage hole at the intersection of two existing pins: session-user (os.user) × relation field × the built-in path had never been evaluated under test. Delivered as an 8-case pin where every case is a discriminating pair (executor loses Edit, any other user keeps it — the "present" half is what a fail-closed fault cannot fake), both payload shapes, plus a no-fault-warning assertion. Reverse verification restored the rc.6 state and reddened exactly the predicted five. Fixes line is correct: with behavior verified and the pin added, this card closes on merge.

    @baozhoutao — same disposition as #8500/#8282: your rc.6 measurement was accurate for rc.6; on ≥17.5.0 userActions.edit.visibleWhen governs the header's built-in Edit exactly as delete. Please re-test on a refreshed image; reopen with the build hash if it still reproduces.

    Landing: ready + auto-merge once gates conclude green on head 3fcc3311f.


    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

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions