@nyaruka/temba-components 0.171.0 → 0.172.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 (37) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/dist/temba-components.js +1015 -747
  3. package/dist/temba-components.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/RapidElement.ts +83 -41
  6. package/src/display/Button.ts +14 -2
  7. package/src/display/Chat.ts +41 -22
  8. package/src/display/Label.ts +18 -3
  9. package/src/events/eventRenderers.ts +4 -0
  10. package/src/flow/FlowSearch.ts +12 -392
  11. package/src/flow/actions/send_msg.ts +2 -1
  12. package/src/flow/actions/set_contact_field.ts +1 -0
  13. package/src/flow/actions/set_contact_name.ts +1 -0
  14. package/src/flow/nodes/split_by_ticket.ts +1 -0
  15. package/src/flow/nodes/wait_for_dial.ts +1 -0
  16. package/src/form/RangePicker.ts +10 -2
  17. package/src/form/select/Select.ts +15 -2
  18. package/src/layout/SearchModal.ts +564 -0
  19. package/src/list/BroadcastList.ts +17 -6
  20. package/src/list/ContentList.ts +29 -13
  21. package/src/list/FieldList.ts +8 -1
  22. package/src/list/TembaMenu.ts +71 -13
  23. package/src/live/CampaignEvents.ts +31 -11
  24. package/src/live/ContactChat.ts +276 -76
  25. package/src/live/ContactTimeline.ts +46 -24
  26. package/src/live/ContactWatch.ts +166 -63
  27. package/src/live/Realtime.ts +137 -8
  28. package/src/live/SocketService.ts +115 -10
  29. package/src/live/TicketSearch.ts +290 -0
  30. package/src/live/Watchers.ts +93 -0
  31. package/src/simulator/Simulator.ts +59 -36
  32. package/src/store/Store.ts +17 -34
  33. package/src/styles/designTokens.ts +57 -17
  34. package/src/styles/pillVariants.ts +42 -10
  35. package/static/css/design-system.css +47 -12
  36. package/static/css/temba-components.css +31 -9
  37. package/temba-modules.ts +2 -0
@@ -41,6 +41,10 @@ interface MenuItemState {
41
41
  collapsed?: string;
42
42
  }
43
43
 
44
+ // fields the menu maintains on an item rather than reading off the server, so
45
+ // a reload carries them across instead of treating them as dropped
46
+ const OWN_ITEM_FIELDS = ['items', 'level', 'loading'];
47
+
44
48
  const findItem = (
45
49
  items: MenuItem[],
46
50
  id: string
@@ -738,8 +742,17 @@ export class TembaMenu extends ResizeElement {
738
742
  @property({ type: Object })
739
743
  pressedItem: MenuItem;
740
744
 
741
- // http promise to monitor for completeness
742
- public httpComplete: Promise<void>;
745
+ /**
746
+ * Resolves when every load in flight has settled. Awaiting it has to mean
747
+ * "the menu has finished fetching", not "the last load anyone started has
748
+ * finished" - loads overlap (a click while a debounced refresh is armed),
749
+ * and a caller that awaited whichever promise happened to be here last
750
+ * would read the menu mid-update.
751
+ */
752
+ public httpComplete: Promise<void> = Promise.resolve();
753
+
754
+ private loadsInFlight = 0;
755
+ private loadsSettled: () => void;
743
756
 
744
757
  root: MenuItem;
745
758
  selection: string[] = [];
@@ -854,22 +867,66 @@ export class TembaMenu extends ResizeElement {
854
867
  });
855
868
  }
856
869
 
870
+ /**
871
+ * Folds a load into httpComplete, which stays unresolved until the last one
872
+ * outstanding settles.
873
+ */
874
+ private trackLoad(load: Promise<void>): void {
875
+ if (this.loadsInFlight === 0) {
876
+ this.httpComplete = new Promise((resolve) => {
877
+ this.loadsSettled = resolve;
878
+ });
879
+ }
880
+ this.loadsInFlight++;
881
+
882
+ load.then(() => {
883
+ if (--this.loadsInFlight === 0) {
884
+ this.loadsSettled();
885
+ }
886
+ });
887
+ }
888
+
889
+ /**
890
+ * Reloading a level reuses the item objects it already had. Replacing them
891
+ * would orphan anything already pointing at one - a rendered click handler,
892
+ * or a load in flight for that item's own children, which would then land
893
+ * on a copy no longer in the tree.
894
+ */
895
+ private mergeItems(item: MenuItem, items: MenuItem[]): MenuItem[] {
896
+ const previous = item.items || [];
897
+ return items.map((newItem) => {
898
+ const prevItem = previous.find((prev) => prev.id == newItem.id);
899
+ if (!prevItem) {
900
+ return newItem;
901
+ }
902
+
903
+ // sub-items the reload didn't speak to are ours to carry over - either
904
+ // ones we already had or ones still on their way
905
+ const carried = newItem.items || prevItem.items;
906
+
907
+ // anything the reload dropped is gone, so this reads as a replacement
908
+ // everywhere except identity - bar the fields the menu sets on an item
909
+ // itself, which the server never speaks to
910
+ Object.keys(prevItem).forEach((key) => {
911
+ if (!(key in newItem) && !OWN_ITEM_FIELDS.includes(key)) {
912
+ delete prevItem[key];
913
+ }
914
+ });
915
+ Object.assign(prevItem, newItem);
916
+ if (carried) {
917
+ prevItem.items = carried;
918
+ }
919
+ return prevItem;
920
+ });
921
+ }
922
+
857
923
  // eslint-disable-next-line @typescript-eslint/no-empty-function
858
924
  private loadItems(item: MenuItem, event: MouseEvent | KeyboardEvent = null) {
859
925
  if (item && item.endpoint) {
860
926
  item.loading = true;
861
- this.httpComplete = fetchResults(item.endpoint)
927
+ const load = fetchResults(item.endpoint)
862
928
  .then((items: MenuItem[]) => {
863
- items.forEach((newItem) => {
864
- if (!newItem.items) {
865
- const prevItem = (item.items || []).find(
866
- (prev) => prev.id == newItem.id
867
- );
868
- if (prevItem && prevItem.items) {
869
- newItem.items = prevItem.items;
870
- }
871
- }
872
- });
929
+ items = this.mergeItems(item, items);
873
930
 
874
931
  // update our item level
875
932
  items.forEach((subItem) => {
@@ -901,6 +958,7 @@ export class TembaMenu extends ResizeElement {
901
958
  .catch((error) => {
902
959
  this.fireCustomEvent(CustomEventType.Error, { error });
903
960
  });
961
+ this.trackLoad(load);
904
962
  }
905
963
  }
906
964
 
@@ -273,11 +273,20 @@ export class CampaignEvents extends EndpointMonitorElement {
273
273
  justify-content: center;
274
274
  /* icons inside the dot take the dot's hue */
275
275
  --icon-color: var(--dot-color);
276
- background: color-mix(
277
- in srgb,
278
- var(--dot-color) 12%,
279
- var(--color-widget-bg, #fff)
280
- );
276
+ /* plain widget bg for browsers without color-mix(); the tinted
277
+ fill below needs the @supports gate since var() keeps an
278
+ invalid declaration alive until computed-value time */
279
+ background: var(--color-widget-bg, #fff);
280
+ }
281
+
282
+ @supports (color: color-mix(in srgb, red, red)) {
283
+ .dot {
284
+ background: color-mix(
285
+ in srgb,
286
+ var(--dot-color) 12%,
287
+ var(--color-widget-bg, #fff)
288
+ );
289
+ }
281
290
  }
282
291
 
283
292
  /* the anchor marks the field's date itself - painted with the widget
@@ -470,7 +479,8 @@ export class CampaignEvents extends EndpointMonitorElement {
470
479
 
471
480
  .menu-button.destructive:hover {
472
481
  border-color: #dc2626;
473
- background: color-mix(in srgb, #dc2626 6%, var(--surface, #fff));
482
+ /* pre-mixed fallback for browsers without color-mix() */
483
+ background: #fdf2f2;
474
484
  }
475
485
 
476
486
  /* the event's action, contained with a leading type label. Children
@@ -481,16 +491,26 @@ export class CampaignEvents extends EndpointMonitorElement {
481
491
  align-items: flex-start;
482
492
  gap: 0.6em;
483
493
  padding: 0.9em 1em 1em;
484
- background: color-mix(
485
- in srgb,
486
- var(--sunken, #f1f3f5) 45%,
487
- var(--surface, #fff)
488
- );
494
+ /* pre-mixed fallback for browsers without color-mix() */
495
+ background: #f9fafa;
489
496
  border: 1px solid var(--border, #e4e7ec);
490
497
  border-radius: var(--r);
491
498
  line-height: 1.5;
492
499
  }
493
500
 
501
+ @supports (color: color-mix(in srgb, red, red)) {
502
+ .menu-button.destructive:hover {
503
+ background: color-mix(in srgb, #dc2626 6%, var(--surface, #fff));
504
+ }
505
+ .detail-action {
506
+ background: color-mix(
507
+ in srgb,
508
+ var(--sunken, #f1f3f5) 45%,
509
+ var(--surface, #fff)
510
+ );
511
+ }
512
+ }
513
+
494
514
  /* the close (✕) in the header's upper right - the square
495
515
  hover-wash button treatment (the pager buttons'), sitting
496
516
  just outboard of the action buttons */