Skip to content

Commit 1a71947

Browse files
icecrasher321Vikhyath Mondreti
andauthored
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>
1 parent 9584f3c commit 1a71947

5 files changed

Lines changed: 247 additions & 7 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/action-bar/action-bar.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ interface ActionBarProps {
1212
}
1313

1414
export function ActionBar({ blockId, blockType, disabled = false }: ActionBarProps) {
15-
const { collaborativeRemoveBlock, collaborativeToggleBlockEnabled } = useCollaborativeWorkflow()
16-
const toggleBlockHandles = useWorkflowStore((state) => state.toggleBlockHandles)
17-
const duplicateBlock = useWorkflowStore((state) => state.duplicateBlock)
15+
const {
16+
collaborativeRemoveBlock,
17+
collaborativeToggleBlockEnabled,
18+
collaborativeDuplicateBlock,
19+
collaborativeToggleBlockHandles,
20+
} = useCollaborativeWorkflow()
1821
const isEnabled = useWorkflowStore((state) => state.blocks[blockId]?.enabled ?? true)
1922
const horizontalHandles = useWorkflowStore(
2023
(state) => state.blocks[blockId]?.horizontalHandles ?? false
@@ -77,7 +80,7 @@ export function ActionBar({ blockId, blockType, disabled = false }: ActionBarPro
7780
size='sm'
7881
onClick={() => {
7982
if (!disabled) {
80-
duplicateBlock(blockId)
83+
collaborativeDuplicateBlock(blockId)
8184
}
8285
}}
8386
className={cn('text-gray-500', disabled && 'cursor-not-allowed opacity-50')}
@@ -99,7 +102,7 @@ export function ActionBar({ blockId, blockType, disabled = false }: ActionBarPro
99102
size='sm'
100103
onClick={() => {
101104
if (!disabled) {
102-
toggleBlockHandles(blockId)
105+
collaborativeToggleBlockHandles(blockId)
103106
}
104107
}}
105108
className={cn('text-gray-500', disabled && 'cursor-not-allowed opacity-50')}

apps/sim/hooks/use-collaborative-workflow.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,26 @@ export function useCollaborativeWorkflow() {
146146
// For now, we'll use the existing toggle method
147147
workflowStore.toggleBlockAdvancedMode(payload.id)
148148
break
149+
case 'toggle-handles': {
150+
// Apply the handles toggle - we need to set the specific value to ensure consistency
151+
const currentBlock = workflowStore.blocks[payload.id]
152+
if (currentBlock && currentBlock.horizontalHandles !== payload.horizontalHandles) {
153+
workflowStore.toggleBlockHandles(payload.id)
154+
}
155+
break
156+
}
157+
case 'duplicate':
158+
// Apply the duplicate operation by adding the new block
159+
workflowStore.addBlock(
160+
payload.id,
161+
payload.type,
162+
payload.name,
163+
payload.position,
164+
payload.data,
165+
payload.parentId,
166+
payload.extent
167+
)
168+
break
149169
}
150170
} else if (target === 'edge') {
151171
switch (operation) {
@@ -469,6 +489,100 @@ export function useCollaborativeWorkflow() {
469489
[workflowStore, emitWorkflowOperation]
470490
)
471491

492+
const collaborativeToggleBlockHandles = useCallback(
493+
(id: string) => {
494+
// Get the current state before toggling
495+
const currentBlock = workflowStore.blocks[id]
496+
if (!currentBlock) return
497+
498+
// Calculate the new horizontalHandles value
499+
const newHorizontalHandles = !currentBlock.horizontalHandles
500+
501+
// Apply locally first
502+
workflowStore.toggleBlockHandles(id)
503+
504+
// Emit with the calculated new value (don't rely on async state update)
505+
if (!isApplyingRemoteChange.current) {
506+
emitWorkflowOperation('toggle-handles', 'block', {
507+
id,
508+
horizontalHandles: newHorizontalHandles,
509+
})
510+
}
511+
},
512+
[workflowStore, emitWorkflowOperation]
513+
)
514+
515+
const collaborativeDuplicateBlock = useCallback(
516+
(sourceId: string) => {
517+
const sourceBlock = workflowStore.blocks[sourceId]
518+
if (!sourceBlock) return
519+
520+
// Generate new ID and calculate position
521+
const newId = crypto.randomUUID()
522+
const offsetPosition = {
523+
x: sourceBlock.position.x + 250,
524+
y: sourceBlock.position.y + 20,
525+
}
526+
527+
// Generate new name with numbering
528+
const match = sourceBlock.name.match(/(.*?)(\d+)?$/)
529+
const newName = match?.[2]
530+
? `${match[1]}${Number.parseInt(match[2]) + 1}`
531+
: `${sourceBlock.name} 1`
532+
533+
// Create the complete block data for the socket operation
534+
const duplicatedBlockData = {
535+
sourceId,
536+
id: newId,
537+
type: sourceBlock.type,
538+
name: newName,
539+
position: offsetPosition,
540+
data: sourceBlock.data ? JSON.parse(JSON.stringify(sourceBlock.data)) : {},
541+
subBlocks: sourceBlock.subBlocks ? JSON.parse(JSON.stringify(sourceBlock.subBlocks)) : {},
542+
outputs: sourceBlock.outputs ? JSON.parse(JSON.stringify(sourceBlock.outputs)) : {},
543+
parentId: sourceBlock.data?.parentId || null,
544+
extent: sourceBlock.data?.extent || null,
545+
enabled: sourceBlock.enabled ?? true,
546+
horizontalHandles: sourceBlock.horizontalHandles ?? true,
547+
isWide: sourceBlock.isWide ?? false,
548+
height: sourceBlock.height || 0,
549+
}
550+
551+
// Apply locally first using addBlock to ensure consistent IDs
552+
workflowStore.addBlock(
553+
newId,
554+
sourceBlock.type,
555+
newName,
556+
offsetPosition,
557+
sourceBlock.data ? JSON.parse(JSON.stringify(sourceBlock.data)) : {},
558+
sourceBlock.data?.parentId,
559+
sourceBlock.data?.extent
560+
)
561+
562+
// Copy subblock values to the new block
563+
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
564+
if (activeWorkflowId) {
565+
const subBlockValues =
566+
useSubBlockStore.getState().workflowValues[activeWorkflowId]?.[sourceId] || {}
567+
useSubBlockStore.setState((state) => ({
568+
workflowValues: {
569+
...state.workflowValues,
570+
[activeWorkflowId]: {
571+
...state.workflowValues[activeWorkflowId],
572+
[newId]: JSON.parse(JSON.stringify(subBlockValues)),
573+
},
574+
},
575+
}))
576+
}
577+
578+
// Then broadcast to other clients
579+
if (!isApplyingRemoteChange.current) {
580+
emitWorkflowOperation('duplicate', 'block', duplicatedBlockData)
581+
}
582+
},
583+
[workflowStore, emitWorkflowOperation]
584+
)
585+
472586
const collaborativeAddEdge = useCallback(
473587
(edge: Edge) => {
474588
// Apply locally first
@@ -780,6 +894,8 @@ export function useCollaborativeWorkflow() {
780894
collaborativeUpdateParentId,
781895
collaborativeToggleBlockWide,
782896
collaborativeToggleBlockAdvancedMode,
897+
collaborativeToggleBlockHandles,
898+
collaborativeDuplicateBlock,
783899
collaborativeAddEdge,
784900
collaborativeRemoveEdge,
785901
collaborativeSetSubblockValue,

apps/sim/socket-server/database/operations.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,122 @@ async function handleBlockOperationTx(
486486
break
487487
}
488488

489+
case 'toggle-handles': {
490+
if (!payload.id || payload.horizontalHandles === undefined) {
491+
throw new Error('Missing required fields for toggle handles operation')
492+
}
493+
494+
const updateResult = await tx
495+
.update(workflowBlocks)
496+
.set({
497+
horizontalHandles: payload.horizontalHandles,
498+
updatedAt: new Date(),
499+
})
500+
.where(and(eq(workflowBlocks.id, payload.id), eq(workflowBlocks.workflowId, workflowId)))
501+
.returning({ id: workflowBlocks.id })
502+
503+
if (updateResult.length === 0) {
504+
throw new Error(`Block ${payload.id} not found in workflow ${workflowId}`)
505+
}
506+
507+
logger.debug(
508+
`Updated block handles: ${payload.id} -> ${payload.horizontalHandles ? 'horizontal' : 'vertical'}`
509+
)
510+
break
511+
}
512+
513+
case 'duplicate': {
514+
// Validate required fields for duplicate operation
515+
if (!payload.sourceId || !payload.id || !payload.type || !payload.name || !payload.position) {
516+
throw new Error('Missing required fields for duplicate block operation')
517+
}
518+
519+
logger.debug(
520+
`[SERVER] Duplicating block: ${payload.type} (${payload.sourceId} -> ${payload.id})`,
521+
{
522+
isSubflowType: isSubflowBlockType(payload.type),
523+
payload,
524+
}
525+
)
526+
527+
// Extract parentId and extent from payload
528+
const parentId = payload.parentId || null
529+
const extent = payload.extent || null
530+
531+
try {
532+
const insertData = {
533+
id: payload.id,
534+
workflowId,
535+
type: payload.type,
536+
name: payload.name,
537+
positionX: payload.position.x,
538+
positionY: payload.position.y,
539+
data: payload.data || {},
540+
subBlocks: payload.subBlocks || {},
541+
outputs: payload.outputs || {},
542+
parentId,
543+
extent,
544+
enabled: payload.enabled ?? true,
545+
horizontalHandles: payload.horizontalHandles ?? true,
546+
isWide: payload.isWide ?? false,
547+
height: payload.height || 0,
548+
}
549+
550+
await tx.insert(workflowBlocks).values(insertData)
551+
} catch (insertError) {
552+
logger.error(`[SERVER] ❌ Failed to insert duplicated block ${payload.id}:`, insertError)
553+
throw insertError
554+
}
555+
556+
// Auto-create subflow entry for loop/parallel blocks
557+
if (isSubflowBlockType(payload.type)) {
558+
try {
559+
const subflowConfig =
560+
payload.type === SubflowType.LOOP
561+
? {
562+
id: payload.id,
563+
nodes: [], // Empty initially, will be populated when child blocks are added
564+
iterations: payload.data?.count || DEFAULT_LOOP_ITERATIONS,
565+
loopType: payload.data?.loopType || 'for',
566+
forEachItems: payload.data?.collection || '',
567+
}
568+
: {
569+
id: payload.id,
570+
nodes: [], // Empty initially, will be populated when child blocks are added
571+
distribution: payload.data?.collection || '',
572+
}
573+
574+
logger.debug(
575+
`[SERVER] Auto-creating ${payload.type} subflow for duplicated block ${payload.id}:`,
576+
subflowConfig
577+
)
578+
579+
await tx.insert(workflowSubflows).values({
580+
id: payload.id,
581+
workflowId,
582+
type: payload.type,
583+
config: subflowConfig,
584+
})
585+
} catch (subflowError) {
586+
logger.error(
587+
`[SERVER] ❌ Failed to create ${payload.type} subflow for duplicated block ${payload.id}:`,
588+
subflowError
589+
)
590+
throw subflowError
591+
}
592+
}
593+
594+
// If this block has a parent, update the parent's subflow node list
595+
if (parentId) {
596+
await updateSubflowNodeList(tx, workflowId, parentId)
597+
}
598+
599+
logger.debug(
600+
`Duplicated block ${payload.sourceId} -> ${payload.id} (${payload.type}) in workflow ${workflowId}`
601+
)
602+
break
603+
}
604+
489605
// Add other block operations as needed
490606
default:
491607
logger.warn(`Unknown block operation: ${operation}`)

apps/sim/socket-server/middleware/permissions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export async function verifyOperationPermission(
104104
'update-parent',
105105
'update-wide',
106106
'update-advanced-mode',
107+
'toggle-handles',
107108
'duplicate',
108109
],
109110
admin: [
@@ -116,6 +117,7 @@ export async function verifyOperationPermission(
116117
'update-parent',
117118
'update-wide',
118119
'update-advanced-mode',
120+
'toggle-handles',
119121
'duplicate',
120122
],
121123
member: [
@@ -128,6 +130,7 @@ export async function verifyOperationPermission(
128130
'update-parent',
129131
'update-wide',
130132
'update-advanced-mode',
133+
'toggle-handles',
131134
'duplicate',
132135
],
133136
viewer: ['update-position'], // Viewers can only move things around

apps/sim/socket-server/validation/schemas.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,21 @@ export const BlockOperationSchema = z.object({
1515
'update-parent',
1616
'update-wide',
1717
'update-advanced-mode',
18+
'toggle-handles',
1819
'duplicate',
1920
]),
2021
target: z.literal('block'),
2122
payload: z.object({
2223
id: z.string(),
24+
sourceId: z.string().optional(), // For duplicate operations
2325
type: z.string().optional(),
2426
name: z.string().optional(),
2527
position: PositionSchema.optional(),
2628
data: z.record(z.any()).optional(),
2729
subBlocks: z.record(z.any()).optional(),
2830
outputs: z.record(z.any()).optional(),
29-
parentId: z.string().optional(),
30-
extent: z.enum(['parent']).optional(),
31+
parentId: z.string().nullable().optional(),
32+
extent: z.enum(['parent']).nullable().optional(),
3133
enabled: z.boolean().optional(),
3234
horizontalHandles: z.boolean().optional(),
3335
isWide: z.boolean().optional(),

0 commit comments

Comments
 (0)