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 2baee8ed2d3..77a4953a41c 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 @@ -71,11 +71,12 @@ export function DashboardSidebarProjectSection({ >
onToggleCollapse(project.id)} 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 ac4479825dc..d92ab27e590 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 @@ -1,18 +1,26 @@ +import { DndContext } from "@dnd-kit/core"; +import { + SortableContext, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; import { AnimatePresence, motion } from "framer-motion"; -import { type ComponentPropsWithoutRef, forwardRef } from "react"; +import { type ComponentPropsWithoutRef, forwardRef, useMemo } from "react"; import { ProjectThumbnail } from "renderer/routes/_authenticated/components/ProjectThumbnail"; -import type { DashboardSidebarWorkspace } from "../../../../types"; -import { DashboardSidebarWorkspaceItem } from "../../../DashboardSidebarWorkspaceItem"; +import { useSidebarDnd } from "../../../../hooks/useSidebarDnd"; +import { parseId } from "../../../../hooks/useSidebarDnd/useSidebarDnd"; +import type { DashboardSidebarProjectChild } from "../../../../types"; +import { SortableCollapsedWorkspaceItem } from "./components/SortableCollapsedWorkspaceItem"; interface DashboardSidebarCollapsedProjectContentProps extends ComponentPropsWithoutRef<"div"> { + projectId: string; projectName: string; iconUrl: string | null; isCollapsed: boolean; totalWorkspaceCount: number; - workspaces: DashboardSidebarWorkspace[]; + projectChildren: DashboardSidebarProjectChild[]; workspaceShortcutLabels: Map; onWorkspaceHover: (workspaceId: string) => void | Promise; onToggleCollapse: () => void; @@ -24,11 +32,12 @@ export const DashboardSidebarCollapsedProjectContent = forwardRef< >( ( { + projectId, projectName, iconUrl, isCollapsed, totalWorkspaceCount, - workspaces, + projectChildren, workspaceShortcutLabels, onWorkspaceHover, onToggleCollapse, @@ -37,6 +46,22 @@ export const DashboardSidebarCollapsedProjectContent = forwardRef< }, ref, ) => { + const { + sensors, + measuring, + collisionDetection, + flatItems, + workspacesById, + handlers, + } = useSidebarDnd({ projectId, projectChildren }); + + // Sections aren't rendered in the collapsed rail — only workspace icons + // are sortable; useSidebarDnd still persists cross-section moves. + const workspaceItems = useMemo( + () => flatItems.filter((id) => parseId(id)?.type === "workspace"), + [flatItems], + ); + return (
- {workspaces.map((workspace) => ( - onWorkspaceHover(workspace.id)} - shortcutLabel={workspaceShortcutLabels.get(workspace.id)} - isCollapsed - /> - ))} + + + {workspaceItems.map((id) => { + const parsed = parseId(id); + if (!parsed) return null; + const workspace = workspacesById.get(parsed.realId); + if (!workspace) return null; + return ( + + onWorkspaceHover(parsed.realId) + } + shortcutLabel={workspaceShortcutLabels.get( + parsed.realId, + )} + disabled={ + workspace.type === "main" && + workspace.hostType === "local-device" + } + /> + ); + })} + +
)} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarCollapsedProjectContent/components/SortableCollapsedWorkspaceItem/SortableCollapsedWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarCollapsedProjectContent/components/SortableCollapsedWorkspaceItem/SortableCollapsedWorkspaceItem.tsx new file mode 100644 index 00000000000..9fc6a955b6e --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarCollapsedProjectContent/components/SortableCollapsedWorkspaceItem/SortableCollapsedWorkspaceItem.tsx @@ -0,0 +1,52 @@ +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import type { DashboardSidebarWorkspace } from "../../../../../../types"; +import { DashboardSidebarWorkspaceItem } from "../../../../../DashboardSidebarWorkspaceItem"; + +interface SortableCollapsedWorkspaceItemProps { + sortableId: string; + workspace: DashboardSidebarWorkspace; + onHoverCardOpen?: () => void; + shortcutLabel?: string; + disabled?: boolean; +} + +export function SortableCollapsedWorkspaceItem({ + sortableId, + workspace, + onHoverCardOpen, + shortcutLabel, + disabled, +}: SortableCollapsedWorkspaceItemProps) { + const { + setNodeRef, + attributes, + listeners, + isDragging, + transform, + transition, + } = useSortable({ id: sortableId, disabled }); + + return ( +
+ +
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarCollapsedProjectContent/components/SortableCollapsedWorkspaceItem/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarCollapsedProjectContent/components/SortableCollapsedWorkspaceItem/index.ts new file mode 100644 index 00000000000..085b64132b8 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/components/DashboardSidebarCollapsedProjectContent/components/SortableCollapsedWorkspaceItem/index.ts @@ -0,0 +1 @@ +export { SortableCollapsedWorkspaceItem } from "./SortableCollapsedWorkspaceItem"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useSidebarDnd/useSidebarDnd.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useSidebarDnd/useSidebarDnd.ts index df654f453a7..0c67794acc4 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useSidebarDnd/useSidebarDnd.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useSidebarDnd/useSidebarDnd.ts @@ -242,6 +242,31 @@ export function useSidebarDnd({ return null; // ungrouped — no section above }, [activeId, overId, activeType, flatItems, sectionsById]); + // The sidebar data builder always pins local main workspaces first, + // so any drop that lands an item above one would silently revert on + // the next rebuild (e.g. when the sidebar collapses and remounts). + // Normalize drop results to match what actually persists. + const normalizePinnedFirst = useCallback( + (items: UniqueIdentifier[]) => { + const pinned: UniqueIdentifier[] = []; + const rest: UniqueIdentifier[] = []; + for (const id of items) { + const parsed = parseId(id); + const ws = + parsed?.type === "workspace" + ? workspacesById.get(parsed.realId) + : null; + if (ws?.type === "main" && ws.hostType === "local-device") { + pinned.push(id); + } else { + rest.push(id); + } + } + return pinned.length > 0 ? [...pinned, ...rest] : items; + }, + [workspacesById], + ); + // ── Persistence ────────────────────────────────────────────────── const commitToDb = useCallback( @@ -310,13 +335,14 @@ export function useSidebarDnd({ } } - const newItems: UniqueIdentifier[] = [...ungrouped]; + const rebuilt: UniqueIdentifier[] = [...ungrouped]; for (const secSortId of reorderedSections) { - newItems.push(secSortId); + rebuilt.push(secSortId); const wsInSec = sectionGroups.get(String(secSortId)) ?? []; - newItems.push(...wsInSec); + rebuilt.push(...wsInSec); } + const newItems = normalizePinnedFirst(rebuilt); setFlatItems(newItems); commitToDb(newItems); } else { @@ -326,12 +352,14 @@ export function useSidebarDnd({ if (oldIndex === -1 || overIndex === -1 || oldIndex === overIndex) return; - const newItems = arrayMove(flatItems, oldIndex, overIndex); + const newItems = normalizePinnedFirst( + arrayMove(flatItems, oldIndex, overIndex), + ); setFlatItems(newItems); commitToDb(newItems); } }, - [flatItems, commitToDb], + [flatItems, commitToDb, normalizePinnedFirst], ); const onDragCancel = useCallback(() => {