Skip to content

fix(plugin-list): a list-view with no authored columns renders the grid's defaults, not an empty table - #6679

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-6598-html-tier-columns-remeasure
Aug 28, 2026
Merged

fix(plugin-list): a list-view with no authored columns renders the grid's defaults, not an empty table#6679
os-sales merged 2 commits into
mainfrom
claude/issue-6598-html-tier-columns-remeasure

Conversation

@claude

@claudeclaudeBot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Part of #6598

The card was dispatched as a re-measurement: PR #6669 had landed objectui#6614's Q1-A an hour earlier, and the standing rule is that the merge discharging a blocker is the one most likely to have already discharged the downstream. It had — for two of the eight reported spellings. This PR carries the measurement and fixes the one remaining form that failed with no diagnostic at all.

The eight reported forms, re-measured on the merged ref

All measured through the live path on origin/main @ ece68882: registry-built manifest (the exact getJsxManifest construction in components/src/renderers/layout/page.tsx), the real html-kind compile, SchemaRenderer, the real plugin-list registration, and — for the column counts — the realobject-grid, counting rendered header cells. Object opportunity, fields id (hidden) / name / stage / amount / close_date.

#reported spellingcompile diagnosticsdata columns renderedverdict
control:columns={["name","amount"]} (strict JSON)none2worked before, works now
1columns={['name','amount']}none2discharged in #6669
2columns='["name","amount"]' (JSON string)warning type-mismatch0refused, and correctly diagnosed
3columns={[{field:'name',label:'Name'}, …]}none2discharged in #6669
4viewName="all"warning unknown-prop0not the spelling — see below
5view="all"warning unknown-prop0not the spelling — see below
6child column elements4 × errorpage does not compileloud, out of grammar
7no columns at allnone0 — index column onlythis PR
8aobject-name="opportunity" (kebab)error missing-required-proppage does not compileloud
8bview-name="all" (kebab)warning unknown-prop0same as 4/5

Forms 1 and 3 are exactly the grammar cases #6669 legalised, and they are not re-implemented here — the credit is entirely #6669's. Forms 2, 6 and 8a fail loudly and arguably correctly: a JSON string where an array is declared is a type mismatch (accepting it would be a consumer-side tolerance alias), and the other two are page-fatal compile errors the author cannot miss.

Form 7 is the one that was silent, and it is the one an AI drafts first.

Form 7: the mechanism, and the single-variable control that isolated it

ListView computes effectiveFields and hands it to the child grid as both fields and columns. When the author declared no columns that value is [] — and an empty array is truthy, so ObjectGrid's if (schemaFields) fieldsToShow = schemaFields; pins the projection at zero and neither of its default-column derivations can run. normalizeColumns had already read the empty columns as unauthored, so the two keys disagreed about one fact and the stricter reading won.

The control, same tier, same page kind, same data source, same object — only whether list-view sits in between:

html-kind page sourcedata columns
object-grid with objectName, no columns4
list-view with objectName, no columns0

That is the reporter's "no data columns at all — only the index column", reproduced on the merged ref.

The change

ListView now asks whether the author declared a projection — columns present and non-empty, read after the legacy fields fold — and hands the grid nothing at all when they did not, so the grid's own defaults apply.

⚠️ The predicate reads the authored value and never what survived filtering, and that distinction is load-bearing rather than stylistic. effectiveFields is also [] when the author DID declare columns and the field gate removed every one of them; that case must keep sending the empty projection, because ObjectGrid re-applies FLS on its derived column path only, never on the explicit-columns path — treating it as "unauthored" would put fields on screen past the field gate. It is pinned as its own case.

⛔ What this does NOT do, stated so it is not over-read

It restores the grid's defaults; it does not decide which defaults. With a host owning the fetch, ObjectGrid takes its inline-data branch and derives from the row payload's keys, so a bare list-view now shows every fetched field — measured 10 columns including Id and Created By, where the same object with the grid owning the fetch shows 5 under the object-schema policy (hidden dropped, readonly system-managed dropped, highlightFields honoured). That precedence lives in packages/plugin-grid, which was fenced for this dispatch, and is filed as #6677.

So: usable table instead of an unusable one, and the noisy-vs-policy question is #6677's. ⚠️Sequencing is a maintainer call — this can land alone (a noisy table is self-correcting for an author; an empty one reads as a broken platform) or wait for #6677.

Forms 4, 5 and 8b are also restated rather than fixed. The spec's dataSource={{object:'opportunity', view:'all'}}does resolve a saved view on this tier — measured, columns arrive — but it draws unknown-prop because no registration declares it and it is not a base prop, so the tier reported the working spelling exactly like the two broken ones. Filed as #6678; it needs a declaration-surface ruling, not a patch.

A suspected FLS bypass on the SDUI path was disproven by measurement, not assumed away: with amount denied and columns={['name','amount']} authored, both the html-tier path and the direct host path drop the column from the headers and from $select. No finding filed.

Tests

Two pins, because the mechanism and the symptom are at different layers:

  • ListView.unauthoredColumnProjection-6598.test.tsx — the handoff, against a stub object-grid (the idiom ListView.findParamsHandoff.test.tsx set; plugin-list does not depend on plugin-grid). Four cases: unauthored, empty-columns-is-unauthored, the authored positive control, and the FLS case that must stay explicitly empty. Assertions read props.schema, the copy ObjectGrid actually reads, with a truthiness check on the accessor so a renamed prop cannot turn toBeUndefined() into a pass over nothing.
  • htmlTierListViewDefaultColumns-6598.test.tsx — the reported page end to end over the real grid, counting header cells. Registered in heavyDomTests for the setup's plugin-grid registration, the route ListView.crossPageSelectAll.test.tsx takes. A stub cannot see this defect: it is two components disagreeing, and the stub stands in for one of them.

Reverse verification

From the committed state, ListView.tsx reverted to base ece68882 and the mutation confirmed on disk (HEAD blob 9e68c598…f5245657…, hasAuthoredColumns hits 4 → 0):

× sends NO projection when the author declared no columns
× treats an empty `columns` as unauthored too
× renders the object's default columns when the author declared none
AssertionError: expected 1 to be greater than 1 ← literally "only the index column"
AssertionError: expected [] to be undefined
Tests 3 failed | 3 passed (6)

3 red, 3 green — the authored control, the FLS case and the authored end-to-end case all hold at base, so the pins are not red for a general reason. Restored with git checkout HEAD -- PATH; git diff HEAD empty, blob back to 9e68c598…, marker hits back to 4.

Gates

Union run at final HEAD 69447464, each verdict line as the gate printed it, exit status captured before any pipe:

gateverdict lineexit
pnpm exec vitest run packages/plugin-list/Test Files 51 passed (51) · Tests 702 passed (702)0
pnpm --filter @object-ui/plugin-list run type-checkclean (tsc --noEmit && tsc -p tsconfig.test.json)0
pnpm check:control-bytes✅ check-control-bytes: OK (scanned 5519 tracked text file(s); skipped 85 binary).0
pnpm --filter @object-ui/plugin-list lint✖ 428 problems (0 errors, 428 warnings)0
pnpm lint:root (covers vitest.config.mts)✖ 28 problems (0 errors, 28 warnings)0
pnpm check:phantom-deps✅ Every in-scope import is declared by the package that publishes it.0
vitest run scripts/__tests__/vitest-invocation-guard.test.tsTests 30 passed (30)0

The four gate runs were chained with && so the status certifies all of them, under the shared verify lock (VERDICT command-exit 0).

"Type-check is clean" is measured, not assumed: tsc -p tsconfig.test.json --listFiles puts both new test files and ListView.tsx inside the closure (1 hit each), so the green covers them.

Lint scope: turbo run lint runs one eslint . unit per package plus the root unit. Both units containing a changed file were run whole — no within-unit narrowing — and no other package's unit reads a file this diff touches. Changed files carry 0 errors: ListView.tsx 0/177 warnings (pre-existing no-explicit-any in a 3.8k-line file; the added lines contribute none), the two new tests 0/4 and 0/2 (no-explicit-any on the stub props, matching the sibling tests), vitest.config.mts 0/1.

vitest-invocation-guard.test.ts is run because this diff edits vitest.config.mts — deriving the gate families from the changed paths does not answer "what tests test the thing I changed".

Notes

  • Changeset: @object-ui/plugin-list patch.
  • The card's title and its "Triage anchoring evidence" section remain disproven (the block is list-view, the live registration does declare columns, and the declared-inputs hypothesis could not have produced the symptom). Nothing here implements from them. Re-titling is triage's write.
  • Not touched, per the dispatch fences: packages/plugin-grid, packages/types, packages/components/src/renderers/complex/data-table.tsx, and the objectstack half of the sdui-parser lockstep.

Generated by Claude Code

os-salesand others added 2 commits August 28, 2026 13:23
…jection
A production `kind:'html'` page carried `<list-view objectName="opportunity">`
with no `columns` and rendered the row count, the filter/group/sort toolbar and
the index column — and not one data column, with no diagnostic anywhere.
`ObjectGrid` derives default columns for exactly that case ("Default columns
priority (when schema doesn't specify columns)"), and it never ran: the
derivation is gated on `schema.fields` being ABSENT, `ListView` sent `fields:
[]`, and an empty array is truthy. `normalizeColumns` had already read the empty
`columns` as unauthored, so the two keys disagreed about one fact and the
stricter reading won.
`ListView` now asks whether the AUTHOR declared a projection — `columns` present
and non-empty, after the legacy `fields` fold — and hands the child grid nothing
when they did not. The predicate reads the authored value and never what
survived filtering: when the author DID declare columns and the field gate
removed every one, the empty projection is still sent, because `ObjectGrid`
re-applies FLS on its derived path only.
Measured single-variable on the html tier: a bare `<object-grid objectName=…/>`
renders the object's default columns; the same object behind `<list-view>`
rendered none. Pinned at the handoff and end to end over the real grid.
Part of objectui#6598.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
`SchemaRenderer` hands a registered component its schema and also spreads the
schema's keys as props; `ObjectGrid` reads the `schema` one. The handoff pin was
asserting the spread copy — the same value today, and the copy that would keep
agreeing if the two ever diverged. It now reads `props.schema`, with a truthiness
assertion on the accessor itself so a renamed prop cannot turn every
`toBeUndefined()` into a pass over nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3237.0 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-Bcm_iaTe.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
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)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
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)509.24KB115.61KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)239.05KB60.06KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
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.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
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.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.43KB34.48KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.51KB54.54KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)113.01KB27.57KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.44KB7.59KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
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)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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-sales
os-sales marked this pull request as ready for review August 28, 2026 14:05
@os-sales
os-sales added this pull request to the merge queueAug 28, 2026
Merged via the queue into main with commit babe956Aug 28, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-6598-html-tier-columns-remeasure branch August 28, 2026 14:31
os-sales pushed a commit that referenced this pull request Aug 29, 2026
…nd:'html' page
The reported defect no longer reproduces on main. Re-measured every one of the
eight spellings objectstack#12649 tried: none lands in the reported state (a
populated table whose only header is the index column, with zero diagnostics).
Six render data columns — forms 1 and 3 render exactly the authored projection,
forms 2, 4, 5, 7 and 8b render the block's defaults — and forms 6 and 8a fail
loudly with the compile-error block on screen.
Three separate mechanisms produced one symptom, and each was fixed elsewhere:
the braced literal subset (#6614 / PR #6669), the unauthored-projection handoff
(PR #6679), and the grid's default-column derivation (#6677).
What none of those pinned is the card's own claim, which is a statement about
all eight spellings at once. This adds that matrix, through the real page
renderer, the real html-tier compile against the real registry manifest, the
real `list-view` registration and the real `object-grid` — so no future change
can put any spelling back into the reported state.
Tests only; the changeset declares an empty frontmatter accordingly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-sales