diff --git a/packages/frontend/editor-ui/src/__tests__/data/canvas.ts b/packages/frontend/editor-ui/src/__tests__/data/canvas.ts index 08330730c3..4230779894 100644 --- a/packages/frontend/editor-ui/src/__tests__/data/canvas.ts +++ b/packages/frontend/editor-ui/src/__tests__/data/canvas.ts @@ -55,7 +55,7 @@ export function createCanvasNodeData({ export function createCanvasNodeElement({ id = '1', - type = 'default', + type = 'canvas-node', label = 'Node', position = { x: 100, y: 100 }, data, diff --git a/packages/frontend/editor-ui/src/__tests__/mocks.ts b/packages/frontend/editor-ui/src/__tests__/mocks.ts index 6db800e785..a2827c2cfc 100644 --- a/packages/frontend/editor-ui/src/__tests__/mocks.ts +++ b/packages/frontend/editor-ui/src/__tests__/mocks.ts @@ -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 = [NodeConnectionType.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({ name, + icon, displayName: name, description: '', version, @@ -101,6 +105,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 }), ]; diff --git a/packages/frontend/editor-ui/src/components/TestDefinition/EditDefinition/NodesPinning.vue b/packages/frontend/editor-ui/src/components/TestDefinition/EditDefinition/NodesPinning.vue index 687f18006a..4cfdcdb86e 100644 --- a/packages/frontend/editor-ui/src/components/TestDefinition/EditDefinition/NodesPinning.vue +++ b/packages/frontend/editor-ui/src/components/TestDefinition/EditDefinition/NodesPinning.vue @@ -11,6 +11,7 @@ import { N8nTooltip } from '@n8n/design-system'; import { createEventBus } from '@n8n/utils/event-bus'; import { computed, onMounted, ref, useCssModule } from 'vue'; import { useRoute, useRouter } from 'vue-router'; +import type { INodeTypeDescription } from 'n8n-workflow'; const workflowsStore = useWorkflowsStore(); const nodeTypesStore = useNodeTypesStore(); @@ -51,6 +52,23 @@ const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping( connections, workflowObject, }); + +const nodeTypeDescriptions = computed(() => { + return nodes.value.reduce>((acc, node) => { + const key = `${node.type}@${node.typeVersion}`; + if (acc[key]) { + return acc; + } + + const nodeTypeDescription = nodeTypesStore.getNodeType(node.type, node.typeVersion); + if (nodeTypeDescription) { + acc[key] = nodeTypeDescription; + } + + return acc; + }, {}); +}); + async function loadData() { workflowsStore.resetState(); resetWorkspace(); @@ -141,6 +159,7 @@ onMounted(loadData); :class="{ [$style.canvas]: true }" :nodes="mappedNodes" :connections="mappedConnections" + :node-type-descriptions="nodeTypeDescriptions" :show-bug-reporting-button="false" :read-only="true" :event-bus="eventBus" diff --git a/packages/frontend/editor-ui/src/components/canvas/Canvas.test.ts b/packages/frontend/editor-ui/src/components/canvas/Canvas.test.ts index c40d75c6be..b8f01ec5e0 100644 --- a/packages/frontend/editor-ui/src/components/canvas/Canvas.test.ts +++ b/packages/frontend/editor-ui/src/components/canvas/Canvas.test.ts @@ -8,6 +8,8 @@ import { createCanvasConnection, createCanvasNodeElement } from '@/__tests__/dat import { NodeConnectionType } from 'n8n-workflow'; import type { useDeviceSupport } from '@n8n/composables/useDeviceSupport'; import { useVueFlow } from '@vue-flow/core'; +import { defaultNodeTypes, mockNodeTypeDescription } from '@/__tests__/mocks'; +import { SET_NODE_TYPE, SIMULATE_NODE_TYPE } from '@/constants'; const matchMedia = global.window.matchMedia; // @ts-expect-error Initialize window object @@ -273,4 +275,37 @@ describe('Canvas', () => { expect(patternCanvas?.innerHTML).not.toContain(' { + 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, + simulatedType: SET_NODE_TYPE, + }, + }), + ]; + + const nodeTypeDescriptions = { + [SET_NODE_TYPE]: mockNodeTypeDescription({ name: SET_NODE_TYPE, icon: 'fa:pen' }), + [`${SIMULATE_NODE_TYPE}@1`]: defaultNodeTypes[SIMULATE_NODE_TYPE].type.description, + }; + + const { container } = renderComponent({ + props: { + nodes, + nodeTypeDescriptions, + }, + }); + + await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(1)); + + expect(container.querySelector('.icon')).toBeInTheDocument(); + }); + }); }); diff --git a/packages/frontend/editor-ui/src/components/canvas/Canvas.vue b/packages/frontend/editor-ui/src/components/canvas/Canvas.vue index 918b0b06a3..0a2ac3c3d5 100644 --- a/packages/frontend/editor-ui/src/components/canvas/Canvas.vue +++ b/packages/frontend/editor-ui/src/components/canvas/Canvas.vue @@ -45,6 +45,7 @@ import { onKeyDown, onKeyUp, useThrottleFn } from '@vueuse/core'; import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue'; import CanvasBackground from './elements/background/CanvasBackground.vue'; import { useCanvasTraversal } from '@/composables/useCanvasTraversal'; +import type { INodeTypeDescription } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow'; import { useCanvasNodeHover } from '@/composables/useCanvasNodeHover'; import { useCanvasLayout } from '@/composables/useCanvasLayout'; @@ -97,6 +98,7 @@ const props = withDefaults( id?: string; nodes: CanvasNode[]; connections: CanvasConnection[]; + nodeTypeDescriptions?: Record; controlsPosition?: PanelPosition; eventBus?: EventBus; readOnly?: boolean; @@ -108,6 +110,7 @@ const props = withDefaults( id: 'canvas', nodes: () => [], connections: () => [], + nodeTypeDescriptions: () => ({}), controlsPosition: PanelPosition.BottomLeft, eventBus: () => createEventBus(), readOnly: false, @@ -804,6 +807,10 @@ provide(CanvasKey, { :event-bus="eventBus" :hovered="nodesHoveredById[nodeProps.id]" :nearby-hovered="nodeProps.id === hoveredTriggerNode.id.value" + :node-type-description=" + nodeTypeDescriptions[`${nodeProps.data.type}@${nodeProps.data.typeVersion}`] + " + :simulated-node-type-description="nodeTypeDescriptions[nodeProps.data.simulatedType]" @delete="onDeleteNode" @run="onRunNode" @select="onSelectNode" diff --git a/packages/frontend/editor-ui/src/components/canvas/WorkflowCanvas.vue b/packages/frontend/editor-ui/src/components/canvas/WorkflowCanvas.vue index b91c6dba37..cc037c41bc 100644 --- a/packages/frontend/editor-ui/src/components/canvas/WorkflowCanvas.vue +++ b/packages/frontend/editor-ui/src/components/canvas/WorkflowCanvas.vue @@ -1,13 +1,14 @@