fix(app-shell): each toolbar save starts from the row the store holds, so a second change keeps the first (objectui#11642) - #11646
Merged
Conversation
…, so a second change keeps the first `persistViewPatch` sent every whole-document PUT from the tab as it was at page load, or from the pending patch alone for an overlay, and nothing moved that base after a write landed. A second toolbar change in one session therefore dropped the first, on the envelope row, the flat row and the overlay. Each write now reads the stored row through `loadViewOverrides` when it runs (the PUT answer carries no row, and the door drops undeclared keys), places the patch on it for a saved view, and composes the stored overlay's own keys with the patch for an overlay. Writes to one view are chained so a write never reads the row from before the previous one landed. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…ew-save-base Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…nused in the objectui#11642 pin `tsc -p tsconfig.test.json` refused it (TS6133). Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
… for the write-refusal census The chained write ran through a named local `write` function, so `writeRefusalCensus-11583.test.ts` keyed the `updateViewConfig` call under `write` and found none under `persistViewPatch`. The chain is now one assignment with an anonymous step, and the new helper types its rows as `Record<string, unknown>`. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
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
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #11642
Clause-②: no
What changed
persistViewPatch(packages/app-shell/src/views/ObjectView.tsx) sends each grid toolbar change as a whole-documentPUT /api/v1/meta/view/NAME. It built every body from the active tab as it was when the page loaded, or, for an overlay, from the pending patch alone. Nothing moved that base after a write landed, so a second change in one session dropped the first, on every row kind.Each write now starts from the row the store holds when the write runs:
toolbarWriteInputs, reads the row throughloadViewOverrides, the same reader the page load uses.buildPersistedViewBodyplaces the patch on. The tab is the base only when no row was read (a draft-only view in preview mode, or a failed read). That is the old behaviour.VIEW_OVERLAY_OWNED_KEYSnames, the setloadViewOverridesnarrows a marked overlay to. The tab still suppliesviewKindonly, so the body still carries nothing the shadowed view owns (objectui#5233).buildPersistedViewBodykeeps its shapes; its doc now says what the caller hands it. No export, prop, type member or i18n key is added, and the server side is untouched.Where the stored row comes from
Measured on the wire against objectstack
maina3ffc451(showcase app,objectstack dev --seed-admin --fresh):PUT /api/v1/meta/view/NAMEanswer carries no row:{ success, version, seq, state, message }, the shapeSaveMetaItemResponseSchemadeclares.rowHeightbesideconfig, and a sort entry carrying a rowid, read back without the top-levelrowHeightand without theid(ADR-0005 appendix (c)).GET /api/v1/meta/view/NAMEand the list readGET /api/v1/meta/viewreturned the same stored item.So the source is a read-back: the list read
GET /api/v1/meta/view, throughloadViewOverrides, taken when each write runs. The adapter drops that read's cache on every view write (invalidateViewKeys), so the read after a write returns the post-write row. The live runs show it: each run's second PUT follows oneGET /api/v1/meta/view, and the first PUT adds no request because it reads the cache the page load filled.Reading when the write runs, instead of keeping a copy of the row after each write, also composes with a row another handler on the page wrote in between (set as default, pin, rename), and leaves no copy to keep fresh.
Measured live, before and after
Stack: the console from this worktree (Vite, workspace
srcaliases) against objectstackmaina3ffc451, showcase app, fresh database, admin session, Chromium. Each run opens the view, toggles density, waits 1.5 s (past the debounce and the round trip), clicks the Title header sort, captures both PUTs, reads the row back withGET /api/v1/meta/view/NAME, then reloads in a fresh browser context. "Before" ran the base59917c4. "After" ran the final head5af8b90; the fix commit096bbf3gave the same readings. Rows were reset between runs.showcase_task.in_progressconfig.sortonly ·config.sort, norowHeight· "Density: Compact", sort keptconfig.rowHeight: "medium"andconfig.sort· both · "Density: Comfortable", data requestsort=titleshowcase_task.flat_probe, seeded by PUT withviewKind: "list", noconfig, no markersortonly ·sort, norowHeight· "Density: Compact"rowHeight: "medium"andsort· both · "Density: Comfortable",sort=titleshowcase_task.done, seeded as the marked overlay{ rowHeight: "compact", viewKind, object, name, _isOverride }{ sort, viewKind, object, name, _isOverride }·sort, norowHeight· "Density: Compact"{ rowHeight: "medium", sort, viewKind, object, name, _isOverride }· both · "Density: Comfortable",sort=titleconfig.rowHeightandconfig.sort· both · "Density: Comfortable"Every PUT answered
200. No overlay PUT body carriedfilter,columns,labelorconfig.Tests
ObjectView.toolbarWritesCompose-11642.test.tsx. It mounts the real object page with the realpersistViewPatchand its debounce, over the realObjectStackAdapter. The adapter's metadata client is a store that judges every PUT with the spec'sViewMetadataSchema, keeps the parsed value of the keys the body carried, and answers with no row, as the door does.ListViewis stubbed to capture the schema, so a change is the page's ownonDensityChange/onSortChangecall. A reload remounts the page over a fresh adapter on the same store.rowHeight,sort) all carry both changes. The overlay case also asserts that no body carries a source key, and that the earlier session'scolumnStaterides along.5af8b90, each throughablation-replace.mjsin wrap mode. Each reported the anchor hit once and the blob changed, then restored the HEAD blob with an emptygit diff HEAD.Tests 4 failed | 28 passed (32), every failureexpected undefined to be 'medium'. The coalescing case,ObjectView.densityInsideConfig-11625.test.tsandObjectView.overlayPatchOnly.test.tsstayed green.Tests 1 failed | 4 passed (5).5af8b90, the final head, each exit code written to a file as it landed:pnpm exec vitest runover the new pin,writeRefusalCensus-11583.test.ts,ObjectView.densityInsideConfig-11625.test.ts,ObjectView.overlayPatchOnly.test.ts,ObjectView.viewWriteRefusal-11583.test.tsx,ObjectView.densityModeThroughFold-7924.test.tsx,ObjectView.viewOverrides.test.tsx,ObjectView.setDefaultViewIdentity.test.tsx,InterfaceListPage.hollowOverlayHydration.test.tsxandObjectView.emptyColumnsNoProjection-10694.test.tsx:Test Files 10 passed (10),Tests 104 passed (104).pnpm --filter @object-ui/app-shell type-check(tsc --noEmit && tsc -p tsconfig.test.json, dependency closure built first withturbo run build --filter='@object-ui/app-shell^...'): exit 0. The test config covers the new pin: an earlier run refused an unused parameter in it (TS6133), fixed in000c155.pnpm --filter @object-ui/app-shell lint: exit 0, 0 errors.pnpm exec vitest run packages/app-shell/: exit 0,Test Files 1003 passed | 1 skipped (1004),Tests 9966 passed | 9 skipped (9975).check:new-line-citations,check:control-bytes,check:test-path-roots,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:metadata-write-doors,check:changeset-claims,check:pending-changeset-literals,check:handler-key-reads,check:unreferenced-sources,check:shell-escape-residue,scripts/check-changeset-presence.mjsandscripts/check-changeset-no-major.mjs. A control-byte scan of the three changed files found nothing.writeRefusalCensus-11583.test.tskeys each metadata write by its nearest named function. The first cut ran the write through a named local function, so the census read it under that name. The app-shell suite at the merge head75d028fcaught it (Tests 2 failed, both in that census). The chain is now one assignment with an anonymous step, so the write stays keyed underpersistViewPatchwith its refusal path on the.catch. The census ledger is unchanged.Acceptance notes
columns,type,dataandfilter, plus the tab'sidandisDefault. The door dropped most of them, butisDefaultlanded: in the before-runs the stored row gained anisDefaultkey after a density toggle. The first body is now the stored row, so a toolbar change writes only what the user changed.?preview=draftthe active tab can carry a pending draft. A toolbar write used to build the active row from that tab, which published the draft's content without the Publish step. It now starts from the published row. Not driven live.GET /api/v1/meta/view(the list read) before its PUT. Toolbar writes are debounced, so that is one read per settled change.showcase_task.done, the reloaded data request carries nostatus = donefilter. PR objectui#11639's Acceptance notes reported this to the seat. This change neither causes nor repairs it.ObjectStackAdapter.updateViewConfig's doc says it returns the persisted item when the server echoes one and undefined otherwise. The door never echoes a row, and the method returns the save answer object instead of undefined. Its only caller ignores the return. Carrier: none.Session:
https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsALGenerated by Claude Code