mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(editor): Prevent tooltip flickering when a trigger node is pinned (#19233)
This commit is contained in:
@@ -12,7 +12,7 @@ import type {
|
|||||||
INodeIssues,
|
INodeIssues,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeConnectionTypes, NodeHelpers, Workflow } from 'n8n-workflow';
|
import { FORM_TRIGGER_NODE_TYPE, NodeConnectionTypes, NodeHelpers, Workflow } from 'n8n-workflow';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
|
||||||
@@ -68,6 +68,7 @@ export const mockNodeTypeDescription = ({
|
|||||||
hidden,
|
hidden,
|
||||||
description,
|
description,
|
||||||
webhooks,
|
webhooks,
|
||||||
|
eventTriggerDescription,
|
||||||
}: {
|
}: {
|
||||||
name?: INodeTypeDescription['name'];
|
name?: INodeTypeDescription['name'];
|
||||||
displayName?: INodeTypeDescription['displayName'];
|
displayName?: INodeTypeDescription['displayName'];
|
||||||
@@ -82,6 +83,7 @@ export const mockNodeTypeDescription = ({
|
|||||||
hidden?: INodeTypeDescription['hidden'];
|
hidden?: INodeTypeDescription['hidden'];
|
||||||
description?: INodeTypeDescription['description'];
|
description?: INodeTypeDescription['description'];
|
||||||
webhooks?: INodeTypeDescription['webhooks'];
|
webhooks?: INodeTypeDescription['webhooks'];
|
||||||
|
eventTriggerDescription?: INodeTypeDescription['eventTriggerDescription'];
|
||||||
} = {}) =>
|
} = {}) =>
|
||||||
mock<INodeTypeDescription>({
|
mock<INodeTypeDescription>({
|
||||||
name,
|
name,
|
||||||
@@ -105,6 +107,7 @@ export const mockNodeTypeDescription = ({
|
|||||||
webhooks,
|
webhooks,
|
||||||
parameterPane: undefined,
|
parameterPane: undefined,
|
||||||
hidden,
|
hidden,
|
||||||
|
eventTriggerDescription,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mockLoadedNodeType = (name: string) =>
|
export const mockLoadedNodeType = (name: string) =>
|
||||||
@@ -121,6 +124,7 @@ export const mockNodes = [
|
|||||||
mockNode({ name: 'Code', type: CODE_NODE_TYPE }),
|
mockNode({ name: 'Code', type: CODE_NODE_TYPE }),
|
||||||
mockNode({ name: 'Rename', type: SET_NODE_TYPE }),
|
mockNode({ name: 'Rename', type: SET_NODE_TYPE }),
|
||||||
mockNode({ name: 'Chat Trigger', type: CHAT_TRIGGER_NODE_TYPE }),
|
mockNode({ name: 'Chat Trigger', type: CHAT_TRIGGER_NODE_TYPE }),
|
||||||
|
mockNode({ name: 'Form Trigger', type: FORM_TRIGGER_NODE_TYPE }),
|
||||||
mockNode({ name: 'Agent', type: AGENT_NODE_TYPE }),
|
mockNode({ name: 'Agent', type: AGENT_NODE_TYPE }),
|
||||||
mockNode({ name: 'Sticky', type: STICKY_NODE_TYPE }),
|
mockNode({ name: 'Sticky', type: STICKY_NODE_TYPE }),
|
||||||
mockNode({ name: 'Simulate', type: SIMULATE_NODE_TYPE }),
|
mockNode({ name: 'Simulate', type: SIMULATE_NODE_TYPE }),
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
|
import type { INode, NodeApiError, Workflow } from 'n8n-workflow';
|
||||||
|
import { NodeConnectionTypes } from 'n8n-workflow';
|
||||||
|
import { setActivePinia } from 'pinia';
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { NodeConnectionTypes } from 'n8n-workflow';
|
|
||||||
import type { Workflow, INode, NodeApiError } from 'n8n-workflow';
|
|
||||||
import { setActivePinia } from 'pinia';
|
|
||||||
|
|
||||||
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
|
||||||
import type { INodeUi } from '@/Interface';
|
|
||||||
import {
|
import {
|
||||||
createTestNode,
|
createTestNode,
|
||||||
createTestWorkflowObject,
|
createTestWorkflowObject,
|
||||||
@@ -13,16 +11,23 @@ import {
|
|||||||
mockNodes,
|
mockNodes,
|
||||||
mockNodeTypeDescription,
|
mockNodeTypeDescription,
|
||||||
} from '@/__tests__/mocks';
|
} from '@/__tests__/mocks';
|
||||||
import { STORES } from '@n8n/stores';
|
|
||||||
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
||||||
import { createCanvasConnectionHandleString, createCanvasConnectionId } from '@/utils/canvasUtils';
|
|
||||||
import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types';
|
|
||||||
import { MarkerType } from '@vue-flow/core';
|
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
|
||||||
import { mockedStore } from '@/__tests__/utils';
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { useCanvasMapping } from '@/composables/useCanvasMapping';
|
||||||
|
import {
|
||||||
|
FORM_TRIGGER_NODE_TYPE,
|
||||||
|
MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
SET_NODE_TYPE,
|
||||||
|
STICKY_NODE_TYPE,
|
||||||
|
} from '@/constants';
|
||||||
|
import type { INodeUi } from '@/Interface';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { CanvasConnectionMode, CanvasNodeRenderType, type CanvasNodeDefaultRender } from '@/types';
|
||||||
|
import { createCanvasConnectionHandleString, createCanvasConnectionId } from '@/utils/canvasUtils';
|
||||||
|
import { STORES } from '@n8n/stores';
|
||||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { MarkerType } from '@vue-flow/core';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const pinia = createTestingPinia({
|
const pinia = createTestingPinia({
|
||||||
@@ -35,6 +40,14 @@ beforeEach(() => {
|
|||||||
[MANUAL_TRIGGER_NODE_TYPE]: {
|
[MANUAL_TRIGGER_NODE_TYPE]: {
|
||||||
1: mockNodeTypeDescription({
|
1: mockNodeTypeDescription({
|
||||||
name: MANUAL_TRIGGER_NODE_TYPE,
|
name: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
group: ['trigger'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
[FORM_TRIGGER_NODE_TYPE]: {
|
||||||
|
1: mockNodeTypeDescription({
|
||||||
|
name: FORM_TRIGGER_NODE_TYPE,
|
||||||
|
group: ['trigger'],
|
||||||
|
eventTriggerDescription: 'Waiting for you to submit the form',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
[SET_NODE_TYPE]: {
|
[SET_NODE_TYPE]: {
|
||||||
@@ -1690,6 +1703,124 @@ describe('useCanvasMapping', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('trigger tooltip behavior with pinned data', () => {
|
||||||
|
it('should show tooltip for trigger node with no pinned data when workflow is running', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const triggerNode = mockNode({
|
||||||
|
name: 'Manual Trigger',
|
||||||
|
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
const nodesList = [triggerNode];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes: nodesList,
|
||||||
|
connections,
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.isWorkflowRunning = true;
|
||||||
|
workflowsStore.getWorkflowRunData = {};
|
||||||
|
workflowsStore.pinDataByNodeName.mockReturnValue(undefined);
|
||||||
|
|
||||||
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
|
nodes: ref(nodesList),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderOptions = mappedNodes.value[0]?.data?.render as CanvasNodeDefaultRender;
|
||||||
|
expect(renderOptions.options.tooltip).toBe(
|
||||||
|
'Waiting for you to create an event in n8n-nodes-base.manualTrigger',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the node has a custom eventTriggerDescription', () => {
|
||||||
|
it('should show tooltip for trigger node with no pinned data when workflow is running', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const triggerNode = mockNode({
|
||||||
|
name: 'Form Trigger',
|
||||||
|
type: FORM_TRIGGER_NODE_TYPE,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
const nodesList = [triggerNode];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes: nodesList,
|
||||||
|
connections,
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.isWorkflowRunning = true;
|
||||||
|
workflowsStore.getWorkflowRunData = {};
|
||||||
|
workflowsStore.pinDataByNodeName.mockReturnValue(undefined);
|
||||||
|
|
||||||
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
|
nodes: ref(nodesList),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderOptions = mappedNodes.value[0]?.data?.render as CanvasNodeDefaultRender;
|
||||||
|
expect(renderOptions.options.tooltip).toBe('Waiting for you to submit the form');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show tooltip for trigger node with pinned data when workflow is running', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const triggerNode = mockNode({
|
||||||
|
name: 'Manual Trigger',
|
||||||
|
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
const nodesList = [triggerNode];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes: nodesList,
|
||||||
|
connections,
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.isWorkflowRunning = true;
|
||||||
|
workflowsStore.getWorkflowRunData = {};
|
||||||
|
workflowsStore.pinDataByNodeName.mockReturnValue([{ json: {} }]); // Node has pinned data
|
||||||
|
|
||||||
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
|
nodes: ref(nodesList),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderOptions = mappedNodes.value[0]?.data?.render as CanvasNodeDefaultRender;
|
||||||
|
expect(renderOptions.options.tooltip).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show tooltip when workflow is not running', () => {
|
||||||
|
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
const triggerNode = mockNode({
|
||||||
|
name: 'Manual Trigger',
|
||||||
|
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
const nodesList = [triggerNode];
|
||||||
|
const connections = {};
|
||||||
|
const workflowObject = createTestWorkflowObject({
|
||||||
|
nodes: nodesList,
|
||||||
|
connections,
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.isWorkflowRunning = false;
|
||||||
|
workflowsStore.getWorkflowRunData = {};
|
||||||
|
workflowsStore.pinDataByNodeName.mockReturnValue(undefined);
|
||||||
|
|
||||||
|
const { nodes: mappedNodes } = useCanvasMapping({
|
||||||
|
nodes: ref(nodesList),
|
||||||
|
connections: ref(connections),
|
||||||
|
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderOptions = mappedNodes.value[0]?.data?.render as CanvasNodeDefaultRender;
|
||||||
|
expect(renderOptions.options.tooltip).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('connections', () => {
|
describe('connections', () => {
|
||||||
it('should map connections to canvas connections', () => {
|
it('should map connections to canvas connections', () => {
|
||||||
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
||||||
|
|||||||
@@ -299,12 +299,13 @@ export function useCanvasMapping({
|
|||||||
if (
|
if (
|
||||||
!!node.disabled ||
|
!!node.disabled ||
|
||||||
(triggerNodeName !== undefined && triggerNodeName !== node.name) ||
|
(triggerNodeName !== undefined && triggerNodeName !== node.name) ||
|
||||||
!['new', 'unknown', 'waiting'].includes(nodeExecutionStatusById.value[node.id])
|
!['new', 'unknown', 'waiting'].includes(nodeExecutionStatusById.value[node.id]) ||
|
||||||
|
nodePinnedDataById.value[node.id]
|
||||||
) {
|
) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('eventTriggerDescription' in nodeTypeDescription) {
|
if (typeof nodeTypeDescription.eventTriggerDescription === 'string') {
|
||||||
const nodeName = i18n.shortNodeType(nodeTypeDescription.name);
|
const nodeName = i18n.shortNodeType(nodeTypeDescription.name);
|
||||||
const { eventTriggerDescription } = nodeTypeDescription;
|
const { eventTriggerDescription } = nodeTypeDescription;
|
||||||
acc[node.id] = i18n
|
acc[node.id] = i18n
|
||||||
|
|||||||
Reference in New Issue
Block a user