…bar changes for the session (objectui#11643)
On an object that declares no list view, buildViewTabs pushes a tab the
console makes itself (id `all`). Its toolbar changes took the overlay
branch of persistViewPatch and were sent as PUT /meta/view/all with no
viewKind; the door refused them with 422 INVALID_METADATA and the change
was gone on reload.
The tab is now marked where it is made, with a module-local symbol, and
persistViewPatch returns before scheduling anything for a marked tab that
no stored row backs. One rule covers density, sort, hidden fields, column
state and inline edit. A served view whose id is also `all`, a saved view
beside the tab, and a stored row named `all` that shadows it keep saving.
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
Fixes #11643
Clause-②: no
What changed
On an object that declares no list view,
buildViewTabspushes a tab the console makes itself: "All Records", idall. Its toolbar changes took the overlay branch ofpersistViewPatchlike a served view's and were sent asPUT /api/v1/meta/view/allwith noviewKind. The door refused them (422 INVALID_METADATA), an error toast appeared, and the change was gone on reload.Triage's ruling (comment
5988346142) chose "not offered as saved": the tab's toolbar changes apply for the session and send no metadata write, and no view is created to hold them.packages/app-shell/src/views/ObjectView.tsx:fallbackTab) with a module-local symbol,CONSOLE_MADE_TAB. It is not exported. A symbol key rides every spread of the tab the page takes, andObject.entries,Object.keysandJSON.stringifyskip it, so it never reaches a request body.persistViewPatchreturns before it schedules anything (no read, no PUT, no toast) when the tab carries the mark and no stored row backs it. "Stored row" isisSavedViewId, the classification the write below it already uses.persistViewPatch: density, sort (the schema's and the list's callback), hidden fields, column order and widths (both callbacks), and inline edit. The list keeps each change in its own state, so it applies for the session.all, a saved view beside the console's tab, and a stored row namedallthat shadows that tab all keep their save path.No export, prop, exported type member or i18n key is added to any
@object-ui/*package. There is no server-side change. The served-view and saved-view save paths are unchanged.Measured
Live before and after
Backend: objectstack
main27991556,examples/app-showcase,objectstack dev --seed-admin --fresh, built in a separate worktree. Two consoles ran against that one backend: base531b26cand this branch. A headless Chromium logged in as the seeded admin, clicked the toolbar's Density button, recorded every/meta/viewrequest and answer, read the toasts and the console errors, and reloaded the page.showcase_accountdeclares no view:GET /api/v1/meta/view?object=showcase_accountanswersitems: [], and its object document carries nolistViewsand nolist. The only tab is "All Records".531b26cDensity: Comfortable, row 41 to 45 pxDensity: Comfortable, row 41 to 45 px/meta/viewPUT /api/v1/meta/view/all, body{"rowHeight":"medium","object":"showcase_account","name":"all","_isOverride":true}422,INVALID_METADATA, "Unrecognized key(s) on this view container:rowHeight,_isOverride"[ObjectView] Failed to persist view config: ...Density: CompactDensity: Compact(session-only, as ruled)The served view
showcase_task.defaultstill saves on this branch. The click sentPUT /api/v1/meta/view/showcase_task.defaultand got200, and after reload the button readDensity: Comfortable(45 px). Base did the same:PUTanswered200, and the density survived the reload.Pins
New file:
ObjectView.fallbackTabSessionOnly-11643.test.tsx. It reuses the objectui#11642 harness: the real page, the realpersistViewPatchwith its debounce, and the realObjectStackAdapterover a store that judges eachPUTwith the spec'sViewMetadataSchema.persistViewPatchsends noupdateViewConfig, nosaveItemand nogetItem, and raises no toast and no persist error.isSavedView: true) and survives a reload.all(a bare key in the object's ownlistViews) keeps its overlay save path (isSavedView: false), composes with its stored overlay, and survives a reload.allthat shadows the console-made tab is a saved row. Its write is sent (isSavedView: true) and survives a reload.Reverse verification
Run on the committed fix (
80b203a) throughablation-replace.mjs. That tool checked the anchor count, checked that the blob changed, and restored the file to the HEAD blob withgit diff HEADempty after each leg.viewIdLocal === 'all'isSavedViewIdclause droppedWith the guard removed, case 1 failed on its write count. The one coalesced write it caught was
updateViewConfig('track_note', 'all', { rowHeight, sort, inlineEdit, columnState }, { isSavedView: false }).hiddenFieldswas not in it, becausepersistHiddenFieldsalready kept that toggle session-only on a tab that declares no columns. The prediction for that leg named case 1 only. Case 2 also went red, because its first half drives the console-made tab. The test header records the measured direction.Gates
All local gates ran on head
bd7343f, which is80b203aplus a test-header commit plus a merge oforigin/main0baf86f. That merge does not touchObjectView.tsx. The live run and the reverse verification ran on80b203a. Exit codes are below.pnpm exec vitest run packages/app-shell/(repo root, under the shared verify lock): exit 0.Test Files 1005 passed | 1 skipped (1006)andTests 9979 passed | 9 skipped (9988).vitest list --filesOnlycounts the same 1006 files, the new one among them.pnpm --filter @object-ui/app-shell type-check, after the@object-ui/app-shell^...closure build: exit 0. The script echoestsc --noEmit && tsc -p tsconfig.test.json, and--listFilesOnlyontsconfig.test.jsonincludes the new test file.pnpm --filter @object-ui/app-shell lint: exit 0, with 0 errors. Every warning isno-explicit-any, and the new test file's warnings follow the objectui#11642 harness it copies.check-object-metadata-write-doors,check-new-cross-file-line-citations,check-control-bytes,check-changeset-presence,check-changeset-no-major,check-changeset-fixed,check-changeset-claims,check-changeset-overwrite,check-pending-changeset-literals,check-vi-mock-specifiers,check-vi-mock-inherit,check-vi-mock-override-shape,check-test-path-roots,check-type-check-coverage,check-unreferenced-sources,check-i18n-call-site-keysandcheck-handler-key-read-sites.Changeset:
.changeset/11643-fallback-tab-session-only.md,@object-ui/app-shellpatch.Acceptance notes
persistHiddenFieldsalready returned early on the console-made tab, because the tab declares nocolumnsand no row backs it. The new rule covers that toggle as well, so all five controls answer to one rule. The early return inpersistHiddenFieldsitself is unchanged.allexists in no example. CRM declareslistViews.alloncrm_lead,crm_activityandcrm_opportunity, but the spec'sexpandViewContainernames thosecrm_lead.alland so on (measured forcrm_lead).ViewItemNameSchemarequires a dotted name, so a served ViewItem cannot be named a bareall. A barealltab id can only come from an object document's ownlistViews. Pin 3 covers that case.listViewscarries noviewKind. Its first overlayPUTdepends on the door inheriting one from a registry entry of the same name. No example has such an object, so this was not exercised live. Pin 3 seeds a stored overlay that carriesviewKind. Carrier: none.The session that wrote this is
https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL.Generated by Claude Code