@nyaruka/temba-components 0.156.8 → 0.156.10

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 (59) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/temba-components.js +678 -631
  3. package/dist/temba-components.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/display/Chat.ts +8 -8
  6. package/src/display/FloatingTab.ts +2 -2
  7. package/src/display/Options.ts +8 -2
  8. package/src/flow/CanvasMenu.ts +20 -25
  9. package/src/flow/CanvasNode.ts +16 -12
  10. package/src/flow/DragManager.ts +93 -33
  11. package/src/flow/Editor.ts +64 -59
  12. package/src/flow/EditorToolbar.ts +19 -20
  13. package/src/flow/FlowSearch.ts +9 -7
  14. package/src/flow/MessageTable.ts +181 -74
  15. package/src/flow/NodeEditor.ts +55 -72
  16. package/src/flow/RevisionsWindow.ts +2 -4
  17. package/src/flow/ZoomManager.ts +1 -2
  18. package/src/flow/actions/play_audio.ts +1 -28
  19. package/src/flow/actions/say_msg.ts +1 -40
  20. package/src/flow/actions/send_broadcast.ts +1 -2
  21. package/src/flow/actions/send_email.ts +5 -56
  22. package/src/flow/actions/send_msg.ts +10 -2
  23. package/src/flow/actions/start_session.ts +1 -2
  24. package/src/flow/categoryLocalization.ts +1 -5
  25. package/src/flow/categoryUtils.ts +139 -0
  26. package/src/flow/nodes/shared-rules.ts +6 -16
  27. package/src/flow/nodes/shared.ts +113 -6
  28. package/src/flow/nodes/split_by_airtime.ts +41 -63
  29. package/src/flow/nodes/split_by_contact_field.ts +8 -17
  30. package/src/flow/nodes/split_by_expression.ts +8 -17
  31. package/src/flow/nodes/split_by_groups.ts +34 -112
  32. package/src/flow/nodes/split_by_llm.ts +1 -7
  33. package/src/flow/nodes/split_by_llm_categorize.ts +27 -43
  34. package/src/flow/nodes/split_by_random.ts +39 -99
  35. package/src/flow/nodes/split_by_resthook.ts +5 -19
  36. package/src/flow/nodes/split_by_run_result.ts +8 -17
  37. package/src/flow/nodes/split_by_scheme.ts +39 -124
  38. package/src/flow/nodes/split_by_subflow.ts +1 -7
  39. package/src/flow/nodes/split_by_ticket.ts +1 -7
  40. package/src/flow/nodes/split_by_webhook.ts +2 -8
  41. package/src/flow/nodes/wait_for_audio.ts +1 -7
  42. package/src/flow/nodes/wait_for_dial.ts +2 -8
  43. package/src/flow/nodes/wait_for_digits.ts +5 -7
  44. package/src/flow/nodes/wait_for_menu.ts +5 -7
  45. package/src/flow/nodes/wait_for_response.ts +10 -18
  46. package/src/flow/types.ts +27 -0
  47. package/src/flow/utils.ts +111 -3
  48. package/src/form/Compose.ts +11 -4
  49. package/src/form/MessageEditor.ts +5 -3
  50. package/src/form/RichEditor.ts +3 -1
  51. package/src/form/TemplateEditor.ts +5 -1
  52. package/src/form/select/Select.ts +11 -9
  53. package/src/layout/AccordionSection.ts +9 -3
  54. package/src/layout/Modax.ts +1 -3
  55. package/src/live/ContactChat.ts +54 -46
  56. package/src/simulator/Simulator.ts +9 -3
  57. package/src/store/AppState.ts +1 -1
  58. package/src/store/Store.ts +6 -1
  59. package/src/utils.ts +21 -16
@@ -34,7 +34,12 @@ import { PRIMARY_LANGUAGE_OPTION_VALUE } from './EditorToolbar';
34
34
  import { calculateLayeredLayout, placeStickyNotes } from './reflow';
35
35
  import type { RevisionsWindow } from './RevisionsWindow';
36
36
 
37
- import { ACTION_GROUP_METADATA } from './types';
37
+ import {
38
+ ACTION_GROUP_METADATA,
39
+ CONTEXT_MENU_SHORTCUTS,
40
+ FlowType,
41
+ FlowTypes
42
+ } from './types';
38
43
 
39
44
  import {
40
45
  Plumber,
@@ -201,7 +206,7 @@ export class Editor extends RapidElement {
201
206
  public version: string;
202
207
 
203
208
  @property({ type: String })
204
- public flowType: string = 'message';
209
+ public flowType: FlowType = FlowTypes.MESSAGE;
205
210
 
206
211
  @property({ type: Array })
207
212
  public features: string[] = [];
@@ -1360,7 +1365,7 @@ export class Editor extends RapidElement {
1360
1365
  },
1361
1366
  false, // Don't show sticky note option for connection drops
1362
1367
  false,
1363
- this.flowType === 'message'
1368
+ CONTEXT_MENU_SHORTCUTS[this.flowType]
1364
1369
  );
1365
1370
  }
1366
1371
  }
@@ -1537,17 +1542,17 @@ export class Editor extends RapidElement {
1537
1542
  * FlowDefinition uses: 'messaging', 'messaging_background', 'messaging_offline', 'voice'
1538
1543
  * Editor uses: 'message', 'voice', 'background'
1539
1544
  */
1540
- private getFlowTypeFromDefinition(definitionType: string): string {
1545
+ private getFlowTypeFromDefinition(definitionType: string): FlowType {
1541
1546
  if (definitionType === 'voice') {
1542
- return 'voice';
1547
+ return FlowTypes.VOICE;
1543
1548
  } else if (
1544
1549
  definitionType === 'messaging_background' ||
1545
1550
  definitionType === 'messaging_offline'
1546
1551
  ) {
1547
- return 'background';
1552
+ return FlowTypes.BACKGROUND;
1548
1553
  } else {
1549
1554
  // 'messaging' or any other messaging type defaults to 'message'
1550
- return 'message';
1555
+ return FlowTypes.MESSAGE;
1551
1556
  }
1552
1557
  }
1553
1558
 
@@ -1769,10 +1774,10 @@ export class Editor extends RapidElement {
1769
1774
  this.definition?._ui?.languages &&
1770
1775
  this.definition._ui.languages.length > 0
1771
1776
  ) {
1772
- return this.definition._ui.languages.map((lang: any) => ({
1773
- code: typeof lang === 'string' ? lang : lang.iso || lang.code,
1774
- name: typeof lang === 'string' ? lang : lang.name
1775
- }));
1777
+ return this.definition._ui.languages.map((lang: any) => {
1778
+ const code = typeof lang === 'string' ? lang : lang.iso || lang.code;
1779
+ return { code, name: getLanguageDisplayName(code) };
1780
+ });
1776
1781
  }
1777
1782
 
1778
1783
  // No languages available
@@ -2193,7 +2198,8 @@ export class Editor extends RapidElement {
2193
2198
  search.definition = this.definition;
2194
2199
  search.languageCode = this.languageCode || '';
2195
2200
  search.scope = this.showMessageTable ? 'table' : 'flow';
2196
- search.includeCategories = this.isTranslating && this.hasAnyNodeWithLocalizeCategories();
2201
+ search.includeCategories =
2202
+ this.isTranslating && this.hasAnyNodeWithLocalizeCategories();
2197
2203
  search.show();
2198
2204
  }
2199
2205
 
@@ -2858,7 +2864,7 @@ export class Editor extends RapidElement {
2858
2864
  },
2859
2865
  true,
2860
2866
  hasNodes,
2861
- this.flowType === 'message'
2867
+ CONTEXT_MENU_SHORTCUTS[this.flowType]
2862
2868
  );
2863
2869
  }
2864
2870
  }
@@ -2887,7 +2893,7 @@ export class Editor extends RapidElement {
2887
2893
  { x: nodeLeft, y: nodeTop },
2888
2894
  false,
2889
2895
  false,
2890
- this.flowType === 'message'
2896
+ CONTEXT_MENU_SHORTCUTS[this.flowType]
2891
2897
  );
2892
2898
  }
2893
2899
  }
@@ -2920,19 +2926,6 @@ export class Editor extends RapidElement {
2920
2926
  this.connectionSourceX = null;
2921
2927
  this.connectionSourceY = null;
2922
2928
  this.dragFromNodeId = null;
2923
- } else if (
2924
- selection.action === 'send_msg' ||
2925
- selection.action === 'wait_for_response'
2926
- ) {
2927
- // Go directly to the node editor (skip node type selector)
2928
- this.handleNodeTypeSelection(
2929
- new CustomEvent(CustomEventType.Selection, {
2930
- detail: {
2931
- nodeType: selection.action,
2932
- position: selection.position
2933
- } as NodeTypeSelection
2934
- })
2935
- );
2936
2929
  } else if (selection.action === 'other') {
2937
2930
  // Show unified node type selector
2938
2931
  const selector = this.querySelector(
@@ -2943,6 +2936,17 @@ export class Editor extends RapidElement {
2943
2936
  }
2944
2937
  // Note: we don't clear pendingCanvasConnection or placeholder here,
2945
2938
  // they will be used in handleNodeTypeSelection
2939
+ } else {
2940
+ // Configured shortcut — go directly to the node editor with the
2941
+ // action/node type carried in selection.action.
2942
+ this.handleNodeTypeSelection(
2943
+ new CustomEvent(CustomEventType.Selection, {
2944
+ detail: {
2945
+ nodeType: selection.action,
2946
+ position: selection.position
2947
+ } as NodeTypeSelection
2948
+ })
2949
+ );
2946
2950
  }
2947
2951
  }
2948
2952
 
@@ -3855,9 +3859,7 @@ export class Editor extends RapidElement {
3855
3859
 
3856
3860
  this.focusNode(issue.node_uuid);
3857
3861
 
3858
- const node = this.definition.nodes.find(
3859
- (n) => n.uuid === issue.node_uuid
3860
- );
3862
+ const node = this.definition.nodes.find((n) => n.uuid === issue.node_uuid);
3861
3863
  if (!node) return;
3862
3864
 
3863
3865
  if (issue.action_uuid) {
@@ -3915,7 +3917,7 @@ export class Editor extends RapidElement {
3915
3917
  const baseLanguage = this.definition?.language;
3916
3918
  const baseLanguageName =
3917
3919
  availableLanguages.find((lang) => lang.code === baseLanguage)?.name ||
3918
- baseLanguage ||
3920
+ (baseLanguage ? getLanguageDisplayName(baseLanguage) : '') ||
3919
3921
  'Primary language';
3920
3922
  const isBaseSelected =
3921
3923
  !this.languageCode ||
@@ -3934,25 +3936,30 @@ export class Editor extends RapidElement {
3934
3936
  const percent = Math.round(
3935
3937
  (progress.localized / Math.max(progress.total, 1)) * 100
3936
3938
  );
3937
- const languageOptions = [
3938
- {
3939
- name: baseLanguageName,
3940
- value: PRIMARY_LANGUAGE_OPTION_VALUE
3941
- },
3942
- ...languages.map((lang) => {
3943
- const localizationProgress = this.getLocalizationProgress(lang.code);
3944
- const localizationPercent = Math.round(
3945
- (localizationProgress.localized /
3946
- Math.max(localizationProgress.total, 1)) *
3947
- 100
3948
- );
3949
- return {
3950
- name: lang.name,
3951
- value: lang.code,
3952
- percent: localizationPercent
3953
- };
3954
- })
3955
- ];
3939
+ const isEmptyFlow = !this.definition || this.definition.nodes.length === 0;
3940
+ const languageOptions = isEmptyFlow
3941
+ ? []
3942
+ : [
3943
+ {
3944
+ name: baseLanguageName,
3945
+ value: PRIMARY_LANGUAGE_OPTION_VALUE
3946
+ },
3947
+ ...languages.map((lang) => {
3948
+ const localizationProgress = this.getLocalizationProgress(
3949
+ lang.code
3950
+ );
3951
+ const localizationPercent = Math.round(
3952
+ (localizationProgress.localized /
3953
+ Math.max(localizationProgress.total, 1)) *
3954
+ 100
3955
+ );
3956
+ return {
3957
+ name: lang.name,
3958
+ value: lang.code,
3959
+ percent: localizationPercent
3960
+ };
3961
+ })
3962
+ ];
3956
3963
 
3957
3964
  return html`
3958
3965
  <temba-editor-toolbar
@@ -3962,7 +3969,8 @@ export class Editor extends RapidElement {
3962
3969
  ?zoom-fitted=${this.zoomManager.isZoomFitted}
3963
3970
  ?revisions-active=${!this.revisionsWindowHidden}
3964
3971
  ?is-saving=${this.isSaving}
3965
- ?search-disabled=${this.getRevisionsWindow()?.isViewingRevision ?? false}
3972
+ ?search-disabled=${this.getRevisionsWindow()?.isViewingRevision ??
3973
+ false}
3966
3974
  .languageOptions=${languageOptions}
3967
3975
  current-language-name=${currentLanguage.name}
3968
3976
  ?is-base-language=${isBaseSelected}
@@ -4219,9 +4227,7 @@ export class Editor extends RapidElement {
4219
4227
  : ''}
4220
4228
  <div
4221
4229
  id="grid"
4222
- class="${this.viewingRevision
4223
- ? 'viewing-revision'
4224
- : ''}"
4230
+ class="${this.viewingRevision ? 'viewing-revision' : ''}"
4225
4231
  style="min-width:${100 / this.zoom}%;min-height:${100 /
4226
4232
  this.zoom}%;width:${this.canvasSize.width}px; height:${this
4227
4233
  .canvasSize.height}px;transform:scale(${this.zoom})"
@@ -4229,11 +4235,9 @@ export class Editor extends RapidElement {
4229
4235
  <div
4230
4236
  id="canvas"
4231
4237
  class="${getClasses({
4232
- 'viewing-revision':
4233
- this.viewingRevision,
4238
+ 'viewing-revision': this.viewingRevision,
4234
4239
  'read-only-connections':
4235
- this.viewingRevision ||
4236
- this.isTranslating
4240
+ this.viewingRevision || this.isTranslating
4237
4241
  })}"
4238
4242
  >
4239
4243
  ${this.definition && !hasCorruptedUI
@@ -4354,7 +4358,8 @@ export class Editor extends RapidElement {
4354
4358
  : ''}
4355
4359
  <temba-flow-search
4356
4360
  .scope=${this.showMessageTable ? 'table' : 'flow'}
4357
- .includeCategories=${this.isTranslating && this.hasAnyNodeWithLocalizeCategories()}
4361
+ .includeCategories=${this.isTranslating &&
4362
+ this.hasAnyNodeWithLocalizeCategories()}
4358
4363
  @temba-search-result-selected=${this.handleSearchResultSelected}
4359
4364
  ></temba-flow-search>
4360
4365
  ${!this.showMessageTable && this.flowIssues?.length
@@ -311,10 +311,7 @@ export class EditorToolbar extends RapidElement {
311
311
  `;
312
312
  }
313
313
 
314
- private renderShortcutLabel(
315
- label: string,
316
- shortcut: string
317
- ): TemplateResult {
314
+ private renderShortcutLabel(label: string, shortcut: string): TemplateResult {
318
315
  return html`<span style="display:inline-flex; align-items:center; gap:8px;">
319
316
  <span>${label}</span>
320
317
  <kbd>${shortcut}</kbd>
@@ -348,7 +345,9 @@ export class EditorToolbar extends RapidElement {
348
345
 
349
346
  return html`
350
347
  <div
351
- style="display:flex; align-items:center; justify-content:space-between; gap:8px; padding:6px 10px; ${optionBg ? `background:${optionBg};` : ''} ${optionRadius}"
348
+ style="display:flex; align-items:center; justify-content:space-between; gap:8px; padding:6px 10px; ${optionBg
349
+ ? `background:${optionBg};`
350
+ : ''} ${optionRadius}"
352
351
  @mouseenter=${isComplete
353
352
  ? (e: MouseEvent) => {
354
353
  (e.currentTarget as HTMLElement).style.background = optionHoverBg;
@@ -372,9 +371,7 @@ export class EditorToolbar extends RapidElement {
372
371
 
373
372
  public render(): TemplateResult {
374
373
  const showLanguageControls = this.languageOptions.length > 1;
375
- const searchTargetLabel = this.messageView
376
- ? 'Search table'
377
- : 'Search flow';
374
+ const searchTargetLabel = this.messageView ? 'Search table' : 'Search flow';
378
375
 
379
376
  return html`
380
377
  <div class="editor-toolbar">
@@ -384,7 +381,8 @@ export class EditorToolbar extends RapidElement {
384
381
  html`
385
382
  <button
386
383
  class="toolbar-btn ${!this.messageView ? 'active' : ''}"
387
- @click=${() => this.fireToolbarAction('view-change', { view: 'flow' })}
384
+ @click=${() =>
385
+ this.fireToolbarAction('view-change', { view: 'flow' })}
388
386
  aria-label="Flow View"
389
387
  >
390
388
  <temba-icon name="flow" size="1"></temba-icon>
@@ -396,7 +394,8 @@ export class EditorToolbar extends RapidElement {
396
394
  html`
397
395
  <button
398
396
  class="toolbar-btn ${this.messageView ? 'active' : ''}"
399
- @click=${() => this.fireToolbarAction('view-change', { view: 'table' })}
397
+ @click=${() =>
398
+ this.fireToolbarAction('view-change', { view: 'table' })}
400
399
  aria-label="Table View"
401
400
  >
402
401
  <temba-icon name=${Icon.quick_replies} size="1"></temba-icon>
@@ -412,7 +411,11 @@ export class EditorToolbar extends RapidElement {
412
411
  'Change language',
413
412
  html`
414
413
  <button
415
- class="language-pill ${this.isBaseLanguage ? 'primary' : this.languagePercent === 100 ? 'complete' : ''}"
414
+ class="language-pill ${this.isBaseLanguage
415
+ ? 'primary'
416
+ : this.languagePercent === 100
417
+ ? 'complete'
418
+ : ''}"
416
419
  id="language-btn"
417
420
  @click=${this.handleLanguageIconClick}
418
421
  aria-label="Change language"
@@ -434,7 +437,9 @@ export class EditorToolbar extends RapidElement {
434
437
  `
435
438
  )}
436
439
  <temba-options
437
- .anchorTo=${this.shadowRoot?.querySelector('#language-btn') as HTMLElement}
440
+ .anchorTo=${this.shadowRoot?.querySelector(
441
+ '#language-btn'
442
+ ) as HTMLElement}
438
443
  .options=${this.languageOptions}
439
444
  .renderOption=${this.renderLanguageOption}
440
445
  ?visible=${this.showLanguageOptions}
@@ -462,10 +467,7 @@ export class EditorToolbar extends RapidElement {
462
467
  ?disabled=${!this.zoomInitialized || this.zoomFitted}
463
468
  aria-label="Zoom to fit"
464
469
  >
465
- <temba-icon
466
- name=${Icon.zoom_fit}
467
- size="1"
468
- ></temba-icon>
470
+ <temba-icon name=${Icon.zoom_fit} size="1"></temba-icon>
469
471
  </button>
470
472
  `
471
473
  )}
@@ -511,10 +513,7 @@ export class EditorToolbar extends RapidElement {
511
513
  ?disabled=${!this.zoomInitialized || this.zoom >= 1.0}
512
514
  aria-label="Zoom to 100%"
513
515
  >
514
- <temba-icon
515
- name=${Icon.zoom_in}
516
- size="1"
517
- ></temba-icon>
516
+ <temba-icon name=${Icon.zoom_in} size="1"></temba-icon>
518
517
  </button>
519
518
  `
520
519
  )}
@@ -265,9 +265,7 @@ function getNodeSearchTexts(
265
265
  if (node.router?.categories) {
266
266
  for (const cat of node.router.categories) {
267
267
  if (cat.name && cat.name !== 'Other' && cat.name !== 'All Responses') {
268
- texts.push(
269
- localizeCategoryName(cat.uuid, cat.name, langLocalization)
270
- );
268
+ texts.push(localizeCategoryName(cat.uuid, cat.name, langLocalization));
271
269
  }
272
270
  }
273
271
  }
@@ -748,7 +746,8 @@ export class FlowSearch extends LitElement {
748
746
  const actionConfig = ACTION_CONFIG[action.type];
749
747
  if (
750
748
  action.type !== 'send_msg' &&
751
- (!actionConfig?.localizable || actionConfig.localizable.length === 0)
749
+ (!actionConfig?.localizable ||
750
+ actionConfig.localizable.length === 0)
752
751
  ) {
753
752
  continue;
754
753
  }
@@ -907,7 +906,10 @@ export class FlowSearch extends LitElement {
907
906
  >
908
907
  <div
909
908
  class="result-type-badge"
910
- style="background:${result.color};border-color:${result.borderColor || result.color}${result.textColor ? `;color:${result.textColor}` : ''}"
909
+ style="background:${result.color};border-color:${result.borderColor ||
910
+ result.color}${result.textColor
911
+ ? `;color:${result.textColor}`
912
+ : ''}"
911
913
  >
912
914
  ${result.typeName}
913
915
  </div>
@@ -925,8 +927,8 @@ export class FlowSearch extends LitElement {
925
927
  `
926
928
  : html`<div class="no-results">No matches found</div>`
927
929
  : html`<div class="hint">
928
- <kbd>↑</kbd> <kbd>↓</kbd> to navigate &nbsp;
929
- <kbd>Enter</kbd> to open &nbsp; <kbd>Esc</kbd> to close
930
+ <kbd>↑</kbd> <kbd>↓</kbd> to navigate &nbsp; <kbd>Enter</kbd> to
931
+ open &nbsp; <kbd>Esc</kbd> to close
930
932
  </div>`}
931
933
  </div>
932
934
  `;