diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.test.ts index 9598bddd90b..d46fa7436a3 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.test.ts @@ -10,6 +10,7 @@ describe("getRightPaneOptions", () => { "terminal", "browser", "diff", + "changes-list", "files", "pages-list", "desktop", @@ -34,11 +35,15 @@ describe("createRightPane", () => { "terminal", "browser", "diff", + "changes-list", "files", "pages-list", "desktop", "mobile", ]); + expect(panes.find((pane) => pane.kind === "changes-list")?.data).toEqual({ + kind: "changes-list", + }); expect(panes.find((pane) => pane.kind === "browser")?.data).toEqual({ url: "https://example.com", }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.ts index af2f65a50eb..726f9d5aa71 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.ts @@ -3,7 +3,7 @@ import { msg } from "@lingui/core/macro"; import type { CreatePaneInput } from "@superset/panes"; import type { IconType } from "react-icons"; import { BsTerminalPlus } from "react-icons/bs"; -import { LuFileDiff, LuFileText, LuFolderTree } from "react-icons/lu"; +import { LuFileDiff, LuFiles, LuFileText, LuFolderTree } from "react-icons/lu"; import { TbDeviceDesktop, TbDeviceMobile, TbWorld } from "react-icons/tb"; import type { HotkeyId } from "renderer/hotkeys"; import type { @@ -46,6 +46,11 @@ const RIGHT_PANE_OPTIONS: RightPaneOption[] = [ Icon: LuFileDiff, hotkeyId: "OPEN_DIFF_VIEWER", }, + { + kind: "changes-list", + label: msg({ message: "Files changed" }), + Icon: LuFiles, + }, { kind: "files", label: msg({ message: "Files" }), Icon: LuFolderTree }, { kind: "pages-list", label: msg({ message: "Pages" }), Icon: LuFileText }, { @@ -77,6 +82,7 @@ export function createRightPane( ): CreatePaneInput { switch (kind) { case "files": + case "changes-list": case "pages-list": case "desktop": case "mobile": diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/types.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/types.ts index 60ba063611c..c8ebcbe4a63 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/types.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/types.ts @@ -1,6 +1,7 @@ export type RightPaneKind = | "files" | "diff" + | "changes-list" | "pages-list" | "browser" | "desktop" diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.test.ts index 9e90322a728..1086731f12e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.test.ts @@ -18,7 +18,7 @@ describe("placeInTabs", () => { store.getState().addTab({ id: "t1", panes: [pane("p1")] }); store.getState().addTab({ id: "t2", panes: [pane("list", "list")] }); - store.getState().openPane({ pane: pane("p2"), beside: "list" }); + store.getState().openPane({ pane: pane("p2") }); expect(paneIdsByTab()).toEqual([["p2"], ["list"]]); expect(store.getState().activeTabId).toBe("t1"); @@ -27,6 +27,28 @@ describe("placeInTabs", () => { expect(paneIdsByTab()).toEqual([["p2"], ["list"], ["p3"]]); }); + it("stacks a beside pane above its anchor, then reuses it", () => { + const store = createWorkspaceStore({ placePane: placeInTabs }); + store.getState().addTab({ id: "t1", panes: [pane("p1")] }); + store.getState().addTab({ id: "t2", panes: [pane("list", "list")] }); + + store.getState().openPane({ pane: pane("d1", "diff"), beside: "list" }); + store.getState().openPane({ pane: pane("d2", "diff"), beside: "list" }); + + const tab = store.getState().getTab("t2"); + expect(store.getState().tabs).toHaveLength(2); + expect(tab?.layout).toMatchObject({ + type: "split", + direction: "vertical", + second: { type: "pane", paneId: "list" }, + }); + expect(Object.values(tab?.panes ?? {}).map((p) => p.data.label)).toEqual([ + "list", + "d2", + ]); + expect(store.getState().activeTabId).toBe("t2"); + }); + it("leaves focus alone when openPane is told not to focus", () => { const store = createWorkspaceStore({ placePane: placeInTabs }); store diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.ts index 873e4387062..5c45b628a2b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.ts @@ -2,8 +2,33 @@ import type { OpenPaneArgs, WorkspaceStore } from "@superset/panes"; export function placeInTabs( store: WorkspaceStore, - { pane, focus, reuse }: OpenPaneArgs, + { pane, beside, focus, reuse }: OpenPaneArgs, ): void { + const anchor = beside ? store.getPane(beside) : null; + const anchorTab = anchor ? store.getTab(anchor.tabId) : null; + if (anchor && anchorTab) { + const reusable = Object.values(anchorTab.panes).find( + (candidate) => + candidate.kind === pane.kind && + !candidate.pinned && + candidate.id !== anchor.pane.id, + ); + if (reusable) { + store.setPaneData({ paneId: reusable.id, data: pane.data }); + store.setActivePane({ tabId: anchorTab.id, paneId: reusable.id }); + return; + } + // Panes are at least 260px wide, so a side-by-side split pushes a narrow anchor off screen. + store.splitPane({ + tabId: anchorTab.id, + paneId: anchor.pane.id, + position: "top", + newPane: pane, + focus: false, + }); + return; + } + const tabs = [ ...store.tabs.filter((tab) => tab.id === store.activeTabId), ...store.tabs.filter((tab) => tab.id !== store.activeTabId),