Skip to content

Commit 0aec9ef

Browse files
feat(export): added the ability to export workflow (#2777)
* feat(export): added the ability to export workflow * improvement(import): loading animation * fixed flicker on importing multiple workflows * ack pr comments * standardized import/export hooks * upgraded turborepo * cleaned up --------- Co-authored-by: Emir Karabeg <[email protected]>
1 parent cb4db20 commit 0aec9ef

File tree

23 files changed

+589
-439
lines changed

23 files changed

+589
-439
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export function Panel() {
108108
// Delete workflow hook
109109
const { isDeleting, handleDeleteWorkflow } = useDeleteWorkflow({
110110
workspaceId,
111-
getWorkflowIds: () => activeWorkflowId || '',
111+
workflowIds: activeWorkflowId || '',
112112
isActive: true,
113113
onSuccess: () => setIsDeleteModalOpen(false),
114114
})

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ export function ContextMenu({
373373
onKeyDown={handleHexKeyDown}
374374
onFocus={handleHexFocus}
375375
onClick={(e) => e.stopPropagation()}
376-
className='h-[20px] min-w-0 flex-1 rounded-[4px] bg-[#363636] px-[6px] text-[11px] text-white uppercase focus:outline-none'
376+
className='h-[20px] min-w-0 flex-1 rounded-[4px] bg-[#363636] px-[6px] text-[11px] text-white uppercase caret-white focus:outline-none'
377377
/>
378378
<button
379379
type='button'

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

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
useCanDelete,
2121
useDeleteFolder,
2222
useDuplicateFolder,
23+
useExportFolder,
2324
} from '@/app/workspace/[workspaceId]/w/hooks'
2425
import { useCreateFolder, useUpdateFolder } from '@/hooks/queries/folders'
2526
import { useCreateWorkflow } from '@/hooks/queries/workflows'
@@ -57,23 +58,24 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
5758
const { canDeleteFolder } = useCanDelete({ workspaceId })
5859
const canDelete = useMemo(() => canDeleteFolder(folder.id), [canDeleteFolder, folder.id])
5960

60-
// Delete modal state
6161
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
6262

63-
// Delete folder hook
6463
const { isDeleting, handleDeleteFolder } = useDeleteFolder({
6564
workspaceId,
66-
getFolderIds: () => folder.id,
65+
folderIds: folder.id,
6766
onSuccess: () => setIsDeleteModalOpen(false),
6867
})
6968

70-
// Duplicate folder hook
7169
const { handleDuplicateFolder } = useDuplicateFolder({
7270
workspaceId,
73-
getFolderIds: () => folder.id,
71+
folderIds: folder.id,
72+
})
73+
74+
const { isExporting, hasWorkflows, handleExportFolder } = useExportFolder({
75+
workspaceId,
76+
folderId: folder.id,
7477
})
7578

76-
// Folder expand hook - must be declared before callbacks that use expandFolder
7779
const {
7880
isExpanded,
7981
handleToggleExpanded,
@@ -90,7 +92,6 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
9092
*/
9193
const handleCreateWorkflowInFolder = useCallback(async () => {
9294
try {
93-
// Generate name and color upfront for optimistic updates
9495
const name = generateCreativeWorkflowName()
9596
const color = getNextWorkflowColor()
9697

@@ -103,15 +104,12 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
103104

104105
if (result.id) {
105106
router.push(`/workspace/${workspaceId}/w/${result.id}`)
106-
// Expand the parent folder so the new workflow is visible
107107
expandFolder()
108-
// Scroll to the newly created workflow
109108
window.dispatchEvent(
110109
new CustomEvent(SIDEBAR_SCROLL_EVENT, { detail: { itemId: result.id } })
111110
)
112111
}
113112
} catch (error) {
114-
// Error already handled by mutation's onError callback
115113
logger.error('Failed to create workflow in folder:', error)
116114
}
117115
}, [createWorkflowMutation, workspaceId, folder.id, router, expandFolder])
@@ -128,9 +126,7 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
128126
parentId: folder.id,
129127
})
130128
if (result.id) {
131-
// Expand the parent folder so the new folder is visible
132129
expandFolder()
133-
// Scroll to the newly created folder
134130
window.dispatchEvent(
135131
new CustomEvent(SIDEBAR_SCROLL_EVENT, { detail: { itemId: result.id } })
136132
)
@@ -147,7 +143,6 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
147143
*/
148144
const onDragStart = useCallback(
149145
(e: React.DragEvent) => {
150-
// Don't start drag if editing
151146
if (isEditing) {
152147
e.preventDefault()
153148
return
@@ -159,12 +154,10 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
159154
[folder.id]
160155
)
161156

162-
// Item drag hook
163157
const { isDragging, shouldPreventClickRef, handleDragStart, handleDragEnd } = useItemDrag({
164158
onDragStart,
165159
})
166160

167-
// Context menu hook
168161
const {
169162
isOpen: isContextMenuOpen,
170163
position,
@@ -174,7 +167,6 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
174167
preventDismiss,
175168
} = useContextMenu()
176169

177-
// Rename hook
178170
const {
179171
isEditing,
180172
editValue,
@@ -258,7 +250,6 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
258250
e.preventDefault()
259251
e.stopPropagation()
260252

261-
// Toggle: close if open, open if closed
262253
if (isContextMenuOpen) {
263254
closeMenu()
264255
return
@@ -365,13 +356,16 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
365356
onCreate={handleCreateWorkflowInFolder}
366357
onCreateFolder={handleCreateFolderInFolder}
367358
onDuplicate={handleDuplicateFolder}
359+
onExport={handleExportFolder}
368360
onDelete={() => setIsDeleteModalOpen(true)}
369361
showCreate={true}
370362
showCreateFolder={true}
363+
showExport={true}
371364
disableRename={!userPermissions.canEdit}
372365
disableCreate={!userPermissions.canEdit || createWorkflowMutation.isPending}
373366
disableCreateFolder={!userPermissions.canEdit || createFolderMutation.isPending}
374-
disableDuplicate={!userPermissions.canEdit}
367+
disableDuplicate={!userPermissions.canEdit || !hasWorkflows}
368+
disableExport={!userPermissions.canEdit || isExporting || !hasWorkflows}
375369
disableDelete={!userPermissions.canEdit || !canDelete}
376370
/>
377371

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

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,15 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
4646
const userPermissions = useUserPermissionsContext()
4747
const isSelected = selectedWorkflows.has(workflow.id)
4848

49-
// Can delete check hook
5049
const { canDeleteWorkflows } = useCanDelete({ workspaceId })
5150

52-
// Delete modal state
5351
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
5452
const [workflowIdsToDelete, setWorkflowIdsToDelete] = useState<string[]>([])
5553
const [deleteModalNames, setDeleteModalNames] = useState<string | string[]>('')
5654
const [canDeleteCaptured, setCanDeleteCaptured] = useState(true)
5755

58-
// Presence avatars state
5956
const [hasAvatars, setHasAvatars] = useState(false)
6057

61-
// Capture selection at right-click time (using ref to persist across renders)
6258
const capturedSelectionRef = useRef<{
6359
workflowIds: string[]
6460
workflowNames: string | string[]
@@ -68,50 +64,39 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
6864
* Handle opening the delete modal - uses pre-captured selection state
6965
*/
7066
const handleOpenDeleteModal = useCallback(() => {
71-
// Use the selection captured at right-click time
7267
if (capturedSelectionRef.current) {
7368
setWorkflowIdsToDelete(capturedSelectionRef.current.workflowIds)
7469
setDeleteModalNames(capturedSelectionRef.current.workflowNames)
7570
setIsDeleteModalOpen(true)
7671
}
7772
}, [])
7873

79-
// Delete workflow hook
8074
const { isDeleting, handleDeleteWorkflow } = useDeleteWorkflow({
8175
workspaceId,
82-
getWorkflowIds: () => workflowIdsToDelete,
76+
workflowIds: workflowIdsToDelete,
8377
isActive: (workflowIds) => workflowIds.includes(params.workflowId as string),
8478
onSuccess: () => setIsDeleteModalOpen(false),
8579
})
8680

87-
// Duplicate workflow hook
88-
const { handleDuplicateWorkflow } = useDuplicateWorkflow({
89-
workspaceId,
90-
getWorkflowIds: () => {
91-
// Use the selection captured at right-click time
92-
return capturedSelectionRef.current?.workflowIds || []
93-
},
94-
})
81+
const { handleDuplicateWorkflow: duplicateWorkflow } = useDuplicateWorkflow({ workspaceId })
9582

96-
// Export workflow hook
97-
const { handleExportWorkflow } = useExportWorkflow({
98-
workspaceId,
99-
getWorkflowIds: () => {
100-
// Use the selection captured at right-click time
101-
return capturedSelectionRef.current?.workflowIds || []
102-
},
103-
})
83+
const { handleExportWorkflow: exportWorkflow } = useExportWorkflow({ workspaceId })
84+
const handleDuplicateWorkflow = useCallback(() => {
85+
const workflowIds = capturedSelectionRef.current?.workflowIds || []
86+
if (workflowIds.length === 0) return
87+
duplicateWorkflow(workflowIds)
88+
}, [duplicateWorkflow])
89+
90+
const handleExportWorkflow = useCallback(() => {
91+
const workflowIds = capturedSelectionRef.current?.workflowIds || []
92+
if (workflowIds.length === 0) return
93+
exportWorkflow(workflowIds)
94+
}, [exportWorkflow])
10495

105-
/**
106-
* Opens the workflow in a new browser tab
107-
*/
10896
const handleOpenInNewTab = useCallback(() => {
10997
window.open(`/workspace/${workspaceId}/w/${workflow.id}`, '_blank')
11098
}, [workspaceId, workflow.id])
11199

112-
/**
113-
* Changes the workflow color
114-
*/
115100
const handleColorChange = useCallback(
116101
(color: string) => {
117102
updateWorkflow(workflow.id, { color })
@@ -126,7 +111,6 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
126111
*/
127112
const onDragStart = useCallback(
128113
(e: React.DragEvent) => {
129-
// Don't start drag if editing
130114
if (isEditing) {
131115
e.preventDefault()
132116
return
@@ -141,12 +125,10 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
141125
[isSelected, selectedWorkflows, workflow.id]
142126
)
143127

144-
// Item drag hook
145128
const { isDragging, shouldPreventClickRef, handleDragStart, handleDragEnd } = useItemDrag({
146129
onDragStart,
147130
})
148131

149-
// Context menu hook
150132
const {
151133
isOpen: isContextMenuOpen,
152134
position,
@@ -215,14 +197,12 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
215197
e.preventDefault()
216198
e.stopPropagation()
217199

218-
// Toggle: close if open, open if closed
219200
if (isContextMenuOpen) {
220201
closeMenu()
221202
return
222203
}
223204

224205
captureSelectionState()
225-
// Open context menu aligned with the button
226206
const rect = e.currentTarget.getBoundingClientRect()
227207
handleContextMenuBase({
228208
preventDefault: () => {},
@@ -234,7 +214,6 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
234214
[isContextMenuOpen, closeMenu, captureSelectionState, handleContextMenuBase]
235215
)
236216

237-
// Rename hook
238217
const {
239218
isEditing,
240219
editValue,
@@ -281,12 +260,10 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
281260

282261
const isModifierClick = e.shiftKey || e.metaKey || e.ctrlKey
283262

284-
// Prevent default link behavior when using modifier keys
285263
if (isModifierClick) {
286264
e.preventDefault()
287265
}
288266

289-
// Use metaKey (Cmd on Mac) or ctrlKey (Ctrl on Windows/Linux)
290267
onWorkflowClick(workflow.id, e.shiftKey, e.metaKey || e.ctrlKey)
291268
},
292269
[shouldPreventClickRef, workflow.id, onWorkflowClick, isEditing]

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
useDragDrop,
1010
useWorkflowSelection,
1111
} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
12-
import { useImportWorkflow } from '@/app/workspace/[workspaceId]/w/hooks/use-import-workflow'
1312
import { useFolders } from '@/hooks/queries/folders'
1413
import { useFolderStore } from '@/stores/folders/store'
1514
import type { FolderTreeNode } from '@/stores/folders/types'
@@ -25,24 +24,21 @@ const TREE_SPACING = {
2524
interface WorkflowListProps {
2625
regularWorkflows: WorkflowMetadata[]
2726
isLoading?: boolean
28-
isImporting: boolean
29-
setIsImporting: (value: boolean) => void
27+
handleFileChange: (event: React.ChangeEvent<HTMLInputElement>) => void
3028
fileInputRef: React.RefObject<HTMLInputElement | null>
3129
scrollContainerRef: React.RefObject<HTMLDivElement | null>
3230
}
3331

3432
/**
3533
* WorkflowList component displays workflows organized by folders with drag-and-drop support.
36-
* Uses the workflow import hook for handling JSON imports.
3734
*
3835
* @param props - Component props
3936
* @returns Workflow list with folders and drag-drop support
4037
*/
4138
export function WorkflowList({
4239
regularWorkflows,
4340
isLoading = false,
44-
isImporting,
45-
setIsImporting,
41+
handleFileChange,
4642
fileInputRef,
4743
scrollContainerRef,
4844
}: WorkflowListProps) {
@@ -65,9 +61,6 @@ export function WorkflowList({
6561
createFolderHeaderHoverHandlers,
6662
} = useDragDrop()
6763

68-
// Workflow import hook
69-
const { handleFileChange } = useImportWorkflow({ workspaceId })
70-
7164
// Set scroll container when ref changes
7265
useEffect(() => {
7366
if (scrollContainerRef.current) {

0 commit comments

Comments
 (0)