-
Notifications
You must be signed in to change notification settings - Fork 962
feat(desktop): drag-and-drop reordering for v2 sidebar #3222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6a12647
d751262
4d3a4d9
69979a7
92acb0d
f7dd3d7
7ee9b13
7e760dd
e47c14b
473659f
50b1da6
0d29663
81a5769
b41c49c
829394c
470e54d
625756b
f8afc6b
d1e04b8
5a61525
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,34 +1,188 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||
| closestCenter, | ||||||||||||||||||||||||||||||||||||||||||||||
| DndContext, | ||||||||||||||||||||||||||||||||||||||||||||||
| type DragEndEvent, | ||||||||||||||||||||||||||||||||||||||||||||||
| DragOverlay, | ||||||||||||||||||||||||||||||||||||||||||||||
| KeyboardSensor, | ||||||||||||||||||||||||||||||||||||||||||||||
| MeasuringStrategy, | ||||||||||||||||||||||||||||||||||||||||||||||
| MouseSensor, | ||||||||||||||||||||||||||||||||||||||||||||||
| TouchSensor, | ||||||||||||||||||||||||||||||||||||||||||||||
| useSensor, | ||||||||||||||||||||||||||||||||||||||||||||||
| useSensors, | ||||||||||||||||||||||||||||||||||||||||||||||
| } from "@dnd-kit/core"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||
| arrayMove, | ||||||||||||||||||||||||||||||||||||||||||||||
| SortableContext, | ||||||||||||||||||||||||||||||||||||||||||||||
| sortableKeyboardCoordinates, | ||||||||||||||||||||||||||||||||||||||||||||||
| useSortable, | ||||||||||||||||||||||||||||||||||||||||||||||
| verticalListSortingStrategy, | ||||||||||||||||||||||||||||||||||||||||||||||
| } from "@dnd-kit/sortable"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { CSS } from "@dnd-kit/utilities"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { createPortal } from "react-dom"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { DashboardSidebarHeader } from "./components/DashboardSidebarHeader"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { DashboardSidebarProjectSection } from "./components/DashboardSidebarProjectSection"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { useDashboardSidebarData } from "./hooks/useDashboardSidebarData"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { useDashboardSidebarShortcuts } from "./hooks/useDashboardSidebarShortcuts"; | ||||||||||||||||||||||||||||||||||||||||||||||
| import type { DashboardSidebarProject } from "./types"; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| interface DashboardSidebarProps { | ||||||||||||||||||||||||||||||||||||||||||||||
| isCollapsed?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| function SortableProjectWrapper({ | ||||||||||||||||||||||||||||||||||||||||||||||
| project, | ||||||||||||||||||||||||||||||||||||||||||||||
| isCollapsed, | ||||||||||||||||||||||||||||||||||||||||||||||
| isDraggingProject, | ||||||||||||||||||||||||||||||||||||||||||||||
| workspaceShortcutLabels, | ||||||||||||||||||||||||||||||||||||||||||||||
| onWorkspaceHover, | ||||||||||||||||||||||||||||||||||||||||||||||
| onToggleCollapse, | ||||||||||||||||||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||||||||||||||||||
| project: DashboardSidebarProject; | ||||||||||||||||||||||||||||||||||||||||||||||
| isCollapsed: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||
| isDraggingProject: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||
| workspaceShortcutLabels: Map<string, string>; | ||||||||||||||||||||||||||||||||||||||||||||||
| onWorkspaceHover: (workspaceId: string) => void | Promise<void>; | ||||||||||||||||||||||||||||||||||||||||||||||
| onToggleCollapse: (projectId: string) => void; | ||||||||||||||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||||||||||||||||||||||
| attributes, | ||||||||||||||||||||||||||||||||||||||||||||||
| listeners, | ||||||||||||||||||||||||||||||||||||||||||||||
| setNodeRef, | ||||||||||||||||||||||||||||||||||||||||||||||
| transform, | ||||||||||||||||||||||||||||||||||||||||||||||
| transition, | ||||||||||||||||||||||||||||||||||||||||||||||
| isDragging, | ||||||||||||||||||||||||||||||||||||||||||||||
| } = useSortable({ id: project.id }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||||
| ref={setNodeRef} | ||||||||||||||||||||||||||||||||||||||||||||||
| style={{ | ||||||||||||||||||||||||||||||||||||||||||||||
| transform: CSS.Translate.toString(transform), | ||||||||||||||||||||||||||||||||||||||||||||||
| transition, | ||||||||||||||||||||||||||||||||||||||||||||||
| opacity: isDragging ? 0.5 : undefined, | ||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| <DashboardSidebarProjectSection | ||||||||||||||||||||||||||||||||||||||||||||||
| project={project} | ||||||||||||||||||||||||||||||||||||||||||||||
| isSidebarCollapsed={isCollapsed} | ||||||||||||||||||||||||||||||||||||||||||||||
| isDraggingProject={isDraggingProject} | ||||||||||||||||||||||||||||||||||||||||||||||
| workspaceShortcutLabels={workspaceShortcutLabels} | ||||||||||||||||||||||||||||||||||||||||||||||
| onWorkspaceHover={onWorkspaceHover} | ||||||||||||||||||||||||||||||||||||||||||||||
| onToggleCollapse={onToggleCollapse} | ||||||||||||||||||||||||||||||||||||||||||||||
| dragHandleListeners={listeners} | ||||||||||||||||||||||||||||||||||||||||||||||
| dragHandleAttributes={attributes} | ||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export function DashboardSidebar({ | ||||||||||||||||||||||||||||||||||||||||||||||
| isCollapsed = false, | ||||||||||||||||||||||||||||||||||||||||||||||
| }: DashboardSidebarProps) { | ||||||||||||||||||||||||||||||||||||||||||||||
| const { groups, refreshWorkspacePullRequest, toggleProjectCollapsed } = | ||||||||||||||||||||||||||||||||||||||||||||||
| useDashboardSidebarData(); | ||||||||||||||||||||||||||||||||||||||||||||||
| const workspaceShortcutLabels = useDashboardSidebarShortcuts(groups); | ||||||||||||||||||||||||||||||||||||||||||||||
| const { reorderProjects } = useDashboardSidebarState(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const sensors = useSensors( | ||||||||||||||||||||||||||||||||||||||||||||||
| useSensor(MouseSensor, { activationConstraint: { distance: 8 } }), | ||||||||||||||||||||||||||||||||||||||||||||||
| useSensor(TouchSensor, { | ||||||||||||||||||||||||||||||||||||||||||||||
| activationConstraint: { delay: 200, tolerance: 5 }, | ||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||
| useSensor(KeyboardSensor, { | ||||||||||||||||||||||||||||||||||||||||||||||
| coordinateGetter: sortableKeyboardCoordinates, | ||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const [activeProject, setActiveProject] = | ||||||||||||||||||||||||||||||||||||||||||||||
| useState<DashboardSidebarProject | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // Local project order — syncs from groups, updated on drag end | ||||||||||||||||||||||||||||||||||||||||||||||
| const [projectOrder, setProjectOrder] = useState(() => | ||||||||||||||||||||||||||||||||||||||||||||||
| groups.map((p) => p.id), | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||
| setProjectOrder(groups.map((p) => p.id)); | ||||||||||||||||||||||||||||||||||||||||||||||
| }, [groups]); | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+106
to
+108
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2 - Style] projectOrder resets unconditionally on any groups changeUnlike Consider applying the same fingerprint guard used in
Suggested change
Comment on lines
+106
to
+108
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Unlike
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const orderedGroups = useMemo(() => { | ||||||||||||||||||||||||||||||||||||||||||||||
| const byId = new Map(groups.map((g) => [g.id, g])); | ||||||||||||||||||||||||||||||||||||||||||||||
| return projectOrder | ||||||||||||||||||||||||||||||||||||||||||||||
| .map((id) => byId.get(id)) | ||||||||||||||||||||||||||||||||||||||||||||||
| .filter((g): g is DashboardSidebarProject => g != null); | ||||||||||||||||||||||||||||||||||||||||||||||
| }, [groups, projectOrder]); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const handleDragEnd = useCallback( | ||||||||||||||||||||||||||||||||||||||||||||||
| ({ active, over }: DragEndEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (over && active.id !== over.id) { | ||||||||||||||||||||||||||||||||||||||||||||||
| const oldIndex = projectOrder.indexOf(String(active.id)); | ||||||||||||||||||||||||||||||||||||||||||||||
| const newIndex = projectOrder.indexOf(String(over.id)); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (oldIndex !== -1 && newIndex !== -1) { | ||||||||||||||||||||||||||||||||||||||||||||||
| const reordered = arrayMove(projectOrder, oldIndex, newIndex); | ||||||||||||||||||||||||||||||||||||||||||||||
| setProjectOrder(reordered); | ||||||||||||||||||||||||||||||||||||||||||||||
| reorderProjects(reordered); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| setActiveProject(null); | ||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||
| [projectOrder, reorderProjects], | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex h-full flex-col border-r border-border bg-muted/45 dark:bg-muted/35"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <DashboardSidebarHeader isCollapsed={isCollapsed} /> | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex-1 overflow-y-auto hide-scrollbar"> | ||||||||||||||||||||||||||||||||||||||||||||||
| {groups.map((project) => ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <DashboardSidebarProjectSection | ||||||||||||||||||||||||||||||||||||||||||||||
| key={project.id} | ||||||||||||||||||||||||||||||||||||||||||||||
| project={project} | ||||||||||||||||||||||||||||||||||||||||||||||
| isSidebarCollapsed={isCollapsed} | ||||||||||||||||||||||||||||||||||||||||||||||
| workspaceShortcutLabels={workspaceShortcutLabels} | ||||||||||||||||||||||||||||||||||||||||||||||
| onWorkspaceHover={refreshWorkspacePullRequest} | ||||||||||||||||||||||||||||||||||||||||||||||
| onToggleCollapse={toggleProjectCollapsed} | ||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||
| <DndContext | ||||||||||||||||||||||||||||||||||||||||||||||
| sensors={sensors} | ||||||||||||||||||||||||||||||||||||||||||||||
| collisionDetection={closestCenter} | ||||||||||||||||||||||||||||||||||||||||||||||
| measuring={{ | ||||||||||||||||||||||||||||||||||||||||||||||
| droppable: { strategy: MeasuringStrategy.Always }, | ||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||
| onDragStart={({ active }) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| const project = groups.find((p) => p.id === active.id); | ||||||||||||||||||||||||||||||||||||||||||||||
| setActiveProject(project ?? null); | ||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||
| onDragEnd={handleDragEnd} | ||||||||||||||||||||||||||||||||||||||||||||||
| onDragCancel={() => setActiveProject(null)} | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| <SortableContext | ||||||||||||||||||||||||||||||||||||||||||||||
| items={projectOrder} | ||||||||||||||||||||||||||||||||||||||||||||||
| strategy={verticalListSortingStrategy} | ||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||
| {orderedGroups.map((project) => ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <SortableProjectWrapper | ||||||||||||||||||||||||||||||||||||||||||||||
| key={project.id} | ||||||||||||||||||||||||||||||||||||||||||||||
| project={project} | ||||||||||||||||||||||||||||||||||||||||||||||
| isCollapsed={isCollapsed} | ||||||||||||||||||||||||||||||||||||||||||||||
| isDraggingProject={activeProject != null} | ||||||||||||||||||||||||||||||||||||||||||||||
| workspaceShortcutLabels={workspaceShortcutLabels} | ||||||||||||||||||||||||||||||||||||||||||||||
| onWorkspaceHover={refreshWorkspacePullRequest} | ||||||||||||||||||||||||||||||||||||||||||||||
| onToggleCollapse={toggleProjectCollapsed} | ||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||
| </SortableContext> | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| {createPortal( | ||||||||||||||||||||||||||||||||||||||||||||||
| <DragOverlay dropAnimation={null}> | ||||||||||||||||||||||||||||||||||||||||||||||
| {activeProject && ( | ||||||||||||||||||||||||||||||||||||||||||||||
| <div className="bg-background shadow-lg border-b border-border"> | ||||||||||||||||||||||||||||||||||||||||||||||
| <DashboardSidebarProjectSection | ||||||||||||||||||||||||||||||||||||||||||||||
| project={activeProject} | ||||||||||||||||||||||||||||||||||||||||||||||
| isSidebarCollapsed={isCollapsed} | ||||||||||||||||||||||||||||||||||||||||||||||
| isDraggingProject | ||||||||||||||||||||||||||||||||||||||||||||||
| workspaceShortcutLabels={workspaceShortcutLabels} | ||||||||||||||||||||||||||||||||||||||||||||||
| onWorkspaceHover={() => {}} | ||||||||||||||||||||||||||||||||||||||||||||||
| onToggleCollapse={() => {}} | ||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||
| </DragOverlay>, | ||||||||||||||||||||||||||||||||||||||||||||||
| document.body, | ||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||
| </DndContext> | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: This effect can overwrite
flatItemsduring an active drag ifprojectChildrenchanges externally (e.g., a workspace is created while the user is mid-drag). This snaps items back to the server order and breaks the drag. Add an early return whenactiveIdis set, and includeactiveIdin the dependency array.Prompt for AI agents