@nyaruka/temba-components 0.163.0 → 0.165.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 (66) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/DEV_DATA.md +11 -11
  3. package/README.md +4 -4
  4. package/TEST_OPTIMIZATION.md +8 -11
  5. package/dist/locales/es.js +4 -0
  6. package/dist/locales/es.js.map +1 -1
  7. package/dist/locales/fr.js +4 -0
  8. package/dist/locales/fr.js.map +1 -1
  9. package/dist/locales/pt.js +4 -0
  10. package/dist/locales/pt.js.map +1 -1
  11. package/dist/static/svg/index.svg +1 -1
  12. package/dist/temba-components.js +5266 -3031
  13. package/dist/temba-components.js.map +1 -1
  14. package/orca/setup.sh +2 -2
  15. package/package.json +13 -18
  16. package/scripts/dev-data-sync.mjs +4 -4
  17. package/src/Icons.ts +3 -2
  18. package/src/display/Chat.ts +3 -2
  19. package/src/display/Lightbox.ts +57 -109
  20. package/src/display/ProgressBar.ts +15 -2
  21. package/src/display/Thumbnail.ts +34 -7
  22. package/src/form/Compose.ts +31 -1
  23. package/src/form/ContactSearch.ts +229 -126
  24. package/src/interfaces.ts +131 -2
  25. package/src/layout/Card.ts +336 -0
  26. package/src/layout/CardLayout.ts +425 -0
  27. package/src/layout/CardStack.ts +131 -0
  28. package/src/layout/Dialog.ts +33 -24
  29. package/src/layout/HeaderBar.ts +41 -0
  30. package/src/layout/PageHeader.ts +5 -6
  31. package/src/layout/Resizer.ts +20 -0
  32. package/src/list/BroadcastList.ts +912 -0
  33. package/src/list/CampaignList.ts +144 -0
  34. package/src/list/ContactList.ts +3 -1
  35. package/src/list/ContentList.ts +46 -10
  36. package/src/list/FieldList.ts +1057 -0
  37. package/src/list/FlowList.ts +31 -7
  38. package/src/list/MsgList.ts +18 -12
  39. package/src/list/SortableList.ts +52 -8
  40. package/src/list/TembaList.ts +8 -0
  41. package/src/list/TembaMenu.ts +5 -38
  42. package/src/list/TriggerList.ts +538 -0
  43. package/src/live/CampaignEvents.ts +1108 -0
  44. package/src/live/ContactChat.ts +7 -1
  45. package/src/live/ContactDetails.ts +20 -14
  46. package/src/live/ContactFieldEditor.ts +7 -3
  47. package/src/live/ContactFields.ts +1 -1
  48. package/src/live/ContactNameFetch.ts +5 -2
  49. package/src/live/ContactNotepad.ts +88 -2
  50. package/src/live/ContactStoreElement.ts +15 -3
  51. package/src/live/ContactTimeline.ts +28 -7
  52. package/src/locales/es.ts +4 -0
  53. package/src/locales/fr.ts +4 -0
  54. package/src/locales/pt.ts +4 -0
  55. package/src/utils.ts +19 -0
  56. package/static/svg/index.svg +1 -1
  57. package/static/svg/packs/2-temba/star-filled.svg +3 -0
  58. package/static/svg/work/traced/star-filled.svg +1 -0
  59. package/static/svg/work/used/star-filled.svg +3 -0
  60. package/stress-test.js +3 -3
  61. package/temba-modules.ts +18 -0
  62. package/web-dev-server.config.mjs +54 -0
  63. package/web-test-runner.config.mjs +1 -1
  64. package/xliff/es.xlf +12 -0
  65. package/xliff/fr.xlf +12 -0
  66. package/xliff/pt.xlf +12 -0
@@ -14,23 +14,38 @@ export class FlowList extends ContentList<Flow> {
14
14
  static get styles() {
15
15
  return css`
16
16
  ${ContentList.styles}
17
+ /* The name cell is a flex row (name text, issue icon, label
18
+ chips) that fills the grow column. When space runs out the
19
+ text spans ellipsize — the flex items shrink instead of the
20
+ cell hard-clipping them mid-glyph, which is what happens to
21
+ an inline-flex atomic inline under .cell-inner's ellipsis. */
17
22
  .flow-name {
18
23
  color: inherit;
19
24
  font-weight: var(--w-medium);
25
+ display: flex;
26
+ align-items: center;
27
+ gap: 6px;
28
+ min-width: 0;
29
+ }
30
+ .flow-name .name {
20
31
  overflow: hidden;
21
32
  text-overflow: ellipsis;
22
33
  white-space: nowrap;
23
- display: inline-flex;
24
- align-items: center;
25
- gap: 6px;
26
34
  }
27
35
  .flow-labels {
28
- display: inline-flex;
36
+ display: flex;
29
37
  align-items: center;
30
38
  gap: 4px;
31
- margin-left: 4px;
39
+ min-width: 0;
40
+ }
41
+ /* Chips shrink with the row and ellipsize internally (the
42
+ label's own slot handles that) rather than being clipped
43
+ mid-chip at the column edge. */
44
+ .flow-labels temba-label {
45
+ min-width: 0;
32
46
  }
33
47
  .issue-icon {
48
+ flex: none;
34
49
  --icon-color: var(--warning);
35
50
  }
36
51
  .num {
@@ -84,13 +99,20 @@ export class FlowList extends ContentList<Flow> {
84
99
  this.valueKey = 'uuid';
85
100
  this.emptyMessage = 'No flows';
86
101
  this.searchPlaceholder = 'Search flows';
102
+ // The name column is a `grow` column — it pools the table's
103
+ // leftover width so long names (and their label chips) get all
104
+ // available space before truncating, instead of clipping against
105
+ // a fixed cap while slack sits in the spacer. minTableWidth arms
106
+ // a horizontal scroll once the container is too narrow to keep
107
+ // the columns usable, rather than clipping anything.
108
+ this.minTableWidth = '640px';
87
109
  this.columns = [
88
110
  {
89
111
  key: 'name',
90
112
  label: 'Name',
91
113
  sortable: true,
92
114
  minWidth: '160px',
93
- maxWidth: '280px',
115
+ grow: true,
94
116
  pinned: true
95
117
  },
96
118
  {
@@ -174,7 +196,9 @@ export class FlowList extends ContentList<Flow> {
174
196
  case 'name': {
175
197
  const labels = item.labels || [];
176
198
  return html`<span class="flow-name"
177
- >${item.name || ''}
199
+ ><span class="name" title="${item.name || ''}"
200
+ >${item.name || ''}</span
201
+ >
178
202
  ${item.has_issues
179
203
  ? html`<temba-icon
180
204
  class="issue-icon"
@@ -2,6 +2,7 @@ import { css, html, TemplateResult } from 'lit';
2
2
  import { ContentList, ContentListColumn } from './ContentList';
3
3
  import { Icon } from '../Icons';
4
4
  import { Msg } from '../interfaces';
5
+ import { attachmentAsString } from '../utils';
5
6
 
6
7
  /**
7
8
  * Message CRUDL list — drop-in replacement for the rapidpro
@@ -14,9 +15,9 @@ export class MsgList extends ContentList<Msg> {
14
15
  static get styles() {
15
16
  return css`
16
17
  ${ContentList.styles}
17
- /* The message cell holds the body text with its attachment
18
- thumbnails right after it, and the flow / label pills pushed
19
- to the trailing edge. The text sizes to content and
18
+ /* The message cell holds the body text on the leading edge, with
19
+ its attachment thumbnails and the flow / label pills grouped
20
+ and pushed to the trailing edge. The text sizes to content and
20
21
  ellipsizes when squeezed — resolved per row, so a busy row
21
22
  doesn't widen a column for all of them. */
22
23
  .msg-cell {
@@ -34,7 +35,7 @@ export class MsgList extends ContentList<Msg> {
34
35
  text-overflow: ellipsis;
35
36
  white-space: nowrap;
36
37
  }
37
- /* Attachment thumbnails sit immediately after the text. */
38
+ /* Attachment thumbnails, immediately after the message text. */
38
39
  .msg-attachments {
39
40
  flex: 0 0 auto;
40
41
  display: flex;
@@ -44,10 +45,10 @@ export class MsgList extends ContentList<Msg> {
44
45
  /* Flow + label pills, pushed to the trailing edge of the cell. */
45
46
  .cell-pills {
46
47
  flex: 0 0 auto;
48
+ margin-left: auto;
47
49
  display: flex;
48
50
  align-items: center;
49
51
  gap: 6px;
50
- margin-left: auto;
51
52
  }
52
53
  /* Attachment thumbnails are sized well below the 44px row so
53
54
  they never grow its height — a small square preview rather
@@ -160,13 +161,18 @@ export class MsgList extends ContentList<Msg> {
160
161
  }
161
162
  }
162
163
 
163
- /** The message cell — body text, its attachment thumbnails, then
164
- * the trailing flow / label pills. Each piece sizes to content, so
165
- * the split is resolved independently for every row. */
164
+ /** The message cell — body text on the leading edge with its
165
+ * attachment thumbnails immediately after it, and the flow / label
166
+ * pills pushed to the trailing edge. Each piece sizes to content, so
167
+ * the split is resolved independently for every row. A message with
168
+ * no text renders no text span at all — otherwise its min-width
169
+ * would strand an attachment-only row's thumbnails away from the
170
+ * leading edge. */
166
171
  private renderMessageCell(item: Msg): TemplateResult {
172
+ const text = this.renderMessageText(item);
167
173
  return html`
168
174
  <div class="msg-cell">
169
- <span class="msg-text">${this.renderMessageText(item)}</span>
175
+ ${text ? html`<span class="msg-text">${text}</span>` : ''}
170
176
  ${this.renderAttachments(item)}${this.renderPills(item)}
171
177
  </div>
172
178
  `;
@@ -208,8 +214,8 @@ export class MsgList extends ContentList<Msg> {
208
214
  return item.text || '';
209
215
  }
210
216
 
211
- /** Attachment thumbnails for a row, sitting immediately after the
212
- * message text, or '' when the row carries none. */
217
+ /** Attachment thumbnails for a row, immediately after the message
218
+ * text, or '' when the row carries none. */
213
219
  private renderAttachments(item: Msg): TemplateResult | string {
214
220
  const attachments = item.attachments || [];
215
221
  if (!attachments.length) return '';
@@ -219,7 +225,7 @@ export class MsgList extends ContentList<Msg> {
219
225
  (a) => html`
220
226
  <temba-thumbnail
221
227
  class="msg-thumb"
222
- attachment=${a}
228
+ attachment=${attachmentAsString(a)}
223
229
  ></temba-thumbnail>
224
230
  `
225
231
  )}
@@ -92,6 +92,7 @@ export class SortableList extends RapidElement {
92
92
 
93
93
  draggingIdx = -1;
94
94
  draggingEle = null;
95
+ ghostOriginalDisplay = '';
95
96
  dropPlaceholder: HTMLDivElement = null;
96
97
  pendingDropIndex = -1;
97
98
  pendingTargetElement: HTMLElement = null;
@@ -111,9 +112,11 @@ export class SortableList extends RapidElement {
111
112
  }
112
113
 
113
114
  private getSortableElements(): Element[] {
115
+ // flatten so wrapper components can forward their host's children
116
+ // through a nested <slot>
114
117
  const eles = this.shadowRoot
115
118
  .querySelector('slot')
116
- .assignedElements()
119
+ .assignedElements({ flatten: true })
117
120
  .filter(
118
121
  (ele) =>
119
122
  ele.classList.contains('sortable') &&
@@ -539,11 +542,38 @@ export class SortableList extends RapidElement {
539
542
  private beginDrag(
540
543
  target: HTMLElement,
541
544
  clientX: number,
542
- clientY: number
545
+ clientY: number,
546
+ path: EventTarget[] = []
543
547
  ): boolean {
544
- // if we have a drag handle, only allow dragging from that element
548
+ // if we have a drag handle, only allow dragging from that element. The
549
+ // handle may live inside a sortable child's shadow DOM (where event
550
+ // retargeting hides it from event.target), so walk the composed path up
551
+ // to the first .sortable host looking for it — and require that host to
552
+ // actually be one of ours, so a same-named class elsewhere in the path
553
+ // can't start a drag.
545
554
  if (this.dragHandle) {
546
- if (!target.classList.contains(this.dragHandle)) {
555
+ const searchPath = path.length > 0 ? path : [target];
556
+ let onHandle = false;
557
+ let handleHost: Element = null;
558
+ for (const hop of searchPath) {
559
+ // shadow roots appear as non-Element hops mid-path (e.g. an icon
560
+ // inside the handle) — skip them rather than giving up
561
+ if (!(hop instanceof Element)) {
562
+ continue;
563
+ }
564
+ if (!onHandle && hop.classList.contains(this.dragHandle)) {
565
+ onHandle = true;
566
+ }
567
+ if (hop.classList.contains('sortable')) {
568
+ handleHost = onHandle ? hop : null;
569
+ break;
570
+ }
571
+ }
572
+ if (
573
+ !onHandle ||
574
+ !handleHost ||
575
+ !this.getSortableElements().includes(handleHost)
576
+ ) {
547
577
  return false;
548
578
  }
549
579
  }
@@ -572,7 +602,12 @@ export class SortableList extends RapidElement {
572
602
 
573
603
  private handleMouseDown(event: MouseEvent) {
574
604
  if (
575
- this.beginDrag(event.target as HTMLElement, event.clientX, event.clientY)
605
+ this.beginDrag(
606
+ event.target as HTMLElement,
607
+ event.clientX,
608
+ event.clientY,
609
+ event.composedPath()
610
+ )
576
611
  ) {
577
612
  event.preventDefault();
578
613
  event.stopPropagation();
@@ -586,7 +621,12 @@ export class SortableList extends RapidElement {
586
621
  const touch = event.touches[0];
587
622
  if (!touch) return;
588
623
  if (
589
- this.beginDrag(event.target as HTMLElement, touch.clientX, touch.clientY)
624
+ this.beginDrag(
625
+ event.target as HTMLElement,
626
+ touch.clientX,
627
+ touch.clientY,
628
+ event.composedPath()
629
+ )
590
630
  ) {
591
631
  event.stopPropagation();
592
632
  document.addEventListener('touchmove', this.handleTouchMove, {
@@ -690,8 +730,11 @@ export class SortableList extends RapidElement {
690
730
  this.isExternalDrag = true;
691
731
  this.hideDropPlaceholder();
692
732
 
693
- // hide the ghost element when dragging externally
733
+ // hide the ghost element when dragging externally, remembering its
734
+ // display (inlined from the original's computed style) so re-entry
735
+ // can restore it - forcing 'block' would break flex-laid-out ghosts
694
736
  if (this.ghostElement) {
737
+ this.ghostOriginalDisplay = this.ghostElement.style.display;
695
738
  this.ghostElement.style.display = 'none';
696
739
  }
697
740
 
@@ -706,7 +749,8 @@ export class SortableList extends RapidElement {
706
749
 
707
750
  // show the ghost element again when dragging internally
708
751
  if (this.ghostElement) {
709
- this.ghostElement.style.display = 'block';
752
+ this.ghostElement.style.display =
753
+ this.ghostOriginalDisplay || 'block';
710
754
  }
711
755
 
712
756
  this.fireCustomEvent(CustomEventType.DragInternal, {
@@ -220,6 +220,14 @@ export class TembaList extends RapidElement {
220
220
  return this.selected;
221
221
  }
222
222
 
223
+ /** Deselects without firing a change — e.g. mobile going back to the
224
+ * list, so the same row can be selected again. */
225
+ public clearSelection() {
226
+ this.cursorIndex = -1;
227
+ this.selected = null;
228
+ this.value = null;
229
+ }
230
+
223
231
  public refresh(): void {
224
232
  this.refreshKey = 'requested_' + new Date().getTime();
225
233
  }
@@ -33,7 +33,6 @@ export interface MenuItem {
33
33
  avatar?: string;
34
34
  trigger?: boolean;
35
35
  event?: string;
36
- mobile?: boolean;
37
36
  initial?: string;
38
37
  }
39
38
 
@@ -401,29 +400,13 @@ export class TembaMenu extends ResizeElement {
401
400
  flex-direction: row;
402
401
  }
403
402
 
404
- .root.fully-collapsed.mobile .level.level-0 > .item {
403
+ /* when collapsed on mobile, the only thing in the header is the
404
+ hamburger — any new direct child of level-0 must opt in with
405
+ the .top class to stay visible here */
406
+ .root.fully-collapsed.mobile .level.level-0 > *:not(.top) {
405
407
  display: none;
406
408
  }
407
409
 
408
- .root.fully-collapsed.mobile .level.level-0 > .empty {
409
- display: block;
410
- width: 100%;
411
- min-width: inherit;
412
- max-width: inherit;
413
- }
414
-
415
- .root .level.level-0 > .show-mobile {
416
- display: none;
417
- }
418
-
419
- .root.mobile .level.level-0 > .show-mobile {
420
- display: flex;
421
- }
422
-
423
- .root.fully-collapsed.mobile .level.level-0 > .show-mobile {
424
- display: contents !important;
425
- }
426
-
427
410
  .root.fully-collapsed.mobile .level.level-0 .expand-icon {
428
411
  max-height: 100%;
429
412
  padding: 1em;
@@ -455,27 +438,12 @@ export class TembaMenu extends ResizeElement {
455
438
  min-width: inherit;
456
439
  }
457
440
 
458
- .mobile.fully-collapsed .item {
459
- }
460
-
461
441
  .mobile .expand-icon {
462
442
  transition: none;
463
443
  transform: rotate(-90deg);
464
444
  align-self: center;
465
445
  }
466
446
 
467
- .mobile.fully-collapsed .level-0 .empty {
468
- flex-grow: 1;
469
- }
470
-
471
- .mobile.fully-collapsed .top-spacer {
472
- flex-grow: 0;
473
- }
474
-
475
- .mobile.fully-collapsed #dd-workspace {
476
- display: none;
477
- }
478
-
479
447
  .mobile.fully-collapsed .expand-icon {
480
448
  transform: none;
481
449
  }
@@ -1203,8 +1171,7 @@ export class TembaMenu extends ResizeElement {
1203
1171
  expanding: this.expanding && this.expanding === menuItem.id,
1204
1172
  expanded: this.isExpanded(menuItem),
1205
1173
  iconless: !icon && !collapsedIcon && !menuItem.avatar,
1206
- pressed: this.pressedItem && this.pressedItem.id == menuItem.id,
1207
- 'show-mobile': menuItem.mobile
1174
+ pressed: this.pressedItem && this.pressedItem.id == menuItem.id
1208
1175
  });
1209
1176
 
1210
1177
  if (menuItem.avatar) {