@nyaruka/temba-components 0.43.7 → 0.44.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.
@@ -12,8 +12,6 @@ import {
12
12
  WebResponse,
13
13
  } from '../utils';
14
14
  import { Completion } from '../completion/Completion';
15
- import { VectorIcon } from '../vectoricon/VectorIcon';
16
- import { Button } from '../button/Button';
17
15
 
18
16
  export interface Attachment {
19
17
  uuid: string;
@@ -146,7 +144,7 @@ export class Compose extends FormElement {
146
144
  padding: 0.2em;
147
145
  }
148
146
 
149
- #upload-files {
147
+ #upload-input {
150
148
  display: none;
151
149
  }
152
150
  .upload-label {
@@ -195,8 +193,8 @@ export class Compose extends FormElement {
195
193
  @property({ type: Boolean })
196
194
  button: boolean;
197
195
 
198
- @property({ type: String, attribute: false })
199
- currentChat = '';
196
+ @property({ type: String })
197
+ currentText = '';
200
198
 
201
199
  @property({ type: String })
202
200
  accept = ''; //e.g. ".xls,.xlsx"
@@ -207,10 +205,11 @@ export class Compose extends FormElement {
207
205
  @property({ type: Boolean, attribute: false })
208
206
  uploading: boolean;
209
207
 
210
- // values = valid and uploaded attachments
211
- // errorValues = invalid and not-uploaded attachments
208
+ @property({ type: Array })
209
+ currentAttachments: Attachment[] = [];
210
+
212
211
  @property({ type: Array, attribute: false })
213
- errorValues: Attachment[] = [];
212
+ failedAttachments: Attachment[] = [];
214
213
 
215
214
  @property({ type: String })
216
215
  buttonName = 'Send';
@@ -221,24 +220,65 @@ export class Compose extends FormElement {
221
220
  @property({ type: String, attribute: false })
222
221
  buttonError = '';
223
222
 
223
+ @property({ type: Boolean, attribute: 'widget_only' })
224
+ widgetOnly: boolean;
225
+
226
+ @property({ type: Array })
227
+ errors: string[];
228
+
229
+ @property({ type: String })
230
+ value = '';
231
+
224
232
  public constructor() {
225
233
  super();
226
234
  }
227
235
 
236
+ private deserializeComposeValue(): void {
237
+ if (this.value) {
238
+ const parsed_value = JSON.parse(this.value);
239
+ if (this.chatbox) {
240
+ this.currentText = parsed_value.text;
241
+ }
242
+ if (this.attachments) {
243
+ this.currentAttachments = parsed_value.attachments;
244
+ }
245
+ }
246
+ }
247
+
248
+ private serializeComposeValue(): void {
249
+ const composeValue = {
250
+ text: this.currentText,
251
+ attachments: this.currentAttachments,
252
+ };
253
+ // update this.value...
254
+ this.value = JSON.stringify(composeValue);
255
+ // and then also update this.values...
256
+ // so that the hidden input is updated via FormElement.updateInputs()
257
+ this.values = [composeValue];
258
+ }
259
+
260
+ public firstUpdated(changes: Map<string, any>): void {
261
+ super.firstUpdated(changes);
262
+
263
+ this.deserializeComposeValue();
264
+ this.setFocusOnChatbox();
265
+ }
266
+
228
267
  public updated(changes: Map<string, any>): void {
229
268
  super.updated(changes);
230
269
 
231
- if (changes.has('currentChat') || changes.has('values')) {
232
- this.buttonError = '';
270
+ if (changes.has('currentText') || changes.has('currentAttachments')) {
233
271
  this.toggleButton();
272
+ this.setFocusOnChatbox();
273
+ this.serializeComposeValue();
234
274
  }
235
- }
236
275
 
237
- firstUpdated(): void {
238
- this.setFocusOnChatbox();
276
+ if (changes.has('buttonError')) {
277
+ this.setFocusOnChatbox();
278
+ }
239
279
  }
240
280
 
241
- setFocusOnChatbox(): void {
281
+ private setFocusOnChatbox(): void {
242
282
  if (this.chatbox) {
243
283
  const completion = this.shadowRoot.querySelector(
244
284
  'temba-completion'
@@ -253,17 +293,15 @@ export class Compose extends FormElement {
253
293
  }
254
294
 
255
295
  public reset(): void {
256
- this.currentChat = '';
257
- this.values = [];
258
- this.errorValues = [];
296
+ this.currentText = '';
297
+ this.currentAttachments = [];
298
+ this.failedAttachments = [];
259
299
  this.buttonError = '';
260
300
  }
261
301
 
262
302
  private handleChatboxChange(evt: Event) {
263
303
  const completion = evt.target as Completion;
264
- const textInput = completion.textInputElement;
265
- this.currentChat = textInput.value;
266
- this.preventDefaults(evt);
304
+ this.currentText = completion.value;
267
305
  }
268
306
 
269
307
  private handleDragEnter(evt: DragEvent): void {
@@ -303,11 +341,11 @@ export class Compose extends FormElement {
303
341
  this.preventDefaults(evt);
304
342
  }
305
343
 
306
- private handleAddAttachments(): void {
344
+ private handleUploadFileIconClicked(): void {
307
345
  this.dispatchEvent(new Event('change'));
308
346
  }
309
347
 
310
- private handleUploadFileChanged(evt: Event): void {
348
+ private handleUploadFileInputChanged(evt: Event): void {
311
349
  const target = evt.target as HTMLInputElement;
312
350
  const files = target.files;
313
351
  this.uploadFiles(files);
@@ -315,11 +353,11 @@ export class Compose extends FormElement {
315
353
 
316
354
  public uploadFiles(files: FileList): void {
317
355
  let filesToUpload = [];
318
- if (this.values && this.values.length > 0) {
356
+ if (this.currentAttachments && this.currentAttachments.length > 0) {
319
357
  //remove duplicate files that have already been uploaded
320
358
  filesToUpload = [...files].filter(file => {
321
- const index = this.values.findIndex(
322
- value => value.name === file.name && value.size === file.size
359
+ const index = this.currentAttachments.findIndex(
360
+ value => value.filename === file.name && value.size === file.size
323
361
  );
324
362
  if (index === -1) {
325
363
  return file;
@@ -343,27 +381,39 @@ export class Compose extends FormElement {
343
381
  .then((response: WebResponse) => {
344
382
  const attachment = response.json as Attachment;
345
383
  if (attachment) {
346
- this.addValue(attachment);
347
- this.fireCustomEvent(CustomEventType.AttachmentAdded, attachment);
384
+ this.addCurrentAttachment(attachment);
348
385
  }
349
386
  })
350
387
  .catch((error: WebResponse) => {
351
- let fileError = '';
388
+ let uploadError = '';
352
389
  if (error.status === 400) {
353
- fileError = error.json.file[0];
390
+ uploadError = error.json.file[0];
354
391
  } else {
355
- fileError = 'Server failure';
392
+ uploadError = 'Server failure';
356
393
  }
357
- console.error(fileError);
358
- this.addErrorValue(file, fileError);
394
+ console.error(uploadError);
395
+ this.addFailedAttachment(file, uploadError);
359
396
  })
360
397
  .finally(() => {
361
398
  this.uploading = false;
362
399
  });
363
400
  }
364
401
 
365
- private addErrorValue(file: File, error: string) {
366
- const errorValue = {
402
+ private addCurrentAttachment(attachmentToAdd: any) {
403
+ this.currentAttachments.push(attachmentToAdd);
404
+ this.requestUpdate('currentAttachments');
405
+ this.fireCustomEvent(CustomEventType.AttachmentAdded, attachmentToAdd);
406
+ }
407
+ private removeCurrentAttachment(attachmentToRemove: any) {
408
+ this.currentAttachments = this.currentAttachments.filter(
409
+ currentAttachment => currentAttachment !== attachmentToRemove
410
+ );
411
+ this.requestUpdate('currentAttachments');
412
+ this.fireCustomEvent(CustomEventType.AttachmentRemoved, attachmentToRemove);
413
+ }
414
+
415
+ private addFailedAttachment(file: File, error: string) {
416
+ const failedAttachment = {
367
417
  uuid: Math.random().toString(36).slice(2, 6),
368
418
  content_type: file.type,
369
419
  filename: file.name,
@@ -371,37 +421,40 @@ export class Compose extends FormElement {
371
421
  size: file.size,
372
422
  error: error,
373
423
  } as Attachment;
374
- this.errorValues.push(errorValue);
375
- this.requestUpdate('errorValues');
424
+ this.failedAttachments.push(failedAttachment);
425
+ this.requestUpdate('failedAttachments');
376
426
  }
377
- public removeErrorValue(valueToRemove: any) {
378
- this.errorValues = this.errorValues.filter(
379
- (value: any) => value !== valueToRemove
427
+ private removeFailedAttachment(attachmentToRemove: any) {
428
+ this.failedAttachments = this.failedAttachments.filter(
429
+ (failedAttachment: any) => failedAttachment !== attachmentToRemove
380
430
  );
381
- this.requestUpdate('errorValues');
431
+ this.requestUpdate('failedAttachments');
432
+ this.fireCustomEvent(CustomEventType.AttachmentRemoved, attachmentToRemove);
382
433
  }
383
434
 
384
- private handleRemoveAttachment(evt: Event): void {
435
+ private handleRemoveFileClicked(evt: Event): void {
385
436
  const target = evt.target as HTMLDivElement;
386
437
 
387
- const attachment = this.values.find(({ uuid }) => uuid === target.id);
388
- if (attachment) {
389
- this.removeValue(attachment);
390
- this.fireCustomEvent(CustomEventType.AttachmentRemoved, attachment);
438
+ const currentAttachmentToRemove = this.currentAttachments.find(
439
+ ({ uuid }) => uuid === target.id
440
+ );
441
+ if (currentAttachmentToRemove) {
442
+ this.removeCurrentAttachment(currentAttachmentToRemove);
391
443
  }
392
- const errorAttachment = this.errorValues.find(
444
+
445
+ const failedAttachmentToRemove = this.failedAttachments.find(
393
446
  ({ uuid }) => uuid === target.id
394
447
  );
395
- if (errorAttachment) {
396
- this.removeErrorValue(errorAttachment);
397
- this.fireCustomEvent(CustomEventType.AttachmentRemoved, attachment);
448
+ if (failedAttachmentToRemove) {
449
+ this.removeFailedAttachment(failedAttachmentToRemove);
398
450
  }
399
451
  }
400
452
 
401
453
  public toggleButton() {
402
454
  if (this.button) {
403
- const chatboxEmpty = this.currentChat.trim().length === 0;
404
- const attachmentsEmpty = this.values.length === 0;
455
+ this.buttonError = '';
456
+ const chatboxEmpty = this.currentText.trim().length === 0;
457
+ const attachmentsEmpty = this.currentAttachments.length === 0;
405
458
  if (this.chatbox && this.attachments) {
406
459
  this.buttonDisabled = chatboxEmpty && attachmentsEmpty;
407
460
  } else if (this.chatbox) {
@@ -414,7 +467,8 @@ export class Compose extends FormElement {
414
467
  }
415
468
  }
416
469
 
417
- private handleSendClick() {
470
+ private handleSendClick(evt: Event) {
471
+ evt.stopPropagation();
418
472
  this.handleSend();
419
473
  }
420
474
 
@@ -433,37 +487,43 @@ export class Compose extends FormElement {
433
487
  this.buttonDisabled = true;
434
488
  const name = this.buttonName;
435
489
  this.fireCustomEvent(CustomEventType.ButtonClicked, { name });
436
-
437
- //after send, return focus to chatbox
438
- this.setFocusOnChatbox();
439
490
  }
440
491
  }
441
492
 
442
493
  public render(): TemplateResult {
443
494
  return html`
444
- <div
445
- class=${getClasses({ container: true, highlight: this.pendingDrop })}
446
- @dragenter="${this.handleDragEnter}"
447
- @dragover="${this.handleDragOver}"
448
- @dragleave="${this.handleDragLeave}"
449
- @drop="${this.handleDrop}"
495
+ <temba-field
496
+ name=${this.name}
497
+ .errors=${this.errors}
498
+ .widgetOnly=${this.widgetOnly}
499
+ value=${this.value}
450
500
  >
451
- <div class="drop-mask"><div>Upload Attachment</div></div>
452
-
453
- ${this.chatbox
454
- ? html`<div class="items chatbox">${this.getChatbox()}</div>`
455
- : null}
456
- ${this.attachments
457
- ? html`<div class="items attachments">${this.getAttachments()}</div>`
458
- : null}
459
- <div class="items actions">${this.getActions()}</div>
460
- </div>
501
+ <div
502
+ class=${getClasses({ container: true, highlight: this.pendingDrop })}
503
+ @dragenter="${this.handleDragEnter}"
504
+ @dragover="${this.handleDragOver}"
505
+ @dragleave="${this.handleDragLeave}"
506
+ @drop="${this.handleDrop}"
507
+ >
508
+ <div class="drop-mask"><div>Upload Attachment</div></div>
509
+
510
+ ${this.chatbox
511
+ ? html`<div class="items chatbox">${this.getChatbox()}</div>`
512
+ : null}
513
+ ${this.attachments
514
+ ? html`<div class="items attachments">
515
+ ${this.getAttachments()}
516
+ </div>`
517
+ : null}
518
+ <div class="items actions">${this.getActions()}</div>
519
+ </div>
520
+ </temba-field>
461
521
  `;
462
522
  }
463
523
 
464
524
  private getChatbox(): TemplateResult {
465
525
  return html` <temba-completion
466
- value=${this.currentChat}
526
+ value=${this.currentText}
467
527
  gsm
468
528
  textarea
469
529
  autogrow
@@ -476,51 +536,51 @@ export class Compose extends FormElement {
476
536
 
477
537
  private getAttachments(): TemplateResult {
478
538
  return html`
479
- ${(this.values && this.values.length > 0) ||
480
- (this.errorValues && this.errorValues.length > 0)
539
+ ${(this.currentAttachments && this.currentAttachments.length > 0) ||
540
+ (this.failedAttachments && this.failedAttachments.length > 0)
481
541
  ? html` <div class="attachments-list">
482
- ${this.values.map(attachment => {
542
+ ${this.currentAttachments.map(validAttachment => {
483
543
  return html` <div class="attachment-item">
484
544
  <div
485
545
  class="remove-item"
486
- @click="${this.handleRemoveAttachment}"
546
+ @click="${this.handleRemoveFileClicked}"
487
547
  >
488
548
  <temba-icon
489
- id="${attachment.uuid}"
549
+ id="${validAttachment.uuid}"
490
550
  name="${Icon.delete_small}"
491
551
  ></temba-icon>
492
552
  </div>
493
553
  <div class="attachment-name">
494
554
  <span
495
- title="${attachment.filename} (${formatFileSize(
496
- attachment.size,
555
+ title="${validAttachment.filename} (${formatFileSize(
556
+ validAttachment.size,
497
557
  2
498
- )}) ${attachment.content_type}"
499
- >${truncate(attachment.filename, 25)}
500
- (${formatFileSize(attachment.size, 0)})
501
- ${formatFileType(attachment.content_type)}</span
558
+ )}) ${validAttachment.content_type}"
559
+ >${truncate(validAttachment.filename, 25)}
560
+ (${formatFileSize(validAttachment.size, 0)})
561
+ ${formatFileType(validAttachment.content_type)}</span
502
562
  >
503
563
  </div>
504
564
  </div>`;
505
565
  })}
506
- ${this.errorValues.map(errorAttachment => {
566
+ ${this.failedAttachments.map(invalidAttachment => {
507
567
  return html` <div class="attachment-item error">
508
568
  <div
509
569
  class="remove-item error"
510
- @click="${this.handleRemoveAttachment}"
570
+ @click="${this.handleRemoveFileClicked}"
511
571
  >
512
572
  <temba-icon
513
- id="${errorAttachment.uuid}"
573
+ id="${invalidAttachment.uuid}"
514
574
  name="${Icon.delete_small}"
515
575
  ></temba-icon>
516
576
  </div>
517
577
  <div class="attachment-name">
518
578
  <span
519
- title="${errorAttachment.filename} (${formatFileSize(
579
+ title="${invalidAttachment.filename} (${formatFileSize(
520
580
  0,
521
581
  0
522
- )}) - Attachment failed - ${errorAttachment.error}"
523
- >${truncate(errorAttachment.filename, 25)}
582
+ )}) - Attachment failed - ${invalidAttachment.error}"
583
+ >${truncate(invalidAttachment.filename, 25)}
524
584
  (${formatFileSize(0, 0)}) - Attachment failed</span
525
585
  >
526
586
  </div>
@@ -553,16 +613,21 @@ export class Compose extends FormElement {
553
613
  } else {
554
614
  return html` <input
555
615
  type="file"
556
- id="upload-files"
616
+ id="upload-input"
557
617
  multiple
558
618
  accept="${this.accept}"
559
- @change="${this.handleUploadFileChanged}"
619
+ @change="${this.handleUploadFileInputChanged}"
560
620
  />
561
- <label class="actions-left upload-label" for="upload-files">
621
+ <label
622
+ id="upload-label"
623
+ class="actions-left upload-label"
624
+ for="upload-input"
625
+ >
562
626
  <temba-icon
627
+ id="upload-icon"
563
628
  class="upload-icon"
564
629
  name="${Icon.attachment}"
565
- @click="${this.handleAddAttachments}"
630
+ @click="${this.handleUploadFileIconClicked}"
566
631
  clickable
567
632
  ></temba-icon>
568
633
  </label>`;
@@ -570,7 +635,7 @@ export class Compose extends FormElement {
570
635
  }
571
636
 
572
637
  private getCounter(): TemplateResult {
573
- return html`<temba-charcount text="${this.currentChat}"></temba-charcount>`;
638
+ return html`<temba-charcount text="${this.currentText}"></temba-charcount>`;
574
639
  }
575
640
 
576
641
  private getButton(): TemplateResult {
@@ -166,9 +166,6 @@ export class ContactChat extends ContactStoreElement {
166
166
  @property({ type: String })
167
167
  contactsEndpoint = '/api/v2/contacts.json';
168
168
 
169
- @property({ type: String })
170
- currentChat = '';
171
-
172
169
  @property({ type: String })
173
170
  currentNote = '';
174
171
 
@@ -271,16 +268,16 @@ export class ContactChat extends ContactStoreElement {
271
268
  };
272
269
  const compose = evt.currentTarget as Compose;
273
270
  if (compose) {
274
- const text = compose.currentChat;
275
- if (text.length > 0) {
271
+ const text = compose.currentText;
272
+ if (text && text.length > 0) {
276
273
  payload['text'] = text;
277
274
  }
278
- const attachments = compose.values.map(attachment => {
279
- const content_type = attachment.content_type;
280
- return content_type + ':' + attachment.url;
281
- });
282
- if (attachments.length > 0) {
283
- payload['attachments'] = attachments;
275
+ const attachments = compose.currentAttachments;
276
+ if (attachments && attachments.length > 0) {
277
+ const attachment_uuids = attachments.map(
278
+ attachment => attachment.uuid
279
+ );
280
+ payload['attachments'] = attachment_uuids;
284
281
  }
285
282
  }
286
283
  if (this.currentTicket) {
@@ -1,4 +1,4 @@
1
- import { html, TemplateResult } from 'lit';
1
+ import { css, html, TemplateResult } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
3
  import { Checkbox } from '../checkbox/Checkbox';
4
4
  import { Select } from '../select/Select';
@@ -26,6 +26,26 @@ export class RunList extends TembaList {
26
26
 
27
27
  private resultKeys = {};
28
28
 
29
+ static get styles() {
30
+ return css`
31
+ :host {
32
+ overflow-y: auto !important;
33
+ }
34
+
35
+ @media only screen and (max-height: 768px) {
36
+ temba-options {
37
+ max-height: 20vh;
38
+ }
39
+ }
40
+
41
+ temba-options {
42
+ display: block;
43
+ width: 100%;
44
+ flex-grow: 1;
45
+ }
46
+ `;
47
+ }
48
+
29
49
  public firstUpdated(changedProperties: Map<string, any>) {
30
50
  super.firstUpdated(changedProperties);
31
51
  }
@@ -294,9 +314,7 @@ export class RunList extends TembaList {
294
314
 
295
315
  ${resultKeys.length > 0
296
316
  ? html`
297
- <div
298
- style="padding:1em;overflow-y:auto;overflow-x:hidden;max-height:15vh;"
299
- >
317
+ <div style="padding:1em;">
300
318
  <div
301
319
  style="display:flex;font-size:1.2em;position:relative;right:0px"
302
320
  >
@@ -79,9 +79,6 @@ export class TembaList extends RapidElement {
79
79
 
80
80
  static get styles() {
81
81
  return css`
82
- :host {
83
- }
84
-
85
82
  temba-options {
86
83
  display: block;
87
84
  width: 100%;