fix(editor): Fix canvas layouting when tab is not active (#17638)

This commit is contained in:
oleg
2025-07-28 10:54:59 +02:00
committed by GitHub
parent 2043c6da53
commit 2df76e020e
2 changed files with 214 additions and 5 deletions

View File

@@ -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);
});
});

View File

@@ -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,
};
}