From 295035395298867224db086683f3db262adcc375 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Sat, 13 Sep 2025 15:32:26 -0700 Subject: [PATCH 1/2] added suplication --- apps/sim/app/api/workflows/[id]/yaml/route.ts | 31 +- .../components/subflows/subflow-node.tsx | 47 ++- apps/sim/hooks/use-collaborative-workflow.ts | 279 ++++++++++++++++++ apps/sim/lib/workflows/reference-utils.ts | 46 +++ apps/sim/socket-server/database/operations.ts | 102 +++++++ .../socket-server/middleware/permissions.ts | 2 + apps/sim/socket-server/validation/schemas.ts | 61 +++- 7 files changed, 515 insertions(+), 53 deletions(-) create mode 100644 apps/sim/lib/workflows/reference-utils.ts diff --git a/apps/sim/app/api/workflows/[id]/yaml/route.ts b/apps/sim/app/api/workflows/[id]/yaml/route.ts index 54e33237fa..e1c0f7f3b3 100644 --- a/apps/sim/app/api/workflows/[id]/yaml/route.ts +++ b/apps/sim/app/api/workflows/[id]/yaml/route.ts @@ -11,6 +11,7 @@ import { saveWorkflowToNormalizedTables, } from '@/lib/workflows/db-helpers' import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/validation' +import { updateBlockReferences } from '@/lib/workflows/reference-utils' import { getUserId } from '@/app/api/auth/oauth/utils' import { getAllBlocks, getBlock } from '@/blocks' import type { BlockConfig } from '@/blocks/types' @@ -31,35 +32,7 @@ const YamlWorkflowRequestSchema = z.object({ createCheckpoint: z.boolean().optional().default(false), }) -function updateBlockReferences( - value: any, - blockIdMapping: Map, - requestId: string -): any { - if (typeof value === 'string') { - // Replace references in string values - for (const [oldId, newId] of blockIdMapping.entries()) { - if (value.includes(oldId)) { - value = value.replaceAll(`<${oldId}.`, `<${newId}.`).replaceAll(`%${oldId}.`, `%${newId}.`) - } - } - return value - } - - if (Array.isArray(value)) { - return value.map((item) => updateBlockReferences(item, blockIdMapping, requestId)) - } - - if (value && typeof value === 'object') { - const result: Record = {} - for (const [key, val] of Object.entries(value)) { - result[key] = updateBlockReferences(val, blockIdMapping, requestId) - } - return result - } - - return value -} +// moved to shared util in '@/lib/workflows/reference-utils' /** * Helper function to create a checkpoint before workflow changes diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx index c4ea4c6c65..cb7e7d4d4f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx @@ -1,6 +1,6 @@ import type React from 'react' import { memo, useMemo, useRef } from 'react' -import { Trash2 } from 'lucide-react' +import { Copy, Trash2 } from 'lucide-react' import { Handle, type NodeProps, Position, useReactFlow } from 'reactflow' import { StartIcon } from '@/components/icons' import { Button } from '@/components/ui/button' @@ -10,6 +10,9 @@ import { type DiffStatus, hasDiffStatus } from '@/lib/workflows/diff/types' import { IterationBadges } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/components/iteration-badges/iteration-badges' import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' +import { createLogger } from '@/lib/logs/console/logger' + +const logger = createLogger('SubflowNode') const SubflowNodeStyles: React.FC = () => { return ( @@ -74,7 +77,7 @@ export interface SubflowNodeData { export const SubflowNodeComponent = memo(({ data, id }: NodeProps) => { const { getNodes } = useReactFlow() - const { collaborativeRemoveBlock } = useCollaborativeWorkflow() + const { collaborativeRemoveBlock, collaborativeDuplicateSubflow } = useCollaborativeWorkflow() const blockRef = useRef(null) const currentWorkflow = useCurrentWorkflow() @@ -171,18 +174,34 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps {!isPreview && ( - +
+ + +
)} {/* Subflow Start */} diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index 03ab9b37db..0a0949fd88 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -4,6 +4,7 @@ import { useSession } from '@/lib/auth-client' import { createLogger } from '@/lib/logs/console/logger' import { getBlock } from '@/blocks' import { resolveOutputType } from '@/blocks/utils' +import { updateBlockReferences } from '@/lib/workflows/reference-utils' import { useSocket } from '@/contexts/socket-context' import { registerEmitFunctions, useOperationQueue } from '@/stores/operation-queue/store' import { useVariablesStore } from '@/stores/panel/variables/store' @@ -254,6 +255,75 @@ export function useCollaborativeWorkflow() { } } break + case 'duplicate-with-children': { + // Apply a duplicated subflow subtree from a remote collaborator + const parent = payload.parent + const children = Array.isArray(payload.children) ? payload.children : [] + const edges = Array.isArray(payload.edges) ? payload.edges : [] + + // Add parent block + workflowStore.addBlock( + parent.id, + parent.type, + parent.name, + parent.position, + parent.data, + parent.parentId, + parent.extent, + { + enabled: parent.enabled, + horizontalHandles: parent.horizontalHandles, + isWide: parent.isWide, + advancedMode: parent.advancedMode, + triggerMode: parent.triggerMode ?? false, + height: parent.height, + } + ) + + // Add children blocks + children.forEach((child: any) => { + workflowStore.addBlock( + child.id, + child.type, + child.name, + child.position, + child.data, + child.parentId, + child.extent, + { + enabled: child.enabled, + horizontalHandles: child.horizontalHandles, + isWide: child.isWide, + advancedMode: child.advancedMode, + triggerMode: child.triggerMode ?? false, + height: child.height, + } + ) + + // Apply subblock values for collaborators to see immediately + if (child.subBlocks && typeof child.subBlocks === 'object') { + Object.entries(child.subBlocks).forEach(([subblockId, subblock]) => { + const value = (subblock as any)?.value + if (value !== undefined) { + subBlockStore.setValue(child.id, subblockId, value) + } + }) + } + }) + + // Add internal edges + edges.forEach((edge: any) => { + workflowStore.addEdge({ + id: edge.id, + source: edge.source, + target: edge.target, + sourceHandle: edge.sourceHandle, + targetHandle: edge.targetHandle, + }) + }) + + break + } } } else if (target === 'variable') { switch (operation) { @@ -1061,6 +1131,214 @@ export function useCollaborativeWorkflow() { ] ) + const collaborativeDuplicateSubflow = useCallback( + (subflowId: string) => { + if (isShowingDiff) { + logger.debug('Skipping subflow duplication in diff mode') + return + } + if (!isInActiveRoom()) { + logger.debug('Skipping subflow duplication - not in active workflow', { + currentWorkflowId, + activeWorkflowId, + subflowId, + }) + return + } + + const parent = workflowStore.blocks[subflowId] + if (!parent || (parent.type !== 'loop' && parent.type !== 'parallel')) return + + const newParentId = crypto.randomUUID() + const parentOffsetPosition = { + x: parent.position.x + 250, + y: parent.position.y + 20, + } + + // Name bump similar to duplicateBlock + // Build a set of existing names to ensure uniqueness across the workflow + const existingNames = new Set(Object.values(workflowStore.blocks).map((b) => b.name)) + + const match = parent.name.match(/(.*?)(\d+)?$/) + let newParentName = match?.[2] + ? `${match[1]}${Number.parseInt(match[2]) + 1}` + : `${parent.name} 1` + if (existingNames.has(newParentName)) { + const base = match ? match[1] : `${parent.name} ` + let idx = match?.[2] ? Number.parseInt(match[2]) + 1 : 1 + while (existingNames.has(`${base}${idx}`)) idx++ + newParentName = `${base}${idx}` + } + existingNames.add(newParentName) + + // Collect children and internal edges + const allBlocks = workflowStore.blocks + const children = Object.values(allBlocks).filter((b) => b.data?.parentId === subflowId) + const childIdSet = new Set(children.map((c) => c.id)) + const allEdges = workflowStore.edges + + const startHandle = parent.type === 'loop' ? 'loop-start-source' : 'parallel-start-source' + const internalEdges = allEdges.filter( + (e) => + (e.source === subflowId && e.sourceHandle === startHandle && childIdSet.has(e.target)) || + (childIdSet.has(e.source) && childIdSet.has(e.target)) + ) + + // Build ID map + const idMap = new Map() + idMap.set(subflowId, newParentId) + children.forEach((c) => idMap.set(c.id, crypto.randomUUID())) + + // Construct parent payload + const parentPayload: any = { + id: newParentId, + sourceId: subflowId, + type: parent.type, + name: newParentName, + position: parentOffsetPosition, + data: parent.data ? JSON.parse(JSON.stringify(parent.data)) : {}, + subBlocks: {}, + outputs: parent.outputs ? JSON.parse(JSON.stringify(parent.outputs)) : {}, + parentId: parent.data?.parentId || null, + extent: parent.data?.extent || null, + enabled: parent.enabled ?? true, + horizontalHandles: parent.horizontalHandles ?? true, + isWide: parent.isWide ?? false, + advancedMode: parent.advancedMode ?? false, + triggerMode: false, + height: parent.height || 0, + } + + // Optimistic add of parent + workflowStore.addBlock( + newParentId, + parent.type, + newParentName, + parentOffsetPosition, + parentPayload.data, + parentPayload.parentId, + parentPayload.extent, + { + enabled: parentPayload.enabled, + horizontalHandles: parentPayload.horizontalHandles, + isWide: parentPayload.isWide, + advancedMode: parentPayload.advancedMode, + triggerMode: false, + height: parentPayload.height, + } + ) + + // Build children payloads, copy subblocks with values and update references + const activeId = activeWorkflowId || '' + const subblockValuesForWorkflow = subBlockStore.workflowValues[activeId] || {} + + const childPayloads = children.map((child) => { + const newId = idMap.get(child.id) as string + // Name bump logic identical to duplicateBlock + const childNameMatch = child.name.match(/(.*?)(\d+)?$/) + let newChildName = childNameMatch?.[2] + ? `${childNameMatch[1]}${Number.parseInt(childNameMatch[2]) + 1}` + : `${child.name} 1` + if (existingNames.has(newChildName)) { + const base = childNameMatch ? childNameMatch[1] : `${child.name} ` + let idx = childNameMatch?.[2] ? Number.parseInt(childNameMatch[2]) + 1 : 1 + while (existingNames.has(`${base}${idx}`)) idx++ + newChildName = `${base}${idx}` + } + existingNames.add(newChildName) + const clonedSubBlocks = child.subBlocks ? JSON.parse(JSON.stringify(child.subBlocks)) : {} + const values = subblockValuesForWorkflow[child.id] || {} + Object.entries(values).forEach(([subblockId, value]) => { + const processed = updateBlockReferences(value, idMap, 'duplicate-subflow') + if (!clonedSubBlocks[subblockId]) { + clonedSubBlocks[subblockId] = { id: subblockId, type: 'unknown', value: processed } + } else { + clonedSubBlocks[subblockId].value = processed + } + }) + + // Optimistic add child + workflowStore.addBlock( + newId, + child.type, + newChildName, + child.position, + { ...(child.data ? JSON.parse(JSON.stringify(child.data)) : {}), parentId: newParentId, extent: 'parent' }, + newParentId, + 'parent', + { + enabled: child.enabled, + horizontalHandles: child.horizontalHandles, + isWide: child.isWide, + advancedMode: child.advancedMode, + triggerMode: child.triggerMode ?? false, + height: child.height, + } + ) + + // Apply subblock values locally for immediate feedback + Object.entries(clonedSubBlocks).forEach(([subblockId, sub]) => { + const v = (sub as any)?.value + if (v !== undefined) { + subBlockStore.setValue(newId, subblockId, v) + } + }) + + return { + id: newId, + sourceId: child.id, + type: child.type, + name: newChildName, + position: child.position, + data: { ...(child.data ? JSON.parse(JSON.stringify(child.data)) : {}), parentId: newParentId, extent: 'parent' }, + subBlocks: clonedSubBlocks, + outputs: child.outputs ? JSON.parse(JSON.stringify(child.outputs)) : {}, + parentId: newParentId, + extent: 'parent', + enabled: child.enabled ?? true, + horizontalHandles: child.horizontalHandles ?? true, + isWide: child.isWide ?? false, + advancedMode: child.advancedMode ?? false, + triggerMode: child.triggerMode ?? false, + height: child.height || 0, + } + }) + + // Duplicate internal edges with remapped IDs + const edgePayloads = internalEdges.map((e) => ({ + id: crypto.randomUUID(), + source: idMap.get(e.source) || e.source, + target: idMap.get(e.target) || e.target, + sourceHandle: e.sourceHandle, + targetHandle: e.targetHandle, + })) + + // Optimistic add edges + edgePayloads.forEach((edge) => workflowStore.addEdge(edge)) + + // Queue server op + executeQueuedOperation( + 'duplicate-with-children', + 'subflow', + { + parent: parentPayload, + children: childPayloads, + edges: edgePayloads, + }, + () => {} + ) + }, + [ + isShowingDiff, + isInActiveRoom, + currentWorkflowId, + activeWorkflowId, + workflowStore, + subBlockStore, + executeQueuedOperation, + ] + ) + const collaborativeUpdateLoopType = useCallback( (loopId: string, loopType: 'for' | 'forEach') => { const currentBlock = workflowStore.blocks[loopId] @@ -1311,6 +1589,7 @@ export function useCollaborativeWorkflow() { collaborativeRemoveEdge, collaborativeSetSubblockValue, collaborativeSetTagSelection, + collaborativeDuplicateSubflow, // Collaborative variable operations collaborativeUpdateVariable, diff --git a/apps/sim/lib/workflows/reference-utils.ts b/apps/sim/lib/workflows/reference-utils.ts new file mode 100644 index 0000000000..eff16b304d --- /dev/null +++ b/apps/sim/lib/workflows/reference-utils.ts @@ -0,0 +1,46 @@ +import { createLogger } from '@/lib/logs/console/logger' + +const logger = createLogger('WorkflowReferenceUtils') + +/** + * Recursively update block ID references in a value using a provided ID mapping. + * Handles strings, arrays, and objects. Strings are searched for `", + contextId?: string +): any { + try { + if (typeof value === 'string') { + let result = value + for (const [oldId, newId] of blockIdMapping.entries()) { + if (result.includes(oldId)) { + result = result + .replaceAll(`<${oldId}.`, `<${newId}.`) + .replaceAll(`%${oldId}.`, `%${newId}.`) + } + } + return result + } + + if (Array.isArray(value)) { + return value.map((item) => updateBlockReferences(item, blockIdMapping, contextId)) + } + + if (value && typeof value === 'object') { + const result: Record = {} + for (const [key, val] of Object.entries(value)) { + result[key] = updateBlockReferences(val, blockIdMapping, contextId) + } + return result + } + + return value + } catch (err) { + logger.warn('Failed to update block references', { contextId, error: err instanceof Error ? err.message : String(err) }) + return value + } +} + + diff --git a/apps/sim/socket-server/database/operations.ts b/apps/sim/socket-server/database/operations.ts index 2ad00b49c6..35b1713fce 100644 --- a/apps/sim/socket-server/database/operations.ts +++ b/apps/sim/socket-server/database/operations.ts @@ -868,6 +868,108 @@ async function handleSubflowOperationTx( break } + case 'duplicate-with-children': { + // Validate required structure + const parent = payload?.parent + const children = Array.isArray(payload?.children) ? payload.children : [] + const edges = Array.isArray(payload?.edges) ? payload.edges : [] + + if (!parent || !parent.id || !parent.type || !parent.name || !parent.position) { + throw new Error('Invalid payload for subflow duplication: missing parent fields') + } + + if (!isSubflowBlockType(parent.type)) { + throw new Error('Invalid subflow type for duplication') + } + + // Insert parent block + await tx.insert(workflowBlocks).values({ + id: parent.id, + workflowId, + type: parent.type, + name: parent.name, + positionX: parent.position.x, + positionY: parent.position.y, + data: parent.data || {}, + subBlocks: parent.subBlocks || {}, + outputs: parent.outputs || {}, + parentId: parent.parentId || null, + extent: parent.extent || null, + enabled: parent.enabled ?? true, + horizontalHandles: parent.horizontalHandles ?? true, + isWide: parent.isWide ?? false, + advancedMode: parent.advancedMode ?? false, + height: parent.height || 0, + }) + + // Create subflow entry for parent + const subflowConfig = + parent.type === SubflowType.LOOP + ? { + id: parent.id, + nodes: [], + iterations: parent.data?.count || DEFAULT_LOOP_ITERATIONS, + loopType: parent.data?.loopType || 'for', + forEachItems: parent.data?.collection || '', + } + : { + id: parent.id, + nodes: [], + distribution: parent.data?.collection || '', + ...(parent.data?.parallelType ? { parallelType: parent.data.parallelType } : {}), + ...(parent.data?.count ? { count: parent.data.count } : {}), + } + + await tx.insert(workflowSubflows).values({ + id: parent.id, + workflowId, + type: parent.type, + config: subflowConfig, + }) + + // Insert child blocks + for (const child of children) { + await tx.insert(workflowBlocks).values({ + id: child.id, + workflowId, + type: child.type, + name: child.name, + positionX: child.position.x, + positionY: child.position.y, + data: child.data || {}, + subBlocks: child.subBlocks || {}, + outputs: child.outputs || {}, + parentId: parent.id, + extent: 'parent', + enabled: child.enabled ?? true, + horizontalHandles: child.horizontalHandles ?? true, + isWide: child.isWide ?? false, + advancedMode: child.advancedMode ?? false, + height: child.height || 0, + }) + } + + // Insert internal edges + for (const edge of edges) { + await tx.insert(workflowEdges).values({ + id: edge.id, + workflowId, + sourceBlockId: edge.source, + targetBlockId: edge.target, + sourceHandle: edge.sourceHandle || null, + targetHandle: edge.targetHandle || null, + }) + } + + // Update subflow node list with newly inserted children + await updateSubflowNodeList(tx, workflowId, parent.id) + + logger.debug( + `[SERVER] Duplicated subflow subtree ${parent.id} with ${children.length} children and ${edges.length} edges` + ) + break + } + // Add other subflow operations as needed default: logger.warn(`Unknown subflow operation: ${operation}`) diff --git a/apps/sim/socket-server/middleware/permissions.ts b/apps/sim/socket-server/middleware/permissions.ts index a94ae21b1e..1eeb386fe2 100644 --- a/apps/sim/socket-server/middleware/permissions.ts +++ b/apps/sim/socket-server/middleware/permissions.ts @@ -105,6 +105,7 @@ export async function verifyOperationPermission( 'update-trigger-mode', 'toggle-handles', 'duplicate', + 'duplicate-with-children', ], write: [ 'add', @@ -119,6 +120,7 @@ export async function verifyOperationPermission( 'update-trigger-mode', 'toggle-handles', 'duplicate', + 'duplicate-with-children', ], read: ['update-position'], // Read-only users can only move things around } diff --git a/apps/sim/socket-server/validation/schemas.ts b/apps/sim/socket-server/validation/schemas.ts index cd84bd67a1..ca02560259 100644 --- a/apps/sim/socket-server/validation/schemas.ts +++ b/apps/sim/socket-server/validation/schemas.ts @@ -67,18 +67,59 @@ export const EdgeOperationSchema = z.object({ operationId: z.string().optional(), }) -export const SubflowOperationSchema = z.object({ - operation: z.enum(['add', 'remove', 'update']), - target: z.literal('subflow'), - payload: z.object({ - id: z.string(), - type: z.enum(['loop', 'parallel']).optional(), - config: z.record(z.any()).optional(), - }), - timestamp: z.number(), - operationId: z.string().optional(), +// Shared schemas for subflow duplication +const BlockInsertPayloadSchema = z.object({ + id: z.string(), + sourceId: z.string().optional(), + type: z.string(), + name: z.string(), + position: PositionSchema, + data: z.record(z.any()).optional(), + subBlocks: z.record(z.any()).optional(), + outputs: z.record(z.any()).optional(), + parentId: z.string().nullable().optional(), + extent: z.enum(['parent']).nullable().optional(), + enabled: z.boolean().optional(), + horizontalHandles: z.boolean().optional(), + isWide: z.boolean().optional(), + advancedMode: z.boolean().optional(), + triggerMode: z.boolean().optional(), + height: z.number().optional(), +}) + +const EdgeInsertPayloadSchema = z.object({ + id: z.string(), + source: z.string(), + target: z.string(), + sourceHandle: z.string().nullable().optional(), + targetHandle: z.string().nullable().optional(), }) +export const SubflowOperationSchema = z.union([ + z.object({ + operation: z.literal('update'), + target: z.literal('subflow'), + payload: z.object({ + id: z.string(), + type: z.enum(['loop', 'parallel']).optional(), + config: z.record(z.any()).optional(), + }), + timestamp: z.number(), + operationId: z.string().optional(), + }), + z.object({ + operation: z.literal('duplicate-with-children'), + target: z.literal('subflow'), + payload: z.object({ + parent: BlockInsertPayloadSchema, + children: z.array(BlockInsertPayloadSchema), + edges: z.array(EdgeInsertPayloadSchema), + }), + timestamp: z.number(), + operationId: z.string().optional(), + }), +]) + export const VariableOperationSchema = z.union([ z.object({ operation: z.literal('add'), From 1e13f6ee752ac167014cf0df441c5193fa83d32d Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Mon, 15 Sep 2025 00:18:11 -0700 Subject: [PATCH 2/2] duplicated subflow --- apps/sim/app/api/workflows/[id]/yaml/route.ts | 4 +--- .../components/subflows/subflow-node.tsx | 7 +++++-- apps/sim/hooks/use-collaborative-workflow.ts | 14 +++++++++++--- apps/sim/lib/workflows/reference-utils.ts | 7 ++++--- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/apps/sim/app/api/workflows/[id]/yaml/route.ts b/apps/sim/app/api/workflows/[id]/yaml/route.ts index e1c0f7f3b3..87130e3895 100644 --- a/apps/sim/app/api/workflows/[id]/yaml/route.ts +++ b/apps/sim/app/api/workflows/[id]/yaml/route.ts @@ -10,8 +10,8 @@ import { loadWorkflowFromNormalizedTables, saveWorkflowToNormalizedTables, } from '@/lib/workflows/db-helpers' -import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/validation' import { updateBlockReferences } from '@/lib/workflows/reference-utils' +import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/validation' import { getUserId } from '@/app/api/auth/oauth/utils' import { getAllBlocks, getBlock } from '@/blocks' import type { BlockConfig } from '@/blocks/types' @@ -32,8 +32,6 @@ const YamlWorkflowRequestSchema = z.object({ createCheckpoint: z.boolean().optional().default(false), }) -// moved to shared util in '@/lib/workflows/reference-utils' - /** * Helper function to create a checkpoint before workflow changes */ diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx index cb7e7d4d4f..b242851f71 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node.tsx @@ -5,12 +5,12 @@ import { Handle, type NodeProps, Position, useReactFlow } from 'reactflow' import { StartIcon } from '@/components/icons' import { Button } from '@/components/ui/button' import { Card } from '@/components/ui/card' +import { createLogger } from '@/lib/logs/console/logger' import { cn } from '@/lib/utils' import { type DiffStatus, hasDiffStatus } from '@/lib/workflows/diff/types' import { IterationBadges } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/components/iteration-badges/iteration-badges' import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' -import { createLogger } from '@/lib/logs/console/logger' const logger = createLogger('SubflowNode') @@ -174,7 +174,10 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps {!isPreview && ( -
+