Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions apps/sim/hooks/use-collaborative-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
95 changes: 0 additions & 95 deletions apps/sim/lib/workflows/db-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
30 changes: 0 additions & 30 deletions apps/sim/lib/workflows/db-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
20 changes: 10 additions & 10 deletions apps/sim/stores/workflows/workflow/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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')
Expand All @@ -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')
Expand All @@ -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')
Expand All @@ -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')
Expand All @@ -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('')
Expand All @@ -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', () => {
Expand All @@ -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')
Expand Down
19 changes: 9 additions & 10 deletions apps/sim/stores/workflows/workflow/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@ export const useWorkflowStore = create<WorkflowStore>()(

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)
Expand All @@ -646,7 +646,7 @@ export const useWorkflowStore = create<WorkflowStore>()(
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
Expand All @@ -666,12 +666,13 @@ export const useWorkflowStore = create<WorkflowStore>()(
// 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]) => {
Expand Down Expand Up @@ -730,19 +731,17 @@ export const useWorkflowStore = create<WorkflowStore>()(
[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) => {
Expand Down
8 changes: 7 additions & 1 deletion apps/sim/stores/workflows/workflow/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down