feat(editor): Add support for Simulate nodes in new canvas (no-changelog) (#13675)

This commit is contained in:
Alex Grozav
2025-03-25 12:15:03 +02:00
committed by GitHub
parent 9e3bfe23f6
commit 30e2df3218
5 changed files with 69 additions and 8 deletions

View File

@@ -56,7 +56,7 @@ export function createCanvasNodeData({
export function createCanvasNodeElement({
id = '1',
type = 'default',
type = 'canvas-node',
label = 'Node',
position = { x: 100, y: 100 },
data,

View File

@@ -23,6 +23,7 @@ import {
MANUAL_TRIGGER_NODE_TYPE,
NO_OP_NODE_TYPE,
SET_NODE_TYPE,
SIMULATE_NODE_TYPE,
STICKY_NODE_TYPE,
} from '@/constants';
import type { INodeUi, IWorkflowDb } from '@/Interface';
@@ -50,6 +51,7 @@ export const mockNode = ({
export const mockNodeTypeDescription = ({
name = SET_NODE_TYPE,
icon = 'fa:pen',
version = 1,
credentials = [],
inputs = [NodeConnectionTypes.Main],
@@ -58,6 +60,7 @@ export const mockNodeTypeDescription = ({
properties = [],
}: {
name?: INodeTypeDescription['name'];
icon?: INodeTypeDescription['icon'];
version?: INodeTypeDescription['version'];
credentials?: INodeTypeDescription['credentials'];
inputs?: INodeTypeDescription['inputs'];
@@ -67,6 +70,7 @@ export const mockNodeTypeDescription = ({
} = {}) =>
mock<INodeTypeDescription>({
name,
icon,
displayName: name,
description: '',
version,
@@ -102,6 +106,7 @@ export const mockNodes = [
mockNode({ name: 'Chat Trigger', type: CHAT_TRIGGER_NODE_TYPE }),
mockNode({ name: 'Agent', type: AGENT_NODE_TYPE }),
mockNode({ name: 'Sticky', type: STICKY_NODE_TYPE }),
mockNode({ name: 'Simulate', type: SIMULATE_NODE_TYPE }),
mockNode({ name: CanvasNodeRenderType.AddNodes, type: CanvasNodeRenderType.AddNodes }),
mockNode({ name: 'End', type: NO_OP_NODE_TYPE }),
];

View File

@@ -8,6 +8,7 @@ import { createCanvasConnection, createCanvasNodeElement } from '@/__tests__/dat
import { NodeConnectionTypes } from 'n8n-workflow';
import type { useDeviceSupport } from '@n8n/composables/useDeviceSupport';
import { useVueFlow } from '@vue-flow/core';
import { SIMULATE_NODE_TYPE } from '@/constants';
const matchMedia = global.window.matchMedia;
// @ts-expect-error Initialize window object
@@ -273,4 +274,30 @@ describe('Canvas', () => {
expect(patternCanvas?.innerHTML).not.toContain('<circle');
});
});
describe('simulate', () => {
it('should render simulate node', async () => {
const nodes = [
createCanvasNodeElement({
id: '1',
label: 'Node',
position: { x: 200, y: 200 },
data: {
type: SIMULATE_NODE_TYPE,
typeVersion: 1,
},
}),
];
const { container } = renderComponent({
props: {
nodes,
},
});
await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(1));
expect(container.querySelector('.icon')).toBeInTheDocument();
});
});
});

View File

@@ -1,10 +1,7 @@
<script lang="ts" setup>
import ContextMenu from '@/components/ContextMenu/ContextMenu.vue';
import {
type CanvasLayoutEvent,
type CanvasLayoutSource,
useCanvasLayout,
} from '@/composables/useCanvasLayout';
import type { CanvasLayoutEvent, CanvasLayoutSource } from '@/composables/useCanvasLayout';
import { useCanvasLayout } from '@/composables/useCanvasLayout';
import { useCanvasNodeHover } from '@/composables/useCanvasNodeHover';
import { useCanvasTraversal } from '@/composables/useCanvasTraversal';
import { type ContextMenuAction, useContextMenu } from '@/composables/useContextMenu';

View File

@@ -44,7 +44,14 @@ import {
WAIT_INDEFINITELY,
} from 'n8n-workflow';
import type { INodeUi } from '@/Interface';
import { CUSTOM_API_CALL_KEY, FORM_NODE_TYPE, STICKY_NODE_TYPE, WAIT_NODE_TYPE } from '@/constants';
import {
CUSTOM_API_CALL_KEY,
FORM_NODE_TYPE,
SIMULATE_NODE_TYPE,
SIMULATE_TRIGGER_NODE_TYPE,
STICKY_NODE_TYPE,
WAIT_NODE_TYPE,
} from '@/constants';
import { sanitizeHtml } from '@/utils/htmlUtils';
import { MarkerType } from '@vue-flow/core';
import { useNodeHelpers } from './useNodeHelpers';
@@ -88,7 +95,12 @@ export function useCanvasMapping({
function createDefaultNodeRenderType(node: INodeUi): CanvasNodeDefaultRender {
const nodeType = nodeTypeDescriptionByNodeId.value[node.id];
const icon = getNodeIconSource(nodeType);
const icon = getNodeIconSource(
simulatedNodeTypeDescriptionByNodeId.value[node.id]
? simulatedNodeTypeDescriptionByNodeId.value[node.id]
: nodeType,
);
return {
type: CanvasNodeRenderType.Default,
options: {
@@ -514,6 +526,26 @@ export function useCanvasMapping({
);
});
const simulatedNodeTypeDescriptionByNodeId = computed(() => {
return nodes.value.reduce<Record<string, INodeTypeDescription | null>>((acc, node) => {
if ([SIMULATE_NODE_TYPE, SIMULATE_TRIGGER_NODE_TYPE].includes(node.type)) {
const icon = node.parameters?.icon as string;
const iconValue = workflowObject.value.expression.getSimpleParameterValue(
node,
icon,
'internal',
{},
);
if (iconValue && typeof iconValue === 'string') {
acc[node.id] = nodeTypesStore.getNodeType(iconValue);
}
}
return acc;
}, {});
});
const mappedNodes = computed<CanvasNode[]>(() => [
...nodes.value.map<CanvasNode>((node) => {
const inputConnections = workflowObject.value.connectionsByDestinationNode[node.name] ?? {};