mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(editor): Fix canvas layouting when tab is not active (#17638)
This commit is contained in:
@@ -168,4 +168,168 @@ describe('useCanvasLayout', () => {
|
||||
expect(result).toMatchSnapshot();
|
||||
expect(matchesGrid(result)).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle nodes with missing dimensions', () => {
|
||||
const nodes = [
|
||||
createCanvasGraphNode({
|
||||
id: 'node1',
|
||||
dimensions: undefined,
|
||||
}),
|
||||
createCanvasGraphNode({
|
||||
id: 'node2',
|
||||
dimensions: { width: 0, height: 0 },
|
||||
}),
|
||||
createCanvasGraphNode({
|
||||
id: 'node3',
|
||||
dimensions: { width: 100, height: 100 },
|
||||
}),
|
||||
];
|
||||
|
||||
const connections: Array<[string, string]> = [
|
||||
['node1', 'node2'],
|
||||
['node2', 'node3'],
|
||||
];
|
||||
|
||||
const { layout } = createTestSetup(nodes, connections);
|
||||
const result = layout('all');
|
||||
|
||||
// Should complete without errors
|
||||
expect(result).toBeDefined();
|
||||
expect(result.nodes).toHaveLength(3);
|
||||
|
||||
// All nodes should have valid positions
|
||||
result.nodes.forEach((node) => {
|
||||
expect(node.x).toBeDefined();
|
||||
expect(node.y).toBeDefined();
|
||||
expect(typeof node.x).toBe('number');
|
||||
expect(typeof node.y).toBe('number');
|
||||
expect(isFinite(node.x)).toBe(true);
|
||||
expect(isFinite(node.y)).toBe(true);
|
||||
});
|
||||
|
||||
const node1 = result.nodes.find((n) => n.id === 'node1');
|
||||
const node2 = result.nodes.find((n) => n.id === 'node2');
|
||||
const node3 = result.nodes.find((n) => n.id === 'node3');
|
||||
|
||||
assert(node1);
|
||||
assert(node2);
|
||||
assert(node3);
|
||||
|
||||
// Nodes should be positioned in a logical order (node1 -> node2 -> node3)
|
||||
expect(node2.x).toBeGreaterThan(node1.x);
|
||||
expect(node3.x).toBeGreaterThan(node2.x);
|
||||
});
|
||||
|
||||
test('should calculate dimensions for configurable nodes with missing dimensions', () => {
|
||||
const nodes = [
|
||||
createCanvasGraphNode({
|
||||
id: 'configurableNode',
|
||||
data: {
|
||||
render: {
|
||||
type: CanvasNodeRenderType.Default,
|
||||
options: { configurable: true },
|
||||
},
|
||||
inputs: [
|
||||
{ type: 'main', index: 0 },
|
||||
{ type: 'main', index: 1 },
|
||||
{ type: 'ai_tool', index: 0 },
|
||||
],
|
||||
outputs: [{ type: 'main', index: 0 }],
|
||||
},
|
||||
dimensions: undefined,
|
||||
}),
|
||||
createCanvasGraphNode({
|
||||
id: 'configurationNode',
|
||||
data: {
|
||||
render: {
|
||||
type: CanvasNodeRenderType.Default,
|
||||
options: { configuration: true },
|
||||
},
|
||||
inputs: [{ type: 'main', index: 0 }],
|
||||
outputs: [{ type: 'main', index: 0 }],
|
||||
},
|
||||
dimensions: { width: 0, height: 0 },
|
||||
}),
|
||||
];
|
||||
|
||||
const connections: Array<[string, string]> = [['configurationNode', 'configurableNode']];
|
||||
|
||||
const { layout } = createTestSetup(nodes, connections);
|
||||
const result = layout('all');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.nodes).toHaveLength(2);
|
||||
|
||||
// All nodes should have valid positions
|
||||
result.nodes.forEach((node) => {
|
||||
expect(node.x).toBeDefined();
|
||||
expect(node.y).toBeDefined();
|
||||
expect(typeof node.x).toBe('number');
|
||||
expect(typeof node.y).toBe('number');
|
||||
expect(isFinite(node.x)).toBe(true);
|
||||
expect(isFinite(node.y)).toBe(true);
|
||||
});
|
||||
|
||||
// Both nodes should be positioned correctly
|
||||
const configNode = result.nodes.find((n) => n.id === 'configurationNode');
|
||||
const configurableNode = result.nodes.find((n) => n.id === 'configurableNode');
|
||||
|
||||
assert(configNode);
|
||||
assert(configurableNode);
|
||||
|
||||
expect(configNode).toBeDefined();
|
||||
expect(configurableNode).toBeDefined();
|
||||
|
||||
// The layout should work despite missing dimensions
|
||||
// The exact positioning depends on whether they're recognized as AI nodes
|
||||
expect(
|
||||
Math.abs(configNode.x - configurableNode.x) + Math.abs(configNode.y - configurableNode.y),
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should handle mixed scenarios with sticky notes and missing dimensions', () => {
|
||||
const nodes = [
|
||||
createCanvasGraphNode({
|
||||
id: 'node1',
|
||||
}),
|
||||
createCanvasGraphNode({
|
||||
id: 'node2',
|
||||
dimensions: { width: 100, height: 100 },
|
||||
}),
|
||||
createCanvasGraphNode({
|
||||
id: 'sticky',
|
||||
data: { type: STICKY_NODE_TYPE },
|
||||
dimensions: { width: 500, height: 400 },
|
||||
position: { x: 0, y: -100 },
|
||||
}),
|
||||
];
|
||||
|
||||
const connections: Array<[string, string]> = [['node1', 'node2']];
|
||||
|
||||
const { layout } = createTestSetup(nodes, connections);
|
||||
const result = layout('all');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
// Should include both regular nodes and sticky
|
||||
expect(result.nodes.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// All nodes should have valid positions
|
||||
result.nodes.forEach((node) => {
|
||||
expect(node.x).toBeDefined();
|
||||
expect(node.y).toBeDefined();
|
||||
expect(typeof node.x).toBe('number');
|
||||
expect(typeof node.y).toBe('number');
|
||||
expect(isFinite(node.x)).toBe(true);
|
||||
expect(isFinite(node.y)).toBe(true);
|
||||
});
|
||||
|
||||
// Non-sticky nodes should be positioned correctly
|
||||
const node1 = result.nodes.find((n) => n.id === 'node1');
|
||||
const node2 = result.nodes.find((n) => n.id === 'node2');
|
||||
|
||||
assert(node1);
|
||||
assert(node2);
|
||||
|
||||
expect(node2.x).toBeGreaterThan(node1.x);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
type CanvasNodeData,
|
||||
} from '../types';
|
||||
import { isPresent } from '../utils/typesUtils';
|
||||
import { DEFAULT_NODE_SIZE, GRID_SIZE } from '../utils/nodeViewUtils';
|
||||
import { DEFAULT_NODE_SIZE, GRID_SIZE, calculateNodeSize } from '../utils/nodeViewUtils';
|
||||
|
||||
export type CanvasLayoutOptions = { id?: string };
|
||||
export type CanvasLayoutTarget = 'selection' | 'all';
|
||||
@@ -85,6 +85,48 @@ export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
|
||||
return { x: edge.targetX, y: edge.targetY };
|
||||
}
|
||||
|
||||
function getNodeDimensions(node: GraphNode<CanvasNodeData>): { width: number; height: number } {
|
||||
// Check if dimensions exist and have valid values
|
||||
if (
|
||||
node.dimensions &&
|
||||
typeof node.dimensions.width === 'number' &&
|
||||
typeof node.dimensions.height === 'number' &&
|
||||
node.dimensions.width > 0 &&
|
||||
node.dimensions.height > 0
|
||||
) {
|
||||
return { width: node.dimensions.width, height: node.dimensions.height };
|
||||
}
|
||||
|
||||
// Calculate dimensions based on node data
|
||||
if (node.data && node.data.render) {
|
||||
const isConfiguration =
|
||||
node.data.render.type === CanvasNodeRenderType.Default &&
|
||||
node.data.render.options.configuration === true;
|
||||
const isConfigurable =
|
||||
node.data.render.type === CanvasNodeRenderType.Default &&
|
||||
node.data.render.options.configurable === true;
|
||||
|
||||
// Get input/output counts from node data
|
||||
const mainInputCount = node.data.inputs.filter((input) => input.type === 'main').length || 1;
|
||||
const mainOutputCount =
|
||||
node.data.outputs.filter((output) => output.type === 'main').length || 1;
|
||||
const nonMainInputCount =
|
||||
node.data.inputs.filter((input) => input.type !== 'main').length +
|
||||
node.data.outputs.filter((output) => output.type !== 'main').length;
|
||||
|
||||
return calculateNodeSize(
|
||||
isConfiguration,
|
||||
isConfigurable,
|
||||
mainInputCount,
|
||||
mainOutputCount,
|
||||
nonMainInputCount,
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to default size
|
||||
return { width: DEFAULT_NODE_SIZE[0], height: DEFAULT_NODE_SIZE[1] };
|
||||
}
|
||||
|
||||
function createDagreGraph({ nodes, edges }: CanvasLayoutTargetData) {
|
||||
const graph = new dagre.graphlib.Graph();
|
||||
graph.setDefaultEdgeLabel(() => ({}));
|
||||
@@ -96,8 +138,10 @@ export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
|
||||
|
||||
const nodeIdSet = new Set(nodes.map((node) => node.id));
|
||||
|
||||
graphNodes.forEach(({ id: nodeId, position: { x, y }, dimensions: { width, height } }) => {
|
||||
graph.setNode(nodeId, { width, height, x, y });
|
||||
graphNodes.forEach((node) => {
|
||||
const { width, height } = getNodeDimensions(node);
|
||||
const { x, y } = node.position;
|
||||
graph.setNode(node.id, { width, height, x, y });
|
||||
});
|
||||
|
||||
edges
|
||||
@@ -221,11 +265,12 @@ export function useCanvasLayout({ id: canvasId }: CanvasLayoutOptions = {}) {
|
||||
}
|
||||
|
||||
function boundingBoxFromCanvasNode(node: GraphNode<CanvasNodeData>): BoundingBox {
|
||||
const { width, height } = getNodeDimensions(node);
|
||||
return {
|
||||
x: node.position.x,
|
||||
y: node.position.y,
|
||||
width: node.dimensions.width,
|
||||
height: node.dimensions.height,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user