Skip to content

Commit 575db52

Browse files
fix: excalidraw arrow binding on drag (#24)
* fix: bind arrows to nodes in base generator * fix: attach special-case arrows to their elements * test: normalize boundElements in integration snapshots * chore: fix eslint formatting
1 parent 3019264 commit 575db52

File tree

7 files changed

+61
-8
lines changed

7 files changed

+61
-8
lines changed

src/generators/generators/__tests__/base-node.generator.test.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { BaseNodeGenerator } from '../base-node.generator';
55
import { ExecutionPlanNode } from '../../../types/execution-plan.types';
66
import { NodeInfo } from '../../types/node-info.types';
77
import { GenerationContext } from '../../types/generation-context.types';
8-
import { ExcalidrawElement } from '../../../types/excalidraw.types';
8+
import { ExcalidrawArrow, ExcalidrawElement } from '../../../types/excalidraw.types';
99

1010
/**
1111
* Test implementation to test createOperatorText protected method
@@ -234,6 +234,29 @@ describe('BaseNodeGenerator', () => {
234234
// Should create arrows with column labels if applicable
235235
TestHelpers.assertHasArrows(result);
236236
});
237+
238+
it('should bind arrows to connected elements', () => {
239+
const childNode = NodeBuilder.createSimpleNode('ChildOp');
240+
const node = NodeBuilder.createNodeWithChildren('ParentOp', [childNode]);
241+
const result = generator.generate(node);
242+
243+
const arrows = TestHelpers.getArrows(result.elements) as ExcalidrawArrow[];
244+
expect(arrows.length).toBe(1);
245+
const arrow = arrows[0];
246+
const rectangles = TestHelpers.getRectangles(result.elements);
247+
248+
const parentRect = rectangles.find((rect) => rect.id === arrow.endBinding?.elementId);
249+
const childRect = rectangles.find((rect) => rect.id === arrow.startBinding?.elementId);
250+
251+
expect(parentRect).toBeDefined();
252+
expect(childRect).toBeDefined();
253+
254+
const parentBindings = parentRect?.boundElements ?? [];
255+
const childBindings = childRect?.boundElements ?? [];
256+
257+
expect(parentBindings).toEqual(expect.arrayContaining([{ id: arrow.id, type: 'arrow' }]));
258+
expect(childBindings).toEqual(expect.arrayContaining([{ id: arrow.id, type: 'arrow' }]));
259+
});
237260
});
238261

239262
describe('createArrowsToParent', () => {
@@ -311,4 +334,3 @@ describe('BaseNodeGenerator', () => {
311334
});
312335
});
313336
});
314-

src/generators/generators/base-node.generator.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,30 @@ export abstract class BaseNodeGenerator implements NodeGeneratorStrategy {
167167
);
168168
}
169169

170+
/**
171+
* Binds an arrow to the connected elements so Excalidraw keeps them attached when moved
172+
*/
173+
protected bindArrowToElements(
174+
context: GenerationContext,
175+
arrowId: string,
176+
elementIds: string[]
177+
): void {
178+
for (const element of context.elements) {
179+
if (!elementIds.includes(element.id)) {
180+
continue;
181+
}
182+
183+
if (!element.boundElements) {
184+
element.boundElements = [];
185+
}
186+
187+
const alreadyBound = element.boundElements.some((binding) => binding.id === arrowId);
188+
if (!alreadyBound) {
189+
element.boundElements.push({ id: arrowId, type: 'arrow' });
190+
}
191+
}
192+
}
193+
170194
/**
171195
* Helper method for creating arrows with ellipsis
172196
* Uses ArrowPositionCalculator and ColumnLabelRenderer for consistent behavior
@@ -201,6 +225,7 @@ export abstract class BaseNodeGenerator implements NodeGeneratorStrategy {
201225
strokeColor: context.config.arrowColor,
202226
});
203227
context.elements.push(arrow);
228+
this.bindArrowToElements(context, arrowId, [childRectId, parentRectId]);
204229
}
205230

206231
// Add "..." text if needed
@@ -238,6 +263,7 @@ export abstract class BaseNodeGenerator implements NodeGeneratorStrategy {
238263
strokeColor: context.config.arrowColor,
239264
});
240265
context.elements.push(arrow);
266+
this.bindArrowToElements(context, arrowId, [childRectId, parentRectId]);
241267
}
242268
}
243269

@@ -353,4 +379,3 @@ export abstract class BaseNodeGenerator implements NodeGeneratorStrategy {
353379
return baseHeight;
354380
}
355381
}
356-

src/generators/generators/data-source-node.generator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ export class DataSourceNodeGenerator extends BaseNodeGenerator {
383383
strokeColor: context.config.arrowColor,
384384
});
385385
context.elements.push(arrow);
386+
this.bindArrowToElements(context, arrowId, [arrowStartElementId, rectId]);
386387
}
387388

388389
// Create projection text element at the middle of the edges (arrows)
@@ -543,4 +544,3 @@ export class DataSourceNodeGenerator extends BaseNodeGenerator {
543544
};
544545
}
545546
}
546-

src/generators/generators/hash-join-node.generator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ export class HashJoinNodeGenerator extends BaseNodeGenerator {
193193
strokeColor: context.config.arrowColor,
194194
});
195195
context.elements.push(arrow);
196+
this.bindArrowToElements(context, arrowId, [buildSideInfo.rectId, hashTableId]);
196197
}
197198

198199
// Display columns on arrows from build side (using build side's columns and sort order)
@@ -306,6 +307,7 @@ export class HashJoinNodeGenerator extends BaseNodeGenerator {
306307
strokeColor: context.config.arrowColor,
307308
});
308309
context.elements.push(arrow);
310+
this.bindArrowToElements(context, arrowId, [probeSideInfo.rectId, hashTableId]);
309311
}
310312

311313
// Display columns on arrows from probe side (using probe side's columns and sort order)
@@ -411,4 +413,3 @@ export class HashJoinNodeGenerator extends BaseNodeGenerator {
411413
};
412414
}
413415
}
414-

src/generators/generators/sort-merge-join-node.generator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ export class SortMergeJoinNodeGenerator extends BaseNodeGenerator {
188188
strokeColor: context.config.arrowColor,
189189
});
190190
context.elements.push(arrow);
191+
this.bindArrowToElements(context, arrowId, [leftSideInfo.rectId, rectId]);
191192
}
192193

193194
// Display columns on arrows from left side (using left side's columns and sort order)
@@ -277,6 +278,7 @@ export class SortMergeJoinNodeGenerator extends BaseNodeGenerator {
277278
strokeColor: context.config.arrowColor,
278279
});
279280
context.elements.push(arrow);
281+
this.bindArrowToElements(context, arrowId, [rightSideInfo.rectId, rectId]);
280282
}
281283

282284
// Display columns on arrows from right side (using right side's columns and sort order)
@@ -432,4 +434,3 @@ export class SortMergeJoinNodeGenerator extends BaseNodeGenerator {
432434
};
433435
}
434436
}
435-

src/generators/generators/union-node.generator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ export class UnionNodeGenerator extends BaseNodeGenerator {
241241
strokeColor: context.config.arrowColor,
242242
});
243243
context.elements.push(arrow);
244+
this.bindArrowToElements(context, arrowId, [childInfo.rectId, rectId]);
244245
}
245246

246247
// Add column labels if available (once per child, not per arrow)
@@ -324,4 +325,3 @@ export class UnionNodeGenerator extends BaseNodeGenerator {
324325
};
325326
}
326327
}
327-

tests/integration.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ function normalizeExcalidraw(data: ExcalidrawData): ExcalidrawData {
3939
normalizedElement.groupIds = [];
4040
}
4141

42+
// Remove boundElements because bindings depend on generated IDs
43+
if (normalizedElement.boundElements) {
44+
normalizedElement.boundElements = [];
45+
}
46+
4247
return normalizedElement;
4348
});
4449

@@ -123,4 +128,3 @@ describe('Examples Integration Tests', () => {
123128
expect(orphanedExpected).toEqual([]);
124129
});
125130
});
126-

0 commit comments

Comments
 (0)