diff --git a/cypress/e2e/10-undo-redo.cy.ts b/cypress/e2e/10-undo-redo.cy.ts index f87fee5b78..5df21afb57 100644 --- a/cypress/e2e/10-undo-redo.cy.ts +++ b/cypress/e2e/10-undo-redo.cy.ts @@ -218,6 +218,7 @@ describe('Undo/Redo', () => { WorkflowPage.getters.nodeConnections().should('have.length', 1); cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1); + cy.wait(1000); // Clipboard paste is throttled cy.fixture('Test_workflow_form_switch.json').then((data) => { cy.get('body').paste(JSON.stringify(data)); }); diff --git a/cypress/e2e/12-canvas.cy.ts b/cypress/e2e/12-canvas.cy.ts index 3035a61ca2..51ab50bccf 100644 --- a/cypress/e2e/12-canvas.cy.ts +++ b/cypress/e2e/12-canvas.cy.ts @@ -383,9 +383,12 @@ describe('Canvas Node Manipulation and Navigation', () => { WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); + WorkflowPage.getters.zoomOutButton().click(); + WorkflowPage.getters.zoomOutButton().click(); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); - // At this point last added node should be off-screen + WorkflowPage.getters.zoomInButton().click(); + WorkflowPage.getters.zoomInButton().click(); WorkflowPage.getters.canvasNodes().last().should('not.be.visible'); WorkflowPage.getters.zoomToFitButton().click(); WorkflowPage.getters.canvasNodes().last().should('be.visible'); diff --git a/packages/frontend/editor-ui/src/components/canvas/Canvas.vue b/packages/frontend/editor-ui/src/components/canvas/Canvas.vue index 44f89d44a2..e7f9f745a8 100644 --- a/packages/frontend/editor-ui/src/components/canvas/Canvas.vue +++ b/packages/frontend/editor-ui/src/components/canvas/Canvas.vue @@ -19,7 +19,7 @@ import type { CanvasNodeData, } from '@/types'; import { CanvasNodeRenderType } from '@/types'; -import { GRID_SIZE } from '@/utils/nodeViewUtils'; +import { getMousePosition, GRID_SIZE } from '@/utils/nodeViewUtils'; import { isPresent } from '@/utils/typesUtils'; import { useDeviceSupport } from '@n8n/composables/useDeviceSupport'; import { useShortKeyPress } from '@n8n/composables/useShortKeyPress'; @@ -27,9 +27,11 @@ import type { EventBus } from '@n8n/utils/event-bus'; import { createEventBus } from '@n8n/utils/event-bus'; import type { Connection, + Dimensions, GraphNode, NodeDragEvent, NodeMouseEvent, + ViewportTransform, XYPosition, } from '@vue-flow/core'; import { MarkerType, PanelPosition, useVueFlow, VueFlow } from '@vue-flow/core'; @@ -67,7 +69,7 @@ const emit = defineEmits<{ 'update:node:parameters': [id: string, parameters: Record]; 'update:node:inputs': [id: string]; 'update:node:outputs': [id: string]; - 'click:node': [id: string]; + 'click:node': [id: string, position: XYPosition]; 'click:node:add': [id: string, handle: string]; 'run:node': [id: string]; 'delete:node': [id: string]; @@ -95,6 +97,8 @@ const emit = defineEmits<{ 'create:workflow': []; 'drag-and-drop': [position: XYPosition, event: DragEvent]; 'tidy-up': [CanvasLayoutEvent]; + 'viewport:change': [viewport: ViewportTransform, dimensions: Dimensions]; + 'selection:end': [position: XYPosition]; 'open:sub-workflow': [nodeId: string]; }>(); @@ -143,6 +147,7 @@ const { onNodesInitialized, findNode, viewport, + dimensions, nodesSelectionActive, setViewport, onEdgeMouseLeave, @@ -351,7 +356,7 @@ function onNodeDragStop(event: NodeDragEvent) { } function onNodeClick({ event, node }: NodeMouseEvent) { - emit('click:node', node.id); + emit('click:node', node.id, getProjectedPosition(event)); if (event.ctrlKey || event.metaKey || selectedNodes.value.length < 2) { return; @@ -364,10 +369,12 @@ function onSelectionDragStop(event: NodeDragEvent) { onUpdateNodesPosition(event.nodes.map(({ id, position }) => ({ id, position }))); } -function onSelectionEnd() { +function onSelectionEnd(event: MouseEvent) { if (selectedNodes.value.length === 1) { nodesSelectionActive.value = false; } + + emit('selection:end', getProjectedPosition(event)); } function onSetNodeActivated(id: string, event?: MouseEvent) { @@ -543,10 +550,9 @@ const isPaneMoving = ref(false); useViewportAutoAdjust(viewportRef, viewport, setViewport); -function getProjectedPosition(event?: Pick) { +function getProjectedPosition(event?: MouseEvent | TouchEvent) { const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 }; - const offsetX = event?.clientX ?? 0; - const offsetY = event?.clientY ?? 0; + const [offsetX, offsetY] = event ? getMousePosition(event) : [0, 0]; return project({ x: offsetX - bounds.left, @@ -591,6 +597,10 @@ function onPaneMoveEnd() { isPaneMoving.value = false; } +function onViewportChange() { + emit('viewport:change', viewport.value, dimensions.value); +} + // #AI-716: Due to a bug in vue-flow reactivity, the node data is not updated when the node is added // resulting in outdated data. We use this computed property as a workaround to get the latest node data. const nodeDataById = computed(() => { @@ -828,6 +838,7 @@ provide(CanvasKey, { @selection-context-menu="onOpenSelectionContextMenu" @dragover="onDragOver" @drop="onDrop" + @viewport-change="onViewportChange" >