diff --git a/packages/frontend/editor-ui/src/composables/useCanvasLayout.test.ts b/packages/frontend/editor-ui/src/composables/useCanvasLayout.test.ts index 57cbb3c5d2..9eab5f56f8 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasLayout.test.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasLayout.test.ts @@ -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); + }); }); diff --git a/packages/frontend/editor-ui/src/composables/useCanvasLayout.ts b/packages/frontend/editor-ui/src/composables/useCanvasLayout.ts index 6476bb682c..756b22cb0d 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasLayout.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasLayout.ts @@ -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): { 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): 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, }; }