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,23 +1,25 @@
<script lang="ts" setup>
import type { CanvasConnection, CanvasElement } from '@/types';
import type { NodeDragEvent, Connection } from '@vue-flow/core';
import { VueFlow, PanelPosition } from '@vue-flow/core';
import { useVueFlow, VueFlow, PanelPosition } from '@vue-flow/core';
import { Background } from '@vue-flow/background';
import { Controls } from '@vue-flow/controls';
import { MiniMap } from '@vue-flow/minimap';
import CanvasNode from './elements/nodes/CanvasNode.vue';
import CanvasEdge from './elements/edges/CanvasEdge.vue';
import { useCssModule } from 'vue';
import { onMounted, onUnmounted, useCssModule } from 'vue';
const $style = useCssModule();
const emit = defineEmits<{
'update:modelValue': [elements: CanvasElement[]];
'update:node:position': [id: string, position: { x: number; y: number }];
'delete:node': [id: string];
'delete:connection': [connection: Connection];
'create:connection': [connection: Connection];
}>();
withDefaults(
const props = withDefaults(
defineProps<{
id?: string;
elements: CanvasElement[];
@@ -32,15 +34,40 @@ withDefaults(
},
);
const { getSelectedEdges, getSelectedNodes } = useVueFlow({ id: props.id });
onMounted(() => {
document.addEventListener('keydown', onKeyDown);
});
onUnmounted(() => {
document.removeEventListener('keydown', onKeyDown);
});
function onNodeDragStop(e: NodeDragEvent) {
e.nodes.forEach((node) => {
emit('update:node:position', node.id, node.position);
});
}
function onDeleteNode(id: string) {
emit('delete:node', id);
}
function onDeleteConnection(connection: Connection) {
emit('delete:connection', connection);
}
function onConnect(...args: unknown[]) {
emit('create:connection', args[0] as Connection);
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Delete') {
getSelectedEdges.value.forEach(onDeleteConnection);
getSelectedNodes.value.forEach(({ id }) => onDeleteNode(id));
}
}
</script>
<template>
@@ -58,11 +85,11 @@ function onConnect(...args: unknown[]) {
@connect="onConnect"
>
<template #node-canvas-node="canvasNodeProps">
<CanvasNode v-bind="canvasNodeProps" />
<CanvasNode v-bind="canvasNodeProps" @delete="onDeleteNode" />
</template>
<template #edge-canvas-edge="canvasEdgeProps">
<CanvasEdge v-bind="canvasEdgeProps" />
<CanvasEdge v-bind="canvasEdgeProps" @delete="onDeleteConnection" />
</template>
<Background data-test-id="canvas-background" pattern-color="#aaa" :gap="16" />

View File

@@ -1,15 +1,28 @@
<script lang="ts" setup>
import type { EdgeProps } from '@vue-flow/core';
import { BaseEdge, getBezierPath } from '@vue-flow/core';
import { computed } from 'vue';
/* eslint-disable vue/no-multiple-template-root */
import type { Connection, EdgeProps } from '@vue-flow/core';
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from '@vue-flow/core';
import { computed, useCssModule } from 'vue';
import { useI18n } from '@/composables/useI18n';
const emit = defineEmits<{
delete: [connection: Connection];
}>();
const props = defineProps<EdgeProps>();
const i18n = useI18n();
const $style = useCssModule();
const edgeStyle = computed(() => ({
strokeWidth: 2,
...props.style,
}));
const edgeLabelStyle = computed(() => ({
transform: `translate(-50%, -50%) translate(${path.value[1]}px,${path.value[2]}px)`,
}));
const path = computed(() =>
getBezierPath({
sourceX: props.sourceX,
@@ -20,6 +33,17 @@ const path = computed(() =>
targetPosition: props.targetPosition,
}),
);
const connection = computed<Connection>(() => ({
source: props.source,
target: props.target,
sourceHandle: props.sourceHandleId,
targetHandle: props.targetHandleId,
}));
function onDelete() {
emit('delete', connection.value);
}
</script>
<template>
@@ -37,4 +61,23 @@ const path = computed(() =>
:label-bg-padding="[2, 4]"
:label-bg-border-radius="2"
/>
<EdgeLabelRenderer>
<div :class="[$style.edgeToolbar, 'nodrag', 'nopan']" :style="edgeLabelStyle">
<N8nIconButton
data-test-id="delete-connection-button"
type="tertiary"
size="small"
icon="trash"
:title="i18n.baseText('node.delete')"
@click="onDelete"
/>
</div>
</EdgeLabelRenderer>
</template>
<style lang="scss" module>
.edgeToolbar {
pointer-events: all;
position: absolute;
}
</style>

View File

@@ -15,6 +15,10 @@ import { useNodeConnections } from '@/composables/useNodeConnections';
import { CanvasNodeKey } from '@/constants';
import type { NodeProps } from '@vue-flow/core';
const emit = defineEmits<{
delete: [id: string];
}>();
const props = defineProps<NodeProps<CanvasElementData>>();
const inputs = computed(() => props.data.inputs);
@@ -89,6 +93,10 @@ provide(CanvasNodeKey, {
selected,
nodeType,
});
function onDelete() {
emit('delete', props.id);
}
</script>
<template>
@@ -121,6 +129,7 @@ provide(CanvasNodeKey, {
v-if="nodeType"
data-test-id="canvas-node-toolbar"
:class="$style.canvasNodeToolbar"
@delete="onDelete"
/>
<CanvasNodeRenderer v-if="nodeType">

View File

@@ -1,12 +1,16 @@
<script setup lang="ts">
import { computed, inject, useCssModule } from 'vue';
import { CanvasNodeKey } from '@/constants';
import { useI18n } from '@/composables/useI18n';
const emit = defineEmits(['delete']);
const $style = useCssModule();
const node = inject(CanvasNodeKey);
const data = computed(() => node?.data.value);
const i18n = useI18n();
const $style = useCssModule();
const data = computed(() => node?.data.value);
// @TODO
const workflowRunning = false;
@@ -20,8 +24,9 @@ function executeNode() {}
// @TODO
function toggleDisableNode() {}
// @TODO
function deleteNode() {}
function deleteNode() {
emit('delete');
}
// @TODO
function openContextMenu(_e: MouseEvent, _type: string) {}
@@ -38,7 +43,7 @@ function openContextMenu(_e: MouseEvent, _type: string) {}
size="small"
icon="play"
:disabled="workflowRunning"
:title="$locale.baseText('node.testStep')"
:title="i18n.baseText('node.testStep')"
@click="executeNode"
/>
<N8nIconButton
@@ -56,7 +61,7 @@ function openContextMenu(_e: MouseEvent, _type: string) {}
size="small"
text
icon="trash"
:title="$locale.baseText('node.delete')"
:title="i18n.baseText('node.delete')"
@click="deleteNode"
/>
<N8nIconButton