11import { RenameTaskDialog } from "@components/RenameTaskDialog" ;
2+ import type { DragDropEvents } from "@dnd-kit/react" ;
3+ import { DragDropProvider , DragOverlay , PointerSensor } from "@dnd-kit/react" ;
24import { useTaskExecutionStore } from "@features/task-detail/stores/taskExecutionStore" ;
35import { useTasks } from "@features/tasks/hooks/useTasks" ;
46import { useTaskStore } from "@features/tasks/stores/taskStore" ;
57import { useMeQuery } from "@hooks/useMeQuery" ;
68import { useTaskContextMenu } from "@hooks/useTaskContextMenu" ;
7- import { FolderIcon } from "@phosphor-icons/react" ;
9+ import { FolderIcon , FolderOpenIcon } from "@phosphor-icons/react" ;
810import { Box , Flex } from "@radix-ui/themes" ;
911import { useRegisteredFoldersStore } from "@renderer/stores/registeredFoldersStore" ;
1012import type { Task } from "@shared/types" ;
1113import { useNavigationStore } from "@stores/navigationStore" ;
12- import { memo } from "react" ;
14+ import { memo , useCallback } from "react" ;
1315import { useWorkspaceStore } from "@/renderer/features/workspace/stores/workspaceStore" ;
1416import { useSidebarData } from "../hooks/useSidebarData" ;
1517import { useSidebarStore } from "../stores/sidebarStore" ;
18+ import { useTaskViewedStore } from "../stores/taskViewedStore" ;
1619import { HomeItem } from "./items/HomeItem" ;
1720import { NewTaskItem } from "./items/NewTaskItem" ;
18- import { ProjectsItem } from "./items/ProjectsItem" ;
1921import { TaskItem } from "./items/TaskItem" ;
20- import { ViewItem } from "./items/ViewItem" ;
21- import { SidebarSection } from "./SidebarSection" ;
22+ import { SortableFolderSection } from "./SortableFolderSection" ;
2223
2324function SidebarMenuComponent ( ) {
24- const { view, navigateToTaskList, navigateToTask, navigateToTaskInput } =
25- useNavigationStore ( ) ;
25+ const { view, navigateToTask, navigateToTaskInput } = useNavigationStore ( ) ;
2626
2727 const activeFilters = useTaskStore ( ( state ) => state . activeFilters ) ;
28- const setActiveFilters = useTaskStore ( ( state ) => state . setActiveFilters ) ;
2928 const { data : currentUser } = useMeQuery ( ) ;
3029 const { data : allTasks = [ ] } = useTasks ( ) ;
3130 const { folders, removeFolder } = useRegisteredFoldersStore ( ) ;
3231
3332 const collapsedSections = useSidebarStore ( ( state ) => state . collapsedSections ) ;
3433 const toggleSection = useSidebarStore ( ( state ) => state . toggleSection ) ;
34+ const folderOrder = useSidebarStore ( ( state ) => state . folderOrder ) ;
35+ const reorderFolders = useSidebarStore ( ( state ) => state . reorderFolders ) ;
3536 const workspaces = useWorkspaceStore . use . workspaces ( ) ;
3637 const taskStates = useTaskExecutionStore ( ( state ) => state . taskStates ) ;
38+ const markAsViewed = useTaskViewedStore ( ( state ) => state . markAsViewed ) ;
3739
3840 const { showContextMenu, renameTask, renameDialogOpen, setRenameDialogOpen } =
3941 useTaskContextMenu ( ) ;
@@ -44,6 +46,35 @@ function SidebarMenuComponent() {
4446 currentUser,
4547 } ) ;
4648
49+ const handleDragOver : DragDropEvents [ "dragover" ] = useCallback (
50+ ( event ) => {
51+ const source = event . operation . source ;
52+ const target = event . operation . target ;
53+
54+ // type is at sortable level, not in data
55+ if ( source ?. type !== "folder" || target ?. type !== "folder" ) {
56+ return ;
57+ }
58+
59+ const sourceId = source ?. id ;
60+ const targetId = target ?. id ;
61+
62+ if ( ! sourceId || ! targetId || sourceId === targetId ) return ;
63+
64+ const sourceIndex = folderOrder . indexOf ( String ( sourceId ) ) ;
65+ const targetIndex = folderOrder . indexOf ( String ( targetId ) ) ;
66+
67+ if (
68+ sourceIndex !== - 1 &&
69+ targetIndex !== - 1 &&
70+ sourceIndex !== targetIndex
71+ ) {
72+ reorderFolders ( sourceIndex , targetIndex ) ;
73+ }
74+ } ,
75+ [ folderOrder , reorderFolders ] ,
76+ ) ;
77+
4778 const taskMap = new Map < string , Task > ( ) ;
4879 for ( const task of allTasks ) {
4980 taskMap . set ( task . id , task ) ;
@@ -53,21 +84,10 @@ function SidebarMenuComponent() {
5384 navigateToTaskInput ( ) ;
5485 } ;
5586
56- const handleViewClick = ( filters : typeof activeFilters ) => {
57- setActiveFilters ( filters ) ;
58- navigateToTaskList ( ) ;
59- } ;
60-
61- const handleProjectClick = ( repository : string ) => {
62- const newFilters = { ...activeFilters } ;
63- newFilters . repository = [ { value : repository , operator : "is" } ] ;
64- setActiveFilters ( newFilters ) ;
65- navigateToTaskList ( ) ;
66- } ;
67-
6887 const handleTaskClick = ( taskId : string ) => {
6988 const task = taskMap . get ( taskId ) ;
7089 if ( task ) {
90+ markAsViewed ( taskId ) ;
7191 navigateToTask ( task ) ;
7292 }
7393 } ;
@@ -135,59 +155,77 @@ function SidebarMenuComponent() {
135155 overflowX : "hidden" ,
136156 } }
137157 >
138- < Flex direction = "column" p = "2" >
158+ < Flex direction = "column" py = "2" >
139159 < HomeItem
140160 isActive = { sidebarData . isHomeActive }
141161 onClick = { handleHomeClick }
142162 />
143163
144- { sidebarData . views . map ( ( view ) => (
145- < ViewItem
146- key = { view . id }
147- label = { view . label }
148- isActive = { sidebarData . activeViewId === view . id }
149- onClick = { ( ) => handleViewClick ( view . filters ) }
150- />
151- ) ) }
152-
153- < ProjectsItem
154- repositories = { sidebarData . repositories }
155- isLoading = { sidebarData . isLoading }
156- activeRepository = { sidebarData . activeRepository }
157- onProjectClick = { handleProjectClick }
158- />
159-
160- { sidebarData . folders . map ( ( folder , index ) => (
161- < SidebarSection
162- key = { folder . id }
163- id = { folder . id }
164- label = { folder . name }
165- icon = { < FolderIcon size = { 14 } weight = "regular" /> }
166- isExpanded = { ! collapsedSections . has ( folder . id ) }
167- onToggle = { ( ) => toggleSection ( folder . id ) }
168- addSpacingBefore = { index === 0 }
169- onContextMenu = { ( e ) => handleFolderContextMenu ( folder . id , e ) }
170- >
171- < NewTaskItem onClick = { ( ) => handleFolderNewTask ( folder . id ) } />
172- { folder . tasks . map ( ( task ) => (
173- < TaskItem
174- key = { task . id }
175- id = { task . id }
176- label = { task . title }
177- status = { task . status }
178- isActive = { sidebarData . activeTaskId === task . id }
179- worktreeName = { workspaces [ task . id ] ?. worktreeName ?? undefined }
180- worktreePath = {
181- workspaces [ task . id ] ?. worktreePath ??
182- workspaces [ task . id ] ?. folderPath
164+ < DragDropProvider
165+ onDragOver = { handleDragOver }
166+ sensors = { [
167+ PointerSensor . configure ( {
168+ activationConstraints : {
169+ distance : { value : 5 } ,
170+ } ,
171+ } ) ,
172+ ] }
173+ >
174+ { sidebarData . folders . map ( ( folder , index ) => {
175+ const isExpanded = ! collapsedSections . has ( folder . id ) ;
176+ return (
177+ < SortableFolderSection
178+ key = { folder . id }
179+ id = { folder . id }
180+ index = { index }
181+ label = { folder . name }
182+ icon = {
183+ isExpanded ? (
184+ < FolderOpenIcon size = { 14 } weight = "regular" />
185+ ) : (
186+ < FolderIcon size = { 14 } weight = "regular" />
187+ )
183188 }
184- workspaceMode = { taskStates [ task . id ] ?. workspaceMode }
185- onClick = { ( ) => handleTaskClick ( task . id ) }
186- onContextMenu = { ( e ) => handleTaskContextMenu ( task . id , e ) }
187- />
188- ) ) }
189- </ SidebarSection >
190- ) ) }
189+ isExpanded = { isExpanded }
190+ onToggle = { ( ) => toggleSection ( folder . id ) }
191+ onContextMenu = { ( e ) => handleFolderContextMenu ( folder . id , e ) }
192+ >
193+ < NewTaskItem onClick = { ( ) => handleFolderNewTask ( folder . id ) } />
194+ { folder . tasks . map ( ( task ) => (
195+ < TaskItem
196+ key = { task . id }
197+ id = { task . id }
198+ label = { task . title }
199+ isActive = { sidebarData . activeTaskId === task . id }
200+ worktreeName = {
201+ workspaces [ task . id ] ?. worktreeName ?? undefined
202+ }
203+ worktreePath = {
204+ workspaces [ task . id ] ?. worktreePath ??
205+ workspaces [ task . id ] ?. folderPath
206+ }
207+ workspaceMode = { taskStates [ task . id ] ?. workspaceMode }
208+ lastActivityAt = { task . lastActivityAt }
209+ isGenerating = { task . isGenerating }
210+ isUnread = { task . isUnread }
211+ onClick = { ( ) => handleTaskClick ( task . id ) }
212+ onContextMenu = { ( e ) => handleTaskContextMenu ( task . id , e ) }
213+ />
214+ ) ) }
215+ </ SortableFolderSection >
216+ ) ;
217+ } ) }
218+ < DragOverlay >
219+ { ( source ) =>
220+ source ?. type === "folder" ? (
221+ < div className = "flex w-full items-center gap-1 rounded bg-gray-2 px-2 py-1 font-mono text-[12px] text-gray-11 shadow-lg" >
222+ < FolderIcon size = { 14 } weight = "regular" />
223+ < span className = "font-medium" > { source . data ?. label } </ span >
224+ </ div >
225+ ) : null
226+ }
227+ </ DragOverlay >
228+ </ DragDropProvider >
191229 </ Flex >
192230 </ Box >
193231 </ >
0 commit comments