@nyaruka/temba-components 0.112.0 → 0.114.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 (130) hide show
  1. package/CHANGELOG.md +27 -2
  2. package/demo/index.html +1 -1
  3. package/dist/temba-components.js +794 -968
  4. package/dist/temba-components.js.map +1 -1
  5. package/out-tsc/src/aliaseditor/AliasEditor.js.map +1 -1
  6. package/out-tsc/src/button/Button.js +6 -2
  7. package/out-tsc/src/button/Button.js.map +1 -1
  8. package/out-tsc/src/chat/Chat.js +29 -7
  9. package/out-tsc/src/chat/Chat.js.map +1 -1
  10. package/out-tsc/src/compose/Compose.js +16 -20
  11. package/out-tsc/src/compose/Compose.js.map +1 -1
  12. package/out-tsc/src/contacts/ContactChat.js +240 -114
  13. package/out-tsc/src/contacts/ContactChat.js.map +1 -1
  14. package/out-tsc/src/contacts/ContactFieldEditor.js.map +1 -1
  15. package/out-tsc/src/contacts/events.js.map +1 -1
  16. package/out-tsc/src/contacts/helpers.js +5 -1
  17. package/out-tsc/src/contacts/helpers.js.map +1 -1
  18. package/out-tsc/src/contactsearch/ContactSearch.js +1 -1
  19. package/out-tsc/src/contactsearch/ContactSearch.js.map +1 -1
  20. package/out-tsc/src/dropdown/Dropdown.js +121 -108
  21. package/out-tsc/src/dropdown/Dropdown.js.map +1 -1
  22. package/out-tsc/src/interfaces.js +2 -0
  23. package/out-tsc/src/interfaces.js.map +1 -1
  24. package/out-tsc/src/list/ContentMenu.js +11 -8
  25. package/out-tsc/src/list/ContentMenu.js.map +1 -1
  26. package/out-tsc/src/list/RunList.js.map +1 -1
  27. package/out-tsc/src/list/TembaList.js +21 -14
  28. package/out-tsc/src/list/TembaList.js.map +1 -1
  29. package/out-tsc/src/list/TembaMenu.js +11 -12
  30. package/out-tsc/src/list/TembaMenu.js.map +1 -1
  31. package/out-tsc/src/list/TicketList.js +10 -0
  32. package/out-tsc/src/list/TicketList.js.map +1 -1
  33. package/out-tsc/src/omnibox/Omnibox.js +33 -90
  34. package/out-tsc/src/omnibox/Omnibox.js.map +1 -1
  35. package/out-tsc/src/options/Options.js +49 -47
  36. package/out-tsc/src/options/Options.js.map +1 -1
  37. package/out-tsc/src/select/PopupSelect.js +57 -0
  38. package/out-tsc/src/select/PopupSelect.js.map +1 -0
  39. package/out-tsc/src/select/Select.js +194 -144
  40. package/out-tsc/src/select/Select.js.map +1 -1
  41. package/out-tsc/src/select/UserSelect.js +67 -0
  42. package/out-tsc/src/select/UserSelect.js.map +1 -0
  43. package/out-tsc/src/store/Store.js +65 -14
  44. package/out-tsc/src/store/Store.js.map +1 -1
  45. package/out-tsc/src/tabpane/TabPane.js +82 -115
  46. package/out-tsc/src/tabpane/TabPane.js.map +1 -1
  47. package/out-tsc/src/textinput/TextInput.js +1 -0
  48. package/out-tsc/src/textinput/TextInput.js.map +1 -1
  49. package/out-tsc/src/user/TembaUser.js +24 -37
  50. package/out-tsc/src/user/TembaUser.js.map +1 -1
  51. package/out-tsc/src/utils/index.js +13 -6
  52. package/out-tsc/src/utils/index.js.map +1 -1
  53. package/out-tsc/temba-modules.js +4 -2
  54. package/out-tsc/temba-modules.js.map +1 -1
  55. package/out-tsc/test/temba-omnibox.test.js +43 -4
  56. package/out-tsc/test/temba-omnibox.test.js.map +1 -1
  57. package/out-tsc/test/temba-select.test.js +121 -65
  58. package/out-tsc/test/temba-select.test.js.map +1 -1
  59. package/out-tsc/test/utils.test.js +4 -0
  60. package/out-tsc/test/utils.test.js.map +1 -1
  61. package/package.json +1 -1
  62. package/screenshots/truth/compose/attachments-tab.png +0 -0
  63. package/screenshots/truth/compose/attachments-with-files-focused.png +0 -0
  64. package/screenshots/truth/compose/attachments-with-files.png +0 -0
  65. package/screenshots/truth/compose/intial-text.png +0 -0
  66. package/screenshots/truth/compose/no-counter.png +0 -0
  67. package/screenshots/truth/compose/wraps-text-and-spaces.png +0 -0
  68. package/screenshots/truth/compose/wraps-text-and-url.png +0 -0
  69. package/screenshots/truth/compose/wraps-text-no-spaces.png +0 -0
  70. package/screenshots/truth/contacts/chat-failure.png +0 -0
  71. package/screenshots/truth/contacts/chat-for-active-contact.png +0 -0
  72. package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
  73. package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
  74. package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
  75. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  76. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  77. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  78. package/screenshots/truth/content-menu/item-no-buttons.png +0 -0
  79. package/screenshots/truth/content-menu/items-and-buttons.png +0 -0
  80. package/screenshots/truth/omnibox/selected.png +0 -0
  81. package/screenshots/truth/select/enabled-multi-selection.png +0 -0
  82. package/screenshots/truth/select/endpoint-initial-value-updated.png +0 -0
  83. package/screenshots/truth/select/endpoint-initial-value.png +0 -0
  84. package/screenshots/truth/select/expressions.png +0 -0
  85. package/screenshots/truth/select/functions.png +0 -0
  86. package/screenshots/truth/select/initial-value.png +0 -0
  87. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  88. package/screenshots/truth/select/multiple-initial-values.png +0 -0
  89. package/screenshots/truth/select/selected-multi-test.png +0 -0
  90. package/screenshots/truth/select/static-initial-value.png +0 -0
  91. package/screenshots/truth/select/static-initial-via-selected.png +0 -0
  92. package/screenshots/truth/select/value-initial.png +0 -0
  93. package/src/aliaseditor/AliasEditor.ts +1 -1
  94. package/src/button/Button.ts +6 -2
  95. package/src/chat/Chat.ts +28 -6
  96. package/src/compose/Compose.ts +17 -22
  97. package/src/contacts/ContactChat.ts +260 -118
  98. package/src/contacts/ContactFieldEditor.ts +1 -1
  99. package/src/contacts/events.ts +1 -0
  100. package/src/contacts/helpers.ts +8 -1
  101. package/src/contactsearch/ContactSearch.ts +3 -3
  102. package/src/dropdown/Dropdown.ts +142 -103
  103. package/src/interfaces.ts +4 -1
  104. package/src/list/ContentMenu.ts +11 -9
  105. package/src/list/RunList.ts +3 -1
  106. package/src/list/TembaList.ts +24 -14
  107. package/src/list/TembaMenu.ts +14 -15
  108. package/src/list/TicketList.ts +11 -0
  109. package/src/omnibox/Omnibox.ts +34 -95
  110. package/src/options/Options.ts +57 -60
  111. package/src/select/PopupSelect.ts +53 -0
  112. package/src/select/Select.ts +182 -112
  113. package/src/select/UserSelect.ts +71 -0
  114. package/src/store/Store.ts +70 -21
  115. package/src/tabpane/TabPane.ts +91 -113
  116. package/src/textinput/TextInput.ts +1 -0
  117. package/src/user/TembaUser.ts +30 -41
  118. package/src/utils/index.ts +12 -8
  119. package/temba-modules.ts +4 -2
  120. package/test/temba-omnibox.test.ts +56 -4
  121. package/test/temba-select.test.ts +170 -56
  122. package/test/utils.test.ts +5 -0
  123. package/test-assets/select/omnibox.json +55 -0
  124. package/web-test-runner.config.mjs +16 -4
  125. package/out-tsc/src/contacts/ContactTickets.js +0 -462
  126. package/out-tsc/src/contacts/ContactTickets.js.map +0 -1
  127. package/out-tsc/test/temba-contact-tickets.test.js +0 -36
  128. package/out-tsc/test/temba-contact-tickets.test.js.map +0 -1
  129. package/src/contacts/ContactTickets.ts +0 -490
  130. package/test/temba-contact-tickets.test.ts +0 -52
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/no-empty-function */
2
- import { TemplateResult, html, css } from 'lit';
2
+ import { TemplateResult, html, css, CSSResult, CSSResultArray } from 'lit';
3
3
  import { property } from 'lit/decorators.js';
4
4
  import {
5
5
  getUrl,
@@ -26,12 +26,20 @@ import { msg } from '@lit/localize';
26
26
 
27
27
  const LOOK_AHEAD = 20;
28
28
 
29
- export class Select extends FormElement {
29
+ export interface SelectOption {
30
+ name: string;
31
+ value?: string;
32
+ expression?: boolean;
33
+ selected?: boolean;
34
+ }
35
+
36
+ export class Select<T extends SelectOption> extends FormElement {
30
37
  private hiddenInputs: HTMLInputElement[] = [];
31
38
 
32
- static get styles() {
39
+ static get styles(): CSSResult | CSSResultArray {
33
40
  return css`
34
41
  :host {
42
+ --transition-speed: 0;
35
43
  font-family: var(--font-family);
36
44
  transition: all ease-in-out var(--transition-speed);
37
45
  display: inline;
@@ -44,6 +52,7 @@ export class Select extends FormElement {
44
52
  temba-options {
45
53
  --temba-options-font-size: var(--temba-select-selected-font-size);
46
54
  --icon-color: var(--color-text-dark);
55
+ --color-options-bg: #fff;
47
56
  }
48
57
 
49
58
  :host:focus {
@@ -370,6 +379,9 @@ export class Select extends FormElement {
370
379
  @property({ type: Boolean })
371
380
  fetching: boolean;
372
381
 
382
+ @property({ type: Boolean })
383
+ resolving: boolean;
384
+
373
385
  @property({ type: Boolean })
374
386
  searchable = false;
375
387
 
@@ -428,7 +440,7 @@ export class Select extends FormElement {
428
440
  infoText = '';
429
441
 
430
442
  @property({ type: Array })
431
- values: any[] = [];
443
+ values: T[] = [];
432
444
 
433
445
  @property({ type: Object })
434
446
  selection: any;
@@ -438,10 +450,7 @@ export class Select extends FormElement {
438
450
  option[this.nameKey || 'name'];
439
451
 
440
452
  @property({ attribute: false })
441
- isMatch: (option: any, q: string) => boolean = (option: any, q: string) => {
442
- const name = this.getName(option) || '';
443
- return name.toLowerCase().indexOf(q) > -1;
444
- };
453
+ isMatch: (option: any, q: string) => boolean = this.isMatchDefault;
445
454
 
446
455
  @property({ attribute: false })
447
456
  getValue: (option: any) => string = (option: any) =>
@@ -481,7 +490,7 @@ export class Select extends FormElement {
481
490
  getOptions: (response: WebResponse) => any[] = this.getOptionsDefault;
482
491
 
483
492
  @property({ attribute: false })
484
- prepareOptions: (options: any[]) => any[] = (options: any[]) => options;
493
+ prepareOptions: (options: any[]) => any[] = this.prepareOptionsDefault;
485
494
 
486
495
  @property({ attribute: false })
487
496
  isComplete: (newestOptions: any[], response: WebResponse) => boolean =
@@ -500,18 +509,32 @@ export class Select extends FormElement {
500
509
 
501
510
  private lastQuery: number;
502
511
 
503
- // private cancelToken: CancelTokenSource;
504
512
  private complete: boolean;
505
513
  private page: number;
506
514
  private next: string = null;
507
515
  private query: string;
508
516
 
509
- private removingSelection: boolean;
510
-
511
517
  private lruCache = lru(20, 60000);
512
518
 
519
+ constructor() {
520
+ super();
521
+ this.renderOptionDefault = this.renderOptionDefault.bind(this);
522
+ this.renderSelectedItemDefault = this.renderSelectedItemDefault.bind(this);
523
+ this.prepareOptionsDefault = this.prepareOptionsDefault.bind(this);
524
+ this.isMatchDefault = this.isMatchDefault.bind(this);
525
+ }
526
+
527
+ public prepareOptionsDefault(options: T[]): T[] {
528
+ return options;
529
+ }
530
+
531
+ public isMatchDefault(option: T, q: string) {
532
+ const name = this.getName(option) || '';
533
+ return name.toLowerCase().indexOf(q) > -1;
534
+ }
535
+
513
536
  public handleSlotChange() {
514
- if (this.staticOptions.length === 0) {
537
+ if (this.staticOptions && this.staticOptions.length === 0) {
515
538
  for (const child of this.children) {
516
539
  if (child.tagName === 'TEMBA-OPTION') {
517
540
  const option: any = {};
@@ -520,12 +543,26 @@ export class Select extends FormElement {
520
543
  }
521
544
 
522
545
  if (option) {
546
+ let selected = false;
547
+
548
+ // if the option is marked as selected then accept it
549
+ if (option['selected'] !== undefined) {
550
+ delete option['selected'];
551
+ selected = true;
552
+ }
553
+
554
+ // the option value might also match the widget value
555
+ const selectValue = this.value || this.getAttribute('value');
556
+ if (selectValue) {
557
+ const optionValue = this.getValue(option);
558
+ if (optionValue == selectValue) {
559
+ selected = true;
560
+ }
561
+ }
562
+
523
563
  this.staticOptions.push(option);
524
- if (
525
- child.getAttribute('selected') !== null ||
526
- this.getValue(option) == this.value
527
- ) {
528
- if (this.getAttribute('multi') !== null) {
564
+ if (selected) {
565
+ if (this.multi) {
529
566
  this.addValue(option);
530
567
  } else {
531
568
  this.setValues([option]);
@@ -536,43 +573,49 @@ export class Select extends FormElement {
536
573
  }
537
574
  }
538
575
 
539
- this.checkSelectedOption();
540
-
541
576
  if (this.searchable && this.staticOptions.length === 0) {
542
577
  this.quietMillis = 200;
543
578
  }
544
579
  }
545
580
 
546
581
  private checkSelectedOption() {
547
- if (this.values.length === 0 && (!this.placeholder || this.value)) {
548
- if (this.staticOptions.length == 0 && this.endpoint) {
549
- const value = this.value;
550
- // see if we need fetch to select an option
551
- fetchResults(this.endpoint).then((results: any) => {
552
- if (results && results.length > 0) {
553
- if (value) {
554
- // if we started with a value, see if we can find it in the results
555
- const existing = results.find((option) => {
556
- return this.getValue(option) === value;
557
- });
558
-
559
- if (existing) {
560
- this.setValues([existing]);
561
- return;
562
- }
582
+ // see if we need fetch to select an option
583
+ if (
584
+ this.value &&
585
+ this.values.length == 0 &&
586
+ this.staticOptions.length == 0 &&
587
+ this.endpoint
588
+ ) {
589
+ const value = this.value;
590
+ this.resolving = true;
591
+
592
+ fetchResults(this.endpoint).then((results: any) => {
593
+ if (results && results.length > 0) {
594
+ if (value) {
595
+ // if we started with a value, see if we can find it in the results
596
+ const existing = results.find((option) => {
597
+ return this.getValue(option) === value;
598
+ });
599
+ if (existing) {
600
+ this.resolving = false;
601
+ this.fetching = false;
602
+ this.setValues([existing]);
603
+ return;
563
604
  }
564
- this.setValues([results[0]]);
565
605
  }
566
- });
567
- } else if (this.staticOptions.length > 0) {
568
- if (this.getAttribute('multi') !== null) {
569
- this.addValue(this.staticOptions[0]);
606
+
607
+ this.setValues([results[0]]);
608
+ this.resolving = false;
609
+ }
610
+ });
611
+ } else if (this.staticOptions.length > 0) {
612
+ if (this.getAttribute('multi') !== null) {
613
+ this.addValue(this.staticOptions[0]);
614
+ } else {
615
+ if (this.getAttribute('value')) {
616
+ this.setSelectedValue(this.getAttribute('value'));
570
617
  } else {
571
- if (this.getAttribute('value')) {
572
- this.setSelectedValue(this.getAttribute('value'));
573
- } else {
574
- this.setValues([this.staticOptions[0]]);
575
- }
618
+ this.setValues([this.staticOptions[0]]);
576
619
  }
577
620
  }
578
621
  }
@@ -586,40 +629,37 @@ export class Select extends FormElement {
586
629
  'slotchange',
587
630
  this.handleSlotChange.bind(this)
588
631
  );
589
-
590
- this.checkSelectedOption();
591
632
  }
592
633
 
593
- public updated(changedProperties: Map<string, any>) {
594
- super.updated(changedProperties);
634
+ public updated(changes: Map<string, any>) {
635
+ super.updated(changes);
595
636
 
596
- if (changedProperties.has('sorted')) {
637
+ if (changes.has('sorted')) {
597
638
  this.sortFunction = this.sorted ? this.alphaSort : null;
598
639
  }
599
640
 
600
- if (changedProperties.has('values')) {
641
+ if (changes.has('value')) {
642
+ if (this.value && !this.values.length) {
643
+ this.setSelectedValue(this.value);
644
+ }
645
+ }
646
+
647
+ if (changes.has('values')) {
601
648
  this.updateInputs();
602
- if (
603
- this.multi ||
604
- this.values.length === 1 ||
605
- // fire change if being cleared
606
- (this.values.length == 0 &&
607
- changedProperties.get('values') &&
608
- changedProperties.get('values').length > 0)
609
- ) {
649
+ if (this.hasChanges(changes.get('values'))) {
610
650
  this.fireEvent('change');
611
651
  }
612
652
  }
613
653
 
614
654
  // if our cache key changes, clear it out
615
- if (changedProperties.has('cacheKey')) {
655
+ if (changes.has('cacheKey')) {
616
656
  this.lruCache.clear();
617
657
  }
618
658
 
619
659
  if (
620
- changedProperties.has('input') &&
621
- !changedProperties.has('values') &&
622
- !changedProperties.has('options') &&
660
+ changes.has('input') &&
661
+ !changes.has('values') &&
662
+ !changes.has('options') &&
623
663
  this.focused
624
664
  ) {
625
665
  if (this.lastQuery) {
@@ -635,7 +675,7 @@ export class Select extends FormElement {
635
675
  }, this.quietMillis);
636
676
  }
637
677
 
638
- if (this.endpoint && changedProperties.has('fetching')) {
678
+ if (this.endpoint && changes.has('fetching')) {
639
679
  if (!this.fetching && !this.isPastFetchThreshold()) {
640
680
  this.fireCustomEvent(CustomEventType.FetchComplete);
641
681
  }
@@ -643,8 +683,7 @@ export class Select extends FormElement {
643
683
 
644
684
  // if our cursor changed, lets make sure our scrollbox is showing it
645
685
  if (
646
- (changedProperties.has('cursorIndex') ||
647
- changedProperties.has('visibleOptions')) &&
686
+ (changes.has('cursorIndex') || changes.has('visibleOptions')) &&
648
687
  this.endpoint &&
649
688
  !this.fetching
650
689
  ) {
@@ -657,11 +696,6 @@ export class Select extends FormElement {
657
696
  }
658
697
  }
659
698
 
660
- // if they set an inital value, look through our static options for it
661
- if (changedProperties.has('value') && this.value && !this.values.length) {
662
- this.setSelectedValue(this.value);
663
- }
664
-
665
699
  // default to the first option if we don't have a placeholder
666
700
  if (
667
701
  this.values.length === 0 &&
@@ -672,6 +706,29 @@ export class Select extends FormElement {
672
706
  }
673
707
  }
674
708
 
709
+ private hasChanges(prev: T[]): boolean {
710
+ if (prev === undefined) {
711
+ return false;
712
+ }
713
+
714
+ let prevValues = undefined;
715
+ if (prev !== undefined) {
716
+ prevValues = (prev || [])
717
+ .map((option: T) => {
718
+ return this.getValue(option);
719
+ })
720
+ .join(',');
721
+ }
722
+
723
+ const newValues = (this.values || [])
724
+ .map((option: T) => {
725
+ return this.getValue(option);
726
+ })
727
+ .join(',');
728
+
729
+ return prevValues !== newValues;
730
+ }
731
+
675
732
  public setSelectedValue(value: string) {
676
733
  if (this.staticOptions.length > 0) {
677
734
  const existing = this.staticOptions.find((option) => {
@@ -681,6 +738,8 @@ export class Select extends FormElement {
681
738
  if (existing) {
682
739
  this.setValues([existing]);
683
740
  }
741
+ } else {
742
+ this.checkSelectedOption();
684
743
  }
685
744
  }
686
745
 
@@ -742,6 +801,9 @@ export class Select extends FormElement {
742
801
  }
743
802
 
744
803
  public handleOptionSelection(event: CustomEvent) {
804
+ event.preventDefault();
805
+ event.stopPropagation();
806
+
745
807
  const selected = event.detail.selected;
746
808
  // check if we should post it
747
809
  if (selected.post && this.endpoint) {
@@ -776,7 +838,7 @@ export class Select extends FormElement {
776
838
  }
777
839
  }
778
840
 
779
- private getNameInternal: (option: any) => string = (option: any) => {
841
+ protected getNameInternal: (option: T) => string = (option: T) => {
780
842
  return this.getName(option);
781
843
  };
782
844
 
@@ -802,7 +864,9 @@ export class Select extends FormElement {
802
864
  }
803
865
 
804
866
  public open(): void {
805
- this.requestUpdate('input');
867
+ (
868
+ this.shadowRoot.querySelector('.select-container') as HTMLDivElement
869
+ ).click();
806
870
  }
807
871
 
808
872
  public isOpen(): boolean {
@@ -925,11 +989,6 @@ export class Select extends FormElement {
925
989
  if (!this.fetching) {
926
990
  this.fetching = true;
927
991
 
928
- // make sure we cancel any previous request
929
- // if (this.cancelToken) {
930
- // this.cancelToken.cancel();
931
- // }
932
-
933
992
  const options: any = [...this.staticOptions];
934
993
  const q = (query || '').trim().toLowerCase();
935
994
 
@@ -1055,9 +1114,6 @@ export class Select extends FormElement {
1055
1114
  private handleFocus(): void {
1056
1115
  if (!this.focused && this.visibleOptions.length === 0) {
1057
1116
  this.focused = true;
1058
- if (this.searchOnFocus && !this.removingSelection) {
1059
- this.requestUpdate('input');
1060
- }
1061
1117
  }
1062
1118
  }
1063
1119
 
@@ -1087,7 +1143,7 @@ export class Select extends FormElement {
1087
1143
 
1088
1144
  if (this.multi) {
1089
1145
  if (
1090
- !this.values.find((option) => {
1146
+ !this.values.find((option: T) => {
1091
1147
  return (
1092
1148
  option.expression &&
1093
1149
  option.value &&
@@ -1204,25 +1260,25 @@ export class Select extends FormElement {
1204
1260
  }
1205
1261
 
1206
1262
  private handleArrowClick(event: MouseEvent): void {
1207
- event.preventDefault();
1208
- event.stopPropagation();
1209
- if (this.visibleOptions.length > 0) {
1210
- this.visibleOptions = [];
1211
- } else {
1212
- this.handleContainerClick(event);
1263
+ if (this.isOpen()) {
1264
+ event.preventDefault();
1265
+ event.stopPropagation();
1266
+ this.blur();
1213
1267
  }
1214
1268
  }
1215
1269
 
1216
- private renderSelectedItemDefault(option: any): TemplateResult {
1270
+ public renderOptionDefault(option: T): TemplateResult {
1217
1271
  if (!option) {
1218
1272
  return null;
1219
1273
  }
1220
1274
 
1275
+ // special case for icons on any option type
1276
+ const icon = (option as any).icon;
1221
1277
  return html`
1222
1278
  <div class="option-name" style="display:flex">
1223
- ${option.icon
1279
+ ${icon
1224
1280
  ? html`<temba-icon
1225
- name="${option.icon}"
1281
+ name="${icon}"
1226
1282
  style="margin-right:0.5em;"
1227
1283
  ></temba-icon>`
1228
1284
  : null}<span>${this.getName(option)}</span>
@@ -1230,6 +1286,11 @@ export class Select extends FormElement {
1230
1286
  `;
1231
1287
  }
1232
1288
 
1289
+ public renderSelectedItemDefault(option: T): TemplateResult {
1290
+ const renderFn = this.renderOption || this.renderOptionDefault;
1291
+ return renderFn(option, true);
1292
+ }
1293
+
1233
1294
  public serializeValue(value: any): string {
1234
1295
  // static options just use their value
1235
1296
  if (!this.jsonValue && (this.staticOptions.length > 0 || this.tags)) {
@@ -1241,8 +1302,8 @@ export class Select extends FormElement {
1241
1302
 
1242
1303
  public setSelection(value: string): void {
1243
1304
  for (const option of this.staticOptions) {
1244
- if (this.getValue(option.value) === value) {
1245
- if (this.values.length === 0 || this.values[0].value !== '' + value) {
1305
+ if (this.getValue(option) === value) {
1306
+ if (this.values.length === 0 || this.values[0].value != '' + value) {
1246
1307
  this.setValues([option]);
1247
1308
  }
1248
1309
  return;
@@ -1258,34 +1319,43 @@ export class Select extends FormElement {
1258
1319
  this.visibleOptions = [];
1259
1320
  this.requestUpdate();
1260
1321
  }
1322
+
1323
+ this.fireCustomEvent(CustomEventType.Selection, {
1324
+ selected: null
1325
+ });
1261
1326
  }
1262
1327
 
1263
1328
  public setValues(values: any[]) {
1329
+ const oldValues = this.values;
1264
1330
  this.values = values;
1265
- this.requestUpdate('values');
1331
+ this.requestUpdate('values', oldValues);
1266
1332
  }
1267
1333
 
1268
1334
  public addValue(value: any) {
1335
+ const oldValues = [...this.values];
1269
1336
  this.values.push(value);
1270
- this.requestUpdate('values');
1337
+ this.requestUpdate('values', oldValues);
1271
1338
  }
1272
1339
 
1273
1340
  public removeValue(valueToRemove: any) {
1341
+ const oldValues = [...this.values];
1274
1342
  const idx = this.values.indexOf(valueToRemove);
1275
1343
  if (idx > -1) {
1276
1344
  this.values.splice(idx, 1);
1277
1345
  }
1278
- this.requestUpdate('values');
1346
+ this.requestUpdate('values', oldValues);
1279
1347
  }
1280
1348
 
1281
1349
  public popValue() {
1350
+ const oldValues = [...this.values];
1282
1351
  this.values.pop();
1283
- this.requestUpdate('values');
1352
+ this.requestUpdate('values', oldValues);
1284
1353
  }
1285
1354
 
1286
1355
  public clear() {
1356
+ const oldValues = this.values;
1287
1357
  this.values = [];
1288
- this.requestUpdate('values');
1358
+ this.requestUpdate('values', oldValues);
1289
1359
  }
1290
1360
 
1291
1361
  public render(): TemplateResult {
@@ -1361,11 +1431,17 @@ export class Select extends FormElement {
1361
1431
  class="select-container ${classes}"
1362
1432
  @click=${this.handleContainerClick}
1363
1433
  >
1364
-
1365
- <div class="left-side">
1434
+ <div class="left-side" >
1366
1435
  <slot name="prefix"></slot>
1367
- <div class="selected">
1368
- ${!this.multi ? input : null}
1436
+ <div class="selected" >
1437
+ ${
1438
+ this.resolving
1439
+ ? html`<temba-loading
1440
+ style="margin-left:1em"
1441
+ ></temba-loading>`
1442
+ : null
1443
+ }
1444
+ ${!this.multi && !this.resolving ? input : null}
1369
1445
  ${this.values.map(
1370
1446
  (selected: any, index: number) => html`
1371
1447
  <div
@@ -1378,12 +1454,6 @@ export class Select extends FormElement {
1378
1454
  <div
1379
1455
  class="remove-item"
1380
1456
  style="margin-top:1px"
1381
- @mousedown=${() => {
1382
- this.removingSelection = true;
1383
- }}
1384
- @mouseup=${() => {
1385
- this.removingSelection = false;
1386
- }}
1387
1457
  @click=${(evt: MouseEvent) => {
1388
1458
  evt.preventDefault();
1389
1459
  evt.stopPropagation();
@@ -1412,7 +1482,7 @@ export class Select extends FormElement {
1412
1482
  ${
1413
1483
  !this.tags
1414
1484
  ? html`<div
1415
- class="right-side"
1485
+ class="right-side arrow"
1416
1486
  style="display:block;margin-right:5px"
1417
1487
  @click=${this.handleArrowClick}
1418
1488
  >
@@ -1439,13 +1509,13 @@ export class Select extends FormElement {
1439
1509
  .cursorIndex=${this.cursorIndex}
1440
1510
  .renderOptionDetail=${this.renderOptionDetail}
1441
1511
  .renderOptionName=${this.renderOptionName}
1442
- .renderOption=${this.renderOption}
1512
+ .renderOption=${this.renderOption || this.renderOptionDefault}
1443
1513
  .anchorTo=${this.anchorElement}
1444
1514
  .options=${this.visibleOptions}
1445
1515
  .spaceSelect=${this.spaceSelect}
1446
1516
  .nameKey=${this.nameKey}
1447
1517
  .getName=${this.getNameInternal}
1448
- static-width=${this.optionWidth}
1518
+ ?static-width=${this.optionWidth}
1449
1519
  ?anchor-right=${this.anchorRight}
1450
1520
  ?visible=${this.visibleOptions.length > 0}
1451
1521
  ></temba-options>
@@ -0,0 +1,71 @@
1
+ import { css, CSSResultArray, html, TemplateResult } from 'lit';
2
+ import { Select, SelectOption } from './Select';
3
+ import { property } from 'lit/decorators.js';
4
+ import { getFullName } from '../user/TembaUser';
5
+
6
+ export interface UserOption extends SelectOption {
7
+ email: string;
8
+ name: string;
9
+ avatar: string;
10
+ }
11
+
12
+ export class UserSelect extends Select<UserOption> {
13
+ static get styles(): CSSResultArray {
14
+ return [
15
+ super.styles,
16
+ css`
17
+ :host {
18
+ width: 150px;
19
+ display: block;
20
+ }
21
+ `
22
+ ];
23
+ }
24
+
25
+ @property({ type: String })
26
+ endpoint = '/api/v2/users.json';
27
+
28
+ @property({ type: String })
29
+ nameKey = 'name';
30
+
31
+ @property({ type: String })
32
+ valueKey = 'email';
33
+
34
+ @property({ type: String })
35
+ placeholder: string = 'Select a user';
36
+
37
+ @property({ type: Boolean })
38
+ sorted: boolean = true;
39
+
40
+ @property({ type: Object })
41
+ user: UserOption;
42
+
43
+ constructor() {
44
+ super();
45
+ this.shouldExclude = (option: UserOption) => {
46
+ const selected = this.values[0];
47
+ return option.email === selected?.email;
48
+ };
49
+ }
50
+
51
+ public prepareOptionsDefault(options: UserOption[]): UserOption[] {
52
+ options.forEach((option) => {
53
+ option.name = getFullName(option);
54
+ });
55
+ return options;
56
+ }
57
+
58
+ public renderOptionDefault(option: UserOption): TemplateResult {
59
+ if (!option) {
60
+ return html``;
61
+ }
62
+
63
+ return html`<temba-user
64
+ email=${option.email}
65
+ name=${option.name}
66
+ avatar=${option.avatar}
67
+ scale="0.8"
68
+ showname
69
+ ></temba-user>`;
70
+ }
71
+ }