mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Connect adjacent nodes when a node is deleted in canvas v2 (no-changelog) (#10125)
This commit is contained in:
parent
b1816db449
commit
a036aa43dc
|
@ -398,6 +398,70 @@ describe('useCanvasOperations', () => {
|
||||||
expect(removeNodeExecutionDataByIdSpy).toHaveBeenCalledWith(id);
|
expect(removeNodeExecutionDataByIdSpy).toHaveBeenCalledWith(id);
|
||||||
expect(pushCommandToUndoSpy).not.toHaveBeenCalled();
|
expect(pushCommandToUndoSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should connect adjacent nodes when deleting a node surrounded by other nodes', () => {
|
||||||
|
nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'node' })]);
|
||||||
|
const nodes = [
|
||||||
|
createTestNode({
|
||||||
|
id: 'input',
|
||||||
|
type: 'node',
|
||||||
|
position: [10, 20],
|
||||||
|
name: 'Input Node',
|
||||||
|
}),
|
||||||
|
createTestNode({
|
||||||
|
id: 'middle',
|
||||||
|
type: 'node',
|
||||||
|
position: [10, 20],
|
||||||
|
name: 'Middle Node',
|
||||||
|
}),
|
||||||
|
createTestNode({
|
||||||
|
id: 'output',
|
||||||
|
type: 'node',
|
||||||
|
position: [10, 20],
|
||||||
|
name: 'Output Node',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
workflowsStore.setNodes(nodes);
|
||||||
|
workflowsStore.setConnections({
|
||||||
|
'Input Node': {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'Middle Node',
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'Middle Node': {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'Output Node',
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
canvasOperations.deleteNode('middle');
|
||||||
|
expect(workflowsStore.allConnections).toEqual({
|
||||||
|
'Input Node': {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'Output Node',
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('revertDeleteNode', () => {
|
describe('revertDeleteNode', () => {
|
||||||
|
|
|
@ -74,6 +74,7 @@ import type {
|
||||||
IPinData,
|
IPinData,
|
||||||
ITelemetryTrackProperties,
|
ITelemetryTrackProperties,
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
|
NodeInputConnections,
|
||||||
NodeParameterValueType,
|
NodeParameterValueType,
|
||||||
Workflow,
|
Workflow,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
@ -202,6 +203,49 @@ export function useCanvasOperations({
|
||||||
await renameNode(currentName, previousName);
|
await renameNode(currentName, previousName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function connectAdjacentNodes(id: string) {
|
||||||
|
const node = workflowsStore.getNodeById(id);
|
||||||
|
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputConnectionsByType = workflowsStore.outgoingConnectionsByNodeName(node.name);
|
||||||
|
const incomingConnectionsByType = workflowsStore.incomingConnectionsByNodeName(node.name);
|
||||||
|
|
||||||
|
for (const [type, incomingConnectionsByInputIndex] of Object.entries(
|
||||||
|
incomingConnectionsByType,
|
||||||
|
) as Array<[NodeConnectionType, NodeInputConnections]>) {
|
||||||
|
// Only connect nodes connected to the first input of a type
|
||||||
|
for (const incomingConnection of incomingConnectionsByInputIndex.at(0) ?? []) {
|
||||||
|
const incomingNodeId = workflowsStore.getNodeByName(incomingConnection.node)?.id;
|
||||||
|
|
||||||
|
if (!incomingNodeId) continue;
|
||||||
|
|
||||||
|
// Only connect to nodes connected to the first output of a type
|
||||||
|
// For example on an If node, connect to the "true" main output
|
||||||
|
for (const outgoingConnection of outputConnectionsByType[type]?.at(0) ?? []) {
|
||||||
|
const outgoingNodeId = workflowsStore.getNodeByName(outgoingConnection.node)?.id;
|
||||||
|
|
||||||
|
if (!outgoingNodeId) continue;
|
||||||
|
|
||||||
|
createConnection({
|
||||||
|
source: incomingNodeId,
|
||||||
|
sourceHandle: createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Output,
|
||||||
|
type,
|
||||||
|
}),
|
||||||
|
target: outgoingNodeId,
|
||||||
|
targetHandle: createCanvasConnectionHandleString({
|
||||||
|
mode: CanvasConnectionMode.Input,
|
||||||
|
type,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function deleteNode(id: string, { trackHistory = false, trackBulk = true } = {}) {
|
function deleteNode(id: string, { trackHistory = false, trackBulk = true } = {}) {
|
||||||
const node = workflowsStore.getNodeById(id);
|
const node = workflowsStore.getNodeById(id);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
|
@ -212,6 +256,7 @@ export function useCanvasOperations({
|
||||||
historyStore.startRecordingUndo();
|
historyStore.startRecordingUndo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectAdjacentNodes(id);
|
||||||
workflowsStore.removeNodeConnectionsById(id);
|
workflowsStore.removeNodeConnectionsById(id);
|
||||||
workflowsStore.removeNodeExecutionDataById(id);
|
workflowsStore.removeNodeExecutionDataById(id);
|
||||||
workflowsStore.removeNodeById(id);
|
workflowsStore.removeNodeById(id);
|
||||||
|
|
|
@ -224,6 +224,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function incomingConnectionsByNodeName(nodeName: string): INodeConnections {
|
||||||
|
return getCurrentWorkflow().connectionsByDestinationNode[nodeName] ?? {};
|
||||||
|
}
|
||||||
|
|
||||||
function nodeHasOutputConnection(nodeName: string): boolean {
|
function nodeHasOutputConnection(nodeName: string): boolean {
|
||||||
return workflow.value.connections.hasOwnProperty(nodeName);
|
return workflow.value.connections.hasOwnProperty(nodeName);
|
||||||
}
|
}
|
||||||
|
@ -885,7 +889,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
uiStore.stateIsDirty = true;
|
uiStore.stateIsDirty = true;
|
||||||
|
|
||||||
// Remove all source connections
|
// Remove all source connections
|
||||||
if (!preserveOutputConnections && workflow.value.connections.hasOwnProperty(node.name)) {
|
if (!preserveOutputConnections) {
|
||||||
delete workflow.value.connections[node.name];
|
delete workflow.value.connections[node.name];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1583,6 +1587,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
getTotalFinishedExecutionsCount,
|
getTotalFinishedExecutionsCount,
|
||||||
getPastChatMessages,
|
getPastChatMessages,
|
||||||
outgoingConnectionsByNodeName,
|
outgoingConnectionsByNodeName,
|
||||||
|
incomingConnectionsByNodeName,
|
||||||
nodeHasOutputConnection,
|
nodeHasOutputConnection,
|
||||||
isNodeInOutgoingNodeConnections,
|
isNodeInOutgoingNodeConnections,
|
||||||
getWorkflowById,
|
getWorkflowById,
|
||||||
|
|
Loading…
Reference in a new issue