diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index 2e038927f3..848afe30dc 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -937,10 +937,42 @@ export function useCollaborativeWorkflow() { const collaborativeUpdateBlockName = useCallback( (id: string, name: string) => { executeQueuedOperation('update-name', 'block', { id, name }, () => { - workflowStore.updateBlockName(id, name) + const result = workflowStore.updateBlockName(id, name) + + if (result.success && result.changedSubblocks.length > 0) { + logger.info('Emitting cascaded subblock updates from block rename', { + blockId: id, + newName: name, + updateCount: result.changedSubblocks.length, + }) + + result.changedSubblocks.forEach( + ({ + blockId, + subBlockId, + newValue, + }: { + blockId: string + subBlockId: string + newValue: any + }) => { + const operationId = crypto.randomUUID() + addToQueue({ + id: operationId, + operation: { + operation: 'subblock-update', + target: 'subblock', + payload: { blockId, subBlockId, value: newValue }, + }, + workflowId: activeWorkflowId || '', + userId: session?.user?.id || 'unknown', + }) + } + ) + } }) }, - [executeQueuedOperation, workflowStore] + [executeQueuedOperation, workflowStore, addToQueue, activeWorkflowId, session?.user?.id] ) const collaborativeToggleBlockEnabled = useCallback( diff --git a/apps/sim/lib/workflows/db-helpers.test.ts b/apps/sim/lib/workflows/db-helpers.test.ts index 505609ccc9..29d65ffec6 100644 --- a/apps/sim/lib/workflows/db-helpers.test.ts +++ b/apps/sim/lib/workflows/db-helpers.test.ts @@ -791,101 +791,6 @@ describe('Database Helpers', () => { }) }) - describe('migrateWorkflowToNormalizedTables', () => { - const mockJsonState = { - blocks: mockWorkflowState.blocks, - edges: mockWorkflowState.edges, - loops: mockWorkflowState.loops, - parallels: mockWorkflowState.parallels, - lastSaved: Date.now(), - isDeployed: false, - deploymentStatuses: {}, - } - - it('should successfully migrate workflow from JSON to normalized tables', async () => { - const mockTransaction = vi.fn().mockImplementation(async (callback) => { - const tx = { - select: vi.fn().mockReturnValue({ - from: vi.fn().mockReturnValue({ - where: vi.fn().mockResolvedValue([]), - }), - }), - delete: vi.fn().mockReturnValue({ - where: vi.fn().mockResolvedValue([]), - }), - insert: vi.fn().mockReturnValue({ - values: vi.fn().mockResolvedValue([]), - }), - } - return await callback(tx) - }) - - mockDb.transaction = mockTransaction - - const result = await dbHelpers.migrateWorkflowToNormalizedTables( - mockWorkflowId, - mockJsonState - ) - - expect(result.success).toBe(true) - expect(result.error).toBeUndefined() - }) - - it('should return error when migration fails', async () => { - const mockTransaction = vi.fn().mockRejectedValue(new Error('Migration failed')) - mockDb.transaction = mockTransaction - - const result = await dbHelpers.migrateWorkflowToNormalizedTables( - mockWorkflowId, - mockJsonState - ) - - expect(result.success).toBe(false) - expect(result.error).toBe('Migration failed') - }) - - it('should handle missing properties in JSON state gracefully', async () => { - const incompleteJsonState = { - blocks: mockWorkflowState.blocks, - edges: mockWorkflowState.edges, - // Missing loops, parallels, and other properties - } - - const mockTransaction = vi.fn().mockImplementation(async (callback) => { - const tx = { - select: vi.fn().mockReturnValue({ - from: vi.fn().mockReturnValue({ - where: vi.fn().mockResolvedValue([]), - }), - }), - delete: vi.fn().mockReturnValue({ - where: vi.fn().mockResolvedValue([]), - }), - insert: vi.fn().mockReturnValue({ - values: vi.fn().mockResolvedValue([]), - }), - } - return await callback(tx) - }) - - mockDb.transaction = mockTransaction - - const result = await dbHelpers.migrateWorkflowToNormalizedTables( - mockWorkflowId, - incompleteJsonState - ) - - expect(result.success).toBe(true) - }) - - it('should handle null/undefined JSON state', async () => { - const result = await dbHelpers.migrateWorkflowToNormalizedTables(mockWorkflowId, null) - - expect(result.success).toBe(false) - expect(result.error).toContain('Cannot read properties') - }) - }) - describe('error handling and edge cases', () => { it('should handle very large workflow data', async () => { const largeWorkflowState: WorkflowState = { diff --git a/apps/sim/lib/workflows/db-helpers.ts b/apps/sim/lib/workflows/db-helpers.ts index b8b77afe6e..fb1848ef58 100644 --- a/apps/sim/lib/workflows/db-helpers.ts +++ b/apps/sim/lib/workflows/db-helpers.ts @@ -402,36 +402,6 @@ export async function workflowExistsInNormalizedTables(workflowId: string): Prom } } -/** - * Migrate a workflow from JSON blob to normalized tables - */ -export async function migrateWorkflowToNormalizedTables( - workflowId: string, - jsonState: any -): Promise<{ success: boolean; error?: string }> { - try { - // Convert JSON state to WorkflowState format - // Only include fields that are actually persisted to normalized tables - const workflowState: WorkflowState = { - blocks: jsonState.blocks || {}, - edges: jsonState.edges || [], - loops: jsonState.loops || {}, - parallels: jsonState.parallels || {}, - lastSaved: jsonState.lastSaved, - isDeployed: jsonState.isDeployed, - deployedAt: jsonState.deployedAt, - } - - return await saveWorkflowToNormalizedTables(workflowId, workflowState) - } catch (error) { - logger.error(`Error migrating workflow ${workflowId} to normalized tables:`, error) - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - } - } -} - /** * Deploy a workflow by creating a new deployment version */ diff --git a/apps/sim/stores/workflows/workflow/store.test.ts b/apps/sim/stores/workflows/workflow/store.test.ts index 77bc7d56d0..21c13539c8 100644 --- a/apps/sim/stores/workflows/workflow/store.test.ts +++ b/apps/sim/stores/workflows/workflow/store.test.ts @@ -587,7 +587,7 @@ describe('workflow store', () => { const result = updateBlockName('block1', 'Data Processor') - expect(result).toBe(true) + expect(result.success).toBe(true) const state = useWorkflowStore.getState() expect(state.blocks.block1.name).toBe('Data Processor') @@ -598,7 +598,7 @@ describe('workflow store', () => { const result = updateBlockName('block1', 'column ad') - expect(result).toBe(true) + expect(result.success).toBe(true) const state = useWorkflowStore.getState() expect(state.blocks.block1.name).toBe('column ad') @@ -609,7 +609,7 @@ describe('workflow store', () => { const result = updateBlockName('block2', 'Column AD') - expect(result).toBe(false) + expect(result.success).toBe(false) const state = useWorkflowStore.getState() expect(state.blocks.block2.name).toBe('Employee Length') @@ -620,7 +620,7 @@ describe('workflow store', () => { const result = updateBlockName('block2', 'columnad') - expect(result).toBe(false) + expect(result.success).toBe(false) const state = useWorkflowStore.getState() expect(state.blocks.block2.name).toBe('Employee Length') @@ -631,7 +631,7 @@ describe('workflow store', () => { const result = updateBlockName('block3', 'employee length') - expect(result).toBe(false) + expect(result.success).toBe(false) const state = useWorkflowStore.getState() expect(state.blocks.block3.name).toBe('Start') @@ -641,10 +641,10 @@ describe('workflow store', () => { const { updateBlockName } = useWorkflowStore.getState() const result1 = updateBlockName('block1', '') - expect(result1).toBe(true) + expect(result1.success).toBe(true) const result2 = updateBlockName('block2', ' ') - expect(result2).toBe(true) + expect(result2.success).toBe(true) const state = useWorkflowStore.getState() expect(state.blocks.block1.name).toBe('') @@ -656,7 +656,7 @@ describe('workflow store', () => { const result = updateBlockName('nonexistent', 'New Name') - expect(result).toBe(false) + expect(result.success).toBe(false) }) it('should handle complex normalization cases correctly', () => { @@ -673,11 +673,11 @@ describe('workflow store', () => { for (const name of conflictingNames) { const result = updateBlockName('block2', name) - expect(result).toBe(false) + expect(result.success).toBe(false) } const result = updateBlockName('block2', 'Unique Name') - expect(result).toBe(true) + expect(result.success).toBe(true) const state = useWorkflowStore.getState() expect(state.blocks.block2.name).toBe('Unique Name') diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts index 63167a2f7b..599cbe4fe2 100644 --- a/apps/sim/stores/workflows/workflow/store.ts +++ b/apps/sim/stores/workflows/workflow/store.ts @@ -626,7 +626,7 @@ export const useWorkflowStore = create()( updateBlockName: (id: string, name: string) => { const oldBlock = get().blocks[id] - if (!oldBlock) return false + if (!oldBlock) return { success: false, changedSubblocks: [] } // Check for normalized name collisions const normalizedNewName = normalizeBlockName(name) @@ -646,7 +646,7 @@ export const useWorkflowStore = create()( logger.error( `Cannot rename block to "${name}" - another block "${conflictingBlock[1].name}" already uses the normalized name "${normalizedNewName}"` ) - return false + return { success: false, changedSubblocks: [] } } // Create a new state with the updated block name @@ -666,12 +666,13 @@ export const useWorkflowStore = create()( // Update references in subblock store const subBlockStore = useSubBlockStore.getState() const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId + const changedSubblocks: Array<{ blockId: string; subBlockId: string; newValue: any }> = [] + if (activeWorkflowId) { // Get the workflow values for the active workflow // workflowValues: {[block_id]:{[subblock_id]:[subblock_value]}} const workflowValues = subBlockStore.workflowValues[activeWorkflowId] || {} const updatedWorkflowValues = { ...workflowValues } - const changedSubblocks: Array<{ blockId: string; subBlockId: string; newValue: any }> = [] // Loop through blocks Object.entries(workflowValues).forEach(([blockId, blockValues]) => { @@ -730,19 +731,17 @@ export const useWorkflowStore = create()( [activeWorkflowId]: updatedWorkflowValues, }, }) - - // Store changed subblocks for collaborative sync - if (changedSubblocks.length > 0) { - // Store the changed subblocks for the collaborative function to pick up - ;(window as any).__pendingSubblockUpdates = changedSubblocks - } } set(newState) get().updateLastSaved() // Note: Socket.IO handles real-time sync automatically - return true + // Return both success status and changed subblocks for collaborative sync + return { + success: true, + changedSubblocks, + } }, setBlockAdvancedMode: (id: string, advancedMode: boolean) => { diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts index 58294b15f9..17dbc1ca70 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -198,7 +198,13 @@ export interface WorkflowActions { toggleBlockEnabled: (id: string) => void duplicateBlock: (id: string) => void toggleBlockHandles: (id: string) => void - updateBlockName: (id: string, name: string) => boolean + updateBlockName: ( + id: string, + name: string + ) => { + success: boolean + changedSubblocks: Array<{ blockId: string; subBlockId: string; newValue: any }> + } setBlockAdvancedMode: (id: string, advancedMode: boolean) => void setBlockTriggerMode: (id: string, triggerMode: boolean) => void updateBlockLayoutMetrics: (id: string, dimensions: { width: number; height: number }) => void