mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Make new canvas connections go underneath node when looping backwards (#11833)
This commit is contained in:
parent
459e6aa9bc
commit
91d1bd8d33
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -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],
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './getEdgeRenderData';
|
Loading…
Reference in a new issue