@nyaruka/temba-components 0.133.0 → 0.134.1

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 (72) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/demo/components/webchat/example.html +1 -1
  3. package/dist/locales/es.js +5 -5
  4. package/dist/locales/es.js.map +1 -1
  5. package/dist/locales/fr.js +5 -5
  6. package/dist/locales/fr.js.map +1 -1
  7. package/dist/locales/locale-codes.js +2 -11
  8. package/dist/locales/locale-codes.js.map +1 -1
  9. package/dist/locales/pt.js +5 -5
  10. package/dist/locales/pt.js.map +1 -1
  11. package/dist/temba-components.js +307 -259
  12. package/dist/temba-components.js.map +1 -1
  13. package/out-tsc/src/display/Chat.js +223 -90
  14. package/out-tsc/src/display/Chat.js.map +1 -1
  15. package/out-tsc/src/display/TembaUser.js +3 -3
  16. package/out-tsc/src/display/TembaUser.js.map +1 -1
  17. package/out-tsc/src/events.js.map +1 -1
  18. package/out-tsc/src/flow/CanvasNode.js +8 -0
  19. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  20. package/out-tsc/src/flow/Editor.js +117 -28
  21. package/out-tsc/src/flow/Editor.js.map +1 -1
  22. package/out-tsc/src/flow/utils.js +141 -0
  23. package/out-tsc/src/flow/utils.js.map +1 -1
  24. package/out-tsc/src/interfaces.js.map +1 -1
  25. package/out-tsc/src/live/ContactChat.js +122 -170
  26. package/out-tsc/src/live/ContactChat.js.map +1 -1
  27. package/out-tsc/src/locales/es.js +5 -5
  28. package/out-tsc/src/locales/es.js.map +1 -1
  29. package/out-tsc/src/locales/fr.js +5 -5
  30. package/out-tsc/src/locales/fr.js.map +1 -1
  31. package/out-tsc/src/locales/locale-codes.js +2 -11
  32. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  33. package/out-tsc/src/locales/pt.js +5 -5
  34. package/out-tsc/src/locales/pt.js.map +1 -1
  35. package/out-tsc/src/store/AppState.js +3 -0
  36. package/out-tsc/src/store/AppState.js.map +1 -1
  37. package/out-tsc/src/store/Store.js +5 -5
  38. package/out-tsc/src/store/Store.js.map +1 -1
  39. package/out-tsc/src/webchat/WebChat.js +22 -9
  40. package/out-tsc/src/webchat/WebChat.js.map +1 -1
  41. package/out-tsc/test/actions/send_broadcast.test.js +9 -4
  42. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
  43. package/out-tsc/test/temba-flow-collision.test.js +673 -0
  44. package/out-tsc/test/temba-flow-collision.test.js.map +1 -0
  45. package/out-tsc/test/temba-flow-editor-node.test.js +128 -42
  46. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  47. package/package.json +1 -1
  48. package/screenshots/truth/contacts/chat-failure.png +0 -0
  49. package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
  50. package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
  51. package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
  52. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  53. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  54. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  55. package/src/display/Chat.ts +303 -129
  56. package/src/display/TembaUser.ts +3 -2
  57. package/src/events.ts +11 -8
  58. package/src/flow/CanvasNode.ts +10 -0
  59. package/src/flow/Editor.ts +156 -28
  60. package/src/flow/utils.ts +207 -1
  61. package/src/interfaces.ts +7 -0
  62. package/src/live/ContactChat.ts +129 -180
  63. package/src/locales/es.ts +13 -18
  64. package/src/locales/fr.ts +13 -18
  65. package/src/locales/locale-codes.ts +2 -11
  66. package/src/locales/pt.ts +13 -18
  67. package/src/store/AppState.ts +2 -0
  68. package/src/store/Store.ts +5 -5
  69. package/src/webchat/WebChat.ts +24 -10
  70. package/test/actions/send_broadcast.test.ts +2 -1
  71. package/test/temba-flow-collision.test.ts +833 -0
  72. package/test/temba-flow-editor-node.test.ts +142 -47
@@ -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);
@@ -105,6 +106,7 @@ export class Editor extends RapidElement {
105
106
 
106
107
  #canvas > .dragging {
107
108
  z-index: 99999 !important;
109
+ transition: none !important;
108
110
  }
109
111
 
110
112
  body .jtk-endpoint {
@@ -912,6 +914,60 @@ export class Editor extends RapidElement {
912
914
  </div>
913
915
  </div>`;
914
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
+ }
915
971
  handleMouseMove(event) {
916
972
  // Handle selection box drawing
917
973
  if (this.canvasMouseDown && !this.isMouseDown) {
@@ -1031,9 +1087,48 @@ export class Editor extends RapidElement {
1031
1087
  });
1032
1088
  if (Object.keys(newPositions).length > 0) {
1033
1089
  getStore().getState().updateCanvasPositions(newPositions);
1034
- setTimeout(() => {
1035
- this.plumber.repaintEverything();
1036
- }, 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
+ }
1037
1132
  }
1038
1133
  this.selectedItems.clear();
1039
1134
  }
@@ -1297,23 +1392,18 @@ export class Editor extends RapidElement {
1297
1392
  // Reset the creation flags
1298
1393
  this.isCreatingNewNode = false;
1299
1394
  this.pendingNodePosition = null;
1300
- // Repaint jsplumb connections
1301
- if (this.plumber) {
1302
- requestAnimationFrame(() => {
1303
- this.plumber.repaintEverything();
1304
- });
1305
- }
1395
+ // Check for collisions and reflow
1396
+ requestAnimationFrame(() => {
1397
+ this.checkCollisionsAndReflow([updatedNode.uuid]);
1398
+ });
1306
1399
  }
1307
1400
  else {
1308
1401
  // Update existing node in the store
1309
1402
  (_c = getStore()) === null || _c === void 0 ? void 0 : _c.getState().updateNode(this.editingNode.uuid, updatedNode);
1310
- // Repaint jsplumb connections in case node size changed
1311
- if (this.plumber) {
1312
- // Use requestAnimationFrame to ensure DOM has been updated first
1313
- requestAnimationFrame(() => {
1314
- this.plumber.repaintEverything();
1315
- });
1316
- }
1403
+ // Check for collisions and reflow in case node size changed
1404
+ requestAnimationFrame(() => {
1405
+ this.checkCollisionsAndReflow([this.editingNode.uuid]);
1406
+ });
1317
1407
  }
1318
1408
  }
1319
1409
  this.closeNodeEditor();
@@ -1347,6 +1437,10 @@ export class Editor extends RapidElement {
1347
1437
  // Reset the creation flags
1348
1438
  this.isCreatingNewNode = false;
1349
1439
  this.pendingNodePosition = null;
1440
+ // Check for collisions and reflow
1441
+ requestAnimationFrame(() => {
1442
+ this.checkCollisionsAndReflow([updatedNode.uuid]);
1443
+ });
1350
1444
  }
1351
1445
  else {
1352
1446
  // This is an existing node - update it
@@ -1368,12 +1462,9 @@ export class Editor extends RapidElement {
1368
1462
  if (uiConfig) {
1369
1463
  (_d = getStore()) === null || _d === void 0 ? void 0 : _d.getState().updateNodeUIConfig(updatedNode.uuid, uiConfig);
1370
1464
  }
1371
- }
1372
- // Repaint jsplumb connections in case node size changed
1373
- if (this.plumber) {
1374
- // Use requestAnimationFrame to ensure DOM has been updated first
1465
+ // Check for collisions and reflow in case node size changed
1375
1466
  requestAnimationFrame(() => {
1376
- this.plumber.repaintEverything();
1467
+ this.checkCollisionsAndReflow([this.editingNode.uuid]);
1377
1468
  });
1378
1469
  }
1379
1470
  }
@@ -1606,12 +1697,10 @@ export class Editor extends RapidElement {
1606
1697
  // clear the preview
1607
1698
  this.canvasDropPreview = null;
1608
1699
  this.actionDragTargetNodeUuid = null;
1609
- // repaint connections
1610
- if (this.plumber) {
1611
- requestAnimationFrame(() => {
1612
- this.plumber.repaintEverything();
1613
- });
1614
- }
1700
+ // Check for collisions and reflow after adding new node
1701
+ requestAnimationFrame(() => {
1702
+ this.checkCollisionsAndReflow([newNode.uuid]);
1703
+ });
1615
1704
  }
1616
1705
  getLocalizationLanguages() {
1617
1706
  if (!this.definition) {
@@ -2209,7 +2298,7 @@ export class Editor extends RapidElement {
2209
2298
  @mousedown=${this.handleMouseDown.bind(this)}
2210
2299
  uuid=${node.uuid}
2211
2300
  data-node-uuid=${node.uuid}
2212
- 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;"
2213
2302
  .plumber=${this.plumber}
2214
2303
  .node=${node}
2215
2304
  .ui=${this.definition._ui.nodes[node.uuid]}