Skip to content

Commit 64a0be1

Browse files
committed
Fix Disconnected Output Nodes when Copy + Paste between Tabs
1 parent 70314be commit 64a0be1

File tree

3 files changed

+71
-23
lines changed

3 files changed

+71
-23
lines changed

src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
type ComponentSpec,
3737
type InputSpec,
3838
isNotMaterializedComponentReference,
39+
type TaskOutputArgument,
3940
type TaskSpec,
4041
} from "@/utils/componentSpec";
4142
import { loadComponentAsRefFromText } from "@/utils/componentStore";
@@ -812,14 +813,47 @@ const FlowCanvas = ({
812813
const onCopy = useCallback(() => {
813814
// Copy selected nodes to clipboard
814815
if (selectedNodes.length > 0) {
815-
const selectedNodesJson = JSON.stringify(selectedNodes);
816-
navigator.clipboard.writeText(selectedNodesJson).catch((err) => {
816+
const outputNodes = selectedNodes.filter(
817+
(node) => node.type === "output",
818+
);
819+
820+
const relevantOutputValues: Record<string, TaskOutputArgument> = {};
821+
822+
if (outputNodes.length > 0 && graphSpec.outputValues) {
823+
outputNodes.forEach((node) => {
824+
const outputName = nodeManager.getRefId(node.id);
825+
826+
if (outputName && graphSpec.outputValues?.[outputName]) {
827+
const outputValue = graphSpec.outputValues[outputName];
828+
829+
// Only copy the output value if its task is also being copied -- otherwise the connection is not needed
830+
if (
831+
selectedNodes.some(
832+
(node) => node.data.taskId === outputValue.taskOutput.taskId,
833+
)
834+
) {
835+
relevantOutputValues[outputName] = outputValue;
836+
}
837+
}
838+
});
839+
}
840+
841+
const clipboardData = {
842+
nodes: selectedNodes,
843+
graphOutputValues: relevantOutputValues,
844+
version: "1.0",
845+
};
846+
847+
const clipboardJson = JSON.stringify(clipboardData);
848+
849+
navigator.clipboard.writeText(clipboardJson).catch((err) => {
817850
console.error("Failed to copy nodes to clipboard:", err);
818851
});
852+
819853
const message = `Copied ${selectedNodes.length} nodes to clipboard`;
820854
notify(message, "success");
821855
}
822-
}, [selectedNodes]);
856+
}, [selectedNodes, nodeManager, graphSpec, notify]);
823857

824858
const onPaste = useCallback(() => {
825859
if (readOnly) return;
@@ -835,7 +869,9 @@ const FlowCanvas = ({
835869
return;
836870
}
837871

838-
const nodesToPaste: Node[] = parsedData;
872+
const nodesToPaste: Node[] = parsedData.nodes;
873+
const graphOutputValues: Record<string, TaskOutputArgument> =
874+
parsedData.graphOutputValues || {};
839875

840876
// Get the center of the canvas
841877
const { domNode } = store.getState();
@@ -856,7 +892,11 @@ const FlowCanvas = ({
856892
componentSpec,
857893
nodesToPaste,
858894
nodeManager,
859-
{ position: reactFlowCenter, connection: "internal" },
895+
{
896+
position: reactFlowCenter,
897+
connection: "internal",
898+
originGraphOutputValues: graphOutputValues,
899+
},
860900
);
861901

862902
// Deselect all existing nodes

src/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const duplicateNodes = (
4848
position?: XYPosition;
4949
connection?: ConnectionMode;
5050
status?: boolean;
51+
originGraphOutputValues?: Record<string, TaskOutputArgument>;
5152
},
5253
) => {
5354
if (!isGraphImplementation(componentSpec.implementation)) {
@@ -223,13 +224,11 @@ export const duplicateNodes = (
223224

224225
const oldOutputName = originalOutputNode.data.spec.name;
225226

226-
const originalOutputValue = graphSpec.outputValues?.[oldOutputName];
227+
const originalOutputValue =
228+
graphSpec.outputValues?.[oldOutputName] ??
229+
config?.originGraphOutputValues?.[oldOutputName];
227230

228231
if (!originalOutputValue) {
229-
// 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)
230-
console.warn(
231-
`No output value found for output ${oldOutputName} in graph spec.`,
232-
);
233232
return;
234233
}
235234

@@ -243,11 +242,20 @@ export const duplicateNodes = (
243242
const connectedTaskId = originalOutputValue.taskOutput.taskId;
244243
const connectedOutputName = originalOutputValue.taskOutput.outputName;
245244

246-
const originalNodeId = nodeManager.getNodeId(connectedTaskId, "task");
245+
const connectedTaskNode = nodesToDuplicate.find(
246+
(node) => isTaskNode(node) && node.data.taskId === connectedTaskId,
247+
);
248+
249+
if (!connectedTaskNode) {
250+
console.warn(
251+
`Connected task ${connectedTaskId} not found in duplicated nodes`,
252+
);
253+
return;
254+
}
247255

248-
const newTaskNodeId = nodeIdMap[originalNodeId];
256+
const newTaskNodeId = nodeIdMap[connectedTaskNode.id];
249257
if (!newTaskNodeId) {
250-
console.warn(`No mapping found for task node ${originalNodeId}`);
258+
console.warn(`No mapping found for task node ${connectedTaskNode.id}`);
251259
return;
252260
}
253261

src/components/shared/ReactFlow/FlowCanvas/utils/handleConnection.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ describe("handleConnection", () => {
180180
handleId === "target-handle"
181181
? {
182182
handleName: "taskInput",
183-
handleType: "handle_in",
183+
handleType: "handle-in",
184184
parentRefId: "task-1",
185185
}
186186
: undefined,
@@ -219,7 +219,7 @@ describe("handleConnection", () => {
219219
);
220220
vi.mocked(mockNodeManager.getHandleInfo).mockReturnValue({
221221
handleName: "",
222-
handleType: "handle_in",
222+
handleType: "handle-in",
223223
parentRefId: "task-1",
224224
});
225225

@@ -245,14 +245,14 @@ describe("handleConnection", () => {
245245
if (handleId === "source-handle") {
246246
return {
247247
handleName: "output1",
248-
handleType: "handle_out",
248+
handleType: "handle-out",
249249
parentRefId: "task-1",
250250
};
251251
}
252252
if (handleId === "target-handle") {
253253
return {
254254
handleName: "input1",
255-
handleType: "handle_in",
255+
handleType: "handle-in",
256256
parentRefId: "task-2",
257257
};
258258
}
@@ -294,14 +294,14 @@ describe("handleConnection", () => {
294294
if (handleId === "source-handle") {
295295
return {
296296
handleName: "",
297-
handleType: "handle_out",
297+
handleType: "handle-out",
298298
parentRefId: "task-1",
299299
};
300300
}
301301
if (handleId === "target-handle") {
302302
return {
303303
handleName: "input1",
304-
handleType: "handle_in",
304+
handleType: "handle-in",
305305
parentRefId: "task-2",
306306
};
307307
}
@@ -330,14 +330,14 @@ describe("handleConnection", () => {
330330
if (handleId === "source-handle") {
331331
return {
332332
handleName: "output1",
333-
handleType: "handle_out",
333+
handleType: "handle-out",
334334
parentRefId: "task-1",
335335
};
336336
}
337337
if (handleId === "target-handle") {
338338
return {
339339
handleName: "",
340-
handleType: "handle_in",
340+
handleType: "handle-in",
341341
parentRefId: "task-2",
342342
};
343343
}
@@ -368,7 +368,7 @@ describe("handleConnection", () => {
368368
handleId === "source-handle"
369369
? {
370370
handleName: "taskOutput",
371-
handleType: "handle_out",
371+
handleType: "handle-out",
372372
parentRefId: "task-1",
373373
}
374374
: undefined,
@@ -409,7 +409,7 @@ describe("handleConnection", () => {
409409
);
410410
vi.mocked(mockNodeManager.getHandleInfo).mockReturnValue({
411411
handleName: "", // Empty handle name
412-
handleType: "handle_out",
412+
handleType: "handle-out",
413413
parentRefId: "task-1",
414414
});
415415

0 commit comments

Comments
 (0)