mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-10 07:27:57 -05:00
Compare commits
1 Commits
dependabot
...
feat/while
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f450c4f8e |
@@ -143,6 +143,7 @@ export const sampleWorkflowState = {
|
||||
],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
whiles: {},
|
||||
lastSaved: Date.now(),
|
||||
isDeployed: false,
|
||||
}
|
||||
|
||||
@@ -420,7 +420,7 @@ export async function executeWorkflowForChat(
|
||||
|
||||
// Use deployed state for chat execution (this is the stable, deployed version)
|
||||
const deployedState = workflowResult[0].deployedState as WorkflowState
|
||||
const { blocks, edges, loops, parallels } = deployedState
|
||||
const { blocks, edges, loops, parallels, whiles } = deployedState
|
||||
|
||||
// Prepare for execution, similar to use-workflow-execution.ts
|
||||
const mergedStates = mergeSubblockState(blocks)
|
||||
@@ -497,6 +497,7 @@ export async function executeWorkflowForChat(
|
||||
filteredEdges,
|
||||
loops,
|
||||
parallels,
|
||||
whiles,
|
||||
true // Enable validation during execution
|
||||
)
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ export async function POST(request: NextRequest) {
|
||||
edges: checkpointState?.edges || [],
|
||||
loops: checkpointState?.loops || {},
|
||||
parallels: checkpointState?.parallels || {},
|
||||
whiles: checkpointState?.whiles || {},
|
||||
isDeployed: checkpointState?.isDeployed || false,
|
||||
deploymentStatuses: checkpointState?.deploymentStatuses || {},
|
||||
hasActiveWebhook: checkpointState?.hasActiveWebhook || false,
|
||||
|
||||
@@ -23,6 +23,7 @@ describe('Scheduled Workflow Execution API Route', () => {
|
||||
edges: sampleWorkflowState.edges || [],
|
||||
loops: sampleWorkflowState.loops || {},
|
||||
parallels: {},
|
||||
whiles: {},
|
||||
isFromNormalizedTables: true,
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -230,6 +230,7 @@ export async function GET() {
|
||||
const edges = normalizedData.edges
|
||||
const loops = normalizedData.loops
|
||||
const parallels = normalizedData.parallels
|
||||
const whiles = normalizedData.whiles
|
||||
logger.info(
|
||||
`[${requestId}] Loaded scheduled workflow ${schedule.workflowId} from normalized tables`
|
||||
)
|
||||
@@ -384,6 +385,7 @@ export async function GET() {
|
||||
edges,
|
||||
loops,
|
||||
parallels,
|
||||
whiles,
|
||||
true // Enable validation during execution
|
||||
)
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ const CreateTemplateSchema = z.object({
|
||||
edges: z.array(z.any()),
|
||||
loops: z.record(z.any()),
|
||||
parallels: z.record(z.any()),
|
||||
whiles: z.record(z.any()),
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@@ -153,6 +153,7 @@ describe('Webhook Trigger API Route', () => {
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
whiles: {},
|
||||
isFromNormalizedTables: true,
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -11,7 +11,11 @@ import type { BlockConfig } from '@/blocks/types'
|
||||
import { resolveOutputType } from '@/blocks/utils'
|
||||
import { db } from '@/db'
|
||||
import { workflow as workflowTable } from '@/db/schema'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
import {
|
||||
generateLoopBlocks,
|
||||
generateParallelBlocks,
|
||||
generateWhileBlocks,
|
||||
} from '@/stores/workflows/workflow/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
@@ -125,6 +129,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
edges: currentWorkflowData.edges,
|
||||
loops: currentWorkflowData.loops || {},
|
||||
parallels: currentWorkflowData.parallels || {},
|
||||
whiles: currentWorkflowData.whiles || {},
|
||||
}
|
||||
|
||||
const autoLayoutOptions = {
|
||||
@@ -166,6 +171,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
utilities: {
|
||||
generateLoopBlocks: generateLoopBlocks.toString(),
|
||||
generateParallelBlocks: generateParallelBlocks.toString(),
|
||||
generateWhileBlocks: generateWhileBlocks.toString(),
|
||||
resolveOutputType: resolveOutputType.toString(),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -69,6 +69,7 @@ describe('Workflow Deployment API Route', () => {
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
whiles: {},
|
||||
isFromNormalizedTables: true,
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -109,6 +109,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
edges: normalizedData.edges,
|
||||
loops: normalizedData.loops,
|
||||
parallels: normalizedData.parallels,
|
||||
whiles: normalizedData.whiles,
|
||||
}
|
||||
|
||||
const { hasWorkflowChanged } = await import('@/lib/workflows/utils')
|
||||
@@ -192,6 +193,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
const blocksMap: Record<string, any> = {}
|
||||
const loops: Record<string, any> = {}
|
||||
const parallels: Record<string, any> = {}
|
||||
const whiles: Record<string, any> = {}
|
||||
|
||||
// Process blocks
|
||||
blocks.forEach((block) => {
|
||||
@@ -206,7 +208,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
}
|
||||
})
|
||||
|
||||
// Process subflows (loops and parallels)
|
||||
// Process subflows (loops, parallels, and whiles)
|
||||
subflows.forEach((subflow) => {
|
||||
const config = (subflow.config as any) || {}
|
||||
if (subflow.type === 'loop') {
|
||||
@@ -225,6 +227,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
distribution: config.distribution || '',
|
||||
parallelType: config.parallelType || 'count',
|
||||
}
|
||||
} else if (subflow.type === 'while') {
|
||||
whiles[subflow.id] = {
|
||||
id: subflow.id,
|
||||
nodes: config.nodes || [],
|
||||
iterations: config.iterations || 1,
|
||||
whileType: config.whileType || 'while',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -244,6 +253,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
edges: edgesArray,
|
||||
loops,
|
||||
parallels,
|
||||
whiles,
|
||||
lastSaved: Date.now(),
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getUserEntityPermissions } from '@/lib/permissions/utils'
|
||||
import { db } from '@/db'
|
||||
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@/db/schema'
|
||||
import type { LoopConfig, ParallelConfig } from '@/stores/workflows/workflow/types'
|
||||
import type { LoopConfig, ParallelConfig, WhileConfig } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('WorkflowDuplicateAPI')
|
||||
|
||||
@@ -209,16 +209,16 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
})
|
||||
|
||||
// Update block references in subflow config
|
||||
let updatedConfig: LoopConfig | ParallelConfig = subflow.config as
|
||||
let updatedConfig: LoopConfig | ParallelConfig | WhileConfig = subflow.config as
|
||||
| LoopConfig
|
||||
| ParallelConfig
|
||||
| WhileConfig
|
||||
if (subflow.config && typeof subflow.config === 'object') {
|
||||
updatedConfig = JSON.parse(JSON.stringify(subflow.config)) as
|
||||
| LoopConfig
|
||||
| ParallelConfig
|
||||
|
||||
| WhileConfig
|
||||
// Update the config ID to match the new subflow ID
|
||||
|
||||
;(updatedConfig as any).id = newSubflowId
|
||||
|
||||
// Update node references in config if they exist
|
||||
|
||||
@@ -121,6 +121,7 @@ describe('Workflow Execution API Route', () => {
|
||||
],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
whiles: {},
|
||||
isFromNormalizedTables: false, // Changed to false since it's from deployed state
|
||||
}),
|
||||
}))
|
||||
@@ -559,6 +560,7 @@ describe('Workflow Execution API Route', () => {
|
||||
],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
whiles: {},
|
||||
isFromNormalizedTables: false, // Changed to false since it's from deployed state
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -115,13 +115,14 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any): P
|
||||
const deployedData = await loadDeployedWorkflowState(workflowId)
|
||||
|
||||
// Use deployed data as primary source for API executions
|
||||
const { blocks, edges, loops, parallels } = deployedData
|
||||
const { blocks, edges, loops, parallels, whiles } = deployedData
|
||||
logger.info(`[${requestId}] Using deployed state for workflow execution: ${workflowId}`)
|
||||
logger.debug(`[${requestId}] Deployed data loaded:`, {
|
||||
blocksCount: Object.keys(blocks || {}).length,
|
||||
edgesCount: (edges || []).length,
|
||||
loopsCount: Object.keys(loops || {}).length,
|
||||
parallelsCount: Object.keys(parallels || {}).length,
|
||||
whilesCount: Object.keys(whiles || {}).length,
|
||||
})
|
||||
|
||||
// Use the same execution flow as in scheduled executions
|
||||
@@ -275,6 +276,7 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any): P
|
||||
edges,
|
||||
loops,
|
||||
parallels,
|
||||
whiles,
|
||||
true // Enable validation during execution
|
||||
)
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
edgesCount: deployedState.edges.length,
|
||||
loopsCount: Object.keys(deployedState.loops || {}).length,
|
||||
parallelsCount: Object.keys(deployedState.parallels || {}).length,
|
||||
whilesCount: Object.keys(deployedState.whiles || {}).length,
|
||||
})
|
||||
|
||||
// Save deployed state to normalized tables
|
||||
@@ -60,6 +61,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
edges: deployedState.edges,
|
||||
loops: deployedState.loops || {},
|
||||
parallels: deployedState.parallels || {},
|
||||
whiles: deployedState.whiles || {},
|
||||
lastSaved: Date.now(),
|
||||
isDeployed: workflowData.isDeployed,
|
||||
deployedAt: workflowData.deployedAt,
|
||||
|
||||
@@ -96,6 +96,7 @@ describe('Workflow By ID API Route', () => {
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
whiles: {},
|
||||
isFromNormalizedTables: true,
|
||||
}
|
||||
|
||||
@@ -145,6 +146,7 @@ describe('Workflow By ID API Route', () => {
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
whiles: {},
|
||||
isFromNormalizedTables: true,
|
||||
}
|
||||
|
||||
@@ -241,6 +243,7 @@ describe('Workflow By ID API Route', () => {
|
||||
edges: [{ id: 'edge-1', source: 'block-1', target: 'block-2' }],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
whiles: {},
|
||||
isFromNormalizedTables: true,
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
edgesCount: normalizedData.edges.length,
|
||||
loopsCount: Object.keys(normalizedData.loops).length,
|
||||
parallelsCount: Object.keys(normalizedData.parallels).length,
|
||||
whilesCount: Object.keys(normalizedData.whiles).length,
|
||||
loops: normalizedData.loops,
|
||||
})
|
||||
|
||||
@@ -141,6 +142,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
edges: normalizedData.edges,
|
||||
loops: normalizedData.loops,
|
||||
parallels: normalizedData.parallels,
|
||||
whiles: normalizedData.whiles,
|
||||
lastSaved: Date.now(),
|
||||
isDeployed: workflowData.isDeployed || false,
|
||||
deployedAt: workflowData.deployedAt,
|
||||
|
||||
@@ -24,6 +24,7 @@ const BlockDataSchema = z.object({
|
||||
count: z.number().optional(),
|
||||
loopType: z.enum(['for', 'forEach']).optional(),
|
||||
parallelType: z.enum(['collection', 'count']).optional(),
|
||||
whileType: z.enum(['while', 'doWhile']).optional(),
|
||||
type: z.string().optional(),
|
||||
})
|
||||
|
||||
@@ -87,6 +88,13 @@ const ParallelSchema = z.object({
|
||||
parallelType: z.enum(['count', 'collection']).optional(),
|
||||
})
|
||||
|
||||
const WhileSchema = z.object({
|
||||
id: z.string(),
|
||||
nodes: z.array(z.string()),
|
||||
iterations: z.number(),
|
||||
whileType: z.enum(['while', 'doWhile']),
|
||||
})
|
||||
|
||||
const DeploymentStatusSchema = z.object({
|
||||
id: z.string(),
|
||||
status: z.enum(['deploying', 'deployed', 'failed', 'stopping', 'stopped']),
|
||||
@@ -99,6 +107,7 @@ const WorkflowStateSchema = z.object({
|
||||
edges: z.array(EdgeSchema),
|
||||
loops: z.record(LoopSchema).optional(),
|
||||
parallels: z.record(ParallelSchema).optional(),
|
||||
whiles: z.record(WhileSchema).optional(),
|
||||
lastSaved: z.number().optional(),
|
||||
isDeployed: z.boolean().optional(),
|
||||
deployedAt: z.date().optional(),
|
||||
@@ -197,6 +206,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
edges: state.edges,
|
||||
loops: state.loops || {},
|
||||
parallels: state.parallels || {},
|
||||
whiles: state.whiles || {},
|
||||
lastSaved: state.lastSaved || Date.now(),
|
||||
isDeployed: state.isDeployed || false,
|
||||
deployedAt: state.deployedAt,
|
||||
@@ -231,6 +241,9 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
success: true,
|
||||
blocksCount: Object.keys(filteredBlocks).length,
|
||||
edgesCount: state.edges.length,
|
||||
loopsCount: Object.keys(state.loops || {}).length,
|
||||
parallelsCount: Object.keys(state.parallels || {}).length,
|
||||
whilesCount: Object.keys(state.whiles || {}).length,
|
||||
},
|
||||
{ status: 200 }
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
const blocksMap: Record<string, any> = {}
|
||||
const loops: Record<string, any> = {}
|
||||
const parallels: Record<string, any> = {}
|
||||
|
||||
const whiles: Record<string, any> = {}
|
||||
// Process blocks
|
||||
blocks.forEach((block) => {
|
||||
blocksMap[block.id] = {
|
||||
@@ -71,6 +71,13 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
distribution: config.distribution || '',
|
||||
parallelType: config.parallelType || 'count',
|
||||
}
|
||||
} else if (subflow.type === 'while') {
|
||||
whiles[subflow.id] = {
|
||||
id: subflow.id,
|
||||
nodes: config.nodes || [],
|
||||
iterations: config.iterations || 1,
|
||||
whileType: config.whileType || 'while',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -90,6 +97,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
edges: edgesArray,
|
||||
loops,
|
||||
parallels,
|
||||
whiles,
|
||||
lastSaved: Date.now(),
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,11 @@ import type { BlockConfig } from '@/blocks/types'
|
||||
import { resolveOutputType } from '@/blocks/utils'
|
||||
import { db } from '@/db'
|
||||
import { workflowCheckpoints, workflow as workflowTable } from '@/db/schema'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
import {
|
||||
generateLoopBlocks,
|
||||
generateParallelBlocks,
|
||||
generateWhileBlocks,
|
||||
} from '@/stores/workflows/workflow/utils'
|
||||
|
||||
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
|
||||
|
||||
@@ -80,6 +84,7 @@ async function createWorkflowCheckpoint(
|
||||
generateLoopBlocks: generateLoopBlocks.toString(),
|
||||
generateParallelBlocks: generateParallelBlocks.toString(),
|
||||
resolveOutputType: resolveOutputType.toString(),
|
||||
generateWhileBlocks: generateWhileBlocks.toString(),
|
||||
},
|
||||
}),
|
||||
})
|
||||
@@ -293,6 +298,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
generateLoopBlocks: generateLoopBlocks.toString(),
|
||||
generateParallelBlocks: generateParallelBlocks.toString(),
|
||||
resolveOutputType: resolveOutputType.toString(),
|
||||
generateWhileBlocks: generateWhileBlocks.toString(),
|
||||
},
|
||||
options: {
|
||||
generateNewIds: false, // We'll handle ID generation manually for now
|
||||
@@ -373,6 +379,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
edges: [] as any[],
|
||||
loops: {} as Record<string, any>,
|
||||
parallels: {} as Record<string, any>,
|
||||
whiles: {} as Record<string, any>,
|
||||
lastSaved: Date.now(),
|
||||
isDeployed: false,
|
||||
deployedAt: undefined,
|
||||
@@ -391,7 +398,10 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
// Get block configuration for proper setup
|
||||
const blockConfig = getBlock(block.type)
|
||||
|
||||
if (!blockConfig && (block.type === 'loop' || block.type === 'parallel')) {
|
||||
if (
|
||||
!blockConfig &&
|
||||
(block.type === 'loop' || block.type === 'parallel' || block.type === 'while')
|
||||
) {
|
||||
// Handle loop/parallel blocks (they don't have regular block configs)
|
||||
// Preserve parentId if it exists (though loop/parallel shouldn't have parents)
|
||||
const containerData = block.data || {}
|
||||
@@ -414,7 +424,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
height: 0,
|
||||
data: containerData,
|
||||
}
|
||||
logger.debug(`[${requestId}] Processed loop/parallel block: ${block.id} -> ${newId}`)
|
||||
logger.debug(`[${requestId}] Processed loop/parallel/while block: ${block.id} -> ${newId}`)
|
||||
} else if (blockConfig) {
|
||||
// Handle regular blocks with proper configuration
|
||||
const subBlocks: Record<string, any> = {}
|
||||
@@ -545,14 +555,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
// Generate loop and parallel configurations
|
||||
const loops = generateLoopBlocks(newWorkflowState.blocks)
|
||||
const parallels = generateParallelBlocks(newWorkflowState.blocks)
|
||||
const whiles = generateWhileBlocks(newWorkflowState.blocks)
|
||||
newWorkflowState.loops = loops
|
||||
newWorkflowState.parallels = parallels
|
||||
newWorkflowState.whiles = whiles
|
||||
|
||||
logger.info(`[${requestId}] Generated workflow state`, {
|
||||
blocksCount: Object.keys(newWorkflowState.blocks).length,
|
||||
edgesCount: newWorkflowState.edges.length,
|
||||
loopsCount: Object.keys(loops).length,
|
||||
parallelsCount: Object.keys(parallels).length,
|
||||
whilesCount: Object.keys(whiles).length,
|
||||
})
|
||||
|
||||
// Apply intelligent autolayout if requested
|
||||
@@ -566,6 +579,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
edges: newWorkflowState.edges,
|
||||
loops: newWorkflowState.loops || {},
|
||||
parallels: newWorkflowState.parallels || {},
|
||||
whiles: newWorkflowState.whiles || {},
|
||||
}
|
||||
|
||||
const autoLayoutOptions = {
|
||||
@@ -608,6 +622,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
generateLoopBlocks: generateLoopBlocks.toString(),
|
||||
generateParallelBlocks: generateParallelBlocks.toString(),
|
||||
resolveOutputType: resolveOutputType.toString(),
|
||||
generateWhileBlocks: generateWhileBlocks.toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -685,6 +700,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
edgesCount: newWorkflowState.edges.length,
|
||||
loopsCount: Object.keys(loops).length,
|
||||
parallelsCount: Object.keys(parallels).length,
|
||||
whilesCount: Object.keys(whiles).length,
|
||||
},
|
||||
errors: [],
|
||||
warnings,
|
||||
|
||||
@@ -4,7 +4,11 @@ import { simAgentClient } from '@/lib/sim-agent'
|
||||
import { getAllBlocks } from '@/blocks/registry'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { resolveOutputType } from '@/blocks/utils'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
import {
|
||||
generateLoopBlocks,
|
||||
generateParallelBlocks,
|
||||
generateWhileBlocks,
|
||||
} from '@/stores/workflows/workflow/utils'
|
||||
|
||||
const logger = createLogger('WorkflowYamlAPI')
|
||||
|
||||
@@ -50,6 +54,7 @@ export async function POST(request: NextRequest) {
|
||||
generateLoopBlocks: generateLoopBlocks.toString(),
|
||||
generateParallelBlocks: generateParallelBlocks.toString(),
|
||||
resolveOutputType: resolveOutputType.toString(),
|
||||
generateWhileBlocks: generateWhileBlocks.toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -10,7 +10,11 @@ import type { BlockConfig } from '@/blocks/types'
|
||||
import { resolveOutputType } from '@/blocks/utils'
|
||||
import { db } from '@/db'
|
||||
import { workflow } from '@/db/schema'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
import {
|
||||
generateLoopBlocks,
|
||||
generateParallelBlocks,
|
||||
generateWhileBlocks,
|
||||
} from '@/stores/workflows/workflow/utils'
|
||||
|
||||
const logger = createLogger('WorkflowYamlExportAPI')
|
||||
|
||||
@@ -144,6 +148,7 @@ export async function GET(request: NextRequest) {
|
||||
generateLoopBlocks: generateLoopBlocks.toString(),
|
||||
generateParallelBlocks: generateParallelBlocks.toString(),
|
||||
resolveOutputType: resolveOutputType.toString(),
|
||||
generateWhileBlocks: generateWhileBlocks.toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -9,10 +9,12 @@ import { resolveOutputType } from '@/blocks/utils'
|
||||
import {
|
||||
convertLoopBlockToLoop,
|
||||
convertParallelBlockToParallel,
|
||||
convertWhileBlockToWhile,
|
||||
findAllDescendantNodes,
|
||||
findChildNodes,
|
||||
generateLoopBlocks,
|
||||
generateParallelBlocks,
|
||||
generateWhileBlocks,
|
||||
} from '@/stores/workflows/workflow/utils'
|
||||
|
||||
const logger = createLogger('YamlAutoLayoutAPI')
|
||||
@@ -26,6 +28,7 @@ const AutoLayoutRequestSchema = z.object({
|
||||
edges: z.array(z.any()),
|
||||
loops: z.record(z.any()).optional().default({}),
|
||||
parallels: z.record(z.any()).optional().default({}),
|
||||
whiles: z.record(z.any()).optional().default({}),
|
||||
}),
|
||||
options: z
|
||||
.object({
|
||||
@@ -36,6 +39,7 @@ const AutoLayoutRequestSchema = z.object({
|
||||
horizontal: z.number().optional(),
|
||||
vertical: z.number().optional(),
|
||||
layer: z.number().optional(),
|
||||
while: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
alignment: z.enum(['start', 'center', 'end']).optional(),
|
||||
@@ -45,6 +49,12 @@ const AutoLayoutRequestSchema = z.object({
|
||||
y: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
while: z
|
||||
.object({
|
||||
x: z.number().optional(),
|
||||
y: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
@@ -133,8 +143,10 @@ export async function POST(request: NextRequest) {
|
||||
resolveOutputType: resolveOutputType.toString(),
|
||||
convertLoopBlockToLoop: convertLoopBlockToLoop.toString(),
|
||||
convertParallelBlockToParallel: convertParallelBlockToParallel.toString(),
|
||||
convertWhileBlockToWhile: convertWhileBlockToWhile.toString(),
|
||||
findChildNodes: findChildNodes.toString(),
|
||||
findAllDescendantNodes: findAllDescendantNodes.toString(),
|
||||
generateWhileBlocks: generateWhileBlocks.toString(),
|
||||
},
|
||||
}),
|
||||
})
|
||||
@@ -192,6 +204,7 @@ export async function POST(request: NextRequest) {
|
||||
edges: workflowState.edges || [],
|
||||
loops: workflowState.loops || {},
|
||||
parallels: workflowState.parallels || {},
|
||||
whiles: workflowState.whiles || {},
|
||||
},
|
||||
errors: result.errors,
|
||||
}
|
||||
|
||||
@@ -9,10 +9,12 @@ import { resolveOutputType } from '@/blocks/utils'
|
||||
import {
|
||||
convertLoopBlockToLoop,
|
||||
convertParallelBlockToParallel,
|
||||
convertWhileBlockToWhile,
|
||||
findAllDescendantNodes,
|
||||
findChildNodes,
|
||||
generateLoopBlocks,
|
||||
generateParallelBlocks,
|
||||
generateWhileBlocks,
|
||||
} from '@/stores/workflows/workflow/utils'
|
||||
|
||||
const logger = createLogger('YamlDiffCreateAPI')
|
||||
@@ -130,8 +132,10 @@ export async function POST(request: NextRequest) {
|
||||
resolveOutputType: resolveOutputType.toString(),
|
||||
convertLoopBlockToLoop: convertLoopBlockToLoop.toString(),
|
||||
convertParallelBlockToParallel: convertParallelBlockToParallel.toString(),
|
||||
convertWhileBlockToWhile: convertWhileBlockToWhile.toString(),
|
||||
findChildNodes: findChildNodes.toString(),
|
||||
findAllDescendantNodes: findAllDescendantNodes.toString(),
|
||||
generateWhileBlocks: generateWhileBlocks.toString(),
|
||||
},
|
||||
options,
|
||||
}),
|
||||
@@ -168,7 +172,7 @@ export async function POST(request: NextRequest) {
|
||||
dataKeys: block.data ? Object.keys(block.data) : [],
|
||||
})
|
||||
}
|
||||
if (block.type === 'loop' || block.type === 'parallel') {
|
||||
if (block.type === 'loop' || block.type === 'parallel' || block.type === 'while') {
|
||||
logger.info(`[${requestId}] Container block ${blockId} (${block.name}):`, {
|
||||
type: block.type,
|
||||
hasData: !!block.data,
|
||||
@@ -180,8 +184,10 @@ export async function POST(request: NextRequest) {
|
||||
// Log existing loops/parallels from sim-agent
|
||||
const loops = result.diff?.proposedState?.loops || result.loops || {}
|
||||
const parallels = result.diff?.proposedState?.parallels || result.parallels || {}
|
||||
const whiles = result.diff?.proposedState?.whiles || result.whiles || {}
|
||||
logger.info(`[${requestId}] Sim agent loops:`, loops)
|
||||
logger.info(`[${requestId}] Sim agent parallels:`, parallels)
|
||||
logger.info(`[${requestId}] Sim agent whiles:`, whiles)
|
||||
}
|
||||
|
||||
// Log diff analysis specifically
|
||||
@@ -207,7 +213,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Find all loop and parallel blocks
|
||||
const containerBlocks = Object.values(blocks).filter(
|
||||
(block: any) => block.type === 'loop' || block.type === 'parallel'
|
||||
(block: any) => block.type === 'loop' || block.type === 'parallel' || block.type === 'while'
|
||||
)
|
||||
|
||||
// For each container, find its children based on loop-start edges
|
||||
@@ -251,17 +257,23 @@ export async function POST(request: NextRequest) {
|
||||
// Now regenerate loops and parallels with the fixed relationships
|
||||
const loops = generateLoopBlocks(result.diff.proposedState.blocks)
|
||||
const parallels = generateParallelBlocks(result.diff.proposedState.blocks)
|
||||
|
||||
const whiles = generateWhileBlocks(result.diff.proposedState.blocks)
|
||||
result.diff.proposedState.loops = loops
|
||||
result.diff.proposedState.parallels = parallels
|
||||
result.diff.proposedState.whiles = whiles
|
||||
|
||||
logger.info(`[${requestId}] Regenerated loops and parallels after fixing parent-child:`, {
|
||||
loopsCount: Object.keys(loops).length,
|
||||
parallelsCount: Object.keys(parallels).length,
|
||||
whilesCount: Object.keys(whiles).length,
|
||||
loops: Object.keys(loops).map((id) => ({
|
||||
id,
|
||||
nodes: loops[id].nodes,
|
||||
})),
|
||||
whiles: Object.keys(whiles).map((id) => ({
|
||||
id,
|
||||
nodes: whiles[id].nodes,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -309,7 +321,7 @@ export async function POST(request: NextRequest) {
|
||||
// Generate loops and parallels for the blocks with fixed relationships
|
||||
const loops = generateLoopBlocks(result.blocks)
|
||||
const parallels = generateParallelBlocks(result.blocks)
|
||||
|
||||
const whiles = generateWhileBlocks(result.blocks)
|
||||
const transformedResult = {
|
||||
success: result.success,
|
||||
diff: {
|
||||
@@ -318,6 +330,7 @@ export async function POST(request: NextRequest) {
|
||||
edges: result.edges || [],
|
||||
loops: loops,
|
||||
parallels: parallels,
|
||||
whiles: whiles,
|
||||
},
|
||||
diffAnalysis: diffAnalysis,
|
||||
metadata: result.metadata || {
|
||||
|
||||
@@ -9,10 +9,12 @@ import { resolveOutputType } from '@/blocks/utils'
|
||||
import {
|
||||
convertLoopBlockToLoop,
|
||||
convertParallelBlockToParallel,
|
||||
convertWhileBlockToWhile,
|
||||
findAllDescendantNodes,
|
||||
findChildNodes,
|
||||
generateLoopBlocks,
|
||||
generateParallelBlocks,
|
||||
generateWhileBlocks,
|
||||
} from '@/stores/workflows/workflow/utils'
|
||||
|
||||
const logger = createLogger('YamlDiffMergeAPI')
|
||||
@@ -27,6 +29,7 @@ const MergeDiffRequestSchema = z.object({
|
||||
edges: z.array(z.any()),
|
||||
loops: z.record(z.any()).optional(),
|
||||
parallels: z.record(z.any()).optional(),
|
||||
whiles: z.record(z.any()).optional(),
|
||||
}),
|
||||
diffAnalysis: z.any().optional(),
|
||||
metadata: z.object({
|
||||
@@ -103,6 +106,8 @@ export async function POST(request: NextRequest) {
|
||||
convertParallelBlockToParallel: convertParallelBlockToParallel.toString(),
|
||||
findChildNodes: findChildNodes.toString(),
|
||||
findAllDescendantNodes: findAllDescendantNodes.toString(),
|
||||
generateWhileBlocks: generateWhileBlocks.toString(),
|
||||
convertWhileBlockToWhile: convertWhileBlockToWhile.toString(),
|
||||
},
|
||||
options,
|
||||
}),
|
||||
@@ -139,7 +144,7 @@ export async function POST(request: NextRequest) {
|
||||
dataKeys: block.data ? Object.keys(block.data) : [],
|
||||
})
|
||||
}
|
||||
if (block.type === 'loop' || block.type === 'parallel') {
|
||||
if (block.type === 'loop' || block.type === 'parallel' || block.type === 'while') {
|
||||
logger.info(`[${requestId}] Container block ${blockId} (${block.name}):`, {
|
||||
type: block.type,
|
||||
hasData: !!block.data,
|
||||
@@ -151,8 +156,10 @@ export async function POST(request: NextRequest) {
|
||||
// Log existing loops/parallels from sim-agent
|
||||
const loops = result.diff?.proposedState?.loops || result.loops || {}
|
||||
const parallels = result.diff?.proposedState?.parallels || result.parallels || {}
|
||||
const whiles = result.diff?.proposedState?.whiles || result.whiles || {}
|
||||
logger.info(`[${requestId}] Sim agent loops:`, loops)
|
||||
logger.info(`[${requestId}] Sim agent parallels:`, parallels)
|
||||
logger.info(`[${requestId}] Sim agent whiles:`, whiles)
|
||||
}
|
||||
|
||||
// Post-process the result to ensure loops and parallels are properly generated
|
||||
@@ -165,13 +172,16 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Find all loop and parallel blocks
|
||||
const containerBlocks = Object.values(blocks).filter(
|
||||
(block: any) => block.type === 'loop' || block.type === 'parallel'
|
||||
(block: any) => block.type === 'loop' || block.type === 'parallel' || block.type === 'while'
|
||||
)
|
||||
|
||||
// For each container, find its children based on loop-start edges
|
||||
containerBlocks.forEach((container: any) => {
|
||||
const childEdges = edges.filter(
|
||||
(edge: any) => edge.source === container.id && edge.sourceHandle === 'loop-start-source'
|
||||
(edge: any) =>
|
||||
edge.source === container.id &&
|
||||
(edge.sourceHandle === 'loop-start-source' ||
|
||||
edge.sourceHandle === 'while-start-source')
|
||||
)
|
||||
|
||||
childEdges.forEach((edge: any) => {
|
||||
@@ -198,17 +208,23 @@ export async function POST(request: NextRequest) {
|
||||
// Now regenerate loops and parallels with the fixed relationships
|
||||
const loops = generateLoopBlocks(result.diff.proposedState.blocks)
|
||||
const parallels = generateParallelBlocks(result.diff.proposedState.blocks)
|
||||
|
||||
const whiles = generateWhileBlocks(result.diff.proposedState.blocks)
|
||||
result.diff.proposedState.loops = loops
|
||||
result.diff.proposedState.parallels = parallels
|
||||
result.diff.proposedState.whiles = whiles
|
||||
|
||||
logger.info(`[${requestId}] Regenerated loops and parallels after fixing parent-child:`, {
|
||||
loopsCount: Object.keys(loops).length,
|
||||
parallelsCount: Object.keys(parallels).length,
|
||||
whilesCount: Object.keys(whiles).length,
|
||||
loops: Object.keys(loops).map((id) => ({
|
||||
id,
|
||||
nodes: loops[id].nodes,
|
||||
})),
|
||||
whiles: Object.keys(whiles).map((id) => ({
|
||||
id,
|
||||
nodes: whiles[id].nodes,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -223,13 +239,16 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
// Find all loop and parallel blocks
|
||||
const containerBlocks = Object.values(blocks).filter(
|
||||
(block: any) => block.type === 'loop' || block.type === 'parallel'
|
||||
(block: any) => block.type === 'loop' || block.type === 'parallel' || block.type === 'while'
|
||||
)
|
||||
|
||||
// For each container, find its children based on loop-start edges
|
||||
containerBlocks.forEach((container: any) => {
|
||||
const childEdges = edges.filter(
|
||||
(edge: any) => edge.source === container.id && edge.sourceHandle === 'loop-start-source'
|
||||
(edge: any) =>
|
||||
edge.source === container.id &&
|
||||
(edge.sourceHandle === 'loop-start-source' ||
|
||||
edge.sourceHandle === 'while-start-source')
|
||||
)
|
||||
|
||||
childEdges.forEach((edge: any) => {
|
||||
@@ -256,7 +275,7 @@ export async function POST(request: NextRequest) {
|
||||
// Generate loops and parallels for the blocks with fixed relationships
|
||||
const loops = generateLoopBlocks(result.blocks)
|
||||
const parallels = generateParallelBlocks(result.blocks)
|
||||
|
||||
const whiles = generateWhileBlocks(result.blocks)
|
||||
const transformedResult = {
|
||||
success: result.success,
|
||||
diff: {
|
||||
@@ -265,6 +284,7 @@ export async function POST(request: NextRequest) {
|
||||
edges: result.edges || existingDiff.proposedState.edges || [],
|
||||
loops: loops,
|
||||
parallels: parallels,
|
||||
whiles: whiles,
|
||||
},
|
||||
diffAnalysis: diffAnalysis,
|
||||
metadata: result.metadata || {
|
||||
|
||||
@@ -6,7 +6,11 @@ import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
|
||||
import { getAllBlocks } from '@/blocks/registry'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { resolveOutputType } from '@/blocks/utils'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
import {
|
||||
generateLoopBlocks,
|
||||
generateParallelBlocks,
|
||||
generateWhileBlocks,
|
||||
} from '@/stores/workflows/workflow/utils'
|
||||
|
||||
const logger = createLogger('YamlGenerateAPI')
|
||||
|
||||
@@ -60,6 +64,7 @@ export async function POST(request: NextRequest) {
|
||||
generateLoopBlocks: generateLoopBlocks.toString(),
|
||||
generateParallelBlocks: generateParallelBlocks.toString(),
|
||||
resolveOutputType: resolveOutputType.toString(),
|
||||
generateWhileBlocks: generateWhileBlocks.toString(),
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -6,7 +6,11 @@ import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
|
||||
import { getAllBlocks } from '@/blocks/registry'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { resolveOutputType } from '@/blocks/utils'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
import {
|
||||
generateLoopBlocks,
|
||||
generateParallelBlocks,
|
||||
generateWhileBlocks,
|
||||
} from '@/stores/workflows/workflow/utils'
|
||||
|
||||
const logger = createLogger('YamlParseAPI')
|
||||
|
||||
@@ -57,6 +61,7 @@ export async function POST(request: NextRequest) {
|
||||
generateLoopBlocks: generateLoopBlocks.toString(),
|
||||
generateParallelBlocks: generateParallelBlocks.toString(),
|
||||
resolveOutputType: resolveOutputType.toString(),
|
||||
generateWhileBlocks: generateWhileBlocks.toString(),
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -6,7 +6,11 @@ import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
|
||||
import { getAllBlocks } from '@/blocks/registry'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { resolveOutputType } from '@/blocks/utils'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
import {
|
||||
generateLoopBlocks,
|
||||
generateParallelBlocks,
|
||||
generateWhileBlocks,
|
||||
} from '@/stores/workflows/workflow/utils'
|
||||
|
||||
const logger = createLogger('YamlToWorkflowAPI')
|
||||
|
||||
@@ -65,6 +69,7 @@ export async function POST(request: NextRequest) {
|
||||
generateLoopBlocks: generateLoopBlocks.toString(),
|
||||
generateParallelBlocks: generateParallelBlocks.toString(),
|
||||
resolveOutputType: resolveOutputType.toString(),
|
||||
generateWhileBlocks: generateWhileBlocks.toString(),
|
||||
},
|
||||
options,
|
||||
}),
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
.workflow-container .react-flow__node-loopNode,
|
||||
.workflow-container .react-flow__node-parallelNode,
|
||||
.workflow-container .react-flow__node-whileNode,
|
||||
.workflow-container .react-flow__node-subflowNode {
|
||||
z-index: -1 !important;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export function DeployedWorkflowModal({
|
||||
edges: state.edges,
|
||||
loops: state.loops,
|
||||
parallels: state.parallels,
|
||||
whiles: state.whiles,
|
||||
}))
|
||||
|
||||
const handleRevert = () => {
|
||||
|
||||
@@ -83,6 +83,7 @@ export function DiffControls() {
|
||||
edges: rawState.edges || [],
|
||||
loops: rawState.loops || {},
|
||||
parallels: rawState.parallels || {},
|
||||
whiles: rawState.whiles || {},
|
||||
lastSaved: rawState.lastSaved || Date.now(),
|
||||
isDeployed: rawState.isDeployed || false,
|
||||
deploymentStatuses: rawState.deploymentStatuses || {},
|
||||
@@ -98,6 +99,7 @@ export function DiffControls() {
|
||||
edgesCount: workflowState.edges.length,
|
||||
loopsCount: Object.keys(workflowState.loops).length,
|
||||
parallelsCount: Object.keys(workflowState.parallels).length,
|
||||
whilesCount: Object.keys(workflowState.whiles).length,
|
||||
hasRequiredFields: Object.values(workflowState.blocks).every(
|
||||
(block) => block.id && block.type && block.name && block.position
|
||||
),
|
||||
@@ -146,6 +148,7 @@ export function DiffControls() {
|
||||
workflowId: activeWorkflowId,
|
||||
chatId: currentChat.id,
|
||||
messageId,
|
||||
whiles: workflowState.whiles,
|
||||
workflowState: JSON.stringify(workflowState),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -12,9 +12,10 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/themes/prism.css'
|
||||
|
||||
type IterationType = 'loop' | 'parallel'
|
||||
type IterationType = 'loop' | 'parallel' | 'while'
|
||||
type LoopType = 'for' | 'forEach'
|
||||
type ParallelType = 'count' | 'collection'
|
||||
type WhileType = 'while' | 'doWhile'
|
||||
|
||||
interface IterationNodeData {
|
||||
width?: number
|
||||
@@ -25,9 +26,11 @@ interface IterationNodeData {
|
||||
extent?: 'parent'
|
||||
loopType?: LoopType
|
||||
parallelType?: ParallelType
|
||||
whileType?: WhileType
|
||||
// Common
|
||||
count?: number
|
||||
collection?: string | any[] | Record<string, any>
|
||||
condition?: string
|
||||
isPreview?: boolean
|
||||
executionState?: {
|
||||
currentIteration?: number
|
||||
@@ -65,6 +68,12 @@ const CONFIG = {
|
||||
items: 'distribution' as const,
|
||||
},
|
||||
},
|
||||
while: {
|
||||
typeLabels: { while: 'While Loop', doWhile: 'Do While' },
|
||||
typeKey: 'whileType' as const,
|
||||
storeKey: 'whiles' as const,
|
||||
maxIterations: 100,
|
||||
},
|
||||
} as const
|
||||
|
||||
export function IterationBadges({ nodeId, data, iterationType }: IterationBadgesProps) {
|
||||
@@ -77,9 +86,21 @@ export function IterationBadges({ nodeId, data, iterationType }: IterationBadges
|
||||
|
||||
// Determine current type and values
|
||||
const currentType = (data?.[config.typeKey] ||
|
||||
(iterationType === 'loop' ? 'for' : 'count')) as any
|
||||
const configIterations = (nodeConfig as any)?.[config.configKeys.iterations] ?? data?.count ?? 5
|
||||
const configCollection = (nodeConfig as any)?.[config.configKeys.items] ?? data?.collection ?? ''
|
||||
(iterationType === 'loop' ? 'for' : iterationType === 'parallel' ? 'count' : 'while')) as any
|
||||
|
||||
const configIterations =
|
||||
iterationType === 'loop'
|
||||
? ((nodeConfig as any)?.[CONFIG.loop.configKeys.iterations] ?? data?.count ?? 5)
|
||||
: iterationType === 'parallel'
|
||||
? ((nodeConfig as any)?.[CONFIG.parallel.configKeys.iterations] ?? data?.count ?? 5)
|
||||
: ((nodeConfig as any)?.iterations ?? data?.count ?? 5)
|
||||
|
||||
const configCollection =
|
||||
iterationType === 'loop'
|
||||
? ((nodeConfig as any)?.[CONFIG.loop.configKeys.items] ?? data?.collection ?? '')
|
||||
: iterationType === 'parallel'
|
||||
? ((nodeConfig as any)?.[CONFIG.parallel.configKeys.items] ?? data?.collection ?? '')
|
||||
: ''
|
||||
|
||||
const iterations = configIterations
|
||||
const collectionString =
|
||||
@@ -87,8 +108,10 @@ export function IterationBadges({ nodeId, data, iterationType }: IterationBadges
|
||||
|
||||
// State management
|
||||
const [tempInputValue, setTempInputValue] = useState<string | null>(null)
|
||||
const isWhile = iterationType === 'while'
|
||||
const [whileValue, setWhileValue] = useState<string>(data?.condition || '')
|
||||
const inputValue = tempInputValue ?? iterations.toString()
|
||||
const editorValue = collectionString
|
||||
const editorValue = isWhile ? whileValue : collectionString
|
||||
const [typePopoverOpen, setTypePopoverOpen] = useState(false)
|
||||
const [configPopoverOpen, setConfigPopoverOpen] = useState(false)
|
||||
const [showTagDropdown, setShowTagDropdown] = useState(false)
|
||||
@@ -100,6 +123,7 @@ export function IterationBadges({ nodeId, data, iterationType }: IterationBadges
|
||||
const {
|
||||
collaborativeUpdateLoopType,
|
||||
collaborativeUpdateParallelType,
|
||||
collaborativeUpdateWhileType,
|
||||
collaborativeUpdateIterationCount,
|
||||
collaborativeUpdateIterationCollection,
|
||||
} = useCollaborativeWorkflow()
|
||||
@@ -110,12 +134,21 @@ export function IterationBadges({ nodeId, data, iterationType }: IterationBadges
|
||||
if (isPreview) return
|
||||
if (iterationType === 'loop') {
|
||||
collaborativeUpdateLoopType(nodeId, newType)
|
||||
} else {
|
||||
} else if (iterationType === 'parallel') {
|
||||
collaborativeUpdateParallelType(nodeId, newType)
|
||||
} else {
|
||||
collaborativeUpdateWhileType(nodeId, newType)
|
||||
}
|
||||
setTypePopoverOpen(false)
|
||||
},
|
||||
[nodeId, iterationType, collaborativeUpdateLoopType, collaborativeUpdateParallelType, isPreview]
|
||||
[
|
||||
nodeId,
|
||||
iterationType,
|
||||
collaborativeUpdateLoopType,
|
||||
collaborativeUpdateParallelType,
|
||||
collaborativeUpdateWhileType,
|
||||
isPreview,
|
||||
]
|
||||
)
|
||||
|
||||
// Handle iterations input change
|
||||
@@ -141,7 +174,9 @@ export function IterationBadges({ nodeId, data, iterationType }: IterationBadges
|
||||
|
||||
if (!Number.isNaN(value)) {
|
||||
const newValue = Math.min(config.maxIterations, Math.max(1, value))
|
||||
collaborativeUpdateIterationCount(nodeId, iterationType, newValue)
|
||||
if (iterationType === 'loop' || iterationType === 'parallel') {
|
||||
collaborativeUpdateIterationCount(nodeId, iterationType, newValue)
|
||||
}
|
||||
}
|
||||
setTempInputValue(null)
|
||||
setConfigPopoverOpen(false)
|
||||
@@ -158,7 +193,11 @@ export function IterationBadges({ nodeId, data, iterationType }: IterationBadges
|
||||
const handleEditorChange = useCallback(
|
||||
(value: string) => {
|
||||
if (isPreview) return
|
||||
collaborativeUpdateIterationCollection(nodeId, iterationType, value)
|
||||
if (iterationType === 'loop' || iterationType === 'parallel') {
|
||||
collaborativeUpdateIterationCollection(nodeId, iterationType, value)
|
||||
} else if (isWhile) {
|
||||
setWhileValue(value)
|
||||
}
|
||||
|
||||
const textarea = editorContainerRef.current?.querySelector('textarea')
|
||||
if (textarea) {
|
||||
@@ -170,14 +209,18 @@ export function IterationBadges({ nodeId, data, iterationType }: IterationBadges
|
||||
setShowTagDropdown(triggerCheck.show)
|
||||
}
|
||||
},
|
||||
[nodeId, iterationType, collaborativeUpdateIterationCollection, isPreview]
|
||||
[nodeId, iterationType, collaborativeUpdateIterationCollection, isPreview, isWhile]
|
||||
)
|
||||
|
||||
// Handle tag selection
|
||||
const handleTagSelect = useCallback(
|
||||
(newValue: string) => {
|
||||
if (isPreview) return
|
||||
collaborativeUpdateIterationCollection(nodeId, iterationType, newValue)
|
||||
if (iterationType === 'loop' || iterationType === 'parallel') {
|
||||
collaborativeUpdateIterationCollection(nodeId, iterationType, newValue)
|
||||
} else if (isWhile) {
|
||||
setWhileValue(newValue)
|
||||
}
|
||||
setShowTagDropdown(false)
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -187,7 +230,7 @@ export function IterationBadges({ nodeId, data, iterationType }: IterationBadges
|
||||
}
|
||||
}, 0)
|
||||
},
|
||||
[nodeId, iterationType, collaborativeUpdateIterationCollection, isPreview]
|
||||
[nodeId, iterationType, collaborativeUpdateIterationCollection, isPreview, isWhile]
|
||||
)
|
||||
|
||||
// Determine if we're in count mode or collection mode
|
||||
@@ -223,7 +266,11 @@ export function IterationBadges({ nodeId, data, iterationType }: IterationBadges
|
||||
<PopoverContent className='w-48 p-3' align='center' onClick={(e) => e.stopPropagation()}>
|
||||
<div className='space-y-2'>
|
||||
<div className='font-medium text-muted-foreground text-xs'>
|
||||
{iterationType === 'loop' ? 'Loop Type' : 'Parallel Type'}
|
||||
{iterationType === 'loop'
|
||||
? 'Loop Type'
|
||||
: iterationType === 'parallel'
|
||||
? 'Parallel Type'
|
||||
: 'While Type'}
|
||||
</div>
|
||||
<div className='space-y-1'>
|
||||
{typeOptions.map(([typeValue, typeLabel]) => (
|
||||
@@ -259,24 +306,63 @@ export function IterationBadges({ nodeId, data, iterationType }: IterationBadges
|
||||
)}
|
||||
style={{ pointerEvents: isPreview ? 'none' : 'auto' }}
|
||||
>
|
||||
{isCountMode ? `Iterations: ${iterations}` : 'Items'}
|
||||
{isWhile ? 'Condition' : isCountMode ? `Iterations: ${iterations}` : 'Items'}
|
||||
{!isPreview && <ChevronDown className='h-3 w-3 text-muted-foreground' />}
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
{!isPreview && (
|
||||
<PopoverContent
|
||||
className={cn('p-3', !isCountMode ? 'w-72' : 'w-48')}
|
||||
className={cn('p-3', isWhile || !isCountMode ? 'w-72' : 'w-48')}
|
||||
align='center'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className='space-y-2'>
|
||||
<div className='font-medium text-muted-foreground text-xs'>
|
||||
{isCountMode
|
||||
? `${iterationType === 'loop' ? 'Loop' : 'Parallel'} Iterations`
|
||||
: `${iterationType === 'loop' ? 'Collection' : 'Parallel'} Items`}
|
||||
{isWhile
|
||||
? 'While Condition'
|
||||
: isCountMode
|
||||
? `${iterationType === 'loop' ? 'Loop' : 'Parallel'} Iterations`
|
||||
: `${iterationType === 'loop' ? 'Collection' : 'Parallel'} Items`}
|
||||
</div>
|
||||
|
||||
{isCountMode ? (
|
||||
{isWhile ? (
|
||||
// Code editor for while condition
|
||||
<div ref={editorContainerRef} className='relative'>
|
||||
<div className='relative min-h-[80px] rounded-md border border-input bg-background px-3 pt-2 pb-3 font-mono text-sm'>
|
||||
{editorValue === '' && (
|
||||
<div className='pointer-events-none absolute top-[8.5px] left-3 select-none text-muted-foreground/50'>
|
||||
condition === true
|
||||
</div>
|
||||
)}
|
||||
<Editor
|
||||
value={editorValue}
|
||||
onValueChange={handleEditorChange}
|
||||
highlight={(code) => highlight(code, languages.javascript, 'javascript')}
|
||||
padding={0}
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
lineHeight: '21px',
|
||||
}}
|
||||
className='w-full focus:outline-none'
|
||||
textareaClassName='focus:outline-none focus:ring-0 bg-transparent resize-none w-full overflow-hidden whitespace-pre-wrap'
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-2 text-[10px] text-muted-foreground'>
|
||||
Enter a boolean expression.
|
||||
</div>
|
||||
{showTagDropdown && (
|
||||
<TagDropdown
|
||||
visible={showTagDropdown}
|
||||
onSelect={handleTagSelect}
|
||||
blockId={nodeId}
|
||||
activeSourceBlockId={null}
|
||||
inputValue={editorValue}
|
||||
cursorPosition={cursorPosition}
|
||||
onClose={() => setShowTagDropdown(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : isCountMode ? (
|
||||
// Number input for count-based mode
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
|
||||
@@ -136,12 +136,40 @@ describe('SubflowNodeComponent', () => {
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it.concurrent('should accept while kind in NodeProps data', () => {
|
||||
const validProps = {
|
||||
id: 'test-id-while',
|
||||
type: 'subflowNode' as const,
|
||||
data: {
|
||||
width: 400,
|
||||
height: 300,
|
||||
isPreview: false,
|
||||
kind: 'while' as const,
|
||||
},
|
||||
selected: false,
|
||||
zIndex: 1,
|
||||
isConnectable: true,
|
||||
xPos: 0,
|
||||
yPos: 0,
|
||||
dragging: false,
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
const _component: typeof SubflowNodeComponent = SubflowNodeComponent
|
||||
expect(_component).toBeDefined()
|
||||
expect(validProps.type).toBe('subflowNode')
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it.concurrent('should handle different data configurations', () => {
|
||||
const configurations = [
|
||||
{ width: 500, height: 300, isPreview: false, kind: 'loop' as const },
|
||||
{ width: 800, height: 600, isPreview: true, kind: 'parallel' as const },
|
||||
{ width: 500, height: 300, isPreview: false, kind: 'while' as const },
|
||||
{ width: 0, height: 0, isPreview: false, kind: 'loop' as const },
|
||||
{ width: 0, height: 0, isPreview: false, kind: 'while' as const },
|
||||
{ kind: 'loop' as const },
|
||||
{ kind: 'while' as const },
|
||||
]
|
||||
|
||||
configurations.forEach((data) => {
|
||||
@@ -306,10 +334,20 @@ describe('SubflowNodeComponent', () => {
|
||||
})
|
||||
|
||||
it.concurrent('should generate correct handle IDs for parallel kind', () => {
|
||||
type SubflowKind = 'loop' | 'parallel'
|
||||
type SubflowKind = 'loop' | 'parallel' | 'while'
|
||||
const testHandleGeneration = (kind: SubflowKind) => {
|
||||
const startHandleId = kind === 'loop' ? 'loop-start-source' : 'parallel-start-source'
|
||||
const endHandleId = kind === 'loop' ? 'loop-end-source' : 'parallel-end-source'
|
||||
const startHandleId =
|
||||
kind === 'loop'
|
||||
? 'loop-start-source'
|
||||
: kind === 'parallel'
|
||||
? 'parallel-start-source'
|
||||
: 'while-start-source'
|
||||
const endHandleId =
|
||||
kind === 'loop'
|
||||
? 'loop-end-source'
|
||||
: kind === 'parallel'
|
||||
? 'parallel-end-source'
|
||||
: 'while-end-source'
|
||||
return { startHandleId, endHandleId }
|
||||
}
|
||||
|
||||
@@ -318,6 +356,29 @@ describe('SubflowNodeComponent', () => {
|
||||
expect(result.endHandleId).toBe('parallel-end-source')
|
||||
})
|
||||
|
||||
it.concurrent('should generate correct handle IDs for while kind', () => {
|
||||
type SubflowKind = 'loop' | 'parallel' | 'while'
|
||||
const testHandleGeneration = (kind: SubflowKind) => {
|
||||
const startHandleId =
|
||||
kind === 'loop'
|
||||
? 'loop-start-source'
|
||||
: kind === 'parallel'
|
||||
? 'parallel-start-source'
|
||||
: 'while-start-source'
|
||||
const endHandleId =
|
||||
kind === 'loop'
|
||||
? 'loop-end-source'
|
||||
: kind === 'parallel'
|
||||
? 'parallel-end-source'
|
||||
: 'while-end-source'
|
||||
return { startHandleId, endHandleId }
|
||||
}
|
||||
|
||||
const result = testHandleGeneration('while')
|
||||
expect(result.startHandleId).toBe('while-start-source')
|
||||
expect(result.endHandleId).toBe('while-end-source')
|
||||
})
|
||||
|
||||
it.concurrent('should generate correct background colors for loop kind', () => {
|
||||
const loopData = { ...defaultProps.data, kind: 'loop' as const }
|
||||
const startBg = loopData.kind === 'loop' ? '#2FB3FF' : '#FEE12B'
|
||||
@@ -326,21 +387,41 @@ describe('SubflowNodeComponent', () => {
|
||||
})
|
||||
|
||||
it.concurrent('should generate correct background colors for parallel kind', () => {
|
||||
type SubflowKind = 'loop' | 'parallel'
|
||||
type SubflowKind = 'loop' | 'parallel' | 'while'
|
||||
const testBgGeneration = (kind: SubflowKind) => {
|
||||
return kind === 'loop' ? '#2FB3FF' : '#FEE12B'
|
||||
return kind === 'loop' ? '#2FB3FF' : kind === 'parallel' ? '#FEE12B' : '#57D9A3'
|
||||
}
|
||||
|
||||
const startBg = testBgGeneration('parallel')
|
||||
expect(startBg).toBe('#FEE12B')
|
||||
})
|
||||
|
||||
it.concurrent('should generate correct background colors for while kind', () => {
|
||||
type SubflowKind = 'loop' | 'parallel' | 'while'
|
||||
const testBgGeneration = (kind: SubflowKind) => {
|
||||
return kind === 'loop' ? '#2FB3FF' : kind === 'parallel' ? '#FEE12B' : '#57D9A3'
|
||||
}
|
||||
|
||||
const startBg = testBgGeneration('while')
|
||||
expect(startBg).toBe('#57D9A3')
|
||||
})
|
||||
|
||||
it.concurrent('should demonstrate handle ID generation for any kind', () => {
|
||||
type SubflowKind = 'loop' | 'parallel'
|
||||
type SubflowKind = 'loop' | 'parallel' | 'while'
|
||||
const testKind = (kind: SubflowKind) => {
|
||||
const data = { kind }
|
||||
const startHandleId = data.kind === 'loop' ? 'loop-start-source' : 'parallel-start-source'
|
||||
const endHandleId = data.kind === 'loop' ? 'loop-end-source' : 'parallel-end-source'
|
||||
const startHandleId =
|
||||
data.kind === 'loop'
|
||||
? 'loop-start-source'
|
||||
: data.kind === 'parallel'
|
||||
? 'parallel-start-source'
|
||||
: 'while-start-source'
|
||||
const endHandleId =
|
||||
data.kind === 'loop'
|
||||
? 'loop-end-source'
|
||||
: data.kind === 'parallel'
|
||||
? 'parallel-end-source'
|
||||
: 'while-end-source'
|
||||
return { startHandleId, endHandleId }
|
||||
}
|
||||
|
||||
@@ -351,6 +432,10 @@ describe('SubflowNodeComponent', () => {
|
||||
const parallelResult = testKind('parallel')
|
||||
expect(parallelResult.startHandleId).toBe('parallel-start-source')
|
||||
expect(parallelResult.endHandleId).toBe('parallel-end-source')
|
||||
|
||||
const whileResult = testKind('while')
|
||||
expect(whileResult.startHandleId).toBe('while-start-source')
|
||||
expect(whileResult.endHandleId).toBe('while-end-source')
|
||||
})
|
||||
|
||||
it.concurrent('should pass correct iterationType to IterationBadges for loop', () => {
|
||||
@@ -368,25 +453,49 @@ describe('SubflowNodeComponent', () => {
|
||||
expect(parallelProps.data.kind).toBe('parallel')
|
||||
})
|
||||
|
||||
it.concurrent('should handle both kinds in configuration arrays', () => {
|
||||
const bothKinds = ['loop', 'parallel'] as const
|
||||
bothKinds.forEach((kind) => {
|
||||
it.concurrent('should pass correct iterationType to IterationBadges for while', () => {
|
||||
const whileProps = {
|
||||
...defaultProps,
|
||||
data: { ...defaultProps.data, kind: 'while' as const },
|
||||
}
|
||||
// Mock IterationBadges should receive the kind as iterationType
|
||||
expect(whileProps.data.kind).toBe('while')
|
||||
})
|
||||
|
||||
it.concurrent('should handle loop, parallel, and while kinds in configuration arrays', () => {
|
||||
const allKinds = ['loop', 'parallel', 'while'] as const
|
||||
allKinds.forEach((kind) => {
|
||||
const data = { ...defaultProps.data, kind }
|
||||
expect(['loop', 'parallel']).toContain(data.kind)
|
||||
expect(['loop', 'parallel', 'while']).toContain(data.kind)
|
||||
|
||||
// Test handle ID generation for both kinds
|
||||
const startHandleId = data.kind === 'loop' ? 'loop-start-source' : 'parallel-start-source'
|
||||
const endHandleId = data.kind === 'loop' ? 'loop-end-source' : 'parallel-end-source'
|
||||
const startBg = data.kind === 'loop' ? '#2FB3FF' : '#FEE12B'
|
||||
const startHandleId =
|
||||
data.kind === 'loop'
|
||||
? 'loop-start-source'
|
||||
: data.kind === 'parallel'
|
||||
? 'parallel-start-source'
|
||||
: 'while-start-source'
|
||||
const endHandleId =
|
||||
data.kind === 'loop'
|
||||
? 'loop-end-source'
|
||||
: data.kind === 'parallel'
|
||||
? 'parallel-end-source'
|
||||
: 'while-end-source'
|
||||
const startBg =
|
||||
data.kind === 'loop' ? '#2FB3FF' : data.kind === 'parallel' ? '#FEE12B' : '#57D9A3'
|
||||
|
||||
if (kind === 'loop') {
|
||||
expect(startHandleId).toBe('loop-start-source')
|
||||
expect(endHandleId).toBe('loop-end-source')
|
||||
expect(startBg).toBe('#2FB3FF')
|
||||
} else {
|
||||
} else if (kind === 'parallel') {
|
||||
expect(startHandleId).toBe('parallel-start-source')
|
||||
expect(endHandleId).toBe('parallel-end-source')
|
||||
expect(startBg).toBe('#FEE12B')
|
||||
} else {
|
||||
expect(startHandleId).toBe('while-start-source')
|
||||
expect(endHandleId).toBe('while-end-source')
|
||||
expect(startBg).toBe('#57D9A3')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -433,10 +542,15 @@ describe('SubflowNodeComponent', () => {
|
||||
...defaultProps,
|
||||
data: { ...defaultProps.data, kind: 'parallel' as const },
|
||||
}
|
||||
const whileProps = {
|
||||
...defaultProps,
|
||||
data: { ...defaultProps.data, kind: 'while' as const },
|
||||
}
|
||||
|
||||
// The iterationType should match the kind
|
||||
expect(loopProps.data.kind).toBe('loop')
|
||||
expect(parallelProps.data.kind).toBe('parallel')
|
||||
expect(whileProps.data.kind).toBe('while')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -26,6 +26,12 @@ const SubflowNodeStyles: React.FC = () => {
|
||||
100% { box-shadow: 0 0 0 0 rgba(139, 195, 74, 0); }
|
||||
}
|
||||
|
||||
@keyframes while-node-pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(255, 159, 67, 0.3); }
|
||||
70% { box-shadow: 0 0 0 6px rgba(255, 159, 67, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(255, 159, 67, 0); }
|
||||
}
|
||||
|
||||
.loop-node-drag-over {
|
||||
animation: loop-node-pulse 1.2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
border-style: solid !important;
|
||||
@@ -40,6 +46,13 @@ const SubflowNodeStyles: React.FC = () => {
|
||||
box-shadow: 0 0 0 8px rgba(139, 195, 74, 0.1);
|
||||
}
|
||||
|
||||
.while-node-drag-over {
|
||||
animation: while-node-pulse 1.2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
border-style: solid !important;
|
||||
background-color: rgba(255, 159, 67, 0.08) !important;
|
||||
box-shadow: 0 0 0 8px rgba(255, 159, 67, 0.1);
|
||||
}
|
||||
|
||||
.react-flow__node-group:hover,
|
||||
.hover-highlight {
|
||||
border-color: #1e293b !important;
|
||||
@@ -69,7 +82,7 @@ export interface SubflowNodeData {
|
||||
extent?: 'parent'
|
||||
hasNestedError?: boolean
|
||||
isPreview?: boolean
|
||||
kind: 'loop' | 'parallel'
|
||||
kind: 'loop' | 'parallel' | 'while'
|
||||
}
|
||||
|
||||
export const SubflowNodeComponent = memo(({ data, id }: NodeProps<SubflowNodeData>) => {
|
||||
@@ -114,9 +127,26 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps<SubflowNodeDat
|
||||
|
||||
const nestedStyles = getNestedStyles()
|
||||
|
||||
const startHandleId = data.kind === 'loop' ? 'loop-start-source' : 'parallel-start-source'
|
||||
const endHandleId = data.kind === 'loop' ? 'loop-end-source' : 'parallel-end-source'
|
||||
const startBg = data.kind === 'loop' ? '#2FB3FF' : '#FEE12B'
|
||||
const startHandleId =
|
||||
data.kind === 'loop'
|
||||
? 'loop-start-source'
|
||||
: data.kind === 'parallel'
|
||||
? 'parallel-start-source'
|
||||
: 'while-start-source'
|
||||
const endHandleId =
|
||||
data.kind === 'loop'
|
||||
? 'loop-end-source'
|
||||
: data.kind === 'parallel'
|
||||
? 'parallel-end-source'
|
||||
: 'while-end-source'
|
||||
const startBg =
|
||||
data.kind === 'loop'
|
||||
? '#2FB3FF'
|
||||
: data.kind === 'parallel'
|
||||
? '#FEE12B'
|
||||
: data.kind === 'while'
|
||||
? '#FF9F43'
|
||||
: '#2FB3FF'
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { RefreshCwIcon } from 'lucide-react'
|
||||
|
||||
export const WhileTool = {
|
||||
id: 'while',
|
||||
type: 'while',
|
||||
name: 'While',
|
||||
description: 'While Loop',
|
||||
icon: RefreshCwIcon,
|
||||
bgColor: '#CC5500',
|
||||
data: {
|
||||
label: 'While',
|
||||
whileType: 'while' as 'while' | 'doWhile',
|
||||
condition: '',
|
||||
width: 500,
|
||||
height: 300,
|
||||
extent: 'parent',
|
||||
executionState: {
|
||||
currentIteration: 0,
|
||||
isExecuting: false,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
},
|
||||
},
|
||||
style: {
|
||||
width: 500,
|
||||
height: 300,
|
||||
},
|
||||
isResizable: true,
|
||||
}
|
||||
@@ -106,32 +106,34 @@ export function ActionBar({ blockId, blockType, disabled = false }: ActionBarPro
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Remove from subflow - only show when inside loop/parallel */}
|
||||
{!isStarterBlock && parentId && (parentType === 'loop' || parentType === 'parallel') && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
if (!disabled && userPermissions.canEdit) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('remove-from-subflow', { detail: { blockId } })
|
||||
)
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'text-gray-500',
|
||||
(disabled || !userPermissions.canEdit) && 'cursor-not-allowed opacity-50'
|
||||
)}
|
||||
disabled={disabled || !userPermissions.canEdit}
|
||||
>
|
||||
<LogOut className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='right'>{getTooltipMessage('Remove From Subflow')}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* Remove from subflow - only show when inside loop/parallel/while */}
|
||||
{!isStarterBlock &&
|
||||
parentId &&
|
||||
(parentType === 'loop' || parentType === 'parallel' || parentType === 'while') && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
if (!disabled && userPermissions.canEdit) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('remove-from-subflow', { detail: { blockId } })
|
||||
)
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'text-gray-500',
|
||||
(disabled || !userPermissions.canEdit) && 'cursor-not-allowed opacity-50'
|
||||
)}
|
||||
disabled={disabled || !userPermissions.canEdit}
|
||||
>
|
||||
<LogOut className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='right'>{getTooltipMessage('Remove From Subflow')}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -79,7 +79,7 @@ export function ConnectionBlocks({
|
||||
const blockConfig = getBlock(connection.type)
|
||||
const displayName = connection.name // Use the actual block name instead of transforming it
|
||||
|
||||
// Handle special blocks that aren't in the registry (loop and parallel)
|
||||
// Handle special blocks that aren't in the registry (loop, parallel, while)
|
||||
let Icon = blockConfig?.icon
|
||||
let bgColor = blockConfig?.bgColor || '#6B7280' // Fallback to gray
|
||||
|
||||
@@ -90,6 +90,9 @@ export function ConnectionBlocks({
|
||||
} else if (connection.type === 'parallel') {
|
||||
Icon = SplitIcon as typeof Icon
|
||||
bgColor = '#FEE12B' // Yellow color for parallel blocks
|
||||
} else if (connection.type === 'while') {
|
||||
Icon = RepeatIcon as typeof Icon
|
||||
bgColor = '#FF9F43' // Orange color for while blocks
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ export function generateFullWorkflowData() {
|
||||
edges: workflowState.edges,
|
||||
loops: workflowState.loops,
|
||||
parallels: workflowState.parallels,
|
||||
whiles: workflowState.whiles,
|
||||
},
|
||||
subBlockValues,
|
||||
exportedAt: new Date().toISOString(),
|
||||
|
||||
@@ -3,7 +3,13 @@ import type { Edge } from 'reactflow'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||
import type { DeploymentStatus } from '@/stores/workflows/registry/types'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
import type {
|
||||
BlockState,
|
||||
Loop,
|
||||
Parallel,
|
||||
While,
|
||||
WorkflowState,
|
||||
} from '@/stores/workflows/workflow/types'
|
||||
|
||||
/**
|
||||
* Interface for the current workflow abstraction
|
||||
@@ -14,6 +20,7 @@ export interface CurrentWorkflow {
|
||||
edges: Edge[]
|
||||
loops: Record<string, Loop>
|
||||
parallels: Record<string, Parallel>
|
||||
whiles: Record<string, While>
|
||||
lastSaved?: number
|
||||
isDeployed?: boolean
|
||||
deployedAt?: Date
|
||||
@@ -61,6 +68,7 @@ export function useCurrentWorkflow(): CurrentWorkflow {
|
||||
edges: activeWorkflow.edges,
|
||||
loops: activeWorkflow.loops || {},
|
||||
parallels: activeWorkflow.parallels || {},
|
||||
whiles: activeWorkflow.whiles || {},
|
||||
lastSaved: activeWorkflow.lastSaved,
|
||||
isDeployed: activeWorkflow.isDeployed,
|
||||
deployedAt: activeWorkflow.deployedAt,
|
||||
|
||||
@@ -522,6 +522,7 @@ export function useWorkflowExecution() {
|
||||
edges: workflowEdges,
|
||||
loops: workflowLoops,
|
||||
parallels: workflowParallels,
|
||||
whiles: workflowWhiles,
|
||||
} = currentWorkflow
|
||||
|
||||
// Filter out blocks without type (these are layout-only blocks)
|
||||
@@ -633,7 +634,8 @@ export function useWorkflowExecution() {
|
||||
filteredStates,
|
||||
filteredEdges,
|
||||
workflowLoops,
|
||||
workflowParallels
|
||||
workflowParallels,
|
||||
workflowWhiles
|
||||
)
|
||||
|
||||
// If this is a chat execution, get the selected outputs
|
||||
|
||||
@@ -104,6 +104,7 @@ export async function executeWorkflowWithLogging(
|
||||
edges: workflowEdges,
|
||||
loops: workflowLoops,
|
||||
parallels: workflowParallels,
|
||||
whiles: workflowWhiles,
|
||||
} = currentWorkflow
|
||||
|
||||
// Filter out blocks without type (these are layout-only blocks)
|
||||
@@ -201,7 +202,8 @@ export async function executeWorkflowWithLogging(
|
||||
filteredStates,
|
||||
filteredEdges,
|
||||
workflowLoops,
|
||||
workflowParallels
|
||||
workflowParallels,
|
||||
workflowWhiles
|
||||
)
|
||||
|
||||
// If this is a chat execution, get the selected outputs
|
||||
|
||||
@@ -15,7 +15,8 @@ const isContainerType = (blockType: string): boolean => {
|
||||
blockType === 'parallel' ||
|
||||
blockType === 'loopNode' ||
|
||||
blockType === 'parallelNode' ||
|
||||
blockType === 'subflowNode'
|
||||
blockType === 'subflowNode' ||
|
||||
blockType === 'while'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -302,7 +303,7 @@ export const calculateRelativePosition = (
|
||||
* @param getNodes Function to retrieve all nodes from ReactFlow
|
||||
* @param updateBlockPosition Function to update the position of a block
|
||||
* @param updateParentId Function to update the parent ID of a block
|
||||
* @param resizeLoopNodes Function to resize loop nodes after parent update
|
||||
* @param resizeLoopNodes Function to resize loop or parallel or while nodes after parent update
|
||||
*/
|
||||
export const updateNodeParent = (
|
||||
nodeId: string,
|
||||
@@ -336,7 +337,7 @@ export const updateNodeParent = (
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a point is inside a loop or parallel node
|
||||
* Checks if a point is inside a loop or parallel or while node
|
||||
* @param position Position coordinates to check
|
||||
* @param getNodes Function to retrieve all nodes from ReactFlow
|
||||
* @returns The smallest container node containing the point, or null if none
|
||||
@@ -390,7 +391,7 @@ export const isPointInLoopNode = (
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates appropriate dimensions for a loop or parallel node based on its children
|
||||
* Calculates appropriate dimensions for a loop or parallel or while node based on its children
|
||||
* @param nodeId ID of the container node
|
||||
* @param getNodes Function to retrieve all nodes from ReactFlow
|
||||
* @param blocks Block states from workflow store
|
||||
|
||||
@@ -47,6 +47,7 @@ export async function applyAutoLayoutToWorkflow(
|
||||
edges: any[],
|
||||
loops: Record<string, any> = {},
|
||||
parallels: Record<string, any> = {},
|
||||
whiles: Record<string, any> = {},
|
||||
options: AutoLayoutOptions = {}
|
||||
): Promise<{
|
||||
success: boolean
|
||||
@@ -152,7 +153,7 @@ export async function applyAutoLayoutAndUpdateStore(
|
||||
const { useWorkflowStore } = await import('@/stores/workflows/workflow/store')
|
||||
|
||||
const workflowStore = useWorkflowStore.getState()
|
||||
const { blocks, edges, loops = {}, parallels = {} } = workflowStore
|
||||
const { blocks, edges, loops = {}, parallels = {}, whiles = {} } = workflowStore
|
||||
|
||||
logger.info('Auto layout store data:', {
|
||||
workflowId,
|
||||
@@ -160,6 +161,7 @@ export async function applyAutoLayoutAndUpdateStore(
|
||||
edgeCount: edges.length,
|
||||
loopCount: Object.keys(loops).length,
|
||||
parallelCount: Object.keys(parallels).length,
|
||||
whileCount: Object.keys(whiles).length,
|
||||
})
|
||||
|
||||
if (Object.keys(blocks).length === 0) {
|
||||
@@ -174,6 +176,7 @@ export async function applyAutoLayoutAndUpdateStore(
|
||||
edges,
|
||||
loops,
|
||||
parallels,
|
||||
whiles,
|
||||
options
|
||||
)
|
||||
|
||||
@@ -265,5 +268,5 @@ export async function applyAutoLayoutToBlocks(
|
||||
layoutedBlocks?: Record<string, any>
|
||||
error?: string
|
||||
}> {
|
||||
return applyAutoLayoutToWorkflow('preview', blocks, edges, {}, {}, options)
|
||||
return applyAutoLayoutToWorkflow('preview', blocks, edges, {}, {}, {}, options)
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ const WorkflowContent = React.memo(() => {
|
||||
useStreamCleanup(copilotCleanup)
|
||||
|
||||
// Extract workflow data from the abstraction
|
||||
const { blocks, edges, loops, parallels, isDiffMode } = currentWorkflow
|
||||
const { blocks, edges, isDiffMode } = currentWorkflow
|
||||
|
||||
// Get diff analysis for edge reconstruction
|
||||
const { diffAnalysis, isShowingDiff, isDiffReady } = useWorkflowDiffStore()
|
||||
@@ -462,6 +462,8 @@ const WorkflowContent = React.memo(() => {
|
||||
sourceHandle = 'loop-end-source'
|
||||
} else if (block.type === 'parallel') {
|
||||
sourceHandle = 'parallel-end-source'
|
||||
} else if (block.type === 'while') {
|
||||
sourceHandle = 'while-end-source'
|
||||
}
|
||||
|
||||
return sourceHandle
|
||||
@@ -481,14 +483,19 @@ const WorkflowContent = React.memo(() => {
|
||||
if (type === 'connectionBlock') return
|
||||
|
||||
// Special handling for container nodes (loop or parallel)
|
||||
if (type === 'loop' || type === 'parallel') {
|
||||
if (type === 'loop' || type === 'parallel' || type === 'while') {
|
||||
// Create a unique ID and name for the container
|
||||
const id = crypto.randomUUID()
|
||||
|
||||
// Auto-number the blocks based on existing blocks of the same type
|
||||
const existingBlocksOfType = Object.values(blocks).filter((b) => b.type === type)
|
||||
const blockNumber = existingBlocksOfType.length + 1
|
||||
const name = type === 'loop' ? `Loop ${blockNumber}` : `Parallel ${blockNumber}`
|
||||
const name =
|
||||
type === 'loop'
|
||||
? `Loop ${blockNumber}`
|
||||
: type === 'parallel'
|
||||
? `Parallel ${blockNumber}`
|
||||
: `While ${blockNumber}`
|
||||
|
||||
// Calculate the center position of the viewport
|
||||
const centerPosition = project({
|
||||
@@ -615,21 +622,30 @@ const WorkflowContent = React.memo(() => {
|
||||
|
||||
// Clear any drag-over styling
|
||||
document
|
||||
.querySelectorAll('.loop-node-drag-over, .parallel-node-drag-over')
|
||||
.querySelectorAll('.loop-node-drag-over, .parallel-node-drag-over, .while-node-drag-over')
|
||||
.forEach((el) => {
|
||||
el.classList.remove('loop-node-drag-over', 'parallel-node-drag-over')
|
||||
el.classList.remove(
|
||||
'loop-node-drag-over',
|
||||
'parallel-node-drag-over',
|
||||
'while-node-drag-over'
|
||||
)
|
||||
})
|
||||
document.body.style.cursor = ''
|
||||
|
||||
// Special handling for container nodes (loop or parallel)
|
||||
if (data.type === 'loop' || data.type === 'parallel') {
|
||||
if (data.type === 'loop' || data.type === 'parallel' || data.type === 'while') {
|
||||
// Create a unique ID and name for the container
|
||||
const id = crypto.randomUUID()
|
||||
|
||||
// Auto-number the blocks based on existing blocks of the same type
|
||||
const existingBlocksOfType = Object.values(blocks).filter((b) => b.type === data.type)
|
||||
const blockNumber = existingBlocksOfType.length + 1
|
||||
const name = data.type === 'loop' ? `Loop ${blockNumber}` : `Parallel ${blockNumber}`
|
||||
const name =
|
||||
data.type === 'loop'
|
||||
? `Loop ${blockNumber}`
|
||||
: data.type === 'parallel'
|
||||
? `Parallel ${blockNumber}`
|
||||
: `While ${blockNumber}`
|
||||
|
||||
// Check if we're dropping inside another container
|
||||
if (containerInfo) {
|
||||
@@ -691,7 +707,12 @@ const WorkflowContent = React.memo(() => {
|
||||
}
|
||||
|
||||
const blockConfig = getBlock(data.type)
|
||||
if (!blockConfig && data.type !== 'loop' && data.type !== 'parallel') {
|
||||
if (
|
||||
!blockConfig &&
|
||||
data.type !== 'loop' &&
|
||||
data.type !== 'parallel' &&
|
||||
data.type !== 'while'
|
||||
) {
|
||||
logger.error('Invalid block type:', { data })
|
||||
return
|
||||
}
|
||||
@@ -703,7 +724,9 @@ const WorkflowContent = React.memo(() => {
|
||||
? `Loop ${Object.values(blocks).filter((b) => b.type === 'loop').length + 1}`
|
||||
: data.type === 'parallel'
|
||||
? `Parallel ${Object.values(blocks).filter((b) => b.type === 'parallel').length + 1}`
|
||||
: `${blockConfig!.name} ${Object.values(blocks).filter((b) => b.type === data.type).length + 1}`
|
||||
: data.type === 'while'
|
||||
? `While ${Object.values(blocks).filter((b) => b.type === 'while').length + 1}`
|
||||
: `${blockConfig!.name} ${Object.values(blocks).filter((b) => b.type === data.type).length + 1}`
|
||||
|
||||
if (containerInfo) {
|
||||
// Calculate position relative to the container node
|
||||
@@ -762,7 +785,9 @@ const WorkflowContent = React.memo(() => {
|
||||
const startSourceHandle =
|
||||
(containerNode?.data as any)?.kind === 'loop'
|
||||
? 'loop-start-source'
|
||||
: 'parallel-start-source'
|
||||
: data.type === 'parallel'
|
||||
? 'parallel-start-source'
|
||||
: 'while-start-source'
|
||||
|
||||
addEdge({
|
||||
id: crypto.randomUUID(),
|
||||
@@ -833,9 +858,13 @@ const WorkflowContent = React.memo(() => {
|
||||
|
||||
// Clear any previous highlighting
|
||||
document
|
||||
.querySelectorAll('.loop-node-drag-over, .parallel-node-drag-over')
|
||||
.querySelectorAll('.loop-node-drag-over, .parallel-node-drag-over, .while-node-drag-over')
|
||||
.forEach((el) => {
|
||||
el.classList.remove('loop-node-drag-over', 'parallel-node-drag-over')
|
||||
el.classList.remove(
|
||||
'loop-node-drag-over',
|
||||
'parallel-node-drag-over',
|
||||
'while-node-drag-over'
|
||||
)
|
||||
})
|
||||
|
||||
// If hovering over a container node, highlight it
|
||||
@@ -854,6 +883,11 @@ const WorkflowContent = React.memo(() => {
|
||||
(containerNode.data as any)?.kind === 'parallel'
|
||||
) {
|
||||
containerElement.classList.add('parallel-node-drag-over')
|
||||
} else if (
|
||||
containerNode?.type === 'subflowNode' &&
|
||||
(containerNode.data as any)?.kind === 'while'
|
||||
) {
|
||||
containerElement.classList.add('while-node-drag-over')
|
||||
}
|
||||
document.body.style.cursor = 'copy'
|
||||
}
|
||||
@@ -983,7 +1017,7 @@ const WorkflowContent = React.memo(() => {
|
||||
}
|
||||
|
||||
// Handle container nodes differently
|
||||
if (block.type === 'loop' || block.type === 'parallel') {
|
||||
if (block.type === 'loop' || block.type === 'parallel' || block.type === 'while') {
|
||||
const hasNestedError = nestedSubflowErrors.has(block.id)
|
||||
nodeArray.push({
|
||||
id: block.id,
|
||||
@@ -997,7 +1031,7 @@ const WorkflowContent = React.memo(() => {
|
||||
width: block.data?.width || 500,
|
||||
height: block.data?.height || 300,
|
||||
hasNestedError,
|
||||
kind: block.type === 'loop' ? 'loop' : 'parallel',
|
||||
kind: block.type === 'loop' ? 'loop' : block.type === 'parallel' ? 'parallel' : 'while',
|
||||
},
|
||||
})
|
||||
return
|
||||
@@ -1144,7 +1178,8 @@ const WorkflowContent = React.memo(() => {
|
||||
const sourceParentId =
|
||||
sourceNode.parentId ||
|
||||
(connection.sourceHandle === 'loop-start-source' ||
|
||||
connection.sourceHandle === 'parallel-start-source'
|
||||
connection.sourceHandle === 'parallel-start-source' ||
|
||||
connection.sourceHandle === 'while-start-source'
|
||||
? connection.source
|
||||
: undefined)
|
||||
const targetParentId = targetNode.parentId
|
||||
@@ -1155,7 +1190,8 @@ const WorkflowContent = React.memo(() => {
|
||||
// Special case for container start source: Always allow connections to nodes within the same container
|
||||
if (
|
||||
(connection.sourceHandle === 'loop-start-source' ||
|
||||
connection.sourceHandle === 'parallel-start-source') &&
|
||||
connection.sourceHandle === 'parallel-start-source' ||
|
||||
connection.sourceHandle === 'while-start-source') &&
|
||||
targetNode.parentId === sourceNode.id
|
||||
) {
|
||||
// This is a connection from container start to a node inside the container - always allow
|
||||
@@ -1222,7 +1258,11 @@ const WorkflowContent = React.memo(() => {
|
||||
if (potentialParentId) {
|
||||
const prevElement = document.querySelector(`[data-id="${potentialParentId}"]`)
|
||||
if (prevElement) {
|
||||
prevElement.classList.remove('loop-node-drag-over', 'parallel-node-drag-over')
|
||||
prevElement.classList.remove(
|
||||
'loop-node-drag-over',
|
||||
'parallel-node-drag-over',
|
||||
'while-node-drag-over'
|
||||
)
|
||||
}
|
||||
setPotentialParentId(null)
|
||||
document.body.style.cursor = ''
|
||||
@@ -1342,6 +1382,11 @@ const WorkflowContent = React.memo(() => {
|
||||
(bestContainerMatch.container.data as any)?.kind === 'parallel'
|
||||
) {
|
||||
containerElement.classList.add('parallel-node-drag-over')
|
||||
} else if (
|
||||
bestContainerMatch.container.type === 'subflowNode' &&
|
||||
(bestContainerMatch.container.data as any)?.kind === 'while'
|
||||
) {
|
||||
containerElement.classList.add('while-node-drag-over')
|
||||
}
|
||||
document.body.style.cursor = 'copy'
|
||||
}
|
||||
@@ -1350,7 +1395,11 @@ const WorkflowContent = React.memo(() => {
|
||||
if (potentialParentId) {
|
||||
const prevElement = document.querySelector(`[data-id="${potentialParentId}"]`)
|
||||
if (prevElement) {
|
||||
prevElement.classList.remove('loop-node-drag-over', 'parallel-node-drag-over')
|
||||
prevElement.classList.remove(
|
||||
'loop-node-drag-over',
|
||||
'parallel-node-drag-over',
|
||||
'while-node-drag-over'
|
||||
)
|
||||
}
|
||||
setPotentialParentId(null)
|
||||
document.body.style.cursor = ''
|
||||
@@ -1382,9 +1431,15 @@ const WorkflowContent = React.memo(() => {
|
||||
const onNodeDragStop = useCallback(
|
||||
(_event: React.MouseEvent, node: any) => {
|
||||
// Clear UI effects
|
||||
document.querySelectorAll('.loop-node-drag-over, .parallel-node-drag-over').forEach((el) => {
|
||||
el.classList.remove('loop-node-drag-over', 'parallel-node-drag-over')
|
||||
})
|
||||
document
|
||||
.querySelectorAll('.loop-node-drag-over, .parallel-node-drag-over, .while-node-drag-over')
|
||||
.forEach((el) => {
|
||||
el.classList.remove(
|
||||
'loop-node-drag-over',
|
||||
'parallel-node-drag-over',
|
||||
'while-node-drag-over'
|
||||
)
|
||||
})
|
||||
document.body.style.cursor = ''
|
||||
|
||||
// Emit collaborative position update for the final position
|
||||
@@ -1477,7 +1532,9 @@ const WorkflowContent = React.memo(() => {
|
||||
const startSourceHandle =
|
||||
(containerNode?.data as any)?.kind === 'loop'
|
||||
? 'loop-start-source'
|
||||
: 'parallel-start-source'
|
||||
: (containerNode?.data as any)?.kind === 'parallel'
|
||||
? 'parallel-start-source'
|
||||
: 'while-start-source'
|
||||
|
||||
addEdge({
|
||||
id: crypto.randomUUID(),
|
||||
|
||||
@@ -148,7 +148,7 @@ export function SearchModal({
|
||||
})
|
||||
)
|
||||
|
||||
// Add special blocks (loop and parallel)
|
||||
// Add special blocks (loop, parallel, and while)
|
||||
const specialBlocks: BlockItem[] = [
|
||||
{
|
||||
id: 'loop',
|
||||
@@ -166,6 +166,14 @@ export function SearchModal({
|
||||
bgColor: '#FEE12B',
|
||||
type: 'parallel',
|
||||
},
|
||||
{
|
||||
id: 'while',
|
||||
name: 'While',
|
||||
description: 'While Loop',
|
||||
icon: RepeatIcon,
|
||||
bgColor: '#FF9F43',
|
||||
type: 'while',
|
||||
},
|
||||
]
|
||||
|
||||
return [...regularBlocks, ...specialBlocks].sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useCallback } from 'react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
|
||||
import { WhileTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/while/while-config'
|
||||
|
||||
type WhileToolbarItemProps = {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
// Custom component for the While Tool
|
||||
export default function WhileToolbarItem({ disabled = false }: WhileToolbarItemProps) {
|
||||
const userPermissions = useUserPermissionsContext()
|
||||
|
||||
const handleDragStart = (e: React.DragEvent) => {
|
||||
if (disabled) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
// Only send the essential data for the while node
|
||||
const simplifiedData = {
|
||||
type: 'while',
|
||||
}
|
||||
e.dataTransfer.setData('application/json', JSON.stringify(simplifiedData))
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
}
|
||||
|
||||
// Handle click to add while block
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (disabled) return
|
||||
|
||||
// Dispatch a custom event to be caught by the workflow component
|
||||
const event = new CustomEvent('add-block-from-toolbar', {
|
||||
detail: {
|
||||
type: 'while',
|
||||
clientX: e.clientX,
|
||||
clientY: e.clientY,
|
||||
},
|
||||
})
|
||||
window.dispatchEvent(event)
|
||||
},
|
||||
[disabled]
|
||||
)
|
||||
|
||||
const blockContent = (
|
||||
<div
|
||||
draggable={!disabled}
|
||||
onDragStart={handleDragStart}
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
'group flex h-8 items-center gap-[10px] rounded-[8px] p-2 transition-colors',
|
||||
disabled
|
||||
? 'cursor-not-allowed opacity-60'
|
||||
: 'cursor-pointer hover:bg-muted active:cursor-grabbing'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className='relative flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded-[6px]'
|
||||
style={{ backgroundColor: WhileTool.bgColor }}
|
||||
>
|
||||
<WhileTool.icon
|
||||
className={cn(
|
||||
'h-[14px] w-[14px] text-white transition-transform duration-200',
|
||||
!disabled && 'group-hover:scale-110'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<span className='font-medium text-sm leading-none'>{WhileTool.name}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{blockContent}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{userPermissions.isOfflineMode
|
||||
? 'Connection lost - please refresh'
|
||||
: 'Edit permissions required to add blocks'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return blockContent
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { ToolbarBlock } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-block/toolbar-block'
|
||||
import LoopToolbarItem from '@/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-loop-block/toolbar-loop-block'
|
||||
import ParallelToolbarItem from '@/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-parallel-block/toolbar-parallel-block'
|
||||
import WhileToolbarItem from '@/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-while-block/toolbar-while-block'
|
||||
import { getAllBlocks } from '@/blocks'
|
||||
import type { WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
|
||||
|
||||
@@ -54,7 +55,7 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }:
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
// Create special blocks (loop and parallel) if they match search
|
||||
// Create special blocks (loop, parallel, and while) if they match search
|
||||
const specialBlockItems: BlockItem[] = []
|
||||
|
||||
if (!searchQuery.trim() || 'loop'.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||
@@ -73,6 +74,14 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }:
|
||||
})
|
||||
}
|
||||
|
||||
if (!searchQuery.trim() || 'while'.toLowerCase().includes(searchQuery.toLowerCase())) {
|
||||
specialBlockItems.push({
|
||||
name: 'While',
|
||||
type: 'while',
|
||||
isCustom: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort special blocks alphabetically
|
||||
specialBlockItems.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
@@ -128,7 +137,7 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }:
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Special Blocks Section (Loop & Parallel) */}
|
||||
{/* Special Blocks Section (Loop, Parallel, and While) */}
|
||||
{specialBlocks.map((block) => {
|
||||
if (block.type === 'loop') {
|
||||
return <LoopToolbarItem key={block.type} disabled={!userPermissions.canEdit} />
|
||||
@@ -136,6 +145,9 @@ export function Toolbar({ userPermissions, isWorkspaceSelectorVisible = false }:
|
||||
if (block.type === 'parallel') {
|
||||
return <ParallelToolbarItem key={block.type} disabled={!userPermissions.canEdit} />
|
||||
}
|
||||
if (block.type === 'while') {
|
||||
return <WhileToolbarItem key={block.type} disabled={!userPermissions.canEdit} />
|
||||
}
|
||||
return null
|
||||
})}
|
||||
|
||||
|
||||
@@ -83,6 +83,14 @@ export function WorkflowPreview({
|
||||
}
|
||||
}, [workflowState.parallels, isValidWorkflowState])
|
||||
|
||||
const whilesStructure = useMemo(() => {
|
||||
if (!isValidWorkflowState) return { count: 0, ids: '' }
|
||||
return {
|
||||
count: Object.keys(workflowState.whiles || {}).length,
|
||||
ids: Object.keys(workflowState.whiles || {}).join(','),
|
||||
}
|
||||
}, [workflowState.whiles, isValidWorkflowState])
|
||||
|
||||
const edgesStructure = useMemo(() => {
|
||||
if (!isValidWorkflowState) return { count: 0, ids: '' }
|
||||
return {
|
||||
@@ -166,6 +174,26 @@ export function WorkflowPreview({
|
||||
return
|
||||
}
|
||||
|
||||
if (block.type === 'while') {
|
||||
nodeArray.push({
|
||||
id: block.id,
|
||||
type: 'subflowNode',
|
||||
position: absolutePosition,
|
||||
parentId: block.data?.parentId,
|
||||
extent: block.data?.extent || undefined,
|
||||
draggable: false,
|
||||
data: {
|
||||
...block.data,
|
||||
width: block.data?.width || 500,
|
||||
height: block.data?.height || 300,
|
||||
state: 'valid',
|
||||
isPreview: true,
|
||||
kind: 'while',
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const blockConfig = getBlock(block.type)
|
||||
if (!blockConfig) {
|
||||
logger.error(`No configuration found for block type: ${block.type}`, { blockId })
|
||||
@@ -229,6 +257,7 @@ export function WorkflowPreview({
|
||||
blocksStructure,
|
||||
loopsStructure,
|
||||
parallelsStructure,
|
||||
whilesStructure,
|
||||
showSubBlocks,
|
||||
workflowState.blocks,
|
||||
isValidWorkflowState,
|
||||
|
||||
@@ -129,7 +129,7 @@ export async function executeWebhookJob(payload: WebhookExecutionPayload) {
|
||||
edges,
|
||||
loops || {},
|
||||
parallels || {},
|
||||
true // Enable validation during execution
|
||||
{} // Enable validation during execution
|
||||
)
|
||||
|
||||
// Handle special Airtable case
|
||||
|
||||
@@ -118,7 +118,7 @@ export async function executeWorkflowJob(payload: WorkflowExecutionPayload) {
|
||||
edges,
|
||||
loops || {},
|
||||
parallels || {},
|
||||
true // Enable validation during execution
|
||||
{} // Enable validation during execution
|
||||
)
|
||||
|
||||
// Create executor and execute
|
||||
|
||||
@@ -55,6 +55,7 @@ const BLOCK_COLORS = {
|
||||
DEFAULT: '#2F55FF',
|
||||
LOOP: '#2FB3FF',
|
||||
PARALLEL: '#FEE12B',
|
||||
WHILE: '#57D9A3',
|
||||
} as const
|
||||
|
||||
const TAG_PREFIXES = {
|
||||
@@ -294,6 +295,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const blocks = useWorkflowStore((state) => state.blocks)
|
||||
const loops = useWorkflowStore((state) => state.loops)
|
||||
const parallels = useWorkflowStore((state) => state.parallels)
|
||||
const whiles = useWorkflowStore((state) => state.whiles)
|
||||
const edges = useWorkflowStore((state) => state.edges)
|
||||
const workflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
|
||||
|
||||
@@ -321,7 +323,11 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const blockConfig = getBlock(sourceBlock.type)
|
||||
|
||||
if (!blockConfig) {
|
||||
if (sourceBlock.type === 'loop' || sourceBlock.type === 'parallel') {
|
||||
if (
|
||||
sourceBlock.type === 'loop' ||
|
||||
sourceBlock.type === 'parallel' ||
|
||||
sourceBlock.type === 'while'
|
||||
) {
|
||||
const mockConfig = {
|
||||
outputs: {
|
||||
results: 'array',
|
||||
@@ -467,13 +473,26 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}
|
||||
|
||||
const serializer = new Serializer()
|
||||
const serializedWorkflow = serializer.serializeWorkflow(blocks, edges, loops, parallels)
|
||||
const serializedWorkflow = serializer.serializeWorkflow(blocks, edges, loops, parallels, whiles)
|
||||
|
||||
const accessibleBlockIds = BlockPathCalculator.findAllPathNodes(
|
||||
serializedWorkflow.connections,
|
||||
blockId
|
||||
)
|
||||
|
||||
// If editing a while block condition, also include children inside the while container
|
||||
const sourceBlock = blocks[blockId]
|
||||
if (sourceBlock && sourceBlock.type === 'while') {
|
||||
const whileCfg = whiles[blockId]
|
||||
if (whileCfg && Array.isArray(whileCfg.nodes)) {
|
||||
whileCfg.nodes.forEach((childId: string) => {
|
||||
if (!accessibleBlockIds.includes(childId)) {
|
||||
accessibleBlockIds.push(childId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const starterBlock = Object.values(blocks).find((block) => block.type === 'starter')
|
||||
if (starterBlock && !accessibleBlockIds.includes(starterBlock.id)) {
|
||||
accessibleBlockIds.push(starterBlock.id)
|
||||
@@ -551,6 +570,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}
|
||||
|
||||
let parallelBlockGroup: BlockTagGroup | null = null
|
||||
let whileBlockGroup: BlockTagGroup | null = null
|
||||
const containingParallel = Object.entries(parallels || {}).find(([_, parallel]) =>
|
||||
parallel.nodes.includes(blockId)
|
||||
)
|
||||
@@ -579,6 +599,27 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const containingWhile = Object.entries(whiles || {}).find(([_, w]) => w.nodes.includes(blockId))
|
||||
let containingWhileBlockId: string | null = null
|
||||
if (containingWhile) {
|
||||
const [whileId] = containingWhile
|
||||
containingWhileBlockId = whileId
|
||||
const contextualTags: string[] = ['index']
|
||||
|
||||
const containingWhileBlock = blocks[whileId]
|
||||
if (containingWhileBlock) {
|
||||
const whileBlockName = containingWhileBlock.name || containingWhileBlock.type
|
||||
|
||||
whileBlockGroup = {
|
||||
blockName: whileBlockName,
|
||||
blockId: whileId,
|
||||
blockType: 'while',
|
||||
tags: contextualTags,
|
||||
distance: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blockTagGroups: BlockTagGroup[] = []
|
||||
const allBlockTags: string[] = []
|
||||
|
||||
@@ -589,11 +630,16 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const blockConfig = getBlock(accessibleBlock.type)
|
||||
|
||||
if (!blockConfig) {
|
||||
if (accessibleBlock.type === 'loop' || accessibleBlock.type === 'parallel') {
|
||||
if (
|
||||
accessibleBlock.type === 'loop' ||
|
||||
accessibleBlock.type === 'parallel' ||
|
||||
accessibleBlock.type === 'while'
|
||||
) {
|
||||
// Skip this block if it's the containing loop/parallel block - we'll handle it with contextual tags
|
||||
if (
|
||||
accessibleBlockId === containingLoopBlockId ||
|
||||
accessibleBlockId === containingParallelBlockId
|
||||
accessibleBlockId === containingParallelBlockId ||
|
||||
accessibleBlockId === containingWhileBlockId
|
||||
) {
|
||||
continue
|
||||
}
|
||||
@@ -729,6 +775,9 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
if (parallelBlockGroup) {
|
||||
finalBlockTagGroups.push(parallelBlockGroup)
|
||||
}
|
||||
if (whileBlockGroup) {
|
||||
finalBlockTagGroups.push(whileBlockGroup)
|
||||
}
|
||||
|
||||
blockTagGroups.sort((a, b) => a.distance - b.distance)
|
||||
finalBlockTagGroups.push(...blockTagGroups)
|
||||
@@ -740,13 +789,16 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
if (parallelBlockGroup) {
|
||||
contextualTags.push(...parallelBlockGroup.tags)
|
||||
}
|
||||
if (whileBlockGroup) {
|
||||
contextualTags.push(...whileBlockGroup.tags)
|
||||
}
|
||||
|
||||
return {
|
||||
tags: [...variableTags, ...contextualTags, ...allBlockTags],
|
||||
variableInfoMap,
|
||||
blockTagGroups: finalBlockTagGroups,
|
||||
}
|
||||
}, [blocks, edges, loops, parallels, blockId, activeSourceBlockId, workflowVariables])
|
||||
}, [blocks, edges, loops, parallels, whiles, blockId, activeSourceBlockId, workflowVariables])
|
||||
|
||||
const filteredTags = useMemo(() => {
|
||||
if (!searchTerm) return tags
|
||||
@@ -806,9 +858,11 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
})
|
||||
} else {
|
||||
const path = tagParts.slice(1).join('.')
|
||||
// Handle contextual tags for loop/parallel blocks (single words like 'index', 'currentItem')
|
||||
// Handle contextual tags for loop/parallel/while blocks (single words like 'index', 'currentItem')
|
||||
if (
|
||||
(group.blockType === 'loop' || group.blockType === 'parallel') &&
|
||||
(group.blockType === 'loop' ||
|
||||
group.blockType === 'parallel' ||
|
||||
group.blockType === 'while') &&
|
||||
tagParts.length === 1
|
||||
) {
|
||||
directTags.push({
|
||||
@@ -912,7 +966,9 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}
|
||||
} else if (
|
||||
blockGroup &&
|
||||
(blockGroup.blockType === 'loop' || blockGroup.blockType === 'parallel')
|
||||
(blockGroup.blockType === 'loop' ||
|
||||
blockGroup.blockType === 'parallel' ||
|
||||
blockGroup.blockType === 'while')
|
||||
) {
|
||||
if (!tag.includes('.') && ['index', 'currentItem', 'items'].includes(tag)) {
|
||||
processedTag = `${blockGroup.blockType}.${tag}`
|
||||
@@ -1283,6 +1339,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
blockColor = BLOCK_COLORS.LOOP
|
||||
} else if (group.blockType === 'parallel') {
|
||||
blockColor = BLOCK_COLORS.PARALLEL
|
||||
} else if (group.blockType === 'while') {
|
||||
blockColor = BLOCK_COLORS.WHILE
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -1305,7 +1363,9 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
let tagIcon = group.blockName.charAt(0).toUpperCase()
|
||||
|
||||
if (
|
||||
(group.blockType === 'loop' || group.blockType === 'parallel') &&
|
||||
(group.blockType === 'loop' ||
|
||||
group.blockType === 'parallel' ||
|
||||
group.blockType === 'while') &&
|
||||
!nestedTag.key.includes('.')
|
||||
) {
|
||||
if (nestedTag.key === 'index') {
|
||||
|
||||
@@ -391,6 +391,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
||||
edges: mergedEdges,
|
||||
loops: workflowState.loops || existing.loops || {},
|
||||
parallels: workflowState.parallels || existing.parallels || {},
|
||||
whiles: workflowState.whiles || existing.whiles || {},
|
||||
lastSaved: workflowState.lastSaved || existing.lastSaved || Date.now(),
|
||||
isDeployed: workflowState.isDeployed ?? existing.isDeployed ?? false,
|
||||
deployedAt: workflowState.deployedAt || existing.deployedAt,
|
||||
@@ -532,6 +533,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
|
||||
edges: mergedEdges,
|
||||
loops: workflowState.loops || existing.loops || {},
|
||||
parallels: workflowState.parallels || existing.parallels || {},
|
||||
whiles: workflowState.whiles || existing.whiles || {},
|
||||
lastSaved: workflowState.lastSaved || existing.lastSaved || Date.now(),
|
||||
isDeployed: workflowState.isDeployed ?? existing.isDeployed ?? false,
|
||||
deployedAt: workflowState.deployedAt || existing.deployedAt,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
export enum BlockType {
|
||||
PARALLEL = 'parallel',
|
||||
LOOP = 'loop',
|
||||
WHILE = 'while',
|
||||
ROUTER = 'router',
|
||||
CONDITION = 'condition',
|
||||
FUNCTION = 'function',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { FunctionBlockHandler } from '@/executor/handlers/function/function-hand
|
||||
import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler'
|
||||
import { LoopBlockHandler } from '@/executor/handlers/loop/loop-handler'
|
||||
import { ParallelBlockHandler } from '@/executor/handlers/parallel/parallel-handler'
|
||||
// import { WhileBlockHandler } from '@/executor/handlers/while/while-handler'
|
||||
import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler'
|
||||
import { RouterBlockHandler } from '@/executor/handlers/router/router-handler'
|
||||
import { TriggerBlockHandler } from '@/executor/handlers/trigger/trigger-handler'
|
||||
@@ -20,6 +21,7 @@ export {
|
||||
GenericBlockHandler,
|
||||
LoopBlockHandler,
|
||||
ParallelBlockHandler,
|
||||
// WhileBlockHandler,
|
||||
ResponseBlockHandler,
|
||||
RouterBlockHandler,
|
||||
TriggerBlockHandler,
|
||||
|
||||
0
apps/sim/executor/handlers/while/while-handler.ts
Normal file
0
apps/sim/executor/handlers/while/while-handler.ts
Normal file
@@ -750,6 +750,7 @@ describe('Executor', () => {
|
||||
],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
whiles: {},
|
||||
}
|
||||
|
||||
const executor = new Executor(routerWorkflow)
|
||||
@@ -1066,6 +1067,7 @@ describe('Executor', () => {
|
||||
],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
whiles: {},
|
||||
}
|
||||
|
||||
const executor = new Executor(workflow)
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
GenericBlockHandler,
|
||||
LoopBlockHandler,
|
||||
ParallelBlockHandler,
|
||||
// WhileBlockHandler,
|
||||
ResponseBlockHandler,
|
||||
RouterBlockHandler,
|
||||
TriggerBlockHandler,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
} from '@/executor/handlers'
|
||||
import { LoopManager } from '@/executor/loops/loops'
|
||||
import { ParallelManager } from '@/executor/parallels/parallels'
|
||||
// import { WhileManager } from '@/executor/whiles/whiles'
|
||||
import { PathTracker } from '@/executor/path/path'
|
||||
import { InputResolver } from '@/executor/resolver/resolver'
|
||||
import type {
|
||||
@@ -73,6 +75,7 @@ export class Executor {
|
||||
private resolver: InputResolver
|
||||
private loopManager: LoopManager
|
||||
private parallelManager: ParallelManager
|
||||
// private whileManager: WhileManager
|
||||
private pathTracker: PathTracker
|
||||
private blockHandlers: BlockHandler[]
|
||||
private workflowInput: any
|
||||
@@ -134,6 +137,7 @@ export class Executor {
|
||||
|
||||
this.loopManager = new LoopManager(this.actualWorkflow.loops || {})
|
||||
this.parallelManager = new ParallelManager(this.actualWorkflow.parallels || {})
|
||||
// this.whileManager = new WhileManager(this.actualWorkflow.whiles || {})
|
||||
|
||||
// Calculate accessible blocks for consistent reference resolution
|
||||
const accessibleBlocksMap = BlockPathCalculator.calculateAccessibleBlocksForWorkflow(
|
||||
@@ -159,6 +163,7 @@ export class Executor {
|
||||
new ApiBlockHandler(),
|
||||
new LoopBlockHandler(this.resolver, this.pathTracker),
|
||||
new ParallelBlockHandler(this.resolver, this.pathTracker),
|
||||
// new WhileBlockHandler(this.resolver, this.pathTracker),
|
||||
new ResponseBlockHandler(),
|
||||
new WorkflowBlockHandler(),
|
||||
new GenericBlockHandler(),
|
||||
@@ -417,6 +422,9 @@ export class Executor {
|
||||
// Process parallel iterations - similar to loops but conceptually for parallel execution
|
||||
await this.parallelManager.processParallelIterations(context)
|
||||
|
||||
// Process while iterations - similar concept to loops but condition-driven
|
||||
// await this.whileManager.processWhileIterations(context)
|
||||
|
||||
// Continue execution for any newly activated paths
|
||||
// Only stop execution if there are no more blocks to execute
|
||||
const updatedNextLayer = this.getNextExecutionLayer(context)
|
||||
@@ -560,6 +568,7 @@ export class Executor {
|
||||
}
|
||||
await this.loopManager.processLoopIterations(context)
|
||||
await this.parallelManager.processParallelIterations(context)
|
||||
// await this.whileManager.processWhileIterations(context)
|
||||
const nextLayer = this.getNextExecutionLayer(context)
|
||||
setPendingBlocks(nextLayer)
|
||||
|
||||
@@ -759,6 +768,13 @@ export class Executor {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize while iterations
|
||||
if (this.actualWorkflow.whiles) {
|
||||
for (const whileId of Object.keys(this.actualWorkflow.whiles)) {
|
||||
context.loopIterations.set(whileId, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which block to initialize as the starting point
|
||||
let initBlock: SerializedBlock | undefined
|
||||
if (startBlockId) {
|
||||
@@ -1207,6 +1223,20 @@ export class Executor {
|
||||
return loopCompleted
|
||||
}
|
||||
|
||||
// Special handling for while-start-source connections
|
||||
if (conn.sourceHandle === 'while-start-source') {
|
||||
// Activated when while block executes
|
||||
return sourceExecuted
|
||||
}
|
||||
|
||||
// Special handling for while-end-source connections
|
||||
if (conn.sourceHandle === 'while-end-source') {
|
||||
// Activated when while block has completed (condition false or max iterations)
|
||||
const whileState = context.blockStates.get(conn.source)
|
||||
const whileCompleted = Boolean(whileState?.output?.completed)
|
||||
return sourceExecuted && whileCompleted
|
||||
}
|
||||
|
||||
// Special handling for parallel-start-source connections
|
||||
if (conn.sourceHandle === 'parallel-start-source') {
|
||||
// This block is connected to a parallel's start output
|
||||
@@ -1643,7 +1673,11 @@ export class Executor {
|
||||
context.blockLogs.push(blockLog)
|
||||
|
||||
// Skip console logging for infrastructure blocks like loops and parallels
|
||||
if (block.metadata?.id !== BlockType.LOOP && block.metadata?.id !== BlockType.PARALLEL) {
|
||||
if (
|
||||
block.metadata?.id !== BlockType.LOOP &&
|
||||
block.metadata?.id !== BlockType.PARALLEL &&
|
||||
block.metadata?.id !== BlockType.WHILE
|
||||
) {
|
||||
// Determine iteration context for this block
|
||||
let iterationCurrent: number | undefined
|
||||
let iterationTotal: number | undefined
|
||||
@@ -1755,7 +1789,11 @@ export class Executor {
|
||||
context.blockLogs.push(blockLog)
|
||||
|
||||
// Skip console logging for infrastructure blocks like loops and parallels
|
||||
if (block.metadata?.id !== BlockType.LOOP && block.metadata?.id !== BlockType.PARALLEL) {
|
||||
if (
|
||||
block.metadata?.id !== BlockType.LOOP &&
|
||||
block.metadata?.id !== BlockType.PARALLEL
|
||||
// block.metadata?.id !== BlockType.WHILE
|
||||
) {
|
||||
// Determine iteration context for this block
|
||||
let iterationCurrent: number | undefined
|
||||
let iterationTotal: number | undefined
|
||||
@@ -1927,6 +1965,7 @@ export class Executor {
|
||||
block?.metadata?.id === BlockType.CONDITION ||
|
||||
block?.metadata?.id === BlockType.LOOP ||
|
||||
block?.metadata?.id === BlockType.PARALLEL
|
||||
// block?.metadata?.id === BlockType.WHILE
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export class InputResolver {
|
||||
private blockByNormalizedName: Map<string, SerializedBlock>
|
||||
private loopsByBlockId: Map<string, string> // Maps block ID to containing loop ID
|
||||
private parallelsByBlockId: Map<string, string> // Maps block ID to containing parallel ID
|
||||
// private whilesByBlockId: Map<string, string> // Maps block ID to containing while ID
|
||||
|
||||
constructor(
|
||||
private workflow: SerializedWorkflow,
|
||||
@@ -70,6 +71,14 @@ export class InputResolver {
|
||||
this.parallelsByBlockId.set(blockId, parallelId)
|
||||
}
|
||||
}
|
||||
|
||||
// Create efficient while lookup map
|
||||
// this.whilesByBlockId = new Map()
|
||||
// for (const [whileId, whileCfg] of Object.entries(workflow.whiles || {})) {
|
||||
// for (const blockId of whileCfg.nodes) {
|
||||
// this.whilesByBlockId.set(blockId, whileId)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1103,6 +1112,17 @@ export class InputResolver {
|
||||
}
|
||||
}
|
||||
|
||||
// Special case: blocks in the same while can reference each other
|
||||
// const currentBlockWhile = this.whilesByBlockId.get(currentBlockId)
|
||||
// if (currentBlockWhile) {
|
||||
// const whileCfg = this.workflow.whiles?.[currentBlockWhile]
|
||||
// if (whileCfg) {
|
||||
// for (const nodeId of whileCfg.nodes) {
|
||||
// accessibleBlocks.add(nodeId)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return accessibleBlocks
|
||||
}
|
||||
|
||||
@@ -1867,4 +1887,14 @@ export class InputResolver {
|
||||
getContainingParallelId(blockId: string): string | undefined {
|
||||
return this.parallelsByBlockId.get(blockId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the containing while ID for a block
|
||||
* @param blockId - The ID of the block
|
||||
* @returns The containing while ID or undefined if not in a while
|
||||
*/
|
||||
getContainingWhileId(blockId: string): string | undefined {
|
||||
// return this.whilesByBlockId.get(blockId)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ export class Routing {
|
||||
// Flow control blocks
|
||||
[BlockType.PARALLEL]: BlockCategory.FLOW_CONTROL,
|
||||
[BlockType.LOOP]: BlockCategory.FLOW_CONTROL,
|
||||
[BlockType.WHILE]: BlockCategory.FLOW_CONTROL,
|
||||
[BlockType.WORKFLOW]: BlockCategory.FLOW_CONTROL,
|
||||
|
||||
// Routing blocks
|
||||
@@ -139,6 +140,8 @@ export class Routing {
|
||||
'parallel-end-source',
|
||||
'loop-start-source',
|
||||
'loop-end-source',
|
||||
'while-start-source',
|
||||
'while-end-source',
|
||||
]
|
||||
|
||||
if (flowControlHandles.includes(sourceHandle || '')) {
|
||||
|
||||
@@ -145,6 +145,17 @@ export interface ExecutionContext {
|
||||
}
|
||||
>
|
||||
|
||||
// While execution tracking
|
||||
whileExecutions?: Map<
|
||||
string,
|
||||
{
|
||||
maxIterations: number
|
||||
loopType: 'while' | 'doWhile'
|
||||
executionResults: Map<string, any> // iteration_0, iteration_1, etc.
|
||||
currentIteration: number
|
||||
}
|
||||
>
|
||||
|
||||
// Mapping for virtual parallel block IDs to their original blocks
|
||||
parallelBlockMapping?: Map<
|
||||
string,
|
||||
|
||||
0
apps/sim/executor/whiles/whiles.test.ts
Normal file
0
apps/sim/executor/whiles/whiles.test.ts
Normal file
0
apps/sim/executor/whiles/whiles.ts
Normal file
0
apps/sim/executor/whiles/whiles.ts
Normal file
@@ -562,8 +562,8 @@ export function useCollaborativeWorkflow() {
|
||||
|
||||
const blockConfig = getBlock(type)
|
||||
|
||||
// Handle loop/parallel blocks that don't use BlockConfig
|
||||
if (!blockConfig && (type === 'loop' || type === 'parallel')) {
|
||||
// Handle loop/parallel/while blocks that don't use BlockConfig
|
||||
if (!blockConfig && (type === 'loop' || type === 'parallel' || type === 'while')) {
|
||||
// For loop/parallel blocks, use empty subBlocks and outputs
|
||||
const completeBlockData = {
|
||||
id,
|
||||
@@ -1129,6 +1129,16 @@ export function useCollaborativeWorkflow() {
|
||||
[executeQueuedOperation, workflowStore]
|
||||
)
|
||||
|
||||
// UI-only while type toggle (no server op yet)
|
||||
const collaborativeUpdateWhileType = useCallback(
|
||||
(whileId: string, whileType: 'while' | 'doWhile') => {
|
||||
const currentBlock = workflowStore.blocks[whileId]
|
||||
if (!currentBlock || currentBlock.type !== 'while') return
|
||||
workflowStore.updateWhileType(whileId, whileType)
|
||||
},
|
||||
[workflowStore]
|
||||
)
|
||||
|
||||
// Unified iteration management functions - count and collection only
|
||||
const collaborativeUpdateIterationCount = useCallback(
|
||||
(nodeId: string, iterationType: 'loop' | 'parallel', count: number) => {
|
||||
@@ -1321,6 +1331,7 @@ export function useCollaborativeWorkflow() {
|
||||
// Collaborative loop/parallel operations
|
||||
collaborativeUpdateLoopType,
|
||||
collaborativeUpdateParallelType,
|
||||
collaborativeUpdateWhileType,
|
||||
|
||||
// Unified iteration operations
|
||||
collaborativeUpdateIterationCount,
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface NormalizedWorkflowData {
|
||||
edges: any[]
|
||||
loops: Record<string, any>
|
||||
parallels: Record<string, any>
|
||||
whiles: Record<string, any>
|
||||
isFromNormalizedTables: boolean // Flag to indicate source (true = normalized tables, false = deployed state)
|
||||
}
|
||||
|
||||
@@ -49,6 +50,7 @@ export async function loadDeployedWorkflowState(
|
||||
edges: deployedState.edges || [],
|
||||
loops: deployedState.loops || {},
|
||||
parallels: deployedState.parallels || {},
|
||||
whiles: deployedState.whiles || {},
|
||||
isFromNormalizedTables: false, // Flag to indicate this came from deployed state
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -126,6 +128,7 @@ export async function loadWorkflowFromNormalizedTables(
|
||||
// Convert subflows to loops and parallels
|
||||
const loops: Record<string, any> = {}
|
||||
const parallels: Record<string, any> = {}
|
||||
const whiles: Record<string, any> = {}
|
||||
|
||||
subflows.forEach((subflow) => {
|
||||
const config = subflow.config || {}
|
||||
@@ -140,6 +143,11 @@ export async function loadWorkflowFromNormalizedTables(
|
||||
id: subflow.id,
|
||||
...config,
|
||||
}
|
||||
} else if (subflow.type === SUBFLOW_TYPES.WHILE) {
|
||||
whiles[subflow.id] = {
|
||||
id: subflow.id,
|
||||
...config,
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Unknown subflow type: ${subflow.type} for subflow ${subflow.id}`)
|
||||
}
|
||||
@@ -150,6 +158,7 @@ export async function loadWorkflowFromNormalizedTables(
|
||||
edges: edgesArray,
|
||||
loops,
|
||||
parallels,
|
||||
whiles,
|
||||
isFromNormalizedTables: true,
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -238,6 +247,15 @@ export async function saveWorkflowToNormalizedTables(
|
||||
})
|
||||
})
|
||||
|
||||
Object.values(state.whiles || {}).forEach((whileSubflow) => {
|
||||
subflowInserts.push({
|
||||
id: whileSubflow.id,
|
||||
workflowId: workflowId,
|
||||
type: SUBFLOW_TYPES.WHILE,
|
||||
config: whileSubflow,
|
||||
})
|
||||
})
|
||||
|
||||
if (subflowInserts.length > 0) {
|
||||
await tx.insert(workflowSubflows).values(subflowInserts)
|
||||
}
|
||||
@@ -251,6 +269,7 @@ export async function saveWorkflowToNormalizedTables(
|
||||
edges: state.edges,
|
||||
loops: state.loops || {},
|
||||
parallels: state.parallels || {},
|
||||
whiles: state.whiles || {},
|
||||
lastSaved: Date.now(),
|
||||
isDeployed: state.isDeployed,
|
||||
deployedAt: state.deployedAt,
|
||||
@@ -303,6 +322,7 @@ export async function migrateWorkflowToNormalizedTables(
|
||||
edges: jsonState.edges || [],
|
||||
loops: jsonState.loops || {},
|
||||
parallels: jsonState.parallels || {},
|
||||
whiles: jsonState.whiles || {},
|
||||
lastSaved: jsonState.lastSaved,
|
||||
isDeployed: jsonState.isDeployed,
|
||||
deployedAt: jsonState.deployedAt,
|
||||
|
||||
@@ -15,6 +15,7 @@ export function buildWorkflowStateForTemplate(workflowId: string) {
|
||||
// Generate loops and parallels in the same format as deployment
|
||||
const loops = workflowStore.generateLoopBlocks()
|
||||
const parallels = workflowStore.generateParallelBlocks()
|
||||
const whiles = workflowStore.generateWhileBlocks()
|
||||
|
||||
// Build the state object in the same format as deployment
|
||||
const state = {
|
||||
@@ -22,6 +23,7 @@ export function buildWorkflowStateForTemplate(workflowId: string) {
|
||||
edges,
|
||||
loops,
|
||||
parallels,
|
||||
whiles,
|
||||
lastSaved: Date.now(),
|
||||
}
|
||||
|
||||
|
||||
@@ -330,6 +330,32 @@ export function hasWorkflowChanged(
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Compare whiles
|
||||
|
||||
const currentWhiles = currentState.whiles || {}
|
||||
const deployedWhiles = deployedState.whiles || {}
|
||||
|
||||
const currentWhileIds = Object.keys(currentWhiles).sort()
|
||||
const deployedWhileIds = Object.keys(deployedWhiles).sort()
|
||||
|
||||
if (
|
||||
currentWhileIds.length !== deployedWhileIds.length ||
|
||||
normalizedStringify(currentWhileIds) !== normalizedStringify(deployedWhileIds)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Compare each while with normalized values
|
||||
for (const whileId of currentWhileIds) {
|
||||
const normalizedCurrentWhile = normalizeValue(currentWhiles[whileId])
|
||||
const normalizedDeployedWhile = normalizeValue(deployedWhiles[whileId])
|
||||
|
||||
if (
|
||||
normalizedStringify(normalizedCurrentWhile) !== normalizedStringify(normalizedDeployedWhile)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getBlock } from '@/blocks'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
|
||||
import type { BlockState, Loop, Parallel, While } from '@/stores/workflows/workflow/types'
|
||||
import { getTool } from '@/tools/utils'
|
||||
|
||||
const logger = createLogger('Serializer')
|
||||
@@ -27,6 +27,7 @@ export class Serializer {
|
||||
edges: Edge[],
|
||||
loops: Record<string, Loop>,
|
||||
parallels?: Record<string, Parallel>,
|
||||
whiles?: Record<string, While>,
|
||||
validateRequired = false
|
||||
): SerializedWorkflow {
|
||||
return {
|
||||
@@ -40,12 +41,13 @@ export class Serializer {
|
||||
})),
|
||||
loops,
|
||||
parallels,
|
||||
whiles,
|
||||
}
|
||||
}
|
||||
|
||||
private serializeBlock(block: BlockState, validateRequired = false): SerializedBlock {
|
||||
// Special handling for subflow blocks (loops, parallels, etc.)
|
||||
if (block.type === 'loop' || block.type === 'parallel') {
|
||||
if (block.type === 'loop' || block.type === 'parallel' || block.type === 'while') {
|
||||
return {
|
||||
id: block.id,
|
||||
position: block.position,
|
||||
@@ -58,9 +60,15 @@ export class Serializer {
|
||||
metadata: {
|
||||
id: block.type,
|
||||
name: block.name,
|
||||
description: block.type === 'loop' ? 'Loop container' : 'Parallel container',
|
||||
description:
|
||||
block.type === 'loop'
|
||||
? 'Loop container'
|
||||
: block.type === 'parallel'
|
||||
? 'Parallel container'
|
||||
: 'While container',
|
||||
category: 'subflow',
|
||||
color: block.type === 'loop' ? '#3b82f6' : '#8b5cf6',
|
||||
color:
|
||||
block.type === 'loop' ? '#3b82f6' : block.type === 'parallel' ? '#8b5cf6' : '#FF9F43', // Orange color for while blocks
|
||||
},
|
||||
enabled: block.enabled,
|
||||
}
|
||||
@@ -211,8 +219,8 @@ export class Serializer {
|
||||
|
||||
private extractParams(block: BlockState): Record<string, any> {
|
||||
// Special handling for subflow blocks (loops, parallels, etc.)
|
||||
if (block.type === 'loop' || block.type === 'parallel') {
|
||||
return {} // Loop and parallel blocks don't have traditional params
|
||||
if (block.type === 'loop' || block.type === 'parallel' || block.type === 'while') {
|
||||
return {} // Loop, parallel, and while blocks don't have traditional params
|
||||
}
|
||||
|
||||
const blockConfig = getBlock(block.type)
|
||||
@@ -359,13 +367,15 @@ export class Serializer {
|
||||
}
|
||||
|
||||
// Special handling for subflow blocks (loops, parallels, etc.)
|
||||
if (blockType === 'loop' || blockType === 'parallel') {
|
||||
if (blockType === 'loop' || blockType === 'parallel' || blockType === 'while') {
|
||||
return {
|
||||
id: serializedBlock.id,
|
||||
type: blockType,
|
||||
name: serializedBlock.metadata?.name || (blockType === 'loop' ? 'Loop' : 'Parallel'),
|
||||
name:
|
||||
serializedBlock.metadata?.name ||
|
||||
(blockType === 'loop' ? 'Loop' : blockType === 'parallel' ? 'Parallel' : 'While'),
|
||||
position: serializedBlock.position,
|
||||
subBlocks: {}, // Loops and parallels don't have traditional subBlocks
|
||||
subBlocks: {}, // Loops, parallels, and whiles don't have traditional subBlocks
|
||||
outputs: serializedBlock.outputs,
|
||||
enabled: serializedBlock.enabled ?? true,
|
||||
data: serializedBlock.config.params, // Preserve the data (parallelType, count, etc.)
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface SerializedWorkflow {
|
||||
connections: SerializedConnection[]
|
||||
loops: Record<string, SerializedLoop>
|
||||
parallels?: Record<string, SerializedParallel>
|
||||
whiles?: Record<string, SerializedWhile>
|
||||
}
|
||||
|
||||
export interface SerializedConnection {
|
||||
@@ -55,3 +56,10 @@ export interface SerializedParallel {
|
||||
count?: number // Number of parallel executions for count-based parallel
|
||||
parallelType?: 'count' | 'collection' // Explicit parallel type to avoid inference bugs
|
||||
}
|
||||
|
||||
export interface SerializedWhile {
|
||||
id: string
|
||||
nodes: string[]
|
||||
iterations: number
|
||||
whileType?: 'while' | 'doWhile'
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ async function insertAutoConnectEdge(
|
||||
enum SubflowType {
|
||||
LOOP = 'loop',
|
||||
PARALLEL = 'parallel',
|
||||
WHILE = 'while',
|
||||
}
|
||||
|
||||
// Helper function to check if a block type is a subflow type
|
||||
@@ -134,6 +135,7 @@ export async function getWorkflowState(workflowId: string) {
|
||||
edges: normalizedData.edges,
|
||||
loops: normalizedData.loops,
|
||||
parallels: normalizedData.parallels,
|
||||
whiles: normalizedData.whiles,
|
||||
lastSaved: Date.now(),
|
||||
isDeployed: workflowData[0].isDeployed || false,
|
||||
deployedAt: workflowData[0].deployedAt,
|
||||
@@ -280,7 +282,7 @@ async function handleBlockOperationTx(
|
||||
throw insertError
|
||||
}
|
||||
|
||||
// Auto-create subflow entry for loop/parallel blocks
|
||||
// Auto-create subflow entry for loop/parallel/while blocks
|
||||
if (isSubflowBlockType(payload.type)) {
|
||||
try {
|
||||
const subflowConfig =
|
||||
@@ -672,7 +674,7 @@ async function handleBlockOperationTx(
|
||||
throw insertError
|
||||
}
|
||||
|
||||
// Auto-create subflow entry for loop/parallel blocks
|
||||
// Auto-create subflow entry for loop/parallel/while blocks
|
||||
if (isSubflowBlockType(payload.type)) {
|
||||
try {
|
||||
const subflowConfig =
|
||||
@@ -832,7 +834,7 @@ async function handleSubflowOperationTx(
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
|
||||
} else if (payload.type === 'parallel') {
|
||||
} else if (payload.type === 'parallel' || payload.type === 'while') {
|
||||
// Update the parallel block's data properties
|
||||
const blockData = {
|
||||
...payload.config,
|
||||
|
||||
@@ -18,7 +18,11 @@ import type {
|
||||
SyncControl,
|
||||
WorkflowState,
|
||||
} from '@/stores/workflows/workflow/types'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
import {
|
||||
generateLoopBlocks,
|
||||
generateParallelBlocks,
|
||||
generateWhileBlocks,
|
||||
} from '@/stores/workflows/workflow/utils'
|
||||
|
||||
const logger = createLogger('WorkflowStore')
|
||||
|
||||
@@ -35,6 +39,7 @@ const initialState = {
|
||||
deploymentStatuses: {},
|
||||
needsRedeployment: false,
|
||||
hasActiveWebhook: false,
|
||||
whiles: {},
|
||||
history: {
|
||||
past: [],
|
||||
present: {
|
||||
@@ -43,6 +48,7 @@ const initialState = {
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
whiles: {},
|
||||
isDeployed: false,
|
||||
isPublished: false,
|
||||
},
|
||||
@@ -106,8 +112,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
}
|
||||
) => {
|
||||
const blockConfig = getBlock(type)
|
||||
// For custom nodes like loop and parallel that don't use BlockConfig
|
||||
if (!blockConfig && (type === 'loop' || type === 'parallel')) {
|
||||
// For custom nodes like loop, parallel, and while that don't use BlockConfig
|
||||
if (!blockConfig && (type === 'loop' || type === 'parallel' || type === 'while')) {
|
||||
// Merge parentId and extent into data if provided
|
||||
const nodeData = {
|
||||
...data,
|
||||
@@ -136,6 +142,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: [...get().edges],
|
||||
loops: get().generateLoopBlocks(),
|
||||
parallels: get().generateParallelBlocks(),
|
||||
whiles: get().generateWhileBlocks(),
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -187,6 +194,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: [...get().edges],
|
||||
loops: get().generateLoopBlocks(),
|
||||
parallels: get().generateParallelBlocks(),
|
||||
whiles: get().generateWhileBlocks(),
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -287,6 +295,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: [...get().edges],
|
||||
loops: { ...get().loops },
|
||||
parallels: { ...get().parallels },
|
||||
whiles: { ...get().whiles },
|
||||
}
|
||||
|
||||
logger.info('[WorkflowStore/updateParentId] Updated parentId relationship:', {
|
||||
@@ -316,6 +325,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: [...get().edges].filter((edge) => edge.source !== id && edge.target !== id),
|
||||
loops: { ...get().loops },
|
||||
parallels: { ...get().parallels },
|
||||
whiles: { ...get().whiles },
|
||||
}
|
||||
|
||||
// Find and remove all child blocks if this is a parent node
|
||||
@@ -407,6 +417,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: newEdges,
|
||||
loops: generateLoopBlocks(get().blocks),
|
||||
parallels: get().generateParallelBlocks(),
|
||||
whiles: get().generateWhileBlocks(),
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -430,6 +441,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: newEdges,
|
||||
loops: generateLoopBlocks(get().blocks),
|
||||
parallels: get().generateParallelBlocks(),
|
||||
whiles: get().generateWhileBlocks(),
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -452,6 +464,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: [],
|
||||
loops: {},
|
||||
parallels: {},
|
||||
whiles: {},
|
||||
isDeployed: false,
|
||||
isPublished: false,
|
||||
},
|
||||
@@ -484,6 +497,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: state.edges,
|
||||
loops: state.loops,
|
||||
parallels: state.parallels,
|
||||
whiles: state.whiles,
|
||||
lastSaved: state.lastSaved,
|
||||
isDeployed: state.isDeployed,
|
||||
deployedAt: state.deployedAt,
|
||||
@@ -505,6 +519,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: [...get().edges],
|
||||
loops: { ...get().loops },
|
||||
parallels: { ...get().parallels },
|
||||
whiles: { ...get().whiles },
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -557,6 +572,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: [...get().edges],
|
||||
loops: get().generateLoopBlocks(),
|
||||
parallels: get().generateParallelBlocks(),
|
||||
whiles: get().generateWhileBlocks(),
|
||||
}
|
||||
|
||||
// Update the subblock store with the duplicated values
|
||||
@@ -641,6 +657,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: [...get().edges],
|
||||
loops: { ...get().loops },
|
||||
parallels: { ...get().parallels },
|
||||
whiles: { ...get().whiles },
|
||||
}
|
||||
|
||||
// Update references in subblock store
|
||||
@@ -914,6 +931,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: deployedState.edges,
|
||||
loops: deployedState.loops || {},
|
||||
parallels: deployedState.parallels || {},
|
||||
whiles: deployedState.whiles || {},
|
||||
isDeployed: true,
|
||||
needsRedeployment: false,
|
||||
hasActiveWebhook: false, // Reset webhook status
|
||||
@@ -1037,6 +1055,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: filteredEdges,
|
||||
loops: { ...get().loops },
|
||||
parallels: { ...get().parallels },
|
||||
whiles: { ...get().whiles },
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -1106,6 +1125,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: [...get().edges],
|
||||
loops: { ...get().loops },
|
||||
parallels: generateParallelBlocks(newBlocks), // Regenerate parallels
|
||||
whiles: { ...get().whiles },
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -1134,6 +1154,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: [...get().edges],
|
||||
loops: { ...get().loops },
|
||||
parallels: generateParallelBlocks(newBlocks), // Regenerate parallels
|
||||
whiles: { ...get().whiles },
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -1162,6 +1183,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
edges: [...get().edges],
|
||||
loops: { ...get().loops },
|
||||
parallels: generateParallelBlocks(newBlocks), // Regenerate parallels
|
||||
whiles: { ...get().whiles },
|
||||
}
|
||||
|
||||
set(newState)
|
||||
@@ -1174,6 +1196,39 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
generateParallelBlocks: () => {
|
||||
return generateParallelBlocks(get().blocks)
|
||||
},
|
||||
|
||||
// While block methods implementation (UI-only toggle)
|
||||
updateWhileType: (whileId: string, whileType) => {
|
||||
const block = get().blocks[whileId]
|
||||
if (!block || block.type !== 'while') return
|
||||
|
||||
const newBlocks = {
|
||||
...get().blocks,
|
||||
[whileId]: {
|
||||
...block,
|
||||
data: {
|
||||
...block.data,
|
||||
whileType,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const newState = {
|
||||
blocks: newBlocks,
|
||||
edges: [...get().edges],
|
||||
loops: { ...get().loops },
|
||||
parallels: { ...get().parallels },
|
||||
whiles: get().generateWhileBlocks(),
|
||||
}
|
||||
|
||||
set(newState)
|
||||
pushHistory(set, get, newState, `Update while type`)
|
||||
get().updateLastSaved()
|
||||
},
|
||||
|
||||
generateWhileBlocks: () => {
|
||||
return generateWhileBlocks(get().blocks)
|
||||
},
|
||||
})),
|
||||
{ name: 'workflow-store' }
|
||||
)
|
||||
|
||||
@@ -5,8 +5,16 @@ import type { DeploymentStatus } from '@/stores/workflows/registry/types'
|
||||
export const SUBFLOW_TYPES = {
|
||||
LOOP: 'loop',
|
||||
PARALLEL: 'parallel',
|
||||
WHILE: 'while',
|
||||
} as const
|
||||
|
||||
export const WHILE_TYPES = {
|
||||
WHILE: 'while',
|
||||
DO_WHILE: 'doWhile',
|
||||
} as const
|
||||
|
||||
export type WhileType = (typeof WHILE_TYPES)[keyof typeof WHILE_TYPES]
|
||||
|
||||
export type SubflowType = (typeof SUBFLOW_TYPES)[keyof typeof SUBFLOW_TYPES]
|
||||
|
||||
export function isValidSubflowType(type: string): type is SubflowType {
|
||||
@@ -26,12 +34,18 @@ export interface ParallelConfig {
|
||||
parallelType?: 'count' | 'collection'
|
||||
}
|
||||
|
||||
export interface WhileConfig {
|
||||
nodes: string[]
|
||||
iterations: number
|
||||
whileType: WhileType
|
||||
}
|
||||
|
||||
// Generic subflow interface
|
||||
export interface Subflow {
|
||||
id: string
|
||||
workflowId: string
|
||||
type: SubflowType
|
||||
config: LoopConfig | ParallelConfig
|
||||
config: LoopConfig | ParallelConfig | WhileConfig
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
@@ -58,6 +72,9 @@ export interface BlockData {
|
||||
// Parallel-specific properties
|
||||
parallelType?: 'collection' | 'count' // Type of parallel execution
|
||||
|
||||
// While-specific properties
|
||||
whileType?: WhileType
|
||||
|
||||
// Container node type (for ReactFlow node type determination)
|
||||
type?: string
|
||||
}
|
||||
@@ -112,6 +129,13 @@ export interface ParallelBlock {
|
||||
}
|
||||
}
|
||||
|
||||
export interface While {
|
||||
id: string
|
||||
nodes: string[]
|
||||
iterations: number
|
||||
whileType: WhileType
|
||||
}
|
||||
|
||||
export interface Loop {
|
||||
id: string
|
||||
nodes: string[]
|
||||
@@ -134,6 +158,7 @@ export interface WorkflowState {
|
||||
lastSaved?: number
|
||||
loops: Record<string, Loop>
|
||||
parallels: Record<string, Parallel>
|
||||
whiles: Record<string, While>
|
||||
lastUpdate?: number
|
||||
// Legacy deployment fields (keeping for compatibility)
|
||||
isDeployed?: boolean
|
||||
@@ -196,8 +221,10 @@ export interface WorkflowActions {
|
||||
updateParallelCount: (parallelId: string, count: number) => void
|
||||
updateParallelCollection: (parallelId: string, collection: string) => void
|
||||
updateParallelType: (parallelId: string, parallelType: 'count' | 'collection') => void
|
||||
updateWhileType: (whileId: string, whileType: WhileType) => void
|
||||
generateLoopBlocks: () => Record<string, Loop>
|
||||
generateParallelBlocks: () => Record<string, Parallel>
|
||||
generateWhileBlocks: () => Record<string, While>
|
||||
setNeedsRedeploymentFlag: (needsRedeployment: boolean) => void
|
||||
setWebhookStatus: (hasActiveWebhook: boolean) => void
|
||||
revertToDeployedState: (deployedState: WorkflowState) => void
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
|
||||
import type { BlockState, Loop, Parallel, While } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const DEFAULT_LOOP_ITERATIONS = 5
|
||||
const DEFAULT_WHILE_ITERATIONS = 1000
|
||||
|
||||
/**
|
||||
* Convert UI loop block to executor Loop format
|
||||
@@ -39,6 +40,37 @@ export function convertLoopBlockToLoop(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert UI while block to executor While format
|
||||
*
|
||||
* @param whileBlockId - ID of the while block to convert
|
||||
* @param blocks - Record of all blocks in the workflow
|
||||
* @returns While object for execution engine or undefined if not a valid while
|
||||
*/
|
||||
export function convertWhileBlockToWhile(
|
||||
whileBlockId: string,
|
||||
blocks: Record<string, BlockState>
|
||||
): While | undefined {
|
||||
const whileBlock = blocks[whileBlockId]
|
||||
if (!whileBlock || whileBlock.type !== 'while') return undefined
|
||||
|
||||
// Default iterations as a safety cap; higher for whiles
|
||||
const iterations =
|
||||
(whileBlock.data as any)?.iterations ||
|
||||
(whileBlock.data as any)?.count ||
|
||||
DEFAULT_WHILE_ITERATIONS
|
||||
|
||||
// Default whileType to 'while' when not provided
|
||||
const whileType = (whileBlock.data as any)?.whileType || 'while'
|
||||
|
||||
return {
|
||||
id: whileBlockId,
|
||||
nodes: findChildNodes(whileBlockId, blocks),
|
||||
iterations,
|
||||
whileType,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert UI parallel block to executor Parallel format
|
||||
*
|
||||
@@ -162,3 +194,25 @@ export function generateParallelBlocks(
|
||||
|
||||
return parallels
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a complete collection of while blocks from the UI blocks
|
||||
*
|
||||
* @param blocks - Record of all blocks in the workflow
|
||||
* @returns Record of While objects for execution engine
|
||||
*/
|
||||
export function generateWhileBlocks(blocks: Record<string, BlockState>): Record<string, While> {
|
||||
const whiles: Record<string, While> = {}
|
||||
|
||||
// Find all while nodes
|
||||
Object.entries(blocks)
|
||||
.filter(([_, block]) => block.type === 'while')
|
||||
.forEach(([id]) => {
|
||||
const whileCfg = convertWhileBlockToWhile(id, blocks)
|
||||
if (whileCfg) {
|
||||
whiles[id] = whileCfg
|
||||
}
|
||||
})
|
||||
|
||||
return whiles
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user