diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FileViewLayout/FileViewLayout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FileViewLayout/FileViewLayout.tsx index 43d63294ea4..fbf006b77d4 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FileViewLayout/FileViewLayout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FileViewLayout/FileViewLayout.tsx @@ -1,4 +1,3 @@ -import { Trans } from "@lingui/react/macro"; import type { RendererContext } from "@superset/panes"; import { ResizableHandle, @@ -11,9 +10,9 @@ import type { ImperativePanelHandle } from "react-resizable-panels"; import { useFileTreeVisibilityStore } from "../../../../state/fileTreeVisibilityStore"; import type { FilePaneData, PaneViewerData } from "../../../../types"; import { openFileFromTree } from "../../../../utils/openFileFromTree"; +import { FileViewTree } from "../FileViewTree"; import { FileBreadcrumb } from "./components/FileBreadcrumb"; import { FileViewFloatingActions } from "./components/FileViewFloatingActions"; -import { FileViewTree } from "./components/FileViewTree"; const TREE_DEFAULT_SIZE = 35; const TREE_TOGGLE_MS = 200; @@ -23,7 +22,7 @@ interface FileViewLayoutProps { workspaceId: string; showTree: boolean; onSearch?: () => void; - children?: ReactNode; + children: ReactNode; } export function FileViewLayout({ @@ -37,9 +36,8 @@ export function FileViewLayout({ context.pane.kind === "file" ? (context.pane.data as FilePaneData).filePath : undefined; - const storedVisible = useFileTreeVisibilityStore((state) => state.visible); + const visible = useFileTreeVisibilityStore((state) => state.visible); const setVisible = useFileTreeVisibilityStore((state) => state.setVisible); - const visible = filePath ? storedVisible : true; const groupRef = useRef>(null); const treePanelRef = useRef(null); const [animating, setAnimating] = useState(false); @@ -58,9 +56,9 @@ export function FileViewLayout({ return () => clearTimeout(timer); }, [visible]); - const tabHasTreePane = - context.pane.kind !== "files" && - Object.values(context.tab.panes).some((pane) => pane.kind === "files"); + const tabHasTreePane = Object.values(context.tab.panes).some( + (pane) => pane.kind === "files", + ); if (!showTree || tabHasTreePane) return children; const panelTransition = @@ -68,44 +66,21 @@ export function FileViewLayout({ return (
- -
- {children ?? ( -
- Select a file to view it here -
- )} -
-
- - groupRef.current?.setLayout([ - 100 - TREE_DEFAULT_SIZE, - TREE_DEFAULT_SIZE, - ]) - } - /> setVisible(false)} onExpand={() => setVisible(true)} className={cn(panelTransition)} >
-
+
+ + groupRef.current?.setLayout([ + TREE_DEFAULT_SIZE, + 100 - TREE_DEFAULT_SIZE, + ]) + } + /> + +
{children}
+
{filePath && ( ; @@ -14,11 +14,12 @@ export function FilesTreePane({ onSearch, }: FilesTreePaneProps) { return ( - +
+ +
); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx index 0fb5f904674..0a5f19379e4 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx @@ -2,6 +2,7 @@ import { Trans, useLingui } from "@lingui/react/macro"; import { errorMessage } from "@superset/i18n/errors"; import type { ContextMenuActionConfig, + Pane, PaneRegistry, RendererContext, WorkspaceStore, @@ -82,6 +83,7 @@ import { } from "../../utils/focusTerminalPane"; import { openPagePaneInStore } from "../../utils/openPagePaneInStore"; import { replaceNewTabPane } from "../../utils/replaceNewTabPane"; +import { returnFileToTree } from "../../utils/returnFileToTree"; import { useAgentSessionLauncher } from "../useAgentSessionLauncher"; import type { OpenReviewDiff } from "../useReviewCommentNavigation"; import type { TerminalLauncher } from "../useV2TerminalLauncher"; @@ -282,6 +284,72 @@ export function usePaneRegistry({ [workspaceId], ); + const confirmFileClose = useCallback( + (pane: Pane): boolean | Promise => { + const data = pane.data as FilePaneData; + const doc = getDocument(workspaceId, data.filePath); + if (!doc?.dirty) return true; + const name = getFileName(data.filePath); + return new Promise((resolve) => { + alert({ + onDismiss: () => resolve(false), + title: t({ + message: `Do you want to save the changes you made to ${name}?`, + }), + description: t({ + message: "Your changes will be lost if you don't save them.", + }), + actions: [ + { + label: t({ + message: "Save", + }), + onClick: async () => { + const doc = getDocument(workspaceId, data.filePath); + if (!doc) { + resolve(true); + return; + } + const result = await doc.save(); + if (result.status !== "saved") { + const target = store.getState().getPane(pane.id); + if (target) + store.getState().setActivePane({ + tabId: target.tabId, + paneId: pane.id, + }); + } + // Only proceed to close if the save succeeded; otherwise + // leave the pane open so the user can see the conflict / + // error state and retry. + resolve(result.status === "saved"); + }, + }, + { + label: t({ + message: "Don't Save", + }), + variant: "secondary", + onClick: async () => { + const doc = getDocument(workspaceId, data.filePath); + if (doc) await doc.reload(); + resolve(true); + }, + }, + { + label: t({ + message: "Cancel", + }), + variant: "ghost", + onClick: () => resolve(false), + }, + ], + }); + }); + }, + [workspaceId, store, t], + ); + return useMemo>( () => ({ file: { @@ -329,67 +397,9 @@ export function usePaneRegistry({ ), onHeaderClick: (ctx: RendererContext) => ctx.actions.pin(), - onBeforeClose: (pane) => { - const data = pane.data as FilePaneData; - const doc = getDocument(workspaceId, data.filePath); - if (!doc?.dirty) return true; - const name = getFileName(data.filePath); - return new Promise((resolve) => { - alert({ - onDismiss: () => resolve(false), - title: t({ - message: `Do you want to save the changes you made to ${name}?`, - }), - description: t({ - message: "Your changes will be lost if you don't save them.", - }), - actions: [ - { - label: t({ - message: "Save", - }), - onClick: async () => { - const doc = getDocument(workspaceId, data.filePath); - if (!doc) { - resolve(true); - return; - } - const result = await doc.save(); - if (result.status !== "saved") { - const target = store.getState().getPane(pane.id); - if (target) - store.getState().setActivePane({ - tabId: target.tabId, - paneId: pane.id, - }); - } - // Only proceed to close if the save succeeded; otherwise - // leave the pane open so the user can see the conflict / - // error state and retry. - resolve(result.status === "saved"); - }, - }, - { - label: t({ - message: "Don't Save", - }), - variant: "secondary", - onClick: async () => { - const doc = getDocument(workspaceId, data.filePath); - if (doc) await doc.reload(); - resolve(true); - }, - }, - { - label: t({ - message: "Cancel", - }), - variant: "ghost", - onClick: () => resolve(false), - }, - ], - }); - }); + onBeforeClose: async (pane) => { + if (!(await confirmFileClose(pane))) return false; + return !(rightStore && returnFileToTree(rightStore, pane.id)); }, contextMenuActions: (_ctx, defaults) => defaults.map((d) => @@ -1155,6 +1165,7 @@ export function usePaneRegistry({ chatPaneIcon, onOpenSubagent, rightStore, + confirmFileClose, ], ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx index 8181c2c25ec..97d1ba66eb9 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/page.tsx @@ -464,6 +464,11 @@ function V2WorkspaceContent() { rightStore, (state) => findVisibleChangesPane(state) != null, ); + const isRightTreeOnly = useStore(rightStore, (state) => { + const tab = state.tabs.find((t) => t.id === state.activeTabId); + const panes = tab ? Object.values(tab.panes) : []; + return panes.length === 1 && panes[0]?.kind === "files"; + }); const openRightChangesTab = useCallback(() => { setRightSidebarOpen(true); openChangesPaneInStore(rightStore, "tab"); @@ -641,7 +646,7 @@ function V2WorkspaceContent() { ({ + id, + kind: "file", + pinned, + data: { filePath: `/${id}.ts`, mode: "editor" as const }, +}); + +type Store = ReturnType>; + +const kinds = (store: Store) => + store + .getState() + .tabs.map((tab) => Object.values(tab.panes).map((pane) => pane.kind)); + +describe("returnFileToTree", () => { + it("turns a file alone in its tab back into the Files tree", () => { + const store = createWorkspaceStore(); + store.getState().addTab({ panes: [file("a")] }); + + expect(returnFileToTree(store, "a")).toBe(true); + expect(kinds(store)).toEqual([["files"]]); + }); + + it("does it for a pinned file too", () => { + const store = createWorkspaceStore(); + store.getState().addTab({ panes: [file("a", true)] }); + + expect(returnFileToTree(store, "a")).toBe(true); + expect(kinds(store)).toEqual([["files"]]); + }); + + it("leaves a file that shares its tab to close normally", () => { + const store = createWorkspaceStore(); + store.getState().addTab({ panes: [file("a"), file("b")] }); + + expect(returnFileToTree(store, "a")).toBe(false); + expect(kinds(store)).toEqual([["file", "file"]]); + }); +}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/returnFileToTree/returnFileToTree.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/returnFileToTree/returnFileToTree.ts new file mode 100644 index 00000000000..e04d971c36c --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/returnFileToTree/returnFileToTree.ts @@ -0,0 +1,22 @@ +import type { WorkspaceStore } from "@superset/panes"; +import type { StoreApi } from "zustand/vanilla"; +import type { PaneViewerData } from "../../types"; + +export function returnFileToTree( + store: StoreApi>, + paneId: string, +): boolean { + const state = store.getState(); + const location = state.getPane(paneId); + if (location?.pane.kind !== "file") return false; + const tab = state.getTab(location.tabId); + if (!tab || Object.keys(tab.panes).length !== 1) return false; + + state.setPanePinned({ paneId, pinned: false }); + state.replacePane({ + tabId: location.tabId, + paneId, + newPane: { kind: "files", data: { kind: "files" } }, + }); + return true; +} diff --git a/packages/i18n/locales/cs/messages.po b/packages/i18n/locales/cs/messages.po index 1881f0b4c74..aa125b5a397 100644 --- a/packages/i18n/locales/cs/messages.po +++ b/packages/i18n/locales/cs/messages.po @@ -16701,9 +16701,6 @@ msgstr "Podívejte se, kde se vašich posledních 30 dní umístí ve veřejném msgid "Select" msgstr "Výběr" -msgid "Select a file to view it here" -msgstr "Vyberte soubor a zobrazí se zde" - msgid "Select a project" msgstr "Vyberte projekt" diff --git a/packages/i18n/locales/de/messages.po b/packages/i18n/locales/de/messages.po index a807e402f0d..78d6de7456f 100644 --- a/packages/i18n/locales/de/messages.po +++ b/packages/i18n/locales/de/messages.po @@ -16701,9 +16701,6 @@ msgstr "Sieh, wo deine letzten 30 Tage in der öffentlichen Bestenliste landen. msgid "Select" msgstr "Select" -msgid "Select a file to view it here" -msgstr "Wähle eine Datei aus, um sie hier anzuzeigen" - msgid "Select a project" msgstr "Wähle ein Projekt" diff --git a/packages/i18n/locales/en/messages.po b/packages/i18n/locales/en/messages.po index 712f7a7602d..cb52e8794b8 100644 --- a/packages/i18n/locales/en/messages.po +++ b/packages/i18n/locales/en/messages.po @@ -16701,9 +16701,6 @@ msgstr "See where your last 30 days land on the public leaderboard. Token counts msgid "Select" msgstr "Select" -msgid "Select a file to view it here" -msgstr "Select a file to view it here" - msgid "Select a project" msgstr "Select a project" diff --git a/packages/i18n/locales/es/messages.po b/packages/i18n/locales/es/messages.po index cf038bcdd92..ec34bc11162 100644 --- a/packages/i18n/locales/es/messages.po +++ b/packages/i18n/locales/es/messages.po @@ -16701,9 +16701,6 @@ msgstr "Descubre en qué puesto de la clasificación pública quedan tus último msgid "Select" msgstr "Selector" -msgid "Select a file to view it here" -msgstr "Selecciona un archivo para verlo aquí" - msgid "Select a project" msgstr "Selecciona un proyecto" diff --git a/packages/i18n/locales/fr/messages.po b/packages/i18n/locales/fr/messages.po index f1526c63765..940ddf14ef4 100644 --- a/packages/i18n/locales/fr/messages.po +++ b/packages/i18n/locales/fr/messages.po @@ -16701,9 +16701,6 @@ msgstr "Découvrez où se situent vos 30 derniers jours dans le classement publi msgid "Select" msgstr "Sélecteur" -msgid "Select a file to view it here" -msgstr "Sélectionnez un fichier pour l’afficher ici" - msgid "Select a project" msgstr "Sélectionnez un projet" diff --git a/packages/i18n/locales/id/messages.po b/packages/i18n/locales/id/messages.po index 43af48d7e13..64200723d6a 100644 --- a/packages/i18n/locales/id/messages.po +++ b/packages/i18n/locales/id/messages.po @@ -16701,9 +16701,6 @@ msgstr "Lihat posisi 30 hari terakhir Anda di papan peringkat publik. Hanya juml msgid "Select" msgstr "Select" -msgid "Select a file to view it here" -msgstr "Pilih file untuk melihatnya di sini" - msgid "Select a project" msgstr "Pilih sebuah proyek" diff --git a/packages/i18n/locales/it/messages.po b/packages/i18n/locales/it/messages.po index 3528a0834c9..7a4dcf0967d 100644 --- a/packages/i18n/locales/it/messages.po +++ b/packages/i18n/locales/it/messages.po @@ -16701,9 +16701,6 @@ msgstr "Scopri dove si collocano i tuoi ultimi 30 giorni nella classifica pubbli msgid "Select" msgstr "Select" -msgid "Select a file to view it here" -msgstr "Seleziona un file per visualizzarlo qui" - msgid "Select a project" msgstr "Seleziona un progetto" diff --git a/packages/i18n/locales/ja/messages.po b/packages/i18n/locales/ja/messages.po index 7342ae62240..254cd6eebbb 100644 --- a/packages/i18n/locales/ja/messages.po +++ b/packages/i18n/locales/ja/messages.po @@ -16701,9 +16701,6 @@ msgstr "直近30日の実績が公開リーダーボードでどの位置にな msgid "Select" msgstr "Select" -msgid "Select a file to view it here" -msgstr "ファイルを選択するとここに表示されます" - msgid "Select a project" msgstr "プロジェクトを選択してください" diff --git a/packages/i18n/locales/ko/messages.po b/packages/i18n/locales/ko/messages.po index ce1d846f9c2..ec36e749e24 100644 --- a/packages/i18n/locales/ko/messages.po +++ b/packages/i18n/locales/ko/messages.po @@ -16701,9 +16701,6 @@ msgstr "최근 30일 사용량이 공개 리더보드에서 어디쯤인지 확 msgid "Select" msgstr "선택" -msgid "Select a file to view it here" -msgstr "파일을 선택하면 여기에 표시됩니다" - msgid "Select a project" msgstr "프로젝트를 선택하세요" diff --git a/packages/i18n/locales/nl/messages.po b/packages/i18n/locales/nl/messages.po index 694042e79fd..5969dd262ef 100644 --- a/packages/i18n/locales/nl/messages.po +++ b/packages/i18n/locales/nl/messages.po @@ -16701,9 +16701,6 @@ msgstr "Bekijk waar je laatste 30 dagen staan op de publieke ranglijst. Alleen t msgid "Select" msgstr "Select" -msgid "Select a file to view it here" -msgstr "Selecteer een bestand om het hier te bekijken" - msgid "Select a project" msgstr "Kies een project" diff --git a/packages/i18n/locales/pl/messages.po b/packages/i18n/locales/pl/messages.po index 576277091c3..d759a360d91 100644 --- a/packages/i18n/locales/pl/messages.po +++ b/packages/i18n/locales/pl/messages.po @@ -16701,9 +16701,6 @@ msgstr "Zobacz, jak twoje ostatnie 30 dni wypada w publicznym rankingu. Tylko li msgid "Select" msgstr "Select" -msgid "Select a file to view it here" -msgstr "Wybierz plik, aby go tutaj wyświetlić" - msgid "Select a project" msgstr "Wybierz projekt" diff --git a/packages/i18n/locales/pt-BR/messages.po b/packages/i18n/locales/pt-BR/messages.po index 23deb99f29b..09716ed0fd2 100644 --- a/packages/i18n/locales/pt-BR/messages.po +++ b/packages/i18n/locales/pt-BR/messages.po @@ -16701,9 +16701,6 @@ msgstr "Veja em que posição seus últimos 30 dias ficam no ranking público. A msgid "Select" msgstr "Select" -msgid "Select a file to view it here" -msgstr "Selecione um arquivo para visualizá-lo aqui" - msgid "Select a project" msgstr "Selecione um projeto" diff --git a/packages/i18n/locales/ru/messages.po b/packages/i18n/locales/ru/messages.po index 4e5b469abb7..cb297d5faaa 100644 --- a/packages/i18n/locales/ru/messages.po +++ b/packages/i18n/locales/ru/messages.po @@ -16701,9 +16701,6 @@ msgstr "Посмотрите, какое место ваши последние msgid "Select" msgstr "Селект" -msgid "Select a file to view it here" -msgstr "Выберите файл, чтобы просмотреть его здесь" - msgid "Select a project" msgstr "Выберите проект" diff --git a/packages/i18n/locales/tr/messages.po b/packages/i18n/locales/tr/messages.po index ae3331cf194..c99b37bbd6e 100644 --- a/packages/i18n/locales/tr/messages.po +++ b/packages/i18n/locales/tr/messages.po @@ -16701,9 +16701,6 @@ msgstr "Son 30 gününüzün herkese açık liderlik tablosunda nereye düştü msgid "Select" msgstr "Seçim" -msgid "Select a file to view it here" -msgstr "Burada görüntülemek için bir dosya seçin" - msgid "Select a project" msgstr "Bir proje seçin" diff --git a/packages/i18n/locales/vi/messages.po b/packages/i18n/locales/vi/messages.po index f397ffedccd..fef5eb25e1c 100644 --- a/packages/i18n/locales/vi/messages.po +++ b/packages/i18n/locales/vi/messages.po @@ -16701,9 +16701,6 @@ msgstr "Xem 30 ngày qua của bạn đứng ở đâu trên bảng xếp hạng msgid "Select" msgstr "Select" -msgid "Select a file to view it here" -msgstr "Chọn một tệp để xem tại đây" - msgid "Select a project" msgstr "Chọn một dự án" diff --git a/packages/i18n/locales/zh-CN/messages.po b/packages/i18n/locales/zh-CN/messages.po index 051477dfeb0..42b59da0d9c 100644 --- a/packages/i18n/locales/zh-CN/messages.po +++ b/packages/i18n/locales/zh-CN/messages.po @@ -16701,9 +16701,6 @@ msgstr "看看你最近30天在公开排行榜上的位置。仅包含token数 msgid "Select" msgstr "选择框" -msgid "Select a file to view it here" -msgstr "选择一个文件以在此处查看" - msgid "Select a project" msgstr "请选择项目" diff --git a/packages/i18n/locales/zh-TW/messages.po b/packages/i18n/locales/zh-TW/messages.po index 74017ecd659..7af174be2da 100644 --- a/packages/i18n/locales/zh-TW/messages.po +++ b/packages/i18n/locales/zh-TW/messages.po @@ -16701,9 +16701,6 @@ msgstr "看看你最近 30 天的表現在公開排行榜上的位置。僅包 msgid "Select" msgstr "選擇框" -msgid "Select a file to view it here" -msgstr "選取檔案以在此處檢視" - msgid "Select a project" msgstr "請選擇專案"