Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 45 additions & 5 deletions src/components/shared/ReactFlow/FlowCanvas/FlowCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
type ComponentSpec,
type InputSpec,
isNotMaterializedComponentReference,
type TaskOutputArgument,
type TaskSpec,
} from "@/utils/componentSpec";
import { loadComponentAsRefFromText } from "@/utils/componentStore";
Expand Down Expand Up @@ -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<string, TaskOutputArgument> = {};

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;
Expand All @@ -889,7 +923,9 @@ const FlowCanvas = ({
return;
}

const nodesToPaste: Node[] = parsedData;
const nodesToPaste: Node[] = parsedData.nodes;
const graphOutputValues: Record<string, TaskOutputArgument> =
parsedData.graphOutputValues || {};

// Get the center of the canvas
const { domNode } = store.getState();
Expand All @@ -910,7 +946,11 @@ const FlowCanvas = ({
componentSpec,
nodesToPaste,
nodeManager,
{ position: reactFlowCenter, connection: "internal" },
{
position: reactFlowCenter,
connection: "internal",
originGraphOutputValues: graphOutputValues,
},
);

// Deselect all existing nodes
Expand Down
24 changes: 16 additions & 8 deletions src/components/shared/ReactFlow/FlowCanvas/utils/duplicateNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const duplicateNodes = (
position?: XYPosition;
connection?: ConnectionMode;
status?: boolean;
originGraphOutputValues?: Record<string, TaskOutputArgument>;
},
) => {
if (!isGraphImplementation(componentSpec.implementation)) {
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ describe("handleConnection", () => {
handleId === "target-handle"
? {
handleName: "taskInput",
handleType: "handle_in",
handleType: "handle-in",
parentRefId: "task-1",
}
: undefined,
Expand Down Expand Up @@ -219,7 +219,7 @@ describe("handleConnection", () => {
);
vi.mocked(mockNodeManager.getHandleInfo).mockReturnValue({
handleName: "",
handleType: "handle_in",
handleType: "handle-in",
parentRefId: "task-1",
});

Expand All @@ -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",
};
}
Expand Down Expand Up @@ -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",
};
}
Expand Down Expand Up @@ -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",
};
}
Expand Down Expand Up @@ -368,7 +368,7 @@ describe("handleConnection", () => {
handleId === "source-handle"
? {
handleName: "taskOutput",
handleType: "handle_out",
handleType: "handle-out",
parentRefId: "task-1",
}
: undefined,
Expand Down Expand Up @@ -409,7 +409,7 @@ describe("handleConnection", () => {
);
vi.mocked(mockNodeManager.getHandleInfo).mockReturnValue({
handleName: "", // Empty handle name
handleType: "handle_out",
handleType: "handle-out",
parentRefId: "task-1",
});

Expand Down