diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 53d1395fc..6117a9334 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -517,8 +517,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(() => { @@ -754,25 +753,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) diff --git a/apps/sim/stores/workflows/workflow/store.test.ts b/apps/sim/stores/workflows/workflow/store.test.ts index 1ed122c23..2590d5580 100644 --- a/apps/sim/stores/workflows/workflow/store.test.ts +++ b/apps/sim/stores/workflows/workflow/store.test.ts @@ -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() diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 36445b226..74a82ba3e 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -2,20 +2,15 @@ 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 { 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 +134,30 @@ export const useWorkflowStore = create()( ...(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 +210,31 @@ export const useWorkflowStore = create()( 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() }, @@ -451,20 +446,23 @@ export const useWorkflowStore = create()( // 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: 16, y: 50 + 16 } // leftPadding, headerHeight + topPadding + 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 - // Calculate absolute position by traversing up the (now-deleted) parent chain + // Child positions are relative to container content area (after header + padding) let absoluteX = block.position.x let absoluteY = block.position.y - // Try to get parent's position from original blocks before deletion + // Traverse up the parent chain, adding position + container offset for each level let currentParentId: string | undefined = parentId - while (currentParentId && currentBlocks[currentParentId]) { - const parent = currentBlocks[currentParentId] - absoluteX += parent.position.x - absoluteY += parent.position.y + 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 } @@ -663,66 +661,6 @@ export const useWorkflowStore = create()( 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 @@ -918,27 +856,10 @@ export const useWorkflowStore = create()( 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 } @@ -960,7 +881,6 @@ export const useWorkflowStore = create()( } }) get().updateLastSaved() - // No sync needed for layout changes, just visual }, updateLoopCount: (loopId: string, count: number) => @@ -1078,30 +998,6 @@ export const useWorkflowStore = create()( } }), - 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, @@ -1189,28 +1085,6 @@ export const useWorkflowStore = create()( } }, - 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] @@ -1236,7 +1110,6 @@ export const useWorkflowStore = create()( set(newState) get().updateLastSaved() - // Note: Socket.IO handles real-time sync automatically }, updateParallelCollection: (parallelId: string, collection: string) => { @@ -1263,7 +1136,6 @@ export const useWorkflowStore = create()( set(newState) get().updateLastSaved() - // Note: Socket.IO handles real-time sync automatically }, updateParallelType: (parallelId: string, parallelType: 'count' | 'collection') => { @@ -1290,12 +1162,6 @@ export const useWorkflowStore = create()( 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) => { diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts index f348bf0f6..f7fafb819 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -214,7 +214,6 @@ export interface WorkflowActions { clear: () => Partial 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 - generateParallelBlocks: () => Record setNeedsRedeploymentFlag: (needsRedeployment: boolean) => void revertToDeployedState: (deployedState: WorkflowState) => void - toggleBlockAdvancedMode: (id: string) => void setDragStartPosition: (position: DragStartPosition | null) => void getDragStartPosition: () => DragStartPosition | null getWorkflowState: () => WorkflowState