refactor: Document and rename canvasUtils (no-changelog) (#13423)

This commit is contained in:
Alex Grozav 2025-02-24 10:05:15 +02:00 committed by GitHub
parent 2ef6f111d0
commit dd43854a8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 90 additions and 29 deletions

View file

@ -6,7 +6,7 @@ import { computed, onMounted, ref, useCssModule } from 'vue';
import { getEdgeRenderData } from './utils'; import { getEdgeRenderData } from './utils';
import { useCanvas } from '@/composables/useCanvas'; import { useCanvas } from '@/composables/useCanvas';
import { NodeConnectionType } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow';
import { parseCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; import { parseCanvasConnectionHandleString } from '@/utils/canvasUtils';
const props = defineProps<ConnectionLineProps>(); const props = defineProps<ConnectionLineProps>();

View file

@ -31,7 +31,7 @@ import { useCanvas } from '@/composables/useCanvas';
import { import {
createCanvasConnectionHandleString, createCanvasConnectionHandleString,
insertSpacersBetweenEndpoints, insertSpacersBetweenEndpoints,
} from '@/utils/canvasUtilsV2'; } from '@/utils/canvasUtils';
import type { EventBus } from 'n8n-design-system'; import type { EventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system'; import { createEventBus } from 'n8n-design-system';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';

View file

@ -15,10 +15,7 @@ import {
} from '@/__tests__/mocks'; } from '@/__tests__/mocks';
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE, STICKY_NODE_TYPE, STORES } from '@/constants'; import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE, STICKY_NODE_TYPE, STORES } from '@/constants';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { import { createCanvasConnectionHandleString, createCanvasConnectionId } from '@/utils/canvasUtils';
createCanvasConnectionHandleString,
createCanvasConnectionId,
} from '@/utils/canvasUtilsV2';
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types'; import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
import { MarkerType } from '@vue-flow/core'; import { MarkerType } from '@vue-flow/core';
import { createTestingPinia } from '@pinia/testing'; import { createTestingPinia } from '@pinia/testing';

View file

@ -27,7 +27,7 @@ import {
mapLegacyConnectionsToCanvasConnections, mapLegacyConnectionsToCanvasConnections,
mapLegacyEndpointsToCanvasConnectionPort, mapLegacyEndpointsToCanvasConnectionPort,
parseCanvasConnectionHandleString, parseCanvasConnectionHandleString,
} from '@/utils/canvasUtilsV2'; } from '@/utils/canvasUtils';
import type { import type {
ExecutionStatus, ExecutionStatus,
ExecutionSummary, ExecutionSummary,

View file

@ -48,7 +48,7 @@ import {
} from '@/constants'; } from '@/constants';
import type { Connection } from '@vue-flow/core'; import type { Connection } from '@vue-flow/core';
import { useClipboard } from '@/composables/useClipboard'; import { useClipboard } from '@/composables/useClipboard';
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; import { createCanvasConnectionHandleString } from '@/utils/canvasUtils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { useProjectsStore } from '@/stores/projects.store'; import { useProjectsStore } from '@/stores/projects.store';

View file

@ -65,7 +65,7 @@ import {
mapLegacyConnectionsToCanvasConnections, mapLegacyConnectionsToCanvasConnections,
mapLegacyConnectionToCanvasConnection, mapLegacyConnectionToCanvasConnection,
parseCanvasConnectionHandleString, parseCanvasConnectionHandleString,
} from '@/utils/canvasUtilsV2'; } from '@/utils/canvasUtils';
import * as NodeViewUtils from '@/utils/nodeViewUtils'; import * as NodeViewUtils from '@/utils/nodeViewUtils';
import { import {
CONFIGURABLE_NODE_SIZE, CONFIGURABLE_NODE_SIZE,

View file

@ -3,7 +3,7 @@ import { NodeConnectionType } from 'n8n-workflow';
import { useNodeConnections } from '@/composables/useNodeConnections'; import { useNodeConnections } from '@/composables/useNodeConnections';
import type { CanvasNodeData } from '@/types'; import type { CanvasNodeData } from '@/types';
import { CanvasConnectionMode } from '@/types'; import { CanvasConnectionMode } from '@/types';
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; import { createCanvasConnectionHandleString } from '@/utils/canvasUtils';
describe('useNodeConnections', () => { describe('useNodeConnections', () => {
const defaultConnections = { const defaultConnections = {

View file

@ -4,7 +4,7 @@ import type { MaybeRef } from 'vue';
import { computed, unref } from 'vue'; import { computed, unref } from 'vue';
import { NodeConnectionType } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow';
import type { Connection } from '@vue-flow/core'; import type { Connection } from '@vue-flow/core';
import { parseCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; import { parseCanvasConnectionHandleString } from '@/utils/canvasUtils';
export function useNodeConnections({ export function useNodeConnections({
inputs, inputs,

View file

@ -7,7 +7,7 @@ import {
REGULAR_NODE_CREATOR_VIEW, REGULAR_NODE_CREATOR_VIEW,
} from '@/constants'; } from '@/constants';
import type { INodeCreateElement } from '@/Interface'; import type { INodeCreateElement } from '@/Interface';
import { parseCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; import { parseCanvasConnectionHandleString } from '@/utils/canvasUtils';
import { NodeConnectionType } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow';
import { CanvasConnectionMode } from '@/types'; import { CanvasConnectionMode } from '@/types';
@ -56,7 +56,7 @@ vi.mock('@/stores/workflows.store', () => {
}; };
}); });
vi.mock('@/utils/canvasUtilsV2', () => { vi.mock('@/utils/canvasUtils', () => {
return { return {
parseCanvasConnectionHandleString: vi.fn(), parseCanvasConnectionHandleString: vi.fn(),
}; };

View file

@ -31,7 +31,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { import {
createCanvasConnectionHandleString, createCanvasConnectionHandleString,
parseCanvasConnectionHandleString, parseCanvasConnectionHandleString,
} from '@/utils/canvasUtilsV2'; } from '@/utils/canvasUtils';
import type { Connection } from '@vue-flow/core'; import type { Connection } from '@vue-flow/core';
import { CanvasConnectionMode } from '@/types'; import { CanvasConnectionMode } from '@/types';
import { isVueFlowConnection } from '@/utils/typeGuards'; import { isVueFlowConnection } from '@/utils/typeGuards';

View file

@ -12,7 +12,7 @@ import type {
} from '@/Interface'; } from '@/Interface';
import { useSettingsStore } from './settings.store'; import { useSettingsStore } from './settings.store';
import * as templatesApi from '@/api/templates'; import * as templatesApi from '@/api/templates';
import { getFixedNodesList } from '@/utils/nodeViewUtils'; import { getNodesWithNormalizedPosition } from '@/utils/nodeViewUtils';
import { useRootStore } from '@/stores/root.store'; import { useRootStore } from '@/stores/root.store';
import { useUsersStore } from './users.store'; import { useUsersStore } from './users.store';
import { useWorkflowsStore } from './workflows.store'; import { useWorkflowsStore } from './workflows.store';
@ -399,7 +399,9 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, () => {
): Promise<IWorkflowTemplate | undefined> => { ): Promise<IWorkflowTemplate | undefined> => {
const template = await getWorkflowTemplate(templateId); const template = await getWorkflowTemplate(templateId);
if (template?.workflow?.nodes) { if (template?.workflow?.nodes) {
template.workflow.nodes = getFixedNodesList(template.workflow.nodes) as INodeUi[]; template.workflow.nodes = getNodesWithNormalizedPosition(
template.workflow.nodes,
) as INodeUi[];
template.workflow.nodes?.forEach((node) => { template.workflow.nodes?.forEach((node) => {
if (node.credentials) { if (node.credentials) {
delete node.credentials; delete node.credentials;

View file

@ -7,7 +7,7 @@ import {
mapLegacyConnectionsToCanvasConnections, mapLegacyConnectionsToCanvasConnections,
mapLegacyEndpointsToCanvasConnectionPort, mapLegacyEndpointsToCanvasConnectionPort,
parseCanvasConnectionHandleString, parseCanvasConnectionHandleString,
} from '@/utils/canvasUtilsV2'; } from '@/utils/canvasUtils';
import type { IConnection, IConnections, INodeTypeDescription } from 'n8n-workflow'; import type { IConnection, IConnections, INodeTypeDescription } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow';
import type { CanvasConnection } from '@/types'; import type { CanvasConnection } from '@/types';

View file

@ -6,6 +6,9 @@ import type { Connection } from '@vue-flow/core';
import { isValidCanvasConnectionMode, isValidNodeConnectionType } from '@/utils/typeGuards'; import { isValidCanvasConnectionMode, isValidNodeConnectionType } from '@/utils/typeGuards';
import { NodeConnectionType } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow';
/**
* Maps multiple legacy n8n connections to VueFlow connections
*/
export function mapLegacyConnectionsToCanvasConnections( export function mapLegacyConnectionsToCanvasConnections(
legacyConnections: IConnections, legacyConnections: IConnections,
nodes: INodeUi[], nodes: INodeUi[],
@ -75,6 +78,9 @@ export function mapLegacyConnectionsToCanvasConnections(
return mappedConnections; return mappedConnections;
} }
/**
* Maps a single legacy n8n connection to a VueFlow connection
*/
export function mapLegacyConnectionToCanvasConnection( export function mapLegacyConnectionToCanvasConnection(
sourceNode: INodeUi, sourceNode: INodeUi,
targetNode: INodeUi, targetNode: INodeUi,
@ -101,6 +107,12 @@ export function mapLegacyConnectionToCanvasConnection(
}; };
} }
/**
* Parses a canvas connection handle string into its parts:
* - mode
* - type
* - index
*/
export function parseCanvasConnectionHandleString(handle: string | null | undefined) { export function parseCanvasConnectionHandleString(handle: string | null | undefined) {
const [mode, type, index] = (handle ?? '').split('/'); const [mode, type, index] = (handle ?? '').split('/');
@ -118,6 +130,9 @@ export function parseCanvasConnectionHandleString(handle: string | null | undefi
}; };
} }
/**
* Creates a canvas connection handle string from its parts
*/
export function createCanvasConnectionHandleString({ export function createCanvasConnectionHandleString({
mode, mode,
type = NodeConnectionType.Main, type = NodeConnectionType.Main,
@ -130,10 +145,16 @@ export function createCanvasConnectionHandleString({
return `${mode}/${type}/${index}`; return `${mode}/${type}/${index}`;
} }
/**
* Creates a canvas connection ID from a connection
*/
export function createCanvasConnectionId(connection: Connection) { export function createCanvasConnectionId(connection: Connection) {
return `[${connection.source}/${connection.sourceHandle}][${connection.target}/${connection.targetHandle}]`; return `[${connection.source}/${connection.sourceHandle}][${connection.target}/${connection.targetHandle}]`;
} }
/**
* Maps a VueFlow connection to a legacy n8n connection
*/
export function mapCanvasConnectionToLegacyConnection( export function mapCanvasConnectionToLegacyConnection(
sourceNode: INodeUi, sourceNode: INodeUi,
targetNode: INodeUi, targetNode: INodeUi,
@ -165,6 +186,9 @@ export function mapCanvasConnectionToLegacyConnection(
]; ];
} }
/**
* Maps legacy n8n node inputs to VueFlow connection handles
*/
export function mapLegacyEndpointsToCanvasConnectionPort( export function mapLegacyEndpointsToCanvasConnectionPort(
endpoints: INodeTypeDescription['inputs'], endpoints: INodeTypeDescription['inputs'],
endpointNames: string[] = [], endpointNames: string[] = [],
@ -196,6 +220,9 @@ export function mapLegacyEndpointsToCanvasConnectionPort(
}); });
} }
/**
* Checks if two bounding boxes overlap
*/
export function checkOverlap(node1: BoundingBox, node2: BoundingBox) { export function checkOverlap(node1: BoundingBox, node2: BoundingBox) {
return !( return !(
// node1 is completely to the left of node2 // node1 is completely to the left of node2
@ -211,6 +238,9 @@ export function checkOverlap(node1: BoundingBox, node2: BoundingBox) {
); );
} }
/**
* Inserts spacers between endpoints to visually separate them
*/
export function insertSpacersBetweenEndpoints<T>( export function insertSpacersBetweenEndpoints<T>(
endpoints: T[], endpoints: T[],
requiredEndpointsCount = 0, requiredEndpointsCount = 0,

View file

@ -34,6 +34,9 @@ export const HEADER_HEIGHT = 65;
export const MAX_X_TO_PUSH_DOWNSTREAM_NODES = 300; export const MAX_X_TO_PUSH_DOWNSTREAM_NODES = 300;
export const PUSH_NODES_OFFSET = NODE_SIZE * 2 + GRID_SIZE; export const PUSH_NODES_OFFSET = NODE_SIZE * 2 + GRID_SIZE;
/**
* Returns the leftmost and topmost node from the given list of nodes
*/
export const getLeftmostTopNode = <T extends { position: XYPosition }>(nodes: T[]): T => { export const getLeftmostTopNode = <T extends { position: XYPosition }>(nodes: T[]): T => {
return nodes.reduce((leftmostTop, node) => { return nodes.reduce((leftmostTop, node) => {
if (node.position[0] > leftmostTop.position[0] || node.position[1] > leftmostTop.position[1]) { if (node.position[0] > leftmostTop.position[0] || node.position[1] > leftmostTop.position[1]) {
@ -44,6 +47,9 @@ export const getLeftmostTopNode = <T extends { position: XYPosition }>(nodes: T[
}, nodes[0]); }, nodes[0]);
}; };
/**
* Checks if the given position is available for a new node
*/
const canUsePosition = (position1: XYPosition, position2: XYPosition) => { const canUsePosition = (position1: XYPosition, position2: XYPosition) => {
if (Math.abs(position1[0] - position2[0]) <= 100) { if (Math.abs(position1[0] - position2[0]) <= 100) {
if (Math.abs(position1[1] - position2[1]) <= 50) { if (Math.abs(position1[1] - position2[1]) <= 50) {
@ -54,6 +60,9 @@ const canUsePosition = (position1: XYPosition, position2: XYPosition) => {
return true; return true;
}; };
/**
* Returns the closest number divisible by the given number
*/
const closestNumberDivisibleBy = (inputNumber: number, divisibleBy: number): number => { const closestNumberDivisibleBy = (inputNumber: number, divisibleBy: number): number => {
const quotient = Math.ceil(inputNumber / divisibleBy); const quotient = Math.ceil(inputNumber / divisibleBy);
@ -73,6 +82,9 @@ const closestNumberDivisibleBy = (inputNumber: number, divisibleBy: number): num
return inputNumber2; return inputNumber2;
}; };
/**
* Returns the new position for a node based on the given position and the nodes in the workflow
*/
export const getNewNodePosition = ( export const getNewNodePosition = (
nodes: INodeUi[], nodes: INodeUi[],
newPosition: XYPosition, newPosition: XYPosition,
@ -113,6 +125,9 @@ export const getNewNodePosition = (
return targetPosition; return targetPosition;
}; };
/**
* Returns the position of a mouse or touch event
*/
export const getMousePosition = (e: MouseEvent | TouchEvent): XYPosition => { export const getMousePosition = (e: MouseEvent | TouchEvent): XYPosition => {
// @ts-ignore // @ts-ignore
const x = e.pageX !== undefined ? e.pageX : e.touches?.[0]?.pageX ? e.touches[0].pageX : 0; const x = e.pageX !== undefined ? e.pageX : e.touches?.[0]?.pageX ? e.touches[0].pageX : 0;
@ -122,6 +137,9 @@ export const getMousePosition = (e: MouseEvent | TouchEvent): XYPosition => {
return [x, y]; return [x, y];
}; };
/**
* Returns the relative position of a point on the canvas
*/
export const getRelativePosition = ( export const getRelativePosition = (
x: number, x: number,
y: number, y: number,
@ -131,12 +149,9 @@ export const getRelativePosition = (
return [(x - offset[0]) / scale, (y - offset[1]) / scale]; return [(x - offset[0]) / scale, (y - offset[1]) / scale];
}; };
export const getMidCanvasPosition = (scale: number, offset: XYPosition): XYPosition => { /**
const { editorWidth, editorHeight } = getContentDimensions(); * Returns the width and height of the node view content
*/
return getRelativePosition(editorWidth / 2, (editorHeight - HEADER_HEIGHT) / 2, scale, offset);
};
const getContentDimensions = (): { editorWidth: number; editorHeight: number } => { const getContentDimensions = (): { editorWidth: number; editorHeight: number } => {
let contentWidth = window.innerWidth; let contentWidth = window.innerWidth;
let contentHeight = window.innerHeight; let contentHeight = window.innerHeight;
@ -153,7 +168,21 @@ const getContentDimensions = (): { editorWidth: number; editorHeight: number } =
}; };
}; };
export const getFixedNodesList = <T extends { position: XYPosition }>(workflowNodes: T[]): T[] => { /**
* Returns the position of the canvas center
*/
export const getMidCanvasPosition = (scale: number, offset: XYPosition): XYPosition => {
const { editorWidth, editorHeight } = getContentDimensions();
return getRelativePosition(editorWidth / 2, (editorHeight - HEADER_HEIGHT) / 2, scale, offset);
};
/**
* Normalize node positions based on the leftmost top node
*/
export const getNodesWithNormalizedPosition = <T extends { position: XYPosition }>(
workflowNodes: T[],
): T[] => {
const nodes = [...workflowNodes]; const nodes = [...workflowNodes];
if (nodes.length) { if (nodes.length) {
@ -229,6 +258,9 @@ export function isElementIntersection(
return isWithinVerticalBounds && isWithinHorizontalBounds; return isWithinVerticalBounds && isWithinHorizontalBounds;
} }
/**
* Returns the node hints based on the node type and execution data
*/
export function getGenericHints({ export function getGenericHints({
workflowNode, workflowNode,
node, node,

View file

@ -9,7 +9,7 @@ import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants';
import type { useRootStore } from '@/stores/root.store'; import type { useRootStore } from '@/stores/root.store';
import type { PosthogStore } from '@/stores/posthog.store'; import type { PosthogStore } from '@/stores/posthog.store';
import type { useWorkflowsStore } from '@/stores/workflows.store'; import type { useWorkflowsStore } from '@/stores/workflows.store';
import { getFixedNodesList } from '@/utils/nodeViewUtils'; import { getNodesWithNormalizedPosition } from '@/utils/nodeViewUtils';
import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms'; import type { NodeTypeProvider } from '@/utils/nodeTypes/nodeTypeTransforms';
import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms'; import type { TemplateCredentialKey } from '@/utils/templates/templateTransforms';
import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms'; import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms';
@ -43,7 +43,7 @@ export async function createWorkflowFromTemplate(opts: {
template.workflow.nodes, template.workflow.nodes,
credentialOverrides, credentialOverrides,
); );
const nodes = getFixedNodesList(nodesWithCreds) as INodeUi[]; const nodes = getNodesWithNormalizedPosition(nodesWithCreds) as INodeUi[];
const connections = template.workflow.connections; const connections = template.workflow.connections;
const workflowToCreate: IWorkflowData = { const workflowToCreate: IWorkflowData = {

View file

@ -95,7 +95,7 @@ import { sourceControlEventBus } from '@/event-bus/source-control';
import { useTagsStore } from '@/stores/tags.store'; import { useTagsStore } from '@/stores/tags.store';
import { usePushConnectionStore } from '@/stores/pushConnection.store'; import { usePushConnectionStore } from '@/stores/pushConnection.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { getFixedNodesList, getNodeViewTab } from '@/utils/nodeViewUtils'; import { getNodesWithNormalizedPosition, getNodeViewTab } from '@/utils/nodeViewUtils';
import CanvasStopCurrentExecutionButton from '@/components/canvas/elements/buttons/CanvasStopCurrentExecutionButton.vue'; import CanvasStopCurrentExecutionButton from '@/components/canvas/elements/buttons/CanvasStopCurrentExecutionButton.vue';
import CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.vue'; import CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.vue';
import CanvasClearExecutionDataButton from '@/components/canvas/elements/buttons/CanvasClearExecutionDataButton.vue'; import CanvasClearExecutionDataButton from '@/components/canvas/elements/buttons/CanvasClearExecutionDataButton.vue';
@ -108,7 +108,7 @@ import { useClipboard } from '@/composables/useClipboard';
import { useBeforeUnload } from '@/composables/useBeforeUnload'; import { useBeforeUnload } from '@/composables/useBeforeUnload';
import { getResourcePermissions } from '@/permissions'; import { getResourcePermissions } from '@/permissions';
import NodeViewUnfinishedWorkflowMessage from '@/components/NodeViewUnfinishedWorkflowMessage.vue'; import NodeViewUnfinishedWorkflowMessage from '@/components/NodeViewUnfinishedWorkflowMessage.vue';
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; import { createCanvasConnectionHandleString } from '@/utils/canvasUtils';
import { isValidNodeConnectionType } from '@/utils/typeGuards'; import { isValidNodeConnectionType } from '@/utils/typeGuards';
import { getEasyAiWorkflowJson } from '@/utils/easyAiWorkflowUtils'; import { getEasyAiWorkflowJson } from '@/utils/easyAiWorkflowUtils';
@ -910,7 +910,7 @@ async function importWorkflowExact({ workflow: workflowData }: { workflow: IWork
initializeWorkspace({ initializeWorkspace({
...workflowData, ...workflowData,
nodes: getFixedNodesList<INodeUi>(workflowData.nodes), nodes: getNodesWithNormalizedPosition<INodeUi>(workflowData.nodes),
} as IWorkflowDb); } as IWorkflowDb);
fitView(); fitView();