@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.
Files changed (196) hide show
  1. package/.github/workflows/cla.yml +1 -1
  2. package/.github/workflows/copilot-setup-steps.yml +6 -1
  3. package/CHANGELOG.md +26 -0
  4. package/demo/data/flows/sample-flow.json +24 -0
  5. package/dist/locales/es.js +5 -5
  6. package/dist/locales/es.js.map +1 -1
  7. package/dist/locales/fr.js +5 -5
  8. package/dist/locales/fr.js.map +1 -1
  9. package/dist/locales/locale-codes.js +2 -11
  10. package/dist/locales/locale-codes.js.map +1 -1
  11. package/dist/locales/pt.js +5 -5
  12. package/dist/locales/pt.js.map +1 -1
  13. package/dist/temba-components.js +1112 -882
  14. package/dist/temba-components.js.map +1 -1
  15. package/out-tsc/src/display/Chat.js +10 -7
  16. package/out-tsc/src/display/Chat.js.map +1 -1
  17. package/out-tsc/src/display/Dropdown.js +3 -1
  18. package/out-tsc/src/display/Dropdown.js.map +1 -1
  19. package/out-tsc/src/display/FloatingTab.js +25 -32
  20. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  21. package/out-tsc/src/display/Thumbnail.js +163 -5
  22. package/out-tsc/src/display/Thumbnail.js.map +1 -1
  23. package/out-tsc/src/flow/CanvasMenu.js +5 -3
  24. package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
  25. package/out-tsc/src/flow/CanvasNode.js +70 -29
  26. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  27. package/out-tsc/src/flow/Editor.js +290 -239
  28. package/out-tsc/src/flow/Editor.js.map +1 -1
  29. package/out-tsc/src/flow/NodeEditor.js +118 -10
  30. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  31. package/out-tsc/src/flow/Plumber.js +757 -403
  32. package/out-tsc/src/flow/Plumber.js.map +1 -1
  33. package/out-tsc/src/flow/StickyNote.js +13 -4
  34. package/out-tsc/src/flow/StickyNote.js.map +1 -1
  35. package/out-tsc/src/flow/actions/audio-player.js +112 -0
  36. package/out-tsc/src/flow/actions/audio-player.js.map +1 -0
  37. package/out-tsc/src/flow/actions/enter_flow.js +43 -0
  38. package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
  39. package/out-tsc/src/flow/actions/play_audio.js +57 -4
  40. package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
  41. package/out-tsc/src/flow/actions/say_msg.js +86 -3
  42. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  43. package/out-tsc/src/flow/config.js +11 -3
  44. package/out-tsc/src/flow/config.js.map +1 -1
  45. package/out-tsc/src/flow/nodes/shared-rules.js +1 -1
  46. package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -1
  47. package/out-tsc/src/flow/nodes/terminal.js +7 -0
  48. package/out-tsc/src/flow/nodes/terminal.js.map +1 -0
  49. package/out-tsc/src/flow/nodes/wait_for_audio.js +77 -0
  50. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
  51. package/out-tsc/src/flow/nodes/wait_for_dial.js +151 -0
  52. package/out-tsc/src/flow/nodes/wait_for_dial.js.map +1 -0
  53. package/out-tsc/src/flow/nodes/wait_for_digits.js +61 -1
  54. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
  55. package/out-tsc/src/flow/nodes/wait_for_menu.js +173 -2
  56. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
  57. package/out-tsc/src/flow/operators.js +21 -5
  58. package/out-tsc/src/flow/operators.js.map +1 -1
  59. package/out-tsc/src/flow/types.js.map +1 -1
  60. package/out-tsc/src/flow/utils.js +213 -65
  61. package/out-tsc/src/flow/utils.js.map +1 -1
  62. package/out-tsc/src/form/ArrayEditor.js +4 -2
  63. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  64. package/out-tsc/src/form/FieldRenderer.js +49 -0
  65. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  66. package/out-tsc/src/interfaces.js +2 -0
  67. package/out-tsc/src/interfaces.js.map +1 -1
  68. package/out-tsc/src/layout/Dialog.js +52 -7
  69. package/out-tsc/src/layout/Dialog.js.map +1 -1
  70. package/out-tsc/src/list/TicketList.js +4 -1
  71. package/out-tsc/src/list/TicketList.js.map +1 -1
  72. package/out-tsc/src/live/TembaChart.js.map +1 -1
  73. package/out-tsc/src/locales/es.js +5 -5
  74. package/out-tsc/src/locales/es.js.map +1 -1
  75. package/out-tsc/src/locales/fr.js +5 -5
  76. package/out-tsc/src/locales/fr.js.map +1 -1
  77. package/out-tsc/src/locales/locale-codes.js +2 -11
  78. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  79. package/out-tsc/src/locales/pt.js +5 -5
  80. package/out-tsc/src/locales/pt.js.map +1 -1
  81. package/out-tsc/src/simulator/Simulator.js +10 -3
  82. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  83. package/out-tsc/src/store/AppState.js +89 -3
  84. package/out-tsc/src/store/AppState.js.map +1 -1
  85. package/out-tsc/test/actions/play_audio.test.js +118 -0
  86. package/out-tsc/test/actions/play_audio.test.js.map +1 -0
  87. package/out-tsc/test/actions/say_msg.test.js +158 -0
  88. package/out-tsc/test/actions/say_msg.test.js.map +1 -0
  89. package/out-tsc/test/nodes/wait_for_audio.test.js +156 -0
  90. package/out-tsc/test/nodes/wait_for_audio.test.js.map +1 -0
  91. package/out-tsc/test/nodes/wait_for_dial.test.js +336 -0
  92. package/out-tsc/test/nodes/wait_for_dial.test.js.map +1 -0
  93. package/out-tsc/test/nodes/wait_for_digits.test.js +198 -84
  94. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  95. package/out-tsc/test/nodes/wait_for_menu.test.js +340 -0
  96. package/out-tsc/test/nodes/wait_for_menu.test.js.map +1 -0
  97. package/out-tsc/test/temba-floating-tab.test.js +4 -6
  98. package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
  99. package/out-tsc/test/temba-flow-collision.test.js +473 -220
  100. package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
  101. package/out-tsc/test/temba-flow-editor.test.js +0 -2
  102. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  103. package/out-tsc/test/temba-flow-plumber-connections.test.js +83 -84
  104. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  105. package/out-tsc/test/temba-flow-plumber.test.js +102 -93
  106. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  107. package/out-tsc/test/temba-node-type-selector.test.js +6 -6
  108. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
  109. package/package.json +1 -1
  110. package/screenshots/truth/actions/play_audio/editor/expression-url.png +0 -0
  111. package/screenshots/truth/actions/play_audio/editor/static-url.png +0 -0
  112. package/screenshots/truth/actions/play_audio/render/expression-url.png +0 -0
  113. package/screenshots/truth/actions/play_audio/render/static-url.png +0 -0
  114. package/screenshots/truth/actions/say_msg/editor/multiline-text.png +0 -0
  115. package/screenshots/truth/actions/say_msg/editor/simple-text.png +0 -0
  116. package/screenshots/truth/actions/say_msg/editor/text-with-audio-url.png +0 -0
  117. package/screenshots/truth/actions/say_msg/render/multiline-text.png +0 -0
  118. package/screenshots/truth/actions/say_msg/render/simple-text.png +0 -0
  119. package/screenshots/truth/actions/say_msg/render/text-with-audio-url.png +0 -0
  120. package/screenshots/truth/editor/router.png +0 -0
  121. package/screenshots/truth/editor/wait.png +0 -0
  122. package/screenshots/truth/nodes/wait_for_audio/editor/basic-audio-wait.png +0 -0
  123. package/screenshots/truth/nodes/wait_for_audio/render/basic-audio-wait.png +0 -0
  124. package/screenshots/truth/nodes/wait_for_dial/editor/basic-dial.png +0 -0
  125. package/screenshots/truth/nodes/wait_for_dial/editor/dial-with-limits.png +0 -0
  126. package/screenshots/truth/nodes/wait_for_dial/render/basic-dial.png +0 -0
  127. package/screenshots/truth/nodes/wait_for_dial/render/dial-with-limits.png +0 -0
  128. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  129. package/screenshots/truth/nodes/wait_for_digits/editor/digits-with-rules.png +0 -0
  130. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  131. package/screenshots/truth/nodes/wait_for_digits/render/digits-with-rules.png +0 -0
  132. package/screenshots/truth/nodes/wait_for_menu/editor/menu-with-digits.png +0 -0
  133. package/screenshots/truth/nodes/wait_for_menu/render/menu-with-digits.png +0 -0
  134. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  135. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  136. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  137. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  138. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  139. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  140. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  141. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  142. package/src/display/Chat.ts +13 -7
  143. package/src/display/Dropdown.ts +3 -1
  144. package/src/display/FloatingTab.ts +24 -33
  145. package/src/display/Thumbnail.ts +162 -2
  146. package/src/flow/CanvasMenu.ts +8 -3
  147. package/src/flow/CanvasNode.ts +75 -30
  148. package/src/flow/Editor.ts +336 -288
  149. package/src/flow/NodeEditor.ts +137 -9
  150. package/src/flow/Plumber.ts +1011 -457
  151. package/src/flow/StickyNote.ts +14 -4
  152. package/src/flow/actions/audio-player.ts +127 -0
  153. package/src/flow/actions/enter_flow.ts +44 -0
  154. package/src/flow/actions/play_audio.ts +64 -5
  155. package/src/flow/actions/say_msg.ts +94 -4
  156. package/src/flow/config.ts +11 -3
  157. package/src/flow/nodes/shared-rules.ts +1 -1
  158. package/src/flow/nodes/terminal.ts +9 -0
  159. package/src/flow/nodes/wait_for_audio.ts +88 -0
  160. package/src/flow/nodes/wait_for_dial.ts +176 -0
  161. package/src/flow/nodes/wait_for_digits.ts +86 -2
  162. package/src/flow/nodes/wait_for_menu.ts +209 -3
  163. package/src/flow/operators.ts +23 -5
  164. package/src/flow/types.ts +23 -1
  165. package/src/flow/utils.ts +238 -81
  166. package/src/form/ArrayEditor.ts +4 -2
  167. package/src/form/FieldRenderer.ts +64 -1
  168. package/src/interfaces.ts +3 -1
  169. package/src/layout/Dialog.ts +53 -7
  170. package/src/list/TicketList.ts +4 -1
  171. package/src/live/TembaChart.ts +1 -1
  172. package/src/locales/es.ts +13 -18
  173. package/src/locales/fr.ts +13 -18
  174. package/src/locales/locale-codes.ts +2 -11
  175. package/src/locales/pt.ts +13 -18
  176. package/src/simulator/Simulator.ts +13 -3
  177. package/src/store/AppState.ts +105 -1
  178. package/src/store/flow-definition.d.ts +2 -0
  179. package/test/actions/play_audio.test.ts +155 -0
  180. package/test/actions/say_msg.test.ts +196 -0
  181. package/test/nodes/wait_for_audio.test.ts +182 -0
  182. package/test/nodes/wait_for_dial.test.ts +382 -0
  183. package/test/nodes/wait_for_digits.test.ts +233 -109
  184. package/test/nodes/wait_for_menu.test.ts +383 -0
  185. package/test/temba-floating-tab.test.ts +4 -6
  186. package/test/temba-flow-collision.test.ts +495 -293
  187. package/test/temba-flow-editor.test.ts +0 -2
  188. package/test/temba-flow-plumber-connections.test.ts +97 -97
  189. package/test/temba-flow-plumber.test.ts +116 -103
  190. package/test/temba-node-type-selector.test.ts +6 -6
  191. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  192. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  193. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  194. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  195. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  196. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
@@ -11,6 +11,7 @@ import {
11
11
  import { getStore } from '../store/Store';
12
12
  import {
13
13
  AppState,
14
+ FlowIssue,
14
15
  fromStore,
15
16
  zustand,
16
17
  FLOW_SPEC_VERSION
@@ -19,6 +20,13 @@ import { RapidElement } from '../RapidElement';
19
20
  import { repeat } from 'lit-html/directives/repeat.js';
20
21
  import { CustomEventType, Workspace } from '../interfaces';
21
22
  import { generateUUID, postJSON, fetchResults, getClasses } from '../utils';
23
+ import {
24
+ formatIssueMessage,
25
+ getNodeBounds,
26
+ calculateReflowPositions,
27
+ NodeBounds,
28
+ snapToGrid
29
+ } from './utils';
22
30
  import { ACTION_CONFIG, NODE_CONFIG } from './config';
23
31
 
24
32
  interface Revision {
@@ -37,25 +45,20 @@ interface Revision {
37
45
  import { ACTION_GROUP_METADATA } from './types';
38
46
  import { Checkbox } from '../form/Checkbox';
39
47
 
40
- import { Plumber } from './Plumber';
48
+ import {
49
+ Plumber,
50
+ calculateFlowchartPath,
51
+ ARROW_LENGTH,
52
+ ARROW_HALF_WIDTH,
53
+ CURSOR_GAP
54
+ } from './Plumber';
41
55
  import { CanvasNode } from './CanvasNode';
42
56
  import { Dialog } from '../layout/Dialog';
43
- import { Connection } from '@jsplumb/browser-ui';
57
+
44
58
  import { CanvasMenu, CanvasMenuSelection } from './CanvasMenu';
45
59
  import { NodeTypeSelector, NodeTypeSelection } from './NodeTypeSelector';
46
- import {
47
- getNodeBounds,
48
- calculateReflowPositions,
49
- NodeBounds,
50
- nodesOverlap
51
- } from './utils';
52
60
  import { FloatingWindow } from '../layout/FloatingWindow';
53
61
 
54
- export function snapToGrid(value: number): number {
55
- const snapped = Math.round(value / 20) * 20;
56
- return Math.max(snapped, 0);
57
- }
58
-
59
62
  export function findNodeForExit(
60
63
  definition: FlowDefinition,
61
64
  exitUuid: string
@@ -122,7 +125,7 @@ const DROP_PREVIEW_OFFSET_X = 20;
122
125
  const DROP_PREVIEW_OFFSET_Y = 20;
123
126
 
124
127
  export class Editor extends RapidElement {
125
- // unfortunately, jsplumb requires that we be in light DOM
128
+ // connection SVGs are appended directly to the canvas, so we need light DOM
126
129
  createRenderRoot() {
127
130
  return this;
128
131
  }
@@ -172,6 +175,9 @@ export class Editor extends RapidElement {
172
175
  @fromStore(zustand, (state: AppState) => state.getCurrentActivity())
173
176
  private activityData!: any;
174
177
 
178
+ @fromStore(zustand, (state: AppState) => state.flowInfo?.issues || [])
179
+ private flowIssues!: FlowIssue[];
180
+
175
181
  // Drag state
176
182
  @state()
177
183
  private isDragging = false;
@@ -212,6 +218,13 @@ export class Editor extends RapidElement {
212
218
  @state()
213
219
  private isValidTarget = true;
214
220
 
221
+ // Canvas-relative source exit position (set at drag start)
222
+ private connectionSourceX: number | null = null;
223
+ private connectionSourceY: number | null = null;
224
+
225
+ @state()
226
+ private issuesWindowHidden = true;
227
+
215
228
  @state()
216
229
  private localizationWindowHidden = true;
217
230
 
@@ -264,6 +277,8 @@ export class Editor extends RapidElement {
264
277
  @state()
265
278
  private editingAction: Action | null = null;
266
279
 
280
+ private dialogOrigin: { x: number; y: number } | null = null;
281
+
267
282
  @state()
268
283
  private isCreatingNewNode = false;
269
284
 
@@ -294,6 +309,7 @@ export class Editor extends RapidElement {
294
309
  private connectionPlaceholder: {
295
310
  position: FlowPosition;
296
311
  visible: boolean;
312
+ dragUp?: boolean;
297
313
  } | null = null;
298
314
 
299
315
  // Track pending connection when dropping on canvas
@@ -351,6 +367,10 @@ export class Editor extends RapidElement {
351
367
  -webkit-font-smoothing: antialiased;
352
368
  }
353
369
 
370
+ temba-floating-tab {
371
+ --floating-tab-right: 15px;
372
+ }
373
+
354
374
  #grid {
355
375
  position: relative;
356
376
  background-color: #f9f9f9;
@@ -401,100 +421,56 @@ export class Editor extends RapidElement {
401
421
  }
402
422
 
403
423
  #grid.viewing-revision temba-flow-node,
404
- #grid.viewing-revision svg.jtk-connector,
405
- #grid.viewing-revision .activity-overlay {
424
+ #grid.viewing-revision svg.plumb-connector {
406
425
  opacity: 0.5;
407
426
  }
408
427
 
409
- body .jtk-endpoint {
410
- width: initial;
411
- height: initial;
412
- }
413
-
414
- .jtk-endpoint {
415
- z-index: 600;
416
- opacity: 0;
417
- }
418
-
419
- .plumb-source {
420
- z-index: 600;
421
- cursor: pointer;
422
- opacity: 0;
423
- }
424
-
425
- .plumb-source.connected {
426
- border-radius: 50%;
427
- pointer-events: none;
428
+ svg.plumb-connector {
429
+ z-index: 10;
428
430
  }
429
431
 
430
- .plumb-source circle {
431
- fill: purple;
432
+ svg.plumb-connector path {
433
+ stroke: var(--color-connectors);
434
+ stroke-width: 3px;
432
435
  }
433
436
 
434
- .plumb-target {
435
- z-index: 600;
436
- opacity: 0;
437
- cursor: pointer;
438
- fill: transparent;
437
+ svg.plumb-connector .plumb-arrow {
438
+ fill: var(--color-connectors);
439
+ stroke: none;
439
440
  }
440
441
 
441
- body svg.jtk-connector.plumb-connector path {
442
- stroke: var(--color-connectors) !important;
443
- stroke-width: 3px;
442
+ svg.plumb-connector.hover path {
443
+ stroke: var(--color-success);
444
444
  }
445
445
 
446
- body .plumb-connector {
447
- z-index: 10 !important;
446
+ svg.plumb-connector.hover .plumb-arrow {
447
+ fill: var(--color-success);
448
448
  }
449
449
 
450
- body .plumb-connector .plumb-arrow {
451
- fill: var(--color-connectors);
450
+ #canvas.read-only-connections svg.plumb-connector.hover path {
452
451
  stroke: var(--color-connectors);
453
- stroke-width: 0px !important;
454
- margin-top: 6px;
455
- z-index: 10;
456
452
  }
457
453
 
458
- body svg.jtk-connector.jtk-hover path {
459
- stroke: var(--color-success) !important;
460
- stroke-width: 3px;
461
- }
462
-
463
- body #canvas.read-only-connections svg.jtk-connector.jtk-hover path {
464
- stroke: var(--color-connectors) !important;
454
+ #canvas.read-only-connections svg.plumb-connector.hover .plumb-arrow {
455
+ fill: var(--color-connectors);
465
456
  }
466
457
 
467
- body .plumb-connector.jtk-hover .plumb-arrow {
468
- fill: var(--color-success) !important;
469
- stroke-width: 0px;
470
- z-index: 10;
458
+ #canvas.read-only-connections svg.plumb-connector,
459
+ #canvas.read-only-connections svg.plumb-connector * {
460
+ pointer-events: none !important;
461
+ cursor: default !important;
471
462
  }
472
463
 
473
- body
474
- #canvas.read-only-connections
475
- .plumb-connector.jtk-hover
476
- .plumb-arrow {
477
- fill: var(--color-connectors) !important;
478
- ponter-events: none;
464
+ svg.plumb-connector.removing path {
465
+ stroke: var(--color-error);
479
466
  }
480
467
 
481
- body #canvas.read-only-connections svg {
482
- pointer-events: none;
468
+ svg.plumb-connector.removing .plumb-arrow {
469
+ fill: var(--color-error);
483
470
  }
484
471
 
485
- /* Activity overlays on connections */
486
- .jtk-overlay.activity-overlay {
487
- background: #f3f3f3;
488
- border: 1px solid #d9d9d9;
489
- color: #333;
490
- border-radius: 4px;
491
- padding: 2px 4px;
492
- font-size: 10px;
493
- font-weight: 600;
494
- line-height: 0.9;
495
- cursor: pointer;
496
- z-index: 500;
497
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
472
+ svg.plumb-connector.dragging {
473
+ z-index: 99999;
498
474
  }
499
475
 
500
476
  /* Active contact count on nodes */
@@ -516,6 +492,30 @@ export class Editor extends RapidElement {
516
492
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
517
493
  }
518
494
 
495
+ /* Activity overlay badges on connection exit stubs */
496
+ .activity-overlay {
497
+ position: absolute;
498
+ background: #f3f3f3;
499
+ border: 1px solid #d9d9d9;
500
+ color: #333;
501
+ border-radius: 4px;
502
+ padding: 2px 4px;
503
+ font-size: 10px;
504
+ font-weight: 600;
505
+ line-height: 0.9;
506
+ cursor: pointer;
507
+ z-index: 10;
508
+ pointer-events: auto;
509
+ white-space: nowrap;
510
+ user-select: none;
511
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
512
+ }
513
+
514
+ #grid.viewing-revision .activity-overlay {
515
+ opacity: 0.5;
516
+ pointer-events: none;
517
+ }
518
+
519
519
  /* Recent contacts popup */
520
520
  @keyframes popupBounceIn {
521
521
  0% {
@@ -585,7 +585,6 @@ export class Editor extends RapidElement {
585
585
 
586
586
  .recent-contacts-popup .contact-name:hover {
587
587
  text-decoration: underline;
588
- color: var(--color-link-primary, #1d4ed8);
589
588
  }
590
589
 
591
590
  .recent-contacts-popup .contact-operand {
@@ -601,17 +600,6 @@ export class Editor extends RapidElement {
601
600
  color: #999;
602
601
  }
603
602
 
604
- /* Connection dragging feedback */
605
- body svg.jtk-connector.jtk-dragging {
606
- z-index: 99999 !important;
607
- }
608
-
609
- .katavorio-drag-no-select svg.jtk-connector path,
610
- .katavorio-drag-no-select svg.jtk-endpoint path {
611
- pointer-events: none !important;
612
- border: 1px solid purple;
613
- }
614
-
615
603
  /* Connection target feedback */
616
604
  temba-flow-node.connection-target-valid {
617
605
  outline: 3px solid var(--color-success, #22c55e) !important;
@@ -641,10 +629,6 @@ export class Editor extends RapidElement {
641
629
  border-radius: var(--curvature);
642
630
  }
643
631
 
644
- .jtk-floating-endpoint {
645
- pointer-events: none;
646
- }
647
-
648
632
  .localization-window-content {
649
633
  display: flex;
650
634
  flex-direction: column;
@@ -835,6 +819,26 @@ export class Editor extends RapidElement {
835
819
  color: #9ca3af;
836
820
  white-space: nowrap;
837
821
  }
822
+
823
+ .issue-list-item {
824
+ display: flex;
825
+ align-items: center;
826
+ gap: 8px;
827
+ padding: 8px;
828
+ border-radius: 4px;
829
+ cursor: pointer;
830
+ font-size: 13px;
831
+ color: #333;
832
+ }
833
+
834
+ .issue-list-item:hover {
835
+ background: #fff5f5;
836
+ }
837
+
838
+ .issue-list-item temba-icon {
839
+ color: tomato;
840
+ flex-shrink: 0;
841
+ }
838
842
  `;
839
843
  }
840
844
 
@@ -852,17 +856,15 @@ export class Editor extends RapidElement {
852
856
  getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
853
857
  }
854
858
 
855
- this.plumber.on('connection:drag', (connection: Connection) => {
856
- // console.log('connection:drag', connection);
857
- this.dragFromNodeId =
858
- connection.data.nodeId ||
859
- document.getElementById(connection.sourceId).closest('.node').id;
859
+ this.plumber.on('connection:drag', (connection: any) => {
860
+ this.dragFromNodeId = connection.data.nodeId;
860
861
  this.sourceId = connection.sourceId;
862
+ this.connectionSourceX = connection.sourceX;
863
+ this.connectionSourceY = connection.sourceY;
861
864
  this.originalConnectionTargetId = connection.target.id;
862
865
  });
863
866
 
864
867
  this.plumber.on('connection:abort', (info) => {
865
- // console.log('Connection aborted', info);
866
868
  this.makeConnection(info);
867
869
  });
868
870
 
@@ -898,6 +900,7 @@ export class Editor extends RapidElement {
898
900
  left: snapToGrid(this.connectionPlaceholder.position.left),
899
901
  top: snapToGrid(this.connectionPlaceholder.position.top)
900
902
  };
903
+ const isDragUp = !!this.connectionPlaceholder.dragUp;
901
904
 
902
905
  // Update the placeholder to the snapped position
903
906
  this.connectionPlaceholder.position = snappedPosition;
@@ -909,12 +912,14 @@ export class Editor extends RapidElement {
909
912
  position: snappedPosition
910
913
  };
911
914
 
912
- // Show the context menu just below the placeholder
915
+ // Show the context menu near the placeholder
913
916
  const canvas = this.querySelector('#canvas');
914
917
  if (canvas) {
915
918
  const canvasRect = canvas.getBoundingClientRect();
916
- const menuX = canvasRect.left + snappedPosition.left - 40; // center horizontally
917
- const menuY = canvasRect.top + snappedPosition.top + 80; // just below placeholder
919
+ const menuX = canvasRect.left + snappedPosition.left - 40;
920
+ const menuY = isDragUp
921
+ ? canvasRect.top + snappedPosition.top + 74 // just below placeholder bottom
922
+ : canvasRect.top + snappedPosition.top + 80; // just below placeholder
918
923
 
919
924
  const canvasMenu = this.querySelector(
920
925
  'temba-canvas-menu'
@@ -950,6 +955,8 @@ export class Editor extends RapidElement {
950
955
  // Clear connection state (but keep sourceId/dragFromNodeId if we have a pending connection)
951
956
  if (!this.pendingCanvasConnection) {
952
957
  this.sourceId = null;
958
+ this.connectionSourceX = null;
959
+ this.connectionSourceY = null;
953
960
  this.dragFromNodeId = null;
954
961
  }
955
962
  this.targetId = null;
@@ -1130,7 +1137,7 @@ export class Editor extends RapidElement {
1130
1137
  }
1131
1138
 
1132
1139
  this.activityTimer = window.setTimeout(() => {
1133
- // this.fetchActivityData();
1140
+ this.fetchActivityData();
1134
1141
  }, this.activityInterval);
1135
1142
  });
1136
1143
  }
@@ -1246,6 +1253,7 @@ export class Editor extends RapidElement {
1246
1253
  if (event.button !== 0) return;
1247
1254
 
1248
1255
  if (this.isReadOnly()) return;
1256
+ this.blurActiveContentEditable();
1249
1257
 
1250
1258
  const element = event.currentTarget as HTMLElement;
1251
1259
  // Only start dragging if clicking on the element itself, not on exits or other interactive elements
@@ -1315,8 +1323,22 @@ export class Editor extends RapidElement {
1315
1323
  this.handleCanvasMouseDown(event);
1316
1324
  }
1317
1325
 
1326
+ private blurActiveContentEditable(): void {
1327
+ let active: Element | null = document.activeElement;
1328
+ while (active?.shadowRoot?.activeElement) {
1329
+ active = active.shadowRoot.activeElement;
1330
+ }
1331
+ if (
1332
+ active instanceof HTMLElement &&
1333
+ active.getAttribute('contenteditable') === 'true'
1334
+ ) {
1335
+ active.blur();
1336
+ }
1337
+ }
1338
+
1318
1339
  private handleCanvasMouseDown(event: MouseEvent): void {
1319
1340
  if (this.isReadOnly()) return;
1341
+ this.blurActiveContentEditable();
1320
1342
 
1321
1343
  const target = event.target as HTMLElement;
1322
1344
  if (target.id === 'canvas' || target.id === 'grid') {
@@ -1570,86 +1592,71 @@ export class Editor extends RapidElement {
1570
1592
  if (!this.connectionPlaceholder || !this.connectionPlaceholder.visible)
1571
1593
  return '';
1572
1594
 
1573
- const { position } = this.connectionPlaceholder;
1595
+ const { position, dragUp } = this.connectionPlaceholder;
1574
1596
 
1575
1597
  // Render connection line when we have a pending connection (after drop)
1576
1598
  let svgPath = null;
1577
- if (this.sourceId && this.dragFromNodeId && this.pendingCanvasConnection) {
1578
- const sourceElement = document.getElementById(this.sourceId);
1579
- if (sourceElement) {
1580
- const sourceRect = sourceElement.getBoundingClientRect();
1581
- const canvas = this.querySelector('#canvas');
1582
- const canvasRect = canvas.getBoundingClientRect();
1583
-
1584
- // Source point (bottom center of exit)
1585
- const sourceX =
1586
- sourceRect.left + sourceRect.width / 2 - canvasRect.left;
1587
- const sourceY = sourceRect.bottom - canvasRect.top;
1588
-
1589
- // Target point (top center of placeholder)
1590
- const targetX = position.left + 100; // 100 is half the placeholder width (200px)
1591
- const targetY = position.top;
1592
-
1593
- // Use jsPlumb FlowchartConnector parameters: stub [20, 10], cornerRadius 5
1594
- const stubStart = 20;
1595
- const stubEnd = 10;
1596
- const cornerRadius = 5;
1597
-
1598
- // Calculate flowchart path with corners
1599
- const verticalStart = sourceY + stubStart;
1600
- const verticalEnd = targetY - stubEnd;
1601
- const midY = (verticalStart + verticalEnd) / 2;
1602
-
1603
- // Build path with rounded corners (flowchart style)
1604
- let pathData = `M ${sourceX} ${sourceY} L ${sourceX} ${verticalStart}`;
1605
-
1606
- if (sourceX !== targetX) {
1607
- // Horizontal segment needed
1608
- if (Math.abs(verticalEnd - verticalStart) > cornerRadius * 2) {
1609
- // Enough space for corners
1610
- pathData += ` L ${sourceX} ${midY - cornerRadius}`;
1611
- pathData += ` Q ${sourceX} ${midY}, ${
1612
- sourceX + (targetX > sourceX ? cornerRadius : -cornerRadius)
1613
- } ${midY}`;
1614
- pathData += ` L ${
1615
- targetX - (targetX > sourceX ? cornerRadius : -cornerRadius)
1616
- } ${midY}`;
1617
- pathData += ` Q ${targetX} ${midY}, ${targetX} ${
1618
- midY + cornerRadius
1619
- }`;
1620
- pathData += ` L ${targetX} ${verticalEnd}`;
1621
- } else {
1622
- // Direct horizontal transition
1623
- pathData += ` L ${targetX} ${verticalStart}`;
1624
- pathData += ` L ${targetX} ${verticalEnd}`;
1625
- }
1626
- } else {
1627
- // Straight vertical line
1628
- pathData += ` L ${targetX} ${verticalEnd}`;
1629
- }
1630
-
1631
- pathData += ` L ${targetX} ${targetY}`;
1599
+ if (
1600
+ this.sourceId &&
1601
+ this.dragFromNodeId &&
1602
+ this.pendingCanvasConnection &&
1603
+ this.connectionSourceX != null &&
1604
+ this.connectionSourceY != null
1605
+ ) {
1606
+ const sourceX = this.connectionSourceX;
1607
+ const sourceY = this.connectionSourceY;
1608
+ const targetX = position.left + 100;
1609
+ // When dragging up, connect to the placeholder bottom; otherwise to the top
1610
+ const targetY = dragUp ? position.top + 64 : position.top;
1611
+
1612
+ const routeFace: 'top' | 'left' | 'right' = dragUp
1613
+ ? targetX < sourceX
1614
+ ? 'left'
1615
+ : 'right'
1616
+ : 'top';
1617
+
1618
+ const pathData = calculateFlowchartPath(
1619
+ sourceX,
1620
+ sourceY,
1621
+ targetX,
1622
+ targetY,
1623
+ 20,
1624
+ dragUp ? 0 : 10,
1625
+ 5,
1626
+ routeFace
1627
+ );
1632
1628
 
1633
- svgPath = html`
1634
- <svg
1635
- style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;"
1636
- >
1637
- <path
1638
- d="${pathData}"
1639
- fill="none"
1640
- stroke="var(--color-connectors, #ccc)"
1641
- stroke-width="3"
1642
- class="plumb-connector"
1643
- />
1644
- <polygon
1645
- points="${targetX},${targetY} ${targetX - 6.5},${targetY -
1646
- 13} ${targetX + 6.5},${targetY - 13}"
1647
- fill="var(--color-connectors, #ccc)"
1648
- class="plumb-arrow"
1649
- />
1650
- </svg>
1651
- `;
1629
+ const aw = ARROW_HALF_WIDTH;
1630
+ const al = ARROW_LENGTH;
1631
+ let arrowPoints: string;
1632
+ if (dragUp) {
1633
+ // Arrow tip pointing up, base at placeholder bottom
1634
+ arrowPoints = `${targetX},${targetY - al} ${targetX - aw},${targetY} ${
1635
+ targetX + aw
1636
+ },${targetY}`;
1637
+ } else {
1638
+ // Arrow pointing down into top of placeholder
1639
+ arrowPoints = `${targetX},${targetY} ${targetX - aw},${targetY - al} ${
1640
+ targetX + aw
1641
+ },${targetY - al}`;
1652
1642
  }
1643
+
1644
+ svgPath = html`
1645
+ <svg
1646
+ style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;"
1647
+ >
1648
+ <path
1649
+ d="${pathData}"
1650
+ fill="none"
1651
+ stroke="var(--color-connectors, #ccc)"
1652
+ stroke-width="3"
1653
+ />
1654
+ <polygon
1655
+ points="${arrowPoints}"
1656
+ fill="var(--color-connectors, #ccc)"
1657
+ />
1658
+ </svg>
1659
+ `;
1653
1660
  }
1654
1661
 
1655
1662
  return html`${svgPath}
@@ -1673,22 +1680,13 @@ export class Editor extends RapidElement {
1673
1680
 
1674
1681
  /**
1675
1682
  * Checks for node collisions and reflows nodes as needed.
1676
- * Nodes are only moved downward to resolve collisions.
1677
- *
1678
- * @param movedNodeUuids - UUIDs of nodes that were just moved/dropped
1679
- * @param droppedNodeUuid - UUID of the specific node that was dropped (if applicable)
1680
- * @param dropTargetBounds - Bounds of the node that was dropped onto (if applicable)
1683
+ * Sacred nodes (just moved/dropped) keep their positions while
1684
+ * other nodes are moved in the least-disruptive direction.
1681
1685
  */
1682
- private checkCollisionsAndReflow(
1683
- movedNodeUuids: string[],
1684
- droppedNodeUuid: string | null = null,
1685
- dropTargetBounds: NodeBounds | null = null
1686
- ): void {
1686
+ private checkCollisionsAndReflow(sacredNodeUuids: string[]): void {
1687
1687
  if (!this.definition) return;
1688
1688
 
1689
- // Get all node bounds (only for actual nodes, not stickies)
1690
1689
  const allBounds: NodeBounds[] = [];
1691
-
1692
1690
  for (const node of this.definition.nodes) {
1693
1691
  const nodeUI = this.definition._ui?.nodes[node.uuid];
1694
1692
  if (!nodeUI?.position) continue;
@@ -1699,45 +1697,17 @@ export class Editor extends RapidElement {
1699
1697
  }
1700
1698
  }
1701
1699
 
1702
- // Check if we need to determine midpoint priority for a dropped node
1703
- let targetHasPriority = false;
1704
- if (droppedNodeUuid && dropTargetBounds) {
1705
- const droppedBounds = allBounds.find((b) => b.uuid === droppedNodeUuid);
1706
- if (droppedBounds) {
1707
- // Check if the bottom of the dropped node is below the midpoint of the target
1708
- // If bottom is above midpoint, dropped node gets preference (targetHasPriority = false)
1709
- // If bottom is below midpoint, target gets preference (targetHasPriority = true)
1710
- const droppedBottom = droppedBounds.bottom;
1711
- const targetMidpoint =
1712
- dropTargetBounds.top + dropTargetBounds.height / 2;
1713
- targetHasPriority = droppedBottom > targetMidpoint;
1714
- }
1715
- }
1716
-
1717
- // Calculate reflow positions for each moved node
1718
- const allReflowPositions: { [uuid: string]: FlowPosition } = {};
1719
-
1720
- for (const movedUuid of movedNodeUuids) {
1721
- const movedBounds = allBounds.find((b) => b.uuid === movedUuid);
1722
- if (!movedBounds) continue;
1723
-
1724
- // Calculate reflow for this moved node
1725
- const reflowPositions = calculateReflowPositions(
1726
- movedUuid,
1727
- movedBounds,
1728
- allBounds,
1729
- droppedNodeUuid === movedUuid ? targetHasPriority : false
1730
- );
1700
+ const reflowPositions = calculateReflowPositions(
1701
+ sacredNodeUuids,
1702
+ allBounds
1703
+ );
1731
1704
 
1732
- // Merge into all reflow positions
1705
+ if (reflowPositions.size > 0) {
1706
+ const positions: { [uuid: string]: FlowPosition } = {};
1733
1707
  for (const [uuid, position] of reflowPositions.entries()) {
1734
- allReflowPositions[uuid] = position;
1708
+ positions[uuid] = position;
1735
1709
  }
1736
- }
1737
-
1738
- // If there are positions to update, apply them
1739
- if (Object.keys(allReflowPositions).length > 0) {
1740
- getStore().getState().updateCanvasPositions(allReflowPositions);
1710
+ getStore().getState().updateCanvasPositions(positions);
1741
1711
  }
1742
1712
  }
1743
1713
 
@@ -1780,23 +1750,41 @@ export class Editor extends RapidElement {
1780
1750
  this.isValidTarget = true;
1781
1751
 
1782
1752
  // Show connection placeholder when over empty canvas
1783
- // Calculate position: horizontally centered at mouse, vertically just below mouse
1784
1753
  const canvas = this.querySelector('#canvas');
1785
1754
  if (canvas) {
1786
1755
  const canvasRect = canvas.getBoundingClientRect();
1787
1756
  const relativeX = event.clientX - canvasRect.left;
1788
1757
  const relativeY = event.clientY - canvasRect.top;
1789
1758
 
1790
- // offset the placeholder so it's centered horizontally and just below the mouse
1791
- const placeholderWidth = 200; // approximate node width
1792
- const placeholderOffset = 20; // distance below mouse cursor
1759
+ const placeholderWidth = 200;
1760
+ const placeholderHeight = 64;
1761
+ const arrowLength = ARROW_LENGTH;
1762
+ const cursorGap = CURSOR_GAP;
1763
+
1764
+ // Determine if cursor is above the source exit using stored sourceY
1765
+ const dragUp =
1766
+ this.connectionSourceY != null
1767
+ ? relativeY < this.connectionSourceY
1768
+ : false;
1769
+
1770
+ let top: number;
1771
+ if (dragUp) {
1772
+ // Arrow points up: tip at cy + cursorGap.
1773
+ // Placeholder bottom should sit just above the arrow tip.
1774
+ top = relativeY + cursorGap - placeholderHeight;
1775
+ } else {
1776
+ // Arrow points down: tip at cy - cursorGap + arrowLength.
1777
+ // Placeholder top sits just below the arrow tip.
1778
+ top = relativeY - cursorGap + arrowLength;
1779
+ }
1793
1780
 
1794
1781
  this.connectionPlaceholder = {
1795
1782
  position: {
1796
1783
  left: relativeX - placeholderWidth / 2,
1797
- top: relativeY + placeholderOffset
1784
+ top
1798
1785
  },
1799
- visible: true
1786
+ visible: true,
1787
+ dragUp
1800
1788
  };
1801
1789
  }
1802
1790
  }
@@ -1924,49 +1912,7 @@ export class Editor extends RapidElement {
1924
1912
  if (nodeUuids.length > 0) {
1925
1913
  // Allow DOM to update before checking collisions
1926
1914
  setTimeout(() => {
1927
- // If only one node was moved, detect which node it might have been dropped onto
1928
- let droppedNodeUuid: string | null = null;
1929
- let dropTargetBounds: NodeBounds | null = null;
1930
-
1931
- if (nodeUuids.length === 1) {
1932
- droppedNodeUuid = nodeUuids[0];
1933
- const droppedNodeUI = this.definition._ui?.nodes[droppedNodeUuid];
1934
-
1935
- if (droppedNodeUI?.position) {
1936
- const droppedBounds = getNodeBounds(
1937
- droppedNodeUuid,
1938
- droppedNodeUI.position
1939
- );
1940
-
1941
- if (droppedBounds) {
1942
- // Find which node (if any) the dropped node overlaps with
1943
- for (const node of this.definition.nodes) {
1944
- if (node.uuid === droppedNodeUuid) continue;
1945
-
1946
- const nodeUI = this.definition._ui?.nodes[node.uuid];
1947
- if (!nodeUI?.position) continue;
1948
-
1949
- const targetBounds = getNodeBounds(
1950
- node.uuid,
1951
- nodeUI.position
1952
- );
1953
- if (
1954
- targetBounds &&
1955
- nodesOverlap(droppedBounds, targetBounds)
1956
- ) {
1957
- dropTargetBounds = targetBounds;
1958
- break; // Use the first overlapping node
1959
- }
1960
- }
1961
- }
1962
- }
1963
- }
1964
-
1965
- this.checkCollisionsAndReflow(
1966
- nodeUuids,
1967
- droppedNodeUuid,
1968
- dropTargetBounds
1969
- );
1915
+ this.checkCollisionsAndReflow(nodeUuids);
1970
1916
  }, 0);
1971
1917
  } else {
1972
1918
  // No nodes moved, just repaint connections
@@ -2091,6 +2037,8 @@ export class Editor extends RapidElement {
2091
2037
  this.pendingCanvasConnection = null;
2092
2038
  this.connectionPlaceholder = null;
2093
2039
  this.sourceId = null;
2040
+ this.connectionSourceX = null;
2041
+ this.connectionSourceY = null;
2094
2042
  this.dragFromNodeId = null;
2095
2043
  } else {
2096
2044
  // Show node type selector
@@ -2126,6 +2074,8 @@ export class Editor extends RapidElement {
2126
2074
  this.pendingCanvasConnection = null;
2127
2075
  this.connectionPlaceholder = null;
2128
2076
  this.sourceId = null;
2077
+ this.connectionSourceX = null;
2078
+ this.connectionSourceY = null;
2129
2079
  this.dragFromNodeId = null;
2130
2080
  this.originalConnectionTargetId = null;
2131
2081
  }
@@ -2259,6 +2209,10 @@ export class Editor extends RapidElement {
2259
2209
  private handleActionEditRequested(event: CustomEvent): void {
2260
2210
  // For action editing, we set the action and find the corresponding node
2261
2211
  this.editingAction = event.detail.action;
2212
+ this.dialogOrigin =
2213
+ event.detail.originX != null
2214
+ ? { x: event.detail.originX, y: event.detail.originY }
2215
+ : null;
2262
2216
 
2263
2217
  // Find the node that contains this action
2264
2218
  const nodeUuid = event.detail.nodeUuid;
@@ -2304,6 +2258,10 @@ export class Editor extends RapidElement {
2304
2258
  private handleNodeEditRequested(event: CustomEvent): void {
2305
2259
  this.editingNode = event.detail.node;
2306
2260
  this.editingNodeUI = event.detail.nodeUI;
2261
+ this.dialogOrigin =
2262
+ event.detail.originX != null
2263
+ ? { x: event.detail.originX, y: event.detail.originY }
2264
+ : null;
2307
2265
  }
2308
2266
 
2309
2267
  private handleNodeDeleted(event: CustomEvent): void {
@@ -2362,6 +2320,8 @@ export class Editor extends RapidElement {
2362
2320
  this.pendingCanvasConnection = null;
2363
2321
  this.connectionPlaceholder = null;
2364
2322
  this.sourceId = null;
2323
+ this.connectionSourceX = null;
2324
+ this.connectionSourceY = null;
2365
2325
  this.dragFromNodeId = null;
2366
2326
  }
2367
2327
 
@@ -2391,6 +2351,7 @@ export class Editor extends RapidElement {
2391
2351
  this.editingNode = null;
2392
2352
  this.editingNodeUI = null;
2393
2353
  this.editingAction = null;
2354
+ this.dialogOrigin = null;
2394
2355
  }
2395
2356
 
2396
2357
  private handleActionEditCanceled(): void {
@@ -2431,6 +2392,8 @@ export class Editor extends RapidElement {
2431
2392
  this.pendingCanvasConnection = null;
2432
2393
  this.connectionPlaceholder = null;
2433
2394
  this.sourceId = null;
2395
+ this.connectionSourceX = null;
2396
+ this.connectionSourceY = null;
2434
2397
  this.dragFromNodeId = null;
2435
2398
  }
2436
2399
 
@@ -3043,6 +3006,7 @@ export class Editor extends RapidElement {
3043
3006
 
3044
3007
  this.localizationWindowHidden = false;
3045
3008
  this.revisionsWindowHidden = true;
3009
+ this.issuesWindowHidden = true;
3046
3010
 
3047
3011
  const alreadySelected = languages.some(
3048
3012
  (lang) => lang.code === this.languageCode
@@ -3305,11 +3269,47 @@ export class Editor extends RapidElement {
3305
3269
  this.autoTranslating = false;
3306
3270
  }
3307
3271
 
3272
+ private handleIssuesTabClick(): void {
3273
+ this.issuesWindowHidden = false;
3274
+ this.revisionsWindowHidden = true;
3275
+ this.localizationWindowHidden = true;
3276
+ }
3277
+
3278
+ private handleIssuesWindowClosed(): void {
3279
+ this.issuesWindowHidden = true;
3280
+ }
3281
+
3282
+ private handleIssueItemClick(issue: FlowIssue): void {
3283
+ const issuesWindow = document.getElementById(
3284
+ 'issues-window'
3285
+ ) as FloatingWindow;
3286
+ issuesWindow?.handleClose();
3287
+ this.issuesWindowHidden = true;
3288
+
3289
+ this.focusNode(issue.node_uuid);
3290
+
3291
+ const node = this.definition.nodes.find((n) => n.uuid === issue.node_uuid);
3292
+ if (!node) return;
3293
+
3294
+ if (issue.action_uuid) {
3295
+ const action = node.actions?.find((a) => a.uuid === issue.action_uuid);
3296
+ if (action) {
3297
+ this.editingAction = action;
3298
+ this.editingNode = node;
3299
+ this.editingNodeUI = this.definition._ui.nodes[issue.node_uuid];
3300
+ }
3301
+ } else {
3302
+ this.editingNode = node;
3303
+ this.editingNodeUI = this.definition._ui.nodes[issue.node_uuid];
3304
+ }
3305
+ }
3306
+
3308
3307
  private handleRevisionsTabClick(): void {
3309
3308
  if (this.revisionsWindowHidden) {
3310
3309
  this.fetchRevisions();
3311
3310
  this.revisionsWindowHidden = false;
3312
- this.localizationWindowHidden = true; // Close other window
3311
+ this.issuesWindowHidden = true;
3312
+ this.localizationWindowHidden = true;
3313
3313
  }
3314
3314
  }
3315
3315
 
@@ -3423,6 +3423,51 @@ export class Editor extends RapidElement {
3423
3423
  getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
3424
3424
  }
3425
3425
 
3426
+ private renderIssuesTab(): TemplateResult | string {
3427
+ if (!this.flowIssues?.length) return '';
3428
+ return html`
3429
+ <temba-floating-tab
3430
+ id="issues-tab"
3431
+ icon="alert_warning"
3432
+ label="Flow Issues"
3433
+ color="tomato"
3434
+ order="1"
3435
+ .hidden=${!this.issuesWindowHidden}
3436
+ @temba-button-clicked=${this.handleIssuesTabClick}
3437
+ ></temba-floating-tab>
3438
+ `;
3439
+ }
3440
+
3441
+ private renderIssuesWindow(): TemplateResult | string {
3442
+ if (!this.flowIssues?.length) return '';
3443
+ return html`
3444
+ <temba-floating-window
3445
+ id="issues-window"
3446
+ header="Flow Issues"
3447
+ .width=${360}
3448
+ .maxHeight=${600}
3449
+ .top=${75}
3450
+ color="tomato"
3451
+ .hidden=${this.issuesWindowHidden}
3452
+ @temba-dialog-hidden=${this.handleIssuesWindowClosed}
3453
+ >
3454
+ <div style="display:flex; flex-direction:column; gap:2px;">
3455
+ ${this.flowIssues.map(
3456
+ (issue) => html`
3457
+ <div
3458
+ class="issue-list-item"
3459
+ @click=${() => this.handleIssueItemClick(issue)}
3460
+ >
3461
+ <temba-icon name="alert_warning" size="1.2"></temba-icon>
3462
+ <span>${formatIssueMessage(issue)}</span>
3463
+ </div>
3464
+ `
3465
+ )}
3466
+ </div>
3467
+ </temba-floating-window>
3468
+ `;
3469
+ }
3470
+
3426
3471
  private renderRevisionsTab(): TemplateResult | string {
3427
3472
  return html`
3428
3473
  <temba-floating-tab
@@ -3430,7 +3475,7 @@ export class Editor extends RapidElement {
3430
3475
  icon="revisions"
3431
3476
  label="Revisions"
3432
3477
  color="rgb(142, 94, 167)"
3433
- top="105"
3478
+ order="2"
3434
3479
  .hidden=${!this.revisionsWindowHidden && this.localizationWindowHidden}
3435
3480
  @temba-button-clicked=${this.handleRevisionsTabClick}
3436
3481
  ></temba-floating-tab>
@@ -3728,7 +3773,7 @@ export class Editor extends RapidElement {
3728
3773
  icon="language"
3729
3774
  label="Translate Flow"
3730
3775
  color="#6b7280"
3731
- top="180"
3776
+ order="3"
3732
3777
  .hidden=${!this.localizationWindowHidden}
3733
3778
  @temba-button-clicked=${this.handleLocalizationTabClick}
3734
3779
  ></temba-floating-tab>
@@ -3786,8 +3831,9 @@ export class Editor extends RapidElement {
3786
3831
 
3787
3832
  const stickies = this.definition?._ui?.stickies || {};
3788
3833
 
3789
- return html`${style} ${this.renderRevisionsWindow()}
3790
- ${this.renderLocalizationWindow()} ${this.renderAutoTranslateDialog()}
3834
+ return html`${style} ${this.renderIssuesWindow()}
3835
+ ${this.renderRevisionsWindow()} ${this.renderLocalizationWindow()}
3836
+ ${this.renderAutoTranslateDialog()}
3791
3837
  <div id="editor">
3792
3838
  <div
3793
3839
  id="grid"
@@ -3877,6 +3923,7 @@ export class Editor extends RapidElement {
3877
3923
  .node=${this.editingNode}
3878
3924
  .nodeUI=${this.editingNodeUI}
3879
3925
  .action=${this.editingAction}
3926
+ .dialogOrigin=${this.dialogOrigin}
3880
3927
  @temba-node-saved=${(e: CustomEvent) =>
3881
3928
  this.handleNodeSaved(e.detail.node, e.detail.uiConfig)}
3882
3929
  @temba-action-saved=${(e: CustomEvent) =>
@@ -3892,6 +3939,7 @@ export class Editor extends RapidElement {
3892
3939
  .features=${this.features}
3893
3940
  ></temba-node-type-selector>`
3894
3941
  : ''}
3895
- ${this.renderRevisionsTab()} ${this.renderLocalizationTab()} `;
3942
+ ${this.renderIssuesTab()} ${this.renderRevisionsTab()}
3943
+ ${this.renderLocalizationTab()} `;
3896
3944
  }
3897
3945
  }