feat(editor): Add ability to extract sub-workflows to canvas context menu (#15538)

This commit is contained in:
Charlie Kolb
2025-06-02 12:17:27 +02:00
committed by GitHub
parent 096806af15
commit 5985df6e51
23 changed files with 2070 additions and 373 deletions

View File

@@ -40,6 +40,7 @@ import {
RemoveConnectionCommand,
RemoveNodeCommand,
RenameNodeCommand,
ReplaceNodeParametersCommand,
} from '@/models/history';
import { useCanvasStore } from '@/stores/canvas.store';
import { useCredentialsStore } from '@/stores/credentials.store';
@@ -97,6 +98,7 @@ import type {
NodeParameterValueType,
Workflow,
NodeConnectionType,
INodeParameters,
} from 'n8n-workflow';
import { deepCopy, NodeConnectionTypes, NodeHelpers, TelemetryHelpers } from 'n8n-workflow';
import { computed, nextTick, ref } from 'vue';
@@ -124,6 +126,7 @@ type AddNodesBaseOptions = {
trackHistory?: boolean;
keepPristine?: boolean;
telemetry?: boolean;
forcePosition?: boolean;
viewport?: ViewportBoundaries;
};
@@ -245,6 +248,42 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
updateNodePosition(node.id, position);
}
function replaceNodeParameters(
nodeId: string,
currentParameters: INodeParameters,
newParameters: INodeParameters,
{ trackHistory = false, trackBulk = true } = {},
) {
const node = workflowsStore.getNodeById(nodeId);
if (!node) return;
if (trackHistory && trackBulk) {
historyStore.startRecordingUndo();
}
workflowsStore.setNodeParameters({
name: node.name,
value: newParameters,
});
if (trackHistory) {
historyStore.pushCommandToUndo(
new ReplaceNodeParametersCommand(nodeId, currentParameters, newParameters, Date.now()),
);
}
if (trackHistory && trackBulk) {
historyStore.stopRecordingUndo();
}
}
async function revertReplaceNodeParameters(
nodeId: string,
currentParameters: INodeParameters,
newParameters: INodeParameters,
) {
replaceNodeParameters(nodeId, newParameters, currentParameters);
}
async function renameNode(
currentName: string,
newName: string,
@@ -419,6 +458,63 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
}
}
function replaceNodeConnections(
previousId: string,
newId: string,
{ trackHistory = false, trackBulk = true, replaceInputs = true, replaceOutputs = true } = {},
) {
const previousNode = workflowsStore.getNodeById(previousId);
const newNode = workflowsStore.getNodeById(newId);
if (!previousNode || !newNode) {
return;
}
const wf = workflowsStore.getCurrentWorkflow();
const inputNodeNames = replaceInputs ? wf.getParentNodes(previousNode.name, 'main', 1) : [];
const outputNodeNames = replaceOutputs ? wf.getChildNodes(previousNode.name, 'main', 1) : [];
const connectionPairs = [
...wf.getConnectionsBetweenNodes(inputNodeNames, [previousNode.name]),
...wf.getConnectionsBetweenNodes([previousNode.name], outputNodeNames),
];
if (trackHistory && trackBulk) {
historyStore.startRecordingUndo();
}
for (const pair of connectionPairs) {
const sourceNode = workflowsStore.getNodeByName(pair[0].node);
const targetNode = workflowsStore.getNodeByName(pair[1].node);
if (!sourceNode || !targetNode) continue;
const oldCanvasConnection = mapLegacyConnectionToCanvasConnection(
sourceNode,
targetNode,
pair,
);
deleteConnection(oldCanvasConnection, { trackHistory, trackBulk: false });
const newCanvasConnection = mapLegacyConnectionToCanvasConnection(
sourceNode.name === previousNode.name ? newNode : sourceNode,
targetNode.name === previousNode.name ? newNode : targetNode,
[
{
...pair[0],
node: pair[0].node === previousNode.name ? newNode.name : pair[0].node,
},
{
...pair[1],
node: pair[1].node === previousNode.name ? newNode.name : pair[1].node,
},
],
);
createConnection(newCanvasConnection, { trackHistory });
}
if (trackHistory && trackBulk) {
historyStore.stopRecordingUndo();
}
}
function setNodeActive(id: string) {
const node = workflowsStore.getNodeById(id);
if (!node) {
@@ -710,7 +806,6 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
const lastInteractedWithNodeId = lastInteractedWithNode.id;
const lastInteractedWithNodeConnection = uiStore.lastInteractedWithNodeConnection;
const lastInteractedWithNodeHandle = uiStore.lastInteractedWithNodeHandle;
// If we have a specific endpoint to connect to
if (lastInteractedWithNodeHandle) {
const { type: connectionType, mode } = parseCanvasConnectionHandleString(
@@ -813,15 +908,18 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
function resolveNodeData(
node: AddNodeDataWithTypeVersion,
nodeTypeDescription: INodeTypeDescription,
options: { viewport?: ViewportBoundaries } = {},
options: { viewport?: ViewportBoundaries; forcePosition?: boolean } = {},
) {
const id = node.id ?? nodeHelpers.assignNodeId(node as INodeUi);
const name = node.name ?? (nodeTypeDescription.defaults.name as string);
const type = nodeTypeDescription.name;
const typeVersion = node.typeVersion;
const position = resolveNodePosition(node as INodeUi, nodeTypeDescription, {
viewport: options.viewport,
});
const position =
options.forcePosition && node.position
? node.position
: resolveNodePosition(node as INodeUi, nodeTypeDescription, {
viewport: options.viewport,
});
const disabled = node.disabled ?? false;
const parameters = node.parameters ?? {};
@@ -2110,6 +2208,8 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
setNodeParameters,
renameNode,
revertRenameNode,
replaceNodeParameters,
revertReplaceNodeParameters,
deleteNode,
deleteNodes,
copyNodes,
@@ -2136,6 +2236,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
openExecution,
startChat,
importTemplate,
replaceNodeConnections,
tryToOpenSubworkflowInNewTab,
};
}