Skip to content

Commit 2700fb6

Browse files
committed
Fix Disconnected Output Nodes when Copy + Paste between Tabs
1 parent ff591fa commit 2700fb6

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";
@@ -866,14 +867,47 @@ const FlowCanvas = ({
866867
const onCopy = useCallback(() => {
867868
// Copy selected nodes to clipboard
868869
if (selectedNodes.length > 0) {
869-
const selectedNodesJson = JSON.stringify(selectedNodes);
870-
navigator.clipboard.writeText(selectedNodesJson).catch((err) => {
870+
const outputNodes = selectedNodes.filter(
871+
(node) => node.type === "output",
872+
);
873+
874+
const relevantOutputValues: Record<string, TaskOutputArgument> = {};
875+
876+
if (outputNodes.length > 0 && currentGraphSpec.outputValues) {
877+
outputNodes.forEach((node) => {
878+
const outputName = nodeManager.getRefId(node.id);
879+
880+
if (outputName && currentGraphSpec.outputValues?.[outputName]) {
881+
const outputValue = currentGraphSpec.outputValues[outputName];
882+
883+
// Only copy the output value if its task is also being copied -- otherwise the connection is not needed
884+
if (
885+
selectedNodes.some(
886+
(node) => node.data.taskId === outputValue.taskOutput.taskId,
887+
)
888+
) {
889+
relevantOutputValues[outputName] = outputValue;
890+
}
891+
}
892+
});
893+
}
894+
895+
const clipboardData = {
896+
nodes: selectedNodes,
897+
graphOutputValues: relevantOutputValues,
898+
version: "1.0",
899+
};
900+
901+
const clipboardJson = JSON.stringify(clipboardData);
902+
903+
navigator.clipboard.writeText(clipboardJson).catch((err) => {
871904
console.error("Failed to copy nodes to clipboard:", err);
872905
});
906+
873907
const message = `Copied ${selectedNodes.length} nodes to clipboard`;
874908
notify(message, "success");
875909
}
876-
}, [selectedNodes]);
910+
}, [selectedNodes, nodeManager, currentGraphSpec, notify]);
877911

878912
const onPaste = useCallback(() => {
879913
if (readOnly) return;
@@ -889,7 +923,9 @@ const FlowCanvas = ({
889923
return;
890924
}
891925

892-
const nodesToPaste: Node[] = parsedData;
926+
const nodesToPaste: Node[] = parsedData.nodes;
927+
const graphOutputValues: Record<string, TaskOutputArgument> =
928+
parsedData.graphOutputValues || {};
893929

894930
// Get the center of the canvas
895931
const { domNode } = store.getState();
@@ -910,7 +946,11 @@ const FlowCanvas = ({
910946
componentSpec,
911947
nodesToPaste,
912948
nodeManager,
913-
{ position: reactFlowCenter, connection: "internal" },
949+
{
950+
position: reactFlowCenter,
951+
connection: "internal",
952+
originGraphOutputValues: graphOutputValues,
953+
},
914954
);
915955

916956
// 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)