Compare commits

...

6 Commits

Author SHA1 Message Date
waleed
276ce665e4 ack comments 2026-01-27 14:50:57 -08:00
waleed
08bad4da9f ack comments 2026-01-27 14:38:38 -08:00
waleed
37dbfe393a fix(workflow): preserve parent and position when duplicating/pasting nested blocks
Three related fixes for blocks inside containers (loop/parallel):

1. regenerateBlockIds now preserves parentId when the parent exists in
   the current workflow, not just when it's in the copy set. This keeps
   duplicated blocks inside their container.

2. calculatePasteOffset now uses simple offset for nested blocks instead
   of viewport-center calculation. Since nested blocks use relative
   positioning, the viewport-center offset would place them incorrectly.

3. Use CONTAINER_DIMENSIONS constants instead of hardcoded magic numbers
   in orphan cleanup position calculation.
2026-01-27 14:17:16 -08:00
waleed
503f676910 fix(store): clear extent property when orphaning blocks
When a block's parent is removed, properly clear both parentId and extent
properties from block.data, matching the pattern used in batchUpdateBlocksWithParent.
2026-01-27 14:10:55 -08:00
waleed
f2ca90ae6f refactor(store): remove unused workflow store functions
Remove redundant functions superseded by collaborative workflow patterns:
- duplicateBlock (superseded by collaborative paste flow)
- toggleBlockAdvancedMode (superseded by setBlockAdvancedMode)
- updateLoopCollection (redundant wrapper)
- setBlockTriggerMode (unused)
- generateLoopBlocks/generateParallelBlocks methods (called directly as utils)

Also removes ~160 lines of related tests and cleans up unused imports.
2026-01-27 14:10:55 -08:00
waleed
fe4fd47b9d improvement(workflow): remove useEffect anti-patterns 2026-01-27 14:10:55 -08:00
4 changed files with 131 additions and 417 deletions

View File

@@ -99,19 +99,33 @@ const logger = createLogger('Workflow')
const DEFAULT_PASTE_OFFSET = { x: 50, y: 50 }
/**
* Calculates the offset to paste blocks at viewport center
* Calculates the offset to paste blocks at viewport center, or simple offset for nested blocks
*/
function calculatePasteOffset(
clipboard: {
blocks: Record<string, { position: { x: number; y: number }; type: string; height?: number }>
blocks: Record<
string,
{
position: { x: number; y: number }
type: string
height?: number
data?: { parentId?: string }
}
>
} | null,
viewportCenter: { x: number; y: number }
viewportCenter: { x: number; y: number },
existingBlocks: Record<string, { id: string }> = {}
): { x: number; y: number } {
if (!clipboard) return DEFAULT_PASTE_OFFSET
const clipboardBlocks = Object.values(clipboard.blocks)
if (clipboardBlocks.length === 0) return DEFAULT_PASTE_OFFSET
const allBlocksNested = clipboardBlocks.every(
(b) => b.data?.parentId && existingBlocks[b.data.parentId]
)
if (allBlocksNested) return DEFAULT_PASTE_OFFSET
const minX = Math.min(...clipboardBlocks.map((b) => b.position.x))
const maxX = Math.max(
...clipboardBlocks.map((b) => {
@@ -307,9 +321,7 @@ const WorkflowContent = React.memo(() => {
const isAutoConnectEnabled = useAutoConnect()
const autoConnectRef = useRef(isAutoConnectEnabled)
useEffect(() => {
autoConnectRef.current = isAutoConnectEnabled
}, [isAutoConnectEnabled])
autoConnectRef.current = isAutoConnectEnabled
// Panel open states for context menu
const isVariablesOpen = useVariablesStore((state) => state.isOpen)
@@ -448,11 +460,16 @@ const WorkflowContent = React.memo(() => {
)
/** Re-applies diff markers when blocks change after socket rehydration. */
const blocksRef = useRef(blocks)
const diffBlocksRef = useRef(blocks)
useEffect(() => {
if (!isWorkflowReady) return
if (hasActiveDiff && isDiffReady && blocks !== blocksRef.current) {
blocksRef.current = blocks
const blocksChanged = blocks !== diffBlocksRef.current
if (!blocksChanged) return
diffBlocksRef.current = blocks
if (hasActiveDiff && isDiffReady) {
setTimeout(() => reapplyDiffMarkers(), 0)
}
}, [blocks, hasActiveDiff, isDiffReady, reapplyDiffMarkers, isWorkflowReady])
@@ -515,8 +532,7 @@ const WorkflowContent = React.memo(() => {
})
}, [edges, isShowingDiff, isDiffReady, diffAnalysis, blocks])
const { userPermissions, workspacePermissions, permissionsError } =
useWorkspacePermissionsContext()
const { userPermissions } = useWorkspacePermissionsContext()
/** Returns read-only permissions when viewing snapshot, otherwise user permissions. */
const effectivePermissions = useMemo(() => {
@@ -752,25 +768,6 @@ const WorkflowContent = React.memo(() => {
[isErrorConnectionDrag]
)
/** Logs permission loading results for debugging. */
useEffect(() => {
if (permissionsError) {
logger.error('Failed to load workspace permissions', {
workspaceId,
error: permissionsError,
})
} else if (workspacePermissions) {
logger.info('Workspace permissions loaded in workflow', {
workspaceId,
userCount: workspacePermissions.total,
permissions: workspacePermissions.users.map((u) => ({
email: u.email,
permissions: u.permissionType,
})),
})
}
}, [workspacePermissions, permissionsError, workspaceId])
const updateNodeParent = useCallback(
(nodeId: string, newParentId: string | null, affectedEdges: any[] = []) => {
const node = getNodes().find((n: any) => n.id === nodeId)
@@ -1042,7 +1039,7 @@ const WorkflowContent = React.memo(() => {
executePasteOperation(
'paste',
calculatePasteOffset(clipboard, getViewportCenter()),
calculatePasteOffset(clipboard, getViewportCenter(), blocks),
targetContainer,
flowPosition // Pass the click position so blocks are centered at where user right-clicked
)
@@ -1054,6 +1051,7 @@ const WorkflowContent = React.memo(() => {
screenToFlowPosition,
contextMenuPosition,
isPointInLoopNode,
blocks,
])
const handleContextDuplicate = useCallback(() => {
@@ -1164,7 +1162,10 @@ const WorkflowContent = React.memo(() => {
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
if (effectivePermissions.canEdit && hasClipboard()) {
event.preventDefault()
executePasteOperation('paste', calculatePasteOffset(clipboard, getViewportCenter()))
executePasteOperation(
'paste',
calculatePasteOffset(clipboard, getViewportCenter(), blocks)
)
}
}
}
@@ -1186,6 +1187,7 @@ const WorkflowContent = React.memo(() => {
clipboard,
getViewportCenter,
executePasteOperation,
blocks,
])
/**
@@ -2160,6 +2162,8 @@ const WorkflowContent = React.memo(() => {
// Local state for nodes - allows smooth drag without store updates on every frame
const [displayNodes, setDisplayNodes] = useState<Node[]>([])
// Sync derivedNodes to displayNodes while preserving selection state
// This effect handles both normal sync and pending selection from paste/duplicate
useEffect(() => {
// Check for pending selection (from paste/duplicate), otherwise preserve existing selection
if (pendingSelection && pendingSelection.length > 0) {
@@ -2189,7 +2193,6 @@ const WorkflowContent = React.memo(() => {
}, [derivedNodes, blocks, pendingSelection, clearPendingSelection])
// Phase 2: When displayNodes updates, check if pending zoom blocks are ready
// (Phase 1 is located earlier in the file where pendingZoomBlockIdsRef is defined)
useEffect(() => {
const pendingBlockIds = pendingZoomBlockIdsRef.current
if (!pendingBlockIds || pendingBlockIds.size === 0) {
@@ -2380,40 +2383,6 @@ const WorkflowContent = React.memo(() => {
resizeLoopNodesWrapper()
}, [derivedNodes, resizeLoopNodesWrapper, isWorkflowReady])
/** Cleans up orphaned nodes with invalid parent references after deletion. */
useEffect(() => {
if (!isWorkflowReady) return
// Create a mapping of node IDs to check for missing parent references
const nodeIds = new Set(Object.keys(blocks))
// Check for nodes with invalid parent references and collect updates
const orphanedUpdates: Array<{
id: string
position: { x: number; y: number }
parentId: string
}> = []
Object.entries(blocks).forEach(([id, block]) => {
const parentId = block.data?.parentId
// If block has a parent reference but parent no longer exists
if (parentId && !nodeIds.has(parentId)) {
logger.warn('Found orphaned node with invalid parent reference', {
nodeId: id,
missingParentId: parentId,
})
const absolutePosition = getNodeAbsolutePosition(id)
orphanedUpdates.push({ id, position: absolutePosition, parentId: '' })
}
})
// Batch update all orphaned nodes at once
if (orphanedUpdates.length > 0) {
batchUpdateBlocksWithParent(orphanedUpdates)
}
}, [blocks, batchUpdateBlocksWithParent, getNodeAbsolutePosition, isWorkflowReady])
/** Handles edge removal changes. */
const onEdgesChange = useCallback(
(changes: any) => {

View File

@@ -22,8 +22,6 @@ import {
WorkflowBuilder,
} from '@sim/testing'
import { beforeEach, describe, expect, it } from 'vitest'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
describe('workflow store', () => {
@@ -365,30 +363,6 @@ describe('workflow store', () => {
})
})
describe('duplicateBlock', () => {
it('should duplicate a block', () => {
const { addBlock, duplicateBlock } = useWorkflowStore.getState()
addBlock('original', 'agent', 'Original Agent', { x: 0, y: 0 })
duplicateBlock('original')
const { blocks } = useWorkflowStore.getState()
const blockIds = Object.keys(blocks)
expect(blockIds.length).toBe(2)
const duplicatedId = blockIds.find((id) => id !== 'original')
expect(duplicatedId).toBeDefined()
if (duplicatedId) {
expect(blocks[duplicatedId].type).toBe('agent')
expect(blocks[duplicatedId].name).toContain('Original Agent')
expect(blocks[duplicatedId].position.x).not.toBe(0)
}
})
})
describe('batchUpdatePositions', () => {
it('should update block position', () => {
const { addBlock, batchUpdatePositions } = useWorkflowStore.getState()
@@ -452,29 +426,6 @@ describe('workflow store', () => {
expect(state.loops.loop1.forEachItems).toBe('["a", "b", "c"]')
})
it('should regenerate loops when updateLoopCollection is called', () => {
const { addBlock, updateLoopCollection } = useWorkflowStore.getState()
addBlock(
'loop1',
'loop',
'Test Loop',
{ x: 0, y: 0 },
{
loopType: 'forEach',
collection: '["item1", "item2"]',
}
)
updateLoopCollection('loop1', '["item1", "item2", "item3"]')
const state = useWorkflowStore.getState()
expect(state.blocks.loop1?.data?.collection).toBe('["item1", "item2", "item3"]')
expect(state.loops.loop1).toBeDefined()
expect(state.loops.loop1.forEachItems).toBe('["item1", "item2", "item3"]')
})
it('should clamp loop count between 1 and 1000', () => {
const { addBlock, updateLoopCount } = useWorkflowStore.getState()
@@ -599,118 +550,6 @@ describe('workflow store', () => {
})
})
describe('mode switching', () => {
it('should toggle advanced mode on a block', () => {
const { addBlock, toggleBlockAdvancedMode } = useWorkflowStore.getState()
addBlock('agent1', 'agent', 'Test Agent', { x: 0, y: 0 })
let state = useWorkflowStore.getState()
expect(state.blocks.agent1?.advancedMode).toBe(false)
toggleBlockAdvancedMode('agent1')
state = useWorkflowStore.getState()
expect(state.blocks.agent1?.advancedMode).toBe(true)
toggleBlockAdvancedMode('agent1')
state = useWorkflowStore.getState()
expect(state.blocks.agent1?.advancedMode).toBe(false)
})
it('should preserve systemPrompt and userPrompt when switching modes', () => {
const { addBlock, toggleBlockAdvancedMode } = useWorkflowStore.getState()
const { setState: setSubBlockState } = useSubBlockStore
useWorkflowRegistry.setState({ activeWorkflowId: 'test-workflow' })
addBlock('agent1', 'agent', 'Test Agent', { x: 0, y: 0 })
setSubBlockState({
workflowValues: {
'test-workflow': {
agent1: {
systemPrompt: 'You are a helpful assistant',
userPrompt: 'Hello, how are you?',
},
},
},
})
toggleBlockAdvancedMode('agent1')
let subBlockState = useSubBlockStore.getState()
expect(subBlockState.workflowValues['test-workflow'].agent1.systemPrompt).toBe(
'You are a helpful assistant'
)
expect(subBlockState.workflowValues['test-workflow'].agent1.userPrompt).toBe(
'Hello, how are you?'
)
toggleBlockAdvancedMode('agent1')
subBlockState = useSubBlockStore.getState()
expect(subBlockState.workflowValues['test-workflow'].agent1.systemPrompt).toBe(
'You are a helpful assistant'
)
expect(subBlockState.workflowValues['test-workflow'].agent1.userPrompt).toBe(
'Hello, how are you?'
)
})
it('should preserve memories when switching from advanced to basic mode', () => {
const { addBlock, toggleBlockAdvancedMode } = useWorkflowStore.getState()
const { setState: setSubBlockState } = useSubBlockStore
useWorkflowRegistry.setState({ activeWorkflowId: 'test-workflow' })
addBlock('agent1', 'agent', 'Test Agent', { x: 0, y: 0 })
toggleBlockAdvancedMode('agent1')
setSubBlockState({
workflowValues: {
'test-workflow': {
agent1: {
systemPrompt: 'You are a helpful assistant',
userPrompt: 'What did we discuss?',
memories: [
{ role: 'user', content: 'My name is John' },
{ role: 'assistant', content: 'Nice to meet you, John!' },
],
},
},
},
})
toggleBlockAdvancedMode('agent1')
const subBlockState = useSubBlockStore.getState()
expect(subBlockState.workflowValues['test-workflow'].agent1.systemPrompt).toBe(
'You are a helpful assistant'
)
expect(subBlockState.workflowValues['test-workflow'].agent1.userPrompt).toBe(
'What did we discuss?'
)
expect(subBlockState.workflowValues['test-workflow'].agent1.memories).toEqual([
{ role: 'user', content: 'My name is John' },
{ role: 'assistant', content: 'Nice to meet you, John!' },
])
})
it('should handle mode switching when no subblock values exist', () => {
const { addBlock, toggleBlockAdvancedMode } = useWorkflowStore.getState()
useWorkflowRegistry.setState({ activeWorkflowId: 'test-workflow' })
addBlock('agent1', 'agent', 'Test Agent', { x: 0, y: 0 })
expect(useWorkflowStore.getState().blocks.agent1?.advancedMode).toBe(false)
expect(() => toggleBlockAdvancedMode('agent1')).not.toThrow()
const state = useWorkflowStore.getState()
expect(state.blocks.agent1?.advancedMode).toBe(true)
})
it('should not throw when toggling non-existent block', () => {
const { toggleBlockAdvancedMode } = useWorkflowStore.getState()
expect(() => toggleBlockAdvancedMode('non-existent')).not.toThrow()
})
})
describe('workflow state management', () => {
it('should work with WorkflowBuilder for complex setups', () => {
const workflowState = WorkflowBuilder.linear(3).build()

View File

@@ -2,20 +2,16 @@ import { createLogger } from '@sim/logger'
import type { Edge } from 'reactflow'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
import { CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
import { getBlock } from '@/blocks'
import type { SubBlockConfig } from '@/blocks/types'
import { normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import {
filterNewEdges,
filterValidEdges,
getUniqueBlockName,
mergeSubblockState,
} from '@/stores/workflows/utils'
import { filterNewEdges, filterValidEdges } from '@/stores/workflows/utils'
import type {
BlockState,
Position,
SubBlockState,
WorkflowState,
@@ -139,30 +135,30 @@ export const useWorkflowStore = create<WorkflowStore>()(
...(parentId && { parentId, extent: extent || 'parent' }),
}
const newState = {
blocks: {
...get().blocks,
[id]: {
id,
type,
name,
position,
subBlocks: {},
outputs: {},
enabled: blockProperties?.enabled ?? true,
horizontalHandles: blockProperties?.horizontalHandles ?? true,
advancedMode: blockProperties?.advancedMode ?? false,
triggerMode: blockProperties?.triggerMode ?? false,
height: blockProperties?.height ?? 0,
data: nodeData,
},
const newBlocks = {
...get().blocks,
[id]: {
id,
type,
name,
position,
subBlocks: {},
outputs: {},
enabled: blockProperties?.enabled ?? true,
horizontalHandles: blockProperties?.horizontalHandles ?? true,
advancedMode: blockProperties?.advancedMode ?? false,
triggerMode: blockProperties?.triggerMode ?? false,
height: blockProperties?.height ?? 0,
data: nodeData,
},
edges: [...get().edges],
loops: get().generateLoopBlocks(),
parallels: get().generateParallelBlocks(),
}
set(newState)
set({
blocks: newBlocks,
edges: [...get().edges],
loops: generateLoopBlocks(newBlocks),
parallels: generateParallelBlocks(newBlocks),
})
get().updateLastSaved()
return
}
@@ -215,31 +211,31 @@ export const useWorkflowStore = create<WorkflowStore>()(
const triggerMode = blockProperties?.triggerMode ?? false
const outputs = getBlockOutputs(type, subBlocks, triggerMode)
const newState = {
blocks: {
...get().blocks,
[id]: {
id,
type,
name,
position,
subBlocks,
outputs,
enabled: blockProperties?.enabled ?? true,
horizontalHandles: blockProperties?.horizontalHandles ?? true,
advancedMode: blockProperties?.advancedMode ?? false,
triggerMode: triggerMode,
height: blockProperties?.height ?? 0,
layout: {},
data: nodeData,
},
const newBlocks = {
...get().blocks,
[id]: {
id,
type,
name,
position,
subBlocks,
outputs,
enabled: blockProperties?.enabled ?? true,
horizontalHandles: blockProperties?.horizontalHandles ?? true,
advancedMode: blockProperties?.advancedMode ?? false,
triggerMode: triggerMode,
height: blockProperties?.height ?? 0,
layout: {},
data: nodeData,
},
edges: [...get().edges],
loops: get().generateLoopBlocks(),
parallels: get().generateParallelBlocks(),
}
set(newState)
set({
blocks: newBlocks,
edges: [...get().edges],
loops: generateLoopBlocks(newBlocks),
parallels: generateParallelBlocks(newBlocks),
})
get().updateLastSaved()
},
@@ -448,6 +444,41 @@ export const useWorkflowStore = create<WorkflowStore>()(
delete newBlocks[blockId]
})
// Clean up orphaned nodes - blocks whose parent was removed but weren't descendants
// This can happen in edge cases (e.g., data inconsistency, external modifications)
const remainingBlockIds = new Set(Object.keys(newBlocks))
const CONTAINER_OFFSET = {
x: CONTAINER_DIMENSIONS.LEFT_PADDING,
y: CONTAINER_DIMENSIONS.HEADER_HEIGHT + CONTAINER_DIMENSIONS.TOP_PADDING,
}
Object.entries(newBlocks).forEach(([blockId, block]) => {
const parentId = block.data?.parentId
if (parentId && !remainingBlockIds.has(parentId)) {
// Parent was removed - convert to absolute position and clear parentId
// Child positions are relative to container content area (after header + padding)
let absoluteX = block.position.x
let absoluteY = block.position.y
// Traverse up the parent chain, adding position + container offset for each level
let currentParentId: string | undefined = parentId
while (currentParentId) {
const parent: BlockState | undefined = currentBlocks[currentParentId]
if (!parent) break
absoluteX += parent.position.x + CONTAINER_OFFSET.x
absoluteY += parent.position.y + CONTAINER_OFFSET.y
currentParentId = parent.data?.parentId
}
const { parentId: _removed, extent: _removedExtent, ...restData } = block.data || {}
newBlocks[blockId] = {
...block,
position: { x: absoluteX, y: absoluteY },
data: Object.keys(restData).length > 0 ? restData : undefined,
}
}
})
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (activeWorkflowId) {
const subBlockStore = useSubBlockStore.getState()
@@ -584,7 +615,20 @@ export const useWorkflowStore = create<WorkflowStore>()(
options?: { updateLastSaved?: boolean }
) => {
set((state) => {
const nextBlocks = workflowState.blocks || {}
const incomingBlocks = workflowState.blocks || {}
const nextBlocks: typeof incomingBlocks = {}
for (const [id, block] of Object.entries(incomingBlocks)) {
if (block.data?.parentId && !incomingBlocks[block.data.parentId]) {
nextBlocks[id] = {
...block,
data: { ...block.data, parentId: undefined, extent: undefined },
}
} else {
nextBlocks[id] = block
}
}
const nextEdges = filterValidEdges(workflowState.edges || [], nextBlocks)
const nextLoops =
Object.keys(workflowState.loops || {}).length > 0
@@ -635,66 +679,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
get().updateLastSaved()
},
duplicateBlock: (id: string) => {
const block = get().blocks[id]
if (!block) return
const newId = crypto.randomUUID()
const offsetPosition = {
x: block.position.x + DEFAULT_DUPLICATE_OFFSET.x,
y: block.position.y + DEFAULT_DUPLICATE_OFFSET.y,
}
const newName = getUniqueBlockName(block.name, get().blocks)
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
const mergedBlock = mergeSubblockState(get().blocks, activeWorkflowId || undefined, id)[id]
const newSubBlocks = Object.entries(mergedBlock.subBlocks).reduce(
(acc, [subId, subBlock]) => ({
...acc,
[subId]: {
...subBlock,
value: JSON.parse(JSON.stringify(subBlock.value)),
},
}),
{}
)
const newState = {
blocks: {
...get().blocks,
[newId]: {
...block,
id: newId,
name: newName,
position: offsetPosition,
subBlocks: newSubBlocks,
},
},
edges: [...get().edges],
loops: get().generateLoopBlocks(),
parallels: get().generateParallelBlocks(),
}
if (activeWorkflowId) {
const subBlockValues =
useSubBlockStore.getState().workflowValues[activeWorkflowId]?.[id] || {}
useSubBlockStore.setState((state) => ({
workflowValues: {
...state.workflowValues,
[activeWorkflowId]: {
...state.workflowValues[activeWorkflowId],
[newId]: JSON.parse(JSON.stringify(subBlockValues)),
},
},
}))
}
set(newState)
get().updateLastSaved()
},
setBlockHandles: (id: string, horizontalHandles: boolean) => {
const block = get().blocks[id]
if (!block || block.horizontalHandles === horizontalHandles) return
@@ -890,27 +874,10 @@ export const useWorkflowStore = create<WorkflowStore>()(
get().updateLastSaved()
},
setBlockTriggerMode: (id: string, triggerMode: boolean) => {
set((state) => ({
blocks: {
...state.blocks,
[id]: {
...state.blocks[id],
triggerMode,
},
},
edges: [...state.edges],
loops: { ...state.loops },
}))
get().updateLastSaved()
// Note: Socket.IO handles real-time sync automatically
},
updateBlockLayoutMetrics: (id: string, dimensions: { width: number; height: number }) => {
set((state) => {
const block = state.blocks[id]
if (!block) {
logger.warn(`Cannot update layout metrics: Block ${id} not found in workflow store`)
return state
}
@@ -932,7 +899,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
}
})
get().updateLastSaved()
// No sync needed for layout changes, just visual
},
updateLoopCount: (loopId: string, count: number) =>
@@ -1050,30 +1016,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
}
}),
updateLoopCollection: (loopId: string, collection: string) => {
const store = get()
const block = store.blocks[loopId]
if (!block || block.type !== 'loop') return
const loopType = block.data?.loopType || 'for'
if (loopType === 'while') {
store.setLoopWhileCondition(loopId, collection)
} else if (loopType === 'doWhile') {
store.setLoopDoWhileCondition(loopId, collection)
} else if (loopType === 'forEach') {
store.setLoopForEachItems(loopId, collection)
} else {
// Default to forEach-style storage for backward compatibility
store.setLoopForEachItems(loopId, collection)
}
},
// Function to convert UI loop blocks to execution format
generateLoopBlocks: () => {
return generateLoopBlocks(get().blocks)
},
triggerUpdate: () => {
set((state) => ({
...state,
@@ -1161,28 +1103,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
}
},
toggleBlockAdvancedMode: (id: string) => {
const block = get().blocks[id]
if (!block) return
const newState = {
blocks: {
...get().blocks,
[id]: {
...block,
advancedMode: !block.advancedMode,
},
},
edges: [...get().edges],
loops: { ...get().loops },
}
set(newState)
get().triggerUpdate()
// Note: Socket.IO handles real-time sync automatically
},
// Parallel block methods implementation
updateParallelCount: (parallelId: string, count: number) => {
const block = get().blocks[parallelId]
@@ -1208,7 +1128,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
set(newState)
get().updateLastSaved()
// Note: Socket.IO handles real-time sync automatically
},
updateParallelCollection: (parallelId: string, collection: string) => {
@@ -1235,7 +1154,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
set(newState)
get().updateLastSaved()
// Note: Socket.IO handles real-time sync automatically
},
updateParallelType: (parallelId: string, parallelType: 'count' | 'collection') => {
@@ -1262,12 +1180,6 @@ export const useWorkflowStore = create<WorkflowStore>()(
set(newState)
get().updateLastSaved()
// Note: Socket.IO handles real-time sync automatically
},
// Function to convert UI parallel blocks to execution format
generateParallelBlocks: () => {
return generateParallelBlocks(get().blocks)
},
setDragStartPosition: (position) => {

View File

@@ -214,7 +214,6 @@ export interface WorkflowActions {
clear: () => Partial<WorkflowState>
updateLastSaved: () => void
setBlockEnabled: (id: string, enabled: boolean) => void
duplicateBlock: (id: string) => void
setBlockHandles: (id: string, horizontalHandles: boolean) => void
updateBlockName: (
id: string,
@@ -225,23 +224,18 @@ export interface WorkflowActions {
}
setBlockAdvancedMode: (id: string, advancedMode: boolean) => void
setBlockCanonicalMode: (id: string, canonicalId: string, mode: 'basic' | 'advanced') => void
setBlockTriggerMode: (id: string, triggerMode: boolean) => void
updateBlockLayoutMetrics: (id: string, dimensions: { width: number; height: number }) => void
triggerUpdate: () => void
updateLoopCount: (loopId: string, count: number) => void
updateLoopType: (loopId: string, loopType: 'for' | 'forEach' | 'while' | 'doWhile') => void
updateLoopCollection: (loopId: string, collection: string) => void
setLoopForEachItems: (loopId: string, items: any) => void
setLoopWhileCondition: (loopId: string, condition: string) => void
setLoopDoWhileCondition: (loopId: string, condition: string) => void
updateParallelCount: (parallelId: string, count: number) => void
updateParallelCollection: (parallelId: string, collection: string) => void
updateParallelType: (parallelId: string, parallelType: 'count' | 'collection') => void
generateLoopBlocks: () => Record<string, Loop>
generateParallelBlocks: () => Record<string, Parallel>
setNeedsRedeploymentFlag: (needsRedeployment: boolean) => void
revertToDeployedState: (deployedState: WorkflowState) => void
toggleBlockAdvancedMode: (id: string) => void
setDragStartPosition: (position: DragStartPosition | null) => void
getDragStartPosition: () => DragStartPosition | null
getWorkflowState: () => WorkflowState