@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
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { html } from 'lit-html';
|
|
2
|
+
export function formatIssueMessage(issue) {
|
|
3
|
+
if (issue.dependency) {
|
|
4
|
+
const name = issue.dependency.name || issue.dependency.key;
|
|
5
|
+
return `Cannot find a ${issue.dependency.type} for ${name}`;
|
|
6
|
+
}
|
|
7
|
+
return issue.description;
|
|
8
|
+
}
|
|
9
|
+
const GRID_SIZE = 20;
|
|
10
|
+
export function snapToGrid(value) {
|
|
11
|
+
const snapped = Math.round(value / GRID_SIZE) * GRID_SIZE;
|
|
12
|
+
return Math.max(snapped, 0);
|
|
13
|
+
}
|
|
2
14
|
/**
|
|
3
15
|
* Renders a single line item with optional icon
|
|
4
16
|
*/
|
|
@@ -206,85 +218,221 @@ export const nodesOverlap = (bounds1, bounds2) => {
|
|
|
206
218
|
export const detectCollisions = (targetBounds, allBounds) => {
|
|
207
219
|
return allBounds.filter((bounds) => bounds.uuid !== targetBounds.uuid && nodesOverlap(targetBounds, bounds));
|
|
208
220
|
};
|
|
221
|
+
const DIRECTIONS = ['down', 'up', 'right', 'left'];
|
|
222
|
+
/**
|
|
223
|
+
* Creates a new NodeBounds at a different position
|
|
224
|
+
*/
|
|
225
|
+
const makeBoundsAt = (original, left, top) => ({
|
|
226
|
+
...original,
|
|
227
|
+
left,
|
|
228
|
+
top,
|
|
229
|
+
right: left + original.width,
|
|
230
|
+
bottom: top + original.height
|
|
231
|
+
});
|
|
209
232
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
233
|
+
* Computes the minimum position needed to clear all fixed nodes in a given direction.
|
|
234
|
+
* Returns null if the direction is not viable (e.g., would require negative coordinates
|
|
235
|
+
* and still overlap).
|
|
213
236
|
*/
|
|
214
|
-
|
|
237
|
+
const computeDirectionalClearance = (collider, fixedNodes, direction) => {
|
|
238
|
+
switch (direction) {
|
|
239
|
+
case 'down': {
|
|
240
|
+
const maxBottom = Math.max(...fixedNodes.map((f) => f.bottom));
|
|
241
|
+
const newTop = snapToGrid(maxBottom + MIN_NODE_SPACING);
|
|
242
|
+
return { left: collider.left, top: newTop };
|
|
243
|
+
}
|
|
244
|
+
case 'up': {
|
|
245
|
+
const minTop = Math.min(...fixedNodes.map((f) => f.top));
|
|
246
|
+
const newTop = snapToGrid(minTop - collider.height - MIN_NODE_SPACING);
|
|
247
|
+
if (newTop < 0)
|
|
248
|
+
return { left: collider.left, top: 0 };
|
|
249
|
+
return { left: collider.left, top: newTop };
|
|
250
|
+
}
|
|
251
|
+
case 'right': {
|
|
252
|
+
const maxRight = Math.max(...fixedNodes.map((f) => f.right));
|
|
253
|
+
const newLeft = snapToGrid(maxRight + MIN_NODE_SPACING);
|
|
254
|
+
return { left: newLeft, top: collider.top };
|
|
255
|
+
}
|
|
256
|
+
case 'left': {
|
|
257
|
+
const minLeft = Math.min(...fixedNodes.map((f) => f.left));
|
|
258
|
+
const newLeft = snapToGrid(minLeft - collider.width - MIN_NODE_SPACING);
|
|
259
|
+
if (newLeft < 0)
|
|
260
|
+
return { left: 0, top: collider.top };
|
|
261
|
+
return { left: newLeft, top: collider.top };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* Calculates new positions to resolve all collisions using multi-directional reflow.
|
|
267
|
+
*
|
|
268
|
+
* Sacred nodes (the ones just dropped/created) keep their positions. All other
|
|
269
|
+
* colliding nodes are moved in whichever direction requires the least displacement
|
|
270
|
+
* and causes the fewest cascading collisions.
|
|
271
|
+
*/
|
|
272
|
+
export const calculateReflowPositions = (sacredNodeUuids, allBounds) => {
|
|
215
273
|
const newPositions = new Map();
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
274
|
+
const sacredSet = new Set(sacredNodeUuids);
|
|
275
|
+
// Mutable map of current bounds, updated as collisions are resolved
|
|
276
|
+
const currentBounds = new Map();
|
|
277
|
+
for (const b of allBounds) {
|
|
278
|
+
currentBounds.set(b.uuid, { ...b });
|
|
279
|
+
}
|
|
280
|
+
// A sacred node yields to an existing node at the top of the canvas when
|
|
281
|
+
// the sacred wasn't dropped above it. The existing node keeps its top
|
|
282
|
+
// position and the sacred node moves below instead.
|
|
283
|
+
for (const sacredUuid of [...sacredSet]) {
|
|
284
|
+
const sacred = currentBounds.get(sacredUuid);
|
|
285
|
+
if (!sacred)
|
|
286
|
+
continue;
|
|
287
|
+
for (const [uuid, bounds] of currentBounds) {
|
|
288
|
+
if (uuid === sacredUuid || sacredSet.has(uuid))
|
|
289
|
+
continue;
|
|
290
|
+
if (!nodesOverlap(sacred, bounds))
|
|
291
|
+
continue;
|
|
292
|
+
if (sacred.top > bounds.top && bounds.top < MIN_NODE_SPACING) {
|
|
293
|
+
sacredSet.delete(sacredUuid);
|
|
294
|
+
sacredSet.add(uuid);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Seed the queue with non-sacred nodes that overlap any sacred node
|
|
300
|
+
const queue = [];
|
|
301
|
+
const inQueue = new Set();
|
|
302
|
+
for (const sacredUuid of sacredSet) {
|
|
303
|
+
const sacred = currentBounds.get(sacredUuid);
|
|
304
|
+
if (!sacred)
|
|
305
|
+
continue;
|
|
306
|
+
for (const [uuid, bounds] of currentBounds) {
|
|
307
|
+
if (sacredSet.has(uuid) || inQueue.has(uuid))
|
|
308
|
+
continue;
|
|
309
|
+
if (nodesOverlap(sacred, bounds)) {
|
|
310
|
+
queue.push(uuid);
|
|
311
|
+
inQueue.add(uuid);
|
|
312
|
+
}
|
|
235
313
|
}
|
|
236
314
|
}
|
|
237
|
-
|
|
238
|
-
const processedNodes = new Set();
|
|
239
|
-
processedNodes.add(movedNodeUuid);
|
|
240
|
-
// Keep checking for collisions until none remain
|
|
241
|
-
let hasCollisions = true;
|
|
315
|
+
const resolved = new Set();
|
|
242
316
|
let iterations = 0;
|
|
243
|
-
const maxIterations =
|
|
244
|
-
while (
|
|
245
|
-
hasCollisions = false;
|
|
317
|
+
const maxIterations = 200;
|
|
318
|
+
while (queue.length > 0 && iterations < maxIterations) {
|
|
246
319
|
iterations++;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
320
|
+
const uuid = queue.shift();
|
|
321
|
+
if (resolved.has(uuid))
|
|
322
|
+
continue;
|
|
323
|
+
const collider = currentBounds.get(uuid);
|
|
324
|
+
// Find all fixed nodes (sacred + already-resolved) that overlap this node
|
|
325
|
+
const fixedOverlaps = [];
|
|
326
|
+
for (const [otherUuid, otherBounds] of currentBounds) {
|
|
327
|
+
if (otherUuid === uuid)
|
|
250
328
|
continue;
|
|
329
|
+
if (sacredSet.has(otherUuid) || resolved.has(otherUuid)) {
|
|
330
|
+
if (nodesOverlap(collider, otherBounds)) {
|
|
331
|
+
fixedOverlaps.push(otherBounds);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (fixedOverlaps.length === 0)
|
|
336
|
+
continue;
|
|
337
|
+
// Determine direction constraints and axis bias from sacred node overlaps
|
|
338
|
+
const sacredOverlaps = fixedOverlaps.filter((f) => sacredSet.has(f.uuid));
|
|
339
|
+
const allowedDirections = [...DIRECTIONS];
|
|
340
|
+
let axisBias = null;
|
|
341
|
+
if (sacredOverlaps.length > 0) {
|
|
342
|
+
// Rule 1: don't move a lower node above the sacred node
|
|
343
|
+
// Rule 2: don't move a right-of node to the left of the sacred node
|
|
344
|
+
for (const sacred of sacredOverlaps) {
|
|
345
|
+
if (collider.top > sacred.top) {
|
|
346
|
+
const idx = allowedDirections.indexOf('up');
|
|
347
|
+
if (idx !== -1)
|
|
348
|
+
allowedDirections.splice(idx, 1);
|
|
349
|
+
}
|
|
350
|
+
if (collider.left > sacred.left) {
|
|
351
|
+
const idx = allowedDirections.indexOf('left');
|
|
352
|
+
if (idx !== -1)
|
|
353
|
+
allowedDirections.splice(idx, 1);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Rule 3: bias direction based on overlap shape
|
|
357
|
+
let totalOverlapWidth = 0;
|
|
358
|
+
let totalOverlapHeight = 0;
|
|
359
|
+
for (const sacred of sacredOverlaps) {
|
|
360
|
+
totalOverlapWidth +=
|
|
361
|
+
Math.min(collider.right, sacred.right) -
|
|
362
|
+
Math.max(collider.left, sacred.left);
|
|
363
|
+
totalOverlapHeight +=
|
|
364
|
+
Math.min(collider.bottom, sacred.bottom) -
|
|
365
|
+
Math.max(collider.top, sacred.top);
|
|
366
|
+
}
|
|
367
|
+
if (totalOverlapWidth > totalOverlapHeight) {
|
|
368
|
+
axisBias = 'vertical'; // wide overlap = nodes stacked = prefer up/down
|
|
251
369
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
// Check if this node collides with the moved node or any already repositioned nodes
|
|
255
|
-
let collisionFound = false;
|
|
256
|
-
let maxCollisionBottom = 0;
|
|
257
|
-
// Check against moved node
|
|
258
|
-
if (nodesOverlap(currentBounds, movedNodeBounds)) {
|
|
259
|
-
collisionFound = true;
|
|
260
|
-
maxCollisionBottom = Math.max(maxCollisionBottom, movedNodeBounds.bottom);
|
|
370
|
+
else if (totalOverlapHeight > totalOverlapWidth) {
|
|
371
|
+
axisBias = 'horizontal'; // tall overlap = nodes side-by-side = prefer left/right
|
|
261
372
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
373
|
+
}
|
|
374
|
+
// Try each allowed direction, pick the one with least disruption
|
|
375
|
+
let bestPos = null;
|
|
376
|
+
let bestScore = Infinity;
|
|
377
|
+
for (const dir of allowedDirections) {
|
|
378
|
+
const candidate = computeDirectionalClearance(collider, fixedOverlaps, dir);
|
|
379
|
+
if (!candidate)
|
|
380
|
+
continue;
|
|
381
|
+
const candidateBounds = makeBoundsAt(collider, candidate.left, candidate.top);
|
|
382
|
+
// Verify no overlap with any sacred or resolved node
|
|
383
|
+
let stillOverlaps = false;
|
|
384
|
+
let cascadeCount = 0;
|
|
385
|
+
for (const [otherUuid, otherBounds] of currentBounds) {
|
|
386
|
+
if (otherUuid === uuid)
|
|
265
387
|
continue;
|
|
266
|
-
|
|
267
|
-
if (!otherBounds)
|
|
388
|
+
if (!nodesOverlap(candidateBounds, otherBounds))
|
|
268
389
|
continue;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
bottom: otherPosition.top + otherBounds.height
|
|
273
|
-
};
|
|
274
|
-
if (nodesOverlap(currentBounds, otherUpdatedBounds)) {
|
|
275
|
-
collisionFound = true;
|
|
276
|
-
maxCollisionBottom = Math.max(maxCollisionBottom, otherUpdatedBounds.bottom);
|
|
390
|
+
if (sacredSet.has(otherUuid) || resolved.has(otherUuid)) {
|
|
391
|
+
stillOverlaps = true;
|
|
392
|
+
break;
|
|
277
393
|
}
|
|
394
|
+
cascadeCount++;
|
|
278
395
|
}
|
|
279
|
-
if (
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
396
|
+
if (stillOverlaps)
|
|
397
|
+
continue;
|
|
398
|
+
const distance = Math.abs(candidate.left - collider.left) +
|
|
399
|
+
Math.abs(candidate.top - collider.top);
|
|
400
|
+
// When colliding with sacred nodes, use axis bias scoring;
|
|
401
|
+
// for cascading collisions (no sacred overlap), use original scoring
|
|
402
|
+
let score;
|
|
403
|
+
if (sacredOverlaps.length > 0) {
|
|
404
|
+
const isVerticalDir = dir === 'up' || dir === 'down';
|
|
405
|
+
const axisMatch = axisBias === null ||
|
|
406
|
+
(axisBias === 'vertical' && isVerticalDir) ||
|
|
407
|
+
(axisBias === 'horizontal' && !isVerticalDir);
|
|
408
|
+
const axisPenalty = axisMatch ? 0 : 5000;
|
|
409
|
+
score = cascadeCount * 2000 + axisPenalty + distance;
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
score = cascadeCount * 10000 + distance;
|
|
413
|
+
}
|
|
414
|
+
if (score < bestScore) {
|
|
415
|
+
bestScore = score;
|
|
416
|
+
bestPos = candidate;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (bestPos) {
|
|
420
|
+
newPositions.set(uuid, { left: bestPos.left, top: bestPos.top });
|
|
421
|
+
const newBounds = makeBoundsAt(collider, bestPos.left, bestPos.top);
|
|
422
|
+
currentBounds.set(uuid, newBounds);
|
|
423
|
+
resolved.add(uuid);
|
|
424
|
+
// Enqueue any new cascading collisions
|
|
425
|
+
for (const [otherUuid, otherBounds] of currentBounds) {
|
|
426
|
+
if (otherUuid === uuid)
|
|
427
|
+
continue;
|
|
428
|
+
if (sacredSet.has(otherUuid) || resolved.has(otherUuid))
|
|
429
|
+
continue;
|
|
430
|
+
if (inQueue.has(otherUuid))
|
|
431
|
+
continue;
|
|
432
|
+
if (nodesOverlap(newBounds, otherBounds)) {
|
|
433
|
+
queue.push(otherUuid);
|
|
434
|
+
inQueue.add(otherUuid);
|
|
435
|
+
}
|
|
288
436
|
}
|
|
289
437
|
}
|
|
290
438
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/flow/utils.ts"],"names":[],"mappings":"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","sourcesContent":["import { html } from 'lit-html';\nimport { NamedObject, FlowPosition } from '../store/flow-definition';\n\n/**\n * Renders a single line item with optional icon\n */\nexport const renderLineItem = (name: string, icon?: string) => {\n return html`<div style=\"display:flex;items-align:center;\">\n ${icon\n ? html`<temba-icon name=${icon} style=\"margin-right:0.5em\"></temba-icon>`\n : null}\n <div\n style=\"white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 250px;\"\n >\n ${name}\n </div>\n </div>`;\n};\n\n/**\n * Renders a list of named objects with optional icon, showing up to 3 items\n * with a \"+X more\" indicator if there are more items\n */\nexport const renderNamedObjects = (assets: NamedObject[], icon?: string) => {\n return renderStringList(\n assets.map((asset) => asset.name),\n icon\n );\n};\n\n/**\n * Renders a list of strings with optional icon, showing up to 3 items\n * with a \"+X more\" indicator if there are more items\n */\nexport const renderStringList = (items: string[], icon?: string) => {\n const itemElements = [];\n const maxDisplay = 3;\n\n // Show up to 3 items, or all 4 if exactly 4 items\n const displayCount =\n items.length === 4 ? 4 : Math.min(maxDisplay, items.length);\n\n for (let i = 0; i < displayCount; i++) {\n const item = items[i];\n itemElements.push(renderLineItem(item, icon));\n }\n\n // Add \"+X more\" if there are more than 3 items (and not exactly 4)\n if (items.length > maxDisplay && items.length !== 4) {\n const remainingCount = items.length - maxDisplay;\n itemElements.push(html`<div\n style=\"display:flex;items-align:center;margin-top:0.2em;\"\n >\n ${icon\n ? html`<div style=\"margin-right:0.4em; width: 1em;\"></div>` // spacing placeholder\n : null}\n <div style=\"font-size:0.8em\">+${remainingCount} more</div>\n </div>`);\n }\n return itemElements;\n};\n\nexport interface Scheme {\n scheme: string;\n name: string;\n path: string;\n excludeFromSplit?: boolean;\n}\n\nexport const SCHEMES: Scheme[] = [\n {\n scheme: 'tel',\n name: 'SMS',\n path: 'Phone Number'\n },\n {\n scheme: 'whatsapp',\n name: 'WhatsApp',\n path: 'WhatsApp Number'\n },\n {\n scheme: 'facebook',\n name: 'Facebook',\n path: 'Facebook ID'\n },\n {\n scheme: 'instagram',\n name: 'Instagram',\n path: 'Instagram ID'\n },\n {\n scheme: 'twitterid',\n name: 'Twitter',\n path: 'Twitter ID',\n excludeFromSplit: true\n },\n {\n scheme: 'telegram',\n name: 'Telegram',\n path: 'Telegram ID'\n },\n {\n scheme: 'viber',\n name: 'Viber',\n path: 'Viber ID'\n },\n {\n scheme: 'line',\n name: 'Line',\n path: 'Line ID'\n },\n {\n scheme: 'wechat',\n name: 'WeChat',\n path: 'WeChat ID'\n },\n {\n scheme: 'fcm',\n name: 'Firebase',\n path: 'Firebase ID'\n },\n {\n scheme: 'jiochat',\n name: 'JioChat',\n path: 'JioChat ID'\n },\n {\n scheme: 'freshchat',\n name: 'Freshchat',\n path: 'Freshchat ID'\n },\n {\n scheme: 'mailto',\n name: 'Email',\n path: 'Email Address',\n excludeFromSplit: true\n },\n {\n scheme: 'twitter',\n name: 'Twitter',\n path: 'Twitter Handle',\n excludeFromSplit: true\n },\n {\n scheme: 'vk',\n name: 'VK',\n path: 'VK ID'\n },\n {\n scheme: 'discord',\n name: 'Discord',\n path: 'Discord ID'\n },\n {\n scheme: 'webchat',\n name: 'Webchat',\n path: 'Webchat ID',\n excludeFromSplit: true\n },\n {\n scheme: 'rocketchat',\n name: 'RocketChat',\n path: 'RocketChat ID'\n },\n {\n scheme: 'ext',\n name: 'External',\n path: 'External ID'\n }\n];\n\n/**\n * Represents the bounding box of a node on the canvas\n */\nexport interface NodeBounds {\n uuid: string;\n left: number;\n top: number;\n right: number;\n bottom: number;\n width: number;\n height: number;\n}\n\n/**\n * Minimum vertical spacing between nodes (in pixels)\n */\nconst MIN_NODE_SPACING = 30;\n\n/**\n * Small buffer to avoid floating point precision issues in overlap detection (in pixels)\n * This prevents false positives when nodes are exactly adjacent (e.g., bottom of one node\n * at exactly the same position as top of another)\n */\nconst OVERLAP_BUFFER = 10;\n\n/**\n * Gets the bounding box for a node from the DOM\n *\n * @param nodeUuid - The UUID of the node\n * @param position - The current position of the node\n * @param element - Optional pre-fetched DOM element (recommended for performance when checking multiple nodes)\n * @returns NodeBounds object or null if element not found\n *\n * Note: When element is not provided, performs a DOM query which may impact performance\n * during bulk collision detection. Consider fetching elements beforehand when possible.\n */\nexport const getNodeBounds = (\n nodeUuid: string,\n position: FlowPosition,\n element?: HTMLElement\n): NodeBounds | null => {\n // If element is provided, use it; otherwise try to find it in DOM\n const nodeElement =\n element || (document.querySelector(`[id=\"${nodeUuid}\"]`) as HTMLElement);\n\n if (!nodeElement) {\n return null;\n }\n\n const rect = nodeElement.getBoundingClientRect();\n const width = rect.width;\n const height = rect.height;\n\n return {\n uuid: nodeUuid,\n left: position.left,\n top: position.top,\n right: position.left + width,\n bottom: position.top + height,\n width,\n height\n };\n};\n\n/**\n * Checks if two node bounding boxes overlap\n */\nexport const nodesOverlap = (\n bounds1: NodeBounds,\n bounds2: NodeBounds\n): boolean => {\n // Use a small buffer to avoid floating point precision issues\n const buffer = OVERLAP_BUFFER;\n\n return !(\n bounds1.right <= bounds2.left - buffer ||\n bounds1.left >= bounds2.right + buffer ||\n bounds1.bottom <= bounds2.top - buffer ||\n bounds1.top >= bounds2.bottom + buffer\n );\n};\n\n/**\n * Detects all collisions between a node and other nodes\n */\nexport const detectCollisions = (\n targetBounds: NodeBounds,\n allBounds: NodeBounds[]\n): NodeBounds[] => {\n return allBounds.filter(\n (bounds) =>\n bounds.uuid !== targetBounds.uuid && nodesOverlap(targetBounds, bounds)\n );\n};\n\n/**\n * Calculates the new positions needed to resolve all collisions\n * Nodes are only moved downward, never up, left, or right\n * Returns a map of node UUIDs to their new positions\n */\nexport const calculateReflowPositions = (\n movedNodeUuid: string,\n movedNodeBounds: NodeBounds,\n allBounds: NodeBounds[],\n droppedBelowMidpoint: boolean = false\n): Map<string, FlowPosition> => {\n const newPositions = new Map<string, FlowPosition>();\n\n // If dropped below midpoint, the moved node should move down instead\n if (droppedBelowMidpoint) {\n // Find all nodes that collide with the moved node\n const collisions = detectCollisions(movedNodeBounds, allBounds);\n\n if (collisions.length > 0) {\n // Find the highest bottom position of all colliding nodes\n const maxBottom = Math.max(...collisions.map((b) => b.bottom));\n\n // Move the dropped node below all colliding nodes\n const newTop = maxBottom + MIN_NODE_SPACING;\n newPositions.set(movedNodeUuid, {\n left: movedNodeBounds.left,\n top: newTop\n });\n\n // Update the moved node bounds for further collision checks\n movedNodeBounds = {\n ...movedNodeBounds,\n top: newTop,\n bottom: newTop + movedNodeBounds.height\n };\n }\n }\n\n // Now check for any remaining collisions and move other nodes down\n const processedNodes = new Set<string>();\n processedNodes.add(movedNodeUuid);\n\n // Keep checking for collisions until none remain\n let hasCollisions = true;\n let iterations = 0;\n const maxIterations = 100; // Prevent infinite loops\n\n while (hasCollisions && iterations < maxIterations) {\n hasCollisions = false;\n iterations++;\n\n // Check all nodes for collisions\n for (const bounds of allBounds) {\n if (processedNodes.has(bounds.uuid)) {\n continue;\n }\n\n // Use original bounds since we skip already processed nodes\n const currentBounds = bounds;\n\n // Check if this node collides with the moved node or any already repositioned nodes\n let collisionFound = false;\n let maxCollisionBottom = 0;\n\n // Check against moved node\n if (nodesOverlap(currentBounds, movedNodeBounds)) {\n collisionFound = true;\n maxCollisionBottom = Math.max(\n maxCollisionBottom,\n movedNodeBounds.bottom\n );\n }\n\n // Check against other repositioned nodes\n for (const [otherUuid, otherPosition] of newPositions.entries()) {\n if (otherUuid === bounds.uuid) continue;\n\n const otherBounds = allBounds.find((b) => b.uuid === otherUuid);\n if (!otherBounds) continue;\n\n const otherUpdatedBounds = {\n ...otherBounds,\n top: otherPosition.top,\n bottom: otherPosition.top + otherBounds.height\n };\n\n if (nodesOverlap(currentBounds, otherUpdatedBounds)) {\n collisionFound = true;\n maxCollisionBottom = Math.max(\n maxCollisionBottom,\n otherUpdatedBounds.bottom\n );\n }\n }\n\n if (collisionFound) {\n // Move this node down below the collision\n const newTop = maxCollisionBottom + MIN_NODE_SPACING;\n newPositions.set(bounds.uuid, {\n left: bounds.left,\n top: newTop\n });\n hasCollisions = true;\n processedNodes.add(bounds.uuid);\n }\n }\n }\n\n return newPositions;\n};\n"]}
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/flow/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAIhC,MAAM,UAAU,kBAAkB,CAAC,KAAgB;IACjD,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;QACrB,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;QAC3D,OAAO,iBAAiB,KAAK,CAAC,UAAU,CAAC,IAAI,QAAQ,IAAI,EAAE,CAAC;IAC9D,CAAC;IACD,OAAO,KAAK,CAAC,WAAW,CAAC;AAC3B,CAAC;AAED,MAAM,SAAS,GAAG,EAAE,CAAC;AAErB,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;IAC1D,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;AAC9B,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,IAAY,EAAE,IAAa,EAAE,EAAE;IAC5D,OAAO,IAAI,CAAA;MACP,IAAI;QACJ,CAAC,CAAC,IAAI,CAAA,oBAAoB,IAAI,2CAA2C;QACzE,CAAC,CAAC,IAAI;;;;QAIJ,IAAI;;SAEH,CAAC;AACV,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,MAAqB,EAAE,IAAa,EAAE,EAAE;IACzE,OAAO,gBAAgB,CACrB,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,EACjC,IAAI,CACL,CAAC;AACJ,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,KAAe,EAAE,IAAa,EAAE,EAAE;IACjE,MAAM,YAAY,GAAG,EAAE,CAAC;IACxB,MAAM,UAAU,GAAG,CAAC,CAAC;IAErB,kDAAkD;IAClD,MAAM,YAAY,GAChB,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAE9D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IAChD,CAAC;IAED,mEAAmE;IACnE,IAAI,KAAK,CAAC,MAAM,GAAG,UAAU,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpD,MAAM,cAAc,GAAG,KAAK,CAAC,MAAM,GAAG,UAAU,CAAC;QACjD,YAAY,CAAC,IAAI,CAAC,IAAI,CAAA;;;QAGlB,IAAI;YACJ,CAAC,CAAC,IAAI,CAAA,qDAAqD,CAAC,sBAAsB;YAClF,CAAC,CAAC,IAAI;sCACwB,cAAc;WACzC,CAAC,CAAC;IACX,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC,CAAC;AASF,MAAM,CAAC,MAAM,OAAO,GAAa;IAC/B;QACE,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,KAAK;QACX,IAAI,EAAE,cAAc;KACrB;IACD;QACE,MAAM,EAAE,UAAU;QAClB,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE,iBAAiB;KACxB;IACD;QACE,MAAM,EAAE,UAAU;QAClB,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE,aAAa;KACpB;IACD;QACE,MAAM,EAAE,WAAW;QACnB,IAAI,EAAE,WAAW;QACjB,IAAI,EAAE,cAAc;KACrB;IACD;QACE,MAAM,EAAE,WAAW;QACnB,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,YAAY;QAClB,gBAAgB,EAAE,IAAI;KACvB;IACD;QACE,MAAM,EAAE,UAAU;QAClB,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE,aAAa;KACpB;IACD;QACE,MAAM,EAAE,OAAO;QACf,IAAI,EAAE,OAAO;QACb,IAAI,EAAE,UAAU;KACjB;IACD;QACE,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,SAAS;KAChB;IACD;QACE,MAAM,EAAE,QAAQ;QAChB,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,WAAW;KAClB;IACD;QACE,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE,aAAa;KACpB;IACD;QACE,MAAM,EAAE,SAAS;QACjB,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,YAAY;KACnB;IACD;QACE,MAAM,EAAE,WAAW;QACnB,IAAI,EAAE,WAAW;QACjB,IAAI,EAAE,cAAc;KACrB;IACD;QACE,MAAM,EAAE,QAAQ;QAChB,IAAI,EAAE,OAAO;QACb,IAAI,EAAE,eAAe;QACrB,gBAAgB,EAAE,IAAI;KACvB;IACD;QACE,MAAM,EAAE,SAAS;QACjB,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,gBAAgB;QACtB,gBAAgB,EAAE,IAAI;KACvB;IACD;QACE,MAAM,EAAE,IAAI;QACZ,IAAI,EAAE,IAAI;QACV,IAAI,EAAE,OAAO;KACd;IACD;QACE,MAAM,EAAE,SAAS;QACjB,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,YAAY;KACnB;IACD;QACE,MAAM,EAAE,SAAS;QACjB,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,YAAY;QAClB,gBAAgB,EAAE,IAAI;KACvB;IACD;QACE,MAAM,EAAE,YAAY;QACpB,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE,eAAe;KACtB;IACD;QACE,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,UAAU;QAChB,IAAI,EAAE,aAAa;KACpB;CACF,CAAC;AAeF;;GAEG;AACH,MAAM,gBAAgB,GAAG,EAAE,CAAC;AAE5B;;;;GAIG;AACH,MAAM,cAAc,GAAG,EAAE,CAAC;AAE1B;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,QAAgB,EAChB,QAAsB,EACtB,OAAqB,EACF,EAAE;IACrB,kEAAkE;IAClE,MAAM,WAAW,GACf,OAAO,IAAK,QAAQ,CAAC,aAAa,CAAC,QAAQ,QAAQ,IAAI,CAAiB,CAAC;IAE3E,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,IAAI,GAAG,WAAW,CAAC,qBAAqB,EAAE,CAAC;IACjD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IACzB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAE3B,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,GAAG,EAAE,QAAQ,CAAC,GAAG;QACjB,KAAK,EAAE,QAAQ,CAAC,IAAI,GAAG,KAAK;QAC5B,MAAM,EAAE,QAAQ,CAAC,GAAG,GAAG,MAAM;QAC7B,KAAK;QACL,MAAM;KACP,CAAC;AACJ,CAAC,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,OAAmB,EACnB,OAAmB,EACV,EAAE;IACX,8DAA8D;IAC9D,MAAM,MAAM,GAAG,cAAc,CAAC;IAE9B,OAAO,CAAC,CACN,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,IAAI,GAAG,MAAM;QACtC,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,KAAK,GAAG,MAAM;QACtC,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,GAAG,MAAM;QACtC,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,MAAM,GAAG,MAAM,CACvC,CAAC;AACJ,CAAC,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAC9B,YAAwB,EACxB,SAAuB,EACT,EAAE;IAChB,OAAO,SAAS,CAAC,MAAM,CACrB,CAAC,MAAM,EAAE,EAAE,CACT,MAAM,CAAC,IAAI,KAAK,YAAY,CAAC,IAAI,IAAI,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAC1E,CAAC;AACJ,CAAC,CAAC;AAIF,MAAM,UAAU,GAAgB,CAAC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AAEhE;;GAEG;AACH,MAAM,YAAY,GAAG,CACnB,QAAoB,EACpB,IAAY,EACZ,GAAW,EACC,EAAE,CAAC,CAAC;IAChB,GAAG,QAAQ;IACX,IAAI;IACJ,GAAG;IACH,KAAK,EAAE,IAAI,GAAG,QAAQ,CAAC,KAAK;IAC5B,MAAM,EAAE,GAAG,GAAG,QAAQ,CAAC,MAAM;CAC9B,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,2BAA2B,GAAG,CAClC,QAAoB,EACpB,UAAwB,EACxB,SAAoB,EACkB,EAAE;IACxC,QAAQ,SAAS,EAAE,CAAC;QAClB,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;YAC/D,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,GAAG,gBAAgB,CAAC,CAAC;YACxD,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;QAC9C,CAAC;QACD,KAAK,IAAI,CAAC,CAAC,CAAC;YACV,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACzD,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,GAAG,gBAAgB,CAAC,CAAC;YACvE,IAAI,MAAM,GAAG,CAAC;gBAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;YACvD,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;QAC9C,CAAC;QACD,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;YAC7D,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,GAAG,gBAAgB,CAAC,CAAC;YACxD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,CAAC;QAC9C,CAAC;QACD,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YAC3D,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,GAAG,QAAQ,CAAC,KAAK,GAAG,gBAAgB,CAAC,CAAC;YACxE,IAAI,OAAO,GAAG,CAAC;gBAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,CAAC;YACvD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,CAAC;QAC9C,CAAC;IACH,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CACtC,eAAyB,EACzB,SAAuB,EACI,EAAE;IAC7B,MAAM,YAAY,GAAG,IAAI,GAAG,EAAwB,CAAC;IACrD,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,eAAe,CAAC,CAAC;IAE3C,oEAAoE;IACpE,MAAM,aAAa,GAAG,IAAI,GAAG,EAAsB,CAAC;IACpD,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IACtC,CAAC;IAED,yEAAyE;IACzE,sEAAsE;IACtE,oDAAoD;IACpD,KAAK,MAAM,UAAU,IAAI,CAAC,GAAG,SAAS,CAAC,EAAE,CAAC;QACxC,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC7C,IAAI,CAAC,MAAM;YAAE,SAAS;QAEtB,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,aAAa,EAAE,CAAC;YAC3C,IAAI,IAAI,KAAK,UAAU,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,SAAS;YACzD,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC;gBAAE,SAAS;YAE5C,IAAI,MAAM,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,IAAI,MAAM,CAAC,GAAG,GAAG,gBAAgB,EAAE,CAAC;gBAC7D,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;gBAC7B,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACpB,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,oEAAoE;IACpE,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAElC,KAAK,MAAM,UAAU,IAAI,SAAS,EAAE,CAAC;QACnC,MAAM,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC7C,IAAI,CAAC,MAAM;YAAE,SAAS;QACtB,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,aAAa,EAAE,CAAC;YAC3C,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,SAAS;YACvD,IAAI,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;gBACjC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACjB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,MAAM,aAAa,GAAG,GAAG,CAAC;IAE1B,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,UAAU,GAAG,aAAa,EAAE,CAAC;QACtD,UAAU,EAAE,CAAC;QACb,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,EAAG,CAAC;QAE5B,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAEjC,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC;QAE1C,0EAA0E;QAC1E,MAAM,aAAa,GAAiB,EAAE,CAAC;QACvC,KAAK,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC,IAAI,aAAa,EAAE,CAAC;YACrD,IAAI,SAAS,KAAK,IAAI;gBAAE,SAAS;YACjC,IAAI,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;gBACxD,IAAI,YAAY,CAAC,QAAQ,EAAE,WAAW,CAAC,EAAE,CAAC;oBACxC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;gBAClC,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAEzC,0EAA0E;QAC1E,MAAM,cAAc,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAC1E,MAAM,iBAAiB,GAAgB,CAAC,GAAG,UAAU,CAAC,CAAC;QACvD,IAAI,QAAQ,GAAqC,IAAI,CAAC;QAEtD,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9B,wDAAwD;YACxD,oEAAoE;YACpE,KAAK,MAAM,MAAM,IAAI,cAAc,EAAE,CAAC;gBACpC,IAAI,QAAQ,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC;oBAC9B,MAAM,GAAG,GAAG,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;oBAC5C,IAAI,GAAG,KAAK,CAAC,CAAC;wBAAE,iBAAiB,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBACnD,CAAC;gBACD,IAAI,QAAQ,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;oBAChC,MAAM,GAAG,GAAG,iBAAiB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;oBAC9C,IAAI,GAAG,KAAK,CAAC,CAAC;wBAAE,iBAAiB,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBACnD,CAAC;YACH,CAAC;YAED,gDAAgD;YAChD,IAAI,iBAAiB,GAAG,CAAC,CAAC;YAC1B,IAAI,kBAAkB,GAAG,CAAC,CAAC;YAC3B,KAAK,MAAM,MAAM,IAAI,cAAc,EAAE,CAAC;gBACpC,iBAAiB;oBACf,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC;wBACtC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;gBACvC,kBAAkB;oBAChB,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;wBACxC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;YACvC,CAAC;YACD,IAAI,iBAAiB,GAAG,kBAAkB,EAAE,CAAC;gBAC3C,QAAQ,GAAG,UAAU,CAAC,CAAC,gDAAgD;YACzE,CAAC;iBAAM,IAAI,kBAAkB,GAAG,iBAAiB,EAAE,CAAC;gBAClD,QAAQ,GAAG,YAAY,CAAC,CAAC,wDAAwD;YACnF,CAAC;QACH,CAAC;QAED,iEAAiE;QACjE,IAAI,OAAO,GAAyC,IAAI,CAAC;QACzD,IAAI,SAAS,GAAG,QAAQ,CAAC;QAEzB,KAAK,MAAM,GAAG,IAAI,iBAAiB,EAAE,CAAC;YACpC,MAAM,SAAS,GAAG,2BAA2B,CAC3C,QAAQ,EACR,aAAa,EACb,GAAG,CACJ,CAAC;YACF,IAAI,CAAC,SAAS;gBAAE,SAAS;YAEzB,MAAM,eAAe,GAAG,YAAY,CAClC,QAAQ,EACR,SAAS,CAAC,IAAI,EACd,SAAS,CAAC,GAAG,CACd,CAAC;YAEF,qDAAqD;YACrD,IAAI,aAAa,GAAG,KAAK,CAAC;YAC1B,IAAI,YAAY,GAAG,CAAC,CAAC;YACrB,KAAK,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC,IAAI,aAAa,EAAE,CAAC;gBACrD,IAAI,SAAS,KAAK,IAAI;oBAAE,SAAS;gBACjC,IAAI,CAAC,YAAY,CAAC,eAAe,EAAE,WAAW,CAAC;oBAAE,SAAS;gBAE1D,IAAI,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;oBACxD,aAAa,GAAG,IAAI,CAAC;oBACrB,MAAM;gBACR,CAAC;gBACD,YAAY,EAAE,CAAC;YACjB,CAAC;YACD,IAAI,aAAa;gBAAE,SAAS;YAE5B,MAAM,QAAQ,GACZ,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC;gBACxC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;YAEzC,2DAA2D;YAC3D,qEAAqE;YACrE,IAAI,KAAa,CAAC;YAClB,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC9B,MAAM,aAAa,GAAG,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,MAAM,CAAC;gBACrD,MAAM,SAAS,GACb,QAAQ,KAAK,IAAI;oBACjB,CAAC,QAAQ,KAAK,UAAU,IAAI,aAAa,CAAC;oBAC1C,CAAC,QAAQ,KAAK,YAAY,IAAI,CAAC,aAAa,CAAC,CAAC;gBAChD,MAAM,WAAW,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBACzC,KAAK,GAAG,YAAY,GAAG,IAAI,GAAG,WAAW,GAAG,QAAQ,CAAC;YACvD,CAAC;iBAAM,CAAC;gBACN,KAAK,GAAG,YAAY,GAAG,KAAK,GAAG,QAAQ,CAAC;YAC1C,CAAC;YAED,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;gBACtB,SAAS,GAAG,KAAK,CAAC;gBAClB,OAAO,GAAG,SAAS,CAAC;YACtB,CAAC;QACH,CAAC;QAED,IAAI,OAAO,EAAE,CAAC;YACZ,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;YACjE,MAAM,SAAS,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;YACpE,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;YACnC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAEnB,uCAAuC;YACvC,KAAK,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC,IAAI,aAAa,EAAE,CAAC;gBACrD,IAAI,SAAS,KAAK,IAAI;oBAAE,SAAS;gBACjC,IAAI,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC;oBAAE,SAAS;gBAClE,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;oBAAE,SAAS;gBACrC,IAAI,YAAY,CAAC,SAAS,EAAE,WAAW,CAAC,EAAE,CAAC;oBACzC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;oBACtB,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBACzB,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,YAAY,CAAC;AACtB,CAAC,CAAC","sourcesContent":["import { html } from 'lit-html';\nimport { NamedObject, FlowPosition } from '../store/flow-definition';\nimport { FlowIssue } from '../store/AppState';\n\nexport function formatIssueMessage(issue: FlowIssue): string {\n if (issue.dependency) {\n const name = issue.dependency.name || issue.dependency.key;\n return `Cannot find a ${issue.dependency.type} for ${name}`;\n }\n return issue.description;\n}\n\nconst GRID_SIZE = 20;\n\nexport function snapToGrid(value: number): number {\n const snapped = Math.round(value / GRID_SIZE) * GRID_SIZE;\n return Math.max(snapped, 0);\n}\n\n/**\n * Renders a single line item with optional icon\n */\nexport const renderLineItem = (name: string, icon?: string) => {\n return html`<div style=\"display:flex;items-align:center;\">\n ${icon\n ? html`<temba-icon name=${icon} style=\"margin-right:0.5em\"></temba-icon>`\n : null}\n <div\n style=\"white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 250px;\"\n >\n ${name}\n </div>\n </div>`;\n};\n\n/**\n * Renders a list of named objects with optional icon, showing up to 3 items\n * with a \"+X more\" indicator if there are more items\n */\nexport const renderNamedObjects = (assets: NamedObject[], icon?: string) => {\n return renderStringList(\n assets.map((asset) => asset.name),\n icon\n );\n};\n\n/**\n * Renders a list of strings with optional icon, showing up to 3 items\n * with a \"+X more\" indicator if there are more items\n */\nexport const renderStringList = (items: string[], icon?: string) => {\n const itemElements = [];\n const maxDisplay = 3;\n\n // Show up to 3 items, or all 4 if exactly 4 items\n const displayCount =\n items.length === 4 ? 4 : Math.min(maxDisplay, items.length);\n\n for (let i = 0; i < displayCount; i++) {\n const item = items[i];\n itemElements.push(renderLineItem(item, icon));\n }\n\n // Add \"+X more\" if there are more than 3 items (and not exactly 4)\n if (items.length > maxDisplay && items.length !== 4) {\n const remainingCount = items.length - maxDisplay;\n itemElements.push(html`<div\n style=\"display:flex;items-align:center;margin-top:0.2em;\"\n >\n ${icon\n ? html`<div style=\"margin-right:0.4em; width: 1em;\"></div>` // spacing placeholder\n : null}\n <div style=\"font-size:0.8em\">+${remainingCount} more</div>\n </div>`);\n }\n return itemElements;\n};\n\nexport interface Scheme {\n scheme: string;\n name: string;\n path: string;\n excludeFromSplit?: boolean;\n}\n\nexport const SCHEMES: Scheme[] = [\n {\n scheme: 'tel',\n name: 'SMS',\n path: 'Phone Number'\n },\n {\n scheme: 'whatsapp',\n name: 'WhatsApp',\n path: 'WhatsApp Number'\n },\n {\n scheme: 'facebook',\n name: 'Facebook',\n path: 'Facebook ID'\n },\n {\n scheme: 'instagram',\n name: 'Instagram',\n path: 'Instagram ID'\n },\n {\n scheme: 'twitterid',\n name: 'Twitter',\n path: 'Twitter ID',\n excludeFromSplit: true\n },\n {\n scheme: 'telegram',\n name: 'Telegram',\n path: 'Telegram ID'\n },\n {\n scheme: 'viber',\n name: 'Viber',\n path: 'Viber ID'\n },\n {\n scheme: 'line',\n name: 'Line',\n path: 'Line ID'\n },\n {\n scheme: 'wechat',\n name: 'WeChat',\n path: 'WeChat ID'\n },\n {\n scheme: 'fcm',\n name: 'Firebase',\n path: 'Firebase ID'\n },\n {\n scheme: 'jiochat',\n name: 'JioChat',\n path: 'JioChat ID'\n },\n {\n scheme: 'freshchat',\n name: 'Freshchat',\n path: 'Freshchat ID'\n },\n {\n scheme: 'mailto',\n name: 'Email',\n path: 'Email Address',\n excludeFromSplit: true\n },\n {\n scheme: 'twitter',\n name: 'Twitter',\n path: 'Twitter Handle',\n excludeFromSplit: true\n },\n {\n scheme: 'vk',\n name: 'VK',\n path: 'VK ID'\n },\n {\n scheme: 'discord',\n name: 'Discord',\n path: 'Discord ID'\n },\n {\n scheme: 'webchat',\n name: 'Webchat',\n path: 'Webchat ID',\n excludeFromSplit: true\n },\n {\n scheme: 'rocketchat',\n name: 'RocketChat',\n path: 'RocketChat ID'\n },\n {\n scheme: 'ext',\n name: 'External',\n path: 'External ID'\n }\n];\n\n/**\n * Represents the bounding box of a node on the canvas\n */\nexport interface NodeBounds {\n uuid: string;\n left: number;\n top: number;\n right: number;\n bottom: number;\n width: number;\n height: number;\n}\n\n/**\n * Minimum vertical spacing between nodes (in pixels)\n */\nconst MIN_NODE_SPACING = 30;\n\n/**\n * Small buffer to avoid floating point precision issues in overlap detection (in pixels)\n * This prevents false positives when nodes are exactly adjacent (e.g., bottom of one node\n * at exactly the same position as top of another)\n */\nconst OVERLAP_BUFFER = 10;\n\n/**\n * Gets the bounding box for a node from the DOM\n *\n * @param nodeUuid - The UUID of the node\n * @param position - The current position of the node\n * @param element - Optional pre-fetched DOM element (recommended for performance when checking multiple nodes)\n * @returns NodeBounds object or null if element not found\n *\n * Note: When element is not provided, performs a DOM query which may impact performance\n * during bulk collision detection. Consider fetching elements beforehand when possible.\n */\nexport const getNodeBounds = (\n nodeUuid: string,\n position: FlowPosition,\n element?: HTMLElement\n): NodeBounds | null => {\n // If element is provided, use it; otherwise try to find it in DOM\n const nodeElement =\n element || (document.querySelector(`[id=\"${nodeUuid}\"]`) as HTMLElement);\n\n if (!nodeElement) {\n return null;\n }\n\n const rect = nodeElement.getBoundingClientRect();\n const width = rect.width;\n const height = rect.height;\n\n return {\n uuid: nodeUuid,\n left: position.left,\n top: position.top,\n right: position.left + width,\n bottom: position.top + height,\n width,\n height\n };\n};\n\n/**\n * Checks if two node bounding boxes overlap\n */\nexport const nodesOverlap = (\n bounds1: NodeBounds,\n bounds2: NodeBounds\n): boolean => {\n // Use a small buffer to avoid floating point precision issues\n const buffer = OVERLAP_BUFFER;\n\n return !(\n bounds1.right <= bounds2.left - buffer ||\n bounds1.left >= bounds2.right + buffer ||\n bounds1.bottom <= bounds2.top - buffer ||\n bounds1.top >= bounds2.bottom + buffer\n );\n};\n\n/**\n * Detects all collisions between a node and other nodes\n */\nexport const detectCollisions = (\n targetBounds: NodeBounds,\n allBounds: NodeBounds[]\n): NodeBounds[] => {\n return allBounds.filter(\n (bounds) =>\n bounds.uuid !== targetBounds.uuid && nodesOverlap(targetBounds, bounds)\n );\n};\n\ntype Direction = 'down' | 'up' | 'right' | 'left';\n\nconst DIRECTIONS: Direction[] = ['down', 'up', 'right', 'left'];\n\n/**\n * Creates a new NodeBounds at a different position\n */\nconst makeBoundsAt = (\n original: NodeBounds,\n left: number,\n top: number\n): NodeBounds => ({\n ...original,\n left,\n top,\n right: left + original.width,\n bottom: top + original.height\n});\n\n/**\n * Computes the minimum position needed to clear all fixed nodes in a given direction.\n * Returns null if the direction is not viable (e.g., would require negative coordinates\n * and still overlap).\n */\nconst computeDirectionalClearance = (\n collider: NodeBounds,\n fixedNodes: NodeBounds[],\n direction: Direction\n): { left: number; top: number } | null => {\n switch (direction) {\n case 'down': {\n const maxBottom = Math.max(...fixedNodes.map((f) => f.bottom));\n const newTop = snapToGrid(maxBottom + MIN_NODE_SPACING);\n return { left: collider.left, top: newTop };\n }\n case 'up': {\n const minTop = Math.min(...fixedNodes.map((f) => f.top));\n const newTop = snapToGrid(minTop - collider.height - MIN_NODE_SPACING);\n if (newTop < 0) return { left: collider.left, top: 0 };\n return { left: collider.left, top: newTop };\n }\n case 'right': {\n const maxRight = Math.max(...fixedNodes.map((f) => f.right));\n const newLeft = snapToGrid(maxRight + MIN_NODE_SPACING);\n return { left: newLeft, top: collider.top };\n }\n case 'left': {\n const minLeft = Math.min(...fixedNodes.map((f) => f.left));\n const newLeft = snapToGrid(minLeft - collider.width - MIN_NODE_SPACING);\n if (newLeft < 0) return { left: 0, top: collider.top };\n return { left: newLeft, top: collider.top };\n }\n }\n};\n\n/**\n * Calculates new positions to resolve all collisions using multi-directional reflow.\n *\n * Sacred nodes (the ones just dropped/created) keep their positions. All other\n * colliding nodes are moved in whichever direction requires the least displacement\n * and causes the fewest cascading collisions.\n */\nexport const calculateReflowPositions = (\n sacredNodeUuids: string[],\n allBounds: NodeBounds[]\n): Map<string, FlowPosition> => {\n const newPositions = new Map<string, FlowPosition>();\n const sacredSet = new Set(sacredNodeUuids);\n\n // Mutable map of current bounds, updated as collisions are resolved\n const currentBounds = new Map<string, NodeBounds>();\n for (const b of allBounds) {\n currentBounds.set(b.uuid, { ...b });\n }\n\n // A sacred node yields to an existing node at the top of the canvas when\n // the sacred wasn't dropped above it. The existing node keeps its top\n // position and the sacred node moves below instead.\n for (const sacredUuid of [...sacredSet]) {\n const sacred = currentBounds.get(sacredUuid);\n if (!sacred) continue;\n\n for (const [uuid, bounds] of currentBounds) {\n if (uuid === sacredUuid || sacredSet.has(uuid)) continue;\n if (!nodesOverlap(sacred, bounds)) continue;\n\n if (sacred.top > bounds.top && bounds.top < MIN_NODE_SPACING) {\n sacredSet.delete(sacredUuid);\n sacredSet.add(uuid);\n break;\n }\n }\n }\n\n // Seed the queue with non-sacred nodes that overlap any sacred node\n const queue: string[] = [];\n const inQueue = new Set<string>();\n\n for (const sacredUuid of sacredSet) {\n const sacred = currentBounds.get(sacredUuid);\n if (!sacred) continue;\n for (const [uuid, bounds] of currentBounds) {\n if (sacredSet.has(uuid) || inQueue.has(uuid)) continue;\n if (nodesOverlap(sacred, bounds)) {\n queue.push(uuid);\n inQueue.add(uuid);\n }\n }\n }\n\n const resolved = new Set<string>();\n let iterations = 0;\n const maxIterations = 200;\n\n while (queue.length > 0 && iterations < maxIterations) {\n iterations++;\n const uuid = queue.shift()!;\n\n if (resolved.has(uuid)) continue;\n\n const collider = currentBounds.get(uuid)!;\n\n // Find all fixed nodes (sacred + already-resolved) that overlap this node\n const fixedOverlaps: NodeBounds[] = [];\n for (const [otherUuid, otherBounds] of currentBounds) {\n if (otherUuid === uuid) continue;\n if (sacredSet.has(otherUuid) || resolved.has(otherUuid)) {\n if (nodesOverlap(collider, otherBounds)) {\n fixedOverlaps.push(otherBounds);\n }\n }\n }\n\n if (fixedOverlaps.length === 0) continue;\n\n // Determine direction constraints and axis bias from sacred node overlaps\n const sacredOverlaps = fixedOverlaps.filter((f) => sacredSet.has(f.uuid));\n const allowedDirections: Direction[] = [...DIRECTIONS];\n let axisBias: 'vertical' | 'horizontal' | null = null;\n\n if (sacredOverlaps.length > 0) {\n // Rule 1: don't move a lower node above the sacred node\n // Rule 2: don't move a right-of node to the left of the sacred node\n for (const sacred of sacredOverlaps) {\n if (collider.top > sacred.top) {\n const idx = allowedDirections.indexOf('up');\n if (idx !== -1) allowedDirections.splice(idx, 1);\n }\n if (collider.left > sacred.left) {\n const idx = allowedDirections.indexOf('left');\n if (idx !== -1) allowedDirections.splice(idx, 1);\n }\n }\n\n // Rule 3: bias direction based on overlap shape\n let totalOverlapWidth = 0;\n let totalOverlapHeight = 0;\n for (const sacred of sacredOverlaps) {\n totalOverlapWidth +=\n Math.min(collider.right, sacred.right) -\n Math.max(collider.left, sacred.left);\n totalOverlapHeight +=\n Math.min(collider.bottom, sacred.bottom) -\n Math.max(collider.top, sacred.top);\n }\n if (totalOverlapWidth > totalOverlapHeight) {\n axisBias = 'vertical'; // wide overlap = nodes stacked = prefer up/down\n } else if (totalOverlapHeight > totalOverlapWidth) {\n axisBias = 'horizontal'; // tall overlap = nodes side-by-side = prefer left/right\n }\n }\n\n // Try each allowed direction, pick the one with least disruption\n let bestPos: { left: number; top: number } | null = null;\n let bestScore = Infinity;\n\n for (const dir of allowedDirections) {\n const candidate = computeDirectionalClearance(\n collider,\n fixedOverlaps,\n dir\n );\n if (!candidate) continue;\n\n const candidateBounds = makeBoundsAt(\n collider,\n candidate.left,\n candidate.top\n );\n\n // Verify no overlap with any sacred or resolved node\n let stillOverlaps = false;\n let cascadeCount = 0;\n for (const [otherUuid, otherBounds] of currentBounds) {\n if (otherUuid === uuid) continue;\n if (!nodesOverlap(candidateBounds, otherBounds)) continue;\n\n if (sacredSet.has(otherUuid) || resolved.has(otherUuid)) {\n stillOverlaps = true;\n break;\n }\n cascadeCount++;\n }\n if (stillOverlaps) continue;\n\n const distance =\n Math.abs(candidate.left - collider.left) +\n Math.abs(candidate.top - collider.top);\n\n // When colliding with sacred nodes, use axis bias scoring;\n // for cascading collisions (no sacred overlap), use original scoring\n let score: number;\n if (sacredOverlaps.length > 0) {\n const isVerticalDir = dir === 'up' || dir === 'down';\n const axisMatch =\n axisBias === null ||\n (axisBias === 'vertical' && isVerticalDir) ||\n (axisBias === 'horizontal' && !isVerticalDir);\n const axisPenalty = axisMatch ? 0 : 5000;\n score = cascadeCount * 2000 + axisPenalty + distance;\n } else {\n score = cascadeCount * 10000 + distance;\n }\n\n if (score < bestScore) {\n bestScore = score;\n bestPos = candidate;\n }\n }\n\n if (bestPos) {\n newPositions.set(uuid, { left: bestPos.left, top: bestPos.top });\n const newBounds = makeBoundsAt(collider, bestPos.left, bestPos.top);\n currentBounds.set(uuid, newBounds);\n resolved.add(uuid);\n\n // Enqueue any new cascading collisions\n for (const [otherUuid, otherBounds] of currentBounds) {\n if (otherUuid === uuid) continue;\n if (sacredSet.has(otherUuid) || resolved.has(otherUuid)) continue;\n if (inQueue.has(otherUuid)) continue;\n if (nodesOverlap(newBounds, otherBounds)) {\n queue.push(otherUuid);\n inQueue.add(otherUuid);\n }\n }\n }\n }\n\n return newPositions;\n};\n"]}
|
|
@@ -494,13 +494,15 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
|
|
|
494
494
|
}
|
|
495
495
|
|
|
496
496
|
.removable .remove-btn {
|
|
497
|
-
|
|
497
|
+
opacity: 0.3;
|
|
498
498
|
cursor: default;
|
|
499
|
+
pointer-events: none;
|
|
499
500
|
}
|
|
500
501
|
|
|
501
502
|
.removable .drag-handle {
|
|
502
|
-
|
|
503
|
+
opacity: 0.3;
|
|
503
504
|
cursor: default;
|
|
505
|
+
pointer-events: none;
|
|
504
506
|
}
|
|
505
507
|
|
|
506
508
|
.drag-handle {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ArrayEditor.js","sourceRoot":"","sources":["../../../src/form/ArrayEditor.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, css, TemplateResult } from 'lit';\nimport { customElement, property } from 'lit/decorators.js';\nimport { FieldConfig } from '../flow/types';\nimport { BaseListEditor, ListItem } from './BaseListEditor';\nimport { FieldRenderer } from './FieldRenderer';\nimport '../list/SortableList';\nimport { Icon } from '../Icons';\n\n@customElement('temba-array-editor')\nexport class TembaArrayEditor extends BaseListEditor<ListItem> {\n @property({ type: Object })\n itemConfig: Record<string, FieldConfig> = {};\n\n @property({ type: String })\n itemLabel = 'Item';\n\n @property({ type: Function })\n onItemChange?: (\n itemIndex: number,\n field: string,\n value: any,\n allItems: any[]\n ) => any[];\n\n @property({ type: Function })\n isEmptyItemFn?: (item: any) => boolean;\n\n @property({ type: Boolean })\n sortable = false;\n\n @property({ type: Boolean })\n maintainEmptyItem = true; // Enable by default for better UX\n\n // Focus preservation properties\n private focusInfo: {\n itemIndex: number;\n fieldName: string;\n selectionStart?: number;\n selectionEnd?: number;\n } | null = null;\n\n constructor() {\n super();\n this._items = [];\n }\n\n // External API\n @property({ type: Array })\n get value(): any[] {\n return [...this._items];\n }\n\n set value(newValue: any[]) {\n this._items = newValue || [];\n this.requestUpdate();\n }\n\n // Implement abstract methods\n isEmptyItem(item: ListItem): boolean {\n // Use configurable function if provided\n if (this.isEmptyItemFn) {\n return this.isEmptyItemFn(item);\n }\n\n // Default behavior: check if all values are empty\n const values = Object.values(item);\n if (values.length === 0) {\n return true;\n }\n\n return values.every(\n (value) => value === undefined || value === null || value === ''\n );\n }\n\n // Override cleanItems to be more permissive for form data\n protected cleanItems(items: ListItem[]): any {\n // For runtime attachments, keep items that have at least one non-empty field\n return items.filter((item) => {\n const values = Object.values(item);\n return (\n values.length > 0 &&\n values.some(\n (value) => value !== undefined && value !== null && value !== ''\n )\n );\n });\n }\n\n // Capture focus information before update\n private captureFocus(): void {\n const activeElement = this.shadowRoot?.activeElement as HTMLElement;\n\n // Also try document.activeElement as a fallback\n const globalActive = document.activeElement as HTMLElement;\n let targetElement = activeElement || globalActive;\n\n // If active element is within this component's shadow root, use it\n if (globalActive && this.shadowRoot?.contains(globalActive)) {\n targetElement = globalActive;\n }\n\n if (!targetElement) {\n this.focusInfo = null;\n return;\n }\n\n // Find the array item container by traversing up the DOM\n let currentElement = targetElement;\n let arrayItemElement: HTMLElement | null = null;\n\n // Traverse up through shadow DOM boundaries\n while (currentElement) {\n if (currentElement.classList?.contains('array-item')) {\n arrayItemElement = currentElement;\n break;\n }\n\n // Move up to parent, or cross shadow boundaries\n if (currentElement.parentElement) {\n currentElement = currentElement.parentElement;\n } else if (\n currentElement.parentNode &&\n (currentElement.parentNode as any).host\n ) {\n // Cross shadow boundary\n currentElement = (currentElement.parentNode as any).host;\n } else {\n break;\n }\n }\n\n if (!arrayItemElement) {\n this.focusInfo = null;\n return;\n }\n\n // Find the item index by looking at the item ID\n const itemIdMatch = arrayItemElement.id?.match(/array-item-(\\d+)/);\n if (!itemIdMatch) {\n this.focusInfo = null;\n return;\n }\n\n const itemIndex = parseInt(itemIdMatch[1], 10);\n\n // Determine the field name by examining the input element and its containers\n let fieldName = '';\n\n // First, check if it's a temba component with a name attribute\n if (targetElement.tagName?.toLowerCase().startsWith('temba-')) {\n fieldName =\n (targetElement as any).name || targetElement.getAttribute('name') || '';\n }\n\n // If not found, check regular HTML elements\n if (\n !fieldName &&\n targetElement.hasAttribute &&\n targetElement.hasAttribute('name')\n ) {\n fieldName = targetElement.getAttribute('name') || '';\n }\n\n // If still not found, look for data-field-name in parent containers\n if (!fieldName) {\n let searchElement = targetElement;\n while (searchElement && searchElement !== arrayItemElement) {\n if (\n searchElement.hasAttribute &&\n searchElement.hasAttribute('data-field-name')\n ) {\n fieldName = searchElement.getAttribute('data-field-name') || '';\n break;\n }\n searchElement = searchElement.parentElement;\n }\n }\n\n if (!fieldName) {\n this.focusInfo = null;\n return;\n }\n\n // Capture selection for text inputs (try the actual input element inside temba components)\n let inputForSelection = targetElement;\n if (targetElement.tagName?.toLowerCase().startsWith('temba-')) {\n // Look for the actual input element inside the temba component\n const innerInput =\n targetElement.shadowRoot?.querySelector('input, textarea') ||\n targetElement.querySelector('input, textarea');\n if (innerInput) {\n inputForSelection = innerInput as HTMLElement;\n }\n }\n\n const selectionStart = (inputForSelection as any).selectionStart;\n const selectionEnd = (inputForSelection as any).selectionEnd;\n\n this.focusInfo = {\n itemIndex,\n fieldName,\n selectionStart,\n selectionEnd\n };\n }\n\n // Restore focus after update\n private restoreFocus(): void {\n if (!this.focusInfo) {\n return;\n }\n\n const { itemIndex, fieldName, selectionStart, selectionEnd } =\n this.focusInfo;\n\n // Find the target element by array item index\n const arrayItemId = `array-item-${itemIndex}`;\n const arrayItemElement = this.shadowRoot?.getElementById(arrayItemId);\n\n if (!arrayItemElement) {\n // If the exact item doesn't exist (e.g., due to reordering), try to find by field name\n const allItems = this.shadowRoot?.querySelectorAll('.array-item');\n if (allItems && allItems.length > itemIndex) {\n const fallbackItem = allItems[itemIndex];\n if (fallbackItem) {\n this.attemptFocusRestore(\n fallbackItem as HTMLElement,\n fieldName,\n selectionStart,\n selectionEnd\n );\n }\n }\n this.focusInfo = null;\n return;\n }\n\n this.attemptFocusRestore(\n arrayItemElement,\n fieldName,\n selectionStart,\n selectionEnd\n );\n this.focusInfo = null;\n }\n\n private attemptFocusRestore(\n container: HTMLElement,\n fieldName: string,\n selectionStart?: number,\n selectionEnd?: number\n ): void {\n // Look for the field container first\n const fieldContainer = container.querySelector(\n `[data-field-name=\"${fieldName}\"]`\n );\n\n let targetElement: HTMLElement | null = null;\n\n if (fieldContainer) {\n // Look for temba components or input elements within the field container\n targetElement = fieldContainer.querySelector(\n 'temba-textinput, temba-completion, input, textarea'\n ) as HTMLElement;\n }\n\n // Fallback: search entire container\n if (!targetElement) {\n const selectors = [\n `temba-textinput[name=\"${fieldName}\"]`,\n `temba-completion[name=\"${fieldName}\"]`,\n `input[name=\"${fieldName}\"]`,\n `textarea[name=\"${fieldName}\"]`,\n `[name=\"${fieldName}\"]`\n ];\n\n for (const selector of selectors) {\n targetElement = container.querySelector(selector) as HTMLElement;\n if (targetElement) break;\n }\n }\n\n if (targetElement) {\n // Use multiple animation frames to ensure DOM is fully settled\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n try {\n targetElement.focus();\n\n // Restore selection if it's a text input\n if (selectionStart !== undefined && selectionEnd !== undefined) {\n // For temba components, we need to focus the inner input\n let inputForSelection = targetElement;\n if (targetElement.tagName?.toLowerCase().startsWith('temba-')) {\n const innerInput =\n targetElement.shadowRoot?.querySelector('input, textarea') ||\n targetElement.querySelector('input, textarea');\n if (innerInput && 'setSelectionRange' in innerInput) {\n inputForSelection = innerInput as any;\n }\n }\n\n if ('setSelectionRange' in inputForSelection) {\n (inputForSelection as any).setSelectionRange(\n selectionStart,\n selectionEnd\n );\n }\n }\n } catch (error) {\n // Ignore focus errors - element might not be focusable\n // Focus restoration failed, silently continue\n }\n });\n });\n }\n }\n\n createEmptyItem(): ListItem {\n return {};\n }\n\n protected handleFieldChange(\n itemIndex: number,\n fieldName: string,\n newValue: any\n ) {\n let updatedItems: any[];\n\n if (this.onItemChange) {\n updatedItems = this.onItemChange(\n itemIndex,\n fieldName,\n newValue,\n this._items\n );\n } else {\n updatedItems = [...this._items];\n updatedItems[itemIndex] = {\n ...updatedItems[itemIndex],\n [fieldName]: newValue\n };\n }\n\n this.updateValue(updatedItems);\n }\n\n // Override Lit's update lifecycle methods for focus preservation\n protected willUpdate(changedProperties: Map<string, any>): void {\n super.willUpdate(changedProperties);\n\n // Capture focus before update if items are changing\n if (changedProperties.has('_items') || changedProperties.has('value')) {\n this.captureFocus();\n }\n }\n\n updated(changedProperties: Map<string, any>): void {\n super.updated(changedProperties);\n\n // Restore focus after update if items changed\n if (changedProperties.has('_items') || changedProperties.has('value')) {\n this.restoreFocus();\n }\n }\n\n private handleOrderChanged(event: CustomEvent): void {\n const detail = event.detail;\n\n // Handle swap-based logic from SortableList\n if (detail.swap && Array.isArray(detail.swap) && detail.swap.length === 2) {\n const [fromIdx, toIdx] = detail.swap;\n\n // Only reorder if the indexes are different and valid\n if (\n fromIdx !== toIdx &&\n fromIdx >= 0 &&\n toIdx >= 0 &&\n fromIdx < this._items.length &&\n toIdx < this._items.length\n ) {\n const updatedItems = [...this._items];\n // Move the item using splice operations\n const movedItem = updatedItems.splice(fromIdx, 1)[0];\n updatedItems.splice(toIdx, 0, movedItem);\n this.updateValue(updatedItems);\n }\n }\n }\n\n renderWidget(): TemplateResult {\n const items = this.displayItems;\n\n const itemsContent = items.map((item, index) => {\n const renderedItem = this.renderItem(item, index);\n\n if (this.sortable && !this.isEmptyItem(item)) {\n // Wrap non-empty items with sortable class and unique ID for drag-and-drop\n return html`\n <div class=\"sortable\" id=\"array-item-${index}\">${renderedItem}</div>\n `;\n } else {\n // Non-sortable items or empty items don't get the sortable wrapper\n return renderedItem;\n }\n });\n\n if (this.sortable) {\n return html`\n <div class=${this.getContainerClass()}>\n <temba-sortable-list\n dragHandle=\"drag-handle\"\n gap=\"0.4em\"\n @temba-order-changed=${this.handleOrderChanged}\n style=\"display: grid; grid-template-columns: 1fr; gap: 8px;\"\n >\n ${itemsContent}\n </temba-sortable-list>\n ${this.shouldShowAddButton() ? this.renderAddButton() : ''}\n </div>\n `;\n } else {\n // Non-sortable rendering (original behavior)\n return html`\n <div class=${this.getContainerClass()}>\n <div\n class=\"list-items\"\n style=\"display: grid; grid-template-columns: 1fr; gap: 8px;\"\n >\n ${itemsContent}\n </div>\n ${this.shouldShowAddButton() ? this.renderAddButton() : ''}\n </div>\n `;\n }\n }\n\n private computeFieldValue(\n itemIndex: number,\n fieldName: string,\n config: FieldConfig\n ): any {\n const item = this._items[itemIndex] || {};\n const currentValue = item[fieldName];\n\n if (config.computeValue) {\n return config.computeValue(item, currentValue);\n }\n\n // For select fields, ensure we return the right type\n /*if (config.type === 'select') {\n console.log('computeFieldValue select', currentValue, config);\n const selectConfig = config as SelectFieldConfig;\n if (currentValue === undefined || currentValue === null) {\n return selectConfig.multi ? [] : '';\n }\n }*/\n\n return currentValue;\n }\n\n private renderArrayField(\n itemIndex: number,\n fieldName: string,\n config: FieldConfig\n ): TemplateResult {\n const computedValue = this.computeFieldValue(itemIndex, fieldName, config);\n\n // Extract flavor from select config if available\n const flavor =\n config.type === 'select' ? (config as any).flavor || 'small' : 'small';\n\n // Build container style with width/maxWidth if specified\n let containerStyle = '';\n if (config.width) {\n containerStyle = `width: ${config.width};`;\n } else if (config.maxWidth) {\n containerStyle = `max-width: ${config.maxWidth};`;\n }\n\n // Use FieldRenderer for consistent field rendering\n const fieldContent = FieldRenderer.renderField(\n fieldName,\n config,\n computedValue,\n {\n showLabel: false, // ArrayEditor doesn't show labels for individual fields\n flavor: flavor,\n extraClasses: 'form-control',\n onChange: (e: Event) => {\n let value: any;\n const target = e.target as any;\n\n // Handle different field types and their change events\n if (config.type === 'select') {\n // Use consistent temba-select value normalization\n value = target.values;\n } else {\n // For other field types, use the target value directly\n value = target.value;\n }\n\n this.handleFieldChange(itemIndex, fieldName, value);\n }\n }\n );\n\n // Wrap in container with style if maxWidth is specified\n if (containerStyle) {\n return html`<div style=\"${containerStyle}\">${fieldContent}</div>`;\n }\n\n return fieldContent;\n }\n\n renderItem(item: ListItem, index: number): TemplateResult {\n const canRemove = this.canRemoveItem(index);\n\n // Render fields and track if any value fields are visible\n const fieldElements: TemplateResult[] = [];\n let hasVisibleValueField = false;\n\n Object.entries(this.itemConfig).forEach(([fieldName, config]) => {\n // Check visibility condition\n let isVisible = true;\n if (config.conditions?.visible) {\n try {\n const currentItem = this._items[index] || {};\n isVisible = config.conditions.visible(currentItem);\n } catch (error) {\n console.error(`Error checking visibility for ${fieldName}:`, error);\n }\n }\n\n if (isVisible) {\n // Check if this is a value field (text input without fixed sizing)\n const isValueField =\n !config.width && !config.maxWidth && config.type === 'text';\n if (isValueField) {\n hasVisibleValueField = true;\n }\n\n fieldElements.push(html`\n <div\n data-field-name=\"${fieldName}\"\n style=\"${config.width || config.maxWidth || config.type === 'select'\n ? 'flex:none'\n : 'flex:1'}\"\n >\n ${this.renderArrayField(index, fieldName, config)}\n </div>\n `);\n }\n });\n\n // If no value fields are visible, add a spacer to maintain alignment\n if (!hasVisibleValueField) {\n // Insert spacer after operator (first field) and before category (last field)\n fieldElements.splice(\n -1,\n 0,\n html`<div class=\"field field-flex spacer\" style=\"flex-grow:1\"></div>`\n );\n }\n\n return html`\n <div class=\"array-item\" id=\"array-item-${index}\">\n <div\n class=\"item-fields ${canRemove ? '' : 'removable'}\"\n style=\"display: flex; gap: 12px; align-items: center\"\n >\n ${this.sortable\n ? html`<temba-icon\n name=${Icon.sort}\n style=\"margin-right: -6px;\"\n class=\"drag-handle\"\n ></temba-icon>`\n : null}\n ${fieldElements}\n <button\n @click=${canRemove ? () => this.removeItem(index) : undefined}\n class=\"remove-btn\"\n style=\"\n padding: 4px;\n border: 1px solid #ccc;\n border-radius: 4px;\n background: white;\n cursor: pointer;\n background: #fefefe;\n color: #999;\n font-size: 14px;\n \"\n ?disabled=${!canRemove}\n >\n <temba-icon name=\"x\"></temba-icon>\n </button>\n </div>\n </div>\n `;\n }\n\n protected getContainerClass(): string {\n return 'array-editor';\n }\n\n static get styles() {\n return css`\n ${super.styles}\n\n .item-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n\n .item-title {\n font-weight: 600;\n color: #333;\n }\n\n .field {\n /* Base field styles */\n }\n\n .spacer {\n /* Empty spacer to maintain layout alignment */\n }\n\n .add-btn,\n .remove-btn {\n padding: 4px;\n border: 1px solid #ccc;\n border-radius: 4px;\n background: white;\n cursor: pointer;\n font-size: 14px;\n }\n\n .add-btn:hover,\n .remove-btn:hover {\n background: #f8f8f8;\n }\n\n .remove-btn {\n background: #fefefe;\n color: #999;\n }\n\n .removable .remove-btn {\n visibility: hidden;\n cursor: default;\n }\n\n .removable .drag-handle {\n visibility: hidden;\n cursor: default;\n }\n\n .drag-handle {\n cursor: grab;\n color: #ccc;\n }\n `;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ArrayEditor.js","sourceRoot":"","sources":["../../../src/form/ArrayEditor.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, css, TemplateResult } from 'lit';\nimport { customElement, property } from 'lit/decorators.js';\nimport { FieldConfig } from '../flow/types';\nimport { BaseListEditor, ListItem } from './BaseListEditor';\nimport { FieldRenderer } from './FieldRenderer';\nimport '../list/SortableList';\nimport { Icon } from '../Icons';\n\n@customElement('temba-array-editor')\nexport class TembaArrayEditor extends BaseListEditor<ListItem> {\n @property({ type: Object })\n itemConfig: Record<string, FieldConfig> = {};\n\n @property({ type: String })\n itemLabel = 'Item';\n\n @property({ type: Function })\n onItemChange?: (\n itemIndex: number,\n field: string,\n value: any,\n allItems: any[]\n ) => any[];\n\n @property({ type: Function })\n isEmptyItemFn?: (item: any) => boolean;\n\n @property({ type: Boolean })\n sortable = false;\n\n @property({ type: Boolean })\n maintainEmptyItem = true; // Enable by default for better UX\n\n // Focus preservation properties\n private focusInfo: {\n itemIndex: number;\n fieldName: string;\n selectionStart?: number;\n selectionEnd?: number;\n } | null = null;\n\n constructor() {\n super();\n this._items = [];\n }\n\n // External API\n @property({ type: Array })\n get value(): any[] {\n return [...this._items];\n }\n\n set value(newValue: any[]) {\n this._items = newValue || [];\n this.requestUpdate();\n }\n\n // Implement abstract methods\n isEmptyItem(item: ListItem): boolean {\n // Use configurable function if provided\n if (this.isEmptyItemFn) {\n return this.isEmptyItemFn(item);\n }\n\n // Default behavior: check if all values are empty\n const values = Object.values(item);\n if (values.length === 0) {\n return true;\n }\n\n return values.every(\n (value) => value === undefined || value === null || value === ''\n );\n }\n\n // Override cleanItems to be more permissive for form data\n protected cleanItems(items: ListItem[]): any {\n // For runtime attachments, keep items that have at least one non-empty field\n return items.filter((item) => {\n const values = Object.values(item);\n return (\n values.length > 0 &&\n values.some(\n (value) => value !== undefined && value !== null && value !== ''\n )\n );\n });\n }\n\n // Capture focus information before update\n private captureFocus(): void {\n const activeElement = this.shadowRoot?.activeElement as HTMLElement;\n\n // Also try document.activeElement as a fallback\n const globalActive = document.activeElement as HTMLElement;\n let targetElement = activeElement || globalActive;\n\n // If active element is within this component's shadow root, use it\n if (globalActive && this.shadowRoot?.contains(globalActive)) {\n targetElement = globalActive;\n }\n\n if (!targetElement) {\n this.focusInfo = null;\n return;\n }\n\n // Find the array item container by traversing up the DOM\n let currentElement = targetElement;\n let arrayItemElement: HTMLElement | null = null;\n\n // Traverse up through shadow DOM boundaries\n while (currentElement) {\n if (currentElement.classList?.contains('array-item')) {\n arrayItemElement = currentElement;\n break;\n }\n\n // Move up to parent, or cross shadow boundaries\n if (currentElement.parentElement) {\n currentElement = currentElement.parentElement;\n } else if (\n currentElement.parentNode &&\n (currentElement.parentNode as any).host\n ) {\n // Cross shadow boundary\n currentElement = (currentElement.parentNode as any).host;\n } else {\n break;\n }\n }\n\n if (!arrayItemElement) {\n this.focusInfo = null;\n return;\n }\n\n // Find the item index by looking at the item ID\n const itemIdMatch = arrayItemElement.id?.match(/array-item-(\\d+)/);\n if (!itemIdMatch) {\n this.focusInfo = null;\n return;\n }\n\n const itemIndex = parseInt(itemIdMatch[1], 10);\n\n // Determine the field name by examining the input element and its containers\n let fieldName = '';\n\n // First, check if it's a temba component with a name attribute\n if (targetElement.tagName?.toLowerCase().startsWith('temba-')) {\n fieldName =\n (targetElement as any).name || targetElement.getAttribute('name') || '';\n }\n\n // If not found, check regular HTML elements\n if (\n !fieldName &&\n targetElement.hasAttribute &&\n targetElement.hasAttribute('name')\n ) {\n fieldName = targetElement.getAttribute('name') || '';\n }\n\n // If still not found, look for data-field-name in parent containers\n if (!fieldName) {\n let searchElement = targetElement;\n while (searchElement && searchElement !== arrayItemElement) {\n if (\n searchElement.hasAttribute &&\n searchElement.hasAttribute('data-field-name')\n ) {\n fieldName = searchElement.getAttribute('data-field-name') || '';\n break;\n }\n searchElement = searchElement.parentElement;\n }\n }\n\n if (!fieldName) {\n this.focusInfo = null;\n return;\n }\n\n // Capture selection for text inputs (try the actual input element inside temba components)\n let inputForSelection = targetElement;\n if (targetElement.tagName?.toLowerCase().startsWith('temba-')) {\n // Look for the actual input element inside the temba component\n const innerInput =\n targetElement.shadowRoot?.querySelector('input, textarea') ||\n targetElement.querySelector('input, textarea');\n if (innerInput) {\n inputForSelection = innerInput as HTMLElement;\n }\n }\n\n const selectionStart = (inputForSelection as any).selectionStart;\n const selectionEnd = (inputForSelection as any).selectionEnd;\n\n this.focusInfo = {\n itemIndex,\n fieldName,\n selectionStart,\n selectionEnd\n };\n }\n\n // Restore focus after update\n private restoreFocus(): void {\n if (!this.focusInfo) {\n return;\n }\n\n const { itemIndex, fieldName, selectionStart, selectionEnd } =\n this.focusInfo;\n\n // Find the target element by array item index\n const arrayItemId = `array-item-${itemIndex}`;\n const arrayItemElement = this.shadowRoot?.getElementById(arrayItemId);\n\n if (!arrayItemElement) {\n // If the exact item doesn't exist (e.g., due to reordering), try to find by field name\n const allItems = this.shadowRoot?.querySelectorAll('.array-item');\n if (allItems && allItems.length > itemIndex) {\n const fallbackItem = allItems[itemIndex];\n if (fallbackItem) {\n this.attemptFocusRestore(\n fallbackItem as HTMLElement,\n fieldName,\n selectionStart,\n selectionEnd\n );\n }\n }\n this.focusInfo = null;\n return;\n }\n\n this.attemptFocusRestore(\n arrayItemElement,\n fieldName,\n selectionStart,\n selectionEnd\n );\n this.focusInfo = null;\n }\n\n private attemptFocusRestore(\n container: HTMLElement,\n fieldName: string,\n selectionStart?: number,\n selectionEnd?: number\n ): void {\n // Look for the field container first\n const fieldContainer = container.querySelector(\n `[data-field-name=\"${fieldName}\"]`\n );\n\n let targetElement: HTMLElement | null = null;\n\n if (fieldContainer) {\n // Look for temba components or input elements within the field container\n targetElement = fieldContainer.querySelector(\n 'temba-textinput, temba-completion, input, textarea'\n ) as HTMLElement;\n }\n\n // Fallback: search entire container\n if (!targetElement) {\n const selectors = [\n `temba-textinput[name=\"${fieldName}\"]`,\n `temba-completion[name=\"${fieldName}\"]`,\n `input[name=\"${fieldName}\"]`,\n `textarea[name=\"${fieldName}\"]`,\n `[name=\"${fieldName}\"]`\n ];\n\n for (const selector of selectors) {\n targetElement = container.querySelector(selector) as HTMLElement;\n if (targetElement) break;\n }\n }\n\n if (targetElement) {\n // Use multiple animation frames to ensure DOM is fully settled\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n try {\n targetElement.focus();\n\n // Restore selection if it's a text input\n if (selectionStart !== undefined && selectionEnd !== undefined) {\n // For temba components, we need to focus the inner input\n let inputForSelection = targetElement;\n if (targetElement.tagName?.toLowerCase().startsWith('temba-')) {\n const innerInput =\n targetElement.shadowRoot?.querySelector('input, textarea') ||\n targetElement.querySelector('input, textarea');\n if (innerInput && 'setSelectionRange' in innerInput) {\n inputForSelection = innerInput as any;\n }\n }\n\n if ('setSelectionRange' in inputForSelection) {\n (inputForSelection as any).setSelectionRange(\n selectionStart,\n selectionEnd\n );\n }\n }\n } catch (error) {\n // Ignore focus errors - element might not be focusable\n // Focus restoration failed, silently continue\n }\n });\n });\n }\n }\n\n createEmptyItem(): ListItem {\n return {};\n }\n\n protected handleFieldChange(\n itemIndex: number,\n fieldName: string,\n newValue: any\n ) {\n let updatedItems: any[];\n\n if (this.onItemChange) {\n updatedItems = this.onItemChange(\n itemIndex,\n fieldName,\n newValue,\n this._items\n );\n } else {\n updatedItems = [...this._items];\n updatedItems[itemIndex] = {\n ...updatedItems[itemIndex],\n [fieldName]: newValue\n };\n }\n\n this.updateValue(updatedItems);\n }\n\n // Override Lit's update lifecycle methods for focus preservation\n protected willUpdate(changedProperties: Map<string, any>): void {\n super.willUpdate(changedProperties);\n\n // Capture focus before update if items are changing\n if (changedProperties.has('_items') || changedProperties.has('value')) {\n this.captureFocus();\n }\n }\n\n updated(changedProperties: Map<string, any>): void {\n super.updated(changedProperties);\n\n // Restore focus after update if items changed\n if (changedProperties.has('_items') || changedProperties.has('value')) {\n this.restoreFocus();\n }\n }\n\n private handleOrderChanged(event: CustomEvent): void {\n const detail = event.detail;\n\n // Handle swap-based logic from SortableList\n if (detail.swap && Array.isArray(detail.swap) && detail.swap.length === 2) {\n const [fromIdx, toIdx] = detail.swap;\n\n // Only reorder if the indexes are different and valid\n if (\n fromIdx !== toIdx &&\n fromIdx >= 0 &&\n toIdx >= 0 &&\n fromIdx < this._items.length &&\n toIdx < this._items.length\n ) {\n const updatedItems = [...this._items];\n // Move the item using splice operations\n const movedItem = updatedItems.splice(fromIdx, 1)[0];\n updatedItems.splice(toIdx, 0, movedItem);\n this.updateValue(updatedItems);\n }\n }\n }\n\n renderWidget(): TemplateResult {\n const items = this.displayItems;\n\n const itemsContent = items.map((item, index) => {\n const renderedItem = this.renderItem(item, index);\n\n if (this.sortable && !this.isEmptyItem(item)) {\n // Wrap non-empty items with sortable class and unique ID for drag-and-drop\n return html`\n <div class=\"sortable\" id=\"array-item-${index}\">${renderedItem}</div>\n `;\n } else {\n // Non-sortable items or empty items don't get the sortable wrapper\n return renderedItem;\n }\n });\n\n if (this.sortable) {\n return html`\n <div class=${this.getContainerClass()}>\n <temba-sortable-list\n dragHandle=\"drag-handle\"\n gap=\"0.4em\"\n @temba-order-changed=${this.handleOrderChanged}\n style=\"display: grid; grid-template-columns: 1fr; gap: 8px;\"\n >\n ${itemsContent}\n </temba-sortable-list>\n ${this.shouldShowAddButton() ? this.renderAddButton() : ''}\n </div>\n `;\n } else {\n // Non-sortable rendering (original behavior)\n return html`\n <div class=${this.getContainerClass()}>\n <div\n class=\"list-items\"\n style=\"display: grid; grid-template-columns: 1fr; gap: 8px;\"\n >\n ${itemsContent}\n </div>\n ${this.shouldShowAddButton() ? this.renderAddButton() : ''}\n </div>\n `;\n }\n }\n\n private computeFieldValue(\n itemIndex: number,\n fieldName: string,\n config: FieldConfig\n ): any {\n const item = this._items[itemIndex] || {};\n const currentValue = item[fieldName];\n\n if (config.computeValue) {\n return config.computeValue(item, currentValue);\n }\n\n // For select fields, ensure we return the right type\n /*if (config.type === 'select') {\n console.log('computeFieldValue select', currentValue, config);\n const selectConfig = config as SelectFieldConfig;\n if (currentValue === undefined || currentValue === null) {\n return selectConfig.multi ? [] : '';\n }\n }*/\n\n return currentValue;\n }\n\n private renderArrayField(\n itemIndex: number,\n fieldName: string,\n config: FieldConfig\n ): TemplateResult {\n const computedValue = this.computeFieldValue(itemIndex, fieldName, config);\n\n // Extract flavor from select config if available\n const flavor =\n config.type === 'select' ? (config as any).flavor || 'small' : 'small';\n\n // Build container style with width/maxWidth if specified\n let containerStyle = '';\n if (config.width) {\n containerStyle = `width: ${config.width};`;\n } else if (config.maxWidth) {\n containerStyle = `max-width: ${config.maxWidth};`;\n }\n\n // Use FieldRenderer for consistent field rendering\n const fieldContent = FieldRenderer.renderField(\n fieldName,\n config,\n computedValue,\n {\n showLabel: false, // ArrayEditor doesn't show labels for individual fields\n flavor: flavor,\n extraClasses: 'form-control',\n onChange: (e: Event) => {\n let value: any;\n const target = e.target as any;\n\n // Handle different field types and their change events\n if (config.type === 'select') {\n // Use consistent temba-select value normalization\n value = target.values;\n } else {\n // For other field types, use the target value directly\n value = target.value;\n }\n\n this.handleFieldChange(itemIndex, fieldName, value);\n }\n }\n );\n\n // Wrap in container with style if maxWidth is specified\n if (containerStyle) {\n return html`<div style=\"${containerStyle}\">${fieldContent}</div>`;\n }\n\n return fieldContent;\n }\n\n renderItem(item: ListItem, index: number): TemplateResult {\n const canRemove = this.canRemoveItem(index);\n\n // Render fields and track if any value fields are visible\n const fieldElements: TemplateResult[] = [];\n let hasVisibleValueField = false;\n\n Object.entries(this.itemConfig).forEach(([fieldName, config]) => {\n // Check visibility condition\n let isVisible = true;\n if (config.conditions?.visible) {\n try {\n const currentItem = this._items[index] || {};\n isVisible = config.conditions.visible(currentItem);\n } catch (error) {\n console.error(`Error checking visibility for ${fieldName}:`, error);\n }\n }\n\n if (isVisible) {\n // Check if this is a value field (text input without fixed sizing)\n const isValueField =\n !config.width && !config.maxWidth && config.type === 'text';\n if (isValueField) {\n hasVisibleValueField = true;\n }\n\n fieldElements.push(html`\n <div\n data-field-name=\"${fieldName}\"\n style=\"${config.width || config.maxWidth || config.type === 'select'\n ? 'flex:none'\n : 'flex:1'}\"\n >\n ${this.renderArrayField(index, fieldName, config)}\n </div>\n `);\n }\n });\n\n // If no value fields are visible, add a spacer to maintain alignment\n if (!hasVisibleValueField) {\n // Insert spacer after operator (first field) and before category (last field)\n fieldElements.splice(\n -1,\n 0,\n html`<div class=\"field field-flex spacer\" style=\"flex-grow:1\"></div>`\n );\n }\n\n return html`\n <div class=\"array-item\" id=\"array-item-${index}\">\n <div\n class=\"item-fields ${canRemove ? '' : 'removable'}\"\n style=\"display: flex; gap: 12px; align-items: center\"\n >\n ${this.sortable\n ? html`<temba-icon\n name=${Icon.sort}\n style=\"margin-right: -6px;\"\n class=\"drag-handle\"\n ></temba-icon>`\n : null}\n ${fieldElements}\n <button\n @click=${canRemove ? () => this.removeItem(index) : undefined}\n class=\"remove-btn\"\n style=\"\n padding: 4px;\n border: 1px solid #ccc;\n border-radius: 4px;\n background: white;\n cursor: pointer;\n background: #fefefe;\n color: #999;\n font-size: 14px;\n \"\n ?disabled=${!canRemove}\n >\n <temba-icon name=\"x\"></temba-icon>\n </button>\n </div>\n </div>\n `;\n }\n\n protected getContainerClass(): string {\n return 'array-editor';\n }\n\n static get styles() {\n return css`\n ${super.styles}\n\n .item-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n\n .item-title {\n font-weight: 600;\n color: #333;\n }\n\n .field {\n /* Base field styles */\n }\n\n .spacer {\n /* Empty spacer to maintain layout alignment */\n }\n\n .add-btn,\n .remove-btn {\n padding: 4px;\n border: 1px solid #ccc;\n border-radius: 4px;\n background: white;\n cursor: pointer;\n font-size: 14px;\n }\n\n .add-btn:hover,\n .remove-btn:hover {\n background: #f8f8f8;\n }\n\n .remove-btn {\n background: #fefefe;\n color: #999;\n }\n\n .removable .remove-btn {\n opacity: 0.3;\n cursor: default;\n pointer-events: none;\n }\n\n .removable .drag-handle {\n opacity: 0.3;\n cursor: default;\n pointer-events: none;\n }\n\n .drag-handle {\n cursor: grab;\n color: #ccc;\n }\n `;\n }\n}\n"]}
|