Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(editor): Allow sticky notes alongside fallback nodes in new canvas (no-changelog) #10583

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/editor-ui/src/__tests__/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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 }),
];

Expand Down Expand Up @@ -180,7 +182,7 @@ export function createTestNode(node: Partial<INode> = {}): 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: {},
Expand Down
146 changes: 146 additions & 0 deletions packages/editor-ui/src/components/canvas/WorkflowCanvas.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
11 changes: 8 additions & 3 deletions packages/editor-ui/src/components/canvas/WorkflowCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -32,9 +33,13 @@ 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);

return props.workflow.nodes.length > stickyNoteNodes.length
? props.workflow.nodes
: [...props.fallbackNodes, ...stickyNoteNodes];
});
const connections = computed(() => props.workflow.connections);

const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping({
Expand Down
Loading