mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 23:48:09 -05:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4e627a9f7 | ||
|
|
b0c1547198 | ||
|
|
d19632aec3 | ||
|
|
35ac68f579 | ||
|
|
9c14f5f8fc | ||
|
|
d50db1d3fb |
@@ -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
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 || []}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user