Skip to content

fix(components,plugin-grid): fire column-state persistence on column resize and reorder - #6211

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-6175-column-state-persistence
Aug 25, 2026
Merged

fix(components,plugin-grid): fire column-state persistence on column resize and reorder#6211
yinlianghui merged 2 commits into
mainfrom
claude/issue-6175-column-state-persistence

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Fixes#6175

A user drags a column wider or reorders columns in ObjectGrid, and nothing persists. The
saved state is read back correctly forever; it is simply never written. Both call sites of
saveColumnState were dead, for two different reasons — one per package.

Re-derived line numbers (measured at 2c8474c04, the merge-base of this branch)

The card's numbers were measured 2026-08-24 22:29Z. Re-derived here; every one still held.

SiteLineNote
saveColumnState definitionObjectGrid.tsx:661unchanged
onColumnStateChange notify inside itObjectGrid.tsx:669-670unchanged
const dataTableSchema: anyObjectGrid.tsx:2937unchanged
onColumnResize producer call siteObjectGrid.tsx:3084unchanged
onColumnReorder producer call siteObjectGrid.tsx:3090unchanged
SchemaRenderer schema={dataTableSchema}ObjectGrid.tsx:3735unchanged
onColumnsReorder — the renderer's only reorder calldata-table.tsx:1346-1347unchanged
onColumnResize in data-table.tsxzero occurrences, confirmed
onColumnReorder in data-table.tsxzero occurrences, confirmed
onColumnsReorder?: (columns: TableColumn[]) => voiddata-display.ts:718unchanged
onColumnResize?: (columnKey, width) => voiddata-display.ts:791unchanged
onColumnReorder?: (newOrder: string[]) => voiddata-display.ts:796unchanged

One site the card did not name and that mattered: ObjectGrid.tsx:2992 sets
reorderableColumns: schema.reorderableColumns ?? false, so grid reordering is off by
default
— the reorder test has to enable it explicitly or it measures nothing.

The change

@object-ui/componentsdata-table.tsx.DataTableSchema declared onColumnResize
all along and this renderer invoked it nowhere: the drag updated local columnWidths state
and stopped. handleResizeEnd now reports the settled width once, at mouseup, via a
lastResizeWidth ref (the document listeners are one render's closures and cannot read a
later state update). Once, deliberately: the host turns this into a write to shared view
config, so a per-mousemove callback would be a write storm.

@object-ui/plugin-gridObjectGrid.tsx. The producer emitted onColumnReorder
(singular) while the renderer invokes the near-duplicate onColumnsReorder (with the s) —
a different declared key with a different signature. The producer now emits the spelling the
renderer actually invokes, mapping the reported TableColumn[] to the accessorKey order
columnState stores.

Producer-side, per AGENTS.md #0.1 (fix the producer, don't add a lenient alias in the
consumer
). Teaching the renderer to also call the singular spelling was the alternative
and is rejected here: it would give one event two live call paths, which is the
consumer-side dialect that commandment bans — and it would make the open ruling below
strictly harder, converting a dead-key removal into a behaviour change.

⛔ Nothing is retired, and the deferred ruling is left standing

Both spellings remain declared on DataTableSchema; packages/types/src/data-display.ts is
untouched. onColumnReorder stays declared and stays unwired — exactly its status before
this branch. The two-spelling ruling was not needed and was not taken. PR #6181 parked
that ruling on the card this branch implements, and it is still parked: which spelling
survives is a declared-surface change and remains open.

Which spelling the renderer invokes after this change: onColumnsReorder, the same one it
invoked before.
The renderer's reorder path is unchanged. onColumnReorder is
unchanged in reachability too — still declared, still invoked by nothing. What changed
is that it is no longer emitted by ObjectGrid either, so it is now dead on both ends
rather than written-but-unread.

⚠️ Interaction with the permission gate in flight

onColumnStateChange reaches dataSource.updateViewConfig. That path was unreachable
from a column drag
before this branch and is reachable now — a resize or reorder now
produces a real write to shared view config where it previously produced none. Any
permission gate on that write (#5232, via PR #6125) will therefore see traffic on a path
that had never carried any. updateViewConfig itself is not touched here.

⛔ Not repaired here (deliberately)

const dataTableSchema: any (ObjectGrid.tsx:2937) is the enabling cause and is left
alone — it belongs to #6004, not to this branch. A typed handoff would have caught the
reorder half and not the resize half:
with a real DataTableSchema on the producer,
onColumnReorder would still have type-checked (it is genuinely declared, with the exact
signature ObjectGrid used) — the two declared spellings defeat the type. The resize half is
different: no type system flags a consumer that never reads a declared optional key. That
asymmetry is worth carrying to #6004.

Verification

Reproduce-before-fix, then ablate-after-commit. Both directions predicted before running.

  • Repro (before the change): both columnStatePersistence cases red, and red for a
    value reason — AssertionError: expected "vi.fn()" to be called at least once, not a
    query or harness error.
  • Ablation (change reverted, tests kept, restore under trap … EXIT INT TERM): 3 of 4
    cases red; git diff HEAD --stat empty afterwards. Mutation proven on disk both ways —
    injected strings absent, removed strings back, with handleResizeEnd / saveColumnState
    as controls present in both versions of each file.
  • ⚠️ The one assertion that survives a revert:does not fire when the drag never moved.
    The unrepaired renderer fires nothing at all, so "did not fire" is trivially true there.
    It is a guard against over-firing, not a pin of the change, and the test file says so
    inline. The three cases that do distinguish the two states of the world are the other
    three.

The pins observe the write, never the read-back: localStorage actually written and
onColumnStateChange actually fired. A test that seeds a width and re-reads it passes with
the outbound half still dead, because the already-working inbound half answers it.

GateCommandExit
type-check (both packages, uncached)turbo run type-check --filter=@object-ui/components --filter=@object-ui/plugin-grid --force0Tasks: 15 successful, 15 total
type-check downstream sweep, uncachedturbo run type-check --filter='...@object-ui/components' --filter='...@object-ui/plugin-grid' --force0Tasks: 66 successful, 66 total, 31 packages
vitest, both packagespnpm exec vitest run packages/plugin-grid packages/components0273 passed (273) files, 2516 passed (2516)
vitest, packages/typespnpm exec vitest run packages/types058 passed (58) files, 632 passed (632)
eslint (merge-base delta, plain)pnpm exec eslint over the 5 changed paths0 — 0 errors
check-control-bytesnode scripts/check-control-bytes.mjs0scanned 5125 tracked text file(s)
check-lint-coveragenode scripts/check-lint-coverage.mjs046/46 packages linted
check-type-check-coveragenode scripts/check-type-check-coverage.mjs041/41 packages compile their tests
check-changeset-presencenode scripts/check-changeset-presence.mjs0 — 1 changeset for 5 source files
check-changeset-no-majornode scripts/check-changeset-no-major.mjs0 — changeset is patch

The downstream filter direction is demonstrated, not asserted: --filter '...@object-ui/components'
resolves to 31 packages including plugin-grid, app-shell, console and all four
example-* apps (consumers); --filter '@object-ui/components...' resolves to 9 including
types, core, react (dependencies). The prefix form is the downstream one, and it is
what ran — so ./examples/** is built and type-checked here.

Every turbo gate treated as evidence ran with --force; Cached: 0 cached on the run that
reports it. The ratchet-family gates and eslint were re-run on the final head
a31843d79 with a clean tree, after the last commit.

Deviation from the declared file surface, declared

The dispatch named data-table.tsx, ObjectGrid.tsx, tests and a changeset. One file
beyond that is touched: packages/types/src/__tests__/zod-mirror-parity.test.ts — a doc
comment only
, no assertion and no ledger membership changed (that file's 5 tests pass, and
RuntimeOnlyDeclared is about zod mirroring, not about being read). Its
RuntimeOnlyDeclared note stated that onColumnResize is "read NOWHERE" and that
"column-state persistence never fires". After this branch both sentences are false, and the
note names this very card — leaving it would have left a documented falsehood pointing at
its own repair. The note now restates the two-spelling residue as the deliberate open ruling
it is. No changeset bump for @object-ui/types: the change is a comment.


Generated by Claude Code

…and reorder
`DataTable` declared `onColumnResize` and never invoked it, and `ObjectGrid`
emitted `onColumnReorder` while the renderer invokes `onColumnsReorder`. Both
were the only call sites of `saveColumnState`, so a user's column drag was never
written — not to localStorage, not through `onColumnStateChange`.
Part of #6175
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
…e fires
The ledger note described objectui#6175 as unfixed and named `onColumnResize` as
read nowhere. Both are now false. The two-spelling ruling stays open and is
restated as the deliberate residue it is; no ledger membership changes.
Part of #6175
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3222.6 KB3990.2 KB
Main entry chunk (gzip)153.8 KB350 KB
Entry fileindex-DPsYWkzH.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)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
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)505.28KB114.58KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)171.74KB47.48KB
fields (index.js)238.40KB59.89KB
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.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
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.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.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.45KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.14KB61.94KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)126.07KB30.78KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)201.14KB54.40KB
plugin-kanban (index.js)52.89KB14.59KB
plugin-list (index.js)111.94KB27.24KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.57KB20.74KB
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)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
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)7.54KB2.63KB
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)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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 02:42
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 129bcc5Aug 25, 2026
26 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-6175-column-state-persistence branch August 25, 2026 02:53
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui@claude