Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 97 additions & 1 deletion src/routes/v2/shared/windows/windowPersistence.test.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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<string, ReturnType<typeof createWindowState>>,
) => ({
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();
});
});
38 changes: 33 additions & 5 deletions src/routes/v2/shared/windows/windowPersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ type WindowLayoutStorageMap = Record<string, PersistedWindowLayout>;

const storage = getStorage<string, WindowLayoutStorageMap>();

const CURRENT_VERSION = 4;
const CURRENT_VERSION = 5;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will reset ALL saved states across users, are we sure we want to do this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair thing to double-check — and on that line alone you would be right. What makes it safe is that the same change adds a migration step, so existing layouts get carried forward instead of thrown away. Window positions, sizes, dock widths, ordering and minimised state all survive. The only thing removed is the stuck 300px height that causes the bug, and only where it exactly matches the old default.

Two things worth knowing:

Version 4 is the only version anyone actually has. The counter was introduced at 4 and has never been bumped since, so there are no older layouts out in the wild to discard.

The bump is what keeps this a one-time cleanup. We cannot tell the old stamped 300 apart from someone who deliberately dragged a panel to exactly 300, so there is a judgement call either way. With the version bump it happens once: after a user next moves a window their layout is marked as migrated, and from then on a deliberate 300 sticks. Without the bump we would have to strip 300 on every single load, forever, which would permanently stop anyone from choosing that height on purpose.

On that judgement call — landing on exactly 300 by hand is very unlikely anyway. Resizing starts from the measured content height, so drags land on fractional values, and the minimum heights in play are 50 and 280, so nothing snaps to 300.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ ai comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it is valid; when I tested locally it did not reset my window layout. The only windows that will be reset are ones that are exactly 300px high


function saveWindowLayoutImmediate(store: WindowStoreImpl): void {
const existingLayout = loadWindowLayout();
Expand Down Expand Up @@ -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<string, PersistedWindowState> = {};
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);
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/routes/v2/shared/windows/windowStore.utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading