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 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';

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, 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);

View File

@@ -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"