From 3ec586cb50f993c51ec802a0c787cfaf024e7dd0 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 28 Aug 2024 11:20:02 +0300 Subject: [PATCH 1/3] feat: allow sticky notes alongside fallback nodes --- .../src/components/canvas/WorkflowCanvas.vue | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/editor-ui/src/components/canvas/WorkflowCanvas.vue b/packages/editor-ui/src/components/canvas/WorkflowCanvas.vue index 19482bf699eb7..5853093e299ce 100644 --- a/packages/editor-ui/src/components/canvas/WorkflowCanvas.vue +++ b/packages/editor-ui/src/components/canvas/WorkflowCanvas.vue @@ -6,6 +6,7 @@ import type { IWorkflowDb } from '@/Interface'; import { useCanvasMapping } from '@/composables/useCanvasMapping'; import type { EventBus } from 'n8n-design-system'; import { createEventBus } from 'n8n-design-system'; +import { STICKY_NODE_TYPE } from '@/constants'; defineOptions({ inheritAttrs: false, @@ -32,9 +33,14 @@ const $style = useCssModule(); const workflow = toRef(props, 'workflow'); const workflowObject = toRef(props, 'workflowObject'); -const nodes = computed(() => - props.workflow.nodes.length > 0 ? props.workflow.nodes : props.fallbackNodes, -); +const nodes = computed(() => { + const stickyNoteNodes = props.workflow.nodes.filter((node) => node.type === STICKY_NODE_TYPE); + const nonStickyNoteNodes = props.workflow.nodes.filter((node) => node.type !== STICKY_NODE_TYPE); + + return nonStickyNoteNodes.length > 0 + ? props.workflow.nodes + : [...props.fallbackNodes, ...stickyNoteNodes]; +}); const connections = computed(() => props.workflow.connections); const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping({ From 72b2fe6b22fc96b1d704952d1a26d91345a62f0e Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 28 Aug 2024 11:59:13 +0300 Subject: [PATCH 2/3] test: add workflowcanvas tests --- packages/editor-ui/src/__tests__/mocks.ts | 4 +- .../components/canvas/WorkflowCanvas.spec.ts | 146 ++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 packages/editor-ui/src/components/canvas/WorkflowCanvas.spec.ts diff --git a/packages/editor-ui/src/__tests__/mocks.ts b/packages/editor-ui/src/__tests__/mocks.ts index dac9c9c4789d5..1ab9d3a42fed1 100644 --- a/packages/editor-ui/src/__tests__/mocks.ts +++ b/packages/editor-ui/src/__tests__/mocks.ts @@ -26,6 +26,7 @@ import { STICKY_NODE_TYPE, } from '@/constants'; import type { INodeUi, IWorkflowDb } from '@/Interface'; +import { CanvasNodeRenderType } from '@/types'; export const mockNode = ({ id = uuid(), @@ -94,6 +95,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: CanvasNodeRenderType.AddNodes, type: CanvasNodeRenderType.AddNodes }), mockNode({ name: 'End', type: NO_OP_NODE_TYPE }), ]; @@ -180,7 +182,7 @@ export function createTestNode(node: Partial = {}): INode { return { id: uuid(), name: 'Node', - type: 'n8n-nodes-base.test', + type: 'n8n-nodes-base.set', typeVersion: 1, position: [0, 0] as [number, number], parameters: {}, diff --git a/packages/editor-ui/src/components/canvas/WorkflowCanvas.spec.ts b/packages/editor-ui/src/components/canvas/WorkflowCanvas.spec.ts new file mode 100644 index 0000000000000..75932803d3d5e --- /dev/null +++ b/packages/editor-ui/src/components/canvas/WorkflowCanvas.spec.ts @@ -0,0 +1,146 @@ +import { waitFor } from '@testing-library/vue'; +import { createPinia, setActivePinia } from 'pinia'; +import WorkflowCanvas from '@/components/canvas/WorkflowCanvas.vue'; +import { createEventBus } from 'n8n-design-system'; +import { createCanvasNodeElement, createCanvasConnection } from '@/__tests__/data'; +import type { Workflow } from 'n8n-workflow'; +import { createComponentRenderer } from '@/__tests__/render'; +import { STICKY_NODE_TYPE } from '@/constants'; +import { CanvasNodeRenderType } from '@/types'; +import { + createTestNode, + createTestWorkflow, + createTestWorkflowObject, + defaultNodeDescriptions, +} from '@/__tests__/mocks'; +import { useNodeTypesStore } from '@/stores/nodeTypes.store'; + +const renderComponent = createComponentRenderer(WorkflowCanvas, { + props: { + id: 'canvas', + workflow: { + id: '1', + name: 'Test Workflow', + nodes: [], + connections: [], + }, + workflowObject: {} as Workflow, + eventBus: createEventBus(), + }, +}); + +beforeEach(() => { + const pinia = createPinia(); + setActivePinia(pinia); + + const nodeTypesStore = useNodeTypesStore(); + nodeTypesStore.setNodeTypes(defaultNodeDescriptions); +}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe('WorkflowCanvas', () => { + it('should initialize with default props', () => { + const { getByTestId } = renderComponent(); + + expect(getByTestId('canvas')).toBeVisible(); + }); + + it('should render nodes and connections', async () => { + const nodes = [ + createCanvasNodeElement({ id: '1', label: 'Node 1' }), + createCanvasNodeElement({ id: '2', label: 'Node 2' }), + ]; + const connections = [createCanvasConnection(nodes[0], nodes[1])]; + + const { container } = renderComponent({ + props: { + nodes, + connections, + }, + }); + + await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(2)); + + expect(container.querySelector(`[data-id="${nodes[0].id}"]`)).toBeInTheDocument(); + expect(container.querySelector(`[data-id="${nodes[1].id}"]`)).toBeInTheDocument(); + expect(container.querySelector(`[data-id="${connections[0].id}"]`)).toBeInTheDocument(); + }); + + it('should handle empty nodes and connections gracefully', async () => { + const { container } = renderComponent(); + + await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(0)); + expect(container.querySelectorAll('.vue-flow__connection')).toHaveLength(0); + }); + + it('should render fallback nodes when sticky nodes are present', async () => { + const stickyNodes = [createTestNode({ id: '2', name: 'Sticky Node', type: STICKY_NODE_TYPE })]; + const fallbackNodes = [ + createTestNode({ + id: CanvasNodeRenderType.AddNodes, + type: CanvasNodeRenderType.AddNodes, + name: CanvasNodeRenderType.AddNodes, + }), + ]; + + const workflow = createTestWorkflow({ + id: '1', + name: 'Test Workflow', + nodes: [...stickyNodes], + connections: {}, + }); + + const workflowObject = createTestWorkflowObject(workflow); + + const { container } = renderComponent({ + props: { + workflow, + workflowObject, + fallbackNodes, + }, + }); + + await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(2)); + + expect(container.querySelector(`[data-id="${stickyNodes[0].id}"]`)).toBeInTheDocument(); + expect(container.querySelector(`[data-id="${fallbackNodes[0].id}"]`)).toBeInTheDocument(); + }); + + it('should not render fallback nodes when non-sticky nodes are present', async () => { + const nonStickyNodes = [createTestNode({ id: '1', name: 'Non-Sticky Node 1' })]; + const stickyNodes = [createTestNode({ id: '2', name: 'Sticky Node', type: STICKY_NODE_TYPE })]; + const fallbackNodes = [ + createTestNode({ + id: CanvasNodeRenderType.AddNodes, + type: CanvasNodeRenderType.AddNodes, + name: CanvasNodeRenderType.AddNodes, + }), + ]; + + const workflow = createTestWorkflow({ + id: '1', + name: 'Test Workflow', + nodes: [...nonStickyNodes, ...stickyNodes], + connections: {}, + }); + + const workflowObject = createTestWorkflowObject(workflow); + + const { container } = renderComponent({ + props: { + workflow, + workflowObject, + fallbackNodes, + }, + }); + + await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(2)); + + expect(container.querySelector(`[data-id="${nonStickyNodes[0].id}"]`)).toBeInTheDocument(); + expect(container.querySelector(`[data-id="${stickyNodes[0].id}"]`)).toBeInTheDocument(); + expect(container.querySelector(`[data-id="${fallbackNodes[0].id}"]`)).not.toBeInTheDocument(); + }); +}); From 76ad45056714fb8a6502678120555848c42fa7b2 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 28 Aug 2024 12:50:35 +0300 Subject: [PATCH 3/3] fix: improve condition --- packages/editor-ui/src/components/canvas/WorkflowCanvas.vue | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/editor-ui/src/components/canvas/WorkflowCanvas.vue b/packages/editor-ui/src/components/canvas/WorkflowCanvas.vue index 5853093e299ce..237b2e29e209e 100644 --- a/packages/editor-ui/src/components/canvas/WorkflowCanvas.vue +++ b/packages/editor-ui/src/components/canvas/WorkflowCanvas.vue @@ -35,9 +35,8 @@ const workflowObject = toRef(props, 'workflowObject'); const nodes = computed(() => { const stickyNoteNodes = props.workflow.nodes.filter((node) => node.type === STICKY_NODE_TYPE); - const nonStickyNoteNodes = props.workflow.nodes.filter((node) => node.type !== STICKY_NODE_TYPE); - return nonStickyNoteNodes.length > 0 + return props.workflow.nodes.length > stickyNoteNodes.length ? props.workflow.nodes : [...props.fallbackNodes, ...stickyNoteNodes]; });