From 43c86bf745481858629c8f31af7ad6b591fa87fa Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 13 Jun 2025 18:13:53 -0700 Subject: [PATCH 1/7] added multi-select for folders --- .../folder-tree/components/folder-item.tsx | 129 ++++++ .../folder-tree/components/workflow-item.tsx | 148 +++++++ .../components/folder-tree/folder-tree.tsx | 405 +++++++----------- apps/sim/stores/folders/store.ts | 52 +++ 4 files changed, 484 insertions(+), 250 deletions(-) create mode 100644 apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx create mode 100644 apps/sim/app/w/components/sidebar/components/folder-tree/components/workflow-item.tsx diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx b/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx new file mode 100644 index 00000000000..2b1e9114a35 --- /dev/null +++ b/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx @@ -0,0 +1,129 @@ +'use client' + +import clsx from 'clsx' +import { ChevronDown, ChevronRight, Folder, FolderOpen } from 'lucide-react' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { type FolderTreeNode, useFolderStore } from '@/stores/folders/store' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { FolderContextMenu } from '../../folder-context-menu/folder-context-menu' + +interface FolderItemProps { + folder: FolderTreeNode + isCollapsed?: boolean + onCreateWorkflow: (folderId?: string) => void + dragOver?: boolean + onDragOver?: (e: React.DragEvent) => void + onDragLeave?: (e: React.DragEvent) => void + onDrop?: (e: React.DragEvent) => void +} + +export function FolderItem({ + folder, + isCollapsed, + onCreateWorkflow, + dragOver = false, + onDragOver, + onDragLeave, + onDrop, +}: FolderItemProps) { + const { expandedFolders, toggleExpanded, updateFolderAPI, deleteFolder, selectedWorkflows } = + useFolderStore() + const { updateWorkflow } = useWorkflowRegistry() + + const isExpanded = expandedFolders.has(folder.id) + + const handleToggleExpanded = () => { + toggleExpanded(folder.id) + // Persist to server + updateFolderAPI(folder.id, { isExpanded: !isExpanded }).catch(console.error) + } + + const handleRename = async (folderId: string, newName: string) => { + try { + await updateFolderAPI(folderId, { name: newName }) + } catch (error) { + console.error('Failed to rename folder:', error) + } + } + + const handleDelete = async (folderId: string) => { + if ( + confirm( + `Are you sure you want to delete "${folder.name}"? Child folders and workflows will be moved to the parent folder.` + ) + ) { + try { + await deleteFolder(folderId) + } catch (error) { + console.error('Failed to delete folder:', error) + } + } + } + + if (isCollapsed) { + return ( + + +
+
+ {isExpanded ? ( + + ) : ( + + )} +
+
+
+ +

{folder.name}

+
+
+ ) + } + + return ( +
+
+
+ {isExpanded ? : } +
+ +
+ {isExpanded ? ( + + ) : ( + + )} +
+ + + {folder.name} + + +
e.stopPropagation()}> + +
+
+
+ ) +} diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/components/workflow-item.tsx b/apps/sim/app/w/components/sidebar/components/folder-tree/components/workflow-item.tsx new file mode 100644 index 00000000000..58692728c0e --- /dev/null +++ b/apps/sim/app/w/components/sidebar/components/folder-tree/components/workflow-item.tsx @@ -0,0 +1,148 @@ +'use client' + +import { useRef, useState } from 'react' +import clsx from 'clsx' +import Link from 'next/link' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { useFolderStore, useIsWorkflowSelected } from '@/stores/folders/store' +import type { WorkflowMetadata } from '@/stores/workflows/registry/types' + +interface WorkflowItemProps { + workflow: WorkflowMetadata + active: boolean + isMarketplace?: boolean + isCollapsed?: boolean + level: number + isDragOver?: boolean +} + +export function WorkflowItem({ + workflow, + active, + isMarketplace, + isCollapsed, + level, + isDragOver = false, +}: WorkflowItemProps) { + const [isDragging, setIsDragging] = useState(false) + const dragStartedRef = useRef(false) + const { selectedWorkflows, selectOnly, toggleWorkflowSelection } = useFolderStore() + const isSelected = useIsWorkflowSelected(workflow.id) + + const handleClick = (e: React.MouseEvent) => { + // Don't handle click if a drag operation was started + if (dragStartedRef.current) { + e.preventDefault() + return + } + + if (e.shiftKey) { + // Shift+click: toggle selection and prevent navigation + e.preventDefault() + toggleWorkflowSelection(workflow.id) + } else { + // Regular click: select this workflow only and allow navigation + // Only select if it's not already the only selected workflow to avoid unnecessary re-renders + if (!isSelected || selectedWorkflows.size > 1) { + selectOnly(workflow.id) + } + // Don't prevent default - let Link handle navigation + } + } + + const handleDragStart = (e: React.DragEvent) => { + if (isMarketplace) return // Don't allow dragging marketplace workflows + + // Mark that a drag operation has started + dragStartedRef.current = true + setIsDragging(true) + + // If this workflow is part of a multi-selection, drag all selected workflows + // Otherwise, just drag this single workflow + let workflowIds: string[] + if (isSelected && selectedWorkflows.size > 1) { + workflowIds = Array.from(selectedWorkflows) + } else { + workflowIds = [workflow.id] + // Don't call selectOnly here as it can interfere with the drag operation + // The workflow will be visually highlighted by the isDragging state + } + + e.dataTransfer.setData('workflow-ids', JSON.stringify(workflowIds)) + e.dataTransfer.effectAllowed = 'move' + } + + const handleDragEnd = () => { + setIsDragging(false) + // Use requestAnimationFrame to reset after the current event loop + requestAnimationFrame(() => { + dragStartedRef.current = false + }) + } + + if (isCollapsed) { + return ( + + + 1 && !active && !isDragOver + ? 'bg-accent/70' + : '', + isDragging ? 'opacity-50' : '' + )} + draggable={!isMarketplace} + onDragStart={handleDragStart} + onDragEnd={handleDragEnd} + onClick={handleClick} + > +
+ + + +

+ {workflow.name} + {isMarketplace && ' (Preview)'} +

+
+ + ) + } + + return ( + 1 && !active && !isDragOver ? 'bg-accent/70' : '', + isDragging ? 'opacity-50' : '', + !isMarketplace ? 'cursor-move' : '' + )} + style={{ paddingLeft: isCollapsed ? '0px' : `${(level + 1) * 20 + 8}px` }} + draggable={!isMarketplace} + onDragStart={handleDragStart} + onDragEnd={handleDragEnd} + onClick={handleClick} + > +
+ + {workflow.name} + {isMarketplace && ' (Preview)'} + + + ) +} diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx b/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx index 92a906c3260..bb6dadf4499 100644 --- a/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx +++ b/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx @@ -2,244 +2,138 @@ import { useEffect, useState } from 'react' import clsx from 'clsx' -import { ChevronDown, ChevronRight, Folder, FolderOpen } from 'lucide-react' -import Link from 'next/link' import { usePathname } from 'next/navigation' -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { type FolderTreeNode, useFolderStore } from '@/stores/folders/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import type { WorkflowMetadata } from '@/stores/workflows/registry/types' -import { FolderContextMenu } from '../folder-context-menu/folder-context-menu' +import { FolderItem } from './components/folder-item' +import { WorkflowItem } from './components/workflow-item' -interface FolderItemProps { +interface FolderSectionProps { folder: FolderTreeNode - isCollapsed?: boolean + level: number + isCollapsed: boolean onCreateWorkflow: (folderId?: string) => void + workflowsByFolder: Record + expandedFolders: Set + pathname: string + updateWorkflow: (id: string, updates: any) => void + renderFolderTree: (nodes: FolderTreeNode[], level: number) => React.ReactNode[] } -function FolderItem({ folder, isCollapsed, onCreateWorkflow }: FolderItemProps) { - const [dragOver, setDragOver] = useState(false) - const { expandedFolders, toggleExpanded, updateFolderAPI, deleteFolder } = useFolderStore() - const { updateWorkflow } = useWorkflowRegistry() +function FolderSection({ + folder, + level, + isCollapsed, + onCreateWorkflow, + workflowsByFolder, + expandedFolders, + pathname, + updateWorkflow, + renderFolderTree, +}: FolderSectionProps) { + const { isDragOver, handleDragOver, handleDragLeave, handleDrop } = useDragHandlers( + updateWorkflow, + folder.id, + `Moved workflow(s) to folder ${folder.id}` + ) - const isExpanded = expandedFolders.has(folder.id) + const workflowsInFolder = workflowsByFolder[folder.id] || [] - const handleToggleExpanded = () => { - toggleExpanded(folder.id) - // Persist to server - updateFolderAPI(folder.id, { isExpanded: !isExpanded }).catch(console.error) - } + return ( +
+ {/* Render folder */} +
+ +
- const handleRename = async (folderId: string, newName: string) => { - try { - await updateFolderAPI(folderId, { name: newName }) - } catch (error) { - console.error('Failed to rename folder:', error) - } - } + {/* Render workflows in this folder */} + {expandedFolders.has(folder.id) && workflowsInFolder.length > 0 && ( +
+ {workflowsInFolder.map((workflow) => ( + + ))} +
+ )} - const handleDelete = async (folderId: string) => { - if ( - confirm( - `Are you sure you want to delete "${folder.name}"? Child folders and workflows will be moved to the parent folder.` - ) - ) { - try { - await deleteFolder(folderId) - } catch (error) { - console.error('Failed to delete folder:', error) - } - } - } + {/* Render child folders */} + {expandedFolders.has(folder.id) && folder.children.length > 0 && ( +
{renderFolderTree(folder.children, level + 1)}
+ )} +
+ ) +} + +// Custom hook for drag and drop handling +function useDragHandlers( + updateWorkflow: (id: string, updates: any) => void, + targetFolderId: string | null, // null for root + logMessage?: string +) { + const [isDragOver, setIsDragOver] = useState(false) - // Drag and drop handlers const handleDragOver = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() - setDragOver(true) + setIsDragOver(true) } const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() - setDragOver(false) + setIsDragOver(false) } - const handleDrop = async (e: React.DragEvent) => { + const handleDrop = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() - setDragOver(false) + setIsDragOver(false) + + const workflowIdsData = e.dataTransfer.getData('workflow-ids') + if (workflowIdsData) { + const workflowIds = JSON.parse(workflowIdsData) as string[] - const workflowId = e.dataTransfer.getData('workflow-id') - if (workflowId && workflowId !== folder.id) { try { - // Update workflow to be in this folder - await updateWorkflow(workflowId, { folderId: folder.id }) - console.log(`Moved workflow ${workflowId} to folder ${folder.id}`) + workflowIds.forEach((workflowId) => + updateWorkflow(workflowId, { folderId: targetFolderId }) + ) + console.log(logMessage || `Moved ${workflowIds.length} workflow(s)`) } catch (error) { - console.error('Failed to move workflow to folder:', error) + console.error('Failed to move workflows:', error) } } } - if (isCollapsed) { - return ( - - -
-
- {isExpanded ? ( - - ) : ( - - )} -
-
-
- -

{folder.name}

-
-
- ) + return { + isDragOver, + handleDragOver, + handleDragLeave, + handleDrop, } - - return ( -
-
-
- {isExpanded ? : } -
- -
- {isExpanded ? ( - - ) : ( - - )} -
- - - {folder.name} - - -
e.stopPropagation()}> - -
-
-
- ) -} - -interface WorkflowItemProps { - workflow: WorkflowMetadata - active: boolean - isMarketplace?: boolean - isCollapsed?: boolean - level: number -} - -function WorkflowItem({ workflow, active, isMarketplace, isCollapsed, level }: WorkflowItemProps) { - const [isDragging, setIsDragging] = useState(false) - - const handleDragStart = (e: React.DragEvent) => { - if (isMarketplace) return // Don't allow dragging marketplace workflows - - e.dataTransfer.setData('workflow-id', workflow.id) - e.dataTransfer.effectAllowed = 'move' - setIsDragging(true) - } - - const handleDragEnd = () => { - setIsDragging(false) - } - - if (isCollapsed) { - return ( - - - -
- - - -

- {workflow.name} - {isMarketplace && ' (Preview)'} -

-
- - ) - } - - return ( - -
- - {workflow.name} - {isMarketplace && ' (Preview)'} - - - ) } interface FolderTreeProps { @@ -264,7 +158,9 @@ export function FolderTree({ expandedFolders, fetchFolders, isLoading: foldersLoading, + clearSelection, } = useFolderStore() + const { updateWorkflow } = useWorkflowRegistry() // Fetch folders when workspace changes useEffect(() => { @@ -273,6 +169,11 @@ export function FolderTree({ } }, [activeWorkspaceId, fetchFolders]) + // Clear selection when navigating to different workspace + useEffect(() => { + clearSelection() + }, [activeWorkspaceId, clearSelection]) + const folderTree = activeWorkspaceId ? getFolderTree(activeWorkspaceId) : [] // Group workflows by folder @@ -286,44 +187,29 @@ export function FolderTree({ {} as Record ) - const renderFolderTree = (nodes: FolderTreeNode[], level = 0): React.ReactNode[] => { - const result: React.ReactNode[] = [] - - nodes.forEach((folder) => { - // Render folder - result.push( -
- -
- ) - - // Render workflows in this folder - const workflowsInFolder = workflowsByFolder[folder.id] || [] - if (expandedFolders.has(folder.id) && workflowsInFolder.length > 0) { - workflowsInFolder.forEach((workflow) => { - result.push( - - ) - }) - } - - // Render child folders - if (expandedFolders.has(folder.id) && folder.children.length > 0) { - result.push(...renderFolderTree(folder.children, level + 1)) - } - }) + // Root drag handlers using the custom hook + const { + isDragOver: rootDragOver, + handleDragOver: handleRootDragOver, + handleDragLeave: handleRootDragLeave, + handleDrop: handleRootDrop, + } = useDragHandlers(updateWorkflow, null, 'Moved workflow(s) to root') - return result + const renderFolderTree = (nodes: FolderTreeNode[], level = 0): React.ReactNode[] => { + return nodes.map((folder) => ( + + )) } const showLoading = isLoading || foldersLoading @@ -334,15 +220,33 @@ export function FolderTree({ {renderFolderTree(folderTree)} {/* Root level workflows (no folder) */} - {(workflowsByFolder.root || []).map((workflow) => ( - - ))} +
+ {(workflowsByFolder.root || []).map((workflow) => ( + + ))} +
{/* Marketplace workflows */} {marketplaceWorkflows.length > 0 && ( @@ -362,6 +266,7 @@ export function FolderTree({ isMarketplace isCollapsed={isCollapsed} level={-1} + isDragOver={false} /> ))}
diff --git a/apps/sim/stores/folders/store.ts b/apps/sim/stores/folders/store.ts index de063d4ddc1..2fa9e06d884 100644 --- a/apps/sim/stores/folders/store.ts +++ b/apps/sim/stores/folders/store.ts @@ -23,6 +23,7 @@ interface FolderState { folders: Record isLoading: boolean expandedFolders: Set + selectedWorkflows: Set // Actions setFolders: (folders: WorkflowFolder[]) => void @@ -33,6 +34,14 @@ interface FolderState { toggleExpanded: (folderId: string) => void setExpanded: (folderId: string, expanded: boolean) => void + // Selection actions + selectWorkflow: (workflowId: string) => void + deselectWorkflow: (workflowId: string) => void + toggleWorkflowSelection: (workflowId: string) => void + clearSelection: () => void + selectOnly: (workflowId: string) => void + isWorkflowSelected: (workflowId: string) => boolean + // Computed values getFolderTree: (workspaceId: string) => FolderTreeNode[] getFolderById: (id: string) => WorkflowFolder | undefined @@ -57,6 +66,7 @@ export const useFolderStore = create()( folders: {}, isLoading: false, expandedFolders: new Set(), + selectedWorkflows: new Set(), setFolders: (folders) => set(() => ({ @@ -113,6 +123,44 @@ export const useFolderStore = create()( return { expandedFolders: newExpanded } }), + // Selection actions + selectWorkflow: (workflowId) => + set((state) => { + const newSelected = new Set(state.selectedWorkflows) + newSelected.add(workflowId) + return { selectedWorkflows: newSelected } + }), + + deselectWorkflow: (workflowId) => + set((state) => { + const newSelected = new Set(state.selectedWorkflows) + newSelected.delete(workflowId) + return { selectedWorkflows: newSelected } + }), + + toggleWorkflowSelection: (workflowId) => + set((state) => { + const newSelected = new Set(state.selectedWorkflows) + if (newSelected.has(workflowId)) { + newSelected.delete(workflowId) + } else { + newSelected.add(workflowId) + } + return { selectedWorkflows: newSelected } + }), + + clearSelection: () => + set(() => ({ + selectedWorkflows: new Set(), + })), + + selectOnly: (workflowId) => + set(() => ({ + selectedWorkflows: new Set([workflowId]), + })), + + isWorkflowSelected: (workflowId) => get().selectedWorkflows.has(workflowId), + getFolderTree: (workspaceId) => { const folders = Object.values(get().folders).filter((f) => f.workspaceId === workspaceId) @@ -268,3 +316,7 @@ export const useFolderStore = create()( { name: 'folder-store' } ) ) + +// Selector hook for checking if a workflow is selected (avoids get() calls) +export const useIsWorkflowSelected = (workflowId: string) => + useFolderStore((state) => state.selectedWorkflows.has(workflowId)) From 1971a6bd85c896ad0605a4cf879cc42a314643f6 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 13 Jun 2025 18:24:19 -0700 Subject: [PATCH 2/7] allow drag into root --- .../components/folder-tree/components/workflow-item.tsx | 1 - .../components/sidebar/components/folder-tree/folder-tree.tsx | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/components/workflow-item.tsx b/apps/sim/app/w/components/sidebar/components/folder-tree/components/workflow-item.tsx index 58692728c0e..242094197d4 100644 --- a/apps/sim/app/w/components/sidebar/components/folder-tree/components/workflow-item.tsx +++ b/apps/sim/app/w/components/sidebar/components/folder-tree/components/workflow-item.tsx @@ -74,7 +74,6 @@ export function WorkflowItem({ const handleDragEnd = () => { setIsDragging(false) - // Use requestAnimationFrame to reset after the current event loop requestAnimationFrame(() => { dragStartedRef.current = false }) diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx b/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx index bb6dadf4499..67572fd6ec6 100644 --- a/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx +++ b/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx @@ -223,7 +223,9 @@ export function FolderTree({
Date: Fri, 13 Jun 2025 18:30:28 -0700 Subject: [PATCH 3/7] remove extraneous comments --- .../folder-tree/components/folder-item.tsx | 1 - .../folder-tree/components/workflow-item.tsx | 12 +----------- .../sidebar/components/folder-tree/folder-tree.tsx | 2 -- 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx b/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx index 2b1e9114a35..f6743be2044 100644 --- a/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx +++ b/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx @@ -34,7 +34,6 @@ export function FolderItem({ const handleToggleExpanded = () => { toggleExpanded(folder.id) - // Persist to server updateFolderAPI(folder.id, { isExpanded: !isExpanded }).catch(console.error) } diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/components/workflow-item.tsx b/apps/sim/app/w/components/sidebar/components/folder-tree/components/workflow-item.tsx index 242094197d4..07bf393d9bb 100644 --- a/apps/sim/app/w/components/sidebar/components/folder-tree/components/workflow-item.tsx +++ b/apps/sim/app/w/components/sidebar/components/folder-tree/components/workflow-item.tsx @@ -30,42 +30,32 @@ export function WorkflowItem({ const isSelected = useIsWorkflowSelected(workflow.id) const handleClick = (e: React.MouseEvent) => { - // Don't handle click if a drag operation was started if (dragStartedRef.current) { e.preventDefault() return } if (e.shiftKey) { - // Shift+click: toggle selection and prevent navigation e.preventDefault() toggleWorkflowSelection(workflow.id) } else { - // Regular click: select this workflow only and allow navigation - // Only select if it's not already the only selected workflow to avoid unnecessary re-renders if (!isSelected || selectedWorkflows.size > 1) { selectOnly(workflow.id) } - // Don't prevent default - let Link handle navigation } } const handleDragStart = (e: React.DragEvent) => { - if (isMarketplace) return // Don't allow dragging marketplace workflows + if (isMarketplace) return - // Mark that a drag operation has started dragStartedRef.current = true setIsDragging(true) - // If this workflow is part of a multi-selection, drag all selected workflows - // Otherwise, just drag this single workflow let workflowIds: string[] if (isSelected && selectedWorkflows.size > 1) { workflowIds = Array.from(selectedWorkflows) } else { workflowIds = [workflow.id] - // Don't call selectOnly here as it can interfere with the drag operation - // The workflow will be visually highlighted by the isDragging state } e.dataTransfer.setData('workflow-ids', JSON.stringify(workflowIds)) diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx b/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx index 67572fd6ec6..b7477811500 100644 --- a/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx +++ b/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx @@ -169,7 +169,6 @@ export function FolderTree({ } }, [activeWorkspaceId, fetchFolders]) - // Clear selection when navigating to different workspace useEffect(() => { clearSelection() }, [activeWorkspaceId, clearSelection]) @@ -187,7 +186,6 @@ export function FolderTree({ {} as Record ) - // Root drag handlers using the custom hook const { isDragOver: rootDragOver, handleDragOver: handleRootDragOver, From 48af9399bf786d5ae484946f6d13e941c0d80e9b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 13 Jun 2025 18:56:00 -0700 Subject: [PATCH 4/7] instantly create worfklow on plus --- .../components/create-menu/create-menu.tsx | 103 ++++++++++++------ 1 file changed, 67 insertions(+), 36 deletions(-) diff --git a/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx b/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx index d13e7723a5b..32825965b2b 100644 --- a/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx +++ b/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx @@ -24,6 +24,7 @@ export function CreateMenu({ onCreateWorkflow, isCollapsed }: CreateMenuProps) { const [showFolderDialog, setShowFolderDialog] = useState(false) const [folderName, setFolderName] = useState('') const [isCreating, setIsCreating] = useState(false) + const [dropdownOpen, setDropdownOpen] = useState(false) const { activeWorkspaceId } = useWorkflowRegistry() const { createFolder } = useFolderStore() @@ -34,6 +35,7 @@ export function CreateMenu({ onCreateWorkflow, isCollapsed }: CreateMenuProps) { const handleCreateFolder = () => { setShowFolderDialog(true) + setDropdownOpen(false) } const handleFolderSubmit = async (e: React.FormEvent) => { @@ -61,27 +63,48 @@ export function CreateMenu({ onCreateWorkflow, isCollapsed }: CreateMenuProps) { setShowFolderDialog(false) } + const handleMouseEnter = () => { + setDropdownOpen(true) + } + + const handleMouseLeave = () => { + setDropdownOpen(false) + } + + const handleDropdownItemClick = (action: () => void) => { + action() + setDropdownOpen(false) + } + if (isCollapsed) { return ( <> - - - - - - - - New Workflow - - - - New Folder - - - +
+ + + + + + handleDropdownItemClick(handleCreateWorkflow)}> + + New Workflow + + handleDropdownItemClick(handleCreateFolder)}> + + New Folder + + + +
{/* Folder creation dialog */} @@ -118,24 +141,32 @@ export function CreateMenu({ onCreateWorkflow, isCollapsed }: CreateMenuProps) { return ( <> - - - - - - - - New Workflow - - - - New Folder - - - +
+ + + + + + handleDropdownItemClick(handleCreateWorkflow)}> + + New Workflow + + handleDropdownItemClick(handleCreateFolder)}> + + New Folder + + + +
{/* Folder creation dialog */} From dd0437bf3dcd0b5ec353fd4390a5b6cf698ec140 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 14 Jun 2025 11:21:59 -0700 Subject: [PATCH 5/7] styling improvements, fixed flicker --- .../components/create-menu/create-menu.tsx | 170 ++++++------------ .../folder-tree/components/folder-item.tsx | 36 +++- .../components/folder-tree/folder-tree.tsx | 8 +- apps/sim/stores/folders/store.ts | 5 - 4 files changed, 97 insertions(+), 122 deletions(-) diff --git a/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx b/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx index 32825965b2b..b94bb8b1ef3 100644 --- a/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx +++ b/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx @@ -4,14 +4,10 @@ import { useState } from 'react' import { File, Folder, Plus } from 'lucide-react' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' +import { cn } from '@/lib/utils' import { useFolderStore } from '@/stores/folders/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -24,18 +20,19 @@ export function CreateMenu({ onCreateWorkflow, isCollapsed }: CreateMenuProps) { const [showFolderDialog, setShowFolderDialog] = useState(false) const [folderName, setFolderName] = useState('') const [isCreating, setIsCreating] = useState(false) - const [dropdownOpen, setDropdownOpen] = useState(false) + const [isHoverOpen, setIsHoverOpen] = useState(false) const { activeWorkspaceId } = useWorkflowRegistry() const { createFolder } = useFolderStore() const handleCreateWorkflow = () => { + setIsHoverOpen(false) onCreateWorkflow() } const handleCreateFolder = () => { + setIsHoverOpen(false) setShowFolderDialog(true) - setDropdownOpen(false) } const handleFolderSubmit = async (e: React.FormEvent) => { @@ -52,7 +49,6 @@ export function CreateMenu({ onCreateWorkflow, isCollapsed }: CreateMenuProps) { setShowFolderDialog(false) } catch (error) { console.error('Failed to create folder:', error) - // You could add toast notification here } finally { setIsCreating(false) } @@ -63,110 +59,62 @@ export function CreateMenu({ onCreateWorkflow, isCollapsed }: CreateMenuProps) { setShowFolderDialog(false) } - const handleMouseEnter = () => { - setDropdownOpen(true) - } - - const handleMouseLeave = () => { - setDropdownOpen(false) - } - - const handleDropdownItemClick = (action: () => void) => { - action() - setDropdownOpen(false) - } - - if (isCollapsed) { - return ( - <> -
- - - - - - handleDropdownItemClick(handleCreateWorkflow)}> - - New Workflow - - handleDropdownItemClick(handleCreateFolder)}> - - New Folder - - - -
- - {/* Folder creation dialog */} - - - - Create New Folder - -
-
- - setFolderName(e.target.value)} - placeholder='Enter folder name...' - autoFocus - required - /> -
-
- - -
-
-
-
- - ) - } - return ( <> -
- - - - - - handleDropdownItemClick(handleCreateWorkflow)}> - - New Workflow - - handleDropdownItemClick(handleCreateFolder)}> - - New Folder - - - -
+ + + + + setIsHoverOpen(true)} + onMouseLeave={() => setIsHoverOpen(false)} + onOpenAutoFocus={(e) => e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + > + + + + {/* Folder creation dialog */} diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx b/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx index f6743be2044..521478fa8e4 100644 --- a/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx +++ b/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx @@ -1,5 +1,6 @@ 'use client' +import { useCallback, useEffect, useRef } from 'react' import clsx from 'clsx' import { ChevronDown, ChevronRight, Folder, FolderOpen } from 'lucide-react' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' @@ -31,11 +32,40 @@ export function FolderItem({ const { updateWorkflow } = useWorkflowRegistry() const isExpanded = expandedFolders.has(folder.id) + const updateTimeoutRef = useRef | undefined>(undefined) + const pendingStateRef = useRef(null) - const handleToggleExpanded = () => { + const handleToggleExpanded = useCallback(() => { + const newExpandedState = !isExpanded toggleExpanded(folder.id) - updateFolderAPI(folder.id, { isExpanded: !isExpanded }).catch(console.error) - } + pendingStateRef.current = newExpandedState + + // Clear any pending update + if (updateTimeoutRef.current) { + clearTimeout(updateTimeoutRef.current) + } + + // Debounce the API call to prevent race conditions + updateTimeoutRef.current = setTimeout(() => { + // Only update if the pending state matches what we're about to send + if (pendingStateRef.current === newExpandedState) { + updateFolderAPI(folder.id, { isExpanded: newExpandedState }) + .catch(console.error) + .finally(() => { + pendingStateRef.current = null + }) + } + }, 300) // Increased debounce to prevent rapid toggle issues + }, [folder.id, isExpanded, toggleExpanded, updateFolderAPI]) + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (updateTimeoutRef.current) { + clearTimeout(updateTimeoutRef.current) + } + } + }, []) const handleRename = async (folderId: string, newName: string) => { try { diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx b/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx index b7477811500..4b23a97743e 100644 --- a/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx +++ b/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx @@ -66,7 +66,7 @@ function FolderSection({ {/* Render workflows in this folder */} {expandedFolders.has(folder.id) && workflowsInFolder.length > 0 && ( -
+
{workflowsInFolder.map((workflow) => ( +
{/* Folder tree */} {renderFolderTree(folderTree)} {/* Root level workflows (no folder) */}
()( get().updateFolder(id, processedFolder) - // Update expanded state if isExpanded was changed - if (updates.isExpanded !== undefined) { - get().setExpanded(id, updates.isExpanded) - } - return processedFolder }, From f64ca8b9db0b86c2c178924f336a72f11e366db4 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 14 Jun 2025 13:27:06 -0700 Subject: [PATCH 6/7] small improvement to dragover container --- .../components/create-menu/create-menu.tsx | 1 - .../folder-tree/components/folder-item.tsx | 6 +----- .../components/folder-tree/folder-tree.tsx | 20 +++++++++++++++---- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx b/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx index b94bb8b1ef3..70889b45cfc 100644 --- a/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx +++ b/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx @@ -86,7 +86,6 @@ export function CreateMenu({ onCreateWorkflow, isCollapsed }: CreateMenuProps) { side={isCollapsed ? 'right' : undefined} sideOffset={2} className={cn( - // Use the same animation classes as tooltip 'fade-in-0 zoom-in-95 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95', 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2', 'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx b/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx index 521478fa8e4..c2de6c48cb3 100644 --- a/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx +++ b/apps/sim/app/w/components/sidebar/components/folder-tree/components/folder-item.tsx @@ -40,14 +40,11 @@ export function FolderItem({ toggleExpanded(folder.id) pendingStateRef.current = newExpandedState - // Clear any pending update if (updateTimeoutRef.current) { clearTimeout(updateTimeoutRef.current) } - // Debounce the API call to prevent race conditions updateTimeoutRef.current = setTimeout(() => { - // Only update if the pending state matches what we're about to send if (pendingStateRef.current === newExpandedState) { updateFolderAPI(folder.id, { isExpanded: newExpandedState }) .catch(console.error) @@ -55,10 +52,9 @@ export function FolderItem({ pendingStateRef.current = null }) } - }, 300) // Increased debounce to prevent rapid toggle issues + }, 300) }, [folder.id, isExpanded, toggleExpanded, updateFolderAPI]) - // Cleanup timeout on unmount useEffect(() => { return () => { if (updateTimeoutRef.current) { diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx b/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx index 4b23a97743e..eec2b5d0a92 100644 --- a/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx +++ b/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx @@ -18,7 +18,12 @@ interface FolderSectionProps { expandedFolders: Set pathname: string updateWorkflow: (id: string, updates: any) => void - renderFolderTree: (nodes: FolderTreeNode[], level: number) => React.ReactNode[] + renderFolderTree: ( + nodes: FolderTreeNode[], + level: number, + parentDragOver?: boolean + ) => React.ReactNode[] + parentDragOver?: boolean } function FolderSection({ @@ -31,6 +36,7 @@ function FolderSection({ pathname, updateWorkflow, renderFolderTree, + parentDragOver = false, }: FolderSectionProps) { const { isDragOver, handleDragOver, handleDragLeave, handleDrop } = useDragHandlers( updateWorkflow, @@ -39,6 +45,7 @@ function FolderSection({ ) const workflowsInFolder = workflowsByFolder[folder.id] || [] + const isAnyDragOver = isDragOver || parentDragOver return (
))}
@@ -82,7 +89,7 @@ function FolderSection({ {/* Render child folders */} {expandedFolders.has(folder.id) && folder.children.length > 0 && ( -
{renderFolderTree(folder.children, level + 1)}
+
{renderFolderTree(folder.children, level + 1, isAnyDragOver)}
)}
) @@ -193,7 +200,11 @@ export function FolderTree({ handleDrop: handleRootDrop, } = useDragHandlers(updateWorkflow, null, 'Moved workflow(s) to root') - const renderFolderTree = (nodes: FolderTreeNode[], level = 0): React.ReactNode[] => { + const renderFolderTree = ( + nodes: FolderTreeNode[], + level = 0, + parentDragOver = false + ): React.ReactNode[] => { return nodes.map((folder) => ( )) } From a7416be4aa3cf18da10d651428d0edf224e22101 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 14 Jun 2025 13:42:27 -0700 Subject: [PATCH 7/7] ack PR comments --- .../sidebar/components/create-menu/create-menu.tsx | 2 +- .../components/folder-tree/components/folder-item.tsx | 5 +---- .../sidebar/components/folder-tree/folder-tree.tsx | 4 ++-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx b/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx index 70889b45cfc..5ff60db8657 100644 --- a/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx +++ b/apps/sim/app/w/components/sidebar/components/create-menu/create-menu.tsx @@ -84,7 +84,7 @@ export function CreateMenu({ onCreateWorkflow, isCollapsed }: CreateMenuProps) { | undefined>(undefined) diff --git a/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx b/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx index eec2b5d0a92..818dd1d3b3c 100644 --- a/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx +++ b/apps/sim/app/w/components/sidebar/components/folder-tree/folder-tree.tsx @@ -17,7 +17,7 @@ interface FolderSectionProps { workflowsByFolder: Record expandedFolders: Set pathname: string - updateWorkflow: (id: string, updates: any) => void + updateWorkflow: (id: string, updates: Partial) => void renderFolderTree: ( nodes: FolderTreeNode[], level: number, @@ -97,7 +97,7 @@ function FolderSection({ // Custom hook for drag and drop handling function useDragHandlers( - updateWorkflow: (id: string, updates: any) => void, + updateWorkflow: (id: string, updates: Partial) => void, targetFolderId: string | null, // null for root logMessage?: string ) {