fix(deploy): add sockets op for renaming blocks (#2043)

* fix(deploy): add sockets op for renaming blocks

* remove old unused helpers
This commit is contained in:
Waleed
2025-11-18 15:57:49 -08:00
committed by GitHub
parent e0aade85a6
commit a8a693f1ff
6 changed files with 60 additions and 148 deletions

View File

@@ -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(

View File

@@ -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 = {

View File

@@ -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
*/

View File

@@ -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')

View File

@@ -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)
@@ -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
@@ -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]) => {
@@ -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) => {

View File

@@ -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