@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.
- package/CHANGELOG.md +17 -0
- package/demo/components/webchat/example.html +1 -1
- 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 +307 -259
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +223 -90
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/display/TembaUser.js +3 -3
- package/out-tsc/src/display/TembaUser.js.map +1 -1
- package/out-tsc/src/events.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +8 -0
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +117 -28
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/utils.js +141 -0
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +122 -170
- 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/store/AppState.js +3 -0
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/src/store/Store.js +5 -5
- package/out-tsc/src/store/Store.js.map +1 -1
- package/out-tsc/src/webchat/WebChat.js +22 -9
- package/out-tsc/src/webchat/WebChat.js.map +1 -1
- package/out-tsc/test/actions/send_broadcast.test.js +9 -4
- package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
- package/out-tsc/test/temba-flow-collision.test.js +673 -0
- package/out-tsc/test/temba-flow-collision.test.js.map +1 -0
- package/out-tsc/test/temba-flow-editor-node.test.js +128 -42
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/contacts/chat-failure.png +0 -0
- package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
- package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
- package/src/display/Chat.ts +303 -129
- package/src/display/TembaUser.ts +3 -2
- package/src/events.ts +11 -8
- package/src/flow/CanvasNode.ts +10 -0
- package/src/flow/Editor.ts +156 -28
- package/src/flow/utils.ts +207 -1
- package/src/interfaces.ts +7 -0
- package/src/live/ContactChat.ts +129 -180
- 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/store/AppState.ts +2 -0
- package/src/store/Store.ts +5 -5
- package/src/webchat/WebChat.ts +24 -10
- package/test/actions/send_broadcast.test.ts +2 -1
- package/test/temba-flow-collision.test.ts +833 -0
- 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
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
-
//
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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
|
-
//
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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.
|
|
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
|
-
//
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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]}
|