@nyaruka/temba-components 0.68.0 → 0.71.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 (48) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{dd629ee4.js → 2b9b9834.js} +274 -212
  3. package/dist/index.js +274 -212
  4. package/dist/sw.js +1 -1
  5. package/dist/sw.js.map +1 -1
  6. package/dist/templates/components-body.html +1 -1
  7. package/dist/templates/components-head.html +1 -1
  8. package/out-tsc/src/contacts/ContactHistory.js +20 -13
  9. package/out-tsc/src/contacts/ContactHistory.js.map +1 -1
  10. package/out-tsc/src/contacts/ContactTickets.js +27 -23
  11. package/out-tsc/src/contacts/ContactTickets.js.map +1 -1
  12. package/out-tsc/src/contacts/events.js +7 -8
  13. package/out-tsc/src/contacts/events.js.map +1 -1
  14. package/out-tsc/src/imagepicker/ImagePicker.js +3 -6
  15. package/out-tsc/src/imagepicker/ImagePicker.js.map +1 -1
  16. package/out-tsc/src/list/TicketList.js +4 -5
  17. package/out-tsc/src/list/TicketList.js.map +1 -1
  18. package/out-tsc/src/store/Store.js +3 -0
  19. package/out-tsc/src/store/Store.js.map +1 -1
  20. package/out-tsc/src/store/StoreElement.js +11 -30
  21. package/out-tsc/src/store/StoreElement.js.map +1 -1
  22. package/out-tsc/src/store/StoreMonitorElement.js +50 -0
  23. package/out-tsc/src/store/StoreMonitorElement.js.map +1 -0
  24. package/out-tsc/src/user/TembaUser.js +107 -0
  25. package/out-tsc/src/user/TembaUser.js.map +1 -0
  26. package/out-tsc/src/utils/index.js +1 -1
  27. package/out-tsc/src/utils/index.js.map +1 -1
  28. package/out-tsc/temba-modules.js +2 -0
  29. package/out-tsc/temba-modules.js.map +1 -1
  30. package/out-tsc/test/temba-contact-tickets.test.js +1 -1
  31. package/out-tsc/test/temba-contact-tickets.test.js.map +1 -1
  32. package/package.json +1 -1
  33. package/rollup.config.js +3 -0
  34. package/screenshots/truth/contacts/tickets-assignment.png +0 -0
  35. package/screenshots/truth/contacts/tickets.png +0 -0
  36. package/src/contacts/ContactHistory.ts +28 -14
  37. package/src/contacts/ContactTickets.ts +28 -34
  38. package/src/contacts/events.ts +8 -19
  39. package/src/imagepicker/ImagePicker.ts +3 -7
  40. package/src/list/TicketList.ts +4 -5
  41. package/src/store/Store.ts +4 -0
  42. package/src/store/StoreElement.ts +13 -38
  43. package/src/store/StoreMonitorElement.ts +61 -0
  44. package/src/user/TembaUser.ts +111 -0
  45. package/src/utils/index.ts +1 -2
  46. package/temba-modules.ts +2 -0
  47. package/templates/index.html +7 -4
  48. package/test/temba-contact-tickets.test.ts +1 -3
@@ -2,15 +2,12 @@ import { css, html, PropertyValueMap, TemplateResult } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
3
  import { CustomEventType, Ticket, TicketStatus, User } from '../interfaces';
4
4
  import { StoreElement } from '../store/StoreElement';
5
- import {
6
- getClasses,
7
- getFullName,
8
- postJSON,
9
- renderAvatar,
10
- stopEvent,
11
- } from '../utils';
5
+ import { getClasses, postJSON, stopEvent } from '../utils';
12
6
  import { Icon } from '../vectoricon';
13
7
 
8
+ const dropdownUserScale = 0.7;
9
+ const inlineUserScale = 0.8;
10
+
14
11
  export class ContactTickets extends StoreElement {
15
12
  @property({ type: String })
16
13
  agent: string;
@@ -129,6 +126,10 @@ export class ContactTickets extends StoreElement {
129
126
  border-bottom: 1px solid #f3f3f3;
130
127
  }
131
128
 
129
+ .option-group temba-user {
130
+ flex-grow: 1;
131
+ }
132
+
132
133
  .assigned .user {
133
134
  flex-grow: 1;
134
135
  }
@@ -164,9 +165,6 @@ export class ContactTickets extends StoreElement {
164
165
  background: var(--color-selection);
165
166
  }
166
167
 
167
- .user .avatar {
168
- }
169
-
170
168
  .user .name {
171
169
  display: -webkit-box;
172
170
  -webkit-line-clamp: 1;
@@ -243,16 +241,6 @@ export class ContactTickets extends StoreElement {
243
241
  }
244
242
  }
245
243
 
246
- private renderUser(user: User) {
247
- if (!user) {
248
- return null;
249
- }
250
- return html`<div class="user">
251
- <div class="avatar">${renderAvatar({ user: user, scale: 0.6 })}</div>
252
- <div class="name">${getFullName(user)}</div>
253
- </div>`;
254
- }
255
-
256
244
  private handleClose(uuid: string) {
257
245
  postJSON(`/api/v2/ticket_actions.json`, {
258
246
  tickets: [uuid],
@@ -285,6 +273,7 @@ export class ContactTickets extends StoreElement {
285
273
  if (ticket.assignee && ticket.assignee.email === email) {
286
274
  return;
287
275
  }
276
+ this.blur();
288
277
 
289
278
  postJSON(`/api/v2/ticket_actions.json`, {
290
279
  tickets: [uuid],
@@ -352,13 +341,10 @@ export class ContactTickets extends StoreElement {
352
341
  <div slot="toggle" class="toggle">
353
342
  ${ticket.assignee
354
343
  ? html`
355
- <div>
356
- ${renderAvatar({
357
- name: ticket.assignee.name,
358
- user: ticket.assignee,
359
- scale: 0.7,
360
- })}
361
- </div>
344
+ <temba-user
345
+ email=${ticket.assignee.email}
346
+ scale="${inlineUserScale}"
347
+ ></temba-user>
362
348
  `
363
349
  : html`
364
350
  <temba-button
@@ -384,11 +370,11 @@ export class ContactTickets extends StoreElement {
384
370
  ? 'current-user'
385
371
  : ''}"
386
372
  >
387
- ${this.renderUser(
388
- users.find(
389
- user => user.email === ticket.assignee.email
390
- )
391
- )}
373
+ <temba-user
374
+ email=${ticket.assignee.email}
375
+ name
376
+ scale="${dropdownUserScale}"
377
+ ></temba-user>
392
378
  <temba-button
393
379
  name="Unassign"
394
380
  primary
@@ -418,7 +404,11 @@ export class ContactTickets extends StoreElement {
418
404
  );
419
405
  }}
420
406
  >
421
- ${this.renderUser(agent)}
407
+ <temba-user
408
+ email=${agent.email}
409
+ name
410
+ scale="${dropdownUserScale}"
411
+ ></temba-user>
422
412
  </div>
423
413
  `
424
414
  : null}
@@ -444,7 +434,11 @@ export class ContactTickets extends StoreElement {
444
434
  );
445
435
  }}
446
436
  >
447
- ${this.renderUser(user)}
437
+ <temba-user
438
+ email=${user.email}
439
+ scale="${dropdownUserScale}"
440
+ name
441
+ ></temba-user>
448
442
  </div>`;
449
443
  })}
450
444
  </div>
@@ -1,13 +1,6 @@
1
- import { css, EventPart, html, TemplateResult } from 'lit';
1
+ import { css, html, TemplateResult } from 'lit';
2
2
  import { Msg, ObjectReference, User } from '../interfaces';
3
- import {
4
- getClasses,
5
- NamedObject,
6
- oxford,
7
- oxfordFn,
8
- oxfordNamed,
9
- renderAvatar,
10
- } from '../utils';
3
+ import { getClasses, oxford, oxfordFn, oxfordNamed } from '../utils';
11
4
  import { Icon } from '../vectoricon';
12
5
  import { getDisplayName } from './helpers';
13
6
 
@@ -758,10 +751,7 @@ export const renderAttachment = (attachment: string): TemplateResult => {
758
751
  return html`<div style="">${inner}</div>`;
759
752
  };
760
753
 
761
- export const renderMsgEvent = (
762
- event: MsgEvent,
763
- agent: string
764
- ): TemplateResult => {
754
+ export const renderMsgEvent = (event: MsgEvent): TemplateResult => {
765
755
  const isInbound = event.type === Events.MESSAGE_RECEIVED;
766
756
  const isError = event.status === 'E';
767
757
  const isFailure = event.status === 'F';
@@ -874,9 +864,10 @@ export const renderMsgEvent = (
874
864
  </div>
875
865
 
876
866
  ${!isInbound && event.created_by
877
- ? html`<div style="margin-left:0.8em;margin-top:0.3em;font-size:0.9em">
878
- ${renderAvatar({ user: event.created_by })}
879
- </div>`
867
+ ? html`<temba-user
868
+ style="margin-left:0.5em"
869
+ email=${event.created_by.email}
870
+ ></temba-user>`
880
871
  : null}
881
872
  </div>`;
882
873
  };
@@ -1010,9 +1001,7 @@ export const renderNoteCreated = (event: TicketEvent): TemplateResult => {
1010
1001
  ></temba-date>
1011
1002
  </div>
1012
1003
  </div>
1013
- <div style="margin-left:0.8em;margin-top:0.3em;">
1014
- ${renderAvatar({ user: event.created_by })}
1015
- </div>
1004
+ <temba-user email=${event.created_by.email}></temba-user>
1016
1005
  </div>`;
1017
1006
  };
1018
1007
 
@@ -1,6 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/no-this-alias */
2
2
  import { html, css, PropertyValueMap } from 'lit';
3
- import 'croppie/croppie.js';
4
3
  import { CroppieCSS } from './CroppieCSS';
5
4
  import { property } from 'lit/decorators.js';
6
5
  import { Icon } from '../vectoricon';
@@ -166,11 +165,11 @@ export class ImagePicker extends FormElement {
166
165
  wrapper.removeChild(wrapper.firstChild);
167
166
  }
168
167
  this.showCroppie = true;
169
- const croppie = document.createElement('div');
170
- wrapper.appendChild(croppie);
168
+ const ele = document.createElement('div');
169
+ wrapper.appendChild(ele);
171
170
 
172
171
  const Croppie = (window as any).Croppie;
173
- this.croppie = new Croppie(croppie, {
172
+ this.croppie = new Croppie(ele, {
174
173
  enableExif: true,
175
174
  viewport: {
176
175
  width: 300,
@@ -206,9 +205,6 @@ export class ImagePicker extends FormElement {
206
205
 
207
206
  picker.value = fd;
208
207
  picker.closeCroppie();
209
-
210
- console.log('image changed', picker.name, picker.value, picker.url);
211
- // picker.dispatchEvent(new CustomEvent('image-changed', { detail: resp }));
212
208
  });
213
209
  }
214
210
 
@@ -3,7 +3,6 @@ import { property } from 'lit/decorators.js';
3
3
  import { TembaList } from './TembaList';
4
4
  import { Contact } from '../interfaces';
5
5
  import { Icon } from '../vectoricon';
6
- import { renderAvatar } from '../utils';
7
6
 
8
7
  export class TicketList extends TembaList {
9
8
  @property({ type: String })
@@ -76,10 +75,10 @@ export class TicketList extends TembaList {
76
75
  >
77
76
  <div>
78
77
  ${!contact.ticket.closed_on && contact.ticket.assignee
79
- ? html`${renderAvatar({
80
- user: contact.ticket.assignee,
81
- scale: 0.8,
82
- })}`
78
+ ? html`<temba-user
79
+ email=${contact.ticket.assignee.email}
80
+ scale="0.8"
81
+ ></temba-user>`
83
82
  : null}
84
83
  </div>
85
84
  </div>
@@ -216,6 +216,10 @@ export class Store extends RapidElement {
216
216
  );
217
217
  }
218
218
 
219
+ public getUser(email: string) {
220
+ return this.users.find((user: User) => user.email === email);
221
+ }
222
+
219
223
  public firstUpdated() {
220
224
  this.reset();
221
225
  }
@@ -1,14 +1,15 @@
1
- import { html, PropertyValueMap, TemplateResult } from 'lit';
1
+ import { PropertyValueMap } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
3
  import { CustomEventType } from '../interfaces';
4
- import { RapidElement } from '../RapidElement';
4
+
5
5
  import { Store } from './Store';
6
+ import { StoreMonitorElement } from './StoreMonitorElement';
6
7
 
7
8
  /**
8
9
  * StoreElement is a listener for a given endpoint that re-renders
9
10
  * when the underlying store element changes
10
11
  */
11
- export class StoreElement extends RapidElement {
12
+ export class StoreElement extends StoreMonitorElement {
12
13
  @property({ type: String })
13
14
  url: string;
14
15
 
@@ -31,17 +32,15 @@ export class StoreElement extends RapidElement {
31
32
  });
32
33
  }
33
34
 
34
- private handleStoreUpdated(event: CustomEvent) {
35
- this.store.initialHttpComplete.then(() => {
36
- if (event.detail.url === this.url) {
37
- const previous = this.data;
38
- this.data = event.detail.data;
39
- this.fireCustomEvent(CustomEventType.Refreshed, {
40
- data: event.detail.data,
41
- previous,
42
- });
43
- }
44
- });
35
+ protected storeUpdated(event: CustomEvent) {
36
+ if (event.detail.url === this.url) {
37
+ const previous = this.data;
38
+ this.data = event.detail.data;
39
+ this.fireCustomEvent(CustomEventType.Refreshed, {
40
+ data: event.detail.data,
41
+ previous,
42
+ });
43
+ }
45
44
  }
46
45
 
47
46
  protected updated(
@@ -59,30 +58,6 @@ export class StoreElement extends RapidElement {
59
58
 
60
59
  connectedCallback(): void {
61
60
  super.connectedCallback();
62
- this.store = document.querySelector('temba-store') as Store;
63
- this.handleStoreUpdated = this.handleStoreUpdated.bind(this);
64
61
  this.prepareData = this.prepareData.bind(this);
65
- if (this.store) {
66
- this.store.addEventListener(
67
- CustomEventType.StoreUpdated,
68
- this.handleStoreUpdated
69
- );
70
- }
71
- }
72
-
73
- disconnectedCallback(): void {
74
- super.disconnectedCallback();
75
- if (this.store) {
76
- this.store.removeEventListener(
77
- CustomEventType.StoreUpdated,
78
- this.handleStoreUpdated
79
- );
80
- }
81
- }
82
-
83
- public render(): TemplateResult {
84
- if (!this.store.ready && this.showLoading) {
85
- return html`<temba-loading></temba-loading>`;
86
- }
87
62
  }
88
63
  }
@@ -0,0 +1,61 @@
1
+ import { html, PropertyValueMap, TemplateResult } from 'lit';
2
+ import { property } from 'lit/decorators.js';
3
+ import { CustomEventType } from '../interfaces';
4
+ import { RapidElement } from '../RapidElement';
5
+ import { Store } from './Store';
6
+
7
+ /**
8
+ * StoreMonitorElement notifies when the store is updated and with what url
9
+ */
10
+ export class StoreMonitorElement extends RapidElement {
11
+ @property({ type: String })
12
+ url: string;
13
+
14
+ @property({ type: Boolean })
15
+ showLoading = false;
16
+
17
+ store: Store;
18
+
19
+ private handleStoreUpdated(event: CustomEvent) {
20
+ this.store.initialHttpComplete.then(() => {
21
+ this.storeUpdated(event);
22
+ });
23
+ }
24
+
25
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
26
+ protected storeUpdated(event: CustomEvent) {}
27
+
28
+ protected updated(
29
+ properties: PropertyValueMap<any> | Map<PropertyKey, unknown>
30
+ ): void {
31
+ super.updated(properties);
32
+ }
33
+
34
+ connectedCallback(): void {
35
+ super.connectedCallback();
36
+ this.store = document.querySelector('temba-store') as Store;
37
+ this.handleStoreUpdated = this.handleStoreUpdated.bind(this);
38
+ if (this.store) {
39
+ this.store.addEventListener(
40
+ CustomEventType.StoreUpdated,
41
+ this.handleStoreUpdated
42
+ );
43
+ }
44
+ }
45
+
46
+ disconnectedCallback(): void {
47
+ super.disconnectedCallback();
48
+ if (this.store) {
49
+ this.store.removeEventListener(
50
+ CustomEventType.StoreUpdated,
51
+ this.handleStoreUpdated
52
+ );
53
+ }
54
+ }
55
+
56
+ public render(): TemplateResult {
57
+ if (!this.store.ready && this.showLoading) {
58
+ return html`<temba-loading></temba-loading>`;
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,111 @@
1
+ import { PropertyValueMap, TemplateResult, css, html } from 'lit';
2
+ import { property } from 'lit/decorators.js';
3
+ import { User } from '../interfaces';
4
+ import { StoreMonitorElement } from '../store/StoreMonitorElement';
5
+ import { colorHash, extractInitials } from '../utils';
6
+
7
+ export class TembaUser extends StoreMonitorElement {
8
+ public static styles = css`
9
+ :host {
10
+ display: flex;
11
+ }
12
+
13
+ .wrapper {
14
+ display: flex;
15
+ flex-direction: row;
16
+ align-items: center;
17
+ flex-grow: 1;
18
+ }
19
+
20
+ .name {
21
+ flex-grow: 1;
22
+ display: -webkit-box;
23
+ -webkit-line-clamp: 1;
24
+ -webkit-box-orient: vertical;
25
+ overflow: hidden;
26
+ }
27
+ `;
28
+
29
+ @property({ type: String })
30
+ email: string;
31
+
32
+ @property({ type: Number })
33
+ scale: number;
34
+
35
+ @property({ type: Boolean })
36
+ name: string;
37
+
38
+ @property({ type: Object, attribute: false })
39
+ user: User;
40
+
41
+ @property({ type: String, attribute: false })
42
+ background: string;
43
+
44
+ @property({ type: String, attribute: false })
45
+ initials: string;
46
+
47
+ @property({ type: String, attribute: false })
48
+ fullName: string;
49
+
50
+ public updated(
51
+ changed: PropertyValueMap<any> | Map<PropertyKey, unknown>
52
+ ): void {
53
+ 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
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ public render(): TemplateResult {
70
+ if (!this.user) {
71
+ return null;
72
+ }
73
+
74
+ return html` <div class="wrapper">
75
+ <div
76
+ class="avatar-circle"
77
+ style="
78
+ transform:scale(${this.scale || 1});
79
+ display: flex;
80
+ height: 30px;
81
+ width: 30px;
82
+ flex-direction: row;
83
+ align-items: center;
84
+ color: #fff;
85
+ border-radius: 100%;
86
+ font-weight: 400;
87
+ overflow: hidden;
88
+ font-size: 12px;
89
+ box-shadow: inset 0 0 0 3px rgba(0, 0, 0, 0.1);
90
+ background:${this.background}"
91
+ >
92
+ ${this.initials
93
+ ? html` <div
94
+ style="border: 0px solid red; display:flex; flex-direction: column; align-items:center;flex-grow:1"
95
+ >
96
+ <div style="border:0px solid blue;">${this.initials}</div>
97
+ </div>`
98
+ : null}
99
+ </div>
100
+ ${this.name
101
+ ? html`<div
102
+ class="name"
103
+ style="margin: 0px ${this.scale - 0.5}em;font-size:${this.scale +
104
+ 0.2}em"
105
+ >
106
+ ${this.fullName}
107
+ </div>`
108
+ : null}
109
+ </div>`;
110
+ }
111
+ }
@@ -4,11 +4,10 @@ import { Button } from '../button/Button';
4
4
  import { Dialog } from '../dialog/Dialog';
5
5
  import { Attachment, ContactField, Ticket, User } from '../interfaces';
6
6
  import ColorHash from 'color-hash';
7
- import { userInfo } from 'os';
8
7
 
9
8
  export const DEFAULT_MEDIA_ENDPOINT = '/api/v2/media.json';
10
9
 
11
- const colorHash = new ColorHash();
10
+ export const colorHash = new ColorHash();
12
11
 
13
12
  export type Asset = KeyedAsset & Ticket & ContactField;
14
13
 
package/temba-modules.ts CHANGED
@@ -53,6 +53,7 @@ import { NotificationList } from './src/list/NotificationList';
53
53
  import { WebChat } from './src/webchat/WebChat';
54
54
  import { ImagePicker } from './src/imagepicker/ImagePicker';
55
55
  import { Mask } from './src/mask/Mask';
56
+ import { TembaUser } from './src/user/TembaUser';
56
57
 
57
58
  export function addCustomElement(name: string, comp: any) {
58
59
  if (!window.customElements.get(name)) {
@@ -116,3 +117,4 @@ addCustomElement('temba-thumbnail', Thumbnail);
116
117
  addCustomElement('temba-webchat', WebChat);
117
118
  addCustomElement('temba-image-picker', ImagePicker);
118
119
  addCustomElement('temba-mask', Mask);
120
+ addCustomElement('temba-user', TembaUser);
@@ -1,7 +1,10 @@
1
1
  <!doctype html>
2
2
  <html lang="en">
3
- <head></head>
4
- <body>
5
- <script type="module" src="../out-tsc/temba-components.js"></script>
6
- </body>
3
+
4
+ <head></head>
5
+
6
+ <body>
7
+ <script type="module" src="../out-tsc/temba-components.js"></script>
8
+ </body>
9
+
7
10
  </html>
@@ -48,9 +48,7 @@ describe('temba-contact-tickets', () => {
48
48
  });
49
49
 
50
50
  // click on the avatar element
51
- (
52
- tickets.shadowRoot.querySelector('.avatar-circle') as HTMLDivElement
53
- ).click();
51
+ (tickets.shadowRoot.querySelector('temba-user') as HTMLDivElement).click();
54
52
  assert.instanceOf(tickets, ContactTickets);
55
53
  await assertScreenshot('contacts/tickets-assignment', getClip(tickets));
56
54
  });