Skip to content

Commit b1126e3

Browse files
feat(loop): added loop block & parallel block logic (#411)
* feat(subflows): implement parallel and loop nodes * fixed linter errors * added loop block logic * added parallel executioon * added input resolution for collections for parallel & loop blocks * refactored tests, cleaned up some unnecessary logic * prevent self connections now that we have loop blocks * refactored path tracker * lint * acknowledged PR comments, added tests * added tests for loops --------- Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu>
1 parent ef8ae60 commit b1126e3

File tree

60 files changed

+9771
-2202
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+9771
-2202
lines changed

.prettierignore

Lines changed: 0 additions & 35 deletions
This file was deleted.

apps/sim/app/api/chat/utils.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,8 @@ export async function executeWorkflowForChat(
335335
}
336336

337337
// Use deployed state for execution
338-
const state = (workflowResult[0].deployedState || workflowResult[0].state) as WorkflowState
339-
const { blocks, edges, loops } = state
338+
const state = workflowResult[0].deployedState || workflowResult[0].state
339+
const { blocks, edges, loops, parallels } = state as WorkflowState
340340

341341
// Prepare for execution, similar to use-workflow-execution.ts
342342
const mergedStates = mergeSubblockState(blocks)
@@ -386,7 +386,12 @@ export async function executeWorkflowForChat(
386386
}
387387

388388
// Create serialized workflow
389-
const serializedWorkflow = new Serializer().serializeWorkflow(mergedStates, edges, loops)
389+
const serializedWorkflow = new Serializer().serializeWorkflow(
390+
mergedStates,
391+
edges,
392+
loops,
393+
parallels
394+
)
390395

391396
// Decrypt environment variables
392397
const decryptedEnvVars: Record<string, string> = {}

apps/sim/app/api/schedules/execute/route.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export async function GET(req: NextRequest) {
150150
}
151151

152152
const state = workflowRecord.state as WorkflowState
153-
const { blocks, edges, loops } = state
153+
const { blocks, edges, loops, parallels } = state
154154

155155
const mergedStates = mergeSubblockState(blocks)
156156

@@ -224,7 +224,12 @@ export async function GET(req: NextRequest) {
224224
}
225225
}
226226

227-
const serializedWorkflow = new Serializer().serializeWorkflow(mergedStates, edges, loops)
227+
const serializedWorkflow = new Serializer().serializeWorkflow(
228+
mergedStates,
229+
edges,
230+
loops,
231+
parallels
232+
)
228233

229234
const input = {
230235
workflowId: schedule.workflowId,

apps/sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) {
9797
}
9898

9999
const state = workflowState as WorkflowState
100-
const { blocks, edges, loops } = state
100+
const { blocks, edges, loops, parallels } = state
101101

102102
// Use the same execution flow as in scheduled executions
103103
const mergedStates = mergeSubblockState(blocks)
@@ -227,7 +227,12 @@ async function executeWorkflow(workflow: any, requestId: string, input?: any) {
227227

228228
// Serialize and execute the workflow
229229
logger.debug(`[${requestId}] Serializing workflow: ${workflowId}`)
230-
const serializedWorkflow = new Serializer().serializeWorkflow(mergedStates, edges, loops)
230+
const serializedWorkflow = new Serializer().serializeWorkflow(
231+
mergedStates,
232+
edges,
233+
loops,
234+
parallels
235+
)
231236

232237
const executor = new Executor(
233238
serializedWorkflow,

apps/sim/app/api/workflows/sync/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ const MarketplaceDataSchema = z
2121
const WorkflowStateSchema = z.object({
2222
blocks: z.record(z.any()),
2323
edges: z.array(z.any()),
24-
loops: z.record(z.any()),
24+
loops: z.record(z.any()).default({}),
25+
parallels: z.record(z.any()).default({}),
2526
lastSaved: z.number().optional(),
2627
isDeployed: z.boolean().optional(),
2728
deployedAt: z

apps/sim/app/globals.css

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
}
99

1010
.workflow-container .react-flow__node {
11-
z-index: 20 !important;
11+
z-index: 21 !important;
12+
}
13+
14+
.workflow-container .react-flow__node-loopNode,
15+
.workflow-container .react-flow__node-parallelNode {
16+
z-index: -1 !important;
1217
}
1318

1419
.workflow-container .react-flow__handle {

apps/sim/app/w/[id]/components/control-bar/components/deployment-controls/components/deployed-workflow-modal.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface DeployedWorkflowModalProps {
2626
blocks: Record<string, any>
2727
edges: Array<any>
2828
loops: Record<string, any>
29+
parallels: Record<string, any>
2930
}
3031
}
3132

@@ -43,9 +44,11 @@ export function DeployedWorkflowModal({
4344
blocks: activeWorkflowId ? mergeSubblockState(state.blocks, activeWorkflowId) : state.blocks,
4445
edges: state.edges,
4546
loops: state.loops,
47+
parallels: state.parallels,
4648
}))
4749

4850
const handleRevert = () => {
51+
// Revert to the deployed state
4952
revertToDeployedState(deployedWorkflowState)
5053
setShowRevertDialog(false)
5154
onClose()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
3+
4+
// Mock the store
5+
vi.mock('@/stores/workflows/workflow/store', () => ({
6+
useWorkflowStore: vi.fn(),
7+
}))
8+
9+
describe('LoopBadges Store Integration', () => {
10+
const mockUpdateLoopType = vi.fn()
11+
const mockUpdateLoopCount = vi.fn()
12+
const mockUpdateLoopCollection = vi.fn()
13+
14+
beforeEach(() => {
15+
vi.clearAllMocks()
16+
17+
;(useWorkflowStore as any).mockImplementation((selector: any) => {
18+
const state = {
19+
updateLoopType: mockUpdateLoopType,
20+
updateLoopCount: mockUpdateLoopCount,
21+
updateLoopCollection: mockUpdateLoopCollection,
22+
}
23+
return selector(state)
24+
})
25+
})
26+
27+
it('should call updateLoopType when changing loop type', () => {
28+
// When we update loop type in the UI, it should call the store method
29+
const nodeId = 'loop1'
30+
const newType = 'forEach'
31+
32+
// Simulate the handler being called
33+
mockUpdateLoopType(nodeId, newType)
34+
35+
expect(mockUpdateLoopType).toHaveBeenCalledWith(nodeId, newType)
36+
})
37+
38+
it('should call updateLoopCount when changing loop count', () => {
39+
const nodeId = 'loop1'
40+
const newCount = 15
41+
42+
// Simulate the handler being called
43+
mockUpdateLoopCount(nodeId, newCount)
44+
45+
expect(mockUpdateLoopCount).toHaveBeenCalledWith(nodeId, newCount)
46+
})
47+
48+
it('should call updateLoopCollection when changing collection', () => {
49+
const nodeId = 'loop1'
50+
const newCollection = '["item1", "item2", "item3"]'
51+
52+
// Simulate the handler being called
53+
mockUpdateLoopCollection(nodeId, newCollection)
54+
55+
expect(mockUpdateLoopCollection).toHaveBeenCalledWith(nodeId, newCollection)
56+
})
57+
})

0 commit comments

Comments
 (0)