diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts
index 833ee1f70a4..f2804a3ac85 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts
@@ -61,12 +61,18 @@ export function useDashboardSidebarProjectSectionActions({
};
const confirmRemoveFromSidebar = () => {
- alert.destructive({
+ alert({
title: "Remove project from sidebar?",
description:
"This will remove workspaces from the sidebar and delete all project sections. The workspaces or projects won't be deleted.",
- confirmText: "Remove",
- onConfirm: () => removeProjectFromSidebar(project.id),
+ actions: [
+ { label: "Cancel", variant: "outline", onClick: () => {} },
+ {
+ label: "Remove",
+ variant: "destructive",
+ onClick: () => removeProjectFromSidebar(project.id),
+ },
+ ],
});
};
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/OpenInMenuButton/OpenInMenuButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/OpenInMenuButton/OpenInMenuButton.tsx
index 30e12cf88d9..f47735297b2 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/OpenInMenuButton/OpenInMenuButton.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/OpenInMenuButton/OpenInMenuButton.tsx
@@ -17,7 +17,7 @@ import {
} from "renderer/components/OpenInExternalDropdown";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { useThemeStore } from "renderer/stores";
-import { useHotkeyText } from "renderer/stores/hotkeys";
+import { useAppHotkey, useHotkeyText } from "renderer/stores/hotkeys";
interface OpenInMenuButtonProps {
worktreePath: string;
@@ -80,6 +80,10 @@ export const OpenInMenuButton = memo(function OpenInMenuButton({
copyPath.mutate(worktreePath);
}, [worktreePath, copyPath, openInApp.isPending]);
+ useAppHotkey("OPEN_IN_APP", handleOpenInEditor, undefined, [
+ handleOpenInEditor,
+ ]);
+
return (
{/* Main button - opens in last used app */}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/FilesPane.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/RightSidebar.tsx
similarity index 55%
rename from apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/FilesPane.tsx
rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/RightSidebar.tsx
index e507048180a..b51e8c0a55a 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/FilesPane.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/RightSidebar.tsx
@@ -4,28 +4,27 @@ import {
useWorkspaceFsEvents,
workspaceTrpc,
} from "@superset/workspace-client";
-import { useCallback, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
ROW_HEIGHT,
TREE_INDENT,
} from "renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/constants";
-import { WorkspaceFilePreview } from "./components/WorkspaceFilePreview";
import { WorkspaceFilesSearchResultItem } from "./components/WorkspaceFilesSearchResultItem";
import { WorkspaceFilesToolbar } from "./components/WorkspaceFilesToolbar";
import { WorkspaceFilesTreeItem } from "./components/WorkspaceFilesTreeItem";
import { useWorkspaceFileSearch } from "./hooks/useWorkspaceFileSearch";
-interface WorkspaceFilesProps {
+interface RightSidebarProps {
onSelectFile: (absolutePath: string) => void;
selectedFilePath?: string;
workspaceId: string;
}
-export function FilesPane({
+export function RightSidebar({
onSelectFile,
selectedFilePath,
workspaceId,
-}: WorkspaceFilesProps) {
+}: RightSidebarProps) {
const [isRefreshing, setIsRefreshing] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const utils = workspaceTrpc.useUtils();
@@ -58,12 +57,32 @@ export function FilesPane({
if (searchTerm.trim().length === 0) {
return;
}
-
void utils.filesystem.searchFiles.invalidate();
},
Boolean(workspaceId && searchTerm.trim().length > 0),
);
+ const scrollContainerRef = useRef
(null);
+ const prevSelectedRef = useRef(selectedFilePath);
+
+ useEffect(() => {
+ if (
+ selectedFilePath &&
+ selectedFilePath !== prevSelectedRef.current &&
+ rootPath
+ ) {
+ void fileTree.reveal(selectedFilePath).then(() => {
+ requestAnimationFrame(() => {
+ const el = scrollContainerRef.current?.querySelector(
+ `[data-filepath="${CSS.escape(selectedFilePath)}"]`,
+ );
+ el?.scrollIntoView({ block: "center" });
+ });
+ });
+ }
+ prevSelectedRef.current = selectedFilePath;
+ }, [selectedFilePath, rootPath, fileTree]);
+
const flattenedTreeEntries = useMemo(() => {
const entries: Array<{
depth: number;
@@ -112,68 +131,63 @@ export function FilesPane({
}
return (
-
-
-
{}}
- onNewFolder={() => {}}
- onRefresh={() => void handleRefresh()}
- onSearchChange={setSearchTerm}
- searchTerm={searchTerm}
- />
-
- {hasQuery ? (
- searchResults.length === 0 ? (
-
- {isFetchingSearch ? "Searching files..." : "No matches found"}
-
- ) : (
-
- {searchResults.map((entry) => (
-
- ))}
-
- )
- ) : fileTree.isLoadingRoot && fileTree.rootEntries.length === 0 ? (
+
+
{}}
+ onNewFolder={() => {}}
+ onRefresh={() => void handleRefresh()}
+ onSearchChange={setSearchTerm}
+ searchTerm={searchTerm}
+ />
+
+ {hasQuery ? (
+ searchResults.length === 0 ? (
- Loading files...
-
- ) : fileTree.rootEntries.length === 0 ? (
-
- No files found
+ {isFetchingSearch ? "Searching files..." : "No matches found"}
) : (
- {flattenedTreeEntries.map(({ depth, node }) => (
-
- void fileTree.toggle(absolutePath)
- }
- rowHeight={ROW_HEIGHT}
+ {searchResults.map((entry) => (
+
))}
- )}
-
-
-
-
+ )
+ ) : fileTree.isLoadingRoot && fileTree.rootEntries.length === 0 ? (
+
+ Loading files...
+
+ ) : fileTree.rootEntries.length === 0 ? (
+
+ No files found
+
+ ) : (
+
+ {flattenedTreeEntries.map(({ depth, node }) => (
+
+ void fileTree.toggle(absolutePath)
+ }
+ rowHeight={ROW_HEIGHT}
+ selectedFilePath={selectedFilePath}
+ />
+ ))}
+
+ )}
);
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilesSearchResultItem/WorkspaceFilesSearchResultItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesSearchResultItem/WorkspaceFilesSearchResultItem.tsx
similarity index 100%
rename from apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilesSearchResultItem/WorkspaceFilesSearchResultItem.tsx
rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesSearchResultItem/WorkspaceFilesSearchResultItem.tsx
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilesSearchResultItem/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesSearchResultItem/index.ts
similarity index 100%
rename from apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilesSearchResultItem/index.ts
rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesSearchResultItem/index.ts
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilesToolbar/WorkspaceFilesToolbar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesToolbar/WorkspaceFilesToolbar.tsx
similarity index 100%
rename from apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilesToolbar/WorkspaceFilesToolbar.tsx
rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesToolbar/WorkspaceFilesToolbar.tsx
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilesToolbar/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesToolbar/index.ts
similarity index 100%
rename from apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilesToolbar/index.ts
rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesToolbar/index.ts
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilesTreeItem/WorkspaceFilesTreeItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesTreeItem/WorkspaceFilesTreeItem.tsx
similarity index 97%
rename from apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilesTreeItem/WorkspaceFilesTreeItem.tsx
rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesTreeItem/WorkspaceFilesTreeItem.tsx
index 0e4b85c709e..d6bcdb41adf 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilesTreeItem/WorkspaceFilesTreeItem.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightSidebar/components/WorkspaceFilesTreeItem/WorkspaceFilesTreeItem.tsx
@@ -27,6 +27,7 @@ export function WorkspaceFilesTreeItem({
return (
{
event.stopPropagation();
- alert.destructive({
+ alert({
title: "Delete Chat Session",
description: "Are you sure you want to delete this session?",
- confirmText: "Delete",
- onConfirm: () => {
- toast.promise(onDeleteSession(sessionId), {
- loading: "Deleting session...",
- success: "Session deleted",
- error: "Failed to delete session",
- });
- },
+ actions: [
+ { label: "Cancel", variant: "outline", onClick: () => {} },
+ {
+ label: "Delete",
+ variant: "destructive",
+ onClick: () => {
+ toast.promise(onDeleteSession(sessionId), {
+ loading: "Deleting session...",
+ success: "Session deleted",
+ error: "Failed to delete session",
+ });
+ },
+ },
+ ],
});
}}
>
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/FilePane.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/FilePane.tsx
new file mode 100644
index 00000000000..587a32d0032
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/FilePane.tsx
@@ -0,0 +1,106 @@
+import type { RendererContext } from "@superset/panes";
+import { useFileDocument } from "@superset/workspace-client";
+import { useCallback } from "react";
+import { isImageFile, isMarkdownFile } from "shared/file-types";
+import type { FilePaneData, PaneViewerData } from "../../../../types";
+import { CodeRenderer } from "./renderers/CodeRenderer";
+import { ImageRenderer } from "./renderers/ImageRenderer";
+import { MarkdownRenderer } from "./renderers/MarkdownRenderer";
+
+interface FilePaneProps {
+ context: RendererContext;
+ workspaceId: string;
+}
+
+export function FilePane({ context, workspaceId }: FilePaneProps) {
+ const data = context.pane.data as FilePaneData;
+ const { filePath } = data;
+
+ const document = useFileDocument({
+ workspaceId,
+ absolutePath: filePath,
+ mode: isImageFile(filePath) ? "bytes" : "auto",
+ maxBytes: isImageFile(filePath) ? 10 * 1024 * 1024 : 2 * 1024 * 1024,
+ hasLocalChanges: data.hasChanges,
+ autoReloadWhenClean: true,
+ });
+
+ const handleDirtyChange = useCallback(
+ (dirty: boolean) => {
+ if (dirty !== data.hasChanges) {
+ context.actions.updateData({
+ ...data,
+ hasChanges: dirty,
+ } as PaneViewerData);
+ }
+ },
+ [context.actions, data],
+ );
+
+ const handleSave = useCallback(
+ async (content: string) => {
+ const result = await document.save({ content });
+ if (result.status === "saved") {
+ handleDirtyChange(false);
+ }
+ return result;
+ },
+ [document, handleDirtyChange],
+ );
+
+ if (document.state.kind === "loading") {
+ return null;
+ }
+
+ if (document.state.kind === "not-found") {
+ return (
+
+ File not found
+
+ );
+ }
+
+ if (document.state.kind === "too-large") {
+ return (
+
+ File is too large to display
+
+ );
+ }
+
+ if (document.state.kind === "binary" || document.state.kind === "bytes") {
+ if (isImageFile(filePath) && document.state.kind === "bytes") {
+ return (
+
+ );
+ }
+ return (
+
+ Binary file — cannot display
+
+ );
+ }
+
+ if (isMarkdownFile(filePath)) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/components/ExternalChangeBar/ExternalChangeBar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/components/ExternalChangeBar/ExternalChangeBar.tsx
new file mode 100644
index 00000000000..a634c68478c
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/components/ExternalChangeBar/ExternalChangeBar.tsx
@@ -0,0 +1,18 @@
+interface ExternalChangeBarProps {
+ onReload: () => Promise;
+}
+
+export function ExternalChangeBar({ onReload }: ExternalChangeBarProps) {
+ return (
+
+ File changed on disk.
+ void onReload()}
+ >
+ Reload
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/components/ExternalChangeBar/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/components/ExternalChangeBar/index.ts
new file mode 100644
index 00000000000..fac487bd149
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/components/ExternalChangeBar/index.ts
@@ -0,0 +1 @@
+export { ExternalChangeBar } from "./ExternalChangeBar";
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/index.ts
new file mode 100644
index 00000000000..bf2e051559b
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/index.ts
@@ -0,0 +1 @@
+export { FilePane } from "./FilePane";
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/CodeRenderer/CodeRenderer.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/CodeRenderer/CodeRenderer.tsx
new file mode 100644
index 00000000000..0f896fdf4e0
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/CodeRenderer/CodeRenderer.tsx
@@ -0,0 +1,59 @@
+import { useCallback, useRef, useState } from "react";
+import { CodeEditor } from "renderer/screens/main/components/WorkspaceView/components/CodeEditor";
+import { detectLanguage } from "shared/detect-language";
+import { ExternalChangeBar } from "../../components/ExternalChangeBar";
+
+interface CodeRendererProps {
+ content: string;
+ filePath: string;
+ hasExternalChange: boolean;
+ onDirtyChange: (dirty: boolean) => void;
+ onReload: () => Promise;
+ onSave: (content: string) => Promise;
+}
+
+export function CodeRenderer({
+ content,
+ filePath,
+ hasExternalChange,
+ onDirtyChange,
+ onReload,
+ onSave,
+}: CodeRendererProps) {
+ const language = detectLanguage(filePath);
+ const currentContentRef = useRef(content);
+ const [savedContent, setSavedContent] = useState(content);
+
+ // Track the initial/saved content to detect dirty state
+ if (content !== savedContent && !onDirtyChange) {
+ setSavedContent(content);
+ }
+
+ const handleChange = useCallback(
+ (value: string) => {
+ currentContentRef.current = value;
+ onDirtyChange(value !== savedContent);
+ },
+ [onDirtyChange, savedContent],
+ );
+
+ const handleSave = useCallback(async () => {
+ await onSave(currentContentRef.current);
+ setSavedContent(currentContentRef.current);
+ }, [onSave]);
+
+ return (
+
+ {hasExternalChange &&
}
+
+
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/CodeRenderer/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/CodeRenderer/index.ts
new file mode 100644
index 00000000000..f3eecc8cfa8
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/CodeRenderer/index.ts
@@ -0,0 +1 @@
+export { CodeRenderer } from "./CodeRenderer";
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/ImageRenderer/ImageRenderer.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/ImageRenderer/ImageRenderer.tsx
new file mode 100644
index 00000000000..75e77780927
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/ImageRenderer/ImageRenderer.tsx
@@ -0,0 +1,30 @@
+import { useMemo } from "react";
+import { getImageMimeType } from "shared/file-types";
+
+interface ImageRendererProps {
+ content: Uint8Array;
+ filePath: string;
+}
+
+export function ImageRenderer({ content, filePath }: ImageRendererProps) {
+ const dataUrl = useMemo(() => {
+ const mimeType = getImageMimeType(filePath) ?? "image/png";
+ const base64 = btoa(
+ Array.from(content)
+ .map((b) => String.fromCharCode(b))
+ .join(""),
+ );
+ return `data:${mimeType};base64,${base64}`;
+ }, [content, filePath]);
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/ImageRenderer/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/ImageRenderer/index.ts
new file mode 100644
index 00000000000..d61a1b37f40
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/ImageRenderer/index.ts
@@ -0,0 +1 @@
+export { ImageRenderer } from "./ImageRenderer";
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/MarkdownRenderer/MarkdownRenderer.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/MarkdownRenderer/MarkdownRenderer.tsx
new file mode 100644
index 00000000000..77a25aa3279
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/MarkdownRenderer/MarkdownRenderer.tsx
@@ -0,0 +1,97 @@
+import { useCallback, useRef, useState } from "react";
+import { TipTapMarkdownRenderer } from "renderer/components/MarkdownRenderer/components/TipTapMarkdownRenderer";
+import { CodeEditor } from "renderer/screens/main/components/WorkspaceView/components/CodeEditor";
+import { ExternalChangeBar } from "../../components/ExternalChangeBar";
+
+export type MarkdownViewMode = "rendered" | "raw";
+
+interface MarkdownRendererProps {
+ content: string;
+ hasExternalChange: boolean;
+ onDirtyChange: (dirty: boolean) => void;
+ onReload: () => Promise;
+ onSave: (content: string) => Promise;
+}
+
+export function MarkdownRenderer({
+ content,
+ hasExternalChange,
+ onDirtyChange,
+ onReload,
+ onSave,
+}: MarkdownRendererProps) {
+ const [viewMode, _setViewMode] = useState("rendered");
+ const currentContentRef = useRef(content);
+ const [savedContent, setSavedContent] = useState(content);
+
+ const handleChange = useCallback(
+ (value: string) => {
+ currentContentRef.current = value;
+ onDirtyChange(value !== savedContent);
+ },
+ [onDirtyChange, savedContent],
+ );
+
+ const handleSave = useCallback(async () => {
+ await onSave(currentContentRef.current);
+ setSavedContent(currentContentRef.current);
+ }, [onSave]);
+
+ return (
+
+ {hasExternalChange &&
}
+
+ {viewMode === "rendered" ? (
+
+
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+// Exported for use in renderHeaderExtras
+export type { MarkdownViewMode as ViewMode };
+
+interface ViewModeToggleProps {
+ viewMode: MarkdownViewMode;
+ onViewModeChange: (mode: MarkdownViewMode) => void;
+}
+
+export function MarkdownViewModeToggle({
+ viewMode,
+ onViewModeChange,
+}: ViewModeToggleProps) {
+ return (
+
+ onViewModeChange("rendered")}
+ >
+ Rendered
+
+ onViewModeChange("raw")}
+ >
+ Raw
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/MarkdownRenderer/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/MarkdownRenderer/index.ts
new file mode 100644
index 00000000000..eb4c7b8f970
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilePane/renderers/MarkdownRenderer/index.ts
@@ -0,0 +1,5 @@
+export {
+ MarkdownRenderer,
+ type MarkdownViewMode,
+ MarkdownViewModeToggle,
+} from "./MarkdownRenderer";
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/WorkspaceFilePreview.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/WorkspaceFilePreview.tsx
deleted file mode 100644
index b34d7787ce8..00000000000
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/WorkspaceFilePreview.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { WorkspaceFilePreviewContent } from "./components/WorkspaceFilePreviewContent";
-
-interface WorkspaceFilePreviewProps {
- selectedFilePath?: string;
- workspaceId: string;
-}
-
-export function WorkspaceFilePreview({
- selectedFilePath,
- workspaceId,
-}: WorkspaceFilePreviewProps) {
- if (!selectedFilePath) {
- return (
-
- Select a file to preview it
-
- );
- }
-
- return (
-
- );
-}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/components/WorkspaceFilePreviewContent/WorkspaceFilePreviewContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/components/WorkspaceFilePreviewContent/WorkspaceFilePreviewContent.tsx
deleted file mode 100644
index e293635b91a..00000000000
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/components/WorkspaceFilePreviewContent/WorkspaceFilePreviewContent.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { useFileDocument } from "@superset/workspace-client";
-
-interface WorkspaceFilePreviewContentProps {
- selectedFilePath: string;
- workspaceId: string;
-}
-
-export function WorkspaceFilePreviewContent({
- selectedFilePath,
- workspaceId,
-}: WorkspaceFilePreviewContentProps) {
- const document = useFileDocument({
- workspaceId,
- absolutePath: selectedFilePath,
- mode: "auto",
- });
-
- if (document.state.kind === "loading") {
- return (
-
- Loading file...
-
- );
- }
-
- if (document.state.kind === "not-found") {
- return (
-
- File not found
-
- );
- }
-
- if (document.state.kind === "binary") {
- return (
-
- Binary files are not previewed yet
-
- );
- }
-
- if (document.state.kind === "too-large") {
- return (
-
- File is too large to preview
-
- );
- }
-
- if (document.state.kind === "bytes") {
- return (
-
- Byte previews are not implemented yet
-
- );
- }
-
- return (
-
-
-
-
-
- {document.absolutePath}
-
-
- Revision {document.state.revision}
-
-
-
void document.reload()}
- type="button"
- >
- Reload
-
-
- {document.hasExternalChange ? (
-
- File changed on disk. Reload to sync with the workspace.
-
- ) : null}
-
-
- {document.state.content}
-
-
- );
-}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/components/WorkspaceFilePreviewContent/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/components/WorkspaceFilePreviewContent/index.ts
deleted file mode 100644
index 5a8ef005640..00000000000
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/components/WorkspaceFilePreviewContent/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { WorkspaceFilePreviewContent } from "./WorkspaceFilePreviewContent";
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/index.ts
deleted file mode 100644
index 41335072140..00000000000
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/components/WorkspaceFilePreview/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { WorkspaceFilePreview } from "./WorkspaceFilePreview";
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/index.ts
deleted file mode 100644
index 12a28cf46a2..00000000000
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/FilesPane/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { FilesPane } from "./FilesPane";
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 2d9e5373828..6601402bf6f 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
@@ -1,6 +1,8 @@
import type { PaneRegistry, RendererContext } from "@superset/panes";
-import { FileCode2, Globe, MessageSquare, TerminalSquare } from "lucide-react";
+import { alert } from "@superset/ui/atoms/Alert";
+import { Circle, Globe, MessageSquare, TerminalSquare } from "lucide-react";
import { useMemo } from "react";
+import { FileIcon } from "renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/utils";
import type {
BrowserPaneData,
ChatPaneData,
@@ -9,10 +11,10 @@ import type {
PaneViewerData,
} from "../../types";
import { ChatPane } from "./components/ChatPane";
-import { WorkspaceFilePreview } from "./components/FilesPane/components/WorkspaceFilePreview/WorkspaceFilePreview";
+import { FilePane } from "./components/FilePane";
import { TerminalPane } from "./components/TerminalPane";
-function getFileTitle(filePath: string): string {
+function getFileName(filePath: string): string {
return filePath.split("/").pop() ?? filePath;
}
@@ -22,20 +24,60 @@ export function usePaneRegistry(
return useMemo>(
() => ({
file: {
- getIcon: () => ,
- getTitle: (ctx: RendererContext) => {
+ getIcon: (ctx: RendererContext) => {
const data = ctx.pane.data as FilePaneData;
- return getFileTitle(data.filePath);
+ const name = getFileName(data.filePath);
+ return ;
},
- renderPane: (ctx: RendererContext) => {
+ getTitle: (ctx: RendererContext) => {
const data = ctx.pane.data as FilePaneData;
+ const name = getFileName(data.filePath);
return (
-
+
+
+ {name}
+
+ {data.hasChanges && (
+
+ )}
+
);
},
+ renderPane: (ctx: RendererContext) => (
+
+ ),
+ onHeaderClick: (ctx: RendererContext) =>
+ ctx.actions.pin(),
+ onBeforeClose: (pane) => {
+ const data = pane.data as FilePaneData;
+ if (!data.hasChanges) return true;
+ const name = data.filePath.split("/").pop();
+ return new Promise((resolve) => {
+ alert({
+ title: `Do you want to save the changes you made to ${name}?`,
+ description: "Your changes will be lost if you don't save them.",
+ actions: [
+ {
+ label: "Save",
+ onClick: () => {
+ // TODO: wire up save via editor ref
+ resolve(true);
+ },
+ },
+ {
+ label: "Don't Save",
+ variant: "secondary",
+ onClick: () => resolve(true),
+ },
+ {
+ label: "Cancel",
+ variant: "ghost",
+ onClick: () => resolve(false),
+ },
+ ],
+ });
+ });
+ },
},
terminal: {
getIcon: () => ,
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 fedbe7a2b74..901487c56d0 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
@@ -1,4 +1,10 @@
import { type PaneActionConfig, Workspace } from "@superset/panes";
+import { alert } from "@superset/ui/atoms/Alert";
+import {
+ ResizableHandle,
+ ResizablePanel,
+ ResizablePanelGroup,
+} from "@superset/ui/resizable";
import { eq } from "@tanstack/db";
import { useLiveQuery } from "@tanstack/react-db";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
@@ -14,7 +20,9 @@ import {
} from "renderer/screens/main/components/CommandPalette";
import { PresetsBar } from "renderer/screens/main/components/WorkspaceView/ContentView/components/PresetsBar";
import { useAppHotkey } from "renderer/stores/hotkeys";
+import { useStore } from "zustand";
import { AddTabMenu } from "./components/AddTabMenu";
+import { RightSidebar } from "./components/RightSidebar";
import { WorkspaceEmptyState } from "./components/WorkspaceEmptyState";
import { WorkspaceNotFoundState } from "./components/WorkspaceNotFoundState";
import { usePaneRegistry } from "./hooks/usePaneRegistry";
@@ -73,7 +81,10 @@ function WorkspaceContent({
workspaceName: string;
}) {
const navigate = useNavigate();
- const { store } = useV2WorkspacePaneLayout({ projectId, workspaceId });
+ const { localWorkspaceState, store } = useV2WorkspacePaneLayout({
+ projectId,
+ workspaceId,
+ });
const paneRegistry = usePaneRegistry(workspaceId);
const utils = electronTrpc.useUtils();
@@ -98,9 +109,26 @@ function WorkspaceContent({
},
);
+ const selectedFilePath = useStore(store, (s) => {
+ const tab = s.tabs.find((t) => t.id === s.activeTabId);
+ if (!tab?.activePaneId) return undefined;
+ const pane = tab.panes[tab.activePaneId];
+ if (pane?.kind === "file") return (pane.data as FilePaneData).filePath;
+ return undefined;
+ });
+
const openFilePane = useCallback(
(filePath: string) => {
- store.getState().openPane({
+ const state = store.getState();
+ const active = state.getActivePane();
+ if (
+ active?.pane.kind === "file" &&
+ (active.pane.data as FilePaneData).filePath === filePath
+ ) {
+ state.setPanePinned({ paneId: active.pane.id, pinned: true });
+ return;
+ }
+ state.openPane({
pane: {
kind: "file",
data: {
@@ -216,6 +244,16 @@ function WorkspaceContent({
[workspaceId, workspaceName],
);
+ const collections = useCollections();
+ const sidebarOpen = localWorkspaceState?.rightSidebarOpen ?? false;
+ const toggleSidebar = useCallback(() => {
+ if (!collections.v2WorkspaceLocalState.get(workspaceId)) return;
+ collections.v2WorkspaceLocalState.update(workspaceId, (draft) => {
+ draft.rightSidebarOpen = !draft.rightSidebarOpen;
+ });
+ }, [collections, workspaceId]);
+
+ useAppHotkey("TOGGLE_SIDEBAR", toggleSidebar, undefined, [toggleSidebar]);
useAppHotkey("NEW_GROUP", addTerminalTab, undefined, [addTerminalTab]);
useAppHotkey("NEW_CHAT", addChatTab, undefined, [addChatTab]);
useAppHotkey("NEW_BROWSER", addBrowserTab, undefined, [addBrowserTab]);
@@ -223,36 +261,93 @@ function WorkspaceContent({
return (
<>
-
- {!isLoadingPresetsBar && showPresetsBar ?
: null}
-
- registry={paneRegistry}
- paneActions={defaultPaneActions}
- renderAddTabMenu={() => (
-
- setShowPresetsBar.mutate({ enabled })
- }
- />
- )}
- renderEmptyState={() => (
-
+
+
+ {!isLoadingPresetsBar && showPresetsBar ?
: null}
+
+ registry={paneRegistry}
+ paneActions={defaultPaneActions}
+ renderAddTabMenu={() => (
+
+ setShowPresetsBar.mutate({ enabled })
+ }
+ />
+ )}
+ renderEmptyState={() => (
+
+ )}
+ onBeforeCloseTab={(tab) => {
+ const dirtyFiles = Object.values(tab.panes)
+ .filter(
+ (p) =>
+ p.kind === "file" && (p.data as FilePaneData).hasChanges,
+ )
+ .map((p) =>
+ (p.data as FilePaneData).filePath.split("/").pop(),
+ );
+ if (dirtyFiles.length === 0) return true;
+ const title =
+ dirtyFiles.length === 1
+ ? `Do you want to save the changes you made to ${dirtyFiles[0]}?`
+ : `Do you want to save changes to ${dirtyFiles.length} files?`;
+ return new Promise((resolve) => {
+ alert({
+ title,
+ description:
+ "Your changes will be lost if you don't save them.",
+ actions: [
+ {
+ label: "Save All",
+ onClick: () => {
+ // TODO: wire up save via editor refs
+ resolve(true);
+ },
+ },
+ {
+ label: "Don't Save",
+ variant: "secondary",
+ onClick: () => resolve(true),
+ },
+ {
+ label: "Cancel",
+ variant: "ghost",
+ onClick: () => resolve(false),
+ },
+ ],
+ });
+ });
+ }}
+ store={store}
/>
- )}
- store={store}
- />
-
+
+
+ {sidebarOpen && (
+ <>
+
+
+
+
+ >
+ )}
+
{
- alert.destructive({
+ alert({
title: "Revoke API Key",
description: `Are you sure you want to revoke "${name ?? "Unnamed Key"}"? This action cannot be undone.`,
- confirmText: "Revoke",
- onConfirm: async () => {
- await authClient.apiKey.delete({ keyId: id });
- toast.success("API key revoked");
- },
+ actions: [
+ { label: "Cancel", variant: "outline", onClick: () => {} },
+ {
+ label: "Revoke",
+ variant: "destructive",
+ onClick: async () => {
+ await authClient.apiKey.delete({ keyId: id });
+ toast.success("API key revoked");
+ },
+ },
+ ],
});
};
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/components/InviteMemberButton/InviteMemberButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/components/InviteMemberButton/InviteMemberButton.tsx
index a17e45e44d3..bb3f8907410 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/components/InviteMemberButton/InviteMemberButton.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/components/InviteMemberButton/InviteMemberButton.tsx
@@ -36,9 +36,10 @@ export function InviteMemberButton({
title: "This will affect your billing",
description:
"Adding members will increase your subscription cost, prorated to your billing cycle.",
- confirmText: "Continue",
- cancelText: "Cancel",
- onConfirm: () => setOpen(true),
+ actions: [
+ { label: "Cancel", variant: "outline", onClick: () => {} },
+ { label: "Continue", onClick: () => setOpen(true) },
+ ],
});
});
};
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/components/MemberActions/MemberActions.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/components/MemberActions/MemberActions.tsx
index ea8c1bc8770..70408da1054 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/components/MemberActions/MemberActions.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/components/MemberActions/MemberActions.tsx
@@ -90,16 +90,19 @@ export function MemberActions({
? " Your subscription will be adjusted accordingly."
: "";
- alert.destructive({
+ alert({
title: isCurrentUser ? "Leave organization?" : "Remove team member?",
description: isCurrentUser
? `Are you sure you want to leave this organization? You will lose access immediately.${billingNote}`
: `Are you sure you want to remove ${member.name} (${member.email}) from the organization? They will lose access immediately.${billingNote}`,
- confirmText: isCurrentUser ? "Leave Organization" : "Remove Member",
- cancelText: "Cancel",
- onConfirm: () => {
- handleRemove();
- },
+ actions: [
+ { label: "Cancel", variant: "outline", onClick: () => {} },
+ {
+ label: isCurrentUser ? "Leave Organization" : "Remove Member",
+ variant: "destructive",
+ onClick: () => handleRemove(),
+ },
+ ],
});
};
@@ -126,14 +129,17 @@ export function MemberActions({
isCurrentUser && getRoleLevel(newRole) < getRoleLevel(member.role);
if (isSelfDemotion) {
- alert.destructive({
+ alert({
title: "Demote yourself?",
description: `You're about to change your role from ${ORGANIZATION_ROLES[member.role].name} to ${ORGANIZATION_ROLES[newRole].name}. Another owner will need to restore your permissions. Are you sure?`,
- confirmText: "Yes, demote me",
- cancelText: "Cancel",
- onConfirm: async () => {
- await handleChangeRole(newRole);
- },
+ actions: [
+ { label: "Cancel", variant: "outline", onClick: () => {} },
+ {
+ label: "Yes, demote me",
+ variant: "destructive",
+ onClick: () => handleChangeRole(newRole),
+ },
+ ],
});
} else {
handleChangeRole(newRole);
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/cloud/secrets/components/SecretsSettings/components/AddSecretSheet/AddSecretSheet.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/cloud/secrets/components/SecretsSettings/components/AddSecretSheet/AddSecretSheet.tsx
index 760e4e30c2f..39a7cbc8119 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/cloud/secrets/components/SecretsSettings/components/AddSecretSheet/AddSecretSheet.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/cloud/secrets/components/SecretsSettings/components/AddSecretSheet/AddSecretSheet.tsx
@@ -73,12 +73,18 @@ export function AddSecretSheet({
const handleOpenChange = (nextOpen: boolean) => {
if (!nextOpen && hasContent) {
- alert.destructive({
+ alert({
title: "Discard unsaved changes?",
description:
"You have unsaved environment variables. Are you sure you want to close?",
- confirmText: "Discard",
- onConfirm: () => onOpenChange(false),
+ actions: [
+ { label: "Cancel", variant: "outline", onClick: () => {} },
+ {
+ label: "Discard",
+ variant: "destructive",
+ onClick: () => onOpenChange(false),
+ },
+ ],
});
return;
}
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/components/SessionSelector/components/SessionSelectorItem/SessionSelectorItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/components/SessionSelector/components/SessionSelectorItem/SessionSelectorItem.tsx
index 87150c06a3f..a97f0b09753 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/components/SessionSelector/components/SessionSelectorItem/SessionSelectorItem.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/components/SessionSelector/components/SessionSelectorItem/SessionSelectorItem.tsx
@@ -36,17 +36,23 @@ export function SessionSelectorItem({
className="shrink-0 rounded p-0.5 opacity-0 transition-opacity hover:bg-destructive/10 hover:text-destructive group-hover:opacity-100"
onClick={(event) => {
event.stopPropagation();
- alert.destructive({
+ alert({
title: "Delete Chat Session",
description: "Are you sure you want to delete this session?",
- confirmText: "Delete",
- onConfirm: () => {
- toast.promise(onDeleteSession(sessionId), {
- loading: "Deleting session...",
- success: "Session deleted",
- error: "Failed to delete session",
- });
- },
+ actions: [
+ { label: "Cancel", variant: "outline", onClick: () => {} },
+ {
+ label: "Delete",
+ variant: "destructive",
+ onClick: () => {
+ toast.promise(onDeleteSession(sessionId), {
+ loading: "Deleting session...",
+ success: "Session deleted",
+ error: "Failed to delete session",
+ });
+ },
+ },
+ ],
});
}}
>
diff --git a/packages/panes/src/core/store/store.test.ts b/packages/panes/src/core/store/store.test.ts
index 23b556e3fed..de9d8143be7 100644
--- a/packages/panes/src/core/store/store.test.ts
+++ b/packages/panes/src/core/store/store.test.ts
@@ -138,7 +138,6 @@ describe("pane operations", () => {
store.getState().addTab({ id: "t1", panes: [tp("p1")] });
store.getState().setPanePinned({
- tabId: "t1",
paneId: "p1",
pinned: true,
});
diff --git a/packages/panes/src/core/store/store.ts b/packages/panes/src/core/store/store.ts
index 2086e054bdf..8bfeff72a86 100644
--- a/packages/panes/src/core/store/store.ts
+++ b/packages/panes/src/core/store/store.ts
@@ -105,11 +105,7 @@ export interface WorkspaceStore extends WorkspaceState {
paneId: string;
titleOverride?: string;
}) => void;
- setPanePinned: (args: {
- tabId: string;
- paneId: string;
- pinned: boolean;
- }) => void;
+ setPanePinned: (args: { paneId: string; pinned: boolean }) => void;
replacePane: (args: {
tabId: string;
paneId: string;
@@ -334,26 +330,25 @@ export function createWorkspaceStore(
setPanePinned: (args) => {
set((s) => {
- const tab = s.tabs.find((t) => t.id === args.tabId);
- const pane = tab?.panes[args.paneId];
- if (!tab || !pane) return s;
-
- return {
- tabs: s.tabs.map((t) =>
- t.id === args.tabId
- ? {
- ...t,
- panes: {
- ...t.panes,
- [args.paneId]: {
- ...pane,
- pinned: args.pinned,
- },
- },
- }
- : t,
- ),
- };
+ for (const tab of s.tabs) {
+ const pane = tab.panes[args.paneId];
+ if (pane) {
+ return {
+ tabs: s.tabs.map((t) =>
+ t.id === tab.id
+ ? {
+ ...t,
+ panes: {
+ ...t.panes,
+ [args.paneId]: { ...pane, pinned: args.pinned },
+ },
+ }
+ : t,
+ ),
+ };
+ }
+ }
+ return s;
});
},
diff --git a/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx b/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx
index 6114aab8862..d9dde1bc58b 100644
--- a/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx
+++ b/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/Pane.tsx
@@ -81,8 +81,13 @@ export function Pane({
isActive,
store,
actions: {
- close: () =>
- store.getState().closePane({ tabId: tab.id, paneId: pane.id }),
+ close: async () => {
+ if (definition?.onBeforeClose) {
+ const allowed = await definition.onBeforeClose(pane);
+ if (!allowed) return;
+ }
+ store.getState().closePane({ tabId: tab.id, paneId: pane.id });
+ },
focus: () =>
store.getState().setActivePane({ tabId: tab.id, paneId: pane.id }),
setTitle: (title: string) =>
@@ -93,7 +98,6 @@ export function Pane({
}),
pin: () =>
store.getState().setPanePinned({
- tabId: tab.id,
paneId: pane.id,
pinned: true,
}),
@@ -216,6 +220,12 @@ export function Pane({
toolbar={toolbar}
actionsContent={ }
paneId={pane.id}
+ onClick={
+ definition?.onHeaderClick
+ ? () => definition.onHeaderClick?.(context)
+ : context.actions.pin
+ }
+ onMiddleClick={context.actions.close}
/>
{definition ? (
diff --git a/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneHeader/PaneHeader.tsx b/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneHeader/PaneHeader.tsx
index 640a2dda3dc..fbe9aadfdf2 100644
--- a/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneHeader/PaneHeader.tsx
+++ b/packages/panes/src/react/components/Workspace/components/Tab/components/Pane/components/PaneHeader/PaneHeader.tsx
@@ -12,6 +12,8 @@ interface PaneHeaderProps {
actionsContent: ReactNode;
toolbar?: ReactNode;
paneId?: string;
+ onClick?: () => void;
+ onMiddleClick?: () => void;
}
export const PANE_DRAG_TYPE = "pane";
@@ -25,6 +27,8 @@ export function PaneHeader({
actionsContent,
toolbar,
paneId,
+ onClick,
+ onMiddleClick,
}: PaneHeaderProps) {
const [{ isDragging }, connectDrag] = useDrag(
() => ({
@@ -48,6 +52,8 @@ export function PaneHeader({
);
return (
+ // biome-ignore lint/a11y/useKeyWithClickEvents: pane header click-to-pin doesn't need keyboard equivalent
+ // biome-ignore lint/a11y/noStaticElementInteractions: click to pin, middle-click to close
{
+ if (e.button === 1 && onMiddleClick) {
+ e.preventDefault();
+ onMiddleClick();
+ }
+ }}
>
{toolbar ?? (
{
renderTitle?(context: RendererContext): ReactNode;
renderHeaderExtras?(context: RendererContext): ReactNode;
renderToolbar?(context: RendererContext): ReactNode;
+ onHeaderClick?(context: RendererContext): void;
+ onBeforeClose?(pane: Pane): boolean | Promise;
paneActions?:
| PaneActionConfig[]
| ((
diff --git a/packages/ui/src/atoms/Alert/Alert.tsx b/packages/ui/src/atoms/Alert/Alert.tsx
index 3cb36d4ba14..4051ba650b4 100644
--- a/packages/ui/src/atoms/Alert/Alert.tsx
+++ b/packages/ui/src/atoms/Alert/Alert.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Button } from "@superset/ui/button";
+import { Button, type buttonVariants } from "@superset/ui/button";
import {
Dialog,
DialogContent,
@@ -9,100 +9,95 @@ import {
DialogHeader,
DialogTitle,
} from "@superset/ui/dialog";
+import type { VariantProps } from "class-variance-authority";
import { useState } from "react";
+type AlertActionVariant = NonNullable<
+ VariantProps["variant"]
+>;
+
+interface AlertAction {
+ label: string;
+ variant?: AlertActionVariant;
+ onClick: () => void | Promise;
+}
+
type AlertOptions = {
title: string;
description: string;
- confirmText?: string;
- cancelText?: string;
- onConfirm: () => void | Promise;
- onCancel?: () => void;
-};
-
-type InternalAlertOptions = AlertOptions & {
- variant: "default" | "destructive";
+ actions: AlertAction[];
};
-let showAlertFn: ((options: InternalAlertOptions) => void) | null = null;
+let showAlertFn: ((options: AlertOptions) => void) | null = null;
const Alerter = () => {
- const [alertOptions, setAlertOptions] = useState(
- null,
- );
+ const [alertOptions, setAlertOptions] = useState(null);
const [isOpen, setIsOpen] = useState(false);
- const [isLoading, setIsLoading] = useState(false);
+ const [loadingIndex, setLoadingIndex] = useState(null);
showAlertFn = (options) => {
setAlertOptions(options);
+ setLoadingIndex(null);
setIsOpen(true);
};
- const handleConfirm = async () => {
- if (!alertOptions) return;
-
- setIsLoading(true);
+ const handleAction = async (action: AlertAction, index: number) => {
+ setLoadingIndex(index);
try {
- await alertOptions.onConfirm();
+ await action.onClick();
setIsOpen(false);
} catch (error) {
- console.error("[alert] Confirmation failed:", error);
+ console.error("[alert] Action failed:", error);
} finally {
- setIsLoading(false);
+ setLoadingIndex(null);
}
};
- const handleCancel = () => {
- if (!alertOptions) return;
- alertOptions.onCancel?.();
+ const handleClose = () => {
setIsOpen(false);
};
+ if (!alertOptions) return null;
+
+ const actions = [...alertOptions.actions].reverse();
+
return (
!open && handleCancel()}
+ onOpenChange={(open) => !open && handleClose()}
>
- {alertOptions?.title}
- {alertOptions?.description}
+ {alertOptions.title}
+ {alertOptions.description}
-
- {alertOptions?.cancelText ?? "Cancel"}
-
-
- {isLoading
- ? "Loading..."
- : (alertOptions?.confirmText ?? "Confirm")}
-
+ {actions.map((action, i) => (
+ handleAction(action, i)}
+ disabled={loadingIndex !== null}
+ >
+ {loadingIndex === i ? "Loading..." : action.label}
+
+ ))}
);
};
-const createAlert = (variant: "default" | "destructive") => {
- return (options: AlertOptions) => {
- if (!showAlertFn) {
- console.error(
- "[alert] Alerter not mounted. Make sure to render in your app",
- );
- return;
- }
- const internalOptions: InternalAlertOptions = { ...options, variant };
- showAlertFn(internalOptions);
- };
+const alert = (options: AlertOptions) => {
+ if (!showAlertFn) {
+ console.error(
+ "[alert] Alerter not mounted. Make sure to render in your app",
+ );
+ return;
+ }
+ showAlertFn(options);
};
-const alert = Object.assign(createAlert("default"), {
- destructive: createAlert("destructive"),
-});
-
export { Alerter, alert };
+export type { AlertAction, AlertActionVariant, AlertOptions };
diff --git a/packages/ui/src/atoms/Alert/index.ts b/packages/ui/src/atoms/Alert/index.ts
index 382c890a97a..7ba60484d14 100644
--- a/packages/ui/src/atoms/Alert/index.ts
+++ b/packages/ui/src/atoms/Alert/index.ts
@@ -1 +1,2 @@
+export type { AlertAction, AlertActionVariant, AlertOptions } from "./Alert";
export { Alerter, alert } from "./Alert";
diff --git a/packages/workspace-client/src/hooks/useFileTree/useFileTree.ts b/packages/workspace-client/src/hooks/useFileTree/useFileTree.ts
index 172ad9605d1..28272b73d29 100644
--- a/packages/workspace-client/src/hooks/useFileTree/useFileTree.ts
+++ b/packages/workspace-client/src/hooks/useFileTree/useFileTree.ts
@@ -28,6 +28,7 @@ export interface UseFileTreeResult {
toggle: (path: string) => Promise;
refreshAll: () => Promise;
refreshPath: (path: string) => Promise;
+ reveal: (path: string) => Promise;
}
interface FileTreeState {
@@ -214,10 +215,10 @@ export function useFileTree({
});
try {
- const result = await utils.filesystem.listDirectory.fetch({
- workspaceId,
- absolutePath,
- });
+ const result = await utils.filesystem.listDirectory.fetch(
+ { workspaceId, absolutePath },
+ { staleTime: 0 },
+ );
updateState((current) => {
const nextEntries = new Map(current.entriesByPath);
@@ -479,6 +480,27 @@ export function useFileTree({
state.loadingDirectories,
]);
+ const reveal = useCallback(
+ async (absolutePath: string): Promise => {
+ if (!rootPath || !absolutePath.startsWith(rootPath)) return;
+
+ // Collect ancestor directories from rootPath down to the parent of the target
+ const ancestors: string[] = [];
+ let current = getParentPath(absolutePath);
+ while (current.length >= rootPath.length && current !== absolutePath) {
+ ancestors.unshift(current);
+ if (current === rootPath) break;
+ current = getParentPath(current);
+ }
+
+ // Expand all ancestors and load their contents
+ for (const dir of ancestors) {
+ await expand(dir);
+ }
+ },
+ [expand, rootPath],
+ );
+
return {
isLoadingRoot: state.loadingDirectories.has(rootPath),
collapseAll,
@@ -488,5 +510,6 @@ export function useFileTree({
toggle,
refreshAll,
refreshPath,
+ reveal,
};
}