@nyaruka/temba-components 0.135.9 → 0.136.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 (63) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/demo/components/webchat/example.html +4 -2
  3. package/dist/static/svg/index.svg +1 -1
  4. package/dist/temba-components.js +1323 -317
  5. package/dist/temba-components.js.map +1 -1
  6. package/out-tsc/src/Icons.js +2 -1
  7. package/out-tsc/src/Icons.js.map +1 -1
  8. package/out-tsc/src/flow/CanvasNode.js +11 -0
  9. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  10. package/out-tsc/src/flow/Editor.js +224 -2
  11. package/out-tsc/src/flow/Editor.js.map +1 -1
  12. package/out-tsc/src/flow/Plumber.js +320 -1
  13. package/out-tsc/src/flow/Plumber.js.map +1 -1
  14. package/out-tsc/src/interfaces.js +1 -0
  15. package/out-tsc/src/interfaces.js.map +1 -1
  16. package/out-tsc/src/layout/FloatingWindow.js +30 -8
  17. package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
  18. package/out-tsc/src/simulator/Simulator.js +1827 -0
  19. package/out-tsc/src/simulator/Simulator.js.map +1 -0
  20. package/out-tsc/src/store/AppState.js +33 -0
  21. package/out-tsc/src/store/AppState.js.map +1 -1
  22. package/out-tsc/src/utils.js +48 -0
  23. package/out-tsc/src/utils.js.map +1 -1
  24. package/out-tsc/temba-modules.js +2 -0
  25. package/out-tsc/temba-modules.js.map +1 -1
  26. package/out-tsc/test/temba-flow-editor.test.js +1 -1
  27. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  28. package/out-tsc/test/temba-flow-plumber-connections.test.js +3 -1
  29. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  30. package/out-tsc/test/temba-flow-plumber.test.js +3 -1
  31. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  32. package/out-tsc/test/temba-simulator.test.js +642 -0
  33. package/out-tsc/test/temba-simulator.test.js.map +1 -0
  34. package/out-tsc/test/utils.test.js +1 -1
  35. package/out-tsc/test/utils.test.js.map +1 -1
  36. package/package.json +1 -1
  37. package/screenshots/truth/simulator/after-message-sent.png +0 -0
  38. package/screenshots/truth/simulator/after-reset.png +0 -0
  39. package/screenshots/truth/simulator/attachment-menu.png +0 -0
  40. package/screenshots/truth/simulator/context-expanded.png +0 -0
  41. package/screenshots/truth/simulator/context-explorer-open.png +0 -0
  42. package/screenshots/truth/simulator/event-info.png +0 -0
  43. package/screenshots/truth/simulator/image-attachment.png +0 -0
  44. package/screenshots/truth/simulator/open-initial.png +0 -0
  45. package/screenshots/truth/simulator/quick-replies.png +0 -0
  46. package/src/Icons.ts +2 -1
  47. package/src/flow/CanvasNode.ts +12 -0
  48. package/src/flow/Editor.ts +240 -1
  49. package/src/flow/Plumber.ts +371 -2
  50. package/src/interfaces.ts +2 -1
  51. package/src/layout/FloatingWindow.ts +36 -11
  52. package/src/simulator/Simulator.ts +2008 -0
  53. package/src/store/AppState.ts +53 -0
  54. package/src/utils.ts +53 -0
  55. package/static/svg/index.svg +1 -1
  56. package/static/svg/work/traced/route.svg +1 -0
  57. package/static/svg/work/used/route.svg +3 -0
  58. package/temba-modules.ts +2 -0
  59. package/test/temba-flow-editor.test.ts +1 -1
  60. package/test/temba-flow-plumber-connections.test.ts +4 -1
  61. package/test/temba-flow-plumber.test.ts +4 -1
  62. package/test/temba-simulator.test.ts +866 -0
  63. package/test/utils.test.ts +1 -1
@@ -169,6 +169,125 @@ export class Editor extends RapidElement {
169
169
  z-index: 10;
170
170
  }
171
171
 
172
+ /* Activity overlays on connections */
173
+ .jtk-overlay.activity-overlay {
174
+ background: #f3f3f3;
175
+ border: 1px solid #d9d9d9;
176
+ color: #333;
177
+ border-radius: 4px;
178
+ padding: 2px 4px;
179
+ font-size: 10px;
180
+ font-weight: 600;
181
+ line-height: 0.9;
182
+ cursor: pointer;
183
+ z-index: 500;
184
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
185
+ }
186
+
187
+ /* Active contact count on nodes */
188
+ .active-count {
189
+ position: absolute;
190
+ background: var(--color-primary-dark, #3498db);
191
+ border: 1px solid var(--color-primary-darker, #2980b9);
192
+ border-radius: 12px;
193
+ padding: 3px 5px;
194
+ color: #fff;
195
+ font-weight: 500;
196
+ top: -10px;
197
+ left: -10px;
198
+ font-size: 13px;
199
+ min-width: 22px;
200
+ text-align: center;
201
+ z-index: 600;
202
+ line-height: 1;
203
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
204
+ }
205
+
206
+ /* Recent contacts popup */
207
+ @keyframes popupBounceIn {
208
+ 0% {
209
+ transform: scale(0.8);
210
+ opacity: 0;
211
+ }
212
+ 50% {
213
+ transform: scale(1.05);
214
+ }
215
+ 100% {
216
+ transform: scale(1);
217
+ opacity: 1;
218
+ }
219
+ }
220
+
221
+ .recent-contacts-popup {
222
+ display: none;
223
+ position: absolute;
224
+ width: 200px;
225
+ background: #f3f3f3;
226
+ border-radius: 10px;
227
+ box-shadow: 0 1px 3px 1px rgba(130, 130, 130, 0.2);
228
+ z-index: 1015;
229
+ transform-origin: top center;
230
+ }
231
+
232
+ .recent-contacts-popup.show {
233
+ display: block;
234
+ animation: popupBounceIn 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
235
+ }
236
+
237
+ .recent-contacts-popup .popup-title {
238
+ background: #999;
239
+ color: #fff;
240
+ padding: 6px 0;
241
+ text-align: center;
242
+ border-top-left-radius: 10px;
243
+ border-top-right-radius: 10px;
244
+ font-size: 12px;
245
+ }
246
+
247
+ .recent-contacts-popup .no-contacts-message {
248
+ padding: 15px;
249
+ text-align: center;
250
+ color: #999;
251
+ font-size: 12px;
252
+ }
253
+
254
+ .recent-contacts-popup .contact-row {
255
+ padding: 8px 10px;
256
+ border-top: 1px solid #e0e0e0;
257
+ text-align: left;
258
+ }
259
+
260
+ .recent-contacts-popup .contact-row:last-child {
261
+ border-bottom-left-radius: 10px;
262
+ border-bottom-right-radius: 10px;
263
+ }
264
+
265
+ .recent-contacts-popup .contact-name {
266
+ display: block;
267
+ font-weight: 500;
268
+ font-size: 12px;
269
+ color: var(--color-link-primary, #1d4ed8);
270
+ cursor: pointer;
271
+ }
272
+
273
+ .recent-contacts-popup .contact-name:hover {
274
+ text-decoration: underline;
275
+ color: var(--color-link-primary, #1d4ed8);
276
+ }
277
+
278
+ .recent-contacts-popup .contact-operand {
279
+ padding-top: 3px;
280
+ font-size: 11px;
281
+ color: #666;
282
+ word-wrap: break-word;
283
+ }
284
+
285
+ .recent-contacts-popup .contact-time {
286
+ padding-top: 3px;
287
+ font-size: 10px;
288
+ color: #999;
289
+ }
290
+
172
291
  /* Connection dragging feedback */
173
292
  body svg.jtk-connector.jtk-dragging {
174
293
  z-index: 99999 !important;
@@ -399,6 +518,8 @@ export class Editor extends RapidElement {
399
518
  this.saveTimer = null;
400
519
  this.flowType = 'message';
401
520
  this.features = [];
521
+ this.activityTimer = null;
522
+ this.activityInterval = 100; // Start with 100ms interval for fast initial load
402
523
  // Drag state
403
524
  this.isDragging = false;
404
525
  this.isMouseDown = false;
@@ -446,7 +567,7 @@ export class Editor extends RapidElement {
446
567
  }
447
568
  firstUpdated(changes) {
448
569
  super.firstUpdated(changes);
449
- this.plumber = new Plumber(this.querySelector('#canvas'));
570
+ this.plumber = new Plumber(this.querySelector('#canvas'), this);
450
571
  this.setupGlobalEventListeners();
451
572
  if (changes.has('flow')) {
452
573
  getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
@@ -484,7 +605,7 @@ export class Editor extends RapidElement {
484
605
  this.isValidTarget = true;
485
606
  }
486
607
  updated(changes) {
487
- var _b, _c, _d;
608
+ var _b, _c, _d, _e;
488
609
  super.updated(changes);
489
610
  if (changes.has('canvasSize')) {
490
611
  // console.log('Setting canvas size', this.canvasSize);
@@ -505,6 +626,27 @@ export class Editor extends RapidElement {
505
626
  this.translationFilters = normalizedFilters;
506
627
  }
507
628
  this.translationCache.clear();
629
+ // Start fetching activity data when definition is loaded
630
+ if ((_e = this.definition) === null || _e === void 0 ? void 0 : _e.uuid) {
631
+ this.startActivityFetching();
632
+ }
633
+ }
634
+ if (changes.has('simulatorActive')) {
635
+ if (this.simulatorActive) {
636
+ // Stop polling when simulator becomes active
637
+ this.stopActivityFetching();
638
+ }
639
+ else {
640
+ // Resume polling and refresh activity when simulator closes
641
+ this.activityInterval = 100; // Reset to fast initial interval
642
+ this.startActivityFetching();
643
+ }
644
+ }
645
+ if (changes.has('activityData')) {
646
+ // Update plumber with new activity data
647
+ if (this.plumber) {
648
+ this.plumber.setActivityData(this.activityData);
649
+ }
508
650
  }
509
651
  if (changes.has('dirtyDate')) {
510
652
  if (this.dirtyDate) {
@@ -572,6 +714,46 @@ export class Editor extends RapidElement {
572
714
  });
573
715
  getStore().getState().setDirtyDate(null);
574
716
  }
717
+ startActivityFetching() {
718
+ // Don't start if simulator is active
719
+ if (this.simulatorActive) {
720
+ return;
721
+ }
722
+ // Fetch immediately
723
+ this.fetchActivityData();
724
+ }
725
+ stopActivityFetching() {
726
+ if (this.activityTimer !== null) {
727
+ clearTimeout(this.activityTimer);
728
+ this.activityTimer = null;
729
+ }
730
+ }
731
+ fetchActivityData() {
732
+ var _b;
733
+ if (!((_b = this.definition) === null || _b === void 0 ? void 0 : _b.uuid)) {
734
+ return;
735
+ }
736
+ // Don't fetch if simulator is active
737
+ if (this.simulatorActive) {
738
+ return;
739
+ }
740
+ const activityEndpoint = `/flow/activity/${this.definition.uuid}/`;
741
+ const store = getStore();
742
+ if (!store) {
743
+ return;
744
+ }
745
+ const state = store.getState();
746
+ state.fetchActivity(activityEndpoint).then(() => {
747
+ // Schedule next fetch with exponential backoff (max 5 minutes)
748
+ this.activityInterval = Math.min(60000 * 5, this.activityInterval + 100);
749
+ if (this.activityTimer !== null) {
750
+ clearTimeout(this.activityTimer);
751
+ }
752
+ this.activityTimer = window.setTimeout(() => {
753
+ this.fetchActivityData();
754
+ }, this.activityInterval);
755
+ });
756
+ }
575
757
  handleLanguageChange(languageCode) {
576
758
  zustand.getState().setLanguageCode(languageCode);
577
759
  // Repaint connections after language change since node sizes can change
@@ -587,6 +769,10 @@ export class Editor extends RapidElement {
587
769
  clearTimeout(this.saveTimer);
588
770
  this.saveTimer = null;
589
771
  }
772
+ if (this.activityTimer !== null) {
773
+ clearTimeout(this.activityTimer);
774
+ this.activityTimer = null;
775
+ }
590
776
  document.removeEventListener('mousemove', this.boundMouseMove);
591
777
  document.removeEventListener('mouseup', this.boundMouseUp);
592
778
  document.removeEventListener('mousedown', this.boundGlobalMouseDown);
@@ -2264,6 +2450,36 @@ export class Editor extends RapidElement {
2264
2450
  ></temba-floating-tab>
2265
2451
  `;
2266
2452
  }
2453
+ /**
2454
+ * Focus on a specific node by smoothly scrolling it to the center of the canvas
2455
+ */
2456
+ focusNode(nodeUuid) {
2457
+ const nodeElement = this.querySelector(`temba-flow-node[uuid="${nodeUuid}"]`);
2458
+ if (!nodeElement) {
2459
+ return;
2460
+ }
2461
+ const editor = this.querySelector('#editor');
2462
+ if (!editor) {
2463
+ return;
2464
+ }
2465
+ // Get the editor's dimensions and scroll position
2466
+ const editorRect = editor.getBoundingClientRect();
2467
+ const editorCenterX = editorRect.width / 2;
2468
+ const editorCenterY = editorRect.height / 2;
2469
+ // Get node position relative to the editor's scroll container
2470
+ const nodeRect = nodeElement.getBoundingClientRect();
2471
+ const nodeCenterX = nodeElement.offsetLeft + nodeRect.width / 2;
2472
+ const nodeCenterY = nodeElement.offsetTop + nodeRect.height / 2;
2473
+ // Calculate the scroll position needed to center the node
2474
+ const targetScrollX = nodeCenterX - editorCenterX;
2475
+ const targetScrollY = nodeCenterY - editorCenterY;
2476
+ // Smooth scroll the editor container to the target position
2477
+ editor.scrollTo({
2478
+ left: Math.max(0, targetScrollX),
2479
+ top: Math.max(0, targetScrollY),
2480
+ behavior: 'smooth'
2481
+ });
2482
+ }
2267
2483
  render() {
2268
2484
  var _b, _c;
2269
2485
  // we have to embed our own style since we are in light DOM
@@ -2364,6 +2580,9 @@ __decorate([
2364
2580
  __decorate([
2365
2581
  fromStore(zustand, (state) => state.flowDefinition)
2366
2582
  ], Editor.prototype, "definition", void 0);
2583
+ __decorate([
2584
+ fromStore(zustand, (state) => state.simulatorActive)
2585
+ ], Editor.prototype, "simulatorActive", void 0);
2367
2586
  __decorate([
2368
2587
  fromStore(zustand, (state) => state.canvasSize)
2369
2588
  ], Editor.prototype, "canvasSize", void 0);
@@ -2379,6 +2598,9 @@ __decorate([
2379
2598
  __decorate([
2380
2599
  fromStore(zustand, (state) => state.workspace)
2381
2600
  ], Editor.prototype, "workspace", void 0);
2601
+ __decorate([
2602
+ fromStore(zustand, (state) => state.getCurrentActivity())
2603
+ ], Editor.prototype, "activityData", void 0);
2382
2604
  __decorate([
2383
2605
  state()
2384
2606
  ], Editor.prototype, "isDragging", void 0);