Skip to content

Commit c3fb0e6

Browse files
committed
Fix IO Node Copy + Paste
1 parent 443bb5b commit c3fb0e6

File tree

3 files changed

+124
-91
lines changed

3 files changed

+124
-91
lines changed

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

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import { describe, expect, it, vi } from "vitest";
33

44
import { NodeManager } from "@/nodeManager";
55
import type { TaskNodeData } from "@/types/nodes";
6-
import type {
7-
ComponentSpec,
8-
InputSpec,
9-
OutputSpec,
10-
TaskOutputArgument,
11-
TaskSpec,
6+
import {
7+
type ComponentSpec,
8+
type InputSpec,
9+
isGraphImplementation,
10+
type OutputSpec,
11+
type TaskOutputArgument,
12+
type TaskSpec,
1213
} from "@/utils/componentSpec";
1314

1415
import { duplicateNodes } from "./duplicateNodes";
@@ -63,7 +64,7 @@ const createMockComponentSpecWithOutputs = (
6364
acc[output.name] = {
6465
taskOutput: {
6566
taskId: "task1",
66-
outputName: "result",
67+
outputName: output.name,
6768
},
6869
};
6970
return acc;
@@ -123,7 +124,7 @@ const createMockInputNode = (
123124
position,
124125
data: {
125126
label: inputName,
126-
inputSpec: { ...mockInputSpec, name: inputName },
127+
spec: { ...mockInputSpec, name: inputName },
127128
},
128129
selected: false,
129130
dragging: false,
@@ -144,7 +145,7 @@ const createMockOutputNode = (
144145
position,
145146
data: {
146147
label: outputName,
147-
outputSpec: { ...mockOutputSpec, name: outputName },
148+
spec: { ...mockOutputSpec, name: outputName },
148149
},
149150
selected: false,
150151
dragging: false,
@@ -606,7 +607,7 @@ describe("duplicateNodes", () => {
606607
connection: "all",
607608
});
608609

609-
if ("graph" in result.updatedComponentSpec.implementation!) {
610+
if (isGraphImplementation(result.updatedComponentSpec.implementation)) {
610611
const duplicatedTask =
611612
result.updatedComponentSpec.implementation.graph.tasks["task1 2"];
612613
expect(duplicatedTask.arguments?.input1).toEqual({
@@ -618,16 +619,35 @@ describe("duplicateNodes", () => {
618619
});
619620

620621
it("should handle graph output connections", () => {
621-
const taskSpec: TaskSpec = {
622-
...mockTaskSpec,
623-
arguments: {},
624-
};
625-
626622
const outputSpec: OutputSpec = {
627623
...mockOutputSpec,
628624
name: "graph-output",
629625
};
630626

627+
const taskComponentSpec: ComponentSpec = {
628+
name: "task-component",
629+
inputs: [],
630+
outputs: [
631+
{
632+
name: "graph-output",
633+
type: "String",
634+
annotations: {},
635+
},
636+
],
637+
implementation: {
638+
container: { image: "task-image" },
639+
},
640+
};
641+
642+
const taskSpec: TaskSpec = {
643+
...mockTaskSpec,
644+
arguments: {},
645+
componentRef: {
646+
name: "task-component",
647+
spec: taskComponentSpec,
648+
},
649+
};
650+
631651
const componentSpec = createMockComponentSpecWithOutputs(
632652
{ task1: taskSpec },
633653
[],
@@ -653,7 +673,7 @@ describe("duplicateNodes", () => {
653673
expect(outputValues?.["graph-output 2"]).toEqual({
654674
taskOutput: {
655675
taskId: "task1 2",
656-
outputName: "result",
676+
outputName: "graph-output",
657677
},
658678
});
659679
}

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

Lines changed: 85 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,7 @@ export const duplicateNodes = (
7171
const oldNodeId = node.id;
7272

7373
if (isTaskNode(node)) {
74-
const oldTaskId = nodeManager.getRefId(oldNodeId);
75-
if (!oldTaskId) {
76-
console.warn("Could not find taskId for node:", node);
77-
return;
78-
}
79-
74+
const oldTaskId = node.data.taskId;
8075
const newTaskId = getUniqueTaskId(graphSpec, oldTaskId);
8176
const newNodeId = nodeManager.getNodeId(newTaskId, "task");
8277

@@ -101,16 +96,14 @@ export const duplicateNodes = (
10196
};
10297
newTasks[newTaskId] = newTaskSpec;
10398
} else if (isInputNode(node)) {
104-
const inputSpec = componentSpec.inputs?.find(
105-
(input) => input.name === node.data.label,
106-
);
99+
const inputSpec = node.data.spec;
107100

108-
const newInputName = getUniqueInputName(componentSpec, inputSpec?.name);
101+
const newInputName = getUniqueInputName(componentSpec, inputSpec.name);
109102
const newNodeId = nodeManager.getNodeId(newInputName, "input");
110103

111104
nodeIdMap[oldNodeId] = newNodeId;
112105

113-
const annotations = inputSpec?.annotations || {};
106+
const annotations = inputSpec.annotations || {};
114107

115108
const updatedAnnotations = setPositionInAnnotations(annotations, {
116109
x: node.position.x + OFFSET,
@@ -125,19 +118,14 @@ export const duplicateNodes = (
125118

126119
newInputs[newInputName] = newInputSpec;
127120
} else if (isOutputNode(node)) {
128-
const outputSpec = componentSpec.outputs?.find(
129-
(output) => output.name === node.data.label,
130-
);
121+
const outputSpec = node.data.spec;
131122

132-
const newOutputName = getUniqueOutputName(
133-
componentSpec,
134-
outputSpec?.name,
135-
);
123+
const newOutputName = getUniqueOutputName(componentSpec, outputSpec.name);
136124
const newNodeId = nodeManager.getNodeId(newOutputName, "output");
137125

138126
nodeIdMap[oldNodeId] = newNodeId;
139127

140-
const annotations = outputSpec?.annotations || {};
128+
const annotations = outputSpec.annotations || {};
141129

142130
const updatedAnnotations = setPositionInAnnotations(annotations, {
143131
x: node.position.x + OFFSET,
@@ -192,10 +180,11 @@ export const duplicateNodes = (
192180
}
193181
});
194182

195-
// Outputs are defined in the graph spec
196183
const updatedGraphOutputs = { ...graphSpec.outputValues };
197-
if (connection !== "none") {
198-
/* Reconfigure Outputs */
184+
185+
// Output Nodes can only have one input, so the "external" connection mode does not apply to them.
186+
if (connection !== "none" && connection !== "external") {
187+
/* Reconfigure Output Nodes */
199188
Object.entries(newOutputs).forEach((output) => {
200189
const [outputName] = output;
201190
const newNodeId = nodeManager.getNodeId(outputName, "output");
@@ -207,57 +196,62 @@ export const duplicateNodes = (
207196
return;
208197
}
209198

210-
const oldOutputName = nodeManager.getRefId(oldNodeId);
199+
const originalOutputNode = nodesToDuplicate.find(
200+
(node) => node.id === oldNodeId && isOutputNode(node),
201+
);
211202

212-
if (!graphSpec.outputValues || !oldOutputName) {
203+
if (!originalOutputNode || !isOutputNode(originalOutputNode)) {
213204
return;
214205
}
215206

216-
const outputValue = graphSpec.outputValues[oldOutputName];
207+
const oldOutputName = originalOutputNode.data.spec.name;
208+
209+
const originalOutputValue = graphSpec.outputValues?.[oldOutputName];
217210

218-
if (!outputValue) {
211+
if (!originalOutputValue) {
212+
// 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)
213+
console.warn(
214+
`No output value found for output ${oldOutputName} in graph spec.`,
215+
);
219216
return;
220217
}
221218

222-
const updatedOutputValue = { ...outputValue };
223-
224-
// If the outputvalue references a task that was also duplicated (internal connection), we need to update it to refer to the duplicated task id
225-
let isInternal = false;
226-
if (
227-
typeof updatedOutputValue === "object" &&
228-
updatedOutputValue !== null &&
229-
connection !== "external"
230-
) {
231-
if ("taskOutput" in updatedOutputValue) {
232-
const oldTaskId = updatedOutputValue.taskOutput.taskId;
233-
const oldTaskNodeId = nodeManager.getNodeId(oldTaskId, "task");
234-
if (oldTaskNodeId in nodeIdMap) {
235-
const newTaskId = nodeManager.getRefId(nodeIdMap[oldTaskNodeId]);
236-
if (!newTaskId) {
237-
return;
238-
}
239-
240-
updatedOutputValue.taskOutput = {
241-
...updatedOutputValue.taskOutput,
242-
taskId: newTaskId,
243-
};
244-
245-
isInternal = true;
246-
}
247-
}
219+
if (!("taskOutput" in originalOutputValue)) {
220+
console.warn(
221+
`Output ${oldOutputName} is not connected to a task output`,
222+
);
223+
return;
248224
}
249225

250-
if (
251-
(isInternal && connection === "internal") ||
252-
(!isInternal && connection === "external") ||
253-
connection === "all"
254-
) {
255-
updatedGraphOutputs[outputName] = updatedOutputValue;
226+
const connectedTaskId = originalOutputValue.taskOutput.taskId;
227+
const connectedOutputName = originalOutputValue.taskOutput.outputName;
228+
229+
const originalNodeId = nodeManager.getNodeId(connectedTaskId, "task");
230+
231+
const newTaskNodeId = nodeIdMap[originalNodeId];
232+
if (!newTaskNodeId) {
233+
console.warn(`No mapping found for task node ${originalNodeId}`);
234+
return;
256235
}
236+
237+
const newTaskId = nodeManager.getRefId(newTaskNodeId);
238+
if (!newTaskId) {
239+
console.warn(`Could not get new task ID for node ${newTaskNodeId}`);
240+
return;
241+
}
242+
243+
const outputValue: TaskOutputArgument = {
244+
taskOutput: {
245+
taskId: newTaskId,
246+
outputName: connectedOutputName,
247+
},
248+
};
249+
250+
updatedGraphOutputs[outputName] = outputValue;
257251
});
258252
}
259253

260-
/* Update the Graph Spec & Inputs */
254+
/* Update the Graph Spec */
261255
const updatedTasks = { ...graphSpec.tasks, ...newTasks };
262256
const updatedGraphSpec = {
263257
...graphSpec,
@@ -513,43 +507,61 @@ function reconfigureConnections(
513507

514508
if ("taskOutput" in argument) {
515509
const oldTaskId = argument.taskOutput.taskId;
516-
oldNodeId = nodeManager.getNodeId(oldTaskId, "task");
517510

518-
if (!isGraphImplementation(componentSpec.implementation)) {
519-
throw new Error("ComponentSpec does not contain a graph implementation.");
511+
const taskNode = nodes.find(
512+
(node) => isTaskNode(node) && node.data.taskId === oldTaskId,
513+
);
514+
515+
if (taskNode) {
516+
oldNodeId = taskNode.id;
517+
isExternal = false;
518+
} else {
519+
if (!isGraphImplementation(componentSpec.implementation)) {
520+
throw new Error(
521+
"ComponentSpec does not contain a graph implementation.",
522+
);
523+
}
524+
525+
const graphSpec = componentSpec.implementation.graph;
526+
isExternal = oldTaskId in graphSpec.tasks;
520527
}
521528

522-
const graphSpec = componentSpec.implementation.graph;
523-
isExternal = oldTaskId in graphSpec.tasks;
529+
if (!oldNodeId) {
530+
return reconfigureExternalConnection(taskSpec, argKey, mode);
531+
}
524532

525533
const newNodeId = nodeIdMap[oldNodeId];
526-
527534
if (!newNodeId) {
528535
return reconfigureExternalConnection(taskSpec, argKey, mode);
529536
}
530537

531538
const newTaskId = nodeManager.getRefId(newNodeId);
532-
533539
newArgId = newTaskId;
534540
} else if ("graphInput" in argument) {
535541
const oldInputName = argument.graphInput.inputName;
536-
oldNodeId = nodeManager.getNodeId(oldInputName, "input");
537542

538-
if (!("inputs" in componentSpec)) {
539-
throw new Error("ComponentSpec does not contain inputs.");
543+
const inputNode = nodes.find(
544+
(node) => isInputNode(node) && node.data.spec.name === oldInputName,
545+
);
546+
547+
if (inputNode) {
548+
oldNodeId = inputNode.id;
549+
isExternal = false;
550+
} else {
551+
const inputs = componentSpec.inputs || [];
552+
isExternal = inputs.some((input) => input.name === oldInputName);
540553
}
541554

542-
const inputs = componentSpec.inputs || [];
543-
isExternal = inputs.some((input) => input.name === oldInputName);
555+
if (!oldNodeId) {
556+
return reconfigureExternalConnection(taskSpec, argKey, mode);
557+
}
544558

545559
const newNodeId = nodeIdMap[oldNodeId];
546-
547560
if (!newNodeId) {
548561
return reconfigureExternalConnection(taskSpec, argKey, mode);
549562
}
550563

551564
const newInputName = nodeManager.getRefId(newNodeId);
552-
553565
newArgId = newInputName;
554566
}
555567

src/utils/unique.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ const makeNameUniqueByAddingIndex = (
44
name: string,
55
existingNames: Set<string>,
66
): string => {
7-
let finalName = name;
7+
const baseName = name.replace(/ \(\d+\)$/, "");
8+
let finalName = baseName;
89
let index = 1;
910
while (existingNames.has(finalName)) {
1011
index++;
11-
finalName = name + " " + index.toString();
12+
finalName = baseName + " (" + index.toString() + ")";
1213
}
1314
return finalName;
1415
};

0 commit comments

Comments
 (0)