feat(editor): Add remove node and connections functionality to canvas v2 (#9602)
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user