From 208ec2fc40968bd07360f72ebdfda14a0927c96b Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 13 Jan 2026 16:45:35 -0800 Subject: [PATCH 1/5] feat(desktop): add drag-and-drop reordering for projects in sidebar Allow users to reorder projects in the sidebar using drag-and-drop, similar to how workspaces can already be reordered within projects. Server mutation is called only on drop (not during hover) to avoid excessive requests and race conditions. --- .../renderer/react-query/projects/index.ts | 1 + .../projects/useReorderProjects.ts | 20 +++++ .../ProjectSection/ProjectSection.tsx | 74 +++++++++++++++++-- .../WorkspaceSidebar/WorkspaceSidebar.tsx | 1 + 4 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 apps/desktop/src/renderer/react-query/projects/useReorderProjects.ts diff --git a/apps/desktop/src/renderer/react-query/projects/index.ts b/apps/desktop/src/renderer/react-query/projects/index.ts index d309afb1647..f18b249d9b3 100644 --- a/apps/desktop/src/renderer/react-query/projects/index.ts +++ b/apps/desktop/src/renderer/react-query/projects/index.ts @@ -1,2 +1,3 @@ export { useOpenNew } from "./useOpenNew"; +export { useReorderProjects } from "./useReorderProjects"; export { useUpdateProject } from "./useUpdateProject"; diff --git a/apps/desktop/src/renderer/react-query/projects/useReorderProjects.ts b/apps/desktop/src/renderer/react-query/projects/useReorderProjects.ts new file mode 100644 index 00000000000..964d7947dd2 --- /dev/null +++ b/apps/desktop/src/renderer/react-query/projects/useReorderProjects.ts @@ -0,0 +1,20 @@ +import { trpc } from "renderer/lib/trpc"; + +/** + * Mutation hook for reordering projects + * Automatically invalidates workspace and project queries on success + */ +export function useReorderProjects( + options?: Parameters[0], +) { + const utils = trpc.useUtils(); + + return trpc.projects.reorder.useMutation({ + ...options, + onSuccess: async (...args) => { + await utils.workspaces.getAllGrouped.invalidate(); + await utils.projects.getRecents.invalidate(); + await options?.onSuccess?.(...args); + }, + }); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx index d7fdb7e34a2..79d7a6c6eb7 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx @@ -1,9 +1,14 @@ +import { cn } from "@superset/ui/utils"; import { AnimatePresence, motion } from "framer-motion"; +import { useDrag, useDrop } from "react-dnd"; +import { useReorderProjects } from "renderer/react-query/projects"; import { useWorkspaceSidebarStore } from "renderer/stores"; import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; import { WorkspaceListItem } from "../WorkspaceListItem"; import { ProjectHeader } from "./ProjectHeader"; +const PROJECT_TYPE = "PROJECT"; + interface Workspace { id: string; projectId: string; @@ -25,6 +30,8 @@ interface ProjectSectionProps { activeWorkspaceId: string | null; /** Base index for keyboard shortcuts (0-based) */ shortcutBaseIndex: number; + /** Index for drag-and-drop reordering */ + index: number; /** Whether the sidebar is in collapsed mode */ isCollapsed?: boolean; } @@ -38,11 +45,13 @@ export function ProjectSection({ workspaces, activeWorkspaceId, shortcutBaseIndex, + index, isCollapsed: isSidebarCollapsed = false, }: ProjectSectionProps) { const { isProjectCollapsed, toggleProjectCollapsed } = useWorkspaceSidebarStore(); const openModal = useOpenNewWorkspaceModal(); + const reorderProjects = useReorderProjects(); const isCollapsed = isProjectCollapsed(projectId); @@ -50,10 +59,50 @@ export function ProjectSection({ openModal(projectId); }; + // Drag and drop for project reordering + const [{ isDragging }, drag] = useDrag( + () => ({ + type: PROJECT_TYPE, + item: { projectId, index, originalIndex: index }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }), + [projectId, index], + ); + + const [, drop] = useDrop({ + accept: PROJECT_TYPE, + hover: (item: { projectId: string; index: number; originalIndex: number }) => { + // Update local index for UI feedback only (no server call) + if (item.index !== index) { + item.index = index; + } + }, + drop: (item: { projectId: string; index: number; originalIndex: number }) => { + // Only call server mutation once on drop, if position changed + if (item.originalIndex !== item.index) { + reorderProjects.mutate({ + fromIndex: item.originalIndex, + toIndex: item.index, + }); + } + }, + }); + // When sidebar is collapsed, show compact view with just thumbnail and workspace icons if (isSidebarCollapsed) { return ( -
+
{ + drag(drop(node)); + }} + className={cn( + "flex flex-col items-center py-2 border-b border-border last:border-b-0", + isDragging && "opacity-30", + )} + style={{ cursor: isDragging ? "grabbing" : "grab" }} + >
- {workspaces.map((workspace, index) => ( + {workspaces.map((workspace, wsIndex) => ( ))} @@ -101,7 +150,16 @@ export function ProjectSection({ } return ( -
+
{ + drag(drop(node)); + }} + className={cn( + "border-b border-border last:border-b-0", + isDragging && "opacity-30", + )} + style={{ cursor: isDragging ? "grabbing" : "grab" }} + >
- {workspaces.map((workspace, index) => ( + {workspaces.map((workspace, wsIndex) => ( ))}
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx index 2515bdfc862..9e4765db15f 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx @@ -43,6 +43,7 @@ export function WorkspaceSidebar({ workspaces={group.workspaces} activeWorkspaceId={activeWorkspaceId} shortcutBaseIndex={projectShortcutIndices[index]} + index={index} isCollapsed={isCollapsed} /> ))} From 5a3aec03aa53c36dcda4b543fbc73ccdf139670e Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 13 Jan 2026 17:04:26 -0800 Subject: [PATCH 2/5] fix(desktop): add optimistic cache update for project drag UI feedback Use React Query cache manipulation during hover to provide immediate visual feedback while still only calling the server mutation on drop. --- .../ProjectSection/ProjectSection.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx index 79d7a6c6eb7..98e6622a7bc 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx @@ -1,6 +1,7 @@ import { cn } from "@superset/ui/utils"; import { AnimatePresence, motion } from "framer-motion"; import { useDrag, useDrop } from "react-dnd"; +import { trpc } from "renderer/lib/trpc"; import { useReorderProjects } from "renderer/react-query/projects"; import { useWorkspaceSidebarStore } from "renderer/stores"; import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; @@ -52,6 +53,7 @@ export function ProjectSection({ useWorkspaceSidebarStore(); const openModal = useOpenNewWorkspaceModal(); const reorderProjects = useReorderProjects(); + const utils = trpc.useUtils(); const isCollapsed = isProjectCollapsed(projectId); @@ -74,8 +76,15 @@ export function ProjectSection({ const [, drop] = useDrop({ accept: PROJECT_TYPE, hover: (item: { projectId: string; index: number; originalIndex: number }) => { - // Update local index for UI feedback only (no server call) if (item.index !== index) { + // Optimistically reorder in cache for immediate UI feedback + utils.workspaces.getAllGrouped.setData(undefined, (oldData) => { + if (!oldData) return oldData; + const newGroups = [...oldData]; + const [moved] = newGroups.splice(item.index, 1); + newGroups.splice(index, 0, moved); + return newGroups; + }); item.index = index; } }, From 0ba7529c07119c6ae7b260654b9fd0d19af2ad5c Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 13 Jan 2026 17:05:10 -0800 Subject: [PATCH 3/5] chore: clean up comments --- .../WorkspaceSidebar/ProjectSection/ProjectSection.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx index 98e6622a7bc..fd0796a8278 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx @@ -61,7 +61,6 @@ export function ProjectSection({ openModal(projectId); }; - // Drag and drop for project reordering const [{ isDragging }, drag] = useDrag( () => ({ type: PROJECT_TYPE, @@ -77,7 +76,6 @@ export function ProjectSection({ accept: PROJECT_TYPE, hover: (item: { projectId: string; index: number; originalIndex: number }) => { if (item.index !== index) { - // Optimistically reorder in cache for immediate UI feedback utils.workspaces.getAllGrouped.setData(undefined, (oldData) => { if (!oldData) return oldData; const newGroups = [...oldData]; @@ -89,7 +87,6 @@ export function ProjectSection({ } }, drop: (item: { projectId: string; index: number; originalIndex: number }) => { - // Only call server mutation once on drop, if position changed if (item.originalIndex !== item.index) { reorderProjects.mutate({ fromIndex: item.originalIndex, @@ -99,7 +96,6 @@ export function ProjectSection({ }, }); - // When sidebar is collapsed, show compact view with just thumbnail and workspace icons if (isSidebarCollapsed) { return (
Date: Tue, 13 Jan 2026 17:11:05 -0800 Subject: [PATCH 4/5] fix: add error toast for project reorder failures --- .../WorkspaceSidebar/ProjectSection/ProjectSection.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx index fd0796a8278..5e3b97dead7 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx @@ -1,3 +1,4 @@ +import { toast } from "@superset/ui/sonner"; import { cn } from "@superset/ui/utils"; import { AnimatePresence, motion } from "framer-motion"; import { useDrag, useDrop } from "react-dnd"; @@ -88,10 +89,10 @@ export function ProjectSection({ }, drop: (item: { projectId: string; index: number; originalIndex: number }) => { if (item.originalIndex !== item.index) { - reorderProjects.mutate({ - fromIndex: item.originalIndex, - toIndex: item.index, - }); + reorderProjects.mutate( + { fromIndex: item.originalIndex, toIndex: item.index }, + { onError: (error) => toast.error(`Failed to reorder: ${error.message}`) }, + ); } }, }); From 33deba6bd61052d12e4d08768136ab126305d544 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 13 Jan 2026 17:12:01 -0800 Subject: [PATCH 5/5] Fix lint --- .../ProjectSection/ProjectSection.tsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx index 5e3b97dead7..35456c95d52 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx @@ -75,7 +75,11 @@ export function ProjectSection({ const [, drop] = useDrop({ accept: PROJECT_TYPE, - hover: (item: { projectId: string; index: number; originalIndex: number }) => { + hover: (item: { + projectId: string; + index: number; + originalIndex: number; + }) => { if (item.index !== index) { utils.workspaces.getAllGrouped.setData(undefined, (oldData) => { if (!oldData) return oldData; @@ -87,11 +91,18 @@ export function ProjectSection({ item.index = index; } }, - drop: (item: { projectId: string; index: number; originalIndex: number }) => { + drop: (item: { + projectId: string; + index: number; + originalIndex: number; + }) => { if (item.originalIndex !== item.index) { reorderProjects.mutate( { fromIndex: item.originalIndex, toIndex: item.index }, - { onError: (error) => toast.error(`Failed to reorder: ${error.message}`) }, + { + onError: (error) => + toast.error(`Failed to reorder: ${error.message}`), + }, ); } },