mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Add canvas edge toolbar hover show/hide support (no-changelog) (#9699)
This commit is contained in:
parent
90f8b919fa
commit
daf85b4439
|
@ -1,13 +1,13 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { CanvasConnection, CanvasElement } from '@/types';
|
import type { CanvasConnection, CanvasElement } from '@/types';
|
||||||
import type { NodeDragEvent, Connection } from '@vue-flow/core';
|
import type { EdgeMouseEvent, NodeDragEvent, Connection } from '@vue-flow/core';
|
||||||
import { useVueFlow, VueFlow, PanelPosition } from '@vue-flow/core';
|
import { useVueFlow, VueFlow, PanelPosition } from '@vue-flow/core';
|
||||||
import { Background } from '@vue-flow/background';
|
import { Background } from '@vue-flow/background';
|
||||||
import { Controls } from '@vue-flow/controls';
|
import { Controls } from '@vue-flow/controls';
|
||||||
import { MiniMap } from '@vue-flow/minimap';
|
import { MiniMap } from '@vue-flow/minimap';
|
||||||
import CanvasNode from './elements/nodes/CanvasNode.vue';
|
import CanvasNode from './elements/nodes/CanvasNode.vue';
|
||||||
import CanvasEdge from './elements/edges/CanvasEdge.vue';
|
import CanvasEdge from './elements/edges/CanvasEdge.vue';
|
||||||
import { onMounted, onUnmounted, useCssModule } from 'vue';
|
import { onMounted, onUnmounted, ref, useCssModule } from 'vue';
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
|
@ -36,6 +36,8 @@ const props = withDefaults(
|
||||||
|
|
||||||
const { getSelectedEdges, getSelectedNodes } = useVueFlow({ id: props.id });
|
const { getSelectedEdges, getSelectedNodes } = useVueFlow({ id: props.id });
|
||||||
|
|
||||||
|
const hoveredEdges = ref<Record<string, boolean>>({});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('keydown', onKeyDown);
|
document.addEventListener('keydown', onKeyDown);
|
||||||
});
|
});
|
||||||
|
@ -68,6 +70,14 @@ function onKeyDown(e: KeyboardEvent) {
|
||||||
getSelectedNodes.value.forEach(({ id }) => onDeleteNode(id));
|
getSelectedNodes.value.forEach(({ id }) => onDeleteNode(id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onMouseEnterEdge(event: EdgeMouseEvent) {
|
||||||
|
hoveredEdges.value[event.edge.id] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseLeaveEdge(event: EdgeMouseEvent) {
|
||||||
|
hoveredEdges.value[event.edge.id] = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -82,6 +92,8 @@ function onKeyDown(e: KeyboardEvent) {
|
||||||
:max-zoom="2"
|
:max-zoom="2"
|
||||||
data-test-id="canvas"
|
data-test-id="canvas"
|
||||||
@node-drag-stop="onNodeDragStop"
|
@node-drag-stop="onNodeDragStop"
|
||||||
|
@edge-mouse-enter="onMouseEnterEdge"
|
||||||
|
@edge-mouse-leave="onMouseLeaveEdge"
|
||||||
@connect="onConnect"
|
@connect="onConnect"
|
||||||
>
|
>
|
||||||
<template #node-canvas-node="canvasNodeProps">
|
<template #node-canvas-node="canvasNodeProps">
|
||||||
|
@ -89,7 +101,11 @@ function onKeyDown(e: KeyboardEvent) {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #edge-canvas-edge="canvasEdgeProps">
|
<template #edge-canvas-edge="canvasEdgeProps">
|
||||||
<CanvasEdge v-bind="canvasEdgeProps" @delete="onDeleteConnection" />
|
<CanvasEdge
|
||||||
|
v-bind="canvasEdgeProps"
|
||||||
|
:hovered="hoveredEdges[canvasEdgeProps.id]"
|
||||||
|
@delete="onDeleteConnection"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Background data-test-id="canvas-background" pattern-color="#aaa" :gap="16" />
|
<Background data-test-id="canvas-background" pattern-color="#aaa" :gap="16" />
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { fireEvent } from '@testing-library/vue';
|
||||||
|
import CanvasEdge from './CanvasEdge.vue';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(CanvasEdge, {
|
||||||
|
props: {
|
||||||
|
sourceX: 0,
|
||||||
|
sourceY: 0,
|
||||||
|
sourcePosition: 'top',
|
||||||
|
targetX: 100,
|
||||||
|
targetY: 100,
|
||||||
|
targetPosition: 'bottom',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CanvasEdge', () => {
|
||||||
|
it('should emit delete event when toolbar delete is clicked', async () => {
|
||||||
|
const { emitted, getByTestId } = renderComponent();
|
||||||
|
const deleteButton = getByTestId('delete-connection-button');
|
||||||
|
|
||||||
|
await fireEvent.click(deleteButton);
|
||||||
|
|
||||||
|
expect(emitted()).toHaveProperty('delete');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compute edgeStyle correctly', () => {
|
||||||
|
const { container } = renderComponent({
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
stroke: 'red',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const edge = container.querySelector('.vue-flow__edge-path');
|
||||||
|
|
||||||
|
expect(edge).toHaveStyle({
|
||||||
|
stroke: 'red',
|
||||||
|
strokeWidth: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,16 +2,19 @@
|
||||||
/* eslint-disable vue/no-multiple-template-root */
|
/* eslint-disable vue/no-multiple-template-root */
|
||||||
import type { Connection, EdgeProps } from '@vue-flow/core';
|
import type { Connection, EdgeProps } from '@vue-flow/core';
|
||||||
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from '@vue-flow/core';
|
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from '@vue-flow/core';
|
||||||
|
import CanvasEdgeToolbar from './CanvasEdgeToolbar.vue';
|
||||||
import { computed, useCssModule } from 'vue';
|
import { computed, useCssModule } from 'vue';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
delete: [connection: Connection];
|
delete: [connection: Connection];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const props = defineProps<EdgeProps>();
|
const props = defineProps<
|
||||||
|
EdgeProps & {
|
||||||
|
hovered?: boolean;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
const i18n = useI18n();
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
const edgeStyle = computed(() => ({
|
const edgeStyle = computed(() => ({
|
||||||
|
@ -19,8 +22,19 @@ const edgeStyle = computed(() => ({
|
||||||
...props.style,
|
...props.style,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const edgeLabelStyle = computed(() => ({
|
const isEdgeToolbarVisible = computed(() => props.selected || props.hovered);
|
||||||
transform: `translate(-50%, -50%) translate(${path.value[1]}px,${path.value[2]}px)`,
|
|
||||||
|
const edgeToolbarStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
transform: `translate(-50%, -50%) translate(${path.value[1]}px,${path.value[2]}px)`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const edgeToolbarClasses = computed(() => ({
|
||||||
|
[$style.edgeToolbar]: true,
|
||||||
|
[$style.edgeToolbarVisible]: isEdgeToolbarVisible.value,
|
||||||
|
nodrag: true,
|
||||||
|
nopan: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const path = computed(() =>
|
const path = computed(() =>
|
||||||
|
@ -60,24 +74,24 @@ function onDelete() {
|
||||||
:label-bg-style="{ fill: 'red' }"
|
:label-bg-style="{ fill: 'red' }"
|
||||||
:label-bg-padding="[2, 4]"
|
:label-bg-padding="[2, 4]"
|
||||||
:label-bg-border-radius="2"
|
:label-bg-border-radius="2"
|
||||||
|
:class="$style.edge"
|
||||||
/>
|
/>
|
||||||
<EdgeLabelRenderer>
|
<EdgeLabelRenderer>
|
||||||
<div :class="[$style.edgeToolbar, 'nodrag', 'nopan']" :style="edgeLabelStyle">
|
<CanvasEdgeToolbar :class="edgeToolbarClasses" :style="edgeToolbarStyle" @delete="onDelete" />
|
||||||
<N8nIconButton
|
|
||||||
data-test-id="delete-connection-button"
|
|
||||||
type="tertiary"
|
|
||||||
size="small"
|
|
||||||
icon="trash"
|
|
||||||
:title="i18n.baseText('node.delete')"
|
|
||||||
@click="onDelete"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</EdgeLabelRenderer>
|
</EdgeLabelRenderer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.edgeToolbar {
|
.edgeToolbar {
|
||||||
pointer-events: all;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&.edgeToolbarVisible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { fireEvent } from '@testing-library/vue';
|
||||||
|
import CanvasEdgeToolbar from './CanvasEdgeToolbar.vue';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(CanvasEdgeToolbar);
|
||||||
|
|
||||||
|
describe('CanvasEdgeToolbar', () => {
|
||||||
|
it('should emit delete event when delete button is clicked', async () => {
|
||||||
|
const { getByTestId, emitted } = renderComponent();
|
||||||
|
const deleteButton = getByTestId('delete-connection-button');
|
||||||
|
|
||||||
|
await fireEvent.click(deleteButton);
|
||||||
|
|
||||||
|
expect(emitted()).toHaveProperty('delete');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,42 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { computed, useCssModule } from 'vue';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
delete: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const $style = useCssModule();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const classes = computed(() => ({
|
||||||
|
[$style.canvasEdgeToolbar]: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function onDelete() {
|
||||||
|
emit('delete');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="classes" data-test-id="canvas-edge-toolbar">
|
||||||
|
<N8nIconButton
|
||||||
|
data-test-id="delete-connection-button"
|
||||||
|
type="tertiary"
|
||||||
|
size="small"
|
||||||
|
icon="trash"
|
||||||
|
:title="i18n.baseText('node.delete')"
|
||||||
|
@click="onDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.canvasEdgeToolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in a new issue