Skip to content

fix(plugin-detail): the record page's inline edit reads the stored row's field locks - #27226

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-fsl9rk
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-fsl9rk

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #27146

维护者速读

  • 改了什么:记录详情页的行内编辑(record:details 正文与 record:highlights 高亮条)改问服务器对这条记录的字段投影,不再只读不含行的 /auth/me/permissions。
  • 为什么改:带 when 的字段锁(如审批后锁定摘要)在草稿行上也被禁用,而服务器其实允许写;编辑对话框已在 fix(ui): the record form's edit dialog reads the record's field projection #27137 修好,详情页是剩下的一处。
  • 风险与回滚:只在行内编辑会话可编辑、记录有 id、数据源有 getRecordFieldAccess 时才多发一次请求;两个区块同页时各问一次。回滚即还原三个 plugin-detail 文件。
  • 先看:ui/packages/plugin-detail/src/recordFieldAccess.tsx、renderers/record-details.tsx、renderers/record-highlights.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-detail/src/renderers/__tests__/record-blocks.recordFieldAccess-27146.test.tsx,预期 10 个测试全绿。
  • 需要你做的:无,席位落地。

Clause-②: no
Metadata-protocol: none

What changes

  • New ui/packages/plugin-detail/src/recordFieldAccess.tsx (RecordFieldAccessForRecord, internal, not exported): mounts RecordFieldAccessScope (@object-ui/permissions) for the block's stored record when the page's inline-edit session may edit (canEdit), the block offers in-place edit, the record has an id and the data source carries getRecordFieldAccess. Below it, the unchanged resolveFieldAffordance('recordEdit', …) calls in DetailSection and HeaderHighlight read the server's projection for that row (ruling 6457467702, letter B: the server's answer, no client when evaluator). It asks again on a write the data source reports for the record and on the data-invalidation bus for it, the triggers the form's RecordFieldAccessForForm holds.
  • record:details wraps its DetailView in it (gated on its own inlineEdit verdict); record:highlights wraps its HeaderHighlight.
  • content/docs/permissions/field-level-security.mdx: the record-scoped projection paragraph now names the record page's in-place edit as a reader, and states the card's declared limitation — a list view's inline edit locks a whole column and keeps the row-blind answer.
  • Changeset: @object-ui/plugin-detail patch.

Why the mount sits on the two blocks, not on the page host

Measured on origin/main b322b5da: record:details and record:highlights are sibling blocks of the page schema; their nearest common ancestors are the page host's providers in app-shell RecordDetailView and the generic page renderer, so no plugin-detail point covers both. The dispatch's fallback was one wrapper in RecordDetailView, but a scope there would also cover the page's action dialogs and related lists: RecordFieldAccessScope dedupes only a nested scope for the SAME record, so a form editing ANOTHER row of the same object (a self-referencing child, say) would read this row's answer until its own arrived, or for good if its ask failed. Mounting on the two bodies keeps the answer on the surfaces that edit this row. The cost: a page drawing both blocks asks twice per load and per save.

Why a copy of the trigger lines rather than a shared export

RecordFieldAccessForForm lives in @object-ui/plugin-form and is not exported; plugin-detail does not depend on plugin-form, and @object-ui/permissions does not depend on @object-ui/react (the bus hook). Sharing it would mean a new published export (Clause-② yes) or a new dependency edge, so the ~20 trigger lines are repeated with a note naming the twin.

Tests

ui/packages/plugin-detail/src/renderers/__tests__/record-blocks.recordFieldAccess-27146.test.tsx, for each of record:details and record:highlights under the record page's providers (MePermissionsProvider serving the conditioned entry as its lock, record context, inline-edit session):

  • a draft the server unlocks: summary edits in place, ref_no (locked on every row) stays a read display;
  • an approved row: summary stays locked;
  • a write after which the server judges the row approved: asked again, summary locks;
  • CONTROL: a data source without getRecordFieldAccess keeps the row-blind lock;
  • CONTROL: a session that may not edit asks nothing.

Ablation (one-shot, not kept): scripts/ablation-replace.mjs turned the wrapper's if (!asks) into an always-true condition (the scope never mounts; anchor 1 → 0, blob 2cbc16ec79d0 → c38590de709a): 6 of 10 red (the draft, approved and re-ask rows for both blocks), the 4 controls green; restored blob == HEAD, git diff HEAD empty.

Acceptance notes

  • The record overlay payload RecordDetailPanel (kanban / calendar / gantt record drawers) draws the same DetailSection and its in-place edit still reads the row-blind answer. Not changed here: its saves run through the host's onFieldSave, so the re-ask trigger needs its own reading. Noted for the seat.
  • HeaderHighlight inside DetailView (an authored highlightFields header) is covered on the record page through the record:details mount.

Verification

Local runs
  • New suite: 10/10 passed.
  • pnpm --dir ui exec vitest run packages/plugin-detail/ + pnpm --filter @object-ui/plugin-detail type-check on 7d0d12c8: Test Files 322 passed, 1 skipped; Tests 2951 passed, 18 skipped; type-check exit 0 (lock VERDICT command-exit 0).
  • After merging origin/main (feat(app-shell,plugin-detail,components,i18n): the record page's timeline and related columns are each reader's per-object switches, toggled from the crumb row #27120 landed in plugin-detail, components, i18n): rebuilt @object-ui/i18n and @object-ui/components, re-ran packages/plugin-detail/src/renderers/ plus DetailSection.fieldWriteGrant-12103: 108 files, 1090 tests passed.
  • dispatch-gates --commands: 68 derived families, each run on 9baa1e1a and recorded exit 0 (check:skill-examples first exited 3, PREREQUISITE NOT MET, until @objectstack/client-react was built, then 0); --ran reconciliation: 68 run, 0 NOT-MEASURED, 0 UNRUN. check-sdui-manifest exit 0 among them.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (CI).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec untouched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the dispatch's required Chinese digest).
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…w's field locks

record:details and record:highlights wrap their bodies in the record field
projection scope, so a when-conditioned field lock disables a field only on
the rows the server's write gate refuses.

Claude-Session: https://claude.ai/code/session_01JQwxbZ6ALxKhRMeuJrvUeU
Co-authored-by: Claude <noreply@anthropic.com>
…ord field projection

Claude-Session: https://claude.ai/code/session_01JQwxbZ6ALxKhRMeuJrvUeU
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · seat domain:ui#5 · session_01JQwxbXGpVJHZTXXerMoY4C · head 9baa1e1a · 2026-10-28T11:32Z

席位意见:记录详情页的行内编辑原先读 /auth/me/permissions 的字段锁,这个答案不看具体行,所以带 when 条件的锁会把每一行都锁住,草稿行也锁。现在 record:details 和 record:highlights 两个块改为向服务端询问这一行自己的字段投影,用的是编辑对话框同一个来源(裁决 B),客户端不求值 when。

  1. Shape: Fixes #27146; 6 files, +349/−16: a new internal plugin-detail/src/recordFieldAccess.tsx (RecordFieldAccessForRecord, not exported), one wrapper each in renderers/record-details.tsx and renderers/record-highlights.tsx, one sentence in content/docs/permissions/field-level-security.mdx (the list view's column lock keeps the row-blind answer), 1 test file (10 tests), and an @object-ui/plugin-detail patch changeset.
  2. Premise measured: the order's mechanism held: DetailSection and HeaderHighlight read usePermissions() through resolveFieldAffordance, and under the published RecordFieldAccessScope that answer comes from the row's projection, so neither call site changed. The seat read the scope: it overrides only write for the scoped object, only for a field an entry locks, and keeps the parent answer until the row's answer lands (locked, never briefly open). load is not a trigger, so the inline closure does not re-ask on every render; revision (the data source's write report for this record plus the invalidation bus) does.
  3. Gates / check-runs: PR check-runs on 9baa1e1a: 50 success, 8 skipped (path-filtered), 0 failure; mergeable_state clean. Dev: 68 derived gates, all exit 0; 10/10 new tests; ablation turns 6 red, restored; plugin-detail suite 322 files green; main merged after feat(app-shell,plugin-detail,components,i18n): the record page's timeline and related columns are each reader's per-object switches, toggled from the crumb row #27120 landed.
  4. Accepted deviations: (a) mounted per block, not in RecordDetailView. A page-host scope would also cover action dialogs and related lists, where a form for another row of the same object would read this row's answer until its own landed. The cost is two asks on a page drawing both blocks. (b) The ~35-line ask/refresh wrapper repeats plugin-form's unexported RecordFieldAccessForForm rather than adding a new export (Clause-②: no); the reason is stated in the module header. Both are within the order's allowance.
  5. Out of scope: RecordDetailPanel (grid/kanban/calendar/gantt overlay) still reads the row-blind lock. Pointed at console: the grid, kanban, calendar and gantt record drawers mount the one record page (embedded RecordDetailView) instead of the quick-look RecordDetailPanel / DetailView path — one record rendering, the drawer stack and tiers everywhere #27057, which mounts the record page in those drawers (comment 6467597251); not filed separately. Skills-docs: field-level-security.mdx sentence added.
  6. Landing: not Tier H (no governed path); ready for landing (ready + auto-merge).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 11:34
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 11:34
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 2594f5b Oct 28, 2026
59 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-fsl9rk branch October 28, 2026 11:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

2 participants