fix(editor): Fix nodes and connection debouncing during execution (no-changelog) (#14208)

This commit is contained in:
Alex Grozav
2025-03-27 11:59:04 +02:00
committed by GitHub
parent 5f4e56f75b
commit a4a34a2745
4 changed files with 60 additions and 93 deletions

View File

@@ -42,6 +42,7 @@ export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
export const CODE_NODE_NAME = 'Code';
export const SET_NODE_NAME = 'Set';
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 MERGE_NODE_NAME = 'Merge';
export const SWITCH_NODE_NAME = 'Switch';

View File

@@ -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');
});
});

View File

@@ -14,16 +14,13 @@ import {
defaultNodeDescriptions,
} from '@/__tests__/mocks';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import * as lodash from 'lodash-es';
import * as vueuse from '@vueuse/core';
vi.mock('lodash-es', async () => {
const actual = await vi.importActual('lodash-es');
vi.mock('@vueuse/core', async () => {
const actual = await vi.importActual('@vueuse/core');
return {
...actual,
debounce: vi.fn((fn) => {
// Return a function that immediately calls the provided function
return (...args: unknown[]) => fn(...args);
}),
debouncedRef: vi.fn(actual.debouncedRef as typeof vueuse.debouncedRef),
};
});
@@ -157,34 +154,6 @@ describe('WorkflowCanvas', () => {
});
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 () => {
renderComponent({
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
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);
expect(executingCalls.length).toBeGreaterThanOrEqual(2);

View File

@@ -1,15 +1,14 @@
<script setup lang="ts">
import Canvas from '@/components/canvas/Canvas.vue';
import type { WatchStopHandle } from 'vue';
import { computed, ref, toRef, useCssModule, watch } from 'vue';
import { computed, ref, toRef, useCssModule } from 'vue';
import type { Workflow } from 'n8n-workflow';
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 { CanvasConnection, CanvasEventBusEvents, CanvasNode } from '@/types';
import type { CanvasEventBusEvents } from '@/types';
import { useVueFlow } from '@vue-flow/core';
import { debounce } from 'lodash-es';
import { debouncedRef } from '@vueuse/core';
defineOptions({
inheritAttrs: false,
@@ -62,56 +61,8 @@ onNodesInitialized(() => {
}
});
// Debounced versions of nodes and connections and watchers
const nodesDebounced = ref<CanvasNode[]>([]);
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 },
);
}
const mappedNodesDebounced = debouncedRef(mappedNodes, 200, { maxWait: 50 });
const mappedConnectionsDebounced = debouncedRef(mappedConnections, 200, { maxWait: 50 });
</script>
<template>
@@ -120,8 +71,8 @@ function setupDebouncedWatchers() {
<Canvas
v-if="workflow"
:id="id"
:nodes="nodesDebounced"
:connections="connectionsDebounced"
:nodes="executing ? mappedNodesDebounced : mappedNodes"
:connections="executing ? mappedConnectionsDebounced : mappedConnections"
:event-bus="eventBus"
:read-only="readOnly"
v-bind="$attrs"