From 03fb11c7ca2463e53b8811ea2b86a2bc6e30e0e1 Mon Sep 17 00:00:00 2001 From: Camiel van Schoonhoven Date: Thu, 27 Aug 2026 16:01:00 -0700 Subject: [PATCH] fix: Pipeline Details Docked Window Too Small --- .../shared/windows/windowPersistence.test.ts | 98 ++++++++++++++++++- .../v2/shared/windows/windowPersistence.ts | 38 ++++++- .../shared/windows/windowStore.utils.test.ts | 6 +- 3 files changed, 133 insertions(+), 9 deletions(-) diff --git a/src/routes/v2/shared/windows/windowPersistence.test.ts b/src/routes/v2/shared/windows/windowPersistence.test.ts index 1826ebd874..92724f7246 100644 --- a/src/routes/v2/shared/windows/windowPersistence.test.ts +++ b/src/routes/v2/shared/windows/windowPersistence.test.ts @@ -1,6 +1,10 @@ import { afterEach, describe, expect, it } from "vitest"; -import { clearLayout, TOUR_WINDOW_LAYOUT_ID } from "./windowPersistence"; +import { + clearLayout, + migrateLayout, + TOUR_WINDOW_LAYOUT_ID, +} from "./windowPersistence"; afterEach(() => { localStorage.clear(); @@ -28,3 +32,95 @@ describe("clearLayout", () => { expect(TOUR_WINDOW_LAYOUT_ID).not.toBe("editor"); }); }); + +const createWindowState = (overrides = {}) => ({ + position: { x: 1472, y: 103 }, + size: { width: 280, height: 350 }, + dockState: "right" as const, + isHidden: false, + isMinimized: false, + ...overrides, +}); + +const createLayout = ( + version: number, + windows: Record>, +) => ({ + windows, + windowOrder: Object.keys(windows), + dockAreas: { + left: { width: 320, collapsed: false, windowOrder: [] }, + right: { width: 320, collapsed: false, windowOrder: Object.keys(windows) }, + }, + version, +}); + +describe("migrateLayout", () => { + it("drops the stamped dockedHeight from every window in a version 4 layout", () => { + const layout = createLayout(4, { + "pipeline-details": createWindowState({ dockedHeight: 300 }), + history: createWindowState({ dockedHeight: 300 }), + }); + + const migrated = migrateLayout(layout); + + expect(migrated?.version).toBe(5); + expect(migrated?.windows["pipeline-details"]).not.toHaveProperty( + "dockedHeight", + ); + expect(migrated?.windows.history).not.toHaveProperty("dockedHeight"); + }); + + it("keeps a dockedHeight the user chose", () => { + const layout = createLayout(4, { + "pipeline-details": createWindowState({ dockedHeight: 300 }), + history: createWindowState({ dockedHeight: 512.5 }), + }); + + const migrated = migrateLayout(layout); + + expect(migrated?.windows["pipeline-details"]).not.toHaveProperty( + "dockedHeight", + ); + expect(migrated?.windows.history.dockedHeight).toBe(512.5); + }); + + it("preserves every other window field while migrating", () => { + const layout = createLayout(4, { + "pipeline-details": createWindowState({ + dockedHeight: 300, + isMinimized: true, + preDockedSize: { width: 280, height: 350 }, + }), + }); + + const migrated = migrateLayout(layout); + + expect(migrated?.windows["pipeline-details"]).toEqual({ + position: { x: 1472, y: 103 }, + size: { width: 280, height: 350 }, + dockState: "right", + isHidden: false, + isMinimized: true, + preDockedSize: { width: 280, height: 350 }, + }); + expect(migrated?.dockAreas).toEqual(layout.dockAreas); + expect(migrated?.windowOrder).toEqual(layout.windowOrder); + }); + + it("returns a current-version layout untouched", () => { + const layout = createLayout(5, { + "pipeline-details": createWindowState({ dockedHeight: 250 }), + }); + + expect(migrateLayout(layout)).toBe(layout); + }); + + it("discards layouts older than version 4", () => { + const layout = createLayout(3, { + "pipeline-details": createWindowState(), + }); + + expect(migrateLayout(layout)).toBeNull(); + }); +}); diff --git a/src/routes/v2/shared/windows/windowPersistence.ts b/src/routes/v2/shared/windows/windowPersistence.ts index b76e483b6d..cfa519e676 100644 --- a/src/routes/v2/shared/windows/windowPersistence.ts +++ b/src/routes/v2/shared/windows/windowPersistence.ts @@ -72,7 +72,7 @@ type WindowLayoutStorageMap = Record; const storage = getStorage(); -const CURRENT_VERSION = 4; +const CURRENT_VERSION = 5; function saveWindowLayoutImmediate(store: WindowStoreImpl): void { const existingLayout = loadWindowLayout(); @@ -138,12 +138,40 @@ function isPersistedLayout(value: unknown): value is PersistedWindowLayout { ); } +// The height version 4 stamped onto docked windows. Frozen here rather than +// imported from DEFAULT_DOCKED_HEIGHT: this migration must keep matching the +// historical value even if that constant changes. +const V4_STAMPED_DOCKED_HEIGHT = 300; + +/** + * Version 4 stamped a default `dockedHeight` onto every window dragged into a + * dock. That value was inert then, but is now an enforced pixel height, so it + * pins panels that should size to their content. Dropping it restores + * fit-to-content; an explicit resize writes the field again. Heights the user + * actually chose are left alone — a drag starts from a fractional measured + * height, so landing on the stamp exactly is vanishingly unlikely. + */ +export function migrateLayout( + layout: PersistedWindowLayout, +): PersistedWindowLayout | null { + if (layout.version === CURRENT_VERSION) return layout; + if (layout.version !== 4) return null; + + const windows: Record = {}; + for (const [id, win] of Object.entries(layout.windows)) { + const { dockedHeight, ...withoutDockedHeight } = win; + windows[id] = + dockedHeight === V4_STAMPED_DOCKED_HEIGHT ? withoutDockedHeight : win; + } + + return { ...layout, windows, version: CURRENT_VERSION }; +} + function loadWindowLayout(): PersistedWindowLayout | null { const parsed = storage.getItem(getStorageKey()); - if (!isPersistedLayout(parsed) || parsed.version !== CURRENT_VERSION) { - return null; - } - return parsed; + if (!isPersistedLayout(parsed)) return null; + + return migrateLayout(parsed); } /** diff --git a/src/routes/v2/shared/windows/windowStore.utils.test.ts b/src/routes/v2/shared/windows/windowStore.utils.test.ts index 8d2b2e25b7..9eed477fae 100644 --- a/src/routes/v2/shared/windows/windowStore.utils.test.ts +++ b/src/routes/v2/shared/windows/windowStore.utils.test.ts @@ -11,9 +11,9 @@ import { DEFAULT_VIEW_PRESET } from "./viewPresets"; import { buildWindowModelInit } from "./windowStore.utils"; // Mirrors CURRENT_VERSION in windowPersistence.ts. If the schema version is -// bumped there, these fixtures must be updated (and loadWindowLayout would -// otherwise discard them, causing these tests to fail loudly). -const LAYOUT_VERSION = 4; +// bumped there, these fixtures must be updated — loadWindowLayout migrates or +// discards older versions, so a stale fixture stops exercising what it claims. +const LAYOUT_VERSION = 5; // Default storage key used when no active layout id is set (see getStorageKey). const STORAGE_KEY = "editorV2-window-layout";