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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,12 @@ export function DashboardSidebarProjectSection({
>
<div className={cn("border-b border-border last:border-b-0")}>
<DashboardSidebarCollapsedProjectContent
projectId={project.id}
projectName={project.name}
iconUrl={project.iconUrl}
isCollapsed={project.isCollapsed}
totalWorkspaceCount={totalWorkspaceCount}
workspaces={flattenedCollapsedWorkspaces}
projectChildren={project.children}
workspaceShortcutLabels={workspaceShortcutLabels}
onWorkspaceHover={onWorkspaceHover}
onToggleCollapse={() => onToggleCollapse(project.id)}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, string>;
onWorkspaceHover: (workspaceId: string) => void | Promise<void>;
onToggleCollapse: () => void;
Expand All @@ -24,11 +32,12 @@ export const DashboardSidebarCollapsedProjectContent = forwardRef<
>(
(
{
projectId,
projectName,
iconUrl,
isCollapsed,
totalWorkspaceCount,
workspaces,
projectChildren,
workspaceShortcutLabels,
onWorkspaceHover,
onToggleCollapse,
Expand All @@ -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 (
<div
ref={ref}
Expand Down Expand Up @@ -78,15 +103,41 @@ export const DashboardSidebarCollapsedProjectContent = forwardRef<
className="overflow-hidden w-full"
>
<div className="flex w-full flex-col pt-1">
{workspaces.map((workspace) => (
<DashboardSidebarWorkspaceItem
key={workspace.id}
workspace={workspace}
onHoverCardOpen={() => onWorkspaceHover(workspace.id)}
shortcutLabel={workspaceShortcutLabels.get(workspace.id)}
isCollapsed
/>
))}
<DndContext
sensors={sensors}
collisionDetection={collisionDetection}
measuring={measuring}
{...handlers}
>
<SortableContext
items={workspaceItems}
strategy={verticalListSortingStrategy}
>
{workspaceItems.map((id) => {
const parsed = parseId(id);
if (!parsed) return null;
const workspace = workspacesById.get(parsed.realId);
if (!workspace) return null;
return (
<SortableCollapsedWorkspaceItem
key={String(id)}
sortableId={String(id)}
workspace={workspace}
onHoverCardOpen={() =>
onWorkspaceHover(parsed.realId)
}
shortcutLabel={workspaceShortcutLabels.get(
parsed.realId,
)}
disabled={
workspace.type === "main" &&
workspace.hostType === "local-device"
}
/>
);
})}
</SortableContext>
</DndContext>
</div>
</motion.div>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div
ref={setNodeRef}
className="w-full"
style={{
transform: CSS.Translate.toString(transform),
transition,
opacity: isDragging ? 0.5 : undefined,
position: isDragging ? "relative" : undefined,
zIndex: isDragging ? 10 : undefined,
}}
{...attributes}
{...listeners}
>
<DashboardSidebarWorkspaceItem
workspace={workspace}
onHoverCardOpen={onHoverCardOpen}
shortcutLabel={shortcutLabel}
isCollapsed
/>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SortableCollapsedWorkspaceItem } from "./SortableCollapsedWorkspaceItem";
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand All @@ -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(() => {
Expand Down
Loading