Skip to content

Commit 5f957d4

Browse files
committed
feat(chat): add task dragging and visible drag ghost for sidebar items
1 parent 1110709 commit 5f957d4

File tree

8 files changed

+127
-6
lines changed

8 files changed

+127
-6
lines changed

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import type {
2727
import { useFolders } from '@/hooks/queries/folders'
2828
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
2929
import { useTablesList } from '@/hooks/queries/tables'
30+
import { useTasks } from '@/hooks/queries/tasks'
3031
import { useWorkflows } from '@/hooks/queries/workflows'
3132
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
3233

@@ -53,6 +54,7 @@ export function useAvailableResources(
5354
const { data: files = [] } = useWorkspaceFiles(workspaceId)
5455
const { data: knowledgeBases } = useKnowledgeBasesQuery(workspaceId)
5556
const { data: folders = [] } = useFolders(workspaceId)
57+
const { data: tasks = [] } = useTasks(workspaceId)
5658

5759
return useMemo(
5860
() => [
@@ -97,8 +99,16 @@ export function useAvailableResources(
9799
isOpen: existingKeys.has(`knowledgebase:${kb.id}`),
98100
})),
99101
},
102+
{
103+
type: 'task' as const,
104+
items: tasks.map((t) => ({
105+
id: t.id,
106+
name: t.name,
107+
isOpen: existingKeys.has(`task:${t.id}`),
108+
})),
109+
},
100110
],
101-
[workflows, folders, tables, files, knowledgeBases, existingKeys]
111+
[workflows, folders, tables, files, knowledgeBases, tasks, existingKeys]
102112
)
103113
}
104114

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type ElementType, type ReactNode, useMemo } from 'react'
44
import type { QueryClient } from '@tanstack/react-query'
55
import { useParams } from 'next/navigation'
66
import {
7+
Blimp,
78
Database,
89
File as FileIcon,
910
Folder as FolderIcon,
@@ -19,6 +20,7 @@ import type {
1920
} from '@/app/workspace/[workspaceId]/home/types'
2021
import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
2122
import { tableKeys } from '@/hooks/queries/tables'
23+
import { taskKeys } from '@/hooks/queries/tasks'
2224
import { folderKeys } from '@/hooks/queries/utils/folder-keys'
2325
import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists'
2426
import { useWorkflows } from '@/hooks/queries/workflows'
@@ -151,6 +153,15 @@ export const RESOURCE_REGISTRY: Record<MothershipResourceType, ResourceTypeConfi
151153
),
152154
renderDropdownItem: (props) => <IconDropdownItem {...props} icon={FolderIcon} />,
153155
},
156+
task: {
157+
type: 'task',
158+
label: 'Tasks',
159+
icon: Blimp,
160+
renderTabIcon: (_resource, className) => (
161+
<Blimp className={cn(className, 'text-[var(--text-icon)]')} />
162+
),
163+
renderDropdownItem: (props) => <IconDropdownItem {...props} icon={Blimp} />,
164+
},
154165
} as const
155166

156167
export const RESOURCE_TYPES = Object.values(RESOURCE_REGISTRY)
@@ -185,6 +196,9 @@ const RESOURCE_INVALIDATORS: Record<
185196
folder: (qc) => {
186197
qc.invalidateQueries({ queryKey: folderKeys.lists() })
187198
},
199+
task: (qc, wId) => {
200+
qc.invalidateQueries({ queryKey: taskKeys.list(wId) })
201+
},
188202
}
189203

190204
/**

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ export function mapResourceToContext(resource: MothershipResource): ChatContext
8989
return { kind: 'file', fileId: resource.id, label: resource.title }
9090
case 'folder':
9191
return { kind: 'folder', folderId: resource.id, label: resource.title }
92+
case 'task':
93+
return { kind: 'past_chat', chatId: resource.id, label: resource.title }
9294
default:
9395
return { kind: 'docs', label: resource.title }
9496
}

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import {
1919
useSidebarDragContext,
2020
} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
2121
import { SIDEBAR_SCROLL_EVENT } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
22-
import { buildDragResources } from '@/app/workspace/[workspaceId]/w/components/sidebar/utils'
22+
import {
23+
buildDragResources,
24+
createSidebarDragGhost,
25+
} from '@/app/workspace/[workspaceId]/w/components/sidebar/utils'
2326
import {
2427
useCanDelete,
2528
useDeleteFolder,
@@ -138,6 +141,7 @@ export function FolderItem({
138141
})
139142

140143
const isEditingRef = useRef(false)
144+
const dragGhostRef = useRef<HTMLElement | null>(null)
141145

142146
const handleCreateWorkflowInFolder = useCallback(() => {
143147
const name = generateCreativeWorkflowName()
@@ -205,9 +209,16 @@ export function FolderItem({
205209
e.dataTransfer.setData(SIM_RESOURCES_DRAG_TYPE, JSON.stringify(resources))
206210
}
207211

212+
const total = selection.folderIds.length + selection.workflowIds.length
213+
const ghostLabel = total > 1 ? `${folder.name} +${total - 1} more` : folder.name
214+
const ghost = createSidebarDragGhost(ghostLabel)
215+
void ghost.offsetHeight
216+
e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2)
217+
dragGhostRef.current = ghost
218+
208219
onDragStartProp?.()
209220
},
210-
[folder.id, workspaceId, onDragStartProp]
221+
[folder.id, folder.name, workspaceId, onDragStartProp]
211222
)
212223

213224
const {
@@ -220,6 +231,10 @@ export function FolderItem({
220231
})
221232

222233
const handleDragEnd = useCallback(() => {
234+
if (dragGhostRef.current) {
235+
dragGhostRef.current.remove()
236+
dragGhostRef.current = null
237+
}
223238
handleDragEndBase()
224239
onDragEndProp?.()
225240
}, [handleDragEndBase, onDragEndProp])

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ import {
1717
useItemRename,
1818
useSidebarDragContext,
1919
} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
20-
import { buildDragResources } from '@/app/workspace/[workspaceId]/w/components/sidebar/utils'
20+
import {
21+
buildDragResources,
22+
createSidebarDragGhost,
23+
} from '@/app/workspace/[workspaceId]/w/components/sidebar/utils'
2124
import {
2225
useCanDelete,
2326
useDeleteSelection,
@@ -200,6 +203,7 @@ export function WorkflowItem({
200203
}, [isActiveWorkflow, isWorkflowLocked])
201204

202205
const isEditingRef = useRef(false)
206+
const dragGhostRef = useRef<HTMLElement | null>(null)
203207

204208
const {
205209
isOpen: isContextMenuOpen,
@@ -346,9 +350,17 @@ export function WorkflowItem({
346350
e.dataTransfer.setData(SIM_RESOURCES_DRAG_TYPE, JSON.stringify(resources))
347351
}
348352

353+
const total = selection.workflowIds.length + selection.folderIds.length
354+
const ghostLabel = total > 1 ? `${workflow.name} +${total - 1} more` : workflow.name
355+
const ghost = createSidebarDragGhost(ghostLabel)
356+
// Force reflow so the browser can capture the rendered element
357+
void ghost.offsetHeight
358+
e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2)
359+
dragGhostRef.current = ghost
360+
349361
onDragStartProp?.()
350362
},
351-
[workflow.id, workspaceId, onDragStartProp]
363+
[workflow.id, workflow.name, workspaceId, onDragStartProp]
352364
)
353365

354366
const {
@@ -361,6 +373,10 @@ export function WorkflowItem({
361373
})
362374

363375
const handleDragEnd = useCallback(() => {
376+
if (dragGhostRef.current) {
377+
dragGhostRef.current.remove()
378+
dragGhostRef.current = null
379+
}
364380
handleDragEndBase()
365381
onDragEndProp?.()
366382
}, [handleDragEndBase, onDragEndProp])

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
Wordmark,
3838
} from '@/components/emcn/icons'
3939
import { useSession } from '@/lib/auth/auth-client'
40+
import { SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types'
4041
import { cn } from '@/lib/core/utils/cn'
4142
import { isMacPlatform } from '@/lib/core/utils/platform'
4243
import { buildFolderTree } from '@/lib/folders/tree'
@@ -72,7 +73,10 @@ import {
7273
useWorkflowOperations,
7374
useWorkspaceManagement,
7475
} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
75-
import { groupWorkflowsByFolder } from '@/app/workspace/[workspaceId]/w/components/sidebar/utils'
76+
import {
77+
createSidebarDragGhost,
78+
groupWorkflowsByFolder,
79+
} from '@/app/workspace/[workspaceId]/w/components/sidebar/utils'
7680
import {
7781
useDuplicateWorkspace,
7882
useExportWorkspace,
@@ -159,6 +163,30 @@ const SidebarTaskItem = memo(function SidebarTaskItem({
159163
onMorePointerDown: () => void
160164
onMoreClick: (e: React.MouseEvent<HTMLButtonElement>, taskId: string) => void
161165
}) {
166+
const dragGhostRef = useRef<HTMLElement | null>(null)
167+
168+
const handleDragStart = useCallback(
169+
(e: React.DragEvent) => {
170+
e.dataTransfer.effectAllowed = 'copyMove'
171+
e.dataTransfer.setData(
172+
SIM_RESOURCES_DRAG_TYPE,
173+
JSON.stringify([{ type: 'task', id: task.id, title: task.name }])
174+
)
175+
const ghost = createSidebarDragGhost(task.name)
176+
void ghost.offsetHeight
177+
e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2)
178+
dragGhostRef.current = ghost
179+
},
180+
[task.id, task.name]
181+
)
182+
183+
const handleDragEnd = useCallback(() => {
184+
if (dragGhostRef.current) {
185+
dragGhostRef.current.remove()
186+
dragGhostRef.current = null
187+
}
188+
}, [])
189+
162190
return (
163191
<SidebarTooltip label={task.name} enabled={showCollapsedTooltips}>
164192
<Link
@@ -182,6 +210,9 @@ const SidebarTaskItem = memo(function SidebarTaskItem({
182210
}
183211
}}
184212
onContextMenu={task.id !== 'new' ? (e) => onContextMenu(e, task.id) : undefined}
213+
draggable={task.id !== 'new'}
214+
onDragStart={task.id !== 'new' ? handleDragStart : undefined}
215+
onDragEnd={task.id !== 'new' ? handleDragEnd : undefined}
185216
>
186217
<Blimp className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
187218
<div className='min-w-0 flex-1 truncate font-base text-[var(--text-body)]'>{task.name}</div>

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,38 @@ export function buildDragResources(
2828
]
2929
}
3030

31+
/**
32+
* Creates a lightweight drag ghost element showing the label of the item(s) being dragged.
33+
* Append to `document.body`, pass to `e.dataTransfer.setDragImage`, then remove on dragend.
34+
*/
35+
export function createSidebarDragGhost(label: string): HTMLElement {
36+
const ghost = document.createElement('div')
37+
ghost.style.cssText = `
38+
position: fixed;
39+
top: -500px;
40+
left: 0;
41+
display: inline-flex;
42+
align-items: center;
43+
padding: 4px 10px;
44+
background: var(--surface-active);
45+
border: 1px solid rgba(255,255,255,0.08);
46+
border-radius: 8px;
47+
font-family: system-ui, -apple-system, sans-serif;
48+
font-size: 13px;
49+
color: var(--text-body);
50+
white-space: nowrap;
51+
max-width: 220px;
52+
overflow: hidden;
53+
text-overflow: ellipsis;
54+
pointer-events: none;
55+
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
56+
z-index: 9999;
57+
`
58+
ghost.textContent = label
59+
document.body.appendChild(ghost)
60+
return ghost
61+
}
62+
3163
export function compareByOrder<T extends { sortOrder: number; createdAt?: Date; id: string }>(
3264
a: T,
3365
b: T

apps/sim/lib/copilot/resource-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type MothershipResourceType =
44
| 'workflow'
55
| 'knowledgebase'
66
| 'folder'
7+
| 'task'
78
| 'generic'
89

910
export interface MothershipResource {

0 commit comments

Comments
 (0)