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
2 changes: 2 additions & 0 deletions src/routes/v2/pages/Editor/EditorV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { usePipelineTreeWindow } from "./hooks/usePipelineTreeWindow";
import { usePropertiesWindowPositioning } from "./hooks/usePropertiesWindowPositioning";
import { useRecentRunsWindow } from "./hooks/useRecentRunsWindow";
import { useRunsAndSubmissionWindow } from "./hooks/useRunsAndSubmissionWindow";
import { useSeedInitialDockLayoutFromPreset } from "./hooks/useSeedInitialDockLayoutFromPreset";
import { useSelectionWindowSync } from "./hooks/useSelectionWindowSync";
import { useSpecLifecycle } from "./hooks/useSpecLifecycle";
import { useUndoRedoKeyboard } from "./hooks/useUndoRedoKeyboard";
Expand Down Expand Up @@ -87,6 +88,7 @@ const PipelineEditor = withSuspenseWrapper(
useShortcutListener();
useEditorEscapeShortcut();
useDebugPanelWindow();
useSeedInitialDockLayoutFromPreset();

const activeSpec = navigation.activeSpec;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect } from "react";

import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";
import { DEFAULT_VIEW_PRESET } from "@/routes/v2/shared/windows/viewPresets";
import { hasPersistedLayout } from "@/routes/v2/shared/windows/windowPersistence";

/**
* On first editor visit (no window layout in localStorage), reorder dock stacks
* to match `DEFAULT_VIEW_PRESET`.
*
* Must run in an effect declared after the editor `use*Window` hooks in
* `PipelineEditor` so those hooks’ effects have already opened windows in the
* store. Only runs when `hasPersistedLayout()` is false.
*/
export function useSeedInitialDockLayoutFromPreset(): void {
const { windows } = useSharedStores();

useEffect(() => {
if (!hasPersistedLayout()) {
windows.seedInitialDockLayoutFromPreset(DEFAULT_VIEW_PRESET);
}
}, [windows]);
}
47 changes: 34 additions & 13 deletions src/routes/v2/shared/windows/viewPresets.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,43 @@
/** Ordered window ids per dock column; array order is the default stack order (first visit only). */
interface PresetDockAreas {
left: string[];
right: string[];
}

export interface ViewPreset {
label: string;
description: string;
visible: Set<string>;
/** Default dock positions to restore when applying this preset. */
dockPositions?: Record<string, "left" | "right">;
/** Default dock columns: ids per side, order matters for first-visit layout seeding. */
dockAreas?: PresetDockAreas;
}

const DEFAULT_DOCK_POSITIONS: Record<string, "left" | "right"> = {
"runs-and-submission": "left",
"component-library": "left",
"pipeline-tree": "left",
history: "left",
"debug-panel": "left",
"pipeline-details": "right",
"recent-runs": "left",
"context-panel": "right",
export const DEFAULT_DOCK_AREAS: PresetDockAreas = {
left: [
"runs-and-submission",
"component-library",
"pipeline-tree",
"history",
"debug-panel",
"recent-runs",
],
right: ["pipeline-details", "context-panel"],
};

/** Target dock side for each window id listed in a preset's `dockAreas`. Right wins if listed on both. */
export function dockSideByWindowId(
areas: PresetDockAreas,
): Map<string, "left" | "right"> {
const map = new Map<string, "left" | "right">();
for (const id of areas.left) {
map.set(id, "left");
}
for (const id of areas.right) {
map.set(id, "right");
}
return map;
}

export const DEFAULT_VIEW_PRESET: ViewPreset = {
label: "Default",
description: "Components, Runs & Submissions, Recent Runs, Pipeline Details",
Expand All @@ -26,7 +47,7 @@ export const DEFAULT_VIEW_PRESET: ViewPreset = {
"component-library",
"pipeline-details",
]),
dockPositions: DEFAULT_DOCK_POSITIONS,
dockAreas: DEFAULT_DOCK_AREAS,
};

export const VIEW_PRESETS: ViewPreset[] = [
Expand All @@ -44,7 +65,7 @@ export const VIEW_PRESETS: ViewPreset[] = [
"debug-panel",
"recent-runs",
]),
dockPositions: DEFAULT_DOCK_POSITIONS,
dockAreas: DEFAULT_DOCK_AREAS,
},
{
label: "Minimal",
Expand Down
66 changes: 62 additions & 4 deletions src/routes/v2/shared/windows/windowStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
type WindowOptions,
type WindowRef,
} from "./types";
import type { ViewPreset } from "./viewPresets";
import { dockSideByWindowId, type ViewPreset } from "./viewPresets";
import { WindowModel, type WindowStoreRef } from "./windowModel";
import { buildWindowModelInit } from "./windowStore.utils";

Expand Down Expand Up @@ -263,7 +263,64 @@ export class WindowStoreImpl implements WindowStoreRef {

// -- View presets --

/** Apply a view preset: toggle visibility and reset dock positions. */
/**
* First-visit only: align dock sides and stack order with `preset.dockAreas`.
* Call only when there is no persisted layout (e.g. gated on `hasPersistedLayout()`).
*/
@action seedInitialDockLayoutFromPreset(preset: ViewPreset): void {
const areas = preset.dockAreas;
if (!areas) return;

const presetIdSet = new Set([...areas.left, ...areas.right]);
this.alignDockSidesWithPresetAreas(areas);
this.alignDockStackOrderWithPresetAreas(areas, presetIdSet);
}

private alignDockSidesWithPresetAreas(
areas: NonNullable<ViewPreset["dockAreas"]>,
): void {
for (const id of areas.left) {
this.moveWindowToDockSideIfNeeded(id, "left");
}
for (const id of areas.right) {
this.moveWindowToDockSideIfNeeded(id, "right");
}
}

private moveWindowToDockSideIfNeeded(
id: string,
side: "left" | "right",
): void {
const win = this.windows[id];
if (!win) return;
if (win.dockState === side) return;
this.undockWindow(id);
this.dockWindow(id, side);
}

private alignDockStackOrderWithPresetAreas(
areas: NonNullable<ViewPreset["dockAreas"]>,
presetIdSet: Set<string>,
): void {
this.alignDockSideStackOrder("left", areas.left, presetIdSet);
this.alignDockSideStackOrder("right", areas.right, presetIdSet);
}

private alignDockSideStackOrder(
side: "left" | "right",
presetOrderOnSide: string[],
presetIdSet: Set<string>,
): void {
const desired = presetOrderOnSide.filter(
(id) => this.windows[id]?.dockState === side,
);
const remaining = this.dockAreas[side].windowOrder.filter(
(id) => !presetIdSet.has(id),
);
this.dockAreas[side].windowOrder = [...desired, ...remaining];
}

/** Apply a view preset: toggle visibility and correct dock sides (does not reshuffle stack order). */
@action applyViewPreset(preset: ViewPreset): void {
const allWindows = this.getAllWindows();
for (const win of allWindows) {
Expand All @@ -273,8 +330,9 @@ export class WindowStoreImpl implements WindowStoreRef {
if (win.state !== "hidden") win.hide();
}
}
if (preset.dockPositions) {
for (const [id, side] of Object.entries(preset.dockPositions)) {
if (preset.dockAreas) {
const sides = dockSideByWindowId(preset.dockAreas);
for (const [id, side] of sides) {
const win = this.windows[id];
if (win && win.dockState !== side) {
this.undockWindow(id);
Expand Down
69 changes: 69 additions & 0 deletions src/routes/v2/shared/windows/windowStore.viewPreset.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { runInAction } from "mobx";
import { createElement } from "react";
import { describe, expect, it } from "vitest";

import { DEFAULT_DOCK_AREAS, DEFAULT_VIEW_PRESET } from "./viewPresets";
import { WindowStoreImpl } from "./windowStore";

const stubContent = createElement("span");

describe("WindowStoreImpl view preset dock layout", () => {
it("seedInitialDockLayoutFromPreset applies DEFAULT_DOCK_AREAS stack order on the left", () => {
const store = new WindowStoreImpl();
store.enableDockSide("left");
store.enableDockSide("right");

store.openWindow(stubContent, {
id: "component-library",
title: "Components",
defaultDockState: "left",
});
store.openWindow(stubContent, {
id: "runs-and-submission",
title: "Runs",
defaultDockState: "left",
});

expect(store.getDockAreaWindowIds("left")).toEqual([
"component-library",
"runs-and-submission",
]);

store.seedInitialDockLayoutFromPreset(DEFAULT_VIEW_PRESET);

expect(store.getDockAreaWindowIds("left")).toEqual(
DEFAULT_DOCK_AREAS.left.filter((id) => store.getWindowById(id)),
);
});

it("applyViewPreset does not replace dock windowOrder when sides already match preset", () => {
const store = new WindowStoreImpl();
store.enableDockSide("left");
store.enableDockSide("right");

store.openWindow(stubContent, {
id: "component-library",
title: "Components",
defaultDockState: "left",
});
store.openWindow(stubContent, {
id: "runs-and-submission",
title: "Runs",
defaultDockState: "left",
});

runInAction(() => {
store.dockAreas.left.windowOrder = [
"component-library",
"runs-and-submission",
];
});

store.applyViewPreset(DEFAULT_VIEW_PRESET);

expect(store.getDockAreaWindowIds("left")).toEqual([
"component-library",
"runs-and-submission",
]);
});
});
Loading