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