Repository navigation
fix(plugin-form): a line-items grid does not offer a cell the caller may read but not edit (objectui#10163) - #10233
Conversation
…may read but not edit The `record:line_items` panel read only `schema.readonly`, so a column the caller's permission set marks `editable: false` rendered as a live editable cell on the same page where the record form already disables that field. Route the panel's columns through the ONE render pass the form containers share (`fieldWriteGate.ts`), adapted to the grid's single per-cell lock channel: refused columns lock on every row, unreadable columns are omitted, permitted columns are untouched. Adding/removing lines stay on `schema.readonly`; with no permission provider the grid is unchanged (fail-open, as before). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Rendered by an isolated review subagent spawned by this seat and fed only the card, the PR, PR objectui#10161's landed diff and the two trees. Its transcript stamp, verbatim: 「You are powered by the model named Fable 5.1. The exact model ID is claude-fable-5-1.」 Adopted by this seat. ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Landing: draft until Generated by Claude Code |
…ne-items-field-write-gate
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Part of #10163 — this lands the
record:line_itemshalf (LineItemsPanel). The master-detail form's own child grid (MasterDetailForm,object-master-detail-form) carries the same gap, measured below, and could not be reached here without editingMasterDetailForm.tsx, which this dispatch fences (objectui#9909's branch edits that file). So the card stays open for that half.Clause-②: no — measured on this diff, not asserted:
@object-ui/plugin-formpublishes.only, fromindex.tsx, which does not re-exportfieldWriteGate.ts. After a build on this head,dist/index.d.tsanswersapplyColumnPermissions0 hits andfieldWriteGate0 hits, with the positive controlLineItemsPanel2 hits in the same file — so the zeros are measured absences.LineItemsPanel's props are unchanged, no authored key changes meaning, no accept set is relaxed, and the fail-open-with-no-provider posture objectui#10161 kept is unchanged.Step 1 — what reproduced on
main, with the lit controlRun on unmodified
origin/mainat62597c58, before any source change, with the card's sibling setup: areporterprincipal whose permission set markskpi_entry_line.scorereadable: true, editable: false(plusreviewer_notereadable: false), and an object-level grant that allows editing everything else on the child object.scoreactual_value/indicator_name(same grid, same rows)record:line_itemspanel (LineItemsPanel)MasterDetailForm)The pin file added here, run against that unmodified tree: 2 failed / 4 passed. The two reds are the refused-column row (
expected false to be trueon thescorecells'disabled) and the unreadable-column row (3Reviewer notecells rendered where 0 are expected). The four greens are the controls: add/remove lines, an unrestricted caller, no permission provider, and areadonlypanel. So the finding is a difference inside one grid, not an absence.plugin-form/src—MasterDetailFormrenders the sameLineItemsFieldand consults no permission either (it does not readschema.readonlyat all). And there was no cell-level adapter infieldWriteGate.ts: handing the gridapplyFieldPermissions' output would drop unreadable columns and lock nothing, because a grid column does not readdisabled/readOnly.The route — the one pass, reused
fieldWriteGate.ts: the loopapplyFieldPermissionscarried is nowgateByPermission, the ONE pass that askscheckField(object, field, 'read' | 'write')and decides omit / keep / mark.applyFieldPermissionscalls it with the form's mark (readOnly+disabled, plus the optional description), soObjectForm,ModalFormandDrawerFormrender exactly what they rendered before. The newapplyColumnPermissionscalls the SAME pass with the grid's mark. ⛔ No verdict is re-derived.readonlyWhen: 'true'. AGridColumnhas exactly one per-cell lock channel,readonlyWhen, which the grid evaluates per row and renders as a disabled control; a field-level refusal does not depend on the row, so the predicate that expresses it is the constant one. A refused column that declared its own narrowerreadonlyWhenis locked on every row regardless (pinned).LineItemsPanel.tsx:usePermissions()plus one call,applyColumnPermissions(schema.columns, { perms, objectName: schema.childObject }), inside the existinggridFieldmemo.allow_add/allow_deletestay onschema.readonly, untouched.What changes, observably
Ablation — committed first, mutated, restored
Mutation through
ablation-replace.mjs(wrap mode, restore armed on exit and signals), on the committed headf87c020e: the gate wiringcolumns: applyColumnPermissions(schema.columns, { perms, objectName: schema.childObject }),replaced bycolumns: schema.columns,inLineItemsPanel.tsx.44b47677f220to7b8ee94590f9.44b47677f220equals theHEADblob, andgit diff HEADis empty.dist/is involved: the pins import./LineItemsPanelby relative path, and the root vitest config aliases every@object-ui/*specifier tosrc.Checks on this head (
f87c020e)pnpm exec vitest run packages/plugin-form/from the repo root, through the shared verify lock: 109 files, 1051 passed, 1 skipped, exit 0.pnpm --filter @object-ui/plugin-form type-check: exit 0, after building the dependency closure (turbo run build --filter=@object-ui/plugin-form^...). The script runstsc -p tsconfig.test.jsonas well, and--listFilesshows the new test file in it.eslint .inpackages/plugin-form: exit 0, 0 errors across 142 files.check:control-bytes,check:new-line-citations(0 new citations),node scripts/check-changeset-presence.mjs,check-changeset-no-major,check:changeset-claims,check:pending-changeset-literals,check:test-path-roots,check:vi-mock-override-shape,check:unreferenced-sources,check:phantom-deps,check-lint-coverage;check-governed-queue-guard --teston the four paths answers NOT GOVERNED.Acceptance notes
null. Updates are already narrowed to dirty cells by objectui#10154.packages/fields/src/widgets/(fenced for this dispatch, objectui#10225) and a new member on the publishedGridColumntype. Owner of that file: none named.fieldWriteGate.tsgains twono-explicit-anywarnings, from the sameRecordconstraint the existingapplyFieldPermissionssignature already carries.Co-Authored-Bytrailer; noted here rather than rewritten (history is not rewritten on this repository).editable:falsecolumn renders as a live editable cell, one container outside objectui#10120's fix #10163: routeMasterDetailForm'sLineItemsFieldcolumns throughapplyColumnPermissions(the child object name is already on each detail entry): ausePermissions()read plus one call, once that file is free.Dispatched by the
domain:uiseat; this session:https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC.Generated by Claude Code