feat(editor): Improve UX of embedded NDV experiment (no-changelog) (#17515)

This commit is contained in:
Suguru Inoue
2025-07-23 14:04:16 +02:00
committed by GitHub
parent 4b2be26379
commit 7330e2d0af
16 changed files with 223 additions and 89 deletions

View File

@@ -1569,6 +1569,8 @@
"nodeView.setupTemplate": "Set up template", "nodeView.setupTemplate": "Set up template",
"nodeView.expandAllNodes": "Expand all nodes", "nodeView.expandAllNodes": "Expand all nodes",
"nodeView.collapseAllNodes": "Collapse all nodes", "nodeView.collapseAllNodes": "Collapse all nodes",
"nodeView.enterZoomMode": "Enter zoom mode",
"nodeView.leaveZoomMode": "Leave zoom mode",
"nodeViewV2.showError.editingNotAllowed": "Editing is not allowed", "nodeViewV2.showError.editingNotAllowed": "Editing is not allowed",
"nodeViewV2.showError.failedToCreateNode": "Failed to create node", "nodeViewV2.showError.failedToCreateNode": "Failed to create node",
"contextMenu.node": "node | nodes", "contextMenu.node": "node | nodes",

View File

@@ -50,7 +50,7 @@
"@typescript/vfs": "^1.6.0", "@typescript/vfs": "^1.6.0",
"@vue-flow/background": "^1.3.2", "@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.2", "@vue-flow/controls": "^1.1.2",
"@vue-flow/core": "^1.42.1", "@vue-flow/core": "^1.45.0",
"@vue-flow/minimap": "^1.5.2", "@vue-flow/minimap": "^1.5.2",
"@vue-flow/node-resizer": "^1.4.0", "@vue-flow/node-resizer": "^1.4.0",
"@vueuse/components": "^10.11.0", "@vueuse/components": "^10.11.0",

View File

@@ -92,7 +92,10 @@ export function createCanvasGraphNode({
isParent: false, isParent: false,
selected: false, selected: false,
resizing: false, resizing: false,
handleBounds: {}, handleBounds: {
source: null,
target: null,
},
events: {}, events: {},
data: createCanvasNodeData({ id, type, ...data }), data: createCanvasNodeData({ id, type, ...data }),
...rest, ...rest,

View File

@@ -70,7 +70,6 @@ const props = withDefaults(
inputSize: number; inputSize: number;
activeNode?: INodeUi; activeNode?: INodeUi;
isEmbeddedInCanvas?: boolean; isEmbeddedInCanvas?: boolean;
noWheel?: boolean;
subTitle?: string; subTitle?: string;
}>(), }>(),
{ {
@@ -81,7 +80,6 @@ const props = withDefaults(
blockUI: false, blockUI: false,
activeNode: undefined, activeNode: undefined,
isEmbeddedInCanvas: false, isEmbeddedInCanvas: false,
noWheel: false,
subTitle: undefined, subTitle: undefined,
}, },
); );
@@ -98,6 +96,7 @@ const emit = defineEmits<{
]; ];
activate: []; activate: [];
execute: []; execute: [];
captureWheelBody: [WheelEvent];
}>(); }>();
const slots = defineSlots<{ actions?: {} }>(); const slots = defineSlots<{ actions?: {} }>();
@@ -814,12 +813,6 @@ function displayCredentials(credentialTypeDescription: INodeCredentialDescriptio
nodeHelpers.displayParameter(node.value.parameters, credentialTypeDescription, '', node.value) nodeHelpers.displayParameter(node.value.parameters, credentialTypeDescription, '', node.value)
); );
} }
function handleWheelEvent(event: WheelEvent) {
if (event.ctrlKey) {
event.preventDefault();
}
}
</script> </script>
<template> <template>
@@ -941,11 +934,10 @@ function handleWheelEvent(event: WheelEvent) {
:class="[ :class="[
'node-parameters-wrapper', 'node-parameters-wrapper',
shouldShowStaticScrollbar ? 'with-static-scrollbar' : '', shouldShowStaticScrollbar ? 'with-static-scrollbar' : '',
noWheel && shouldShowStaticScrollbar ? 'nowheel' : '',
{ 'ndv-v2': isNDVV2 }, { 'ndv-v2': isNDVV2 },
]" ]"
data-test-id="node-parameters" data-test-id="node-parameters"
@wheel="noWheel ? handleWheelEvent : undefined" @wheel.capture="emit('captureWheelBody', $event)"
> >
<N8nNotice <N8nNotice
v-if="hasForeignCredential && !isHomeProjectTeam" v-if="hasForeignCredential && !isHomeProjectTeam"
@@ -1148,7 +1140,7 @@ function handleWheelEvent(event: WheelEvent) {
} }
&.embedded .node-parameters-wrapper.with-static-scrollbar { &.embedded .node-parameters-wrapper.with-static-scrollbar {
padding: 0 var(--spacing-2xs) var(--spacing-xs) var(--spacing-xs); padding: 0 var(--spacing-4xs) var(--spacing-xs) var(--spacing-xs);
@supports not (selector(::-webkit-scrollbar)) { @supports not (selector(::-webkit-scrollbar)) {
scrollbar-width: thin; scrollbar-width: thin;

View File

@@ -160,6 +160,7 @@ const {
nodesSelectionActive, nodesSelectionActive,
userSelectionRect, userSelectionRect,
setViewport, setViewport,
setCenter,
onEdgeMouseLeave, onEdgeMouseLeave,
onEdgeMouseEnter, onEdgeMouseEnter,
onEdgeMouseMove, onEdgeMouseMove,
@@ -285,6 +286,18 @@ function selectUpstreamNodes(id: string) {
onSelectNodes({ ids: [...upstreamNodes.map((node) => node.id), id] }); onSelectNodes({ ids: [...upstreamNodes.map((node) => node.id), id] });
} }
function onToggleZoomMode() {
experimentalNdvStore.toggleZoomMode({
canvasViewport: viewport.value,
canvasDimensions: dimensions.value,
selectedNodes: selectedNodes.value,
setViewport,
fitView,
zoomTo,
setCenter,
});
}
const keyMap = computed(() => { const keyMap = computed(() => {
const readOnlyKeymap: KeyMap = { const readOnlyKeymap: KeyMap = {
ctrl_shift_o: emitWithLastSelectedNode((id) => emit('open:sub-workflow', id)), ctrl_shift_o: emitWithLastSelectedNode((id) => emit('open:sub-workflow', id)),
@@ -308,6 +321,7 @@ const keyMap = computed(() => {
l: () => emit('update:logs-open'), l: () => emit('update:logs-open'),
i: () => emit('update:logs:input-open'), i: () => emit('update:logs:input-open'),
o: () => emit('update:logs:output-open'), o: () => emit('update:logs:output-open'),
z: onToggleZoomMode,
}; };
if (props.readOnly) return readOnlyKeymap; if (props.readOnly) return readOnlyKeymap;
@@ -431,7 +445,7 @@ function onSelectNodes({ ids, panIntoView }: CanvasEventBusEvents['nodes:select'
const newViewport = updateViewportToContainNodes(viewport.value, dimensions.value, nodes, 100); const newViewport = updateViewportToContainNodes(viewport.value, dimensions.value, nodes, 100);
void setViewport(newViewport, { duration: 200 }); void setViewport(newViewport, { duration: 200, interpolate: 'linear' });
} }
} }
@@ -455,6 +469,18 @@ function onUpdateNodeOutputs(id: string) {
emit('update:node:outputs', id); emit('update:node:outputs', id);
} }
function onFocusNode(id: string) {
const node = vueFlow.nodeLookup.value.get(id);
if (node) {
experimentalNdvStore.focusNode(node, {
canvasViewport: viewport.value,
canvasDimensions: dimensions.value,
setCenter,
});
}
}
/** /**
* Connections / Edges * Connections / Edges
*/ */
@@ -896,7 +922,6 @@ provide(CanvasKey, {
:event-bus="eventBus" :event-bus="eventBus"
:hovered="nodesHoveredById[nodeProps.id]" :hovered="nodesHoveredById[nodeProps.id]"
:nearby-hovered="nodeProps.id === hoveredTriggerNode.id.value" :nearby-hovered="nodeProps.id === hoveredTriggerNode.id.value"
:is-experimental-ndv-active="isExperimentalNdvActive"
@delete="onDeleteNode" @delete="onDeleteNode"
@run="onRunNode" @run="onRunNode"
@select="onSelectNode" @select="onSelectNode"
@@ -909,6 +934,7 @@ provide(CanvasKey, {
@update:outputs="onUpdateNodeOutputs" @update:outputs="onUpdateNodeOutputs"
@move="onUpdateNodePosition" @move="onUpdateNodePosition"
@add="onClickNodeAdd" @add="onClickNodeAdd"
@focus="onFocusNode"
> >
<template v-if="$slots.nodeToolbar" #toolbar="toolbarProps"> <template v-if="$slots.nodeToolbar" #toolbar="toolbarProps">
<slot name="nodeToolbar" v-bind="toolbarProps" /> <slot name="nodeToolbar" v-bind="toolbarProps" />
@@ -972,6 +998,7 @@ provide(CanvasKey, {
@zoom-out="onZoomOut" @zoom-out="onZoomOut"
@reset-zoom="onResetZoom" @reset-zoom="onResetZoom"
@tidy-up="onTidyUp({ source: 'canvas-button' })" @tidy-up="onTidyUp({ source: 'canvas-button' })"
@toggle-zoom-mode="onToggleZoomMode"
/> />
<Suspense> <Suspense>
@@ -1004,7 +1031,7 @@ provide(CanvasKey, {
} }
&.isExperimentalNdvActive { &.isExperimentalNdvActive {
--canvas-zoom-compensation-factor: 0.67; --canvas-zoom-compensation-factor: 0.5;
} }
} }
</style> </style>

View File

@@ -5,12 +5,12 @@ import { useI18n } from '@n8n/i18n';
import { Controls } from '@vue-flow/controls'; import { Controls } from '@vue-flow/controls';
import { computed } from 'vue'; import { computed } from 'vue';
import { useExperimentalNdvStore } from '../../experimental/experimentalNdv.store'; import { useExperimentalNdvStore } from '../../experimental/experimentalNdv.store';
import { N8nIconButton } from '@n8n/design-system';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
zoom?: number; zoom?: number;
readOnly?: boolean; readOnly?: boolean;
isExperimentalNdvActive: boolean;
}>(), }>(),
{ {
zoom: 1, zoom: 1,
@@ -24,13 +24,18 @@ const emit = defineEmits<{
'zoom-out': []; 'zoom-out': [];
'zoom-to-fit': []; 'zoom-to-fit': [];
'tidy-up': []; 'tidy-up': [];
'toggle-zoom-mode': [];
}>(); }>();
const i18n = useI18n(); const i18n = useI18n();
const experimentalNdvStore = useExperimentalNdvStore(); const experimentalNdvStore = useExperimentalNdvStore();
const isResetZoomVisible = computed(() => props.zoom !== 1); const isExperimentalNdvActive = computed(() => experimentalNdvStore.isActive(props.zoom));
const isToggleZoomVisible = computed(() => experimentalNdvStore.isEnabled);
const isResetZoomVisible = computed(() => !isToggleZoomVisible.value && props.zoom !== 1);
function onResetZoom() { function onResetZoom() {
emit('reset-zoom'); emit('reset-zoom');
@@ -84,6 +89,22 @@ function onTidyUp() {
@click="onZoomOut" @click="onZoomOut"
/> />
</KeyboardShortcutTooltip> </KeyboardShortcutTooltip>
<KeyboardShortcutTooltip
v-if="isToggleZoomVisible"
:label="
i18n.baseText(isExperimentalNdvActive ? 'nodeView.leaveZoomMode' : 'nodeView.enterZoomMode')
"
:shortcut="{ keys: ['Z'] }"
>
<N8nIconButton
square
type="tertiary"
size="large"
:class="$style.iconButton"
:icon="isExperimentalNdvActive ? 'undo-2' : 'crosshair'"
@click="emit('toggle-zoom-mode')"
/>
</KeyboardShortcutTooltip>
<KeyboardShortcutTooltip <KeyboardShortcutTooltip
v-if="isResetZoomVisible" v-if="isResetZoomVisible"
:label="i18n.baseText('nodeView.resetZoom')" :label="i18n.baseText('nodeView.resetZoom')"

View File

@@ -18,6 +18,7 @@ exports[`CanvasControlButtons > should render correctly 1`] = `
<!--teleport start--> <!--teleport start-->
<!--teleport end--> <!--teleport end-->
<!--v-if--> <!--v-if-->
<!--v-if-->
<n8n-button-stub block="false" element="button" label="" square="true" active="false" disabled="false" loading="false" outline="false" size="large" text="false" type="tertiary" data-test-id="tidy-up-button" class="iconButton el-tooltip__trigger el-tooltip__trigger"></n8n-button-stub> <n8n-button-stub block="false" element="button" label="" square="true" active="false" disabled="false" loading="false" outline="false" size="large" text="false" type="tertiary" data-test-id="tidy-up-button" class="iconButton el-tooltip__trigger el-tooltip__trigger"></n8n-button-stub>
<!--teleport start--> <!--teleport start-->
<!--teleport end--> <!--teleport end-->

View File

@@ -64,6 +64,7 @@ const emit = defineEmits<{
'update:inputs': [id: string]; 'update:inputs': [id: string];
'update:outputs': [id: string]; 'update:outputs': [id: string];
move: [id: string, position: XYPosition]; move: [id: string, position: XYPosition];
focus: [id: string];
}>(); }>();
const style = useCssModule(); const style = useCssModule();
@@ -270,6 +271,10 @@ function onMove(position: XYPosition) {
emit('move', props.id, position); emit('move', props.id, position);
} }
function onFocus(id: string) {
emit('focus', id);
}
function onUpdateClass({ className, add = true }: CanvasNodeEventBusEvents['update:node:class']) { function onUpdateClass({ className, add = true }: CanvasNodeEventBusEvents['update:node:class']) {
nodeClasses.value = add nodeClasses.value = add
? [...new Set([...nodeClasses.value, className])] ? [...new Set([...nodeClasses.value, className])]
@@ -393,6 +398,7 @@ onBeforeUnmount(() => {
@run="onRun" @run="onRun"
@update="onUpdate" @update="onUpdate"
@open:contextmenu="onOpenContextMenuFromToolbar" @open:contextmenu="onOpenContextMenuFromToolbar"
@focus="onFocus"
/> />
<CanvasNodeRenderer <CanvasNodeRenderer

View File

@@ -14,6 +14,7 @@ const emit = defineEmits<{
run: []; run: [];
update: [parameters: Record<string, unknown>]; update: [parameters: Record<string, unknown>];
'open:contextmenu': [event: MouseEvent]; 'open:contextmenu': [event: MouseEvent];
focus: [id: string];
}>(); }>();
const props = defineProps<{ const props = defineProps<{
@@ -105,7 +106,7 @@ function onMouseLeave() {
function onFocusNode() { function onFocusNode() {
if (node.value) { if (node.value) {
experimentalNdvStore.focusNode(node.value.id); emit('focus', node.value.id);
} }
} }
</script> </script>
@@ -189,6 +190,7 @@ function onFocusNode() {
&.isExperimentalNdvActive { &.isExperimentalNdvActive {
justify-content: center; justify-content: center;
padding-bottom: var(--spacing-3xs);
} }
} }

View File

@@ -6,9 +6,8 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { createEventBus } from '@n8n/utils/event-bus'; import { createEventBus } from '@n8n/utils/event-bus';
import { computed } from 'vue'; import { computed } from 'vue';
const { nodeId, noWheel, isReadOnly, subTitle } = defineProps<{ const { nodeId, isReadOnly, subTitle } = defineProps<{
nodeId: string; nodeId: string;
noWheel?: boolean;
isReadOnly?: boolean; isReadOnly?: boolean;
subTitle?: string; subTitle?: string;
}>(); }>();
@@ -26,6 +25,29 @@ function handleValueChanged(parameterData: IUpdateInformation) {
void renameNode(parameterData.oldValue as string, parameterData.value as string); void renameNode(parameterData.oldValue as string, parameterData.value as string);
} }
} }
function handleCaptureWheelEvent(event: WheelEvent) {
if (event.ctrlKey) {
// If the event is pinch, let it propagate and zoom canvas
return;
}
if (
event.currentTarget instanceof HTMLElement &&
event.currentTarget.scrollHeight <= event.currentTarget.offsetHeight
) {
// If the settings pane doesn't have to scroll, let it propagate and move the canvas
return;
}
// If the event has larger horizontal element, let it propagate and move the canvas
if (Math.abs(event.deltaX) >= Math.abs(event.deltaY)) {
return;
}
// Otherwise, let it scroll the settings pane
event.stopImmediatePropagation();
}
</script> </script>
<template> <template>
@@ -40,9 +62,9 @@ function handleValueChanged(parameterData: IUpdateInformation) {
:executable="false" :executable="false"
:input-size="0" :input-size="0"
is-embedded-in-canvas is-embedded-in-canvas
:no-wheel="noWheel"
:sub-title="subTitle" :sub-title="subTitle"
@value-changed="handleValueChanged" @value-changed="handleValueChanged"
@capture-wheel-body="handleCaptureWheelEvent"
> >
<template #actions> <template #actions>
<slot name="actions" /> <slot name="actions" />

View File

@@ -75,6 +75,9 @@ onBeforeUnmount(() => {
:width="360" :width="360"
:offset="8" :offset="8"
:append-to="vf.viewportRef?.value" :append-to="vf.viewportRef?.value"
:popper-options="{
modifiers: [{ name: 'flip', enabled: false }],
}"
> >
<template #reference> <template #reference>
<slot /> <slot />

View File

@@ -58,8 +58,8 @@ const isVisible = computed(() =>
{ {
x: -vf.viewport.value.x / vf.viewport.value.zoom, x: -vf.viewport.value.x / vf.viewport.value.zoom,
y: -vf.viewport.value.y / vf.viewport.value.zoom, y: -vf.viewport.value.y / vf.viewport.value.zoom,
width: vf.viewportRef.value?.offsetWidth ?? 0, width: vf.dimensions.value.width,
height: vf.viewportRef.value?.offsetHeight ?? 0, height: vf.dimensions.value.height,
}, },
), ),
); );
@@ -148,6 +148,8 @@ watchOnce(isVisible, (visible) => {
:style="{ :style="{
'--zoom': `${1 / experimentalNdvStore.maxCanvasZoom}`, '--zoom': `${1 / experimentalNdvStore.maxCanvasZoom}`,
'--node-width-scaler': isConfigurable ? 1 : 1.5, '--node-width-scaler': isConfigurable ? 1 : 1.5,
'--max-height-on-focus': `${(vf.dimensions.value.height * 0.8) / experimentalNdvStore.maxCanvasZoom}px`,
pointerEvents: isMoving ? 'none' : 'auto', // Don't interrupt canvas panning
}" }"
> >
<template v-if="!node || !isOnceVisible" /> <template v-if="!node || !isOnceVisible" />
@@ -162,9 +164,6 @@ watchOnce(isVisible, (visible) => {
tabindex="-1" tabindex="-1"
:node-id="nodeId" :node-id="nodeId"
:class="$style.settingsView" :class="$style.settingsView"
:no-wheel="
!isMoving /* to not interrupt panning while allowing scroll of the settings pane, allow wheel event while panning */
"
:is-read-only="isReadOnly" :is-read-only="isReadOnly"
:sub-title="subTitle" :sub-title="subTitle"
> >
@@ -197,23 +196,31 @@ watchOnce(isVisible, (visible) => {
</template> </template>
<style lang="scss" module> <style lang="scss" module>
:root .component { .component {
position: relative;
align-items: flex-start; align-items: flex-start;
justify-content: stretch; justify-content: stretch;
border-width: 1px !important; border-width: 1px !important;
border-radius: var(--border-radius-base) !important; border-radius: var(--border-radius-base) !important;
width: calc(var(--canvas-node--width) * var(--node-width-scaler)); width: calc(var(--canvas-node--width) * var(--node-width-scaler)) !important;
overflow: hidden; overflow: hidden;
--canvas-node--border-color: var(--color-text-lighter); --canvas-node--border-color: var(--color-text-lighter);
--expanded-max-height: min(
calc(var(--canvas-node--height) * 2),
var(--max-height-on-focus),
300px
);
&.expanded { &.expanded {
user-select: text; user-select: text;
cursor: auto; cursor: auto;
height: auto; height: auto;
max-height: min(calc(var(--canvas-node--height) * 2), 300px); max-height: var(--expanded-max-height);
min-height: var(--spacing-3xl); min-height: var(--spacing-3xl);
:global(.selected) & {
max-height: var(--max-height-on-focus);
}
} }
&.collapsed { &.collapsed {
overflow: hidden; overflow: hidden;
@@ -229,14 +236,18 @@ watchOnce(isVisible, (visible) => {
} }
} }
:root .collapsedContent, .collapsedContent,
:root .settingsView { .settingsView {
z-index: 1000; z-index: 1000;
width: 100%; width: 100%;
height: auto; height: auto;
max-height: calc(min(calc(var(--canvas-node--height) * 2), 300px) - var(--border-width-base) * 2); max-height: calc(var(--expanded-max-height) - var(--border-width-base) * 2);
min-height: var(--spacing-2xl); // should be multiple of GRID_SIZE min-height: var(--spacing-2xl); // should be multiple of GRID_SIZE
:global(.selected) & {
max-height: calc(var(--max-height-on-focus) - var(--border-width-base) * 2);
}
} }
.collapsedContent { .collapsedContent {

View File

@@ -1,9 +1,17 @@
import { computed, shallowRef } from 'vue'; import { computed, ref, shallowRef } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useVueFlow } from '@vue-flow/core'; import {
import { calculateNodeSize } from '@/utils/nodeViewUtils'; type Dimensions,
type FitView,
type GraphNode,
type SetCenter,
type SetViewport,
type ViewportTransform,
type ZoomTo,
} from '@vue-flow/core';
import { CanvasNodeRenderType, type CanvasNodeData } from '@/types';
export const useExperimentalNdvStore = defineStore('experimentalNdv', () => { export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
const workflowStore = useWorkflowsStore(); const workflowStore = useWorkflowsStore();
@@ -17,6 +25,7 @@ export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
isEnabled.value ? settingsStore.experimental__minZoomNodeSettingsInCanvas : 4, isEnabled.value ? settingsStore.experimental__minZoomNodeSettingsInCanvas : 4,
); );
const previousViewport = ref<ViewportTransform>();
const collapsedNodes = shallowRef<Partial<Record<string, boolean>>>({}); const collapsedNodes = shallowRef<Partial<Record<string, boolean>>>({});
function setNodeExpanded(nodeId: string, isExpanded?: boolean) { function setNodeExpanded(nodeId: string, isExpanded?: boolean) {
@@ -41,51 +50,89 @@ export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
} }
function isActive(canvasZoom: number) { function isActive(canvasZoom: number) {
return isEnabled.value && canvasZoom === maxCanvasZoom.value; return isEnabled.value && Math.abs(canvasZoom - maxCanvasZoom.value) < 0.000001;
} }
function focusNode(nodeId: string) { interface FocusNodeOptions {
const nodeToFocus = workflowStore.getNodeById(nodeId); collapseOthers?: boolean;
canvasViewport: ViewportTransform;
canvasDimensions: Dimensions;
setCenter: SetCenter;
}
if (!nodeToFocus) { function focusNode(
node: GraphNode<CanvasNodeData>,
{ collapseOthers = true, canvasDimensions, canvasViewport, setCenter }: FocusNodeOptions,
) {
collapsedNodes.value = collapseOthers
? workflowStore.allNodes.reduce<Partial<Record<string, boolean>>>((acc, n) => {
acc[n.id] = n.id !== node.id;
return acc;
}, {})
: { ...collapsedNodes.value, [node.id]: false };
const topMargin = 80; // pixels
const nodeWidth = node.dimensions.width * (isActive(canvasViewport.zoom) ? 1 : 1.5);
// Move the node to top center of the canvas
void setCenter(
node.position.x + nodeWidth / 2,
node.position.y + (canvasDimensions.height * (1 / 2) - topMargin) / maxCanvasZoom.value,
{
duration: 200,
zoom: maxCanvasZoom.value,
interpolate: 'linear',
},
);
}
interface ToggleZoomModeOptions {
canvasViewport: ViewportTransform;
canvasDimensions: Dimensions;
selectedNodes: Array<GraphNode<CanvasNodeData>>;
setViewport: SetViewport;
fitView: FitView;
zoomTo: ZoomTo;
setCenter: SetCenter;
}
function toggleZoomMode(options: ToggleZoomModeOptions) {
if (isActive(options.canvasViewport.zoom)) {
if (previousViewport.value === undefined) {
void options.fitView({ duration: 200, interpolate: 'linear' });
return; return;
} }
// Call useVueFlow() here because having it in setup fn scope seem to cause initialization problem void options.setViewport(previousViewport.value, { duration: 200, interpolate: 'linear' });
const vueFlow = useVueFlow(workflowStore.workflow.id); return;
}
collapsedNodes.value = workflowStore.allNodes.reduce<Partial<Record<string, boolean>>>( previousViewport.value = options.canvasViewport;
(acc, node) => {
acc[node.id] = node.id !== nodeId;
return acc;
},
{},
);
const workflow = workflowStore.getCurrentWorkflow(); const toFocus = options.selectedNodes
const nodeSize = calculateNodeSize( .filter((node) => node.data.render.type === CanvasNodeRenderType.Default)
workflow.getChildNodes(nodeToFocus.name, 'ALL_NON_MAIN').length > 0, .toSorted((a, b) =>
workflow.getParentNodes(nodeToFocus.name, 'ALL_NON_MAIN').length > 0, a.position.y === b.position.y ? a.position.x - b.position.x : a.position.y - b.position.y,
workflow.getParentNodes(nodeToFocus.name, 'main').length, )[0];
workflow.getChildNodes(nodeToFocus.name, 'main').length,
workflow.getParentNodes(nodeToFocus.name, 'ALL_NON_MAIN').length,
);
void vueFlow.setCenter( if (toFocus) {
nodeToFocus.position[0] + (nodeSize.width * 1.5) / 2, focusNode(toFocus, { ...options, collapseOthers: false });
nodeToFocus.position[1] + 80, return;
{ duration: 200, zoom: maxCanvasZoom.value }, }
);
void options.zoomTo(maxCanvasZoom.value, { duration: 200, interpolate: 'linear' });
} }
return { return {
isEnabled, isEnabled,
maxCanvasZoom, maxCanvasZoom,
previousZoom: computed(() => previousViewport.value),
collapsedNodes: computed(() => collapsedNodes.value), collapsedNodes: computed(() => collapsedNodes.value),
isActive, isActive,
setNodeExpanded, setNodeExpanded,
expandAllNodes, expandAllNodes,
collapseAllNodes, collapseAllNodes,
toggleZoomMode,
focusNode, focusNode,
}; };
}); });

View File

@@ -15,7 +15,6 @@ import { useUIStore } from '@/stores/ui.store';
import { shallowRef, watch } from 'vue'; import { shallowRef, watch } from 'vue';
import { computed, type ComputedRef } from 'vue'; import { computed, type ComputedRef } from 'vue';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useExperimentalNdvStore } from '@/components/canvas/experimental/experimentalNdv.store';
export function useLogsSelection( export function useLogsSelection(
execution: ComputedRef<IExecutionResponse | undefined>, execution: ComputedRef<IExecutionResponse | undefined>,
@@ -34,20 +33,14 @@ export function useLogsSelection(
const uiStore = useUIStore(); const uiStore = useUIStore();
const canvasStore = useCanvasStore(); const canvasStore = useCanvasStore();
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const experimentalNdvStore = useExperimentalNdvStore();
function syncSelectionToCanvasIfEnabled(value: LogEntry) { function syncSelectionToCanvasIfEnabled(value: LogEntry) {
if (!logsStore.isLogSelectionSyncedWithCanvas) { if (!logsStore.isLogSelectionSyncedWithCanvas) {
return; return;
} }
if (experimentalNdvStore.isEnabled) {
canvasEventBus.emit('nodes:select', { ids: [value.node.id], panIntoView: false });
experimentalNdvStore.focusNode(value.node.id);
} else {
canvasEventBus.emit('nodes:select', { ids: [value.node.id], panIntoView: true }); canvasEventBus.emit('nodes:select', { ids: [value.node.id], panIntoView: true });
} }
}
function select(value: LogEntry | undefined) { function select(value: LogEntry | undefined) {
manualLogEntrySelection.value = manualLogEntrySelection.value =

View File

@@ -544,7 +544,10 @@ describe('calculateNodeSize', () => {
function createTestGraphNode(data: Partial<GraphNode> = {}): GraphNode { function createTestGraphNode(data: Partial<GraphNode> = {}): GraphNode {
return { return {
computedPosition: { z: 0, ...(data.position ?? { x: 0, y: 0 }) }, computedPosition: { z: 0, ...(data.position ?? { x: 0, y: 0 }) },
handleBounds: {}, handleBounds: {
source: null,
target: null,
},
dimensions: { width: 0, height: 0 }, dimensions: { width: 0, height: 0 },
isParent: true, isParent: true,
selected: false, selected: false,

35
pnpm-lock.yaml generated
View File

@@ -2360,19 +2360,19 @@ importers:
version: 1.6.0(typescript@5.8.3) version: 1.6.0(typescript@5.8.3)
'@vue-flow/background': '@vue-flow/background':
specifier: ^1.3.2 specifier: ^1.3.2
version: 1.3.2(@vue-flow/core@1.42.1(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3)) version: 1.3.2(@vue-flow/core@1.45.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
'@vue-flow/controls': '@vue-flow/controls':
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.1.2(@vue-flow/core@1.42.1(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3)) version: 1.1.2(@vue-flow/core@1.45.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
'@vue-flow/core': '@vue-flow/core':
specifier: ^1.42.1 specifier: ^1.45.0
version: 1.42.1(vue@3.5.13(typescript@5.8.3)) version: 1.45.0(vue@3.5.13(typescript@5.8.3))
'@vue-flow/minimap': '@vue-flow/minimap':
specifier: ^1.5.2 specifier: ^1.5.2
version: 1.5.2(@vue-flow/core@1.42.1(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3)) version: 1.5.2(@vue-flow/core@1.45.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
'@vue-flow/node-resizer': '@vue-flow/node-resizer':
specifier: ^1.4.0 specifier: ^1.4.0
version: 1.4.0(@vue-flow/core@1.42.1(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3)) version: 1.4.0(@vue-flow/core@1.45.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
'@vueuse/components': '@vueuse/components':
specifier: ^10.11.0 specifier: ^10.11.0
version: 10.11.0(vue@3.5.13(typescript@5.8.3)) version: 10.11.0(vue@3.5.13(typescript@5.8.3))
@@ -7706,8 +7706,8 @@ packages:
'@vue-flow/core': ^1.23.0 '@vue-flow/core': ^1.23.0
vue: ^3.3.0 vue: ^3.3.0
'@vue-flow/core@1.42.1': '@vue-flow/core@1.45.0':
resolution: {integrity: sha512-QzzTxMAXfOeETKc+N3XMp5XpiPxKBHK5kq98avgTsE6MXyeU2E8EkANwwgSB/hvJ/k36RjU0Y7BOwCHiqiI1tw==} resolution: {integrity: sha512-+Qd4fTnCfrhfYQzlHyf5Jt7rNE4PlDnEJEJZH9v6hDZoTOeOy1RhS85cSxKYxdsJ31Ttj2v3yabhoVfBf+bmJA==}
peerDependencies: peerDependencies:
vue: ^3.3.0 vue: ^3.3.0
@@ -22328,36 +22328,37 @@ snapshots:
path-browserify: 1.0.1 path-browserify: 1.0.1
vscode-uri: 3.0.8 vscode-uri: 3.0.8
'@vue-flow/background@1.3.2(@vue-flow/core@1.42.1(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))': '@vue-flow/background@1.3.2(@vue-flow/core@1.45.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))':
dependencies: dependencies:
'@vue-flow/core': 1.42.1(vue@3.5.13(typescript@5.8.3)) '@vue-flow/core': 1.45.0(vue@3.5.13(typescript@5.8.3))
vue: 3.5.13(typescript@5.8.3) vue: 3.5.13(typescript@5.8.3)
'@vue-flow/controls@1.1.2(@vue-flow/core@1.42.1(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))': '@vue-flow/controls@1.1.2(@vue-flow/core@1.45.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))':
dependencies: dependencies:
'@vue-flow/core': 1.42.1(vue@3.5.13(typescript@5.8.3)) '@vue-flow/core': 1.45.0(vue@3.5.13(typescript@5.8.3))
vue: 3.5.13(typescript@5.8.3) vue: 3.5.13(typescript@5.8.3)
'@vue-flow/core@1.42.1(vue@3.5.13(typescript@5.8.3))': '@vue-flow/core@1.45.0(vue@3.5.13(typescript@5.8.3))':
dependencies: dependencies:
'@vueuse/core': 10.11.0(vue@3.5.13(typescript@5.8.3)) '@vueuse/core': 10.11.0(vue@3.5.13(typescript@5.8.3))
d3-drag: 3.0.0 d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0 d3-selection: 3.0.0
d3-zoom: 3.0.0 d3-zoom: 3.0.0
vue: 3.5.13(typescript@5.8.3) vue: 3.5.13(typescript@5.8.3)
transitivePeerDependencies: transitivePeerDependencies:
- '@vue/composition-api' - '@vue/composition-api'
'@vue-flow/minimap@1.5.2(@vue-flow/core@1.42.1(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))': '@vue-flow/minimap@1.5.2(@vue-flow/core@1.45.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))':
dependencies: dependencies:
'@vue-flow/core': 1.42.1(vue@3.5.13(typescript@5.8.3)) '@vue-flow/core': 1.45.0(vue@3.5.13(typescript@5.8.3))
d3-selection: 3.0.0 d3-selection: 3.0.0
d3-zoom: 3.0.0 d3-zoom: 3.0.0
vue: 3.5.13(typescript@5.8.3) vue: 3.5.13(typescript@5.8.3)
'@vue-flow/node-resizer@1.4.0(@vue-flow/core@1.42.1(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))': '@vue-flow/node-resizer@1.4.0(@vue-flow/core@1.45.0(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))':
dependencies: dependencies:
'@vue-flow/core': 1.42.1(vue@3.5.13(typescript@5.8.3)) '@vue-flow/core': 1.45.0(vue@3.5.13(typescript@5.8.3))
d3-drag: 3.0.0 d3-drag: 3.0.0
d3-selection: 3.0.0 d3-selection: 3.0.0
vue: 3.5.13(typescript@5.8.3) vue: 3.5.13(typescript@5.8.3)