Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-grid): gate ObjectGrid's inline add-record row on can(object, 'create') - #5333
Conversation
…ct, 'create') (#5148) The add row was gated on the author-declared `operations.create` alone, while `permissionUpdate` / `permissionDelete` in the same file each carry the author declaration AND the principal's own grant (#4096). There was no `permissionCreate` in the component at all, so a principal with no create grant was offered the row and stopped only by the server's 403 — while the toolbar's New button on the same screen had already hidden itself. `showAddRow` is now the authored request AND the principal's verdict, matching the conjunction #4646 / PR #5145 established for the related-list "+ New" and #5143 established for this file's `editable` key. Narrows only; fail-open with no PermissionProvider is preserved and pinned, since plugin-designer's FieldDesigner and ObjectManager both declare `operations.create` and render without a provider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
os-support-ai
commented
Aug 19, 2026
ACCEPT — PM review, round 17.
The gate is a conjunction with the authored key on the left, which is what makes it safeThe authored declaration stays the left half, so this narrows and cannot widen: no verdict turns the add row on for a grid that never asked for it, and a grid with no Fail-open is preserved and verified at source, not assumed: with no The open sub-question was answered from precedent rather than escalated, as the dispatch asked: VerificationSeven legs against the real The build-artifact question was answered rather than waved at: the test imports One judgement call I agree with: the pre-existing Merging via the queue. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#5148
The defect
ObjectGridgated its Airtable-style inline add-record row on the author-declaredoperations.createalone:operationsis an authoring flag — it says whether the affordance was wired, neverthat the caller may use it. The same file has resolved
permissionUpdate/permissionDeletethroughperms.can(...)since #4096 and ANDs each into the affordanceit governs. There was no
permissionCreatein the component at all, socreatecarriedthe author declaration where
updateanddeleteeach carried declaration ANDprincipal check.
The symptom is the one #5143 and #4646 each settled on a neighbouring surface: a principal
with no
creategrant was offered the add row, filled it in, and was stopped only by theserver's 403 — while the toolbar's New button on the very same screen had already hidden
itself for that principal. No data ever landed (the server gate is solid); the cost was a
round-trip the UI guaranteed would fail, and one component answering "may this user create
records here?" two opposite ways at once.
The fix — precedent, not invention
Which sibling I matched: #4646 / PR #5145, whose create verdict is
objectCanCreate = affordances.create ∧ can(obj, 'create'), and #5143, which spelledthe same conjunction for this file's
editablekey. Operation moved tocreate:The authored key stays the gate's left half, so this narrows and never widens: no
verdict turns the add row on for a grid that did not ask for it, and a grid declaring no
operationsblock keeps falling through the{ update: !!onEdit, delete: !!onDelete }default that carries no
createkey.On the sub-question the card left open (whether this also ANDs in
resolveCrudAffordances().createPredicates): following the #4646 / PR #5145 precedentrather than escalating — it does not, and the reason is in that PR. It binds
createPredicatesonce per toolbar, against the host record in scope. An add-recordrow is not a toolbar and has no record to bind. That precedent also surfaces predicates
only after the object-level verdict has passed — predicates narrow further, they never
substitute for the verdict. The conjunct that was missing here is that verdict, which
is exactly what this adds. Recorded in a code comment so the next reader does not re-open it.
Reachability — correcting the card body
The card states "nothing shipped declares
operations: { create: true }". That is falseon today's main, re-measured here rather than carried from the card or from triage:
packages/plugin-designer/src/FieldDesigner.tsx:172packages/plugin-designer/src/ObjectManager.tsx:119Both build grids with
operations: { create: true, update: true, delete: true }when notread-only. The affordance is live, not dormant.
What keeps that from being a live hole — and what this change must not break — is that
designer surfaces typically run with no PermissionProvider, where
can()fails open.Verified at source: with no provider mounted
usePermissionsreturnscheck: () => ({ allowed: true }). So the visible consequence there was nil before andstays nil after. That is pinned by test
drather than left to inspection: if this gateever makes the designer's add row disappear, a missing check has been converted into a
regression, and a test goes red.
Tests
New:
packages/plugin-grid/src/__tests__/addRowCreatePermissionGate.test.tsx, modelled onthe #5143 sibling
inlineEditPermissionGate.test.tsx. It probes the realObjectGridthrough
usePermissionsand asserts the user-visible outcome — isadd-record-rowin theDOM — not the prop that produces it. The no-provider leg runs the real provider-less
hook rather than an imitation of it.
operationsblock + can createobjectNameat allReverse-verification
ObjectGrid.tsxrestored toorigin/main, tests kept. Predicted before running: b, e, fred; a, c, d, g green.Observed: exactly b, e, f red — 3 failed / 4 passed, matching
by name and by count. Fix restored afterwards and confirmed byte-identical to the commit.
Build artifacts between the edit and the thing under test: none, on every leg. The edit
is to
packages/plugin-grid/src/ObjectGrid.tsx, which the test imports as../ObjectGrid—source, not
dist. The packages that are consumed throughdist(@object-ui/components,which renders the
add-record-rowmarkup, pluscore/permissions/react/fields)are untouched by this change and were built once before the baseline, so they are fixed
constants across every leg;
add-record-rowwas confirmed present inpackages/components/distbefore any leg ran. No leg needed a rebuild, and none was skipped.Gates run — all at
04fae0370, the head of this branchRun from the repo root (
plugin-gridowns a standalonevitest.config.ts, whichassertCanonicalVitestInvocationdoes not cover).pnpm vitest run packages/plugin-grid— 79 files / 712 tests passedpnpm vitest run packages/plugin-designer packages/plugin-list— 50 files / 681 testspassed (the designer is the named regression risk;
plugin-listhosts this grid)pnpm --filter @object-ui/plugin-grid type-check— clean (script name echoed, so itgenuinely ran rather than zero-matching)
pnpm vitest run scripts/__tests__/vitest-invocation-guard.test.ts scripts/__tests__/turbo-test-inputs.test.ts scripts/__tests__/vitest-config-alias-targets-3944.test.ts— 109 passedeslinton both changed files — 0 errors (warnings are the file's pre-existing set;the test file's 4
anywarnings match the sibling test's 5 exactly)Scope
Two regions of
ObjectGrid.tsxonly — the permission block beside its two siblings, andthe
showAddRowcomputation — plus the new test and apatchchangeset for@object-ui/plugin-grid. Nothing else in that 2800-line file. No schema accept/rejectboundary moves and no public surface widens; per-principal visibility changes by design, in
the tightening direction, restoring
declared = enforcedforcreate.Generated by Claude Code