Skip to content

fix(plugin-grid): gate ObjectGrid's inline add-record row on can(object, 'create') - #5333

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5148-grid-add-row-create-permission
Aug 19, 2026
Merged

fix(plugin-grid): gate ObjectGrid's inline add-record row on can(object, 'create')#5333
os-support-ai merged 1 commit into
mainfrom
claude/issue-5148-grid-add-row-create-permission

Conversation

@claude

@claudeclaudeBot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Fixes#5148

The defect

ObjectGrid gated its Airtable-style inline add-record row on the author-declared
operations.createalone:

showAddRow: !!operations?.create,

operations is an authoring flag — it says whether the affordance was wired, never
that the caller may use it. The same file has resolved permissionUpdate /
permissionDelete through perms.can(...) since #4096 and ANDs each into the affordance
it governs. There was no permissionCreate in the component at all, so create carried
the author declaration where update and delete each carried declaration AND
principal check.

The symptom is the one #5143 and #4646 each settled on a neighbouring surface: a principal
with no create grant was offered the add row, filled it in, and was stopped only by the
server'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 spelled
the same conjunction for this file's editable key. Operation moved to create:

constpermissionCreate=objectName ? perms.can(objectName,'create') : undefined;// …showAddRow: !!operations?.create&&(permissionCreate??true),

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
operations block keeps falling through the { update: !!onEdit, delete: !!onDelete }
default that carries no create key.

On the sub-question the card left open (whether this also ANDs in
resolveCrudAffordances().createPredicates): following the #4646 / PR #5145 precedent
rather 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-record
row 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 false
on today's main
, re-measured here rather than carried from the card or from triage:

  • packages/plugin-designer/src/FieldDesigner.tsx:172
  • packages/plugin-designer/src/ObjectManager.tsx:119

Both build grids with operations: { create: true, update: true, delete: true } when not
read-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 usePermissions returns
check: () => ({ allowed: true }). So the visible consequence there was nil before and
stays nil after. That is pinned by test d rather than left to inspection: if this gate
ever 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 on
the #5143 sibling inlineEditPermissionGate.test.tsx. It probes the realObjectGrid
through usePermissions and asserts the user-visible outcome — is add-record-row in the
DOM — not the prop that produces it. The no-provider leg runs the real provider-less
hook rather than an imitation of it.

legcaseexpected
acan create + declaredshown
bcannot create + declaredhidden (the defect)
cdeclared false / no operations block + can createhidden
dno PermissionProvider (designer path)shown (fail-open)
eboth directions pinned togetheragree
fobject addressed via data config onlygated
gno objectName at alluntouched

Reverse-verification

ObjectGrid.tsx restored to origin/main, tests kept. Predicted before running: b, e, f
red; 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 through dist (@object-ui/components,
which renders the add-record-row markup, plus core / 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-row was confirmed present in
packages/components/dist before any leg ran. No leg needed a rebuild, and none was skipped.

Gates run — all at 04fae0370, the head of this branch

Run from the repo root (plugin-grid owns a standalone vitest.config.ts, which
assertCanonicalVitestInvocation does not cover).

  • pnpm vitest run packages/plugin-grid79 files / 712 tests passed
  • pnpm vitest run packages/plugin-designer packages/plugin-list50 files / 681 tests
    passed
    (the designer is the named regression risk; plugin-list hosts this grid)
  • pnpm --filter @object-ui/plugin-grid type-check — clean (script name echoed, so it
    genuinely 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.ts109 passed
  • eslint on both changed files — 0 errors (warnings are the file's pre-existing set;
    the test file's 4 any warnings match the sibling test's 5 exactly)
  • control-byte scan on all changed files — clean

Scope

Two regions of ObjectGrid.tsx only — the permission block beside its two siblings, and
the showAddRow computation — plus the new test and a patch changeset for
@object-ui/plugin-grid. Nothing else in that 2800-line file. No schema accept/reject
boundary moves and no public surface widens; per-principal visibility changes by design, in
the tightening direction, restoring declared = enforced for create.


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-CeJbVvt7.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.83KB3.70KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.33KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.79KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.71KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.75KB113.40KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.34KB
fields (index.js)237.07KB59.46KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.42KB1.39KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)29.43KB7.15KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)39.16KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)127.92KB32.80KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)241.46KB60.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)124.19KB30.20KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)198.27KB53.29KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.66KB27.13KB
plugin-map (index.js)20.08KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.55KB20.67KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)36.10KB12.26KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-support-ai
os-support-ai marked this pull request as ready for review August 19, 2026 16:10
@os-support-aiClaude

Copy link
Copy Markdown
Collaborator

ACCEPT — PM review, round 17.

  • Path surface (git diff --name-only origin/main... @ 04fae03): 3 files — packages/plugin-grid/src/ObjectGrid.tsx (two regions, +39/−1, almost all docblock), one new test file, one changeset. Zero governed-surface hits, and nothing else in a 2800-line file that four other cards queue behind.
  • Gates: every gate job completed: success — Lint, Type Check, Test shards 1–4, Build & E2E, Build Docs, Doc Snippet / Doc Component Type Check, Changeset Declaration / Bump Policy / Fixed Group, Bundle Analysis, Control Byte Scan, Internal Docs Link Check, Skill Guide Path Check. No cancelled, no in_progress.
  • Bump: patch on @object-ui/plugin-grid. Correct — no contract moves.

The gate is a conjunction with the authored key on the left, which is what makes it safe

showAddRow: !!operations?.create && (permissionCreate ?? true)

The 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 operations block keeps falling through the { update: !!onEdit, delete: !!onDelete } default that carries no create key. permissionCreate sits beside permissionUpdate / permissionDelete at their existing site, so the three read as one block rather than drifting apart again — which is how the third one went missing in the first place.

Fail-open is preserved and verified at source, not assumed: with no PermissionProvider mounted, usePermissions returns check: () => ({ allowed: true }), so can() answers true; permissionCreate is undefined when no object name resolves. Leg d pins it. That mattered: plugin-designer's FieldDesigner and ObjectManager both declare operations: { create: true, … } and typically render with no provider, so a fix that "just ANDed the verdict" would have deleted their add row.

The open sub-question was answered from precedent rather than escalated, as the dispatch asked: createPredicates is not ANDed in, because PR5145 binds those once per toolbar against the host record in scope — an add-record row is neither a toolbar nor has a record to bind, and that precedent surfaces predicates only after the object-level verdict passes. The missing conjunct here is that verdict. Recorded in a code comment so the next reader does not re-open it.

Verification

Seven legs against the real ObjectGrid through usePermissions, asserting the user-visible outcome (is add-record-row in the DOM) rather than the prop, with the no-provider leg running the real provider-less hook instead of an imitation. Reverse-verification predicted b/e/f red and a/c/d/g green before running; observed Tests 3 failed | 4 passed (7), failures matching by name and by count, twice. Restore confirmed byte-identical.

The build-artifact question was answered rather than waved at: the test imports ../ObjectGridsource, not dist — so no artifact sits between the edit and any leg; the packages consumed through dist (@object-ui/components, which renders the row markup) are untouched by this change and were built once before the baseline, with grep -c add-record-row packages/components/dist/index.js → 1 proving the observable reached dist before any leg ran. Fixed constants across every leg, no leg needing a rebuild and none skipped.

One judgement call I agree with: the pre-existing react-hooks/refs warning at :3614 was flagged here rather than filed — it is 1 of 240 warnings the repo's own config grades as warnings and prints on every CI lint run, so it carries no hidden information. Filing it would add noise, not signal. Leaving it unfiled.

Merging via the queue.


Generated by Claude Code

@os-support-ai
os-support-ai added this pull request to the merge queueAug 19, 2026
Merged via the queue into main with commit 3e0214cAug 19, 2026
22 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-5148-grid-add-row-create-permission branch August 19, 2026 16:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ObjectGrid's inline add-record row rides on the author-declared operations.create with no can(object,'create') gate

2 participants

@os-support-ai@claude