@nyaruka/temba-components 0.138.6 → 0.140.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/.github/workflows/cla.yml +1 -1
- package/.github/workflows/copilot-setup-steps.yml +6 -1
- package/CHANGELOG.md +26 -0
- package/demo/data/flows/sample-flow.json +24 -0
- 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 +1112 -882
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +10 -7
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/display/Dropdown.js +3 -1
- package/out-tsc/src/display/Dropdown.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +25 -32
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/display/Thumbnail.js +163 -5
- package/out-tsc/src/display/Thumbnail.js.map +1 -1
- package/out-tsc/src/flow/CanvasMenu.js +5 -3
- package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +70 -29
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +290 -239
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +118 -10
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +757 -403
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/flow/StickyNote.js +13 -4
- package/out-tsc/src/flow/StickyNote.js.map +1 -1
- package/out-tsc/src/flow/actions/audio-player.js +112 -0
- package/out-tsc/src/flow/actions/audio-player.js.map +1 -0
- package/out-tsc/src/flow/actions/enter_flow.js +43 -0
- package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
- package/out-tsc/src/flow/actions/play_audio.js +57 -4
- package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
- package/out-tsc/src/flow/actions/say_msg.js +86 -3
- package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
- package/out-tsc/src/flow/config.js +11 -3
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/flow/nodes/shared-rules.js +1 -1
- package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -1
- package/out-tsc/src/flow/nodes/terminal.js +7 -0
- package/out-tsc/src/flow/nodes/terminal.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_audio.js +77 -0
- package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_dial.js +151 -0
- package/out-tsc/src/flow/nodes/wait_for_dial.js.map +1 -0
- package/out-tsc/src/flow/nodes/wait_for_digits.js +61 -1
- package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_menu.js +173 -2
- package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
- package/out-tsc/src/flow/operators.js +21 -5
- package/out-tsc/src/flow/operators.js.map +1 -1
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/flow/utils.js +213 -65
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +4 -2
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/FieldRenderer.js +49 -0
- package/out-tsc/src/form/FieldRenderer.js.map +1 -1
- package/out-tsc/src/interfaces.js +2 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/layout/Dialog.js +52 -7
- package/out-tsc/src/layout/Dialog.js.map +1 -1
- package/out-tsc/src/list/TicketList.js +4 -1
- package/out-tsc/src/list/TicketList.js.map +1 -1
- package/out-tsc/src/live/TembaChart.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/simulator/Simulator.js +10 -3
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/src/store/AppState.js +89 -3
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/test/actions/play_audio.test.js +118 -0
- package/out-tsc/test/actions/play_audio.test.js.map +1 -0
- package/out-tsc/test/actions/say_msg.test.js +158 -0
- package/out-tsc/test/actions/say_msg.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_audio.test.js +156 -0
- package/out-tsc/test/nodes/wait_for_audio.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_dial.test.js +336 -0
- package/out-tsc/test/nodes/wait_for_dial.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_digits.test.js +198 -84
- package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
- package/out-tsc/test/nodes/wait_for_menu.test.js +340 -0
- package/out-tsc/test/nodes/wait_for_menu.test.js.map +1 -0
- package/out-tsc/test/temba-floating-tab.test.js +4 -6
- package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
- package/out-tsc/test/temba-flow-collision.test.js +473 -220
- package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor.test.js +0 -2
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js +83 -84
- package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +102 -93
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
- package/out-tsc/test/temba-node-type-selector.test.js +6 -6
- package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/play_audio/editor/expression-url.png +0 -0
- package/screenshots/truth/actions/play_audio/editor/static-url.png +0 -0
- package/screenshots/truth/actions/play_audio/render/expression-url.png +0 -0
- package/screenshots/truth/actions/play_audio/render/static-url.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/multiline-text.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/say_msg/editor/text-with-audio-url.png +0 -0
- package/screenshots/truth/actions/say_msg/render/multiline-text.png +0 -0
- package/screenshots/truth/actions/say_msg/render/simple-text.png +0 -0
- package/screenshots/truth/actions/say_msg/render/text-with-audio-url.png +0 -0
- package/screenshots/truth/editor/router.png +0 -0
- package/screenshots/truth/editor/wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_audio/editor/basic-audio-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_audio/render/basic-audio-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/editor/basic-dial.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/editor/dial-with-limits.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/render/basic-dial.png +0 -0
- package/screenshots/truth/nodes/wait_for_dial/render/dial-with-limits.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/digits-with-rules.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/digits-with-rules.png +0 -0
- package/screenshots/truth/nodes/wait_for_menu/editor/menu-with-digits.png +0 -0
- package/screenshots/truth/nodes/wait_for_menu/render/menu-with-digits.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
- package/src/display/Chat.ts +13 -7
- package/src/display/Dropdown.ts +3 -1
- package/src/display/FloatingTab.ts +24 -33
- package/src/display/Thumbnail.ts +162 -2
- package/src/flow/CanvasMenu.ts +8 -3
- package/src/flow/CanvasNode.ts +75 -30
- package/src/flow/Editor.ts +336 -288
- package/src/flow/NodeEditor.ts +137 -9
- package/src/flow/Plumber.ts +1011 -457
- package/src/flow/StickyNote.ts +14 -4
- package/src/flow/actions/audio-player.ts +127 -0
- package/src/flow/actions/enter_flow.ts +44 -0
- package/src/flow/actions/play_audio.ts +64 -5
- package/src/flow/actions/say_msg.ts +94 -4
- package/src/flow/config.ts +11 -3
- package/src/flow/nodes/shared-rules.ts +1 -1
- package/src/flow/nodes/terminal.ts +9 -0
- package/src/flow/nodes/wait_for_audio.ts +88 -0
- package/src/flow/nodes/wait_for_dial.ts +176 -0
- package/src/flow/nodes/wait_for_digits.ts +86 -2
- package/src/flow/nodes/wait_for_menu.ts +209 -3
- package/src/flow/operators.ts +23 -5
- package/src/flow/types.ts +23 -1
- package/src/flow/utils.ts +238 -81
- package/src/form/ArrayEditor.ts +4 -2
- package/src/form/FieldRenderer.ts +64 -1
- package/src/interfaces.ts +3 -1
- package/src/layout/Dialog.ts +53 -7
- package/src/list/TicketList.ts +4 -1
- package/src/live/TembaChart.ts +1 -1
- 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/simulator/Simulator.ts +13 -3
- package/src/store/AppState.ts +105 -1
- package/src/store/flow-definition.d.ts +2 -0
- package/test/actions/play_audio.test.ts +155 -0
- package/test/actions/say_msg.test.ts +196 -0
- package/test/nodes/wait_for_audio.test.ts +182 -0
- package/test/nodes/wait_for_dial.test.ts +382 -0
- package/test/nodes/wait_for_digits.test.ts +233 -109
- package/test/nodes/wait_for_menu.test.ts +383 -0
- package/test/temba-floating-tab.test.ts +4 -6
- package/test/temba-flow-collision.test.ts +495 -293
- package/test/temba-flow-editor.test.ts +0 -2
- package/test/temba-flow-plumber-connections.test.ts +97 -97
- package/test/temba-flow-plumber.test.ts +116 -103
- package/test/temba-node-type-selector.test.ts +6 -6
- package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
package/src/flow/utils.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { html } from 'lit-html';
|
|
2
2
|
import { NamedObject, FlowPosition } from '../store/flow-definition';
|
|
3
|
+
import { FlowIssue } from '../store/AppState';
|
|
4
|
+
|
|
5
|
+
export function formatIssueMessage(issue: FlowIssue): string {
|
|
6
|
+
if (issue.dependency) {
|
|
7
|
+
const name = issue.dependency.name || issue.dependency.key;
|
|
8
|
+
return `Cannot find a ${issue.dependency.type} for ${name}`;
|
|
9
|
+
}
|
|
10
|
+
return issue.description;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const GRID_SIZE = 20;
|
|
14
|
+
|
|
15
|
+
export function snapToGrid(value: number): number {
|
|
16
|
+
const snapped = Math.round(value / GRID_SIZE) * GRID_SIZE;
|
|
17
|
+
return Math.max(snapped, 0);
|
|
18
|
+
}
|
|
3
19
|
|
|
4
20
|
/**
|
|
5
21
|
* Renders a single line item with optional icon
|
|
@@ -264,110 +280,251 @@ export const detectCollisions = (
|
|
|
264
280
|
);
|
|
265
281
|
};
|
|
266
282
|
|
|
283
|
+
type Direction = 'down' | 'up' | 'right' | 'left';
|
|
284
|
+
|
|
285
|
+
const DIRECTIONS: Direction[] = ['down', 'up', 'right', 'left'];
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Creates a new NodeBounds at a different position
|
|
289
|
+
*/
|
|
290
|
+
const makeBoundsAt = (
|
|
291
|
+
original: NodeBounds,
|
|
292
|
+
left: number,
|
|
293
|
+
top: number
|
|
294
|
+
): NodeBounds => ({
|
|
295
|
+
...original,
|
|
296
|
+
left,
|
|
297
|
+
top,
|
|
298
|
+
right: left + original.width,
|
|
299
|
+
bottom: top + original.height
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Computes the minimum position needed to clear all fixed nodes in a given direction.
|
|
304
|
+
* Returns null if the direction is not viable (e.g., would require negative coordinates
|
|
305
|
+
* and still overlap).
|
|
306
|
+
*/
|
|
307
|
+
const computeDirectionalClearance = (
|
|
308
|
+
collider: NodeBounds,
|
|
309
|
+
fixedNodes: NodeBounds[],
|
|
310
|
+
direction: Direction
|
|
311
|
+
): { left: number; top: number } | null => {
|
|
312
|
+
switch (direction) {
|
|
313
|
+
case 'down': {
|
|
314
|
+
const maxBottom = Math.max(...fixedNodes.map((f) => f.bottom));
|
|
315
|
+
const newTop = snapToGrid(maxBottom + MIN_NODE_SPACING);
|
|
316
|
+
return { left: collider.left, top: newTop };
|
|
317
|
+
}
|
|
318
|
+
case 'up': {
|
|
319
|
+
const minTop = Math.min(...fixedNodes.map((f) => f.top));
|
|
320
|
+
const newTop = snapToGrid(minTop - collider.height - MIN_NODE_SPACING);
|
|
321
|
+
if (newTop < 0) return { left: collider.left, top: 0 };
|
|
322
|
+
return { left: collider.left, top: newTop };
|
|
323
|
+
}
|
|
324
|
+
case 'right': {
|
|
325
|
+
const maxRight = Math.max(...fixedNodes.map((f) => f.right));
|
|
326
|
+
const newLeft = snapToGrid(maxRight + MIN_NODE_SPACING);
|
|
327
|
+
return { left: newLeft, top: collider.top };
|
|
328
|
+
}
|
|
329
|
+
case 'left': {
|
|
330
|
+
const minLeft = Math.min(...fixedNodes.map((f) => f.left));
|
|
331
|
+
const newLeft = snapToGrid(minLeft - collider.width - MIN_NODE_SPACING);
|
|
332
|
+
if (newLeft < 0) return { left: 0, top: collider.top };
|
|
333
|
+
return { left: newLeft, top: collider.top };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
267
338
|
/**
|
|
268
|
-
* Calculates
|
|
269
|
-
*
|
|
270
|
-
*
|
|
339
|
+
* Calculates new positions to resolve all collisions using multi-directional reflow.
|
|
340
|
+
*
|
|
341
|
+
* Sacred nodes (the ones just dropped/created) keep their positions. All other
|
|
342
|
+
* colliding nodes are moved in whichever direction requires the least displacement
|
|
343
|
+
* and causes the fewest cascading collisions.
|
|
271
344
|
*/
|
|
272
345
|
export const calculateReflowPositions = (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
allBounds: NodeBounds[],
|
|
276
|
-
droppedBelowMidpoint: boolean = false
|
|
346
|
+
sacredNodeUuids: string[],
|
|
347
|
+
allBounds: NodeBounds[]
|
|
277
348
|
): Map<string, FlowPosition> => {
|
|
278
349
|
const newPositions = new Map<string, FlowPosition>();
|
|
350
|
+
const sacredSet = new Set(sacredNodeUuids);
|
|
279
351
|
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
352
|
+
// Mutable map of current bounds, updated as collisions are resolved
|
|
353
|
+
const currentBounds = new Map<string, NodeBounds>();
|
|
354
|
+
for (const b of allBounds) {
|
|
355
|
+
currentBounds.set(b.uuid, { ...b });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// A sacred node yields to an existing node at the top of the canvas when
|
|
359
|
+
// the sacred wasn't dropped above it. The existing node keeps its top
|
|
360
|
+
// position and the sacred node moves below instead.
|
|
361
|
+
for (const sacredUuid of [...sacredSet]) {
|
|
362
|
+
const sacred = currentBounds.get(sacredUuid);
|
|
363
|
+
if (!sacred) continue;
|
|
364
|
+
|
|
365
|
+
for (const [uuid, bounds] of currentBounds) {
|
|
366
|
+
if (uuid === sacredUuid || sacredSet.has(uuid)) continue;
|
|
367
|
+
if (!nodesOverlap(sacred, bounds)) continue;
|
|
368
|
+
|
|
369
|
+
if (sacred.top > bounds.top && bounds.top < MIN_NODE_SPACING) {
|
|
370
|
+
sacredSet.delete(sacredUuid);
|
|
371
|
+
sacredSet.add(uuid);
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
302
374
|
}
|
|
303
375
|
}
|
|
304
376
|
|
|
305
|
-
//
|
|
306
|
-
const
|
|
307
|
-
|
|
377
|
+
// Seed the queue with non-sacred nodes that overlap any sacred node
|
|
378
|
+
const queue: string[] = [];
|
|
379
|
+
const inQueue = new Set<string>();
|
|
380
|
+
|
|
381
|
+
for (const sacredUuid of sacredSet) {
|
|
382
|
+
const sacred = currentBounds.get(sacredUuid);
|
|
383
|
+
if (!sacred) continue;
|
|
384
|
+
for (const [uuid, bounds] of currentBounds) {
|
|
385
|
+
if (sacredSet.has(uuid) || inQueue.has(uuid)) continue;
|
|
386
|
+
if (nodesOverlap(sacred, bounds)) {
|
|
387
|
+
queue.push(uuid);
|
|
388
|
+
inQueue.add(uuid);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
308
392
|
|
|
309
|
-
|
|
310
|
-
let hasCollisions = true;
|
|
393
|
+
const resolved = new Set<string>();
|
|
311
394
|
let iterations = 0;
|
|
312
|
-
const maxIterations =
|
|
395
|
+
const maxIterations = 200;
|
|
313
396
|
|
|
314
|
-
while (
|
|
315
|
-
hasCollisions = false;
|
|
397
|
+
while (queue.length > 0 && iterations < maxIterations) {
|
|
316
398
|
iterations++;
|
|
399
|
+
const uuid = queue.shift()!;
|
|
317
400
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
401
|
+
if (resolved.has(uuid)) continue;
|
|
402
|
+
|
|
403
|
+
const collider = currentBounds.get(uuid)!;
|
|
404
|
+
|
|
405
|
+
// Find all fixed nodes (sacred + already-resolved) that overlap this node
|
|
406
|
+
const fixedOverlaps: NodeBounds[] = [];
|
|
407
|
+
for (const [otherUuid, otherBounds] of currentBounds) {
|
|
408
|
+
if (otherUuid === uuid) continue;
|
|
409
|
+
if (sacredSet.has(otherUuid) || resolved.has(otherUuid)) {
|
|
410
|
+
if (nodesOverlap(collider, otherBounds)) {
|
|
411
|
+
fixedOverlaps.push(otherBounds);
|
|
412
|
+
}
|
|
322
413
|
}
|
|
414
|
+
}
|
|
323
415
|
|
|
324
|
-
|
|
325
|
-
const currentBounds = bounds;
|
|
416
|
+
if (fixedOverlaps.length === 0) continue;
|
|
326
417
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
418
|
+
// Determine direction constraints and axis bias from sacred node overlaps
|
|
419
|
+
const sacredOverlaps = fixedOverlaps.filter((f) => sacredSet.has(f.uuid));
|
|
420
|
+
const allowedDirections: Direction[] = [...DIRECTIONS];
|
|
421
|
+
let axisBias: 'vertical' | 'horizontal' | null = null;
|
|
330
422
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
423
|
+
if (sacredOverlaps.length > 0) {
|
|
424
|
+
// Rule 1: don't move a lower node above the sacred node
|
|
425
|
+
// Rule 2: don't move a right-of node to the left of the sacred node
|
|
426
|
+
for (const sacred of sacredOverlaps) {
|
|
427
|
+
if (collider.top > sacred.top) {
|
|
428
|
+
const idx = allowedDirections.indexOf('up');
|
|
429
|
+
if (idx !== -1) allowedDirections.splice(idx, 1);
|
|
430
|
+
}
|
|
431
|
+
if (collider.left > sacred.left) {
|
|
432
|
+
const idx = allowedDirections.indexOf('left');
|
|
433
|
+
if (idx !== -1) allowedDirections.splice(idx, 1);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Rule 3: bias direction based on overlap shape
|
|
438
|
+
let totalOverlapWidth = 0;
|
|
439
|
+
let totalOverlapHeight = 0;
|
|
440
|
+
for (const sacred of sacredOverlaps) {
|
|
441
|
+
totalOverlapWidth +=
|
|
442
|
+
Math.min(collider.right, sacred.right) -
|
|
443
|
+
Math.max(collider.left, sacred.left);
|
|
444
|
+
totalOverlapHeight +=
|
|
445
|
+
Math.min(collider.bottom, sacred.bottom) -
|
|
446
|
+
Math.max(collider.top, sacred.top);
|
|
338
447
|
}
|
|
448
|
+
if (totalOverlapWidth > totalOverlapHeight) {
|
|
449
|
+
axisBias = 'vertical'; // wide overlap = nodes stacked = prefer up/down
|
|
450
|
+
} else if (totalOverlapHeight > totalOverlapWidth) {
|
|
451
|
+
axisBias = 'horizontal'; // tall overlap = nodes side-by-side = prefer left/right
|
|
452
|
+
}
|
|
453
|
+
}
|
|
339
454
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
455
|
+
// Try each allowed direction, pick the one with least disruption
|
|
456
|
+
let bestPos: { left: number; top: number } | null = null;
|
|
457
|
+
let bestScore = Infinity;
|
|
458
|
+
|
|
459
|
+
for (const dir of allowedDirections) {
|
|
460
|
+
const candidate = computeDirectionalClearance(
|
|
461
|
+
collider,
|
|
462
|
+
fixedOverlaps,
|
|
463
|
+
dir
|
|
464
|
+
);
|
|
465
|
+
if (!candidate) continue;
|
|
466
|
+
|
|
467
|
+
const candidateBounds = makeBoundsAt(
|
|
468
|
+
collider,
|
|
469
|
+
candidate.left,
|
|
470
|
+
candidate.top
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
// Verify no overlap with any sacred or resolved node
|
|
474
|
+
let stillOverlaps = false;
|
|
475
|
+
let cascadeCount = 0;
|
|
476
|
+
for (const [otherUuid, otherBounds] of currentBounds) {
|
|
477
|
+
if (otherUuid === uuid) continue;
|
|
478
|
+
if (!nodesOverlap(candidateBounds, otherBounds)) continue;
|
|
479
|
+
|
|
480
|
+
if (sacredSet.has(otherUuid) || resolved.has(otherUuid)) {
|
|
481
|
+
stillOverlaps = true;
|
|
482
|
+
break;
|
|
359
483
|
}
|
|
484
|
+
cascadeCount++;
|
|
485
|
+
}
|
|
486
|
+
if (stillOverlaps) continue;
|
|
487
|
+
|
|
488
|
+
const distance =
|
|
489
|
+
Math.abs(candidate.left - collider.left) +
|
|
490
|
+
Math.abs(candidate.top - collider.top);
|
|
491
|
+
|
|
492
|
+
// When colliding with sacred nodes, use axis bias scoring;
|
|
493
|
+
// for cascading collisions (no sacred overlap), use original scoring
|
|
494
|
+
let score: number;
|
|
495
|
+
if (sacredOverlaps.length > 0) {
|
|
496
|
+
const isVerticalDir = dir === 'up' || dir === 'down';
|
|
497
|
+
const axisMatch =
|
|
498
|
+
axisBias === null ||
|
|
499
|
+
(axisBias === 'vertical' && isVerticalDir) ||
|
|
500
|
+
(axisBias === 'horizontal' && !isVerticalDir);
|
|
501
|
+
const axisPenalty = axisMatch ? 0 : 5000;
|
|
502
|
+
score = cascadeCount * 2000 + axisPenalty + distance;
|
|
503
|
+
} else {
|
|
504
|
+
score = cascadeCount * 10000 + distance;
|
|
360
505
|
}
|
|
361
506
|
|
|
362
|
-
if (
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
507
|
+
if (score < bestScore) {
|
|
508
|
+
bestScore = score;
|
|
509
|
+
bestPos = candidate;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (bestPos) {
|
|
514
|
+
newPositions.set(uuid, { left: bestPos.left, top: bestPos.top });
|
|
515
|
+
const newBounds = makeBoundsAt(collider, bestPos.left, bestPos.top);
|
|
516
|
+
currentBounds.set(uuid, newBounds);
|
|
517
|
+
resolved.add(uuid);
|
|
518
|
+
|
|
519
|
+
// Enqueue any new cascading collisions
|
|
520
|
+
for (const [otherUuid, otherBounds] of currentBounds) {
|
|
521
|
+
if (otherUuid === uuid) continue;
|
|
522
|
+
if (sacredSet.has(otherUuid) || resolved.has(otherUuid)) continue;
|
|
523
|
+
if (inQueue.has(otherUuid)) continue;
|
|
524
|
+
if (nodesOverlap(newBounds, otherBounds)) {
|
|
525
|
+
queue.push(otherUuid);
|
|
526
|
+
inQueue.add(otherUuid);
|
|
527
|
+
}
|
|
371
528
|
}
|
|
372
529
|
}
|
|
373
530
|
}
|
package/src/form/ArrayEditor.ts
CHANGED
|
@@ -648,13 +648,15 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
|
|
|
648
648
|
}
|
|
649
649
|
|
|
650
650
|
.removable .remove-btn {
|
|
651
|
-
|
|
651
|
+
opacity: 0.3;
|
|
652
652
|
cursor: default;
|
|
653
|
+
pointer-events: none;
|
|
653
654
|
}
|
|
654
655
|
|
|
655
656
|
.removable .drag-handle {
|
|
656
|
-
|
|
657
|
+
opacity: 0.3;
|
|
657
658
|
cursor: default;
|
|
659
|
+
pointer-events: none;
|
|
658
660
|
}
|
|
659
661
|
|
|
660
662
|
.drag-handle {
|
|
@@ -8,8 +8,11 @@ import {
|
|
|
8
8
|
CheckboxFieldConfig,
|
|
9
9
|
MessageEditorFieldConfig,
|
|
10
10
|
KeyValueFieldConfig,
|
|
11
|
-
ArrayFieldConfig
|
|
11
|
+
ArrayFieldConfig,
|
|
12
|
+
MediaFieldConfig
|
|
12
13
|
} from '../flow/types';
|
|
14
|
+
import { Attachment } from '../interfaces';
|
|
15
|
+
import { DEFAULT_MEDIA_ENDPOINT } from '../utils';
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* FieldRenderer provides a consistent way to render field configurations
|
|
@@ -90,6 +93,14 @@ export class FieldRenderer {
|
|
|
90
93
|
context
|
|
91
94
|
);
|
|
92
95
|
|
|
96
|
+
case 'media':
|
|
97
|
+
return FieldRenderer.renderMedia(
|
|
98
|
+
fieldName,
|
|
99
|
+
config as MediaFieldConfig,
|
|
100
|
+
value,
|
|
101
|
+
context
|
|
102
|
+
);
|
|
103
|
+
|
|
93
104
|
default:
|
|
94
105
|
return html`<div>Unsupported field type: ${(config as any).type}</div>`;
|
|
95
106
|
}
|
|
@@ -367,6 +378,58 @@ export class FieldRenderer {
|
|
|
367
378
|
</div>`;
|
|
368
379
|
}
|
|
369
380
|
|
|
381
|
+
private static urlToAttachments(url: string): Attachment[] {
|
|
382
|
+
if (!url || !url.trim()) return [];
|
|
383
|
+
const filename = url.split('/').pop() || 'recording';
|
|
384
|
+
const ext = filename.split('.').pop()?.toLowerCase() || '';
|
|
385
|
+
const contentTypes: Record<string, string> = {
|
|
386
|
+
mp3: 'audio/mpeg',
|
|
387
|
+
wav: 'audio/wav',
|
|
388
|
+
ogg: 'audio/ogg',
|
|
389
|
+
m4a: 'audio/mp4'
|
|
390
|
+
};
|
|
391
|
+
return [
|
|
392
|
+
{
|
|
393
|
+
uuid: '',
|
|
394
|
+
content_type: contentTypes[ext] || 'audio/mpeg',
|
|
395
|
+
url,
|
|
396
|
+
filename,
|
|
397
|
+
size: 0,
|
|
398
|
+
error: ''
|
|
399
|
+
}
|
|
400
|
+
];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private static renderMedia(
|
|
404
|
+
fieldName: string,
|
|
405
|
+
config: MediaFieldConfig,
|
|
406
|
+
value: any,
|
|
407
|
+
context: FieldRenderContext
|
|
408
|
+
): TemplateResult {
|
|
409
|
+
const { onChange, showLabel = true } = context;
|
|
410
|
+
const endpoint = config.endpoint || DEFAULT_MEDIA_ENDPOINT;
|
|
411
|
+
const attachments = FieldRenderer.urlToAttachments(value);
|
|
412
|
+
|
|
413
|
+
return html`
|
|
414
|
+
<div>
|
|
415
|
+
${showLabel && config.label
|
|
416
|
+
? html`<label
|
|
417
|
+
style="margin-bottom: 5px; margin-left: 4px; display: block; font-weight: 400; font-size: var(--label-size); letter-spacing: 0.05em; line-height: normal; color: var(--color-label, #777);"
|
|
418
|
+
>${config.label}</label
|
|
419
|
+
>`
|
|
420
|
+
: ''}
|
|
421
|
+
<temba-media-picker
|
|
422
|
+
name="${fieldName}"
|
|
423
|
+
accept="${config.accept || ''}"
|
|
424
|
+
endpoint="${endpoint}"
|
|
425
|
+
max="1"
|
|
426
|
+
.attachments="${attachments}"
|
|
427
|
+
@change="${onChange || (() => {})}"
|
|
428
|
+
></temba-media-picker>
|
|
429
|
+
</div>
|
|
430
|
+
`;
|
|
431
|
+
}
|
|
432
|
+
|
|
370
433
|
private static renderMessageEditor(
|
|
371
434
|
fieldName: string,
|
|
372
435
|
config: MessageEditorFieldConfig,
|
package/src/interfaces.ts
CHANGED
|
@@ -303,5 +303,7 @@ export enum CustomEventType {
|
|
|
303
303
|
NodeEditRequested = 'temba-node-edit-requested',
|
|
304
304
|
NodeSaved = 'temba-node-saved',
|
|
305
305
|
NodeEditCancelled = 'temba-node-edit-cancelled',
|
|
306
|
-
FollowSimulation = 'temba-follow-simulation'
|
|
306
|
+
FollowSimulation = 'temba-follow-simulation',
|
|
307
|
+
ContactClicked = 'temba-contact-clicked',
|
|
308
|
+
ShowIssue = 'temba-show-issue'
|
|
307
309
|
}
|
package/src/layout/Dialog.ts
CHANGED
|
@@ -95,8 +95,9 @@ export class Dialog extends ResizeElement {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
.dialog-mask .dialog-container {
|
|
98
|
+
|
|
98
99
|
position: relative;
|
|
99
|
-
transition: transform var(--transition-speed)
|
|
100
|
+
transition: transform var(--transition-speed) ease-in-out,
|
|
100
101
|
opacity ease-in-out calc(var(--transition-speed) - 50ms);
|
|
101
102
|
border-radius: var(--curvature);
|
|
102
103
|
box-shadow: 0px 0px 2px 4px rgba(0, 0, 0, 0.06);
|
|
@@ -111,6 +112,7 @@ export class Dialog extends ResizeElement {
|
|
|
111
112
|
.dialog-body {
|
|
112
113
|
background: #fff;
|
|
113
114
|
overflow-y: auto;
|
|
115
|
+
overflow-x: hidden;
|
|
114
116
|
flex-grow: 1;
|
|
115
117
|
}
|
|
116
118
|
|
|
@@ -124,7 +126,7 @@ export class Dialog extends ResizeElement {
|
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
.dialog-mask.dialog-animation-end .dialog-container {
|
|
127
|
-
transform: scale(1) !important;
|
|
129
|
+
transform: scale(1) translate(0, 0) !important;
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
.dialog-mask.dialog-ready .dialog-container {
|
|
@@ -253,6 +255,12 @@ export class Dialog extends ResizeElement {
|
|
|
253
255
|
@property({ attribute: false })
|
|
254
256
|
onButtonClicked: (button: Button) => void;
|
|
255
257
|
|
|
258
|
+
@property({ type: Number })
|
|
259
|
+
originX: number | null = null;
|
|
260
|
+
|
|
261
|
+
@property({ type: Number })
|
|
262
|
+
originY: number | null = null;
|
|
263
|
+
|
|
256
264
|
scrollOffset: any = 0;
|
|
257
265
|
|
|
258
266
|
public constructor() {
|
|
@@ -289,11 +297,49 @@ export class Dialog extends ResizeElement {
|
|
|
289
297
|
const body = document.querySelector('body');
|
|
290
298
|
|
|
291
299
|
if (this.open) {
|
|
292
|
-
this.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
300
|
+
if (this.originX != null && this.originY != null) {
|
|
301
|
+
// Spring-from-origin animation: measure final position, then
|
|
302
|
+
// set initial transform at click point and transition to center
|
|
303
|
+
const ox = this.originX;
|
|
304
|
+
const oy = this.originY;
|
|
305
|
+
this.originX = null;
|
|
306
|
+
this.originY = null;
|
|
307
|
+
|
|
308
|
+
requestAnimationFrame(() => {
|
|
309
|
+
const container = this.shadowRoot?.querySelector(
|
|
310
|
+
'.dialog-container'
|
|
311
|
+
) as HTMLElement;
|
|
312
|
+
if (container) {
|
|
313
|
+
const rect = container.getBoundingClientRect();
|
|
314
|
+
const cx = rect.left + rect.width / 2;
|
|
315
|
+
const cy = rect.top + rect.height / 2;
|
|
316
|
+
const dx = ox - cx;
|
|
317
|
+
const dy = oy - cy;
|
|
318
|
+
|
|
319
|
+
// Disable transition so we can set the start position instantly
|
|
320
|
+
container.style.transition = 'none';
|
|
321
|
+
container.style.transform = `translate(${dx}px, ${dy}px) scale(0.2)`;
|
|
322
|
+
// Force reflow to register the start position
|
|
323
|
+
container.getBoundingClientRect();
|
|
324
|
+
|
|
325
|
+
// Re-enable transition and trigger animation to final position
|
|
326
|
+
container.style.transition = '';
|
|
327
|
+
this.animationEnd = true;
|
|
328
|
+
window.setTimeout(() => {
|
|
329
|
+
this.ready = true;
|
|
330
|
+
this.animationEnd = false;
|
|
331
|
+
container.style.transform = '';
|
|
332
|
+
}, 400);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
} else {
|
|
336
|
+
// Default animation (no origin)
|
|
337
|
+
this.animationEnd = true;
|
|
338
|
+
window.setTimeout(() => {
|
|
339
|
+
this.ready = true;
|
|
340
|
+
this.animationEnd = false;
|
|
341
|
+
}, 400);
|
|
342
|
+
}
|
|
297
343
|
|
|
298
344
|
this.scrollOffset = -document.documentElement.scrollTop;
|
|
299
345
|
body.style.position = 'fixed';
|
package/src/list/TicketList.ts
CHANGED
|
@@ -13,7 +13,10 @@ export class TicketList extends TembaList {
|
|
|
13
13
|
const lastActivity = this.items[0].ticket.last_activity_on;
|
|
14
14
|
const separator = this.endpoint.includes('?') ? '&' : '?';
|
|
15
15
|
return (
|
|
16
|
-
this.endpoint +
|
|
16
|
+
this.endpoint +
|
|
17
|
+
separator +
|
|
18
|
+
'after=' +
|
|
19
|
+
new Date(lastActivity).getTime() * 1000
|
|
17
20
|
);
|
|
18
21
|
}
|
|
19
22
|
return this.endpoint;
|
package/src/live/TembaChart.ts
CHANGED
|
@@ -759,7 +759,7 @@ export class TembaChart extends RapidElement {
|
|
|
759
759
|
private getValueAxisConfig() {
|
|
760
760
|
return {
|
|
761
761
|
min: 0,
|
|
762
|
-
...(this.showPercent && { max:
|
|
762
|
+
...(this.showPercent && { max:this.getInflatedMax() }),
|
|
763
763
|
stacked: true,
|
|
764
764
|
grid: {
|
|
765
765
|
color: 'rgba(0,0,0,0.04)',
|
package/src/locales/es.ts
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
's8f02e3a18ffc083a': `Are not currently in a flow`,
|
|
15
|
-
's638236250662c6b3': `Have sent a message in the last`,
|
|
16
|
-
's4788ee206c4570c7': `Have not started this flow in the last 90 days`,
|
|
17
|
-
};
|
|
18
|
-
|
|
1
|
+
// Do not modify this file by hand!
|
|
2
|
+
// Re-generate this file by running lit-localize
|
|
3
|
+
|
|
4
|
+
/* eslint-disable no-irregular-whitespace */
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
6
|
+
|
|
7
|
+
export const templates = {
|
|
8
|
+
scf1453991c986b25: `Tab para completar, enter para seleccionar`,
|
|
9
|
+
s73b4d70c02f4b4e0: `No options`,
|
|
10
|
+
s8f02e3a18ffc083a: `Are not currently in a flow`,
|
|
11
|
+
s638236250662c6b3: `Have sent a message in the last`,
|
|
12
|
+
s4788ee206c4570c7: `Have not started this flow in the last 90 days`
|
|
13
|
+
};
|
package/src/locales/fr.ts
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
's8f02e3a18ffc083a': `Are not currently in a flow`,
|
|
15
|
-
's638236250662c6b3': `Have sent a message in the last`,
|
|
16
|
-
's4788ee206c4570c7': `Have not started this flow in the last 90 days`,
|
|
17
|
-
};
|
|
18
|
-
|
|
1
|
+
// Do not modify this file by hand!
|
|
2
|
+
// Re-generate this file by running lit-localize
|
|
3
|
+
|
|
4
|
+
/* eslint-disable no-irregular-whitespace */
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
6
|
+
|
|
7
|
+
export const templates = {
|
|
8
|
+
s73b4d70c02f4b4e0: `No options`,
|
|
9
|
+
scf1453991c986b25: `Tab to complete, enter to select`,
|
|
10
|
+
s8f02e3a18ffc083a: `Are not currently in a flow`,
|
|
11
|
+
s638236250662c6b3: `Have sent a message in the last`,
|
|
12
|
+
s4788ee206c4570c7: `Have not started this flow in the last 90 days`
|
|
13
|
+
};
|