@nyaruka/temba-components 0.121.7 → 0.123.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 (224) hide show
  1. package/.github/copilot-instructions.md +163 -0
  2. package/.github/workflows/build.yml +3 -3
  3. package/.github/workflows/cla.yml +6 -6
  4. package/.github/workflows/copilot-setup-steps.yml +86 -0
  5. package/CHANGELOG.md +41 -0
  6. package/demo/index.html +61 -12
  7. package/dist/locales/es.js +1 -0
  8. package/dist/locales/es.js.map +1 -1
  9. package/dist/locales/fr.js +1 -0
  10. package/dist/locales/fr.js.map +1 -1
  11. package/dist/locales/pt.js +1 -0
  12. package/dist/locales/pt.js.map +1 -1
  13. package/dist/temba-components.js +555 -465
  14. package/dist/temba-components.js.map +1 -1
  15. package/out-tsc/src/chart/TembaChart.js +377 -0
  16. package/out-tsc/src/chart/TembaChart.js.map +1 -0
  17. package/out-tsc/src/list/RunList.js +13 -8
  18. package/out-tsc/src/list/RunList.js.map +1 -1
  19. package/out-tsc/src/locales/es.js +1 -0
  20. package/out-tsc/src/locales/es.js.map +1 -1
  21. package/out-tsc/src/locales/fr.js +1 -0
  22. package/out-tsc/src/locales/fr.js.map +1 -1
  23. package/out-tsc/src/locales/pt.js +1 -0
  24. package/out-tsc/src/locales/pt.js.map +1 -1
  25. package/out-tsc/src/options/Options.js +37 -13
  26. package/out-tsc/src/options/Options.js.map +1 -1
  27. package/out-tsc/src/select/Select.js +28 -5
  28. package/out-tsc/src/select/Select.js.map +1 -1
  29. package/out-tsc/src/store/AppState.js +3 -3
  30. package/out-tsc/src/store/AppState.js.map +1 -1
  31. package/out-tsc/src/utils/index.js +6 -1
  32. package/out-tsc/src/utils/index.js.map +1 -1
  33. package/out-tsc/src/vectoricon/VectorIcon.js +2 -1
  34. package/out-tsc/src/vectoricon/VectorIcon.js.map +1 -1
  35. package/out-tsc/temba-modules.js +2 -2
  36. package/out-tsc/temba-modules.js.map +1 -1
  37. package/out-tsc/test/temba-appstate-language.test.js +176 -0
  38. package/out-tsc/test/temba-appstate-language.test.js.map +1 -0
  39. package/out-tsc/test/temba-chart.test.js +171 -0
  40. package/out-tsc/test/temba-chart.test.js.map +1 -0
  41. package/out-tsc/test/temba-dropdown.test.js +317 -0
  42. package/out-tsc/test/temba-dropdown.test.js.map +1 -0
  43. package/out-tsc/test/temba-run-list.test.js +588 -0
  44. package/out-tsc/test/temba-run-list.test.js.map +1 -0
  45. package/out-tsc/test/temba-select.test.js +16 -0
  46. package/out-tsc/test/temba-select.test.js.map +1 -1
  47. package/out-tsc/test/temba-toast.test.js +299 -0
  48. package/out-tsc/test/temba-toast.test.js.map +1 -0
  49. package/out-tsc/test/temba-utils-index.test.js +1178 -0
  50. package/out-tsc/test/temba-utils-index.test.js.map +1 -0
  51. package/out-tsc/test/temba-webchat.test.js +816 -0
  52. package/out-tsc/test/temba-webchat.test.js.map +1 -0
  53. package/out-tsc/test/utils.test.js +3 -1
  54. package/out-tsc/test/utils.test.js.map +1 -1
  55. package/package.json +8 -8
  56. package/screenshots/truth/alert/error.png +0 -0
  57. package/screenshots/truth/alert/info.png +0 -0
  58. package/screenshots/truth/alert/warning.png +0 -0
  59. package/screenshots/truth/checkbox/checkbox-label-background-hover.png +0 -0
  60. package/screenshots/truth/checkbox/checked.png +0 -0
  61. package/screenshots/truth/checkbox/default.png +0 -0
  62. package/screenshots/truth/colorpicker/default.png +0 -0
  63. package/screenshots/truth/colorpicker/focused.png +0 -0
  64. package/screenshots/truth/colorpicker/initialized.png +0 -0
  65. package/screenshots/truth/colorpicker/selected.png +0 -0
  66. package/screenshots/truth/compose/attachments-tab.png +0 -0
  67. package/screenshots/truth/compose/attachments-with-files-focused.png +0 -0
  68. package/screenshots/truth/compose/attachments-with-files.png +0 -0
  69. package/screenshots/truth/compose/intial-text.png +0 -0
  70. package/screenshots/truth/compose/no-counter.png +0 -0
  71. package/screenshots/truth/compose/wraps-text-and-spaces.png +0 -0
  72. package/screenshots/truth/compose/wraps-text-and-url.png +0 -0
  73. package/screenshots/truth/compose/wraps-text-no-spaces.png +0 -0
  74. package/screenshots/truth/contacts/badges.png +0 -0
  75. package/screenshots/truth/contacts/chat-failure.png +0 -0
  76. package/screenshots/truth/contacts/chat-for-active-contact.png +0 -0
  77. package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
  78. package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
  79. package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
  80. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  81. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  82. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  83. package/screenshots/truth/content-menu/button-no-items.png +0 -0
  84. package/screenshots/truth/content-menu/items-and-buttons.png +0 -0
  85. package/screenshots/truth/counter/summary.png +0 -0
  86. package/screenshots/truth/counter/text.png +0 -0
  87. package/screenshots/truth/counter/unicode-variables.png +0 -0
  88. package/screenshots/truth/counter/unicode.png +0 -0
  89. package/screenshots/truth/counter/variable.png +0 -0
  90. package/screenshots/truth/date/date-inline.png +0 -0
  91. package/screenshots/truth/date/date.png +0 -0
  92. package/screenshots/truth/date/datetime.png +0 -0
  93. package/screenshots/truth/date/duration.png +0 -0
  94. package/screenshots/truth/date/timedate.png +0 -0
  95. package/screenshots/truth/datepicker/date-truncated-time.png +0 -0
  96. package/screenshots/truth/datepicker/date.png +0 -0
  97. package/screenshots/truth/datepicker/initial-timezone.png +0 -0
  98. package/screenshots/truth/datepicker/updated-keyboard-date.png +0 -0
  99. package/screenshots/truth/dialog/focused.png +0 -0
  100. package/screenshots/truth/dropdown/after-blur.png +0 -0
  101. package/screenshots/truth/dropdown/bottom-edge-collision.png +0 -0
  102. package/screenshots/truth/dropdown/custom-arrow-size.png +0 -0
  103. package/screenshots/truth/dropdown/default.png +0 -0
  104. package/screenshots/truth/dropdown/narrow-toggle.png +0 -0
  105. package/screenshots/truth/dropdown/no-mask.png +0 -0
  106. package/screenshots/truth/dropdown/opened.png +0 -0
  107. package/screenshots/truth/dropdown/positioned.png +0 -0
  108. package/screenshots/truth/dropdown/right-edge-collision.png +0 -0
  109. package/screenshots/truth/dropdown/with-mask.png +0 -0
  110. package/screenshots/truth/label/custom.png +0 -0
  111. package/screenshots/truth/label/danger.png +0 -0
  112. package/screenshots/truth/label/dark.png +0 -0
  113. package/screenshots/truth/label/default-icon.png +0 -0
  114. package/screenshots/truth/label/no-icon.png +0 -0
  115. package/screenshots/truth/label/primary.png +0 -0
  116. package/screenshots/truth/label/secondary.png +0 -0
  117. package/screenshots/truth/label/shadow.png +0 -0
  118. package/screenshots/truth/label/tertiary.png +0 -0
  119. package/screenshots/truth/lightbox/img-zoomed.png +0 -0
  120. package/screenshots/truth/list/fields-dragging.png +0 -0
  121. package/screenshots/truth/list/fields-filtered.png +0 -0
  122. package/screenshots/truth/list/fields-hovered.png +0 -0
  123. package/screenshots/truth/list/fields.png +0 -0
  124. package/screenshots/truth/list/items-selected.png +0 -0
  125. package/screenshots/truth/list/items-updated.png +0 -0
  126. package/screenshots/truth/list/items.png +0 -0
  127. package/screenshots/truth/list/sortable-dragging.png +0 -0
  128. package/screenshots/truth/list/sortable-dropped.png +0 -0
  129. package/screenshots/truth/list/sortable.png +0 -0
  130. package/screenshots/truth/menu/menu-focused-with items.png +0 -0
  131. package/screenshots/truth/menu/menu-refresh-1.png +0 -0
  132. package/screenshots/truth/menu/menu-refresh-2.png +0 -0
  133. package/screenshots/truth/menu/menu-root.png +0 -0
  134. package/screenshots/truth/menu/menu-submenu.png +0 -0
  135. package/screenshots/truth/menu/menu-tasks-nextup.png +0 -0
  136. package/screenshots/truth/menu/menu-tasks.png +0 -0
  137. package/screenshots/truth/modax/form.png +0 -0
  138. package/screenshots/truth/modax/simple.png +0 -0
  139. package/screenshots/truth/omnibox/selected.png +0 -0
  140. package/screenshots/truth/options/block.png +0 -0
  141. package/screenshots/truth/run-list/basic.png +0 -0
  142. package/screenshots/truth/select/disabled-multi-selection.png +0 -0
  143. package/screenshots/truth/select/disabled-selection.png +0 -0
  144. package/screenshots/truth/select/disabled.png +0 -0
  145. package/screenshots/truth/select/embedded.png +0 -0
  146. package/screenshots/truth/select/empty-options.png +0 -0
  147. package/screenshots/truth/select/expression-selected.png +0 -0
  148. package/screenshots/truth/select/expressions.png +0 -0
  149. package/screenshots/truth/select/functions.png +0 -0
  150. package/screenshots/truth/select/local-options.png +0 -0
  151. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  152. package/screenshots/truth/select/multiple-initial-values.png +0 -0
  153. package/screenshots/truth/select/remote-options.png +0 -0
  154. package/screenshots/truth/select/search-enabled.png +0 -0
  155. package/screenshots/truth/select/search-multi-no-matches.png +0 -0
  156. package/screenshots/truth/select/search-selected-focus.png +0 -0
  157. package/screenshots/truth/select/search-selected.png +0 -0
  158. package/screenshots/truth/select/search-with-selected.png +0 -0
  159. package/screenshots/truth/select/searching.png +0 -0
  160. package/screenshots/truth/select/selected-multi-maxitems-reached.png +0 -0
  161. package/screenshots/truth/select/selected-multi.png +0 -0
  162. package/screenshots/truth/select/selected-single.png +0 -0
  163. package/screenshots/truth/select/selection-clearable.png +0 -0
  164. package/screenshots/truth/select/static-initial-value.png +0 -0
  165. package/screenshots/truth/select/static-initial-via-selected.png +0 -0
  166. package/screenshots/truth/select/truncated-selection.png +0 -0
  167. package/screenshots/truth/select/with-placeholder.png +0 -0
  168. package/screenshots/truth/select/without-placeholder.png +0 -0
  169. package/screenshots/truth/slider/custom-min-custom-max-valid-value.png +0 -0
  170. package/screenshots/truth/slider/custom-min-default-max-no-value.png +0 -0
  171. package/screenshots/truth/slider/default-min-custom-max-no-value.png +0 -0
  172. package/screenshots/truth/slider/default-min-default-max-invalid-value.png +0 -0
  173. package/screenshots/truth/slider/default-min-default-max-valid-value.png +0 -0
  174. package/screenshots/truth/slider/update-slider-on-value-change.png +0 -0
  175. package/screenshots/truth/templates/default.png +0 -0
  176. package/screenshots/truth/templates/unapproved.png +0 -0
  177. package/screenshots/truth/textinput/input-disabled.png +0 -0
  178. package/screenshots/truth/textinput/input-focused.png +0 -0
  179. package/screenshots/truth/textinput/input-form.png +0 -0
  180. package/screenshots/truth/textinput/input-inserted.png +0 -0
  181. package/screenshots/truth/textinput/input-placeholder.png +0 -0
  182. package/screenshots/truth/textinput/input-updated.png +0 -0
  183. package/screenshots/truth/textinput/input.png +0 -0
  184. package/screenshots/truth/textinput/textarea-focused.png +0 -0
  185. package/screenshots/truth/textinput/textarea.png +0 -0
  186. package/screenshots/truth/tip/bottom.png +0 -0
  187. package/screenshots/truth/tip/left.png +0 -0
  188. package/screenshots/truth/tip/right.png +0 -0
  189. package/screenshots/truth/tip/top.png +0 -0
  190. package/screenshots/truth/webchat/closed-widget.png +0 -0
  191. package/screenshots/truth/webchat/connected-state.png +0 -0
  192. package/screenshots/truth/webchat/connecting-state.png +0 -0
  193. package/screenshots/truth/webchat/disconnected-state.png +0 -0
  194. package/screenshots/truth/webchat/opened-widget.png +0 -0
  195. package/src/chart/TembaChart.ts +399 -0
  196. package/src/list/RunList.ts +11 -8
  197. package/src/locales/es.ts +1 -0
  198. package/src/locales/fr.ts +1 -0
  199. package/src/locales/pt.ts +1 -0
  200. package/src/options/Options.ts +39 -13
  201. package/src/select/Select.ts +32 -5
  202. package/src/store/AppState.ts +3 -3
  203. package/src/utils/index.ts +17 -5
  204. package/src/vectoricon/VectorIcon.ts +2 -1
  205. package/temba-modules.ts +2 -2
  206. package/test/temba-appstate-language.test.ts +218 -0
  207. package/test/temba-chart.test.ts +215 -0
  208. package/test/temba-dropdown.test.ts +444 -0
  209. package/test/temba-run-list.test.ts +774 -0
  210. package/test/temba-select.test.ts +27 -0
  211. package/test/temba-toast.test.ts +386 -0
  212. package/test/temba-utils-index.test.ts +1547 -0
  213. package/test/temba-webchat.test.ts +1095 -0
  214. package/test/utils.test.ts +4 -2
  215. package/test-assets/list/flow-results.json +17 -0
  216. package/test-assets/list/runs.json +126 -0
  217. package/test-assets/style.css +23 -0
  218. package/web-test-runner.config.mjs +33 -7
  219. package/xliff/es.xlf +3 -0
  220. package/xliff/fr.xlf +3 -0
  221. package/xliff/pt.xlf +3 -0
  222. package/out-tsc/src/outboxmonitor/OutboxMonitor.js +0 -136
  223. package/out-tsc/src/outboxmonitor/OutboxMonitor.js.map +0 -1
  224. package/src/outboxmonitor/OutboxMonitor.ts +0 -148
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-empty-function */
2
2
  import { TemplateResult, html, css, CSSResult, CSSResultArray } from 'lit';
3
- import { property } from 'lit/decorators.js';
3
+ import { property, state } from 'lit/decorators.js';
4
4
  import {
5
5
  getUrl,
6
6
  getClasses,
@@ -410,6 +410,9 @@ export class Select<T extends SelectOption> extends FormElement {
410
410
  @property({ type: Boolean })
411
411
  disabled = false;
412
412
 
413
+ @state()
414
+ attemptedOpen = false;
415
+
413
416
  @property({ attribute: false })
414
417
  selectedIndex = -1;
415
418
 
@@ -863,6 +866,7 @@ export class Select<T extends SelectOption> extends FormElement {
863
866
  }
864
867
 
865
868
  this.visibleOptions = [];
869
+ this.attemptedOpen = false;
866
870
  this.input = '';
867
871
  this.next = null;
868
872
  this.complete = true;
@@ -955,7 +959,9 @@ export class Select<T extends SelectOption> extends FormElement {
955
959
  }
956
960
 
957
961
  public isOpen(): boolean {
958
- return this.visibleOptions.length > 0;
962
+ return (
963
+ this.visibleOptions.length > 0 || (this.attemptedOpen && this.focused)
964
+ );
959
965
  }
960
966
 
961
967
  public setOptions(options: any[]): void {
@@ -1212,6 +1218,7 @@ export class Select<T extends SelectOption> extends FormElement {
1212
1218
 
1213
1219
  private handleBlur() {
1214
1220
  this.focused = false;
1221
+ this.attemptedOpen = false;
1215
1222
  if (this.visibleOptions.length > 0) {
1216
1223
  this.input = '';
1217
1224
  this.next = null;
@@ -1285,8 +1292,10 @@ export class Select<T extends SelectOption> extends FormElement {
1285
1292
  ) {
1286
1293
  if (
1287
1294
  this.visibleOptions.length === 0 &&
1288
- this.completionOptions.length === 0
1295
+ this.completionOptions.length === 0 &&
1296
+ !this.input
1289
1297
  ) {
1298
+ this.attemptedOpen = true;
1290
1299
  this.requestUpdate('input');
1291
1300
  return;
1292
1301
  }
@@ -1322,6 +1331,7 @@ export class Select<T extends SelectOption> extends FormElement {
1322
1331
 
1323
1332
  private handleCancel() {
1324
1333
  this.visibleOptions = [];
1334
+ this.attemptedOpen = false;
1325
1335
  }
1326
1336
 
1327
1337
  private handleCursorChanged(event: CustomEvent) {
@@ -1340,10 +1350,15 @@ export class Select<T extends SelectOption> extends FormElement {
1340
1350
  return;
1341
1351
  }
1342
1352
 
1343
- if (this.visibleOptions.length > 0) {
1353
+ // Check if dropdown is currently open (either with options or showing "No options")
1354
+ if (this.isOpen()) {
1344
1355
  this.visibleOptions = [];
1356
+ this.attemptedOpen = false;
1345
1357
  } else {
1358
+ this.attemptedOpen = true;
1346
1359
  this.requestUpdate('input');
1360
+ // Also trigger an immediate update to show empty dropdown
1361
+ this.requestUpdate();
1347
1362
  }
1348
1363
  }
1349
1364
  }
@@ -1461,6 +1476,17 @@ export class Select<T extends SelectOption> extends FormElement {
1461
1476
  this.requestUpdate('values', oldValues);
1462
1477
  }
1463
1478
 
1479
+ private shouldShowEmptyMessage(): boolean {
1480
+ return (
1481
+ this.attemptedOpen &&
1482
+ this.focused &&
1483
+ this.visibleOptions.length === 0 &&
1484
+ !this.input &&
1485
+ this.staticOptions.length === 0 &&
1486
+ !this.endpoint
1487
+ );
1488
+ }
1489
+
1464
1490
  public render(): TemplateResult {
1465
1491
  const placeholder = this.values.length === 0 ? this.placeholder : '';
1466
1492
  const placeholderDiv = html`
@@ -1620,7 +1646,8 @@ export class Select<T extends SelectOption> extends FormElement {
1620
1646
  .getName=${this.getNameInternal}
1621
1647
  ?static-width=${this.optionWidth}
1622
1648
  ?anchor-right=${this.anchorRight}
1623
- ?visible=${this.visibleOptions.length > 0}
1649
+ ?visible=${this.visibleOptions.length > 0 || this.shouldShowEmptyMessage()}
1650
+ ?showEmptyMessage=${this.shouldShowEmptyMessage()}
1624
1651
  ></temba-options>
1625
1652
 
1626
1653
  <temba-options
@@ -157,11 +157,11 @@ export const zustand = createStore<AppState>()(
157
157
  setFlowContents: (flow: FlowContents) => {
158
158
  set((state: AppState) => {
159
159
  const flowLang = flow.definition.language;
160
- const languageCode = state.languageCode || flowLang;
161
160
  state.flowDefinition = flow.definition;
162
161
  state.flowInfo = flow.info;
163
- state.isTranslating = flowLang !== languageCode;
164
- state.languageCode = languageCode || flowLang;
162
+ // Reset to the flow's default language when loading a new flow
163
+ state.languageCode = flowLang;
164
+ state.isTranslating = false;
165
165
  });
166
166
  },
167
167
 
@@ -494,7 +494,7 @@ export const getScrollParent = (node: any): any => {
494
494
  return null;
495
495
  };
496
496
 
497
- export const isElementVisible = (el: any, holder: any) => {
497
+ export const isElementVisible = (el: any, holder?: any) => {
498
498
  holder = holder || document.body;
499
499
  const { top, bottom } = el.getBoundingClientRect();
500
500
  const holderRect = holder.getBoundingClientRect();
@@ -575,7 +575,7 @@ export const timeSince = (
575
575
  }
576
576
  };
577
577
 
578
- export const isDate = (value: string): boolean => {
578
+ export const isDate = (value: any): boolean => {
579
579
  if (toString.call(value) === '[object Date]') {
580
580
  return true;
581
581
  }
@@ -635,6 +635,10 @@ export const truncate = (input: string, max: number): string => {
635
635
  };
636
636
 
637
637
  export const oxford = (items: any[], joiner = 'and'): any => {
638
+ if (items.length === 0) {
639
+ return '';
640
+ }
641
+
638
642
  if (items.length === 1) {
639
643
  return items[0];
640
644
  }
@@ -657,7 +661,9 @@ export const oxford = (items: any[], joiner = 'and'): any => {
657
661
  });
658
662
  }
659
663
 
660
- return items.join(', ') + joiner + items[items.length - 1];
664
+ const allButLast = items.slice(0, -1);
665
+ const last = items[items.length - 1];
666
+ return allButLast.join(', ') + ', ' + joiner + ' ' + last;
661
667
  };
662
668
 
663
669
  export const oxfordFn = (
@@ -713,13 +719,19 @@ export enum COOKIE_KEYS {
713
719
  TICKET_SHOW_DETAILS = 'tickets.show-details'
714
720
  }
715
721
 
716
- export const capitalize = ([first, ...rest], locale = navigator.language) =>
722
+ export const capitalize = (
723
+ [first, ...rest]: string[] | string,
724
+ locale = navigator.language
725
+ ) =>
717
726
  first === undefined ? '' : first.toLocaleUpperCase(locale) + rest.join('');
718
727
 
719
728
  export const formatFileType = (type: string): string => {
720
729
  return type.split('/')[1];
721
730
  };
722
- export const formatFileSize = (bytes: number, decimalPoint: number): string => {
731
+ export const formatFileSize = (
732
+ bytes: number,
733
+ decimalPoint?: number
734
+ ): string => {
723
735
  if (bytes == 0) return '0 KB';
724
736
  const k = 1024,
725
737
  dm = decimalPoint || 2,
@@ -127,9 +127,10 @@ export class VectorIcon extends LitElement {
127
127
 
128
128
  .spin-forever {
129
129
  animation-name: spin;
130
- animation-duration: 2000ms;
130
+ animation-duration: var(--test-animation-duration, 2000ms);
131
131
  animation-iteration-count: infinite;
132
132
  animation-timing-function: linear;
133
+ animation-play-state: var(--test-animation-play-state, running);
133
134
  }
134
135
 
135
136
  @keyframes spin {
package/temba-modules.ts CHANGED
@@ -58,13 +58,13 @@ import { MediaPicker } from './src/mediapicker/MediaPicker';
58
58
  import { Editor } from './src/flow/Editor';
59
59
  import { EditorNode } from './src/flow/EditorNode';
60
60
  import { ContactNotepad } from './src/contacts/ContactNotepad';
61
- import { OutboxMonitor } from './src/outboxmonitor/OutboxMonitor';
62
61
  import { ProgressBar } from './src/progress/ProgressBar';
63
62
  import { StartProgress } from './src/progress/StartProgress';
64
63
  import { ShortcutList } from './src/list/ShortcutList';
65
64
  import { PopupSelect } from './src/select/PopupSelect';
66
65
  import { UserSelect } from './src/select/UserSelect';
67
66
  import { WorkspaceSelect } from './src/select/WorkspaceSelect';
67
+ import { TembaChart } from './src/chart/TembaChart';
68
68
 
69
69
  export function addCustomElement(name: string, comp: any) {
70
70
  if (!window.customElements.get(name)) {
@@ -133,10 +133,10 @@ addCustomElement('temba-media-picker', MediaPicker);
133
133
  addCustomElement('temba-flow-editor', Editor);
134
134
  addCustomElement('temba-flow-node', EditorNode);
135
135
  addCustomElement('temba-contact-notepad', ContactNotepad);
136
- addCustomElement('temba-outbox-monitor', OutboxMonitor);
137
136
  addCustomElement('temba-progress', ProgressBar);
138
137
  addCustomElement('temba-start-progress', StartProgress);
139
138
  addCustomElement('temba-shortcuts', ShortcutList);
140
139
  addCustomElement('temba-popup-select', PopupSelect);
141
140
  addCustomElement('temba-user-select', UserSelect);
142
141
  addCustomElement('temba-workspace-select', WorkspaceSelect);
142
+ addCustomElement('temba-chart', TembaChart);
@@ -0,0 +1,218 @@
1
+ import { assert } from '@open-wc/testing';
2
+ import { zustand } from '../src/store/AppState';
3
+
4
+ describe('AppState Language Reset', () => {
5
+ beforeEach(() => {
6
+ // Reset the store state before each test
7
+ const state = zustand.getState();
8
+ zustand.setState({
9
+ ...state,
10
+ languageCode: '',
11
+ isTranslating: false,
12
+ flowDefinition: null,
13
+ flowInfo: null
14
+ });
15
+ });
16
+
17
+ it('should reset language when loading a new flow', () => {
18
+ const state = zustand.getState();
19
+
20
+ // First, load an initial flow to establish state
21
+ const initialFlowContents = {
22
+ definition: {
23
+ language: 'en',
24
+ localization: {},
25
+ name: 'Initial Flow',
26
+ nodes: [],
27
+ uuid: 'initial-uuid',
28
+ type: 'messaging' as const,
29
+ revision: 1,
30
+ spec_version: '14.3',
31
+ _ui: {
32
+ nodes: {},
33
+ languages: []
34
+ }
35
+ },
36
+ info: {
37
+ results: [],
38
+ dependencies: [],
39
+ counts: { nodes: 0, languages: 1 },
40
+ locals: []
41
+ }
42
+ };
43
+
44
+ state.setFlowContents(initialFlowContents);
45
+
46
+ // Simulate having a previous flow with localization
47
+ state.setLanguageCode('es'); // User selected Spanish for localization
48
+ assert.equal(zustand.getState().languageCode, 'es');
49
+ assert.equal(zustand.getState().isTranslating, true); // Now translating from English to Spanish
50
+
51
+ // Simulate loading a new flow with English as default
52
+ const mockFlowContents = {
53
+ definition: {
54
+ language: 'en',
55
+ localization: {},
56
+ name: 'Test Flow',
57
+ nodes: [],
58
+ uuid: 'test-uuid',
59
+ type: 'messaging' as const,
60
+ revision: 1,
61
+ spec_version: '14.3',
62
+ _ui: {
63
+ nodes: {},
64
+ languages: []
65
+ }
66
+ },
67
+ info: {
68
+ results: [],
69
+ dependencies: [],
70
+ counts: { nodes: 0, languages: 1 },
71
+ locals: []
72
+ }
73
+ };
74
+
75
+ state.setFlowContents(mockFlowContents);
76
+
77
+ // The language should reset to the flow's default language
78
+ assert.equal(
79
+ zustand.getState().languageCode,
80
+ 'en',
81
+ 'Language should reset to flow default'
82
+ );
83
+ assert.equal(
84
+ zustand.getState().isTranslating,
85
+ false,
86
+ 'Should not be in translation mode'
87
+ );
88
+ });
89
+
90
+ it('should set isTranslating correctly when languages differ', () => {
91
+ const state = zustand.getState();
92
+
93
+ // Load a flow with Spanish as default
94
+ const mockFlowContents = {
95
+ definition: {
96
+ language: 'es',
97
+ localization: {},
98
+ name: 'Test Flow',
99
+ nodes: [],
100
+ uuid: 'test-uuid',
101
+ type: 'messaging' as const,
102
+ revision: 1,
103
+ spec_version: '14.3',
104
+ _ui: {
105
+ nodes: {},
106
+ languages: []
107
+ }
108
+ },
109
+ info: {
110
+ results: [],
111
+ dependencies: [],
112
+ counts: { nodes: 0, languages: 1 },
113
+ locals: []
114
+ }
115
+ };
116
+
117
+ state.setFlowContents(mockFlowContents);
118
+
119
+ // Should be in Spanish and not translating
120
+ assert.equal(zustand.getState().languageCode, 'es');
121
+ assert.equal(zustand.getState().isTranslating, false);
122
+
123
+ // Now switch to English for localization
124
+ state.setLanguageCode('en');
125
+ assert.equal(zustand.getState().languageCode, 'en');
126
+ assert.equal(
127
+ zustand.getState().isTranslating,
128
+ true,
129
+ 'Should be translating when language differs from flow default'
130
+ );
131
+ });
132
+
133
+ it('should preserve flow default language when no previous language is set', () => {
134
+ const state = zustand.getState();
135
+
136
+ // Load a flow with French as default, no previous language set
137
+ const mockFlowContents = {
138
+ definition: {
139
+ language: 'fr',
140
+ localization: {},
141
+ name: 'Test Flow',
142
+ nodes: [],
143
+ uuid: 'test-uuid',
144
+ type: 'messaging' as const,
145
+ revision: 1,
146
+ spec_version: '14.3',
147
+ _ui: {
148
+ nodes: {},
149
+ languages: []
150
+ }
151
+ },
152
+ info: {
153
+ results: [],
154
+ dependencies: [],
155
+ counts: { nodes: 0, languages: 1 },
156
+ locals: []
157
+ }
158
+ };
159
+
160
+ state.setFlowContents(mockFlowContents);
161
+
162
+ // Should use the flow's default language
163
+ assert.equal(
164
+ zustand.getState().languageCode,
165
+ 'fr',
166
+ 'Should use flow default language'
167
+ );
168
+ assert.equal(
169
+ zustand.getState().isTranslating,
170
+ false,
171
+ 'Should not be translating'
172
+ );
173
+ });
174
+
175
+ it('should handle language switching after flow is loaded', () => {
176
+ const state = zustand.getState();
177
+
178
+ // Load a flow with English as default
179
+ const mockFlowContents = {
180
+ definition: {
181
+ language: 'en',
182
+ localization: {},
183
+ name: 'Test Flow',
184
+ nodes: [],
185
+ uuid: 'test-uuid',
186
+ type: 'messaging' as const,
187
+ revision: 1,
188
+ spec_version: '14.3',
189
+ _ui: {
190
+ nodes: {},
191
+ languages: []
192
+ }
193
+ },
194
+ info: {
195
+ results: [],
196
+ dependencies: [],
197
+ counts: { nodes: 0, languages: 1 },
198
+ locals: []
199
+ }
200
+ };
201
+
202
+ state.setFlowContents(mockFlowContents);
203
+
204
+ // Should start in English, not translating
205
+ assert.equal(zustand.getState().languageCode, 'en');
206
+ assert.equal(zustand.getState().isTranslating, false);
207
+
208
+ // Switch to Spanish for localization
209
+ state.setLanguageCode('es');
210
+ assert.equal(zustand.getState().languageCode, 'es');
211
+ assert.equal(zustand.getState().isTranslating, true);
212
+
213
+ // Switch back to English
214
+ state.setLanguageCode('en');
215
+ assert.equal(zustand.getState().languageCode, 'en');
216
+ assert.equal(zustand.getState().isTranslating, false);
217
+ });
218
+ });
@@ -0,0 +1,215 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import {
3
+ RapidChartData,
4
+ TembaChart,
5
+ formatDurationFromSeconds
6
+ } from '../src/chart/TembaChart';
7
+ import { getComponent } from './utils.test';
8
+
9
+ const sampleData: RapidChartData = {
10
+ labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
11
+ datasets: [
12
+ {
13
+ label: 'General',
14
+ data: [65, 59, 80, 81, 56, 55, 40]
15
+ },
16
+ {
17
+ label: 'Support',
18
+ data: [28, 48, 40, 19, 86, 27, 90]
19
+ },
20
+ {
21
+ label: 'VIPs',
22
+ data: [58, 28, 10, 1, 0, 19, 20]
23
+ }
24
+ ]
25
+ };
26
+
27
+ const TAG = 'temba-chart';
28
+ const getChart = async (attrs: any = {}) => {
29
+ const picker = (await getComponent(TAG, attrs, '', 400)) as TembaChart;
30
+ return picker;
31
+ };
32
+
33
+ describe('temba-chart', () => {
34
+ it('calculates others', async () => {
35
+ const chart: TembaChart = await getChart();
36
+
37
+ chart.data = sampleData;
38
+ await chart.updateComplete;
39
+
40
+ // if we haven't set any splits, everything should be summed as "All"
41
+ expect(chart.datasets[0].data).to.deep.equal([
42
+ 151, 135, 130, 101, 142, 101, 150
43
+ ]);
44
+
45
+ // add a split
46
+ chart.splits = ['General'];
47
+ await chart.updateComplete;
48
+
49
+ // now we should get an "Others" dataset
50
+ expect(chart.datasets[1].data).to.deep.equal([86, 76, 50, 20, 86, 46, 110]);
51
+
52
+ // add another split
53
+ chart.splits = ['General', 'Support'];
54
+ await chart.updateComplete;
55
+
56
+ // now others should be everything but general and support
57
+ expect(chart.datasets[2].data).to.deep.equal([58, 28, 10, 1, 0, 19, 20]);
58
+ });
59
+
60
+ it('supports duration formatting', async () => {
61
+ const chart: TembaChart = await getChart();
62
+
63
+ // Test that formatDuration property exists and defaults to false
64
+ expect(chart.formatDuration).to.equal(false);
65
+
66
+ // Test that we can set formatDuration to true
67
+ chart.formatDuration = true;
68
+ expect(chart.formatDuration).to.equal(true);
69
+ });
70
+
71
+ it('formats duration values correctly', async () => {
72
+ const chart: TembaChart = await getChart();
73
+
74
+ // Access the formatDurationFromSeconds function through the chart's module
75
+ // We need to test the duration formatting logic
76
+ const durationData: RapidChartData = {
77
+ labels: ['Day 1', 'Day 2', 'Day 3'],
78
+ datasets: [
79
+ {
80
+ label: 'Process Time',
81
+ data: [68787, 958000, 3661] // seconds that should be formatted as durations
82
+ }
83
+ ]
84
+ };
85
+
86
+ chart.formatDuration = true;
87
+ chart.data = durationData;
88
+ await chart.updateComplete;
89
+
90
+ // Wait for the chart to be created after data is set
91
+ await new Promise((resolve) => setTimeout(resolve, 100));
92
+
93
+ // Test that the chart was created and has the duration formatting enabled
94
+ expect(chart.formatDuration).to.equal(true);
95
+ expect(chart.chart).to.exist;
96
+
97
+ // Test that the chart configuration includes the duration formatting
98
+ const chartConfig = chart.chart.options;
99
+ expect(chartConfig.scales.y.ticks).to.exist;
100
+ expect(chartConfig.scales.y.ticks.callback).to.be.a('function');
101
+
102
+ // Test the tick callback function formatting
103
+ const tickCallback = chartConfig.scales.y.ticks.callback;
104
+ expect(tickCallback.call({}, 68787, 0, [])).to.equal('19h 6m');
105
+ expect(tickCallback.call({}, 958000, 1, [])).to.equal('11d 2h');
106
+ expect(tickCallback.call({}, 3661, 2, [])).to.equal('1h 1m');
107
+ expect(tickCallback.call({}, 120, 3, [])).to.equal('2m');
108
+ expect(tickCallback.call({}, 0, 4, [])).to.equal('0s');
109
+
110
+ // Test tooltip formatting
111
+ expect(chartConfig.plugins.tooltip.callbacks.label).to.be.a('function');
112
+ const tooltipCallback = chartConfig.plugins.tooltip.callbacks.label;
113
+
114
+ const mockContext = {
115
+ dataset: { label: 'Process Time' },
116
+ parsed: { y: 68787 }
117
+ };
118
+ expect(tooltipCallback.call({}, mockContext)).to.equal(
119
+ 'Process Time: 19h 6m'
120
+ );
121
+ });
122
+
123
+ it('formats various duration edge cases correctly', async () => {
124
+ const chart: TembaChart = await getChart();
125
+ chart.formatDuration = true;
126
+ chart.data = sampleData;
127
+ await chart.updateComplete;
128
+
129
+ // Wait for the chart to be created after data is set
130
+ await new Promise((resolve) => setTimeout(resolve, 100));
131
+
132
+ expect(chart.chart).to.exist;
133
+ const tickCallback = chart.chart.options.scales.y.ticks.callback;
134
+
135
+ // Test edge cases for duration formatting
136
+ expect(tickCallback.call({}, 0, 0, [])).to.equal('0s');
137
+ expect(tickCallback.call({}, 1, 1, [])).to.equal('1s');
138
+ expect(tickCallback.call({}, 59, 2, [])).to.equal('59s');
139
+ expect(tickCallback.call({}, 60, 3, [])).to.equal('1m');
140
+ expect(tickCallback.call({}, 61, 4, [])).to.equal('1m 1s');
141
+ expect(tickCallback.call({}, 3600, 5, [])).to.equal('1h');
142
+ expect(tickCallback.call({}, 3661, 6, [])).to.equal('1h 1m');
143
+ expect(tickCallback.call({}, 86400, 7, [])).to.equal('1d');
144
+ expect(tickCallback.call({}, 90061, 8, [])).to.equal('1d 1h'); // 1 day, 1 hour, 1 minute, 1 second - should show only first two units
145
+ expect(tickCallback.call({}, 604800, 9, [])).to.equal('7d'); // 1 week in seconds
146
+ expect(tickCallback.call({}, 1209600, 10, [])).to.equal('14d'); // 2 weeks in seconds
147
+ });
148
+
149
+ it('respects formatDuration property state', async () => {
150
+ const chart: TembaChart = await getChart();
151
+
152
+ // Test default state
153
+ expect(chart.formatDuration).to.equal(false);
154
+
155
+ chart.data = sampleData;
156
+ await chart.updateComplete;
157
+
158
+ // Test that formatDuration property can be toggled
159
+ chart.formatDuration = true;
160
+ expect(chart.formatDuration).to.equal(true);
161
+
162
+ chart.formatDuration = false;
163
+ expect(chart.formatDuration).to.equal(false);
164
+ });
165
+ });
166
+
167
+ describe('formatDurationFromSeconds', () => {
168
+ it('formats zero correctly', () => {
169
+ expect(formatDurationFromSeconds(0)).to.equal('0s');
170
+ });
171
+
172
+ it('formats seconds only', () => {
173
+ expect(formatDurationFromSeconds(1)).to.equal('1s');
174
+ expect(formatDurationFromSeconds(30)).to.equal('30s');
175
+ expect(formatDurationFromSeconds(59)).to.equal('59s');
176
+ });
177
+
178
+ it('formats minutes and seconds', () => {
179
+ expect(formatDurationFromSeconds(60)).to.equal('1m');
180
+ expect(formatDurationFromSeconds(61)).to.equal('1m 1s');
181
+ expect(formatDurationFromSeconds(90)).to.equal('1m 30s');
182
+ expect(formatDurationFromSeconds(120)).to.equal('2m');
183
+ expect(formatDurationFromSeconds(3599)).to.equal('59m 59s');
184
+ });
185
+
186
+ it('formats hours and minutes', () => {
187
+ expect(formatDurationFromSeconds(3600)).to.equal('1h');
188
+ expect(formatDurationFromSeconds(3661)).to.equal('1h 1m');
189
+ expect(formatDurationFromSeconds(7200)).to.equal('2h');
190
+ expect(formatDurationFromSeconds(68787)).to.equal('19h 6m');
191
+ });
192
+
193
+ it('formats days and hours', () => {
194
+ expect(formatDurationFromSeconds(86400)).to.equal('1d');
195
+ expect(formatDurationFromSeconds(90000)).to.equal('1d 1h');
196
+ expect(formatDurationFromSeconds(958000)).to.equal('11d 2h');
197
+ });
198
+
199
+ it('shows only two most significant units', () => {
200
+ // 1 day, 1 hour, 1 minute, 1 second - should show only "1d 1h"
201
+ expect(formatDurationFromSeconds(90061)).to.equal('1d 1h');
202
+
203
+ // 2 hours, 30 minutes, 45 seconds - should show only "2h 30m"
204
+ expect(formatDurationFromSeconds(9045)).to.equal('2h 30m');
205
+
206
+ // 5 minutes, 30 seconds - should show "5m 30s"
207
+ expect(formatDurationFromSeconds(330)).to.equal('5m 30s');
208
+ });
209
+
210
+ it('handles large durations', () => {
211
+ expect(formatDurationFromSeconds(604800)).to.equal('7d'); // 1 week
212
+ expect(formatDurationFromSeconds(1209600)).to.equal('14d'); // 2 weeks
213
+ expect(formatDurationFromSeconds(2678400)).to.equal('31d'); // ~1 month
214
+ });
215
+ });