@nyaruka/temba-components 0.138.6 → 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.
Files changed (66) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/locales/es.js +5 -5
  3. package/dist/locales/es.js.map +1 -1
  4. package/dist/locales/fr.js +5 -5
  5. package/dist/locales/fr.js.map +1 -1
  6. package/dist/locales/locale-codes.js +2 -11
  7. package/dist/locales/locale-codes.js.map +1 -1
  8. package/dist/locales/pt.js +5 -5
  9. package/dist/locales/pt.js.map +1 -1
  10. package/dist/temba-components.js +815 -851
  11. package/dist/temba-components.js.map +1 -1
  12. package/out-tsc/src/display/FloatingTab.js +23 -30
  13. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  14. package/out-tsc/src/flow/CanvasMenu.js +5 -3
  15. package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
  16. package/out-tsc/src/flow/CanvasNode.js +6 -7
  17. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  18. package/out-tsc/src/flow/Editor.js +152 -235
  19. package/out-tsc/src/flow/Editor.js.map +1 -1
  20. package/out-tsc/src/flow/Plumber.js +757 -403
  21. package/out-tsc/src/flow/Plumber.js.map +1 -1
  22. package/out-tsc/src/flow/utils.js +138 -66
  23. package/out-tsc/src/flow/utils.js.map +1 -1
  24. package/out-tsc/src/interfaces.js +1 -0
  25. package/out-tsc/src/interfaces.js.map +1 -1
  26. package/out-tsc/src/list/TicketList.js +4 -1
  27. package/out-tsc/src/list/TicketList.js.map +1 -1
  28. package/out-tsc/src/locales/es.js +5 -5
  29. package/out-tsc/src/locales/es.js.map +1 -1
  30. package/out-tsc/src/locales/fr.js +5 -5
  31. package/out-tsc/src/locales/fr.js.map +1 -1
  32. package/out-tsc/src/locales/locale-codes.js +2 -11
  33. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  34. package/out-tsc/src/locales/pt.js +5 -5
  35. package/out-tsc/src/locales/pt.js.map +1 -1
  36. package/out-tsc/src/simulator/Simulator.js +1 -0
  37. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  38. package/out-tsc/test/temba-floating-tab.test.js +4 -6
  39. package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
  40. package/out-tsc/test/temba-flow-collision.test.js +221 -223
  41. package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
  42. package/out-tsc/test/temba-flow-editor.test.js +0 -2
  43. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  44. package/out-tsc/test/temba-flow-plumber-connections.test.js +83 -84
  45. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  46. package/out-tsc/test/temba-flow-plumber.test.js +102 -93
  47. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  48. package/package.json +1 -1
  49. package/src/display/FloatingTab.ts +22 -31
  50. package/src/flow/CanvasMenu.ts +8 -3
  51. package/src/flow/CanvasNode.ts +6 -7
  52. package/src/flow/Editor.ts +184 -279
  53. package/src/flow/Plumber.ts +1011 -457
  54. package/src/flow/utils.ts +162 -84
  55. package/src/interfaces.ts +2 -1
  56. package/src/list/TicketList.ts +4 -1
  57. package/src/locales/es.ts +13 -18
  58. package/src/locales/fr.ts +13 -18
  59. package/src/locales/locale-codes.ts +2 -11
  60. package/src/locales/pt.ts +13 -18
  61. package/src/simulator/Simulator.ts +1 -0
  62. package/test/temba-floating-tab.test.ts +4 -6
  63. package/test/temba-flow-collision.test.ts +225 -303
  64. package/test/temba-flow-editor.test.ts +0 -2
  65. package/test/temba-flow-plumber-connections.test.ts +97 -97
  66. package/test/temba-flow-plumber.test.ts +116 -103
@@ -10,13 +10,9 @@ import { CustomEventType } from '../interfaces';
10
10
  import { generateUUID, postJSON, fetchResults, getClasses } from '../utils';
11
11
  import { ACTION_CONFIG, NODE_CONFIG } from './config';
12
12
  import { ACTION_GROUP_METADATA } from './types';
13
- import { Plumber } from './Plumber';
13
+ import { Plumber, calculateFlowchartPath, ARROW_LENGTH, ARROW_HALF_WIDTH, CURSOR_GAP } from './Plumber';
14
14
  import { CanvasNode } from './CanvasNode';
15
- import { getNodeBounds, calculateReflowPositions, nodesOverlap } from './utils';
16
- export function snapToGrid(value) {
17
- const snapped = Math.round(value / 20) * 20;
18
- return Math.max(snapped, 0);
19
- }
15
+ import { getNodeBounds, calculateReflowPositions, snapToGrid } from './utils';
20
16
  export function findNodeForExit(definition, exitUuid) {
21
17
  for (const node of definition.nodes) {
22
18
  const exit = node.exits.find((e) => e.uuid === exitUuid);
@@ -34,7 +30,7 @@ const AUTO_TRANSLATE_MODELS_ENDPOINT = '/api/internal/llms.json';
34
30
  const DROP_PREVIEW_OFFSET_X = 20;
35
31
  const DROP_PREVIEW_OFFSET_Y = 20;
36
32
  export class Editor extends RapidElement {
37
- // unfortunately, jsplumb requires that we be in light DOM
33
+ // connection SVGs are appended directly to the canvas, so we need light DOM
38
34
  createRenderRoot() {
39
35
  return this;
40
36
  }
@@ -128,100 +124,56 @@ export class Editor extends RapidElement {
128
124
  }
129
125
 
130
126
  #grid.viewing-revision temba-flow-node,
131
- #grid.viewing-revision svg.jtk-connector,
132
- #grid.viewing-revision .activity-overlay {
127
+ #grid.viewing-revision svg.plumb-connector {
133
128
  opacity: 0.5;
134
129
  }
135
130
 
136
- body .jtk-endpoint {
137
- width: initial;
138
- height: initial;
139
- }
140
-
141
- .jtk-endpoint {
142
- z-index: 600;
143
- opacity: 0;
144
- }
145
-
146
- .plumb-source {
147
- z-index: 600;
148
- cursor: pointer;
149
- opacity: 0;
150
- }
151
-
152
- .plumb-source.connected {
153
- border-radius: 50%;
154
- pointer-events: none;
131
+ svg.plumb-connector {
132
+ z-index: 10;
155
133
  }
156
134
 
157
- .plumb-source circle {
158
- fill: purple;
135
+ svg.plumb-connector path {
136
+ stroke: var(--color-connectors);
137
+ stroke-width: 3px;
159
138
  }
160
139
 
161
- .plumb-target {
162
- z-index: 600;
163
- opacity: 0;
164
- cursor: pointer;
165
- fill: transparent;
140
+ svg.plumb-connector .plumb-arrow {
141
+ fill: var(--color-connectors);
142
+ stroke: none;
166
143
  }
167
144
 
168
- body svg.jtk-connector.plumb-connector path {
169
- stroke: var(--color-connectors) !important;
170
- stroke-width: 3px;
145
+ svg.plumb-connector.hover path {
146
+ stroke: var(--color-success);
171
147
  }
172
148
 
173
- body .plumb-connector {
174
- z-index: 10 !important;
149
+ svg.plumb-connector.hover .plumb-arrow {
150
+ fill: var(--color-success);
175
151
  }
176
152
 
177
- body .plumb-connector .plumb-arrow {
178
- fill: var(--color-connectors);
153
+ #canvas.read-only-connections svg.plumb-connector.hover path {
179
154
  stroke: var(--color-connectors);
180
- stroke-width: 0px !important;
181
- margin-top: 6px;
182
- z-index: 10;
183
- }
184
-
185
- body svg.jtk-connector.jtk-hover path {
186
- stroke: var(--color-success) !important;
187
- stroke-width: 3px;
188
155
  }
189
156
 
190
- body #canvas.read-only-connections svg.jtk-connector.jtk-hover path {
191
- stroke: var(--color-connectors) !important;
157
+ #canvas.read-only-connections svg.plumb-connector.hover .plumb-arrow {
158
+ fill: var(--color-connectors);
192
159
  }
193
160
 
194
- body .plumb-connector.jtk-hover .plumb-arrow {
195
- fill: var(--color-success) !important;
196
- stroke-width: 0px;
197
- z-index: 10;
161
+ #canvas.read-only-connections svg.plumb-connector,
162
+ #canvas.read-only-connections svg.plumb-connector * {
163
+ pointer-events: none !important;
164
+ cursor: default !important;
198
165
  }
199
166
 
200
- body
201
- #canvas.read-only-connections
202
- .plumb-connector.jtk-hover
203
- .plumb-arrow {
204
- fill: var(--color-connectors) !important;
205
- ponter-events: none;
167
+ svg.plumb-connector.removing path {
168
+ stroke: var(--color-error);
206
169
  }
207
170
 
208
- body #canvas.read-only-connections svg {
209
- pointer-events: none;
171
+ svg.plumb-connector.removing .plumb-arrow {
172
+ fill: var(--color-error);
210
173
  }
211
174
 
212
- /* Activity overlays on connections */
213
- .jtk-overlay.activity-overlay {
214
- background: #f3f3f3;
215
- border: 1px solid #d9d9d9;
216
- color: #333;
217
- border-radius: 4px;
218
- padding: 2px 4px;
219
- font-size: 10px;
220
- font-weight: 600;
221
- line-height: 0.9;
222
- cursor: pointer;
223
- z-index: 500;
224
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
175
+ svg.plumb-connector.dragging {
176
+ z-index: 99999;
225
177
  }
226
178
 
227
179
  /* Active contact count on nodes */
@@ -243,6 +195,30 @@ export class Editor extends RapidElement {
243
195
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
244
196
  }
245
197
 
198
+ /* Activity overlay badges on connection exit stubs */
199
+ .activity-overlay {
200
+ position: absolute;
201
+ background: #f3f3f3;
202
+ border: 1px solid #d9d9d9;
203
+ color: #333;
204
+ border-radius: 4px;
205
+ padding: 2px 4px;
206
+ font-size: 10px;
207
+ font-weight: 600;
208
+ line-height: 0.9;
209
+ cursor: pointer;
210
+ z-index: 500;
211
+ pointer-events: auto;
212
+ white-space: nowrap;
213
+ user-select: none;
214
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
215
+ }
216
+
217
+ #grid.viewing-revision .activity-overlay {
218
+ opacity: 0.5;
219
+ pointer-events: none;
220
+ }
221
+
246
222
  /* Recent contacts popup */
247
223
  @keyframes popupBounceIn {
248
224
  0% {
@@ -312,7 +288,6 @@ export class Editor extends RapidElement {
312
288
 
313
289
  .recent-contacts-popup .contact-name:hover {
314
290
  text-decoration: underline;
315
- color: var(--color-link-primary, #1d4ed8);
316
291
  }
317
292
 
318
293
  .recent-contacts-popup .contact-operand {
@@ -328,17 +303,6 @@ export class Editor extends RapidElement {
328
303
  color: #999;
329
304
  }
330
305
 
331
- /* Connection dragging feedback */
332
- body svg.jtk-connector.jtk-dragging {
333
- z-index: 99999 !important;
334
- }
335
-
336
- .katavorio-drag-no-select svg.jtk-connector path,
337
- .katavorio-drag-no-select svg.jtk-endpoint path {
338
- pointer-events: none !important;
339
- border: 1px solid purple;
340
- }
341
-
342
306
  /* Connection target feedback */
343
307
  temba-flow-node.connection-target-valid {
344
308
  outline: 3px solid var(--color-success, #22c55e) !important;
@@ -368,10 +332,6 @@ export class Editor extends RapidElement {
368
332
  border-radius: var(--curvature);
369
333
  }
370
334
 
371
- .jtk-floating-endpoint {
372
- pointer-events: none;
373
- }
374
-
375
335
  .localization-window-content {
376
336
  display: flex;
377
337
  flex-direction: column;
@@ -587,6 +547,9 @@ export class Editor extends RapidElement {
587
547
  this.dragFromNodeId = null;
588
548
  this.originalConnectionTargetId = null;
589
549
  this.isValidTarget = true;
550
+ // Canvas-relative source exit position (set at drag start)
551
+ this.connectionSourceX = null;
552
+ this.connectionSourceY = null;
590
553
  this.localizationWindowHidden = true;
591
554
  this.translationFilters = {
592
555
  categories: false
@@ -635,15 +598,13 @@ export class Editor extends RapidElement {
635
598
  getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
636
599
  }
637
600
  this.plumber.on('connection:drag', (connection) => {
638
- // console.log('connection:drag', connection);
639
- this.dragFromNodeId =
640
- connection.data.nodeId ||
641
- document.getElementById(connection.sourceId).closest('.node').id;
601
+ this.dragFromNodeId = connection.data.nodeId;
642
602
  this.sourceId = connection.sourceId;
603
+ this.connectionSourceX = connection.sourceX;
604
+ this.connectionSourceY = connection.sourceY;
643
605
  this.originalConnectionTargetId = connection.target.id;
644
606
  });
645
607
  this.plumber.on('connection:abort', (info) => {
646
- // console.log('Connection aborted', info);
647
608
  this.makeConnection(info);
648
609
  });
649
610
  this.plumber.on('connection:detach', (info) => {
@@ -672,6 +633,7 @@ export class Editor extends RapidElement {
672
633
  left: snapToGrid(this.connectionPlaceholder.position.left),
673
634
  top: snapToGrid(this.connectionPlaceholder.position.top)
674
635
  };
636
+ const isDragUp = !!this.connectionPlaceholder.dragUp;
675
637
  // Update the placeholder to the snapped position
676
638
  this.connectionPlaceholder.position = snappedPosition;
677
639
  // Store the pending connection info
@@ -680,12 +642,14 @@ export class Editor extends RapidElement {
680
642
  exitId: this.sourceId,
681
643
  position: snappedPosition
682
644
  };
683
- // Show the context menu just below the placeholder
645
+ // Show the context menu near the placeholder
684
646
  const canvas = this.querySelector('#canvas');
685
647
  if (canvas) {
686
648
  const canvasRect = canvas.getBoundingClientRect();
687
- const menuX = canvasRect.left + snappedPosition.left - 40; // center horizontally
688
- const menuY = canvasRect.top + snappedPosition.top + 80; // just below placeholder
649
+ const menuX = canvasRect.left + snappedPosition.left - 40;
650
+ const menuY = isDragUp
651
+ ? canvasRect.top + snappedPosition.top + 74 // just below placeholder bottom
652
+ : canvasRect.top + snappedPosition.top + 80; // just below placeholder
689
653
  const canvasMenu = this.querySelector('temba-canvas-menu');
690
654
  if (canvasMenu) {
691
655
  canvasMenu.show(menuX, menuY, {
@@ -706,6 +670,8 @@ export class Editor extends RapidElement {
706
670
  // Clear connection state (but keep sourceId/dragFromNodeId if we have a pending connection)
707
671
  if (!this.pendingCanvasConnection) {
708
672
  this.sourceId = null;
673
+ this.connectionSourceX = null;
674
+ this.connectionSourceY = null;
709
675
  this.dragFromNodeId = null;
710
676
  }
711
677
  this.targetId = null;
@@ -862,7 +828,7 @@ export class Editor extends RapidElement {
862
828
  clearTimeout(this.activityTimer);
863
829
  }
864
830
  this.activityTimer = window.setTimeout(() => {
865
- // this.fetchActivityData();
831
+ this.fetchActivityData();
866
832
  }, this.activityInterval);
867
833
  });
868
834
  }
@@ -1219,72 +1185,52 @@ export class Editor extends RapidElement {
1219
1185
  renderConnectionPlaceholder() {
1220
1186
  if (!this.connectionPlaceholder || !this.connectionPlaceholder.visible)
1221
1187
  return '';
1222
- const { position } = this.connectionPlaceholder;
1188
+ const { position, dragUp } = this.connectionPlaceholder;
1223
1189
  // Render connection line when we have a pending connection (after drop)
1224
1190
  let svgPath = null;
1225
- if (this.sourceId && this.dragFromNodeId && this.pendingCanvasConnection) {
1226
- const sourceElement = document.getElementById(this.sourceId);
1227
- if (sourceElement) {
1228
- const sourceRect = sourceElement.getBoundingClientRect();
1229
- const canvas = this.querySelector('#canvas');
1230
- const canvasRect = canvas.getBoundingClientRect();
1231
- // Source point (bottom center of exit)
1232
- const sourceX = sourceRect.left + sourceRect.width / 2 - canvasRect.left;
1233
- const sourceY = sourceRect.bottom - canvasRect.top;
1234
- // Target point (top center of placeholder)
1235
- const targetX = position.left + 100; // 100 is half the placeholder width (200px)
1236
- const targetY = position.top;
1237
- // Use jsPlumb FlowchartConnector parameters: stub [20, 10], cornerRadius 5
1238
- const stubStart = 20;
1239
- const stubEnd = 10;
1240
- const cornerRadius = 5;
1241
- // Calculate flowchart path with corners
1242
- const verticalStart = sourceY + stubStart;
1243
- const verticalEnd = targetY - stubEnd;
1244
- const midY = (verticalStart + verticalEnd) / 2;
1245
- // Build path with rounded corners (flowchart style)
1246
- let pathData = `M ${sourceX} ${sourceY} L ${sourceX} ${verticalStart}`;
1247
- if (sourceX !== targetX) {
1248
- // Horizontal segment needed
1249
- if (Math.abs(verticalEnd - verticalStart) > cornerRadius * 2) {
1250
- // Enough space for corners
1251
- pathData += ` L ${sourceX} ${midY - cornerRadius}`;
1252
- pathData += ` Q ${sourceX} ${midY}, ${sourceX + (targetX > sourceX ? cornerRadius : -cornerRadius)} ${midY}`;
1253
- pathData += ` L ${targetX - (targetX > sourceX ? cornerRadius : -cornerRadius)} ${midY}`;
1254
- pathData += ` Q ${targetX} ${midY}, ${targetX} ${midY + cornerRadius}`;
1255
- pathData += ` L ${targetX} ${verticalEnd}`;
1256
- }
1257
- else {
1258
- // Direct horizontal transition
1259
- pathData += ` L ${targetX} ${verticalStart}`;
1260
- pathData += ` L ${targetX} ${verticalEnd}`;
1261
- }
1262
- }
1263
- else {
1264
- // Straight vertical line
1265
- pathData += ` L ${targetX} ${verticalEnd}`;
1266
- }
1267
- pathData += ` L ${targetX} ${targetY}`;
1268
- svgPath = html `
1269
- <svg
1270
- style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;"
1271
- >
1272
- <path
1273
- d="${pathData}"
1274
- fill="none"
1275
- stroke="var(--color-connectors, #ccc)"
1276
- stroke-width="3"
1277
- class="plumb-connector"
1278
- />
1279
- <polygon
1280
- points="${targetX},${targetY} ${targetX - 6.5},${targetY -
1281
- 13} ${targetX + 6.5},${targetY - 13}"
1282
- fill="var(--color-connectors, #ccc)"
1283
- class="plumb-arrow"
1284
- />
1285
- </svg>
1286
- `;
1191
+ if (this.sourceId &&
1192
+ this.dragFromNodeId &&
1193
+ this.pendingCanvasConnection &&
1194
+ this.connectionSourceX != null &&
1195
+ this.connectionSourceY != null) {
1196
+ const sourceX = this.connectionSourceX;
1197
+ const sourceY = this.connectionSourceY;
1198
+ const targetX = position.left + 100;
1199
+ // When dragging up, connect to the placeholder bottom; otherwise to the top
1200
+ const targetY = dragUp ? position.top + 64 : position.top;
1201
+ const routeFace = dragUp
1202
+ ? targetX < sourceX
1203
+ ? 'left'
1204
+ : 'right'
1205
+ : 'top';
1206
+ const pathData = calculateFlowchartPath(sourceX, sourceY, targetX, targetY, 20, dragUp ? 0 : 10, 5, routeFace);
1207
+ const aw = ARROW_HALF_WIDTH;
1208
+ const al = ARROW_LENGTH;
1209
+ let arrowPoints;
1210
+ if (dragUp) {
1211
+ // Arrow tip pointing up, base at placeholder bottom
1212
+ arrowPoints = `${targetX},${targetY - al} ${targetX - aw},${targetY} ${targetX + aw},${targetY}`;
1213
+ }
1214
+ else {
1215
+ // Arrow pointing down into top of placeholder
1216
+ arrowPoints = `${targetX},${targetY} ${targetX - aw},${targetY - al} ${targetX + aw},${targetY - al}`;
1287
1217
  }
1218
+ svgPath = html `
1219
+ <svg
1220
+ style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;"
1221
+ >
1222
+ <path
1223
+ d="${pathData}"
1224
+ fill="none"
1225
+ stroke="var(--color-connectors, #ccc)"
1226
+ stroke-width="3"
1227
+ />
1228
+ <polygon
1229
+ points="${arrowPoints}"
1230
+ fill="var(--color-connectors, #ccc)"
1231
+ />
1232
+ </svg>
1233
+ `;
1288
1234
  }
1289
1235
  return html `${svgPath}
1290
1236
  <div
@@ -1306,17 +1252,13 @@ export class Editor extends RapidElement {
1306
1252
  }
1307
1253
  /**
1308
1254
  * Checks for node collisions and reflows nodes as needed.
1309
- * Nodes are only moved downward to resolve collisions.
1310
- *
1311
- * @param movedNodeUuids - UUIDs of nodes that were just moved/dropped
1312
- * @param droppedNodeUuid - UUID of the specific node that was dropped (if applicable)
1313
- * @param dropTargetBounds - Bounds of the node that was dropped onto (if applicable)
1255
+ * Sacred nodes (just moved/dropped) keep their positions while
1256
+ * other nodes are moved in the least-disruptive direction.
1314
1257
  */
1315
- checkCollisionsAndReflow(movedNodeUuids, droppedNodeUuid = null, dropTargetBounds = null) {
1258
+ checkCollisionsAndReflow(sacredNodeUuids) {
1316
1259
  var _b;
1317
1260
  if (!this.definition)
1318
1261
  return;
1319
- // Get all node bounds (only for actual nodes, not stickies)
1320
1262
  const allBounds = [];
1321
1263
  for (const node of this.definition.nodes) {
1322
1264
  const nodeUI = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.nodes[node.uuid];
@@ -1327,35 +1269,13 @@ export class Editor extends RapidElement {
1327
1269
  allBounds.push(bounds);
1328
1270
  }
1329
1271
  }
1330
- // Check if we need to determine midpoint priority for a dropped node
1331
- let targetHasPriority = false;
1332
- if (droppedNodeUuid && dropTargetBounds) {
1333
- const droppedBounds = allBounds.find((b) => b.uuid === droppedNodeUuid);
1334
- if (droppedBounds) {
1335
- // Check if the bottom of the dropped node is below the midpoint of the target
1336
- // If bottom is above midpoint, dropped node gets preference (targetHasPriority = false)
1337
- // If bottom is below midpoint, target gets preference (targetHasPriority = true)
1338
- const droppedBottom = droppedBounds.bottom;
1339
- const targetMidpoint = dropTargetBounds.top + dropTargetBounds.height / 2;
1340
- targetHasPriority = droppedBottom > targetMidpoint;
1341
- }
1342
- }
1343
- // Calculate reflow positions for each moved node
1344
- const allReflowPositions = {};
1345
- for (const movedUuid of movedNodeUuids) {
1346
- const movedBounds = allBounds.find((b) => b.uuid === movedUuid);
1347
- if (!movedBounds)
1348
- continue;
1349
- // Calculate reflow for this moved node
1350
- const reflowPositions = calculateReflowPositions(movedUuid, movedBounds, allBounds, droppedNodeUuid === movedUuid ? targetHasPriority : false);
1351
- // Merge into all reflow positions
1272
+ const reflowPositions = calculateReflowPositions(sacredNodeUuids, allBounds);
1273
+ if (reflowPositions.size > 0) {
1274
+ const positions = {};
1352
1275
  for (const [uuid, position] of reflowPositions.entries()) {
1353
- allReflowPositions[uuid] = position;
1276
+ positions[uuid] = position;
1354
1277
  }
1355
- }
1356
- // If there are positions to update, apply them
1357
- if (Object.keys(allReflowPositions).length > 0) {
1358
- getStore().getState().updateCanvasPositions(allReflowPositions);
1278
+ getStore().getState().updateCanvasPositions(positions);
1359
1279
  }
1360
1280
  }
1361
1281
  handleMouseMove(event) {
@@ -1390,21 +1310,37 @@ export class Editor extends RapidElement {
1390
1310
  this.targetId = null;
1391
1311
  this.isValidTarget = true;
1392
1312
  // Show connection placeholder when over empty canvas
1393
- // Calculate position: horizontally centered at mouse, vertically just below mouse
1394
1313
  const canvas = this.querySelector('#canvas');
1395
1314
  if (canvas) {
1396
1315
  const canvasRect = canvas.getBoundingClientRect();
1397
1316
  const relativeX = event.clientX - canvasRect.left;
1398
1317
  const relativeY = event.clientY - canvasRect.top;
1399
- // offset the placeholder so it's centered horizontally and just below the mouse
1400
- const placeholderWidth = 200; // approximate node width
1401
- const placeholderOffset = 20; // distance below mouse cursor
1318
+ const placeholderWidth = 200;
1319
+ const placeholderHeight = 64;
1320
+ const arrowLength = ARROW_LENGTH;
1321
+ const cursorGap = CURSOR_GAP;
1322
+ // Determine if cursor is above the source exit using stored sourceY
1323
+ const dragUp = this.connectionSourceY != null
1324
+ ? relativeY < this.connectionSourceY
1325
+ : false;
1326
+ let top;
1327
+ if (dragUp) {
1328
+ // Arrow points up: tip at cy + cursorGap.
1329
+ // Placeholder bottom should sit just above the arrow tip.
1330
+ top = relativeY + cursorGap - placeholderHeight;
1331
+ }
1332
+ else {
1333
+ // Arrow points down: tip at cy - cursorGap + arrowLength.
1334
+ // Placeholder top sits just below the arrow tip.
1335
+ top = relativeY - cursorGap + arrowLength;
1336
+ }
1402
1337
  this.connectionPlaceholder = {
1403
1338
  position: {
1404
1339
  left: relativeX - placeholderWidth / 2,
1405
- top: relativeY + placeholderOffset
1340
+ top
1406
1341
  },
1407
- visible: true
1342
+ visible: true,
1343
+ dragUp
1408
1344
  };
1409
1345
  }
1410
1346
  }
@@ -1505,34 +1441,7 @@ export class Editor extends RapidElement {
1505
1441
  if (nodeUuids.length > 0) {
1506
1442
  // Allow DOM to update before checking collisions
1507
1443
  setTimeout(() => {
1508
- var _b, _c;
1509
- // If only one node was moved, detect which node it might have been dropped onto
1510
- let droppedNodeUuid = null;
1511
- let dropTargetBounds = null;
1512
- if (nodeUuids.length === 1) {
1513
- droppedNodeUuid = nodeUuids[0];
1514
- const droppedNodeUI = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.nodes[droppedNodeUuid];
1515
- if (droppedNodeUI === null || droppedNodeUI === void 0 ? void 0 : droppedNodeUI.position) {
1516
- const droppedBounds = getNodeBounds(droppedNodeUuid, droppedNodeUI.position);
1517
- if (droppedBounds) {
1518
- // Find which node (if any) the dropped node overlaps with
1519
- for (const node of this.definition.nodes) {
1520
- if (node.uuid === droppedNodeUuid)
1521
- continue;
1522
- const nodeUI = (_c = this.definition._ui) === null || _c === void 0 ? void 0 : _c.nodes[node.uuid];
1523
- if (!(nodeUI === null || nodeUI === void 0 ? void 0 : nodeUI.position))
1524
- continue;
1525
- const targetBounds = getNodeBounds(node.uuid, nodeUI.position);
1526
- if (targetBounds &&
1527
- nodesOverlap(droppedBounds, targetBounds)) {
1528
- dropTargetBounds = targetBounds;
1529
- break; // Use the first overlapping node
1530
- }
1531
- }
1532
- }
1533
- }
1534
- }
1535
- this.checkCollisionsAndReflow(nodeUuids, droppedNodeUuid, dropTargetBounds);
1444
+ this.checkCollisionsAndReflow(nodeUuids);
1536
1445
  }, 0);
1537
1446
  }
1538
1447
  else {
@@ -1642,6 +1551,8 @@ export class Editor extends RapidElement {
1642
1551
  this.pendingCanvasConnection = null;
1643
1552
  this.connectionPlaceholder = null;
1644
1553
  this.sourceId = null;
1554
+ this.connectionSourceX = null;
1555
+ this.connectionSourceY = null;
1645
1556
  this.dragFromNodeId = null;
1646
1557
  }
1647
1558
  else {
@@ -1669,6 +1580,8 @@ export class Editor extends RapidElement {
1669
1580
  this.pendingCanvasConnection = null;
1670
1581
  this.connectionPlaceholder = null;
1671
1582
  this.sourceId = null;
1583
+ this.connectionSourceX = null;
1584
+ this.connectionSourceY = null;
1672
1585
  this.dragFromNodeId = null;
1673
1586
  this.originalConnectionTargetId = null;
1674
1587
  }
@@ -1856,6 +1769,8 @@ export class Editor extends RapidElement {
1856
1769
  this.pendingCanvasConnection = null;
1857
1770
  this.connectionPlaceholder = null;
1858
1771
  this.sourceId = null;
1772
+ this.connectionSourceX = null;
1773
+ this.connectionSourceY = null;
1859
1774
  this.dragFromNodeId = null;
1860
1775
  }
1861
1776
  // Reset the creation flags
@@ -1910,6 +1825,8 @@ export class Editor extends RapidElement {
1910
1825
  this.pendingCanvasConnection = null;
1911
1826
  this.connectionPlaceholder = null;
1912
1827
  this.sourceId = null;
1828
+ this.connectionSourceX = null;
1829
+ this.connectionSourceY = null;
1913
1830
  this.dragFromNodeId = null;
1914
1831
  }
1915
1832
  // Reset the creation flags
@@ -2655,7 +2572,7 @@ export class Editor extends RapidElement {
2655
2572
  icon="revisions"
2656
2573
  label="Revisions"
2657
2574
  color="rgb(142, 94, 167)"
2658
- top="105"
2575
+ order="1"
2659
2576
  .hidden=${!this.revisionsWindowHidden && this.localizationWindowHidden}
2660
2577
  @temba-button-clicked=${this.handleRevisionsTabClick}
2661
2578
  ></temba-floating-tab>
@@ -2926,7 +2843,7 @@ export class Editor extends RapidElement {
2926
2843
  icon="language"
2927
2844
  label="Translate Flow"
2928
2845
  color="#6b7280"
2929
- top="180"
2846
+ order="2"
2930
2847
  .hidden=${!this.localizationWindowHidden}
2931
2848
  @temba-button-clicked=${this.handleLocalizationTabClick}
2932
2849
  ></temba-floating-tab>