mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 15:38:00 -05:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4e627a9f7 | ||
|
|
b0c1547198 | ||
|
|
d19632aec3 | ||
|
|
35ac68f579 | ||
|
|
9c14f5f8fc | ||
|
|
d50db1d3fb | ||
|
|
b3960ad77a |
@@ -4,6 +4,7 @@ import { createLogger } from '@/lib/logs/console-logger'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
export const maxDuration = 60
|
||||
|
||||
const logger = createLogger('FunctionExecuteAPI')
|
||||
|
||||
@@ -14,45 +15,16 @@ const logger = createLogger('FunctionExecuteAPI')
|
||||
* @param envVars - Environment variables from the workflow
|
||||
* @returns Resolved code
|
||||
*/
|
||||
/**
|
||||
* Safely serialize a value to JSON string with proper escaping
|
||||
* This prevents JavaScript syntax errors when the serialized data is injected into code
|
||||
*/
|
||||
function safeJSONStringify(value: any): string {
|
||||
try {
|
||||
// Use JSON.stringify with proper escaping
|
||||
// The key is to let JSON.stringify handle the escaping properly
|
||||
return JSON.stringify(value)
|
||||
} catch (error) {
|
||||
// If JSON.stringify fails (e.g., circular references), return a safe fallback
|
||||
try {
|
||||
// Try to create a safe representation by removing circular references
|
||||
const seen = new WeakSet()
|
||||
const cleanValue = JSON.parse(
|
||||
JSON.stringify(value, (key, val) => {
|
||||
if (typeof val === 'object' && val !== null) {
|
||||
if (seen.has(val)) {
|
||||
return '[Circular Reference]'
|
||||
}
|
||||
seen.add(val)
|
||||
}
|
||||
return val
|
||||
})
|
||||
)
|
||||
return JSON.stringify(cleanValue)
|
||||
} catch {
|
||||
// If that also fails, return a safe string representation
|
||||
return JSON.stringify(String(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCodeVariables(
|
||||
code: string,
|
||||
params: Record<string, any>,
|
||||
envVars: Record<string, 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> = {}
|
||||
|
||||
// Resolve environment variables with {{var_name}} syntax
|
||||
const envVarMatches = resolvedCode.match(/\{\{([^}]+)\}\}/g) || []
|
||||
@@ -60,25 +32,82 @@ function resolveCodeVariables(
|
||||
const varName = match.slice(2, -2).trim()
|
||||
// Priority: 1. Environment variables from workflow, 2. Params
|
||||
const varValue = envVars[varName] || params[varName] || ''
|
||||
// Use safe JSON stringify to prevent syntax errors
|
||||
resolvedCode = resolvedCode.replace(
|
||||
new RegExp(escapeRegExp(match), 'g'),
|
||||
safeJSONStringify(varValue)
|
||||
)
|
||||
|
||||
// Instead of injecting large JSON directly, create a variable reference
|
||||
const safeVarName = `__var_${varName.replace(/[^a-zA-Z0-9_]/g, '_')}`
|
||||
contextVariables[safeVarName] = varValue
|
||||
|
||||
// Replace the template with a variable reference
|
||||
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] || ''
|
||||
resolvedCode = resolvedCode.replace(
|
||||
new RegExp(escapeRegExp(match), 'g'),
|
||||
safeJSONStringify(tagValue)
|
||||
)
|
||||
|
||||
// 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, '_')}`
|
||||
contextVariables[safeVarName] = tagValue
|
||||
|
||||
// Replace the template with a variable reference
|
||||
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
|
||||
}
|
||||
|
||||
return resolvedCode
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,6 +130,8 @@ export async function POST(req: NextRequest) {
|
||||
params = {},
|
||||
timeout = 5000,
|
||||
envVars = {},
|
||||
blockData = {},
|
||||
blockNameMapping = {},
|
||||
workflowId,
|
||||
isCustomTool = false,
|
||||
} = body
|
||||
@@ -118,7 +149,13 @@ export async function POST(req: NextRequest) {
|
||||
})
|
||||
|
||||
// Resolve variables in the code with workflow environment variables
|
||||
const resolvedCode = resolveCodeVariables(code, executionParams, envVars)
|
||||
const { resolvedCode, contextVariables } = resolveCodeVariables(
|
||||
code,
|
||||
executionParams,
|
||||
envVars,
|
||||
blockData,
|
||||
blockNameMapping
|
||||
)
|
||||
|
||||
const executionMethod = 'vm' // Default execution method
|
||||
|
||||
@@ -280,6 +317,7 @@ export async function POST(req: NextRequest) {
|
||||
const context = createContext({
|
||||
params: executionParams,
|
||||
environmentVariables: envVars,
|
||||
...contextVariables, // Add resolved variables directly to context
|
||||
fetch: globalThis.fetch || require('node-fetch').default,
|
||||
console: {
|
||||
log: (...args: any[]) => {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
interface ConnectionStatusProps {
|
||||
isConnected: boolean
|
||||
}
|
||||
|
||||
export function ConnectionStatus({ isConnected }: ConnectionStatusProps) {
|
||||
const [showOfflineNotice, setShowOfflineNotice] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout
|
||||
|
||||
if (!isConnected) {
|
||||
// Show offline notice after 6 seconds of being disconnected
|
||||
timeoutId = setTimeout(() => {
|
||||
setShowOfflineNotice(true)
|
||||
}, 6000) // 6 seconds
|
||||
} else {
|
||||
// Hide notice immediately when reconnected
|
||||
setShowOfflineNotice(false)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
}, [isConnected])
|
||||
|
||||
// Don't render anything if connected or if we haven't been disconnected long enough
|
||||
if (!showOfflineNotice) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<div className='flex items-center gap-1.5 text-red-600'>
|
||||
<div className='relative flex items-center justify-center'>
|
||||
<div className='absolute h-3 w-3 animate-ping rounded-full bg-red-500/20' />
|
||||
<div className='relative h-2 w-2 rounded-full bg-red-500' />
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<span className='font-medium text-xs leading-tight'>Connection lost</span>
|
||||
<span className='text-xs leading-tight opacity-90'>
|
||||
Changes not saved - please refresh
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { usePresence } from '../../../../hooks/use-presence'
|
||||
import { ConnectionStatus } from './components/connection-status/connection-status'
|
||||
import { UserAvatar } from './components/user-avatar/user-avatar'
|
||||
|
||||
interface User {
|
||||
@@ -25,7 +26,7 @@ export function UserAvatarStack({
|
||||
className = '',
|
||||
}: UserAvatarStackProps) {
|
||||
// Use presence data if no users are provided via props
|
||||
const { users: presenceUsers } = usePresence()
|
||||
const { users: presenceUsers, isConnected } = usePresence()
|
||||
const users = propUsers || presenceUsers
|
||||
|
||||
// Memoize the processed users to avoid unnecessary re-renders
|
||||
@@ -43,10 +44,14 @@ export function UserAvatarStack({
|
||||
}
|
||||
}, [users, maxVisible])
|
||||
|
||||
// Show connection status component regardless of user count
|
||||
// This will handle the offline notice when disconnected for 15 seconds
|
||||
const connectionStatusElement = <ConnectionStatus isConnected={isConnected} />
|
||||
|
||||
// Only show presence when there are multiple users (>1)
|
||||
// Don't render anything if there are no users or only 1 user
|
||||
// But always show connection status
|
||||
if (users.length <= 1) {
|
||||
return null
|
||||
return connectionStatusElement
|
||||
}
|
||||
|
||||
// Determine spacing based on size
|
||||
@@ -58,6 +63,9 @@ export function UserAvatarStack({
|
||||
|
||||
return (
|
||||
<div className={`flex items-center ${spacingClass} ${className}`}>
|
||||
{/* Connection status - always present */}
|
||||
{connectionStatusElement}
|
||||
|
||||
{/* Render visible user avatars */}
|
||||
{visibleUsers.map((user, index) => (
|
||||
<UserAvatar
|
||||
|
||||
@@ -683,7 +683,6 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<UserAvatarStack className='ml-3' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1275,8 +1274,10 @@ export function ControlBar({ hasValidationErrors = false }: ControlBarProps) {
|
||||
{/* Left Section - Workflow Info */}
|
||||
<div className='pl-4'>{renderWorkflowName()}</div>
|
||||
|
||||
{/* Middle Section - Reserved for future use */}
|
||||
<div className='flex-1' />
|
||||
{/* Middle Section - Connection Status */}
|
||||
<div className='flex flex-1 justify-center'>
|
||||
<UserAvatarStack />
|
||||
</div>
|
||||
|
||||
{/* Right Section - Actions */}
|
||||
<div className='flex items-center gap-1 pr-4'>
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
|
||||
|
||||
@@ -44,9 +44,7 @@ export class LoopBlockHandler implements BlockHandler {
|
||||
}
|
||||
|
||||
const currentIteration = context.loopIterations.get(block.id) || 0
|
||||
let maxIterations = loop.iterations || DEFAULT_MAX_ITERATIONS
|
||||
|
||||
// For forEach loops, we need to check the actual items length
|
||||
let maxIterations: number
|
||||
let forEachItems: any[] | Record<string, any> | null = null
|
||||
if (loop.loopType === 'forEach') {
|
||||
if (
|
||||
@@ -71,14 +69,19 @@ export class LoopBlockHandler implements BlockHandler {
|
||||
)
|
||||
}
|
||||
|
||||
// Adjust max iterations based on actual items
|
||||
// For forEach, max iterations = items length
|
||||
const itemsLength = Array.isArray(forEachItems)
|
||||
? forEachItems.length
|
||||
: Object.keys(forEachItems).length
|
||||
maxIterations = Math.min(maxIterations, itemsLength)
|
||||
|
||||
maxIterations = itemsLength
|
||||
|
||||
logger.info(
|
||||
`Loop ${block.id} max iterations set to ${maxIterations} based on ${itemsLength} items`
|
||||
`forEach loop ${block.id} - Items: ${itemsLength}, Max iterations: ${maxIterations}`
|
||||
)
|
||||
} else {
|
||||
maxIterations = loop.iterations || DEFAULT_MAX_ITERATIONS
|
||||
logger.info(`For loop ${block.id} - Max iterations: ${maxIterations}`)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -81,7 +81,7 @@ export class LoopManager {
|
||||
// Determine the maximum iterations
|
||||
let maxIterations = loop.iterations || this.defaultIterations
|
||||
|
||||
// For forEach loops, check the actual items length
|
||||
// For forEach loops, use the actual items length
|
||||
if (loop.loopType === 'forEach' && loop.forEachItems) {
|
||||
// First check if the items have already been evaluated and stored by the loop handler
|
||||
const storedItems = context.loopItems.get(`${loopId}_items`)
|
||||
@@ -89,15 +89,18 @@ export class LoopManager {
|
||||
const itemsLength = Array.isArray(storedItems)
|
||||
? storedItems.length
|
||||
: Object.keys(storedItems).length
|
||||
maxIterations = Math.min(maxIterations, itemsLength)
|
||||
|
||||
maxIterations = itemsLength
|
||||
logger.info(
|
||||
`Loop ${loopId} using stored items length: ${itemsLength} (max iterations: ${maxIterations})`
|
||||
`forEach loop ${loopId} - Items: ${itemsLength}, Max iterations: ${maxIterations}`
|
||||
)
|
||||
} else {
|
||||
// Fallback to parsing the forEachItems string if it's not a reference
|
||||
const itemsLength = this.getItemsLength(loop.forEachItems)
|
||||
if (itemsLength > 0) {
|
||||
maxIterations = Math.min(maxIterations, itemsLength)
|
||||
maxIterations = itemsLength
|
||||
logger.info(
|
||||
`forEach loop ${loopId} - Parsed items: ${itemsLength}, Max iterations: ${maxIterations}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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