From dd43854a8e177b231f3f0a6dbf2b0650012a81d5 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Mon, 24 Feb 2025 10:05:15 +0200 Subject: [PATCH] refactor: Document and rename canvasUtils (no-changelog) (#13423) --- .../elements/edges/CanvasConnectionLine.vue | 2 +- .../canvas/elements/nodes/CanvasNode.vue | 2 +- .../src/composables/useCanvasMapping.test.ts | 5 +- .../src/composables/useCanvasMapping.ts | 2 +- .../composables/useCanvasOperations.test.ts | 2 +- .../src/composables/useCanvasOperations.ts | 2 +- .../composables/useNodeConnections.test.ts | 2 +- .../src/composables/useNodeConnections.ts | 2 +- .../src/stores/nodeCreator.store.test.ts | 4 +- .../editor-ui/src/stores/nodeCreator.store.ts | 2 +- .../editor-ui/src/stores/templates.store.ts | 6 ++- ...vasUtilsV2.test.ts => canvasUtils.test.ts} | 2 +- .../{canvasUtilsV2.ts => canvasUtils.ts} | 30 ++++++++++++ packages/editor-ui/src/utils/nodeViewUtils.ts | 46 ++++++++++++++++--- .../src/utils/templates/templateActions.ts | 4 +- packages/editor-ui/src/views/NodeView.vue | 6 +-- 16 files changed, 90 insertions(+), 29 deletions(-) rename packages/editor-ui/src/utils/{canvasUtilsV2.test.ts => canvasUtils.test.ts} (99%) rename packages/editor-ui/src/utils/{canvasUtilsV2.ts => canvasUtils.ts} (91%) diff --git a/packages/editor-ui/src/components/canvas/elements/edges/CanvasConnectionLine.vue b/packages/editor-ui/src/components/canvas/elements/edges/CanvasConnectionLine.vue index e4833cb4c2..69018e6540 100644 --- a/packages/editor-ui/src/components/canvas/elements/edges/CanvasConnectionLine.vue +++ b/packages/editor-ui/src/components/canvas/elements/edges/CanvasConnectionLine.vue @@ -6,7 +6,7 @@ import { computed, onMounted, ref, useCssModule } from 'vue'; import { getEdgeRenderData } from './utils'; import { useCanvas } from '@/composables/useCanvas'; import { NodeConnectionType } from 'n8n-workflow'; -import { parseCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; +import { parseCanvasConnectionHandleString } from '@/utils/canvasUtils'; const props = defineProps(); diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue index 3c88db08f0..feaec44b3f 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue @@ -31,7 +31,7 @@ import { useCanvas } from '@/composables/useCanvas'; import { createCanvasConnectionHandleString, insertSpacersBetweenEndpoints, -} from '@/utils/canvasUtilsV2'; +} from '@/utils/canvasUtils'; import type { EventBus } from 'n8n-design-system'; import { createEventBus } from 'n8n-design-system'; import { isEqual } from 'lodash-es'; diff --git a/packages/editor-ui/src/composables/useCanvasMapping.test.ts b/packages/editor-ui/src/composables/useCanvasMapping.test.ts index 49ea69506f..8556708d82 100644 --- a/packages/editor-ui/src/composables/useCanvasMapping.test.ts +++ b/packages/editor-ui/src/composables/useCanvasMapping.test.ts @@ -15,10 +15,7 @@ import { } from '@/__tests__/mocks'; import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE, STICKY_NODE_TYPE, STORES } from '@/constants'; import { useWorkflowsStore } from '@/stores/workflows.store'; -import { - createCanvasConnectionHandleString, - createCanvasConnectionId, -} from '@/utils/canvasUtilsV2'; +import { createCanvasConnectionHandleString, createCanvasConnectionId } from '@/utils/canvasUtils'; import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types'; import { MarkerType } from '@vue-flow/core'; import { createTestingPinia } from '@pinia/testing'; diff --git a/packages/editor-ui/src/composables/useCanvasMapping.ts b/packages/editor-ui/src/composables/useCanvasMapping.ts index 087a18a662..dbdcc31ed0 100644 --- a/packages/editor-ui/src/composables/useCanvasMapping.ts +++ b/packages/editor-ui/src/composables/useCanvasMapping.ts @@ -27,7 +27,7 @@ import { mapLegacyConnectionsToCanvasConnections, mapLegacyEndpointsToCanvasConnectionPort, parseCanvasConnectionHandleString, -} from '@/utils/canvasUtilsV2'; +} from '@/utils/canvasUtils'; import type { ExecutionStatus, ExecutionSummary, diff --git a/packages/editor-ui/src/composables/useCanvasOperations.test.ts b/packages/editor-ui/src/composables/useCanvasOperations.test.ts index b0c054fff4..a93d14d7cb 100644 --- a/packages/editor-ui/src/composables/useCanvasOperations.test.ts +++ b/packages/editor-ui/src/composables/useCanvasOperations.test.ts @@ -48,7 +48,7 @@ import { } from '@/constants'; import type { Connection } from '@vue-flow/core'; import { useClipboard } from '@/composables/useClipboard'; -import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; +import { createCanvasConnectionHandleString } from '@/utils/canvasUtils'; import { nextTick } from 'vue'; import { useProjectsStore } from '@/stores/projects.store'; diff --git a/packages/editor-ui/src/composables/useCanvasOperations.ts b/packages/editor-ui/src/composables/useCanvasOperations.ts index 66b33ebc6b..3f898b01ab 100644 --- a/packages/editor-ui/src/composables/useCanvasOperations.ts +++ b/packages/editor-ui/src/composables/useCanvasOperations.ts @@ -65,7 +65,7 @@ import { mapLegacyConnectionsToCanvasConnections, mapLegacyConnectionToCanvasConnection, parseCanvasConnectionHandleString, -} from '@/utils/canvasUtilsV2'; +} from '@/utils/canvasUtils'; import * as NodeViewUtils from '@/utils/nodeViewUtils'; import { CONFIGURABLE_NODE_SIZE, diff --git a/packages/editor-ui/src/composables/useNodeConnections.test.ts b/packages/editor-ui/src/composables/useNodeConnections.test.ts index fa532d6cd8..2a714ae945 100644 --- a/packages/editor-ui/src/composables/useNodeConnections.test.ts +++ b/packages/editor-ui/src/composables/useNodeConnections.test.ts @@ -3,7 +3,7 @@ import { NodeConnectionType } from 'n8n-workflow'; import { useNodeConnections } from '@/composables/useNodeConnections'; import type { CanvasNodeData } from '@/types'; import { CanvasConnectionMode } from '@/types'; -import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; +import { createCanvasConnectionHandleString } from '@/utils/canvasUtils'; describe('useNodeConnections', () => { const defaultConnections = { diff --git a/packages/editor-ui/src/composables/useNodeConnections.ts b/packages/editor-ui/src/composables/useNodeConnections.ts index ffbedad6f9..6aede40bcc 100644 --- a/packages/editor-ui/src/composables/useNodeConnections.ts +++ b/packages/editor-ui/src/composables/useNodeConnections.ts @@ -4,7 +4,7 @@ import type { MaybeRef } from 'vue'; import { computed, unref } from 'vue'; import { NodeConnectionType } from 'n8n-workflow'; import type { Connection } from '@vue-flow/core'; -import { parseCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; +import { parseCanvasConnectionHandleString } from '@/utils/canvasUtils'; export function useNodeConnections({ inputs, diff --git a/packages/editor-ui/src/stores/nodeCreator.store.test.ts b/packages/editor-ui/src/stores/nodeCreator.store.test.ts index 27ed48bf19..dd2dd54765 100644 --- a/packages/editor-ui/src/stores/nodeCreator.store.test.ts +++ b/packages/editor-ui/src/stores/nodeCreator.store.test.ts @@ -7,7 +7,7 @@ import { REGULAR_NODE_CREATOR_VIEW, } from '@/constants'; import type { INodeCreateElement } from '@/Interface'; -import { parseCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; +import { parseCanvasConnectionHandleString } from '@/utils/canvasUtils'; import { NodeConnectionType } from 'n8n-workflow'; import { CanvasConnectionMode } from '@/types'; @@ -56,7 +56,7 @@ vi.mock('@/stores/workflows.store', () => { }; }); -vi.mock('@/utils/canvasUtilsV2', () => { +vi.mock('@/utils/canvasUtils', () => { return { parseCanvasConnectionHandleString: vi.fn(), }; diff --git a/packages/editor-ui/src/stores/nodeCreator.store.ts b/packages/editor-ui/src/stores/nodeCreator.store.ts index 9137a63505..d1bc1d5d22 100644 --- a/packages/editor-ui/src/stores/nodeCreator.store.ts +++ b/packages/editor-ui/src/stores/nodeCreator.store.ts @@ -31,7 +31,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { createCanvasConnectionHandleString, parseCanvasConnectionHandleString, -} from '@/utils/canvasUtilsV2'; +} from '@/utils/canvasUtils'; import type { Connection } from '@vue-flow/core'; import { CanvasConnectionMode } from '@/types'; import { isVueFlowConnection } from '@/utils/typeGuards'; diff --git a/packages/editor-ui/src/stores/templates.store.ts b/packages/editor-ui/src/stores/templates.store.ts index be00083d99..afb14b9aee 100644 --- a/packages/editor-ui/src/stores/templates.store.ts +++ b/packages/editor-ui/src/stores/templates.store.ts @@ -12,7 +12,7 @@ import type { } from '@/Interface'; import { useSettingsStore } from './settings.store'; import * as templatesApi from '@/api/templates'; -import { getFixedNodesList } from '@/utils/nodeViewUtils'; +import { getNodesWithNormalizedPosition } from '@/utils/nodeViewUtils'; import { useRootStore } from '@/stores/root.store'; import { useUsersStore } from './users.store'; import { useWorkflowsStore } from './workflows.store'; @@ -399,7 +399,9 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, () => { ): Promise => { const template = await getWorkflowTemplate(templateId); 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) => { if (node.credentials) { delete node.credentials; diff --git a/packages/editor-ui/src/utils/canvasUtilsV2.test.ts b/packages/editor-ui/src/utils/canvasUtils.test.ts similarity index 99% rename from packages/editor-ui/src/utils/canvasUtilsV2.test.ts rename to packages/editor-ui/src/utils/canvasUtils.test.ts index 63c7321808..ba1a9a37a8 100644 --- a/packages/editor-ui/src/utils/canvasUtilsV2.test.ts +++ b/packages/editor-ui/src/utils/canvasUtils.test.ts @@ -7,7 +7,7 @@ import { mapLegacyConnectionsToCanvasConnections, mapLegacyEndpointsToCanvasConnectionPort, parseCanvasConnectionHandleString, -} from '@/utils/canvasUtilsV2'; +} from '@/utils/canvasUtils'; import type { IConnection, IConnections, INodeTypeDescription } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow'; import type { CanvasConnection } from '@/types'; diff --git a/packages/editor-ui/src/utils/canvasUtilsV2.ts b/packages/editor-ui/src/utils/canvasUtils.ts similarity index 91% rename from packages/editor-ui/src/utils/canvasUtilsV2.ts rename to packages/editor-ui/src/utils/canvasUtils.ts index f8e412caea..756e913cd3 100644 --- a/packages/editor-ui/src/utils/canvasUtilsV2.ts +++ b/packages/editor-ui/src/utils/canvasUtils.ts @@ -6,6 +6,9 @@ import type { Connection } from '@vue-flow/core'; import { isValidCanvasConnectionMode, isValidNodeConnectionType } from '@/utils/typeGuards'; import { NodeConnectionType } from 'n8n-workflow'; +/** + * Maps multiple legacy n8n connections to VueFlow connections + */ export function mapLegacyConnectionsToCanvasConnections( legacyConnections: IConnections, nodes: INodeUi[], @@ -75,6 +78,9 @@ export function mapLegacyConnectionsToCanvasConnections( return mappedConnections; } +/** + * Maps a single legacy n8n connection to a VueFlow connection + */ export function mapLegacyConnectionToCanvasConnection( sourceNode: 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) { 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({ mode, type = NodeConnectionType.Main, @@ -130,10 +145,16 @@ export function createCanvasConnectionHandleString({ return `${mode}/${type}/${index}`; } +/** + * Creates a canvas connection ID from a connection + */ export function createCanvasConnectionId(connection: Connection) { return `[${connection.source}/${connection.sourceHandle}][${connection.target}/${connection.targetHandle}]`; } +/** + * Maps a VueFlow connection to a legacy n8n connection + */ export function mapCanvasConnectionToLegacyConnection( sourceNode: INodeUi, targetNode: INodeUi, @@ -165,6 +186,9 @@ export function mapCanvasConnectionToLegacyConnection( ]; } +/** + * Maps legacy n8n node inputs to VueFlow connection handles + */ export function mapLegacyEndpointsToCanvasConnectionPort( endpoints: INodeTypeDescription['inputs'], endpointNames: string[] = [], @@ -196,6 +220,9 @@ export function mapLegacyEndpointsToCanvasConnectionPort( }); } +/** + * Checks if two bounding boxes overlap + */ export function checkOverlap(node1: BoundingBox, node2: BoundingBox) { return !( // 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( endpoints: T[], requiredEndpointsCount = 0, diff --git a/packages/editor-ui/src/utils/nodeViewUtils.ts b/packages/editor-ui/src/utils/nodeViewUtils.ts index 80ec02b15d..adebd1c08c 100644 --- a/packages/editor-ui/src/utils/nodeViewUtils.ts +++ b/packages/editor-ui/src/utils/nodeViewUtils.ts @@ -34,6 +34,9 @@ export const HEADER_HEIGHT = 65; export const MAX_X_TO_PUSH_DOWNSTREAM_NODES = 300; 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 = (nodes: T[]): T => { return nodes.reduce((leftmostTop, node) => { if (node.position[0] > leftmostTop.position[0] || node.position[1] > leftmostTop.position[1]) { @@ -44,6 +47,9 @@ export const getLeftmostTopNode = (nodes: T[ }, nodes[0]); }; +/** + * Checks if the given position is available for a new node + */ const canUsePosition = (position1: XYPosition, position2: XYPosition) => { if (Math.abs(position1[0] - position2[0]) <= 100) { if (Math.abs(position1[1] - position2[1]) <= 50) { @@ -54,6 +60,9 @@ const canUsePosition = (position1: XYPosition, position2: XYPosition) => { return true; }; +/** + * Returns the closest number divisible by the given number + */ const closestNumberDivisibleBy = (inputNumber: number, divisibleBy: number): number => { const quotient = Math.ceil(inputNumber / divisibleBy); @@ -73,6 +82,9 @@ const closestNumberDivisibleBy = (inputNumber: number, divisibleBy: number): num return inputNumber2; }; +/** + * Returns the new position for a node based on the given position and the nodes in the workflow + */ export const getNewNodePosition = ( nodes: INodeUi[], newPosition: XYPosition, @@ -113,6 +125,9 @@ export const getNewNodePosition = ( return targetPosition; }; +/** + * Returns the position of a mouse or touch event + */ export const getMousePosition = (e: MouseEvent | TouchEvent): XYPosition => { // @ts-ignore 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]; }; +/** + * Returns the relative position of a point on the canvas + */ export const getRelativePosition = ( x: number, y: number, @@ -131,12 +149,9 @@ export const getRelativePosition = ( return [(x - offset[0]) / scale, (y - offset[1]) / scale]; }; -export const getMidCanvasPosition = (scale: number, offset: XYPosition): XYPosition => { - const { editorWidth, editorHeight } = getContentDimensions(); - - return getRelativePosition(editorWidth / 2, (editorHeight - HEADER_HEIGHT) / 2, scale, offset); -}; - +/** + * Returns the width and height of the node view content + */ const getContentDimensions = (): { editorWidth: number; editorHeight: number } => { let contentWidth = window.innerWidth; let contentHeight = window.innerHeight; @@ -153,7 +168,21 @@ const getContentDimensions = (): { editorWidth: number; editorHeight: number } = }; }; -export const getFixedNodesList = (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 = ( + workflowNodes: T[], +): T[] => { const nodes = [...workflowNodes]; if (nodes.length) { @@ -229,6 +258,9 @@ export function isElementIntersection( return isWithinVerticalBounds && isWithinHorizontalBounds; } +/** + * Returns the node hints based on the node type and execution data + */ export function getGenericHints({ workflowNode, node, diff --git a/packages/editor-ui/src/utils/templates/templateActions.ts b/packages/editor-ui/src/utils/templates/templateActions.ts index 27e22c0329..c2c5c0e057 100644 --- a/packages/editor-ui/src/utils/templates/templateActions.ts +++ b/packages/editor-ui/src/utils/templates/templateActions.ts @@ -9,7 +9,7 @@ import { TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, VIEWS } from '@/constants'; import type { useRootStore } from '@/stores/root.store'; import type { PosthogStore } from '@/stores/posthog.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 { TemplateCredentialKey } from '@/utils/templates/templateTransforms'; import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms'; @@ -43,7 +43,7 @@ export async function createWorkflowFromTemplate(opts: { template.workflow.nodes, credentialOverrides, ); - const nodes = getFixedNodesList(nodesWithCreds) as INodeUi[]; + const nodes = getNodesWithNormalizedPosition(nodesWithCreds) as INodeUi[]; const connections = template.workflow.connections; const workflowToCreate: IWorkflowData = { diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index b321ccac23..cd973c5cef 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -95,7 +95,7 @@ import { sourceControlEventBus } from '@/event-bus/source-control'; import { useTagsStore } from '@/stores/tags.store'; import { usePushConnectionStore } from '@/stores/pushConnection.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 CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.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 { getResourcePermissions } from '@/permissions'; import NodeViewUnfinishedWorkflowMessage from '@/components/NodeViewUnfinishedWorkflowMessage.vue'; -import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; +import { createCanvasConnectionHandleString } from '@/utils/canvasUtils'; import { isValidNodeConnectionType } from '@/utils/typeGuards'; import { getEasyAiWorkflowJson } from '@/utils/easyAiWorkflowUtils'; @@ -910,7 +910,7 @@ async function importWorkflowExact({ workflow: workflowData }: { workflow: IWork initializeWorkspace({ ...workflowData, - nodes: getFixedNodesList(workflowData.nodes), + nodes: getNodesWithNormalizedPosition(workflowData.nodes), } as IWorkflowDb); fitView();