Skip to content

refactor(plugin-grid): retire the dead per-column wrap forward, measured against data-table's read set - #6651

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-5453-wrap-forward-enforce-or-remove
Aug 28, 2026
Merged

refactor(plugin-grid): retire the dead per-column wrap forward, measured against data-table's read set#6651
os-sales merged 1 commit into
mainfrom
claude/issue-5453-wrap-forward-enforce-or-remove

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#5453

ObjectGrid.generateColumns() copied a per-column wrap onto every column object handed to data-table, and packages/components' data-table.tsx never read it. This retires the forward.

The measurement that decided the fork

Triage graded this card with two legs and told the dev to pick by measurement, not preference: does data-table.tsx today offer any clamp / expand / wrap affordance for long cell text? If no, delete the forward; if yes, the implement leg becomes natural and is a behaviour addition that must stop and report. The answer is no, on two independent counts.

1. Nothing reads the key. Every column-key read in data-table.tsx, comments stripped so prose mentions cannot be counted as reads:

keyreads
accessorKey34
width8
align5
header4
className4
sortable3
fitContent2
headerIcon2
cellClassName1
wrap0

The sibling counts are the positive control the dispatch asked for: the same query shape does find the keys that are genuinely consumed, so the zero is a measurement and not a mis-aimed grep. The raw string wrap does occur in that file — every occurrence is flex-wrap, whitespace-nowrap, or a variable named wrapper.

2. There is nothing for it to switch on. The implement leg was wrap: true dropping the truncate, which presupposes the renderer has somewhere to put a second line. It does not. The cell content wrapper is a two-way switch with no third state:

className={isFit ? 'w-full whitespace-nowrap' : 'truncate w-full'}

isFit is col.fitContent, and it selects single-line, no clipping — the opposite of wrapping. A native title tooltip is the only concession to overflow. There is no line-clamp, no whitespace-normal, no break-words, no expand affordance, and the file does not read density or rowHeight at all. So the enforce-or-remove default applies and the removal leg fires.

Why removed rather than declared

wrap had no second road to a consumer. That check is what separates this verdict from pinned, which data-table also never reads (0 reads, same query) and which is nonetheless kept: ObjectGrid's own reorder pass consumes pinned before the array reaches the slot and re-expresses it as the sticky className the renderer does read. wrap had no such pass anywhere in the repo.

Retired, not merely deleted — the key is typed, as both gradings required

Both the original grading and triage's restatement said either resolution must also type the key, since it was read off an untyped col. #6637 (#6459) landed the machinery for that yesterday, so this card had only to move wrap across it:

  • wrap is no longer carved out of RetiredListColumnKey's Exclude, so the derived tombstone band now types it never.
  • Its ObjectGridColumnHolds member is gone — that hold existed only because this card was pm:blocked.

Re-adding ...(col.wrap !== undefined && { wrap: col.wrap }) is now a compile error naming wrap. Otherwise "retired" is just a deleted line the next edit can put back for free. Because the band derives from the authored ListColumn, removing the carve-out was the whole edit — no hand-maintained list to update.

Shown able to fail

The pin is compile-time, so a rendering test is blind to it. The refusal was ablated: putting the 'wrap' carve-out back into the Exclude (mutation confirmed on disk by blob hash a4072b0e to 59da928d, plus anchored grep counts in both directions) turns tsc -p tsconfig.test.jsonred, exit 2, with both predicted diagnostics:

columnEmitBoundary-6004.test.ts(192,57): error TS2339: Property 'wrap' does not exist on type 'ObjectGridColumnDraft'.
columnEmitBoundary-6004.test.ts(196,5): error TS2578: Unused '@ts-expect-error' directive.

Restore proven byte-identical to HEAD (blob back to a4072b0e, git diff HEAD empty). Note the standing property this relies on: TS2578 makes an unused @ts-expect-error an error, so a green type-check is itself proof the tombstone bites. --listFiles confirms the pin file is in the test program (tsconfig.json excludes __tests__, tsconfig.test.json includes it), so the check is not silently skipping it.

What is deliberately NOT changed

The authorable spelling is untouched. @objectstack/spec still declares ListColumn.wrap, and packages/react's spec-bridge still forwards it into the grid schema — its SpecBridge pin test still passes unchanged. Whether that spec property should keep being declared with no renderer anywhere is a spec-side enforce-or-remove question outside this card's fence; filed as #6650 rather than settled from inside this renderer.

Runtime behaviour is unchanged: an authored wrap did nothing before and does nothing now. What changed is that the code no longer implies data-table might consume it.

Verification

All runs from the repo root. Final head fc721d2e; the union below was re-run on that head.

checkresult
tsc --noEmit + tsc -p tsconfig.test.json (plugin-grid)VERDICT command-exit 0
vitest run packages/plugin-grid/src/__tests__/73 files, 668 tests passed
vitest run components/renderers/complex + spec-bridge15 files, 108 tests passed
turbo run type-check --filter=...@object-ui/plugin-grid46 successful, 46 total
eslint (plugin-grid, whole package)0 errors
check-control-bytesOK, 5492 tracked text files
check-vi-mock-specifiersOK
check-package-self-importOK
check-changeset-presence / -no-major / -fixedall OK

The type-check filter is the downstream direction — verified by enumerating it rather than assuming: console, app-shell, plugin-designer, plugin-report, plugin-view, site, and three example apps. Consumers read a rebuilt .d.ts, since type-checkdependsOn^build.

The changeset gate ruled one owed and it is declared minor (major is mechanically refused repo-wide): runtime behaviour is unchanged, but @object-ui/plugin-grid's exported emit types narrow — ObjectGridColumnHolds loses wrap, and ObjectGridColumnDraft['wrap'] becomes never.

One stale claim corrected in passing

The docblock this PR edits asserted the held keys are "undeclared by TableColumn". That is no longer true of headerIconTableColumn declares it today (packages/types/src/data-display.ts:369, inside the interface closing at 414), so its ObjectGridColumnHolds entry is redundant rather than load-bearing. Only the sentence was corrected, since the hold itself is #6424's to settle; flagged here rather than silently restated.


Generated by Claude Code

`generateColumns()` copied a per-column `wrap` onto the column object handed
to `data-table`, and `data-table.tsx` never read it. Measured on this ref with
comments stripped: a column-level `wrap` scores 0 reads, against 34 for
`accessorKey`, 5 for `align`, 4 for `header`, 4 for `className` and 2 for
`fitContent` in the same query shape.
There is also nothing for it to switch on. `data-table`'s cell wrapper is a
two-way `isFit ? 'w-full whitespace-nowrap' : 'truncate w-full'`, with a
`title` tooltip as the only concession to overflow, and the file does not read
`density` or `rowHeight` at all — no clamp, no expand, no wrap affordance.
Unlike `pinned` (also unread by `data-table`, but consumed by this file's own
reorder pass and re-expressed as a sticky `className`), `wrap` had no second
road to any consumer, so enforce-or-remove resolves to remove.
Retired rather than merely deleted: `wrap` is no longer carved out of
`RetiredListColumnKey`'s Exclude, so the derived tombstone band now types it
`never` and re-adding the forward is a compile error naming the key.
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.3 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-Z94jPzVs.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)507.87KB115.07KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
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.41KB34.47KB
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.62KB54.56KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
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.72KB7.71KB
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)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)9.30KB3.22KB
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 09:11
@os-sales
os-sales added this pull request to the merge queueAug 28, 2026
Merged via the queue into main with commit 67a87d9Aug 28, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-5453-wrap-forward-enforce-or-remove branch August 28, 2026 09:23
os-sales pushed a commit that referenced this pull request Aug 28, 2026
…derIcon hold
`ObjectGridColumnHolds` exists for keys `data-table` reads that `TableColumn`
does not declare. `headerIcon` stopped meeting that entry condition when
objectui#6615 declared it on `TableColumn`, and nothing went red at the moment
of loss — the same silent expiry recorded for `options`.
Measured rather than derived (the previous round could only derive it, because
the file was fenced behind another claim):
- `Equal<ObjectGridColumn['headerIcon'], TableColumn['headerIcon']>` is `true`,
and so is the `ObjectGridColumnDraft` form. Instrument controls in the same
shape answer `false`/`true` as expected, so the probe has resolution.
- Ablation on the resolved emit types (every member's type printed via the
TypeScript checker, through the same program the test project uses): with
the member deleted both types are byte-identical at 27 members. Positive
control — deleting `pinned` instead takes them to 26, `pinned` gone.
- `headerIcon` is not a `ListColumn` member (control: `width` is), so it was
never in the derived `RetiredListColumnKey` band (control: `wrap` is).
`pinned` is untouched and stays load-bearing on both counts: `TableColumn` does
not declare it and the band carves it out, so the hold is its only declaration
on the emit types, and this file's reorder pass re-expresses it as the sticky
`className` `data-table` actually reads.
Removing the hold moves a liveness claim rather than deleting one: the emit
types now get `headerIcon` from `TableColumn` alone. `columnHoldsExpiry-6424`
pins that dependency, so reverting the declaration is loud instead of silent.
Also corrects the docblock's general claim that the held keys are "undeclared
by `TableColumn`", false as written since #6615, in the places #6651 left
alone — including the emit-boundary pin that still said the interface declares
two keys.
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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] ObjectGrid forwards a per-column wrap into the DataTable column object, but nothing in data-table.tsx ever reads it

2 participants

@os-sales@claude