diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx index 7c562c6db42..f892c4918b8 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx @@ -1,9 +1,28 @@ +import type { TerminalPreset } from "@superset/local-db"; import { Button } from "@superset/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@superset/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; -import { useMemo } from "react"; -import { HiMiniPlus } from "react-icons/hi2"; +import { useCallback, useMemo, useRef, useState } from "react"; +import { + HiMiniChevronDown, + HiMiniCog6Tooth, + HiMiniCommandLine, + HiMiniPlus, +} from "react-icons/hi2"; +import { + getPresetIcon, + useIsDarkTheme, +} from "renderer/assets/app-icons/preset-icons"; import { HotkeyTooltipContent } from "renderer/components/HotkeyTooltipContent"; import { trpc } from "renderer/lib/trpc"; +import { usePresets } from "renderer/react-query/presets"; +import { useOpenSettings } from "renderer/stores"; import { useTabsStore } from "renderer/stores/tabs/store"; import { GroupItem } from "./GroupItem"; @@ -15,9 +34,34 @@ export function GroupStrip() { const panes = useTabsStore((s) => s.panes); const activeTabIds = useTabsStore((s) => s.activeTabIds); const addTab = useTabsStore((s) => s.addTab); + const renameTab = useTabsStore((s) => s.renameTab); const removeTab = useTabsStore((s) => s.removeTab); const setActiveTab = useTabsStore((s) => s.setActiveTab); + const { presets } = usePresets(); + const isDark = useIsDarkTheme(); + const openSettings = useOpenSettings(); + const [dropdownOpen, setDropdownOpen] = useState(false); + const hoverTimeoutRef = useRef | null>(null); + + const handleDropdownMouseEnter = useCallback(() => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + } + hoverTimeoutRef.current = setTimeout(() => { + setDropdownOpen(true); + }, 150); + }, []); + + const handleDropdownMouseLeave = useCallback(() => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + } + hoverTimeoutRef.current = setTimeout(() => { + setDropdownOpen(false); + }, 150); + }, []); + const tabs = useMemo( () => activeWorkspaceId @@ -47,6 +91,26 @@ export function GroupStrip() { } }; + const handleSelectPreset = (preset: TerminalPreset) => { + if (!activeWorkspaceId) return; + + const { tabId } = addTab(activeWorkspaceId, { + initialCommands: preset.commands, + initialCwd: preset.cwd || undefined, + }); + + if (preset.name) { + renameTab(tabId, preset.name); + } + + setDropdownOpen(false); + }; + + const handleOpenPresetsSettings = () => { + openSettings("presets"); + setDropdownOpen(false); + }; + const handleSelectGroup = (tabId: string) => { if (activeWorkspaceId) { setActiveTab(activeWorkspaceId, tabId); @@ -78,21 +142,76 @@ export function GroupStrip() { ))} )} - - - + + + + + + + + + + + {presets.length > 0 && ( + <> + {presets.map((preset) => { + const presetIcon = getPresetIcon(preset.name, isDark); + return ( + handleSelectPreset(preset)} + className="gap-2" + > + {presetIcon ? ( + + ) : ( + + )} + {preset.name || "default"} + + ); + })} + + + )} + - - - - - - - + + Configure Presets + + + ); } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/PresetContextMenu/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/PresetContextMenu/index.tsx deleted file mode 100644 index a9b7009d4a4..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/PresetContextMenu/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuSeparator, - ContextMenuTrigger, -} from "@superset/ui/context-menu"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; -import type React from "react"; - -interface PresetContextMenuProps { - hasActiveTab: boolean; - tooltipText?: string; - onOpenAsNewTab: () => void; - onOpenAsPane: () => void; - children: React.ReactNode; -} - -export function PresetContextMenu({ - hasActiveTab, - tooltipText, - onOpenAsNewTab, - onOpenAsPane, - children, -}: PresetContextMenuProps) { - const contextMenuContent = ( - - - Open as New Tab - - {hasActiveTab && ( - <> - - - Open as Pane in Current Tab - - - )} - - ); - - if (!tooltipText) { - return ( - - {children} - {contextMenuContent} - - ); - } - - return ( - - - - {children} - - {contextMenuContent} - - {tooltipText} - - ); -} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabItem/TabContextMenu.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabItem/TabContextMenu.tsx deleted file mode 100644 index 0ce5492def6..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabItem/TabContextMenu.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuSeparator, - ContextMenuTrigger, -} from "@superset/ui/context-menu"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; -import type React from "react"; - -interface TabContextMenuProps { - paneCount: number; - onClose: () => void; - onRename: () => void; - children: React.ReactNode; -} - -export function TabContextMenu({ - paneCount, - onClose, - onRename, - children, -}: TabContextMenuProps) { - const hasMultiplePanes = paneCount > 1; - - const handleRenameSelect = (event: Event) => { - // Prevent default to stop Radix from restoring focus to the trigger - event.preventDefault(); - onRename(); - }; - - const contextMenuContent = ( - - {children} - - - Rename Tab - - - - Close Tab - - - - ); - - if (!hasMultiplePanes) { - return contextMenuContent; - } - - return ( - - - - {children} - - - - Rename Tab - - - - Close Tab - - - - -
{paneCount} terminals
-
-
- ); -} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabItem/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabItem/index.tsx deleted file mode 100644 index 07a860bed16..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabItem/index.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import { Button } from "@superset/ui/button"; -import { Input } from "@superset/ui/input"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { useDrag, useDrop } from "react-dnd"; -import { HiMiniCommandLine, HiMiniXMark } from "react-icons/hi2"; -import { trpc } from "renderer/lib/trpc"; -import { useTabsStore } from "renderer/stores/tabs/store"; -import type { Tab } from "renderer/stores/tabs/types"; -import { getTabDisplayName } from "renderer/stores/tabs/utils"; -import { TabContextMenu } from "./TabContextMenu"; - -const DRAG_TYPE = "TAB"; - -interface DragItem { - type: typeof DRAG_TYPE; - tabId: string; - index: number; -} - -interface TabItemProps { - tab: Tab; - index: number; - isActive: boolean; -} - -export function TabItem({ tab, index, isActive }: TabItemProps) { - const { data: activeWorkspace } = trpc.workspaces.getActive.useQuery(); - const activeWorkspaceId = activeWorkspace?.id; - const removeTab = useTabsStore((s) => s.removeTab); - const setActiveTab = useTabsStore((s) => s.setActiveTab); - const renameTab = useTabsStore((s) => s.renameTab); - const panes = useTabsStore((s) => s.panes); - const needsAttention = useTabsStore((s) => - Object.values(s.panes).some((p) => p.tabId === tab.id && p.needsAttention), - ); - - const paneCount = useMemo( - () => Object.values(panes).filter((p) => p.tabId === tab.id).length, - [panes, tab.id], - ); - - const [isRenaming, setIsRenaming] = useState(false); - const [renameValue, setRenameValue] = useState(""); - const inputRef = useRef(null); - - // Drag source for tab reordering - const [{ isDragging }, drag] = useDrag< - DragItem, - void, - { isDragging: boolean } - >({ - type: DRAG_TYPE, - item: { type: DRAG_TYPE, tabId: tab.id, index }, - collect: (monitor) => ({ - isDragging: monitor.isDragging(), - }), - }); - - // Drop target (just for visual feedback, actual drop is handled by parent) - const [{ isDragOver }, drop] = useDrop< - DragItem, - void, - { isDragOver: boolean } - >({ - accept: DRAG_TYPE, - collect: (monitor) => ({ - isDragOver: monitor.isOver(), - }), - }); - - const displayName = getTabDisplayName(tab); - - const handleRemoveTab = (e?: React.MouseEvent) => { - e?.stopPropagation(); - removeTab(tab.id); - }; - - const handleTabClick = () => { - if (isRenaming) return; - if (activeWorkspaceId) { - setActiveTab(activeWorkspaceId, tab.id); - } - }; - - const startRename = () => { - setRenameValue(tab.userTitle ?? tab.name ?? displayName); - setIsRenaming(true); - }; - - // Focus input when entering rename mode - useEffect(() => { - if (isRenaming && inputRef.current) { - inputRef.current.focus(); - inputRef.current.select(); - } - }, [isRenaming]); - - const submitRename = () => { - const trimmedValue = renameValue.trim(); - const currentUserTitle = tab.userTitle?.trim() ?? ""; - if (trimmedValue !== currentUserTitle) { - renameTab(tab.id, trimmedValue); - } - setIsRenaming(false); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault(); - submitRename(); - } else if (e.key === "Escape") { - setIsRenaming(false); - } - }; - - const attachRef = (el: HTMLElement | null) => { - drag(el); - drop(el); - }; - - // When renaming, render outside TabContextMenu to avoid Radix focus interference - if (isRenaming) { - return ( -
-
-
- -
- setRenameValue(e.target.value)} - onBlur={submitRename} - onKeyDown={handleKeyDown} - onClick={(e) => e.stopPropagation()} - className="flex-1" - /> -
-
- -
-
- ); - } - - return ( -
- -
- - -
-
-
- ); -} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx deleted file mode 100644 index 63ee4320e2c..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/TabsCommandDialog/index.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import type { TerminalPreset } from "@superset/local-db"; -import { - CommandDialog, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@superset/ui/command"; -import { - HiMiniCommandLine, - HiMiniPlus, - HiOutlineCog6Tooth, -} from "react-icons/hi2"; -import { - getPresetIcon, - useIsDarkTheme, -} from "renderer/assets/app-icons/preset-icons"; - -interface TabsCommandDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - onAddTab: () => void; - onOpenPresetsSettings: () => void; - presets: TerminalPreset[]; - onSelectPreset: (preset: TerminalPreset) => void; -} - -export function TabsCommandDialog({ - open, - onOpenChange, - onAddTab, - onOpenPresetsSettings, - presets, - onSelectPreset, -}: TabsCommandDialogProps) { - const isDark = useIsDarkTheme(); - - return ( - - - - No results found. - - - - New Terminal - - - {presets.length > 0 && ( - - {presets.map((preset) => { - const presetIcon = getPresetIcon(preset.name, isDark); - return ( - onSelectPreset(preset)} - > - {presetIcon ? ( - - ) : ( - - )} - - {preset.name || "default"} - - {preset.description ? ( - - {preset.description} - - ) : ( - preset.cwd && ( - - {preset.cwd} - - ) - )} - - ); - })} - - )} - - - - Configure Presets - - - - - ); -} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx deleted file mode 100644 index 06841519dc3..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/TabsView/index.tsx +++ /dev/null @@ -1,283 +0,0 @@ -import type { TerminalPreset } from "@superset/local-db"; -import { Button } from "@superset/ui/button"; -import { ButtonGroup } from "@superset/ui/button-group"; -import { LayoutGroup, motion } from "framer-motion"; -import { useMemo, useRef, useState } from "react"; -import { useDrop } from "react-dnd"; -import { - HiMiniCommandLine, - HiMiniEllipsisHorizontal, - HiMiniPlus, -} from "react-icons/hi2"; -import { - getPresetIcon, - useIsDarkTheme, -} from "renderer/assets/app-icons/preset-icons"; -import { trpc } from "renderer/lib/trpc"; -import { usePresets } from "renderer/react-query/presets"; -import { useOpenSettings, useSidebarStore } from "renderer/stores"; -import { useTabsStore } from "renderer/stores/tabs/store"; - -import { PresetContextMenu } from "./PresetContextMenu"; -import { TabItem } from "./TabItem"; -import { TabsCommandDialog } from "./TabsCommandDialog"; - -const DRAG_TYPE = "TAB"; - -interface DragItem { - type: typeof DRAG_TYPE; - tabId: string; - index: number; -} - -export function TabsView() { - const isResizing = useSidebarStore((s) => s.isResizing); - const { data: activeWorkspace } = trpc.workspaces.getActive.useQuery(); - const activeWorkspaceId = activeWorkspace?.id; - const allTabs = useTabsStore((s) => s.tabs); - const addTab = useTabsStore((s) => s.addTab); - const addPane = useTabsStore((s) => s.addPane); - const renameTab = useTabsStore((s) => s.renameTab); - const reorderTabById = useTabsStore((s) => s.reorderTabById); - const activeTabIds = useTabsStore((s) => s.activeTabIds); - const [dropIndex, setDropIndex] = useState(null); - const [commandOpen, setCommandOpen] = useState(false); - const openSettings = useOpenSettings(); - const containerRef = useRef(null); - - const { presets } = usePresets(); - const isDark = useIsDarkTheme(); - - const tabs = useMemo( - () => - activeWorkspaceId - ? allTabs.filter((tab) => tab.workspaceId === activeWorkspaceId) - : [], - [activeWorkspaceId, allTabs], - ); - - const handleAddTab = () => { - if (activeWorkspaceId) { - addTab(activeWorkspaceId); - setCommandOpen(false); - } - }; - - const handleAddPane = () => { - if (!activeWorkspaceId) return; - - const activeTabId = activeTabIds[activeWorkspaceId]; - if (!activeTabId) { - // Fall back to creating a new tab if no active tab - handleAddTab(); - return; - } - - addPane(activeTabId); - setCommandOpen(false); - }; - - const handleOpenPresetsSettings = () => { - openSettings("presets"); - setCommandOpen(false); - }; - - const handleSelectPreset = (preset: TerminalPreset) => { - if (!activeWorkspaceId) return; - - // Pass preset options to addTab - Terminal component will read them from pane state - const { tabId } = addTab(activeWorkspaceId, { - initialCommands: preset.commands, - initialCwd: preset.cwd || undefined, - }); - - // Rename the tab to the preset name - if (preset.name) { - renameTab(tabId, preset.name); - } - - setCommandOpen(false); - }; - - const handleSelectPresetAsPane = (preset: TerminalPreset) => { - if (!activeWorkspaceId) return; - - const activeTabId = activeTabIds[activeWorkspaceId]; - if (!activeTabId) { - // Fall back to opening as new tab if no active tab - handleSelectPreset(preset); - return; - } - - // Add pane to current tab with preset options - addPane(activeTabId, { - initialCommands: preset.commands, - initialCwd: preset.cwd || undefined, - }); - - setCommandOpen(false); - }; - - const [{ isOver }, drop] = useDrop({ - accept: DRAG_TYPE, - hover: (item, monitor) => { - if (!containerRef.current) return; - - const clientOffset = monitor.getClientOffset(); - if (!clientOffset) return; - - const tabItems = containerRef.current.querySelectorAll("[data-tab-item]"); - let newDropIndex = tabs.length; - - tabItems.forEach((element, index) => { - const rect = element.getBoundingClientRect(); - const midY = rect.top + rect.height / 2; - - if (clientOffset.y < midY && index < newDropIndex) { - newDropIndex = index; - } - }); - - if (newDropIndex === item.index || newDropIndex === item.index + 1) { - setDropIndex(null); - } else { - setDropIndex(newDropIndex); - } - }, - drop: (item) => { - if (dropIndex !== null && dropIndex !== item.index) { - const targetIndex = dropIndex > item.index ? dropIndex - 1 : dropIndex; - reorderTabById(item.tabId, targetIndex); - } - setDropIndex(null); - }, - collect: (monitor) => ({ - isOver: monitor.isOver(), - }), - }); - - if (!isOver && dropIndex !== null) { - setDropIndex(null); - } - - return ( - - ); -}