mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-28 08:18:09 -05:00
Compare commits
6 Commits
fix/keyboa
...
cleanup
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
276ce665e4 | ||
|
|
08bad4da9f | ||
|
|
37dbfe393a | ||
|
|
503f676910 | ||
|
|
f2ca90ae6f | ||
|
|
fe4fd47b9d |
@@ -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) => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user