diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index e7c649694..5bf0340a3 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -58,14 +58,12 @@ function resolveCodeVariables( // Find the block ID by looking for a block name that normalizes to this value let blockId = null - let matchedBlockName = null for (const [blockName, id] of Object.entries(blockNameMapping)) { // Apply the same normalization logic as the UI: remove spaces and lowercase const normalizedName = blockName.replace(/\s+/g, '').toLowerCase() if (normalizedName === normalizedBlockName) { blockId = id - matchedBlockName = blockName break } } @@ -151,9 +149,6 @@ export async function POST(req: NextRequest) { }) // Resolve variables in the code with workflow environment variables - logger.info(`[${requestId}] Original code:`, code.substring(0, 200)) - logger.info(`[${requestId}] Execution params keys:`, Object.keys(executionParams)) - const { resolvedCode, contextVariables } = resolveCodeVariables( code, executionParams, @@ -162,9 +157,6 @@ export async function POST(req: NextRequest) { blockNameMapping ) - logger.info(`[${requestId}] Resolved code:`, resolvedCode.substring(0, 200)) - logger.info(`[${requestId}] Context variables keys:`, Object.keys(contextVariables)) - const executionMethod = 'vm' // Default execution method // // Try to use Freestyle if the API key is available diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-selector/document-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-selector/document-selector.tsx index d1a3ab06f..85c9e08dd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-selector/document-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/document-selector/document-selector.tsx @@ -13,6 +13,7 @@ import { } from '@/components/ui/command' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import type { SubBlockConfig } from '@/blocks/types' +import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useSubBlockStore } from '@/stores/workflows/subblock/store' interface DocumentData { @@ -50,7 +51,8 @@ export function DocumentSelector({ isPreview = false, previewValue, }: DocumentSelectorProps) { - const { getValue, setValue } = useSubBlockStore() + const { getValue } = useSubBlockStore() + const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() const [documents, setDocuments] = useState([]) const [error, setError] = useState(null) @@ -117,7 +119,7 @@ export function DocumentSelector({ if (selectedId && !fetchedDocuments.some((doc: DocumentData) => doc.id === selectedId)) { setSelectedId('') if (!isPreview) { - setValue(blockId, subBlock.id, '') + collaborativeSetSubblockValue(blockId, subBlock.id, '') } } @@ -131,7 +133,7 @@ export function DocumentSelector({ setSelectedId(singleDoc.id) setSelectedDocument(singleDoc) if (!isPreview) { - setValue(blockId, subBlock.id, singleDoc.id) + collaborativeSetSubblockValue(blockId, subBlock.id, singleDoc.id) } onDocumentSelect?.(singleDoc.id) } @@ -141,7 +143,15 @@ export function DocumentSelector({ setError((err as Error).message) setDocuments([]) } - }, [knowledgeBaseId, selectedId, setValue, blockId, subBlock.id, isPreview, onDocumentSelect]) + }, [ + knowledgeBaseId, + selectedId, + collaborativeSetSubblockValue, + blockId, + subBlock.id, + isPreview, + onDocumentSelect, + ]) // Handle dropdown open/close - fetch documents when opening const handleOpenChange = (isOpen: boolean) => { @@ -163,7 +173,7 @@ export function DocumentSelector({ setSelectedId(document.id) if (!isPreview) { - setValue(blockId, subBlock.id, document.id) + collaborativeSetSubblockValue(blockId, subBlock.id, document.id) } onDocumentSelect?.(document.id) @@ -193,10 +203,10 @@ export function DocumentSelector({ setInitialFetchDone(false) setError(null) if (!isPreview) { - setValue(blockId, subBlock.id, '') + collaborativeSetSubblockValue(blockId, subBlock.id, '') } } - }, [knowledgeBaseId, blockId, subBlock.id, setValue, isPreview]) + }, [knowledgeBaseId, blockId, subBlock.id, collaborativeSetSubblockValue, isPreview]) // Fetch documents when knowledge base is available and we haven't fetched yet useEffect(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx index 44aee048e..475f7b5e5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-selector/file-selector-input.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { env } from '@/lib/env' import type { SubBlockConfig } from '@/blocks/types' +import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import type { ConfluenceFileInfo } from './components/confluence-file-selector' @@ -36,7 +37,8 @@ export function FileSelectorInput({ isPreview = false, previewValue, }: FileSelectorInputProps) { - const { getValue, setValue } = useSubBlockStore() + const { getValue } = useSubBlockStore() + const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() const { activeWorkflowId } = useWorkflowRegistry() const [selectedFileId, setSelectedFileId] = useState('') const [_fileInfo, setFileInfo] = useState(null) @@ -115,19 +117,19 @@ export function FileSelectorInput({ const handleFileChange = (fileId: string, info?: any) => { setSelectedFileId(fileId) setFileInfo(info || null) - setValue(blockId, subBlock.id, fileId) + collaborativeSetSubblockValue(blockId, subBlock.id, fileId) } // Handle issue selection const handleIssueChange = (issueKey: string, info?: JiraIssueInfo) => { setSelectedIssueId(issueKey) setIssueInfo(info || null) - setValue(blockId, subBlock.id, issueKey) + collaborativeSetSubblockValue(blockId, subBlock.id, issueKey) // Clear the fields when a new issue is selected if (isJira) { - setValue(blockId, 'summary', '') - setValue(blockId, 'description', '') + collaborativeSetSubblockValue(blockId, 'summary', '') + collaborativeSetSubblockValue(blockId, 'description', '') } } @@ -135,14 +137,14 @@ export function FileSelectorInput({ const handleChannelChange = (channelId: string, info?: DiscordChannelInfo) => { setSelectedChannelId(channelId) setChannelInfo(info || null) - setValue(blockId, subBlock.id, channelId) + collaborativeSetSubblockValue(blockId, subBlock.id, channelId) } // Handle calendar selection const handleCalendarChange = (calendarId: string, info?: GoogleCalendarInfo) => { setSelectedCalendarId(calendarId) setCalendarInfo(info || null) - setValue(blockId, subBlock.id, calendarId) + collaborativeSetSubblockValue(blockId, subBlock.id, calendarId) } // For Google Drive @@ -337,7 +339,7 @@ export function FileSelectorInput({ onChange={(value, info) => { setSelectedMessageId(value) setMessageInfo(info || null) - setValue(blockId, subBlock.id, value) + collaborativeSetSubblockValue(blockId, subBlock.id, value) }} provider='microsoft-teams' requiredScopes={subBlock.requiredScopes || []} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-upload.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-upload.tsx index 3a508d00a..e77746e32 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-upload.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/file-upload.tsx @@ -4,9 +4,9 @@ import { useRef, useState } from 'react' import { X } from 'lucide-react' import { Button } from '@/components/ui/button' import { Progress } from '@/components/ui/progress' +import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useNotificationStore } from '@/stores/notifications/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' import { useSubBlockValue } from '../hooks/use-sub-block-value' @@ -58,6 +58,7 @@ export function FileUpload({ // Stores const { addNotification } = useNotificationStore() const { activeWorkflowId } = useWorkflowRegistry() + const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() // Use preview value when in preview mode, otherwise use store value const value = isPreview ? previewValue : storeValue @@ -298,15 +299,15 @@ export function FileUpload({ setStoreValue(newFiles) - // Make sure to update the subblock store value for the workflow execution - useSubBlockStore.getState().setValue(blockId, subBlockId, newFiles) + // Use collaborative update for persistence + collaborativeSetSubblockValue(blockId, subBlockId, newFiles) useWorkflowStore.getState().triggerUpdate() } else { // For single file: Replace with last uploaded file setStoreValue(uploadedFiles[0] || null) - // Make sure to update the subblock store value for the workflow execution - useSubBlockStore.getState().setValue(blockId, subBlockId, uploadedFiles[0] || null) + // Use collaborative update for persistence + collaborativeSetSubblockValue(blockId, subBlockId, uploadedFiles[0] || null) useWorkflowStore.getState().triggerUpdate() } } catch (error) { @@ -363,16 +364,18 @@ export function FileUpload({ const updatedFiles = filesArray.filter((f) => f.path !== file.path) setStoreValue(updatedFiles.length > 0 ? updatedFiles : null) - // Make sure to update the subblock store value for the workflow execution - useSubBlockStore - .getState() - .setValue(blockId, subBlockId, updatedFiles.length > 0 ? updatedFiles : null) + // Use collaborative update for persistence + collaborativeSetSubblockValue( + blockId, + subBlockId, + updatedFiles.length > 0 ? updatedFiles : null + ) } else { // For single file: Clear the value setStoreValue(null) - // Make sure to update the subblock store - useSubBlockStore.getState().setValue(blockId, subBlockId, null) + // Use collaborative update for persistence + collaborativeSetSubblockValue(blockId, subBlockId, null) } useWorkflowStore.getState().triggerUpdate() @@ -413,7 +416,7 @@ export function FileUpload({ // Clear input state immediately for better UX setStoreValue(null) - useSubBlockStore.getState().setValue(blockId, subBlockId, null) + collaborativeSetSubblockValue(blockId, subBlockId, null) useWorkflowStore.getState().triggerUpdate() if (fileInputRef.current) { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx index 8012fb32f..2085c2839 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/knowledge-base-selector/knowledge-base-selector.tsx @@ -14,6 +14,7 @@ import { } from '@/components/ui/command' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import type { SubBlockConfig } from '@/blocks/types' +import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { type KnowledgeBaseData, useKnowledgeStore } from '@/stores/knowledge/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' @@ -36,7 +37,8 @@ export function KnowledgeBaseSelector({ }: KnowledgeBaseSelectorProps) { const { getKnowledgeBasesList, knowledgeBasesList, loadingKnowledgeBasesList } = useKnowledgeStore() - const { getValue, setValue } = useSubBlockStore() + const { getValue } = useSubBlockStore() + const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() const [knowledgeBases, setKnowledgeBases] = useState([]) const [loading, setLoading] = useState(false) @@ -90,7 +92,8 @@ export function KnowledgeBaseSelector({ setSelectedKnowledgeBases([knowledgeBase]) if (!isPreview) { - setValue(blockId, subBlock.id, knowledgeBase.id) + // Use collaborative update for both local store and persistence + collaborativeSetSubblockValue(blockId, subBlock.id, knowledgeBase.id) } onKnowledgeBaseSelect?.(knowledgeBase.id) @@ -117,7 +120,8 @@ export function KnowledgeBaseSelector({ if (!isPreview) { const selectedIds = newSelected.map((kb) => kb.id) const valueToStore = selectedIds.length === 1 ? selectedIds[0] : selectedIds.join(',') - setValue(blockId, subBlock.id, valueToStore) + // Use collaborative update for both local store and persistence + collaborativeSetSubblockValue(blockId, subBlock.id, valueToStore) } onKnowledgeBaseSelect?.(newSelected.map((kb) => kb.id)) @@ -133,7 +137,8 @@ export function KnowledgeBaseSelector({ if (!isPreview) { const selectedIds = newSelected.map((kb) => kb.id) const valueToStore = selectedIds.length === 1 ? selectedIds[0] : selectedIds.join(',') - setValue(blockId, subBlock.id, valueToStore) + // Use collaborative update for both local store and persistence + collaborativeSetSubblockValue(blockId, subBlock.id, valueToStore) } onKnowledgeBaseSelect?.(newSelected.map((kb) => kb.id)) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx index c67c37937..783720c51 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/project-selector/project-selector-input.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import type { SubBlockConfig } from '@/blocks/types' +import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { type DiscordServerInfo, DiscordServerSelector } from './components/discord-server-selector' import { type JiraProjectInfo, JiraProjectSelector } from './components/jira-project-selector' @@ -26,7 +27,8 @@ export function ProjectSelectorInput({ isPreview = false, previewValue, }: ProjectSelectorInputProps) { - const { getValue, setValue } = useSubBlockStore() + const { getValue } = useSubBlockStore() + const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() const [selectedProjectId, setSelectedProjectId] = useState('') const [_projectInfo, setProjectInfo] = useState(null) @@ -58,21 +60,21 @@ export function ProjectSelectorInput({ ) => { setSelectedProjectId(projectId) setProjectInfo(info || null) - setValue(blockId, subBlock.id, projectId) + collaborativeSetSubblockValue(blockId, subBlock.id, projectId) // Clear the issue-related fields when a new project is selected if (provider === 'jira') { - setValue(blockId, 'summary', '') - setValue(blockId, 'description', '') - setValue(blockId, 'issueKey', '') + collaborativeSetSubblockValue(blockId, 'summary', '') + collaborativeSetSubblockValue(blockId, 'description', '') + collaborativeSetSubblockValue(blockId, 'issueKey', '') } else if (provider === 'discord') { - setValue(blockId, 'channelId', '') + collaborativeSetSubblockValue(blockId, 'channelId', '') } else if (provider === 'linear') { if (subBlock.id === 'teamId') { - setValue(blockId, 'teamId', projectId) - setValue(blockId, 'projectId', '') + collaborativeSetSubblockValue(blockId, 'teamId', projectId) + collaborativeSetSubblockValue(blockId, 'projectId', '') } else if (subBlock.id === 'projectId') { - setValue(blockId, 'projectId', projectId) + collaborativeSetSubblockValue(blockId, 'projectId', projectId) } } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index dc3509a81..93575b4e7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -419,6 +419,7 @@ const WorkflowContent = React.memo(() => { } const { type } = event.detail + console.log('🛠️ Adding block from toolbar:', type) if (!type) return if (type === 'connectionBlock') return @@ -439,32 +440,42 @@ const WorkflowContent = React.memo(() => { y: window.innerHeight / 2, }) - // Add the container node directly to canvas with default dimensions - addBlock(id, type, name, centerPosition, { - width: 500, - height: 300, - type: type === 'loop' ? 'loopNode' : 'parallelNode', - }) - // Auto-connect logic for container nodes const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled + let autoConnectEdge if (isAutoConnectEnabled) { const closestBlock = findClosestOutput(centerPosition) if (closestBlock) { // Get appropriate source handle const sourceHandle = determineSourceHandle(closestBlock) - addEdge({ + autoConnectEdge = { id: crypto.randomUUID(), source: closestBlock.id, target: id, sourceHandle, targetHandle: 'target', type: 'workflowEdge', - }) + } } } + // Add the container node directly to canvas with default dimensions and auto-connect edge + addBlock( + id, + type, + name, + centerPosition, + { + width: 500, + height: 300, + type: type === 'loop' ? 'loopNode' : 'parallelNode', + }, + undefined, + undefined, + autoConnectEdge + ) + return } @@ -486,27 +497,30 @@ const WorkflowContent = React.memo(() => { Object.values(blocks).filter((b) => b.type === type).length + 1 }` - // Add the block to the workflow - addBlock(id, type, name, centerPosition) - // Auto-connect logic const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled + let autoConnectEdge if (isAutoConnectEnabled && type !== 'starter') { const closestBlock = findClosestOutput(centerPosition) + console.log('🎯 Closest block found:', closestBlock) if (closestBlock) { // Get appropriate source handle const sourceHandle = determineSourceHandle(closestBlock) - addEdge({ + autoConnectEdge = { id: crypto.randomUUID(), source: closestBlock.id, target: id, sourceHandle, targetHandle: 'target', type: 'workflowEdge', - }) + } + console.log('✅ Auto-connect edge created:', autoConnectEdge) } } + + // Add the block to the workflow with auto-connect edge + addBlock(id, type, name, centerPosition, undefined, undefined, undefined, autoConnectEdge) } window.addEventListener('add-block-from-toolbar', handleAddBlockFromToolbar as EventListener) @@ -583,30 +597,40 @@ const WorkflowContent = React.memo(() => { // Resize the parent container to fit the new child container resizeLoopNodesWrapper() } else { - // Add the container node directly to canvas with default dimensions - addBlock(id, data.type, name, position, { - width: 500, - height: 300, - type: data.type === 'loop' ? 'loopNode' : 'parallelNode', - }) - // Auto-connect the container to the closest node on the canvas const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled + let autoConnectEdge if (isAutoConnectEnabled) { const closestBlock = findClosestOutput(position) if (closestBlock) { const sourceHandle = determineSourceHandle(closestBlock) - addEdge({ + autoConnectEdge = { id: crypto.randomUUID(), source: closestBlock.id, target: id, sourceHandle, targetHandle: 'target', type: 'workflowEdge', - }) + } } } + + // Add the container node directly to canvas with default dimensions and auto-connect edge + addBlock( + id, + data.type, + name, + position, + { + width: 500, + height: 300, + type: data.type === 'loop' ? 'loopNode' : 'parallelNode', + }, + undefined, + undefined, + autoConnectEdge + ) } return @@ -706,26 +730,27 @@ const WorkflowContent = React.memo(() => { } } } else { - // Regular canvas drop - addBlock(id, data.type, name, position) - // Regular auto-connect logic const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled + let autoConnectEdge if (isAutoConnectEnabled && data.type !== 'starter') { const closestBlock = findClosestOutput(position) if (closestBlock) { const sourceHandle = determineSourceHandle(closestBlock) - addEdge({ + autoConnectEdge = { id: crypto.randomUUID(), source: closestBlock.id, target: id, sourceHandle, targetHandle: 'target', type: 'workflowEdge', - }) + } } } + + // Regular canvas drop with auto-connect edge + addBlock(id, data.type, name, position, undefined, undefined, undefined, autoConnectEdge) } } catch (err) { logger.error('Error dropping block:', { err }) diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index 9353e28da..8fec7a7b8 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -91,6 +91,10 @@ export function useCollaborativeWorkflow() { payload.parentId, payload.extent ) + // Handle auto-connect edge if present + if (payload.autoConnectEdge) { + workflowStore.addEdge(payload.autoConnectEdge) + } break case 'update-position': { // Apply position update only if it's newer than the last applied timestamp @@ -164,6 +168,10 @@ export function useCollaborativeWorkflow() { payload.parentId, payload.extent ) + // Handle auto-connect edge if present + if (payload.autoConnectEdge) { + workflowStore.addEdge(payload.autoConnectEdge) + } break } } else if (target === 'edge') { @@ -284,7 +292,8 @@ export function useCollaborativeWorkflow() { position: Position, data?: Record, parentId?: string, - extent?: 'parent' + extent?: 'parent', + autoConnectEdge?: Edge ) => { // Create complete block data upfront using the same logic as the store const blockConfig = getBlock(type) @@ -306,10 +315,14 @@ export function useCollaborativeWorkflow() { height: 0, parentId, extent, + autoConnectEdge, // Include edge data for atomic operation } // Apply locally first workflowStore.addBlock(id, type, name, position, data, parentId, extent) + if (autoConnectEdge) { + workflowStore.addEdge(autoConnectEdge) + } // Then broadcast to other clients with complete block data if (!isApplyingRemoteChange.current) { @@ -354,10 +367,14 @@ export function useCollaborativeWorkflow() { height: 0, // Default height, will be set by the UI parentId, extent, + autoConnectEdge, // Include edge data for atomic operation } // Apply locally first workflowStore.addBlock(id, type, name, position, data, parentId, extent) + if (autoConnectEdge) { + workflowStore.addEdge(autoConnectEdge) + } // Then broadcast to other clients with complete block data if (!isApplyingRemoteChange.current) { diff --git a/apps/sim/socket-server/database/operations.ts b/apps/sim/socket-server/database/operations.ts index 88d424a86..c03d06198 100644 --- a/apps/sim/socket-server/database/operations.ts +++ b/apps/sim/socket-server/database/operations.ts @@ -29,6 +29,34 @@ const db = socketDb // Constants const DEFAULT_LOOP_ITERATIONS = 5 +/** + * Shared function to handle auto-connect edge insertion + * @param tx - Database transaction + * @param workflowId - The workflow ID + * @param autoConnectEdge - The auto-connect edge data + * @param logger - Logger instance + */ +async function insertAutoConnectEdge( + tx: any, + workflowId: string, + autoConnectEdge: any, + logger: any +) { + if (!autoConnectEdge) return + + await tx.insert(workflowEdges).values({ + id: autoConnectEdge.id, + workflowId, + sourceBlockId: autoConnectEdge.source, + targetBlockId: autoConnectEdge.target, + sourceHandle: autoConnectEdge.sourceHandle || null, + targetHandle: autoConnectEdge.targetHandle || null, + }) + logger.debug( + `Added auto-connect edge ${autoConnectEdge.id}: ${autoConnectEdge.source} -> ${autoConnectEdge.target}` + ) +} + // Enum for subflow types enum SubflowType { LOOP = 'loop', @@ -246,6 +274,9 @@ async function handleBlockOperationTx( } await tx.insert(workflowBlocks).values(insertData) + + // Handle auto-connect edge if present + await insertAutoConnectEdge(tx, workflowId, payload.autoConnectEdge, logger) } catch (insertError) { logger.error(`[SERVER] ❌ Failed to insert block ${payload.id}:`, insertError) throw insertError @@ -592,6 +623,9 @@ async function handleBlockOperationTx( } await tx.insert(workflowBlocks).values(insertData) + + // Handle auto-connect edge if present + await insertAutoConnectEdge(tx, workflowId, payload.autoConnectEdge, logger) } catch (insertError) { logger.error(`[SERVER] ❌ Failed to insert duplicated block ${payload.id}:`, insertError) throw insertError diff --git a/apps/sim/socket-server/index.test.ts b/apps/sim/socket-server/index.test.ts index e09c6f1b9..d14e375b0 100644 --- a/apps/sim/socket-server/index.test.ts +++ b/apps/sim/socket-server/index.test.ts @@ -279,6 +279,32 @@ describe('Socket Server Index Integration', () => { expect(() => WorkflowOperationSchema.parse(validOperation)).not.toThrow() }) + it.concurrent('should validate block operations with autoConnectEdge', async () => { + const { WorkflowOperationSchema } = await import('./validation/schemas') + + const validOperationWithAutoEdge = { + operation: 'add', + target: 'block', + payload: { + id: 'test-block', + type: 'action', + name: 'Test Block', + position: { x: 100, y: 200 }, + autoConnectEdge: { + id: 'auto-edge-123', + source: 'source-block', + target: 'test-block', + sourceHandle: 'output', + targetHandle: 'target', + type: 'workflowEdge', + }, + }, + timestamp: Date.now(), + } + + expect(() => WorkflowOperationSchema.parse(validOperationWithAutoEdge)).not.toThrow() + }) + it.concurrent('should validate edge operations', async () => { const { WorkflowOperationSchema } = await import('./validation/schemas') diff --git a/apps/sim/socket-server/validation/schemas.ts b/apps/sim/socket-server/validation/schemas.ts index a238f46f3..2927a0891 100644 --- a/apps/sim/socket-server/validation/schemas.ts +++ b/apps/sim/socket-server/validation/schemas.ts @@ -5,6 +5,16 @@ const PositionSchema = z.object({ y: z.number(), }) +// Schema for auto-connect edge data +const AutoConnectEdgeSchema = z.object({ + id: z.string(), + source: z.string(), + target: z.string(), + sourceHandle: z.string().nullable().optional(), + targetHandle: z.string().nullable().optional(), + type: z.string().optional(), +}) + export const BlockOperationSchema = z.object({ operation: z.enum([ 'add', @@ -35,6 +45,7 @@ export const BlockOperationSchema = z.object({ isWide: z.boolean().optional(), advancedMode: z.boolean().optional(), height: z.number().optional(), + autoConnectEdge: AutoConnectEdgeSchema.optional(), // Add support for auto-connect edges }), timestamp: z.number(), }) @@ -69,4 +80,4 @@ export const WorkflowOperationSchema = z.union([ SubflowOperationSchema, ]) -export { PositionSchema } +export { PositionSchema, AutoConnectEdgeSchema }