mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
fix(editor): Fix nodes and connection debouncing during execution (no-changelog) (#14208)
This commit is contained in:
@@ -42,6 +42,7 @@ export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
|
|||||||
export const CODE_NODE_NAME = 'Code';
|
export const CODE_NODE_NAME = 'Code';
|
||||||
export const SET_NODE_NAME = 'Set';
|
export const SET_NODE_NAME = 'Set';
|
||||||
export const EDIT_FIELDS_SET_NODE_NAME = 'Edit Fields';
|
export const EDIT_FIELDS_SET_NODE_NAME = 'Edit Fields';
|
||||||
|
export const LOOP_OVER_ITEMS_NODE_NAME = 'Loop Over Items';
|
||||||
export const IF_NODE_NAME = 'If';
|
export const IF_NODE_NAME = 'If';
|
||||||
export const MERGE_NODE_NAME = 'Merge';
|
export const MERGE_NODE_NAME = 'Merge';
|
||||||
export const SWITCH_NODE_NAME = 'Switch';
|
export const SWITCH_NODE_NAME = 'Switch';
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { EDIT_FIELDS_SET_NODE_NAME, LOOP_OVER_ITEMS_NODE_NAME } from '../constants';
|
||||||
|
import { NodeCreator } from '../pages/features/node-creator';
|
||||||
|
import { NDV } from '../pages/ndv';
|
||||||
|
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||||
|
|
||||||
|
const nodeCreatorFeature = new NodeCreator();
|
||||||
|
const WorkflowPage = new WorkflowPageClass();
|
||||||
|
const NDVModal = new NDV();
|
||||||
|
|
||||||
|
describe('CAT-726 Node connectors not rendered when nodes inserted on the canvas', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
WorkflowPage.actions.visit();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly append a No Op node when Loop Over Items node is added (from add button)', () => {
|
||||||
|
nodeCreatorFeature.actions.openNodeCreator();
|
||||||
|
nodeCreatorFeature.getters.searchBar().find('input').type(EDIT_FIELDS_SET_NODE_NAME);
|
||||||
|
nodeCreatorFeature.getters.getCreatorItem(EDIT_FIELDS_SET_NODE_NAME).click();
|
||||||
|
NDVModal.actions.close();
|
||||||
|
|
||||||
|
WorkflowPage.actions.executeWorkflow();
|
||||||
|
|
||||||
|
cy.getByTestId('edge-label').realHover();
|
||||||
|
cy.getByTestId('add-connection-button').realClick();
|
||||||
|
|
||||||
|
nodeCreatorFeature.getters.searchBar().find('input').type(LOOP_OVER_ITEMS_NODE_NAME);
|
||||||
|
nodeCreatorFeature.getters.getCreatorItem(LOOP_OVER_ITEMS_NODE_NAME).click();
|
||||||
|
NDVModal.actions.close();
|
||||||
|
|
||||||
|
WorkflowPage.getters.canvasNodes().should('have.length', 4);
|
||||||
|
WorkflowPage.getters.nodeConnections().should('have.length', 4);
|
||||||
|
|
||||||
|
WorkflowPage.getters
|
||||||
|
.getConnectionBetweenNodes(LOOP_OVER_ITEMS_NODE_NAME, 'Replace Me')
|
||||||
|
.should('exist')
|
||||||
|
.should('be.visible');
|
||||||
|
WorkflowPage.getters
|
||||||
|
.getConnectionBetweenNodes(LOOP_OVER_ITEMS_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME)
|
||||||
|
.should('exist')
|
||||||
|
.should('be.visible');
|
||||||
|
WorkflowPage.getters
|
||||||
|
.getConnectionBetweenNodes('Replace Me', LOOP_OVER_ITEMS_NODE_NAME)
|
||||||
|
.should('exist')
|
||||||
|
.should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -14,16 +14,13 @@ import {
|
|||||||
defaultNodeDescriptions,
|
defaultNodeDescriptions,
|
||||||
} from '@/__tests__/mocks';
|
} from '@/__tests__/mocks';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import * as lodash from 'lodash-es';
|
import * as vueuse from '@vueuse/core';
|
||||||
|
|
||||||
vi.mock('lodash-es', async () => {
|
vi.mock('@vueuse/core', async () => {
|
||||||
const actual = await vi.importActual('lodash-es');
|
const actual = await vi.importActual('@vueuse/core');
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
debounce: vi.fn((fn) => {
|
debouncedRef: vi.fn(actual.debouncedRef as typeof vueuse.debouncedRef),
|
||||||
// Return a function that immediately calls the provided function
|
|
||||||
return (...args: unknown[]) => fn(...args);
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,34 +154,6 @@ describe('WorkflowCanvas', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('debouncing behavior', () => {
|
describe('debouncing behavior', () => {
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should initialize debounced watchers on component mount', async () => {
|
|
||||||
renderComponent();
|
|
||||||
|
|
||||||
expect(lodash.debounce).toHaveBeenCalledTimes(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should configure debouncing with no delay when not executing', async () => {
|
|
||||||
renderComponent({
|
|
||||||
props: {
|
|
||||||
executing: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(lodash.debounce).toHaveBeenCalledTimes(3);
|
|
||||||
|
|
||||||
// Find calls related to our specific debouncing logic
|
|
||||||
const calls = vi.mocked(lodash.debounce).mock.calls;
|
|
||||||
const nonExecutingCalls = calls.filter((call) => call[1] === 0 && call[2]?.maxWait === 0);
|
|
||||||
|
|
||||||
expect(nonExecutingCalls.length).toBeGreaterThanOrEqual(2);
|
|
||||||
expect(nonExecutingCalls[0][1]).toBe(0);
|
|
||||||
expect(nonExecutingCalls[0][2]).toEqual({ maxWait: 0 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should configure debouncing with delay when executing', async () => {
|
it('should configure debouncing with delay when executing', async () => {
|
||||||
renderComponent({
|
renderComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -192,10 +161,10 @@ describe('WorkflowCanvas', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(lodash.debounce).toHaveBeenCalledTimes(3);
|
expect(vueuse.debouncedRef).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
// Find calls related to our specific debouncing logic
|
// Find calls related to our specific debouncing logic
|
||||||
const calls = vi.mocked(lodash.debounce).mock.calls;
|
const calls = vi.mocked(vueuse.debouncedRef).mock.calls;
|
||||||
const executingCalls = calls.filter((call) => call[1] === 200 && call[2]?.maxWait === 50);
|
const executingCalls = calls.filter((call) => call[1] === 200 && call[2]?.maxWait === 50);
|
||||||
|
|
||||||
expect(executingCalls.length).toBeGreaterThanOrEqual(2);
|
expect(executingCalls.length).toBeGreaterThanOrEqual(2);
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Canvas from '@/components/canvas/Canvas.vue';
|
import Canvas from '@/components/canvas/Canvas.vue';
|
||||||
import type { WatchStopHandle } from 'vue';
|
import { computed, ref, toRef, useCssModule } from 'vue';
|
||||||
import { computed, ref, toRef, useCssModule, watch } from 'vue';
|
|
||||||
import type { Workflow } from 'n8n-workflow';
|
import type { Workflow } from 'n8n-workflow';
|
||||||
import type { IWorkflowDb } from '@/Interface';
|
import type { IWorkflowDb } from '@/Interface';
|
||||||
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
||||||
import type { EventBus } from '@n8n/utils/event-bus';
|
import type { EventBus } from '@n8n/utils/event-bus';
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
import type { CanvasConnection, CanvasEventBusEvents, CanvasNode } from '@/types';
|
import type { CanvasEventBusEvents } from '@/types';
|
||||||
import { useVueFlow } from '@vue-flow/core';
|
import { useVueFlow } from '@vue-flow/core';
|
||||||
import { debounce } from 'lodash-es';
|
import { debouncedRef } from '@vueuse/core';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
@@ -62,56 +61,8 @@ onNodesInitialized(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Debounced versions of nodes and connections and watchers
|
const mappedNodesDebounced = debouncedRef(mappedNodes, 200, { maxWait: 50 });
|
||||||
const nodesDebounced = ref<CanvasNode[]>([]);
|
const mappedConnectionsDebounced = debouncedRef(mappedConnections, 200, { maxWait: 50 });
|
||||||
const connectionsDebounced = ref<CanvasConnection[]>([]);
|
|
||||||
const debounceNodesWatcher = ref<WatchStopHandle>();
|
|
||||||
const debounceConnectionsWatcher = ref<WatchStopHandle>();
|
|
||||||
|
|
||||||
// Update debounce watchers when execution state changes
|
|
||||||
watch(() => props.executing, setupDebouncedWatchers, { immediate: true });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up debounced watchers for nodes and connections
|
|
||||||
* Uses different debounce times based on execution state:
|
|
||||||
* - During execution: Debounce updates to reduce performance impact for large number of nodes/items
|
|
||||||
* - Otherwise: Update immediately
|
|
||||||
*/
|
|
||||||
function setupDebouncedWatchers() {
|
|
||||||
// Clear existing watchers if they exist
|
|
||||||
debounceNodesWatcher.value?.();
|
|
||||||
debounceConnectionsWatcher.value?.();
|
|
||||||
|
|
||||||
// Configure debounce parameters based on execution state
|
|
||||||
const debounceTime = props.executing ? 200 : 0;
|
|
||||||
const maxWait = props.executing ? 50 : 0;
|
|
||||||
|
|
||||||
// Set up debounced watcher for nodes
|
|
||||||
debounceNodesWatcher.value = watch(
|
|
||||||
mappedNodes,
|
|
||||||
debounce(
|
|
||||||
(value) => {
|
|
||||||
nodesDebounced.value = value;
|
|
||||||
},
|
|
||||||
debounceTime,
|
|
||||||
{ maxWait },
|
|
||||||
),
|
|
||||||
{ immediate: true, deep: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set up debounced watcher for connections
|
|
||||||
debounceConnectionsWatcher.value = watch(
|
|
||||||
mappedConnections,
|
|
||||||
debounce(
|
|
||||||
(value) => {
|
|
||||||
connectionsDebounced.value = value;
|
|
||||||
},
|
|
||||||
debounceTime,
|
|
||||||
{ maxWait },
|
|
||||||
),
|
|
||||||
{ immediate: true, deep: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -120,8 +71,8 @@ function setupDebouncedWatchers() {
|
|||||||
<Canvas
|
<Canvas
|
||||||
v-if="workflow"
|
v-if="workflow"
|
||||||
:id="id"
|
:id="id"
|
||||||
:nodes="nodesDebounced"
|
:nodes="executing ? mappedNodesDebounced : mappedNodes"
|
||||||
:connections="connectionsDebounced"
|
:connections="executing ? mappedConnectionsDebounced : mappedConnections"
|
||||||
:event-bus="eventBus"
|
:event-bus="eventBus"
|
||||||
:read-only="readOnly"
|
:read-only="readOnly"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
|
|||||||
Reference in New Issue
Block a user