@nyaruka/temba-components 0.135.9 → 0.136.1

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 (144) hide show
  1. package/CHANGELOG.md +25 -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 +1351 -322
  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/display/FloatingTab.js +2 -6
  9. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  10. package/out-tsc/src/flow/CanvasNode.js +29 -1
  11. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  12. package/out-tsc/src/flow/Editor.js +229 -5
  13. package/out-tsc/src/flow/Editor.js.map +1 -1
  14. package/out-tsc/src/flow/Plumber.js +320 -1
  15. package/out-tsc/src/flow/Plumber.js.map +1 -1
  16. package/out-tsc/src/interfaces.js +1 -0
  17. package/out-tsc/src/interfaces.js.map +1 -1
  18. package/out-tsc/src/layout/FloatingWindow.js +30 -8
  19. package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
  20. package/out-tsc/src/simulator/Simulator.js +1861 -0
  21. package/out-tsc/src/simulator/Simulator.js.map +1 -0
  22. package/out-tsc/src/store/AppState.js +66 -0
  23. package/out-tsc/src/store/AppState.js.map +1 -1
  24. package/out-tsc/src/utils.js +48 -0
  25. package/out-tsc/src/utils.js.map +1 -1
  26. package/out-tsc/temba-modules.js +2 -0
  27. package/out-tsc/temba-modules.js.map +1 -1
  28. package/out-tsc/test/temba-appstate-node-sorting.test.js +430 -0
  29. package/out-tsc/test/temba-appstate-node-sorting.test.js.map +1 -0
  30. package/out-tsc/test/temba-floating-tab.test.js +0 -9
  31. package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
  32. package/out-tsc/test/temba-flow-editor.test.js +262 -1
  33. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  34. package/out-tsc/test/temba-flow-plumber-connections.test.js +3 -1
  35. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  36. package/out-tsc/test/temba-flow-plumber.test.js +3 -1
  37. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  38. package/out-tsc/test/temba-simulator.test.js +642 -0
  39. package/out-tsc/test/temba-simulator.test.js.map +1 -0
  40. package/out-tsc/test/utils.test.js +1 -1
  41. package/out-tsc/test/utils.test.js.map +1 -1
  42. package/package.json +1 -1
  43. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  44. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  45. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  46. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  47. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  48. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  49. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  50. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  51. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  52. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  53. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  54. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  55. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  56. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  57. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  58. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  59. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  60. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  61. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  62. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  63. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  64. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  65. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  66. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  67. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  68. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  69. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  70. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  71. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  72. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  73. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  74. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  75. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  76. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  77. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  78. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  79. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  80. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  81. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  82. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  83. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  84. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  85. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  86. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  87. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  88. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  89. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  90. package/screenshots/truth/floating-tab/gray.png +0 -0
  91. package/screenshots/truth/floating-tab/green.png +0 -0
  92. package/screenshots/truth/floating-tab/purple.png +0 -0
  93. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  94. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  95. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  96. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  97. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  98. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  99. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  100. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  101. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  102. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  103. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  104. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  105. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  106. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  107. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  108. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  109. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  110. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  111. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  112. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  113. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  114. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  115. package/screenshots/truth/simulator/after-message-sent.png +0 -0
  116. package/screenshots/truth/simulator/after-reset.png +0 -0
  117. package/screenshots/truth/simulator/attachment-menu.png +0 -0
  118. package/screenshots/truth/simulator/context-expanded.png +0 -0
  119. package/screenshots/truth/simulator/context-explorer-open.png +0 -0
  120. package/screenshots/truth/simulator/event-info.png +0 -0
  121. package/screenshots/truth/simulator/image-attachment.png +0 -0
  122. package/screenshots/truth/simulator/open-initial.png +0 -0
  123. package/screenshots/truth/simulator/quick-replies.png +0 -0
  124. package/src/Icons.ts +2 -1
  125. package/src/display/FloatingTab.ts +2 -7
  126. package/src/flow/CanvasNode.ts +30 -1
  127. package/src/flow/Editor.ts +246 -4
  128. package/src/flow/Plumber.ts +371 -2
  129. package/src/interfaces.ts +2 -1
  130. package/src/layout/FloatingWindow.ts +37 -12
  131. package/src/simulator/Simulator.ts +2061 -0
  132. package/src/store/AppState.ts +109 -0
  133. package/src/utils.ts +53 -0
  134. package/static/svg/index.svg +1 -1
  135. package/static/svg/work/traced/route.svg +1 -0
  136. package/static/svg/work/used/route.svg +3 -0
  137. package/temba-modules.ts +2 -0
  138. package/test/temba-appstate-node-sorting.test.ts +506 -0
  139. package/test/temba-floating-tab.test.ts +0 -11
  140. package/test/temba-flow-editor.test.ts +298 -1
  141. package/test/temba-flow-plumber-connections.test.ts +4 -1
  142. package/test/temba-flow-plumber.test.ts +4 -1
  143. package/test/temba-simulator.test.ts +866 -0
  144. package/test/utils.test.ts +1 -1
@@ -125,9 +125,15 @@ export class Editor extends RapidElement {
125
125
  @property({ type: Array })
126
126
  public features: string[] = [];
127
127
 
128
+ private activityTimer: number | null = null;
129
+ private activityInterval = 100; // Start with 100ms interval for fast initial load
130
+
128
131
  @fromStore(zustand, (state: AppState) => state.flowDefinition)
129
132
  private definition!: FlowDefinition;
130
133
 
134
+ @fromStore(zustand, (state: AppState) => state.simulatorActive)
135
+ private simulatorActive!: boolean;
136
+
131
137
  @fromStore(zustand, (state: AppState) => state.canvasSize)
132
138
  private canvasSize!: { width: number; height: number };
133
139
 
@@ -143,6 +149,9 @@ export class Editor extends RapidElement {
143
149
  @fromStore(zustand, (state: AppState) => state.workspace)
144
150
  private workspace!: Workspace;
145
151
 
152
+ @fromStore(zustand, (state: AppState) => state.getCurrentActivity())
153
+ private activityData!: any;
154
+
146
155
  // Drag state
147
156
  @state()
148
157
  private isDragging = false;
@@ -379,6 +388,125 @@ export class Editor extends RapidElement {
379
388
  z-index: 10;
380
389
  }
381
390
 
391
+ /* Activity overlays on connections */
392
+ .jtk-overlay.activity-overlay {
393
+ background: #f3f3f3;
394
+ border: 1px solid #d9d9d9;
395
+ color: #333;
396
+ border-radius: 4px;
397
+ padding: 2px 4px;
398
+ font-size: 10px;
399
+ font-weight: 600;
400
+ line-height: 0.9;
401
+ cursor: pointer;
402
+ z-index: 500;
403
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
404
+ }
405
+
406
+ /* Active contact count on nodes */
407
+ .active-count {
408
+ position: absolute;
409
+ background: #3498db;
410
+ border: 1px solid #2980b9;
411
+ border-radius: 12px;
412
+ padding: 3px 5px;
413
+ color: #fff;
414
+ font-weight: 500;
415
+ top: -10px;
416
+ left: -10px;
417
+ font-size: 13px;
418
+ min-width: 22px;
419
+ text-align: center;
420
+ z-index: 600;
421
+ line-height: 1;
422
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
423
+ }
424
+
425
+ /* Recent contacts popup */
426
+ @keyframes popupBounceIn {
427
+ 0% {
428
+ transform: scale(0.8);
429
+ opacity: 0;
430
+ }
431
+ 50% {
432
+ transform: scale(1.05);
433
+ }
434
+ 100% {
435
+ transform: scale(1);
436
+ opacity: 1;
437
+ }
438
+ }
439
+
440
+ .recent-contacts-popup {
441
+ display: none;
442
+ position: absolute;
443
+ width: 200px;
444
+ background: #f3f3f3;
445
+ border-radius: 10px;
446
+ box-shadow: 0 1px 3px 1px rgba(130, 130, 130, 0.2);
447
+ z-index: 1015;
448
+ transform-origin: top center;
449
+ }
450
+
451
+ .recent-contacts-popup.show {
452
+ display: block;
453
+ animation: popupBounceIn 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
454
+ }
455
+
456
+ .recent-contacts-popup .popup-title {
457
+ background: #999;
458
+ color: #fff;
459
+ padding: 6px 0;
460
+ text-align: center;
461
+ border-top-left-radius: 10px;
462
+ border-top-right-radius: 10px;
463
+ font-size: 12px;
464
+ }
465
+
466
+ .recent-contacts-popup .no-contacts-message {
467
+ padding: 15px;
468
+ text-align: center;
469
+ color: #999;
470
+ font-size: 12px;
471
+ }
472
+
473
+ .recent-contacts-popup .contact-row {
474
+ padding: 8px 10px;
475
+ border-top: 1px solid #e0e0e0;
476
+ text-align: left;
477
+ }
478
+
479
+ .recent-contacts-popup .contact-row:last-child {
480
+ border-bottom-left-radius: 10px;
481
+ border-bottom-right-radius: 10px;
482
+ }
483
+
484
+ .recent-contacts-popup .contact-name {
485
+ display: block;
486
+ font-weight: 500;
487
+ font-size: 12px;
488
+ color: var(--color-link-primary, #1d4ed8);
489
+ cursor: pointer;
490
+ }
491
+
492
+ .recent-contacts-popup .contact-name:hover {
493
+ text-decoration: underline;
494
+ color: var(--color-link-primary, #1d4ed8);
495
+ }
496
+
497
+ .recent-contacts-popup .contact-operand {
498
+ padding-top: 3px;
499
+ font-size: 11px;
500
+ color: #666;
501
+ word-wrap: break-word;
502
+ }
503
+
504
+ .recent-contacts-popup .contact-time {
505
+ padding-top: 3px;
506
+ font-size: 10px;
507
+ color: #999;
508
+ }
509
+
382
510
  /* Connection dragging feedback */
383
511
  body svg.jtk-connector.jtk-dragging {
384
512
  z-index: 99999 !important;
@@ -612,7 +740,7 @@ export class Editor extends RapidElement {
612
740
  changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
613
741
  ): void {
614
742
  super.firstUpdated(changes);
615
- this.plumber = new Plumber(this.querySelector('#canvas'));
743
+ this.plumber = new Plumber(this.querySelector('#canvas'), this);
616
744
  this.setupGlobalEventListeners();
617
745
  if (changes.has('flow')) {
618
746
  getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
@@ -692,6 +820,29 @@ export class Editor extends RapidElement {
692
820
  }
693
821
 
694
822
  this.translationCache.clear();
823
+
824
+ // Start fetching activity data when definition is loaded
825
+ if (this.definition?.uuid) {
826
+ this.startActivityFetching();
827
+ }
828
+ }
829
+
830
+ if (changes.has('simulatorActive')) {
831
+ if (this.simulatorActive) {
832
+ // Stop polling when simulator becomes active
833
+ this.stopActivityFetching();
834
+ } else {
835
+ // Resume polling and refresh activity when simulator closes
836
+ this.activityInterval = 100; // Reset to fast initial interval
837
+ this.startActivityFetching();
838
+ }
839
+ }
840
+
841
+ if (changes.has('activityData')) {
842
+ // Update plumber with new activity data
843
+ if (this.plumber) {
844
+ this.plumber.setActivityData(this.activityData);
845
+ }
695
846
  }
696
847
 
697
848
  if (changes.has('dirtyDate')) {
@@ -768,6 +919,52 @@ export class Editor extends RapidElement {
768
919
  getStore().getState().setDirtyDate(null);
769
920
  }
770
921
 
922
+ private startActivityFetching(): void {
923
+ // Don't start if simulator is active
924
+ if (this.simulatorActive) {
925
+ return;
926
+ }
927
+ // Fetch immediately
928
+ this.fetchActivityData();
929
+ }
930
+
931
+ private stopActivityFetching(): void {
932
+ if (this.activityTimer !== null) {
933
+ clearTimeout(this.activityTimer);
934
+ this.activityTimer = null;
935
+ }
936
+ }
937
+
938
+ private fetchActivityData(): void {
939
+ if (!this.definition?.uuid) {
940
+ return;
941
+ }
942
+
943
+ // Don't fetch if simulator is active
944
+ if (this.simulatorActive) {
945
+ return;
946
+ }
947
+
948
+ const activityEndpoint = `/flow/activity/${this.definition.uuid}/`;
949
+ const store = getStore();
950
+ if (!store) {
951
+ return;
952
+ }
953
+ const state = store.getState();
954
+ state.fetchActivity(activityEndpoint).then(() => {
955
+ // Schedule next fetch with exponential backoff (max 5 minutes)
956
+ this.activityInterval = Math.min(60000 * 5, this.activityInterval + 100);
957
+
958
+ if (this.activityTimer !== null) {
959
+ clearTimeout(this.activityTimer);
960
+ }
961
+
962
+ this.activityTimer = window.setTimeout(() => {
963
+ this.fetchActivityData();
964
+ }, this.activityInterval);
965
+ });
966
+ }
967
+
771
968
  private handleLanguageChange(languageCode: string): void {
772
969
  zustand.getState().setLanguageCode(languageCode);
773
970
 
@@ -785,6 +982,10 @@ export class Editor extends RapidElement {
785
982
  clearTimeout(this.saveTimer);
786
983
  this.saveTimer = null;
787
984
  }
985
+ if (this.activityTimer !== null) {
986
+ clearTimeout(this.activityTimer);
987
+ this.activityTimer = null;
988
+ }
788
989
  document.removeEventListener('mousemove', this.boundMouseMove);
789
990
  document.removeEventListener('mouseup', this.boundMouseUp);
790
991
  document.removeEventListener('mousedown', this.boundGlobalMouseDown);
@@ -2755,7 +2956,7 @@ export class Editor extends RapidElement {
2755
2956
  header="Translations"
2756
2957
  .width=${360}
2757
2958
  .maxHeight=${600}
2758
- .top=${170}
2959
+ .top=${75}
2759
2960
  color="#6b7280"
2760
2961
  .hidden=${this.localizationWindowHidden}
2761
2962
  @temba-dialog-hidden=${this.handleLocalizationWindowClosed}
@@ -2927,6 +3128,44 @@ export class Editor extends RapidElement {
2927
3128
  `;
2928
3129
  }
2929
3130
 
3131
+ /**
3132
+ * Focus on a specific node by smoothly scrolling it to the center of the canvas
3133
+ */
3134
+ public focusNode(nodeUuid: string) {
3135
+ const nodeElement = this.querySelector(
3136
+ `temba-flow-node[uuid="${nodeUuid}"]`
3137
+ ) as HTMLElement;
3138
+ if (!nodeElement) {
3139
+ return;
3140
+ }
3141
+
3142
+ const editor = this.querySelector('#editor') as HTMLElement;
3143
+ if (!editor) {
3144
+ return;
3145
+ }
3146
+
3147
+ // Get the editor's dimensions and scroll position
3148
+ const editorRect = editor.getBoundingClientRect();
3149
+ const editorCenterX = editorRect.width / 2;
3150
+ const editorCenterY = editorRect.height / 2;
3151
+
3152
+ // Get node position relative to the editor's scroll container
3153
+ const nodeRect = nodeElement.getBoundingClientRect();
3154
+ const nodeCenterX = nodeElement.offsetLeft + nodeRect.width / 2;
3155
+ const nodeCenterY = nodeElement.offsetTop + nodeRect.height / 2;
3156
+
3157
+ // Calculate the scroll position needed to center the node
3158
+ const targetScrollX = nodeCenterX - editorCenterX;
3159
+ const targetScrollY = nodeCenterY - editorCenterY;
3160
+
3161
+ // Smooth scroll the editor container to the target position
3162
+ editor.scrollTo({
3163
+ left: Math.max(0, targetScrollX),
3164
+ top: Math.max(0, targetScrollY),
3165
+ behavior: 'smooth'
3166
+ });
3167
+ }
3168
+
2930
3169
  public render(): TemplateResult {
2931
3170
  // we have to embed our own style since we are in light DOM
2932
3171
  const style = html`<style>
@@ -2949,7 +3188,7 @@ export class Editor extends RapidElement {
2949
3188
  ? repeat(
2950
3189
  this.definition.nodes,
2951
3190
  (node) => node.uuid,
2952
- (node) => {
3191
+ (node, index) => {
2953
3192
  const position = this.definition._ui?.nodes[node.uuid]
2954
3193
  ?.position || {
2955
3194
  left: 0,
@@ -2962,10 +3201,13 @@ export class Editor extends RapidElement {
2962
3201
 
2963
3202
  const selected = this.selectedItems.has(node.uuid);
2964
3203
 
3204
+ // first node is the flow start (nodes are sorted by position)
3205
+ const isFlowStart = index === 0;
3206
+
2965
3207
  return html`<temba-flow-node
2966
3208
  class="draggable ${dragging ? 'dragging' : ''} ${selected
2967
3209
  ? 'selected'
2968
- : ''}"
3210
+ : ''} ${isFlowStart ? 'flow-start' : ''}"
2969
3211
  @mousedown=${this.handleMouseDown.bind(this)}
2970
3212
  uuid=${node.uuid}
2971
3213
  data-node-uuid=${node.uuid}