diff --git a/src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx b/src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx index ab4c75e54..b8bafcd66 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx +++ b/src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx @@ -36,6 +36,7 @@ import { type ComponentSpec, type InputSpec, isNotMaterializedComponentReference, + type TaskOutputArgument, type TaskSpec, } from "@/utils/componentSpec"; import { loadComponentAsRefFromText } from "@/utils/componentStore"; @@ -866,14 +867,47 @@ const FlowCanvas = ({ const onCopy = useCallback(() => { // Copy selected nodes to clipboard if (selectedNodes.length > 0) { - const selectedNodesJson = JSON.stringify(selectedNodes); - navigator.clipboard.writeText(selectedNodesJson).catch((err) => { + const outputNodes = selectedNodes.filter( + (node) => node.type === "output", + ); + + const relevantOutputValues: Record = {}; + + if (outputNodes.length > 0 && currentGraphSpec.outputValues) { + outputNodes.forEach((node) => { + const outputName = nodeManager.getRefId(node.id); + + if (outputName && currentGraphSpec.outputValues?.[outputName]) { + const outputValue = currentGraphSpec.outputValues[outputName]; + + // Only copy the output value if its task is also being copied -- otherwise the connection is not needed + if ( + selectedNodes.some( + (node) => node.data.taskId === outputValue.taskOutput.taskId, + ) + ) { + relevantOutputValues[outputName] = outputValue; + } + } + }); + } + + const clipboardData = { + nodes: selectedNodes, + graphOutputValues: relevantOutputValues, + version: "1.0", + }; + + const clipboardJson = JSON.stringify(clipboardData); + + navigator.clipboard.writeText(clipboardJson).catch((err) => { console.error("Failed to copy nodes to clipboard:", err); }); + const message = `Copied ${selectedNodes.length} nodes to clipboard`; notify(message, "success"); } - }, [selectedNodes]); + }, [selectedNodes, nodeManager, currentGraphSpec, notify]); const onPaste = useCallback(() => { if (readOnly) return; @@ -889,7 +923,9 @@ const FlowCanvas = ({ return; } - const nodesToPaste: Node[] = parsedData; + const nodesToPaste: Node[] = parsedData.nodes; + const graphOutputValues: Record = + parsedData.graphOutputValues || {}; // Get the center of the canvas const { domNode } = store.getState(); @@ -910,7 +946,11 @@ const FlowCanvas = ({ componentSpec, nodesToPaste, nodeManager, - { position: reactFlowCenter, connection: "internal" }, + { + position: reactFlowCenter, + connection: "internal", + originGraphOutputValues: graphOutputValues, + }, ); // Deselect all existing nodes diff --git a/src/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes.ts b/src/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes.ts index fc160146b..2717b6f7d 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes.ts +++ b/src/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes.ts @@ -48,6 +48,7 @@ export const duplicateNodes = ( position?: XYPosition; connection?: ConnectionMode; status?: boolean; + originGraphOutputValues?: Record; }, ) => { if (!isGraphImplementation(componentSpec.implementation)) { @@ -223,13 +224,11 @@ export const duplicateNodes = ( const oldOutputName = originalOutputNode.data.spec.name; - const originalOutputValue = graphSpec.outputValues?.[oldOutputName]; + const originalOutputValue = + graphSpec.outputValues?.[oldOutputName] ?? + config?.originGraphOutputValues?.[oldOutputName]; if (!originalOutputValue) { - // Todo: Handle cross-instance copy + paste for output nodes (we don't have the original graphSpec available so we can't look up the output value) - console.warn( - `No output value found for output ${oldOutputName} in graph spec.`, - ); return; } @@ -243,11 +242,20 @@ export const duplicateNodes = ( const connectedTaskId = originalOutputValue.taskOutput.taskId; const connectedOutputName = originalOutputValue.taskOutput.outputName; - const originalNodeId = nodeManager.getNodeId(connectedTaskId, "task"); + const connectedTaskNode = nodesToDuplicate.find( + (node) => isTaskNode(node) && node.data.taskId === connectedTaskId, + ); + + if (!connectedTaskNode) { + console.warn( + `Connected task ${connectedTaskId} not found in duplicated nodes`, + ); + return; + } - const newTaskNodeId = nodeIdMap[originalNodeId]; + const newTaskNodeId = nodeIdMap[connectedTaskNode.id]; if (!newTaskNodeId) { - console.warn(`No mapping found for task node ${originalNodeId}`); + console.warn(`No mapping found for task node ${connectedTaskNode.id}`); return; } diff --git a/src/components/shared/ReactFlow/FlowCanvas/utils/handleConnection.test.ts b/src/components/shared/ReactFlow/FlowCanvas/utils/handleConnection.test.ts index 8933b6eb3..6d44f85e0 100644 --- a/src/components/shared/ReactFlow/FlowCanvas/utils/handleConnection.test.ts +++ b/src/components/shared/ReactFlow/FlowCanvas/utils/handleConnection.test.ts @@ -180,7 +180,7 @@ describe("handleConnection", () => { handleId === "target-handle" ? { handleName: "taskInput", - handleType: "handle_in", + handleType: "handle-in", parentRefId: "task-1", } : undefined, @@ -219,7 +219,7 @@ describe("handleConnection", () => { ); vi.mocked(mockNodeManager.getHandleInfo).mockReturnValue({ handleName: "", - handleType: "handle_in", + handleType: "handle-in", parentRefId: "task-1", }); @@ -245,14 +245,14 @@ describe("handleConnection", () => { if (handleId === "source-handle") { return { handleName: "output1", - handleType: "handle_out", + handleType: "handle-out", parentRefId: "task-1", }; } if (handleId === "target-handle") { return { handleName: "input1", - handleType: "handle_in", + handleType: "handle-in", parentRefId: "task-2", }; } @@ -294,14 +294,14 @@ describe("handleConnection", () => { if (handleId === "source-handle") { return { handleName: "", - handleType: "handle_out", + handleType: "handle-out", parentRefId: "task-1", }; } if (handleId === "target-handle") { return { handleName: "input1", - handleType: "handle_in", + handleType: "handle-in", parentRefId: "task-2", }; } @@ -330,14 +330,14 @@ describe("handleConnection", () => { if (handleId === "source-handle") { return { handleName: "output1", - handleType: "handle_out", + handleType: "handle-out", parentRefId: "task-1", }; } if (handleId === "target-handle") { return { handleName: "", - handleType: "handle_in", + handleType: "handle-in", parentRefId: "task-2", }; } @@ -368,7 +368,7 @@ describe("handleConnection", () => { handleId === "source-handle" ? { handleName: "taskOutput", - handleType: "handle_out", + handleType: "handle-out", parentRefId: "task-1", } : undefined, @@ -409,7 +409,7 @@ describe("handleConnection", () => { ); vi.mocked(mockNodeManager.getHandleInfo).mockReturnValue({ handleName: "", // Empty handle name - handleType: "handle_out", + handleType: "handle-out", parentRefId: "task-1", });