You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Record detail header ignores userActions.delete.visibleWhen — the BOOLEAN form of the same key reaches all three delete surfaces, the predicate form only reaches the row kebab #4419
userActions.delete reaches the record detail header in its boolean form but not in its predicate form. Same key, same button, two different scopes — and nothing warns the author.
userActions: { delete: false } → Delete disappears from the row kebab, the selection bar and the detail header.
userActions: { delete: { visibleWhen: … } } → the row kebab honours the predicate; the detail header keeps rendering Delete on records the predicate excludes.
So an author who upgrades from "nobody may delete this object" to "these rows may not be deleted" — the exact reason the object form exists — silently loses two of the three surfaces they had.
Repro (framework examples/app-showcase, console at pinned 6314e87, signed in as ADMIN so permissions are not a variable)
showcase_invoice already ships the declaration (src/data/objects/invoice.object.ts:58):
Row kebab evaluates the predicates: plugin-grid/src/components/RowActionMenu.tsx:299-301 → isBuiltinRowActionVisible(input.deletePredicates, 'delete', row, scope, objectFields), fed by resolveRowCrudAffordances (ObjectGrid.tsx:1659). The child-row menu does the same in plugin-detail/src/RelatedList.tsx:1111-1112.
Detail header does not: plugin-detail/src/DetailView.tsx:306-307 resolves showDelete: gatedSchema.showDelete && objectAllowsDelete && canDeleteRecord — bucket ∧ object affordance ∧ record writability. userActionPredicates(objectSchema?.userActions?.delete) is never called on this path, even though RelatedList in the same package already imports it.
The record is in hand on this surface and the CEL evaluator is already in context (the header renders readonlyWhen-driven field states from the same record), so the input the predicate needs is present.
Why this is a defect and not "per spec"
The predicate block's doc-comment (spec/src/data/object.zod.ts:1053-1070) says "the row's button", which the current implementation matches literally. But the key it modifies is documented one level up as governing "the generic CRUD affordances that the UI surfaces" (object.zod.ts:1410-1421), and its boolean form demonstrably does reach all three. A key whose scope shrinks when you add a predicate to it is not something an author can predict, and the failure is silent — ADR-0078 (no silently-inert metadata) is the rule this lands on.
Consequence in practice: visibleWhen is advisory UI gating (the spec says so — enforcement stays with permissions/hooks), so a hidden-on-the-list record stays deletable from its own detail page. An author who tested the list believes it is gated.
Suggested fix
Fold userActionPredicates(objectSchema?.userActions?.delete / .edit) into DetailView's showDelete / showEdit the way RelatedList already does, evaluated against the open record. disabledWhen should map to a disabled header button for symmetry with the kebab.
Ruling (delegated authority; open veto window): fix as the card suggests — the detail header folds userActionPredicates(objectSchema?.userActions?.delete / .edit) into showDelete/showEdit, evaluated against the open record, through the same helper family the row kebab and RelatedList already use (one evaluator, one answer across surfaces — no fresh predicate-evaluation code on this path). disabledWhen maps to a DISABLED header button, kebab symmetry. The card's ADR-0078 framing is adopted: a key whose scope shrinks when the author upgrades boolean → predicate is silently-inert metadata.
Scope guards: (1) #4420 (selection-bar half) is NOT here — it needs the per-record-predicate-over-a-SET semantics decision first and stays its own card. (2) #4296 (row kebab's permission-layer gap) is NOT here. (3) The header's existing gates (gatedSchema.showDelete ∧ objectAllowsDelete ∧ canDeleteRecord) all REMAIN — the predicate is a fourth conjunct, not a replacement.
Red-first (the card's own repro shape): record where the predicate is FALSE (paid invoice) → pre-fix the detail header still offers Delete (capture), post-fix hidden. Must-not-change (green both sides): boolean delete: false still hides all three surfaces; predicate-TRUE record keeps Delete; disabledWhen on edit renders the header Edit disabled (new behavior — red-first it too); permissions/writability gates unchanged.
Grading: patch @object-ui/plugin-detail (.d.ts measured both ways). Never major.
Surface (mutual exclusion): packages/plugin-detail/src/DetailView.tsx + tests + one changeset. Coordination: in-flight #4468 (date-locale census) was instructed to REPORT rather than edit unexpected surfaces — you own DetailView.tsx; if its census names a date site in your file, it reports and you are unaffected. ⛔ NOT: RelatedList.tsx (already correct — read-only reference), plugin-grid (in-flight #4501 armed), selection-bar components (#4420), console/**, content/docs/releases/.
Activity
yinlianghui commentedon Aug 13, 2026
CLAIM — session
session_017Qqyix2QcnpUC9XeYVDzx3, branchclaude/issue-4419-detail-header-predicates.Ruling (delegated authority; open veto window): fix as the card suggests — the detail header folds
userActionPredicates(objectSchema?.userActions?.delete / .edit)intoshowDelete/showEdit, evaluated against the open record, through the same helper family the row kebab and RelatedList already use (one evaluator, one answer across surfaces — no fresh predicate-evaluation code on this path).disabledWhenmaps to a DISABLED header button, kebab symmetry. The card's ADR-0078 framing is adopted: a key whose scope shrinks when the author upgrades boolean → predicate is silently-inert metadata.Scope guards: (1) #4420 (selection-bar half) is NOT here — it needs the per-record-predicate-over-a-SET semantics decision first and stays its own card. (2) #4296 (row kebab's permission-layer gap) is NOT here. (3) The header's existing gates (
gatedSchema.showDelete ∧ objectAllowsDelete ∧ canDeleteRecord) all REMAIN — the predicate is a fourth conjunct, not a replacement.Red-first (the card's own repro shape): record where the predicate is FALSE (paid invoice) → pre-fix the detail header still offers Delete (capture), post-fix hidden. Must-not-change (green both sides): boolean
delete: falsestill hides all three surfaces; predicate-TRUE record keeps Delete;disabledWhenon edit renders the header Edit disabled (new behavior — red-first it too); permissions/writability gates unchanged.Grading: patch
@object-ui/plugin-detail(.d.tsmeasured both ways). Never major.Surface (mutual exclusion):
packages/plugin-detail/src/DetailView.tsx+ tests + one changeset. Coordination: in-flight #4468 (date-locale census) was instructed to REPORT rather than edit unexpected surfaces — you own DetailView.tsx; if its census names a date site in your file, it reports and you are unaffected. ⛔ NOT: RelatedList.tsx (already correct — read-only reference), plugin-grid (in-flight #4501 armed), selection-bar components (#4420), console/**,content/docs/releases/.Generated by Claude Code
Generated by Claude Code
fix(plugin-detail): the record detail header honors userActions predi…
fix(app-shell,plugin-list): 关联列表「+ New」消费 create 谓词,行内编辑开关补 update 权限门 (