Uh oh!
There was an error while loading. Please reload this page.
test: pin the detail/form edit/delete gate to the server's effective operation set (#3546) - #2876
Merged
Merged
Conversation
…operation set (#3546) objectui#2823 (PR-4) intersected the LIST/TOOLBAR affordances with the server-resolved effective operation set, and objectui#2832 extended that intersection to the detail/form surfaces. That PR's coverage stopped at `@object-ui/core`'s `isObjectInlineEditable` plus a RelatedRecordActionsBridge component test, leaving the three surfaces the issue actually names — the detail header's Edit/Delete, the record-body inline-edit gate, and the form's blanket field lock — pinned only by "existing suites still pass". This closes the issue's last checklist item: button show/hide when the effective set does and does not carry `update` / `delete`. - app-shell `RecordDetailView.headerActionGates` (11 cases) — the synthesized `sys_edit` / `sys_delete` gate across full-CRUD / read-only / update-only / delete-only / empty / undefined / null effective sets. - plugin-detail `record-details.effectiveOps` (7 cases) — the `inlineEdit` flag handed to DetailView, incl. the author `inlineEdit: false` opt-out. - plugin-form `ObjectForm.effectiveOps` (7 cases) — the rendered input's `disabled` state, incl. create mode keying off `create` rather than `update`. Each suite also pins the two properties that make this an INTERSECTION and not a union: a server grant never re-opens an affordance the lifecycle bucket closed, and a `userActions` opt-in never survives a server denial. The `undefined` effective set (unrestricted object / old backend / no PermissionProvider) is asserted to preserve the pre-#3546 behavior. To make the header gate reachable from a unit test, its resolution is extracted from the `RecordDetailView` body into an exported `resolveRecordHeaderActionGates(objectDef, effectiveApiOperations)` — the same pattern `ObjectView` uses for `defaultListColumnsFromObject`, since the record page is wired into routing, auth, presence and data fetching too deeply to render in a unit test. Behavior-preserving: it returns the `edit` / `delete` bits of the identical `resolveCrudAffordances(objectDef, effectiveApiOperations)` call it replaces, and it is not added to the package's public export list. No changeset — no behavior or public API change. Verified by mutation: dropping the effective-ops argument at each of the three call sites fails 14 of the new tests, so they pin the wiring rather than restating core's unit tests. Full suites for the touched packages green (212 files / 1953 tests); tsc build green; eslint 0 errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YEcwUzbuzU8LM5dk4pYQQA
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-zhuang
marked this pull request as ready for review
July 27, 2026 15:48
Uh oh!
There was an error while loading. Please reload this page.
os-zhuang added a commit
that referenced
this pull request
Jul 28, 2026
…e operation set (objectstack#3720) (#2889) The fourth surface objectstack#3391 left open. The toolbar (#2823), detail/form (#2832 + #2876) and related lists (#2832) all route through `resolveCrudAffordances`; the main list's row CRUD has its own resolver and none of those rounds reached it. Its gate was `operations ?? { update: !!onEdit, delete: !!onDelete }`, and ObjectView wires onEdit/onDelete unconditionally while view JSON rarely declares `operations` — so it was effectively always-on. A caller whose effective set carried neither `update` nor `delete` still got the row kebab's Edit/Delete and the bulk delete. - plugin-grid `resolveRowCrudAffordances` takes `managedBy` + `effectiveApiOperations` and resolves the object verdict through the shared `resolveCrudAffordances` policy, so the row gate is the same decision every other face makes. It also returns `objectCanDelete` — bulk delete rides `onBulkDelete`, a different callback from the row `onDelete`, so it must not be judged by whether the row handler happens to be wired. - plugin-grid `ObjectGrid` threads its existing `effectiveApiOps` (until now fed only to Export) into the row gate, and applies the delete verdict to bulk delete: the implicit `['delete']`, a declared `bulkActions: ['delete']`, and any `bulkActionDefs` entry with `operation: 'delete'`. Custom ids and non-delete operations pass through untouched. - plugin-list `ListView`'s own bulk bar (the non-grid views) drops its built-in `delete` under the same verdict. Also closes the ADR-0103 gap on this chain: the bucket lock was documented as applied upstream via the view's `operations.*`, but the all-open default meant it never was — an engine-owned system / append-only / better-auth object leaked a generic row Edit/Delete the engine rejects. A `userActions` opt-in still re-opens it. Intersection, never union. A missing effective set preserves current behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
objectstack#3546 的实现已由 objectui#2832 合入 main —— detail/form 面的 edit/delete 确实已经与服务端 effective 操作集(
/me/permissions的apiOperations)取交集。但该 issue 的第三条勾选项(「测试:effective 含/不含 update·delete 时按钮显隐」)只完成了一半:@object-ui/core的isObjectInlineEditable单测 +RelatedRecordActionsBridge组件测试(子对象相关列表)。也就是说:在这三处任一处把
resolveCrudAffordances/isObjectInlineEditable的第二参删掉,合入前的测试全绿。本 PR 补齐这部分覆盖,不改变任何行为。改动
新增测试(25 例)
RecordDetailView.headerActionGates.test.tsx(11 例)—— 合成的sys_edit/sys_delete门,覆盖 full-CRUD / read-only / update-only / delete-only / 空集 /undefined/null七种 effective 集。record-details.effectiveOps.test.tsx(7 例)—— 交给<DetailView>的schema.inlineEdit,含作者inlineEdit: falseopt-out 优先级。ObjectForm.effectiveOps.test.tsx(7 例)—— 渲染出的 inputdisabled实际状态,含 create 模式按create(而非update)判定。三个套件都额外钉住使其成为交集而非并集的两条性质:
system即使 effective 全开仍锁);userActionsopt-in 永不盖过服务端拒绝(sys_user开了edit,但 effective 无update时仍锁)。以及向后兼容:effective 缺失(全开对象 / 旧后端 / 未挂
PermissionProvider)保持 #3546 之前的行为。生产代码(行为不变)
detail header 的门原本内联在
RecordDetailView函数体里,而该组件深度耦合 routing / auth / presence / 数据拉取,无法在单测中渲染。故把这一处判定抽成导出的纯函数resolveRecordHeaderActionGates(objectDef, effectiveApiOperations)——与ObjectView的defaultListColumnsFromObject同一套路。行为等价:返回的就是它所替换的那次
resolveCrudAffordances(objectDef, effectiveApiOperations)的edit/delete两位;两个调用点(header 合成 +InlineEditProvider的canEdit)原样改调。该函数未加入包的 public export 列表,不构成公开 API 变化。验证
tsc构建绿(app-shell / plugin-detail / plugin-form 及其依赖,29 个 task)。no-explicit-anywarning)。无 changeset
无行为变化、无公开 API 变化,按 AGENTS.md §9(feature 才需要 changeset)不写 changeset。CI 的
changeset-check只校验 fixed group 配置,不要求每个 PR 带 changeset。关联
objectstack#3546(本 issue,实现已由 objectui#2832 合入,本 PR 补其测试项)、objectstack#3391(跟踪)、objectui#2823(PR-4)。
Generated by Claude Code