feat(editor): Make new canvas connections go underneath node when looping backwards (#11833)

This commit is contained in:
Alex Grozav 2024-11-25 14:29:37 +02:00 committed by GitHub
parent 459e6aa9bc
commit 91d1bd8d33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 102 additions and 105 deletions

View file

@ -46,32 +46,15 @@ describe('CanvasConnectionLine', () => {
},
});
const edge = container.querySelector('.vue-flow__edge-path');
const edges = container.querySelectorAll('.vue-flow__edge-path');
expect(edge).toHaveAttribute(
expect(edges[0]).toHaveAttribute(
'd',
'M0 0L 24,0Q 40,0 40,16L 40,124Q 40,140 24,140L1 140L0 140M0 140L-40 140L -124,140Q -140,140 -140,124L -140,-84Q -140,-100 -124,-100L-100 -100',
'M0 0L 24,0Q 40,0 40,16L 40,114Q 40,130 24,130L-10 130L-50 130',
);
});
it('should only avoid obstacles when the edge intersects the nodes ', () => {
const { container } = renderComponent({
props: {
...DEFAULT_PROPS,
sourceX: -72,
sourceY: -290,
sourcePosition: Position.Right,
targetX: -344,
targetY: -30,
targetPosition: Position.Left,
},
});
const edge = container.querySelector('.vue-flow__edge-path');
expect(edge).toHaveAttribute(
expect(edges[1]).toHaveAttribute(
'd',
'M-72 -290L -62,-290Q -52,-290 -52,-280L -52,-176Q -52,-160 -68,-160L -348,-160Q -364,-160 -364,-144L -364,-40Q -364,-30 -354,-30L-344 -30',
'M-50 130L-90 130L -124,130Q -140,130 -140,114L -140,-84Q -140,-100 -124,-100L-100 -100',
);
});
});

View file

@ -3,7 +3,7 @@
import type { ConnectionLineProps } from '@vue-flow/core';
import { BaseEdge } from '@vue-flow/core';
import { computed, useCssModule } from 'vue';
import { getCustomPath } from './utils/edgePath';
import { getEdgeRenderData } from './utils';
import { useCanvas } from '@/composables/useCanvas';
import { NodeConnectionType } from 'n8n-workflow';
import { parseCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
@ -32,11 +32,22 @@ const edgeStyle = computed(() => ({
stroke: edgeColor.value,
}));
const path = computed(() => getCustomPath(props, { connectionType: connectionType.value }));
const renderData = computed(() =>
getEdgeRenderData(props, { connectionType: connectionType.value }),
);
const segments = computed(() => renderData.value.segments);
</script>
<template>
<BaseEdge :class="$style.edge" :style="edgeStyle" :path="path[0]" :marker-end="markerEnd" />
<BaseEdge
v-for="segment in segments"
:key="segment[0]"
:class="$style.edge"
:style="edgeStyle"
:path="segment[0]"
:marker-end="markerEnd"
/>
</template>
<style lang="scss" module>

View file

@ -128,11 +128,15 @@ describe('CanvasEdge', () => {
},
});
const edge = container.querySelector('.vue-flow__edge-path');
const edges = container.querySelectorAll('.vue-flow__edge-path');
expect(edge).toHaveAttribute(
expect(edges[0]).toHaveAttribute(
'd',
'M0 0L 24,0Q 40,0 40,16L 40,124Q 40,140 24,140L1 140L0 140M0 140L-40 140L -124,140Q -140,140 -140,124L -140,-84Q -140,-100 -124,-100L-100 -100',
'M0 0L 24,0Q 40,0 40,16L 40,114Q 40,130 24,130L-10 130L-50 130',
);
expect(edges[1]).toHaveAttribute(
'd',
'M-50 130L-90 130L -124,130Q -140,130 -140,114L -140,-84Q -140,-100 -124,-100L-100 -100',
);
});

View file

@ -7,7 +7,7 @@ import { BaseEdge, EdgeLabelRenderer } from '@vue-flow/core';
import { NodeConnectionType } from 'n8n-workflow';
import { computed, useCssModule, toRef } from 'vue';
import CanvasEdgeToolbar from './CanvasEdgeToolbar.vue';
import { getCustomPath } from './utils/edgePath';
import { getEdgeRenderData } from './utils';
const emit = defineEmits<{
add: [connection: Connection];
@ -73,9 +73,8 @@ const edgeLabelStyle = computed(() => ({
}));
const edgeToolbarStyle = computed(() => {
const [, labelX, labelY] = path.value;
return {
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
transform: `translate(-50%, -50%) translate(${labelPosition.value[0]}px,${labelPosition.value[1]}px)`,
...(props.hovered ? { zIndex: 1 } : {}),
};
});
@ -86,12 +85,16 @@ const edgeToolbarClasses = computed(() => ({
selected: props.selected,
}));
const path = computed(() =>
getCustomPath(props, {
const renderData = computed(() =>
getEdgeRenderData(props, {
connectionType: connectionType.value,
}),
);
const segments = computed(() => renderData.value.segments);
const labelPosition = computed(() => renderData.value.labelPosition);
const connection = computed<Connection>(() => ({
source: props.source,
target: props.target,
@ -118,10 +121,12 @@ function onEdgeLabelMouseLeave() {
<template>
<BaseEdge
:id="id"
v-for="(segment, index) in segments"
:id="`${id}-${index}`"
:key="segment[0]"
:class="edgeClasses"
:style="edgeStyle"
:path="path[0]"
:path="segment[0]"
:marker-end="markerEnd"
:interaction-width="40"
/>

View file

@ -1,70 +0,0 @@
import type { EdgeProps } from '@vue-flow/core';
import { getBezierPath, getSmoothStepPath, Position } from '@vue-flow/core';
import { NodeConnectionType } from 'n8n-workflow';
const EDGE_PADDING_TOP = 80;
const EDGE_PADDING_BOTTOM = 140;
const EDGE_PADDING_X = 40;
const EDGE_BORDER_RADIUS = 16;
const HANDLE_SIZE = 20; // Required to avoid connection line glitching when initially interacting with the handle
const isRightOfSourceHandle = (sourceX: number, targetX: number) => sourceX - HANDLE_SIZE > targetX;
const pathIntersectsNodes = (targetY: number, sourceY: number) =>
Math.abs(targetY - sourceY) < EDGE_PADDING_BOTTOM;
export function getCustomPath(
props: Pick<
EdgeProps,
'sourceX' | 'sourceY' | 'sourcePosition' | 'targetX' | 'targetY' | 'targetPosition'
>,
{
connectionType = NodeConnectionType.Main,
}: {
connectionType?: NodeConnectionType;
} = {},
) {
const { targetX, targetY, sourceX, sourceY, sourcePosition, targetPosition } = props;
const yDiff = targetY - sourceY;
if (!isRightOfSourceHandle(sourceX, targetX) || connectionType !== NodeConnectionType.Main) {
return getBezierPath(props);
}
// Connection is backwards and the source is on the right side
// -> We need to avoid overlapping the source node
if (pathIntersectsNodes(targetY, sourceY)) {
const direction = yDiff < -EDGE_PADDING_BOTTOM || yDiff > 0 ? 'up' : 'down';
const firstSegmentTargetX = sourceX;
const firstSegmentTargetY =
sourceY + (direction === 'up' ? -EDGE_PADDING_TOP : EDGE_PADDING_BOTTOM);
const [firstSegmentPath] = getSmoothStepPath({
sourceX,
sourceY,
targetX: firstSegmentTargetX,
targetY: firstSegmentTargetY,
sourcePosition,
targetPosition: Position.Right,
borderRadius: EDGE_BORDER_RADIUS,
offset: EDGE_PADDING_X,
});
const path = getSmoothStepPath({
sourceX: firstSegmentTargetX,
sourceY: firstSegmentTargetY,
targetX,
targetY,
sourcePosition: Position.Left,
targetPosition,
borderRadius: EDGE_BORDER_RADIUS,
offset: EDGE_PADDING_X,
});
path[0] = firstSegmentPath + path[0];
return path;
}
return getSmoothStepPath({
...props,
borderRadius: EDGE_BORDER_RADIUS,
});
}

View file

@ -0,0 +1,63 @@
import type { EdgeProps } from '@vue-flow/core';
import { getBezierPath, getSmoothStepPath, Position } from '@vue-flow/core';
import { NodeConnectionType } from 'n8n-workflow';
const EDGE_PADDING_BOTTOM = 130;
const EDGE_PADDING_X = 40;
const EDGE_BORDER_RADIUS = 16;
const HANDLE_SIZE = 20; // Required to avoid connection line glitching when initially interacting with the handle
const isRightOfSourceHandle = (sourceX: number, targetX: number) => sourceX - HANDLE_SIZE > targetX;
export function getEdgeRenderData(
props: Pick<
EdgeProps,
'sourceX' | 'sourceY' | 'sourcePosition' | 'targetX' | 'targetY' | 'targetPosition'
>,
{
connectionType = NodeConnectionType.Main,
}: {
connectionType?: NodeConnectionType;
} = {},
) {
const { targetX, targetY, sourceX, sourceY, sourcePosition, targetPosition } = props;
if (!isRightOfSourceHandle(sourceX, targetX) || connectionType !== NodeConnectionType.Main) {
const segment = getBezierPath(props);
return {
segments: [segment],
labelPosition: [segment[1], segment[2]],
};
}
// Connection is backwards and the source is on the right side
// -> We need to avoid overlapping the source node
const firstSegmentTargetX = (sourceX + targetX) / 2;
const firstSegmentTargetY = sourceY + EDGE_PADDING_BOTTOM;
const firstSegment = getSmoothStepPath({
sourceX,
sourceY,
targetX: firstSegmentTargetX,
targetY: firstSegmentTargetY,
sourcePosition,
targetPosition: Position.Right,
borderRadius: EDGE_BORDER_RADIUS,
offset: EDGE_PADDING_X,
});
const secondSegment = getSmoothStepPath({
sourceX: firstSegmentTargetX,
sourceY: firstSegmentTargetY,
targetX,
targetY,
sourcePosition: Position.Left,
targetPosition,
borderRadius: EDGE_BORDER_RADIUS,
offset: EDGE_PADDING_X,
});
return {
segments: [firstSegment, secondSegment],
labelPosition: [firstSegmentTargetX, firstSegmentTargetY],
};
}

View file

@ -0,0 +1 @@
export * from './getEdgeRenderData';