diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/DashboardSidebarHeader.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/DashboardSidebarHeader.tsx
index 7afa1861c8f..0e2e180d51e 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/DashboardSidebarHeader.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/DashboardSidebarHeader.tsx
@@ -1,12 +1,9 @@
import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
import { cn } from "@superset/ui/utils";
-import { useMatchRoute, useNavigate } from "@tanstack/react-router";
-import { LuLayers, LuPlus } from "react-icons/lu";
+import { LuFolderPlus, LuPlus } from "react-icons/lu";
import { useHotkeyDisplay } from "renderer/hotkeys";
-import {
- STROKE_WIDTH,
- STROKE_WIDTH_THICK,
-} from "renderer/screens/main/components/WorkspaceSidebar/constants";
+import { OrganizationDropdown } from "renderer/routes/_authenticated/_dashboard/components/TopBar/components/OrganizationDropdown";
+import { STROKE_WIDTH_THICK } from "renderer/screens/main/components/WorkspaceSidebar/constants";
import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal";
interface DashboardSidebarHeaderProps {
@@ -16,35 +13,24 @@ interface DashboardSidebarHeaderProps {
export function DashboardSidebarHeader({
isCollapsed = false,
}: DashboardSidebarHeaderProps) {
- const navigate = useNavigate();
- const matchRoute = useMatchRoute();
const openModal = useOpenNewWorkspaceModal();
const shortcutText = useHotkeyDisplay("NEW_WORKSPACE").text;
- const isWorkspacesPageOpen = !!matchRoute({ to: "/v2-workspaces" });
-
- const handleWorkspacesClick = () => {
- navigate({ to: "/v2-workspaces" });
- };
if (isCollapsed) {
return (
+
+
- Workspaces
+ Add Repository
@@ -67,21 +53,22 @@ export function DashboardSidebarHeader({
return (
-
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/OrganizationDropdown/OrganizationDropdown.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/OrganizationDropdown/OrganizationDropdown.tsx
index 04eb749c50f..d918461f319 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/OrganizationDropdown/OrganizationDropdown.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/OrganizationDropdown/OrganizationDropdown.tsx
@@ -32,7 +32,11 @@ import { authClient } from "renderer/lib/auth-client";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
-export function OrganizationDropdown() {
+export function OrganizationDropdown({
+ variant = "topbar",
+}: {
+ variant?: "topbar" | "expanded" | "collapsed";
+}) {
const { data: session } = authClient.useSession();
const collections = useCollections();
const signOutMutation = electronTrpc.auth.signOut.useMutation();
@@ -65,27 +69,60 @@ export function OrganizationDropdown() {
const userName = session?.user?.name;
const displayName = activeOrganization?.name ?? userName ?? "Organization";
+ const triggerButton =
+ variant === "collapsed" ? (
+
+
+
+ ) : variant === "expanded" ? (
+
+
+ {displayName}
+
+
+ ) : (
+
+
+
+ {displayName}
+
+
+
+ );
+
+ const contentAlign = variant === "topbar" ? "end" : "start";
+
return (
-
-
-
-
- {displayName}
-
-
-
-
-
+ {triggerButton}
+
{/* Organization */}
navigate({ to: "/settings/account" })}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/RightSidebarToggle/RightSidebarToggle.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/RightSidebarToggle/RightSidebarToggle.tsx
new file mode 100644
index 00000000000..8484e02f3fa
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/RightSidebarToggle/RightSidebarToggle.tsx
@@ -0,0 +1,55 @@
+import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
+import {
+ LuPanelRight,
+ LuPanelRightClose,
+ LuPanelRightOpen,
+} from "react-icons/lu";
+import { HotkeyLabel } from "renderer/hotkeys";
+import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
+
+export function RightSidebarToggle({ workspaceId }: { workspaceId: string }) {
+ const collections = useCollections();
+ const localState = collections.v2WorkspaceLocalState.get(workspaceId);
+ const isOpen = localState?.rightSidebarOpen ?? false;
+
+ const toggle = () => {
+ collections.v2WorkspaceLocalState.update(workspaceId, (draft) => {
+ draft.rightSidebarOpen = !draft.rightSidebarOpen;
+ });
+ };
+
+ const getToggleIcon = (isHovering: boolean) => {
+ if (!isOpen) {
+ return isHovering ? (
+
+ ) : (
+
+ );
+ }
+ return isHovering ? (
+
+ ) : (
+
+ );
+ };
+
+ return (
+
+
+
+ {getToggleIcon(false)}
+
+ {getToggleIcon(true)}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/RightSidebarToggle/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/RightSidebarToggle/index.ts
new file mode 100644
index 00000000000..8990c30415b
--- /dev/null
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/RightSidebarToggle/index.ts
@@ -0,0 +1 @@
+export { RightSidebarToggle } from "./RightSidebarToggle";
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/V2OpenInMenuButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/V2OpenInMenuButton.tsx
index a0b11900b00..2e1091ec1f8 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/V2OpenInMenuButton.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/V2OpenInMenuButton.tsx
@@ -1,37 +1,201 @@
-import { useQuery } from "@tanstack/react-query";
-import { getHostServiceClientByUrl } from "renderer/lib/host-service-client";
-import { OpenInMenuButton } from "../OpenInMenuButton";
+import type { ExternalApp } from "@superset/local-db";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuShortcut,
+ DropdownMenuTrigger,
+} from "@superset/ui/dropdown-menu";
+import { toast } from "@superset/ui/sonner";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip";
+import { cn } from "@superset/ui/utils";
+import { useCallback, useMemo, useState } from "react";
+import { HiChevronDown } from "react-icons/hi2";
+import {
+ getAppOption,
+ OpenInExternalDropdownItems,
+} from "renderer/components/OpenInExternalDropdown";
+import { HotkeyLabel, useHotkey, useHotkeyDisplay } from "renderer/hotkeys";
+import { electronTrpc } from "renderer/lib/electron-trpc";
+import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
+import { useThemeStore } from "renderer/stores";
interface V2OpenInMenuButtonProps {
+ worktreePath: string;
branch: string;
- hostUrl: string;
- projectId: string;
workspaceId: string;
}
export function V2OpenInMenuButton({
+ worktreePath,
branch,
- hostUrl,
- projectId,
workspaceId,
}: V2OpenInMenuButtonProps) {
- const workspaceQuery = useQuery({
- queryKey: ["v2-open-in-workspace", hostUrl, workspaceId],
- queryFn: () =>
- getHostServiceClientByUrl(hostUrl).workspace.get.query({
- id: workspaceId,
- }),
+ const collections = useCollections();
+ const activeTheme = useThemeStore((state) => state.activeTheme);
+
+ const localState = collections.v2WorkspaceLocalState.get(workspaceId);
+ const [defaultApp, setDefaultApp] = useState(
+ (localState?.defaultOpenInApp as ExternalApp) ?? "finder",
+ );
+
+ const handleDefaultAppChange = useCallback(
+ (app: ExternalApp) => {
+ setDefaultApp(app);
+ collections.v2WorkspaceLocalState.update(workspaceId, (draft) => {
+ draft.defaultOpenInApp = app;
+ });
+ },
+ [collections, workspaceId],
+ );
+
+ const openInApp = electronTrpc.external.openInApp.useMutation({
+ onSuccess: (_data, variables) => {
+ handleDefaultAppChange(variables.app);
+ },
+ onError: (error) => toast.error(`Failed to open: ${error.message}`),
+ });
+ const copyPath = electronTrpc.external.copyPath.useMutation({
+ onSuccess: () => toast.success("Path copied to clipboard"),
+ onError: (error) => toast.error(`Failed to copy path: ${error.message}`),
});
- if (!workspaceQuery.data?.worktreePath) {
- return null;
- }
+ const currentApp = useMemo(
+ () => getAppOption(defaultApp) ?? null,
+ [defaultApp],
+ );
+ const openInDisplay = useHotkeyDisplay("OPEN_IN_APP");
+ const copyPathDisplay = useHotkeyDisplay("COPY_PATH");
+ const showOpenInShortcut = openInDisplay.text !== "Unassigned";
+ const showCopyPathShortcut = copyPathDisplay.text !== "Unassigned";
+ const isLoading = openInApp.isPending || copyPath.isPending;
+ const isDark = activeTheme?.type === "dark";
+
+ const handleOpenInEditor = useCallback(() => {
+ if (openInApp.isPending || copyPath.isPending) return;
+ openInApp.mutate({ path: worktreePath, app: defaultApp });
+ }, [worktreePath, defaultApp, openInApp, copyPath.isPending]);
+
+ const handleOpenInOtherApp = useCallback(
+ (appId: ExternalApp) => {
+ if (openInApp.isPending || copyPath.isPending) return;
+ openInApp.mutate({ path: worktreePath, app: appId });
+ },
+ [worktreePath, openInApp, copyPath.isPending],
+ );
+
+ const handleCopyPath = useCallback(() => {
+ if (openInApp.isPending || copyPath.isPending) return;
+ copyPath.mutate(worktreePath);
+ }, [worktreePath, copyPath, openInApp.isPending]);
+
+ useHotkey("OPEN_IN_APP", handleOpenInEditor);
return (
-
+
+
+
+
+ {currentApp && (
+
+ )}
+ {branch && (
+
+ /{branch}
+
+ )}
+
+ Open
+
+
+
+
+ {currentApp ? (
+
+ ) : (
+ "Select an editor from the dropdown"
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {
+ if (
+ appId !== defaultApp ||
+ !showOpenInShortcut ||
+ group === "jetbrains"
+ ) {
+ return null;
+ }
+ return (
+
+ {openInDisplay.text}
+
+ );
+ }}
+ copyPathTrailing={
+ showCopyPathShortcut ? (
+
+ {copyPathDisplay.text}
+
+ ) : null
+ }
+ subContentClassName="w-40"
+ appContentClassName="gap-0"
+ appIconClassName="size-4 object-contain mr-2"
+ subTriggerIconClassName="size-4 object-contain mr-2"
+ subTriggerContentClassName="flex items-center gap-0"
+ copyPathContentClassName="gap-0"
+ copyPathIconClassName="mr-2"
+ />
+
+
+
);
}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx
index ed03ea90592..ccfb65d3a1f 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx
+++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx
@@ -1,6 +1,8 @@
import { and, eq } from "@tanstack/db";
import { useLiveQuery } from "@tanstack/react-db";
+import { useQuery } from "@tanstack/react-query";
import { electronTrpc } from "renderer/lib/electron-trpc";
+import { getHostServiceClientByUrl } from "renderer/lib/host-service-client";
import { useCollections } from "../../../../../providers/CollectionsProvider";
import { useHostService } from "../../../../../providers/HostServiceProvider/HostServiceProvider";
import { V2OpenInMenuButton } from "../V2OpenInMenuButton";
@@ -42,15 +44,27 @@ export function V2WorkspaceOpenInButton({
const isLocalWorkspace =
Boolean(workspace) && workspace.deviceId === currentDevice?.id;
+ const workspaceQuery = useQuery({
+ queryKey: ["v2-open-in-workspace", hostUrl, workspaceId],
+ queryFn: () =>
+ getHostServiceClientByUrl(hostUrl as string).workspace.get.query({
+ id: workspaceId,
+ }),
+ enabled: !!workspace && !!hostUrl && isLocalWorkspace,
+ });
+
if (!workspace || !hostUrl || !isLocalWorkspace) {
return null;
}
+ if (!workspaceQuery.data?.worktreePath) {
+ return null;
+ }
+
return (
);
diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts
index f42d80b5597..a34167897b5 100644
--- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts
+++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts
@@ -39,6 +39,7 @@ export const workspaceLocalStateSchema = z.object({
}),
paneLayout: paneWorkspaceStateSchema,
rightSidebarOpen: z.boolean().default(false),
+ defaultOpenInApp: z.string().nullable().default(null),
});
export const dashboardSidebarSectionSchema = z.object({