feat(editor): Improve edges and connection lines rendering on new canvas (no-changelog) (#11036)
This commit is contained in:
@@ -50,7 +50,7 @@ describe('CanvasConnectionLine', () => {
|
|||||||
|
|
||||||
expect(edge).toHaveAttribute(
|
expect(edge).toHaveAttribute(
|
||||||
'd',
|
'd',
|
||||||
'M0 0L 32,0Q 40,0 40,8L 40,132Q 40,140 32,140L1 140L0 140M0 140L-40 140L -132,140Q -140,140 -140,132L -140,-92Q -140,-100 -132,-100L-100 -100',
|
'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',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ describe('CanvasConnectionLine', () => {
|
|||||||
|
|
||||||
expect(edge).toHaveAttribute(
|
expect(edge).toHaveAttribute(
|
||||||
'd',
|
'd',
|
||||||
'M-72 -290L -57,-290Q -52,-290 -52,-285L -52,-165Q -52,-160 -57,-160L -359,-160Q -364,-160 -364,-155L -364,-35Q -364,-30 -359,-30L-344 -30',
|
'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',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,16 +4,35 @@ import type { ConnectionLineProps } from '@vue-flow/core';
|
|||||||
import { BaseEdge } from '@vue-flow/core';
|
import { BaseEdge } from '@vue-flow/core';
|
||||||
import { computed, useCssModule } from 'vue';
|
import { computed, useCssModule } from 'vue';
|
||||||
import { getCustomPath } from './utils/edgePath';
|
import { getCustomPath } from './utils/edgePath';
|
||||||
|
import { useCanvas } from '@/composables/useCanvas';
|
||||||
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
import { parseCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
||||||
|
|
||||||
const props = defineProps<ConnectionLineProps>();
|
const props = defineProps<ConnectionLineProps>();
|
||||||
|
|
||||||
const $style = useCssModule();
|
const $style = useCssModule();
|
||||||
|
|
||||||
|
const { connectingHandle } = useCanvas();
|
||||||
|
|
||||||
|
const connectionType = computed(
|
||||||
|
() => parseCanvasConnectionHandleString(connectingHandle.value?.handleId).type,
|
||||||
|
);
|
||||||
|
|
||||||
|
const edgeColor = computed(() => {
|
||||||
|
if (connectionType.value !== NodeConnectionType.Main) {
|
||||||
|
return 'var(--node-type-supplemental-color)';
|
||||||
|
} else {
|
||||||
|
return 'var(--color-foreground-xdark)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const edgeStyle = computed(() => ({
|
const edgeStyle = computed(() => ({
|
||||||
|
...(connectionType.value === NodeConnectionType.Main ? {} : { strokeDasharray: '8,8' }),
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
|
stroke: edgeColor.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const path = computed(() => getCustomPath(props));
|
const path = computed(() => getCustomPath(props, { connectionType: connectionType.value }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -124,7 +124,31 @@ describe('CanvasEdge', () => {
|
|||||||
|
|
||||||
expect(edge).toHaveAttribute(
|
expect(edge).toHaveAttribute(
|
||||||
'd',
|
'd',
|
||||||
'M0 0L 32,0Q 40,0 40,8L 40,132Q 40,140 32,140L1 140L0 140M0 140L-40 140L -132,140Q -140,140 -140,132L -140,-92Q -140,-100 -132,-100L-100 -100',
|
'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',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render a correct bezier path when the connection is backwards and node connection type is non-main', () => {
|
||||||
|
const { container } = renderComponent({
|
||||||
|
props: {
|
||||||
|
...DEFAULT_PROPS,
|
||||||
|
data: {
|
||||||
|
...DEFAULT_PROPS.data,
|
||||||
|
source: {
|
||||||
|
type: NodeConnectionType.AiTool,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sourceX: 0,
|
||||||
|
sourceY: 0,
|
||||||
|
sourcePosition: Position.Right,
|
||||||
|
targetX: -100,
|
||||||
|
targetY: -100,
|
||||||
|
targetPosition: Position.Left,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const edge = container.querySelector('.vue-flow__edge-path');
|
||||||
|
|
||||||
|
expect(edge).toHaveAttribute('d', 'M0,0 C62.5,0 -162.5,-100 -100,-100');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ const renderToolbar = computed(() => (props.selected || isHovered.value) && !pro
|
|||||||
const isMainConnection = computed(() => data.value.source.type === NodeConnectionType.Main);
|
const isMainConnection = computed(() => data.value.source.type === NodeConnectionType.Main);
|
||||||
|
|
||||||
const status = computed(() => props.data.status);
|
const status = computed(() => props.data.status);
|
||||||
const statusColor = computed(() => {
|
|
||||||
|
const edgeColor = computed(() => {
|
||||||
if (props.selected) {
|
if (props.selected) {
|
||||||
return 'var(--color-background-dark)';
|
return 'var(--color-background-dark)';
|
||||||
} else if (status.value === 'success') {
|
} else if (status.value === 'success') {
|
||||||
@@ -69,10 +70,10 @@ const edgeStyle = computed(() => ({
|
|||||||
...props.style,
|
...props.style,
|
||||||
...(isMainConnection.value ? {} : { strokeDasharray: '8,8' }),
|
...(isMainConnection.value ? {} : { strokeDasharray: '8,8' }),
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
stroke: isHovered.value ? 'var(--color-primary)' : statusColor.value,
|
stroke: isHovered.value ? 'var(--color-primary)' : edgeColor.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const edgeLabelStyle = computed(() => ({ color: statusColor.value }));
|
const edgeLabelStyle = computed(() => ({ color: edgeColor.value }));
|
||||||
|
|
||||||
const edgeToolbarStyle = computed(() => {
|
const edgeToolbarStyle = computed(() => {
|
||||||
const [, labelX, labelY] = path.value;
|
const [, labelX, labelY] = path.value;
|
||||||
@@ -81,7 +82,11 @@ const edgeToolbarStyle = computed(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const path = computed(() => getCustomPath(props));
|
const path = computed(() =>
|
||||||
|
getCustomPath(props, {
|
||||||
|
connectionType: connectionType.value,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const connection = computed<Connection>(() => ({
|
const connection = computed<Connection>(() => ({
|
||||||
source: props.source,
|
source: props.source,
|
||||||
|
|||||||
@@ -1,38 +1,43 @@
|
|||||||
import type { EdgeProps } from '@vue-flow/core';
|
import type { EdgeProps } from '@vue-flow/core';
|
||||||
import { getBezierPath, getSmoothStepPath, Position } from '@vue-flow/core';
|
import { getBezierPath, getSmoothStepPath, Position } from '@vue-flow/core';
|
||||||
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
|
||||||
const EDGE_PADDING_Y = 140;
|
const EDGE_PADDING_TOP = 80;
|
||||||
const EDGE_PADDING_Y_TOP = 80;
|
const EDGE_PADDING_BOTTOM = 140;
|
||||||
const EDGE_BORDER_RADIUS = 8;
|
const EDGE_PADDING_X = 40;
|
||||||
const EDGE_OFFSET = 40;
|
const EDGE_BORDER_RADIUS = 16;
|
||||||
// const HANDLE_SIZE = 16;
|
const HANDLE_SIZE = 20; // Required to avoid connection line glitching when initially interacting with the handle
|
||||||
|
|
||||||
const isTargetHandlePositionedLeftOfSourceTarget = (sourceX: number, targetX: number) =>
|
const isRightOfSourceHandle = (sourceX: number, targetX: number) => sourceX - HANDLE_SIZE > targetX;
|
||||||
sourceX > targetX;
|
|
||||||
|
|
||||||
const pathIntersectsNodes = (targetY: number, sourceY: number) =>
|
const pathIntersectsNodes = (targetY: number, sourceY: number) =>
|
||||||
Math.abs(targetY - sourceY) < EDGE_PADDING_Y;
|
Math.abs(targetY - sourceY) < EDGE_PADDING_BOTTOM;
|
||||||
|
|
||||||
export function getCustomPath(
|
export function getCustomPath(
|
||||||
props: Pick<
|
props: Pick<
|
||||||
EdgeProps,
|
EdgeProps,
|
||||||
'sourceX' | 'sourceY' | 'sourcePosition' | 'targetX' | 'targetY' | 'targetPosition'
|
'sourceX' | 'sourceY' | 'sourcePosition' | 'targetX' | 'targetY' | 'targetPosition'
|
||||||
>,
|
>,
|
||||||
|
{
|
||||||
|
connectionType = NodeConnectionType.Main,
|
||||||
|
}: {
|
||||||
|
connectionType?: NodeConnectionType;
|
||||||
|
} = {},
|
||||||
) {
|
) {
|
||||||
const { targetX, targetY, sourceX, sourceY, sourcePosition, targetPosition } = props;
|
const { targetX, targetY, sourceX, sourceY, sourcePosition, targetPosition } = props;
|
||||||
const yDiff = targetY - sourceY;
|
const yDiff = targetY - sourceY;
|
||||||
|
|
||||||
if (!isTargetHandlePositionedLeftOfSourceTarget(sourceX, targetX)) {
|
if (!isRightOfSourceHandle(sourceX, targetX) || connectionType !== NodeConnectionType.Main) {
|
||||||
return getBezierPath(props);
|
return getBezierPath(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection is backwards and the source is on the right side
|
// Connection is backwards and the source is on the right side
|
||||||
// -> We need to avoid overlapping the source node
|
// -> We need to avoid overlapping the source node
|
||||||
if (pathIntersectsNodes(targetY, sourceY)) {
|
if (pathIntersectsNodes(targetY, sourceY)) {
|
||||||
const direction = yDiff < -EDGE_PADDING_Y || yDiff > 0 ? 'up' : 'down';
|
const direction = yDiff < -EDGE_PADDING_BOTTOM || yDiff > 0 ? 'up' : 'down';
|
||||||
const firstSegmentTargetX = sourceX;
|
const firstSegmentTargetX = sourceX;
|
||||||
const firstSegmentTargetY =
|
const firstSegmentTargetY =
|
||||||
sourceY + (direction === 'up' ? -EDGE_PADDING_Y_TOP : EDGE_PADDING_Y);
|
sourceY + (direction === 'up' ? -EDGE_PADDING_TOP : EDGE_PADDING_BOTTOM);
|
||||||
const [firstSegmentPath] = getSmoothStepPath({
|
const [firstSegmentPath] = getSmoothStepPath({
|
||||||
sourceX,
|
sourceX,
|
||||||
sourceY,
|
sourceY,
|
||||||
@@ -41,7 +46,7 @@ export function getCustomPath(
|
|||||||
sourcePosition,
|
sourcePosition,
|
||||||
targetPosition: Position.Right,
|
targetPosition: Position.Right,
|
||||||
borderRadius: EDGE_BORDER_RADIUS,
|
borderRadius: EDGE_BORDER_RADIUS,
|
||||||
offset: EDGE_OFFSET,
|
offset: EDGE_PADDING_X,
|
||||||
});
|
});
|
||||||
const path = getSmoothStepPath({
|
const path = getSmoothStepPath({
|
||||||
sourceX: firstSegmentTargetX,
|
sourceX: firstSegmentTargetX,
|
||||||
@@ -51,12 +56,15 @@ export function getCustomPath(
|
|||||||
sourcePosition: Position.Left,
|
sourcePosition: Position.Left,
|
||||||
targetPosition,
|
targetPosition,
|
||||||
borderRadius: EDGE_BORDER_RADIUS,
|
borderRadius: EDGE_BORDER_RADIUS,
|
||||||
offset: EDGE_OFFSET,
|
offset: EDGE_PADDING_X,
|
||||||
});
|
});
|
||||||
|
|
||||||
path[0] = firstSegmentPath + path[0];
|
path[0] = firstSegmentPath + path[0];
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getSmoothStepPath(props);
|
return getSmoothStepPath({
|
||||||
|
...props,
|
||||||
|
borderRadius: EDGE_BORDER_RADIUS,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user