@nyaruka/temba-components 0.139.0 → 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 (155) hide show
  1. package/.github/workflows/cla.yml +1 -1
  2. package/.github/workflows/copilot-setup-steps.yml +6 -1
  3. package/CHANGELOG.md +17 -0
  4. package/demo/data/flows/sample-flow.json +24 -0
  5. package/dist/temba-components.js +562 -296
  6. package/dist/temba-components.js.map +1 -1
  7. package/out-tsc/src/display/Chat.js +10 -7
  8. package/out-tsc/src/display/Chat.js.map +1 -1
  9. package/out-tsc/src/display/Dropdown.js +3 -1
  10. package/out-tsc/src/display/Dropdown.js.map +1 -1
  11. package/out-tsc/src/display/FloatingTab.js +3 -3
  12. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  13. package/out-tsc/src/display/Thumbnail.js +163 -5
  14. package/out-tsc/src/display/Thumbnail.js.map +1 -1
  15. package/out-tsc/src/flow/CanvasNode.js +64 -22
  16. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  17. package/out-tsc/src/flow/Editor.js +142 -8
  18. package/out-tsc/src/flow/Editor.js.map +1 -1
  19. package/out-tsc/src/flow/NodeEditor.js +118 -10
  20. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  21. package/out-tsc/src/flow/StickyNote.js +13 -4
  22. package/out-tsc/src/flow/StickyNote.js.map +1 -1
  23. package/out-tsc/src/flow/actions/audio-player.js +112 -0
  24. package/out-tsc/src/flow/actions/audio-player.js.map +1 -0
  25. package/out-tsc/src/flow/actions/enter_flow.js +43 -0
  26. package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
  27. package/out-tsc/src/flow/actions/play_audio.js +57 -4
  28. package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
  29. package/out-tsc/src/flow/actions/say_msg.js +86 -3
  30. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  31. package/out-tsc/src/flow/config.js +11 -3
  32. package/out-tsc/src/flow/config.js.map +1 -1
  33. package/out-tsc/src/flow/nodes/shared-rules.js +1 -1
  34. package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -1
  35. package/out-tsc/src/flow/nodes/terminal.js +7 -0
  36. package/out-tsc/src/flow/nodes/terminal.js.map +1 -0
  37. package/out-tsc/src/flow/nodes/wait_for_audio.js +77 -0
  38. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
  39. package/out-tsc/src/flow/nodes/wait_for_dial.js +151 -0
  40. package/out-tsc/src/flow/nodes/wait_for_dial.js.map +1 -0
  41. package/out-tsc/src/flow/nodes/wait_for_digits.js +61 -1
  42. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
  43. package/out-tsc/src/flow/nodes/wait_for_menu.js +173 -2
  44. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
  45. package/out-tsc/src/flow/operators.js +21 -5
  46. package/out-tsc/src/flow/operators.js.map +1 -1
  47. package/out-tsc/src/flow/types.js.map +1 -1
  48. package/out-tsc/src/flow/utils.js +79 -3
  49. package/out-tsc/src/flow/utils.js.map +1 -1
  50. package/out-tsc/src/form/ArrayEditor.js +4 -2
  51. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  52. package/out-tsc/src/form/FieldRenderer.js +49 -0
  53. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  54. package/out-tsc/src/interfaces.js +1 -0
  55. package/out-tsc/src/interfaces.js.map +1 -1
  56. package/out-tsc/src/layout/Dialog.js +52 -7
  57. package/out-tsc/src/layout/Dialog.js.map +1 -1
  58. package/out-tsc/src/live/TembaChart.js.map +1 -1
  59. package/out-tsc/src/simulator/Simulator.js +10 -4
  60. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  61. package/out-tsc/src/store/AppState.js +89 -3
  62. package/out-tsc/src/store/AppState.js.map +1 -1
  63. package/out-tsc/test/actions/play_audio.test.js +118 -0
  64. package/out-tsc/test/actions/play_audio.test.js.map +1 -0
  65. package/out-tsc/test/actions/say_msg.test.js +158 -0
  66. package/out-tsc/test/actions/say_msg.test.js.map +1 -0
  67. package/out-tsc/test/nodes/wait_for_audio.test.js +156 -0
  68. package/out-tsc/test/nodes/wait_for_audio.test.js.map +1 -0
  69. package/out-tsc/test/nodes/wait_for_dial.test.js +336 -0
  70. package/out-tsc/test/nodes/wait_for_dial.test.js.map +1 -0
  71. package/out-tsc/test/nodes/wait_for_digits.test.js +198 -84
  72. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  73. package/out-tsc/test/nodes/wait_for_menu.test.js +340 -0
  74. package/out-tsc/test/nodes/wait_for_menu.test.js.map +1 -0
  75. package/out-tsc/test/temba-flow-collision.test.js +261 -6
  76. package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
  77. package/out-tsc/test/temba-node-type-selector.test.js +6 -6
  78. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
  79. package/package.json +1 -1
  80. package/screenshots/truth/actions/play_audio/editor/expression-url.png +0 -0
  81. package/screenshots/truth/actions/play_audio/editor/static-url.png +0 -0
  82. package/screenshots/truth/actions/play_audio/render/expression-url.png +0 -0
  83. package/screenshots/truth/actions/play_audio/render/static-url.png +0 -0
  84. package/screenshots/truth/actions/say_msg/editor/multiline-text.png +0 -0
  85. package/screenshots/truth/actions/say_msg/editor/simple-text.png +0 -0
  86. package/screenshots/truth/actions/say_msg/editor/text-with-audio-url.png +0 -0
  87. package/screenshots/truth/actions/say_msg/render/multiline-text.png +0 -0
  88. package/screenshots/truth/actions/say_msg/render/simple-text.png +0 -0
  89. package/screenshots/truth/actions/say_msg/render/text-with-audio-url.png +0 -0
  90. package/screenshots/truth/editor/router.png +0 -0
  91. package/screenshots/truth/editor/wait.png +0 -0
  92. package/screenshots/truth/nodes/wait_for_audio/editor/basic-audio-wait.png +0 -0
  93. package/screenshots/truth/nodes/wait_for_audio/render/basic-audio-wait.png +0 -0
  94. package/screenshots/truth/nodes/wait_for_dial/editor/basic-dial.png +0 -0
  95. package/screenshots/truth/nodes/wait_for_dial/editor/dial-with-limits.png +0 -0
  96. package/screenshots/truth/nodes/wait_for_dial/render/basic-dial.png +0 -0
  97. package/screenshots/truth/nodes/wait_for_dial/render/dial-with-limits.png +0 -0
  98. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  99. package/screenshots/truth/nodes/wait_for_digits/editor/digits-with-rules.png +0 -0
  100. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  101. package/screenshots/truth/nodes/wait_for_digits/render/digits-with-rules.png +0 -0
  102. package/screenshots/truth/nodes/wait_for_menu/editor/menu-with-digits.png +0 -0
  103. package/screenshots/truth/nodes/wait_for_menu/render/menu-with-digits.png +0 -0
  104. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  105. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  106. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  107. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  108. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  109. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  110. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  111. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  112. package/src/display/Chat.ts +13 -7
  113. package/src/display/Dropdown.ts +3 -1
  114. package/src/display/FloatingTab.ts +3 -3
  115. package/src/display/Thumbnail.ts +162 -2
  116. package/src/flow/CanvasNode.ts +69 -23
  117. package/src/flow/Editor.ts +156 -13
  118. package/src/flow/NodeEditor.ts +137 -9
  119. package/src/flow/StickyNote.ts +14 -4
  120. package/src/flow/actions/audio-player.ts +127 -0
  121. package/src/flow/actions/enter_flow.ts +44 -0
  122. package/src/flow/actions/play_audio.ts +64 -5
  123. package/src/flow/actions/say_msg.ts +94 -4
  124. package/src/flow/config.ts +11 -3
  125. package/src/flow/nodes/shared-rules.ts +1 -1
  126. package/src/flow/nodes/terminal.ts +9 -0
  127. package/src/flow/nodes/wait_for_audio.ts +88 -0
  128. package/src/flow/nodes/wait_for_dial.ts +176 -0
  129. package/src/flow/nodes/wait_for_digits.ts +86 -2
  130. package/src/flow/nodes/wait_for_menu.ts +209 -3
  131. package/src/flow/operators.ts +23 -5
  132. package/src/flow/types.ts +23 -1
  133. package/src/flow/utils.ts +82 -3
  134. package/src/form/ArrayEditor.ts +4 -2
  135. package/src/form/FieldRenderer.ts +64 -1
  136. package/src/interfaces.ts +2 -1
  137. package/src/layout/Dialog.ts +53 -7
  138. package/src/live/TembaChart.ts +1 -1
  139. package/src/simulator/Simulator.ts +13 -4
  140. package/src/store/AppState.ts +105 -1
  141. package/src/store/flow-definition.d.ts +2 -0
  142. package/test/actions/play_audio.test.ts +155 -0
  143. package/test/actions/say_msg.test.ts +196 -0
  144. package/test/nodes/wait_for_audio.test.ts +182 -0
  145. package/test/nodes/wait_for_dial.test.ts +382 -0
  146. package/test/nodes/wait_for_digits.test.ts +233 -109
  147. package/test/nodes/wait_for_menu.test.ts +383 -0
  148. package/test/temba-flow-collision.test.ts +286 -6
  149. package/test/temba-node-type-selector.test.ts +6 -6
  150. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  151. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  152. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  153. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  154. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  155. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
@@ -1,7 +1,7 @@
1
1
  import { __decorate } from "tslib";
2
2
  import { css, html } from 'lit';
3
3
  import { RapidElement } from '../RapidElement';
4
- import { property } from 'lit/decorators.js';
4
+ import { property, state } from 'lit/decorators.js';
5
5
  import { getClasses } from '../utils';
6
6
  import { WebChatIcon } from '../webchat';
7
7
  var ThumbnailContentType;
@@ -27,6 +27,11 @@ export class Thumbnail extends RapidElement {
27
27
  this.preview = true;
28
28
  // cached tile URL for location thumbnails
29
29
  this.tileUrl = '';
30
+ // audio player state
31
+ this.audio = null;
32
+ this.audioPlaying = false;
33
+ this.audioProgress = 0;
34
+ this.audioDuration = 0;
30
35
  }
31
36
  static get styles() {
32
37
  return css `
@@ -72,11 +77,57 @@ export class Thumbnail extends RapidElement {
72
77
  }
73
78
 
74
79
  .thumb.document,
75
- .thumb.audio,
76
80
  .thumb.video {
77
81
  border: 1px solid #eee;
78
82
  }
79
83
 
84
+ .audio-player {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: 6px;
88
+ padding: 6px 8px;
89
+ background: rgba(0, 0, 0, 0.05);
90
+ border-radius: var(--curvature);
91
+ cursor: default;
92
+ }
93
+
94
+ .audio-play-btn {
95
+ cursor: pointer;
96
+ color: #666;
97
+ display: flex;
98
+ align-items: center;
99
+ flex-shrink: 0;
100
+ }
101
+
102
+ .audio-play-btn:hover {
103
+ color: #333;
104
+ }
105
+
106
+ .audio-progress-bar {
107
+ flex: 1;
108
+ height: 3px;
109
+ background: #ddd;
110
+ border-radius: 2px;
111
+ overflow: hidden;
112
+ min-width: 60px;
113
+ cursor: pointer;
114
+ }
115
+
116
+ .audio-progress-fill {
117
+ height: 100%;
118
+ background: var(--color-primary, #2387ca);
119
+ border-radius: 2px;
120
+ transition: width 0.15s linear;
121
+ }
122
+
123
+ .audio-time {
124
+ font-size: 11px;
125
+ color: #999;
126
+ flex-shrink: 0;
127
+ min-width: 28px;
128
+ text-align: right;
129
+ }
130
+
80
131
  .wrapper:hover .thumb.icon {
81
132
  }
82
133
 
@@ -109,6 +160,58 @@ export class Thumbnail extends RapidElement {
109
160
  }
110
161
  `;
111
162
  }
163
+ handleAudioPlayClick(e) {
164
+ e.stopPropagation();
165
+ if (!this.audio) {
166
+ this.audio = new Audio(this.url);
167
+ this.audio.addEventListener('timeupdate', () => {
168
+ if (this.audio.duration) {
169
+ this.audioProgress = this.audio.currentTime / this.audio.duration;
170
+ this.audioDuration = this.audio.duration;
171
+ }
172
+ });
173
+ this.audio.addEventListener('ended', () => {
174
+ this.audioPlaying = false;
175
+ this.audioProgress = 0;
176
+ });
177
+ this.audio.addEventListener('error', () => {
178
+ this.audioPlaying = false;
179
+ this.audioProgress = 0;
180
+ });
181
+ }
182
+ if (this.audioPlaying) {
183
+ this.audio.pause();
184
+ this.audioPlaying = false;
185
+ }
186
+ else {
187
+ this.audio.play().catch(() => {
188
+ this.audioPlaying = false;
189
+ });
190
+ this.audioPlaying = true;
191
+ }
192
+ }
193
+ handleProgressClick(e) {
194
+ e.stopPropagation();
195
+ if (!this.audio || !this.audio.duration)
196
+ return;
197
+ const bar = e.currentTarget;
198
+ const rect = bar.getBoundingClientRect();
199
+ const pct = (e.clientX - rect.left) / rect.width;
200
+ this.audio.currentTime = pct * this.audio.duration;
201
+ }
202
+ formatTime(seconds) {
203
+ const s = Math.floor(seconds);
204
+ const m = Math.floor(s / 60);
205
+ const rem = s % 60;
206
+ return `${m}:${rem.toString().padStart(2, '0')}`;
207
+ }
208
+ disconnectedCallback() {
209
+ super.disconnectedCallback();
210
+ if (this.audio) {
211
+ this.audio.pause();
212
+ this.audio = null;
213
+ }
214
+ }
112
215
  // convert lat/lng to tile coordinates for OSM
113
216
  latLngToTile(lat, lng, zoom) {
114
217
  const n = Math.pow(2, zoom);
@@ -198,6 +301,9 @@ export class Thumbnail extends RapidElement {
198
301
  const osmUrl = `https://www.openstreetmap.org/?mlat=${this.latitude}&mlon=${this.longitude}#map=15/${this.latitude}/${this.longitude}`;
199
302
  window.open(osmUrl, '_blank');
200
303
  }
304
+ else if (this.contentType === ThumbnailContentType.AUDIO) {
305
+ // audio has inline controls, no click action needed
306
+ }
201
307
  else {
202
308
  window.open(this.url, '_blank');
203
309
  }
@@ -209,10 +315,14 @@ export class Thumbnail extends RapidElement {
209
315
  window.open(this.url, '_blank');
210
316
  }
211
317
  render() {
318
+ var _a;
212
319
  return html `
213
320
  <div
214
321
  @click=${this.handleThumbnailClicked.bind(this)}
215
322
  class="${getClasses({ wrapper: true, zoom: this.zoom })}"
323
+ style="${this.contentType === ThumbnailContentType.AUDIO
324
+ ? 'cursor: default;'
325
+ : ''}"
216
326
  url=${this.url}
217
327
  >
218
328
  ${this.contentType === ThumbnailContentType.IMAGE && this.preview
@@ -220,14 +330,53 @@ export class Thumbnail extends RapidElement {
220
330
  class="observe thumb ${this.contentType}"
221
331
  src="${this.url}"
222
332
  ></img></div>`
223
- : html `
333
+ : this.contentType === ThumbnailContentType.AUDIO
334
+ ? html `<div class="audio-player">
335
+ <div class="audio-play-btn" @click=${this.handleAudioPlayClick}>
336
+ ${this.audioPlaying
337
+ ? html `<svg
338
+ viewBox="0 0 24 24"
339
+ width="14"
340
+ height="14"
341
+ fill="currentColor"
342
+ >
343
+ <rect x="5" y="3" width="4" height="18" />
344
+ <rect x="15" y="3" width="4" height="18" />
345
+ </svg>`
346
+ : html `<svg
347
+ viewBox="0 0 24 24"
348
+ width="14"
349
+ height="14"
350
+ fill="currentColor"
351
+ >
352
+ <polygon points="6,3 20,12 6,21" />
353
+ </svg>`}
354
+ </div>
355
+ <div
356
+ class="audio-progress-bar"
357
+ @click=${this.handleProgressClick}
358
+ >
359
+ <div
360
+ class="audio-progress-fill"
361
+ style="width: ${this.audioProgress * 100}%"
362
+ ></div>
363
+ </div>
364
+ <div class="audio-time">
365
+ ${this.audioDuration
366
+ ? this.formatTime(this.audioPlaying || this.audioProgress > 0
367
+ ? ((_a = this.audio) === null || _a === void 0 ? void 0 : _a.currentTime) || 0
368
+ : this.audioDuration)
369
+ : ''}
370
+ </div>
371
+ </div>`
372
+ : html `
224
373
  ${this.contentType === ThumbnailContentType.LOCATION
225
- ? html `<img
374
+ ? html `<img
226
375
  style="height:125px;margin-bottom:-3px;border-radius:var(--curvature);"
227
376
  src="${this.tileUrl}"
228
377
  alt="Location preview"
229
378
  />`
230
- : html `<div
379
+ : html `<div
231
380
  style="padding:1em; background:rgba(0,0,0,.05);border-radius:var(--curvature);"
232
381
  >
233
382
  <temba-icon
@@ -267,4 +416,13 @@ __decorate([
267
416
  __decorate([
268
417
  property({ type: String, attribute: false })
269
418
  ], Thumbnail.prototype, "tileUrl", void 0);
419
+ __decorate([
420
+ state()
421
+ ], Thumbnail.prototype, "audioPlaying", void 0);
422
+ __decorate([
423
+ state()
424
+ ], Thumbnail.prototype, "audioProgress", void 0);
425
+ __decorate([
426
+ state()
427
+ ], Thumbnail.prototype, "audioDuration", void 0);
270
428
  //# sourceMappingURL=Thumbnail.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Thumbnail.js","sourceRoot":"","sources":["../../../src/display/Thumbnail.ts"],"names":[],"mappings":"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","sourcesContent":["import { PropertyValueMap, css, html } from 'lit';\nimport { RapidElement } from '../RapidElement';\nimport { property } from 'lit/decorators.js';\nimport { getClasses } from '../utils';\nimport { Lightbox } from './Lightbox';\nimport { WebChatIcon } from '../webchat';\n\nenum ThumbnailContentType {\n IMAGE = 'image',\n AUDIO = 'audio',\n VIDEO = 'video',\n DOCUMENT = 'document',\n LOCATION = 'location',\n OTHER = 'other'\n}\n\nconst ThumbnailIcons = {\n [ThumbnailContentType.IMAGE]: WebChatIcon.attachment_image,\n [ThumbnailContentType.AUDIO]: WebChatIcon.attachment_audio,\n [ThumbnailContentType.VIDEO]: WebChatIcon.attachment_video,\n [ThumbnailContentType.DOCUMENT]: WebChatIcon.attachment_document,\n [ThumbnailContentType.OTHER]: WebChatIcon.attachment\n};\n\nexport class Thumbnail extends RapidElement {\n static get styles() {\n return css`\n :host {\n display: inline;\n }\n\n .wrapper {\n padding: var(--thumb-padding, 0.4em);\n background: var(--thumb-background, #fff);\n box-shadow: var(--widget-box-shadow);\n cursor: pointer;\n border-radius: calc(var(--curvature) * 1.5);\n border: 0px solid #f3f3f3;\n }\n\n .wrapper.zoom {\n border: none;\n padding: 0 !important;\n border-radius: 0 !important;\n overflow: hidden !important;\n }\n\n .zoom .thumb {\n border-radius: 0px !important;\n width: calc(var(--thumb-size, 4em) + 0.8em);\n max-height: calc(90vh - 10em);\n }\n\n .thumb {\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n border-radius: var(--curvature);\n max-height: calc(var(--thumb-size, 4em) * 2);\n max-width: calc(var(--thumb-size, 4em) * 2);\n height: var(--thumb-size, 4em);\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 400;\n color: var(--thumb-icon, #bbb);\n }\n\n .thumb.document,\n .thumb.audio,\n .thumb.video {\n border: 1px solid #eee;\n }\n\n .wrapper:hover .thumb.icon {\n }\n\n .viewer {\n display: block;\n }\n\n .zoom .viewer {\n display: block;\n }\n\n .download {\n display: none;\n position: absolute;\n right: 0em;\n bottom: 0em;\n border-radius: var(--curvature);\n transform: scale(0.2) translate(3em, 3em);\n padding: 0.4em;\n }\n\n .zoom .download {\n display: block;\n background: rgba(0, 0, 0, 0.5);\n }\n\n .zoom .download:hover {\n background: rgba(0, 0, 0, 0.6);\n cursor: pointer;\n }\n `;\n }\n\n @property({ type: String })\n url: string;\n\n @property({ type: String })\n attachment: string;\n\n @property({ type: Number })\n ratio: number = 0;\n\n @property({ type: Boolean })\n preview: boolean = true;\n\n @property({ type: Boolean, attribute: false })\n zoom: boolean;\n\n @property({ type: String, attribute: true })\n contentType: string;\n\n @property({ type: Number, attribute: false })\n latitude: number;\n\n @property({ type: Number, attribute: false })\n longitude: number;\n\n // cached tile URL for location thumbnails\n @property({ type: String, attribute: false })\n private tileUrl: string = '';\n\n // convert lat/lng to tile coordinates for OSM\n private latLngToTile(lat: number, lng: number, zoom: number) {\n const n = Math.pow(2, zoom);\n const x = Math.floor(((lng + 180) / 360) * n);\n const latRad = (lat * Math.PI) / 180;\n const y = Math.floor(\n ((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2) *\n n\n );\n return { x, y, z: zoom };\n }\n\n protected updated(\n changes: PropertyValueMap<any> | Map<PropertyKey, unknown>\n ): void {\n super.updated(changes);\n\n if (\n changes.has('contentType') &&\n this.contentType === ThumbnailContentType.IMAGE\n ) {\n const toObserve = this.shadowRoot.querySelector('.observe');\n if (toObserve) {\n new ResizeObserver((e, observer) => {\n if (toObserve.clientHeight > 0 && toObserve.clientWidth > 0) {\n this.ratio = toObserve.clientHeight / toObserve.clientWidth;\n this.preview =\n this.ratio === 0 || (this.ratio > 0.25 && this.ratio <= 1.5);\n observer.disconnect();\n }\n }).observe(toObserve);\n }\n }\n\n // convert our attachment to a url and label\n if (changes.has('attachment')) {\n if (this.attachment) {\n const splitIndex = this.attachment.indexOf(':');\n if (splitIndex === -1) {\n this.url = this.attachment;\n } else {\n const contentType = this.attachment.substring(0, splitIndex);\n this.url = this.attachment.substring(splitIndex + 1);\n\n if (contentType.startsWith('image')) {\n this.contentType = ThumbnailContentType.IMAGE;\n } else if (contentType.startsWith('audio')) {\n this.contentType = ThumbnailContentType.AUDIO;\n } else if (contentType.startsWith('video')) {\n this.contentType = ThumbnailContentType.VIDEO;\n } else if (contentType.startsWith('application')) {\n this.contentType = ThumbnailContentType.DOCUMENT;\n } else if (contentType.startsWith('geo')) {\n this.contentType = ThumbnailContentType.LOCATION;\n // Parse coordinates from URL which is already stripped of \"geo:\" prefix\n // Format is now just: lat,lng\n const coords = this.url.match(/^([^,]+),([^,]+)/);\n if (coords) {\n this.latitude = parseFloat(coords[1]);\n this.longitude = parseFloat(coords[2]);\n }\n } else {\n this.contentType = ThumbnailContentType.OTHER;\n }\n }\n }\n }\n\n // calculate tile URL when latitude/longitude changes\n if (changes.has('latitude') || changes.has('longitude')) {\n if (\n this.latitude !== undefined &&\n this.longitude !== undefined &&\n !isNaN(this.latitude) &&\n !isNaN(this.longitude)\n ) {\n const tile = this.latLngToTile(this.latitude, this.longitude, 13);\n this.tileUrl = `https://tile.openstreetmap.org/${tile.z}/${tile.x}/${tile.y}.png`;\n } else {\n this.tileUrl = '';\n }\n }\n }\n\n public handleThumbnailClicked() {\n if (this.contentType === ThumbnailContentType.IMAGE && this.preview) {\n window.setTimeout(() => {\n const lightbox = document.querySelector('temba-lightbox') as Lightbox;\n lightbox.showElement(this);\n }, 100);\n } else if (this.contentType === ThumbnailContentType.LOCATION) {\n // open location in openstreetmap\n const osmUrl = `https://www.openstreetmap.org/?mlat=${this.latitude}&mlon=${this.longitude}#map=15/${this.latitude}/${this.longitude}`;\n window.open(osmUrl, '_blank');\n } else {\n window.open(this.url, '_blank');\n }\n }\n\n public handleDownload(e: Event) {\n e.stopPropagation();\n e.preventDefault();\n\n // open this.url in another tab\n window.open(this.url, '_blank');\n }\n\n public render() {\n return html`\n <div\n @click=${this.handleThumbnailClicked.bind(this)}\n class=\"${getClasses({ wrapper: true, zoom: this.zoom })}\"\n url=${this.url}\n >\n ${this.contentType === ThumbnailContentType.IMAGE && this.preview\n ? html`<div class=\"\"><div class=\"download\" @click=${this.handleDownload.bind(\n this\n )}><temba-icon size=\"1\" style=\"color:#fff;\" name=\"download\"></temba-icon></div><img\n class=\"observe thumb ${this.contentType}\"\n src=\"${this.url}\"\n ></img></div>`\n : html`\n ${this.contentType === ThumbnailContentType.LOCATION\n ? html`<img\n style=\"height:125px;margin-bottom:-3px;border-radius:var(--curvature);\"\n src=\"${this.tileUrl}\"\n alt=\"Location preview\"\n />`\n : html`<div\n style=\"padding:1em; background:rgba(0,0,0,.05);border-radius:var(--curvature);\"\n >\n <temba-icon\n size=\"1.5\"\n name=\"${ThumbnailIcons[this.contentType]}\"\n ></temba-icon>\n </div>`}\n `}\n </div>\n `;\n }\n}\n"]}
1
+ {"version":3,"file":"Thumbnail.js","sourceRoot":"","sources":["../../../src/display/Thumbnail.ts"],"names":[],"mappings":";AAAA,OAAO,EAAoB,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,IAAK,oBAOJ;AAPD,WAAK,oBAAoB;IACvB,uCAAe,CAAA;IACf,uCAAe,CAAA;IACf,uCAAe,CAAA;IACf,6CAAqB,CAAA;IACrB,6CAAqB,CAAA;IACrB,uCAAe,CAAA;AACjB,CAAC,EAPI,oBAAoB,KAApB,oBAAoB,QAOxB;AAED,MAAM,cAAc,GAAG;IACrB,CAAC,oBAAoB,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC,gBAAgB;IAC1D,CAAC,oBAAoB,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC,gBAAgB;IAC1D,CAAC,oBAAoB,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC,gBAAgB;IAC1D,CAAC,oBAAoB,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC,mBAAmB;IAChE,CAAC,oBAAoB,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC,UAAU;CACrD,CAAC;AAEF,MAAM,OAAO,SAAU,SAAQ,YAAY;IAA3C;;QAwIE,UAAK,GAAW,CAAC,CAAC;QAGlB,YAAO,GAAY,IAAI,CAAC;QAcxB,0CAA0C;QAElC,YAAO,GAAW,EAAE,CAAC;QAE7B,qBAAqB;QACb,UAAK,GAA4B,IAAI,CAAC;QAGtC,iBAAY,GAAG,KAAK,CAAC;QAGrB,kBAAa,GAAG,CAAC,CAAC;QAGlB,kBAAa,GAAG,CAAC,CAAC;IAoP5B,CAAC;IA1ZC,MAAM,KAAK,MAAM;QACf,OAAO,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA4HT,CAAC;IACJ,CAAC;IA0CO,oBAAoB,CAAC,CAAQ;QACnC,CAAC,CAAC,eAAe,EAAE,CAAC;QAEpB,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,YAAY,EAAE,GAAG,EAAE;gBAC7C,IAAI,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;oBACxB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;oBAClE,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;gBAC3C,CAAC;YACH,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;gBACxC,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;gBAC1B,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;YACzB,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;gBACxC,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;gBAC1B,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;YACzB,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YACnB,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC5B,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE;gBAC3B,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;YAC5B,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QAC3B,CAAC;IACH,CAAC;IAEO,mBAAmB,CAAC,CAAa;QACvC,CAAC,CAAC,eAAe,EAAE,CAAC;QACpB,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ;YAAE,OAAO;QAChD,MAAM,GAAG,GAAG,CAAC,CAAC,aAA4B,CAAC;QAC3C,MAAM,IAAI,GAAG,GAAG,CAAC,qBAAqB,EAAE,CAAC;QACzC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC;QACjD,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;IACrD,CAAC;IAEO,UAAU,CAAC,OAAe;QAChC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC9B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;QAC7B,MAAM,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC;QACnB,OAAO,GAAG,CAAC,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;IACnD,CAAC;IAED,oBAAoB;QAClB,KAAK,CAAC,oBAAoB,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YACnB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QACpB,CAAC;IACH,CAAC;IAED,8CAA8C;IACtC,YAAY,CAAC,GAAW,EAAE,GAAW,EAAE,IAAY;QACzD,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QAC5B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAClB,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;YACrE,CAAC,CACJ,CAAC;QACF,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;IAES,OAAO,CACf,OAA0D;QAE1D,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAEvB,IACE,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;YAC1B,IAAI,CAAC,WAAW,KAAK,oBAAoB,CAAC,KAAK,EAC/C,CAAC;YACD,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;YAC5D,IAAI,SAAS,EAAE,CAAC;gBACd,IAAI,cAAc,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE;oBACjC,IAAI,SAAS,CAAC,YAAY,GAAG,CAAC,IAAI,SAAS,CAAC,WAAW,GAAG,CAAC,EAAE,CAAC;wBAC5D,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC,YAAY,GAAG,SAAS,CAAC,WAAW,CAAC;wBAC5D,IAAI,CAAC,OAAO;4BACV,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC;wBAC/D,QAAQ,CAAC,UAAU,EAAE,CAAC;oBACxB,CAAC;gBACH,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YACxB,CAAC;QACH,CAAC;QAED,4CAA4C;QAC5C,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;YAC9B,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;gBACpB,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;gBAChD,IAAI,UAAU,KAAK,CAAC,CAAC,EAAE,CAAC;oBACtB,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC;gBAC7B,CAAC;qBAAM,CAAC;oBACN,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;oBAC7D,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC;oBAErD,IAAI,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;wBACpC,IAAI,CAAC,WAAW,GAAG,oBAAoB,CAAC,KAAK,CAAC;oBAChD,CAAC;yBAAM,IAAI,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;wBAC3C,IAAI,CAAC,WAAW,GAAG,oBAAoB,CAAC,KAAK,CAAC;oBAChD,CAAC;yBAAM,IAAI,WAAW,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;wBAC3C,IAAI,CAAC,WAAW,GAAG,oBAAoB,CAAC,KAAK,CAAC;oBAChD,CAAC;yBAAM,IAAI,WAAW,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;wBACjD,IAAI,CAAC,WAAW,GAAG,oBAAoB,CAAC,QAAQ,CAAC;oBACnD,CAAC;yBAAM,IAAI,WAAW,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;wBACzC,IAAI,CAAC,WAAW,GAAG,oBAAoB,CAAC,QAAQ,CAAC;wBACjD,wEAAwE;wBACxE,8BAA8B;wBAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC;wBAClD,IAAI,MAAM,EAAE,CAAC;4BACX,IAAI,CAAC,QAAQ,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;4BACtC,IAAI,CAAC,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;wBACzC,CAAC;oBACH,CAAC;yBAAM,CAAC;wBACN,IAAI,CAAC,WAAW,GAAG,oBAAoB,CAAC,KAAK,CAAC;oBAChD,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,qDAAqD;QACrD,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;YACxD,IACE,IAAI,CAAC,QAAQ,KAAK,SAAS;gBAC3B,IAAI,CAAC,SAAS,KAAK,SAAS;gBAC5B,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC;gBACrB,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EACtB,CAAC;gBACD,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;gBAClE,IAAI,CAAC,OAAO,GAAG,kCAAkC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC;YACpF,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;YACpB,CAAC;QACH,CAAC;IACH,CAAC;IAEM,sBAAsB;QAC3B,IAAI,IAAI,CAAC,WAAW,KAAK,oBAAoB,CAAC,KAAK,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACpE,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE;gBACrB,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,gBAAgB,CAAa,CAAC;gBACtE,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YAC7B,CAAC,EAAE,GAAG,CAAC,CAAC;QACV,CAAC;aAAM,IAAI,IAAI,CAAC,WAAW,KAAK,oBAAoB,CAAC,QAAQ,EAAE,CAAC;YAC9D,iCAAiC;YACjC,MAAM,MAAM,GAAG,uCAAuC,IAAI,CAAC,QAAQ,SAAS,IAAI,CAAC,SAAS,WAAW,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACvI,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAChC,CAAC;aAAM,IAAI,IAAI,CAAC,WAAW,KAAK,oBAAoB,CAAC,KAAK,EAAE,CAAC;YAC3D,oDAAoD;QACtD,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAEM,cAAc,CAAC,CAAQ;QAC5B,CAAC,CAAC,eAAe,EAAE,CAAC;QACpB,CAAC,CAAC,cAAc,EAAE,CAAC;QAEnB,+BAA+B;QAC/B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAClC,CAAC;IAEM,MAAM;;QACX,OAAO,IAAI,CAAA;;iBAEE,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC;iBACtC,UAAU,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;iBAC9C,IAAI,CAAC,WAAW,KAAK,oBAAoB,CAAC,KAAK;YACtD,CAAC,CAAC,kBAAkB;YACpB,CAAC,CAAC,EAAE;cACA,IAAI,CAAC,GAAG;;UAEZ,IAAI,CAAC,WAAW,KAAK,oBAAoB,CAAC,KAAK,IAAI,IAAI,CAAC,OAAO;YAC/D,CAAC,CAAC,IAAI,CAAA,8CAA8C,IAAI,CAAC,cAAc,CAAC,IAAI,CACxE,IAAI,CACL;iCACoB,IAAI,CAAC,WAAW;iBAChC,IAAI,CAAC,GAAG;sBACH;YACZ,CAAC,CAAC,IAAI,CAAC,WAAW,KAAK,oBAAoB,CAAC,KAAK;gBACjD,CAAC,CAAC,IAAI,CAAA;mDACmC,IAAI,CAAC,oBAAoB;kBAC1D,IAAI,CAAC,YAAY;oBACjB,CAAC,CAAC,IAAI,CAAA;;;;;;;;2BAQG;oBACT,CAAC,CAAC,IAAI,CAAA;;;;;;;2BAOG;;;;yBAIF,IAAI,CAAC,mBAAmB;;;;kCAIf,IAAI,CAAC,aAAa,GAAG,GAAG;;;;kBAIxC,IAAI,CAAC,aAAa;oBAClB,CAAC,CAAC,IAAI,CAAC,UAAU,CACb,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,aAAa,GAAG,CAAC;wBACzC,CAAC,CAAC,CAAA,MAAA,IAAI,CAAC,KAAK,0CAAE,WAAW,KAAI,CAAC;wBAC9B,CAAC,CAAC,IAAI,CAAC,aAAa,CACvB;oBACH,CAAC,CAAC,EAAE;;mBAEH;gBACT,CAAC,CAAC,IAAI,CAAA;gBACA,IAAI,CAAC,WAAW,KAAK,oBAAoB,CAAC,QAAQ;oBAClD,CAAC,CAAC,IAAI,CAAA;;2BAEK,IAAI,CAAC,OAAO;;qBAElB;oBACL,CAAC,CAAC,IAAI,CAAA;;;;;8BAKQ,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC;;yBAErC;aACZ;;KAER,CAAC;IACJ,CAAC;CACF;AAzRC;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;sCACf;AAGZ;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;6CACR;AAGnB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;wCACT;AAGlB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;0CACJ;AAGxB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;uCAChC;AAGd;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;8CACxB;AAGpB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;2CAC5B;AAGjB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;4CAC3B;AAIV;IADP,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;0CAChB;AAMrB;IADP,KAAK,EAAE;+CACqB;AAGrB;IADP,KAAK,EAAE;gDACkB;AAGlB;IADP,KAAK,EAAE;gDACkB","sourcesContent":["import { PropertyValueMap, css, html } from 'lit';\nimport { RapidElement } from '../RapidElement';\nimport { property, state } from 'lit/decorators.js';\nimport { getClasses } from '../utils';\nimport { Lightbox } from './Lightbox';\nimport { WebChatIcon } from '../webchat';\n\nenum ThumbnailContentType {\n IMAGE = 'image',\n AUDIO = 'audio',\n VIDEO = 'video',\n DOCUMENT = 'document',\n LOCATION = 'location',\n OTHER = 'other'\n}\n\nconst ThumbnailIcons = {\n [ThumbnailContentType.IMAGE]: WebChatIcon.attachment_image,\n [ThumbnailContentType.AUDIO]: WebChatIcon.attachment_audio,\n [ThumbnailContentType.VIDEO]: WebChatIcon.attachment_video,\n [ThumbnailContentType.DOCUMENT]: WebChatIcon.attachment_document,\n [ThumbnailContentType.OTHER]: WebChatIcon.attachment\n};\n\nexport class Thumbnail extends RapidElement {\n static get styles() {\n return css`\n :host {\n display: inline;\n }\n\n .wrapper {\n padding: var(--thumb-padding, 0.4em);\n background: var(--thumb-background, #fff);\n box-shadow: var(--widget-box-shadow);\n cursor: pointer;\n border-radius: calc(var(--curvature) * 1.5);\n border: 0px solid #f3f3f3;\n }\n\n .wrapper.zoom {\n border: none;\n padding: 0 !important;\n border-radius: 0 !important;\n overflow: hidden !important;\n }\n\n .zoom .thumb {\n border-radius: 0px !important;\n width: calc(var(--thumb-size, 4em) + 0.8em);\n max-height: calc(90vh - 10em);\n }\n\n .thumb {\n background-size: cover;\n background-position: center;\n background-repeat: no-repeat;\n border-radius: var(--curvature);\n max-height: calc(var(--thumb-size, 4em) * 2);\n max-width: calc(var(--thumb-size, 4em) * 2);\n height: var(--thumb-size, 4em);\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: 400;\n color: var(--thumb-icon, #bbb);\n }\n\n .thumb.document,\n .thumb.video {\n border: 1px solid #eee;\n }\n\n .audio-player {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 6px 8px;\n background: rgba(0, 0, 0, 0.05);\n border-radius: var(--curvature);\n cursor: default;\n }\n\n .audio-play-btn {\n cursor: pointer;\n color: #666;\n display: flex;\n align-items: center;\n flex-shrink: 0;\n }\n\n .audio-play-btn:hover {\n color: #333;\n }\n\n .audio-progress-bar {\n flex: 1;\n height: 3px;\n background: #ddd;\n border-radius: 2px;\n overflow: hidden;\n min-width: 60px;\n cursor: pointer;\n }\n\n .audio-progress-fill {\n height: 100%;\n background: var(--color-primary, #2387ca);\n border-radius: 2px;\n transition: width 0.15s linear;\n }\n\n .audio-time {\n font-size: 11px;\n color: #999;\n flex-shrink: 0;\n min-width: 28px;\n text-align: right;\n }\n\n .wrapper:hover .thumb.icon {\n }\n\n .viewer {\n display: block;\n }\n\n .zoom .viewer {\n display: block;\n }\n\n .download {\n display: none;\n position: absolute;\n right: 0em;\n bottom: 0em;\n border-radius: var(--curvature);\n transform: scale(0.2) translate(3em, 3em);\n padding: 0.4em;\n }\n\n .zoom .download {\n display: block;\n background: rgba(0, 0, 0, 0.5);\n }\n\n .zoom .download:hover {\n background: rgba(0, 0, 0, 0.6);\n cursor: pointer;\n }\n `;\n }\n\n @property({ type: String })\n url: string;\n\n @property({ type: String })\n attachment: string;\n\n @property({ type: Number })\n ratio: number = 0;\n\n @property({ type: Boolean })\n preview: boolean = true;\n\n @property({ type: Boolean, attribute: false })\n zoom: boolean;\n\n @property({ type: String, attribute: true })\n contentType: string;\n\n @property({ type: Number, attribute: false })\n latitude: number;\n\n @property({ type: Number, attribute: false })\n longitude: number;\n\n // cached tile URL for location thumbnails\n @property({ type: String, attribute: false })\n private tileUrl: string = '';\n\n // audio player state\n private audio: HTMLAudioElement | null = null;\n\n @state()\n private audioPlaying = false;\n\n @state()\n private audioProgress = 0;\n\n @state()\n private audioDuration = 0;\n\n private handleAudioPlayClick(e: Event) {\n e.stopPropagation();\n\n if (!this.audio) {\n this.audio = new Audio(this.url);\n this.audio.addEventListener('timeupdate', () => {\n if (this.audio.duration) {\n this.audioProgress = this.audio.currentTime / this.audio.duration;\n this.audioDuration = this.audio.duration;\n }\n });\n this.audio.addEventListener('ended', () => {\n this.audioPlaying = false;\n this.audioProgress = 0;\n });\n this.audio.addEventListener('error', () => {\n this.audioPlaying = false;\n this.audioProgress = 0;\n });\n }\n\n if (this.audioPlaying) {\n this.audio.pause();\n this.audioPlaying = false;\n } else {\n this.audio.play().catch(() => {\n this.audioPlaying = false;\n });\n this.audioPlaying = true;\n }\n }\n\n private handleProgressClick(e: MouseEvent) {\n e.stopPropagation();\n if (!this.audio || !this.audio.duration) return;\n const bar = e.currentTarget as HTMLElement;\n const rect = bar.getBoundingClientRect();\n const pct = (e.clientX - rect.left) / rect.width;\n this.audio.currentTime = pct * this.audio.duration;\n }\n\n private formatTime(seconds: number): string {\n const s = Math.floor(seconds);\n const m = Math.floor(s / 60);\n const rem = s % 60;\n return `${m}:${rem.toString().padStart(2, '0')}`;\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n if (this.audio) {\n this.audio.pause();\n this.audio = null;\n }\n }\n\n // convert lat/lng to tile coordinates for OSM\n private latLngToTile(lat: number, lng: number, zoom: number) {\n const n = Math.pow(2, zoom);\n const x = Math.floor(((lng + 180) / 360) * n);\n const latRad = (lat * Math.PI) / 180;\n const y = Math.floor(\n ((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2) *\n n\n );\n return { x, y, z: zoom };\n }\n\n protected updated(\n changes: PropertyValueMap<any> | Map<PropertyKey, unknown>\n ): void {\n super.updated(changes);\n\n if (\n changes.has('contentType') &&\n this.contentType === ThumbnailContentType.IMAGE\n ) {\n const toObserve = this.shadowRoot.querySelector('.observe');\n if (toObserve) {\n new ResizeObserver((e, observer) => {\n if (toObserve.clientHeight > 0 && toObserve.clientWidth > 0) {\n this.ratio = toObserve.clientHeight / toObserve.clientWidth;\n this.preview =\n this.ratio === 0 || (this.ratio > 0.25 && this.ratio <= 1.5);\n observer.disconnect();\n }\n }).observe(toObserve);\n }\n }\n\n // convert our attachment to a url and label\n if (changes.has('attachment')) {\n if (this.attachment) {\n const splitIndex = this.attachment.indexOf(':');\n if (splitIndex === -1) {\n this.url = this.attachment;\n } else {\n const contentType = this.attachment.substring(0, splitIndex);\n this.url = this.attachment.substring(splitIndex + 1);\n\n if (contentType.startsWith('image')) {\n this.contentType = ThumbnailContentType.IMAGE;\n } else if (contentType.startsWith('audio')) {\n this.contentType = ThumbnailContentType.AUDIO;\n } else if (contentType.startsWith('video')) {\n this.contentType = ThumbnailContentType.VIDEO;\n } else if (contentType.startsWith('application')) {\n this.contentType = ThumbnailContentType.DOCUMENT;\n } else if (contentType.startsWith('geo')) {\n this.contentType = ThumbnailContentType.LOCATION;\n // Parse coordinates from URL which is already stripped of \"geo:\" prefix\n // Format is now just: lat,lng\n const coords = this.url.match(/^([^,]+),([^,]+)/);\n if (coords) {\n this.latitude = parseFloat(coords[1]);\n this.longitude = parseFloat(coords[2]);\n }\n } else {\n this.contentType = ThumbnailContentType.OTHER;\n }\n }\n }\n }\n\n // calculate tile URL when latitude/longitude changes\n if (changes.has('latitude') || changes.has('longitude')) {\n if (\n this.latitude !== undefined &&\n this.longitude !== undefined &&\n !isNaN(this.latitude) &&\n !isNaN(this.longitude)\n ) {\n const tile = this.latLngToTile(this.latitude, this.longitude, 13);\n this.tileUrl = `https://tile.openstreetmap.org/${tile.z}/${tile.x}/${tile.y}.png`;\n } else {\n this.tileUrl = '';\n }\n }\n }\n\n public handleThumbnailClicked() {\n if (this.contentType === ThumbnailContentType.IMAGE && this.preview) {\n window.setTimeout(() => {\n const lightbox = document.querySelector('temba-lightbox') as Lightbox;\n lightbox.showElement(this);\n }, 100);\n } else if (this.contentType === ThumbnailContentType.LOCATION) {\n // open location in openstreetmap\n const osmUrl = `https://www.openstreetmap.org/?mlat=${this.latitude}&mlon=${this.longitude}#map=15/${this.latitude}/${this.longitude}`;\n window.open(osmUrl, '_blank');\n } else if (this.contentType === ThumbnailContentType.AUDIO) {\n // audio has inline controls, no click action needed\n } else {\n window.open(this.url, '_blank');\n }\n }\n\n public handleDownload(e: Event) {\n e.stopPropagation();\n e.preventDefault();\n\n // open this.url in another tab\n window.open(this.url, '_blank');\n }\n\n public render() {\n return html`\n <div\n @click=${this.handleThumbnailClicked.bind(this)}\n class=\"${getClasses({ wrapper: true, zoom: this.zoom })}\"\n style=\"${this.contentType === ThumbnailContentType.AUDIO\n ? 'cursor: default;'\n : ''}\"\n url=${this.url}\n >\n ${this.contentType === ThumbnailContentType.IMAGE && this.preview\n ? html`<div class=\"\"><div class=\"download\" @click=${this.handleDownload.bind(\n this\n )}><temba-icon size=\"1\" style=\"color:#fff;\" name=\"download\"></temba-icon></div><img\n class=\"observe thumb ${this.contentType}\"\n src=\"${this.url}\"\n ></img></div>`\n : this.contentType === ThumbnailContentType.AUDIO\n ? html`<div class=\"audio-player\">\n <div class=\"audio-play-btn\" @click=${this.handleAudioPlayClick}>\n ${this.audioPlaying\n ? html`<svg\n viewBox=\"0 0 24 24\"\n width=\"14\"\n height=\"14\"\n fill=\"currentColor\"\n >\n <rect x=\"5\" y=\"3\" width=\"4\" height=\"18\" />\n <rect x=\"15\" y=\"3\" width=\"4\" height=\"18\" />\n </svg>`\n : html`<svg\n viewBox=\"0 0 24 24\"\n width=\"14\"\n height=\"14\"\n fill=\"currentColor\"\n >\n <polygon points=\"6,3 20,12 6,21\" />\n </svg>`}\n </div>\n <div\n class=\"audio-progress-bar\"\n @click=${this.handleProgressClick}\n >\n <div\n class=\"audio-progress-fill\"\n style=\"width: ${this.audioProgress * 100}%\"\n ></div>\n </div>\n <div class=\"audio-time\">\n ${this.audioDuration\n ? this.formatTime(\n this.audioPlaying || this.audioProgress > 0\n ? this.audio?.currentTime || 0\n : this.audioDuration\n )\n : ''}\n </div>\n </div>`\n : html`\n ${this.contentType === ThumbnailContentType.LOCATION\n ? html`<img\n style=\"height:125px;margin-bottom:-3px;border-radius:var(--curvature);\"\n src=\"${this.tileUrl}\"\n alt=\"Location preview\"\n />`\n : html`<div\n style=\"padding:1em; background:rgba(0,0,0,.05);border-radius:var(--curvature);\"\n >\n <temba-icon\n size=\"1.5\"\n name=\"${ThumbnailIcons[this.contentType]}\"\n ></temba-icon>\n </div>`}\n `}\n </div>\n `;\n }\n}\n"]}
@@ -111,6 +111,12 @@ export class CanvasNode extends RapidElement {
111
111
  pointer-events: none !important;
112
112
  }
113
113
 
114
+ /* Issue indicators - hatched red title bar */
115
+ .action-content.has-issues .cn-title,
116
+ .node.has-issues > .router .cn-title {
117
+ background: repeating-linear-gradient(120deg, tomato, tomato 6px, #ff7056 0, #ff7056 18px) !important;
118
+ }
119
+
114
120
  .action.sortable {
115
121
  display: flex;
116
122
  align-items: stretch;
@@ -472,10 +478,14 @@ export class CanvasNode extends RapidElement {
472
478
  // make our initial connections
473
479
  // We use setTimeout to allow for DOM updates to complete before querying for exits
474
480
  this.connectionTimeout = window.setTimeout(() => {
475
- for (const exit of this.node.exits) {
476
- this.plumber.makeSource(exit.uuid);
477
- if (exit.destination_uuid) {
478
- this.plumber.connectIds(this.node.uuid, exit.uuid, exit.destination_uuid);
481
+ var _b;
482
+ // Terminal nodes have no visible exits
483
+ if (((_b = this.ui) === null || _b === void 0 ? void 0 : _b.type) !== 'terminal') {
484
+ for (const exit of this.node.exits) {
485
+ this.plumber.makeSource(exit.uuid);
486
+ if (exit.destination_uuid) {
487
+ this.plumber.connectIds(this.node.uuid, exit.uuid, exit.destination_uuid);
488
+ }
479
489
  }
480
490
  }
481
491
  // Note: revalidation is handled by plumber's processPendingConnections which calls repaintEverything
@@ -771,6 +781,10 @@ export class CanvasNode extends RapidElement {
771
781
  }
772
782
  this.requestUpdate();
773
783
  }
784
+ getTopCenter(el) {
785
+ const rect = el.getBoundingClientRect();
786
+ return { x: rect.left + rect.width / 2, y: rect.top };
787
+ }
774
788
  handleActionMouseDown(event, action) {
775
789
  // Don't handle clicks on the remove button, drag handle, or when action is in removing state
776
790
  const target = event.target;
@@ -812,10 +826,17 @@ export class CanvasNode extends RapidElement {
812
826
  // Only fire the action edit event if we haven't dragged beyond the threshold
813
827
  // AND either there's no Editor parent (test case) or the Editor didn't drag the node
814
828
  if (distance <= DRAG_THRESHOLD && (!editor || !editorWasDragging)) {
829
+ // Use top-center of the action element as the dialog origin
830
+ const actionEl = event.currentTarget;
831
+ const origin = actionEl
832
+ ? this.getTopCenter(actionEl)
833
+ : { x: event.clientX, y: event.clientY };
815
834
  // Fire event to request action editing
816
835
  this.fireCustomEvent(CustomEventType.ActionEditRequested, {
817
836
  action,
818
- nodeUuid: this.node.uuid
837
+ nodeUuid: this.node.uuid,
838
+ originX: origin.x,
839
+ originY: origin.y
819
840
  });
820
841
  }
821
842
  }
@@ -914,18 +935,24 @@ export class CanvasNode extends RapidElement {
914
935
  // Using literal 5 instead of DRAG_THRESHOLD since it's not imported
915
936
  // Fire event to request node editing if the node has a router
916
937
  if (this.node.router) {
938
+ // Use top-center of the node as the dialog origin
939
+ const origin = this.getTopCenter(this);
917
940
  // If router node has exactly one action, open the action editor directly
918
941
  if (this.node.actions && this.node.actions.length === 1) {
919
942
  this.fireCustomEvent(CustomEventType.ActionEditRequested, {
920
943
  action: this.node.actions[0],
921
- nodeUuid: this.node.uuid
944
+ nodeUuid: this.node.uuid,
945
+ originX: origin.x,
946
+ originY: origin.y
922
947
  });
923
948
  }
924
949
  else {
925
950
  // Otherwise open the node editor as before
926
951
  this.fireCustomEvent(CustomEventType.NodeEditRequested, {
927
952
  node: this.node,
928
- nodeUI: this.ui
953
+ nodeUI: this.ui,
954
+ originX: origin.x,
955
+ originY: origin.y
929
956
  });
930
957
  }
931
958
  }
@@ -1061,17 +1088,15 @@ export class CanvasNode extends RapidElement {
1061
1088
  this.requestUpdate();
1062
1089
  }
1063
1090
  renderTitle(config, action, index, isRemoving = false) {
1064
- var _b, _c, _d, _e;
1091
+ var _b, _c, _d, _e, _f;
1065
1092
  const color = config.group
1066
1093
  ? (_b = ACTION_GROUP_METADATA[config.group]) === null || _b === void 0 ? void 0 : _b.color
1067
1094
  : '#aaaaaa';
1095
+ const isTerminal = ((_c = this.ui) === null || _c === void 0 ? void 0 : _c.type) === 'terminal';
1068
1096
  return html `<div class="cn-title" style="background:${color}">
1069
- ${((_c = this.ui) === null || _c === void 0 ? void 0 : _c.type) === 'execute_actions'
1070
- ? html `<temba-icon
1071
- class="drag-handle ${this.isReadOnly() ? 'read-only-hidden' : ''}"
1072
- name="sort"
1073
- ></temba-icon>`
1074
- : ((_e = (_d = this.node) === null || _d === void 0 ? void 0 : _d.actions) === null || _e === void 0 ? void 0 : _e.length) > 1
1097
+ ${isTerminal
1098
+ ? html `<div class="title-spacer"></div>`
1099
+ : ((_d = this.ui) === null || _d === void 0 ? void 0 : _d.type) === 'execute_actions' || ((_f = (_e = this.node) === null || _e === void 0 ? void 0 : _e.actions) === null || _f === void 0 ? void 0 : _f.length) > 1
1075
1100
  ? html `<temba-icon
1076
1101
  class="drag-handle ${this.isReadOnly() ? 'read-only-hidden' : ''}"
1077
1102
  name="sort"
@@ -1080,7 +1105,9 @@ export class CanvasNode extends RapidElement {
1080
1105
 
1081
1106
  <div class="name">${isRemoving ? 'Remove?' : config.name}</div>
1082
1107
  <div
1083
- class="remove-button ${this.isReadOnly() ? 'read-only-hidden' : ''}"
1108
+ class="remove-button ${isTerminal || this.isReadOnly()
1109
+ ? 'read-only-hidden'
1110
+ : ''}"
1084
1111
  @click=${(e) => this.handleActionRemoveClick(e, action, index)}
1085
1112
  title="Remove action"
1086
1113
  >
@@ -1165,7 +1192,7 @@ export class CanvasNode extends RapidElement {
1165
1192
  return localizedAction;
1166
1193
  }
1167
1194
  renderAction(node, action, index) {
1168
- var _b, _c, _d;
1195
+ var _b, _c, _d, _e;
1169
1196
  const config = ACTION_CONFIG[action.type];
1170
1197
  const isRemoving = this.actionRemovingState.has(action.uuid);
1171
1198
  const isLocalizable = (config === null || config === void 0 ? void 0 : config.localizable) && config.localizable.length > 0;
@@ -1176,6 +1203,7 @@ export class CanvasNode extends RapidElement {
1176
1203
  // Get the localized action if translating
1177
1204
  const displayAction = this.getLocalizedAction(action);
1178
1205
  if (config) {
1206
+ const hasIssues = (_e = this.issuesByAction) === null || _e === void 0 ? void 0 : _e.has(action.uuid);
1179
1207
  const classes = [
1180
1208
  'action',
1181
1209
  'sortable',
@@ -1189,7 +1217,7 @@ export class CanvasNode extends RapidElement {
1189
1217
  .join(' ');
1190
1218
  return html `<div class="${classes}" id="action-${index}">
1191
1219
  <div
1192
- class="action-content"
1220
+ class="action-content ${hasIssues ? 'has-issues' : ''}"
1193
1221
  @mousedown=${(e) => !isDisabled && this.handleActionMouseDown(e, action)}
1194
1222
  @mouseup=${(e) => !isDisabled && this.handleActionMouseUp(e, action)}
1195
1223
  style="cursor: ${isDisabled ? 'not-allowed' : 'pointer'}"
@@ -1320,7 +1348,7 @@ export class CanvasNode extends RapidElement {
1320
1348
  return this.viewingRevision || this.isTranslating;
1321
1349
  }
1322
1350
  render() {
1323
- var _b;
1351
+ var _b, _c, _d, _e;
1324
1352
  if (!this.node || !this.ui) {
1325
1353
  return html `<div class="node">Loading...</div>`;
1326
1354
  }
@@ -1332,13 +1360,17 @@ export class CanvasNode extends RapidElement {
1332
1360
  !this.includeCategoriesInTranslation;
1333
1361
  // Get active contact count for this node
1334
1362
  const activeCount = (((_b = this.activity) === null || _b === void 0 ? void 0 : _b.nodes) && this.activity.nodes[this.node.uuid]) || 0;
1363
+ // Check for node-level issues or action-level issues on any action in this node
1364
+ const nodeHasIssues = ((_c = this.issuesByNode) === null || _c === void 0 ? void 0 : _c.has(this.node.uuid)) ||
1365
+ ((_d = this.node.actions) === null || _d === void 0 ? void 0 : _d.some((a) => { var _b; return (_b = this.issuesByAction) === null || _b === void 0 ? void 0 : _b.has(a.uuid); }));
1335
1366
  return html `
1336
1367
  <div
1337
1368
  id="${this.node.uuid}"
1338
1369
  class=${getClasses({
1339
1370
  node: true,
1340
1371
  'execute-actions': this.ui.type === 'execute_actions',
1341
- 'non-localizable': isNodeDisabled
1372
+ 'non-localizable': isNodeDisabled,
1373
+ 'has-issues': nodeHasIssues
1342
1374
  })}
1343
1375
  style="left:${this.ui.position.left}px;top:${this.ui.position.top}px"
1344
1376
  >
@@ -1347,7 +1379,9 @@ export class CanvasNode extends RapidElement {
1347
1379
  ${activeCount.toLocaleString()}
1348
1380
  </div>`
1349
1381
  : ''}
1350
- ${nodeConfig && nodeConfig.type !== 'execute_actions'
1382
+ ${nodeConfig &&
1383
+ nodeConfig.type !== 'execute_actions' &&
1384
+ nodeConfig.type !== 'terminal'
1351
1385
  ? html `<div class="router" style="position: relative;">
1352
1386
  <div
1353
1387
  @mousedown=${(e) => this.handleNodeMouseDown(e)}
@@ -1360,7 +1394,7 @@ export class CanvasNode extends RapidElement {
1360
1394
  : null}
1361
1395
  </div>
1362
1396
  </div>`
1363
- : this.node.actions.length > 0
1397
+ : ((_e = this.node.actions) === null || _e === void 0 ? void 0 : _e.length) > 0
1364
1398
  ? this.ui.type === 'execute_actions'
1365
1399
  ? html `<temba-sortable-list
1366
1400
  dragHandle="drag-handle"
@@ -1388,7 +1422,9 @@ export class CanvasNode extends RapidElement {
1388
1422
  ${this.renderRouter(this.node.router, this.ui)}
1389
1423
  ${this.renderCategories(this.node)}
1390
1424
  </div>`
1391
- : html `<div class="action-exits">
1425
+ : this.ui.type === 'terminal'
1426
+ ? ''
1427
+ : html `<div class="action-exits">
1392
1428
  ${repeat(this.node.exits, (exit) => exit.uuid, (exit) => this.renderExit(exit))}
1393
1429
  </div>`}
1394
1430
  ${this.ui.type === 'execute_actions' && !this.isReadOnly()
@@ -1431,4 +1467,10 @@ __decorate([
1431
1467
  __decorate([
1432
1468
  fromStore(zustand, (state) => state.getCurrentActivity())
1433
1469
  ], CanvasNode.prototype, "activity", void 0);
1470
+ __decorate([
1471
+ fromStore(zustand, (state) => state.issuesByNode)
1472
+ ], CanvasNode.prototype, "issuesByNode", void 0);
1473
+ __decorate([
1474
+ fromStore(zustand, (state) => state.issuesByAction)
1475
+ ], CanvasNode.prototype, "issuesByAction", void 0);
1434
1476
  //# sourceMappingURL=CanvasNode.js.map