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 ( +
+ ); +} 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 ( +
+ {filePath.split("/").pop() +
+ ); +} 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 ( +
+ + +
+ ); +} 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} -

-
- -
- {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} - - + {actions.map((action, i) => ( + + ))} ); }; -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, }; }