@nyaruka/temba-components 0.111.7 → 0.112.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 (61) hide show
  1. package/CHANGELOG.md +14 -9
  2. package/dist/temba-components.js +383 -266
  3. package/dist/temba-components.js.map +1 -1
  4. package/out-tsc/src/button/Button.js +4 -0
  5. package/out-tsc/src/button/Button.js.map +1 -1
  6. package/out-tsc/src/chat/Chat.js +24 -20
  7. package/out-tsc/src/chat/Chat.js.map +1 -1
  8. package/out-tsc/src/compose/Compose.js +9 -7
  9. package/out-tsc/src/compose/Compose.js.map +1 -1
  10. package/out-tsc/src/contacts/ContactChat.js +172 -21
  11. package/out-tsc/src/contacts/ContactChat.js.map +1 -1
  12. package/out-tsc/src/interfaces.js +1 -0
  13. package/out-tsc/src/interfaces.js.map +1 -1
  14. package/out-tsc/src/options/Options.js +1 -0
  15. package/out-tsc/src/options/Options.js.map +1 -1
  16. package/out-tsc/src/select/Select.js +30 -3
  17. package/out-tsc/src/select/Select.js.map +1 -1
  18. package/out-tsc/src/store/EndpointMonitorElement.js +4 -4
  19. package/out-tsc/src/store/EndpointMonitorElement.js.map +1 -1
  20. package/out-tsc/src/store/Store.js +8 -7
  21. package/out-tsc/src/store/Store.js.map +1 -1
  22. package/out-tsc/src/store/StoreMonitorElement.js +1 -6
  23. package/out-tsc/src/store/StoreMonitorElement.js.map +1 -1
  24. package/out-tsc/src/tabpane/TabPane.js +6 -4
  25. package/out-tsc/src/tabpane/TabPane.js.map +1 -1
  26. package/out-tsc/src/user/TembaUser.js +43 -23
  27. package/out-tsc/src/user/TembaUser.js.map +1 -1
  28. package/out-tsc/test/temba-contact-chat.test.js +2 -1
  29. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  30. package/out-tsc/test/temba-contact-tickets.test.js +2 -2
  31. package/out-tsc/test/temba-contact-tickets.test.js.map +1 -1
  32. package/out-tsc/test/utils.test.js +25 -0
  33. package/out-tsc/test/utils.test.js.map +1 -1
  34. package/package.json +1 -1
  35. package/screenshots/truth/contacts/chat-failure.png +0 -0
  36. package/screenshots/truth/contacts/chat-for-active-contact.png +0 -0
  37. package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
  38. package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
  39. package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
  40. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  41. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  42. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  43. package/src/button/Button.ts +4 -0
  44. package/src/chat/Chat.ts +28 -28
  45. package/src/compose/Compose.ts +9 -7
  46. package/src/contacts/ContactChat.ts +176 -23
  47. package/src/interfaces.ts +2 -1
  48. package/src/options/Options.ts +1 -0
  49. package/src/select/Select.ts +34 -8
  50. package/src/store/EndpointMonitorElement.ts +5 -5
  51. package/src/store/Store.ts +8 -9
  52. package/src/store/StoreMonitorElement.ts +2 -10
  53. package/src/tabpane/TabPane.ts +6 -4
  54. package/src/user/TembaUser.ts +48 -26
  55. package/test/temba-contact-chat.test.ts +3 -0
  56. package/test/temba-contact-tickets.test.ts +2 -5
  57. package/test/utils.test.ts +27 -0
  58. package/test-assets/api/users/admin1.json +13 -0
  59. package/test-assets/api/users/agent1.json +13 -0
  60. package/test-assets/api/users/editor1.json +13 -0
  61. package/test-assets/api/users/viewer1.json +13 -0
@@ -81,8 +81,11 @@ export class Select extends FormElement {
81
81
  }
82
82
 
83
83
  .wrapper-bg {
84
- background: #fff;
85
- box-shadow: inset 0px 0px 4px rgb(0 0 0 / 10%);
84
+ background: var(--select-wrapper-bg, #fff);
85
+ box-shadow: var(
86
+ --select-wrapper-shadow,
87
+ inset 0px 0px 4px rgb(0 0 0 / 10%)
88
+ );
86
89
  border-radius: var(--curvature-widget);
87
90
  }
88
91
 
@@ -170,6 +173,10 @@ export class Select extends FormElement {
170
173
  margin: 2px 2px;
171
174
  }
172
175
 
176
+ .option-name > span {
177
+ text-align: left;
178
+ }
179
+
173
180
  .selected-item .option-name {
174
181
  padding: 0px;
175
182
  font-size: var(--temba-select-selected-font-size);
@@ -411,6 +418,9 @@ export class Select extends FormElement {
411
418
  @property({ type: Boolean })
412
419
  clearable: boolean;
413
420
 
421
+ @property({ type: Boolean })
422
+ sorted: boolean;
423
+
414
424
  @property({ type: String })
415
425
  flavor = 'default';
416
426
 
@@ -447,7 +457,7 @@ export class Select extends FormElement {
447
457
  shouldExclude: (option: any) => boolean;
448
458
 
449
459
  @property({ attribute: false })
450
- sortFunction: (a: any, b: any) => number;
460
+ sortFunction: (a: any, b: any) => number = null;
451
461
 
452
462
  @property({ attribute: false })
453
463
  renderOption: (option: any, selected: boolean) => TemplateResult;
@@ -470,6 +480,9 @@ export class Select extends FormElement {
470
480
  @property({ attribute: false })
471
481
  getOptions: (response: WebResponse) => any[] = this.getOptionsDefault;
472
482
 
483
+ @property({ attribute: false })
484
+ prepareOptions: (options: any[]) => any[] = (options: any[]) => options;
485
+
473
486
  @property({ attribute: false })
474
487
  isComplete: (newestOptions: any[], response: WebResponse) => boolean =
475
488
  this.isCompleteDefault;
@@ -477,6 +490,14 @@ export class Select extends FormElement {
477
490
  @property({ type: Array, attribute: 'options' })
478
491
  private staticOptions: any[] = [];
479
492
 
493
+ private alphaSort = (a: any, b: any) => {
494
+ // by default, all endpoint values are sorted by name
495
+ if (this.endpoint) {
496
+ return this.getName(a).localeCompare(this.getName(b));
497
+ }
498
+ return 0;
499
+ };
500
+
480
501
  private lastQuery: number;
481
502
 
482
503
  // private cancelToken: CancelTokenSource;
@@ -572,6 +593,10 @@ export class Select extends FormElement {
572
593
  public updated(changedProperties: Map<string, any>) {
573
594
  super.updated(changedProperties);
574
595
 
596
+ if (changedProperties.has('sorted')) {
597
+ this.sortFunction = this.sorted ? this.alphaSort : null;
598
+ }
599
+
575
600
  if (changedProperties.has('values')) {
576
601
  this.updateInputs();
577
602
  if (
@@ -962,6 +987,8 @@ export class Select extends FormElement {
962
987
  // if we are searchable, but doing it locally, fetch all the options
963
988
  if (this.searchable && !this.queryParam) {
964
989
  fetchResults(url).then((results: any) => {
990
+ results = this.prepareOptions(results);
991
+
965
992
  if (this.cache && !this.tags) {
966
993
  this.lruCache.set(url, {
967
994
  options: results,
@@ -978,11 +1005,10 @@ export class Select extends FormElement {
978
1005
  } else {
979
1006
  getUrl(url)
980
1007
  .then((response: WebResponse) => {
981
- const results = this.getOptions(response).filter(
982
- (option: any) => {
983
- return this.isMatch(option, q);
984
- }
985
- );
1008
+ let results = this.getOptions(response).filter((option: any) => {
1009
+ return this.isMatch(option, q);
1010
+ });
1011
+ results = this.prepareOptions(results);
986
1012
 
987
1013
  this.next = null;
988
1014
  const json = response.json;
@@ -18,6 +18,11 @@ export class EndpointMonitorElement extends StoreMonitorElement {
18
18
  @property({ type: Object, attribute: false })
19
19
  data: any;
20
20
 
21
+ connectedCallback(): void {
22
+ super.connectedCallback();
23
+ this.prepareData = this.prepareData.bind(this);
24
+ }
25
+
21
26
  prepareData(data: any): any {
22
27
  return data;
23
28
  }
@@ -52,9 +57,4 @@ export class EndpointMonitorElement extends StoreMonitorElement {
52
57
  }
53
58
  }
54
59
  }
55
-
56
- connectedCallback(): void {
57
- super.connectedCallback();
58
- this.prepareData = this.prepareData.bind(this);
59
- }
60
60
  }
@@ -104,7 +104,7 @@ export class Store extends RapidElement {
104
104
  private groups: { [uuid: string]: ContactGroup } = {};
105
105
  private shortcuts: Shortcut[] = [];
106
106
  private languages: any = {};
107
- private users: User[];
107
+ private users: User[] = [];
108
108
  private workspace: Workspace;
109
109
  private featuredFields: ContactField[] = [];
110
110
 
@@ -255,10 +255,6 @@ export class Store extends RapidElement {
255
255
  );
256
256
  }
257
257
 
258
- public getUser(email: string) {
259
- return this.users.find((user: User) => user.email === email);
260
- }
261
-
262
258
  public firstUpdated() {
263
259
  this.reset();
264
260
  }
@@ -521,16 +517,19 @@ export class Store extends RapidElement {
521
517
  const previousRequest = this.fetching[url];
522
518
  const now = new Date().getTime();
523
519
  // if the request was recently made, don't do anything
524
- if (previousRequest && now - previousRequest < 500) {
520
+ if (previousRequest) {
521
+ setTimeout(() => {
522
+ this.makeRequest(url, options);
523
+ }, 500);
525
524
  return;
526
525
  }
527
526
 
528
- this.fetching[url] = now;
529
- options = options || {};
530
527
  const cached = this.cache.get(url);
531
528
  if (cached && !options.force) {
532
529
  this.fireCustomEvent(CustomEventType.StoreUpdated, { url, data: cached });
533
530
  } else {
531
+ options = options || {};
532
+ this.fetching[url] = now;
534
533
  fetchResults(url).then((data) => {
535
534
  if (!data) {
536
535
  delete this.fetching[url];
@@ -539,8 +538,8 @@ export class Store extends RapidElement {
539
538
 
540
539
  data = options.prepareData ? options.prepareData(data) : data;
541
540
  this.cache.set(url, data);
542
- this.fireCustomEvent(CustomEventType.StoreUpdated, { url, data });
543
541
  delete this.fetching[url];
542
+ this.fireCustomEvent(CustomEventType.StoreUpdated, { url, data });
544
543
  });
545
544
  }
546
545
  }
@@ -1,4 +1,4 @@
1
- import { html, PropertyValueMap, TemplateResult } from 'lit';
1
+ import { html, TemplateResult } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
3
  import { CustomEventType } from '../interfaces';
4
4
  import { RapidElement } from '../RapidElement';
@@ -39,20 +39,12 @@ export class StoreMonitorElement extends RapidElement {
39
39
  }
40
40
 
41
41
  private handleStoreUpdated(event: CustomEvent) {
42
- this.store.initialHttpComplete.then(() => {
43
- this.storeUpdated(event);
44
- });
42
+ this.storeUpdated(event);
45
43
  }
46
44
 
47
45
  // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
48
46
  protected storeUpdated(event: CustomEvent) {}
49
47
 
50
- protected updated(
51
- properties: PropertyValueMap<any> | Map<PropertyKey, unknown>
52
- ): void {
53
- super.updated(properties);
54
- }
55
-
56
48
  connectedCallback(): void {
57
49
  super.connectedCallback();
58
50
  this.store = document.querySelector('temba-store') as Store;
@@ -223,17 +223,19 @@ export class TabPane extends RapidElement {
223
223
  }
224
224
 
225
225
  .embedded.tabs {
226
- margin: 0;
226
+ padding-top: 0em;
227
227
  }
228
228
 
229
229
  .embedded .tab {
230
- border-top: none !important;
231
230
  border-bottom: none !important;
232
- border-radius: 0px;
231
+ border-radius: 0em;
232
+ border-top: none !important;
233
233
  }
234
234
 
235
235
  .embedded .tab.first {
236
- border-left: none !important;
236
+ margin-left: 0em;
237
+ border-top: none !important;
238
+ border-left: none;
237
239
  }
238
240
 
239
241
  .embedded.tabs .tab.selected {
@@ -1,10 +1,11 @@
1
1
  import { PropertyValueMap, TemplateResult, css, html } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
3
  import { User } from '../interfaces';
4
- import { StoreMonitorElement } from '../store/StoreMonitorElement';
5
4
  import { colorHash, extractInitials } from '../utils';
5
+ import { EndpointMonitorElement } from '../store/EndpointMonitorElement';
6
+ import { DEFAULT_AVATAR } from '../webchat/assets';
6
7
 
7
- export class TembaUser extends StoreMonitorElement {
8
+ export class TembaUser extends EndpointMonitorElement {
8
9
  public static styles = css`
9
10
  :host {
10
11
  display: flex;
@@ -33,45 +34,66 @@ export class TembaUser extends StoreMonitorElement {
33
34
  scale: number;
34
35
 
35
36
  @property({ type: Boolean })
36
- name: string;
37
+ name: boolean;
37
38
 
38
- @property({ type: Object, attribute: false })
39
- user: User;
39
+ @property({ type: Boolean })
40
+ system: boolean;
40
41
 
41
42
  @property({ type: String, attribute: false })
42
- background: string;
43
+ background: string = '#e6e6e6';
43
44
 
44
45
  @property({ type: String, attribute: false })
45
- initials: string;
46
+ initials: string = '';
46
47
 
47
- @property({ type: String, attribute: false })
48
- fullName: string;
48
+ @property({ type: String })
49
+ fullname: string;
50
+
51
+ @property({ type: Object, attribute: false })
52
+ data: User;
53
+
54
+ prepareData(data: any) {
55
+ if (data.length > 0) {
56
+ return data[0];
57
+ }
58
+
59
+ this.fullname = this.email;
60
+ return null;
61
+ }
49
62
 
50
63
  public updated(
51
64
  changed: PropertyValueMap<any> | Map<PropertyKey, unknown>
52
65
  ): void {
53
66
  super.updated(changed);
54
- if (changed.has('email')) {
55
- this.user = this.store.getUser(this.email);
56
- if (this.user) {
57
- this.fullName = [this.user.first_name, this.user.last_name].join(' ');
58
- if (this.user.avatar) {
59
- this.background = `url('${this.user.avatar}') center / contain no-repeat`;
60
- this.initials = '';
61
- } else {
62
- this.background = colorHash.hex(this.fullName);
63
- this.initials = extractInitials(this.fullName);
64
- }
67
+
68
+ if (changed.has('email') && this.email) {
69
+ this.url = `/api/v2/users.json?email=${this.email}`;
70
+ }
71
+
72
+ if (changed.has('system') && this.system) {
73
+ this.background = `url('${DEFAULT_AVATAR}') center / contain no-repeat`;
74
+ }
75
+
76
+ if (changed.has('data') && this.data) {
77
+ if (this.data.first_name && this.data.last_name) {
78
+ this.fullname = [this.data.first_name, this.data.last_name].join(' ');
79
+ this.background = colorHash.hex(this.fullname);
80
+ this.initials = extractInitials(this.fullname);
81
+ }
82
+
83
+ if (this.data.avatar) {
84
+ this.background = `url('${this.data.avatar}') center / contain no-repeat`;
85
+ this.initials = '';
65
86
  }
66
87
  }
67
- }
68
88
 
69
- public render(): TemplateResult {
70
- if (!this.user) {
71
- return null;
89
+ if (changed.has('fullname') && this.fullname && !this.data) {
90
+ this.background = colorHash.hex(this.fullname);
91
+ this.initials = extractInitials(this.fullname);
72
92
  }
93
+ }
73
94
 
74
- return html` <div class="wrapper">
95
+ public render(): TemplateResult {
96
+ return html`<div class="wrapper">
75
97
  <div
76
98
  class="avatar-circle"
77
99
  style="
@@ -103,7 +125,7 @@ export class TembaUser extends StoreMonitorElement {
103
125
  style="margin: 0px ${this.scale - 0.5}em;font-size:${this.scale +
104
126
  0.2}em"
105
127
  >
106
- ${this.fullName}
128
+ ${this.fullname}
107
129
  </div>`
108
130
  : null}
109
131
  </div>`;
@@ -8,6 +8,7 @@ import {
8
8
  getClip,
9
9
  getComponent,
10
10
  loadStore,
11
+ mockAPI,
11
12
  mockGET,
12
13
  mockNow,
13
14
  mockPOST
@@ -58,6 +59,8 @@ describe('temba-contact-chat', () => {
58
59
  /\/contact\/history\/contact-.*/,
59
60
  '/test-assets/contacts/history.json'
60
61
  );
62
+
63
+ mockAPI();
61
64
  clock = useFakeTimers();
62
65
  });
63
66
 
@@ -5,7 +5,7 @@ import {
5
5
  getClip,
6
6
  getComponent,
7
7
  loadStore,
8
- mockGET,
8
+ mockAPI,
9
9
  mockNow
10
10
  } from './utils.test';
11
11
 
@@ -25,10 +25,7 @@ const getContactTickets = async (attrs: any = {}) => {
25
25
  mockNow('2023-04-07T00:00:00.000-00:00');
26
26
  describe('temba-contact-tickets', () => {
27
27
  beforeEach(() => {
28
- mockGET(
29
- /\/api\/v2\/tickets.json\?contact=24d64810-3315-4ff5-be85-48e3fe055bf9/,
30
- '/test-assets/contacts/contact-tickets.json'
31
- );
28
+ mockAPI();
32
29
  loadStore();
33
30
  });
34
31
 
@@ -114,6 +114,33 @@ after(() => {
114
114
  (window.fetch as any).restore();
115
115
  });
116
116
 
117
+ const mockMapping = {
118
+ '/test-assets/api/users/admin1.json': [
119
+ /\/api\/v2\/users.json\?email=admin1@nyaruka.com/
120
+ ],
121
+ '/test-assets/api/users/editor1.json': [
122
+ /\/api\/v2\/users.json\?email=editor1@nyaruka.com/
123
+ ],
124
+ '/test-assets/api/users/agent1.json': [
125
+ /\/api\/v2\/users.json\?email=agent1@nyaruka.com/
126
+ ],
127
+ '/test-assets/api/users/viewer1.json': [
128
+ /\/api\/v2\/users.json\?email=viewer1@nyaruka.com/
129
+ ],
130
+ '/test-assets/contacts/contact-tickets.json': [
131
+ /\/api\/v2\/tickets.json\?contact=24d64810-3315-4ff5-be85-48e3fe055bf9/
132
+ ]
133
+ };
134
+
135
+ export const mockAPI = () => {
136
+ for (const key in mockMapping) {
137
+ const urls = mockMapping[key];
138
+ for (const url of urls) {
139
+ mockGET(url, key);
140
+ }
141
+ }
142
+ };
143
+
117
144
  export const mockGET = (
118
145
  endpoint: RegExp,
119
146
  body: any,
@@ -0,0 +1,13 @@
1
+ {
2
+ "next": null,
3
+ "previous": null,
4
+ "results": [
5
+ {
6
+ "email": "admin1@nyaruka.com",
7
+ "first_name": "Adam",
8
+ "last_name": "McAdmin",
9
+ "role": "administrator",
10
+ "created_on": "2023-01-18T19:33:13.336367Z"
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "next": null,
3
+ "previous": null,
4
+ "results": [
5
+ {
6
+ "email": "agent1@nyaruka.com",
7
+ "first_name": "Agnes",
8
+ "last_name": "McAgent",
9
+ "role": "agent",
10
+ "created_on": "2023-01-18T19:33:13.637127Z"
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "next": null,
3
+ "previous": null,
4
+ "results": [
5
+ {
6
+ "email": "editor1@nyaruka.com",
7
+ "first_name": "Eddy",
8
+ "last_name": "McEditor",
9
+ "role": "editor",
10
+ "created_on": "2023-01-18T19:33:13.410675Z"
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "next": null,
3
+ "previous": null,
4
+ "results": [
5
+ {
6
+ "email": "viewer1@nyaruka.com",
7
+ "first_name": "Veronica",
8
+ "last_name": "McViews",
9
+ "role": "agent",
10
+ "created_on": "2023-01-18T19:33:13.485734Z"
11
+ }
12
+ ]
13
+ }