Skip to content

Commit 282ec8c

Browse files
fix(reorder): drag and drop hook (#3874)
* fix(reorder): drag and drop hook * fix custom tool dropdown color * fix mcp server url change propagation
1 parent e45fbe0 commit 282ec8c

File tree

5 files changed

+142
-74
lines changed

5 files changed

+142
-74
lines changed

apps/sim/app/api/mcp/servers/[id]/refresh/route.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,23 @@ interface SyncResult {
3838
updatedWorkflowIds: string[]
3939
}
4040

41+
interface ServerMetadata {
42+
url?: string
43+
name?: string
44+
}
45+
4146
/**
42-
* Syncs tool schemas from discovered MCP tools to all workflow blocks using those tools.
43-
* Returns the count and IDs of updated workflows.
47+
* Syncs tool schemas and server metadata from discovered MCP tools to all
48+
* workflow blocks using those tools. Updates stored serverUrl/serverName
49+
* when the server's details have changed, preventing stale badges after
50+
* a server URL edit.
4451
*/
4552
async function syncToolSchemasToWorkflows(
4653
workspaceId: string,
4754
serverId: string,
4855
tools: McpTool[],
49-
requestId: string
56+
requestId: string,
57+
serverMeta?: ServerMetadata
5058
): Promise<SyncResult> {
5159
const toolsByName = new Map(tools.map((t) => [t.name, t]))
5260

@@ -94,7 +102,10 @@ async function syncToolSchemasToWorkflows(
94102

95103
const schemasMatch = JSON.stringify(tool.schema) === JSON.stringify(newSchema)
96104

97-
if (!schemasMatch) {
105+
const urlChanged = serverMeta?.url != null && tool.params.serverUrl !== serverMeta.url
106+
const nameChanged = serverMeta?.name != null && tool.params.serverName !== serverMeta.name
107+
108+
if (!schemasMatch || urlChanged || nameChanged) {
98109
hasUpdates = true
99110

100111
const validParamKeys = new Set(Object.keys(newSchema.properties || {}))
@@ -106,6 +117,9 @@ async function syncToolSchemasToWorkflows(
106117
}
107118
}
108119

120+
if (urlChanged) cleanedParams.serverUrl = serverMeta.url
121+
if (nameChanged) cleanedParams.serverName = serverMeta.name
122+
109123
return { ...tool, schema: newSchema, params: cleanedParams }
110124
}
111125

@@ -188,7 +202,8 @@ export const POST = withMcpAuth<{ id: string }>('read')(
188202
workspaceId,
189203
serverId,
190204
discoveredTools,
191-
requestId
205+
requestId,
206+
{ url: server.url ?? undefined, name: server.name ?? undefined }
192207
)
193208
} catch (error) {
194209
connectionStatus = 'error'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/custom-tool-modal/custom-tool-modal.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,11 +1103,9 @@ try {
11031103
}
11041104
}}
11051105
>
1106-
<span className='flex-1 truncate text-[var(--text-primary)]'>
1107-
{param.name}
1108-
</span>
1106+
<span className='flex-1 truncate'>{param.name}</span>
11091107
{param.type && param.type !== 'any' && (
1110-
<span className='ml-auto text-[var(--text-secondary)] text-micro'>
1108+
<span className='ml-auto text-[var(--text-muted-inverse)] text-micro'>
11111109
{param.type}
11121110
</span>
11131111
)}

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-drag-drop.ts

Lines changed: 114 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ const logger = createLogger('WorkflowList:DragDrop')
1313
const SCROLL_THRESHOLD = 60
1414
const SCROLL_SPEED = 8
1515
const HOVER_EXPAND_DELAY = 400
16-
const DRAG_OVER_THROTTLE_MS = 16
1716

1817
export interface DropIndicator {
1918
targetId: string
@@ -32,21 +31,35 @@ type SiblingItem = {
3231
createdAt: Date
3332
}
3433

34+
/** Root folder vs root workflow scope: API/cache may use null or undefined for "no parent". */
35+
function isSameFolderScope(
36+
parentOrFolderId: string | null | undefined,
37+
scope: string | null
38+
): boolean {
39+
return (parentOrFolderId ?? null) === (scope ?? null)
40+
}
41+
3542
export function useDragDrop(options: UseDragDropOptions = {}) {
3643
const { disabled = false } = options
3744
const [dropIndicator, setDropIndicator] = useState<DropIndicator | null>(null)
45+
/**
46+
* Mirrors `dropIndicator` synchronously. `drop` can fire before React commits the last
47+
* `dragOver` state update, so `handleDrop` must read this ref instead of state.
48+
*/
49+
const dropIndicatorRef = useRef<DropIndicator | null>(null)
3850
const [isDragging, setIsDragging] = useState(false)
3951
const [hoverFolderId, setHoverFolderId] = useState<string | null>(null)
4052
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
4153
const scrollAnimationRef = useRef<number | null>(null)
4254
const hoverExpandTimerRef = useRef<number | null>(null)
4355
const lastDragYRef = useRef<number>(0)
44-
const lastDragOverTimeRef = useRef<number>(0)
4556
const draggedSourceFolderRef = useRef<string | null>(null)
4657
const siblingsCacheRef = useRef<Map<string, SiblingItem[]>>(new Map())
58+
const isDraggingRef = useRef(false)
4759

4860
const params = useParams()
4961
const workspaceId = params.workspaceId as string | undefined
62+
5063
const reorderWorkflowsMutation = useReorderWorkflows()
5164
const reorderFoldersMutation = useReorderFolders()
5265
const setExpanded = useFolderStore((s) => s.setExpanded)
@@ -127,6 +140,10 @@ export function useDragDrop(options: UseDragDropOptions = {}) {
127140
}
128141
}, [hoverFolderId, isDragging, expandedFolders, setExpanded])
129142

143+
useEffect(() => {
144+
siblingsCacheRef.current.clear()
145+
}, [workspaceId])
146+
130147
const calculateDropPosition = useCallback(
131148
(e: React.DragEvent, element: HTMLElement): 'before' | 'after' => {
132149
const rect = element.getBoundingClientRect()
@@ -164,12 +181,28 @@ export function useDragDrop(options: UseDragDropOptions = {}) {
164181
: indicator.folderId
165182
}, [])
166183

167-
const calculateInsertIndex = useCallback(
168-
(remaining: SiblingItem[], indicator: DropIndicator): number => {
169-
return indicator.position === 'inside'
170-
? remaining.length
171-
: remaining.findIndex((item) => item.id === indicator.targetId) +
172-
(indicator.position === 'after' ? 1 : 0)
184+
/**
185+
* Insert index into the list of siblings **excluding** moving items. Must use the full
186+
* `siblingItems` list for lookup: when the drop line targets the dragged row,
187+
* `indicator.targetId` is not present in `remaining`, so indexing `remaining` alone
188+
* returns -1 and corrupts the splice.
189+
*/
190+
const getInsertIndexInRemaining = useCallback(
191+
(siblingItems: SiblingItem[], movingIds: Set<string>, indicator: DropIndicator): number => {
192+
if (indicator.position === 'inside') {
193+
return siblingItems.filter((s) => !movingIds.has(s.id)).length
194+
}
195+
196+
const targetIdx = siblingItems.findIndex((s) => s.id === indicator.targetId)
197+
if (targetIdx === -1) {
198+
return siblingItems.filter((s) => !movingIds.has(s.id)).length
199+
}
200+
201+
if (indicator.position === 'before') {
202+
return siblingItems.slice(0, targetIdx).filter((s) => !movingIds.has(s.id)).length
203+
}
204+
205+
return siblingItems.slice(0, targetIdx + 1).filter((s) => !movingIds.has(s.id)).length
173206
},
174207
[]
175208
)
@@ -217,57 +250,65 @@ export function useDragDrop(options: UseDragDropOptions = {}) {
217250
lastDragYRef.current = e.clientY
218251

219252
if (!isDragging) {
253+
isDraggingRef.current = true
220254
setIsDragging(true)
221255
}
222256

223-
const now = performance.now()
224-
if (now - lastDragOverTimeRef.current < DRAG_OVER_THROTTLE_MS) {
225-
return false
226-
}
227-
lastDragOverTimeRef.current = now
228257
return true
229258
},
230259
[isDragging]
231260
)
232261

233-
const getSiblingItems = useCallback((folderId: string | null): SiblingItem[] => {
234-
const cacheKey = folderId ?? 'root'
235-
const cached = siblingsCacheRef.current.get(cacheKey)
236-
if (cached) return cached
237-
238-
const currentFolders = workspaceId ? getFolderMap(workspaceId) : {}
239-
const currentWorkflows = workspaceId ? getWorkflows(workspaceId) : []
240-
const siblings = [
241-
...Object.values(currentFolders)
242-
.filter((f) => f.parentId === folderId)
243-
.map((f) => ({
244-
type: 'folder' as const,
245-
id: f.id,
246-
sortOrder: f.sortOrder,
247-
createdAt: f.createdAt,
248-
})),
249-
...currentWorkflows
250-
.filter((w) => w.folderId === folderId)
251-
.map((w) => ({
252-
type: 'workflow' as const,
253-
id: w.id,
254-
sortOrder: w.sortOrder,
255-
createdAt: w.createdAt,
256-
})),
257-
].sort(compareSiblingItems)
258-
259-
siblingsCacheRef.current.set(cacheKey, siblings)
260-
return siblings
261-
}, [])
262+
const getSiblingItems = useCallback(
263+
(folderId: string | null): SiblingItem[] => {
264+
const cacheKey = folderId ?? 'root'
265+
if (!isDraggingRef.current) {
266+
const cached = siblingsCacheRef.current.get(cacheKey)
267+
if (cached) return cached
268+
}
269+
270+
const currentFolders = workspaceId ? getFolderMap(workspaceId) : {}
271+
const currentWorkflows = workspaceId ? getWorkflows(workspaceId) : []
272+
const siblings = [
273+
...Object.values(currentFolders)
274+
.filter((f) => isSameFolderScope(f.parentId, folderId))
275+
.map((f) => ({
276+
type: 'folder' as const,
277+
id: f.id,
278+
sortOrder: f.sortOrder,
279+
createdAt: f.createdAt,
280+
})),
281+
...currentWorkflows
282+
.filter((w) => isSameFolderScope(w.folderId, folderId))
283+
.map((w) => ({
284+
type: 'workflow' as const,
285+
id: w.id,
286+
sortOrder: w.sortOrder,
287+
createdAt: w.createdAt,
288+
})),
289+
].sort(compareSiblingItems)
290+
291+
if (!isDraggingRef.current) {
292+
siblingsCacheRef.current.set(cacheKey, siblings)
293+
}
294+
return siblings
295+
},
296+
[workspaceId]
297+
)
262298

263299
const setNormalizedDropIndicator = useCallback(
264300
(indicator: DropIndicator | null) => {
265-
setDropIndicator((prev) => {
266-
let next: DropIndicator | null = indicator
301+
if (indicator === null) {
302+
dropIndicatorRef.current = null
303+
setDropIndicator(null)
304+
return
305+
}
267306

268-
if (indicator && indicator.position === 'after' && indicator.targetId !== 'root') {
269-
const siblings = getSiblingItems(indicator.folderId)
270-
const currentIdx = siblings.findIndex((s) => s.id === indicator.targetId)
307+
let next: DropIndicator = indicator
308+
if (indicator.position === 'after' && indicator.targetId !== 'root') {
309+
const siblings = getSiblingItems(indicator.folderId)
310+
const currentIdx = siblings.findIndex((s) => s.id === indicator.targetId)
311+
if (currentIdx !== -1) {
271312
const nextSibling = siblings[currentIdx + 1]
272313
if (nextSibling) {
273314
next = {
@@ -277,15 +318,18 @@ export function useDragDrop(options: UseDragDropOptions = {}) {
277318
}
278319
}
279320
}
321+
}
280322

323+
setDropIndicator((prev) => {
281324
if (
282-
prev?.targetId === next?.targetId &&
283-
prev?.position === next?.position &&
284-
prev?.folderId === next?.folderId
325+
prev?.targetId === next.targetId &&
326+
prev?.position === next.position &&
327+
prev?.folderId === next.folderId
285328
) {
329+
dropIndicatorRef.current = prev
286330
return prev
287331
}
288-
332+
dropIndicatorRef.current = next
289333
return next
290334
})
291335
},
@@ -324,7 +368,7 @@ export function useDragDrop(options: UseDragDropOptions = {}) {
324368
sortOrder: workflow.sortOrder,
325369
createdAt: workflow.createdAt,
326370
}
327-
if (workflow.folderId === destinationFolderId) {
371+
if (isSameFolderScope(workflow.folderId, destinationFolderId)) {
328372
fromDestination.push(item)
329373
} else {
330374
fromOther.push(item)
@@ -340,7 +384,7 @@ export function useDragDrop(options: UseDragDropOptions = {}) {
340384
sortOrder: folder.sortOrder,
341385
createdAt: folder.createdAt,
342386
}
343-
if (folder.parentId === destinationFolderId) {
387+
if (isSameFolderScope(folder.parentId, destinationFolderId)) {
344388
fromDestination.push(item)
345389
} else {
346390
fromOther.push(item)
@@ -352,7 +396,7 @@ export function useDragDrop(options: UseDragDropOptions = {}) {
352396

353397
return { fromDestination, fromOther }
354398
},
355-
[]
399+
[workspaceId]
356400
)
357401

358402
const handleSelectionDrop = useCallback(
@@ -365,7 +409,9 @@ export function useDragDrop(options: UseDragDropOptions = {}) {
365409
try {
366410
const destinationFolderId = getDestinationFolderId(indicator)
367411
const validFolderIds = folderIds.filter((id) => canMoveFolderTo(id, destinationFolderId))
368-
if (workflowIds.length === 0 && validFolderIds.length === 0) return
412+
if (workflowIds.length === 0 && validFolderIds.length === 0) {
413+
return
414+
}
369415

370416
const siblingItems = getSiblingItems(destinationFolderId)
371417
const movingIds = new Set([...workflowIds, ...validFolderIds])
@@ -377,7 +423,7 @@ export function useDragDrop(options: UseDragDropOptions = {}) {
377423
destinationFolderId
378424
)
379425

380-
const insertAt = calculateInsertIndex(remaining, indicator)
426+
const insertAt = getInsertIndexInRemaining(siblingItems, movingIds, indicator)
381427
const newOrder = [
382428
...remaining.slice(0, insertAt),
383429
...fromDestination,
@@ -400,7 +446,7 @@ export function useDragDrop(options: UseDragDropOptions = {}) {
400446
canMoveFolderTo,
401447
getSiblingItems,
402448
collectMovingItems,
403-
calculateInsertIndex,
449+
getInsertIndexInRemaining,
404450
buildAndSubmitUpdates,
405451
]
406452
)
@@ -410,8 +456,10 @@ export function useDragDrop(options: UseDragDropOptions = {}) {
410456
e.preventDefault()
411457
e.stopPropagation()
412458

413-
const indicator = dropIndicator
459+
const indicator = dropIndicatorRef.current
460+
dropIndicatorRef.current = null
414461
setDropIndicator(null)
462+
isDraggingRef.current = false
415463
setIsDragging(false)
416464
siblingsCacheRef.current.clear()
417465

@@ -430,7 +478,7 @@ export function useDragDrop(options: UseDragDropOptions = {}) {
430478
logger.error('Failed to handle drop:', error)
431479
}
432480
},
433-
[dropIndicator, handleSelectionDrop]
481+
[handleSelectionDrop]
434482
)
435483

436484
const createWorkflowDragHandlers = useCallback(
@@ -538,7 +586,9 @@ export function useDragDrop(options: UseDragDropOptions = {}) {
538586
onDragOver: (e: React.DragEvent<HTMLElement>) => {
539587
if (!initDragOver(e)) return
540588
if (itemId) {
541-
setDropIndicator({ targetId: itemId, position, folderId: null })
589+
const edge: DropIndicator = { targetId: itemId, position, folderId: null }
590+
dropIndicatorRef.current = edge
591+
setDropIndicator(edge)
542592
} else {
543593
setNormalizedDropIndicator({ targetId: 'root', position: 'inside', folderId: null })
544594
}
@@ -551,11 +601,15 @@ export function useDragDrop(options: UseDragDropOptions = {}) {
551601

552602
const handleDragStart = useCallback((sourceFolderId: string | null) => {
553603
draggedSourceFolderRef.current = sourceFolderId
604+
siblingsCacheRef.current.clear()
605+
isDraggingRef.current = true
554606
setIsDragging(true)
555607
}, [])
556608

557609
const handleDragEnd = useCallback(() => {
610+
isDraggingRef.current = false
558611
setIsDragging(false)
612+
dropIndicatorRef.current = null
559613
setDropIndicator(null)
560614
draggedSourceFolderRef.current = null
561615
setHoverFolderId(null)

0 commit comments

Comments
 (0)