feat(editor): Add canvas edge toolbar hover show/hide support (no-changelog) (#9699)

This commit is contained in:
Alex Grozav 2024-06-11 19:09:29 +03:00 committed by GitHub
parent 90f8b919fa
commit daf85b4439
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 149 additions and 19 deletions

View file

@ -1,13 +1,13 @@
<script lang="ts" setup>
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 { 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 { onMounted, onUnmounted, useCssModule } from 'vue';
import { onMounted, onUnmounted, ref, useCssModule } from 'vue';
const $style = useCssModule();
@ -36,6 +36,8 @@ const props = withDefaults(
const { getSelectedEdges, getSelectedNodes } = useVueFlow({ id: props.id });
const hoveredEdges = ref<Record<string, boolean>>({});
onMounted(() => {
document.addEventListener('keydown', onKeyDown);
});
@ -68,6 +70,14 @@ function onKeyDown(e: KeyboardEvent) {
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>
<template>
@ -82,6 +92,8 @@ function onKeyDown(e: KeyboardEvent) {
:max-zoom="2"
data-test-id="canvas"
@node-drag-stop="onNodeDragStop"
@edge-mouse-enter="onMouseEnterEdge"
@edge-mouse-leave="onMouseLeaveEdge"
@connect="onConnect"
>
<template #node-canvas-node="canvasNodeProps">
@ -89,7 +101,11 @@ function onKeyDown(e: KeyboardEvent) {
</template>
<template #edge-canvas-edge="canvasEdgeProps">
<CanvasEdge v-bind="canvasEdgeProps" @delete="onDeleteConnection" />
<CanvasEdge
v-bind="canvasEdgeProps"
:hovered="hoveredEdges[canvasEdgeProps.id]"
@delete="onDeleteConnection"
/>
</template>
<Background data-test-id="canvas-background" pattern-color="#aaa" :gap="16" />

View file

@ -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,
});
});
});

View file

@ -2,16 +2,19 @@
/* eslint-disable vue/no-multiple-template-root */
import type { Connection, EdgeProps } from '@vue-flow/core';
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from '@vue-flow/core';
import CanvasEdgeToolbar from './CanvasEdgeToolbar.vue';
import { computed, useCssModule } from 'vue';
import { useI18n } from '@/composables/useI18n';
const emit = defineEmits<{
delete: [connection: Connection];
}>();
const props = defineProps<EdgeProps>();
const props = defineProps<
EdgeProps & {
hovered?: boolean;
}
>();
const i18n = useI18n();
const $style = useCssModule();
const edgeStyle = computed(() => ({
@ -19,8 +22,19 @@ const edgeStyle = computed(() => ({
...props.style,
}));
const edgeLabelStyle = computed(() => ({
const isEdgeToolbarVisible = computed(() => props.selected || props.hovered);
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(() =>
@ -60,24 +74,24 @@ function onDelete() {
:label-bg-style="{ fill: 'red' }"
:label-bg-padding="[2, 4]"
:label-bg-border-radius="2"
:class="$style.edge"
/>
<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>
<CanvasEdgeToolbar :class="edgeToolbarClasses" :style="edgeToolbarStyle" @delete="onDelete" />
</EdgeLabelRenderer>
</template>
<style lang="scss" module>
.edgeToolbar {
pointer-events: all;
position: absolute;
opacity: 0;
&.edgeToolbarVisible {
opacity: 1;
}
&:hover {
opacity: 1;
}
}
</style>

View file

@ -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');
});
});

View file

@ -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>