diff --git a/apps/sim/app/api/folders/[id]/route.ts b/apps/sim/app/api/folders/[id]/route.ts index d86b20012f..9bb4d32875 100644 --- a/apps/sim/app/api/folders/[id]/route.ts +++ b/apps/sim/app/api/folders/[id]/route.ts @@ -160,6 +160,7 @@ async function deleteFolderRecursively( } // Delete all workflows in this folder (workspace-scoped, not user-scoped) + // The database cascade will handle deleting related workflow_blocks, workflow_edges, workflow_subflows const workflowsInFolder = await db .select({ id: workflow.id }) .from(workflow) diff --git a/apps/sim/app/api/workflows/[id]/route.test.ts b/apps/sim/app/api/workflows/[id]/route.test.ts index 9c4462f2af..a7447a4d7a 100644 --- a/apps/sim/app/api/workflows/[id]/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/route.test.ts @@ -274,14 +274,6 @@ describe('Workflow By ID API Route', () => { }), })) - const mockTransaction = vi.fn().mockImplementation(async (callback) => { - await callback({ - delete: vi.fn().mockReturnValue({ - where: vi.fn().mockResolvedValue(undefined), - }), - }) - }) - vi.doMock('@/db', () => ({ db: { select: vi.fn().mockReturnValue({ @@ -291,7 +283,9 @@ describe('Workflow By ID API Route', () => { }), }), }), - transaction: mockTransaction, + delete: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue(undefined), + }), }, })) @@ -326,14 +320,6 @@ describe('Workflow By ID API Route', () => { }), })) - const mockTransaction = vi.fn().mockImplementation(async (callback) => { - await callback({ - delete: vi.fn().mockReturnValue({ - where: vi.fn().mockResolvedValue(undefined), - }), - }) - }) - vi.doMock('@/db', () => ({ db: { select: vi.fn().mockReturnValue({ @@ -343,7 +329,9 @@ describe('Workflow By ID API Route', () => { }), }), }), - transaction: mockTransaction, + delete: vi.fn().mockReturnValue({ + where: vi.fn().mockResolvedValue(undefined), + }), }, })) diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index fb7cc0c824..9730b59e7c 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -7,7 +7,7 @@ import { createLogger } from '@/lib/logs/console-logger' import { getUserEntityPermissions, hasAdminPermission } from '@/lib/permissions/utils' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers' import { db } from '@/db' -import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@/db/schema' +import { workflow } from '@/db/schema' const logger = createLogger('WorkflowByIdAPI') @@ -206,16 +206,7 @@ export async function DELETE( return NextResponse.json({ error: 'Access denied' }, { status: 403 }) } - // Delete workflow and all related data in a transaction - await db.transaction(async (tx) => { - // Delete from normalized tables first (foreign key constraints) - await tx.delete(workflowSubflows).where(eq(workflowSubflows.workflowId, workflowId)) - await tx.delete(workflowEdges).where(eq(workflowEdges.workflowId, workflowId)) - await tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId)) - - // Delete the main workflow record - await tx.delete(workflow).where(eq(workflow.id, workflowId)) - }) + await db.delete(workflow).where(eq(workflow.id, workflowId)) const elapsed = Date.now() - startTime logger.info(`[${requestId}] Successfully deleted workflow ${workflowId} in ${elapsed}ms`) diff --git a/apps/sim/app/api/workspaces/[id]/route.ts b/apps/sim/app/api/workspaces/[id]/route.ts index feb6ed8960..8643365659 100644 --- a/apps/sim/app/api/workspaces/[id]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/route.ts @@ -2,13 +2,7 @@ import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console-logger' -import { - workflow, - workflowBlocks, - workflowEdges, - workflowSubflows, - workspaceMember, -} from '@/db/schema' +import { workflow, workspaceMember } from '@/db/schema' const logger = createLogger('WorkspaceByIdAPI') @@ -126,20 +120,10 @@ export async function DELETE( // Delete workspace and all related data in a transaction await db.transaction(async (tx) => { - // Get all workflows in this workspace - const workspaceWorkflows = await tx - .select({ id: workflow.id }) - .from(workflow) - .where(eq(workflow.workspaceId, workspaceId)) - - // Delete all workflow-related data for each workflow - for (const wf of workspaceWorkflows) { - await tx.delete(workflowSubflows).where(eq(workflowSubflows.workflowId, wf.id)) - await tx.delete(workflowEdges).where(eq(workflowEdges.workflowId, wf.id)) - await tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, wf.id)) - } - - // Delete all workflows in the workspace + // Delete all workflows in the workspace - database cascade will handle all workflow-related data + // The database cascade will handle deleting related workflow_blocks, workflow_edges, workflow_subflows, + // workflow_logs, workflow_execution_snapshots, workflow_execution_logs, workflow_execution_trace_spans, + // workflow_schedule, webhook, marketplace, chat, and memory records await tx.delete(workflow).where(eq(workflow.workspaceId, workspaceId)) // Delete workspace members diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx index a5f30524d0..35b85805a4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/folder-context-menu/folder-context-menu.tsx @@ -63,12 +63,17 @@ export function FolderContextMenu({ setShowRenameDialog(true) } - const handleDelete = () => { + const handleDelete = async () => { if (onDelete) { onDelete(folderId) } else { - // Default delete behavior - deleteFolder(folderId, workspaceId) + // Default delete behavior with proper error handling + try { + await deleteFolder(folderId, workspaceId) + logger.info(`Successfully deleted folder from context menu: ${folderName}`) + } catch (error) { + logger.error('Failed to delete folder from context menu:', { error, folderId, folderName }) + } } } diff --git a/apps/sim/stores/folders/store.ts b/apps/sim/stores/folders/store.ts index 57ad937eb4..ed92e1001f 100644 --- a/apps/sim/stores/folders/store.ts +++ b/apps/sim/stores/folders/store.ts @@ -314,6 +314,7 @@ export const useFolderStore = create()( const responseData = await response.json() + // Remove the folder from local state get().removeFolder(id) // Remove from expanded state @@ -323,33 +324,19 @@ export const useFolderStore = create()( return { expandedFolders: newExpanded } }) - const workflowRegistry = useWorkflowRegistry.getState() - if (responseData.deletedItems) { - try { - const workflows = Object.values(workflowRegistry.workflows) - const workflowsToDelete = workflows.filter( - (workflow) => - workflow.folderId === id || get().isWorkflowInDeletedSubfolder(workflow, id) - ) - - workflowsToDelete.forEach((workflow) => { - workflowRegistry.removeWorkflow(workflow.id) - }) - - get().removeSubfoldersRecursively(id) - - logger.info( - `Deleted ${responseData.deletedItems.workflows} workflow(s) and ${responseData.deletedItems.folders} folder(s)` - ) - } catch (error) { - logger.error('Error updating local state after folder deletion:', error) - } - } + // Remove subfolders from local state + get().removeSubfoldersRecursively(id) + // The backend has already deleted the workflows, so we just need to refresh + // the workflow registry to sync with the server state + const workflowRegistry = useWorkflowRegistry.getState() if (workspaceId) { - // Trigger workflow refresh through registry store - await workflowRegistry.switchToWorkspace(workspaceId) + await workflowRegistry.loadWorkflows(workspaceId) } + + logger.info( + `Deleted ${responseData.deletedItems.workflows} workflow(s) and ${responseData.deletedItems.folders} folder(s)` + ) }, isWorkflowInDeletedSubfolder: (workflow: Workflow, deletedFolderId: string) => {