Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { Button } from "@superset/ui/button";
import { ButtonGroup, ButtonGroupSeparator } from "@superset/ui/button-group";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@superset/ui/dropdown-menu";
import { toast } from "@superset/ui/sonner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
import { useRef } from "react";
import { HiMiniPlus, HiOutlineBolt } from "react-icons/hi2";
import { useCallback, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { HiFolderOpen, HiMiniPlus, HiOutlineBolt } from "react-icons/hi2";
import { trpc } from "renderer/lib/trpc";
import { useOpenNew } from "renderer/react-query/projects";
import {
useCreateBranchWorkspace,
useCreateWorkspace,
} from "renderer/react-query/workspaces";
import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal";
import { HOTKEYS } from "shared/hotkeys";

export interface CreateWorkspaceButtonProps {
className?: string;
Expand All @@ -19,8 +27,7 @@ export interface CreateWorkspaceButtonProps {
export function CreateWorkspaceButton({
className,
}: CreateWorkspaceButtonProps) {
const modalButtonRef = useRef<HTMLButtonElement>(null);
const quickCreateButtonRef = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);

const { data: activeWorkspace } = trpc.workspaces.getActive.useQuery();
const { data: recentProjects = [] } = trpc.projects.getRecents.useQuery();
Expand All @@ -33,29 +40,18 @@ export function CreateWorkspaceButton({
(p) => p.id === activeWorkspace?.projectId,
);

const handleModalCreate = () => {
modalButtonRef.current?.blur();
openModal();
};
const isLoading =
createWorkspace.isPending ||
createBranchWorkspace.isPending ||
openNew.isPending;

const handleQuickCreate = () => {
quickCreateButtonRef.current?.blur();
if (currentProject) {
toast.promise(
createWorkspace.mutateAsync({ projectId: currentProject.id }),
{
loading: "Creating workspace...",
success: "Workspace created",
error: (err) =>
err instanceof Error ? err.message : "Failed to create workspace",
},
);
} else {
handleOpenNewProject();
}
};
const handleModalCreate = useCallback(() => {
setOpen(false);
openModal();
}, [openModal]);

const handleOpenNewProject = async () => {
const handleOpenNewProject = useCallback(async () => {
setOpen(false);
try {
const result = await openNew.mutateAsync(undefined);
if (result.canceled) {
Expand Down Expand Up @@ -90,55 +86,94 @@ export function CreateWorkspaceButton({
error instanceof Error ? error.message : "An unknown error occurred",
});
}
};
}, [openNew, createBranchWorkspace]);

const handleQuickCreate = useCallback(() => {
setOpen(false);
if (currentProject) {
toast.promise(
createWorkspace.mutateAsync({ projectId: currentProject.id }),
{
loading: "Creating workspace...",
success: "Workspace created",
error: (err) =>
err instanceof Error ? err.message : "Failed to create workspace",
},
);
} else {
handleOpenNewProject();
}
}, [currentProject, createWorkspace, handleOpenNewProject]);

// Keyboard shortcuts
const handleQuickCreateHotkey = useCallback(() => {
if (!isLoading) handleQuickCreate();
}, [isLoading, handleQuickCreate]);

const handleOpenProjectHotkey = useCallback(() => {
if (!isLoading) handleOpenNewProject();
}, [isLoading, handleOpenNewProject]);

useHotkeys(HOTKEYS.NEW_WORKSPACE.keys, handleModalCreate);
useHotkeys(HOTKEYS.QUICK_CREATE_WORKSPACE.keys, handleQuickCreateHotkey);
useHotkeys(HOTKEYS.OPEN_PROJECT.keys, handleOpenProjectHotkey);

return (
<ButtonGroup
className={`${className} ml-1 mt-1 rounded-md border border-border/50`}
>
<DropdownMenu open={open} onOpenChange={setOpen}>
<Tooltip>
<TooltipTrigger asChild>
<Button
ref={modalButtonRef}
variant="ghost"
size="sm"
aria-label="New workspace"
className="h-7 gap-1 rounded-r-none px-2 text-muted-foreground hover:bg-accent hover:text-foreground"
onClick={handleModalCreate}
>
<HiMiniPlus className="size-4" />
<span className="text-xs">New</span>
</Button>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label="New workspace"
disabled={isLoading}
className={`${className} flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-all duration-150 hover:bg-foreground/[0.06] hover:text-foreground active:scale-95 disabled:pointer-events-none disabled:opacity-40`}
>
<HiMiniPlus className="size-[18px] stroke-[0.5]" />
</button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
Create workspace or project
</TooltipContent>
</Tooltip>
<ButtonGroupSeparator />
<Tooltip>
<TooltipTrigger asChild>
<Button
ref={quickCreateButtonRef}
variant="ghost"
size="icon"
aria-label="Quick create workspace"
className="size-7 rounded-l-none text-muted-foreground hover:bg-accent hover:text-foreground"
onClick={handleQuickCreate}
disabled={
createWorkspace.isPending ||
createBranchWorkspace.isPending ||
openNew.isPending
}
>
<HiOutlineBolt className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
{currentProject
? `Quick create in ${currentProject.name}`
: "Quick create workspace"}
</TooltipContent>
</Tooltip>
</ButtonGroup>
<DropdownMenuContent
align="end"
sideOffset={8}
className="w-48 rounded-lg border-border/40 bg-popover/95 p-1 shadow-lg backdrop-blur-sm"
>
<DropdownMenuItem
onClick={handleModalCreate}
className="rounded-md text-[13px]"
>
<HiMiniPlus className="size-[14px] opacity-60" />
New Workspace
<DropdownMenuShortcut className="opacity-40">⌘N</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem
onClick={handleQuickCreate}
disabled={isLoading}
className="rounded-md text-[13px]"
>
<HiOutlineBolt className="size-[14px] opacity-60" />
Quick Create
<DropdownMenuShortcut className="opacity-40">
⌘⇧N
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator className="my-1 bg-border/40" />
<DropdownMenuItem
onClick={handleOpenNewProject}
disabled={isLoading}
className="rounded-md text-[13px]"
>
<HiFolderOpen className="size-[14px] opacity-60" />
Open Project
<DropdownMenuShortcut className="opacity-40">
⌘⇧O
</DropdownMenuShortcut>
</DropdownMenuItem>
Comment on lines +145 to +175
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use dynamic keyboard shortcuts for cross-platform support.

The dropdown menu hardcodes Mac-specific keyboard shortcut symbols (⌘N, ⌘⇧N, ⌘⇧O). This won't adapt to Windows (Ctrl+N) or Linux (Super+N) conventions.

🔎 Proposed fix using HOTKEYS.display
 <DropdownMenuItem
   onClick={handleModalCreate}
   className="rounded-md text-[13px]"
 >
   <HiMiniPlus className="size-[14px] opacity-60" />
   New Workspace
-  <DropdownMenuShortcut className="opacity-40">⌘N</DropdownMenuShortcut>
+  <DropdownMenuShortcut className="opacity-40">
+    {HOTKEYS.NEW_WORKSPACE.display.join("")}
+  </DropdownMenuShortcut>
 </DropdownMenuItem>
 <DropdownMenuItem
   onClick={handleQuickCreate}
   disabled={isLoading}
   className="rounded-md text-[13px]"
 >
   <HiOutlineBolt className="size-[14px] opacity-60" />
   Quick Create
   <DropdownMenuShortcut className="opacity-40">
-    ⌘⇧N
+    {HOTKEYS.QUICK_CREATE_WORKSPACE.display.join("")}
   </DropdownMenuShortcut>
 </DropdownMenuItem>
 <DropdownMenuSeparator className="my-1 bg-border/40" />
 <DropdownMenuItem
   onClick={handleOpenNewProject}
   disabled={isLoading}
   className="rounded-md text-[13px]"
 >
   <HiFolderOpen className="size-[14px] opacity-60" />
   Open Project
   <DropdownMenuShortcut className="opacity-40">
-    ⌘⇧O
+    {HOTKEYS.OPEN_PROJECT.display.join("")}
   </DropdownMenuShortcut>
 </DropdownMenuItem>

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/CreateWorkspaceButton.tsx
around lines 143-173, the shortcut labels are hardcoded to macOS symbols;
replace those literals with the app's hotkey display helper so shortcuts render
per-platform. Import and use the project's HOTKEYS/display utility (the same IDs
used when registering the shortcuts) instead of "⌘N", "⌘⇧N", "⌘⇧O" — e.g.
DropdownMenuShortcut children should be HOTKEYS.display('new-workspace'),
HOTKEYS.display('quick-create'), HOTKEYS.display('open-project') (or the exact
registration keys used elsewhere); ensure the HOTKEYS import path matches the
project's hotkey util and that components still receive isLoading/disabled props
unchanged.

</DropdownMenuContent>
</DropdownMenu>
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Fragment, useEffect, useRef, useState } from "react";
import { Fragment, useCallback, useEffect, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { trpc } from "renderer/lib/trpc";
import {
Expand All @@ -16,7 +16,7 @@ import { WorkspaceGroup } from "./WorkspaceGroup";

const MIN_WORKSPACE_WIDTH = 60;
const MAX_WORKSPACE_WIDTH = 160;
const ADD_BUTTON_WIDTH = 48;
const ADD_BUTTON_WIDTH = 40;

export function WorkspacesTabs() {
const { data: groups = [] } = trpc.workspaces.getAllGrouped.useQuery();
Expand Down Expand Up @@ -74,7 +74,7 @@ export function WorkspacesTabs() {
// Only create one at a time
break;
}
}, [groups, isCreating, createBranchWorkspace.mutate]);
}, [groups, isCreating, createBranchWorkspace]);

// Flatten workspaces for keyboard navigation
const allWorkspaces = groups.flatMap((group) => group.workspaces);
Expand All @@ -84,9 +84,9 @@ export function WorkspacesTabs() {
{ length: 9 },
(_, i) => `meta+${i + 1}`,
).join(", ");
useHotkeys(
workspaceKeys,
(event) => {

const handleWorkspaceSwitch = useCallback(
(event: KeyboardEvent) => {
const num = Number(event.key);
if (num >= 1 && num <= 9) {
const workspace = allWorkspaces[num - 1];
Expand All @@ -98,8 +98,7 @@ export function WorkspacesTabs() {
[allWorkspaces, setActiveWorkspace],
);

// Navigate to previous workspace (⌘+←)
useHotkeys(HOTKEYS.PREV_WORKSPACE.keys, () => {
const handlePrevWorkspace = useCallback(() => {
if (!activeWorkspaceId) return;
const currentIndex = allWorkspaces.findIndex(
(w) => w.id === activeWorkspaceId,
Expand All @@ -109,8 +108,7 @@ export function WorkspacesTabs() {
}
}, [activeWorkspaceId, allWorkspaces, setActiveWorkspace]);

// Navigate to next workspace (⌘+→)
useHotkeys(HOTKEYS.NEXT_WORKSPACE.keys, () => {
const handleNextWorkspace = useCallback(() => {
if (!activeWorkspaceId) return;
const currentIndex = allWorkspaces.findIndex(
(w) => w.id === activeWorkspaceId,
Expand All @@ -120,6 +118,10 @@ export function WorkspacesTabs() {
}
}, [activeWorkspaceId, allWorkspaces, setActiveWorkspace]);

useHotkeys(workspaceKeys, handleWorkspaceSwitch);
useHotkeys(HOTKEYS.PREV_WORKSPACE.keys, handlePrevWorkspace);
useHotkeys(HOTKEYS.NEXT_WORKSPACE.keys, handleNextWorkspace);

useEffect(() => {
const checkScroll = () => {
if (!scrollRef.current) return;
Expand Down Expand Up @@ -163,58 +165,59 @@ export function WorkspacesTabs() {

return (
<div ref={containerRef} className="flex items-center h-full w-full">
<div className="flex items-center h-full min-w-0">
<div className="relative h-full overflow-hidden min-w-0">
<div
ref={scrollRef}
className="flex h-full overflow-x-auto hide-scrollbar gap-4"
>
{groups.map((group, groupIndex) => (
<Fragment key={group.project.id}>
<WorkspaceGroup
projectId={group.project.id}
projectName={group.project.name}
projectColor={group.project.color}
projectIndex={groupIndex}
workspaces={group.workspaces}
activeWorkspaceId={
isSettingsActive ? null : activeWorkspaceId
}
workspaceWidth={workspaceWidth}
hoveredWorkspaceId={hoveredWorkspaceId}
onWorkspaceHover={setHoveredWorkspaceId}
/>
{groupIndex < groups.length - 1 && (
<div className="flex items-center h-full py-2">
<div className="w-px h-full bg-border" />
</div>
)}
</Fragment>
))}
{isSettingsTabOpen && (
<>
{groups.length > 0 && (
<div className="flex items-center h-full py-2">
<div className="w-px h-full bg-border" />
</div>
)}
<SettingsTab
width={workspaceWidth}
isActive={isSettingsActive}
/>
</>
)}
</div>

{/* Fade effects for scroll indication */}
{showStartFade && (
<div className="pointer-events-none absolute left-0 top-0 h-full w-8 bg-linear-to-r from-background to-transparent" />
<div className="relative h-full overflow-hidden min-w-0 flex-1">
<div
ref={scrollRef}
className="flex h-full overflow-x-auto hide-scrollbar gap-4 pr-10"
>
{groups.map((group, groupIndex) => (
<Fragment key={group.project.id}>
<WorkspaceGroup
projectId={group.project.id}
projectName={group.project.name}
projectColor={group.project.color}
projectIndex={groupIndex}
workspaces={group.workspaces}
activeWorkspaceId={isSettingsActive ? null : activeWorkspaceId}
workspaceWidth={workspaceWidth}
hoveredWorkspaceId={hoveredWorkspaceId}
onWorkspaceHover={setHoveredWorkspaceId}
/>
{groupIndex < groups.length - 1 && (
<div className="flex items-center h-full py-2">
<div className="w-px h-full bg-border" />
</div>
)}
</Fragment>
))}
{isSettingsTabOpen && (
<>
{groups.length > 0 && (
<div className="flex items-center h-full py-2">
<div className="w-px h-full bg-border" />
</div>
)}
<SettingsTab width={workspaceWidth} isActive={isSettingsActive} />
</>
)}
</div>

{/* Left fade for scroll indication */}
{showStartFade && (
<div className="pointer-events-none absolute left-0 top-0 h-full w-8 bg-linear-to-r from-background to-transparent" />
)}

{/* Right side: gradient fade + button container */}
<div className="absolute right-0 top-0 h-full flex items-center pointer-events-none">
{/* Gradient fade - only show when content overflows */}
{showEndFade && (
<div className="pointer-events-none absolute right-0 top-0 h-full w-8 bg-linear-to-l from-background to-transparent" />
<div className="h-full w-8 bg-linear-to-l from-background to-transparent" />
)}
{/* Button with solid background */}
<div className="h-full flex items-center bg-background pl-1 pr-2 pointer-events-auto">
<CreateWorkspaceButton className="no-drag" />
</div>
</div>
Comment on lines +206 to 220
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix invalid TailwindCSS gradient class names.

Lines 207 and 214 use bg-linear-to-r and bg-linear-to-l, which are not valid TailwindCSS classes. The correct syntax for gradients in TailwindCSS (including v4) is bg-gradient-to-r and bg-gradient-to-l.

🔎 Proposed fix for gradient class names
 {/* Left fade for scroll indication */}
 {showStartFade && (
-  <div className="pointer-events-none absolute left-0 top-0 h-full w-8 bg-linear-to-r from-background to-transparent" />
+  <div className="pointer-events-none absolute left-0 top-0 h-full w-8 bg-gradient-to-r from-background to-transparent" />
 )}

 {/* Right side: gradient fade + button container */}
 <div className="absolute right-0 top-0 h-full flex items-center pointer-events-none">
   {/* Gradient fade - only show when content overflows */}
   {showEndFade && (
-    <div className="h-full w-8 bg-linear-to-l from-background to-transparent" />
+    <div className="h-full w-8 bg-gradient-to-l from-background to-transparent" />
   )}
   {/* Button with solid background */}
   <div className="h-full flex items-center bg-background pl-1 pr-2 pointer-events-auto">
     <CreateWorkspaceButton className="no-drag" />
   </div>
 </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{showStartFade && (
<div className="pointer-events-none absolute left-0 top-0 h-full w-8 bg-linear-to-r from-background to-transparent" />
)}
{/* Right side: gradient fade + button container */}
<div className="absolute right-0 top-0 h-full flex items-center pointer-events-none">
{/* Gradient fade - only show when content overflows */}
{showEndFade && (
<div className="pointer-events-none absolute right-0 top-0 h-full w-8 bg-linear-to-l from-background to-transparent" />
<div className="h-full w-8 bg-linear-to-l from-background to-transparent" />
)}
{/* Button with solid background */}
<div className="h-full flex items-center bg-background pl-1 pr-2 pointer-events-auto">
<CreateWorkspaceButton className="no-drag" />
</div>
</div>
{showStartFade && (
<div className="pointer-events-none absolute left-0 top-0 h-full w-8 bg-gradient-to-r from-background to-transparent" />
)}
{/* Right side: gradient fade + button container */}
<div className="absolute right-0 top-0 h-full flex items-center pointer-events-none">
{/* Gradient fade - only show when content overflows */}
{showEndFade && (
<div className="h-full w-8 bg-gradient-to-l from-background to-transparent" />
)}
{/* Button with solid background */}
<div className="h-full flex items-center bg-background pl-1 pr-2 pointer-events-auto">
<CreateWorkspaceButton className="no-drag" />
</div>
</div>
🤖 Prompt for AI Agents
In
apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/index.tsx
around lines 206 to 220, the gradient class names are incorrect (bg-linear-to-r
and bg-linear-to-l). Replace those with the correct TailwindCSS gradient utility
names bg-gradient-to-r and bg-gradient-to-l respectively, ensuring other
gradient-related classes (from-... to-...) remain unchanged so the fades render
correctly.

<CreateWorkspaceButton className="no-drag" />
</div>
</div>
);
Expand Down
Loading