@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
@@ -8,15 +8,11 @@ import { RapidElement } from '../RapidElement';
8
8
  import { repeat } from 'lit-html/directives/repeat.js';
9
9
  import { CustomEventType } from '../interfaces';
10
10
  import { generateUUID, postJSON, fetchResults, getClasses } from '../utils';
11
+ import { formatIssueMessage, getNodeBounds, calculateReflowPositions, snapToGrid } from './utils';
11
12
  import { ACTION_CONFIG, NODE_CONFIG } from './config';
12
13
  import { ACTION_GROUP_METADATA } from './types';
13
- import { Plumber } from './Plumber';
14
+ import { Plumber, calculateFlowchartPath, ARROW_LENGTH, ARROW_HALF_WIDTH, CURSOR_GAP } from './Plumber';
14
15
  import { CanvasNode } from './CanvasNode';
15
- import { getNodeBounds, calculateReflowPositions, nodesOverlap } from './utils';
16
- export function snapToGrid(value) {
17
- const snapped = Math.round(value / 20) * 20;
18
- return Math.max(snapped, 0);
19
- }
20
16
  export function findNodeForExit(definition, exitUuid) {
21
17
  for (const node of definition.nodes) {
22
18
  const exit = node.exits.find((e) => e.uuid === exitUuid);
@@ -34,7 +30,7 @@ const AUTO_TRANSLATE_MODELS_ENDPOINT = '/api/internal/llms.json';
34
30
  const DROP_PREVIEW_OFFSET_X = 20;
35
31
  const DROP_PREVIEW_OFFSET_Y = 20;
36
32
  export class Editor extends RapidElement {
37
- // unfortunately, jsplumb requires that we be in light DOM
33
+ // connection SVGs are appended directly to the canvas, so we need light DOM
38
34
  createRenderRoot() {
39
35
  return this;
40
36
  }
@@ -78,6 +74,10 @@ export class Editor extends RapidElement {
78
74
  -webkit-font-smoothing: antialiased;
79
75
  }
80
76
 
77
+ temba-floating-tab {
78
+ --floating-tab-right: 15px;
79
+ }
80
+
81
81
  #grid {
82
82
  position: relative;
83
83
  background-color: #f9f9f9;
@@ -128,100 +128,56 @@ export class Editor extends RapidElement {
128
128
  }
129
129
 
130
130
  #grid.viewing-revision temba-flow-node,
131
- #grid.viewing-revision svg.jtk-connector,
132
- #grid.viewing-revision .activity-overlay {
131
+ #grid.viewing-revision svg.plumb-connector {
133
132
  opacity: 0.5;
134
133
  }
135
134
 
136
- body .jtk-endpoint {
137
- width: initial;
138
- height: initial;
139
- }
140
-
141
- .jtk-endpoint {
142
- z-index: 600;
143
- opacity: 0;
144
- }
145
-
146
- .plumb-source {
147
- z-index: 600;
148
- cursor: pointer;
149
- opacity: 0;
150
- }
151
-
152
- .plumb-source.connected {
153
- border-radius: 50%;
154
- pointer-events: none;
135
+ svg.plumb-connector {
136
+ z-index: 10;
155
137
  }
156
138
 
157
- .plumb-source circle {
158
- fill: purple;
139
+ svg.plumb-connector path {
140
+ stroke: var(--color-connectors);
141
+ stroke-width: 3px;
159
142
  }
160
143
 
161
- .plumb-target {
162
- z-index: 600;
163
- opacity: 0;
164
- cursor: pointer;
165
- fill: transparent;
144
+ svg.plumb-connector .plumb-arrow {
145
+ fill: var(--color-connectors);
146
+ stroke: none;
166
147
  }
167
148
 
168
- body svg.jtk-connector.plumb-connector path {
169
- stroke: var(--color-connectors) !important;
170
- stroke-width: 3px;
149
+ svg.plumb-connector.hover path {
150
+ stroke: var(--color-success);
171
151
  }
172
152
 
173
- body .plumb-connector {
174
- z-index: 10 !important;
153
+ svg.plumb-connector.hover .plumb-arrow {
154
+ fill: var(--color-success);
175
155
  }
176
156
 
177
- body .plumb-connector .plumb-arrow {
178
- fill: var(--color-connectors);
157
+ #canvas.read-only-connections svg.plumb-connector.hover path {
179
158
  stroke: var(--color-connectors);
180
- stroke-width: 0px !important;
181
- margin-top: 6px;
182
- z-index: 10;
183
159
  }
184
160
 
185
- body svg.jtk-connector.jtk-hover path {
186
- stroke: var(--color-success) !important;
187
- stroke-width: 3px;
188
- }
189
-
190
- body #canvas.read-only-connections svg.jtk-connector.jtk-hover path {
191
- stroke: var(--color-connectors) !important;
161
+ #canvas.read-only-connections svg.plumb-connector.hover .plumb-arrow {
162
+ fill: var(--color-connectors);
192
163
  }
193
164
 
194
- body .plumb-connector.jtk-hover .plumb-arrow {
195
- fill: var(--color-success) !important;
196
- stroke-width: 0px;
197
- z-index: 10;
165
+ #canvas.read-only-connections svg.plumb-connector,
166
+ #canvas.read-only-connections svg.plumb-connector * {
167
+ pointer-events: none !important;
168
+ cursor: default !important;
198
169
  }
199
170
 
200
- body
201
- #canvas.read-only-connections
202
- .plumb-connector.jtk-hover
203
- .plumb-arrow {
204
- fill: var(--color-connectors) !important;
205
- ponter-events: none;
171
+ svg.plumb-connector.removing path {
172
+ stroke: var(--color-error);
206
173
  }
207
174
 
208
- body #canvas.read-only-connections svg {
209
- pointer-events: none;
175
+ svg.plumb-connector.removing .plumb-arrow {
176
+ fill: var(--color-error);
210
177
  }
211
178
 
212
- /* Activity overlays on connections */
213
- .jtk-overlay.activity-overlay {
214
- background: #f3f3f3;
215
- border: 1px solid #d9d9d9;
216
- color: #333;
217
- border-radius: 4px;
218
- padding: 2px 4px;
219
- font-size: 10px;
220
- font-weight: 600;
221
- line-height: 0.9;
222
- cursor: pointer;
223
- z-index: 500;
224
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
179
+ svg.plumb-connector.dragging {
180
+ z-index: 99999;
225
181
  }
226
182
 
227
183
  /* Active contact count on nodes */
@@ -243,6 +199,30 @@ export class Editor extends RapidElement {
243
199
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
244
200
  }
245
201
 
202
+ /* Activity overlay badges on connection exit stubs */
203
+ .activity-overlay {
204
+ position: absolute;
205
+ background: #f3f3f3;
206
+ border: 1px solid #d9d9d9;
207
+ color: #333;
208
+ border-radius: 4px;
209
+ padding: 2px 4px;
210
+ font-size: 10px;
211
+ font-weight: 600;
212
+ line-height: 0.9;
213
+ cursor: pointer;
214
+ z-index: 10;
215
+ pointer-events: auto;
216
+ white-space: nowrap;
217
+ user-select: none;
218
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
219
+ }
220
+
221
+ #grid.viewing-revision .activity-overlay {
222
+ opacity: 0.5;
223
+ pointer-events: none;
224
+ }
225
+
246
226
  /* Recent contacts popup */
247
227
  @keyframes popupBounceIn {
248
228
  0% {
@@ -312,7 +292,6 @@ export class Editor extends RapidElement {
312
292
 
313
293
  .recent-contacts-popup .contact-name:hover {
314
294
  text-decoration: underline;
315
- color: var(--color-link-primary, #1d4ed8);
316
295
  }
317
296
 
318
297
  .recent-contacts-popup .contact-operand {
@@ -328,17 +307,6 @@ export class Editor extends RapidElement {
328
307
  color: #999;
329
308
  }
330
309
 
331
- /* Connection dragging feedback */
332
- body svg.jtk-connector.jtk-dragging {
333
- z-index: 99999 !important;
334
- }
335
-
336
- .katavorio-drag-no-select svg.jtk-connector path,
337
- .katavorio-drag-no-select svg.jtk-endpoint path {
338
- pointer-events: none !important;
339
- border: 1px solid purple;
340
- }
341
-
342
310
  /* Connection target feedback */
343
311
  temba-flow-node.connection-target-valid {
344
312
  outline: 3px solid var(--color-success, #22c55e) !important;
@@ -368,10 +336,6 @@ export class Editor extends RapidElement {
368
336
  border-radius: var(--curvature);
369
337
  }
370
338
 
371
- .jtk-floating-endpoint {
372
- pointer-events: none;
373
- }
374
-
375
339
  .localization-window-content {
376
340
  display: flex;
377
341
  flex-direction: column;
@@ -562,6 +526,26 @@ export class Editor extends RapidElement {
562
526
  color: #9ca3af;
563
527
  white-space: nowrap;
564
528
  }
529
+
530
+ .issue-list-item {
531
+ display: flex;
532
+ align-items: center;
533
+ gap: 8px;
534
+ padding: 8px;
535
+ border-radius: 4px;
536
+ cursor: pointer;
537
+ font-size: 13px;
538
+ color: #333;
539
+ }
540
+
541
+ .issue-list-item:hover {
542
+ background: #fff5f5;
543
+ }
544
+
545
+ .issue-list-item temba-icon {
546
+ color: tomato;
547
+ flex-shrink: 0;
548
+ }
565
549
  `;
566
550
  }
567
551
  constructor() {
@@ -587,6 +571,10 @@ export class Editor extends RapidElement {
587
571
  this.dragFromNodeId = null;
588
572
  this.originalConnectionTargetId = null;
589
573
  this.isValidTarget = true;
574
+ // Canvas-relative source exit position (set at drag start)
575
+ this.connectionSourceX = null;
576
+ this.connectionSourceY = null;
577
+ this.issuesWindowHidden = true;
590
578
  this.localizationWindowHidden = true;
591
579
  this.translationFilters = {
592
580
  categories: false
@@ -606,6 +594,7 @@ export class Editor extends RapidElement {
606
594
  this.editingNode = null;
607
595
  this.editingNodeUI = null;
608
596
  this.editingAction = null;
597
+ this.dialogOrigin = null;
609
598
  this.isCreatingNewNode = false;
610
599
  this.pendingNodePosition = null;
611
600
  // Canvas drop state for dragging actions to canvas
@@ -635,15 +624,13 @@ export class Editor extends RapidElement {
635
624
  getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
636
625
  }
637
626
  this.plumber.on('connection:drag', (connection) => {
638
- // console.log('connection:drag', connection);
639
- this.dragFromNodeId =
640
- connection.data.nodeId ||
641
- document.getElementById(connection.sourceId).closest('.node').id;
627
+ this.dragFromNodeId = connection.data.nodeId;
642
628
  this.sourceId = connection.sourceId;
629
+ this.connectionSourceX = connection.sourceX;
630
+ this.connectionSourceY = connection.sourceY;
643
631
  this.originalConnectionTargetId = connection.target.id;
644
632
  });
645
633
  this.plumber.on('connection:abort', (info) => {
646
- // console.log('Connection aborted', info);
647
634
  this.makeConnection(info);
648
635
  });
649
636
  this.plumber.on('connection:detach', (info) => {
@@ -672,6 +659,7 @@ export class Editor extends RapidElement {
672
659
  left: snapToGrid(this.connectionPlaceholder.position.left),
673
660
  top: snapToGrid(this.connectionPlaceholder.position.top)
674
661
  };
662
+ const isDragUp = !!this.connectionPlaceholder.dragUp;
675
663
  // Update the placeholder to the snapped position
676
664
  this.connectionPlaceholder.position = snappedPosition;
677
665
  // Store the pending connection info
@@ -680,12 +668,14 @@ export class Editor extends RapidElement {
680
668
  exitId: this.sourceId,
681
669
  position: snappedPosition
682
670
  };
683
- // Show the context menu just below the placeholder
671
+ // Show the context menu near the placeholder
684
672
  const canvas = this.querySelector('#canvas');
685
673
  if (canvas) {
686
674
  const canvasRect = canvas.getBoundingClientRect();
687
- const menuX = canvasRect.left + snappedPosition.left - 40; // center horizontally
688
- const menuY = canvasRect.top + snappedPosition.top + 80; // just below placeholder
675
+ const menuX = canvasRect.left + snappedPosition.left - 40;
676
+ const menuY = isDragUp
677
+ ? canvasRect.top + snappedPosition.top + 74 // just below placeholder bottom
678
+ : canvasRect.top + snappedPosition.top + 80; // just below placeholder
689
679
  const canvasMenu = this.querySelector('temba-canvas-menu');
690
680
  if (canvasMenu) {
691
681
  canvasMenu.show(menuX, menuY, {
@@ -706,6 +696,8 @@ export class Editor extends RapidElement {
706
696
  // Clear connection state (but keep sourceId/dragFromNodeId if we have a pending connection)
707
697
  if (!this.pendingCanvasConnection) {
708
698
  this.sourceId = null;
699
+ this.connectionSourceX = null;
700
+ this.connectionSourceY = null;
709
701
  this.dragFromNodeId = null;
710
702
  }
711
703
  this.targetId = null;
@@ -862,7 +854,7 @@ export class Editor extends RapidElement {
862
854
  clearTimeout(this.activityTimer);
863
855
  }
864
856
  this.activityTimer = window.setTimeout(() => {
865
- // this.fetchActivityData();
857
+ this.fetchActivityData();
866
858
  }, this.activityInterval);
867
859
  });
868
860
  }
@@ -949,6 +941,7 @@ export class Editor extends RapidElement {
949
941
  return;
950
942
  if (this.isReadOnly())
951
943
  return;
944
+ this.blurActiveContentEditable();
952
945
  const element = event.currentTarget;
953
946
  // Only start dragging if clicking on the element itself, not on exits or other interactive elements
954
947
  const target = event.target;
@@ -1008,10 +1001,22 @@ export class Editor extends RapidElement {
1008
1001
  // We clicked on empty canvas space, start selection
1009
1002
  this.handleCanvasMouseDown(event);
1010
1003
  }
1004
+ blurActiveContentEditable() {
1005
+ var _b;
1006
+ let active = document.activeElement;
1007
+ while ((_b = active === null || active === void 0 ? void 0 : active.shadowRoot) === null || _b === void 0 ? void 0 : _b.activeElement) {
1008
+ active = active.shadowRoot.activeElement;
1009
+ }
1010
+ if (active instanceof HTMLElement &&
1011
+ active.getAttribute('contenteditable') === 'true') {
1012
+ active.blur();
1013
+ }
1014
+ }
1011
1015
  handleCanvasMouseDown(event) {
1012
1016
  var _b;
1013
1017
  if (this.isReadOnly())
1014
1018
  return;
1019
+ this.blurActiveContentEditable();
1015
1020
  const target = event.target;
1016
1021
  if (target.id === 'canvas' || target.id === 'grid') {
1017
1022
  // Ignore clicks on exits
@@ -1219,72 +1224,52 @@ export class Editor extends RapidElement {
1219
1224
  renderConnectionPlaceholder() {
1220
1225
  if (!this.connectionPlaceholder || !this.connectionPlaceholder.visible)
1221
1226
  return '';
1222
- const { position } = this.connectionPlaceholder;
1227
+ const { position, dragUp } = this.connectionPlaceholder;
1223
1228
  // Render connection line when we have a pending connection (after drop)
1224
1229
  let svgPath = null;
1225
- if (this.sourceId && this.dragFromNodeId && this.pendingCanvasConnection) {
1226
- const sourceElement = document.getElementById(this.sourceId);
1227
- if (sourceElement) {
1228
- const sourceRect = sourceElement.getBoundingClientRect();
1229
- const canvas = this.querySelector('#canvas');
1230
- const canvasRect = canvas.getBoundingClientRect();
1231
- // Source point (bottom center of exit)
1232
- const sourceX = sourceRect.left + sourceRect.width / 2 - canvasRect.left;
1233
- const sourceY = sourceRect.bottom - canvasRect.top;
1234
- // Target point (top center of placeholder)
1235
- const targetX = position.left + 100; // 100 is half the placeholder width (200px)
1236
- const targetY = position.top;
1237
- // Use jsPlumb FlowchartConnector parameters: stub [20, 10], cornerRadius 5
1238
- const stubStart = 20;
1239
- const stubEnd = 10;
1240
- const cornerRadius = 5;
1241
- // Calculate flowchart path with corners
1242
- const verticalStart = sourceY + stubStart;
1243
- const verticalEnd = targetY - stubEnd;
1244
- const midY = (verticalStart + verticalEnd) / 2;
1245
- // Build path with rounded corners (flowchart style)
1246
- let pathData = `M ${sourceX} ${sourceY} L ${sourceX} ${verticalStart}`;
1247
- if (sourceX !== targetX) {
1248
- // Horizontal segment needed
1249
- if (Math.abs(verticalEnd - verticalStart) > cornerRadius * 2) {
1250
- // Enough space for corners
1251
- pathData += ` L ${sourceX} ${midY - cornerRadius}`;
1252
- pathData += ` Q ${sourceX} ${midY}, ${sourceX + (targetX > sourceX ? cornerRadius : -cornerRadius)} ${midY}`;
1253
- pathData += ` L ${targetX - (targetX > sourceX ? cornerRadius : -cornerRadius)} ${midY}`;
1254
- pathData += ` Q ${targetX} ${midY}, ${targetX} ${midY + cornerRadius}`;
1255
- pathData += ` L ${targetX} ${verticalEnd}`;
1256
- }
1257
- else {
1258
- // Direct horizontal transition
1259
- pathData += ` L ${targetX} ${verticalStart}`;
1260
- pathData += ` L ${targetX} ${verticalEnd}`;
1261
- }
1262
- }
1263
- else {
1264
- // Straight vertical line
1265
- pathData += ` L ${targetX} ${verticalEnd}`;
1266
- }
1267
- pathData += ` L ${targetX} ${targetY}`;
1268
- svgPath = html `
1269
- <svg
1270
- style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;"
1271
- >
1272
- <path
1273
- d="${pathData}"
1274
- fill="none"
1275
- stroke="var(--color-connectors, #ccc)"
1276
- stroke-width="3"
1277
- class="plumb-connector"
1278
- />
1279
- <polygon
1280
- points="${targetX},${targetY} ${targetX - 6.5},${targetY -
1281
- 13} ${targetX + 6.5},${targetY - 13}"
1282
- fill="var(--color-connectors, #ccc)"
1283
- class="plumb-arrow"
1284
- />
1285
- </svg>
1286
- `;
1230
+ if (this.sourceId &&
1231
+ this.dragFromNodeId &&
1232
+ this.pendingCanvasConnection &&
1233
+ this.connectionSourceX != null &&
1234
+ this.connectionSourceY != null) {
1235
+ const sourceX = this.connectionSourceX;
1236
+ const sourceY = this.connectionSourceY;
1237
+ const targetX = position.left + 100;
1238
+ // When dragging up, connect to the placeholder bottom; otherwise to the top
1239
+ const targetY = dragUp ? position.top + 64 : position.top;
1240
+ const routeFace = dragUp
1241
+ ? targetX < sourceX
1242
+ ? 'left'
1243
+ : 'right'
1244
+ : 'top';
1245
+ const pathData = calculateFlowchartPath(sourceX, sourceY, targetX, targetY, 20, dragUp ? 0 : 10, 5, routeFace);
1246
+ const aw = ARROW_HALF_WIDTH;
1247
+ const al = ARROW_LENGTH;
1248
+ let arrowPoints;
1249
+ if (dragUp) {
1250
+ // Arrow tip pointing up, base at placeholder bottom
1251
+ arrowPoints = `${targetX},${targetY - al} ${targetX - aw},${targetY} ${targetX + aw},${targetY}`;
1252
+ }
1253
+ else {
1254
+ // Arrow pointing down into top of placeholder
1255
+ arrowPoints = `${targetX},${targetY} ${targetX - aw},${targetY - al} ${targetX + aw},${targetY - al}`;
1287
1256
  }
1257
+ svgPath = html `
1258
+ <svg
1259
+ style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;"
1260
+ >
1261
+ <path
1262
+ d="${pathData}"
1263
+ fill="none"
1264
+ stroke="var(--color-connectors, #ccc)"
1265
+ stroke-width="3"
1266
+ />
1267
+ <polygon
1268
+ points="${arrowPoints}"
1269
+ fill="var(--color-connectors, #ccc)"
1270
+ />
1271
+ </svg>
1272
+ `;
1288
1273
  }
1289
1274
  return html `${svgPath}
1290
1275
  <div
@@ -1306,17 +1291,13 @@ export class Editor extends RapidElement {
1306
1291
  }
1307
1292
  /**
1308
1293
  * Checks for node collisions and reflows nodes as needed.
1309
- * Nodes are only moved downward to resolve collisions.
1310
- *
1311
- * @param movedNodeUuids - UUIDs of nodes that were just moved/dropped
1312
- * @param droppedNodeUuid - UUID of the specific node that was dropped (if applicable)
1313
- * @param dropTargetBounds - Bounds of the node that was dropped onto (if applicable)
1294
+ * Sacred nodes (just moved/dropped) keep their positions while
1295
+ * other nodes are moved in the least-disruptive direction.
1314
1296
  */
1315
- checkCollisionsAndReflow(movedNodeUuids, droppedNodeUuid = null, dropTargetBounds = null) {
1297
+ checkCollisionsAndReflow(sacredNodeUuids) {
1316
1298
  var _b;
1317
1299
  if (!this.definition)
1318
1300
  return;
1319
- // Get all node bounds (only for actual nodes, not stickies)
1320
1301
  const allBounds = [];
1321
1302
  for (const node of this.definition.nodes) {
1322
1303
  const nodeUI = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.nodes[node.uuid];
@@ -1327,35 +1308,13 @@ export class Editor extends RapidElement {
1327
1308
  allBounds.push(bounds);
1328
1309
  }
1329
1310
  }
1330
- // Check if we need to determine midpoint priority for a dropped node
1331
- let targetHasPriority = false;
1332
- if (droppedNodeUuid && dropTargetBounds) {
1333
- const droppedBounds = allBounds.find((b) => b.uuid === droppedNodeUuid);
1334
- if (droppedBounds) {
1335
- // Check if the bottom of the dropped node is below the midpoint of the target
1336
- // If bottom is above midpoint, dropped node gets preference (targetHasPriority = false)
1337
- // If bottom is below midpoint, target gets preference (targetHasPriority = true)
1338
- const droppedBottom = droppedBounds.bottom;
1339
- const targetMidpoint = dropTargetBounds.top + dropTargetBounds.height / 2;
1340
- targetHasPriority = droppedBottom > targetMidpoint;
1341
- }
1342
- }
1343
- // Calculate reflow positions for each moved node
1344
- const allReflowPositions = {};
1345
- for (const movedUuid of movedNodeUuids) {
1346
- const movedBounds = allBounds.find((b) => b.uuid === movedUuid);
1347
- if (!movedBounds)
1348
- continue;
1349
- // Calculate reflow for this moved node
1350
- const reflowPositions = calculateReflowPositions(movedUuid, movedBounds, allBounds, droppedNodeUuid === movedUuid ? targetHasPriority : false);
1351
- // Merge into all reflow positions
1311
+ const reflowPositions = calculateReflowPositions(sacredNodeUuids, allBounds);
1312
+ if (reflowPositions.size > 0) {
1313
+ const positions = {};
1352
1314
  for (const [uuid, position] of reflowPositions.entries()) {
1353
- allReflowPositions[uuid] = position;
1315
+ positions[uuid] = position;
1354
1316
  }
1355
- }
1356
- // If there are positions to update, apply them
1357
- if (Object.keys(allReflowPositions).length > 0) {
1358
- getStore().getState().updateCanvasPositions(allReflowPositions);
1317
+ getStore().getState().updateCanvasPositions(positions);
1359
1318
  }
1360
1319
  }
1361
1320
  handleMouseMove(event) {
@@ -1390,21 +1349,37 @@ export class Editor extends RapidElement {
1390
1349
  this.targetId = null;
1391
1350
  this.isValidTarget = true;
1392
1351
  // Show connection placeholder when over empty canvas
1393
- // Calculate position: horizontally centered at mouse, vertically just below mouse
1394
1352
  const canvas = this.querySelector('#canvas');
1395
1353
  if (canvas) {
1396
1354
  const canvasRect = canvas.getBoundingClientRect();
1397
1355
  const relativeX = event.clientX - canvasRect.left;
1398
1356
  const relativeY = event.clientY - canvasRect.top;
1399
- // offset the placeholder so it's centered horizontally and just below the mouse
1400
- const placeholderWidth = 200; // approximate node width
1401
- const placeholderOffset = 20; // distance below mouse cursor
1357
+ const placeholderWidth = 200;
1358
+ const placeholderHeight = 64;
1359
+ const arrowLength = ARROW_LENGTH;
1360
+ const cursorGap = CURSOR_GAP;
1361
+ // Determine if cursor is above the source exit using stored sourceY
1362
+ const dragUp = this.connectionSourceY != null
1363
+ ? relativeY < this.connectionSourceY
1364
+ : false;
1365
+ let top;
1366
+ if (dragUp) {
1367
+ // Arrow points up: tip at cy + cursorGap.
1368
+ // Placeholder bottom should sit just above the arrow tip.
1369
+ top = relativeY + cursorGap - placeholderHeight;
1370
+ }
1371
+ else {
1372
+ // Arrow points down: tip at cy - cursorGap + arrowLength.
1373
+ // Placeholder top sits just below the arrow tip.
1374
+ top = relativeY - cursorGap + arrowLength;
1375
+ }
1402
1376
  this.connectionPlaceholder = {
1403
1377
  position: {
1404
1378
  left: relativeX - placeholderWidth / 2,
1405
- top: relativeY + placeholderOffset
1379
+ top
1406
1380
  },
1407
- visible: true
1381
+ visible: true,
1382
+ dragUp
1408
1383
  };
1409
1384
  }
1410
1385
  }
@@ -1505,34 +1480,7 @@ export class Editor extends RapidElement {
1505
1480
  if (nodeUuids.length > 0) {
1506
1481
  // Allow DOM to update before checking collisions
1507
1482
  setTimeout(() => {
1508
- var _b, _c;
1509
- // If only one node was moved, detect which node it might have been dropped onto
1510
- let droppedNodeUuid = null;
1511
- let dropTargetBounds = null;
1512
- if (nodeUuids.length === 1) {
1513
- droppedNodeUuid = nodeUuids[0];
1514
- const droppedNodeUI = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.nodes[droppedNodeUuid];
1515
- if (droppedNodeUI === null || droppedNodeUI === void 0 ? void 0 : droppedNodeUI.position) {
1516
- const droppedBounds = getNodeBounds(droppedNodeUuid, droppedNodeUI.position);
1517
- if (droppedBounds) {
1518
- // Find which node (if any) the dropped node overlaps with
1519
- for (const node of this.definition.nodes) {
1520
- if (node.uuid === droppedNodeUuid)
1521
- continue;
1522
- const nodeUI = (_c = this.definition._ui) === null || _c === void 0 ? void 0 : _c.nodes[node.uuid];
1523
- if (!(nodeUI === null || nodeUI === void 0 ? void 0 : nodeUI.position))
1524
- continue;
1525
- const targetBounds = getNodeBounds(node.uuid, nodeUI.position);
1526
- if (targetBounds &&
1527
- nodesOverlap(droppedBounds, targetBounds)) {
1528
- dropTargetBounds = targetBounds;
1529
- break; // Use the first overlapping node
1530
- }
1531
- }
1532
- }
1533
- }
1534
- }
1535
- this.checkCollisionsAndReflow(nodeUuids, droppedNodeUuid, dropTargetBounds);
1483
+ this.checkCollisionsAndReflow(nodeUuids);
1536
1484
  }, 0);
1537
1485
  }
1538
1486
  else {
@@ -1642,6 +1590,8 @@ export class Editor extends RapidElement {
1642
1590
  this.pendingCanvasConnection = null;
1643
1591
  this.connectionPlaceholder = null;
1644
1592
  this.sourceId = null;
1593
+ this.connectionSourceX = null;
1594
+ this.connectionSourceY = null;
1645
1595
  this.dragFromNodeId = null;
1646
1596
  }
1647
1597
  else {
@@ -1669,6 +1619,8 @@ export class Editor extends RapidElement {
1669
1619
  this.pendingCanvasConnection = null;
1670
1620
  this.connectionPlaceholder = null;
1671
1621
  this.sourceId = null;
1622
+ this.connectionSourceX = null;
1623
+ this.connectionSourceY = null;
1672
1624
  this.dragFromNodeId = null;
1673
1625
  this.originalConnectionTargetId = null;
1674
1626
  }
@@ -1779,6 +1731,10 @@ export class Editor extends RapidElement {
1779
1731
  handleActionEditRequested(event) {
1780
1732
  // For action editing, we set the action and find the corresponding node
1781
1733
  this.editingAction = event.detail.action;
1734
+ this.dialogOrigin =
1735
+ event.detail.originX != null
1736
+ ? { x: event.detail.originX, y: event.detail.originY }
1737
+ : null;
1782
1738
  // Find the node that contains this action
1783
1739
  const nodeUuid = event.detail.nodeUuid;
1784
1740
  const node = this.definition.nodes.find((n) => n.uuid === nodeUuid);
@@ -1814,6 +1770,10 @@ export class Editor extends RapidElement {
1814
1770
  handleNodeEditRequested(event) {
1815
1771
  this.editingNode = event.detail.node;
1816
1772
  this.editingNodeUI = event.detail.nodeUI;
1773
+ this.dialogOrigin =
1774
+ event.detail.originX != null
1775
+ ? { x: event.detail.originX, y: event.detail.originY }
1776
+ : null;
1817
1777
  }
1818
1778
  handleNodeDeleted(event) {
1819
1779
  const nodeUuid = event.detail.uuid;
@@ -1856,6 +1816,8 @@ export class Editor extends RapidElement {
1856
1816
  this.pendingCanvasConnection = null;
1857
1817
  this.connectionPlaceholder = null;
1858
1818
  this.sourceId = null;
1819
+ this.connectionSourceX = null;
1820
+ this.connectionSourceY = null;
1859
1821
  this.dragFromNodeId = null;
1860
1822
  }
1861
1823
  // Reset the creation flags
@@ -1882,6 +1844,7 @@ export class Editor extends RapidElement {
1882
1844
  this.editingNode = null;
1883
1845
  this.editingNodeUI = null;
1884
1846
  this.editingAction = null;
1847
+ this.dialogOrigin = null;
1885
1848
  }
1886
1849
  handleActionEditCanceled() {
1887
1850
  // If we were creating a new node, just discard it
@@ -1910,6 +1873,8 @@ export class Editor extends RapidElement {
1910
1873
  this.pendingCanvasConnection = null;
1911
1874
  this.connectionPlaceholder = null;
1912
1875
  this.sourceId = null;
1876
+ this.connectionSourceX = null;
1877
+ this.connectionSourceY = null;
1913
1878
  this.dragFromNodeId = null;
1914
1879
  }
1915
1880
  // Reset the creation flags
@@ -2336,6 +2301,7 @@ export class Editor extends RapidElement {
2336
2301
  }
2337
2302
  this.localizationWindowHidden = false;
2338
2303
  this.revisionsWindowHidden = true;
2304
+ this.issuesWindowHidden = true;
2339
2305
  const alreadySelected = languages.some((lang) => lang.code === this.languageCode);
2340
2306
  if (!alreadySelected) {
2341
2307
  this.handleLanguageChange(languages[0].code);
@@ -2543,11 +2509,42 @@ export class Editor extends RapidElement {
2543
2509
  }
2544
2510
  this.autoTranslating = false;
2545
2511
  }
2512
+ handleIssuesTabClick() {
2513
+ this.issuesWindowHidden = false;
2514
+ this.revisionsWindowHidden = true;
2515
+ this.localizationWindowHidden = true;
2516
+ }
2517
+ handleIssuesWindowClosed() {
2518
+ this.issuesWindowHidden = true;
2519
+ }
2520
+ handleIssueItemClick(issue) {
2521
+ var _b;
2522
+ const issuesWindow = document.getElementById('issues-window');
2523
+ issuesWindow === null || issuesWindow === void 0 ? void 0 : issuesWindow.handleClose();
2524
+ this.issuesWindowHidden = true;
2525
+ this.focusNode(issue.node_uuid);
2526
+ const node = this.definition.nodes.find((n) => n.uuid === issue.node_uuid);
2527
+ if (!node)
2528
+ return;
2529
+ if (issue.action_uuid) {
2530
+ const action = (_b = node.actions) === null || _b === void 0 ? void 0 : _b.find((a) => a.uuid === issue.action_uuid);
2531
+ if (action) {
2532
+ this.editingAction = action;
2533
+ this.editingNode = node;
2534
+ this.editingNodeUI = this.definition._ui.nodes[issue.node_uuid];
2535
+ }
2536
+ }
2537
+ else {
2538
+ this.editingNode = node;
2539
+ this.editingNodeUI = this.definition._ui.nodes[issue.node_uuid];
2540
+ }
2541
+ }
2546
2542
  handleRevisionsTabClick() {
2547
2543
  if (this.revisionsWindowHidden) {
2548
2544
  this.fetchRevisions();
2549
2545
  this.revisionsWindowHidden = false;
2550
- this.localizationWindowHidden = true; // Close other window
2546
+ this.issuesWindowHidden = true;
2547
+ this.localizationWindowHidden = true;
2551
2548
  }
2552
2549
  }
2553
2550
  handleRevisionsWindowClosed() {
@@ -2648,6 +2645,51 @@ export class Editor extends RapidElement {
2648
2645
  // Fetch the latest version of the flow to ensure the store is up to date
2649
2646
  getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
2650
2647
  }
2648
+ renderIssuesTab() {
2649
+ var _b;
2650
+ if (!((_b = this.flowIssues) === null || _b === void 0 ? void 0 : _b.length))
2651
+ return '';
2652
+ return html `
2653
+ <temba-floating-tab
2654
+ id="issues-tab"
2655
+ icon="alert_warning"
2656
+ label="Flow Issues"
2657
+ color="tomato"
2658
+ order="1"
2659
+ .hidden=${!this.issuesWindowHidden}
2660
+ @temba-button-clicked=${this.handleIssuesTabClick}
2661
+ ></temba-floating-tab>
2662
+ `;
2663
+ }
2664
+ renderIssuesWindow() {
2665
+ var _b;
2666
+ if (!((_b = this.flowIssues) === null || _b === void 0 ? void 0 : _b.length))
2667
+ return '';
2668
+ return html `
2669
+ <temba-floating-window
2670
+ id="issues-window"
2671
+ header="Flow Issues"
2672
+ .width=${360}
2673
+ .maxHeight=${600}
2674
+ .top=${75}
2675
+ color="tomato"
2676
+ .hidden=${this.issuesWindowHidden}
2677
+ @temba-dialog-hidden=${this.handleIssuesWindowClosed}
2678
+ >
2679
+ <div style="display:flex; flex-direction:column; gap:2px;">
2680
+ ${this.flowIssues.map((issue) => html `
2681
+ <div
2682
+ class="issue-list-item"
2683
+ @click=${() => this.handleIssueItemClick(issue)}
2684
+ >
2685
+ <temba-icon name="alert_warning" size="1.2"></temba-icon>
2686
+ <span>${formatIssueMessage(issue)}</span>
2687
+ </div>
2688
+ `)}
2689
+ </div>
2690
+ </temba-floating-window>
2691
+ `;
2692
+ }
2651
2693
  renderRevisionsTab() {
2652
2694
  return html `
2653
2695
  <temba-floating-tab
@@ -2655,7 +2697,7 @@ export class Editor extends RapidElement {
2655
2697
  icon="revisions"
2656
2698
  label="Revisions"
2657
2699
  color="rgb(142, 94, 167)"
2658
- top="105"
2700
+ order="2"
2659
2701
  .hidden=${!this.revisionsWindowHidden && this.localizationWindowHidden}
2660
2702
  @temba-button-clicked=${this.handleRevisionsTabClick}
2661
2703
  ></temba-floating-tab>
@@ -2926,7 +2968,7 @@ export class Editor extends RapidElement {
2926
2968
  icon="language"
2927
2969
  label="Translate Flow"
2928
2970
  color="#6b7280"
2929
- top="180"
2971
+ order="3"
2930
2972
  .hidden=${!this.localizationWindowHidden}
2931
2973
  @temba-button-clicked=${this.handleLocalizationTabClick}
2932
2974
  ></temba-floating-tab>
@@ -2973,8 +3015,9 @@ export class Editor extends RapidElement {
2973
3015
  ${unsafeCSS(CanvasNode.styles.cssText)}
2974
3016
  </style>`;
2975
3017
  const stickies = ((_c = (_b = this.definition) === null || _b === void 0 ? void 0 : _b._ui) === null || _c === void 0 ? void 0 : _c.stickies) || {};
2976
- return html `${style} ${this.renderRevisionsWindow()}
2977
- ${this.renderLocalizationWindow()} ${this.renderAutoTranslateDialog()}
3018
+ return html `${style} ${this.renderIssuesWindow()}
3019
+ ${this.renderRevisionsWindow()} ${this.renderLocalizationWindow()}
3020
+ ${this.renderAutoTranslateDialog()}
2978
3021
  <div id="editor">
2979
3022
  <div
2980
3023
  id="grid"
@@ -3047,6 +3090,7 @@ export class Editor extends RapidElement {
3047
3090
  .node=${this.editingNode}
3048
3091
  .nodeUI=${this.editingNodeUI}
3049
3092
  .action=${this.editingAction}
3093
+ .dialogOrigin=${this.dialogOrigin}
3050
3094
  @temba-node-saved=${(e) => this.handleNodeSaved(e.detail.node, e.detail.uiConfig)}
3051
3095
  @temba-action-saved=${(e) => this.handleActionSaved(e.detail.action)}
3052
3096
  @temba-node-edit-cancelled=${this.handleNodeEditCanceled}
@@ -3060,7 +3104,8 @@ export class Editor extends RapidElement {
3060
3104
  .features=${this.features}
3061
3105
  ></temba-node-type-selector>`
3062
3106
  : ''}
3063
- ${this.renderRevisionsTab()} ${this.renderLocalizationTab()} `;
3107
+ ${this.renderIssuesTab()} ${this.renderRevisionsTab()}
3108
+ ${this.renderLocalizationTab()} `;
3064
3109
  }
3065
3110
  }
3066
3111
  __decorate([
@@ -3099,6 +3144,9 @@ __decorate([
3099
3144
  __decorate([
3100
3145
  fromStore(zustand, (state) => state.getCurrentActivity())
3101
3146
  ], Editor.prototype, "activityData", void 0);
3147
+ __decorate([
3148
+ fromStore(zustand, (state) => { var _b; return ((_b = state.flowInfo) === null || _b === void 0 ? void 0 : _b.issues) || []; })
3149
+ ], Editor.prototype, "flowIssues", void 0);
3102
3150
  __decorate([
3103
3151
  state()
3104
3152
  ], Editor.prototype, "isDragging", void 0);
@@ -3129,6 +3177,9 @@ __decorate([
3129
3177
  __decorate([
3130
3178
  state()
3131
3179
  ], Editor.prototype, "isValidTarget", void 0);
3180
+ __decorate([
3181
+ state()
3182
+ ], Editor.prototype, "issuesWindowHidden", void 0);
3132
3183
  __decorate([
3133
3184
  state()
3134
3185
  ], Editor.prototype, "localizationWindowHidden", void 0);