mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Add undo/redo support for canvas actions (#4787)
* ✨ Added history store and mixin * ✨ Implemented node position change undo/redo * ✨ Implemented move nodes bulk command * ⚡ Not clearing the redo stack after pushing the bulk command * 🔨 Implemented commands using classes * 🔥 Removed unnecessary interfaces and actions * 🔥 Removing unused constants * 🔨 Refactoring classes file * ⚡ Adding eventBus to command obects * ✨ Added undo/redo support for adding and removing nodes * ✨ Implemented initial add/remove connections undo support * ⚡ Covering some corner cases with reconnecting nodes * ⚡ Adding undo support for reconnecting nodes * ⚡ Fixing going back and forward between undo and redo * ✨ Implemented async command revert * ⚡ Preventing push to undo if bulk redo/undo is in progress * ⚡ Handling re-connecting nodes and stopped pushing empty bulk actions to undo stack * ✨ Handling adding a node between two connected nodes * ⚡ Handling the case of removing multiple connections on the same index. Adding debounce to undo/redo keyboard calls * ⚡ Removing unnecessary timeouts, adding missing awaits, refactoring * ⚡ Resetting history when opening new workflow, fixing incorrect bulk recording when inserting node * ✔️ Fixing lint error * ⚡ Minor refactoring + some temporary debugging logs * ⚡ Preserving node properties when undoing it's removal, removing some unused repaint code * ✨ Added undo/redo support for import workflow and node enable/disable * 🔥 Removing some unused constant * ✨ Added undo/redo support for renaming nodes * ⚡ Fixing rename history recording * ✨ Added undo/redo support for duplicating nodes * 📈 Implemented telemetry events * 🔨 A bit of refactoring * ⚡ Fixing edgecases in removing connection and moving nodes * ⚡ Handling case of adding duplicate nodes when going back and forward in history * ⚡ Recording connections added directly to store * ⚡ Moving main history reset after wf is opened * 🔨 Simplifying rename recording * 📈 Adding NDV telemetry event, updating existing event name case * 📈 Updating telemetry events * ⚡ Fixing duplicate connections on undo/redo * ⚡ Stopping undo events from firing constantly on keydown * 📈 Updated telemetry event for hitting undo in NDV * ⚡ Adding undo support for disabling nodes using keyboard shortcuts * ⚡ Preventing adding duplicate connection commands to history * ⚡ Clearing redo stack when new change is added * ⚡ Preventing adding connection actions to undo stack while redoing them * 👌 Addressing PR comments part 1 * 👌 Moving undo logic for disabling nodes to `NodeView` * 👌 Implemented command comparing logic * ⚡ Fix for not clearing redo stack on every user action * ⚡ Fixing recording when moving nodes * ⚡ Fixing undo for moving connections * ⚡ Fixing tracking new nodes after latest merge * ⚡ Fixing broken bulk delete * ⚡ Preventing undo/redo when not on main node view tab * 👌 Addressing PR comments * 👌 Addressing PR comment
This commit is contained in:
committed by
GitHub
parent
38d7300d2a
commit
b2aba48dfe
@@ -41,7 +41,7 @@
|
||||
@deselectAllNodes="deselectAllNodes"
|
||||
@deselectNode="nodeDeselectedByName"
|
||||
@nodeSelected="nodeSelectedByName"
|
||||
@removeNode="removeNode"
|
||||
@removeNode="(name) => removeNode(name, true)"
|
||||
@runWorkflow="onRunNode"
|
||||
@moved="onNodeMoved"
|
||||
@run="onNodeRun"
|
||||
@@ -64,7 +64,7 @@
|
||||
@deselectAllNodes="deselectAllNodes"
|
||||
@deselectNode="nodeDeselectedByName"
|
||||
@nodeSelected="nodeSelectedByName"
|
||||
@removeNode="removeNode"
|
||||
@removeNode="(name) => removeNode(name, true)"
|
||||
:key="`${nodeData.id}_sticky`"
|
||||
:name="nodeData.name"
|
||||
:isReadOnly="isReadOnly"
|
||||
@@ -233,7 +233,9 @@ import { dataPinningEventBus } from '@/event-bus/data-pinning-event-bus';
|
||||
import { useCanvasStore } from '@/stores/canvas';
|
||||
import useWorkflowsEEStore from "@/stores/workflows.ee";
|
||||
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
||||
import { getAccountAge, getNodeViewTab } from '@/utils';
|
||||
import { getAccountAge, getConnectionInfo, getNodeViewTab } from '@/utils';
|
||||
import { useHistoryStore } from '@/stores/history';
|
||||
import { AddConnectionCommand, AddNodeCommand, MoveNodeCommand, RemoveConnectionCommand, RemoveNodeCommand, RenameNodeCommand } from '@/models/history';
|
||||
|
||||
interface AddNodeOptions {
|
||||
position?: XYPosition;
|
||||
@@ -407,6 +409,7 @@ export default mixins(
|
||||
useUsersStore,
|
||||
useNodeCreatorStore,
|
||||
useWorkflowsEEStore,
|
||||
useHistoryStore,
|
||||
),
|
||||
nativelyNumberSuffixedDefaults(): string[] {
|
||||
return this.rootStore.nativelyNumberSuffixedDefaults;
|
||||
@@ -545,6 +548,10 @@ export default mixins(
|
||||
showTriggerMissingTooltip: false,
|
||||
workflowData: null as INewWorkflowData | null,
|
||||
isProductionExecutionPreview: false,
|
||||
// jsplumb automatically deletes all loose connections which is in turn recorded
|
||||
// in undo history as a user action.
|
||||
// This should prevent automatically removed connections from populating undo stack
|
||||
suspendRecordingDetachedConnections: false,
|
||||
};
|
||||
},
|
||||
beforeDestroy() {
|
||||
@@ -1117,7 +1124,7 @@ export default mixins(
|
||||
if (!this.editAllowedCheck()) {
|
||||
return;
|
||||
}
|
||||
this.disableNodes(this.uiStore.getSelectedNodes);
|
||||
this.disableNodes(this.uiStore.getSelectedNodes, true);
|
||||
},
|
||||
|
||||
deleteSelectedNodes() {
|
||||
@@ -1127,9 +1134,13 @@ export default mixins(
|
||||
const nodesToDelete: string[] = this.uiStore.getSelectedNodes.map((node: INodeUi) => {
|
||||
return node.name;
|
||||
});
|
||||
this.historyStore.startRecordingUndo();
|
||||
nodesToDelete.forEach((nodeName: string) => {
|
||||
this.removeNode(nodeName);
|
||||
this.removeNode(nodeName, true, false);
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.historyStore.stopRecordingUndo();
|
||||
}, 200);
|
||||
},
|
||||
|
||||
selectAllNodes() {
|
||||
@@ -1173,12 +1184,13 @@ export default mixins(
|
||||
this.nodeSelectedByName(lastSelectedNode.name);
|
||||
},
|
||||
|
||||
pushDownstreamNodes(sourceNodeName: string, margin: number) {
|
||||
pushDownstreamNodes(sourceNodeName: string, margin: number, recordHistory = false) {
|
||||
const sourceNode = this.workflowsStore.nodesByName[sourceNodeName];
|
||||
const workflow = this.getCurrentWorkflow();
|
||||
const childNodes = workflow.getChildNodes(sourceNodeName);
|
||||
for (const nodeName of childNodes) {
|
||||
const node = this.workflowsStore.nodesByName[nodeName] as INodeUi;
|
||||
const oldPosition = node.position;
|
||||
|
||||
if (node.position[0] < sourceNode.position[0]) {
|
||||
continue;
|
||||
@@ -1193,6 +1205,10 @@ export default mixins(
|
||||
|
||||
this.workflowsStore.updateNodeProperties(updateInformation);
|
||||
this.onNodeMoved(node);
|
||||
|
||||
if (recordHistory && oldPosition[0] !== node.position[0] || oldPosition[1] !== node.position[1]) {
|
||||
this.historyStore.pushCommandToUndo(new MoveNodeCommand(nodeName, oldPosition, node.position, this), recordHistory);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1621,7 +1637,7 @@ export default mixins(
|
||||
return newNodeData;
|
||||
},
|
||||
|
||||
async injectNode (nodeTypeName: string, options: AddNodeOptions = {}, showDetail = true) {
|
||||
async injectNode (nodeTypeName: string, options: AddNodeOptions = {}, showDetail = true, trackHistory = false) {
|
||||
const nodeTypeData: INodeTypeDescription | null = this.nodeTypesStore.getNodeType(nodeTypeName);
|
||||
|
||||
if (nodeTypeData === null) {
|
||||
@@ -1653,7 +1669,7 @@ export default mixins(
|
||||
if (lastSelectedConnection) { // set when injecting into a connection
|
||||
const [diffX] = NodeViewUtils.getConnectorLengths(lastSelectedConnection);
|
||||
if (diffX <= NodeViewUtils.MAX_X_TO_PUSH_DOWNSTREAM_NODES) {
|
||||
this.pushDownstreamNodes(lastSelectedNode.name, NodeViewUtils.PUSH_NODES_OFFSET);
|
||||
this.pushDownstreamNodes(lastSelectedNode.name, NodeViewUtils.PUSH_NODES_OFFSET, trackHistory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1706,7 +1722,7 @@ export default mixins(
|
||||
newNodeData.webhookId = uuid();
|
||||
}
|
||||
|
||||
await this.addNodes([newNodeData]);
|
||||
await this.addNodes([newNodeData], undefined, trackHistory);
|
||||
|
||||
this.uiStore.stateIsDirty = true;
|
||||
|
||||
@@ -1728,13 +1744,15 @@ export default mixins(
|
||||
}
|
||||
|
||||
// Automatically deselect all nodes and select the current one and also active
|
||||
// current node
|
||||
this.deselectAllNodes();
|
||||
const preventDetailOpen = window.posthog?.getFeatureFlag && window.posthog?.getFeatureFlag('prevent-ndv-auto-open') === 'prevent';
|
||||
if(showDetail && !preventDetailOpen) {
|
||||
setTimeout(() => {
|
||||
this.nodeSelectedByName(newNodeData.name, nodeTypeName !== STICKY_NODE_TYPE);
|
||||
});
|
||||
// current node. But only if it's added manually by the user (not by undo/redo mechanism)
|
||||
if(trackHistory) {
|
||||
this.deselectAllNodes();
|
||||
const preventDetailOpen = window.posthog?.getFeatureFlag && window.posthog?.getFeatureFlag('prevent-ndv-auto-open') === 'prevent';
|
||||
if(showDetail && !preventDetailOpen) {
|
||||
setTimeout(() => {
|
||||
this.nodeSelectedByName(newNodeData.name, nodeTypeName !== STICKY_NODE_TYPE);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return newNodeData;
|
||||
@@ -1751,7 +1769,7 @@ export default mixins(
|
||||
|
||||
return undefined;
|
||||
},
|
||||
connectTwoNodes(sourceNodeName: string, sourceNodeOutputIndex: number, targetNodeName: string, targetNodeOuputIndex: number) {
|
||||
connectTwoNodes(sourceNodeName: string, sourceNodeOutputIndex: number, targetNodeName: string, targetNodeOuputIndex: number, trackHistory = false) {
|
||||
if (this.getConnection(sourceNodeName, sourceNodeOutputIndex, targetNodeName, targetNodeOuputIndex)) {
|
||||
return;
|
||||
}
|
||||
@@ -1771,7 +1789,7 @@ export default mixins(
|
||||
|
||||
this.__addConnection(connectionData, true);
|
||||
},
|
||||
async addNode(nodeTypeName: string, options: AddNodeOptions = {}, showDetail = true) {
|
||||
async addNode(nodeTypeName: string, options: AddNodeOptions = {}, showDetail = true, trackHistory = false) {
|
||||
if (!this.editAllowedCheck()) {
|
||||
return;
|
||||
}
|
||||
@@ -1780,7 +1798,9 @@ export default mixins(
|
||||
const lastSelectedNode = this.lastSelectedNode;
|
||||
const lastSelectedNodeOutputIndex = this.uiStore.lastSelectedNodeOutputIndex;
|
||||
|
||||
const newNodeData = await this.injectNode(nodeTypeName, options, showDetail);
|
||||
this.historyStore.startRecordingUndo();
|
||||
|
||||
const newNodeData = await this.injectNode(nodeTypeName, options, showDetail, trackHistory);
|
||||
if (!newNodeData) {
|
||||
return;
|
||||
}
|
||||
@@ -1792,16 +1812,17 @@ export default mixins(
|
||||
await Vue.nextTick();
|
||||
|
||||
if (lastSelectedConnection && lastSelectedConnection.__meta) {
|
||||
this.__deleteJSPlumbConnection(lastSelectedConnection);
|
||||
this.__deleteJSPlumbConnection(lastSelectedConnection, trackHistory);
|
||||
|
||||
const targetNodeName = lastSelectedConnection.__meta.targetNodeName;
|
||||
const targetOutputIndex = lastSelectedConnection.__meta.targetOutputIndex;
|
||||
this.connectTwoNodes(newNodeData.name, 0, targetNodeName, targetOutputIndex);
|
||||
this.connectTwoNodes(newNodeData.name, 0, targetNodeName, targetOutputIndex, trackHistory);
|
||||
}
|
||||
|
||||
// Connect active node to the newly created one
|
||||
this.connectTwoNodes(lastSelectedNode.name, outputIndex, newNodeData.name, 0);
|
||||
this.connectTwoNodes(lastSelectedNode.name, outputIndex, newNodeData.name, 0, trackHistory);
|
||||
}
|
||||
this.historyStore.stopRecordingUndo();
|
||||
},
|
||||
initNodeView() {
|
||||
this.instance.importDefaults({
|
||||
@@ -1847,7 +1868,7 @@ export default mixins(
|
||||
const sourceNodeName = sourceNode.name;
|
||||
const outputIndex = connection.getParameters().index;
|
||||
|
||||
this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0);
|
||||
this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0, true);
|
||||
this.pullConnActiveNodeName = null;
|
||||
}
|
||||
return;
|
||||
@@ -1988,21 +2009,26 @@ export default mixins(
|
||||
|
||||
NodeViewUtils.moveBackInputLabelPosition(info.targetEndpoint);
|
||||
|
||||
const connectionData: [IConnection, IConnection] = [
|
||||
{
|
||||
node: sourceNodeName,
|
||||
type: sourceInfo.type,
|
||||
index: sourceInfo.index,
|
||||
},
|
||||
{
|
||||
node: targetNodeName,
|
||||
type: targetInfo.type,
|
||||
index: targetInfo.index,
|
||||
},
|
||||
];
|
||||
|
||||
this.workflowsStore.addConnection({
|
||||
connection: [
|
||||
{
|
||||
node: sourceNodeName,
|
||||
type: sourceInfo.type,
|
||||
index: sourceInfo.index,
|
||||
},
|
||||
{
|
||||
node: targetNodeName,
|
||||
type: targetInfo.type,
|
||||
index: targetInfo.index,
|
||||
},
|
||||
],
|
||||
connection: connectionData,
|
||||
setStateDirty: true,
|
||||
});
|
||||
if (!this.suspendRecordingDetachedConnections) {
|
||||
this.historyStore.pushCommandToUndo(new AddConnectionCommand(connectionData, this));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e); // eslint-disable-line no-console
|
||||
}
|
||||
@@ -2040,19 +2066,31 @@ export default mixins(
|
||||
}
|
||||
});
|
||||
|
||||
this.instance.bind('connectionDetached', (info) => {
|
||||
this.instance.bind('connectionDetached', async (info) => {
|
||||
try {
|
||||
const connectionInfo: [IConnection, IConnection] | null = getConnectionInfo(info);
|
||||
NodeViewUtils.resetInputLabelPosition(info.targetEndpoint);
|
||||
info.connection.removeOverlays();
|
||||
this.__removeConnectionByConnectionInfo(info, false);
|
||||
this.__removeConnectionByConnectionInfo(info, false, false);
|
||||
|
||||
if (this.pullConnActiveNodeName) { // establish new connection when dragging connection from one node to another
|
||||
this.historyStore.startRecordingUndo();
|
||||
const sourceNode = this.workflowsStore.getNodeById(info.connection.sourceId);
|
||||
const sourceNodeName = sourceNode.name;
|
||||
const outputIndex = info.connection.getParameters().index;
|
||||
|
||||
this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0);
|
||||
if (connectionInfo) {
|
||||
this.historyStore.pushCommandToUndo(new RemoveConnectionCommand(connectionInfo, this));
|
||||
}
|
||||
this.connectTwoNodes(sourceNodeName, outputIndex, this.pullConnActiveNodeName, 0, true);
|
||||
this.pullConnActiveNodeName = null;
|
||||
await this.$nextTick();
|
||||
this.historyStore.stopRecordingUndo();
|
||||
} else if (!this.historyStore.bulkInProgress && !this.suspendRecordingDetachedConnections && connectionInfo) {
|
||||
// Ff connection being detached by user, save this in history
|
||||
// but skip if it's detached as a side effect of bulk undo/redo or node rename process
|
||||
const removeCommand = new RemoveConnectionCommand(connectionInfo, this);
|
||||
this.historyStore.pushCommandToUndo(removeCommand);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e); // eslint-disable-line no-console
|
||||
@@ -2180,6 +2218,7 @@ export default mixins(
|
||||
}
|
||||
}
|
||||
this.uiStore.nodeViewInitialized = true;
|
||||
this.historyStore.reset();
|
||||
this.workflowsStore.activeWorkflowExecution = null;
|
||||
this.stopLoading();
|
||||
}),
|
||||
@@ -2246,6 +2285,7 @@ export default mixins(
|
||||
await this.newWorkflow();
|
||||
}
|
||||
}
|
||||
this.historyStore.reset();
|
||||
this.uiStore.nodeViewInitialized = true;
|
||||
document.addEventListener('keydown', this.keyDown);
|
||||
document.addEventListener('keyup', this.keyUp);
|
||||
@@ -2313,23 +2353,38 @@ export default mixins(
|
||||
},
|
||||
__removeConnection(connection: [IConnection, IConnection], removeVisualConnection = false) {
|
||||
if (removeVisualConnection) {
|
||||
const sourceId = this.workflowsStore.getNodeByName(connection[0].node);
|
||||
const targetId = this.workflowsStore.getNodeByName(connection[1].node);
|
||||
const sourceNode = this.workflowsStore.getNodeByName(connection[0].node);
|
||||
const targetNode = this.workflowsStore.getNodeByName(connection[1].node);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const connections = this.instance.getConnections({
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
source: sourceNode.id,
|
||||
target: targetNode.id,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
connections.forEach((connectionInstance) => {
|
||||
this.__deleteJSPlumbConnection(connectionInstance);
|
||||
if (connectionInstance.__meta) {
|
||||
// Only delete connections from specific indexes (if it can be determined by meta)
|
||||
if (
|
||||
connectionInstance.__meta.sourceOutputIndex === connection[0].index &&
|
||||
connectionInstance.__meta.targetOutputIndex === connection[1].index
|
||||
) {
|
||||
this.__deleteJSPlumbConnection(connectionInstance);
|
||||
}
|
||||
} else {
|
||||
this.__deleteJSPlumbConnection(connectionInstance);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.workflowsStore.removeConnection({ connection });
|
||||
},
|
||||
__deleteJSPlumbConnection(connection: Connection) {
|
||||
__deleteJSPlumbConnection(connection: Connection, trackHistory = false) {
|
||||
// Make sure to remove the overlay else after the second move
|
||||
// it visibly stays behind free floating without a connection.
|
||||
connection.removeOverlays();
|
||||
@@ -2341,31 +2396,24 @@ export default mixins(
|
||||
const endpoints = this.instance.getEndpoints(sourceEndpoint.elementId);
|
||||
endpoints.forEach((endpoint: Endpoint) => endpoint.repaint()); // repaint both circle and plus endpoint
|
||||
}
|
||||
if (trackHistory && connection.__meta) {
|
||||
const connectionData: [IConnection, IConnection] = [
|
||||
{ index: connection.__meta?.sourceOutputIndex, node: connection.__meta.sourceNodeName, type: 'main' },
|
||||
{ index: connection.__meta?.targetOutputIndex, node: connection.__meta.targetNodeName, type: 'main' },
|
||||
];
|
||||
const removeCommand = new RemoveConnectionCommand(connectionData, this);
|
||||
this.historyStore.pushCommandToUndo(removeCommand);
|
||||
}
|
||||
},
|
||||
__removeConnectionByConnectionInfo(info: OnConnectionBindInfo, removeVisualConnection = false) {
|
||||
const sourceInfo = info.sourceEndpoint.getParameters();
|
||||
const sourceNode = this.workflowsStore.getNodeById(sourceInfo.nodeId);
|
||||
const targetInfo = info.targetEndpoint.getParameters();
|
||||
const targetNode = this.workflowsStore.getNodeById(targetInfo.nodeId);
|
||||
|
||||
if (sourceNode && targetNode) {
|
||||
const connectionInfo = [
|
||||
{
|
||||
node: sourceNode.name,
|
||||
type: sourceInfo.type,
|
||||
index: sourceInfo.index,
|
||||
},
|
||||
{
|
||||
node: targetNode.name,
|
||||
type: targetInfo.type,
|
||||
index: targetInfo.index,
|
||||
},
|
||||
] as [IConnection, IConnection];
|
||||
__removeConnectionByConnectionInfo(info: OnConnectionBindInfo, removeVisualConnection = false, trackHistory = false) {
|
||||
const connectionInfo: [IConnection, IConnection] | null = getConnectionInfo(info);
|
||||
|
||||
if (connectionInfo) {
|
||||
if (removeVisualConnection) {
|
||||
this.__deleteJSPlumbConnection(info.connection);
|
||||
this.__deleteJSPlumbConnection(info.connection, trackHistory);
|
||||
} else if (trackHistory) {
|
||||
this.historyStore.pushCommandToUndo(new RemoveConnectionCommand(connectionInfo, this));
|
||||
}
|
||||
|
||||
this.workflowsStore.removeConnection({ connection: connectionInfo });
|
||||
}
|
||||
},
|
||||
@@ -2414,7 +2462,7 @@ export default mixins(
|
||||
);
|
||||
}
|
||||
|
||||
await this.addNodes([newNodeData]);
|
||||
await this.addNodes([newNodeData], [], true);
|
||||
|
||||
const pinData = this.workflowsStore.pinDataByNodeName(nodeName);
|
||||
if (pinData) {
|
||||
@@ -2562,7 +2610,7 @@ export default mixins(
|
||||
});
|
||||
});
|
||||
},
|
||||
removeNode(nodeName: string) {
|
||||
removeNode(nodeName: string, trackHistory = false, trackBulk = true) {
|
||||
if (!this.editAllowedCheck()) {
|
||||
return;
|
||||
}
|
||||
@@ -2572,6 +2620,10 @@ export default mixins(
|
||||
return;
|
||||
}
|
||||
|
||||
if (trackHistory && trackBulk) {
|
||||
this.historyStore.startRecordingUndo();
|
||||
}
|
||||
|
||||
// "requiredNodeTypes" are also defined in cli/commands/run.ts
|
||||
const requiredNodeTypes: string[] = [];
|
||||
|
||||
@@ -2625,7 +2677,7 @@ export default mixins(
|
||||
const targetNodeOuputIndex = conn2.__meta.targetOutputIndex;
|
||||
|
||||
setTimeout(() => {
|
||||
this.connectTwoNodes(sourceNodeName, sourceNodeOutputIndex, targetNodeName, targetNodeOuputIndex);
|
||||
this.connectTwoNodes(sourceNodeName, sourceNodeOutputIndex, targetNodeName, targetNodeOuputIndex, trackHistory);
|
||||
|
||||
if (waitForNewConnection) {
|
||||
this.instance.setSuspendDrawing(false, true);
|
||||
@@ -2659,7 +2711,16 @@ export default mixins(
|
||||
|
||||
// Remove node from selected index if found in it
|
||||
this.uiStore.removeNodeFromSelection(node);
|
||||
if (trackHistory) {
|
||||
this.historyStore.pushCommandToUndo(new RemoveNodeCommand(node, this));
|
||||
}
|
||||
}, 0); // allow other events to finish like drag stop
|
||||
if (trackHistory && trackBulk) {
|
||||
const recordingTimeout = waitForNewConnection ? 100 : 0;
|
||||
setTimeout(() => {
|
||||
this.historyStore.stopRecordingUndo();
|
||||
}, recordingTimeout);
|
||||
}
|
||||
},
|
||||
valueChanged(parameterData: IUpdateInformation) {
|
||||
if (parameterData.name === 'name' && parameterData.oldValue) {
|
||||
@@ -2694,14 +2755,19 @@ export default mixins(
|
||||
|
||||
const promptResponse = await promptResponsePromise as MessageBoxInputData;
|
||||
|
||||
this.renameNode(currentName, promptResponse.value);
|
||||
this.renameNode(currentName, promptResponse.value, true);
|
||||
} catch (e) { }
|
||||
},
|
||||
async renameNode(currentName: string, newName: string) {
|
||||
async renameNode(currentName: string, newName: string, trackHistory=false) {
|
||||
if (currentName === newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.suspendRecordingDetachedConnections = true;
|
||||
if (trackHistory) {
|
||||
this.historyStore.startRecordingUndo();
|
||||
}
|
||||
|
||||
const activeNodeName = this.activeNode && this.activeNode.name;
|
||||
const isActive = activeNodeName === currentName;
|
||||
if (isActive) {
|
||||
@@ -2717,6 +2783,10 @@ export default mixins(
|
||||
const workflow = this.getCurrentWorkflow(true);
|
||||
workflow.renameNode(currentName, newName);
|
||||
|
||||
if (trackHistory) {
|
||||
this.historyStore.pushCommandToUndo(new RenameNodeCommand(currentName, newName, this));
|
||||
}
|
||||
|
||||
// Update also last selected node and execution data
|
||||
this.workflowsStore.renameNodeSelectedAndExecution({ old: currentName, new: newName });
|
||||
|
||||
@@ -2730,7 +2800,7 @@ export default mixins(
|
||||
await Vue.nextTick();
|
||||
|
||||
// Add the new updated nodes
|
||||
await this.addNodes(Object.values(workflow.nodes), workflow.connectionsBySourceNode);
|
||||
await this.addNodes(Object.values(workflow.nodes), workflow.connectionsBySourceNode, false);
|
||||
|
||||
// Make sure that the node is selected again
|
||||
this.deselectAllNodes();
|
||||
@@ -2740,6 +2810,11 @@ export default mixins(
|
||||
this.ndvStore.activeNodeName = newName;
|
||||
this.renamingActive = false;
|
||||
}
|
||||
|
||||
if (trackHistory) {
|
||||
this.historyStore.stopRecordingUndo();
|
||||
}
|
||||
this.suspendRecordingDetachedConnections = false;
|
||||
},
|
||||
deleteEveryEndpoint() {
|
||||
// Check as it does not exist on first load
|
||||
@@ -2802,7 +2877,7 @@ export default mixins(
|
||||
}
|
||||
});
|
||||
},
|
||||
async addNodes(nodes: INodeUi[], connections?: IConnections) {
|
||||
async addNodes(nodes: INodeUi[], connections?: IConnections, trackHistory = false) {
|
||||
if (!nodes || !nodes.length) {
|
||||
return;
|
||||
}
|
||||
@@ -2859,6 +2934,9 @@ export default mixins(
|
||||
}
|
||||
|
||||
this.workflowsStore.addNode(node);
|
||||
if (trackHistory) {
|
||||
this.historyStore.pushCommandToUndo(new AddNodeCommand(node, this));
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the node to be rendered
|
||||
@@ -3012,7 +3090,9 @@ export default mixins(
|
||||
}
|
||||
|
||||
// Add the nodes with the changed node names, expressions and connections
|
||||
await this.addNodes(Object.values(tempWorkflow.nodes), tempWorkflow.connectionsBySourceNode);
|
||||
this.historyStore.startRecordingUndo();
|
||||
await this.addNodes(Object.values(tempWorkflow.nodes), tempWorkflow.connectionsBySourceNode, true);
|
||||
this.historyStore.stopRecordingUndo();
|
||||
|
||||
this.uiStore.stateIsDirty = true;
|
||||
|
||||
@@ -3259,7 +3339,7 @@ export default mixins(
|
||||
},
|
||||
onAddNode(nodeTypes: Array<{ nodeTypeName: string; position: XYPosition }>, dragAndDrop: boolean) {
|
||||
nodeTypes.forEach(({ nodeTypeName, position }, index) => {
|
||||
this.addNode(nodeTypeName, { position, dragAndDrop }, nodeTypes.length === 1 || index > 0);
|
||||
this.addNode(nodeTypeName, { position, dragAndDrop }, nodeTypes.length === 1 || index > 0, true);
|
||||
if(index === 0) return;
|
||||
// If there's more than one node, we want to connect them
|
||||
// this has to be done in mutation subscriber to make sure both nodes already
|
||||
@@ -3287,6 +3367,51 @@ export default mixins(
|
||||
await this.saveCurrentWorkflow();
|
||||
callback?.();
|
||||
},
|
||||
setSuspendRecordingDetachedConnections(suspend: boolean) {
|
||||
this.suspendRecordingDetachedConnections = suspend;
|
||||
},
|
||||
onMoveNode({nodeName, position}: { nodeName: string, position: XYPosition }): void {
|
||||
this.workflowsStore.updateNodeProperties({ name: nodeName, properties: { position }});
|
||||
setTimeout(() => {
|
||||
const node = this.workflowsStore.getNodeByName(nodeName);
|
||||
if (node) {
|
||||
this.instance.repaintEverything();
|
||||
this.onNodeMoved(node);
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
onRevertAddNode({node}: {node: INodeUi}): void {
|
||||
this.removeNode(node.name, false);
|
||||
},
|
||||
async onRevertRemoveNode({node}: {node: INodeUi}): Promise<void> {
|
||||
const prevNode = this.workflowsStore.workflow.nodes.find(n => n.id === node.id);
|
||||
if (prevNode) {
|
||||
return;
|
||||
}
|
||||
// For some reason, returning node to canvas with old id
|
||||
// makes it's endpoint to render at wrong position
|
||||
node.id = uuid();
|
||||
await this.addNodes([node]);
|
||||
},
|
||||
onRevertAddConnection({ connection }: { connection: [IConnection, IConnection]}) {
|
||||
this.suspendRecordingDetachedConnections = true;
|
||||
this.__removeConnection(connection, true);
|
||||
this.suspendRecordingDetachedConnections = false;
|
||||
},
|
||||
async onRevertRemoveConnection({ connection }: { connection: [IConnection, IConnection]}) {
|
||||
this.suspendRecordingDetachedConnections = true;
|
||||
this.__addConnection(connection, true);
|
||||
this.suspendRecordingDetachedConnections = false;
|
||||
},
|
||||
async onRevertNameChange({ currentName, newName }: { currentName: string, newName: string }) {
|
||||
await this.renameNode(newName, currentName);
|
||||
},
|
||||
onRevertEnableToggle({ nodeName, isDisabled }: { nodeName: string, isDisabled: boolean }) {
|
||||
const node = this.workflowsStore.getNodeByName(nodeName);
|
||||
if (node) {
|
||||
this.disableNodes([node]);
|
||||
}
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.$titleReset();
|
||||
@@ -3389,6 +3514,13 @@ export default mixins(
|
||||
this.$root.$on('newWorkflow', this.newWorkflow);
|
||||
this.$root.$on('importWorkflowData', this.onImportWorkflowDataEvent);
|
||||
this.$root.$on('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
||||
this.$root.$on('nodeMove', this.onMoveNode);
|
||||
this.$root.$on('revertAddNode', this.onRevertAddNode);
|
||||
this.$root.$on('revertRemoveNode', this.onRevertRemoveNode);
|
||||
this.$root.$on('revertAddConnection', this.onRevertAddConnection);
|
||||
this.$root.$on('revertRemoveConnection', this.onRevertRemoveConnection);
|
||||
this.$root.$on('revertRenameNode', this.onRevertNameChange);
|
||||
this.$root.$on('enableNodeToggle', this.onRevertEnableToggle);
|
||||
|
||||
dataPinningEventBus.$on('pin-data', this.addPinDataConnections);
|
||||
dataPinningEventBus.$on('unpin-data', this.removePinDataConnections);
|
||||
@@ -3404,6 +3536,13 @@ export default mixins(
|
||||
this.$root.$off('newWorkflow', this.newWorkflow);
|
||||
this.$root.$off('importWorkflowData', this.onImportWorkflowDataEvent);
|
||||
this.$root.$off('importWorkflowUrl', this.onImportWorkflowUrlEvent);
|
||||
this.$root.$off('nodeMove', this.onMoveNode);
|
||||
this.$root.$off('revertAddNode', this.onRevertAddNode);
|
||||
this.$root.$off('revertRemoveNode', this.onRevertRemoveNode);
|
||||
this.$root.$off('revertAddConnection', this.onRevertAddConnection);
|
||||
this.$root.$off('revertRemoveConnection', this.onRevertRemoveConnection);
|
||||
this.$root.$off('revertRenameNode', this.onRevertNameChange);
|
||||
this.$root.$off('enableNodeToggle', this.onRevertEnableToggle);
|
||||
|
||||
dataPinningEventBus.$off('pin-data', this.addPinDataConnections);
|
||||
dataPinningEventBus.$off('unpin-data', this.removePinDataConnections);
|
||||
|
||||
Reference in New Issue
Block a user