Skip to content

fix(app-shell): write a served view's toolbar change inside config so it survives reload (objectui#11625) - #11639

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11625-density-inside-config
Oct 5, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11625-density-inside-config

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11625

Clause-②: no

What changed

buildPersistedViewBody builds the body persistViewPatch sends through updateViewConfig when a grid toolbar control changes. It now writes the patch inside config when the active tab carries the stored ViewItem envelope { name, object, viewKind, config }:

  • A patch key that ListViewSchema declares goes into config: rowHeight, sort, hiddenFields or inlineEdit. The key set is read off the spec schema, the same derivation buildViewConfigSaveBody uses. A stale copy of that key on the envelope is removed, so the request carries the key once, in the place the door keeps.
  • Any other key stays on the envelope: columnState, plus the row state the envelope already carries (isDefault, isPinned, sortOrder, visibility), which is never moved. This is the objectui#11013 fence.
  • A flat base (no config) still gets the flat spread. The overlay branch (isSavedView: false) is unchanged.

The server side is untouched. The door still drops undeclared envelope keys, which triage (5986778563) keeps as designed. Nothing changes on the @object-ui/app-shell entry: buildPersistedViewBody is exported from ObjectView.tsx, not from the package.

Measured live, before and after

Stack: the console from this worktree (Vite, workspace src aliases) against objectstack main 045f764c, showcase app, objectstack dev --seed-admin --fresh, admin session. Each row is one toolbar action in Chromium. I captured the PUT /api/v1/meta/view/NAME body and answer in the browser, read the row back with GET /api/v1/meta/view/NAME, and then reloaded in a fresh browser context. "Before" ran this branch with ObjectView.tsx checked out at the base 1c2e2c4. "After" ran the fix on a new --fresh database.

Path Before: PUT, stored row, reload After: PUT, stored row, reload
Stock showcase_task.default, density rowHeight: "medium" beside config. Answer 200 "Saved customization overlay". Stored row has no rowHeight, either top-level or in config. Reload shows "Density: Compact". config.rowHeight: "medium", no top-level copy, 200. Stored row has config.rowHeight: "medium". Reload shows "Density: Comfortable" with 45 px rows.
Stock showcase_task.grid, density Same loss as the row above. config.rowHeight: "medium" stored. Reload shows Comfortable, 41 px rows before and 45 px after.
User-created showcase_task.my_tasks, density Same loss. config.rowHeight: "medium" stored. Reload shows Comfortable, 45 px.
Stock showcase_task.default, column-header sort on Title Top-level sort, 200. Stored row has no sort. config.sort stored as [{ field: "title", order: "asc" }]; the door strips the console's row id. The reload's data request carries sort=title.
Stock showcase_task.urgent, hide-fields toggle, hiding Priority Top-level hiddenFields, 200. Stored row has no hiddenFields. Reload shows the Priority column again. config.hiddenFields: ["priority"] stored. Reload shows no Priority column.
Post-merge e7ed7fd, stock showcase_task.tabular, density not run config.rowHeight: "medium" stored beside the view's declared config.sort, which is kept. Reload shows Comfortable, 45 px.

The my_tasks row was created with the envelope "Add View" writes (viewEnvelope), PUT as active. On every showcase view the hide-fields toggle is off by default (userActions.hideFields). For urgent I turned it on with an envelope save carrying config.userActions.hideFields: true, the shape "Edit view config → Save" writes.

What the PM's mechanism assumptions measured:

  • Which branch. Every stock list view and the user-created one take the saved branch. The before-PUT body is the whole served item and carries no _isOverride. The server serves each list view as a view item, so listViews() returns it and isSavedViewId answers true. The tab carries config because MetadataProvider merges every served view into objectDef.listViews, and loadViewOverrides lays the raw stored row over it.
  • The overlay branch's flat body is what the door keeps. I put a marked flat overlay row on showcase_task.done. The density toggle there sent a flat { rowHeight: "tall", viewKind, object, name, _isOverride: true } and got 200. The read-back has a top-level rowHeight: "tall", and the reload shows "Density: Spacious". This is identical before and after, so that branch needs no config and is left alone.
  • The card's control. An envelope PUT with config.rowHeight: "tall" on showcase_task.in_progress reads back config.rowHeight: "tall".
  • The fence. In every saved-branch PUT and read-back, isDefault stayed on the envelope. The unit pins below cover the full row-state set and columnState.

Tests

  • New: ObjectView.densityInsideConfig-11625.test.ts.
    • It pins the body for each branch: an envelope-shaped saved row, a flat saved row and the overlay. It also pins the row-owned fence.
    • It runs a round trip through the real ObjectStackAdapter into a store whose saveItem judges every PUT with the spec's ViewMetadataSchema and keeps the parsed value, as the live door does. It then reads the row back through listViews(), loadViewOverrides and buildViewTabs.
    • The file includes a control: the pre-fix spread is accepted by that store and its density is dropped, so the store is shown to discriminate.
  • Ratchet change in ObjectView.overlayPatchOnly.test.ts. This is the saved-view describe, two cases. Their SAVED_TAB carries a config. The live door was measured stripping a top-level patch key from exactly that shape, so the two assertions that pinned the patch at the top level pinned the defect.
    • carries the whole body now asserts that body.config equals the row's config plus the sort, and that the body has no top-level sort.
    • leaves its definition intact at rest now asserts that the stored config equals the row's config plus rowHeight, and that the row has no top-level rowHeight.
    • Every other assertion in both cases stands: columns, filter, label, and the withheld _isOverride. The overlay-branch cases and both source ratchets are untouched. Each changed case gains an absence assertion, so the cases are tighter than before, not looser.
  • Run at 0113970, before the merge.
    • The four files that pin buildPersistedViewBody or its writes passed: pnpm exec vitest run over ObjectView.densityInsideConfig-11625.test.ts, ObjectView.overlayPatchOnly.test.ts, InterfaceListPage.hollowOverlayHydration.test.tsx and ObjectView.emptyColumnsNoProjection-10694.test.tsx gave Test Files 4 passed (4) and Tests 40 passed (40).
    • pnpm exec vitest run packages/app-shell/ gave Test Files 1001 passed | 1 skipped (1002) and Tests 9940 passed | 9 skipped (9949), exit 0.
    • pnpm --filter @object-ui/app-shell lint exited 0 with 0 errors.
  • Root gates. Each was run before the merge and again on the merged head, exit 0 every time: check:new-line-citations, check:control-bytes, check:test-path-roots, check:metadata-write-doors, check:changeset-claims, check:pending-changeset-literals, scripts/check-changeset-presence.mjs and scripts/check-changeset-no-major.mjs.
  • NOT MEASURED when this PR was opened. The results go in the os-dev-report comment on the card. All of these are queued under the shared verify lock behind another seat's run:
    • type-check: pnpm exec turbo run type-check --filter=@object-ui/app-shell. The plain pnpm --filter @object-ui/app-shell type-check exited 2 on TS2307 (cannot find @object-ui/components and the other dependencies), because the worktree had none of the dependency dist/ built. That is an unmet prerequisite, not a reading on this diff.
    • the one-shot ablation: the pre-fix saved-branch spread put back through ablation-replace.mjs, the two pin files run, then restored.
    • the post-merge rerun of the targeted files, lint and the app-shell suite.

Acceptance notes

  • Still open, measured, and not changed here. Within one page session, a second toolbar change to the same view overwrites the first. Example on showcase_task.in_progress: change density, wait past the debounce, then sort by header. The second PUT's config carries the sort but not the density, because every save is a whole-document PUT built from the tab as it was at page load. After a reload only the sort is left. The overlay branch does the same: on showcase_task.done, the second PUT { sort, … } replaces the { rowHeight, … } row. This predates this change and affects every kind of row. It is reported to the seat with the reproduction, not fixed here: the right shape (accumulate written patches in the session, re-read after a write, or read-modify-write in the adapter) is not pinned. The changeset says it plainly.
  • The overlay branch's only console-reached path today is refused. On an object with no list views (showcase_account), the density toggle sends PUT /api/v1/meta/view/all with the fallback tab's id all and no viewKind. The door answers 422 INVALID_METADATA (container branch, "Unrecognized key(s) on this view container: rowHeight, _isOverride"). Reported to the seat, not touched.
  • A marked flat overlay row on a served view takes that view's place on read. After the showcase_task.done overlay above, GET /api/v1/meta/view/showcase_task.done returns the overlay with no config, columns or filter. The console then loads the Done view without its status = done filter. From a clean state the console does not originate such a row, since every served list view is classed saved. Reported to the seat with the measurement.
  • inlineEdit is routed by the same rule and unit-pinned. Its live toggle was not driven, so that is NOT MEASURED. The same holds for a live columnState drag.

Generated by Claude Code

claude added 3 commits October 5, 2026 02:12
…en the row is an envelope (objectui#11625)

The density toggle on a served view sent the whole active tab, which
carries the stored ViewItem envelope `{ name, object, viewKind, config }`,
with `rowHeight` spread beside `config`. The save door judges a body with
a `config` by the spec's `viewItem` member, strips its top level, answers
200 and stores no density. `buildPersistedViewBody` now writes the patch
keys `ListViewSchema` declares into `config` for an envelope-shaped base,
leaves the row-owned keys (`columnState`, `isDefault`, ...) on the
envelope, and keeps the flat spread for a flat base and the overlay
branch unchanged.

Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
…e `config` (objectui#11625)

Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3314.5 KB 3330.4 KB
Main entry chunk (gzip) 151.6 KB 350 KB
Entry file index-BvN5x7Cl.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.22KB 6.37KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 573.73KB 137.60KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 232.57KB 64.51KB
fields (index.js) 262.75KB 66.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 39.47KB 11.25KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.17KB 15.46KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 143.54KB 38.79KB
plugin-designer (index.js) 231.41KB 48.84KB
plugin-detail (index.js) 245.97KB 64.67KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 235.92KB 64.87KB
plugin-kanban (index.js) 50.06KB 15.74KB
plugin-list (index.js) 116.72KB 29.10KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.80KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.36KB 2.51KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 5, 2026 04:40
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 5, 2026 04:40
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit 59917c4 Oct 5, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11625-density-inside-config branch October 5, 2026 04:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants