@nyaruka/temba-components 0.132.0 → 0.134.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 (181) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/demo/components/flow/example.html +1 -0
  3. package/demo/components/webchat/example.html +1 -1
  4. package/demo/static/css/tailwind.css +30019 -0
  5. package/dist/locales/es.js +5 -5
  6. package/dist/locales/es.js.map +1 -1
  7. package/dist/locales/fr.js +5 -5
  8. package/dist/locales/fr.js.map +1 -1
  9. package/dist/locales/locale-codes.js +2 -11
  10. package/dist/locales/locale-codes.js.map +1 -1
  11. package/dist/locales/pt.js +5 -5
  12. package/dist/locales/pt.js.map +1 -1
  13. package/dist/temba-components.js +555 -476
  14. package/dist/temba-components.js.map +1 -1
  15. package/out-tsc/src/display/Chat.js +248 -95
  16. package/out-tsc/src/display/Chat.js.map +1 -1
  17. package/out-tsc/src/display/FloatingTab.js +4 -4
  18. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  19. package/out-tsc/src/display/TembaUser.js +3 -3
  20. package/out-tsc/src/display/TembaUser.js.map +1 -1
  21. package/out-tsc/src/events.js.map +1 -1
  22. package/out-tsc/src/flow/CanvasNode.js +132 -58
  23. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  24. package/out-tsc/src/flow/Editor.js +183 -58
  25. package/out-tsc/src/flow/Editor.js.map +1 -1
  26. package/out-tsc/src/flow/utils.js +141 -0
  27. package/out-tsc/src/flow/utils.js.map +1 -1
  28. package/out-tsc/src/interfaces.js.map +1 -1
  29. package/out-tsc/src/layout/FloatingWindow.js +1 -2
  30. package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
  31. package/out-tsc/src/list/ContentMenu.js +1 -0
  32. package/out-tsc/src/list/ContentMenu.js.map +1 -1
  33. package/out-tsc/src/list/SortableList.js +3 -2
  34. package/out-tsc/src/list/SortableList.js.map +1 -1
  35. package/out-tsc/src/live/ContactChat.js +184 -205
  36. package/out-tsc/src/live/ContactChat.js.map +1 -1
  37. package/out-tsc/src/locales/es.js +5 -5
  38. package/out-tsc/src/locales/es.js.map +1 -1
  39. package/out-tsc/src/locales/fr.js +5 -5
  40. package/out-tsc/src/locales/fr.js.map +1 -1
  41. package/out-tsc/src/locales/locale-codes.js +2 -11
  42. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  43. package/out-tsc/src/locales/pt.js +5 -5
  44. package/out-tsc/src/locales/pt.js.map +1 -1
  45. package/out-tsc/src/store/AppState.js +34 -0
  46. package/out-tsc/src/store/AppState.js.map +1 -1
  47. package/out-tsc/src/store/Store.js +5 -5
  48. package/out-tsc/src/store/Store.js.map +1 -1
  49. package/out-tsc/src/utils.js +3 -3
  50. package/out-tsc/src/utils.js.map +1 -1
  51. package/out-tsc/src/webchat/WebChat.js +22 -9
  52. package/out-tsc/src/webchat/WebChat.js.map +1 -1
  53. package/out-tsc/test/ActionHelper.js +6 -5
  54. package/out-tsc/test/ActionHelper.js.map +1 -1
  55. package/out-tsc/test/actions/send_broadcast.test.js +9 -4
  56. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
  57. package/out-tsc/test/temba-contact-chat.test.js +1 -1
  58. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  59. package/out-tsc/test/temba-floating-window.test.js +0 -2
  60. package/out-tsc/test/temba-floating-window.test.js.map +1 -1
  61. package/out-tsc/test/temba-flow-collision.test.js +673 -0
  62. package/out-tsc/test/temba-flow-collision.test.js.map +1 -0
  63. package/out-tsc/test/temba-flow-editor-node.test.js +195 -0
  64. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  65. package/out-tsc/test/temba-utils-uuid.test.js +45 -1
  66. package/out-tsc/test/temba-utils-uuid.test.js.map +1 -1
  67. package/out-tsc/test/utils.test.js +2 -2
  68. package/out-tsc/test/utils.test.js.map +1 -1
  69. package/package.json +1 -1
  70. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  71. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  72. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  73. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  74. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  75. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  76. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  77. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  78. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  79. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  80. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  81. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  82. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  83. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  84. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  85. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  86. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  87. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  88. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  89. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  90. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  91. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  92. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  93. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  94. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  95. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  96. package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
  97. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  98. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  99. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  100. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  101. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  102. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  103. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  104. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  105. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  106. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  107. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  108. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  109. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  110. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  111. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  112. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  113. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  114. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  115. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  116. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  117. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  118. package/screenshots/truth/contacts/chat-failure.png +0 -0
  119. package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
  120. package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
  121. package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
  122. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  123. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  124. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  125. package/screenshots/truth/floating-tab/default.png +0 -0
  126. package/screenshots/truth/floating-tab/gray.png +0 -0
  127. package/screenshots/truth/floating-tab/green.png +0 -0
  128. package/screenshots/truth/floating-tab/hover.png +0 -0
  129. package/screenshots/truth/floating-tab/purple.png +0 -0
  130. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  131. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  132. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  133. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  134. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  135. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  136. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  137. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  138. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  139. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  140. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  141. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  142. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  143. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  144. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  145. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  146. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  147. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  148. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  149. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  150. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  151. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  152. package/src/display/Chat.ts +331 -135
  153. package/src/display/FloatingTab.ts +4 -4
  154. package/src/display/TembaUser.ts +3 -2
  155. package/src/events.ts +12 -12
  156. package/src/flow/CanvasNode.ts +140 -57
  157. package/src/flow/Editor.ts +240 -58
  158. package/src/flow/utils.ts +207 -1
  159. package/src/interfaces.ts +7 -0
  160. package/src/layout/FloatingWindow.ts +1 -3
  161. package/src/list/ContentMenu.ts +1 -0
  162. package/src/list/SortableList.ts +3 -2
  163. package/src/live/ContactChat.ts +195 -221
  164. package/src/locales/es.ts +13 -18
  165. package/src/locales/fr.ts +13 -18
  166. package/src/locales/locale-codes.ts +2 -11
  167. package/src/locales/pt.ts +13 -18
  168. package/src/store/AppState.ts +43 -0
  169. package/src/store/Store.ts +5 -5
  170. package/src/utils.ts +3 -3
  171. package/src/webchat/WebChat.ts +24 -10
  172. package/test/ActionHelper.ts +13 -5
  173. package/test/actions/send_broadcast.test.ts +4 -2
  174. package/test/temba-contact-chat.test.ts +1 -1
  175. package/test/temba-floating-window.test.ts +0 -2
  176. package/test/temba-flow-collision.test.ts +833 -0
  177. package/test/temba-flow-editor-node.test.ts +224 -0
  178. package/test/temba-utils-uuid.test.ts +61 -1
  179. package/test/utils.test.ts +7 -2
  180. package/test-assets/contacts/history.json +22 -9
  181. package/web-test-runner.config.mjs +3 -3
@@ -12,6 +12,7 @@ import { ACTION_CONFIG, NODE_CONFIG } from './config';
12
12
  import { ACTION_GROUP_METADATA } from './types';
13
13
  import { Plumber } from './Plumber';
14
14
  import { CanvasNode } from './CanvasNode';
15
+ import { getNodeBounds, calculateReflowPositions, nodesOverlap } from './utils';
15
16
  export function snapToGrid(value) {
16
17
  const snapped = Math.round(value / 20) * 20;
17
18
  return Math.max(snapped, 0);
@@ -87,8 +88,6 @@ export class Editor extends RapidElement {
87
88
  );
88
89
  background-size: 20px 20px;
89
90
  background-position: 10px 10px;
90
- box-shadow: inset -5px 0 10px rgba(0, 0, 0, 0.05);
91
- border-top: 1px solid #e0e0e0;
92
91
  width: 100%;
93
92
  display: flex;
94
93
  }
@@ -107,6 +106,7 @@ export class Editor extends RapidElement {
107
106
 
108
107
  #canvas > .dragging {
109
108
  z-index: 99999 !important;
109
+ transition: none !important;
110
110
  }
111
111
 
112
112
  body .jtk-endpoint {
@@ -553,11 +553,18 @@ export class Editor extends RapidElement {
553
553
  saveChanges() {
554
554
  // post the flow definition to the server
555
555
  getStore()
556
- .postJSON(`/flow/revisions/${this.flow}`, this.definition)
556
+ .postJSON(`/flow/revisions/${this.flow}/`, this.definition)
557
557
  .then((response) => {
558
- // Update flow info with the response data
559
- if (response.json && response.json.info) {
560
- getStore().getState().setFlowInfo(response.json.info);
558
+ var _b;
559
+ // Update flow info and revision with the response data
560
+ if (response.json) {
561
+ const state = getStore().getState();
562
+ if (response.json.info) {
563
+ state.setFlowInfo(response.json.info);
564
+ }
565
+ if (((_b = response.json.revision) === null || _b === void 0 ? void 0 : _b.revision) !== undefined) {
566
+ state.setRevision(response.json.revision.revision);
567
+ }
561
568
  }
562
569
  })
563
570
  .catch((error) => {
@@ -604,6 +611,8 @@ export class Editor extends RapidElement {
604
611
  this.addEventListener(CustomEventType.AddActionRequested, this.handleAddActionRequested.bind(this));
605
612
  // Listen for node edit requests from flow nodes
606
613
  this.addEventListener(CustomEventType.NodeEditRequested, this.handleNodeEditRequested.bind(this));
614
+ // Listen for node deletion events
615
+ this.addEventListener(CustomEventType.NodeDeleted, this.handleNodeDeleted.bind(this));
607
616
  // Listen for canvas menu selections
608
617
  this.addEventListener(CustomEventType.Selection, (event) => {
609
618
  const target = event.target;
@@ -905,6 +914,60 @@ export class Editor extends RapidElement {
905
914
  </div>
906
915
  </div>`;
907
916
  }
917
+ /**
918
+ * Checks for node collisions and reflows nodes as needed.
919
+ * Nodes are only moved downward to resolve collisions.
920
+ *
921
+ * @param movedNodeUuids - UUIDs of nodes that were just moved/dropped
922
+ * @param droppedNodeUuid - UUID of the specific node that was dropped (if applicable)
923
+ * @param dropTargetBounds - Bounds of the node that was dropped onto (if applicable)
924
+ */
925
+ checkCollisionsAndReflow(movedNodeUuids, droppedNodeUuid = null, dropTargetBounds = null) {
926
+ var _b;
927
+ if (!this.definition)
928
+ return;
929
+ // Get all node bounds (only for actual nodes, not stickies)
930
+ const allBounds = [];
931
+ for (const node of this.definition.nodes) {
932
+ const nodeUI = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.nodes[node.uuid];
933
+ if (!(nodeUI === null || nodeUI === void 0 ? void 0 : nodeUI.position))
934
+ continue;
935
+ const bounds = getNodeBounds(node.uuid, nodeUI.position);
936
+ if (bounds) {
937
+ allBounds.push(bounds);
938
+ }
939
+ }
940
+ // Check if we need to determine midpoint priority for a dropped node
941
+ let targetHasPriority = false;
942
+ if (droppedNodeUuid && dropTargetBounds) {
943
+ const droppedBounds = allBounds.find((b) => b.uuid === droppedNodeUuid);
944
+ if (droppedBounds) {
945
+ // Check if the bottom of the dropped node is below the midpoint of the target
946
+ // If bottom is above midpoint, dropped node gets preference (targetHasPriority = false)
947
+ // If bottom is below midpoint, target gets preference (targetHasPriority = true)
948
+ const droppedBottom = droppedBounds.bottom;
949
+ const targetMidpoint = dropTargetBounds.top + dropTargetBounds.height / 2;
950
+ targetHasPriority = droppedBottom > targetMidpoint;
951
+ }
952
+ }
953
+ // Calculate reflow positions for each moved node
954
+ const allReflowPositions = {};
955
+ for (const movedUuid of movedNodeUuids) {
956
+ const movedBounds = allBounds.find((b) => b.uuid === movedUuid);
957
+ if (!movedBounds)
958
+ continue;
959
+ // Calculate reflow for this moved node
960
+ const reflowPositions = calculateReflowPositions(movedUuid, movedBounds, allBounds, droppedNodeUuid === movedUuid ? targetHasPriority : false);
961
+ // Merge into all reflow positions
962
+ for (const [uuid, position] of reflowPositions.entries()) {
963
+ allReflowPositions[uuid] = position;
964
+ }
965
+ }
966
+ // If there are positions to update, apply them
967
+ if (Object.keys(allReflowPositions).length > 0) {
968
+ getStore().getState().updateCanvasPositions(allReflowPositions);
969
+ }
970
+ }
908
971
  handleMouseMove(event) {
909
972
  // Handle selection box drawing
910
973
  if (this.canvasMouseDown && !this.isMouseDown) {
@@ -1024,9 +1087,48 @@ export class Editor extends RapidElement {
1024
1087
  });
1025
1088
  if (Object.keys(newPositions).length > 0) {
1026
1089
  getStore().getState().updateCanvasPositions(newPositions);
1027
- setTimeout(() => {
1028
- this.plumber.repaintEverything();
1029
- }, 0);
1090
+ // Check for collisions and reflow nodes after updating positions
1091
+ // Filter to only check nodes (not stickies)
1092
+ const nodeUuids = itemsToMove.filter((uuid) => this.definition.nodes.find((node) => node.uuid === uuid));
1093
+ if (nodeUuids.length > 0) {
1094
+ // Allow DOM to update before checking collisions
1095
+ setTimeout(() => {
1096
+ var _b, _c;
1097
+ // If only one node was moved, detect which node it might have been dropped onto
1098
+ let droppedNodeUuid = null;
1099
+ let dropTargetBounds = null;
1100
+ if (nodeUuids.length === 1) {
1101
+ droppedNodeUuid = nodeUuids[0];
1102
+ const droppedNodeUI = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.nodes[droppedNodeUuid];
1103
+ if (droppedNodeUI === null || droppedNodeUI === void 0 ? void 0 : droppedNodeUI.position) {
1104
+ const droppedBounds = getNodeBounds(droppedNodeUuid, droppedNodeUI.position);
1105
+ if (droppedBounds) {
1106
+ // Find which node (if any) the dropped node overlaps with
1107
+ for (const node of this.definition.nodes) {
1108
+ if (node.uuid === droppedNodeUuid)
1109
+ continue;
1110
+ const nodeUI = (_c = this.definition._ui) === null || _c === void 0 ? void 0 : _c.nodes[node.uuid];
1111
+ if (!(nodeUI === null || nodeUI === void 0 ? void 0 : nodeUI.position))
1112
+ continue;
1113
+ const targetBounds = getNodeBounds(node.uuid, nodeUI.position);
1114
+ if (targetBounds &&
1115
+ nodesOverlap(droppedBounds, targetBounds)) {
1116
+ dropTargetBounds = targetBounds;
1117
+ break; // Use the first overlapping node
1118
+ }
1119
+ }
1120
+ }
1121
+ }
1122
+ }
1123
+ this.checkCollisionsAndReflow(nodeUuids, droppedNodeUuid, dropTargetBounds);
1124
+ }, 0);
1125
+ }
1126
+ else {
1127
+ // No nodes moved, just repaint connections
1128
+ setTimeout(() => {
1129
+ this.plumber.repaintEverything();
1130
+ }, 0);
1131
+ }
1030
1132
  }
1031
1133
  this.selectedItems.clear();
1032
1134
  }
@@ -1255,6 +1357,12 @@ export class Editor extends RapidElement {
1255
1357
  this.editingNode = event.detail.node;
1256
1358
  this.editingNodeUI = event.detail.nodeUI;
1257
1359
  }
1360
+ handleNodeDeleted(event) {
1361
+ const nodeUuid = event.detail.uuid;
1362
+ if (nodeUuid) {
1363
+ this.deleteNodes([nodeUuid]);
1364
+ }
1365
+ }
1258
1366
  handleActionSaved(updatedAction) {
1259
1367
  var _b, _c;
1260
1368
  if (this.editingNode && this.editingAction) {
@@ -1284,23 +1392,18 @@ export class Editor extends RapidElement {
1284
1392
  // Reset the creation flags
1285
1393
  this.isCreatingNewNode = false;
1286
1394
  this.pendingNodePosition = null;
1287
- // Repaint jsplumb connections
1288
- if (this.plumber) {
1289
- requestAnimationFrame(() => {
1290
- this.plumber.repaintEverything();
1291
- });
1292
- }
1395
+ // Check for collisions and reflow
1396
+ requestAnimationFrame(() => {
1397
+ this.checkCollisionsAndReflow([updatedNode.uuid]);
1398
+ });
1293
1399
  }
1294
1400
  else {
1295
1401
  // Update existing node in the store
1296
1402
  (_c = getStore()) === null || _c === void 0 ? void 0 : _c.getState().updateNode(this.editingNode.uuid, updatedNode);
1297
- // Repaint jsplumb connections in case node size changed
1298
- if (this.plumber) {
1299
- // Use requestAnimationFrame to ensure DOM has been updated first
1300
- requestAnimationFrame(() => {
1301
- this.plumber.repaintEverything();
1302
- });
1303
- }
1403
+ // Check for collisions and reflow in case node size changed
1404
+ requestAnimationFrame(() => {
1405
+ this.checkCollisionsAndReflow([this.editingNode.uuid]);
1406
+ });
1304
1407
  }
1305
1408
  }
1306
1409
  this.closeNodeEditor();
@@ -1334,6 +1437,10 @@ export class Editor extends RapidElement {
1334
1437
  // Reset the creation flags
1335
1438
  this.isCreatingNewNode = false;
1336
1439
  this.pendingNodePosition = null;
1440
+ // Check for collisions and reflow
1441
+ requestAnimationFrame(() => {
1442
+ this.checkCollisionsAndReflow([updatedNode.uuid]);
1443
+ });
1337
1444
  }
1338
1445
  else {
1339
1446
  // This is an existing node - update it
@@ -1355,12 +1462,9 @@ export class Editor extends RapidElement {
1355
1462
  if (uiConfig) {
1356
1463
  (_d = getStore()) === null || _d === void 0 ? void 0 : _d.getState().updateNodeUIConfig(updatedNode.uuid, uiConfig);
1357
1464
  }
1358
- }
1359
- // Repaint jsplumb connections in case node size changed
1360
- if (this.plumber) {
1361
- // Use requestAnimationFrame to ensure DOM has been updated first
1465
+ // Check for collisions and reflow in case node size changed
1362
1466
  requestAnimationFrame(() => {
1363
- this.plumber.repaintEverything();
1467
+ this.checkCollisionsAndReflow([this.editingNode.uuid]);
1364
1468
  });
1365
1469
  }
1366
1470
  }
@@ -1409,7 +1513,7 @@ export class Editor extends RapidElement {
1409
1513
  return { left, top };
1410
1514
  }
1411
1515
  handleActionDragExternal(event) {
1412
- const { action, nodeUuid, actionIndex, mouseX, mouseY, actionHeight = 60 } = event.detail;
1516
+ const { action, nodeUuid, actionIndex, mouseX, mouseY, actionHeight = 60, isLastAction = false } = event.detail;
1413
1517
  // Check if mouse is over another execute_actions node
1414
1518
  const targetNode = this.getNodeAtPosition(mouseX, mouseY);
1415
1519
  if (targetNode && targetNode !== nodeUuid) {
@@ -1472,23 +1576,38 @@ export class Editor extends RapidElement {
1472
1576
  this.previousActionDragTargetNodeUuid = null;
1473
1577
  }
1474
1578
  this.actionDragTargetNodeUuid = null;
1475
- // Tell source node to hide ghost (we're not over a valid target)
1476
1579
  const sourceElement = this.querySelector(`temba-flow-node[data-node-uuid="${nodeUuid}"]`);
1477
- if (sourceElement) {
1478
- sourceElement.dispatchEvent(new CustomEvent('action-hide-ghost', {
1479
- detail: {},
1480
- bubbles: false
1481
- }));
1580
+ // Show canvas drop preview only if this is NOT the last action
1581
+ // Last actions can only be dropped on other nodes, not on canvas
1582
+ if (!isLastAction) {
1583
+ // Hide ghost when showing canvas preview (for canvas drops)
1584
+ if (sourceElement) {
1585
+ sourceElement.dispatchEvent(new CustomEvent('action-hide-ghost', {
1586
+ detail: {},
1587
+ bubbles: false
1588
+ }));
1589
+ }
1590
+ // Don't snap to grid for preview - let it follow cursor smoothly
1591
+ const position = this.calculateCanvasDropPosition(mouseX, mouseY, false);
1592
+ this.canvasDropPreview = {
1593
+ action,
1594
+ nodeUuid,
1595
+ actionIndex,
1596
+ position,
1597
+ actionHeight
1598
+ };
1599
+ }
1600
+ else {
1601
+ // For last action, keep ghost visible (can't drop on canvas)
1602
+ if (sourceElement) {
1603
+ sourceElement.dispatchEvent(new CustomEvent('action-show-ghost', {
1604
+ detail: {},
1605
+ bubbles: false
1606
+ }));
1607
+ }
1608
+ // Clear any existing preview for last action
1609
+ this.canvasDropPreview = null;
1482
1610
  }
1483
- // Don't snap to grid for preview - let it follow cursor smoothly
1484
- const position = this.calculateCanvasDropPosition(mouseX, mouseY, false);
1485
- this.canvasDropPreview = {
1486
- action,
1487
- nodeUuid,
1488
- actionIndex,
1489
- position,
1490
- actionHeight
1491
- };
1492
1611
  // Force re-render to update preview position
1493
1612
  this.requestUpdate();
1494
1613
  }
@@ -1508,8 +1627,8 @@ export class Editor extends RapidElement {
1508
1627
  this.actionDragTargetNodeUuid = null;
1509
1628
  }
1510
1629
  handleActionDropExternal(event) {
1511
- var _b, _c, _d;
1512
- const { action, nodeUuid, actionIndex, mouseX, mouseY } = event.detail;
1630
+ var _b, _c;
1631
+ const { action, nodeUuid, actionIndex, mouseX, mouseY, isLastAction = false } = event.detail;
1513
1632
  // Check if we're dropping on an existing execute_actions node
1514
1633
  const targetNodeUuid = this.actionDragTargetNodeUuid;
1515
1634
  if (targetNodeUuid && targetNodeUuid !== nodeUuid) {
@@ -1532,6 +1651,13 @@ export class Editor extends RapidElement {
1532
1651
  this.actionDragTargetNodeUuid = null;
1533
1652
  return;
1534
1653
  }
1654
+ // If this is the last action and we're not dropping on another node, do nothing
1655
+ // Last actions can only be moved to other nodes, not dropped on canvas
1656
+ if (isLastAction) {
1657
+ this.canvasDropPreview = null;
1658
+ this.actionDragTargetNodeUuid = null;
1659
+ return;
1660
+ }
1535
1661
  // Not dropping on another node, create a new one on canvas
1536
1662
  // Snap to grid for the final drop position
1537
1663
  const position = this.calculateCanvasDropPosition(mouseX, mouseY, true);
@@ -1542,12 +1668,13 @@ export class Editor extends RapidElement {
1542
1668
  const updatedActions = originalNode.actions.filter((_a, idx) => idx !== actionIndex);
1543
1669
  // if no actions remain, delete the node
1544
1670
  if (updatedActions.length === 0) {
1545
- (_b = getStore()) === null || _b === void 0 ? void 0 : _b.getState().removeNodes([nodeUuid]);
1671
+ // Use deleteNodes to properly clean up Plumber connections before removing
1672
+ this.deleteNodes([nodeUuid]);
1546
1673
  }
1547
1674
  else {
1548
1675
  // update the node
1549
1676
  const updatedNode = { ...originalNode, actions: updatedActions };
1550
- (_c = getStore()) === null || _c === void 0 ? void 0 : _c.getState().updateNode(nodeUuid, updatedNode);
1677
+ (_b = getStore()) === null || _b === void 0 ? void 0 : _b.getState().updateNode(nodeUuid, updatedNode);
1551
1678
  }
1552
1679
  // create a new execute_actions node with the dropped action
1553
1680
  const newNode = {
@@ -1566,16 +1693,14 @@ export class Editor extends RapidElement {
1566
1693
  config: {}
1567
1694
  };
1568
1695
  // add the new node
1569
- (_d = getStore()) === null || _d === void 0 ? void 0 : _d.getState().addNode(newNode, newNodeUI);
1696
+ (_c = getStore()) === null || _c === void 0 ? void 0 : _c.getState().addNode(newNode, newNodeUI);
1570
1697
  // clear the preview
1571
1698
  this.canvasDropPreview = null;
1572
1699
  this.actionDragTargetNodeUuid = null;
1573
- // repaint connections
1574
- if (this.plumber) {
1575
- requestAnimationFrame(() => {
1576
- this.plumber.repaintEverything();
1577
- });
1578
- }
1700
+ // Check for collisions and reflow after adding new node
1701
+ requestAnimationFrame(() => {
1702
+ this.checkCollisionsAndReflow([newNode.uuid]);
1703
+ });
1579
1704
  }
1580
1705
  getLocalizationLanguages() {
1581
1706
  if (!this.definition) {
@@ -1975,7 +2100,7 @@ export class Editor extends RapidElement {
1975
2100
  header="Translations"
1976
2101
  .width=${360}
1977
2102
  .maxHeight=${600}
1978
- .top=${20}
2103
+ .top=${170}
1979
2104
  color="#6b7280"
1980
2105
  .hidden=${this.localizationWindowHidden}
1981
2106
  @temba-dialog-hidden=${this.handleLocalizationWindowClosed}
@@ -2133,6 +2258,7 @@ export class Editor extends RapidElement {
2133
2258
  icon="language"
2134
2259
  label="Translate Flow"
2135
2260
  color="#6b7280"
2261
+ top="180"
2136
2262
  .hidden=${!this.localizationWindowHidden}
2137
2263
  @temba-button-clicked=${this.handleLocalizationTabClick}
2138
2264
  ></temba-floating-tab>
@@ -2172,7 +2298,7 @@ export class Editor extends RapidElement {
2172
2298
  @mousedown=${this.handleMouseDown.bind(this)}
2173
2299
  uuid=${node.uuid}
2174
2300
  data-node-uuid=${node.uuid}
2175
- style="left:${position.left}px; top:${position.top}px"
2301
+ style="left:${position.left}px; top:${position.top}px;transition: all 0.2s ease-in-out;"
2176
2302
  .plumber=${this.plumber}
2177
2303
  .node=${node}
2178
2304
  .ui=${this.definition._ui.nodes[node.uuid]}
@@ -2192,8 +2318,7 @@ export class Editor extends RapidElement {
2192
2318
  ? 'selected'
2193
2319
  : ''}"
2194
2320
  @mousedown=${this.handleMouseDown.bind(this)}
2195
- style="left:${position.left}px; top:${position.top}px; z-index: ${1000 +
2196
- position.top}"
2321
+ style="left:${position.left}px; top:${position.top}px;"
2197
2322
  uuid=${uuid}
2198
2323
  .data=${sticky}
2199
2324
  .dragging=${dragging}