mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
fix(editor): Address edge toolbar rendering glitches (#10839)
This commit is contained in:
parent
7f4ef31507
commit
e0c0ddee59
|
@ -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">
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
: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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue