+
{
},
[STORES.NDV]: {
activeNodeName: 'Test Node',
+ hasInputData: true,
+ isInputPanelEmpty: false,
+ isOutputPanelEmpty: false,
+ ndvInputDataWithPinnedData: [],
+ getHoveringItem: undefined,
+ expressionOutputItemIndex: 0,
+ isTableHoverOnboarded: false,
+ setHighlightDraggables: vi.fn(),
},
[STORES.WORKFLOWS]: {
workflow: {
@@ -185,10 +193,22 @@ describe('SqlEditor.vue', () => {
// Does not hide output when clicking inside the output
await focusEditor(container);
await userEvent.click(getByTestId(EXPRESSION_OUTPUT_TEST_ID));
- await waitFor(() => expect(queryByTestId(EXPRESSION_OUTPUT_TEST_ID)).toBeInTheDocument());
+
+ await waitFor(() =>
+ expect(
+ queryByTestId(EXPRESSION_OUTPUT_TEST_ID)?.closest('[aria-hidden=false]'),
+ ).toBeInTheDocument(),
+ );
// Does hide output when clicking outside the container
await userEvent.click(baseElement);
- await waitFor(() => expect(queryByTestId(EXPRESSION_OUTPUT_TEST_ID)).not.toBeInTheDocument());
+
+ // NOTE: in testing, popover persists regardless of persist option.
+ // See https://github.com/element-plus/element-plus/blob/2.4.3/packages/components/tooltip/src/content.vue#L83-L90
+ await waitFor(() =>
+ expect(
+ queryByTestId(EXPRESSION_OUTPUT_TEST_ID)?.closest('[aria-hidden=true]'),
+ ).toBeInTheDocument(),
+ );
});
});
diff --git a/packages/frontend/editor-ui/src/components/SqlEditor/SqlEditor.vue b/packages/frontend/editor-ui/src/components/SqlEditor/SqlEditor.vue
index ea5fe859e2..cdec7228bf 100644
--- a/packages/frontend/editor-ui/src/components/SqlEditor/SqlEditor.vue
+++ b/packages/frontend/editor-ui/src/components/SqlEditor/SqlEditor.vue
@@ -73,6 +73,7 @@ const emit = defineEmits<{
const container = ref();
const sqlEditor = ref();
const isFocused = ref(false);
+const outputPopover = ref>();
const extensions = computed(() => {
const dialect = SQL_DIALECTS[props.dialect] ?? SQL_DIALECTS.StandardSQL;
@@ -156,9 +157,10 @@ onClickOutside(container, (event) => onBlur(event));
function onBlur(event: FocusEvent | KeyboardEvent) {
if (
event?.target instanceof Element &&
- Array.from(event.target.classList).some((_class) => _class.includes('resizer'))
+ (Array.from(event.target.classList).some((_class) => _class.includes('resizer')) ||
+ outputPopover.value?.contentRef?.contains(event.target))
) {
- return; // prevent blur on resizing
+ return; // prevent blur on resizing or interacting with output popover
}
isFocused.value = false;
@@ -219,9 +221,11 @@ defineExpose({
diff --git a/packages/frontend/editor-ui/src/components/canvas/Canvas.vue b/packages/frontend/editor-ui/src/components/canvas/Canvas.vue
index 710a723a47..441f03ab7a 100644
--- a/packages/frontend/editor-ui/src/components/canvas/Canvas.vue
+++ b/packages/frontend/editor-ui/src/components/canvas/Canvas.vue
@@ -137,6 +137,9 @@ const props = withDefaults(
);
const { isMobileDevice, controlKeyCode } = useDeviceSupport();
+const experimentalNdvStore = useExperimentalNdvStore();
+
+const isExperimentalNdvActive = computed(() => experimentalNdvStore.isActive(viewport.value.zoom));
const vueFlow = useVueFlow(props.id);
const {
@@ -174,14 +177,10 @@ const {
getDownstreamNodes,
getUpstreamNodes,
} = useCanvasTraversal(vueFlow);
-const { layout } = useCanvasLayout({ id: props.id });
-
-const experimentalNdvStore = useExperimentalNdvStore();
+const { layout } = useCanvasLayout(props.id, isExperimentalNdvActive);
const isPaneReady = ref(false);
-const isExperimentalNdvActive = computed(() => experimentalNdvStore.isActive(viewport.value.zoom));
-
const classes = computed(() => ({
[$style.canvas]: true,
[$style.ready]: !props.loading && isPaneReady.value,
@@ -886,7 +885,6 @@ watch([vueFlow.nodes, () => experimentalNdvStore.nodeNameToBeFocused], ([nodes,
// setTimeout() so that this happens after layout recalculation with the node to be focused
setTimeout(() => {
experimentalNdvStore.focusNode(toFocusNode, {
- collapseOthers: false,
canvasViewport: viewport.value,
canvasDimensions: dimensions.value,
setCenter,
diff --git a/packages/frontend/editor-ui/src/components/canvas/WorkflowCanvas.vue b/packages/frontend/editor-ui/src/components/canvas/WorkflowCanvas.vue
index 15eeb50908..699ae67945 100644
--- a/packages/frontend/editor-ui/src/components/canvas/WorkflowCanvas.vue
+++ b/packages/frontend/editor-ui/src/components/canvas/WorkflowCanvas.vue
@@ -70,7 +70,7 @@ const mappedConnectionsThrottled = throttledRef(mappedConnections, 200);
-
+
-
@@ -142,17 +154,21 @@ const commonClasses = computed(() => [$style.status, spinnerScrim ? $style.spinn
.node-waiting-spinner,
.running {
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 3.75em;
- color: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.7);
- position: absolute;
- left: 0;
- top: 0;
- padding: var(--canvas-node--status-icons-offset);
+ color: hsl(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l));
+
+ &.absoluteSpinner {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 3.75em;
+ color: hsla(var(--color-primary-h), var(--color-primary-s), var(--color-primary-l), 0.7);
+ position: absolute;
+ left: 0;
+ top: 0;
+ padding: var(--canvas-node--status-icons-offset);
+ }
&.spinnerScrim {
z-index: 10;
diff --git a/packages/frontend/editor-ui/src/components/canvas/experimental/components/ExperimentalCanvasNodeSettings.vue b/packages/frontend/editor-ui/src/components/canvas/experimental/components/ExperimentalCanvasNodeSettings.vue
index f6a09b2125..48fed40f5a 100644
--- a/packages/frontend/editor-ui/src/components/canvas/experimental/components/ExperimentalCanvasNodeSettings.vue
+++ b/packages/frontend/editor-ui/src/components/canvas/experimental/components/ExperimentalCanvasNodeSettings.vue
@@ -36,6 +36,12 @@ function handleValueChanged(parameterData: IUpdateInformation) {
}
}
+function handleDoubleClickHeader() {
+ if (activeNode.value) {
+ ndvStore.setActiveNodeName(activeNode.value.name);
+ }
+}
+
function handleCaptureWheelEvent(event: WheelEvent) {
if (event.ctrlKey) {
// If the event is pinch, let it propagate and zoom canvas
@@ -55,7 +61,7 @@ function handleCaptureWheelEvent(event: WheelEvent) {
return;
}
- // Otherwise, let it scroll the settings pane
+ // Otherwise, let it scroll the pane
event.stopImmediatePropagation();
}
@@ -71,8 +77,11 @@ function handleCaptureWheelEvent(event: WheelEvent) {
:executable="!isReadOnly"
is-embedded-in-canvas
:sub-title="subTitle"
+ extra-tabs-class-name="nodrag"
+ extra-parameter-wrapper-class-name="nodrag"
@value-changed="handleValueChanged"
@capture-wheel-body="handleCaptureWheelEvent"
+ @dblclick-header="handleDoubleClickHeader"
>
diff --git a/packages/frontend/editor-ui/src/components/canvas/experimental/components/ExperimentalEmbeddedNdvActions.vue b/packages/frontend/editor-ui/src/components/canvas/experimental/components/ExperimentalEmbeddedNdvActions.vue
index 5b4aac963b..ea4aa90627 100644
--- a/packages/frontend/editor-ui/src/components/canvas/experimental/components/ExperimentalEmbeddedNdvActions.vue
+++ b/packages/frontend/editor-ui/src/components/canvas/experimental/components/ExperimentalEmbeddedNdvActions.vue
@@ -1,5 +1,4 @@
-
-
-
-
{
:current-node-name="inputNodeName"
:is-mapping-onboarded="ndvStore.isMappingOnboarded"
:focused-mappable-input="ndvStore.focusedMappableInput"
- >
-
-
- Input
-
-
-
-
+ node-not-run-message-variant="simple"
+ />
+
diff --git a/packages/frontend/editor-ui/src/components/canvas/experimental/experimentalNdv.store.ts b/packages/frontend/editor-ui/src/components/canvas/experimental/experimentalNdv.store.ts
index a4842b6376..74331aa9e4 100644
--- a/packages/frontend/editor-ui/src/components/canvas/experimental/experimentalNdv.store.ts
+++ b/packages/frontend/editor-ui/src/components/canvas/experimental/experimentalNdv.store.ts
@@ -1,7 +1,6 @@
import { computed, ref, shallowRef } from 'vue';
import { defineStore } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows.store';
-import { useSettingsStore } from '@/stores/settings.store';
import {
type Dimensions,
type FitView,
@@ -12,18 +11,18 @@ import {
type ZoomTo,
} from '@vue-flow/core';
import { CanvasNodeRenderType, type CanvasNodeData } from '@/types';
+import { usePostHog } from '@/stores/posthog.store';
+import { CANVAS_ZOOMED_VIEW_EXPERIMENT } from '@/constants';
export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
const workflowStore = useWorkflowsStore();
- const settingsStore = useSettingsStore();
+ const postHogStore = usePostHog();
const isEnabled = computed(
() =>
- !Number.isNaN(settingsStore.experimental__minZoomNodeSettingsInCanvas) &&
- settingsStore.experimental__minZoomNodeSettingsInCanvas > 0,
- );
- const maxCanvasZoom = computed(() =>
- isEnabled.value ? settingsStore.experimental__minZoomNodeSettingsInCanvas : 4,
+ postHogStore.getVariant(CANVAS_ZOOMED_VIEW_EXPERIMENT.name) ===
+ CANVAS_ZOOMED_VIEW_EXPERIMENT.variant,
);
+ const maxCanvasZoom = computed(() => (isEnabled.value ? 2 : 4));
const previousViewport = ref();
const collapsedNodes = shallowRef>>({});
@@ -59,7 +58,6 @@ export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
}
interface FocusNodeOptions {
- collapseOthers?: boolean;
canvasViewport: ViewportTransform;
canvasDimensions: Dimensions;
setCenter: SetCenter;
@@ -67,14 +65,9 @@ export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
function focusNode(
node: GraphNode,
- { collapseOthers = true, canvasDimensions, canvasViewport, setCenter }: FocusNodeOptions,
+ { canvasDimensions, canvasViewport, setCenter }: FocusNodeOptions,
) {
- collapsedNodes.value = collapseOthers
- ? workflowStore.allNodes.reduce>>((acc, n) => {
- acc[n.id] = n.id !== node.id;
- return acc;
- }, {})
- : { ...collapsedNodes.value, [node.id]: false };
+ collapsedNodes.value = { ...collapsedNodes.value, [node.id]: false };
const topMargin = 80; // pixels
const nodeWidth = node.dimensions.width * (isActive(canvasViewport.zoom) ? 1 : 1.5);
@@ -134,7 +127,7 @@ export const useExperimentalNdvStore = defineStore('experimentalNdv', () => {
)[0];
if (toFocus) {
- focusNode(toFocus, { ...options, collapseOthers: false });
+ focusNode(toFocus, options);
return;
}
diff --git a/packages/frontend/editor-ui/src/composables/useCanvasLayout.test.ts b/packages/frontend/editor-ui/src/composables/useCanvasLayout.test.ts
index 9eab5f56f8..f69063ec9f 100644
--- a/packages/frontend/editor-ui/src/composables/useCanvasLayout.test.ts
+++ b/packages/frontend/editor-ui/src/composables/useCanvasLayout.test.ts
@@ -1,5 +1,5 @@
import { useVueFlow, type GraphNode, type VueFlowStore } from '@vue-flow/core';
-import { ref } from 'vue';
+import { computed, ref } from 'vue';
import { createCanvasGraphEdge, createCanvasGraphNode } from '../__tests__/data';
import { CanvasNodeRenderType, type CanvasNodeData } from '../types';
import { useCanvasLayout, type CanvasLayoutResult } from './useCanvasLayout';
@@ -36,7 +36,10 @@ describe('useCanvasLayout', () => {
vi.mocked(useVueFlow).mockReturnValue(vueFlowStoreMock);
- const { layout } = useCanvasLayout();
+ const { layout } = useCanvasLayout(
+ 'test-canvas-id',
+ computed(() => false),
+ );
return { layout };
}
diff --git a/packages/frontend/editor-ui/src/composables/useCanvasLayout.ts b/packages/frontend/editor-ui/src/composables/useCanvasLayout.ts
index 756b22cb0d..02329ee5e4 100644
--- a/packages/frontend/editor-ui/src/composables/useCanvasLayout.ts
+++ b/packages/frontend/editor-ui/src/composables/useCanvasLayout.ts
@@ -10,8 +10,8 @@ import {
} from '../types';
import { isPresent } from '../utils/typesUtils';
import { DEFAULT_NODE_SIZE, GRID_SIZE, calculateNodeSize } from '../utils/nodeViewUtils';
+import type { ComputedRef } from 'vue';
-export type CanvasLayoutOptions = { id?: string };
export type CanvasLayoutTarget = 'selection' | 'all';
export type CanvasLayoutSource =
| 'keyboard-shortcut'
@@ -47,7 +47,7 @@ const AI_X_SPACING = GRID_SIZE * 3;
const AI_Y_SPACING = GRID_SIZE * 8;
const STICKY_BOTTOM_PADDING = GRID_SIZE * 4;
-export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
+export function useCanvasLayout(canvasId: string, isEmbeddedNdvActive: ComputedRef) {
const {
findNode,
findEdge,
@@ -120,6 +120,7 @@ export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
mainInputCount,
mainOutputCount,
nonMainInputCount,
+ isEmbeddedNdvActive.value,
);
}
diff --git a/packages/frontend/editor-ui/src/constants.ts b/packages/frontend/editor-ui/src/constants.ts
index 4fef2a4bf1..5722081c09 100644
--- a/packages/frontend/editor-ui/src/constants.ts
+++ b/packages/frontend/editor-ui/src/constants.ts
@@ -497,8 +497,6 @@ export const LOCAL_STORAGE_LOGS_SYNC_SELECTION = 'N8N_LOGS_SYNC_SELECTION_ENABLE
export const LOCAL_STORAGE_LOGS_PANEL_DETAILS_PANEL = 'N8N_LOGS_DETAILS_PANEL';
export const LOCAL_STORAGE_LOGS_PANEL_DETAILS_PANEL_SUB_NODE = 'N8N_LOGS_DETAILS_PANEL_SUB_NODE';
export const LOCAL_STORAGE_WORKFLOW_LIST_PREFERENCES_KEY = 'N8N_WORKFLOWS_LIST_PREFERENCES';
-export const LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS =
- 'N8N_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS';
export const LOCAL_STORAGE_EXPERIMENTAL_DOCKED_NODE_SETTINGS =
'N8N_EXPERIMENTAL_DOCKED_NODE_SETTINGS';
export const LOCAL_STORAGE_READ_WHATS_NEW_ARTICLES = 'N8N_READ_WHATS_NEW_ARTICLES';
@@ -743,6 +741,12 @@ export const KEEP_AUTH_IN_NDV_FOR_NODES = [
export const MAIN_AUTH_FIELD_NAME = 'authentication';
export const NODE_RESOURCE_FIELD_NAME = 'resource';
+export const CANVAS_ZOOMED_VIEW_EXPERIMENT = {
+ name: 'canvas_zoomed_view',
+ control: 'control',
+ variant: 'variant',
+};
+
export const NDV_UI_OVERHAUL_EXPERIMENT = {
name: '029_ndv_ui_overhaul',
control: 'control',
diff --git a/packages/frontend/editor-ui/src/stores/settings.store.ts b/packages/frontend/editor-ui/src/stores/settings.store.ts
index e3ee750487..83cf8d8954 100644
--- a/packages/frontend/editor-ui/src/stores/settings.store.ts
+++ b/packages/frontend/editor-ui/src/stores/settings.store.ts
@@ -13,7 +13,6 @@ import { testHealthEndpoint } from '@n8n/rest-api-client/api/templates';
import {
INSECURE_CONNECTION_WARNING,
LOCAL_STORAGE_EXPERIMENTAL_DOCKED_NODE_SETTINGS,
- LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS,
} from '@/constants';
import { STORES } from '@n8n/stores';
import { UserManagementAuthenticationMethod } from '@/Interface';
@@ -314,15 +313,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
moduleSettings.value = fetched;
};
- /**
- * (Experimental) Minimum zoom level of the canvas to render node settings in place of nodes, without opening NDV
- */
- const experimental__minZoomNodeSettingsInCanvas = useLocalStorage(
- LOCAL_STORAGE_EXPERIMENTAL_MIN_ZOOM_NODE_SETTINGS_IN_CANVAS,
- 0,
- { writeDefaults: false },
- );
-
/**
* (Experimental) If set to true, show node settings for a selected node in docked pane
*/
@@ -388,7 +378,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
isAskAiEnabled,
isAiCreditsEnabled,
aiCreditsQuota,
- experimental__minZoomNodeSettingsInCanvas,
experimental__dockedNodeSettingsEnabled,
partialExecutionVersion,
reset,
diff --git a/packages/frontend/editor-ui/src/utils/nodeViewUtils.test.ts b/packages/frontend/editor-ui/src/utils/nodeViewUtils.test.ts
index d8a55a1b0f..dab1924474 100644
--- a/packages/frontend/editor-ui/src/utils/nodeViewUtils.test.ts
+++ b/packages/frontend/editor-ui/src/utils/nodeViewUtils.test.ts
@@ -484,6 +484,7 @@ describe('calculateNodeSize', () => {
1,
1,
0,
+ false,
);
// width = GRID_SIZE * 5 = 16 * 5 = 80
// height = GRID_SIZE * 5 = 16 * 5 = 80
@@ -499,7 +500,7 @@ describe('calculateNodeSize', () => {
// maxVerticalHandles = 3
// height = 96 + (3 - 2) * 32 = 96 + 32 = 128
expect(
- calculateNodeSize(false, true, mainInputCount, mainOutputCount, nonMainInputCount),
+ calculateNodeSize(false, true, mainInputCount, mainOutputCount, nonMainInputCount, false),
).toEqual({ width: 272, height: 128 });
});
@@ -507,7 +508,7 @@ describe('calculateNodeSize', () => {
const nonMainInputCount = 2;
// width = 80 + 16 + (max(4, 2) - 1) * 16 * 3 = 240
// height = CONFIGURATION_NODE_SIZE[1] = 16 * 5 = 80
- expect(calculateNodeSize(true, true, 1, 1, nonMainInputCount)).toEqual({
+ expect(calculateNodeSize(true, true, 1, 1, nonMainInputCount, false)).toEqual({
width: 240,
height: 80,
});
@@ -519,7 +520,7 @@ describe('calculateNodeSize', () => {
// width = 96
// maxVerticalHandles = 3
// height = 96 + (3 - 2) * 32 = 128
- expect(calculateNodeSize(false, false, mainInputCount, mainOutputCount, 0)).toEqual({
+ expect(calculateNodeSize(false, false, mainInputCount, mainOutputCount, 0, false)).toEqual({
width: 96,
height: 128,
});
@@ -530,7 +531,9 @@ describe('calculateNodeSize', () => {
const mainOutputCount = 4;
// maxVerticalHandles = 6
// height = 96 + (6 - 2) * 32 = 96 + 128 = 224
- expect(calculateNodeSize(false, false, mainInputCount, mainOutputCount, 0).height).toBe(224);
+ expect(calculateNodeSize(false, false, mainInputCount, mainOutputCount, 0, false).height).toBe(
+ 224,
+ );
});
it('should respect the minimum width for configurable nodes', () => {
@@ -539,7 +542,7 @@ describe('calculateNodeSize', () => {
// height = default path, mainInputCount = 1, mainOutputCount = 1
// maxVerticalHandles = 1
// height = 96 + (1 - 2) * 32 = 96 + 0 = 96
- expect(calculateNodeSize(false, true, 1, 1, nonMainInputCount)).toEqual({
+ expect(calculateNodeSize(false, true, 1, 1, nonMainInputCount, false)).toEqual({
width: 224,
height: 96,
});
@@ -548,7 +551,7 @@ describe('calculateNodeSize', () => {
it('should handle edge case when mainInputCount and mainOutputCount are 0', () => {
// maxVerticalHandles = max(0,0,1) = 1
// height = 96 + (1 - 2) * 32 = 96 + 0 = 96
- expect(calculateNodeSize(false, false, 0, 0, 0).height).toBe(96);
+ expect(calculateNodeSize(false, false, 0, 0, 0, false).height).toBe(96);
});
});
diff --git a/packages/frontend/editor-ui/src/utils/nodeViewUtils.ts b/packages/frontend/editor-ui/src/utils/nodeViewUtils.ts
index 98bf7d1eb0..790d85f6c5 100644
--- a/packages/frontend/editor-ui/src/utils/nodeViewUtils.ts
+++ b/packages/frontend/editor-ui/src/utils/nodeViewUtils.ts
@@ -617,9 +617,11 @@ export function calculateNodeSize(
mainInputCount: number,
mainOutputCount: number,
nonMainInputCount: number,
+ isExperimentalNdvActive: boolean,
): { width: number; height: number } {
const maxVerticalHandles = Math.max(mainInputCount, mainOutputCount, 1);
const height = DEFAULT_NODE_SIZE[1] + Math.max(0, maxVerticalHandles - 2) * GRID_SIZE * 2;
+ const widthScale = isExperimentalNdvActive ? 1.5 : 1;
if (isConfigurable) {
const portCount = Math.max(NODE_MIN_INPUT_ITEMS_COUNT, nonMainInputCount);
@@ -627,15 +629,16 @@ export function calculateNodeSize(
return {
// Configuration node has extra width so that its centered port aligns to the grid
width:
- CONFIGURATION_NODE_RADIUS * 2 +
- GRID_SIZE * ((isConfiguration ? 1 : 0) + (portCount - 1) * 3),
+ (CONFIGURATION_NODE_RADIUS * 2 +
+ GRID_SIZE * ((isConfiguration ? 1 : 0) + (portCount - 1) * 3)) *
+ widthScale,
height: isConfiguration ? CONFIGURATION_NODE_SIZE[1] : height,
};
}
if (isConfiguration) {
- return { width: CONFIGURATION_NODE_SIZE[0], height: CONFIGURATION_NODE_SIZE[1] };
+ return { width: CONFIGURATION_NODE_SIZE[0] * widthScale, height: CONFIGURATION_NODE_SIZE[1] };
}
- return { width: DEFAULT_NODE_SIZE[0], height };
+ return { width: DEFAULT_NODE_SIZE[0] * widthScale, height };
}