fix(sockets events): remaining sockets events (#558)

* add sockets event for duplicate block

* fix lint

* add vertical ports event

* fix lint

---------

Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net>
This commit is contained in:
Vikhyath Mondreti
2025-06-26 18:31:46 -07:00
committed by GitHub
parent 9584f3cb57
commit 1a719470b8
5 changed files with 247 additions and 7 deletions

View File

@@ -12,9 +12,12 @@ interface ActionBarProps {
}
export function ActionBar({ blockId, blockType, disabled = false }: ActionBarProps) {
const { collaborativeRemoveBlock, collaborativeToggleBlockEnabled } = useCollaborativeWorkflow()
const toggleBlockHandles = useWorkflowStore((state) => state.toggleBlockHandles)
const duplicateBlock = useWorkflowStore((state) => state.duplicateBlock)
const {
collaborativeRemoveBlock,
collaborativeToggleBlockEnabled,
collaborativeDuplicateBlock,
collaborativeToggleBlockHandles,
} = useCollaborativeWorkflow()
const isEnabled = useWorkflowStore((state) => state.blocks[blockId]?.enabled ?? true)
const horizontalHandles = useWorkflowStore(
(state) => state.blocks[blockId]?.horizontalHandles ?? false
@@ -77,7 +80,7 @@ export function ActionBar({ blockId, blockType, disabled = false }: ActionBarPro
size='sm'
onClick={() => {
if (!disabled) {
duplicateBlock(blockId)
collaborativeDuplicateBlock(blockId)
}
}}
className={cn('text-gray-500', disabled && 'cursor-not-allowed opacity-50')}
@@ -99,7 +102,7 @@ export function ActionBar({ blockId, blockType, disabled = false }: ActionBarPro
size='sm'
onClick={() => {
if (!disabled) {
toggleBlockHandles(blockId)
collaborativeToggleBlockHandles(blockId)
}
}}
className={cn('text-gray-500', disabled && 'cursor-not-allowed opacity-50')}

View File

@@ -146,6 +146,26 @@ export function useCollaborativeWorkflow() {
// For now, we'll use the existing toggle method
workflowStore.toggleBlockAdvancedMode(payload.id)
break
case 'toggle-handles': {
// Apply the handles toggle - we need to set the specific value to ensure consistency
const currentBlock = workflowStore.blocks[payload.id]
if (currentBlock && currentBlock.horizontalHandles !== payload.horizontalHandles) {
workflowStore.toggleBlockHandles(payload.id)
}
break
}
case 'duplicate':
// Apply the duplicate operation by adding the new block
workflowStore.addBlock(
payload.id,
payload.type,
payload.name,
payload.position,
payload.data,
payload.parentId,
payload.extent
)
break
}
} else if (target === 'edge') {
switch (operation) {
@@ -469,6 +489,100 @@ export function useCollaborativeWorkflow() {
[workflowStore, emitWorkflowOperation]
)
const collaborativeToggleBlockHandles = useCallback(
(id: string) => {
// Get the current state before toggling
const currentBlock = workflowStore.blocks[id]
if (!currentBlock) return
// Calculate the new horizontalHandles value
const newHorizontalHandles = !currentBlock.horizontalHandles
// Apply locally first
workflowStore.toggleBlockHandles(id)
// Emit with the calculated new value (don't rely on async state update)
if (!isApplyingRemoteChange.current) {
emitWorkflowOperation('toggle-handles', 'block', {
id,
horizontalHandles: newHorizontalHandles,
})
}
},
[workflowStore, emitWorkflowOperation]
)
const collaborativeDuplicateBlock = useCallback(
(sourceId: string) => {
const sourceBlock = workflowStore.blocks[sourceId]
if (!sourceBlock) return
// Generate new ID and calculate position
const newId = crypto.randomUUID()
const offsetPosition = {
x: sourceBlock.position.x + 250,
y: sourceBlock.position.y + 20,
}
// Generate new name with numbering
const match = sourceBlock.name.match(/(.*?)(\d+)?$/)
const newName = match?.[2]
? `${match[1]}${Number.parseInt(match[2]) + 1}`
: `${sourceBlock.name} 1`
// Create the complete block data for the socket operation
const duplicatedBlockData = {
sourceId,
id: newId,
type: sourceBlock.type,
name: newName,
position: offsetPosition,
data: sourceBlock.data ? JSON.parse(JSON.stringify(sourceBlock.data)) : {},
subBlocks: sourceBlock.subBlocks ? JSON.parse(JSON.stringify(sourceBlock.subBlocks)) : {},
outputs: sourceBlock.outputs ? JSON.parse(JSON.stringify(sourceBlock.outputs)) : {},
parentId: sourceBlock.data?.parentId || null,
extent: sourceBlock.data?.extent || null,
enabled: sourceBlock.enabled ?? true,
horizontalHandles: sourceBlock.horizontalHandles ?? true,
isWide: sourceBlock.isWide ?? false,
height: sourceBlock.height || 0,
}
// Apply locally first using addBlock to ensure consistent IDs
workflowStore.addBlock(
newId,
sourceBlock.type,
newName,
offsetPosition,
sourceBlock.data ? JSON.parse(JSON.stringify(sourceBlock.data)) : {},
sourceBlock.data?.parentId,
sourceBlock.data?.extent
)
// Copy subblock values to the new block
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (activeWorkflowId) {
const subBlockValues =
useSubBlockStore.getState().workflowValues[activeWorkflowId]?.[sourceId] || {}
useSubBlockStore.setState((state) => ({
workflowValues: {
...state.workflowValues,
[activeWorkflowId]: {
...state.workflowValues[activeWorkflowId],
[newId]: JSON.parse(JSON.stringify(subBlockValues)),
},
},
}))
}
// Then broadcast to other clients
if (!isApplyingRemoteChange.current) {
emitWorkflowOperation('duplicate', 'block', duplicatedBlockData)
}
},
[workflowStore, emitWorkflowOperation]
)
const collaborativeAddEdge = useCallback(
(edge: Edge) => {
// Apply locally first
@@ -780,6 +894,8 @@ export function useCollaborativeWorkflow() {
collaborativeUpdateParentId,
collaborativeToggleBlockWide,
collaborativeToggleBlockAdvancedMode,
collaborativeToggleBlockHandles,
collaborativeDuplicateBlock,
collaborativeAddEdge,
collaborativeRemoveEdge,
collaborativeSetSubblockValue,

View File

@@ -486,6 +486,122 @@ async function handleBlockOperationTx(
break
}
case 'toggle-handles': {
if (!payload.id || payload.horizontalHandles === undefined) {
throw new Error('Missing required fields for toggle handles operation')
}
const updateResult = await tx
.update(workflowBlocks)
.set({
horizontalHandles: payload.horizontalHandles,
updatedAt: new Date(),
})
.where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
.returning({ id: workflowBlocks.id })
if (updateResult.length === 0) {
throw new Error(`Block ${payload.id} not found in workflow ${workflowId}`)
}
logger.debug(
`Updated block handles: ${payload.id} -> ${payload.horizontalHandles ? 'horizontal' : 'vertical'}`
)
break
}
case 'duplicate': {
// Validate required fields for duplicate operation
if (!payload.sourceId || !payload.id || !payload.type || !payload.name || !payload.position) {
throw new Error('Missing required fields for duplicate block operation')
}
logger.debug(
`[SERVER] Duplicating block: ${payload.type} (${payload.sourceId} -> ${payload.id})`,
{
isSubflowType: isSubflowBlockType(payload.type),
payload,
}
)
// Extract parentId and extent from payload
const parentId = payload.parentId || null
const extent = payload.extent || null
try {
const insertData = {
id: payload.id,
workflowId,
type: payload.type,
name: payload.name,
positionX: payload.position.x,
positionY: payload.position.y,
data: payload.data || {},
subBlocks: payload.subBlocks || {},
outputs: payload.outputs || {},
parentId,
extent,
enabled: payload.enabled ?? true,
horizontalHandles: payload.horizontalHandles ?? true,
isWide: payload.isWide ?? false,
height: payload.height || 0,
}
await tx.insert(workflowBlocks).values(insertData)
} catch (insertError) {
logger.error(`[SERVER] ❌ Failed to insert duplicated block ${payload.id}:`, insertError)
throw insertError
}
// Auto-create subflow entry for loop/parallel blocks
if (isSubflowBlockType(payload.type)) {
try {
const subflowConfig =
payload.type === SubflowType.LOOP
? {
id: payload.id,
nodes: [], // Empty initially, will be populated when child blocks are added
iterations: payload.data?.count || DEFAULT_LOOP_ITERATIONS,
loopType: payload.data?.loopType || 'for',
forEachItems: payload.data?.collection || '',
}
: {
id: payload.id,
nodes: [], // Empty initially, will be populated when child blocks are added
distribution: payload.data?.collection || '',
}
logger.debug(
`[SERVER] Auto-creating ${payload.type} subflow for duplicated block ${payload.id}:`,
subflowConfig
)
await tx.insert(workflowSubflows).values({
id: payload.id,
workflowId,
type: payload.type,
config: subflowConfig,
})
} catch (subflowError) {
logger.error(
`[SERVER] ❌ Failed to create ${payload.type} subflow for duplicated block ${payload.id}:`,
subflowError
)
throw subflowError
}
}
// If this block has a parent, update the parent's subflow node list
if (parentId) {
await updateSubflowNodeList(tx, workflowId, parentId)
}
logger.debug(
`Duplicated block ${payload.sourceId} -> ${payload.id} (${payload.type}) in workflow ${workflowId}`
)
break
}
// Add other block operations as needed
default:
logger.warn(`Unknown block operation: ${operation}`)

View File

@@ -104,6 +104,7 @@ export async function verifyOperationPermission(
'update-parent',
'update-wide',
'update-advanced-mode',
'toggle-handles',
'duplicate',
],
admin: [
@@ -116,6 +117,7 @@ export async function verifyOperationPermission(
'update-parent',
'update-wide',
'update-advanced-mode',
'toggle-handles',
'duplicate',
],
member: [
@@ -128,6 +130,7 @@ export async function verifyOperationPermission(
'update-parent',
'update-wide',
'update-advanced-mode',
'toggle-handles',
'duplicate',
],
viewer: ['update-position'], // Viewers can only move things around

View File

@@ -15,19 +15,21 @@ export const BlockOperationSchema = z.object({
'update-parent',
'update-wide',
'update-advanced-mode',
'toggle-handles',
'duplicate',
]),
target: z.literal('block'),
payload: z.object({
id: z.string(),
sourceId: z.string().optional(), // For duplicate operations
type: z.string().optional(),
name: z.string().optional(),
position: PositionSchema.optional(),
data: z.record(z.any()).optional(),
subBlocks: z.record(z.any()).optional(),
outputs: z.record(z.any()).optional(),
parentId: z.string().optional(),
extent: z.enum(['parent']).optional(),
parentId: z.string().nullable().optional(),
extent: z.enum(['parent']).nullable().optional(),
enabled: z.boolean().optional(),
horizontalHandles: z.boolean().optional(),
isWide: z.boolean().optional(),