@nyaruka/temba-components 0.113.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 +21 -2
  2. package/demo/index.html +1 -1
  3. package/dist/temba-components.js +793 -966
  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 +10 -5
  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 +72 -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 +11 -6
  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 +79 -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,490 +0,0 @@
1
- import { css, html, PropertyValueMap, TemplateResult } from 'lit';
2
- import { property } from 'lit/decorators.js';
3
- import { CustomEventType, Ticket, TicketStatus } from '../interfaces';
4
- import { EndpointMonitorElement } from '../store/EndpointMonitorElement';
5
- import { getClasses, postJSON, stopEvent } from '../utils';
6
- import { Icon } from '../vectoricon';
7
-
8
- const dropdownUserScale = 0.7;
9
- const inlineUserScale = 0.8;
10
-
11
- export class ContactTickets extends EndpointMonitorElement {
12
- @property({ type: String })
13
- agent: string;
14
-
15
- @property({ type: String })
16
- contact: string;
17
-
18
- @property({ type: String })
19
- ticket: string;
20
-
21
- @property({ type: Boolean })
22
- clickable = false;
23
-
24
- @property({ type: Boolean })
25
- expandable = false;
26
-
27
- @property({ type: Boolean })
28
- expanded = false;
29
-
30
- @property({ type: Object, attribute: false })
31
- data: Ticket[];
32
-
33
- static get styles() {
34
- return css`
35
- :host {
36
- }
37
-
38
- :hover {
39
- }
40
-
41
- .ticket.expandable:hover,
42
- .ticket.clickable:hover {
43
- cursor: pointer;
44
- box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.055),
45
- 0 0 0px 2px var(--color-link-primary);
46
- }
47
-
48
- .header {
49
- display: flex;
50
- flex-direction: row;
51
- flex-grow: 1;
52
- }
53
-
54
- .tickets {
55
- display: flex;
56
- flex-direction: column;
57
- padding: 0.3em 0.8em;
58
- }
59
-
60
- .count {
61
- margin-left: 0.5em;
62
- }
63
-
64
- .ticket {
65
- background: #fff;
66
- display: flex;
67
- flex-direction: column;
68
- margin-bottom: 0.5em;
69
- border-radius: var(--curvature);
70
- display: flex;
71
- flex-direction: column;
72
- align-items: stretch;
73
- box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.055),
74
- 0 0 0px 1px rgba(0, 0, 0, 0.02);
75
- transition: all 200ms ease-in-out;
76
- }
77
-
78
- .ticket .topic {
79
- overflow: hidden;
80
- text-overflow: ellipsis;
81
- margin: 0.5em 0.75em 0.5em 0.75em;
82
- display: -webkit-box;
83
- -webkit-box-orient: vertical;
84
- -webkit-line-clamp: 1;
85
- flex-grow: 2;
86
- }
87
-
88
- .ticket .body {
89
- max-height: 0px;
90
- overflow-y: auto;
91
- -webkit-line-clamp: none;
92
- white-space: normal;
93
- width: initial;
94
- padding: 0.5em 0.75em 0em 0.75em;
95
- max-height: 40vh;
96
- display: none;
97
- margin-bottom: 0.5em;
98
- border-top: 1px solid #e6e6e6;
99
- }
100
-
101
- .ticket.expanded .body {
102
- display: block;
103
- }
104
-
105
- .status {
106
- --icon-color: #999;
107
- }
108
-
109
- .ticket.closed {
110
- background: #f9f9f9;
111
- color: #888;
112
- }
113
-
114
- .resolve {
115
- color: var(--color-primary-dark);
116
- }
117
-
118
- .dropdown {
119
- color: rgb(45, 45, 45);
120
- z-index: 50;
121
- width: 18em;
122
- }
123
-
124
- .option-group {
125
- padding: 0.4em;
126
- border-bottom: 1px solid #f3f3f3;
127
- }
128
-
129
- .option-group temba-user {
130
- flex-grow: 1;
131
- }
132
-
133
- .assigned .user {
134
- flex-grow: 1;
135
- }
136
-
137
- .assigned {
138
- display: flex;
139
- align-items: center;
140
- }
141
-
142
- .assigned temba-button {
143
- margin-right: 0.75em;
144
- }
145
-
146
- .assigned .user:hover {
147
- cursor: default;
148
- background: none;
149
- }
150
-
151
- .options {
152
- max-height: 40vh;
153
- overflow-y: auto;
154
- border-bottom: none;
155
- }
156
-
157
- .user {
158
- display: flex;
159
- align-items: center;
160
- border-radius: var(--curvature);
161
- cursor: pointer;
162
- }
163
-
164
- .user:hover {
165
- background: var(--color-selection);
166
- }
167
-
168
- .user .name {
169
- display: -webkit-box;
170
- -webkit-line-clamp: 1;
171
- -webkit-box-orient: vertical;
172
- overflow: hidden;
173
- flex-grow: 1;
174
- }
175
-
176
- .user temba-button {
177
- margin-left: 0.5em;
178
- }
179
-
180
- .current-user {
181
- font-weight: 400;
182
- }
183
-
184
- .details {
185
- display: flex;
186
- align-items: center;
187
- flex-shrink: 1;
188
- margin-right: 0.5em;
189
- }
190
-
191
- .details .date {
192
- padding: 0em 0.5em;
193
- }
194
-
195
- .details .toggle {
196
- padding-right: 0.5em;
197
- }
198
- `;
199
- }
200
-
201
- prepareData(data: any): any {
202
- if (data && data.length) {
203
- data.sort((a: Ticket, b: Ticket) => {
204
- if (a.status == TicketStatus.Open && b.status == TicketStatus.Closed) {
205
- return -1;
206
- }
207
-
208
- if (b.status == TicketStatus.Open && a.status == TicketStatus.Closed) {
209
- return 1;
210
- }
211
-
212
- if (
213
- a.status == TicketStatus.Closed &&
214
- b.status == TicketStatus.Closed
215
- ) {
216
- return (
217
- new Date(b.closed_on).getTime() - new Date(a.closed_on).getTime()
218
- );
219
- }
220
-
221
- return (
222
- new Date(b.opened_on).getTime() - new Date(a.opened_on).getTime()
223
- );
224
- });
225
- }
226
- return data;
227
- }
228
-
229
- protected updated(
230
- changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
231
- ): void {
232
- super.updated(changes);
233
-
234
- if (changes.has('data') && this.data) {
235
- this.fireCustomEvent(CustomEventType.DetailsChanged, {
236
- count: this.data.length
237
- });
238
- }
239
-
240
- if (changes.has('contact') || changes.has('ticket')) {
241
- if (this.contact) {
242
- this.url = `/api/v2/tickets.json?contact=${this.contact}${
243
- this.ticket ? '&ticket=' + this.ticket : ''
244
- }`;
245
- } else {
246
- this.url = null;
247
- }
248
- }
249
- }
250
-
251
- private handleClose(uuid: string) {
252
- postJSON(`/api/v2/ticket_actions.json`, {
253
- tickets: [uuid],
254
- action: 'close'
255
- })
256
- .then(() => {
257
- this.refresh();
258
- })
259
- .catch((response: any) => {
260
- console.error(response);
261
- });
262
- }
263
-
264
- private handleReopen(uuid: string) {
265
- postJSON(`/api/v2/ticket_actions.json`, {
266
- tickets: [uuid],
267
- action: 'reopen'
268
- })
269
- .then(() => {
270
- this.refresh();
271
- })
272
- .catch((response: any) => {
273
- console.error(response);
274
- });
275
- }
276
-
277
- public handleTicketAssignment(uuid: string, email: string) {
278
- // if its already assigned to use, it's a noop
279
- const ticket = this.data.find((ticket) => ticket.uuid === uuid);
280
- if (ticket.assignee && ticket.assignee.email === email) {
281
- return;
282
- }
283
- this.blur();
284
-
285
- postJSON(`/api/v2/ticket_actions.json`, {
286
- tickets: [uuid],
287
- action: 'assign',
288
- assignee: email
289
- })
290
- .then(() => {
291
- this.refresh();
292
- })
293
- .catch((response: any) => {
294
- console.error(response);
295
- });
296
- return true;
297
- }
298
-
299
- public renderTicket(ticket: Ticket) {
300
- const date = ticket.opened_on;
301
- const users = this.store.getAssignableUsers();
302
- const agent = users.find((user) => user.email === this.agent);
303
- return html`
304
- <div
305
- @click=${() => {
306
- if (this.clickable) {
307
- this.fireCustomEvent(CustomEventType.ButtonClicked, { ticket });
308
- } else if (this.expandable) {
309
- this.expanded = !this.expanded;
310
- }
311
- }}
312
- class="ticket ${ticket.status} ${getClasses({
313
- clickable: this.clickable,
314
- expandable: this.expandable,
315
- expanded: this.expanded
316
- })}"
317
- >
318
- <div class="header">
319
- <div class="topic">${ticket.topic.name}</div>
320
-
321
- <div class="details">
322
- <div class="date">
323
- <temba-date value="${date}" display="duration"></temba-date>
324
- </div>
325
-
326
- ${ticket.status === TicketStatus.Closed
327
- ? html`<div class="reopen">
328
- <temba-button
329
- primary
330
- small
331
- name="Reopen"
332
- @click=${(event: MouseEvent) => {
333
- event.preventDefault();
334
- event.stopPropagation();
335
- this.handleReopen(ticket.uuid);
336
- }}
337
- ></temba-button>
338
- </div>`
339
- : html`
340
- <div>
341
- <temba-dropdown
342
- right
343
- arrowsize="8"
344
- arrowoffset="-44"
345
- offsety="8"
346
- offsetx=${ticket.assignee ? -42 : -28}
347
- >
348
- <div slot="toggle" class="toggle">
349
- ${ticket.assignee
350
- ? html`
351
- <temba-user
352
- email=${ticket.assignee.email}
353
- scale="${inlineUserScale}"
354
- ></temba-user>
355
- `
356
- : html`
357
- <temba-button
358
- name="Assign"
359
- primary
360
- small
361
- ></temba-button>
362
- `}
363
- </div>
364
-
365
- <div
366
- slot="dropdown"
367
- class="dropdown"
368
- @click=${(event: MouseEvent) => {
369
- stopEvent(event);
370
- }}
371
- >
372
- ${ticket.assignee
373
- ? html`
374
- <div
375
- class="assigned option-group ${agent &&
376
- ticket.assignee.email == agent.email
377
- ? 'current-user'
378
- : ''}"
379
- >
380
- <temba-user
381
- email=${ticket.assignee.email}
382
- name
383
- scale="${dropdownUserScale}"
384
- ></temba-user>
385
- <temba-button
386
- name="Unassign"
387
- primary
388
- small
389
- @click=${(event: MouseEvent) => {
390
- stopEvent(event);
391
- this.handleTicketAssignment(
392
- ticket.uuid,
393
- null
394
- );
395
- }}
396
- ></temba-button>
397
- </div>
398
- `
399
- : null}
400
- ${agent &&
401
- (!ticket.assignee ||
402
- agent.email !== ticket.assignee.email)
403
- ? html`
404
- <div
405
- class="current-user option-group"
406
- @click=${(event: MouseEvent) => {
407
- stopEvent(event);
408
- this.handleTicketAssignment(
409
- ticket.uuid,
410
- agent.email
411
- );
412
- }}
413
- >
414
- <temba-user
415
- email=${agent.email}
416
- name
417
- scale="${dropdownUserScale}"
418
- ></temba-user>
419
- </div>
420
- `
421
- : null}
422
-
423
- <div class="options option-group">
424
- ${this.store.getAssignableUsers().map((user) => {
425
- if (
426
- ticket.assignee &&
427
- user.email === ticket.assignee.email
428
- ) {
429
- return null;
430
- }
431
-
432
- if (user.email === this.agent) {
433
- return null;
434
- }
435
- return html`<div
436
- @click=${(event: MouseEvent) => {
437
- stopEvent(event);
438
- this.handleTicketAssignment(
439
- ticket.uuid,
440
- user.email
441
- );
442
- }}
443
- >
444
- <temba-user
445
- email=${user.email}
446
- scale="${dropdownUserScale}"
447
- name
448
- ></temba-user>
449
- </div>`;
450
- })}
451
- </div>
452
- </div>
453
- </temba-dropdown>
454
- </div>
455
- <temba-tip
456
- text="Resolve"
457
- position="left"
458
- style="width:1.5em"
459
- class="resolve"
460
- >
461
- <temba-icon
462
- size="1.25"
463
- name="${Icon.check}"
464
- @click=${(event: MouseEvent) => {
465
- event.preventDefault();
466
- event.stopPropagation();
467
- this.handleClose(ticket.uuid);
468
- }}
469
- ?clickable=${open}
470
- />
471
- </temba-tip>
472
- `}
473
- </div>
474
- </div>
475
- <div class="body">${ticket.body}</div>
476
- </div>
477
- `;
478
- }
479
-
480
- public render(): TemplateResult {
481
- if (this.data && this.data.length > 0) {
482
- const tickets = this.data.map((ticket) => {
483
- return this.renderTicket(ticket);
484
- });
485
- return html`${tickets}`;
486
- }
487
-
488
- return html`<slot name="empty"></slot>`;
489
- }
490
- }
@@ -1,52 +0,0 @@
1
- import { assert, waitUntil } from '@open-wc/testing';
2
- import { ContactTickets } from '../src/contacts/ContactTickets';
3
- import {
4
- assertScreenshot,
5
- getClip,
6
- getComponent,
7
- loadStore,
8
- mockAPI,
9
- mockNow
10
- } from './utils.test';
11
-
12
- const TAG = 'temba-contact-tickets';
13
- const getContactTickets = async (attrs: any = {}) => {
14
- const contactTickets = (await getComponent(
15
- TAG,
16
- attrs,
17
- '',
18
- 400
19
- )) as ContactTickets;
20
- // wait for our contact to load
21
- await waitUntil(() => !!contactTickets.data);
22
- return contactTickets;
23
- };
24
-
25
- mockNow('2023-04-07T00:00:00.000-00:00');
26
- describe('temba-contact-tickets', () => {
27
- beforeEach(() => {
28
- mockAPI();
29
- loadStore();
30
- });
31
-
32
- it('renders default', async () => {
33
- const tickets: ContactTickets = await getContactTickets({
34
- contact: '24d64810-3315-4ff5-be85-48e3fe055bf9',
35
- agent: 'admin1@nyaruka.com'
36
- });
37
- assert.instanceOf(tickets, ContactTickets);
38
- await assertScreenshot('contacts/tickets', getClip(tickets));
39
- });
40
-
41
- it('shows assignment picker', async () => {
42
- const tickets: ContactTickets = await getContactTickets({
43
- contact: '24d64810-3315-4ff5-be85-48e3fe055bf9',
44
- agent: 'admin1@nyaruka.com'
45
- });
46
-
47
- // click on the avatar element
48
- (tickets.shadowRoot.querySelector('temba-user') as HTMLDivElement).click();
49
- assert.instanceOf(tickets, ContactTickets);
50
- await assertScreenshot('contacts/tickets-assignment', getClip(tickets));
51
- });
52
- });