Skip to content

fix(plugin-list): harvest row-action predicate fields from the object's userActions only - #5434

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5398-listview-userActions-collision
Aug 20, 2026
Merged

fix(plugin-list): harvest row-action predicate fields from the object's userActions only#5434
os-support-ai merged 1 commit into
mainfrom
claude/issue-5398-listview-userActions-collision

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#5398

The sibling read site of the one objectui#5426 fixed in plugin-grid, per the maintainer ruling of 2026-08-20 on objectui#5240 (Q2=B). Held serial rather than batched for one reason: both read sites end up with one shape and one stated reason, not two spellings of one fix. This PR copies objectui#5426's diff — comment structure, pin structure, ablation shape — and adds only what is genuinely different about this instance.

What was wrong

userActions names two different blocks, and ListView's $select harvest read them as one.

  • View-leveluserActions is toolbar policy — the spec's UserActionsConfigSchema: sort, search, filter, refresh, rowHeight, addRecordForm, editInline, buttons. It rejects editby name.
  • Object-leveluserActions is the CRUD-predicate blockedit / delete / create carrying visibleWhen / disabledWhen (objectui#2614). It is the only shape listViewPredicates can read: its loop skips every non-object value.

The harvest that decides which fields the server is asked for read the key view-first, at packages/plugin-list/src/ListView.tsx:

userActions: (schemaasany).userActions??(objectDefasany)?.userActions,

So a perfectly legal toolbar block shadowed the object's CRUD predicates. Every value in it is a boolean, the harvest collected zero predicates, the predicate's operand left the projection, CEL faulted on the absent key, failed closed, and the row Edit/Delete button disappeared for everyone with nothing pointing at the projection — objectui#3501's failure shape, reached with a success receipt at every step.

Why this instance was worse than the grid's — verified, not inherited

The card claimed the app-shell path makes the shadowing total. Re-measured on this branch rather than taken on trust, and it holds — in a slightly stronger form than the card stated.

packages/app-shell/src/views/ObjectView.tsx builds the userActions it hands to ListView as an object literal of two spreads:

userActions: {
...(normalizeListViewSchema(listSchema??{})as{userActions?: object}).userActions,
...(normalizeListViewSchema(viewDef??{})as{userActions?: object}).userActions,},

That literal is on fullSchema, which is handed straight to ListView schema={fullSchema} in the same file. It is not merely unconditional — it is never nullish even when both spreads are empty: the worst case is {}, which ?? does not fall through. So on the app-shell path the object's CRUD block was never consumed at all, whether or not any author ever wrote toolbar policy. The grid's instance needed an authored toolbar block to misfire; this one misfired on every list the app shell renders.

A third producer is in plugin-list's own dependency reach: normalizeListViewSchemamanufactures a view-level userActions out of a legacy show* view that never wrote the key. "Nobody authors this" was never available as a reason here either.

Re-measured against @objectstack/spec@17.0.0 (same probes as objectui#5426, same results):

proberesult
UserActionsConfigSchema.shape keyssort, search, filter, refresh, rowHeight, addRecordForm, editInline, buttons
UserActionsConfigSchema.safeParse({edit:{visibleWhen}})success: false, unrecognized_keys: ["edit"] (same for delete, create)
ListViewSchema.safeParse(… userActions:{sort,search,filter})success: true
ListViewSchema.safeParse(… userActions:{edit:{visibleWhen}})success: false, unrecognized_keys: ["edit"] at path userActions

What changed

  1. packages/plugin-list/src/ListView.tsx — the $select harvest reads objectDef.userActionsonly. The view-schema-first operand is gone.
  2. Both userActions read sites in that file carry a comment naming the collision — including the surviving view-half read at toolbarFlags, which is toolbar policy and therefore correct as schema-only. That mirrors objectui#5426's Q3=B, pointed at the other half: in the grid the surviving read was the object block, here it is the view block.
  3. packages/plugin-list/src/__tests__/ListView.userActionsCollision.test.tsx — a new file pinning each clause of that comment. 12 assertions.
  4. A patch changeset on @object-ui/plugin-list.

The comment states the measured reason. It does not say "non-author surface, deliberately unlisted" — the repository contradicts that, and Q1=A exists precisely to keep the falsified claim out of the census artifact.

Verification — all at 392c6e452

No build artifact sits between any edit and any measurement in the vitest legs.vitest.config.mts aliases every @object-ui/* specifier to that package's src/, and this worktree had no dist/ at all while every suite and both ablation legs ran (packages/core/dist, packages/react/dist, packages/plugin-list/dist all absent — checked). Source is provably what executed. @objectstack/spec is the published 17.0.0 from node_modules; nothing local builds it. The dependency closure was built afterwards, only because tsc --noEmit needs the .d.ts files.

Run from the repository root, per AGENTS.md:

pnpm exec vitest run packages/plugin-list/ packages/app-shell/src/views/ObjectView
-> 55 files, 780 tests passed
pnpm --filter @object-ui/plugin-list type-check -> exit 0 (tsc --noEmit && tsc -p tsconfig.test.json)
pnpm --filter @object-ui/plugin-list lint -> 0 errors, 388 warnings (pre-existing)
pnpm run check:control-bytes / check:spec-symbols / check:phantom-deps / check:self-import -> all OK
pnpm run check:lint-coverage / type-check:coverage / check:esm-specifiers -> all OK
node scripts/check-changeset-presence.mjs / check-changeset-no-major.mjs / check-changeset-fixed.mjs -> OK

The ObjectView slice is the downstream consumer — the producer of the always-truthy literal above.

Reverse-verification (two legs, predicted before running, fix committed first)

Both legs ran against the committed fix, restored with git checkout HEAD -- …; after the restore git diff HEAD was empty, so the tree that produced the green numbers is byte-identical to the commit. No build step sits between either mutation and the run (src alias, no dist/).

Leg 1 — restore the view-first ??. Predicted: two red, not the grid's one — the toolbar-block case and the empty-block case, because {} is truthy; the no-view-key case stays green because ?? legitimately falls through there. Observed: 2 failed | 10 passed, exactly those two. Matches.

Leg 2 — delete the userActions line from the harvest entirely (the "tidy finish" mutation). Predicted: three red — every projection case loses status. Observed: 3 failed | 9 passed. Matches.

Leg 2 is why the first projection case is counted at all: it is green on both legs of leg 1, so on its own it pins nothing about this fix — it pins that the read still exists, and leg 2 is what shows it can go red. Same reasoning as objectui#5426, one case wider.

Scope

plugin-list only. packages/plugin-grid is untouched — objectui#5426 is merged and must not be re-touched. packages/app-shell is untouched: it is evidence here, not a target. The producer-side question — bridgeListView emitting onto an object-grid node a key ComponentPropsMap refuses by name — is routed through triage as its own spec-coordination card; out of scope here, and not addressed by this PR.

Toolbar policy itself is untouched — it was never read through this path, and the two view-level reads that legitimately consume it (toolbarFlags, inlineEditOffered) are unchanged apart from the collision comment.


Generated by Claude Code

…'s userActions only
The `$select` predicate harvest read `userActions` view-first, so a spec-legal
view-level toolbar block shadowed the object's CRUD-predicate block and dropped
its operands from the projection (objectui#3501's fail-closed CEL fault). On the
app-shell path the left operand is always an object literal, so the object block
was never reached at all.
Sibling of objectui#5426, which fixed the same shape in plugin-grid; both read
sites now carry the same comment and the same measured reason.
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-BzYQLBnD.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)6.35KB2.43KB
auth (index.js)2.77KB1.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.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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.94KB113.61KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.34KB
fields (index.js)237.21KB59.50KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)30.51KB7.57KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.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.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
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)128.43KB32.92KB
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)197.28KB53.06KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.64KB27.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.52KB20.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.45KB0.23KB
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

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.

plugin-list ListView.tsx:1591 carries the same view/object userActions collision as #5240 — and here the object's CRUD block is never reached at all

1 participant

@os-support-ai