@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
package/src/flow/Editor.ts
CHANGED
|
@@ -37,25 +37,26 @@ interface Revision {
|
|
|
37
37
|
import { ACTION_GROUP_METADATA } from './types';
|
|
38
38
|
import { Checkbox } from '../form/Checkbox';
|
|
39
39
|
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
Plumber,
|
|
42
|
+
calculateFlowchartPath,
|
|
43
|
+
ARROW_LENGTH,
|
|
44
|
+
ARROW_HALF_WIDTH,
|
|
45
|
+
CURSOR_GAP
|
|
46
|
+
} from './Plumber';
|
|
41
47
|
import { CanvasNode } from './CanvasNode';
|
|
42
48
|
import { Dialog } from '../layout/Dialog';
|
|
43
|
-
|
|
49
|
+
|
|
44
50
|
import { CanvasMenu, CanvasMenuSelection } from './CanvasMenu';
|
|
45
51
|
import { NodeTypeSelector, NodeTypeSelection } from './NodeTypeSelector';
|
|
46
52
|
import {
|
|
47
53
|
getNodeBounds,
|
|
48
54
|
calculateReflowPositions,
|
|
49
55
|
NodeBounds,
|
|
50
|
-
|
|
56
|
+
snapToGrid
|
|
51
57
|
} from './utils';
|
|
52
58
|
import { FloatingWindow } from '../layout/FloatingWindow';
|
|
53
59
|
|
|
54
|
-
export function snapToGrid(value: number): number {
|
|
55
|
-
const snapped = Math.round(value / 20) * 20;
|
|
56
|
-
return Math.max(snapped, 0);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
60
|
export function findNodeForExit(
|
|
60
61
|
definition: FlowDefinition,
|
|
61
62
|
exitUuid: string
|
|
@@ -122,7 +123,7 @@ const DROP_PREVIEW_OFFSET_X = 20;
|
|
|
122
123
|
const DROP_PREVIEW_OFFSET_Y = 20;
|
|
123
124
|
|
|
124
125
|
export class Editor extends RapidElement {
|
|
125
|
-
//
|
|
126
|
+
// connection SVGs are appended directly to the canvas, so we need light DOM
|
|
126
127
|
createRenderRoot() {
|
|
127
128
|
return this;
|
|
128
129
|
}
|
|
@@ -212,6 +213,10 @@ export class Editor extends RapidElement {
|
|
|
212
213
|
@state()
|
|
213
214
|
private isValidTarget = true;
|
|
214
215
|
|
|
216
|
+
// Canvas-relative source exit position (set at drag start)
|
|
217
|
+
private connectionSourceX: number | null = null;
|
|
218
|
+
private connectionSourceY: number | null = null;
|
|
219
|
+
|
|
215
220
|
@state()
|
|
216
221
|
private localizationWindowHidden = true;
|
|
217
222
|
|
|
@@ -294,6 +299,7 @@ export class Editor extends RapidElement {
|
|
|
294
299
|
private connectionPlaceholder: {
|
|
295
300
|
position: FlowPosition;
|
|
296
301
|
visible: boolean;
|
|
302
|
+
dragUp?: boolean;
|
|
297
303
|
} | null = null;
|
|
298
304
|
|
|
299
305
|
// Track pending connection when dropping on canvas
|
|
@@ -401,100 +407,56 @@ export class Editor extends RapidElement {
|
|
|
401
407
|
}
|
|
402
408
|
|
|
403
409
|
#grid.viewing-revision temba-flow-node,
|
|
404
|
-
#grid.viewing-revision svg.
|
|
405
|
-
#grid.viewing-revision .activity-overlay {
|
|
410
|
+
#grid.viewing-revision svg.plumb-connector {
|
|
406
411
|
opacity: 0.5;
|
|
407
412
|
}
|
|
408
413
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
height: initial;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
.jtk-endpoint {
|
|
415
|
-
z-index: 600;
|
|
416
|
-
opacity: 0;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
.plumb-source {
|
|
420
|
-
z-index: 600;
|
|
421
|
-
cursor: pointer;
|
|
422
|
-
opacity: 0;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
.plumb-source.connected {
|
|
426
|
-
border-radius: 50%;
|
|
427
|
-
pointer-events: none;
|
|
414
|
+
svg.plumb-connector {
|
|
415
|
+
z-index: 10;
|
|
428
416
|
}
|
|
429
417
|
|
|
430
|
-
.plumb-
|
|
431
|
-
|
|
418
|
+
svg.plumb-connector path {
|
|
419
|
+
stroke: var(--color-connectors);
|
|
420
|
+
stroke-width: 3px;
|
|
432
421
|
}
|
|
433
422
|
|
|
434
|
-
.plumb-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
cursor: pointer;
|
|
438
|
-
fill: transparent;
|
|
423
|
+
svg.plumb-connector .plumb-arrow {
|
|
424
|
+
fill: var(--color-connectors);
|
|
425
|
+
stroke: none;
|
|
439
426
|
}
|
|
440
427
|
|
|
441
|
-
|
|
442
|
-
stroke: var(--color-
|
|
443
|
-
stroke-width: 3px;
|
|
428
|
+
svg.plumb-connector.hover path {
|
|
429
|
+
stroke: var(--color-success);
|
|
444
430
|
}
|
|
445
431
|
|
|
446
|
-
|
|
447
|
-
|
|
432
|
+
svg.plumb-connector.hover .plumb-arrow {
|
|
433
|
+
fill: var(--color-success);
|
|
448
434
|
}
|
|
449
435
|
|
|
450
|
-
|
|
451
|
-
fill: var(--color-connectors);
|
|
436
|
+
#canvas.read-only-connections svg.plumb-connector.hover path {
|
|
452
437
|
stroke: var(--color-connectors);
|
|
453
|
-
stroke-width: 0px !important;
|
|
454
|
-
margin-top: 6px;
|
|
455
|
-
z-index: 10;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
body svg.jtk-connector.jtk-hover path {
|
|
459
|
-
stroke: var(--color-success) !important;
|
|
460
|
-
stroke-width: 3px;
|
|
461
438
|
}
|
|
462
439
|
|
|
463
|
-
|
|
464
|
-
|
|
440
|
+
#canvas.read-only-connections svg.plumb-connector.hover .plumb-arrow {
|
|
441
|
+
fill: var(--color-connectors);
|
|
465
442
|
}
|
|
466
443
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
444
|
+
#canvas.read-only-connections svg.plumb-connector,
|
|
445
|
+
#canvas.read-only-connections svg.plumb-connector * {
|
|
446
|
+
pointer-events: none !important;
|
|
447
|
+
cursor: default !important;
|
|
471
448
|
}
|
|
472
449
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
.plumb-connector.jtk-hover
|
|
476
|
-
.plumb-arrow {
|
|
477
|
-
fill: var(--color-connectors) !important;
|
|
478
|
-
ponter-events: none;
|
|
450
|
+
svg.plumb-connector.removing path {
|
|
451
|
+
stroke: var(--color-error);
|
|
479
452
|
}
|
|
480
453
|
|
|
481
|
-
|
|
482
|
-
|
|
454
|
+
svg.plumb-connector.removing .plumb-arrow {
|
|
455
|
+
fill: var(--color-error);
|
|
483
456
|
}
|
|
484
457
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
background: #f3f3f3;
|
|
488
|
-
border: 1px solid #d9d9d9;
|
|
489
|
-
color: #333;
|
|
490
|
-
border-radius: 4px;
|
|
491
|
-
padding: 2px 4px;
|
|
492
|
-
font-size: 10px;
|
|
493
|
-
font-weight: 600;
|
|
494
|
-
line-height: 0.9;
|
|
495
|
-
cursor: pointer;
|
|
496
|
-
z-index: 500;
|
|
497
|
-
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
458
|
+
svg.plumb-connector.dragging {
|
|
459
|
+
z-index: 99999;
|
|
498
460
|
}
|
|
499
461
|
|
|
500
462
|
/* Active contact count on nodes */
|
|
@@ -516,6 +478,30 @@ export class Editor extends RapidElement {
|
|
|
516
478
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
517
479
|
}
|
|
518
480
|
|
|
481
|
+
/* Activity overlay badges on connection exit stubs */
|
|
482
|
+
.activity-overlay {
|
|
483
|
+
position: absolute;
|
|
484
|
+
background: #f3f3f3;
|
|
485
|
+
border: 1px solid #d9d9d9;
|
|
486
|
+
color: #333;
|
|
487
|
+
border-radius: 4px;
|
|
488
|
+
padding: 2px 4px;
|
|
489
|
+
font-size: 10px;
|
|
490
|
+
font-weight: 600;
|
|
491
|
+
line-height: 0.9;
|
|
492
|
+
cursor: pointer;
|
|
493
|
+
z-index: 500;
|
|
494
|
+
pointer-events: auto;
|
|
495
|
+
white-space: nowrap;
|
|
496
|
+
user-select: none;
|
|
497
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
#grid.viewing-revision .activity-overlay {
|
|
501
|
+
opacity: 0.5;
|
|
502
|
+
pointer-events: none;
|
|
503
|
+
}
|
|
504
|
+
|
|
519
505
|
/* Recent contacts popup */
|
|
520
506
|
@keyframes popupBounceIn {
|
|
521
507
|
0% {
|
|
@@ -585,7 +571,6 @@ export class Editor extends RapidElement {
|
|
|
585
571
|
|
|
586
572
|
.recent-contacts-popup .contact-name:hover {
|
|
587
573
|
text-decoration: underline;
|
|
588
|
-
color: var(--color-link-primary, #1d4ed8);
|
|
589
574
|
}
|
|
590
575
|
|
|
591
576
|
.recent-contacts-popup .contact-operand {
|
|
@@ -601,17 +586,6 @@ export class Editor extends RapidElement {
|
|
|
601
586
|
color: #999;
|
|
602
587
|
}
|
|
603
588
|
|
|
604
|
-
/* Connection dragging feedback */
|
|
605
|
-
body svg.jtk-connector.jtk-dragging {
|
|
606
|
-
z-index: 99999 !important;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
.katavorio-drag-no-select svg.jtk-connector path,
|
|
610
|
-
.katavorio-drag-no-select svg.jtk-endpoint path {
|
|
611
|
-
pointer-events: none !important;
|
|
612
|
-
border: 1px solid purple;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
589
|
/* Connection target feedback */
|
|
616
590
|
temba-flow-node.connection-target-valid {
|
|
617
591
|
outline: 3px solid var(--color-success, #22c55e) !important;
|
|
@@ -641,10 +615,6 @@ export class Editor extends RapidElement {
|
|
|
641
615
|
border-radius: var(--curvature);
|
|
642
616
|
}
|
|
643
617
|
|
|
644
|
-
.jtk-floating-endpoint {
|
|
645
|
-
pointer-events: none;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
618
|
.localization-window-content {
|
|
649
619
|
display: flex;
|
|
650
620
|
flex-direction: column;
|
|
@@ -852,17 +822,15 @@ export class Editor extends RapidElement {
|
|
|
852
822
|
getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
|
|
853
823
|
}
|
|
854
824
|
|
|
855
|
-
this.plumber.on('connection:drag', (connection:
|
|
856
|
-
|
|
857
|
-
this.dragFromNodeId =
|
|
858
|
-
connection.data.nodeId ||
|
|
859
|
-
document.getElementById(connection.sourceId).closest('.node').id;
|
|
825
|
+
this.plumber.on('connection:drag', (connection: any) => {
|
|
826
|
+
this.dragFromNodeId = connection.data.nodeId;
|
|
860
827
|
this.sourceId = connection.sourceId;
|
|
828
|
+
this.connectionSourceX = connection.sourceX;
|
|
829
|
+
this.connectionSourceY = connection.sourceY;
|
|
861
830
|
this.originalConnectionTargetId = connection.target.id;
|
|
862
831
|
});
|
|
863
832
|
|
|
864
833
|
this.plumber.on('connection:abort', (info) => {
|
|
865
|
-
// console.log('Connection aborted', info);
|
|
866
834
|
this.makeConnection(info);
|
|
867
835
|
});
|
|
868
836
|
|
|
@@ -898,6 +866,7 @@ export class Editor extends RapidElement {
|
|
|
898
866
|
left: snapToGrid(this.connectionPlaceholder.position.left),
|
|
899
867
|
top: snapToGrid(this.connectionPlaceholder.position.top)
|
|
900
868
|
};
|
|
869
|
+
const isDragUp = !!this.connectionPlaceholder.dragUp;
|
|
901
870
|
|
|
902
871
|
// Update the placeholder to the snapped position
|
|
903
872
|
this.connectionPlaceholder.position = snappedPosition;
|
|
@@ -909,12 +878,14 @@ export class Editor extends RapidElement {
|
|
|
909
878
|
position: snappedPosition
|
|
910
879
|
};
|
|
911
880
|
|
|
912
|
-
// Show the context menu
|
|
881
|
+
// Show the context menu near the placeholder
|
|
913
882
|
const canvas = this.querySelector('#canvas');
|
|
914
883
|
if (canvas) {
|
|
915
884
|
const canvasRect = canvas.getBoundingClientRect();
|
|
916
|
-
const menuX = canvasRect.left + snappedPosition.left - 40;
|
|
917
|
-
const menuY =
|
|
885
|
+
const menuX = canvasRect.left + snappedPosition.left - 40;
|
|
886
|
+
const menuY = isDragUp
|
|
887
|
+
? canvasRect.top + snappedPosition.top + 74 // just below placeholder bottom
|
|
888
|
+
: canvasRect.top + snappedPosition.top + 80; // just below placeholder
|
|
918
889
|
|
|
919
890
|
const canvasMenu = this.querySelector(
|
|
920
891
|
'temba-canvas-menu'
|
|
@@ -950,6 +921,8 @@ export class Editor extends RapidElement {
|
|
|
950
921
|
// Clear connection state (but keep sourceId/dragFromNodeId if we have a pending connection)
|
|
951
922
|
if (!this.pendingCanvasConnection) {
|
|
952
923
|
this.sourceId = null;
|
|
924
|
+
this.connectionSourceX = null;
|
|
925
|
+
this.connectionSourceY = null;
|
|
953
926
|
this.dragFromNodeId = null;
|
|
954
927
|
}
|
|
955
928
|
this.targetId = null;
|
|
@@ -1130,7 +1103,7 @@ export class Editor extends RapidElement {
|
|
|
1130
1103
|
}
|
|
1131
1104
|
|
|
1132
1105
|
this.activityTimer = window.setTimeout(() => {
|
|
1133
|
-
|
|
1106
|
+
this.fetchActivityData();
|
|
1134
1107
|
}, this.activityInterval);
|
|
1135
1108
|
});
|
|
1136
1109
|
}
|
|
@@ -1570,86 +1543,71 @@ export class Editor extends RapidElement {
|
|
|
1570
1543
|
if (!this.connectionPlaceholder || !this.connectionPlaceholder.visible)
|
|
1571
1544
|
return '';
|
|
1572
1545
|
|
|
1573
|
-
const { position } = this.connectionPlaceholder;
|
|
1546
|
+
const { position, dragUp } = this.connectionPlaceholder;
|
|
1574
1547
|
|
|
1575
1548
|
// Render connection line when we have a pending connection (after drop)
|
|
1576
1549
|
let svgPath = null;
|
|
1577
|
-
if (
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
if (sourceX !== targetX) {
|
|
1607
|
-
// Horizontal segment needed
|
|
1608
|
-
if (Math.abs(verticalEnd - verticalStart) > cornerRadius * 2) {
|
|
1609
|
-
// Enough space for corners
|
|
1610
|
-
pathData += ` L ${sourceX} ${midY - cornerRadius}`;
|
|
1611
|
-
pathData += ` Q ${sourceX} ${midY}, ${
|
|
1612
|
-
sourceX + (targetX > sourceX ? cornerRadius : -cornerRadius)
|
|
1613
|
-
} ${midY}`;
|
|
1614
|
-
pathData += ` L ${
|
|
1615
|
-
targetX - (targetX > sourceX ? cornerRadius : -cornerRadius)
|
|
1616
|
-
} ${midY}`;
|
|
1617
|
-
pathData += ` Q ${targetX} ${midY}, ${targetX} ${
|
|
1618
|
-
midY + cornerRadius
|
|
1619
|
-
}`;
|
|
1620
|
-
pathData += ` L ${targetX} ${verticalEnd}`;
|
|
1621
|
-
} else {
|
|
1622
|
-
// Direct horizontal transition
|
|
1623
|
-
pathData += ` L ${targetX} ${verticalStart}`;
|
|
1624
|
-
pathData += ` L ${targetX} ${verticalEnd}`;
|
|
1625
|
-
}
|
|
1626
|
-
} else {
|
|
1627
|
-
// Straight vertical line
|
|
1628
|
-
pathData += ` L ${targetX} ${verticalEnd}`;
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
pathData += ` L ${targetX} ${targetY}`;
|
|
1550
|
+
if (
|
|
1551
|
+
this.sourceId &&
|
|
1552
|
+
this.dragFromNodeId &&
|
|
1553
|
+
this.pendingCanvasConnection &&
|
|
1554
|
+
this.connectionSourceX != null &&
|
|
1555
|
+
this.connectionSourceY != null
|
|
1556
|
+
) {
|
|
1557
|
+
const sourceX = this.connectionSourceX;
|
|
1558
|
+
const sourceY = this.connectionSourceY;
|
|
1559
|
+
const targetX = position.left + 100;
|
|
1560
|
+
// When dragging up, connect to the placeholder bottom; otherwise to the top
|
|
1561
|
+
const targetY = dragUp ? position.top + 64 : position.top;
|
|
1562
|
+
|
|
1563
|
+
const routeFace: 'top' | 'left' | 'right' = dragUp
|
|
1564
|
+
? targetX < sourceX
|
|
1565
|
+
? 'left'
|
|
1566
|
+
: 'right'
|
|
1567
|
+
: 'top';
|
|
1568
|
+
|
|
1569
|
+
const pathData = calculateFlowchartPath(
|
|
1570
|
+
sourceX,
|
|
1571
|
+
sourceY,
|
|
1572
|
+
targetX,
|
|
1573
|
+
targetY,
|
|
1574
|
+
20,
|
|
1575
|
+
dragUp ? 0 : 10,
|
|
1576
|
+
5,
|
|
1577
|
+
routeFace
|
|
1578
|
+
);
|
|
1632
1579
|
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
13} ${targetX + 6.5},${targetY - 13}"
|
|
1647
|
-
fill="var(--color-connectors, #ccc)"
|
|
1648
|
-
class="plumb-arrow"
|
|
1649
|
-
/>
|
|
1650
|
-
</svg>
|
|
1651
|
-
`;
|
|
1580
|
+
const aw = ARROW_HALF_WIDTH;
|
|
1581
|
+
const al = ARROW_LENGTH;
|
|
1582
|
+
let arrowPoints: string;
|
|
1583
|
+
if (dragUp) {
|
|
1584
|
+
// Arrow tip pointing up, base at placeholder bottom
|
|
1585
|
+
arrowPoints = `${targetX},${targetY - al} ${targetX - aw},${targetY} ${
|
|
1586
|
+
targetX + aw
|
|
1587
|
+
},${targetY}`;
|
|
1588
|
+
} else {
|
|
1589
|
+
// Arrow pointing down into top of placeholder
|
|
1590
|
+
arrowPoints = `${targetX},${targetY} ${targetX - aw},${targetY - al} ${
|
|
1591
|
+
targetX + aw
|
|
1592
|
+
},${targetY - al}`;
|
|
1652
1593
|
}
|
|
1594
|
+
|
|
1595
|
+
svgPath = html`
|
|
1596
|
+
<svg
|
|
1597
|
+
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;"
|
|
1598
|
+
>
|
|
1599
|
+
<path
|
|
1600
|
+
d="${pathData}"
|
|
1601
|
+
fill="none"
|
|
1602
|
+
stroke="var(--color-connectors, #ccc)"
|
|
1603
|
+
stroke-width="3"
|
|
1604
|
+
/>
|
|
1605
|
+
<polygon
|
|
1606
|
+
points="${arrowPoints}"
|
|
1607
|
+
fill="var(--color-connectors, #ccc)"
|
|
1608
|
+
/>
|
|
1609
|
+
</svg>
|
|
1610
|
+
`;
|
|
1653
1611
|
}
|
|
1654
1612
|
|
|
1655
1613
|
return html`${svgPath}
|
|
@@ -1673,22 +1631,13 @@ export class Editor extends RapidElement {
|
|
|
1673
1631
|
|
|
1674
1632
|
/**
|
|
1675
1633
|
* Checks for node collisions and reflows nodes as needed.
|
|
1676
|
-
*
|
|
1677
|
-
*
|
|
1678
|
-
* @param movedNodeUuids - UUIDs of nodes that were just moved/dropped
|
|
1679
|
-
* @param droppedNodeUuid - UUID of the specific node that was dropped (if applicable)
|
|
1680
|
-
* @param dropTargetBounds - Bounds of the node that was dropped onto (if applicable)
|
|
1634
|
+
* Sacred nodes (just moved/dropped) keep their positions while
|
|
1635
|
+
* other nodes are moved in the least-disruptive direction.
|
|
1681
1636
|
*/
|
|
1682
|
-
private checkCollisionsAndReflow(
|
|
1683
|
-
movedNodeUuids: string[],
|
|
1684
|
-
droppedNodeUuid: string | null = null,
|
|
1685
|
-
dropTargetBounds: NodeBounds | null = null
|
|
1686
|
-
): void {
|
|
1637
|
+
private checkCollisionsAndReflow(sacredNodeUuids: string[]): void {
|
|
1687
1638
|
if (!this.definition) return;
|
|
1688
1639
|
|
|
1689
|
-
// Get all node bounds (only for actual nodes, not stickies)
|
|
1690
1640
|
const allBounds: NodeBounds[] = [];
|
|
1691
|
-
|
|
1692
1641
|
for (const node of this.definition.nodes) {
|
|
1693
1642
|
const nodeUI = this.definition._ui?.nodes[node.uuid];
|
|
1694
1643
|
if (!nodeUI?.position) continue;
|
|
@@ -1699,45 +1648,17 @@ export class Editor extends RapidElement {
|
|
|
1699
1648
|
}
|
|
1700
1649
|
}
|
|
1701
1650
|
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
if (droppedBounds) {
|
|
1707
|
-
// Check if the bottom of the dropped node is below the midpoint of the target
|
|
1708
|
-
// If bottom is above midpoint, dropped node gets preference (targetHasPriority = false)
|
|
1709
|
-
// If bottom is below midpoint, target gets preference (targetHasPriority = true)
|
|
1710
|
-
const droppedBottom = droppedBounds.bottom;
|
|
1711
|
-
const targetMidpoint =
|
|
1712
|
-
dropTargetBounds.top + dropTargetBounds.height / 2;
|
|
1713
|
-
targetHasPriority = droppedBottom > targetMidpoint;
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
// Calculate reflow positions for each moved node
|
|
1718
|
-
const allReflowPositions: { [uuid: string]: FlowPosition } = {};
|
|
1719
|
-
|
|
1720
|
-
for (const movedUuid of movedNodeUuids) {
|
|
1721
|
-
const movedBounds = allBounds.find((b) => b.uuid === movedUuid);
|
|
1722
|
-
if (!movedBounds) continue;
|
|
1723
|
-
|
|
1724
|
-
// Calculate reflow for this moved node
|
|
1725
|
-
const reflowPositions = calculateReflowPositions(
|
|
1726
|
-
movedUuid,
|
|
1727
|
-
movedBounds,
|
|
1728
|
-
allBounds,
|
|
1729
|
-
droppedNodeUuid === movedUuid ? targetHasPriority : false
|
|
1730
|
-
);
|
|
1651
|
+
const reflowPositions = calculateReflowPositions(
|
|
1652
|
+
sacredNodeUuids,
|
|
1653
|
+
allBounds
|
|
1654
|
+
);
|
|
1731
1655
|
|
|
1732
|
-
|
|
1656
|
+
if (reflowPositions.size > 0) {
|
|
1657
|
+
const positions: { [uuid: string]: FlowPosition } = {};
|
|
1733
1658
|
for (const [uuid, position] of reflowPositions.entries()) {
|
|
1734
|
-
|
|
1659
|
+
positions[uuid] = position;
|
|
1735
1660
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
// If there are positions to update, apply them
|
|
1739
|
-
if (Object.keys(allReflowPositions).length > 0) {
|
|
1740
|
-
getStore().getState().updateCanvasPositions(allReflowPositions);
|
|
1661
|
+
getStore().getState().updateCanvasPositions(positions);
|
|
1741
1662
|
}
|
|
1742
1663
|
}
|
|
1743
1664
|
|
|
@@ -1780,23 +1701,41 @@ export class Editor extends RapidElement {
|
|
|
1780
1701
|
this.isValidTarget = true;
|
|
1781
1702
|
|
|
1782
1703
|
// Show connection placeholder when over empty canvas
|
|
1783
|
-
// Calculate position: horizontally centered at mouse, vertically just below mouse
|
|
1784
1704
|
const canvas = this.querySelector('#canvas');
|
|
1785
1705
|
if (canvas) {
|
|
1786
1706
|
const canvasRect = canvas.getBoundingClientRect();
|
|
1787
1707
|
const relativeX = event.clientX - canvasRect.left;
|
|
1788
1708
|
const relativeY = event.clientY - canvasRect.top;
|
|
1789
1709
|
|
|
1790
|
-
|
|
1791
|
-
const
|
|
1792
|
-
const
|
|
1710
|
+
const placeholderWidth = 200;
|
|
1711
|
+
const placeholderHeight = 64;
|
|
1712
|
+
const arrowLength = ARROW_LENGTH;
|
|
1713
|
+
const cursorGap = CURSOR_GAP;
|
|
1714
|
+
|
|
1715
|
+
// Determine if cursor is above the source exit using stored sourceY
|
|
1716
|
+
const dragUp =
|
|
1717
|
+
this.connectionSourceY != null
|
|
1718
|
+
? relativeY < this.connectionSourceY
|
|
1719
|
+
: false;
|
|
1720
|
+
|
|
1721
|
+
let top: number;
|
|
1722
|
+
if (dragUp) {
|
|
1723
|
+
// Arrow points up: tip at cy + cursorGap.
|
|
1724
|
+
// Placeholder bottom should sit just above the arrow tip.
|
|
1725
|
+
top = relativeY + cursorGap - placeholderHeight;
|
|
1726
|
+
} else {
|
|
1727
|
+
// Arrow points down: tip at cy - cursorGap + arrowLength.
|
|
1728
|
+
// Placeholder top sits just below the arrow tip.
|
|
1729
|
+
top = relativeY - cursorGap + arrowLength;
|
|
1730
|
+
}
|
|
1793
1731
|
|
|
1794
1732
|
this.connectionPlaceholder = {
|
|
1795
1733
|
position: {
|
|
1796
1734
|
left: relativeX - placeholderWidth / 2,
|
|
1797
|
-
top
|
|
1735
|
+
top
|
|
1798
1736
|
},
|
|
1799
|
-
visible: true
|
|
1737
|
+
visible: true,
|
|
1738
|
+
dragUp
|
|
1800
1739
|
};
|
|
1801
1740
|
}
|
|
1802
1741
|
}
|
|
@@ -1924,49 +1863,7 @@ export class Editor extends RapidElement {
|
|
|
1924
1863
|
if (nodeUuids.length > 0) {
|
|
1925
1864
|
// Allow DOM to update before checking collisions
|
|
1926
1865
|
setTimeout(() => {
|
|
1927
|
-
|
|
1928
|
-
let droppedNodeUuid: string | null = null;
|
|
1929
|
-
let dropTargetBounds: NodeBounds | null = null;
|
|
1930
|
-
|
|
1931
|
-
if (nodeUuids.length === 1) {
|
|
1932
|
-
droppedNodeUuid = nodeUuids[0];
|
|
1933
|
-
const droppedNodeUI = this.definition._ui?.nodes[droppedNodeUuid];
|
|
1934
|
-
|
|
1935
|
-
if (droppedNodeUI?.position) {
|
|
1936
|
-
const droppedBounds = getNodeBounds(
|
|
1937
|
-
droppedNodeUuid,
|
|
1938
|
-
droppedNodeUI.position
|
|
1939
|
-
);
|
|
1940
|
-
|
|
1941
|
-
if (droppedBounds) {
|
|
1942
|
-
// Find which node (if any) the dropped node overlaps with
|
|
1943
|
-
for (const node of this.definition.nodes) {
|
|
1944
|
-
if (node.uuid === droppedNodeUuid) continue;
|
|
1945
|
-
|
|
1946
|
-
const nodeUI = this.definition._ui?.nodes[node.uuid];
|
|
1947
|
-
if (!nodeUI?.position) continue;
|
|
1948
|
-
|
|
1949
|
-
const targetBounds = getNodeBounds(
|
|
1950
|
-
node.uuid,
|
|
1951
|
-
nodeUI.position
|
|
1952
|
-
);
|
|
1953
|
-
if (
|
|
1954
|
-
targetBounds &&
|
|
1955
|
-
nodesOverlap(droppedBounds, targetBounds)
|
|
1956
|
-
) {
|
|
1957
|
-
dropTargetBounds = targetBounds;
|
|
1958
|
-
break; // Use the first overlapping node
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
this.checkCollisionsAndReflow(
|
|
1966
|
-
nodeUuids,
|
|
1967
|
-
droppedNodeUuid,
|
|
1968
|
-
dropTargetBounds
|
|
1969
|
-
);
|
|
1866
|
+
this.checkCollisionsAndReflow(nodeUuids);
|
|
1970
1867
|
}, 0);
|
|
1971
1868
|
} else {
|
|
1972
1869
|
// No nodes moved, just repaint connections
|
|
@@ -2091,6 +1988,8 @@ export class Editor extends RapidElement {
|
|
|
2091
1988
|
this.pendingCanvasConnection = null;
|
|
2092
1989
|
this.connectionPlaceholder = null;
|
|
2093
1990
|
this.sourceId = null;
|
|
1991
|
+
this.connectionSourceX = null;
|
|
1992
|
+
this.connectionSourceY = null;
|
|
2094
1993
|
this.dragFromNodeId = null;
|
|
2095
1994
|
} else {
|
|
2096
1995
|
// Show node type selector
|
|
@@ -2126,6 +2025,8 @@ export class Editor extends RapidElement {
|
|
|
2126
2025
|
this.pendingCanvasConnection = null;
|
|
2127
2026
|
this.connectionPlaceholder = null;
|
|
2128
2027
|
this.sourceId = null;
|
|
2028
|
+
this.connectionSourceX = null;
|
|
2029
|
+
this.connectionSourceY = null;
|
|
2129
2030
|
this.dragFromNodeId = null;
|
|
2130
2031
|
this.originalConnectionTargetId = null;
|
|
2131
2032
|
}
|
|
@@ -2362,6 +2263,8 @@ export class Editor extends RapidElement {
|
|
|
2362
2263
|
this.pendingCanvasConnection = null;
|
|
2363
2264
|
this.connectionPlaceholder = null;
|
|
2364
2265
|
this.sourceId = null;
|
|
2266
|
+
this.connectionSourceX = null;
|
|
2267
|
+
this.connectionSourceY = null;
|
|
2365
2268
|
this.dragFromNodeId = null;
|
|
2366
2269
|
}
|
|
2367
2270
|
|
|
@@ -2431,6 +2334,8 @@ export class Editor extends RapidElement {
|
|
|
2431
2334
|
this.pendingCanvasConnection = null;
|
|
2432
2335
|
this.connectionPlaceholder = null;
|
|
2433
2336
|
this.sourceId = null;
|
|
2337
|
+
this.connectionSourceX = null;
|
|
2338
|
+
this.connectionSourceY = null;
|
|
2434
2339
|
this.dragFromNodeId = null;
|
|
2435
2340
|
}
|
|
2436
2341
|
|
|
@@ -3430,7 +3335,7 @@ export class Editor extends RapidElement {
|
|
|
3430
3335
|
icon="revisions"
|
|
3431
3336
|
label="Revisions"
|
|
3432
3337
|
color="rgb(142, 94, 167)"
|
|
3433
|
-
|
|
3338
|
+
order="1"
|
|
3434
3339
|
.hidden=${!this.revisionsWindowHidden && this.localizationWindowHidden}
|
|
3435
3340
|
@temba-button-clicked=${this.handleRevisionsTabClick}
|
|
3436
3341
|
></temba-floating-tab>
|
|
@@ -3728,7 +3633,7 @@ export class Editor extends RapidElement {
|
|
|
3728
3633
|
icon="language"
|
|
3729
3634
|
label="Translate Flow"
|
|
3730
3635
|
color="#6b7280"
|
|
3731
|
-
|
|
3636
|
+
order="2"
|
|
3732
3637
|
.hidden=${!this.localizationWindowHidden}
|
|
3733
3638
|
@temba-button-clicked=${this.handleLocalizationTabClick}
|
|
3734
3639
|
></temba-floating-tab>
|