Compare commits

...

6 Commits

Author SHA1 Message Date
Vikhyath Mondreti
f4e627a9f7 v0.2.3: fix (#592)
* fix(variable resolution): use variable references to not have escaping issues (#587)

* fix(variable-resolution): don't inject stringified json, use var refs

* fix lint

* remove unused var

---------

Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@vikhyaths-air.lan>

* fix(subblock updates): special selectors persistence (#591)

* fix(knowledge-base-selector): should trigger sockets event for persistence

* fix subblock value updates for non useSubblockValue components

* fix lint

---------

Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local>

* fix(race-cond): for auto-connect rare race condition between adding edge + block (#582)

* auto connect race condition

* fix lint

* Update apps/sim/hooks/use-collaborative-workflow.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix lint

---------

Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local>

---------

Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@vikhyaths-air.lan>
Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local>
Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-06-30 16:57:48 -07:00
Vikhyath Mondreti
b0c1547198 fix test failure 2025-06-30 15:52:26 -07:00
Vikhyath Mondreti
d19632aec3 fix typing issue 2025-06-30 14:01:21 -07:00
Vikhyath Mondreti
35ac68f579 fix(func var resolution): variable ref codepath triggered - lint fixed 2025-06-30 13:55:12 -07:00
Vikhyath Mondreti
9c14f5f8fc fix(func var resolution): variable ref codepath triggered 2025-06-30 13:54:53 -07:00
Vikhyath Mondreti
d50db1d3fb add dot check 2025-06-30 12:45:36 -07:00
17 changed files with 319 additions and 76 deletions

View File

@@ -19,7 +19,9 @@ const logger = createLogger('FunctionExecuteAPI')
function resolveCodeVariables(
code: string,
params: Record<string, any>,
envVars: Record<string, string> = {}
envVars: Record<string, string> = {},
blockData: Record<string, any> = {},
blockNameMapping: Record<string, string> = {}
): { resolvedCode: string; contextVariables: Record<string, any> } {
let resolvedCode = code
const contextVariables: Record<string, any> = {}
@@ -39,11 +41,52 @@ function resolveCodeVariables(
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
}
// Resolve tags with <tag_name> syntax
const tagMatches = resolvedCode.match(/<([a-zA-Z_][a-zA-Z0-9_]*)>/g) || []
// Resolve tags with <tag_name> syntax (including nested paths like <block.response.data>)
const tagMatches = resolvedCode.match(/<([a-zA-Z_][a-zA-Z0-9_.]*[a-zA-Z0-9_])>/g) || []
for (const match of tagMatches) {
const tagName = match.slice(1, -1).trim()
const tagValue = params[tagName] || ''
// Handle nested paths like "getrecord.response.data" or "function1.response.result"
// First try params, then blockData directly, then try with block name mapping
let tagValue = getNestedValue(params, tagName) || getNestedValue(blockData, tagName) || ''
// If not found and the path starts with a block name, try mapping the block name to ID
if (!tagValue && tagName.includes('.')) {
const pathParts = tagName.split('.')
const normalizedBlockName = pathParts[0] // This should already be normalized like "function1"
// Find the block ID by looking for a block name that normalizes to this value
let blockId = 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
break
}
}
if (blockId) {
const remainingPath = pathParts.slice(1).join('.')
const fullPath = `${blockId}.${remainingPath}`
tagValue = getNestedValue(blockData, fullPath) || ''
}
}
// If the value is a stringified JSON, parse it back to object
if (
typeof tagValue === 'string' &&
tagValue.length > 100 &&
(tagValue.startsWith('{') || tagValue.startsWith('['))
) {
try {
tagValue = JSON.parse(tagValue)
} catch (e) {
// Keep as string if parsing fails
}
}
// Instead of injecting large JSON directly, create a variable reference
const safeVarName = `__tag_${tagName.replace(/[^a-zA-Z0-9_]/g, '_')}`
@@ -56,6 +99,17 @@ function resolveCodeVariables(
return { resolvedCode, contextVariables }
}
/**
* Get nested value from object using dot notation path
*/
function getNestedValue(obj: any, path: string): any {
if (!obj || !path) return undefined
return path.split('.').reduce((current, key) => {
return current && typeof current === 'object' ? current[key] : undefined
}, obj)
}
/**
* Escape special regex characters in a string
*/
@@ -76,6 +130,8 @@ export async function POST(req: NextRequest) {
params = {},
timeout = 5000,
envVars = {},
blockData = {},
blockNameMapping = {},
workflowId,
isCustomTool = false,
} = body
@@ -93,7 +149,13 @@ export async function POST(req: NextRequest) {
})
// Resolve variables in the code with workflow environment variables
const { resolvedCode, contextVariables } = resolveCodeVariables(code, executionParams, envVars)
const { resolvedCode, contextVariables } = resolveCodeVariables(
code,
executionParams,
envVars,
blockData,
blockNameMapping
)
const executionMethod = 'vm' // Default execution method

View File

@@ -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<DocumentData[]>([])
const [error, setError] = useState<string | null>(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(() => {

View File

@@ -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<string>('')
const [_fileInfo, setFileInfo] = useState<FileInfo | ConfluenceFileInfo | null>(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 || []}

View File

@@ -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) {

View File

@@ -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<KnowledgeBaseData[]>([])
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))

View File

@@ -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<string>('')
const [_projectInfo, setProjectInfo] = useState<JiraProjectInfo | DiscordServerInfo | null>(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)
}
}

View File

@@ -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 })

View File

@@ -77,6 +77,8 @@ describe('FunctionBlockHandler', () => {
code: inputs.code,
timeout: inputs.timeout,
envVars: {},
blockData: {},
blockNameMapping: {},
_context: { workflowId: mockContext.workflowId },
}
const expectedOutput: BlockOutput = { response: { result: 'Success' } }
@@ -100,6 +102,8 @@ describe('FunctionBlockHandler', () => {
code: expectedCode,
timeout: inputs.timeout,
envVars: {},
blockData: {},
blockNameMapping: {},
_context: { workflowId: mockContext.workflowId },
}
const expectedOutput: BlockOutput = { response: { result: 'Success' } }
@@ -116,6 +120,8 @@ describe('FunctionBlockHandler', () => {
code: inputs.code,
timeout: 5000, // Default timeout
envVars: {},
blockData: {},
blockNameMapping: {},
_context: { workflowId: mockContext.workflowId },
}

View File

@@ -23,12 +23,29 @@ export class FunctionBlockHandler implements BlockHandler {
? inputs.code.map((c: { content: string }) => c.content).join('\n')
: inputs.code
// Extract block data for variable resolution
const blockData: Record<string, any> = {}
const blockNameMapping: Record<string, string> = {}
for (const [blockId, blockState] of context.blockStates.entries()) {
if (blockState.output) {
blockData[blockId] = blockState.output
// Try to find the block name from the workflow
const workflowBlock = context.workflow?.blocks?.find((b) => b.id === blockId)
if (workflowBlock?.metadata?.name) {
blockNameMapping[workflowBlock.metadata.name] = blockId
}
}
}
// Directly use the function_execute tool which calls the API route
logger.info(`Executing function block via API route: ${block.id}`)
const result = await executeTool('function_execute', {
code: codeContent,
timeout: inputs.timeout || 5000,
envVars: context.environmentVariables || {},
blockData: blockData, // Pass block data for variable resolution
blockNameMapping: blockNameMapping, // Pass block name to ID mapping
_context: { workflowId: context.workflowId },
})

View File

@@ -593,6 +593,7 @@ export class InputResolver {
isInTemplateLiteral
)
} else {
// The function execution API will handle variable resolution within code strings
formattedValue =
typeof replacementValue === 'object'
? JSON.stringify(replacementValue)

View File

@@ -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<string, any>,
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) {

View File

@@ -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

View File

@@ -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')

View File

@@ -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 }

View File

@@ -50,6 +50,8 @@ describe('Function Execute Tool', () => {
expect(body).toEqual({
code: 'return 42',
envVars: {},
blockData: {},
blockNameMapping: {},
isCustomTool: false,
timeout: 5000,
workflowId: undefined,
@@ -73,6 +75,8 @@ describe('Function Execute Tool', () => {
code: 'const x = 40;\nconst y = 2;\nreturn x + y;',
timeout: 10000,
envVars: {},
blockData: {},
blockNameMapping: {},
isCustomTool: false,
workflowId: undefined,
})
@@ -87,6 +91,8 @@ describe('Function Execute Tool', () => {
code: 'return 42',
timeout: 10000,
envVars: {},
blockData: {},
blockNameMapping: {},
isCustomTool: false,
workflowId: undefined,
})

View File

@@ -28,6 +28,18 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
description: 'Environment variables to make available during execution',
default: {},
},
blockData: {
type: 'object',
required: false,
description: 'Block output data for variable resolution',
default: {},
},
blockNameMapping: {
type: 'object',
required: false,
description: 'Mapping of block names to block IDs',
default: {},
},
},
request: {
@@ -45,6 +57,8 @@ export const functionExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOu
code: codeContent,
timeout: params.timeout || DEFAULT_TIMEOUT,
envVars: params.envVars || {},
blockData: params.blockData || {},
blockNameMapping: params.blockNameMapping || {},
workflowId: params._context?.workflowId,
isCustomTool: params.isCustomTool || false,
}

View File

@@ -5,6 +5,8 @@ export interface CodeExecutionInput {
timeout?: number
memoryLimit?: number
envVars?: Record<string, string>
blockData?: Record<string, any>
blockNameMapping?: Record<string, string>
_context?: {
workflowId?: string
}