Repository navigation
fix(plugin-form): the record edit dialog draws a record the caller may read but not write as read-only, says why, and offers no Save - #27313
Merged
Conversation
…l lock Claude-Session: https://claude.ai/code/session_01JQwxbYqMqySPCeFRdqDyHp Co-Authored-By: Claude <noreply@anthropic.com>
…e inline edit Claude-Session: https://claude.ai/code/session_01JQwxbYqMqySPCeFRdqDyHp Co-Authored-By: Claude <noreply@anthropic.com>
…ide double Claude-Session: https://claude.ai/code/session_01JQwxbYqMqySPCeFRdqDyHp Co-Authored-By: Claude <noreply@anthropic.com>
…roval-lock test Lint UI refused it as no-unused-vars. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbYqMqySPCeFRdqDyHp
Contributor
Author
席位核对ACCEPT · 席位意见:记录编辑对话框现在读服务端对“这一行”的
|
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 free
to 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.
Requested by Warren · project thread
Fixes #27138
Clause-②: no
Metadata-protocol: none
维护者速读
writable判决与审批锁;不可写的行整表只读、说明原因、不显示保存。writable: false或审批锁时收紧;缺writable的旧服务端行为不变;回滚即 revert 本 PR。ui/packages/permissions/src/RecordFieldAccessScope.tsx(refusedRow)、ui/packages/plugin-form/src/fieldWriteGate.ts(closedFormAffordance)、ui/packages/app-shell/src/console/AppContent.tsx(formApprovals)。pnpm --dir ui exec vitest run packages/plugin-form/src/recordFieldAccess.rowWritable-27138.test.tsx,预期 10 个用例全绿。What changes
The record edit surfaces now ask the server whether THIS row is writable, and read the approvals record lock the projection leaves out. No client-side re-derivation of sharing (ruling 6457467702, letter B).
RecordFieldAccess(@object-ui/types) gainswritable?: boolean, the record projection's row verdict (specGetRecordFieldAccessResponseSchema, landed by PR feat(security): the record field-access answer says whether the record itself is writable #27232).ObjectStackAdapter.getRecordFieldAccessalready returns the client's answer untouched, so the adapter is unchanged.RecordFieldAccessScope(@object-ui/permissions): onwritable: false,refusedRowanswerswriterefused on every field of the record andcheck(object, 'update')refused with the reasonrecord-not-writable.read,create, other objects and achecknaming another record keep the enclosing answer; an object grant the provider already refuses keeps its own reason. A newlockedBy: 'approval'prop answers the same way withrecord-locked-by-approval, before any answer and on a data source without the projection.writable: trueor an absent key changes nothing. A scope nested under one for the same record adds a host lock without asking again.gateFormFields(field locks plus the form-wide lock), the outboundfieldWriteGate, and the detail page's inline edit (RecordFieldAccessForRecord).closedFormAffordancereportsrecord/approvalfor such a row, read off the scopedcheck's reason.ClosedAffordanceNoticesays "You don't have permission to edit this record. The fields are read-only." (new keyform.noPermissionToEditRecord, all ten packs) or the record page's approval sentence (detail.lockedTooltip, reused).ModalFormandDrawerFormoffer no Save on it (closedForRecord).ModalFormtakeslockedByas a React prop, andRecordFieldAccessForFormforwards it.AppContentreads the approvals state of the record the overlay edits withuseRecordApprovals(the record page's read, joined on the wire when the page already asked) and the page's rule. The rule's input is now one helper,openApprovalOf, used by bothRecordDetailViewandAppContent, so the dialog cannot offer an edit the page locks. The record page's Edit action already read the lock (disabled while approval-locked) and is unchanged.ObjectForm.tsxis untouched (serial constraint with feat(spec,plugin-form)!: a form view's buttons hold submit and cancel only — buttons.reset is retired, because no form ever drew a reset button #27269 / ui(plugin-form):ObjectFormdrops thesplit/drawer/modalbranches andSplitFormretires; showcasetaskviews re-spelled (ADR-0050 cutover, renderer half of #27155) #27159).Why
On a
public_readrow the caller may read but not write, the edit dialog drew every field live and the server refused the save whole. That breaks "a client never advertises an edit the gate refuses" (permission.zod.ts). Seat note 6468378207:writabledoes not cover the approvals record lock, so a row under approval must read that lock too.Tests
ui/packages/permissions/src/__tests__/RecordFieldAccessScope.rowWritable-27138.test.tsx(7 tests).ui/packages/plugin-form/src/recordFieldAccess.rowWritable-27138.test.tsx(10 tests,ModalFormandDrawerForm):writable: falsegives read-only fields, the record notice and no Save. CONTROLwritable: trueand CONTROL no key keep the fields live and Save offered. An object grant refusal keeps the object notice. The dialog underlockedBy: 'approval'(writable row, and a data source without the projection) is read-only with the lock sentence and no Save.ui/packages/plugin-detail/src/renderers/__tests__/record-blocks.rowWritable-27138.test.tsx(4 tests):record:details/record:highlightsoffer no in-place editor onwritable: false; CONTROL writable.ui/packages/app-shell/src/console/__tests__/AppContent.editDialogApprovalLock-27138.test.tsx: a realAppContentmount handsModalFormlockedBy: 'approval'for a reader outside the slate (record lock answers) and for a participant on a locking node. It hands nothing for alockRecord: falsenode, nothing when no approval runs, and nothing (with no approvals read) on a create dialog.Acceptance notes
writable: falserow; the dialog explains and offers no Save. Disabling the button itself would need the page host to fetch the projection, which no block above it does today.subforms) ofModalForm/DrawerFormdraws its Save bar throughObjectForm/MasterDetailForm, which this PR does not touch: its fields lock, its own Save bar stays.DrawerFormreadswritablebut takes no host approval lock: no host renders it with a known lock today.content/docs/permissions/field-level-security.mdxalready states thewritablecontract ("a client need not offer the edit at all"); it is outside the claimed file surface and unchanged.Verification
All readings on head
695f5164(main merged at5b5cff15).AppContentapproval lock 5/5.pnpm --dir ui exec vitest run packages/permissions/ packages/plugin-form/ packages/i18n/ packages/plugin-detail/src/renderers/ packages/app-shell/src/console/ packages/app-shell/src/hooks/): 705/710 files green on the first run. The 5 red files were olderAppContentedit-dialog tests whose dialog now issues the approvals read with no fetch double (the network-escape guard). Each now installs one file-wide approvals double;AppContentre-run 36/36 files, 266/266 tests.RecordDetailView(theopenApprovalOfrefactor) 77/77 files, 512/512 tests.@object-ui/types,permissions,i18n,plugin-form,plugin-detailandapp-shelltype-checkexit 0 after the closure builds.dispatch-gates --commandsderived 34 families, all exit 0 (check-sdui-manifestincluded);--ranreconciliation: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.pnpm --dir ui exec vitest run scripts/timed out twice (15 s) while the suite above held the box; re-run alone under the lock it was 174/174 files green.check:type-check-debtexit 3 (lock queue) on the first pass; it ran under the lock and reported OK.Ablations (one-shot, restored; blob == HEAD after each)
node scripts/ablation-replace.mjsagainstRecordFieldAccessScope.tsx/ModalForm.tsx, tests resolved fromsrcthrough theui/vitest.config.mtsalias, so no dist leg:writableread (answer?.writable === falseto a constantfalse): 6 red, exactly thewritable: falsecases (scope 2, dialog/drawer 2, inline edit 2); the CONTROL cases and the approval cases stay green.lockedBy === 'approval'to a constantfalse): 4 red (scope 2, dialog 2).!closedForRecord(closedAffordance)totrueinModalForm): 3 red.An earlier attempt at the first one was a refused no-op: the replacement was a substring of the anchor, the tool refused it, and nothing ran.
Human Minimum
packages/specholds no runtime logic.pnpm lintis the repo-wide run CI owns (Lint UI,Lint & Repo Gates).main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (not touched)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01JQwxbYqMqySPCeFRdqDyHp
Generated by Claude Code