@nyaruka/temba-components 0.133.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.
- package/CHANGELOG.md +11 -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 +306 -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 +121 -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 +128 -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
package/src/flow/Editor.ts
CHANGED
|
@@ -24,6 +24,12 @@ import { Dialog } from '../layout/Dialog';
|
|
|
24
24
|
import { Connection } from '@jsplumb/browser-ui';
|
|
25
25
|
import { CanvasMenu, CanvasMenuSelection } from './CanvasMenu';
|
|
26
26
|
import { NodeTypeSelector, NodeTypeSelection } from './NodeTypeSelector';
|
|
27
|
+
import {
|
|
28
|
+
getNodeBounds,
|
|
29
|
+
calculateReflowPositions,
|
|
30
|
+
NodeBounds,
|
|
31
|
+
nodesOverlap
|
|
32
|
+
} from './utils';
|
|
27
33
|
|
|
28
34
|
export function snapToGrid(value: number): number {
|
|
29
35
|
const snapped = Math.round(value / 20) * 20;
|
|
@@ -310,6 +316,7 @@ export class Editor extends RapidElement {
|
|
|
310
316
|
|
|
311
317
|
#canvas > .dragging {
|
|
312
318
|
z-index: 99999 !important;
|
|
319
|
+
transition: none !important;
|
|
313
320
|
}
|
|
314
321
|
|
|
315
322
|
body .jtk-endpoint {
|
|
@@ -1186,6 +1193,76 @@ export class Editor extends RapidElement {
|
|
|
1186
1193
|
</div>`;
|
|
1187
1194
|
}
|
|
1188
1195
|
|
|
1196
|
+
/**
|
|
1197
|
+
* Checks for node collisions and reflows nodes as needed.
|
|
1198
|
+
* Nodes are only moved downward to resolve collisions.
|
|
1199
|
+
*
|
|
1200
|
+
* @param movedNodeUuids - UUIDs of nodes that were just moved/dropped
|
|
1201
|
+
* @param droppedNodeUuid - UUID of the specific node that was dropped (if applicable)
|
|
1202
|
+
* @param dropTargetBounds - Bounds of the node that was dropped onto (if applicable)
|
|
1203
|
+
*/
|
|
1204
|
+
private checkCollisionsAndReflow(
|
|
1205
|
+
movedNodeUuids: string[],
|
|
1206
|
+
droppedNodeUuid: string | null = null,
|
|
1207
|
+
dropTargetBounds: NodeBounds | null = null
|
|
1208
|
+
): void {
|
|
1209
|
+
if (!this.definition) return;
|
|
1210
|
+
|
|
1211
|
+
// Get all node bounds (only for actual nodes, not stickies)
|
|
1212
|
+
const allBounds: NodeBounds[] = [];
|
|
1213
|
+
|
|
1214
|
+
for (const node of this.definition.nodes) {
|
|
1215
|
+
const nodeUI = this.definition._ui?.nodes[node.uuid];
|
|
1216
|
+
if (!nodeUI?.position) continue;
|
|
1217
|
+
|
|
1218
|
+
const bounds = getNodeBounds(node.uuid, nodeUI.position);
|
|
1219
|
+
if (bounds) {
|
|
1220
|
+
allBounds.push(bounds);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Check if we need to determine midpoint priority for a dropped node
|
|
1225
|
+
let targetHasPriority = false;
|
|
1226
|
+
if (droppedNodeUuid && dropTargetBounds) {
|
|
1227
|
+
const droppedBounds = allBounds.find((b) => b.uuid === droppedNodeUuid);
|
|
1228
|
+
if (droppedBounds) {
|
|
1229
|
+
// Check if the bottom of the dropped node is below the midpoint of the target
|
|
1230
|
+
// If bottom is above midpoint, dropped node gets preference (targetHasPriority = false)
|
|
1231
|
+
// If bottom is below midpoint, target gets preference (targetHasPriority = true)
|
|
1232
|
+
const droppedBottom = droppedBounds.bottom;
|
|
1233
|
+
const targetMidpoint =
|
|
1234
|
+
dropTargetBounds.top + dropTargetBounds.height / 2;
|
|
1235
|
+
targetHasPriority = droppedBottom > targetMidpoint;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Calculate reflow positions for each moved node
|
|
1240
|
+
const allReflowPositions: { [uuid: string]: FlowPosition } = {};
|
|
1241
|
+
|
|
1242
|
+
for (const movedUuid of movedNodeUuids) {
|
|
1243
|
+
const movedBounds = allBounds.find((b) => b.uuid === movedUuid);
|
|
1244
|
+
if (!movedBounds) continue;
|
|
1245
|
+
|
|
1246
|
+
// Calculate reflow for this moved node
|
|
1247
|
+
const reflowPositions = calculateReflowPositions(
|
|
1248
|
+
movedUuid,
|
|
1249
|
+
movedBounds,
|
|
1250
|
+
allBounds,
|
|
1251
|
+
droppedNodeUuid === movedUuid ? targetHasPriority : false
|
|
1252
|
+
);
|
|
1253
|
+
|
|
1254
|
+
// Merge into all reflow positions
|
|
1255
|
+
for (const [uuid, position] of reflowPositions.entries()) {
|
|
1256
|
+
allReflowPositions[uuid] = position;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// If there are positions to update, apply them
|
|
1261
|
+
if (Object.keys(allReflowPositions).length > 0) {
|
|
1262
|
+
getStore().getState().updateCanvasPositions(allReflowPositions);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1189
1266
|
private handleMouseMove(event: MouseEvent): void {
|
|
1190
1267
|
// Handle selection box drawing
|
|
1191
1268
|
if (this.canvasMouseDown && !this.isMouseDown) {
|
|
@@ -1333,9 +1410,65 @@ export class Editor extends RapidElement {
|
|
|
1333
1410
|
if (Object.keys(newPositions).length > 0) {
|
|
1334
1411
|
getStore().getState().updateCanvasPositions(newPositions);
|
|
1335
1412
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1413
|
+
// Check for collisions and reflow nodes after updating positions
|
|
1414
|
+
// Filter to only check nodes (not stickies)
|
|
1415
|
+
const nodeUuids = itemsToMove.filter((uuid) =>
|
|
1416
|
+
this.definition.nodes.find((node) => node.uuid === uuid)
|
|
1417
|
+
);
|
|
1418
|
+
|
|
1419
|
+
if (nodeUuids.length > 0) {
|
|
1420
|
+
// Allow DOM to update before checking collisions
|
|
1421
|
+
setTimeout(() => {
|
|
1422
|
+
// If only one node was moved, detect which node it might have been dropped onto
|
|
1423
|
+
let droppedNodeUuid: string | null = null;
|
|
1424
|
+
let dropTargetBounds: NodeBounds | null = null;
|
|
1425
|
+
|
|
1426
|
+
if (nodeUuids.length === 1) {
|
|
1427
|
+
droppedNodeUuid = nodeUuids[0];
|
|
1428
|
+
const droppedNodeUI = this.definition._ui?.nodes[droppedNodeUuid];
|
|
1429
|
+
|
|
1430
|
+
if (droppedNodeUI?.position) {
|
|
1431
|
+
const droppedBounds = getNodeBounds(
|
|
1432
|
+
droppedNodeUuid,
|
|
1433
|
+
droppedNodeUI.position
|
|
1434
|
+
);
|
|
1435
|
+
|
|
1436
|
+
if (droppedBounds) {
|
|
1437
|
+
// Find which node (if any) the dropped node overlaps with
|
|
1438
|
+
for (const node of this.definition.nodes) {
|
|
1439
|
+
if (node.uuid === droppedNodeUuid) continue;
|
|
1440
|
+
|
|
1441
|
+
const nodeUI = this.definition._ui?.nodes[node.uuid];
|
|
1442
|
+
if (!nodeUI?.position) continue;
|
|
1443
|
+
|
|
1444
|
+
const targetBounds = getNodeBounds(
|
|
1445
|
+
node.uuid,
|
|
1446
|
+
nodeUI.position
|
|
1447
|
+
);
|
|
1448
|
+
if (
|
|
1449
|
+
targetBounds &&
|
|
1450
|
+
nodesOverlap(droppedBounds, targetBounds)
|
|
1451
|
+
) {
|
|
1452
|
+
dropTargetBounds = targetBounds;
|
|
1453
|
+
break; // Use the first overlapping node
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
this.checkCollisionsAndReflow(
|
|
1461
|
+
nodeUuids,
|
|
1462
|
+
droppedNodeUuid,
|
|
1463
|
+
dropTargetBounds
|
|
1464
|
+
);
|
|
1465
|
+
}, 0);
|
|
1466
|
+
} else {
|
|
1467
|
+
// No nodes moved, just repaint connections
|
|
1468
|
+
setTimeout(() => {
|
|
1469
|
+
this.plumber.repaintEverything();
|
|
1470
|
+
}, 0);
|
|
1471
|
+
}
|
|
1339
1472
|
}
|
|
1340
1473
|
|
|
1341
1474
|
this.selectedItems.clear();
|
|
@@ -1657,23 +1790,18 @@ export class Editor extends RapidElement {
|
|
|
1657
1790
|
this.isCreatingNewNode = false;
|
|
1658
1791
|
this.pendingNodePosition = null;
|
|
1659
1792
|
|
|
1660
|
-
//
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1793
|
+
// Check for collisions and reflow
|
|
1794
|
+
requestAnimationFrame(() => {
|
|
1795
|
+
this.checkCollisionsAndReflow([updatedNode.uuid]);
|
|
1796
|
+
});
|
|
1666
1797
|
} else {
|
|
1667
1798
|
// Update existing node in the store
|
|
1668
1799
|
getStore()?.getState().updateNode(this.editingNode.uuid, updatedNode);
|
|
1669
1800
|
|
|
1670
|
-
//
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
this.plumber.repaintEverything();
|
|
1675
|
-
});
|
|
1676
|
-
}
|
|
1801
|
+
// Check for collisions and reflow in case node size changed
|
|
1802
|
+
requestAnimationFrame(() => {
|
|
1803
|
+
this.checkCollisionsAndReflow([this.editingNode.uuid]);
|
|
1804
|
+
});
|
|
1677
1805
|
}
|
|
1678
1806
|
}
|
|
1679
1807
|
this.closeNodeEditor();
|
|
@@ -1715,6 +1843,11 @@ export class Editor extends RapidElement {
|
|
|
1715
1843
|
// Reset the creation flags
|
|
1716
1844
|
this.isCreatingNewNode = false;
|
|
1717
1845
|
this.pendingNodePosition = null;
|
|
1846
|
+
|
|
1847
|
+
// Check for collisions and reflow
|
|
1848
|
+
requestAnimationFrame(() => {
|
|
1849
|
+
this.checkCollisionsAndReflow([updatedNode.uuid]);
|
|
1850
|
+
});
|
|
1718
1851
|
} else {
|
|
1719
1852
|
// This is an existing node - update it
|
|
1720
1853
|
// Clean up jsPlumb connections for removed exits before updating the node
|
|
@@ -1743,13 +1876,10 @@ export class Editor extends RapidElement {
|
|
|
1743
1876
|
if (uiConfig) {
|
|
1744
1877
|
getStore()?.getState().updateNodeUIConfig(updatedNode.uuid, uiConfig);
|
|
1745
1878
|
}
|
|
1746
|
-
}
|
|
1747
1879
|
|
|
1748
|
-
|
|
1749
|
-
if (this.plumber) {
|
|
1750
|
-
// Use requestAnimationFrame to ensure DOM has been updated first
|
|
1880
|
+
// Check for collisions and reflow in case node size changed
|
|
1751
1881
|
requestAnimationFrame(() => {
|
|
1752
|
-
this.
|
|
1882
|
+
this.checkCollisionsAndReflow([this.editingNode.uuid]);
|
|
1753
1883
|
});
|
|
1754
1884
|
}
|
|
1755
1885
|
}
|
|
@@ -2077,12 +2207,10 @@ export class Editor extends RapidElement {
|
|
|
2077
2207
|
this.canvasDropPreview = null;
|
|
2078
2208
|
this.actionDragTargetNodeUuid = null;
|
|
2079
2209
|
|
|
2080
|
-
//
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
});
|
|
2085
|
-
}
|
|
2210
|
+
// Check for collisions and reflow after adding new node
|
|
2211
|
+
requestAnimationFrame(() => {
|
|
2212
|
+
this.checkCollisionsAndReflow([newNode.uuid]);
|
|
2213
|
+
});
|
|
2086
2214
|
}
|
|
2087
2215
|
|
|
2088
2216
|
private getLocalizationLanguages(): Array<{ code: string; name: string }> {
|
|
@@ -2841,7 +2969,7 @@ export class Editor extends RapidElement {
|
|
|
2841
2969
|
@mousedown=${this.handleMouseDown.bind(this)}
|
|
2842
2970
|
uuid=${node.uuid}
|
|
2843
2971
|
data-node-uuid=${node.uuid}
|
|
2844
|
-
style="left:${position.left}px; top:${position.top}px"
|
|
2972
|
+
style="left:${position.left}px; top:${position.top}px;transition: all 0.2s ease-in-out;"
|
|
2845
2973
|
.plumber=${this.plumber}
|
|
2846
2974
|
.node=${node}
|
|
2847
2975
|
.ui=${this.definition._ui.nodes[node.uuid]}
|
package/src/flow/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { html } from 'lit-html';
|
|
2
|
-
import { NamedObject } from '../store/flow-definition';
|
|
2
|
+
import { NamedObject, FlowPosition } from '../store/flow-definition';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Renders a single line item with optional icon
|
|
@@ -168,3 +168,209 @@ export const SCHEMES: Scheme[] = [
|
|
|
168
168
|
path: 'External ID'
|
|
169
169
|
}
|
|
170
170
|
];
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Represents the bounding box of a node on the canvas
|
|
174
|
+
*/
|
|
175
|
+
export interface NodeBounds {
|
|
176
|
+
uuid: string;
|
|
177
|
+
left: number;
|
|
178
|
+
top: number;
|
|
179
|
+
right: number;
|
|
180
|
+
bottom: number;
|
|
181
|
+
width: number;
|
|
182
|
+
height: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Minimum vertical spacing between nodes (in pixels)
|
|
187
|
+
*/
|
|
188
|
+
const MIN_NODE_SPACING = 30;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Small buffer to avoid floating point precision issues in overlap detection (in pixels)
|
|
192
|
+
* This prevents false positives when nodes are exactly adjacent (e.g., bottom of one node
|
|
193
|
+
* at exactly the same position as top of another)
|
|
194
|
+
*/
|
|
195
|
+
const OVERLAP_BUFFER = 10;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Gets the bounding box for a node from the DOM
|
|
199
|
+
*
|
|
200
|
+
* @param nodeUuid - The UUID of the node
|
|
201
|
+
* @param position - The current position of the node
|
|
202
|
+
* @param element - Optional pre-fetched DOM element (recommended for performance when checking multiple nodes)
|
|
203
|
+
* @returns NodeBounds object or null if element not found
|
|
204
|
+
*
|
|
205
|
+
* Note: When element is not provided, performs a DOM query which may impact performance
|
|
206
|
+
* during bulk collision detection. Consider fetching elements beforehand when possible.
|
|
207
|
+
*/
|
|
208
|
+
export const getNodeBounds = (
|
|
209
|
+
nodeUuid: string,
|
|
210
|
+
position: FlowPosition,
|
|
211
|
+
element?: HTMLElement
|
|
212
|
+
): NodeBounds | null => {
|
|
213
|
+
// If element is provided, use it; otherwise try to find it in DOM
|
|
214
|
+
const nodeElement =
|
|
215
|
+
element || (document.querySelector(`[id="${nodeUuid}"]`) as HTMLElement);
|
|
216
|
+
|
|
217
|
+
if (!nodeElement) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const rect = nodeElement.getBoundingClientRect();
|
|
222
|
+
const width = rect.width;
|
|
223
|
+
const height = rect.height;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
uuid: nodeUuid,
|
|
227
|
+
left: position.left,
|
|
228
|
+
top: position.top,
|
|
229
|
+
right: position.left + width,
|
|
230
|
+
bottom: position.top + height,
|
|
231
|
+
width,
|
|
232
|
+
height
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Checks if two node bounding boxes overlap
|
|
238
|
+
*/
|
|
239
|
+
export const nodesOverlap = (
|
|
240
|
+
bounds1: NodeBounds,
|
|
241
|
+
bounds2: NodeBounds
|
|
242
|
+
): boolean => {
|
|
243
|
+
// Use a small buffer to avoid floating point precision issues
|
|
244
|
+
const buffer = OVERLAP_BUFFER;
|
|
245
|
+
|
|
246
|
+
return !(
|
|
247
|
+
bounds1.right <= bounds2.left - buffer ||
|
|
248
|
+
bounds1.left >= bounds2.right + buffer ||
|
|
249
|
+
bounds1.bottom <= bounds2.top - buffer ||
|
|
250
|
+
bounds1.top >= bounds2.bottom + buffer
|
|
251
|
+
);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Detects all collisions between a node and other nodes
|
|
256
|
+
*/
|
|
257
|
+
export const detectCollisions = (
|
|
258
|
+
targetBounds: NodeBounds,
|
|
259
|
+
allBounds: NodeBounds[]
|
|
260
|
+
): NodeBounds[] => {
|
|
261
|
+
return allBounds.filter(
|
|
262
|
+
(bounds) =>
|
|
263
|
+
bounds.uuid !== targetBounds.uuid && nodesOverlap(targetBounds, bounds)
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Calculates the new positions needed to resolve all collisions
|
|
269
|
+
* Nodes are only moved downward, never up, left, or right
|
|
270
|
+
* Returns a map of node UUIDs to their new positions
|
|
271
|
+
*/
|
|
272
|
+
export const calculateReflowPositions = (
|
|
273
|
+
movedNodeUuid: string,
|
|
274
|
+
movedNodeBounds: NodeBounds,
|
|
275
|
+
allBounds: NodeBounds[],
|
|
276
|
+
droppedBelowMidpoint: boolean = false
|
|
277
|
+
): Map<string, FlowPosition> => {
|
|
278
|
+
const newPositions = new Map<string, FlowPosition>();
|
|
279
|
+
|
|
280
|
+
// If dropped below midpoint, the moved node should move down instead
|
|
281
|
+
if (droppedBelowMidpoint) {
|
|
282
|
+
// Find all nodes that collide with the moved node
|
|
283
|
+
const collisions = detectCollisions(movedNodeBounds, allBounds);
|
|
284
|
+
|
|
285
|
+
if (collisions.length > 0) {
|
|
286
|
+
// Find the highest bottom position of all colliding nodes
|
|
287
|
+
const maxBottom = Math.max(...collisions.map((b) => b.bottom));
|
|
288
|
+
|
|
289
|
+
// Move the dropped node below all colliding nodes
|
|
290
|
+
const newTop = maxBottom + MIN_NODE_SPACING;
|
|
291
|
+
newPositions.set(movedNodeUuid, {
|
|
292
|
+
left: movedNodeBounds.left,
|
|
293
|
+
top: newTop
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Update the moved node bounds for further collision checks
|
|
297
|
+
movedNodeBounds = {
|
|
298
|
+
...movedNodeBounds,
|
|
299
|
+
top: newTop,
|
|
300
|
+
bottom: newTop + movedNodeBounds.height
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Now check for any remaining collisions and move other nodes down
|
|
306
|
+
const processedNodes = new Set<string>();
|
|
307
|
+
processedNodes.add(movedNodeUuid);
|
|
308
|
+
|
|
309
|
+
// Keep checking for collisions until none remain
|
|
310
|
+
let hasCollisions = true;
|
|
311
|
+
let iterations = 0;
|
|
312
|
+
const maxIterations = 100; // Prevent infinite loops
|
|
313
|
+
|
|
314
|
+
while (hasCollisions && iterations < maxIterations) {
|
|
315
|
+
hasCollisions = false;
|
|
316
|
+
iterations++;
|
|
317
|
+
|
|
318
|
+
// Check all nodes for collisions
|
|
319
|
+
for (const bounds of allBounds) {
|
|
320
|
+
if (processedNodes.has(bounds.uuid)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Use original bounds since we skip already processed nodes
|
|
325
|
+
const currentBounds = bounds;
|
|
326
|
+
|
|
327
|
+
// Check if this node collides with the moved node or any already repositioned nodes
|
|
328
|
+
let collisionFound = false;
|
|
329
|
+
let maxCollisionBottom = 0;
|
|
330
|
+
|
|
331
|
+
// Check against moved node
|
|
332
|
+
if (nodesOverlap(currentBounds, movedNodeBounds)) {
|
|
333
|
+
collisionFound = true;
|
|
334
|
+
maxCollisionBottom = Math.max(
|
|
335
|
+
maxCollisionBottom,
|
|
336
|
+
movedNodeBounds.bottom
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Check against other repositioned nodes
|
|
341
|
+
for (const [otherUuid, otherPosition] of newPositions.entries()) {
|
|
342
|
+
if (otherUuid === bounds.uuid) continue;
|
|
343
|
+
|
|
344
|
+
const otherBounds = allBounds.find((b) => b.uuid === otherUuid);
|
|
345
|
+
if (!otherBounds) continue;
|
|
346
|
+
|
|
347
|
+
const otherUpdatedBounds = {
|
|
348
|
+
...otherBounds,
|
|
349
|
+
top: otherPosition.top,
|
|
350
|
+
bottom: otherPosition.top + otherBounds.height
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
if (nodesOverlap(currentBounds, otherUpdatedBounds)) {
|
|
354
|
+
collisionFound = true;
|
|
355
|
+
maxCollisionBottom = Math.max(
|
|
356
|
+
maxCollisionBottom,
|
|
357
|
+
otherUpdatedBounds.bottom
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (collisionFound) {
|
|
363
|
+
// Move this node down below the collision
|
|
364
|
+
const newTop = maxCollisionBottom + MIN_NODE_SPACING;
|
|
365
|
+
newPositions.set(bounds.uuid, {
|
|
366
|
+
left: bounds.left,
|
|
367
|
+
top: newTop
|
|
368
|
+
});
|
|
369
|
+
hasCollisions = true;
|
|
370
|
+
processedNodes.add(bounds.uuid);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return newPositions;
|
|
376
|
+
};
|
package/src/interfaces.ts
CHANGED
|
@@ -103,6 +103,13 @@ export interface Msg {
|
|
|
103
103
|
direction: string;
|
|
104
104
|
type: string;
|
|
105
105
|
attachments: string[];
|
|
106
|
+
unsendable_reason?:
|
|
107
|
+
| 'no_route'
|
|
108
|
+
| 'contact_blocked'
|
|
109
|
+
| 'contact_stopped'
|
|
110
|
+
| 'contact_archived'
|
|
111
|
+
| 'org_suspended'
|
|
112
|
+
| 'looping';
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
export interface ObjectReference {
|