feat(editor): Add remove node and connections functionality to canvas v2 (#9602)

This commit is contained in:
Alex Grozav
2024-06-04 15:36:27 +03:00
committed by GitHub
parent 202c91e7ed
commit f6a466cd87
13 changed files with 876 additions and 125 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, defineAsyncComponent, onMounted, ref, useCssModule } from 'vue';
import { computed, defineAsyncComponent, onBeforeUnmount, onMounted, ref, useCssModule } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import WorkflowCanvas from '@/components/canvas/WorkflowCanvas.vue';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
@@ -31,7 +31,7 @@ import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
import { useTelemetry } from '@/composables/useTelemetry';
import { useExternalHooks } from '@/composables/useExternalHooks';
import * as NodeViewUtils from '@/utils/nodeViewUtils';
import type { INodeTypeDescription } from 'n8n-workflow';
import type { IConnection, INodeTypeDescription } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
import { useToast } from '@/composables/useToast';
import { v4 as uuid } from 'uuid';
@@ -42,7 +42,8 @@ import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
import { useRootStore } from '@/stores/n8nRoot.store';
import { useCollaborationStore } from '@/stores/collaboration.store';
import { getUniqueNodeName } from '@/utils/canvasUtilsV2';
import { isValidNodeConnectionType } from '@/utils/typeGuards';
import { historyBus } from '@/models/history';
import { useCanvasOperations } from '@/composables/useCanvasOperations';
const NodeCreation = defineAsyncComponent(
async () => await import('@/components/Node/NodeCreation.vue'),
@@ -72,6 +73,14 @@ const rootStore = useRootStore();
const collaborationStore = useCollaborationStore();
const { runWorkflow } = useRunWorkflow({ router });
const {
updateNodePosition,
deleteNode,
revertDeleteNode,
createConnection,
deleteConnection,
revertDeleteConnection,
} = useCanvasOperations();
const isLoading = ref(true);
const readOnlyNotification = ref<null | { visible: boolean }>(null);
@@ -141,6 +150,8 @@ async function initialize() {
initializeEditableWorkflow(workflowId.value);
addUndoRedoEventBindings();
if (window.parent) {
window.parent.postMessage(
JSON.stringify({ command: 'n8nReady', version: rootStore.versionCli }),
@@ -151,6 +162,30 @@ async function initialize() {
isLoading.value = false;
}
onBeforeUnmount(() => {
removeUndoRedoEventBindings();
});
function addUndoRedoEventBindings() {
// historyBus.on('nodeMove', onMoveNode);
// historyBus.on('revertAddNode', onRevertAddNode);
historyBus.on('revertRemoveNode', onRevertDeleteNode);
// historyBus.on('revertAddConnection', onRevertAddConnection);
historyBus.on('revertRemoveConnection', onRevertDeleteConnection);
// historyBus.on('revertRenameNode', onRevertNameChange);
// historyBus.on('enableNodeToggle', onRevertEnableToggle);
}
function removeUndoRedoEventBindings() {
// historyBus.off('nodeMove', onMoveNode);
// historyBus.off('revertAddNode', onRevertAddNode);
historyBus.off('revertRemoveNode', onRevertDeleteNode);
// historyBus.off('revertAddConnection', onRevertAddConnection);
historyBus.off('revertRemoveConnection', onRevertDeleteConnection);
// historyBus.off('revertRenameNode', onRevertNameChange);
// historyBus.off('enableNodeToggle', onRevertEnableToggle);
}
// @TODO Maybe move this to the store
function initializeEditableWorkflow(id: string) {
const targetWorkflow = workflowsStore.workflowsById[id];
@@ -198,14 +233,16 @@ async function onRunWorkflow() {
await runWorkflow({});
}
/**
* Map new node position format to the old one and update the store
*
* @param id
* @param position
*/
function onNodePositionUpdate(id: string, position: CanvasElement['position']) {
workflowsStore.setNodePosition(id, [position.x, position.y]);
function onUpdateNodePosition(id: string, position: CanvasElement['position']) {
updateNodePosition(id, position, { trackHistory: true });
}
function onDeleteNode(id: string) {
deleteNode(id, { trackHistory: true });
}
function onRevertDeleteNode({ node }: { node: INodeUi }) {
revertDeleteNode(node);
}
/**
@@ -213,83 +250,16 @@ function onNodePositionUpdate(id: string, position: CanvasElement['position']) {
*
* @param connection
*/
function onCreateNodeConnection(connection: Connection) {
// Output
const sourceNodeId = connection.source;
const sourceNode = workflowsStore.getNodeById(sourceNodeId);
const sourceNodeName = sourceNode?.name ?? '';
const [, sourceType, sourceIndex] = (connection.sourceHandle ?? '')
.split('/')
.filter(isValidNodeConnectionType);
// Input
const targetNodeId = connection.target;
const targetNode = workflowsStore.getNodeById(targetNodeId);
const targetNodeName = targetNode?.name ?? '';
const [, targetType, targetIndex] = (connection.targetHandle ?? '')
.split('/')
.filter(isValidNodeConnectionType);
if (sourceNode && targetNode && !checkIfNodeConnectionIsAllowed(sourceNode, targetNode)) {
return;
}
workflowsStore.addConnection({
connection: [
{
node: sourceNodeName,
type: sourceType,
index: parseInt(sourceIndex, 10),
},
{
node: targetNodeName,
type: targetType,
index: parseInt(targetIndex, 10),
},
],
});
uiStore.stateIsDirty = true;
function onCreateConnection(connection: Connection) {
createConnection(connection);
}
// @TODO Figure out a way to improve this
function checkIfNodeConnectionIsAllowed(_sourceNode: INodeUi, _targetNode: INodeUi): boolean {
// const targetNodeType = nodeTypesStore.getNodeType(
// targetNode.type,
// targetNode.typeVersion,
// );
//
// if (targetNodeType?.inputs?.length) {
// const workflow = this.workflowHelpers.getCurrentWorkflow();
// const workflowNode = workflow.getNode(targetNode.name);
// let inputs: Array<ConnectionTypes | INodeInputConfiguration> = [];
// if (targetNodeType) {
// inputs = NodeHelpers.getNodeInputs(workflow, workflowNode, targetNodeType);
// }
//
// for (const input of inputs || []) {
// if (typeof input === 'string' || input.type !== targetInfoType || !input.filter) {
// // No filters defined or wrong connection type
// continue;
// }
//
// if (input.filter.nodes.length) {
// if (!input.filter.nodes.includes(sourceNode.type)) {
// this.dropPrevented = true;
// this.showToast({
// title: this.$locale.baseText('nodeView.showError.nodeNodeCompatible.title'),
// message: this.$locale.baseText('nodeView.showError.nodeNodeCompatible.message', {
// interpolate: { sourceNodeName: sourceNode.name, targetNodeName: targetNode.name },
// }),
// type: 'error',
// duration: 5000,
// });
// return false;
// }
// }
// }
// }
return true;
function onDeleteConnection(connection: Connection) {
deleteConnection(connection, { trackHistory: true });
}
function onRevertDeleteConnection({ connection }: { connection: [IConnection, IConnection] }) {
revertDeleteConnection(connection);
}
function onToggleNodeCreator({
@@ -346,19 +316,24 @@ async function onAddNodes(
) {
let currentPosition = position;
for (const { type, name, position: nodePosition, isAutoAdd, openDetail } of nodes) {
const _node = await addNode(
{
name,
type,
position: nodePosition ?? currentPosition,
},
{
dragAndDrop,
openNDV: openDetail ?? false,
trackHistory: true,
isAutoAdd,
},
);
try {
await onNodeCreate(
{
name,
type,
position: nodePosition ?? currentPosition,
},
{
dragAndDrop,
openNDV: openDetail ?? false,
trackHistory: true,
isAutoAdd,
},
);
} catch (error) {
toast.showError(error, i18n.baseText('error'));
continue;
}
const lastAddedNode = editableWorkflow.value.nodes[editableWorkflow.value.nodes.length - 1];
currentPosition = [
@@ -372,7 +347,7 @@ async function onAddNodes(
const fromNode = editableWorkflow.value.nodes[newNodesOffset + from.nodeIndex];
const toNode = editableWorkflow.value.nodes[newNodesOffset + to.nodeIndex];
onCreateNodeConnection({
onCreateConnection({
source: fromNode.id,
sourceHandle: `outputs/${NodeConnectionType.Main}/${from.outputIndex ?? 0}`,
target: toNode.id,
@@ -412,14 +387,14 @@ type AddNodeOptions = {
isAutoAdd?: boolean;
};
async function addNode(node: AddNodeData, _options: AddNodeOptions): Promise<INodeUi | undefined> {
async function onNodeCreate(node: AddNodeData, _options: AddNodeOptions = {}): Promise<INodeUi> {
if (!checkIfEditingIsAllowed()) {
return;
throw new Error(i18n.baseText('nodeViewV2.showError.editingNotAllowed'));
}
const newNodeData = await createNodeWithDefaultCredentials(node);
if (!newNodeData) {
return;
throw new Error(i18n.baseText('nodeViewV2.showError.failedToCreateNode'));
}
/**
@@ -929,8 +904,10 @@ function checkIfEditingIsAllowed(): boolean {
v-if="editableWorkflow && editableWorkflowObject"
:workflow="editableWorkflow"
:workflow-object="editableWorkflowObject"
@update:node:position="onNodePositionUpdate"
@create:connection="onCreateNodeConnection"
@update:node:position="onUpdateNodePosition"
@delete:node="onDeleteNode"
@create:connection="onCreateConnection"
@delete:connection="onDeleteConnection"
>
<div :class="$style.executionButtons">
<CanvasExecuteWorkflowButton @click="onRunWorkflow" />