From 1766da9936171854f728efaa46681dec165c4492 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Sun, 15 Mar 2026 22:59:12 -0700 Subject: [PATCH 01/14] WIP --- .../useV2CreateWorkspace.ts | 8 +- .../src/renderer/lib/v2-sidebar-state.ts | 257 ++++++++++++++++++ .../_dashboard/v2-workspace/layout.tsx | 11 + .../_dashboard/v2-workspaces/page.tsx | 32 +++ .../CollectionsProvider/collections.ts | 69 ++++- .../CollectionsProvider/v2-sidebar-local.ts | 33 +++ .../V2WorkspaceSidebar/V2WorkspaceSidebar.tsx | 9 +- .../V2ProjectSection/V2ProjectContextMenu.tsx | 20 +- .../V2ProjectSection/V2ProjectSection.tsx | 137 ++++++++-- .../V2SidebarHeader/V2SidebarHeader.tsx | 72 ++++- .../V2SidebarSection/V2SidebarSection.tsx | 144 ++++++++++ .../components/V2SidebarSection/index.ts | 1 + .../V2WorkspaceContextMenu.tsx | 49 +++- .../V2WorkspaceListItem.tsx | 27 ++ .../hooks/useV2ProjectDnD/useV2ProjectDnD.ts | 12 +- .../useV2SidebarData/useV2SidebarData.ts | 195 +++++++++---- .../useV2WorkspaceDnD/useV2WorkspaceDnD.ts | 32 ++- .../useV2WorkspaceShortcuts.ts | 5 +- .../components/V2WorkspaceSidebar/types.ts | 11 + 19 files changed, 993 insertions(+), 131 deletions(-) create mode 100644 apps/desktop/src/renderer/lib/v2-sidebar-state.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/v2-sidebar-local.ts create mode 100644 apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2SidebarSection/V2SidebarSection.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2SidebarSection/index.ts diff --git a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/hooks/useV2CreateWorkspace/useV2CreateWorkspace.ts b/apps/desktop/src/renderer/components/V2NewWorkspaceModal/hooks/useV2CreateWorkspace/useV2CreateWorkspace.ts index 72292534fa0..9db5eaba5ca 100644 --- a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/hooks/useV2CreateWorkspace/useV2CreateWorkspace.ts +++ b/apps/desktop/src/renderer/components/V2NewWorkspaceModal/hooks/useV2CreateWorkspace/useV2CreateWorkspace.ts @@ -3,6 +3,7 @@ import { getHostServiceClientByUrl, type HostServiceClient, } from "renderer/lib/host-service-client"; +import { useV2SidebarState } from "renderer/lib/v2-sidebar-state"; import { resolveCreateWorkspaceHostUrl, type WorkspaceHostTarget, @@ -19,6 +20,7 @@ interface V2CreateWorkspaceInput { export function useV2CreateWorkspace() { const [isPending, setIsPending] = useState(false); const { localHostService } = useWorkspaceHostOptions(); + const { ensureWorkspaceInSidebar } = useV2SidebarState(); const createWorkspace = useCallback( async (input: V2CreateWorkspaceInput) => { @@ -37,16 +39,18 @@ export function useV2CreateWorkspace() { ? localHostService.client : getHostServiceClientByUrl(hostUrl); - return await client.workspace.create.mutate({ + const workspace = await client.workspace.create.mutate({ projectId: input.projectId, name: input.name, branch: input.branch, }); + ensureWorkspaceInSidebar(workspace.id, input.projectId); + return workspace; } finally { setIsPending(false); } }, - [localHostService], + [ensureWorkspaceInSidebar, localHostService], ); return { createWorkspace, isPending }; diff --git a/apps/desktop/src/renderer/lib/v2-sidebar-state.ts b/apps/desktop/src/renderer/lib/v2-sidebar-state.ts new file mode 100644 index 00000000000..49ef42cc4af --- /dev/null +++ b/apps/desktop/src/renderer/lib/v2-sidebar-state.ts @@ -0,0 +1,257 @@ +import { useCallback } from "react"; +import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import type { AppCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider/collections"; + +function getNextTabOrder(items: Array<{ tabOrder: number }>): number { + const maxTabOrder = items.reduce( + (maxValue, item) => Math.max(maxValue, item.tabOrder), + 0, + ); + return maxTabOrder + 1; +} + +function ensureSidebarProjectRecord( + collections: Pick, + projectId: string, +): void { + if (collections.v2SidebarProjects.get(projectId)) { + return; + } + + collections.v2SidebarProjects.insert({ + projectId, + createdAt: new Date(), + tabOrder: getNextTabOrder([ + ...collections.v2SidebarProjects.state.values(), + ]), + isCollapsed: false, + }); +} + +function ensureSidebarWorkspaceRecord( + collections: Pick< + AppCollections, + "v2SidebarSections" | "v2SidebarWorkspaces" + >, + workspaceId: string, + projectId: string, +): void { + if (collections.v2SidebarWorkspaces.get(workspaceId)) { + return; + } + + const topLevelOrders = [ + ...Array.from(collections.v2SidebarWorkspaces.state.values()).filter( + (item) => item.projectId === projectId && item.sectionId === null, + ), + ...Array.from(collections.v2SidebarSections.state.values()).filter( + (item) => item.projectId === projectId, + ), + ]; + + collections.v2SidebarWorkspaces.insert({ + workspaceId, + projectId, + createdAt: new Date(), + tabOrder: getNextTabOrder(topLevelOrders), + sectionId: null, + }); +} + +export function useV2SidebarState() { + const collections = useCollections(); + + const ensureProjectInSidebar = useCallback( + (projectId: string) => { + ensureSidebarProjectRecord(collections, projectId); + }, + [collections], + ); + + const ensureWorkspaceInSidebar = useCallback( + (workspaceId: string, projectId: string) => { + ensureSidebarProjectRecord(collections, projectId); + ensureSidebarWorkspaceRecord(collections, workspaceId, projectId); + }, + [collections], + ); + + const toggleProjectCollapsed = useCallback( + (projectId: string) => { + const existing = collections.v2SidebarProjects.get(projectId); + if (!existing) return; + collections.v2SidebarProjects.update(projectId, (draft) => { + draft.isCollapsed = !draft.isCollapsed; + }); + }, + [collections], + ); + + const reorderProjects = useCallback( + (projectIds: string[]) => { + projectIds.forEach((projectId, index) => { + if (!collections.v2SidebarProjects.get(projectId)) return; + collections.v2SidebarProjects.update(projectId, (draft) => { + draft.tabOrder = index + 1; + }); + }); + }, + [collections], + ); + + const reorderWorkspaces = useCallback( + (workspaceIds: string[]) => { + workspaceIds.forEach((workspaceId, index) => { + if (!collections.v2SidebarWorkspaces.get(workspaceId)) return; + collections.v2SidebarWorkspaces.update(workspaceId, (draft) => { + draft.tabOrder = index + 1; + }); + }); + }, + [collections], + ); + + const createSection = useCallback( + (projectId: string, name = "New Section") => { + ensureSidebarProjectRecord(collections, projectId); + + const sectionId = crypto.randomUUID(); + const sectionOrders = Array.from( + collections.v2SidebarSections.state.values(), + ).filter((item) => item.projectId === projectId); + + collections.v2SidebarSections.insert({ + sectionId, + projectId, + name, + createdAt: new Date(), + tabOrder: getNextTabOrder(sectionOrders), + isCollapsed: false, + }); + + return sectionId; + }, + [collections], + ); + + const toggleSectionCollapsed = useCallback( + (sectionId: string) => { + if (!collections.v2SidebarSections.get(sectionId)) return; + collections.v2SidebarSections.update(sectionId, (draft) => { + draft.isCollapsed = !draft.isCollapsed; + }); + }, + [collections], + ); + + const renameSection = useCallback( + (sectionId: string, name: string) => { + if (!collections.v2SidebarSections.get(sectionId)) return; + collections.v2SidebarSections.update(sectionId, (draft) => { + draft.name = name.trim(); + }); + }, + [collections], + ); + + const moveWorkspaceToSection = useCallback( + (workspaceId: string, projectId: string, sectionId: string | null) => { + const existing = collections.v2SidebarWorkspaces.get(workspaceId); + if (!existing) return; + + const siblingRows = Array.from( + collections.v2SidebarWorkspaces.state.values(), + ).filter( + (item) => + item.projectId === projectId && + item.workspaceId !== workspaceId && + item.sectionId === sectionId, + ); + + collections.v2SidebarWorkspaces.update(workspaceId, (draft) => { + draft.sectionId = sectionId; + draft.tabOrder = getNextTabOrder(siblingRows); + }); + }, + [collections], + ); + + const deleteSection = useCallback( + (sectionId: string) => { + const section = collections.v2SidebarSections.get(sectionId); + if (!section) return; + + const siblingTopLevelRows = Array.from( + collections.v2SidebarWorkspaces.state.values(), + ).filter( + (item) => + item.projectId === section.projectId && item.sectionId === null, + ); + + let nextOrder = getNextTabOrder(siblingTopLevelRows); + for (const workspace of collections.v2SidebarWorkspaces.state.values()) { + if (workspace.sectionId !== sectionId) continue; + collections.v2SidebarWorkspaces.update( + workspace.workspaceId, + (draft) => { + draft.sectionId = null; + draft.tabOrder = nextOrder; + }, + ); + nextOrder += 1; + } + + collections.v2SidebarSections.delete(sectionId); + }, + [collections], + ); + + const removeWorkspaceFromSidebar = useCallback( + (workspaceId: string) => { + if (!collections.v2SidebarWorkspaces.get(workspaceId)) return; + collections.v2SidebarWorkspaces.delete(workspaceId); + }, + [collections], + ); + + const removeProjectFromSidebar = useCallback( + (projectId: string) => { + const workspaceIds = Array.from( + collections.v2SidebarWorkspaces.state.values(), + ) + .filter((item) => item.projectId === projectId) + .map((item) => item.workspaceId); + const sectionIds = Array.from( + collections.v2SidebarSections.state.values(), + ) + .filter((item) => item.projectId === projectId) + .map((item) => item.sectionId); + + if (workspaceIds.length > 0) { + collections.v2SidebarWorkspaces.delete(workspaceIds); + } + if (sectionIds.length > 0) { + collections.v2SidebarSections.delete(sectionIds); + } + if (collections.v2SidebarProjects.get(projectId)) { + collections.v2SidebarProjects.delete(projectId); + } + }, + [collections], + ); + + return { + createSection, + deleteSection, + ensureProjectInSidebar, + ensureWorkspaceInSidebar, + moveWorkspaceToSection, + removeProjectFromSidebar, + removeWorkspaceFromSidebar, + reorderProjects, + reorderWorkspaces, + renameSection, + toggleProjectCollapsed, + toggleSectionCollapsed, + }; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx index 43c75b474f7..3ddc878bd77 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/layout.tsx @@ -1,7 +1,9 @@ import { and, eq } from "@tanstack/db"; import { useLiveQuery } from "@tanstack/react-db"; import { createFileRoute, Outlet, useMatchRoute } from "@tanstack/react-router"; +import { useEffect, useRef } from "react"; import { electronTrpc } from "renderer/lib/electron-trpc"; +import { useV2SidebarState } from "renderer/lib/v2-sidebar-state"; import { getWorkspaceHostUrlForWorkspace } from "renderer/lib/v2-workspace-host"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { useHostService } from "renderer/routes/_authenticated/providers/HostServiceProvider"; @@ -22,6 +24,7 @@ function V2WorkspaceLayout() { workspaceMatch !== false ? workspaceMatch.workspaceId : null; const collections = useCollections(); const { services } = useHostService(); + const { ensureWorkspaceInSidebar } = useV2SidebarState(); const { data: deviceInfo, isPending: isDeviceInfoPending } = electronTrpc.auth.getDeviceInfo.useQuery(); @@ -56,6 +59,14 @@ function V2WorkspaceLayout() { : workspace.deviceId === currentDevice?.id ? localHostUrl : getWorkspaceHostUrlForWorkspace(workspace.id); + const lastEnsuredWorkspaceIdRef = useRef(null); + + useEffect(() => { + if (!workspace || lastEnsuredWorkspaceIdRef.current === workspace.id) + return; + lastEnsuredWorkspaceIdRef.current = workspace.id; + ensureWorkspaceInSidebar(workspace.id, workspace.projectId); + }, [ensureWorkspaceInSidebar, workspace]); if (!workspaceId || !workspace) { return ; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx new file mode 100644 index 00000000000..92f04c612d4 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/page.tsx @@ -0,0 +1,32 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute( + "/_authenticated/_dashboard/v2-workspaces/", +)({ + component: V2WorkspacesPage, +}); + +function V2WorkspacesPage() { + return ( +
+
+
+

Workspaces

+

+ This page will become the browse surface for all accessible V2 + workspaces, with sidebar workspaces prioritized first. +

+
+ +
+

WIP

+

+ Next up is splitting local sidebar workspaces from the full set of + accessible shared workspaces and giving this page proper search, + filtering, and recents. +

+
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts index 8f9092cc0bf..b537adaeaca 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts @@ -24,16 +24,28 @@ import type { } from "@superset/db/schema"; import type { AppRouter } from "@superset/trpc"; import { electricCollectionOptions } from "@tanstack/electric-db-collection"; -import type { Collection } from "@tanstack/react-db"; +import type { + Collection, + LocalStorageCollectionUtils, +} from "@tanstack/react-db"; import { createCollection, localOnlyCollectionOptions, + localStorageCollectionOptions, } from "@tanstack/react-db"; import { createTRPCProxyClient, httpBatchLink } from "@trpc/client"; import { env } from "renderer/env.renderer"; import { getAuthToken, getJwt } from "renderer/lib/auth-client"; import superjson from "superjson"; import { z } from "zod"; +import { + type V2SidebarProjectRow, + type V2SidebarSectionRow, + type V2SidebarWorkspaceRow, + v2SidebarProjectSchema, + v2SidebarSectionSchema, + v2SidebarWorkspaceSchema, +} from "./v2-sidebar-local"; const columnMapper = snakeCamelMapper(); @@ -54,7 +66,7 @@ type IntegrationConnectionDisplay = Omit< "accessToken" | "refreshToken" >; -interface OrgCollections { +export interface OrgCollections { tasks: Collection; taskStatuses: Collection; projects: Collection; @@ -76,6 +88,27 @@ interface OrgCollections { sessionHosts: Collection; githubRepositories: Collection; githubPullRequests: Collection; + v2SidebarProjects: Collection< + V2SidebarProjectRow, + string, + LocalStorageCollectionUtils, + typeof v2SidebarProjectSchema, + z.input + >; + v2SidebarWorkspaces: Collection< + V2SidebarWorkspaceRow, + string, + LocalStorageCollectionUtils, + typeof v2SidebarWorkspaceSchema, + z.input + >; + v2SidebarSections: Collection< + V2SidebarSectionRow, + string, + LocalStorageCollectionUtils, + typeof v2SidebarSectionSchema, + z.input + >; } // Per-org collections cache @@ -526,6 +559,33 @@ function createOrgCollections( }), ); + const v2SidebarProjects = createCollection( + localStorageCollectionOptions({ + id: `v2_sidebar_projects-${organizationId}`, + storageKey: `v2-sidebar-projects-${organizationId}`, + schema: v2SidebarProjectSchema, + getKey: (item) => item.projectId, + }), + ); + + const v2SidebarWorkspaces = createCollection( + localStorageCollectionOptions({ + id: `v2_sidebar_workspaces-${organizationId}`, + storageKey: `v2-sidebar-workspaces-${organizationId}`, + schema: v2SidebarWorkspaceSchema, + getKey: (item) => item.workspaceId, + }), + ); + + const v2SidebarSections = createCollection( + localStorageCollectionOptions({ + id: `v2_sidebar_sections-${organizationId}`, + storageKey: `v2-sidebar-sections-${organizationId}`, + schema: v2SidebarSectionSchema, + getKey: (item) => item.sectionId, + }), + ); + return { tasks, taskStatuses, @@ -548,6 +608,9 @@ function createOrgCollections( sessionHosts, githubRepositories, githubPullRequests, + v2SidebarProjects, + v2SidebarWorkspaces, + v2SidebarSections, }; } @@ -607,3 +670,5 @@ export function getCollections(organizationId: string, enableV2Cloud = false) { organizations: organizationsCollection, }; } + +export type AppCollections = ReturnType; diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/v2-sidebar-local.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/v2-sidebar-local.ts new file mode 100644 index 00000000000..09767c6553e --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/v2-sidebar-local.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; + +const persistedDateSchema = z + .union([z.string(), z.date()]) + .transform((value) => (typeof value === "string" ? new Date(value) : value)); + +export const v2SidebarProjectSchema = z.object({ + projectId: z.string().uuid(), + createdAt: persistedDateSchema, + isCollapsed: z.boolean().default(false), + tabOrder: z.number().int().default(0), +}); + +export const v2SidebarWorkspaceSchema = z.object({ + workspaceId: z.string().uuid(), + projectId: z.string().uuid(), + createdAt: persistedDateSchema, + tabOrder: z.number().int().default(0), + sectionId: z.string().uuid().nullable().default(null), +}); + +export const v2SidebarSectionSchema = z.object({ + sectionId: z.string().uuid(), + projectId: z.string().uuid(), + name: z.string().trim().min(1), + createdAt: persistedDateSchema, + tabOrder: z.number().int().default(0), + isCollapsed: z.boolean().default(false), +}); + +export type V2SidebarProjectRow = z.infer; +export type V2SidebarWorkspaceRow = z.infer; +export type V2SidebarSectionRow = z.infer; diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/V2WorkspaceSidebar.tsx b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/V2WorkspaceSidebar.tsx index b6ec414e433..4b9d7bb4e33 100644 --- a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/V2WorkspaceSidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/V2WorkspaceSidebar.tsx @@ -22,7 +22,13 @@ export function V2WorkspaceSidebar({ groups.reduce<{ indices: number[]; cumulative: number }>( (acc, group) => ({ indices: [...acc.indices, acc.cumulative], - cumulative: acc.cumulative + group.workspaces.length, + cumulative: + acc.cumulative + + group.workspaces.length + + group.sections.reduce( + (sum, section) => sum + section.workspaces.length, + 0, + ), }), { indices: [], cumulative: 0 }, ).indices, @@ -43,6 +49,7 @@ export function V2WorkspaceSidebar({ isCollapsed={project.isCollapsed} isSidebarCollapsed={isCollapsed} workspaces={project.workspaces} + sections={project.sections} shortcutBaseIndex={projectShortcutIndices[index]} index={index} projectIds={projectIds} diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2ProjectSection/V2ProjectContextMenu.tsx b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2ProjectSection/V2ProjectContextMenu.tsx index 1e5d235d81a..bd4a869cf55 100644 --- a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2ProjectSection/V2ProjectContextMenu.tsx +++ b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2ProjectSection/V2ProjectContextMenu.tsx @@ -6,10 +6,18 @@ import { ContextMenuTrigger, } from "@superset/ui/context-menu"; import { toast } from "@superset/ui/sonner"; -import { LuCopy, LuPencil, LuPlus, LuTrash2 } from "react-icons/lu"; +import { + LuCopy, + LuFolderPlus, + LuPencil, + LuPlus, + LuTrash2, +} from "react-icons/lu"; interface V2ProjectContextMenuProps { id: string; + onCreateSection: () => void; + onRemoveFromSidebar: () => void; onRename: () => void; onDelete: () => void; onNewWorkspace: () => void; @@ -18,6 +26,8 @@ interface V2ProjectContextMenuProps { export function V2ProjectContextMenu({ id, + onCreateSection, + onRemoveFromSidebar, onRename, onDelete, onNewWorkspace, @@ -40,11 +50,19 @@ export function V2ProjectContextMenu({ New Workspace + + + New Section + Copy ID + + + Remove from Sidebar + void; } +function countProjectWorkspaces( + workspaces: V2SidebarWorkspace[], + sections: V2SidebarSection[], +): number { + return ( + workspaces.length + + sections.reduce((sum, section) => sum + section.workspaces.length, 0) + ); +} + export function V2ProjectSection({ projectId, projectName, @@ -35,6 +48,7 @@ export function V2ProjectSection({ isCollapsed, isSidebarCollapsed = false, workspaces, + sections, shortcutBaseIndex, index, projectIds, @@ -43,6 +57,13 @@ export function V2ProjectSection({ const openModal = useOpenNewWorkspaceModal(); const navigate = useNavigate(); const matchRoute = useMatchRoute(); + const { + createSection, + deleteSection, + removeProjectFromSidebar, + renameSection, + toggleSectionCollapsed, + } = useV2SidebarState(); const [isRenaming, setIsRenaming] = useState(false); const [renameValue, setRenameValue] = useState(projectName); @@ -55,7 +76,20 @@ export function V2ProjectSection({ projectIds, }); - const workspaceIds = useMemo(() => workspaces.map((w) => w.id), [workspaces]); + const topLevelWorkspaceIds = useMemo( + () => workspaces.map((workspace) => workspace.id), + [workspaces], + ); + + const allSections = useMemo( + () => sections.map((section) => ({ id: section.id, name: section.name })), + [sections], + ); + + const flattenedCollapsedWorkspaces = useMemo( + () => [...workspaces, ...sections.flatMap((section) => section.workspaces)], + [sections, workspaces], + ); const startRename = () => { setRenameValue(projectName); @@ -88,14 +122,18 @@ export function V2ProjectSection({ setIsDeleting(true); try { await apiTrpcClient.v2Project.delete.mutate({ id: projectId }); + removeProjectFromSidebar(projectId); setIsDeleteDialogOpen(false); toast.success("Project deleted"); - const isInProject = workspaces.some( - (w) => + const isInProject = [ + ...workspaces, + ...sections.flatMap((s) => s.workspaces), + ].some( + (workspace) => !!matchRoute({ to: "/v2-workspace/$workspaceId", - params: { workspaceId: w.id }, + params: { workspaceId: workspace.id }, fuzzy: true, }), ); @@ -115,11 +153,19 @@ export function V2ProjectSection({ openModal(projectId); }; + const handleNewSection = () => { + createSection(projectId); + }; + + const totalWorkspaceCount = countProjectWorkspaces(workspaces, sections); + if (isSidebarCollapsed) { return ( <> removeProjectFromSidebar(projectId)} onRename={startRename} onDelete={() => setIsDeleteDialogOpen(true)} onNewWorkspace={handleNewWorkspace} @@ -152,8 +198,8 @@ export function V2ProjectSection({ {projectName} - {workspaces.length} workspace - {workspaces.length !== 1 ? "s" : ""} + {totalWorkspaceCount} workspace + {totalWorkspaceCount !== 1 ? "s" : ""} @@ -168,19 +214,24 @@ export function V2ProjectSection({ className="overflow-hidden w-full" >
- {workspaces.map((workspace, i) => ( - - ))} + {flattenedCollapsedWorkspaces.map( + (workspace, itemIndex) => ( + item.id, + )} + sections={allSections} + shortcutIndex={shortcutBaseIndex + itemIndex} + isCollapsed + /> + ), + )}
)} @@ -213,6 +264,8 @@ export function V2ProjectSection({ > removeProjectFromSidebar(projectId)} onRename={startRename} onDelete={() => setIsDeleteDialogOpen(true)} onNewWorkspace={handleNewWorkspace} @@ -250,7 +303,7 @@ export function V2ProjectSection({ /> {projectName} - ({workspaces.length}) + ({totalWorkspaceCount}) )} @@ -259,11 +312,11 @@ export function V2ProjectSection({ + + Workspaces + + @@ -41,17 +70,36 @@ export function V2SidebarHeader({ isCollapsed = false }: V2SidebarHeaderProps) { } return ( -
+
+ + diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2SidebarSection/V2SidebarSection.tsx b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2SidebarSection/V2SidebarSection.tsx new file mode 100644 index 00000000000..15bbb0b92a8 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2SidebarSection/V2SidebarSection.tsx @@ -0,0 +1,144 @@ +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@superset/ui/context-menu"; +import { cn } from "@superset/ui/utils"; +import { AnimatePresence, motion } from "framer-motion"; +import { useState } from "react"; +import { HiChevronRight } from "react-icons/hi2"; +import { LuFolderOpen, LuPencil, LuTrash2 } from "react-icons/lu"; +import { RenameInput } from "renderer/screens/main/components/WorkspaceSidebar/RenameInput"; +import type { V2SidebarSection as V2SidebarSectionRecord } from "../../types"; +import { V2WorkspaceListItem } from "../V2WorkspaceListItem"; + +interface V2SidebarSectionProps { + projectId: string; + section: V2SidebarSectionRecord; + shortcutBaseIndex: number; + allSections: Array<{ id: string; name: string }>; + onDelete: (sectionId: string) => void; + onRename: (sectionId: string, name: string) => void; + onToggleCollapse: (sectionId: string) => void; +} + +export function V2SidebarSection({ + projectId, + section, + shortcutBaseIndex, + allSections, + onDelete, + onRename, + onToggleCollapse, +}: V2SidebarSectionProps) { + const [isRenaming, setIsRenaming] = useState(false); + const [renameValue, setRenameValue] = useState(section.name); + + const workspaceIds = section.workspaces.map((workspace) => workspace.id); + + const handleSubmitRename = () => { + const trimmed = renameValue.trim(); + if (trimmed) { + onRename(section.id, trimmed); + } + setIsRenaming(false); + }; + + const handleCancelRename = () => { + setRenameValue(section.name); + setIsRenaming(false); + }; + + return ( +
+ + +
+ {isRenaming ? ( + + ) : ( + + )} +
+
+ + setIsRenaming(true)}> + + Rename Section + + onToggleCollapse(section.id)}> + + {section.isCollapsed ? "Expand Section" : "Collapse Section"} + + onDelete(section.id)} + className="text-destructive focus:text-destructive" + > + + Delete Section + + +
+ + + {!section.isCollapsed && ( + +
+ {section.workspaces.map((workspace, index) => ( + + ))} +
+
+ )} +
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2SidebarSection/index.ts b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2SidebarSection/index.ts new file mode 100644 index 00000000000..d19862b39f7 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2SidebarSection/index.ts @@ -0,0 +1 @@ +export { V2SidebarSection } from "./V2SidebarSection"; diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2WorkspaceListItem/V2WorkspaceContextMenu.tsx b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2WorkspaceListItem/V2WorkspaceContextMenu.tsx index 4fbd0e69005..19b6749b168 100644 --- a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2WorkspaceListItem/V2WorkspaceContextMenu.tsx +++ b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2WorkspaceListItem/V2WorkspaceContextMenu.tsx @@ -3,13 +3,27 @@ import { ContextMenuContent, ContextMenuItem, ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, ContextMenuTrigger, } from "@superset/ui/context-menu"; import { toast } from "@superset/ui/sonner"; -import { LuCopy, LuPencil, LuTrash2 } from "react-icons/lu"; +import { + LuArrowRightLeft, + LuCopy, + LuFolderPlus, + LuMinus, + LuPencil, + LuTrash2, +} from "react-icons/lu"; interface V2WorkspaceContextMenuProps { id: string; + sections: { id: string; name: string }[]; + onCreateSection: () => void; + onMoveToSection: (sectionId: string | null) => void; + onRemoveFromSidebar: () => void; onRename: () => void; onDelete: () => void; children: React.ReactNode; @@ -17,6 +31,10 @@ interface V2WorkspaceContextMenuProps { export function V2WorkspaceContextMenu({ id, + sections, + onCreateSection, + onMoveToSection, + onRemoveFromSidebar, onRename, onDelete, children, @@ -39,6 +57,35 @@ export function V2WorkspaceContextMenu({ Copy ID + + + + Move to Section + + + + + New Section + + onMoveToSection(null)}> + + Ungrouped + + {sections.map((section) => ( + onMoveToSection(section.id)} + > + {section.name} + + ))} + + + + + Remove from Sidebar + + { + const newSectionId = createSection(projectId); + moveWorkspaceToSection(id, projectId, newSectionId); + }} + onMoveToSection={(targetSectionId) => + moveWorkspaceToSection(id, projectId, targetSectionId) + } + onRemoveFromSidebar={() => removeWorkspaceFromSidebar(id)} onRename={startRename} onDelete={() => setIsDeleteDialogOpen(true)} > @@ -166,6 +184,15 @@ export function V2WorkspaceListItem({ <> { + const newSectionId = createSection(projectId); + moveWorkspaceToSection(id, projectId, newSectionId); + }} + onMoveToSection={(targetSectionId) => + moveWorkspaceToSection(id, projectId, targetSectionId) + } + onRemoveFromSidebar={() => removeWorkspaceFromSidebar(id)} onRename={startRename} onDelete={() => setIsDeleteDialogOpen(true)} > diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2ProjectDnD/useV2ProjectDnD.ts b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2ProjectDnD/useV2ProjectDnD.ts index c17cae40520..c68d62ca8d6 100644 --- a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2ProjectDnD/useV2ProjectDnD.ts +++ b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2ProjectDnD/useV2ProjectDnD.ts @@ -1,6 +1,6 @@ import { useCallback } from "react"; import { useDrag, useDrop } from "react-dnd"; -import { useV2ProjectLocalMetaStore } from "renderer/stores/v2-project-local-meta"; +import { useV2SidebarState } from "renderer/lib/v2-sidebar-state"; const V2_PROJECT_DND_TYPE = "V2_PROJECT"; @@ -21,17 +21,13 @@ export function useV2ProjectDnD({ index, projectIds, }: UseV2ProjectDnDOptions) { - const setProjectTabOrder = useV2ProjectLocalMetaStore( - (s) => s.setProjectTabOrder, - ); + const { reorderProjects } = useV2SidebarState(); const commitOrder = useCallback( (orderedIds: string[]) => { - for (let i = 0; i < orderedIds.length; i++) { - setProjectTabOrder(orderedIds[i], i + 1); - } + reorderProjects(orderedIds); }, - [setProjectTabOrder], + [reorderProjects], ); const [{ isDragging }, drag] = useDrag( diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2SidebarData/useV2SidebarData.ts b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2SidebarData/useV2SidebarData.ts index 617862ed4ac..58526341e0b 100644 --- a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2SidebarData/useV2SidebarData.ts +++ b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2SidebarData/useV2SidebarData.ts @@ -1,18 +1,30 @@ import { useLiveQuery } from "@tanstack/react-db"; -import { useCallback, useMemo } from "react"; +import { useMemo } from "react"; +import { useV2SidebarState } from "renderer/lib/v2-sidebar-state"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; -import { useV2ProjectLocalMetaStore } from "renderer/stores/v2-project-local-meta"; -import { useV2WorkspaceLocalMetaStore } from "renderer/stores/v2-workspace-local-meta"; -import type { V2SidebarProject, V2SidebarWorkspace } from "../../types"; - -const DEFAULT_META = { isCollapsed: false, tabOrder: 0 }; +import type { + V2SidebarProject, + V2SidebarSection, + V2SidebarWorkspace, +} from "../../types"; export function useV2SidebarData() { const collections = useCollections(); - const { toggleProjectCollapsed, projects: projectMetas } = - useV2ProjectLocalMetaStore(); - const workspaceSortVersion = useV2WorkspaceLocalMetaStore( - (s) => s.sortVersion, + const { toggleProjectCollapsed } = useV2SidebarState(); + + const { data: sidebarProjects = [] } = useLiveQuery( + (q) => q.from({ sidebarProjects: collections.v2SidebarProjects }), + [collections], + ); + + const { data: sidebarWorkspaces = [] } = useLiveQuery( + (q) => q.from({ sidebarWorkspaces: collections.v2SidebarWorkspaces }), + [collections], + ); + + const { data: sidebarSections = [] } = useLiveQuery( + (q) => q.from({ sidebarSections: collections.v2SidebarSections }), + [collections], ); const { data: projects = [] } = useLiveQuery( @@ -35,19 +47,48 @@ export function useV2SidebarData() { ); const groups = useMemo(() => { - // workspaceSortVersion triggers re-sort when workspace order changes via DnD - void workspaceSortVersion; const repoOwnerMap = new Map(); for (const repo of githubRepos) { repoOwnerMap.set(repo.id, repo.owner); } - const workspacesByProject = new Map(); + const cloudProjectsById = new Map( + projects.map((project) => [project.id, project]), + ); + const cloudWorkspacesById = new Map( + workspaces.map((workspace) => [workspace.id, workspace]), + ); - for (const workspace of workspaces) { - const projectWorkspaces = - workspacesByProject.get(workspace.projectId) ?? []; - projectWorkspaces.push({ + const localSectionsByProject = new Map(); + for (const section of sidebarSections) { + const sectionsForProject = + localSectionsByProject.get(section.projectId) ?? []; + sectionsForProject.push({ + id: section.sectionId, + projectId: section.projectId, + name: section.name, + createdAt: section.createdAt, + isCollapsed: section.isCollapsed, + tabOrder: section.tabOrder, + workspaces: [], + }); + localSectionsByProject.set(section.projectId, sectionsForProject); + } + + for (const sections of localSectionsByProject.values()) { + sections.sort( + (a, b) => a.tabOrder - b.tabOrder || a.name.localeCompare(b.name), + ); + } + + const workspaceRowsByProject = new Map(); + const workspaceRowsBySection = new Map(); + + for (const localWorkspace of sidebarWorkspaces) { + const workspace = cloudWorkspacesById.get(localWorkspace.workspaceId); + if (!workspace) continue; + + const sidebarWorkspace: V2SidebarWorkspace = { id: workspace.id, projectId: workspace.projectId, deviceId: workspace.deviceId, @@ -55,54 +96,90 @@ export function useV2SidebarData() { branch: workspace.branch, createdAt: workspace.createdAt, updatedAt: workspace.updatedAt, - }); - workspacesByProject.set(workspace.projectId, projectWorkspaces); + }; + + if (localWorkspace.sectionId) { + const sectionWorkspaces = + workspaceRowsBySection.get(localWorkspace.sectionId) ?? []; + sectionWorkspaces.push(sidebarWorkspace); + workspaceRowsBySection.set(localWorkspace.sectionId, sectionWorkspaces); + continue; + } + + const projectWorkspaces = + workspaceRowsByProject.get(localWorkspace.projectId) ?? []; + projectWorkspaces.push(sidebarWorkspace); + workspaceRowsByProject.set(localWorkspace.projectId, projectWorkspaces); + } + + const localWorkspaceOrder = new Map( + sidebarWorkspaces.map((workspace) => [ + workspace.workspaceId, + workspace.tabOrder, + ]), + ); + + for (const rows of workspaceRowsByProject.values()) { + rows.sort( + (a, b) => + (localWorkspaceOrder.get(a.id) ?? 0) - + (localWorkspaceOrder.get(b.id) ?? 0) || + a.name.localeCompare(b.name), + ); } - return [...projects] - .sort((a, b) => { - const metaA = projectMetas[a.id] ?? DEFAULT_META; - const metaB = projectMetas[b.id] ?? DEFAULT_META; - const orderDiff = metaA.tabOrder - metaB.tabOrder; - if (orderDiff !== 0) return orderDiff; - return a.name.localeCompare(b.name); - }) - .map((project) => { - const meta = projectMetas[project.id] ?? DEFAULT_META; - const repoId = project.githubRepositoryId ?? null; - return { - id: project.id, - name: project.name, - slug: project.slug, - githubRepositoryId: repoId, - githubOwner: repoId ? (repoOwnerMap.get(repoId) ?? null) : null, - createdAt: project.createdAt, - updatedAt: project.updatedAt, - isCollapsed: meta.isCollapsed, - workspaces: (workspacesByProject.get(project.id) ?? []).sort( - (a, b) => { - const getMeta = - useV2WorkspaceLocalMetaStore.getState().getWorkspaceMeta; - const orderA = getMeta(a.id).tabOrder; - const orderB = getMeta(b.id).tabOrder; - const orderDiff = orderA - orderB; - if (orderDiff !== 0) return orderDiff; - return a.name.localeCompare(b.name); - }, - ), - }; + for (const rows of workspaceRowsBySection.values()) { + rows.sort( + (a, b) => + (localWorkspaceOrder.get(a.id) ?? 0) - + (localWorkspaceOrder.get(b.id) ?? 0) || + a.name.localeCompare(b.name), + ); + } + + const resolvedProjects: V2SidebarProject[] = []; + + for (const localProject of [...sidebarProjects].sort( + (a, b) => a.tabOrder - b.tabOrder, + )) { + const project = cloudProjectsById.get(localProject.projectId); + if (!project) continue; + + const projectSections = ( + localSectionsByProject.get(project.id) ?? [] + ).map((section) => ({ + ...section, + workspaces: workspaceRowsBySection.get(section.id) ?? [], + })); + + const repoId = project.githubRepositoryId ?? null; + + resolvedProjects.push({ + id: project.id, + name: project.name, + slug: project.slug, + githubRepositoryId: repoId, + githubOwner: repoId ? (repoOwnerMap.get(repoId) ?? null) : null, + createdAt: project.createdAt, + updatedAt: project.updatedAt, + isCollapsed: localProject.isCollapsed, + workspaces: workspaceRowsByProject.get(project.id) ?? [], + sections: projectSections, }); - }, [projects, workspaces, projectMetas, githubRepos, workspaceSortVersion]); + } - const handleToggleProjectCollapsed = useCallback( - (projectId: string) => { - toggleProjectCollapsed(projectId); - }, - [toggleProjectCollapsed], - ); + return resolvedProjects; + }, [ + githubRepos, + projects, + sidebarProjects, + sidebarSections, + sidebarWorkspaces, + workspaces, + ]); return { groups, - toggleProjectCollapsed: handleToggleProjectCollapsed, + toggleProjectCollapsed, }; } diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceDnD/useV2WorkspaceDnD.ts b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceDnD/useV2WorkspaceDnD.ts index d2bf8db011a..06ae8e6c5bb 100644 --- a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceDnD/useV2WorkspaceDnD.ts +++ b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceDnD/useV2WorkspaceDnD.ts @@ -1,12 +1,13 @@ import { useCallback } from "react"; import { useDrag, useDrop } from "react-dnd"; -import { useV2WorkspaceLocalMetaStore } from "renderer/stores/v2-workspace-local-meta"; +import { useV2SidebarState } from "renderer/lib/v2-sidebar-state"; const V2_WORKSPACE_DND_TYPE = "V2_WORKSPACE"; interface DragItem { workspaceId: string; projectId: string; + sectionId: string | null; index: number; originalIndex: number; } @@ -14,6 +15,7 @@ interface DragItem { interface UseV2WorkspaceDnDOptions { workspaceId: string; projectId: string; + sectionId: string | null; index: number; workspaceIds: string[]; } @@ -21,24 +23,17 @@ interface UseV2WorkspaceDnDOptions { export function useV2WorkspaceDnD({ workspaceId, projectId, + sectionId, index, workspaceIds, }: UseV2WorkspaceDnDOptions) { - const setWorkspaceTabOrder = useV2WorkspaceLocalMetaStore( - (s) => s.setWorkspaceTabOrder, - ); - const bumpSortVersion = useV2WorkspaceLocalMetaStore( - (s) => s.bumpSortVersion, - ); + const { reorderWorkspaces } = useV2SidebarState(); const commitOrder = useCallback( (orderedIds: string[]) => { - for (let i = 0; i < orderedIds.length; i++) { - setWorkspaceTabOrder(orderedIds[i], i + 1); - } - bumpSortVersion(); + reorderWorkspaces(orderedIds); }, - [setWorkspaceTabOrder, bumpSortVersion], + [reorderWorkspaces], ); const [{ isDragging }, drag] = useDrag( @@ -47,6 +42,7 @@ export function useV2WorkspaceDnD({ item: (): DragItem => ({ workspaceId, projectId, + sectionId, index, originalIndex: index, }), @@ -61,18 +57,24 @@ export function useV2WorkspaceDnD({ commitOrder(ids); }, }), - [workspaceId, projectId, index, workspaceIds, commitOrder], + [workspaceId, projectId, sectionId, index, workspaceIds, commitOrder], ); const [, drop] = useDrop( { accept: V2_WORKSPACE_DND_TYPE, hover: (item: DragItem) => { - if (item.projectId !== projectId || item.index === index) return; + if ( + item.projectId !== projectId || + item.sectionId !== sectionId || + item.index === index + ) { + return; + } item.index = index; }, }, - [projectId, index], + [projectId, sectionId, index], ); return { isDragging, drag, drop }; diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceShortcuts/useV2WorkspaceShortcuts.ts b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceShortcuts/useV2WorkspaceShortcuts.ts index 0ef76b7e828..708df3ce62f 100644 --- a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceShortcuts/useV2WorkspaceShortcuts.ts +++ b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceShortcuts/useV2WorkspaceShortcuts.ts @@ -11,7 +11,10 @@ import type { V2SidebarProject } from "../../types"; export function useV2WorkspaceShortcuts(groups: V2SidebarProject[]) { const navigate = useNavigate(); - const allWorkspaces = groups.flatMap((group) => group.workspaces); + const allWorkspaces = groups.flatMap((group) => [ + ...group.workspaces, + ...group.sections.flatMap((section) => section.workspaces), + ]); const switchToWorkspace = useCallback( (index: number) => { diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/types.ts b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/types.ts index 9fa447c4709..3005237e7be 100644 --- a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/types.ts +++ b/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/types.ts @@ -8,6 +8,16 @@ export interface V2SidebarWorkspace { updatedAt: Date; } +export interface V2SidebarSection { + id: string; + projectId: string; + name: string; + createdAt: Date; + isCollapsed: boolean; + tabOrder: number; + workspaces: V2SidebarWorkspace[]; +} + export interface V2SidebarProject { id: string; name: string; @@ -18,4 +28,5 @@ export interface V2SidebarProject { updatedAt: Date; isCollapsed: boolean; workspaces: V2SidebarWorkspace[]; + sections: V2SidebarSection[]; } From 482976eaf88adc0f191adba5d228ed6f3b185122 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Sun, 15 Mar 2026 23:28:28 -0700 Subject: [PATCH 02/14] WIP - renaming --- .../components/V2BranchesGroup/index.ts | 1 - .../components/V2IssuesGroup/index.ts | 1 - .../V2NewWorkspaceModalContent/index.ts | 1 - .../components/V2ProjectSelector/index.ts | 1 - .../components/V2PromptGroup/index.ts | 1 - .../components/V2PullRequestsGroup/index.ts | 1 - .../hooks/useV2CreateWorkspace/index.ts | 1 - .../components/V2NewWorkspaceModal/index.ts | 1 - .../DashboardSidebar/DashboardSidebar.tsx} | 22 +++---- .../DashboardSidebarDeleteDialog.tsx} | 6 +- .../DashboardSidebarDeleteDialog/index.ts | 1 + .../DashboardSidebarHeader.tsx} | 6 +- .../DashboardSidebarHeader/index.ts | 1 + .../DashboardSidebarProjectSection.tsx} | 61 ++++++++++--------- .../DashboardSidebarProjectContextMenu.tsx} | 6 +- .../index.ts | 1 + .../DashboardSidebarProjectSection/index.ts | 1 + .../DashboardSidebarSection.tsx} | 14 ++--- .../DashboardSidebarSection/index.ts | 1 + .../DashboardSidebarWorkspaceItem.tsx} | 30 ++++----- .../DashboardSidebarWorkspaceContextMenu.tsx} | 6 +- .../index.ts | 1 + .../DashboardSidebarWorkspaceItem/index.ts | 1 + .../hooks/useDashboardSidebarData/index.ts | 1 + .../useDashboardSidebarData.ts} | 30 +++++---- .../useDashboardSidebarProjectDnD/index.ts | 1 + .../useDashboardSidebarProjectDnD.ts} | 10 +-- .../useDashboardSidebarShortcuts/index.ts | 1 + .../useDashboardSidebarShortcuts.ts} | 6 +- .../useDashboardSidebarWorkspaceDnD/index.ts | 1 + .../useDashboardSidebarWorkspaceDnD.ts} | 10 +-- .../components/DashboardSidebar/index.ts | 1 + .../components/DashboardSidebar}/types.ts | 12 ++-- .../_authenticated/_dashboard/layout.tsx | 4 +- .../_dashboard/v2-workspace/layout.tsx | 4 +- .../DashboardNewWorkspaceDraftContext.tsx} | 47 +++++++------- .../DashboardNewWorkspaceModal.tsx} | 12 ++-- .../DashboardNewWorkspaceForm.tsx} | 40 ++++++------ .../BranchesGroup/BranchesGroup.tsx} | 14 ++--- .../components/BranchesGroup/index.ts | 1 + .../components/DevicePicker/DevicePicker.tsx | 0 .../hooks/useWorkspaceHostOptions/index.ts | 0 .../useWorkspaceHostOptions.ts | 0 .../components/DevicePicker/index.ts | 0 .../components/IssuesGroup/IssuesGroup.tsx} | 12 ++-- .../components/IssuesGroup/index.ts | 1 + .../ProjectSelector/ProjectSelector.tsx} | 12 ++-- .../components/ProjectSelector/index.ts | 1 + .../components/PromptGroup/PromptGroup.tsx} | 16 ++--- .../components/PromptGroup/index.ts | 1 + .../PullRequestsGroup/PullRequestsGroup.tsx} | 14 ++--- .../components/PullRequestsGroup/index.ts | 1 + .../DashboardNewWorkspaceForm/index.ts | 1 + .../useCreateDashboardWorkspace/index.ts | 1 + .../useCreateDashboardWorkspace.ts} | 12 ++-- .../DashboardNewWorkspaceModal/index.ts | 1 + .../ProjectThumbnail/ProjectThumbnail.tsx} | 6 +- .../components/ProjectThumbnail/index.ts | 1 + .../hooks/useDashboardSidebarState/index.ts | 1 + .../useDashboardSidebarState.ts} | 2 +- .../renderer/routes/_authenticated/layout.tsx | 8 ++- .../CollectionsProvider/collections.ts | 38 ++++++------ .../dashboardSidebarLocal/index.ts | 1 + .../schema.ts} | 18 ++++-- .../components/V2DeleteDialog/index.ts | 1 - .../components/V2ProjectSection/index.ts | 1 - .../components/V2ProjectThumbnail/index.ts | 1 - .../components/V2SidebarHeader/index.ts | 1 - .../components/V2SidebarSection/index.ts | 1 - .../components/V2WorkspaceListItem/index.ts | 1 - .../hooks/useV2ProjectDnD/index.ts | 1 - .../hooks/useV2SidebarData/index.ts | 1 - .../hooks/useV2WorkspaceDnD/index.ts | 1 - .../hooks/useV2WorkspaceShortcuts/index.ts | 1 - .../components/V2WorkspaceSidebar/index.ts | 1 - 75 files changed, 275 insertions(+), 245 deletions(-) delete mode 100644 apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2BranchesGroup/index.ts delete mode 100644 apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2IssuesGroup/index.ts delete mode 100644 apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2NewWorkspaceModalContent/index.ts delete mode 100644 apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2ProjectSelector/index.ts delete mode 100644 apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2PromptGroup/index.ts delete mode 100644 apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2PullRequestsGroup/index.ts delete mode 100644 apps/desktop/src/renderer/components/V2NewWorkspaceModal/hooks/useV2CreateWorkspace/index.ts delete mode 100644 apps/desktop/src/renderer/components/V2NewWorkspaceModal/index.ts rename apps/desktop/src/renderer/{screens/main/components/V2WorkspaceSidebar/V2WorkspaceSidebar.tsx => routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx} (66%) rename apps/desktop/src/renderer/{screens/main/components/V2WorkspaceSidebar/components/V2DeleteDialog/V2DeleteDialog.tsx => routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/DashboardSidebarDeleteDialog.tsx} (91%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/index.ts rename apps/desktop/src/renderer/{screens/main/components/V2WorkspaceSidebar/components/V2SidebarHeader/V2SidebarHeader.tsx => routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/DashboardSidebarHeader.tsx} (96%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/index.ts rename apps/desktop/src/renderer/{screens/main/components/V2WorkspaceSidebar/components/V2ProjectSection/V2ProjectSection.tsx => routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx} (86%) rename apps/desktop/src/renderer/{screens/main/components/V2WorkspaceSidebar/components/V2ProjectSection/V2ProjectContextMenu.tsx => routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/DashboardSidebarProjectContextMenu.tsx} (91%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/index.ts rename apps/desktop/src/renderer/{screens/main/components/V2WorkspaceSidebar/components/V2SidebarSection/V2SidebarSection.tsx => routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx} (91%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/index.ts rename apps/desktop/src/renderer/{screens/main/components/V2WorkspaceSidebar/components/V2WorkspaceListItem/V2WorkspaceListItem.tsx => routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx} (88%) rename apps/desktop/src/renderer/{screens/main/components/V2WorkspaceSidebar/components/V2WorkspaceListItem/V2WorkspaceContextMenu.tsx => routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx} (93%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/index.ts rename apps/desktop/src/renderer/{screens/main/components/V2WorkspaceSidebar/hooks/useV2SidebarData/useV2SidebarData.ts => routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts} (86%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarProjectDnD/index.ts rename apps/desktop/src/renderer/{screens/main/components/V2WorkspaceSidebar/hooks/useV2ProjectDnD/useV2ProjectDnD.ts => routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarProjectDnD/useDashboardSidebarProjectDnD.ts} (79%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/index.ts rename apps/desktop/src/renderer/{screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceShortcuts/useV2WorkspaceShortcuts.ts => routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts} (92%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarWorkspaceDnD/index.ts rename apps/desktop/src/renderer/{screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceDnD/useV2WorkspaceDnD.ts => routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarWorkspaceDnD/useDashboardSidebarWorkspaceDnD.ts} (82%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/index.ts rename apps/desktop/src/renderer/{screens/main/components/V2WorkspaceSidebar => routes/_authenticated/_dashboard/components/DashboardSidebar}/types.ts (61%) rename apps/desktop/src/renderer/{components/V2NewWorkspaceModal/V2NewWorkspaceModalDraftContext.tsx => routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceDraftContext.tsx} (72%) rename apps/desktop/src/renderer/{components/V2NewWorkspaceModal/V2NewWorkspaceModal.tsx => routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceModal.tsx} (74%) rename apps/desktop/src/renderer/{components/V2NewWorkspaceModal/components/V2NewWorkspaceModalContent/V2NewWorkspaceModalContent.tsx => routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/DashboardNewWorkspaceForm.tsx} (87%) rename apps/desktop/src/renderer/{components/V2NewWorkspaceModal/components/V2BranchesGroup/V2BranchesGroup.tsx => routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/BranchesGroup/BranchesGroup.tsx} (93%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/BranchesGroup/index.ts rename apps/desktop/src/renderer/{components/V2NewWorkspaceModal => routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm}/components/DevicePicker/DevicePicker.tsx (100%) rename apps/desktop/src/renderer/{components/V2NewWorkspaceModal => routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm}/components/DevicePicker/hooks/useWorkspaceHostOptions/index.ts (100%) rename apps/desktop/src/renderer/{components/V2NewWorkspaceModal => routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm}/components/DevicePicker/hooks/useWorkspaceHostOptions/useWorkspaceHostOptions.ts (100%) rename apps/desktop/src/renderer/{components/V2NewWorkspaceModal => routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm}/components/DevicePicker/index.ts (100%) rename apps/desktop/src/renderer/{components/V2NewWorkspaceModal/components/V2IssuesGroup/V2IssuesGroup.tsx => routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/IssuesGroup/IssuesGroup.tsx} (94%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/IssuesGroup/index.ts rename apps/desktop/src/renderer/{components/V2NewWorkspaceModal/components/V2ProjectSelector/V2ProjectSelector.tsx => routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/ProjectSelector/ProjectSelector.tsx} (93%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/ProjectSelector/index.ts rename apps/desktop/src/renderer/{components/V2NewWorkspaceModal/components/V2PromptGroup/V2PromptGroup.tsx => routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/PromptGroup.tsx} (92%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/index.ts rename apps/desktop/src/renderer/{components/V2NewWorkspaceModal/components/V2PullRequestsGroup/V2PullRequestsGroup.tsx => routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PullRequestsGroup/PullRequestsGroup.tsx} (92%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PullRequestsGroup/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCreateDashboardWorkspace/index.ts rename apps/desktop/src/renderer/{components/V2NewWorkspaceModal/hooks/useV2CreateWorkspace/useV2CreateWorkspace.ts => routes/_authenticated/components/DashboardNewWorkspaceModal/hooks/useCreateDashboardWorkspace/useCreateDashboardWorkspace.ts} (73%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/index.ts rename apps/desktop/src/renderer/{screens/main/components/V2WorkspaceSidebar/components/V2ProjectThumbnail/V2ProjectThumbnail.tsx => routes/_authenticated/components/ProjectThumbnail/ProjectThumbnail.tsx} (91%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/ProjectThumbnail/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/index.ts rename apps/desktop/src/renderer/{lib/v2-sidebar-state.ts => routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts} (99%) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/index.ts rename apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/{v2-sidebar-local.ts => dashboardSidebarLocal/schema.ts} (62%) delete mode 100644 apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2DeleteDialog/index.ts delete mode 100644 apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2ProjectSection/index.ts delete mode 100644 apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2ProjectThumbnail/index.ts delete mode 100644 apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2SidebarHeader/index.ts delete mode 100644 apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2SidebarSection/index.ts delete mode 100644 apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2WorkspaceListItem/index.ts delete mode 100644 apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2ProjectDnD/index.ts delete mode 100644 apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2SidebarData/index.ts delete mode 100644 apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceDnD/index.ts delete mode 100644 apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceShortcuts/index.ts delete mode 100644 apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/index.ts diff --git a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2BranchesGroup/index.ts b/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2BranchesGroup/index.ts deleted file mode 100644 index 3456401d7e9..00000000000 --- a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2BranchesGroup/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { V2BranchesGroup } from "./V2BranchesGroup"; diff --git a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2IssuesGroup/index.ts b/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2IssuesGroup/index.ts deleted file mode 100644 index 40eb2647136..00000000000 --- a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2IssuesGroup/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { V2IssuesGroup } from "./V2IssuesGroup"; diff --git a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2NewWorkspaceModalContent/index.ts b/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2NewWorkspaceModalContent/index.ts deleted file mode 100644 index bab96506cd2..00000000000 --- a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2NewWorkspaceModalContent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { V2NewWorkspaceModalContent } from "./V2NewWorkspaceModalContent"; diff --git a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2ProjectSelector/index.ts b/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2ProjectSelector/index.ts deleted file mode 100644 index 0032802e532..00000000000 --- a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2ProjectSelector/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { V2ProjectSelector } from "./V2ProjectSelector"; diff --git a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2PromptGroup/index.ts b/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2PromptGroup/index.ts deleted file mode 100644 index e113f33fba7..00000000000 --- a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2PromptGroup/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { V2PromptGroup } from "./V2PromptGroup"; diff --git a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2PullRequestsGroup/index.ts b/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2PullRequestsGroup/index.ts deleted file mode 100644 index f9b80a8e457..00000000000 --- a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2PullRequestsGroup/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { V2PullRequestsGroup } from "./V2PullRequestsGroup"; diff --git a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/hooks/useV2CreateWorkspace/index.ts b/apps/desktop/src/renderer/components/V2NewWorkspaceModal/hooks/useV2CreateWorkspace/index.ts deleted file mode 100644 index 3bcb17cecc2..00000000000 --- a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/hooks/useV2CreateWorkspace/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useV2CreateWorkspace } from "./useV2CreateWorkspace"; diff --git a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/index.ts b/apps/desktop/src/renderer/components/V2NewWorkspaceModal/index.ts deleted file mode 100644 index e1ea6a5d1c5..00000000000 --- a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { V2NewWorkspaceModal } from "./V2NewWorkspaceModal"; diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/V2WorkspaceSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx similarity index 66% rename from apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/V2WorkspaceSidebar.tsx rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx index 4b9d7bb4e33..f620c8fc355 100644 --- a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/V2WorkspaceSidebar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx @@ -1,19 +1,19 @@ import { useMemo } from "react"; -import { V2ProjectSection } from "./components/V2ProjectSection"; -import { V2SidebarHeader } from "./components/V2SidebarHeader"; -import { useV2SidebarData } from "./hooks/useV2SidebarData"; -import { useV2WorkspaceShortcuts } from "./hooks/useV2WorkspaceShortcuts"; +import { DashboardSidebarHeader } from "./components/DashboardSidebarHeader"; +import { DashboardSidebarProjectSection } from "./components/DashboardSidebarProjectSection"; +import { useDashboardSidebarData } from "./hooks/useDashboardSidebarData"; +import { useDashboardSidebarShortcuts } from "./hooks/useDashboardSidebarShortcuts"; -interface V2WorkspaceSidebarProps { +interface DashboardSidebarProps { isCollapsed?: boolean; } -export function V2WorkspaceSidebar({ +export function DashboardSidebar({ isCollapsed = false, -}: V2WorkspaceSidebarProps) { - const { groups, toggleProjectCollapsed } = useV2SidebarData(); +}: DashboardSidebarProps) { + const { groups, toggleProjectCollapsed } = useDashboardSidebarData(); - useV2WorkspaceShortcuts(groups); + useDashboardSidebarShortcuts(groups); const projectIds = useMemo(() => groups.map((g) => g.id), [groups]); @@ -37,11 +37,11 @@ export function V2WorkspaceSidebar({ return (
- +
{groups.map((project, index) => ( - void; onConfirm: () => void; @@ -17,14 +17,14 @@ interface V2DeleteDialogProps { isPending?: boolean; } -export function V2DeleteDialog({ +export function DashboardSidebarDeleteDialog({ open, onOpenChange, onConfirm, title, description, isPending = false, -}: V2DeleteDialogProps) { +}: DashboardSidebarDeleteDialogProps) { return ( - removeProjectFromSidebar(projectId)} @@ -189,7 +192,7 @@ export function V2ProjectSection({ "hover:bg-muted/50 transition-colors", )} > - @@ -216,7 +219,7 @@ export function V2ProjectSection({
{flattenedCollapsedWorkspaces.map( (workspace, itemIndex) => ( -
-
+ - - removeProjectFromSidebar(projectId)} @@ -278,7 +281,7 @@ export function V2ProjectSection({ > {isRenaming ? (
- @@ -297,7 +300,7 @@ export function V2ProjectSection({ onDoubleClick={startRename} className="flex items-center gap-2 flex-1 min-w-0 py-0.5 text-left cursor-pointer" > - @@ -342,7 +345,7 @@ export function V2ProjectSection({ />
-
+ {!isCollapsed && ( @@ -355,7 +358,7 @@ export function V2ProjectSection({ >
{workspaces.map((workspace, itemIndex) => ( -
- void; onRemoveFromSidebar: () => void; @@ -24,7 +24,7 @@ interface V2ProjectContextMenuProps { children: React.ReactNode; } -export function V2ProjectContextMenu({ +export function DashboardSidebarProjectContextMenu({ id, onCreateSection, onRemoveFromSidebar, @@ -32,7 +32,7 @@ export function V2ProjectContextMenu({ onDelete, onNewWorkspace, children, -}: V2ProjectContextMenuProps) { +}: DashboardSidebarProjectContextMenuProps) { const handleCopyId = () => { navigator.clipboard.writeText(id); toast.success("Project ID copied"); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/index.ts new file mode 100644 index 00000000000..10430789b22 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectContextMenu/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarProjectContextMenu } from "./DashboardSidebarProjectContextMenu"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/index.ts new file mode 100644 index 00000000000..c328e95b622 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarProjectSection } from "./DashboardSidebarProjectSection"; diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2SidebarSection/V2SidebarSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx similarity index 91% rename from apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2SidebarSection/V2SidebarSection.tsx rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx index 15bbb0b92a8..fa7da6503cf 100644 --- a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/components/V2SidebarSection/V2SidebarSection.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx @@ -10,12 +10,12 @@ import { useState } from "react"; import { HiChevronRight } from "react-icons/hi2"; import { LuFolderOpen, LuPencil, LuTrash2 } from "react-icons/lu"; import { RenameInput } from "renderer/screens/main/components/WorkspaceSidebar/RenameInput"; -import type { V2SidebarSection as V2SidebarSectionRecord } from "../../types"; -import { V2WorkspaceListItem } from "../V2WorkspaceListItem"; +import type { DashboardSidebarSection as DashboardSidebarSectionRecord } from "../../types"; +import { DashboardSidebarWorkspaceItem } from "../DashboardSidebarWorkspaceItem"; -interface V2SidebarSectionProps { +interface DashboardSidebarSectionProps { projectId: string; - section: V2SidebarSectionRecord; + section: DashboardSidebarSectionRecord; shortcutBaseIndex: number; allSections: Array<{ id: string; name: string }>; onDelete: (sectionId: string) => void; @@ -23,7 +23,7 @@ interface V2SidebarSectionProps { onToggleCollapse: (sectionId: string) => void; } -export function V2SidebarSection({ +export function DashboardSidebarSection({ projectId, section, shortcutBaseIndex, @@ -31,7 +31,7 @@ export function V2SidebarSection({ onDelete, onRename, onToggleCollapse, -}: V2SidebarSectionProps) { +}: DashboardSidebarSectionProps) { const [isRenaming, setIsRenaming] = useState(false); const [renameValue, setRenameValue] = useState(section.name); @@ -122,7 +122,7 @@ export function V2SidebarSection({ >
{section.workspaces.map((workspace, index) => ( - - { @@ -166,9 +166,9 @@ export function V2WorkspaceListItem({ )} - + - - { @@ -255,9 +255,9 @@ export function V2WorkspaceListItem({ )}
- + - void; @@ -29,7 +29,7 @@ interface V2WorkspaceContextMenuProps { children: React.ReactNode; } -export function V2WorkspaceContextMenu({ +export function DashboardSidebarWorkspaceContextMenu({ id, sections, onCreateSection, @@ -38,7 +38,7 @@ export function V2WorkspaceContextMenu({ onRename, onDelete, children, -}: V2WorkspaceContextMenuProps) { +}: DashboardSidebarWorkspaceContextMenuProps) { const handleCopyId = () => { navigator.clipboard.writeText(id); toast.success("Workspace ID copied"); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/index.ts new file mode 100644 index 00000000000..411c7acb741 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarWorkspaceContextMenu } from "./DashboardSidebarWorkspaceContextMenu"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/index.ts new file mode 100644 index 00000000000..c21d7087d53 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarWorkspaceItem } from "./DashboardSidebarWorkspaceItem"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/index.ts new file mode 100644 index 00000000000..a89b099a842 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/index.ts @@ -0,0 +1 @@ +export { useDashboardSidebarData } from "./useDashboardSidebarData"; diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2SidebarData/useV2SidebarData.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts similarity index 86% rename from apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2SidebarData/useV2SidebarData.ts rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts index 58526341e0b..7a353a3334d 100644 --- a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2SidebarData/useV2SidebarData.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts @@ -1,16 +1,16 @@ import { useLiveQuery } from "@tanstack/react-db"; import { useMemo } from "react"; -import { useV2SidebarState } from "renderer/lib/v2-sidebar-state"; +import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import type { - V2SidebarProject, - V2SidebarSection, - V2SidebarWorkspace, + DashboardSidebarProject, + DashboardSidebarSection, + DashboardSidebarWorkspace, } from "../../types"; -export function useV2SidebarData() { +export function useDashboardSidebarData() { const collections = useCollections(); - const { toggleProjectCollapsed } = useV2SidebarState(); + const { toggleProjectCollapsed } = useDashboardSidebarState(); const { data: sidebarProjects = [] } = useLiveQuery( (q) => q.from({ sidebarProjects: collections.v2SidebarProjects }), @@ -46,7 +46,7 @@ export function useV2SidebarData() { [collections], ); - const groups = useMemo(() => { + const groups = useMemo(() => { const repoOwnerMap = new Map(); for (const repo of githubRepos) { repoOwnerMap.set(repo.id, repo.owner); @@ -59,7 +59,7 @@ export function useV2SidebarData() { workspaces.map((workspace) => [workspace.id, workspace]), ); - const localSectionsByProject = new Map(); + const localSectionsByProject = new Map(); for (const section of sidebarSections) { const sectionsForProject = localSectionsByProject.get(section.projectId) ?? []; @@ -81,14 +81,20 @@ export function useV2SidebarData() { ); } - const workspaceRowsByProject = new Map(); - const workspaceRowsBySection = new Map(); + const workspaceRowsByProject = new Map< + string, + DashboardSidebarWorkspace[] + >(); + const workspaceRowsBySection = new Map< + string, + DashboardSidebarWorkspace[] + >(); for (const localWorkspace of sidebarWorkspaces) { const workspace = cloudWorkspacesById.get(localWorkspace.workspaceId); if (!workspace) continue; - const sidebarWorkspace: V2SidebarWorkspace = { + const sidebarWorkspace: DashboardSidebarWorkspace = { id: workspace.id, projectId: workspace.projectId, deviceId: workspace.deviceId, @@ -137,7 +143,7 @@ export function useV2SidebarData() { ); } - const resolvedProjects: V2SidebarProject[] = []; + const resolvedProjects: DashboardSidebarProject[] = []; for (const localProject of [...sidebarProjects].sort( (a, b) => a.tabOrder - b.tabOrder, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarProjectDnD/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarProjectDnD/index.ts new file mode 100644 index 00000000000..1043ca1c75e --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarProjectDnD/index.ts @@ -0,0 +1 @@ +export { useDashboardSidebarProjectDnD } from "./useDashboardSidebarProjectDnD"; diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2ProjectDnD/useV2ProjectDnD.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarProjectDnD/useDashboardSidebarProjectDnD.ts similarity index 79% rename from apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2ProjectDnD/useV2ProjectDnD.ts rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarProjectDnD/useDashboardSidebarProjectDnD.ts index c68d62ca8d6..62fb693b67c 100644 --- a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2ProjectDnD/useV2ProjectDnD.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarProjectDnD/useDashboardSidebarProjectDnD.ts @@ -1,6 +1,6 @@ import { useCallback } from "react"; import { useDrag, useDrop } from "react-dnd"; -import { useV2SidebarState } from "renderer/lib/v2-sidebar-state"; +import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; const V2_PROJECT_DND_TYPE = "V2_PROJECT"; @@ -10,18 +10,18 @@ interface DragItem { originalIndex: number; } -interface UseV2ProjectDnDOptions { +interface UseDashboardSidebarProjectDnDOptions { projectId: string; index: number; projectIds: string[]; } -export function useV2ProjectDnD({ +export function useDashboardSidebarProjectDnD({ projectId, index, projectIds, -}: UseV2ProjectDnDOptions) { - const { reorderProjects } = useV2SidebarState(); +}: UseDashboardSidebarProjectDnDOptions) { + const { reorderProjects } = useDashboardSidebarState(); const commitOrder = useCallback( (orderedIds: string[]) => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/index.ts new file mode 100644 index 00000000000..a4f0aa62dee --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/index.ts @@ -0,0 +1 @@ +export { useDashboardSidebarShortcuts } from "./useDashboardSidebarShortcuts"; diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceShortcuts/useV2WorkspaceShortcuts.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts similarity index 92% rename from apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceShortcuts/useV2WorkspaceShortcuts.ts rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts index 708df3ce62f..3cb824c9038 100644 --- a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceShortcuts/useV2WorkspaceShortcuts.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts @@ -2,13 +2,15 @@ import { useNavigate } from "@tanstack/react-router"; import { useCallback } from "react"; import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { useAppHotkey } from "renderer/stores/hotkeys"; -import type { V2SidebarProject } from "../../types"; +import type { DashboardSidebarProject } from "../../types"; /** * Keyboard shortcuts for V2 workspace switching (⌘1-9). * Mirrors the legacy useWorkspaceShortcuts hook but for V2 workspaces. */ -export function useV2WorkspaceShortcuts(groups: V2SidebarProject[]) { +export function useDashboardSidebarShortcuts( + groups: DashboardSidebarProject[], +) { const navigate = useNavigate(); const allWorkspaces = groups.flatMap((group) => [ diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarWorkspaceDnD/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarWorkspaceDnD/index.ts new file mode 100644 index 00000000000..5fd3a2431b9 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarWorkspaceDnD/index.ts @@ -0,0 +1 @@ +export { useDashboardSidebarWorkspaceDnD } from "./useDashboardSidebarWorkspaceDnD"; diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceDnD/useV2WorkspaceDnD.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarWorkspaceDnD/useDashboardSidebarWorkspaceDnD.ts similarity index 82% rename from apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceDnD/useV2WorkspaceDnD.ts rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarWorkspaceDnD/useDashboardSidebarWorkspaceDnD.ts index 06ae8e6c5bb..9889abf744d 100644 --- a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/hooks/useV2WorkspaceDnD/useV2WorkspaceDnD.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarWorkspaceDnD/useDashboardSidebarWorkspaceDnD.ts @@ -1,6 +1,6 @@ import { useCallback } from "react"; import { useDrag, useDrop } from "react-dnd"; -import { useV2SidebarState } from "renderer/lib/v2-sidebar-state"; +import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; const V2_WORKSPACE_DND_TYPE = "V2_WORKSPACE"; @@ -12,7 +12,7 @@ interface DragItem { originalIndex: number; } -interface UseV2WorkspaceDnDOptions { +interface UseDashboardSidebarWorkspaceDnDOptions { workspaceId: string; projectId: string; sectionId: string | null; @@ -20,14 +20,14 @@ interface UseV2WorkspaceDnDOptions { workspaceIds: string[]; } -export function useV2WorkspaceDnD({ +export function useDashboardSidebarWorkspaceDnD({ workspaceId, projectId, sectionId, index, workspaceIds, -}: UseV2WorkspaceDnDOptions) { - const { reorderWorkspaces } = useV2SidebarState(); +}: UseDashboardSidebarWorkspaceDnDOptions) { + const { reorderWorkspaces } = useDashboardSidebarState(); const commitOrder = useCallback( (orderedIds: string[]) => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/index.ts new file mode 100644 index 00000000000..2528c2a01be --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/index.ts @@ -0,0 +1 @@ +export { DashboardSidebar } from "./DashboardSidebar"; diff --git a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/types.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts similarity index 61% rename from apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/types.ts rename to apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts index 3005237e7be..43914f876e0 100644 --- a/apps/desktop/src/renderer/screens/main/components/V2WorkspaceSidebar/types.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts @@ -1,4 +1,4 @@ -export interface V2SidebarWorkspace { +export interface DashboardSidebarWorkspace { id: string; projectId: string; deviceId: string; @@ -8,17 +8,17 @@ export interface V2SidebarWorkspace { updatedAt: Date; } -export interface V2SidebarSection { +export interface DashboardSidebarSection { id: string; projectId: string; name: string; createdAt: Date; isCollapsed: boolean; tabOrder: number; - workspaces: V2SidebarWorkspace[]; + workspaces: DashboardSidebarWorkspace[]; } -export interface V2SidebarProject { +export interface DashboardSidebarProject { id: string; name: string; slug: string; @@ -27,6 +27,6 @@ export interface V2SidebarProject { createdAt: Date; updatedAt: Date; isCollapsed: boolean; - workspaces: V2SidebarWorkspace[]; - sections: V2SidebarSection[]; + workspaces: DashboardSidebarWorkspace[]; + sections: DashboardSidebarSection[]; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx index d32557d2ee7..98ee6a4c365 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx @@ -7,8 +7,8 @@ import { } from "@tanstack/react-router"; import { useFeatureFlagEnabled } from "posthog-js/react"; import { electronTrpc } from "renderer/lib/electron-trpc"; +import { DashboardSidebar } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar"; import { ResizablePanel } from "renderer/screens/main/components/ResizablePanel"; -import { V2WorkspaceSidebar } from "renderer/screens/main/components/V2WorkspaceSidebar"; import { WorkspaceSidebar } from "renderer/screens/main/components/WorkspaceSidebar"; import { useAppHotkey } from "renderer/stores/hotkeys"; import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; @@ -112,7 +112,7 @@ function DashboardLayout() { } > {isV2CloudEnabled ? ( - + ) : ( string; } -interface V2NewWorkspaceModalDraftContextValue { - draft: V2NewWorkspaceModalDraft; +interface DashboardNewWorkspaceDraftContextValue { + draft: DashboardNewWorkspaceDraft; draftVersion: number; closeModal: () => void; closeAndResetDraft: () => void; runAsyncAction: ( promise: Promise, - messages: V2NewWorkspaceModalActionMessages, + messages: DashboardNewWorkspaceActionMessages, ) => Promise; - updateDraft: (patch: Partial) => void; + updateDraft: (patch: Partial) => void; resetDraft: () => void; resetDraftIfVersion: (draftVersion: number) => void; } -const V2NewWorkspaceModalDraftContext = - createContext(null); +const DashboardNewWorkspaceDraftContext = + createContext(null); -export function V2NewWorkspaceModalDraftProvider({ +export function DashboardNewWorkspaceDraftProvider({ children, onClose, }: PropsWithChildren<{ onClose: () => void }>) { const [state, setState] = useState(buildInitialDraftState); const updateDraft = useCallback( - (patch: Partial) => { + (patch: Partial) => { setState((state) => ({ ...state, ...patch, @@ -120,7 +120,10 @@ export function V2NewWorkspaceModalDraftProvider({ }, [onClose, resetDraft]); const runAsyncAction = useCallback( - (promise: Promise, messages: V2NewWorkspaceModalActionMessages) => { + ( + promise: Promise, + messages: DashboardNewWorkspaceActionMessages, + ) => { const submitDraftVersion = state.draftVersion; onClose(); toast.promise(promise, { @@ -138,7 +141,7 @@ export function V2NewWorkspaceModalDraftProvider({ [onClose, resetDraftIfVersion, state.draftVersion], ); - const value = useMemo( + const value = useMemo( () => ({ draft: { activeTab: state.activeTab, @@ -174,17 +177,17 @@ export function V2NewWorkspaceModalDraftProvider({ ); return ( - + {children} - + ); } -export function useV2NewWorkspaceModalDraft() { - const context = useContext(V2NewWorkspaceModalDraftContext); +export function useDashboardNewWorkspaceDraft() { + const context = useContext(DashboardNewWorkspaceDraftContext); if (!context) { throw new Error( - "useV2NewWorkspaceModalDraft must be used within V2NewWorkspaceModalDraftProvider", + "useDashboardNewWorkspaceDraft must be used within DashboardNewWorkspaceDraftProvider", ); } return context; diff --git a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/V2NewWorkspaceModal.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceModal.tsx similarity index 74% rename from apps/desktop/src/renderer/components/V2NewWorkspaceModal/V2NewWorkspaceModal.tsx rename to apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceModal.tsx index 78ab0c167c2..12bf45f75db 100644 --- a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/V2NewWorkspaceModal.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceModal.tsx @@ -10,16 +10,16 @@ import { useNewWorkspaceModalOpen, usePreSelectedProjectId, } from "renderer/stores/new-workspace-modal"; -import { V2NewWorkspaceModalContent } from "./components/V2NewWorkspaceModalContent"; -import { V2NewWorkspaceModalDraftProvider } from "./V2NewWorkspaceModalDraftContext"; +import { DashboardNewWorkspaceForm } from "./components/DashboardNewWorkspaceForm"; +import { DashboardNewWorkspaceDraftProvider } from "./DashboardNewWorkspaceDraftContext"; -export function V2NewWorkspaceModal() { +export function DashboardNewWorkspaceModal() { const isOpen = useNewWorkspaceModalOpen(); const closeModal = useCloseNewWorkspaceModal(); const preSelectedProjectId = usePreSelectedProjectId(); return ( - + !open && closeModal()}> New Workspace @@ -31,12 +31,12 @@ export function V2NewWorkspaceModal() { showCloseButton={false} className="bg-popover text-popover-foreground sm:max-w-[560px] max-h-[min(70vh,600px)] !top-[calc(50%-min(35vh,300px))] !-translate-y-0 flex flex-col overflow-hidden p-0" > - - + ); } diff --git a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2NewWorkspaceModalContent/V2NewWorkspaceModalContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/DashboardNewWorkspaceForm.tsx similarity index 87% rename from apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2NewWorkspaceModalContent/V2NewWorkspaceModalContent.tsx rename to apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/DashboardNewWorkspaceForm.tsx index 7590c3c57e1..5ac19be5aa4 100644 --- a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2NewWorkspaceModalContent/V2NewWorkspaceModalContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/DashboardNewWorkspaceForm.tsx @@ -6,30 +6,30 @@ import { useEffect, useMemo, useRef } from "react"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { - useV2NewWorkspaceModalDraft, - type V2NewWorkspaceModalTab, -} from "../../V2NewWorkspaceModalDraftContext"; -import { DevicePicker } from "../DevicePicker"; -import { V2BranchesGroup } from "../V2BranchesGroup"; -import { V2IssuesGroup } from "../V2IssuesGroup"; -import { V2ProjectSelector } from "../V2ProjectSelector"; -import { V2PromptGroup } from "../V2PromptGroup"; -import { V2PullRequestsGroup } from "../V2PullRequestsGroup"; + type DashboardNewWorkspaceTab, + useDashboardNewWorkspaceDraft, +} from "../../DashboardNewWorkspaceDraftContext"; +import { BranchesGroup } from "./components/BranchesGroup"; +import { DevicePicker } from "./components/DevicePicker"; +import { IssuesGroup } from "./components/IssuesGroup"; +import { ProjectSelector } from "./components/ProjectSelector"; +import { PromptGroup } from "./components/PromptGroup"; +import { PullRequestsGroup } from "./components/PullRequestsGroup"; const COMMAND_CLASS_NAME = "[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5 flex h-full w-full flex-1 flex-col overflow-hidden rounded-none"; -interface V2NewWorkspaceModalContentProps { +interface DashboardNewWorkspaceFormProps { isOpen: boolean; preSelectedProjectId: string | null; } -/** V2 content pane for the New Workspace modal with collection-based project selection. */ -export function V2NewWorkspaceModalContent({ +/** Main form for the new workspace modal with collection-based project selection. */ +export function DashboardNewWorkspaceForm({ isOpen, preSelectedProjectId, -}: V2NewWorkspaceModalContentProps) { - const { draft, updateDraft } = useV2NewWorkspaceModalDraft(); +}: DashboardNewWorkspaceFormProps) { + const { draft, updateDraft } = useDashboardNewWorkspaceDraft(); const collections = useCollections(); // Get all v2 projects @@ -159,7 +159,7 @@ export function V2NewWorkspaceModalContent({ - updateDraft({ activeTab: value as V2NewWorkspaceModalTab }) + updateDraft({ activeTab: value as DashboardNewWorkspaceTab }) } > @@ -175,7 +175,7 @@ export function V2NewWorkspaceModalContent({ onSelectHostTarget={(hostTarget) => updateDraft({ hostTarget })} />
- updateDraft({ selectedProjectId }) @@ -200,21 +200,21 @@ export function V2NewWorkspaceModalContent({ {draft.activeTab === "pull-requests" && ( - )} {draft.activeTab === "branches" && ( - )} {draft.activeTab === "issues" && ( - @@ -223,7 +223,7 @@ export function V2NewWorkspaceModalContent({ ) : (
- diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/IssuesGroup/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/IssuesGroup/index.ts new file mode 100644 index 00000000000..c0762c8495d --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/IssuesGroup/index.ts @@ -0,0 +1 @@ +export { IssuesGroup } from "./IssuesGroup"; diff --git a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2ProjectSelector/V2ProjectSelector.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/ProjectSelector/ProjectSelector.tsx similarity index 93% rename from apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2ProjectSelector/V2ProjectSelector.tsx rename to apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/ProjectSelector/ProjectSelector.tsx index 0410669af0b..2ed6690d260 100644 --- a/apps/desktop/src/renderer/components/V2NewWorkspaceModal/components/V2ProjectSelector/V2ProjectSelector.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/ProjectSelector/ProjectSelector.tsx @@ -14,18 +14,18 @@ import { useMemo, useState } from "react"; import { FaGithub } from "react-icons/fa"; import { HiCheck, HiChevronUpDown } from "react-icons/hi2"; import { env } from "renderer/env.renderer"; +import { ProjectThumbnail } from "renderer/routes/_authenticated/components/ProjectThumbnail"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; -import { V2ProjectThumbnail } from "renderer/screens/main/components/V2WorkspaceSidebar/components/V2ProjectThumbnail"; -interface V2ProjectSelectorProps { +interface ProjectSelectorProps { selectedProjectId: string | null; onSelectProject: (projectId: string) => void; } -export function V2ProjectSelector({ +export function ProjectSelector({ selectedProjectId, onSelectProject, -}: V2ProjectSelectorProps) { +}: ProjectSelectorProps) { const [open, setOpen] = useState(false); const collections = useCollections(); @@ -65,7 +65,7 @@ export function V2ProjectSelector({ - - - {projectName} - - {totalWorkspaceCount} workspace - {totalWorkspaceCount !== 1 ? "s" : ""} - - - - - - {!isCollapsed && ( - -
- {flattenedCollapsedWorkspaces.map( - (workspace, itemIndex) => ( - item.id, - )} - sections={allSections} - shortcutIndex={shortcutBaseIndex + itemIndex} - isCollapsed - /> - ), - )} -
-
- )} -
+ item.id)} + allSections={allSections} + workspaceShortcutLabels={workspaceShortcutLabels} + onToggleCollapse={() => onToggleCollapse(projectId)} + />
@@ -273,132 +150,32 @@ export function DashboardSidebarProjectSection({ onDelete={() => setIsDeleteDialogOpen(true)} onNewWorkspace={handleNewWorkspace} > -
- {isRenaming ? ( -
- - -
- ) : ( - - )} - - - - - - - New workspace - - - - +
+ onToggleCollapse(projectId)} + onNewWorkspace={handleNewWorkspace} + onDeleteSection={deleteSection} + onRenameSection={renameSection} + onToggleSectionCollapse={toggleSectionCollapsed} + />
- - - {!isCollapsed && ( - -
- {workspaces.map((workspace, itemIndex) => ( - - ))} - {sections.map((section, sectionIndex) => { - const sectionShortcutBase = - shortcutBaseIndex + - workspaces.length + - sections - .slice(0, sectionIndex) - .reduce( - (sum, currentSection) => - sum + currentSection.workspaces.length, - 0, - ); - - return ( - - ); - })} -
-
- )} -
; + workspaceShortcutLabels: Map; + onToggleCollapse: () => void; +} + +export function DashboardSidebarCollapsedProjectContent({ + projectId, + projectName, + githubOwner, + isCollapsed, + isDragging, + totalWorkspaceCount, + workspaces, + workspaceIds, + allSections, + workspaceShortcutLabels, + onToggleCollapse, +}: DashboardSidebarCollapsedProjectContentProps) { + return ( +
+ + + + + + {projectName} + + {totalWorkspaceCount} workspace + {totalWorkspaceCount !== 1 ? "s" : ""} + + + + + + {!isCollapsed && ( + +
+ {workspaces.map((workspace, index) => ( + + ))} +
+
+ )} +
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarCollapsedProjectContent/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarCollapsedProjectContent/index.ts new file mode 100644 index 00000000000..9ffd4a74949 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarCollapsedProjectContent/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarCollapsedProjectContent } from "./DashboardSidebarCollapsedProjectContent"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx new file mode 100644 index 00000000000..23a5f178061 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx @@ -0,0 +1,115 @@ +import { AnimatePresence, motion } from "framer-motion"; +import type { + DashboardSidebarSection, + DashboardSidebarWorkspace, +} from "../../../../types"; +import { DashboardSidebarSection as DashboardSidebarSectionComponent } from "../../../DashboardSidebarSection"; +import { DashboardSidebarWorkspaceItem } from "../../../DashboardSidebarWorkspaceItem"; +import { DashboardSidebarProjectRow } from "../DashboardSidebarProjectRow"; + +interface DashboardSidebarExpandedProjectContentProps { + projectId: string; + projectName: string; + githubOwner: string | null; + isCollapsed: boolean; + workspaces: DashboardSidebarWorkspace[]; + sections: DashboardSidebarSection[]; + topLevelWorkspaceIds: string[]; + allSections: Array<{ id: string; name: string }>; + workspaceShortcutLabels: Map; + totalWorkspaceCount: number; + isRenaming: boolean; + renameValue: string; + onRenameValueChange: (value: string) => void; + onSubmitRename: () => void; + onCancelRename: () => void; + onStartRename: () => void; + onToggleCollapse: () => void; + onNewWorkspace: () => void; + onDeleteSection: (sectionId: string) => void; + onRenameSection: (sectionId: string, name: string) => void; + onToggleSectionCollapse: (sectionId: string) => void; +} + +export function DashboardSidebarExpandedProjectContent({ + projectId, + projectName, + githubOwner, + isCollapsed, + workspaces, + sections, + topLevelWorkspaceIds, + allSections, + workspaceShortcutLabels, + totalWorkspaceCount, + isRenaming, + renameValue, + onRenameValueChange, + onSubmitRename, + onCancelRename, + onStartRename, + onToggleCollapse, + onNewWorkspace, + onDeleteSection, + onRenameSection, + onToggleSectionCollapse, +}: DashboardSidebarExpandedProjectContentProps) { + return ( + <> + + + + {!isCollapsed && ( + +
+ {workspaces.map((workspace, index) => ( + + ))} + {sections.map((section) => ( + + ))} +
+
+ )} +
+ + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/index.ts new file mode 100644 index 00000000000..40e1f472a28 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarExpandedProjectContent } from "./DashboardSidebarExpandedProjectContent"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx new file mode 100644 index 00000000000..fc37bd04241 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx @@ -0,0 +1,110 @@ +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { cn } from "@superset/ui/utils"; +import { HiChevronRight, HiMiniPlus } from "react-icons/hi2"; +import { ProjectThumbnail } from "renderer/routes/_authenticated/components/ProjectThumbnail"; +import { RenameInput } from "renderer/screens/main/components/WorkspaceSidebar/RenameInput"; + +interface DashboardSidebarProjectRowProps { + projectName: string; + githubOwner: string | null; + totalWorkspaceCount: number; + isCollapsed: boolean; + isRenaming: boolean; + renameValue: string; + onRenameValueChange: (value: string) => void; + onSubmitRename: () => void; + onCancelRename: () => void; + onStartRename: () => void; + onToggleCollapse: () => void; + onNewWorkspace: () => void; +} + +export function DashboardSidebarProjectRow({ + projectName, + githubOwner, + totalWorkspaceCount, + isCollapsed, + isRenaming, + renameValue, + onRenameValueChange, + onSubmitRename, + onCancelRename, + onStartRename, + onToggleCollapse, + onNewWorkspace, +}: DashboardSidebarProjectRowProps) { + return ( +
+ {isRenaming ? ( +
+ + +
+ ) : ( + + )} + + + + + + + New workspace + + + + +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/index.ts new file mode 100644 index 00000000000..75f73f6c477 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarProjectRow } from "./DashboardSidebarProjectRow"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/index.ts new file mode 100644 index 00000000000..8956c3c4e3e --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/index.ts @@ -0,0 +1 @@ +export { useDashboardSidebarProjectSectionActions } from "./useDashboardSidebarProjectSectionActions"; 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 new file mode 100644 index 00000000000..a59eee93d56 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts @@ -0,0 +1,125 @@ +import { toast } from "@superset/ui/sonner"; +import { useMatchRoute, useNavigate } from "@tanstack/react-router"; +import { useState } from "react"; +import { apiTrpcClient } from "renderer/lib/api-trpc-client"; +import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; +import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; +import type { + DashboardSidebarSection, + DashboardSidebarWorkspace, +} from "../../../../types"; + +interface UseDashboardSidebarProjectSectionActionsOptions { + projectId: string; + projectName: string; + workspaces: DashboardSidebarWorkspace[]; + sections: DashboardSidebarSection[]; +} + +export function useDashboardSidebarProjectSectionActions({ + projectId, + projectName, + workspaces, + sections, +}: UseDashboardSidebarProjectSectionActionsOptions) { + const openModal = useOpenNewWorkspaceModal(); + const navigate = useNavigate(); + const matchRoute = useMatchRoute(); + const { + createSection, + deleteSection, + removeProjectFromSidebar, + renameSection, + toggleSectionCollapsed, + } = useDashboardSidebarState(); + + const [isRenaming, setIsRenaming] = useState(false); + const [renameValue, setRenameValue] = useState(projectName); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + + const startRename = () => { + setRenameValue(projectName); + setIsRenaming(true); + }; + + const cancelRename = () => { + setIsRenaming(false); + setRenameValue(projectName); + }; + + const submitRename = async () => { + setIsRenaming(false); + const trimmed = renameValue.trim(); + if (!trimmed || trimmed === projectName) return; + try { + await apiTrpcClient.v2Project.update.mutate({ + id: projectId, + name: trimmed, + slug: trimmed.toLowerCase().replace(/\s+/g, "-"), + }); + } catch (error) { + toast.error( + `Failed to rename: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } + }; + + const handleDelete = async () => { + setIsDeleting(true); + try { + await apiTrpcClient.v2Project.delete.mutate({ id: projectId }); + removeProjectFromSidebar(projectId); + setIsDeleteDialogOpen(false); + toast.success("Project deleted"); + + const isInProject = [ + ...workspaces, + ...sections.flatMap((s) => s.workspaces), + ].some( + (workspace) => + !!matchRoute({ + to: "/v2-workspace/$workspaceId", + params: { workspaceId: workspace.id }, + fuzzy: true, + }), + ); + if (isInProject) { + navigate({ to: "/" }); + } + } catch (error) { + toast.error( + `Failed to delete: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } finally { + setIsDeleting(false); + } + }; + + const handleNewWorkspace = () => { + openModal(projectId); + }; + + const handleNewSection = () => { + createSection(projectId); + }; + + return { + cancelRename, + deleteSection, + handleDelete, + handleNewSection, + handleNewWorkspace, + isDeleteDialogOpen, + isDeleting, + isRenaming, + removeProjectFromSidebar, + renameSection, + renameValue, + setIsDeleteDialogOpen, + setRenameValue, + startRename, + submitRename, + toggleSectionCollapsed, + }; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/utils/countProjectWorkspaces.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/utils/countProjectWorkspaces.ts new file mode 100644 index 00000000000..3ae3e4927bf --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/utils/countProjectWorkspaces.ts @@ -0,0 +1,14 @@ +import type { + DashboardSidebarSection, + DashboardSidebarWorkspace, +} from "../../../types"; + +export function countProjectWorkspaces( + workspaces: DashboardSidebarWorkspace[], + sections: DashboardSidebarSection[], +): number { + return ( + workspaces.length + + sections.reduce((sum, section) => sum + section.workspaces.length, 0) + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx index fa7da6503cf..bdb8dcdf549 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx @@ -1,23 +1,14 @@ -import { - ContextMenu, - ContextMenuContent, - ContextMenuItem, - ContextMenuTrigger, -} from "@superset/ui/context-menu"; -import { cn } from "@superset/ui/utils"; -import { AnimatePresence, motion } from "framer-motion"; import { useState } from "react"; -import { HiChevronRight } from "react-icons/hi2"; -import { LuFolderOpen, LuPencil, LuTrash2 } from "react-icons/lu"; -import { RenameInput } from "renderer/screens/main/components/WorkspaceSidebar/RenameInput"; import type { DashboardSidebarSection as DashboardSidebarSectionRecord } from "../../types"; -import { DashboardSidebarWorkspaceItem } from "../DashboardSidebarWorkspaceItem"; +import { DashboardSidebarSectionContent } from "./components/DashboardSidebarSectionContent"; +import { DashboardSidebarSectionContextMenu } from "./components/DashboardSidebarSectionContextMenu"; +import { DashboardSidebarSectionHeader } from "./components/DashboardSidebarSectionHeader"; interface DashboardSidebarSectionProps { projectId: string; section: DashboardSidebarSectionRecord; - shortcutBaseIndex: number; allSections: Array<{ id: string; name: string }>; + workspaceShortcutLabels: Map; onDelete: (sectionId: string) => void; onRename: (sectionId: string, name: string) => void; onToggleCollapse: (sectionId: string) => void; @@ -26,8 +17,8 @@ interface DashboardSidebarSectionProps { export function DashboardSidebarSection({ projectId, section, - shortcutBaseIndex, allSections, + workspaceShortcutLabels, onDelete, onRename, onToggleCollapse, @@ -35,8 +26,6 @@ export function DashboardSidebarSection({ const [isRenaming, setIsRenaming] = useState(false); const [renameValue, setRenameValue] = useState(section.name); - const workspaceIds = section.workspaces.map((workspace) => workspace.id); - const handleSubmitRename = () => { const trimmed = renameValue.trim(); if (trimmed) { @@ -52,93 +41,33 @@ export function DashboardSidebarSection({ return (
- - -
- {isRenaming ? ( - - ) : ( - - )} -
-
- - setIsRenaming(true)}> - - Rename Section - - onToggleCollapse(section.id)}> - - {section.isCollapsed ? "Expand Section" : "Collapse Section"} - - onDelete(section.id)} - className="text-destructive focus:text-destructive" - > - - Delete Section - - -
+ setIsRenaming(true)} + onToggleCollapse={() => onToggleCollapse(section.id)} + onDelete={() => onDelete(section.id)} + > + { + setRenameValue(section.name); + setIsRenaming(true); + }} + onToggleCollapse={() => onToggleCollapse(section.id)} + /> + - - {!section.isCollapsed && ( - -
- {section.workspaces.map((workspace, index) => ( - - ))} -
-
- )} -
+
); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/DashboardSidebarSectionContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/DashboardSidebarSectionContent.tsx new file mode 100644 index 00000000000..ac9d7c222f2 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/DashboardSidebarSectionContent.tsx @@ -0,0 +1,50 @@ +import { AnimatePresence, motion } from "framer-motion"; +import type { DashboardSidebarSection } from "../../../../types"; +import { DashboardSidebarWorkspaceItem } from "../../../DashboardSidebarWorkspaceItem"; + +interface DashboardSidebarSectionContentProps { + projectId: string; + section: DashboardSidebarSection; + allSections: Array<{ id: string; name: string }>; + workspaceShortcutLabels: Map; +} + +export function DashboardSidebarSectionContent({ + projectId, + section, + allSections, + workspaceShortcutLabels, +}: DashboardSidebarSectionContentProps) { + const workspaceIds = section.workspaces.map((workspace) => workspace.id); + + return ( + + {!section.isCollapsed && ( + +
+ {section.workspaces.map((workspace, index) => ( + + ))} +
+
+ )} +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/index.ts new file mode 100644 index 00000000000..db341d136c1 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarSectionContent } from "./DashboardSidebarSectionContent"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/DashboardSidebarSectionContextMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/DashboardSidebarSectionContextMenu.tsx new file mode 100644 index 00000000000..08927bd1b66 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/DashboardSidebarSectionContextMenu.tsx @@ -0,0 +1,46 @@ +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@superset/ui/context-menu"; +import { LuFolderOpen, LuPencil, LuTrash2 } from "react-icons/lu"; + +interface DashboardSidebarSectionContextMenuProps { + isCollapsed: boolean; + onRename: () => void; + onToggleCollapse: () => void; + onDelete: () => void; + children: React.ReactNode; +} + +export function DashboardSidebarSectionContextMenu({ + isCollapsed, + onRename, + onToggleCollapse, + onDelete, + children, +}: DashboardSidebarSectionContextMenuProps) { + return ( + + {children} + + + + Rename Section + + + + {isCollapsed ? "Expand Section" : "Collapse Section"} + + + + Delete Section + + + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/index.ts new file mode 100644 index 00000000000..eec632d0652 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContextMenu/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarSectionContextMenu } from "./DashboardSidebarSectionContextMenu"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionHeader/DashboardSidebarSectionHeader.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionHeader/DashboardSidebarSectionHeader.tsx new file mode 100644 index 00000000000..5e40981f882 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionHeader/DashboardSidebarSectionHeader.tsx @@ -0,0 +1,63 @@ +import { cn } from "@superset/ui/utils"; +import { HiChevronRight } from "react-icons/hi2"; +import { RenameInput } from "renderer/screens/main/components/WorkspaceSidebar/RenameInput"; +import type { DashboardSidebarSection } from "../../../../types"; + +interface DashboardSidebarSectionHeaderProps { + section: DashboardSidebarSection; + isRenaming: boolean; + renameValue: string; + onRenameValueChange: (value: string) => void; + onSubmitRename: () => void; + onCancelRename: () => void; + onStartRename: () => void; + onToggleCollapse: () => void; +} + +export function DashboardSidebarSectionHeader({ + section, + isRenaming, + renameValue, + onRenameValueChange, + onSubmitRename, + onCancelRename, + onStartRename, + onToggleCollapse, +}: DashboardSidebarSectionHeaderProps) { + return ( +
+ {isRenaming ? ( + + ) : ( + + )} +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionHeader/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionHeader/index.ts new file mode 100644 index 00000000000..c69ccdfb79d --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionHeader/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarSectionHeader } from "./DashboardSidebarSectionHeader"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx index f1a49c0ee6b..ca451712896 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx @@ -1,17 +1,9 @@ -import { toast } from "@superset/ui/sonner"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; -import { cn } from "@superset/ui/utils"; -import { useMatchRoute, useNavigate } from "@tanstack/react-router"; -import { useState } from "react"; -import { GoGitBranch } from "react-icons/go"; -import { apiTrpcClient } from "renderer/lib/api-trpc-client"; -import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; -import { RenameInput } from "renderer/screens/main/components/WorkspaceSidebar/RenameInput"; import { useDashboardSidebarWorkspaceDnD } from "../../hooks/useDashboardSidebarWorkspaceDnD"; import { DashboardSidebarDeleteDialog } from "../DashboardSidebarDeleteDialog"; +import { DashboardSidebarCollapsedWorkspaceButton } from "./components/DashboardSidebarCollapsedWorkspaceButton"; +import { DashboardSidebarExpandedWorkspaceRow } from "./components/DashboardSidebarExpandedWorkspaceRow"; import { DashboardSidebarWorkspaceContextMenu } from "./components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu"; - -const MAX_KEYBOARD_SHORTCUT_INDEX = 9; +import { useDashboardSidebarWorkspaceItemActions } from "./hooks/useDashboardSidebarWorkspaceItemActions"; interface DashboardSidebarWorkspaceItemProps { id: string; @@ -22,7 +14,7 @@ interface DashboardSidebarWorkspaceItemProps { index: number; workspaceIds: string[]; sections?: { id: string; name: string }[]; - shortcutIndex?: number; + shortcutLabel?: string; isCollapsed?: boolean; } @@ -35,18 +27,30 @@ export function DashboardSidebarWorkspaceItem({ index, workspaceIds, sections = [], - shortcutIndex, + shortcutLabel, isCollapsed = false, }: DashboardSidebarWorkspaceItemProps) { - const navigate = useNavigate(); - const matchRoute = useMatchRoute(); - const { createSection, moveWorkspaceToSection, removeWorkspaceFromSidebar } = - useDashboardSidebarState(); - - const [isRenaming, setIsRenaming] = useState(false); - const [renameValue, setRenameValue] = useState(name); - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); + const { + cancelRename, + handleClick, + handleCreateSection, + handleDelete, + isActive, + isDeleteDialogOpen, + isDeleting, + isRenaming, + moveWorkspaceToSection, + removeWorkspaceFromSidebar, + renameValue, + setIsDeleteDialogOpen, + setRenameValue, + startRename, + submitRename, + } = useDashboardSidebarWorkspaceItemActions({ + workspaceId: id, + projectId, + workspaceName: name, + }); const { isDragging, drag, drop } = useDashboardSidebarWorkspaceDnD({ workspaceId: id, @@ -56,77 +60,13 @@ export function DashboardSidebarWorkspaceItem({ workspaceIds, }); - const isActive = !!matchRoute({ - to: "/v2-workspace/$workspaceId", - params: { workspaceId: id }, - fuzzy: true, - }); - - const showBranch = !!name && name !== branch; - - const handleClick = () => { - if (isRenaming) return; - navigate({ - to: "/v2-workspace/$workspaceId", - params: { workspaceId: id }, - }); - }; - - const startRename = () => { - setRenameValue(name); - setIsRenaming(true); - }; - - const submitRename = async () => { - setIsRenaming(false); - const trimmed = renameValue.trim(); - if (!trimmed || trimmed === name) return; - try { - await apiTrpcClient.v2Workspace.update.mutate({ - id, - name: trimmed, - }); - } catch (error) { - toast.error( - `Failed to rename: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } - }; - - const cancelRename = () => { - setIsRenaming(false); - setRenameValue(name); - }; - - const handleDelete = async () => { - setIsDeleting(true); - try { - await apiTrpcClient.v2Workspace.delete.mutate({ id }); - removeWorkspaceFromSidebar(id); - setIsDeleteDialogOpen(false); - toast.success("Workspace deleted"); - if (isActive) { - navigate({ to: "/" }); - } - } catch (error) { - toast.error( - `Failed to delete: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - } finally { - setIsDeleting(false); - } - }; - if (isCollapsed) { return ( <> { - const newSectionId = createSection(projectId); - moveWorkspaceToSection(id, projectId, newSectionId); - }} + onCreateSection={handleCreateSection} onMoveToSection={(targetSectionId) => moveWorkspaceToSection(id, projectId, targetSectionId) } @@ -134,38 +74,16 @@ export function DashboardSidebarWorkspaceItem({ onRename={startRename} onDelete={() => setIsDeleteDialogOpen(true)} > - - - - - - {name || branch} - {showBranch && ( - - {branch} - - )} - - + { + drag(drop(node)); + }} + /> { - const newSectionId = createSection(projectId); - moveWorkspaceToSection(id, projectId, newSectionId); - }} + onCreateSection={handleCreateSection} onMoveToSection={(targetSectionId) => moveWorkspaceToSection(id, projectId, targetSectionId) } @@ -196,65 +111,22 @@ export function DashboardSidebarWorkspaceItem({ onRename={startRename} onDelete={() => setIsDeleteDialogOpen(true)} > - + /> void; + setDragHandle: (node: HTMLButtonElement | null) => void; +} + +export function DashboardSidebarCollapsedWorkspaceButton({ + name, + branch, + isActive, + isDragging, + onClick, + setDragHandle, +}: DashboardSidebarCollapsedWorkspaceButtonProps) { + const showBranch = !!name && name !== branch; + + return ( + + + + + + {name || branch} + {showBranch && ( + + {branch} + + )} + + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/index.ts new file mode 100644 index 00000000000..c9c878972a8 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarCollapsedWorkspaceButton } from "./DashboardSidebarCollapsedWorkspaceButton"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx new file mode 100644 index 00000000000..0ea0fc72e7f --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx @@ -0,0 +1,92 @@ +import { cn } from "@superset/ui/utils"; +import { RenameInput } from "renderer/screens/main/components/WorkspaceSidebar/RenameInput"; + +interface DashboardSidebarExpandedWorkspaceRowProps { + name: string; + branch: string; + isActive: boolean; + isDragging: boolean; + isRenaming: boolean; + renameValue: string; + shortcutLabel?: string; + onClick: () => void; + onRenameValueChange: (value: string) => void; + onSubmitRename: () => void; + onCancelRename: () => void; + setDragHandle: (node: HTMLButtonElement | null) => void; +} + +export function DashboardSidebarExpandedWorkspaceRow({ + name, + branch, + isActive, + isDragging, + isRenaming, + renameValue, + shortcutLabel, + onClick, + onRenameValueChange, + onSubmitRename, + onCancelRename, + setDragHandle, +}: DashboardSidebarExpandedWorkspaceRowProps) { + const showBranch = !!name && name !== branch; + + return ( + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/index.ts new file mode 100644 index 00000000000..77958b03925 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarExpandedWorkspaceRow } from "./DashboardSidebarExpandedWorkspaceRow"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/index.ts new file mode 100644 index 00000000000..8034eec251f --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/index.ts @@ -0,0 +1 @@ +export { useDashboardSidebarWorkspaceItemActions } from "./useDashboardSidebarWorkspaceItemActions"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts new file mode 100644 index 00000000000..ff46dca538a --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -0,0 +1,109 @@ +import { toast } from "@superset/ui/sonner"; +import { useMatchRoute, useNavigate } from "@tanstack/react-router"; +import { useState } from "react"; +import { apiTrpcClient } from "renderer/lib/api-trpc-client"; +import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; + +interface UseDashboardSidebarWorkspaceItemActionsOptions { + workspaceId: string; + projectId: string; + workspaceName: string; +} + +export function useDashboardSidebarWorkspaceItemActions({ + workspaceId, + projectId, + workspaceName, +}: UseDashboardSidebarWorkspaceItemActionsOptions) { + const navigate = useNavigate(); + const matchRoute = useMatchRoute(); + const { createSection, moveWorkspaceToSection, removeWorkspaceFromSidebar } = + useDashboardSidebarState(); + + const [isRenaming, setIsRenaming] = useState(false); + const [renameValue, setRenameValue] = useState(workspaceName); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + + const isActive = !!matchRoute({ + to: "/v2-workspace/$workspaceId", + params: { workspaceId }, + fuzzy: true, + }); + + const handleClick = () => { + if (isRenaming) return; + navigate({ + to: "/v2-workspace/$workspaceId", + params: { workspaceId }, + }); + }; + + const startRename = () => { + setRenameValue(workspaceName); + setIsRenaming(true); + }; + + const cancelRename = () => { + setIsRenaming(false); + setRenameValue(workspaceName); + }; + + const submitRename = async () => { + setIsRenaming(false); + const trimmed = renameValue.trim(); + if (!trimmed || trimmed === workspaceName) return; + try { + await apiTrpcClient.v2Workspace.update.mutate({ + id: workspaceId, + name: trimmed, + }); + } catch (error) { + toast.error( + `Failed to rename: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } + }; + + const handleDelete = async () => { + setIsDeleting(true); + try { + await apiTrpcClient.v2Workspace.delete.mutate({ id: workspaceId }); + removeWorkspaceFromSidebar(workspaceId); + setIsDeleteDialogOpen(false); + toast.success("Workspace deleted"); + if (isActive) { + navigate({ to: "/" }); + } + } catch (error) { + toast.error( + `Failed to delete: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } finally { + setIsDeleting(false); + } + }; + + const handleCreateSection = () => { + const newSectionId = createSection(projectId); + moveWorkspaceToSection(workspaceId, projectId, newSectionId); + }; + + return { + cancelRename, + handleClick, + handleCreateSection, + handleDelete, + isActive, + isDeleteDialogOpen, + isDeleting, + isRenaming, + moveWorkspaceToSection, + removeWorkspaceFromSidebar, + renameValue, + setIsDeleteDialogOpen, + setRenameValue, + startRename, + submitRename, + }; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts index 7a353a3334d..7afa448b97a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts @@ -2,11 +2,8 @@ import { useLiveQuery } from "@tanstack/react-db"; import { useMemo } from "react"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; -import type { - DashboardSidebarProject, - DashboardSidebarSection, - DashboardSidebarWorkspace, -} from "../../types"; +import type { DashboardSidebarProject } from "../../types"; +import { buildDashboardSidebarProjects } from "./utils"; export function useDashboardSidebarData() { const collections = useCollections(); @@ -47,134 +44,14 @@ export function useDashboardSidebarData() { ); const groups = useMemo(() => { - const repoOwnerMap = new Map(); - for (const repo of githubRepos) { - repoOwnerMap.set(repo.id, repo.owner); - } - - const cloudProjectsById = new Map( - projects.map((project) => [project.id, project]), - ); - const cloudWorkspacesById = new Map( - workspaces.map((workspace) => [workspace.id, workspace]), - ); - - const localSectionsByProject = new Map(); - for (const section of sidebarSections) { - const sectionsForProject = - localSectionsByProject.get(section.projectId) ?? []; - sectionsForProject.push({ - id: section.sectionId, - projectId: section.projectId, - name: section.name, - createdAt: section.createdAt, - isCollapsed: section.isCollapsed, - tabOrder: section.tabOrder, - workspaces: [], - }); - localSectionsByProject.set(section.projectId, sectionsForProject); - } - - for (const sections of localSectionsByProject.values()) { - sections.sort( - (a, b) => a.tabOrder - b.tabOrder || a.name.localeCompare(b.name), - ); - } - - const workspaceRowsByProject = new Map< - string, - DashboardSidebarWorkspace[] - >(); - const workspaceRowsBySection = new Map< - string, - DashboardSidebarWorkspace[] - >(); - - for (const localWorkspace of sidebarWorkspaces) { - const workspace = cloudWorkspacesById.get(localWorkspace.workspaceId); - if (!workspace) continue; - - const sidebarWorkspace: DashboardSidebarWorkspace = { - id: workspace.id, - projectId: workspace.projectId, - deviceId: workspace.deviceId, - name: workspace.name, - branch: workspace.branch, - createdAt: workspace.createdAt, - updatedAt: workspace.updatedAt, - }; - - if (localWorkspace.sectionId) { - const sectionWorkspaces = - workspaceRowsBySection.get(localWorkspace.sectionId) ?? []; - sectionWorkspaces.push(sidebarWorkspace); - workspaceRowsBySection.set(localWorkspace.sectionId, sectionWorkspaces); - continue; - } - - const projectWorkspaces = - workspaceRowsByProject.get(localWorkspace.projectId) ?? []; - projectWorkspaces.push(sidebarWorkspace); - workspaceRowsByProject.set(localWorkspace.projectId, projectWorkspaces); - } - - const localWorkspaceOrder = new Map( - sidebarWorkspaces.map((workspace) => [ - workspace.workspaceId, - workspace.tabOrder, - ]), - ); - - for (const rows of workspaceRowsByProject.values()) { - rows.sort( - (a, b) => - (localWorkspaceOrder.get(a.id) ?? 0) - - (localWorkspaceOrder.get(b.id) ?? 0) || - a.name.localeCompare(b.name), - ); - } - - for (const rows of workspaceRowsBySection.values()) { - rows.sort( - (a, b) => - (localWorkspaceOrder.get(a.id) ?? 0) - - (localWorkspaceOrder.get(b.id) ?? 0) || - a.name.localeCompare(b.name), - ); - } - - const resolvedProjects: DashboardSidebarProject[] = []; - - for (const localProject of [...sidebarProjects].sort( - (a, b) => a.tabOrder - b.tabOrder, - )) { - const project = cloudProjectsById.get(localProject.projectId); - if (!project) continue; - - const projectSections = ( - localSectionsByProject.get(project.id) ?? [] - ).map((section) => ({ - ...section, - workspaces: workspaceRowsBySection.get(section.id) ?? [], - })); - - const repoId = project.githubRepositoryId ?? null; - - resolvedProjects.push({ - id: project.id, - name: project.name, - slug: project.slug, - githubRepositoryId: repoId, - githubOwner: repoId ? (repoOwnerMap.get(repoId) ?? null) : null, - createdAt: project.createdAt, - updatedAt: project.updatedAt, - isCollapsed: localProject.isCollapsed, - workspaces: workspaceRowsByProject.get(project.id) ?? [], - sections: projectSections, - }); - } - - return resolvedProjects; + return buildDashboardSidebarProjects({ + githubRepos, + projects, + sidebarProjects, + sidebarSections, + sidebarWorkspaces, + workspaces, + }); }, [ githubRepos, projects, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/utils/buildDashboardSidebarProjects.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/utils/buildDashboardSidebarProjects.ts new file mode 100644 index 00000000000..ee15ebcf805 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/utils/buildDashboardSidebarProjects.ts @@ -0,0 +1,175 @@ +import type { + DashboardSidebarProject, + DashboardSidebarSection, + DashboardSidebarWorkspace, +} from "../../../types"; + +interface BuildDashboardSidebarProjectsOptions { + githubRepos: Array<{ id: string; owner: string }>; + projects: Array<{ + id: string; + name: string; + slug: string; + githubRepositoryId: string | null; + createdAt: Date; + updatedAt: Date; + }>; + sidebarProjects: Array<{ + projectId: string; + isCollapsed: boolean; + tabOrder: number; + }>; + sidebarSections: Array<{ + sectionId: string; + projectId: string; + name: string; + createdAt: Date; + isCollapsed: boolean; + tabOrder: number; + }>; + sidebarWorkspaces: Array<{ + workspaceId: string; + projectId: string; + tabOrder: number; + sectionId: string | null; + }>; + workspaces: Array<{ + id: string; + projectId: string; + deviceId: string; + name: string; + branch: string; + createdAt: Date; + updatedAt: Date; + }>; +} + +export function buildDashboardSidebarProjects({ + githubRepos, + projects, + sidebarProjects, + sidebarSections, + sidebarWorkspaces, + workspaces, +}: BuildDashboardSidebarProjectsOptions): DashboardSidebarProject[] { + const repoOwnerMap = new Map(); + for (const repo of githubRepos) { + repoOwnerMap.set(repo.id, repo.owner); + } + + const cloudProjectsById = new Map( + projects.map((project) => [project.id, project]), + ); + const cloudWorkspacesById = new Map( + workspaces.map((workspace) => [workspace.id, workspace]), + ); + + const localSectionsByProject = new Map(); + for (const section of sidebarSections) { + const sectionsForProject = + localSectionsByProject.get(section.projectId) ?? []; + sectionsForProject.push({ + id: section.sectionId, + projectId: section.projectId, + name: section.name, + createdAt: section.createdAt, + isCollapsed: section.isCollapsed, + tabOrder: section.tabOrder, + workspaces: [], + }); + localSectionsByProject.set(section.projectId, sectionsForProject); + } + + for (const sections of localSectionsByProject.values()) { + sections.sort( + (a, b) => a.tabOrder - b.tabOrder || a.name.localeCompare(b.name), + ); + } + + const workspaceRowsByProject = new Map(); + const workspaceRowsBySection = new Map(); + + for (const localWorkspace of sidebarWorkspaces) { + const workspace = cloudWorkspacesById.get(localWorkspace.workspaceId); + if (!workspace) continue; + + const sidebarWorkspace: DashboardSidebarWorkspace = { + id: workspace.id, + projectId: workspace.projectId, + deviceId: workspace.deviceId, + name: workspace.name, + branch: workspace.branch, + createdAt: workspace.createdAt, + updatedAt: workspace.updatedAt, + }; + + if (localWorkspace.sectionId) { + const sectionWorkspaces = + workspaceRowsBySection.get(localWorkspace.sectionId) ?? []; + sectionWorkspaces.push(sidebarWorkspace); + workspaceRowsBySection.set(localWorkspace.sectionId, sectionWorkspaces); + continue; + } + + const projectWorkspaces = + workspaceRowsByProject.get(localWorkspace.projectId) ?? []; + projectWorkspaces.push(sidebarWorkspace); + workspaceRowsByProject.set(localWorkspace.projectId, projectWorkspaces); + } + + const localWorkspaceOrder = new Map( + sidebarWorkspaces.map((workspace) => [ + workspace.workspaceId, + workspace.tabOrder, + ]), + ); + + for (const rows of workspaceRowsByProject.values()) { + rows.sort( + (a, b) => + (localWorkspaceOrder.get(a.id) ?? 0) - + (localWorkspaceOrder.get(b.id) ?? 0) || a.name.localeCompare(b.name), + ); + } + + for (const rows of workspaceRowsBySection.values()) { + rows.sort( + (a, b) => + (localWorkspaceOrder.get(a.id) ?? 0) - + (localWorkspaceOrder.get(b.id) ?? 0) || a.name.localeCompare(b.name), + ); + } + + const resolvedProjects: DashboardSidebarProject[] = []; + + for (const localProject of [...sidebarProjects].sort( + (a, b) => a.tabOrder - b.tabOrder, + )) { + const project = cloudProjectsById.get(localProject.projectId); + if (!project) continue; + + const projectSections = (localSectionsByProject.get(project.id) ?? []).map( + (section) => ({ + ...section, + workspaces: workspaceRowsBySection.get(section.id) ?? [], + }), + ); + + const repoId = project.githubRepositoryId ?? null; + + resolvedProjects.push({ + id: project.id, + name: project.name, + slug: project.slug, + githubRepositoryId: repoId, + githubOwner: repoId ? (repoOwnerMap.get(repoId) ?? null) : null, + createdAt: project.createdAt, + updatedAt: project.updatedAt, + isCollapsed: localProject.isCollapsed, + workspaces: workspaceRowsByProject.get(project.id) ?? [], + sections: projectSections, + }); + } + + return resolvedProjects; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/utils/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/utils/index.ts new file mode 100644 index 00000000000..87a1f8b98c1 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/utils/index.ts @@ -0,0 +1 @@ +export { buildDashboardSidebarProjects } from "./buildDashboardSidebarProjects"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts index 3cb824c9038..69b7f8ec82a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts @@ -2,22 +2,17 @@ import { useNavigate } from "@tanstack/react-router"; import { useCallback } from "react"; import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { useAppHotkey } from "renderer/stores/hotkeys"; -import type { DashboardSidebarProject } from "../../types"; +import type { DashboardSidebarWorkspace } from "../../types"; /** * Keyboard shortcuts for V2 workspace switching (⌘1-9). * Mirrors the legacy useWorkspaceShortcuts hook but for V2 workspaces. */ export function useDashboardSidebarShortcuts( - groups: DashboardSidebarProject[], + allWorkspaces: DashboardSidebarWorkspace[], ) { const navigate = useNavigate(); - const allWorkspaces = groups.flatMap((group) => [ - ...group.workspaces, - ...group.sections.flatMap((section) => section.workspaces), - ]); - const switchToWorkspace = useCallback( (index: number) => { const workspace = allWorkspaces[index]; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/flattenDashboardSidebarWorkspaces.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/flattenDashboardSidebarWorkspaces.ts new file mode 100644 index 00000000000..cc3bab2b141 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/flattenDashboardSidebarWorkspaces.ts @@ -0,0 +1,13 @@ +import type { + DashboardSidebarProject, + DashboardSidebarWorkspace, +} from "../types"; + +export function flattenDashboardSidebarWorkspaces( + groups: DashboardSidebarProject[], +): DashboardSidebarWorkspace[] { + return groups.flatMap((group) => [ + ...group.workspaces, + ...group.sections.flatMap((section) => section.workspaces), + ]); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getWorkspaceShortcutLabels.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getWorkspaceShortcutLabels.ts new file mode 100644 index 00000000000..9c63fb4bf77 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/utils/getWorkspaceShortcutLabels.ts @@ -0,0 +1,13 @@ +import type { DashboardSidebarWorkspace } from "../types"; + +const MAX_SHORTCUT_COUNT = 9; + +export function getWorkspaceShortcutLabels( + workspaces: DashboardSidebarWorkspace[], +): Map { + return new Map( + workspaces + .slice(0, MAX_SHORTCUT_COUNT) + .map((workspace, index) => [workspace.id, `⌘${index + 1}`]), + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/DashboardNewWorkspaceForm.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/DashboardNewWorkspaceForm.tsx index 5ac19be5aa4..eceb80460a2 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/DashboardNewWorkspaceForm.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/DashboardNewWorkspaceForm.tsx @@ -1,23 +1,9 @@ -import { Command, CommandInput, CommandList } from "@superset/ui/command"; -import { Tabs, TabsList, TabsTrigger } from "@superset/ui/tabs"; -import { eq } from "@tanstack/db"; -import { useLiveQuery } from "@tanstack/react-db"; -import { useEffect, useMemo, useRef } from "react"; -import { electronTrpc } from "renderer/lib/electron-trpc"; -import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; -import { - type DashboardNewWorkspaceTab, - useDashboardNewWorkspaceDraft, -} from "../../DashboardNewWorkspaceDraftContext"; -import { BranchesGroup } from "./components/BranchesGroup"; -import { DevicePicker } from "./components/DevicePicker"; -import { IssuesGroup } from "./components/IssuesGroup"; -import { ProjectSelector } from "./components/ProjectSelector"; -import { PromptGroup } from "./components/PromptGroup"; -import { PullRequestsGroup } from "./components/PullRequestsGroup"; - -const COMMAND_CLASS_NAME = - "[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5 flex h-full w-full flex-1 flex-col overflow-hidden rounded-none"; +import { useDashboardNewWorkspaceDraft } from "../../DashboardNewWorkspaceDraftContext"; +import { DashboardNewWorkspaceFormHeader } from "./components/DashboardNewWorkspaceFormHeader"; +import { DashboardNewWorkspaceListTabContent } from "./components/DashboardNewWorkspaceListTabContent"; +import { DashboardNewWorkspacePromptTabContent } from "./components/DashboardNewWorkspacePromptTabContent"; +import { useDashboardNewWorkspaceProjectSelection } from "./hooks/useDashboardNewWorkspaceProjectSelection"; +import { useResolvedLocalProject } from "./hooks/useResolvedLocalProject"; interface DashboardNewWorkspaceFormProps { isOpen: boolean; @@ -30,106 +16,18 @@ export function DashboardNewWorkspaceForm({ preSelectedProjectId, }: DashboardNewWorkspaceFormProps) { const { draft, updateDraft } = useDashboardNewWorkspaceDraft(); - const collections = useCollections(); - - // Get all v2 projects - const { data: v2ProjectsData } = useLiveQuery( - (q) => - q - .from({ projects: collections.v2Projects }) - .select(({ projects }) => ({ ...projects })), - [collections], - ); - const v2Projects = useMemo(() => v2ProjectsData ?? [], [v2ProjectsData]); - const areV2ProjectsReady = v2ProjectsData !== undefined; - - const appliedPreSelectionRef = useRef(null); - - useEffect(() => { - if (!isOpen) { - appliedPreSelectionRef.current = null; - } - }, [isOpen]); - - // Auto-select first v2 project when modal opens - useEffect(() => { - if (!isOpen) return; - - // Only use preSelectedProjectId if it matches an actual v2 project - if ( - preSelectedProjectId && - preSelectedProjectId !== appliedPreSelectionRef.current - ) { - if (!areV2ProjectsReady) return; - const hasPreSelectedProject = v2Projects.some( - (project) => project.id === preSelectedProjectId, - ); - if (hasPreSelectedProject) { - appliedPreSelectionRef.current = preSelectedProjectId; - if (preSelectedProjectId !== draft.selectedProjectId) { - updateDraft({ selectedProjectId: preSelectedProjectId }); - } - return; - } - } - - if (!areV2ProjectsReady) return; - - const hasSelectedProject = v2Projects.some( - (project) => project.id === draft.selectedProjectId, - ); - if (!hasSelectedProject) { - updateDraft({ selectedProjectId: v2Projects[0]?.id ?? null }); - } - }, [ - draft.selectedProjectId, - areV2ProjectsReady, - isOpen, - preSelectedProjectId, - v2Projects, - updateDraft, - ]); - - // Find selected v2 project - const selectedV2Project = v2Projects.find( - (p) => p.id === draft.selectedProjectId, - ); - - const githubRepositoryId = selectedV2Project?.githubRepositoryId ?? null; - - // Look up github repo details from Electric collection - const { data: githubRepoData } = useLiveQuery( - (q) => - q - .from({ repos: collections.githubRepositories }) - .where(({ repos }) => eq(repos.id, githubRepositoryId ?? "")) - .select(({ repos }) => ({ - id: repos.id, - owner: repos.owner, - name: repos.name, - })), - [collections, githubRepositoryId], - ); - const githubRepo = githubRepoData?.[0] ?? null; - - // Get all local projects to resolve v2 project -> local project - const { data: localProjects = [] } = - electronTrpc.projects.getRecents.useQuery(); - - // Resolve: match local project by github owner + repo name (or directory basename) - const resolvedLocalProjectId = useMemo(() => { - if (!githubRepo) return null; - const match = localProjects.find((lp) => { - if (lp.githubOwner !== githubRepo.owner) return false; - if (lp.name === githubRepo.name) return true; - // Fallback: check directory basename in case user renamed the project - const dirName = lp.mainRepoPath?.split("/").pop(); - return dirName === githubRepo.name; + const { githubRepository, githubRepositoryId } = + useDashboardNewWorkspaceProjectSelection({ + isOpen, + preSelectedProjectId, + selectedProjectId: draft.selectedProjectId, + onSelectProject: (selectedProjectId) => + updateDraft({ selectedProjectId }), }); - return match?.id ?? null; - }, [githubRepo, localProjects]); + const resolvedLocalProjectId = useResolvedLocalProject(githubRepository); - const isListTab = draft.activeTab !== "prompt"; + const listTab = draft.activeTab === "prompt" ? null : draft.activeTab; + const isListTab = listTab !== null; const listQuery = draft.activeTab === "issues" ? draft.issuesQuery @@ -155,80 +53,33 @@ export function DashboardNewWorkspaceForm({ return ( <> -
- - updateDraft({ activeTab: value as DashboardNewWorkspaceTab }) - } - > - - Prompt - Issues - Pull requests - Branches - - -
- updateDraft({ hostTarget })} - /> -
- - updateDraft({ selectedProjectId }) - } - /> -
-
+ updateDraft({ activeTab })} + onSelectHostTarget={(hostTarget) => updateDraft({ hostTarget })} + onSelectProject={(selectedProjectId) => + updateDraft({ selectedProjectId }) + } + /> {isListTab ? ( - - - - - {draft.activeTab === "pull-requests" && ( - - )} - {draft.activeTab === "branches" && ( - - )} - {draft.activeTab === "issues" && ( - - )} - - + ) : ( -
- -
+ )} ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/DashboardNewWorkspaceFormHeader.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/DashboardNewWorkspaceFormHeader.tsx new file mode 100644 index 00000000000..45f2c93e6be --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/DashboardNewWorkspaceFormHeader.tsx @@ -0,0 +1,52 @@ +import { Tabs, TabsList, TabsTrigger } from "@superset/ui/tabs"; +import type { WorkspaceHostTarget } from "renderer/lib/v2-workspace-host"; +import type { DashboardNewWorkspaceTab } from "../../../../DashboardNewWorkspaceDraftContext"; +import { DevicePicker } from "../DevicePicker"; +import { ProjectSelector } from "../ProjectSelector"; + +interface DashboardNewWorkspaceFormHeaderProps { + activeTab: DashboardNewWorkspaceTab; + hostTarget: WorkspaceHostTarget; + selectedProjectId: string | null; + onSelectTab: (tab: DashboardNewWorkspaceTab) => void; + onSelectHostTarget: (hostTarget: WorkspaceHostTarget) => void; + onSelectProject: (projectId: string) => void; +} + +export function DashboardNewWorkspaceFormHeader({ + activeTab, + hostTarget, + selectedProjectId, + onSelectTab, + onSelectHostTarget, + onSelectProject, +}: DashboardNewWorkspaceFormHeaderProps) { + return ( +
+ + onSelectTab(value as DashboardNewWorkspaceTab) + } + > + + Prompt + Issues + Pull requests + Branches + + +
+ +
+ +
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/index.ts new file mode 100644 index 00000000000..f4469410524 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/index.ts @@ -0,0 +1 @@ +export { DashboardNewWorkspaceFormHeader } from "./DashboardNewWorkspaceFormHeader"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/DashboardNewWorkspaceListTabContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/DashboardNewWorkspaceListTabContent.tsx new file mode 100644 index 00000000000..d6584e9ecc9 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/DashboardNewWorkspaceListTabContent.tsx @@ -0,0 +1,65 @@ +import { Command, CommandInput, CommandList } from "@superset/ui/command"; +import type { WorkspaceHostTarget } from "renderer/lib/v2-workspace-host"; +import type { DashboardNewWorkspaceTab } from "../../../../DashboardNewWorkspaceDraftContext"; +import { BranchesGroup } from "../BranchesGroup"; +import { IssuesGroup } from "../IssuesGroup"; +import { PullRequestsGroup } from "../PullRequestsGroup"; + +const COMMAND_CLASS_NAME = + "[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5 flex h-full w-full flex-1 flex-col overflow-hidden rounded-none"; + +interface DashboardNewWorkspaceListTabContentProps { + activeTab: Exclude; + projectId: string | null; + githubRepositoryId: string | null; + hostTarget: WorkspaceHostTarget; + localProjectId: string | null; + query: string; + onQueryChange: (value: string) => void; +} + +export function DashboardNewWorkspaceListTabContent({ + activeTab, + projectId, + githubRepositoryId, + hostTarget, + localProjectId, + query, + onQueryChange, +}: DashboardNewWorkspaceListTabContentProps) { + return ( + + + + + {activeTab === "pull-requests" && ( + + )} + {activeTab === "branches" && ( + + )} + {activeTab === "issues" && ( + + )} + + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/index.ts new file mode 100644 index 00000000000..af9feb38a8c --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/index.ts @@ -0,0 +1 @@ +export { DashboardNewWorkspaceListTabContent } from "./DashboardNewWorkspaceListTabContent"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/DashboardNewWorkspacePromptTabContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/DashboardNewWorkspacePromptTabContent.tsx new file mode 100644 index 00000000000..b1ff2609f7b --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/DashboardNewWorkspacePromptTabContent.tsx @@ -0,0 +1,24 @@ +import type { WorkspaceHostTarget } from "renderer/lib/v2-workspace-host"; +import { PromptGroup } from "../PromptGroup"; + +interface DashboardNewWorkspacePromptTabContentProps { + projectId: string | null; + localProjectId: string | null; + hostTarget: WorkspaceHostTarget; +} + +export function DashboardNewWorkspacePromptTabContent({ + projectId, + localProjectId, + hostTarget, +}: DashboardNewWorkspacePromptTabContentProps) { + return ( +
+ +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/index.ts new file mode 100644 index 00000000000..0dd4c4cbf1a --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/index.ts @@ -0,0 +1 @@ +export { DashboardNewWorkspacePromptTabContent } from "./DashboardNewWorkspacePromptTabContent"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/PromptGroup.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/PromptGroup.tsx index b6809cb0917..38843d7dc4a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/PromptGroup.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/PromptGroup.tsx @@ -4,7 +4,6 @@ import { toast } from "@superset/ui/sonner"; import { Textarea } from "@superset/ui/textarea"; import { useNavigate } from "@tanstack/react-router"; import { useEffect, useMemo, useRef, useState } from "react"; -import { PromptGroupAdvancedOptions } from "renderer/components/NewWorkspaceModal/components/PromptGroup/components/PromptGroupAdvancedOptions"; import { electronTrpc } from "renderer/lib/electron-trpc"; import type { WorkspaceHostTarget } from "renderer/lib/v2-workspace-host"; import { resolveEffectiveWorkspaceBaseBranch } from "renderer/lib/workspaceBaseBranch"; @@ -15,6 +14,7 @@ import { } from "shared/utils/branch"; import { useDashboardNewWorkspaceDraft } from "../../../../DashboardNewWorkspaceDraftContext"; import { useCreateDashboardWorkspace } from "../../../../hooks/useCreateDashboardWorkspace"; +import { PromptGroupAdvancedOptions } from "./components/PromptGroupAdvancedOptions"; interface PromptGroupProps { projectId: string | null; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/components/PromptGroupAdvancedOptions/PromptGroupAdvancedOptions.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/components/PromptGroupAdvancedOptions/PromptGroupAdvancedOptions.tsx new file mode 100644 index 00000000000..255728a23da --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/components/PromptGroupAdvancedOptions/PromptGroupAdvancedOptions.tsx @@ -0,0 +1,215 @@ +import { Button } from "@superset/ui/button"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@superset/ui/collapsible"; +import { + Command, + CommandEmpty, + CommandInput, + CommandItem, + CommandList, +} from "@superset/ui/command"; +import { Input } from "@superset/ui/input"; +import { Label } from "@superset/ui/label"; +import { Popover, PopoverContent, PopoverTrigger } from "@superset/ui/popover"; +import { Switch } from "@superset/ui/switch"; +import { GoGitBranch } from "react-icons/go"; +import { + HiCheck, + HiChevronDown, + HiChevronUpDown, + HiOutlinePencil, +} from "react-icons/hi2"; +import { formatRelativeTime } from "renderer/lib/formatRelativeTime"; + +interface PromptGroupAdvancedOptionsProps { + showAdvanced: boolean; + onShowAdvancedChange: (open: boolean) => void; + branchInputValue: string; + onBranchInputChange: (value: string) => void; + onBranchInputBlur: () => void; + onEditPrefix: () => void; + runSetupScript: boolean; + onRunSetupScriptChange: (checked: boolean) => void; + shortcutHint?: string; + hideSetupScript?: boolean; + isBranchesError?: boolean; + isBranchesLoading?: boolean; + baseBranchOpen?: boolean; + onBaseBranchOpenChange?: (open: boolean) => void; + effectiveBaseBranch?: string | null; + defaultBranch?: string; + branchSearch?: string; + onBranchSearchChange?: (search: string) => void; + filteredBranches?: Array<{ name: string; lastCommitDate: number }>; + onSelectBaseBranch?: (branchName: string) => void; +} + +export function PromptGroupAdvancedOptions({ + showAdvanced, + onShowAdvancedChange, + branchInputValue, + onBranchInputChange, + onBranchInputBlur, + onEditPrefix, + runSetupScript, + onRunSetupScriptChange, + shortcutHint, + hideSetupScript, + isBranchesError, + isBranchesLoading, + baseBranchOpen, + onBaseBranchOpenChange, + effectiveBaseBranch, + defaultBranch, + branchSearch, + onBranchSearchChange, + filteredBranches, + onSelectBaseBranch, +}: PromptGroupAdvancedOptionsProps) { + const showBaseBranch = onBaseBranchOpenChange != null; + + return ( + +
+ + + Advanced options + + {shortcutHint && ( + + {shortcutHint} + + )} +
+ +
+
+ + +
+ onBranchInputChange(event.target.value)} + onBlur={onBranchInputBlur} + /> +
+ + {showBaseBranch && ( +
+ Base branch + {isBranchesError ? ( +
+ Failed to load branches +
+ ) : ( + + + + + event.stopPropagation()} + > + + + + No branches found + {filteredBranches?.map((branch) => ( + onSelectBaseBranch?.(branch.name)} + className="flex items-center justify-between" + > + + + {branch.name} + {branch.name === defaultBranch && ( + + default + + )} + + + {branch.lastCommitDate > 0 && ( + + {formatRelativeTime(branch.lastCommitDate)} + + )} + {effectiveBaseBranch === branch.name && ( + + )} + + + ))} + + + + + )} +
+ )} + + {!hideSetupScript && ( +
+ + +
+ )} +
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/components/PromptGroupAdvancedOptions/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/components/PromptGroupAdvancedOptions/index.ts new file mode 100644 index 00000000000..56182b77e11 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/components/PromptGroupAdvancedOptions/index.ts @@ -0,0 +1 @@ +export { PromptGroupAdvancedOptions } from "./PromptGroupAdvancedOptions"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useDashboardNewWorkspaceProjectSelection/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useDashboardNewWorkspaceProjectSelection/index.ts new file mode 100644 index 00000000000..0119a16a1b6 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useDashboardNewWorkspaceProjectSelection/index.ts @@ -0,0 +1 @@ +export { useDashboardNewWorkspaceProjectSelection } from "./useDashboardNewWorkspaceProjectSelection"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useDashboardNewWorkspaceProjectSelection/useDashboardNewWorkspaceProjectSelection.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useDashboardNewWorkspaceProjectSelection/useDashboardNewWorkspaceProjectSelection.ts new file mode 100644 index 00000000000..1297b6f3814 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useDashboardNewWorkspaceProjectSelection/useDashboardNewWorkspaceProjectSelection.ts @@ -0,0 +1,99 @@ +import { eq } from "@tanstack/db"; +import { useLiveQuery } from "@tanstack/react-db"; +import { useEffect, useMemo, useRef } from "react"; +import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; + +interface UseDashboardNewWorkspaceProjectSelectionOptions { + isOpen: boolean; + preSelectedProjectId: string | null; + selectedProjectId: string | null; + onSelectProject: (projectId: string | null) => void; +} + +export function useDashboardNewWorkspaceProjectSelection({ + isOpen, + preSelectedProjectId, + selectedProjectId, + onSelectProject, +}: UseDashboardNewWorkspaceProjectSelectionOptions) { + const collections = useCollections(); + + const { data: v2ProjectsData } = useLiveQuery( + (q) => + q + .from({ projects: collections.v2Projects }) + .select(({ projects }) => ({ ...projects })), + [collections], + ); + const v2Projects = useMemo(() => v2ProjectsData ?? [], [v2ProjectsData]); + const areV2ProjectsReady = v2ProjectsData !== undefined; + + const appliedPreSelectionRef = useRef(null); + + useEffect(() => { + if (!isOpen) { + appliedPreSelectionRef.current = null; + } + }, [isOpen]); + + useEffect(() => { + if (!isOpen) return; + + if ( + preSelectedProjectId && + preSelectedProjectId !== appliedPreSelectionRef.current + ) { + if (!areV2ProjectsReady) return; + const hasPreSelectedProject = v2Projects.some( + (project) => project.id === preSelectedProjectId, + ); + if (hasPreSelectedProject) { + appliedPreSelectionRef.current = preSelectedProjectId; + if (preSelectedProjectId !== selectedProjectId) { + onSelectProject(preSelectedProjectId); + } + return; + } + } + + if (!areV2ProjectsReady) return; + + const hasSelectedProject = v2Projects.some( + (project) => project.id === selectedProjectId, + ); + if (!hasSelectedProject) { + onSelectProject(v2Projects[0]?.id ?? null); + } + }, [ + selectedProjectId, + areV2ProjectsReady, + isOpen, + onSelectProject, + preSelectedProjectId, + v2Projects, + ]); + + const selectedProject = + v2Projects.find((project) => project.id === selectedProjectId) ?? null; + const githubRepositoryId = selectedProject?.githubRepositoryId ?? null; + + const { data: githubRepoData } = useLiveQuery( + (q) => + q + .from({ repos: collections.githubRepositories }) + .where(({ repos }) => eq(repos.id, githubRepositoryId ?? "")) + .select(({ repos }) => ({ + id: repos.id, + owner: repos.owner, + name: repos.name, + })), + [collections, githubRepositoryId], + ); + + return { + githubRepository: githubRepoData?.[0] ?? null, + githubRepositoryId, + selectedProject, + v2Projects, + }; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useResolvedLocalProject/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useResolvedLocalProject/index.ts new file mode 100644 index 00000000000..dfde2a3b4c3 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useResolvedLocalProject/index.ts @@ -0,0 +1 @@ +export { useResolvedLocalProject } from "./useResolvedLocalProject"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useResolvedLocalProject/useResolvedLocalProject.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useResolvedLocalProject/useResolvedLocalProject.ts new file mode 100644 index 00000000000..f88ca411a3f --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/hooks/useResolvedLocalProject/useResolvedLocalProject.ts @@ -0,0 +1,27 @@ +import { useMemo } from "react"; +import { electronTrpc } from "renderer/lib/electron-trpc"; + +interface ResolvedGithubRepository { + owner: string; + name: string; +} + +export function useResolvedLocalProject( + githubRepository: ResolvedGithubRepository | null, +) { + const { data: localProjects = [] } = + electronTrpc.projects.getRecents.useQuery(); + + return useMemo(() => { + if (!githubRepository) return null; + const match = localProjects.find((localProject) => { + if (localProject.githubOwner !== githubRepository.owner) return false; + if (localProject.name === githubRepository.name) return true; + + const directoryName = localProject.mainRepoPath?.split("/").pop(); + return directoryName === githubRepository.name; + }); + + return match?.id ?? null; + }, [githubRepository, localProjects]); +} From 7178625cd4c2aa0a906f4e886252cc2523f81846 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 16 Mar 2026 00:09:04 -0700 Subject: [PATCH 04/14] WIP - restructuring --- .../DashboardSidebarProjectSection.tsx | 55 +++--- ...ashboardSidebarCollapsedProjectContent.tsx | 167 ++++++++-------- ...DashboardSidebarExpandedProjectContent.tsx | 38 ---- .../DashboardSidebarProjectContextMenu.tsx | 14 +- .../DashboardSidebarProjectRow.tsx | 183 ++++++++++-------- .../DashboardSidebarSection.tsx | 16 +- .../DashboardSidebarSectionContextMenu.tsx | 65 ++++++- .../DashboardSidebarSectionHeader.tsx | 109 ++++++----- .../DashboardSidebarWorkspaceItem.tsx | 33 +++- ...shboardSidebarCollapsedWorkspaceButton.tsx | 78 ++++---- .../DashboardSidebarExpandedWorkspaceRow.tsx | 159 ++++++++------- .../DashboardSidebarWorkspaceContextMenu.tsx | 12 -- .../utils/buildDashboardSidebarProjects.ts | 2 + .../components/DashboardSidebar/types.ts | 1 + .../useDashboardSidebarState.ts | 12 ++ .../dashboardSidebarLocal/schema.ts | 1 + 16 files changed, 512 insertions(+), 433 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx index 9d0e2b78f37..9cf387b99b3 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx @@ -9,6 +9,7 @@ import { DashboardSidebarDeleteDialog } from "../DashboardSidebarDeleteDialog"; import { DashboardSidebarCollapsedProjectContent } from "./components/DashboardSidebarCollapsedProjectContent"; import { DashboardSidebarExpandedProjectContent } from "./components/DashboardSidebarExpandedProjectContent"; import { DashboardSidebarProjectContextMenu } from "./components/DashboardSidebarProjectContextMenu"; +import { DashboardSidebarProjectRow } from "./components/DashboardSidebarProjectRow"; import { useDashboardSidebarProjectSectionActions } from "./hooks/useDashboardSidebarProjectSectionActions"; import { countProjectWorkspaces } from "./utils/countProjectWorkspaces"; @@ -95,7 +96,6 @@ export function DashboardSidebarProjectSection({ onRemoveFromSidebar={() => removeProjectFromSidebar(projectId)} onRename={startRename} onDelete={() => setIsDeleteDialogOpen(true)} - onNewWorkspace={handleNewWorkspace} >
{ @@ -148,34 +148,35 @@ export function DashboardSidebarProjectSection({ onRemoveFromSidebar={() => removeProjectFromSidebar(projectId)} onRename={startRename} onDelete={() => setIsDeleteDialogOpen(true)} - onNewWorkspace={handleNewWorkspace} > -
- onToggleCollapse(projectId)} - onNewWorkspace={handleNewWorkspace} - onDeleteSection={deleteSection} - onRenameSection={renameSection} - onToggleSectionCollapse={toggleSectionCollapsed} - /> -
+ onToggleCollapse(projectId)} + onNewWorkspace={handleNewWorkspace} + /> + +
{ projectId: string; projectName: string; githubOwner: string | null; @@ -19,79 +21,92 @@ interface DashboardSidebarCollapsedProjectContentProps { onToggleCollapse: () => void; } -export function DashboardSidebarCollapsedProjectContent({ - projectId, - projectName, - githubOwner, - isCollapsed, - isDragging, - totalWorkspaceCount, - workspaces, - workspaceIds, - allSections, - workspaceShortcutLabels, - onToggleCollapse, -}: DashboardSidebarCollapsedProjectContentProps) { - return ( -
- - - - - - {projectName} - - {totalWorkspaceCount} workspace - {totalWorkspaceCount !== 1 ? "s" : ""} - - - - - - {!isCollapsed && ( - -
- {workspaces.map((workspace, index) => ( - - ))} -
-
+export const DashboardSidebarCollapsedProjectContent = forwardRef< + HTMLDivElement, + DashboardSidebarCollapsedProjectContentProps +>( + ( + { + projectId, + projectName, + githubOwner, + isCollapsed, + isDragging, + totalWorkspaceCount, + workspaces, + workspaceIds, + allSections, + workspaceShortcutLabels, + onToggleCollapse, + className, + ...props + }, + ref, + ) => { + return ( +
-
- ); -} + {...props} + > + + + + + + {projectName} + + {totalWorkspaceCount} workspace + {totalWorkspaceCount !== 1 ? "s" : ""} + + + + + + {!isCollapsed && ( + +
+ {workspaces.map((workspace, index) => ( + + ))} +
+
+ )} +
+
+ ); + }, +); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx index 23a5f178061..471468fcf69 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx @@ -5,27 +5,15 @@ import type { } from "../../../../types"; import { DashboardSidebarSection as DashboardSidebarSectionComponent } from "../../../DashboardSidebarSection"; import { DashboardSidebarWorkspaceItem } from "../../../DashboardSidebarWorkspaceItem"; -import { DashboardSidebarProjectRow } from "../DashboardSidebarProjectRow"; interface DashboardSidebarExpandedProjectContentProps { projectId: string; - projectName: string; - githubOwner: string | null; isCollapsed: boolean; workspaces: DashboardSidebarWorkspace[]; sections: DashboardSidebarSection[]; topLevelWorkspaceIds: string[]; allSections: Array<{ id: string; name: string }>; workspaceShortcutLabels: Map; - totalWorkspaceCount: number; - isRenaming: boolean; - renameValue: string; - onRenameValueChange: (value: string) => void; - onSubmitRename: () => void; - onCancelRename: () => void; - onStartRename: () => void; - onToggleCollapse: () => void; - onNewWorkspace: () => void; onDeleteSection: (sectionId: string) => void; onRenameSection: (sectionId: string, name: string) => void; onToggleSectionCollapse: (sectionId: string) => void; @@ -33,44 +21,18 @@ interface DashboardSidebarExpandedProjectContentProps { export function DashboardSidebarExpandedProjectContent({ projectId, - projectName, - githubOwner, isCollapsed, workspaces, sections, topLevelWorkspaceIds, allSections, workspaceShortcutLabels, - totalWorkspaceCount, - isRenaming, - renameValue, - onRenameValueChange, - onSubmitRename, - onCancelRename, - onStartRename, - onToggleCollapse, - onNewWorkspace, onDeleteSection, onRenameSection, onToggleSectionCollapse, }: DashboardSidebarExpandedProjectContentProps) { return ( <> - - {!isCollapsed && ( void; onRename: () => void; onDelete: () => void; - onNewWorkspace: () => void; children: React.ReactNode; } @@ -30,7 +23,6 @@ export function DashboardSidebarProjectContextMenu({ onRemoveFromSidebar, onRename, onDelete, - onNewWorkspace, children, }: DashboardSidebarProjectContextMenuProps) { const handleCopyId = () => { @@ -46,10 +38,6 @@ export function DashboardSidebarProjectContextMenu({ Rename - - - New Workspace - New Section diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx index fc37bd04241..efa831f98f1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx @@ -1,10 +1,12 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; +import { type ComponentPropsWithoutRef, forwardRef } from "react"; import { HiChevronRight, HiMiniPlus } from "react-icons/hi2"; import { ProjectThumbnail } from "renderer/routes/_authenticated/components/ProjectThumbnail"; import { RenameInput } from "renderer/screens/main/components/WorkspaceSidebar/RenameInput"; -interface DashboardSidebarProjectRowProps { +interface DashboardSidebarProjectRowProps + extends ComponentPropsWithoutRef<"div"> { projectName: string; githubOwner: string | null; totalWorkspaceCount: number; @@ -19,92 +21,105 @@ interface DashboardSidebarProjectRowProps { onNewWorkspace: () => void; } -export function DashboardSidebarProjectRow({ - projectName, - githubOwner, - totalWorkspaceCount, - isCollapsed, - isRenaming, - renameValue, - onRenameValueChange, - onSubmitRename, - onCancelRename, - onStartRename, - onToggleCollapse, - onNewWorkspace, -}: DashboardSidebarProjectRowProps) { - return ( -
- {isRenaming ? ( -
- - -
- ) : ( +export const DashboardSidebarProjectRow = forwardRef< + HTMLDivElement, + DashboardSidebarProjectRowProps +>( + ( + { + projectName, + githubOwner, + totalWorkspaceCount, + isCollapsed, + isRenaming, + renameValue, + onRenameValueChange, + onSubmitRename, + onCancelRename, + onStartRename, + onToggleCollapse, + onNewWorkspace, + className, + ...props + }, + ref, + ) => { + return ( +
+ {isRenaming ? ( +
+ + +
+ ) : ( + + )} + + + + + + + New workspace + + + - )} - - - - - - - New workspace - - - - -
- ); -} +
+ ); + }, +); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx index bdb8dcdf549..0bb1887c569 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx @@ -1,4 +1,6 @@ import { useState } from "react"; +import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; +import { PROJECT_COLOR_DEFAULT } from "shared/constants/project-colors"; import type { DashboardSidebarSection as DashboardSidebarSectionRecord } from "../../types"; import { DashboardSidebarSectionContent } from "./components/DashboardSidebarSectionContent"; import { DashboardSidebarSectionContextMenu } from "./components/DashboardSidebarSectionContextMenu"; @@ -23,8 +25,16 @@ export function DashboardSidebarSection({ onRename, onToggleCollapse, }: DashboardSidebarSectionProps) { + const { setSectionColor } = useDashboardSidebarState(); const [isRenaming, setIsRenaming] = useState(false); const [renameValue, setRenameValue] = useState(section.name); + const hasColor = + section.color != null && section.color !== PROJECT_COLOR_DEFAULT; + const sectionBorderStyle = { + borderLeft: hasColor + ? `2px solid ${section.color}` + : "2px solid var(--color-border)", + }; const handleSubmitRename = () => { const trimmed = renameValue.trim(); @@ -40,11 +50,11 @@ export function DashboardSidebarSection({ }; return ( -
+
setIsRenaming(true)} - onToggleCollapse={() => onToggleCollapse(section.id)} + onSetColor={(color) => setSectionColor(section.id, color)} onDelete={() => onDelete(section.id)} > void; - onToggleCollapse: () => void; + onSetColor: (color: string | null) => void; onDelete: () => void; children: React.ReactNode; } export function DashboardSidebarSectionContextMenu({ - isCollapsed, + color, onRename, - onToggleCollapse, + onSetColor, onDelete, children, }: DashboardSidebarSectionContextMenuProps) { @@ -29,10 +39,47 @@ export function DashboardSidebarSectionContextMenu({ Rename Section - - - {isCollapsed ? "Expand Section" : "Collapse Section"} - + + + + Set Color + + + {PROJECT_COLORS.map((sectionColor) => { + const isDefault = sectionColor.value === PROJECT_COLOR_DEFAULT; + const isSelected = isDefault + ? color == null + : color === sectionColor.value; + + return ( + + onSetColor(isDefault ? null : sectionColor.value) + } + className="flex items-center gap-2" + > + + {sectionColor.name} + {isSelected && ( + + )} + + ); + })} + + + { section: DashboardSidebarSection; isRenaming: boolean; renameValue: string; @@ -14,50 +16,63 @@ interface DashboardSidebarSectionHeaderProps { onToggleCollapse: () => void; } -export function DashboardSidebarSectionHeader({ - section, - isRenaming, - renameValue, - onRenameValueChange, - onSubmitRename, - onCancelRename, - onStartRename, - onToggleCollapse, -}: DashboardSidebarSectionHeaderProps) { - return ( -
- {isRenaming ? ( - - ) : ( - - )} -
- ); -} + ) : ( + + )} +
+ ); + }, +); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx index ca451712896..1a12d0ef2f9 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx @@ -1,3 +1,4 @@ +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { useDashboardSidebarWorkspaceDnD } from "../../hooks/useDashboardSidebarWorkspaceDnD"; import { DashboardSidebarDeleteDialog } from "../DashboardSidebarDeleteDialog"; import { DashboardSidebarCollapsedWorkspaceButton } from "./components/DashboardSidebarCollapsedWorkspaceButton"; @@ -61,6 +62,8 @@ export function DashboardSidebarWorkspaceItem({ }); if (isCollapsed) { + const showBranch = !!name && name !== branch; + return ( <> setIsDeleteDialogOpen(true)} > - { - drag(drop(node)); - }} - /> + + + { + drag(drop(node)); + }} + /> + + + {name || branch} + {showBranch && ( + + {branch} + + )} + + { isActive: boolean; isDragging: boolean; - onClick: () => void; setDragHandle: (node: HTMLButtonElement | null) => void; } -export function DashboardSidebarCollapsedWorkspaceButton({ - name, - branch, - isActive, - isDragging, - onClick, - setDragHandle, -}: DashboardSidebarCollapsedWorkspaceButtonProps) { - const showBranch = !!name && name !== branch; - +export const DashboardSidebarCollapsedWorkspaceButton = forwardRef< + HTMLButtonElement, + DashboardSidebarCollapsedWorkspaceButtonProps +>(({ isActive, isDragging, setDragHandle, className, ...props }, ref) => { return ( - - - - - - {name || branch} - {showBranch && ( - - {branch} - + ); -} +}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx index 0ea0fc72e7f..9f550c5655a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx @@ -1,7 +1,9 @@ import { cn } from "@superset/ui/utils"; +import { type ComponentPropsWithoutRef, forwardRef } from "react"; import { RenameInput } from "renderer/screens/main/components/WorkspaceSidebar/RenameInput"; -interface DashboardSidebarExpandedWorkspaceRowProps { +interface DashboardSidebarExpandedWorkspaceRowProps + extends ComponentPropsWithoutRef<"button"> { name: string; branch: string; isActive: boolean; @@ -16,77 +18,96 @@ interface DashboardSidebarExpandedWorkspaceRowProps { setDragHandle: (node: HTMLButtonElement | null) => void; } -export function DashboardSidebarExpandedWorkspaceRow({ - name, - branch, - isActive, - isDragging, - isRenaming, - renameValue, - shortcutLabel, - onClick, - onRenameValueChange, - onSubmitRename, - onCancelRename, - setDragHandle, -}: DashboardSidebarExpandedWorkspaceRowProps) { - const showBranch = !!name && name !== branch; +export const DashboardSidebarExpandedWorkspaceRow = forwardRef< + HTMLButtonElement, + DashboardSidebarExpandedWorkspaceRowProps +>( + ( + { + name, + branch, + isActive, + isDragging, + isRenaming, + renameValue, + shortcutLabel, + onClick, + onRenameValueChange, + onSubmitRename, + onCancelRename, + setDragHandle, + className, + ...props + }, + ref, + ) => { + const showBranch = !!name && name !== branch; - return ( - - ); -} + + )} +
+ + ); + }, +); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx index f3ed2d07a3a..d60214ff12f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx @@ -8,10 +8,8 @@ import { ContextMenuSubTrigger, ContextMenuTrigger, } from "@superset/ui/context-menu"; -import { toast } from "@superset/ui/sonner"; import { LuArrowRightLeft, - LuCopy, LuFolderPlus, LuMinus, LuPencil, @@ -30,7 +28,6 @@ interface DashboardSidebarWorkspaceContextMenuProps { } export function DashboardSidebarWorkspaceContextMenu({ - id, sections, onCreateSection, onMoveToSection, @@ -39,11 +36,6 @@ export function DashboardSidebarWorkspaceContextMenu({ onDelete, children, }: DashboardSidebarWorkspaceContextMenuProps) { - const handleCopyId = () => { - navigator.clipboard.writeText(id); - toast.success("Workspace ID copied"); - }; - return ( {children} @@ -52,10 +44,6 @@ export function DashboardSidebarWorkspaceContextMenu({ Rename
- - - Copy ID - diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/utils/buildDashboardSidebarProjects.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/utils/buildDashboardSidebarProjects.ts index ee15ebcf805..3c88f043c85 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/utils/buildDashboardSidebarProjects.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/utils/buildDashboardSidebarProjects.ts @@ -26,6 +26,7 @@ interface BuildDashboardSidebarProjectsOptions { createdAt: Date; isCollapsed: boolean; tabOrder: number; + color: string | null; }>; sidebarWorkspaces: Array<{ workspaceId: string; @@ -75,6 +76,7 @@ export function buildDashboardSidebarProjects({ createdAt: section.createdAt, isCollapsed: section.isCollapsed, tabOrder: section.tabOrder, + color: section.color, workspaces: [], }); localSectionsByProject.set(section.projectId, sectionsForProject); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts index 43914f876e0..ae58d982b66 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts @@ -15,6 +15,7 @@ export interface DashboardSidebarSection { createdAt: Date; isCollapsed: boolean; tabOrder: number; + color: string | null; workspaces: DashboardSidebarWorkspace[]; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts b/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts index d112af2b6cb..81b3533211a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts @@ -127,6 +127,7 @@ export function useDashboardSidebarState() { createdAt: new Date(), tabOrder: getNextTabOrder(sectionOrders), isCollapsed: false, + color: null, }); return sectionId; @@ -154,6 +155,16 @@ export function useDashboardSidebarState() { [collections], ); + const setSectionColor = useCallback( + (sectionId: string, color: string | null) => { + if (!collections.v2SidebarSections.get(sectionId)) return; + collections.v2SidebarSections.update(sectionId, (draft) => { + draft.color = color; + }); + }, + [collections], + ); + const moveWorkspaceToSection = useCallback( (workspaceId: string, projectId: string, sectionId: string | null) => { const existing = collections.v2SidebarWorkspaces.get(workspaceId); @@ -251,6 +262,7 @@ export function useDashboardSidebarState() { reorderProjects, reorderWorkspaces, renameSection, + setSectionColor, toggleProjectCollapsed, toggleSectionCollapsed, }; 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 e9a16a978c4..aca3f3aa649 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 @@ -26,6 +26,7 @@ export const dashboardSidebarSectionSchema = z.object({ createdAt: persistedDateSchema, tabOrder: z.number().int().default(0), isCollapsed: z.boolean().default(false), + color: z.string().nullable().default(null), }); export type DashboardSidebarProjectRow = z.infer< From 99d3c77c4daba869e7e3c07a829e1dedce35eead Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 16 Mar 2026 01:04:02 -0700 Subject: [PATCH 05/14] WIP - restructuring --- ...DashboardSidebarExpandedProjectContent.tsx | 81 ++++--- .../DashboardSidebarSection.tsx | 2 +- .../DashboardSidebarSectionContent.tsx | 1 + .../DashboardSidebarWorkspaceItem.tsx | 31 ++- ...shboardSidebarCollapsedWorkspaceButton.tsx | 74 +++--- .../DashboardSidebarExpandedWorkspaceRow.tsx | 212 +++++++++++++++--- .../DashboardSidebarWorkspaceContextMenu.tsx | 127 +++++++---- .../DashboardSidebarWorkspaceDiffStats.tsx | 27 +++ .../index.ts | 1 + ...hboardSidebarWorkspaceHoverCardContent.tsx | 80 +++++++ .../index.ts | 1 + .../DashboardSidebarWorkspaceIcon.tsx | 53 +++++ .../DashboardSidebarWorkspaceIcon/index.ts | 1 + .../DashboardSidebarWorkspaceStatusBadge.tsx | 76 +++++++ .../index.ts | 1 + .../utils/getWorkspaceRowMocks.ts | 51 +++++ .../utils/index.ts | 2 + 17 files changed, 669 insertions(+), 152 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDiffStats/DashboardSidebarWorkspaceDiffStats.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDiffStats/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/DashboardSidebarWorkspaceHoverCardContent.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceStatusBadge/DashboardSidebarWorkspaceStatusBadge.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceStatusBadge/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/getWorkspaceRowMocks.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/index.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx index 471468fcf69..b042d65f49b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx @@ -32,46 +32,45 @@ export function DashboardSidebarExpandedProjectContent({ onToggleSectionCollapse, }: DashboardSidebarExpandedProjectContentProps) { return ( - <> - - {!isCollapsed && ( - -
- {workspaces.map((workspace, index) => ( - - ))} - {sections.map((section) => ( - - ))} -
-
- )} -
- + + {!isCollapsed && ( + +
+ {workspaces.map((workspace, index) => ( + + ))} + {sections.map((section) => ( + + ))} +
+
+ )} +
); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx index 0bb1887c569..af5c7d34056 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/DashboardSidebarSection.tsx @@ -50,7 +50,7 @@ export function DashboardSidebarSection({ }; return ( -
+
setIsRenaming(true)} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/DashboardSidebarSectionContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/DashboardSidebarSectionContent.tsx index ac9d7c222f2..c03236ef948 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/DashboardSidebarSectionContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/DashboardSidebarSectionContent.tsx @@ -30,6 +30,7 @@ export function DashboardSidebarSectionContent({
{section.workspaces.map((workspace, index) => ( + } sections={sections} onCreateSection={handleCreateSection} onMoveToSection={(targetSectionId) => @@ -82,10 +95,12 @@ export function DashboardSidebarWorkspaceItem({ { drag(drop(node)); }} + workspaceStatus={mockData.workspaceStatus} /> @@ -114,7 +129,13 @@ export function DashboardSidebarWorkspaceItem({ return ( <> + } sections={sections} onCreateSection={handleCreateSection} onMoveToSection={(targetSectionId) => @@ -125,6 +146,7 @@ export function DashboardSidebarWorkspaceItem({ onDelete={() => setIsDeleteDialogOpen(true)} > setIsDeleteDialogOpen(true)} onRenameValueChange={setRenameValue} onSubmitRename={submitRename} onCancelRename={cancelRename} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx index 2cc21b78105..9dd622a79b1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx @@ -1,44 +1,60 @@ import { cn } from "@superset/ui/utils"; import { type ComponentPropsWithoutRef, forwardRef } from "react"; -import { GoGitBranch } from "react-icons/go"; +import type { ActivePaneStatus } from "shared/tabs-types"; +import { DashboardSidebarWorkspaceIcon } from "../DashboardSidebarWorkspaceIcon"; interface DashboardSidebarCollapsedWorkspaceButtonProps extends ComponentPropsWithoutRef<"button"> { isActive: boolean; isDragging: boolean; + isUnread?: boolean; setDragHandle: (node: HTMLButtonElement | null) => void; + workspaceStatus?: ActivePaneStatus | null; } export const DashboardSidebarCollapsedWorkspaceButton = forwardRef< HTMLButtonElement, DashboardSidebarCollapsedWorkspaceButtonProps ->(({ isActive, isDragging, setDragHandle, className, ...props }, ref) => { - return ( - - ); -}); + {...props} + > + + + ); + }, +); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx index 9f550c5655a..57362e0317e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx @@ -1,9 +1,16 @@ +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; import { type ComponentPropsWithoutRef, forwardRef } from "react"; +import { HiMiniXMark } from "react-icons/hi2"; import { RenameInput } from "renderer/screens/main/components/WorkspaceSidebar/RenameInput"; +import type { WorkspaceRowMockData } from "../../utils"; +import { DashboardSidebarWorkspaceDiffStats } from "../DashboardSidebarWorkspaceDiffStats"; +import { DashboardSidebarWorkspaceIcon } from "../DashboardSidebarWorkspaceIcon"; +import { DashboardSidebarWorkspaceStatusBadge } from "../DashboardSidebarWorkspaceStatusBadge"; interface DashboardSidebarExpandedWorkspaceRowProps - extends ComponentPropsWithoutRef<"button"> { + extends ComponentPropsWithoutRef<"div"> { + accentColor?: string | null; name: string; branch: string; isActive: boolean; @@ -11,19 +18,23 @@ interface DashboardSidebarExpandedWorkspaceRowProps isRenaming: boolean; renameValue: string; shortcutLabel?: string; + mockData: WorkspaceRowMockData; onClick: () => void; + onDoubleClick?: () => void; + onDeleteClick: () => void; onRenameValueChange: (value: string) => void; onSubmitRename: () => void; onCancelRename: () => void; - setDragHandle: (node: HTMLButtonElement | null) => void; + setDragHandle: (node: HTMLDivElement | null) => void; } export const DashboardSidebarExpandedWorkspaceRow = forwardRef< - HTMLButtonElement, + HTMLDivElement, DashboardSidebarExpandedWorkspaceRowProps >( ( { + accentColor = null, name, branch, isActive, @@ -31,7 +42,10 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< isRenaming, renameValue, shortcutLabel, + mockData, onClick, + onDoubleClick, + onDeleteClick, onRenameValueChange, onSubmitRename, onCancelRename, @@ -41,11 +55,20 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< }, ref, ) => { - const showBranch = !!name && name !== branch; + const showBranchSubtitle = !!name && name !== branch; + const showSubtitle = showBranchSubtitle || !!mockData.pr; + const showsStandaloneActiveStripe = accentColor == null; + const activeAccentStyle = isActive + ? { + backgroundColor: "var(--color-foreground)", + } + : undefined; return ( - + + + Close workspace + + +
- {showBranch && ( - + {showBranchSubtitle && ( + {branch} )} - + + {mockData.pr && ( + + )} +
+ ) : ( +
+ {isRenaming ? ( + + ) : ( + + {name || branch} + + )} + +
+ +
+ {shortcutLabel && ( + + {shortcutLabel} + + )} + + + + + + Close workspace + + +
+
+
)}
- +
); }, ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx index d60214ff12f..32148fa9e17 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx @@ -8,6 +8,12 @@ import { ContextMenuSubTrigger, ContextMenuTrigger, } from "@superset/ui/context-menu"; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@superset/ui/hover-card"; +import { useState } from "react"; import { LuArrowRightLeft, LuFolderPlus, @@ -17,7 +23,7 @@ import { } from "react-icons/lu"; interface DashboardSidebarWorkspaceContextMenuProps { - id: string; + hoverCardContent?: React.ReactNode; sections: { id: string; name: string }[]; onCreateSection: () => void; onMoveToSection: (sectionId: string | null) => void; @@ -29,6 +35,7 @@ interface DashboardSidebarWorkspaceContextMenuProps { export function DashboardSidebarWorkspaceContextMenu({ sections, + hoverCardContent, onCreateSection, onMoveToSection, onRemoveFromSidebar, @@ -36,52 +43,78 @@ export function DashboardSidebarWorkspaceContextMenu({ onDelete, children, }: DashboardSidebarWorkspaceContextMenuProps) { - return ( - - {children} - - - - Rename - - - - - - Move to Section - - - - - New Section - - onMoveToSection(null)}> - - Ungrouped + const [isContextMenuOpen, setIsContextMenuOpen] = useState(false); + + const menuContent = ( + + + + Rename + + + + + + Move to Section + + + + + New Section + + onMoveToSection(null)}> + + Ungrouped + + {sections.map((section) => ( + onMoveToSection(section.id)} + > + {section.name} - {sections.map((section) => ( - onMoveToSection(section.id)} - > - {section.name} - - ))} - - - - - Remove from Sidebar - - - - - Delete - - - + ))} + + + + + Remove from Sidebar + + + + + Delete + + + ); + + if (!hoverCardContent) { + return ( + + {children} + {menuContent} + + ); + } + + return ( + + + + {children} + + {menuContent} + + + {hoverCardContent} + + ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDiffStats/DashboardSidebarWorkspaceDiffStats.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDiffStats/DashboardSidebarWorkspaceDiffStats.tsx new file mode 100644 index 00000000000..05875fe4393 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDiffStats/DashboardSidebarWorkspaceDiffStats.tsx @@ -0,0 +1,27 @@ +import { cn } from "@superset/ui/utils"; + +interface DashboardSidebarWorkspaceDiffStatsProps { + additions: number; + deletions: number; + isActive?: boolean; +} + +export function DashboardSidebarWorkspaceDiffStats({ + additions, + deletions, + isActive, +}: DashboardSidebarWorkspaceDiffStatsProps) { + return ( +
+
+ +{additions} + −{deletions} +
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDiffStats/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDiffStats/index.ts new file mode 100644 index 00000000000..b10a01444f0 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceDiffStats/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarWorkspaceDiffStats } from "./DashboardSidebarWorkspaceDiffStats"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/DashboardSidebarWorkspaceHoverCardContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/DashboardSidebarWorkspaceHoverCardContent.tsx new file mode 100644 index 00000000000..3b8b2b9114c --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/DashboardSidebarWorkspaceHoverCardContent.tsx @@ -0,0 +1,80 @@ +import { Button } from "@superset/ui/button"; +import { LuExternalLink, LuGitBranch, LuGlobe } from "react-icons/lu"; +import type { WorkspaceRowMockData } from "../../utils"; +import { DashboardSidebarWorkspaceStatusBadge } from "../DashboardSidebarWorkspaceStatusBadge"; + +interface DashboardSidebarWorkspaceHoverCardContentProps { + name: string; + branch: string; + mockData: WorkspaceRowMockData; +} + +export function DashboardSidebarWorkspaceHoverCardContent({ + name, + branch, + mockData, +}: DashboardSidebarWorkspaceHoverCardContentProps) { + return ( +
+
+
{name || branch}
+
+ + Branch + +
+ {branch} + +
+
+ + Updated a few minutes ago + +
+ +
+ Mocked preview of the legacy workspace hover card. +
+ + {mockData.pr ? ( +
+
+
+ +
+
+ + +{mockData.diffStats.additions} + + + -{mockData.diffStats.deletions} + +
+
+

{mockData.pr.title}

+
+ + +
+
+ ) : null} +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/index.ts new file mode 100644 index 00000000000..f6745988375 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceHoverCardContent/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarWorkspaceHoverCardContent } from "./DashboardSidebarWorkspaceHoverCardContent"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx new file mode 100644 index 00000000000..c2288e5b537 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx @@ -0,0 +1,53 @@ +import { cn } from "@superset/ui/utils"; +import { LuFolderGit2 } from "react-icons/lu"; +import { AsciiSpinner } from "renderer/screens/main/components/AsciiSpinner"; +import { StatusIndicator } from "renderer/screens/main/components/StatusIndicator"; +import type { ActivePaneStatus } from "shared/tabs-types"; + +interface DashboardSidebarWorkspaceIconProps { + isActive: boolean; + isUnread?: boolean; + variant: "collapsed" | "expanded"; + workspaceStatus?: ActivePaneStatus | null; +} + +const OVERLAY_POSITION = { + collapsed: "top-1 right-1", + expanded: "-top-0.5 -right-0.5", +} as const; + +export function DashboardSidebarWorkspaceIcon({ + isActive, + isUnread = false, + variant, + workspaceStatus = null, +}: DashboardSidebarWorkspaceIconProps) { + const overlayPosition = OVERLAY_POSITION[variant]; + + return ( + <> + {workspaceStatus === "working" ? ( + + ) : ( + + )} + {workspaceStatus && workspaceStatus !== "working" && ( + + + + )} + {isUnread && !workspaceStatus && ( + + + + )} + + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/index.ts new file mode 100644 index 00000000000..440bf0fed80 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarWorkspaceIcon } from "./DashboardSidebarWorkspaceIcon"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceStatusBadge/DashboardSidebarWorkspaceStatusBadge.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceStatusBadge/DashboardSidebarWorkspaceStatusBadge.tsx new file mode 100644 index 00000000000..908a3d0e80d --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceStatusBadge/DashboardSidebarWorkspaceStatusBadge.tsx @@ -0,0 +1,76 @@ +import { cn } from "@superset/ui/utils"; +import { LuCircleDot, LuGitMerge, LuGitPullRequest } from "react-icons/lu"; + +type MockPrState = "open" | "merged" | "closed" | "draft"; + +interface DashboardSidebarWorkspaceStatusBadgeProps { + state: MockPrState; + prNumber?: number; + className?: string; +} + +export function DashboardSidebarWorkspaceStatusBadge({ + state, + prNumber, + className, +}: DashboardSidebarWorkspaceStatusBadgeProps) { + const iconClass = "h-3 w-3"; + + const config = { + open: { + icon: ( + + ), + bgColor: "bg-emerald-500/10", + }, + merged: { + icon: ( + + ), + bgColor: "bg-purple-500/10", + }, + closed: { + icon: ( + + ), + bgColor: "bg-destructive/10", + }, + draft: { + icon: ( + + ), + bgColor: "bg-muted", + }, + }; + + const { icon, bgColor } = config[state]; + + return ( +
+ {icon} + {prNumber && ( + + #{prNumber} + + )} +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceStatusBadge/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceStatusBadge/index.ts new file mode 100644 index 00000000000..2d849c7784c --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceStatusBadge/index.ts @@ -0,0 +1 @@ +export { DashboardSidebarWorkspaceStatusBadge } from "./DashboardSidebarWorkspaceStatusBadge"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/getWorkspaceRowMocks.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/getWorkspaceRowMocks.ts new file mode 100644 index 00000000000..8b811871541 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/getWorkspaceRowMocks.ts @@ -0,0 +1,51 @@ +import type { ActivePaneStatus } from "shared/tabs-types"; + +type MockPrState = "open" | "merged" | "closed" | "draft"; + +export interface WorkspaceRowMockData { + diffStats: { + additions: number; + deletions: number; + }; + isUnread: boolean; + workspaceStatus: ActivePaneStatus | null; + pr: { + state: MockPrState; + number: number; + title: string; + } | null; +} + +function getSeed(input: string): number { + return [...input].reduce( + (seed, character, index) => seed + character.charCodeAt(0) * (index + 1), + 0, + ); +} + +export function getWorkspaceRowMocks( + workspaceId: string, +): WorkspaceRowMockData { + const seed = getSeed(workspaceId); + const prStates: MockPrState[] = ["open", "draft", "merged", "closed"]; + const paneStatuses: ActivePaneStatus[] = ["permission", "working", "review"]; + const hasPr = seed % 5 !== 0; + const status = + seed % 6 === 0 ? paneStatuses[seed % paneStatuses.length] : null; + + return { + diffStats: { + additions: (seed % 24) + 3, + deletions: (seed % 9) + 1, + }, + isUnread: !status && seed % 4 === 0, + workspaceStatus: status, + pr: hasPr + ? { + state: prStates[seed % prStates.length], + number: 100 + (seed % 900), + title: "Polish workspace sidebar visuals", + } + : null, + }; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/index.ts new file mode 100644 index 00000000000..a6d4a84adf9 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/index.ts @@ -0,0 +1,2 @@ +export type { WorkspaceRowMockData } from "./getWorkspaceRowMocks"; +export { getWorkspaceRowMocks } from "./getWorkspaceRowMocks"; From 3afe33bb81d4da5e760665a3cdfeb528866d2cca Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 16 Mar 2026 13:07:44 -0700 Subject: [PATCH 06/14] WIP - restructuring --- ...ashboardSidebarCollapsedProjectContent.tsx | 1 + ...DashboardSidebarExpandedProjectContent.tsx | 1 + .../DashboardSidebarProjectRow.tsx | 63 +++++++++++-------- .../DashboardSidebarSectionContent.tsx | 3 +- .../DashboardSidebarSectionHeader.tsx | 63 ++++++++++++++----- .../DashboardSidebarWorkspaceItem.tsx | 4 ++ ...shboardSidebarCollapsedWorkspaceButton.tsx | 4 ++ .../DashboardSidebarExpandedWorkspaceRow.tsx | 4 ++ .../DashboardSidebarWorkspaceIcon.tsx | 23 ++++++- .../useDashboardSidebarData.ts | 20 ++++++ .../utils/buildDashboardSidebarProjects.ts | 16 +++++ .../components/DashboardSidebar/types.ts | 6 ++ 12 files changed, 164 insertions(+), 44 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarCollapsedProjectContent/DashboardSidebarCollapsedProjectContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarCollapsedProjectContent/DashboardSidebarCollapsedProjectContent.tsx index 46f19495252..5f744ebe247 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarCollapsedProjectContent/DashboardSidebarCollapsedProjectContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarCollapsedProjectContent/DashboardSidebarCollapsedProjectContent.tsx @@ -93,6 +93,7 @@ export const DashboardSidebarCollapsedProjectContent = forwardRef< key={workspace.id} id={workspace.id} projectId={projectId} + hostType={workspace.hostType} name={workspace.name} branch={workspace.branch} index={index} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx index b042d65f49b..8d7b4b00e29 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarExpandedProjectContent/DashboardSidebarExpandedProjectContent.tsx @@ -48,6 +48,7 @@ export function DashboardSidebarExpandedProjectContent({ id={workspace.id} projectId={projectId} accentColor={null} + hostType={workspace.hostType} name={workspace.name} branch={workspace.branch} index={index} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx index efa831f98f1..523c632dedf 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx @@ -2,6 +2,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; import { type ComponentPropsWithoutRef, forwardRef } from "react"; import { HiChevronRight, HiMiniPlus } from "react-icons/hi2"; +import { LuPencil } from "react-icons/lu"; import { ProjectThumbnail } from "renderer/routes/_authenticated/components/ProjectThumbnail"; import { RenameInput } from "renderer/screens/main/components/WorkspaceSidebar/RenameInput"; @@ -48,43 +49,55 @@ export const DashboardSidebarProjectRow = forwardRef<
- {isRenaming ? ( -
- +
+ + {isRenaming ? ( + ) : ( + + )} +
+ {!isRenaming && ( + + ({totalWorkspaceCount}) + + )} + {!isRenaming && ( + + )}
- ) : ( - - )} +
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/DashboardSidebarSectionContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/DashboardSidebarSectionContent.tsx index c03236ef948..7a5c1fe7486 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/DashboardSidebarSectionContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarSection/components/DashboardSidebarSectionContent/DashboardSidebarSectionContent.tsx @@ -27,7 +27,7 @@ export function DashboardSidebarSectionContent({ transition={{ duration: 0.15, ease: "easeOut" }} className="overflow-hidden" > -
+
{section.workspaces.map((workspace, index) => ( - {isRenaming ? ( - - ) : ( +
- )} + {isRenaming ? ( + + ) : ( + + )} + +
+ {!isRenaming && ( + + ({section.workspaces.length}) + + )} + {!isRenaming && ( + + )} +
+
); }, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx index 6bb506002f9..96cd8dd2756 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx @@ -15,6 +15,7 @@ interface DashboardSidebarWorkspaceItemProps { projectId: string; accentColor?: string | null; sectionId?: string | null; + hostType: "local-device" | "remote-device" | "cloud"; name: string; branch: string; index: number; @@ -29,6 +30,7 @@ export function DashboardSidebarWorkspaceItem({ projectId, accentColor = null, sectionId = null, + hostType, name, branch, index, @@ -93,6 +95,7 @@ export function DashboardSidebarWorkspaceItem({ { + hostType: DashboardSidebarWorkspaceHostType; isActive: boolean; isDragging: boolean; isUnread?: boolean; @@ -18,6 +20,7 @@ export const DashboardSidebarCollapsedWorkspaceButton = forwardRef< >( ( { + hostType, isActive, isDragging, isUnread = false, @@ -49,6 +52,7 @@ export const DashboardSidebarCollapsedWorkspaceButton = forwardRef< {...props} > { accentColor?: string | null; + hostType: DashboardSidebarWorkspaceHostType; name: string; branch: string; isActive: boolean; @@ -35,6 +37,7 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< ( { accentColor = null, + hostType, name, branch, isActive, @@ -109,6 +112,7 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef<
{workspaceStatus === "working" ? ( + ) : hostType === "cloud" ? ( + + ) : hostType === "remote-device" ? ( + ) : ( q.from({ sidebarProjects: collections.v2SidebarProjects }), @@ -34,6 +36,20 @@ export function useDashboardSidebarData() { [collections], ); + const { data: devices = [] } = useLiveQuery( + (q) => + q.from({ v2Devices: collections.v2Devices }).select(({ v2Devices }) => ({ + id: v2Devices.id, + clientId: v2Devices.clientId, + type: v2Devices.type, + })), + [collections], + ); + + const currentDeviceId = + devices.find((device) => device.clientId === deviceInfo?.deviceId)?.id ?? + null; + const { data: githubRepos = [] } = useLiveQuery( (q) => q.from({ repos: collections.githubRepositories }).select(({ repos }) => ({ @@ -45,6 +61,8 @@ export function useDashboardSidebarData() { const groups = useMemo(() => { return buildDashboardSidebarProjects({ + currentDeviceId, + devices, githubRepos, projects, sidebarProjects, @@ -54,6 +72,8 @@ export function useDashboardSidebarData() { }); }, [ githubRepos, + currentDeviceId, + devices, projects, sidebarProjects, sidebarSections, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/utils/buildDashboardSidebarProjects.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/utils/buildDashboardSidebarProjects.ts index 3c88f043c85..294b7c524c6 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/utils/buildDashboardSidebarProjects.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/utils/buildDashboardSidebarProjects.ts @@ -6,6 +6,11 @@ import type { interface BuildDashboardSidebarProjectsOptions { githubRepos: Array<{ id: string; owner: string }>; + currentDeviceId: string | null; + devices: Array<{ + id: string; + type: "host" | "cloud" | "viewer"; + }>; projects: Array<{ id: string; name: string; @@ -47,6 +52,8 @@ interface BuildDashboardSidebarProjectsOptions { export function buildDashboardSidebarProjects({ githubRepos, + currentDeviceId, + devices, projects, sidebarProjects, sidebarSections, @@ -61,6 +68,7 @@ export function buildDashboardSidebarProjects({ const cloudProjectsById = new Map( projects.map((project) => [project.id, project]), ); + const devicesById = new Map(devices.map((device) => [device.id, device])); const cloudWorkspacesById = new Map( workspaces.map((workspace) => [workspace.id, workspace]), ); @@ -94,11 +102,19 @@ export function buildDashboardSidebarProjects({ for (const localWorkspace of sidebarWorkspaces) { const workspace = cloudWorkspacesById.get(localWorkspace.workspaceId); if (!workspace) continue; + const device = devicesById.get(workspace.deviceId); + const hostType = + device?.type === "cloud" + ? "cloud" + : workspace.deviceId === currentDeviceId + ? "local-device" + : "remote-device"; const sidebarWorkspace: DashboardSidebarWorkspace = { id: workspace.id, projectId: workspace.projectId, deviceId: workspace.deviceId, + hostType, name: workspace.name, branch: workspace.branch, createdAt: workspace.createdAt, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts index ae58d982b66..a45ec93ea9f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts @@ -1,7 +1,13 @@ +export type DashboardSidebarWorkspaceHostType = + | "local-device" + | "remote-device" + | "cloud"; + export interface DashboardSidebarWorkspace { id: string; projectId: string; deviceId: string; + hostType: DashboardSidebarWorkspaceHostType; name: string; branch: string; createdAt: Date; From 3253f7d20c929f8ba587c57d6b57e0d7c1c9de1a Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 16 Mar 2026 13:45:33 -0700 Subject: [PATCH 07/14] Refine dashboard sidebar interactions --- .../DashboardSidebar/DashboardSidebar.tsx | 6 +- .../DashboardSidebarProjectSection.tsx | 36 +-------- ...ashboardSidebarCollapsedProjectContent.tsx | 9 +-- ...DashboardSidebarExpandedProjectContent.tsx | 6 +- .../DashboardSidebarProjectRow.tsx | 33 +++++--- .../DashboardSidebarSection.tsx | 2 +- .../DashboardSidebarSectionContent.tsx | 7 +- .../DashboardSidebarSectionHeader.tsx | 75 +++++++++-------- .../DashboardSidebarWorkspaceItem.tsx | 23 ------ ...shboardSidebarCollapsedWorkspaceButton.tsx | 14 +--- .../DashboardSidebarExpandedWorkspaceRow.tsx | 14 +--- .../useDashboardSidebarProjectDnD/index.ts | 1 - .../useDashboardSidebarProjectDnD.ts | 67 --------------- .../useDashboardSidebarWorkspaceDnD/index.ts | 1 - .../useDashboardSidebarWorkspaceDnD.ts | 81 ------------------- 15 files changed, 73 insertions(+), 302 deletions(-) delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarProjectDnD/index.ts delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarProjectDnD/useDashboardSidebarProjectDnD.ts delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarWorkspaceDnD/index.ts delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarWorkspaceDnD/useDashboardSidebarWorkspaceDnD.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx index f653163c5c2..d1936757d4e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/DashboardSidebar.tsx @@ -25,14 +25,12 @@ export function DashboardSidebar({ useDashboardSidebarShortcuts(flattenedWorkspaces); - const projectIds = useMemo(() => groups.map((g) => g.id), [groups]); - return (
- {groups.map((project, index) => ( + {groups.map((project) => ( ))} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx index 9cf387b99b3..5e31ad4fee1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/DashboardSidebarProjectSection.tsx @@ -1,6 +1,5 @@ import { cn } from "@superset/ui/utils"; import { useMemo } from "react"; -import { useDashboardSidebarProjectDnD } from "../../hooks/useDashboardSidebarProjectDnD"; import type { DashboardSidebarSection, DashboardSidebarWorkspace, @@ -22,8 +21,6 @@ interface DashboardSidebarProjectSectionProps { workspaces: DashboardSidebarWorkspace[]; sections: DashboardSidebarSection[]; workspaceShortcutLabels: Map; - index: number; - projectIds: string[]; onToggleCollapse: (projectId: string) => void; } @@ -36,21 +33,8 @@ export function DashboardSidebarProjectSection({ workspaces, sections, workspaceShortcutLabels, - index, - projectIds, onToggleCollapse, }: DashboardSidebarProjectSectionProps) { - const { isDragging, drag, drop } = useDashboardSidebarProjectDnD({ - projectId, - index, - projectIds, - }); - - const topLevelWorkspaceIds = useMemo( - () => workspaces.map((workspace) => workspace.id), - [workspaces], - ); - const allSections = useMemo( () => sections.map((section) => ({ id: section.id, name: section.name })), [sections], @@ -97,21 +81,14 @@ export function DashboardSidebarProjectSection({ onRename={startRename} onDelete={() => setIsDeleteDialogOpen(true)} > -
{ - drag(drop(node)); - }} - className={cn("border-b border-border last:border-b-0")} - > +
item.id)} allSections={allSections} workspaceShortcutLabels={workspaceShortcutLabels} onToggleCollapse={() => onToggleCollapse(projectId)} @@ -133,15 +110,7 @@ export function DashboardSidebarProjectSection({ return ( <> -
{ - drag(drop(node)); - }} - className={cn( - "border-b border-border last:border-b-0", - isDragging && "opacity-30", - )} - > +
; workspaceShortcutLabels: Map; onToggleCollapse: () => void; @@ -31,10 +29,8 @@ export const DashboardSidebarCollapsedProjectContent = forwardRef< projectName, githubOwner, isCollapsed, - isDragging, totalWorkspaceCount, workspaces, - workspaceIds, allSections, workspaceShortcutLabels, onToggleCollapse, @@ -48,7 +44,6 @@ export const DashboardSidebarCollapsedProjectContent = forwardRef< ref={ref} className={cn( "flex flex-col items-center py-2 border-b border-border last:border-b-0", - isDragging && "opacity-30", className, )} {...props} @@ -88,7 +83,7 @@ export const DashboardSidebarCollapsedProjectContent = forwardRef< className="overflow-hidden w-full" >
- {workspaces.map((workspace, index) => ( + {workspaces.map((workspace) => ( ; workspaceShortcutLabels: Map; onDeleteSection: (sectionId: string) => void; @@ -24,7 +23,6 @@ export function DashboardSidebarExpandedProjectContent({ isCollapsed, workspaces, sections, - topLevelWorkspaceIds, allSections, workspaceShortcutLabels, onDeleteSection, @@ -42,7 +40,7 @@ export function DashboardSidebarExpandedProjectContent({ className="overflow-hidden" >
- {workspaces.map((workspace, index) => ( + {workspaces.map((workspace) => ( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx index 523c632dedf..38022615135 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarProjectRow/DashboardSidebarProjectRow.tsx @@ -46,8 +46,22 @@ export const DashboardSidebarProjectRow = forwardRef< ref, ) => { return ( + // biome-ignore lint/a11y/noStaticElementInteractions: The header acts as a single toggle target in view mode while preserving nested inline controls.
{ + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + onToggleCollapse(); + } + } + } className={cn( "group flex min-h-10 w-full items-center pl-3 pr-2 py-1.5 text-sm font-medium", "hover:bg-muted/50 transition-colors", @@ -69,17 +83,11 @@ export const DashboardSidebarProjectRow = forwardRef< className="-ml-1 h-6 min-w-0 flex-1 bg-transparent border-none px-1 py-0 text-sm font-medium outline-none" /> ) : ( - + {projectName} )}
{!isRenaming && ( - + ({totalWorkspaceCount}) )} @@ -90,10 +98,10 @@ export const DashboardSidebarProjectRow = forwardRef< event.stopPropagation(); onStartRename(); }} - className="flex items-center justify-center opacity-0 scale-90 text-muted-foreground transition-all duration-150 group-hover:scale-100 group-hover:opacity-100 group-focus-within:scale-100 group-focus-within:opacity-100 hover:text-foreground" + className="flex items-center justify-center opacity-0 scale-90 text-muted-foreground transition-all duration-150 group-hover:scale-100 group-hover:opacity-100 hover:text-foreground" aria-label="Rename project" > - + )}
@@ -120,7 +128,10 @@ export const DashboardSidebarProjectRow = forwardRef< {isRenaming ? ( ) : ( - + {section.name} )} -
- {!isRenaming && ( - + {!isRenaming && ( +
+ ({section.workspaces.length}) - )} - {!isRenaming && ( - )} -
+
+ )}
+ +
); }, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx index 96cd8dd2756..cf9149d027a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx @@ -1,5 +1,4 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; -import { useDashboardSidebarWorkspaceDnD } from "../../hooks/useDashboardSidebarWorkspaceDnD"; import { DashboardSidebarDeleteDialog } from "../DashboardSidebarDeleteDialog"; import { DashboardSidebarCollapsedWorkspaceButton } from "./components/DashboardSidebarCollapsedWorkspaceButton"; import { DashboardSidebarExpandedWorkspaceRow } from "./components/DashboardSidebarExpandedWorkspaceRow"; @@ -14,12 +13,9 @@ interface DashboardSidebarWorkspaceItemProps { id: string; projectId: string; accentColor?: string | null; - sectionId?: string | null; hostType: "local-device" | "remote-device" | "cloud"; name: string; branch: string; - index: number; - workspaceIds: string[]; sections?: { id: string; name: string }[]; shortcutLabel?: string; isCollapsed?: boolean; @@ -29,12 +25,9 @@ export function DashboardSidebarWorkspaceItem({ id, projectId, accentColor = null, - sectionId = null, hostType, name, branch, - index, - workspaceIds, sections = EMPTY_SECTIONS, shortcutLabel, isCollapsed = false, @@ -62,14 +55,6 @@ export function DashboardSidebarWorkspaceItem({ workspaceName: name, }); - const { isDragging, drag, drop } = useDashboardSidebarWorkspaceDnD({ - workspaceId: id, - projectId, - sectionId, - index, - workspaceIds, - }); - if (isCollapsed) { const showBranch = !!name && name !== branch; @@ -97,12 +82,8 @@ export function DashboardSidebarWorkspaceItem({ { - drag(drop(node)); - }} workspaceStatus={mockData.workspaceStatus} /> @@ -154,7 +135,6 @@ export function DashboardSidebarWorkspaceItem({ name={name} branch={branch} isActive={isActive} - isDragging={isDragging} isRenaming={isRenaming} renameValue={renameValue} shortcutLabel={shortcutLabel} @@ -165,9 +145,6 @@ export function DashboardSidebarWorkspaceItem({ onRenameValueChange={setRenameValue} onSubmitRename={submitRename} onCancelRename={cancelRename} - setDragHandle={(node) => { - drag(drop(node)); - }} /> diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx index 5c319d059ad..ca8378893bf 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx @@ -8,9 +8,7 @@ interface DashboardSidebarCollapsedWorkspaceButtonProps extends ComponentPropsWithoutRef<"button"> { hostType: DashboardSidebarWorkspaceHostType; isActive: boolean; - isDragging: boolean; isUnread?: boolean; - setDragHandle: (node: HTMLButtonElement | null) => void; workspaceStatus?: ActivePaneStatus | null; } @@ -22,9 +20,7 @@ export const DashboardSidebarCollapsedWorkspaceButton = forwardRef< { hostType, isActive, - isDragging, isUnread = false, - setDragHandle, workspaceStatus = null, className, ...props @@ -34,19 +30,11 @@ export const DashboardSidebarCollapsedWorkspaceButton = forwardRef< return (
)} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx index 3abbea7a4a3..a7258f69cb5 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx @@ -31,8 +31,10 @@ export function DashboardSidebarWorkspaceItem({ const { cancelRename, handleClick, + handleCopyPath, handleCreateSection, handleDelete, + handleOpenInFinder, isActive, isDeleteDialogOpen, isDeleting, @@ -66,6 +68,8 @@ export function DashboardSidebarWorkspaceItem({ onMoveToSection={(targetSectionId) => moveWorkspaceToSection(id, projectId, targetSectionId) } + onOpenInFinder={handleOpenInFinder} + onCopyPath={handleCopyPath} onRemoveFromSidebar={() => removeWorkspaceFromSidebar(id)} onRename={startRename} onDelete={() => setIsDeleteDialogOpen(true)} @@ -115,6 +119,8 @@ export function DashboardSidebarWorkspaceItem({ onMoveToSection={(targetSectionId) => moveWorkspaceToSection(id, projectId, targetSectionId) } + onOpenInFinder={handleOpenInFinder} + onCopyPath={handleCopyPath} onRemoveFromSidebar={() => removeWorkspaceFromSidebar(id)} onRename={startRename} onDelete={() => setIsDeleteDialogOpen(true)} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx index 7b40d3f0398..f92a643fda8 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceContextMenu/DashboardSidebarWorkspaceContextMenu.tsx @@ -18,10 +18,13 @@ import { useLiveQuery } from "@tanstack/react-db"; import { useState } from "react"; import { LuArrowRightLeft, + LuCopy, + LuFolderOpen, LuFolderPlus, LuMinus, LuPencil, LuTrash2, + LuX, } from "react-icons/lu"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; @@ -30,6 +33,8 @@ interface DashboardSidebarWorkspaceContextMenuProps { projectId: string; onCreateSection: () => void; onMoveToSection: (sectionId: string | null) => void; + onOpenInFinder: () => void; + onCopyPath: () => void; onRemoveFromSidebar: () => void; onRename: () => void; onDelete: () => void; @@ -41,6 +46,8 @@ export function DashboardSidebarWorkspaceContextMenu({ hoverCardContent, onCreateSection, onMoveToSection, + onOpenInFinder, + onCopyPath, onRemoveFromSidebar, onRename, onDelete, @@ -64,12 +71,21 @@ export function DashboardSidebarWorkspaceContextMenu({ ); const menuContent = ( - + event.preventDefault()}> Rename + + + Open in Finder + + + + Copy Path + + @@ -94,11 +110,14 @@ export function DashboardSidebarWorkspaceContextMenu({ ))} - - + + + Remove from Sidebar - { + toast.info("Open in Finder is coming soon"); + }; + + const handleCopyPath = () => { + toast.info("Copy Path is coming soon"); + }; + return { cancelRename, handleClick, + handleCopyPath, handleCreateSection, handleDelete, + handleOpenInFinder, isActive, isDeleteDialogOpen, isDeleting, From 6d26a14b4e0bd93976c2bf6f879e59e27de797bf Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 16 Mar 2026 15:29:54 -0700 Subject: [PATCH 11/14] Remove flaky terminal host integration tests --- .../src/main/terminal-host/daemon.test.ts | 489 ------------------ .../terminal-host/session-lifecycle.test.ts | 361 ------------- 2 files changed, 850 deletions(-) delete mode 100644 apps/desktop/src/main/terminal-host/daemon.test.ts delete mode 100644 apps/desktop/src/main/terminal-host/session-lifecycle.test.ts diff --git a/apps/desktop/src/main/terminal-host/daemon.test.ts b/apps/desktop/src/main/terminal-host/daemon.test.ts deleted file mode 100644 index 1aa4918c3c3..00000000000 --- a/apps/desktop/src/main/terminal-host/daemon.test.ts +++ /dev/null @@ -1,489 +0,0 @@ -/** - * Terminal Host Daemon Integration Tests - * - * These tests verify the daemon can: - * 1. Start and listen on a Unix socket - * 2. Accept connections and handle NDJSON protocol - * 3. Authenticate clients with token - * 4. Respond to hello requests - */ - -import { afterAll, beforeAll, describe, expect, it } from "bun:test"; -import type { ChildProcess } from "node:child_process"; -import { spawn } from "node:child_process"; -import { - existsSync, - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, -} from "node:fs"; -import { connect, type Socket } from "node:net"; -import { join, resolve } from "node:path"; -import { - type HelloResponse, - type IpcRequest, - type IpcResponse, - PROTOCOL_VERSION, -} from "../lib/terminal-host/types"; -import { supportsLocalSocketBinding } from "./test-helpers"; - -// Test uses a dedicated workspace name for isolation -const SUPERSET_DIR_NAME = ".superset-test"; -const TEST_HOME_DIR = mkdtempSync("/tmp/sth-"); -const SUPERSET_HOME_DIR = join(TEST_HOME_DIR, SUPERSET_DIR_NAME); -const SOCKET_PATH = join(SUPERSET_HOME_DIR, "terminal-host.sock"); -const TOKEN_PATH = join(SUPERSET_HOME_DIR, "terminal-host.token"); -const PID_PATH = join(SUPERSET_HOME_DIR, "terminal-host.pid"); - -// Path to the daemon source file -const DAEMON_PATH = resolve(__dirname, "index.ts"); -// Polyfill for @xterm/headless in Bun (see xterm-env-polyfill.ts for details) -const XTERM_POLYFILL_PATH = resolve(__dirname, "xterm-env-polyfill.ts"); - -// Timeout for daemon operations -const DAEMON_TIMEOUT = 10000; -const CONNECT_TIMEOUT = 5000; - -const canRunTerminalHostIntegration = supportsLocalSocketBinding(); - -if (!canRunTerminalHostIntegration) { - describe("Terminal Host Daemon", () => { - it("skips when local socket binding is unavailable", () => { - expect(true).toBe(true); - }); - }); -} else { - describe("Terminal Host Daemon", () => { - let daemonProcess: ChildProcess | null = null; - - /** - * Clean up any existing daemon artifacts - */ - function cleanup() { - // Kill any existing daemon - if (existsSync(PID_PATH)) { - try { - const pid = Number.parseInt( - readFileSync(PID_PATH, "utf-8").trim(), - 10, - ); - if (pid > 0) { - process.kill(pid, "SIGTERM"); - } - } catch { - // Process might not exist - } - } - - // Remove socket file - if (existsSync(SOCKET_PATH)) { - try { - rmSync(SOCKET_PATH); - } catch { - // Ignore - } - } - - // Remove PID file - if (existsSync(PID_PATH)) { - try { - rmSync(PID_PATH); - } catch { - // Ignore - } - } - - // Remove token file (so we get a fresh one) - if (existsSync(TOKEN_PATH)) { - try { - rmSync(TOKEN_PATH); - } catch { - // Ignore - } - } - } - - /** - * Start the daemon process - */ - async function startDaemon(): Promise { - return new Promise((resolve, reject) => { - // Ensure home directory exists - if (!existsSync(SUPERSET_HOME_DIR)) { - mkdirSync(SUPERSET_HOME_DIR, { recursive: true, mode: 0o700 }); - } - - // Start daemon with --preload to polyfill window for @xterm/headless in Bun - daemonProcess = spawn( - "bun", - ["run", "--preload", XTERM_POLYFILL_PATH, DAEMON_PATH], - { - env: { - ...process.env, - HOME: TEST_HOME_DIR, - NODE_ENV: "development", - SUPERSET_WORKSPACE_NAME: "test", - }, - stdio: ["ignore", "pipe", "pipe"], - detached: true, - }, - ); - - let output = ""; - let stderrOutput = ""; - let settled = false; - let timeoutId: ReturnType; - - daemonProcess.stdout?.on("data", (data) => { - output += data.toString(); - // Check if daemon is ready - if (output.includes("Daemon started")) { - if (settled) return; - settled = true; - clearTimeout(timeoutId); - resolve(); - } - }); - - daemonProcess.stderr?.on("data", (data) => { - const text = data.toString(); - stderrOutput += text; - console.error("Daemon stderr:", text); - }); - - daemonProcess.on("error", (error) => { - if (settled) return; - settled = true; - clearTimeout(timeoutId); - reject( - new Error( - `Failed to start daemon: ${error.message}. stdout=${output} stderr=${stderrOutput}`, - ), - ); - }); - - daemonProcess.on("exit", (code, signal) => { - if (!settled && code !== 0 && code !== null) { - settled = true; - clearTimeout(timeoutId); - reject( - new Error( - `Daemon exited with code ${code}, signal ${signal}. stdout=${output} stderr=${stderrOutput}`, - ), - ); - } - }); - - // Timeout if daemon doesn't start - timeoutId = setTimeout(() => { - if (settled) return; - settled = true; - reject( - new Error( - `Daemon failed to start within ${DAEMON_TIMEOUT}ms. stdout=${output} stderr=${stderrOutput}`, - ), - ); - }, DAEMON_TIMEOUT); - }); - } - - /** - * Stop the daemon process - */ - async function stopDaemon(): Promise { - if (daemonProcess) { - return new Promise((resolve) => { - daemonProcess?.on("exit", () => { - daemonProcess = null; - resolve(); - }); - - daemonProcess?.kill("SIGTERM"); - - // Force kill if it doesn't exit gracefully - setTimeout(() => { - if (daemonProcess) { - daemonProcess.kill("SIGKILL"); - daemonProcess = null; - resolve(); - } - }, 2000); - }); - } - } - - /** - * Connect to the daemon socket - */ - function connectToDaemon(): Promise { - return new Promise((resolve, reject) => { - const socket = connect(SOCKET_PATH); - - socket.on("connect", () => { - resolve(socket); - }); - - socket.on("error", (error) => { - reject(new Error(`Failed to connect to daemon: ${error.message}`)); - }); - - setTimeout(() => { - reject(new Error(`Connection timed out after ${CONNECT_TIMEOUT}ms`)); - }, CONNECT_TIMEOUT); - }); - } - - /** - * Send a request and wait for response - */ - function sendRequest( - socket: Socket, - request: IpcRequest, - ): Promise { - return new Promise((resolve, reject) => { - let buffer = ""; - - const onData = (data: Buffer) => { - buffer += data.toString(); - const newlineIndex = buffer.indexOf("\n"); - if (newlineIndex !== -1) { - const line = buffer.slice(0, newlineIndex); - socket.off("data", onData); - try { - resolve(JSON.parse(line)); - } catch (_error) { - reject(new Error(`Failed to parse response: ${line}`)); - } - } - }; - - socket.on("data", onData); - - socket.write(`${JSON.stringify(request)}\n`); - - setTimeout(() => { - socket.off("data", onData); - reject(new Error("Request timed out")); - }, 5000); - }); - } - - beforeAll(async () => { - cleanup(); - await startDaemon(); - }); - - afterAll(async () => { - await stopDaemon(); - cleanup(); - rmSync(TEST_HOME_DIR, { recursive: true, force: true }); - }); - - describe("hello handshake", () => { - it("should accept valid hello request with correct token", async () => { - const socket = await connectToDaemon(); - - try { - // Read the token that the daemon generated - const token = readFileSync(TOKEN_PATH, "utf-8").trim(); - expect(token).toHaveLength(64); // 32 bytes = 64 hex chars - - // Send hello request - const request: IpcRequest = { - id: "test-1", - type: "hello", - payload: { - token, - protocolVersion: PROTOCOL_VERSION, - clientId: "test-client", - role: "control", - }, - }; - - const response = await sendRequest(socket, request); - - expect(response.id).toBe("test-1"); - expect(response.ok).toBe(true); - - if (response.ok) { - const payload = response.payload as HelloResponse; - expect(payload.protocolVersion).toBe(PROTOCOL_VERSION); - expect(payload.daemonVersion).toBe("1.0.0"); - expect(payload.daemonPid).toBeGreaterThan(0); - } - } finally { - socket.destroy(); - } - }); - - it("should reject hello request with invalid token", async () => { - const socket = await connectToDaemon(); - - try { - const request: IpcRequest = { - id: "test-2", - type: "hello", - payload: { - token: "invalid-token", - protocolVersion: PROTOCOL_VERSION, - clientId: "test-client", - role: "control", - }, - }; - - const response = await sendRequest(socket, request); - - expect(response.id).toBe("test-2"); - expect(response.ok).toBe(false); - - if (!response.ok) { - expect(response.error.code).toBe("AUTH_FAILED"); - } - } finally { - socket.destroy(); - } - }); - - it("should reject hello request with wrong protocol version", async () => { - const socket = await connectToDaemon(); - - try { - const token = readFileSync(TOKEN_PATH, "utf-8").trim(); - - const request: IpcRequest = { - id: "test-3", - type: "hello", - payload: { - token, - protocolVersion: 999, // Invalid version - clientId: "test-client", - role: "control", - }, - }; - - const response = await sendRequest(socket, request); - - expect(response.id).toBe("test-3"); - expect(response.ok).toBe(false); - - if (!response.ok) { - expect(response.error.code).toBe("PROTOCOL_MISMATCH"); - } - } finally { - socket.destroy(); - } - }); - }); - - describe("authentication requirement", () => { - it("should reject requests before authentication", async () => { - const socket = await connectToDaemon(); - - try { - // Try to list sessions without authenticating first - const request: IpcRequest = { - id: "test-4", - type: "listSessions", - payload: undefined, - }; - - const response = await sendRequest(socket, request); - - expect(response.id).toBe("test-4"); - expect(response.ok).toBe(false); - - if (!response.ok) { - expect(response.error.code).toBe("NOT_AUTHENTICATED"); - } - } finally { - socket.destroy(); - } - }); - - it("should allow listSessions after authentication", async () => { - const socket = await connectToDaemon(); - - try { - const token = readFileSync(TOKEN_PATH, "utf-8").trim(); - - // Authenticate first - const helloRequest: IpcRequest = { - id: "test-5a", - type: "hello", - payload: { - token, - protocolVersion: PROTOCOL_VERSION, - clientId: "test-client", - role: "control", - }, - }; - - const helloResponse = await sendRequest(socket, helloRequest); - expect(helloResponse.ok).toBe(true); - - // Now list sessions - const listRequest: IpcRequest = { - id: "test-5b", - type: "listSessions", - payload: undefined, - }; - - const listResponse = await sendRequest(socket, listRequest); - - expect(listResponse.id).toBe("test-5b"); - expect(listResponse.ok).toBe(true); - - if (listResponse.ok) { - const payload = listResponse.payload as { sessions: unknown[] }; - expect(payload.sessions).toEqual([]); - } - } finally { - socket.destroy(); - } - }); - }); - - describe("unknown requests", () => { - it("should return error for unknown request type", async () => { - const socket = await connectToDaemon(); - - try { - const token = readFileSync(TOKEN_PATH, "utf-8").trim(); - - // Authenticate first - const helloRequest: IpcRequest = { - id: "test-6a", - type: "hello", - payload: { - token, - protocolVersion: PROTOCOL_VERSION, - clientId: "test-client", - role: "control", - }, - }; - - await sendRequest(socket, helloRequest); - - // Send unknown request - const unknownRequest: IpcRequest = { - id: "test-6b", - type: "unknownRequestType", - payload: {}, - }; - - const response = await sendRequest(socket, unknownRequest); - - expect(response.id).toBe("test-6b"); - expect(response.ok).toBe(false); - - if (!response.ok) { - expect(response.error.code).toBe("UNKNOWN_REQUEST"); - } - } finally { - socket.destroy(); - } - }); - }); - }); -} diff --git a/apps/desktop/src/main/terminal-host/session-lifecycle.test.ts b/apps/desktop/src/main/terminal-host/session-lifecycle.test.ts deleted file mode 100644 index 1f0e6fdc92b..00000000000 --- a/apps/desktop/src/main/terminal-host/session-lifecycle.test.ts +++ /dev/null @@ -1,361 +0,0 @@ -/** - * Terminal Host Session Lifecycle Integration Tests - * - * Tests the full session lifecycle: - * 1. Create session with PTY - * 2. Write data to terminal - * 3. Receive output events - * 4. Resize terminal - * 5. List sessions - * 6. Kill session - */ - -import { afterAll, beforeAll, describe, expect, it } from "bun:test"; -import type { ChildProcess } from "node:child_process"; -import { spawn } from "node:child_process"; -import { - existsSync, - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, -} from "node:fs"; -import { connect, type Socket } from "node:net"; -import { join, resolve } from "node:path"; -import { - type CreateOrAttachRequest, - type CreateOrAttachResponse, - type IpcRequest, - type IpcResponse, - PROTOCOL_VERSION, -} from "../lib/terminal-host/types"; -import { supportsLocalSocketBinding } from "./test-helpers"; - -// Test uses a dedicated workspace name for isolation -const SUPERSET_DIR_NAME = ".superset-test"; -const TEST_HOME_DIR = mkdtempSync("/tmp/sth-"); -const SUPERSET_HOME_DIR = join(TEST_HOME_DIR, SUPERSET_DIR_NAME); -const SOCKET_PATH = join(SUPERSET_HOME_DIR, "terminal-host.sock"); -const TOKEN_PATH = join(SUPERSET_HOME_DIR, "terminal-host.token"); -const PID_PATH = join(SUPERSET_HOME_DIR, "terminal-host.pid"); - -// Path to the daemon source file -const DAEMON_PATH = resolve(__dirname, "index.ts"); -// Polyfill for @xterm/headless in Bun (see xterm-env-polyfill.ts for details) -const XTERM_POLYFILL_PATH = resolve(__dirname, "xterm-env-polyfill.ts"); - -// Timeouts -const DAEMON_TIMEOUT = 10000; -const CONNECT_TIMEOUT = 5000; - -const canRunSessionLifecycleIntegration = supportsLocalSocketBinding(); - -if (!canRunSessionLifecycleIntegration) { - describe("Terminal Host Session Lifecycle", () => { - it("skips when local socket binding is unavailable", () => { - expect(true).toBe(true); - }); - }); -} else { - describe("Terminal Host Session Lifecycle", () => { - let daemonProcess: ChildProcess | null = null; - - /** - * Clean up any existing daemon artifacts - */ - function cleanup() { - if (existsSync(PID_PATH)) { - try { - const pid = Number.parseInt( - readFileSync(PID_PATH, "utf-8").trim(), - 10, - ); - if (pid > 0) { - process.kill(pid, "SIGTERM"); - } - } catch { - // Process might not exist - } - } - - if (existsSync(SOCKET_PATH)) { - try { - rmSync(SOCKET_PATH); - } catch { - // Ignore - } - } - - if (existsSync(PID_PATH)) { - try { - rmSync(PID_PATH); - } catch { - // Ignore - } - } - - if (existsSync(TOKEN_PATH)) { - try { - rmSync(TOKEN_PATH); - } catch { - // Ignore - } - } - } - - /** - * Start the daemon process - */ - async function startDaemon(): Promise { - return new Promise((resolve, reject) => { - if (!existsSync(SUPERSET_HOME_DIR)) { - mkdirSync(SUPERSET_HOME_DIR, { recursive: true, mode: 0o700 }); - } - - daemonProcess = spawn( - "bun", - ["run", "--preload", XTERM_POLYFILL_PATH, DAEMON_PATH], - { - env: { - ...process.env, - HOME: TEST_HOME_DIR, - NODE_ENV: "development", - SUPERSET_WORKSPACE_NAME: "test", - }, - stdio: ["ignore", "pipe", "pipe"], - detached: true, - }, - ); - - let output = ""; - let stderrOutput = ""; - let settled = false; - let timeoutId: ReturnType; - - daemonProcess.stdout?.on("data", (data) => { - output += data.toString(); - if (output.includes("Daemon started")) { - if (settled) return; - settled = true; - clearTimeout(timeoutId); - resolve(); - } - }); - - daemonProcess.stderr?.on("data", (data) => { - const text = data.toString(); - stderrOutput += text; - console.error("Daemon stderr:", text); - }); - - daemonProcess.on("error", (error) => { - if (settled) return; - settled = true; - clearTimeout(timeoutId); - reject( - new Error( - `Failed to start daemon: ${error.message}. stdout=${output} stderr=${stderrOutput}`, - ), - ); - }); - - daemonProcess.on("exit", (code, signal) => { - if (!settled && code !== 0 && code !== null) { - settled = true; - clearTimeout(timeoutId); - reject( - new Error( - `Daemon exited with code ${code}, signal ${signal}. stdout=${output} stderr=${stderrOutput}`, - ), - ); - } - }); - - timeoutId = setTimeout(() => { - if (settled) return; - settled = true; - reject( - new Error( - `Daemon failed to start within ${DAEMON_TIMEOUT}ms. stdout=${output} stderr=${stderrOutput}`, - ), - ); - }, DAEMON_TIMEOUT); - }); - } - - /** - * Stop the daemon process - */ - async function stopDaemon(): Promise { - if (daemonProcess) { - return new Promise((resolve) => { - daemonProcess?.on("exit", () => { - daemonProcess = null; - resolve(); - }); - - daemonProcess?.kill("SIGTERM"); - - setTimeout(() => { - if (daemonProcess) { - daemonProcess.kill("SIGKILL"); - daemonProcess = null; - resolve(); - } - }, 2000); - }); - } - } - - /** - * Connect to the daemon socket - */ - function connectToDaemon(): Promise { - return new Promise((resolve, reject) => { - const socket = connect(SOCKET_PATH); - - socket.on("connect", () => { - resolve(socket); - }); - - socket.on("error", (error) => { - reject(new Error(`Failed to connect to daemon: ${error.message}`)); - }); - - setTimeout(() => { - reject(new Error(`Connection timed out after ${CONNECT_TIMEOUT}ms`)); - }, CONNECT_TIMEOUT); - }); - } - - /** - * Send a request and wait for response - */ - function sendRequest( - socket: Socket, - request: IpcRequest, - ): Promise { - return new Promise((resolve, reject) => { - let buffer = ""; - let timeoutId: ReturnType; - - const onData = (data: Buffer) => { - buffer += data.toString(); - const newlineIndex = buffer.indexOf("\n"); - if (newlineIndex !== -1) { - const line = buffer.slice(0, newlineIndex); - buffer = buffer.slice(newlineIndex + 1); - socket.off("data", onData); - clearTimeout(timeoutId); - try { - resolve(JSON.parse(line)); - } catch (_error) { - reject(new Error(`Failed to parse response: ${line}`)); - } - } - }; - - socket.on("data", onData); - socket.write(`${JSON.stringify(request)}\n`); - - timeoutId = setTimeout(() => { - socket.off("data", onData); - reject(new Error("Request timed out")); - }, 5000); - }); - } - - /** - * Authenticate with the daemon - */ - async function authenticate({ - socket, - clientId, - role, - }: { - socket: Socket; - clientId: string; - role: "control" | "stream"; - }): Promise { - const token = readFileSync(TOKEN_PATH, "utf-8").trim(); - - const request: IpcRequest = { - id: `auth-${Date.now()}`, - type: "hello", - payload: { - token, - protocolVersion: PROTOCOL_VERSION, - clientId, - role, - }, - }; - - const response = await sendRequest(socket, request); - if (!response.ok) { - throw new Error(`Authentication failed: ${JSON.stringify(response)}`); - } - } - - async function connectClient(): Promise<{ - control: Socket; - stream: Socket; - clientId: string; - }> { - const control = await connectToDaemon(); - const stream = await connectToDaemon(); - const clientId = `test-client-${Date.now()}-${Math.random().toString(16).slice(2)}`; - await authenticate({ socket: control, clientId, role: "control" }); - await authenticate({ socket: stream, clientId, role: "stream" }); - return { control, stream, clientId }; - } - - beforeAll(async () => { - cleanup(); - await startDaemon(); - }); - - afterAll(async () => { - await stopDaemon(); - cleanup(); - rmSync(TEST_HOME_DIR, { recursive: true, force: true }); - }); - - describe("session creation", () => { - it("should create a new session and return snapshot", async () => { - const { control, stream } = await connectClient(); - - try { - const createRequest: IpcRequest = { - id: "test-create-1", - type: "createOrAttach", - payload: { - sessionId: "test-session-1", - workspaceId: "workspace-1", - paneId: "pane-1", - tabId: "tab-1", - cols: 80, - rows: 24, - cwd: process.env.HOME, - } satisfies CreateOrAttachRequest, - }; - - const response = await sendRequest(control, createRequest); - - expect(response.id).toBe("test-create-1"); - expect(response.ok).toBe(true); - - if (response.ok) { - const payload = response.payload as CreateOrAttachResponse; - expect(payload.isNew).toBe(true); - expect(payload.snapshot).toBeDefined(); - expect(payload.snapshot.cols).toBe(80); - expect(payload.snapshot.rows).toBe(24); - } - } finally { - control.destroy(); - stream.destroy(); - } - }); - }); - }); -} From 9bebda960a1b6cda7cd2113628f6804859aec0ec Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 16 Mar 2026 15:36:46 -0700 Subject: [PATCH 12/14] Fix desktop test compatibility with main --- .../routers/workspaces/utils/shell-env.ts | 28 +++++++++++++++++++ .../lib/window-state/bounds-validation.ts | 8 +++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/shell-env.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/shell-env.ts index 97f7965fb6d..c79fc2fa67a 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/shell-env.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/shell-env.ts @@ -16,6 +16,12 @@ const FALLBACK_CACHE_TTL_MS = 10_000; // 10 second cache for fallback (retry soo const TIMEOUT_FALLBACK_CACHE_TTL_MS = 60_000; // 1 minute fallback when shell startup hangs const SHELL_ENV_TIMEOUT_MS = 8_000; let fallbackCacheTtlMs = FALLBACK_CACHE_TTL_MS; +const COMMON_MACOS_PATHS = [ + "/opt/homebrew/bin", + "/opt/homebrew/sbin", + "/usr/local/bin", + "/usr/local/sbin", +]; // Track PATH fix state for macOS GUI app PATH fix let pathFixAttempted = false; @@ -84,6 +90,7 @@ export async function getShellEnvironment( fallback[key] = value; } } + augmentPathForMacOS(fallback); cachedEnv = fallback; cacheTime = now; isFallbackCache = true; @@ -94,6 +101,27 @@ export async function getShellEnvironment( } } +/** + * On macOS, GUI apps get a minimal PATH that may exclude Homebrew and other + * user-installed tool directories. Augment with well-known locations so git + * and similar binaries can be found even during fallback. + */ +export function augmentPathForMacOS( + env: Record, + platform: NodeJS.Platform = process.platform, +): void { + if (platform !== "darwin") return; + + const currentPath = env.PATH ?? ""; + const currentEntries = currentPath.split(":").filter(Boolean); + const pathEntries = new Set(currentEntries); + const missingPaths = COMMON_MACOS_PATHS.filter( + (path) => !pathEntries.has(path), + ); + + env.PATH = [...missingPaths, currentPath].filter(Boolean).join(":"); +} + /** * Clears the cached shell environment. * Useful for testing or when environment changes are expected. diff --git a/apps/desktop/src/main/lib/window-state/bounds-validation.ts b/apps/desktop/src/main/lib/window-state/bounds-validation.ts index 03984773147..0cf3a303f70 100644 --- a/apps/desktop/src/main/lib/window-state/bounds-validation.ts +++ b/apps/desktop/src/main/lib/window-state/bounds-validation.ts @@ -1,5 +1,5 @@ import type { Rectangle } from "electron"; -import { screen } from "electron"; +import * as electron from "electron"; import type { WindowState } from "./window-state"; const MIN_VISIBLE_OVERLAP = 50; @@ -10,7 +10,7 @@ const MIN_WINDOW_SIZE = 400; * Returns false if window would be completely off-screen (e.g., monitor disconnected). */ export function isVisibleOnAnyDisplay(bounds: Rectangle): boolean { - const displays = screen.getAllDisplays(); + const displays = electron.screen.getAllDisplays(); return displays.some((display) => { const db = display.bounds; @@ -31,7 +31,7 @@ function clampToWorkArea( width: number, height: number, ): { width: number; height: number } { - const { workAreaSize } = screen.getPrimaryDisplay(); + const { workAreaSize } = electron.screen.getPrimaryDisplay(); return { width: Math.min(Math.max(width, MIN_WINDOW_SIZE), workAreaSize.width), height: Math.min(Math.max(height, MIN_WINDOW_SIZE), workAreaSize.height), @@ -57,7 +57,7 @@ export interface InitialWindowBounds { export function getInitialWindowBounds( savedState: WindowState | null, ): InitialWindowBounds { - const { workAreaSize } = screen.getPrimaryDisplay(); + const { workAreaSize } = electron.screen.getPrimaryDisplay(); // No saved state → default to primary display size, centered if (!savedState) { From 4b601c423d05b76199f70cef77be35e13487ddcf Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 16 Mar 2026 15:40:43 -0700 Subject: [PATCH 13/14] Align shell env fallback with main --- .../routers/workspaces/utils/shell-env.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/shell-env.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/shell-env.ts index c79fc2fa67a..5222791387d 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/shell-env.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/shell-env.ts @@ -16,12 +16,6 @@ const FALLBACK_CACHE_TTL_MS = 10_000; // 10 second cache for fallback (retry soo const TIMEOUT_FALLBACK_CACHE_TTL_MS = 60_000; // 1 minute fallback when shell startup hangs const SHELL_ENV_TIMEOUT_MS = 8_000; let fallbackCacheTtlMs = FALLBACK_CACHE_TTL_MS; -const COMMON_MACOS_PATHS = [ - "/opt/homebrew/bin", - "/opt/homebrew/sbin", - "/usr/local/bin", - "/usr/local/sbin", -]; // Track PATH fix state for macOS GUI app PATH fix let pathFixAttempted = false; @@ -101,24 +95,29 @@ export async function getShellEnvironment( } } +const COMMON_MACOS_PATHS = [ + "/opt/homebrew/bin", + "/opt/homebrew/sbin", + "/usr/local/bin", + "/usr/local/sbin", +]; + /** - * On macOS, GUI apps get a minimal PATH that may exclude Homebrew and other - * user-installed tool directories. Augment with well-known locations so git - * and similar binaries can be found even during fallback. + * On macOS, Electron GUI apps get a minimal PATH that may exclude + * Homebrew and other user-installed tool directories. Augment with + * well-known locations so git and similar binaries can be found. */ export function augmentPathForMacOS( env: Record, platform: NodeJS.Platform = process.platform, ): void { if (platform !== "darwin") return; - const currentPath = env.PATH ?? ""; const currentEntries = currentPath.split(":").filter(Boolean); const pathEntries = new Set(currentEntries); const missingPaths = COMMON_MACOS_PATHS.filter( (path) => !pathEntries.has(path), ); - env.PATH = [...missingPaths, currentPath].filter(Boolean).join(":"); } From 5ab43c7eecf45b14bcb0777ea2742518f658c2bc Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Mon, 16 Mar 2026 15:54:29 -0700 Subject: [PATCH 14/14] Revert unrelated desktop runtime fixes --- .../routers/workspaces/utils/shell-env.ts | 27 ------------------- .../lib/window-state/bounds-validation.ts | 8 +++--- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/shell-env.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/shell-env.ts index 5222791387d..97f7965fb6d 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/shell-env.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/shell-env.ts @@ -84,7 +84,6 @@ export async function getShellEnvironment( fallback[key] = value; } } - augmentPathForMacOS(fallback); cachedEnv = fallback; cacheTime = now; isFallbackCache = true; @@ -95,32 +94,6 @@ export async function getShellEnvironment( } } -const COMMON_MACOS_PATHS = [ - "/opt/homebrew/bin", - "/opt/homebrew/sbin", - "/usr/local/bin", - "/usr/local/sbin", -]; - -/** - * On macOS, Electron GUI apps get a minimal PATH that may exclude - * Homebrew and other user-installed tool directories. Augment with - * well-known locations so git and similar binaries can be found. - */ -export function augmentPathForMacOS( - env: Record, - platform: NodeJS.Platform = process.platform, -): void { - if (platform !== "darwin") return; - const currentPath = env.PATH ?? ""; - const currentEntries = currentPath.split(":").filter(Boolean); - const pathEntries = new Set(currentEntries); - const missingPaths = COMMON_MACOS_PATHS.filter( - (path) => !pathEntries.has(path), - ); - env.PATH = [...missingPaths, currentPath].filter(Boolean).join(":"); -} - /** * Clears the cached shell environment. * Useful for testing or when environment changes are expected. diff --git a/apps/desktop/src/main/lib/window-state/bounds-validation.ts b/apps/desktop/src/main/lib/window-state/bounds-validation.ts index 0cf3a303f70..03984773147 100644 --- a/apps/desktop/src/main/lib/window-state/bounds-validation.ts +++ b/apps/desktop/src/main/lib/window-state/bounds-validation.ts @@ -1,5 +1,5 @@ import type { Rectangle } from "electron"; -import * as electron from "electron"; +import { screen } from "electron"; import type { WindowState } from "./window-state"; const MIN_VISIBLE_OVERLAP = 50; @@ -10,7 +10,7 @@ const MIN_WINDOW_SIZE = 400; * Returns false if window would be completely off-screen (e.g., monitor disconnected). */ export function isVisibleOnAnyDisplay(bounds: Rectangle): boolean { - const displays = electron.screen.getAllDisplays(); + const displays = screen.getAllDisplays(); return displays.some((display) => { const db = display.bounds; @@ -31,7 +31,7 @@ function clampToWorkArea( width: number, height: number, ): { width: number; height: number } { - const { workAreaSize } = electron.screen.getPrimaryDisplay(); + const { workAreaSize } = screen.getPrimaryDisplay(); return { width: Math.min(Math.max(width, MIN_WINDOW_SIZE), workAreaSize.width), height: Math.min(Math.max(height, MIN_WINDOW_SIZE), workAreaSize.height), @@ -57,7 +57,7 @@ export interface InitialWindowBounds { export function getInitialWindowBounds( savedState: WindowState | null, ): InitialWindowBounds { - const { workAreaSize } = electron.screen.getPrimaryDisplay(); + const { workAreaSize } = screen.getPrimaryDisplay(); // No saved state → default to primary display size, centered if (!savedState) {