fix(editor): Address edge toolbar rendering glitches (#10839)

This commit is contained in:
Raúl Gómez Morales 2024-09-17 11:22:49 +02:00 committed by GitHub
parent 7f4ef31507
commit e0c0ddee59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 94 additions and 66 deletions

View file

@ -7,7 +7,6 @@ import type {
ConnectStartEvent,
} from '@/types';
import type {
EdgeMouseEvent,
Connection,
XYPosition,
ViewportTransform,
@ -31,6 +30,7 @@ import { isPresent } from '@/utils/typesUtils';
import { GRID_SIZE } from '@/utils/nodeViewUtils';
import { CanvasKey } from '@/constants';
import { onKeyDown, onKeyUp } from '@vueuse/core';
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
const $style = useCssModule();
@ -261,19 +261,7 @@ function onClickConnectionAdd(connection: Connection) {
emit('click:connection:add', connection);
}
/**
* Connection hover
*/
const hoveredEdges = ref<Record<string, boolean>>({});
function onMouseEnterEdge(event: EdgeMouseEvent) {
hoveredEdges.value[event.edge.id] = true;
}
function onMouseLeaveEdge(event: EdgeMouseEvent) {
hoveredEdges.value[event.edge.id] = false;
}
const arrowHeadMarkerId = ref('custom-arrow-head');
/**
* Executions
@ -511,8 +499,6 @@ provide(CanvasKey, {
:selection-key-code="selectionKeyCode"
:pan-activation-key-code="panningKeyCode"
data-test-id="canvas"
@edge-mouse-enter="onMouseEnterEdge"
@edge-mouse-leave="onMouseLeaveEdge"
@connect-start="onConnectStart"
@connect="onConnect"
@connect-end="onConnectEnd"
@ -543,8 +529,8 @@ provide(CanvasKey, {
<template #edge-canvas-edge="canvasEdgeProps">
<Edge
v-bind="canvasEdgeProps"
:marker-end="`url(#${arrowHeadMarkerId})`"
:read-only="readOnly"
:hovered="hoveredEdges[canvasEdgeProps.id]"
@add="onClickConnectionAdd"
@delete="onDeleteConnection"
/>
@ -554,6 +540,8 @@ provide(CanvasKey, {
<CanvasConnectionLine v-bind="connectionLineProps" />
</template>
<CanvasArrowHeadMarker :id="arrowHeadMarkerId" />
<Background data-test-id="canvas-background" pattern-color="#aaa" :gap="GRID_SIZE" />
<Transition name="minimap">

View file

@ -0,0 +1,29 @@
<script lang="ts" setup>
defineProps<{ id: string }>();
</script>
<template>
<svg>
<defs>
<marker
:id="id"
viewBox="-10 -10 20 20"
refX="0"
refY="0"
markerWidth="12.5"
markerHeight="12.5"
markerUnits="strokeWidth"
orient="auto-start-reverse"
>
<polyline
stroke-linecap="round"
stroke-linejoin="round"
points="-5,-4 0,0 -5,4 -5,-4"
stroke-width="2"
stroke="context-stroke"
fill="context-stroke"
/>
</marker>
</defs>
</svg>
</template>

View file

@ -1,10 +1,10 @@
import { fireEvent } from '@testing-library/vue';
import CanvasEdge, { type CanvasEdgeProps } from './CanvasEdge.vue';
import { createComponentRenderer } from '@/__tests__/render';
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import { Position } from '@vue-flow/core';
import { NodeConnectionType } from 'n8n-workflow';
import userEvent from '@testing-library/user-event';
const DEFAULT_PROPS = {
sourceX: 0,
@ -31,18 +31,21 @@ beforeEach(() => {
describe('CanvasEdge', () => {
it('should emit delete event when toolbar delete is clicked', async () => {
const { emitted, getByTestId } = renderComponent();
await userEvent.hover(getByTestId('edge-label-wrapper'));
const deleteButton = getByTestId('delete-connection-button');
await fireEvent.click(deleteButton);
await userEvent.click(deleteButton);
expect(emitted()).toHaveProperty('delete');
});
it('should emit add event when toolbar add is clicked', async () => {
const { emitted, getByTestId } = renderComponent();
await userEvent.hover(getByTestId('edge-label-wrapper'));
const addButton = getByTestId('add-connection-button');
await fireEvent.click(addButton);
await userEvent.click(addButton);
expect(emitted()).toHaveProperty('add');
});
@ -54,6 +57,8 @@ describe('CanvasEdge', () => {
},
});
await userEvent.hover(getByTestId('edge-label-wrapper'));
expect(() => getByTestId('add-connection-button')).toThrow();
expect(() => getByTestId('delete-connection-button')).toThrow();
});

View file

@ -3,9 +3,9 @@
import type { CanvasConnectionData } from '@/types';
import { isValidNodeConnectionType } from '@/utils/typeGuards';
import type { Connection, EdgeProps } from '@vue-flow/core';
import { BaseEdge, EdgeLabelRenderer } from '@vue-flow/core';
import { useVueFlow, BaseEdge, EdgeLabelRenderer } from '@vue-flow/core';
import { NodeConnectionType } from 'n8n-workflow';
import { computed, useCssModule } from 'vue';
import { computed, useCssModule, ref } from 'vue';
import CanvasEdgeToolbar from './CanvasEdgeToolbar.vue';
import { getCustomPath } from './utils/edgePath';
@ -21,6 +21,19 @@ export type CanvasEdgeProps = EdgeProps<CanvasConnectionData> & {
const props = defineProps<CanvasEdgeProps>();
const { onEdgeMouseEnter, onEdgeMouseLeave } = useVueFlow();
const isHovered = ref(false);
onEdgeMouseEnter(({ edge }) => {
if (edge.id !== props.id) return;
isHovered.value = true;
});
onEdgeMouseLeave(({ edge }) => {
if (edge.id !== props.id) return;
isHovered.value = false;
});
const $style = useCssModule();
const connectionType = computed(() =>
@ -29,7 +42,7 @@ const connectionType = computed(() =>
: NodeConnectionType.Main,
);
const isFocused = computed(() => props.selected || props.hovered);
const renderToolbar = computed(() => (props.selected || isHovered.value) && !props.readOnly);
const status = computed(() => props.data.status);
const statusColor = computed(() => {
@ -49,22 +62,10 @@ const statusColor = computed(() => {
const edgeStyle = computed(() => ({
...props.style,
strokeWidth: 2,
stroke: statusColor.value,
stroke: isHovered.value ? 'var(--color-primary)' : statusColor.value,
}));
const edgeLabel = computed(() => {
if (isFocused.value && !props.readOnly) {
return '';
}
return props.label;
});
const edgeLabelStyle = computed(() => ({
fill: statusColor.value,
transform: 'translateY(calc(var(--spacing-xs) * -1))',
fontSize: 'var(--font-size-xs)',
}));
const edgeLabelStyle = computed(() => ({ color: statusColor.value }));
const edgeToolbarStyle = computed(() => {
const [, labelX, labelY] = path.value;
@ -73,13 +74,6 @@ const edgeToolbarStyle = computed(() => {
};
});
const edgeToolbarClasses = computed(() => ({
[$style.edgeToolbar]: true,
[$style.edgeToolbarVisible]: isFocused.value,
nodrag: true,
nopan: true,
}));
const path = computed(() => getCustomPath(props));
const connection = computed<Connection>(() => ({
@ -105,39 +99,47 @@ function onDelete() {
:style="edgeStyle"
:path="path[0]"
:marker-end="markerEnd"
:label="edgeLabel"
:label-x="path[1]"
:label-y="path[2]"
:label-style="edgeLabelStyle"
:label-show-bg="false"
:interaction-width="40"
/>
<EdgeLabelRenderer v-if="!readOnly">
<CanvasEdgeToolbar
:type="connectionType"
:class="edgeToolbarClasses"
<EdgeLabelRenderer>
<div
data-test-id="edge-label-wrapper"
:style="edgeToolbarStyle"
@add="onAdd"
@delete="onDelete"
/>
:class="$style.edgeLabelWrapper"
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
>
<CanvasEdgeToolbar
v-if="renderToolbar"
:type="connectionType"
@add="onAdd"
@delete="onDelete"
/>
<div v-else :style="edgeLabelStyle" :class="$style.edgeLabel">{{ label }}</div>
</div>
</EdgeLabelRenderer>
</template>
<style lang="scss" module>
.edge {
transition: stroke 0.3s ease;
transition:
stroke 0.3s ease,
fill 0.3s ease;
}
.edgeToolbar {
.edgeLabelWrapper {
transform: translateY(calc(var(--spacing-xs) * -1));
position: absolute;
opacity: 0;
}
&.edgeToolbarVisible {
opacity: 1;
}
&:hover {
opacity: 1;
}
.edgeLabel {
font-size: var(--font-size-xs);
background-color: hsla(
var(--color-canvas-background-h),
var(--color-canvas-background-s),
var(--color-canvas-background-l),
0.85
);
}
</style>

View file

@ -70,4 +70,8 @@ function onDelete() {
--button-background-color: var(--color-background-base);
--button-hover-background-color: var(--color-background-light);
}
.canvas-edge-toolbar-button {
border-width: 2px;
}
</style>