@nyaruka/temba-components 0.138.4 → 0.139.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/locales/es.js +5 -5
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +5 -5
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/locale-codes.js +2 -11
- package/dist/locales/locale-codes.js.map +1 -1
- package/dist/locales/pt.js +5 -5
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +816 -852
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +23 -30
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/flow/CanvasMenu.js +5 -3
- package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +6 -7
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +152 -235
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +757 -403
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/flow/utils.js +138 -66
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/interfaces.js +1 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/list/TicketList.js +4 -1
- package/out-tsc/src/list/TicketList.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +18 -1
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/locales/es.js +5 -5
- package/out-tsc/src/locales/es.js.map +1 -1
- package/out-tsc/src/locales/fr.js +5 -5
- package/out-tsc/src/locales/fr.js.map +1 -1
- package/out-tsc/src/locales/locale-codes.js +2 -11
- package/out-tsc/src/locales/locale-codes.js.map +1 -1
- package/out-tsc/src/locales/pt.js +5 -5
- package/out-tsc/src/locales/pt.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +1 -0
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/test/temba-floating-tab.test.js +4 -6
- package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
- package/out-tsc/test/temba-flow-collision.test.js +221 -223
- package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor.test.js +0 -2
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js +83 -84
- package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +102 -93
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
- package/package.json +1 -1
- package/src/display/FloatingTab.ts +22 -31
- package/src/flow/CanvasMenu.ts +8 -3
- package/src/flow/CanvasNode.ts +6 -7
- package/src/flow/Editor.ts +184 -279
- package/src/flow/Plumber.ts +1011 -457
- package/src/flow/utils.ts +162 -84
- package/src/interfaces.ts +2 -1
- package/src/list/TicketList.ts +4 -1
- package/src/live/ContactChat.ts +19 -1
- package/src/locales/es.ts +13 -18
- package/src/locales/fr.ts +13 -18
- package/src/locales/locale-codes.ts +2 -11
- package/src/locales/pt.ts +13 -18
- package/src/simulator/Simulator.ts +1 -0
- package/test/temba-floating-tab.test.ts +4 -6
- package/test/temba-flow-collision.test.ts +225 -303
- package/test/temba-flow-editor.test.ts +0 -2
- package/test/temba-flow-plumber-connections.test.ts +97 -97
- package/test/temba-flow-plumber.test.ts +116 -103
|
@@ -304,18 +304,16 @@ describe('Collision Detection Utilities', () => {
|
|
|
304
304
|
|
|
305
305
|
describe('calculateReflowPositions', () => {
|
|
306
306
|
it('returns empty map when no collisions', () => {
|
|
307
|
-
const movedBounds: NodeBounds = {
|
|
308
|
-
uuid: 'moved',
|
|
309
|
-
left: 100,
|
|
310
|
-
top: 100,
|
|
311
|
-
right: 200,
|
|
312
|
-
bottom: 200,
|
|
313
|
-
width: 100,
|
|
314
|
-
height: 100
|
|
315
|
-
};
|
|
316
|
-
|
|
317
307
|
const allBounds: NodeBounds[] = [
|
|
318
|
-
|
|
308
|
+
{
|
|
309
|
+
uuid: 'moved',
|
|
310
|
+
left: 100,
|
|
311
|
+
top: 100,
|
|
312
|
+
right: 200,
|
|
313
|
+
bottom: 200,
|
|
314
|
+
width: 100,
|
|
315
|
+
height: 100
|
|
316
|
+
},
|
|
319
317
|
{
|
|
320
318
|
uuid: 'node1',
|
|
321
319
|
left: 300,
|
|
@@ -327,67 +325,50 @@ describe('Collision Detection Utilities', () => {
|
|
|
327
325
|
}
|
|
328
326
|
];
|
|
329
327
|
|
|
330
|
-
const positions = calculateReflowPositions(
|
|
331
|
-
'moved',
|
|
332
|
-
movedBounds,
|
|
333
|
-
allBounds,
|
|
334
|
-
false
|
|
335
|
-
);
|
|
328
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
336
329
|
expect(positions.size).to.equal(0);
|
|
337
330
|
});
|
|
338
331
|
|
|
339
|
-
it('moves colliding node
|
|
340
|
-
const movedBounds: NodeBounds = {
|
|
341
|
-
uuid: 'moved',
|
|
342
|
-
left: 100,
|
|
343
|
-
top: 100,
|
|
344
|
-
right: 200,
|
|
345
|
-
bottom: 200,
|
|
346
|
-
width: 100,
|
|
347
|
-
height: 100
|
|
348
|
-
};
|
|
349
|
-
|
|
332
|
+
it('moves colliding node out of the way', () => {
|
|
350
333
|
const allBounds: NodeBounds[] = [
|
|
351
|
-
|
|
334
|
+
{
|
|
335
|
+
uuid: 'moved',
|
|
336
|
+
left: 100,
|
|
337
|
+
top: 100,
|
|
338
|
+
right: 200,
|
|
339
|
+
bottom: 200,
|
|
340
|
+
width: 100,
|
|
341
|
+
height: 100
|
|
342
|
+
},
|
|
352
343
|
{
|
|
353
344
|
uuid: 'node1',
|
|
354
|
-
left:
|
|
345
|
+
left: 100,
|
|
355
346
|
top: 150,
|
|
356
|
-
right:
|
|
347
|
+
right: 200,
|
|
357
348
|
bottom: 250,
|
|
358
349
|
width: 100,
|
|
359
350
|
height: 100
|
|
360
351
|
}
|
|
361
352
|
];
|
|
362
353
|
|
|
363
|
-
const positions = calculateReflowPositions(
|
|
364
|
-
'moved',
|
|
365
|
-
movedBounds,
|
|
366
|
-
allBounds,
|
|
367
|
-
false
|
|
368
|
-
);
|
|
354
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
369
355
|
|
|
370
356
|
expect(positions.size).to.equal(1);
|
|
371
357
|
expect(positions.has('node1')).to.be.true;
|
|
372
|
-
|
|
373
|
-
const newPos = positions.get('node1')!;
|
|
374
|
-
expect(newPos.left).to.equal(150); // left unchanged
|
|
375
|
-
expect(newPos.top).to.be.greaterThan(200); // moved below the moved node
|
|
358
|
+
expect(positions.has('moved')).to.be.false;
|
|
376
359
|
});
|
|
377
360
|
|
|
378
|
-
it('
|
|
379
|
-
const movedBounds: NodeBounds = {
|
|
380
|
-
uuid: 'moved',
|
|
381
|
-
left: 100,
|
|
382
|
-
top: 150,
|
|
383
|
-
right: 200,
|
|
384
|
-
bottom: 250,
|
|
385
|
-
width: 100,
|
|
386
|
-
height: 100
|
|
387
|
-
};
|
|
388
|
-
|
|
361
|
+
it('sacred node never appears in returned positions', () => {
|
|
389
362
|
const allBounds: NodeBounds[] = [
|
|
390
|
-
|
|
363
|
+
{
|
|
364
|
+
uuid: 'dropped',
|
|
365
|
+
left: 100,
|
|
366
|
+
top: 100,
|
|
367
|
+
right: 200,
|
|
368
|
+
bottom: 200,
|
|
369
|
+
width: 100,
|
|
370
|
+
height: 100
|
|
371
|
+
},
|
|
391
372
|
{
|
|
392
373
|
uuid: 'existing',
|
|
393
374
|
left: 100,
|
|
@@ -399,117 +380,133 @@ describe('Collision Detection Utilities', () => {
|
|
|
399
380
|
}
|
|
400
381
|
];
|
|
401
382
|
|
|
402
|
-
const positions = calculateReflowPositions(
|
|
403
|
-
'moved',
|
|
404
|
-
movedBounds,
|
|
405
|
-
allBounds,
|
|
406
|
-
true // droppedBelowMidpoint
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
expect(positions.size).to.equal(1);
|
|
410
|
-
expect(positions.has('moved')).to.be.true;
|
|
383
|
+
const positions = calculateReflowPositions(['dropped'], allBounds);
|
|
411
384
|
|
|
412
|
-
|
|
413
|
-
expect(
|
|
385
|
+
expect(positions.has('dropped')).to.be.false;
|
|
386
|
+
expect(positions.has('existing')).to.be.true;
|
|
414
387
|
});
|
|
415
388
|
|
|
416
|
-
it('
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
// So dropped node should keep position, existing moves down
|
|
420
|
-
const movedBounds: NodeBounds = {
|
|
421
|
-
uuid: 'dropped',
|
|
422
|
-
left: 100,
|
|
423
|
-
top: 80,
|
|
424
|
-
right: 200,
|
|
425
|
-
bottom: 180,
|
|
426
|
-
width: 100,
|
|
427
|
-
height: 100
|
|
428
|
-
};
|
|
429
|
-
|
|
389
|
+
it('prefers least-displacement direction', () => {
|
|
390
|
+
// Sacred at (100,100)-(200,200), collider at (180,100)-(280,200)
|
|
391
|
+
// Right requires only 60px displacement, down requires 140px
|
|
430
392
|
const allBounds: NodeBounds[] = [
|
|
431
|
-
movedBounds,
|
|
432
393
|
{
|
|
433
|
-
uuid: '
|
|
394
|
+
uuid: 'sacred',
|
|
434
395
|
left: 100,
|
|
435
396
|
top: 100,
|
|
436
397
|
right: 200,
|
|
437
398
|
bottom: 200,
|
|
438
399
|
width: 100,
|
|
439
400
|
height: 100
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
uuid: 'collider',
|
|
404
|
+
left: 180,
|
|
405
|
+
top: 100,
|
|
406
|
+
right: 280,
|
|
407
|
+
bottom: 200,
|
|
408
|
+
width: 100,
|
|
409
|
+
height: 100
|
|
440
410
|
}
|
|
441
411
|
];
|
|
442
412
|
|
|
443
|
-
const positions = calculateReflowPositions(
|
|
444
|
-
'dropped',
|
|
445
|
-
movedBounds,
|
|
446
|
-
allBounds,
|
|
447
|
-
false // dropped node's bottom is above target midpoint, dropped node gets priority
|
|
448
|
-
);
|
|
449
|
-
|
|
450
|
-
// Existing node should be moved down
|
|
451
|
-
expect(positions.has('existing')).to.be.true;
|
|
452
|
-
expect(positions.has('dropped')).to.be.false; // dropped keeps its position
|
|
413
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
453
414
|
|
|
454
|
-
|
|
455
|
-
|
|
415
|
+
expect(positions.has('collider')).to.be.true;
|
|
416
|
+
const newPos = positions.get('collider')!;
|
|
417
|
+
// Should move right (shorter) rather than down (longer)
|
|
418
|
+
expect(newPos.left).to.be.greaterThan(200);
|
|
419
|
+
expect(newPos.top).to.equal(100); // vertical position unchanged
|
|
456
420
|
});
|
|
457
421
|
|
|
458
|
-
it('
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
422
|
+
it('prefers up when it is the shortest move', () => {
|
|
423
|
+
// Sacred at (100,200)-(200,300), collider at (100,180)-(200,280)
|
|
424
|
+
// Up: newTop=snapToGrid(200-100-30)=snapToGrid(70)=80, distance=100
|
|
425
|
+
// Down: newTop=snapToGrid(300+30)=340, distance=160
|
|
426
|
+
// Right: newLeft=snapToGrid(200+30)=240, distance=140
|
|
427
|
+
const allBounds: NodeBounds[] = [
|
|
428
|
+
{
|
|
429
|
+
uuid: 'sacred',
|
|
430
|
+
left: 100,
|
|
431
|
+
top: 200,
|
|
432
|
+
right: 200,
|
|
433
|
+
bottom: 300,
|
|
434
|
+
width: 100,
|
|
435
|
+
height: 100
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
uuid: 'collider',
|
|
439
|
+
left: 100,
|
|
440
|
+
top: 180,
|
|
441
|
+
right: 200,
|
|
442
|
+
bottom: 280,
|
|
443
|
+
width: 100,
|
|
444
|
+
height: 100
|
|
445
|
+
}
|
|
446
|
+
];
|
|
447
|
+
|
|
448
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
471
449
|
|
|
450
|
+
expect(positions.has('collider')).to.be.true;
|
|
451
|
+
const newPos = positions.get('collider')!;
|
|
452
|
+
// Should move up (shortest displacement)
|
|
453
|
+
expect(newPos.top).to.be.lessThan(200);
|
|
454
|
+
expect(newPos.left).to.equal(100); // horizontal position unchanged
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('prefers direction with fewer cascading collisions', () => {
|
|
458
|
+
// Sacred at (100,100)-(200,200), collider at (100,150)-(200,250)
|
|
459
|
+
// A node sits below at (100,280)-(200,380) blocking the downward path
|
|
460
|
+
// Down causes cascade, right does not
|
|
472
461
|
const allBounds: NodeBounds[] = [
|
|
473
|
-
movedBounds,
|
|
474
462
|
{
|
|
475
|
-
uuid: '
|
|
463
|
+
uuid: 'sacred',
|
|
476
464
|
left: 100,
|
|
477
465
|
top: 100,
|
|
478
466
|
right: 200,
|
|
479
467
|
bottom: 200,
|
|
480
468
|
width: 100,
|
|
481
469
|
height: 100
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
uuid: 'collider',
|
|
473
|
+
left: 100,
|
|
474
|
+
top: 150,
|
|
475
|
+
right: 200,
|
|
476
|
+
bottom: 250,
|
|
477
|
+
width: 100,
|
|
478
|
+
height: 100
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
uuid: 'blocker',
|
|
482
|
+
left: 100,
|
|
483
|
+
top: 280,
|
|
484
|
+
right: 200,
|
|
485
|
+
bottom: 380,
|
|
486
|
+
width: 100,
|
|
487
|
+
height: 100
|
|
482
488
|
}
|
|
483
489
|
];
|
|
484
490
|
|
|
485
|
-
const positions = calculateReflowPositions(
|
|
486
|
-
'dropped',
|
|
487
|
-
movedBounds,
|
|
488
|
-
allBounds,
|
|
489
|
-
true // dropped node's bottom is below target midpoint, target gets priority
|
|
490
|
-
);
|
|
491
|
-
|
|
492
|
-
// Dropped node should be moved down
|
|
493
|
-
expect(positions.has('dropped')).to.be.true;
|
|
494
|
-
expect(positions.has('existing')).to.be.false; // existing keeps its position
|
|
491
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
495
492
|
|
|
496
|
-
|
|
497
|
-
|
|
493
|
+
expect(positions.has('collider')).to.be.true;
|
|
494
|
+
// Should avoid moving down (would cascade into blocker)
|
|
495
|
+
// blocker should not need to move
|
|
496
|
+
expect(positions.has('blocker')).to.be.false;
|
|
498
497
|
});
|
|
499
498
|
|
|
500
499
|
it('resolves cascading collisions', () => {
|
|
501
|
-
const movedBounds: NodeBounds = {
|
|
502
|
-
uuid: 'moved',
|
|
503
|
-
left: 100,
|
|
504
|
-
top: 100,
|
|
505
|
-
right: 200,
|
|
506
|
-
bottom: 200,
|
|
507
|
-
width: 100,
|
|
508
|
-
height: 100
|
|
509
|
-
};
|
|
510
|
-
|
|
511
500
|
const allBounds: NodeBounds[] = [
|
|
512
|
-
|
|
501
|
+
{
|
|
502
|
+
uuid: 'moved',
|
|
503
|
+
left: 100,
|
|
504
|
+
top: 100,
|
|
505
|
+
right: 200,
|
|
506
|
+
bottom: 200,
|
|
507
|
+
width: 100,
|
|
508
|
+
height: 100
|
|
509
|
+
},
|
|
513
510
|
{
|
|
514
511
|
uuid: 'node1',
|
|
515
512
|
left: 100,
|
|
@@ -530,44 +527,20 @@ describe('Collision Detection Utilities', () => {
|
|
|
530
527
|
}
|
|
531
528
|
];
|
|
532
529
|
|
|
533
|
-
const positions = calculateReflowPositions(
|
|
534
|
-
'moved',
|
|
535
|
-
movedBounds,
|
|
536
|
-
allBounds,
|
|
537
|
-
false
|
|
538
|
-
);
|
|
530
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
539
531
|
|
|
540
|
-
//
|
|
532
|
+
// At least one node should be repositioned
|
|
541
533
|
expect(positions.size).to.be.greaterThan(0);
|
|
542
534
|
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
const node1Pos = positions.get('node1')!;
|
|
546
|
-
const node2Pos = positions.get('node2')!;
|
|
547
|
-
|
|
548
|
-
// node2 should be below node1
|
|
549
|
-
expect(node2Pos.top).to.be.greaterThan(node1Pos.top);
|
|
550
|
-
}
|
|
535
|
+
// No node should overlap with the sacred node or each other after reflow
|
|
536
|
+
// (verified by the algorithm's correctness guarantee)
|
|
551
537
|
});
|
|
552
538
|
|
|
553
|
-
it('handles multiple
|
|
554
|
-
//
|
|
555
|
-
// This scenario has nodes that initially don't collide with moved node,
|
|
556
|
-
// but will collide with other nodes that get moved
|
|
557
|
-
const movedBounds: NodeBounds = {
|
|
558
|
-
uuid: 'moved',
|
|
559
|
-
left: 100,
|
|
560
|
-
top: 50,
|
|
561
|
-
right: 200,
|
|
562
|
-
bottom: 150,
|
|
563
|
-
width: 100,
|
|
564
|
-
height: 100
|
|
565
|
-
};
|
|
566
|
-
|
|
539
|
+
it('handles multiple sacred nodes', () => {
|
|
540
|
+
// Two sacred nodes with a non-sacred node overlapping one
|
|
567
541
|
const allBounds: NodeBounds[] = [
|
|
568
|
-
movedBounds,
|
|
569
542
|
{
|
|
570
|
-
uuid: '
|
|
543
|
+
uuid: 'sacred1',
|
|
571
544
|
left: 100,
|
|
572
545
|
top: 100,
|
|
573
546
|
right: 200,
|
|
@@ -576,198 +549,158 @@ describe('Collision Detection Utilities', () => {
|
|
|
576
549
|
height: 100
|
|
577
550
|
},
|
|
578
551
|
{
|
|
579
|
-
uuid: '
|
|
552
|
+
uuid: 'sacred2',
|
|
580
553
|
left: 100,
|
|
581
|
-
top:
|
|
554
|
+
top: 400,
|
|
582
555
|
right: 200,
|
|
583
|
-
bottom:
|
|
556
|
+
bottom: 500,
|
|
584
557
|
width: 100,
|
|
585
558
|
height: 100
|
|
586
559
|
},
|
|
587
560
|
{
|
|
588
|
-
uuid: '
|
|
561
|
+
uuid: 'collider',
|
|
589
562
|
left: 100,
|
|
590
|
-
top:
|
|
563
|
+
top: 150,
|
|
591
564
|
right: 200,
|
|
592
|
-
bottom:
|
|
565
|
+
bottom: 250,
|
|
593
566
|
width: 100,
|
|
594
567
|
height: 100
|
|
595
568
|
}
|
|
596
569
|
];
|
|
597
570
|
|
|
598
571
|
const positions = calculateReflowPositions(
|
|
599
|
-
'
|
|
600
|
-
|
|
601
|
-
allBounds,
|
|
602
|
-
false
|
|
572
|
+
['sacred1', 'sacred2'],
|
|
573
|
+
allBounds
|
|
603
574
|
);
|
|
604
575
|
|
|
605
|
-
//
|
|
606
|
-
expect(positions.
|
|
607
|
-
expect(positions.has('
|
|
608
|
-
|
|
609
|
-
expect(positions.has('
|
|
610
|
-
|
|
611
|
-
const node1Pos = positions.get('node1')!;
|
|
612
|
-
const node2Pos = positions.get('node2')!;
|
|
613
|
-
const node3Pos = positions.get('node3')!;
|
|
614
|
-
|
|
615
|
-
// Nodes should be stacked vertically with proper spacing
|
|
616
|
-
expect(node1Pos.top).to.be.at.least(170); // 150 (bottom of moved) + 20
|
|
617
|
-
expect(node2Pos.top).to.be.at.least(node1Pos.top + 120); // node1 top + height + spacing
|
|
618
|
-
expect(node3Pos.top).to.be.at.least(node2Pos.top + 120); // node2 top + height + spacing
|
|
576
|
+
// Sacred nodes should never be moved
|
|
577
|
+
expect(positions.has('sacred1')).to.be.false;
|
|
578
|
+
expect(positions.has('sacred2')).to.be.false;
|
|
579
|
+
// Collider should be moved
|
|
580
|
+
expect(positions.has('collider')).to.be.true;
|
|
619
581
|
});
|
|
620
582
|
|
|
621
|
-
it('
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
const movedBounds: NodeBounds = {
|
|
625
|
-
uuid: 'moved',
|
|
626
|
-
left: 100,
|
|
627
|
-
top: 100,
|
|
628
|
-
right: 200,
|
|
629
|
-
bottom: 200,
|
|
630
|
-
width: 100,
|
|
631
|
-
height: 100
|
|
632
|
-
};
|
|
633
|
-
|
|
583
|
+
it('does not move a node into another sacred node', () => {
|
|
584
|
+
// Two sacred nodes close together with a collider between them
|
|
585
|
+
// Moving right would overlap sacred2, so it should choose another direction
|
|
634
586
|
const allBounds: NodeBounds[] = [
|
|
635
|
-
movedBounds,
|
|
636
587
|
{
|
|
637
|
-
uuid: '
|
|
588
|
+
uuid: 'sacred1',
|
|
638
589
|
left: 100,
|
|
639
|
-
top:
|
|
590
|
+
top: 100,
|
|
640
591
|
right: 200,
|
|
641
|
-
bottom:
|
|
592
|
+
bottom: 200,
|
|
642
593
|
width: 100,
|
|
643
594
|
height: 100
|
|
644
595
|
},
|
|
645
596
|
{
|
|
646
|
-
uuid: '
|
|
647
|
-
left:
|
|
648
|
-
top:
|
|
649
|
-
right:
|
|
650
|
-
bottom:
|
|
597
|
+
uuid: 'sacred2',
|
|
598
|
+
left: 240,
|
|
599
|
+
top: 100,
|
|
600
|
+
right: 340,
|
|
601
|
+
bottom: 200,
|
|
602
|
+
width: 100,
|
|
603
|
+
height: 100
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
uuid: 'collider',
|
|
607
|
+
left: 150,
|
|
608
|
+
top: 100,
|
|
609
|
+
right: 250,
|
|
610
|
+
bottom: 200,
|
|
651
611
|
width: 100,
|
|
652
612
|
height: 100
|
|
653
613
|
}
|
|
654
614
|
];
|
|
655
615
|
|
|
656
616
|
const positions = calculateReflowPositions(
|
|
657
|
-
'
|
|
658
|
-
|
|
659
|
-
allBounds,
|
|
660
|
-
false
|
|
617
|
+
['sacred1', 'sacred2'],
|
|
618
|
+
allBounds
|
|
661
619
|
);
|
|
662
620
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
expect(node1Pos.top).to.be.at.least(220); // 200 + 20
|
|
673
|
-
|
|
674
|
-
// node2 should be moved below the new position of node1
|
|
675
|
-
// This tests the code path where we check against already repositioned nodes
|
|
676
|
-
expect(node2Pos.top).to.be.at.least(node1Pos.top + 120);
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
it('maintains horizontal position while moving vertically', () => {
|
|
680
|
-
const movedBounds: NodeBounds = {
|
|
681
|
-
uuid: 'moved',
|
|
682
|
-
left: 100,
|
|
683
|
-
top: 100,
|
|
684
|
-
right: 200,
|
|
685
|
-
bottom: 200,
|
|
621
|
+
expect(positions.has('collider')).to.be.true;
|
|
622
|
+
const newPos = positions.get('collider')!;
|
|
623
|
+
// Should not overlap either sacred node after reflow
|
|
624
|
+
const newBounds: NodeBounds = {
|
|
625
|
+
uuid: 'collider',
|
|
626
|
+
left: newPos.left,
|
|
627
|
+
top: newPos.top,
|
|
628
|
+
right: newPos.left + 100,
|
|
629
|
+
bottom: newPos.top + 100,
|
|
686
630
|
width: 100,
|
|
687
631
|
height: 100
|
|
688
632
|
};
|
|
633
|
+
expect(nodesOverlap(newBounds, allBounds[0])).to.be.false;
|
|
634
|
+
expect(nodesOverlap(newBounds, allBounds[1])).to.be.false;
|
|
635
|
+
});
|
|
689
636
|
|
|
637
|
+
it('snaps reflow positions to grid', () => {
|
|
690
638
|
const allBounds: NodeBounds[] = [
|
|
691
|
-
movedBounds,
|
|
692
639
|
{
|
|
693
|
-
uuid: '
|
|
694
|
-
left:
|
|
640
|
+
uuid: 'sacred',
|
|
641
|
+
left: 100,
|
|
642
|
+
top: 100,
|
|
643
|
+
right: 200,
|
|
644
|
+
bottom: 200,
|
|
645
|
+
width: 100,
|
|
646
|
+
height: 100
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
uuid: 'collider',
|
|
650
|
+
left: 100,
|
|
695
651
|
top: 150,
|
|
696
|
-
right:
|
|
652
|
+
right: 200,
|
|
697
653
|
bottom: 250,
|
|
698
654
|
width: 100,
|
|
699
655
|
height: 100
|
|
700
656
|
}
|
|
701
657
|
];
|
|
702
658
|
|
|
703
|
-
const positions = calculateReflowPositions(
|
|
704
|
-
'moved',
|
|
705
|
-
movedBounds,
|
|
706
|
-
allBounds,
|
|
707
|
-
false
|
|
708
|
-
);
|
|
659
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
709
660
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
661
|
+
expect(positions.has('collider')).to.be.true;
|
|
662
|
+
const newPos = positions.get('collider')!;
|
|
663
|
+
// Both coordinates should be multiples of 20 (grid size)
|
|
664
|
+
expect(newPos.left % 20).to.equal(0);
|
|
665
|
+
expect(newPos.top % 20).to.equal(0);
|
|
713
666
|
});
|
|
714
667
|
|
|
715
|
-
it('
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
left: 100,
|
|
719
|
-
top: 100,
|
|
720
|
-
right: 200,
|
|
721
|
-
bottom: 200,
|
|
722
|
-
width: 100,
|
|
723
|
-
height: 100
|
|
724
|
-
};
|
|
725
|
-
|
|
668
|
+
it('clamps positions to zero (no negative coordinates)', () => {
|
|
669
|
+
// Sacred node near top-left, collider above it
|
|
670
|
+
// Moving up would go negative, so it should fall back
|
|
726
671
|
const allBounds: NodeBounds[] = [
|
|
727
|
-
movedBounds,
|
|
728
672
|
{
|
|
729
|
-
uuid: '
|
|
730
|
-
left:
|
|
731
|
-
top:
|
|
673
|
+
uuid: 'sacred',
|
|
674
|
+
left: 0,
|
|
675
|
+
top: 0,
|
|
732
676
|
right: 200,
|
|
733
|
-
bottom:
|
|
734
|
-
width:
|
|
677
|
+
bottom: 200,
|
|
678
|
+
width: 200,
|
|
679
|
+
height: 200
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
uuid: 'collider',
|
|
683
|
+
left: 0,
|
|
684
|
+
top: 100,
|
|
685
|
+
right: 200,
|
|
686
|
+
bottom: 200,
|
|
687
|
+
width: 200,
|
|
735
688
|
height: 100
|
|
736
689
|
}
|
|
737
690
|
];
|
|
738
691
|
|
|
739
|
-
const positions = calculateReflowPositions(
|
|
740
|
-
'moved',
|
|
741
|
-
movedBounds,
|
|
742
|
-
allBounds,
|
|
743
|
-
false
|
|
744
|
-
);
|
|
692
|
+
const positions = calculateReflowPositions(['sacred'], allBounds);
|
|
745
693
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
expect(newPos.
|
|
694
|
+
expect(positions.has('collider')).to.be.true;
|
|
695
|
+
const newPos = positions.get('collider')!;
|
|
696
|
+
expect(newPos.left).to.be.at.least(0);
|
|
697
|
+
expect(newPos.top).to.be.at.least(0);
|
|
749
698
|
});
|
|
750
699
|
});
|
|
751
700
|
|
|
752
701
|
describe('edge cases', () => {
|
|
753
702
|
it('handles empty allBounds array', () => {
|
|
754
|
-
const
|
|
755
|
-
uuid: 'moved',
|
|
756
|
-
left: 100,
|
|
757
|
-
top: 100,
|
|
758
|
-
right: 200,
|
|
759
|
-
bottom: 200,
|
|
760
|
-
width: 100,
|
|
761
|
-
height: 100
|
|
762
|
-
};
|
|
763
|
-
|
|
764
|
-
const positions = calculateReflowPositions(
|
|
765
|
-
'moved',
|
|
766
|
-
movedBounds,
|
|
767
|
-
[],
|
|
768
|
-
false
|
|
769
|
-
);
|
|
770
|
-
|
|
703
|
+
const positions = calculateReflowPositions(['moved'], []);
|
|
771
704
|
expect(positions.size).to.equal(0);
|
|
772
705
|
});
|
|
773
706
|
|
|
@@ -782,13 +715,7 @@ describe('Collision Detection Utilities', () => {
|
|
|
782
715
|
height: 100
|
|
783
716
|
};
|
|
784
717
|
|
|
785
|
-
const positions = calculateReflowPositions(
|
|
786
|
-
'moved',
|
|
787
|
-
movedBounds,
|
|
788
|
-
[movedBounds],
|
|
789
|
-
false
|
|
790
|
-
);
|
|
791
|
-
|
|
718
|
+
const positions = calculateReflowPositions(['moved'], [movedBounds]);
|
|
792
719
|
expect(positions.size).to.equal(0);
|
|
793
720
|
});
|
|
794
721
|
|
|
@@ -819,12 +746,7 @@ describe('Collision Detection Utilities', () => {
|
|
|
819
746
|
}
|
|
820
747
|
|
|
821
748
|
// Should complete without hanging
|
|
822
|
-
const positions = calculateReflowPositions(
|
|
823
|
-
'moved',
|
|
824
|
-
movedBounds,
|
|
825
|
-
allBounds,
|
|
826
|
-
false
|
|
827
|
-
);
|
|
749
|
+
const positions = calculateReflowPositions(['moved'], allBounds);
|
|
828
750
|
|
|
829
751
|
// Should have resolved some collisions
|
|
830
752
|
expect(positions.size).to.be.greaterThan(0);
|