fix(editor): Fix editor pan / zoom when inserting nodes (#22401)

This commit is contained in:
Raúl Gómez Morales
2025-12-01 10:48:56 +01:00
committed by GitHub
parent 1ddb6344a5
commit f4354301ec
2 changed files with 80 additions and 14 deletions
@@ -727,7 +727,9 @@ async function onClipboardPaste(plainTextData: string): Promise<void> {
importTags: false,
viewport: viewportBoundaries.value,
});
selectNodes(result.nodes?.map((node) => node.id) ?? []);
const ids = result.nodes?.map((node) => node.id) ?? [];
canvasRef.value?.ensureNodesAreVisible(ids);
}
async function onCutNodes(ids: string[]) {
@@ -747,7 +749,7 @@ async function onDuplicateNodes(ids: string[]) {
viewport: viewportBoundaries.value,
});
selectNodes(newIds);
canvasRef.value?.ensureNodesAreVisible(newIds);
}
function onPinNodes(ids: string[], source: PinDataSource) {
@@ -1042,8 +1044,7 @@ async function onImportWorkflowUrlEvent(data: IDataObject) {
viewport: viewportBoundaries.value,
});
fitView();
selectNodes(workflowData.nodes?.map((node) => node.id) ?? []);
canvasRef.value?.ensureNodesAreVisible(workflowData.nodes?.map((node) => node.id) ?? []);
}
function addImportEventBindings() {
@@ -1,15 +1,15 @@
<script setup lang="ts">
import Canvas from './Canvas.vue';
import { computed, ref, toRef, useCssModule, useTemplateRef } from 'vue';
import type { Workflow } from 'n8n-workflow';
import type { ContextMenuAction } from '@/features/shared/contextMenu/composables/useContextMenuItems';
import type { IWorkflowDb } from '@/Interface';
import { useCanvasMapping } from '../composables/useCanvasMapping';
import type { EventBus } from '@n8n/utils/event-bus';
import { createEventBus } from '@n8n/utils/event-bus';
import type { CanvasEventBusEvents } from '../canvas.types';
import { useVueFlow } from '@vue-flow/core';
import { getRectOfNodes, useVueFlow } from '@vue-flow/core';
import { throttledRef } from '@vueuse/core';
import type { ContextMenuAction } from '@/features/shared/contextMenu/composables/useContextMenuItems';
import type { Workflow } from 'n8n-workflow';
import { computed, ref, toRef, useCssModule, useTemplateRef } from 'vue';
import type { CanvasEventBusEvents } from '../canvas.types';
import { useCanvasMapping } from '../composables/useCanvasMapping';
import Canvas from './Canvas.vue';
defineOptions({
inheritAttrs: false,
@@ -39,7 +39,7 @@ const props = withDefaults(
const canvasRef = useTemplateRef('canvas');
const $style = useCssModule();
const { onNodesInitialized } = useVueFlow(props.id);
const { onNodesInitialized, viewport, viewportRef, getNodes, fitBounds } = useVueFlow(props.id);
const workflow = toRef(props, 'workflow');
const workflowObject = toRef(props, 'workflowObject');
@@ -58,10 +58,11 @@ const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping(
});
const initialFitViewDone = ref(false); // Workaround for https://github.com/bcakmakoglu/vue-flow/issues/1636
onNodesInitialized(() => {
if (!initialFitViewDone.value || props.showFallbackNodes) {
const { off } = onNodesInitialized(() => {
if (!initialFitViewDone.value) {
props.eventBus.emit('fitView');
initialFitViewDone.value = true;
off();
}
});
@@ -71,6 +72,70 @@ const mappedConnectionsThrottled = throttledRef(mappedConnections, 200);
defineExpose({
executeContextMenuAction: (action: ContextMenuAction, nodeIds: string[]) =>
canvasRef.value?.executeContextMenuAction(action, nodeIds),
ensureNodesAreVisible: (ids: string[]) => {
const canvasElement = viewportRef.value;
if (!canvasElement) {
return;
}
// Find nodes by IDs
const targetNodes = getNodes.value.filter((node) => ids.includes(node.id));
if (targetNodes.length === 0) {
return;
}
const insertionDone = onNodesInitialized(() => {
// Get the current viewport after nodes are initialized
const vp = viewport.value;
const canvasWidth = canvasElement.clientWidth;
const canvasHeight = canvasElement.clientHeight;
// Get the rect of the newly added nodes
const nodesRect = getRectOfNodes(targetNodes);
// Check if nodes are visible in current viewport
const screenX = nodesRect.x * vp.zoom + vp.x;
const screenY = nodesRect.y * vp.zoom + vp.y;
const screenWidth = nodesRect.width * vp.zoom;
const screenHeight = nodesRect.height * vp.zoom;
const isFullyVisible =
screenX >= 0 &&
screenY >= 0 &&
screenX + screenWidth <= canvasWidth &&
screenY + screenHeight <= canvasHeight;
if (!isFullyVisible) {
// Calculate viewport bounds in canvas coordinates
const viewportRect = {
x: -vp.x / vp.zoom,
y: -vp.y / vp.zoom,
width: canvasWidth / vp.zoom,
height: canvasHeight / vp.zoom,
};
// Combine current viewport with nodes rect
const minX = Math.min(viewportRect.x, nodesRect.x);
const minY = Math.min(viewportRect.y, nodesRect.y);
const maxX = Math.max(viewportRect.x + viewportRect.width, nodesRect.x + nodesRect.width);
const maxY = Math.max(viewportRect.y + viewportRect.height, nodesRect.y + nodesRect.height);
const combinedRect = {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
void fitBounds(combinedRect, { padding: 0.15, duration: 100 });
}
props.eventBus.emit('nodes:select', { ids });
insertionDone.off();
});
},
});
</script>