@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.
- package/CHANGELOG.md +27 -0
- package/DEV_DATA.md +11 -11
- package/README.md +4 -4
- package/TEST_OPTIMIZATION.md +8 -11
- package/dist/locales/es.js +4 -0
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +4 -0
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/pt.js +4 -0
- package/dist/locales/pt.js.map +1 -1
- package/dist/static/svg/index.svg +1 -1
- package/dist/temba-components.js +5266 -3031
- package/dist/temba-components.js.map +1 -1
- package/orca/setup.sh +2 -2
- package/package.json +13 -18
- package/scripts/dev-data-sync.mjs +4 -4
- package/src/Icons.ts +3 -2
- package/src/display/Chat.ts +3 -2
- package/src/display/Lightbox.ts +57 -109
- package/src/display/ProgressBar.ts +15 -2
- package/src/display/Thumbnail.ts +34 -7
- package/src/form/Compose.ts +31 -1
- package/src/form/ContactSearch.ts +229 -126
- package/src/interfaces.ts +131 -2
- package/src/layout/Card.ts +336 -0
- package/src/layout/CardLayout.ts +425 -0
- package/src/layout/CardStack.ts +131 -0
- package/src/layout/Dialog.ts +33 -24
- package/src/layout/HeaderBar.ts +41 -0
- package/src/layout/PageHeader.ts +5 -6
- package/src/layout/Resizer.ts +20 -0
- package/src/list/BroadcastList.ts +912 -0
- package/src/list/CampaignList.ts +144 -0
- package/src/list/ContactList.ts +3 -1
- package/src/list/ContentList.ts +46 -10
- package/src/list/FieldList.ts +1057 -0
- package/src/list/FlowList.ts +31 -7
- package/src/list/MsgList.ts +18 -12
- package/src/list/SortableList.ts +52 -8
- package/src/list/TembaList.ts +8 -0
- package/src/list/TembaMenu.ts +5 -38
- package/src/list/TriggerList.ts +538 -0
- package/src/live/CampaignEvents.ts +1108 -0
- package/src/live/ContactChat.ts +7 -1
- package/src/live/ContactDetails.ts +20 -14
- package/src/live/ContactFieldEditor.ts +7 -3
- package/src/live/ContactFields.ts +1 -1
- package/src/live/ContactNameFetch.ts +5 -2
- package/src/live/ContactNotepad.ts +88 -2
- package/src/live/ContactStoreElement.ts +15 -3
- package/src/live/ContactTimeline.ts +28 -7
- package/src/locales/es.ts +4 -0
- package/src/locales/fr.ts +4 -0
- package/src/locales/pt.ts +4 -0
- package/src/utils.ts +19 -0
- package/static/svg/index.svg +1 -1
- package/static/svg/packs/2-temba/star-filled.svg +3 -0
- package/static/svg/work/traced/star-filled.svg +1 -0
- package/static/svg/work/used/star-filled.svg +3 -0
- package/stress-test.js +3 -3
- package/temba-modules.ts +18 -0
- package/web-dev-server.config.mjs +54 -0
- package/web-test-runner.config.mjs +1 -1
- package/xliff/es.xlf +12 -0
- package/xliff/fr.xlf +12 -0
- package/xliff/pt.xlf +12 -0
package/src/list/FlowList.ts
CHANGED
|
@@ -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:
|
|
36
|
+
display: flex;
|
|
29
37
|
align-items: center;
|
|
30
38
|
gap: 4px;
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
package/src/list/MsgList.ts
CHANGED
|
@@ -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
|
|
18
|
-
|
|
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
|
|
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
|
|
164
|
-
*
|
|
165
|
-
* the
|
|
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
|
-
|
|
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,
|
|
212
|
-
*
|
|
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
|
)}
|
package/src/list/SortableList.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
752
|
+
this.ghostElement.style.display =
|
|
753
|
+
this.ghostOriginalDisplay || 'block';
|
|
710
754
|
}
|
|
711
755
|
|
|
712
756
|
this.fireCustomEvent(CustomEventType.DragInternal, {
|
package/src/list/TembaList.ts
CHANGED
|
@@ -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
|
}
|
package/src/list/TembaMenu.ts
CHANGED
|
@@ -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
|
-
|
|
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) {
|