@nyaruka/temba-components 0.159.0 → 0.159.2
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 +13 -0
- package/dist/temba-components.js +86 -11
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/events/eventRenderers.ts +29 -0
- package/src/events.ts +22 -0
- package/src/interfaces.ts +1 -0
- package/src/list/ContentList.ts +11 -0
- package/src/live/{ContactEvents.ts → ContactTimeline.ts} +79 -9
- package/temba-modules.ts +2 -2
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { html, TemplateResult } from 'lit';
|
|
2
2
|
import {
|
|
3
|
+
AirtimeCreatedEvent,
|
|
3
4
|
AirtimeTransferredEvent,
|
|
4
5
|
CallEvent,
|
|
5
6
|
ChatStartedEvent,
|
|
@@ -17,6 +18,7 @@ import { getLanguageName } from '../languages';
|
|
|
17
18
|
import { oxfordFn } from '../utils';
|
|
18
19
|
|
|
19
20
|
export enum Events {
|
|
21
|
+
AIRTIME_CREATED = 'airtime_created',
|
|
20
22
|
AIRTIME_TRANSFERRED = 'airtime_transferred',
|
|
21
23
|
BROADCAST_CREATED = 'broadcast_created',
|
|
22
24
|
CALL_CREATED = 'call_created',
|
|
@@ -370,6 +372,30 @@ export const renderAirtimeTransferredEvent = (
|
|
|
370
372
|
</div>`;
|
|
371
373
|
};
|
|
372
374
|
|
|
375
|
+
export const renderAirtimeCreatedEvent = (
|
|
376
|
+
event: AirtimeCreatedEvent
|
|
377
|
+
): TemplateResult => {
|
|
378
|
+
const status = event._status?.status ?? 'created';
|
|
379
|
+
const amount = html`${valueText(event.amount)} ${event.currency}`;
|
|
380
|
+
|
|
381
|
+
switch (status) {
|
|
382
|
+
case 'reversed':
|
|
383
|
+
return html`<div>Airtime transfer reversed</div>`;
|
|
384
|
+
case 'rejected':
|
|
385
|
+
case 'cancelled':
|
|
386
|
+
case 'declined':
|
|
387
|
+
return html`<div>Airtime transfer failed</div>`;
|
|
388
|
+
case 'completed':
|
|
389
|
+
return html`<div style=${eventLineStyle}>
|
|
390
|
+
Transferred ${amount} of airtime
|
|
391
|
+
</div>`;
|
|
392
|
+
default:
|
|
393
|
+
return html`<div style=${eventLineStyle}>
|
|
394
|
+
Sending ${amount} of airtime
|
|
395
|
+
</div>`;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
373
399
|
export const renderContactLanguageChangedEvent = (
|
|
374
400
|
event: ContactLanguageChangedEvent
|
|
375
401
|
): TemplateResult => {
|
|
@@ -560,6 +586,9 @@ export const renderEvent = (
|
|
|
560
586
|
case Events.WARNING:
|
|
561
587
|
content = renderDiagnosticEvent(event, isSimulation);
|
|
562
588
|
break;
|
|
589
|
+
case Events.AIRTIME_CREATED:
|
|
590
|
+
content = renderAirtimeCreatedEvent(event as AirtimeCreatedEvent);
|
|
591
|
+
break;
|
|
563
592
|
case Events.AIRTIME_TRANSFERRED:
|
|
564
593
|
content = renderAirtimeTransferredEvent(event as AirtimeTransferredEvent);
|
|
565
594
|
break;
|
package/src/events.ts
CHANGED
|
@@ -90,6 +90,28 @@ export interface AirtimeTransferredEvent extends ContactEvent {
|
|
|
90
90
|
amount: string;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
export type AirtimeStatus =
|
|
94
|
+
| 'created'
|
|
95
|
+
| 'confirmed'
|
|
96
|
+
| 'rejected'
|
|
97
|
+
| 'cancelled'
|
|
98
|
+
| 'submitted'
|
|
99
|
+
| 'completed'
|
|
100
|
+
| 'reversed'
|
|
101
|
+
| 'declined';
|
|
102
|
+
|
|
103
|
+
export interface AirtimeCreatedEvent extends ContactEvent {
|
|
104
|
+
sender: string;
|
|
105
|
+
recipient: string;
|
|
106
|
+
currency: string;
|
|
107
|
+
amount: string;
|
|
108
|
+
external_id?: string;
|
|
109
|
+
_status?: {
|
|
110
|
+
created_on: string;
|
|
111
|
+
status: AirtimeStatus;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
93
115
|
export type CallStartedEvent = ContactEvent;
|
|
94
116
|
|
|
95
117
|
export interface ContactHistoryPage {
|
package/src/interfaces.ts
CHANGED
package/src/list/ContentList.ts
CHANGED
|
@@ -716,9 +716,20 @@ export class ContentList<T = any> extends RapidElement {
|
|
|
716
716
|
padding: 0 6px 0 0;
|
|
717
717
|
--icon-color: var(--text-3);
|
|
718
718
|
}
|
|
719
|
+
/* Reserve the icon's footprint on the wrapper itself so the
|
|
720
|
+
icon column's intrinsic width is the same whether
|
|
721
|
+
<temba-icon> has upgraded or not — without this, the column
|
|
722
|
+
briefly measures as just the cell's right-padding (6px) and
|
|
723
|
+
the downstream pinned columns end up positioned ~14px to
|
|
724
|
+
the left, which races with whatever moment we snapshot.
|
|
725
|
+
<temba-icon size="1"> renders at 1em, so we reserve 1em
|
|
726
|
+
square and let the icon paint into it. */
|
|
719
727
|
.icon-inner {
|
|
720
728
|
display: flex;
|
|
721
729
|
align-items: center;
|
|
730
|
+
justify-content: center;
|
|
731
|
+
width: 1em;
|
|
732
|
+
height: 1em;
|
|
722
733
|
}
|
|
723
734
|
tr.row.selected .icon-cell {
|
|
724
735
|
--icon-color: var(--accent-700);
|
|
@@ -41,7 +41,7 @@ const BROADCAST_COLOR = '#8e5ea7';
|
|
|
41
41
|
// triggers use the same green as the flow pill
|
|
42
42
|
const TRIGGER_COLOR = '#16a34a';
|
|
43
43
|
|
|
44
|
-
export class
|
|
44
|
+
export class ContactTimeline extends EndpointMonitorElement {
|
|
45
45
|
@property({ type: String })
|
|
46
46
|
contact: string;
|
|
47
47
|
|
|
@@ -64,7 +64,14 @@ export class ContactEvents extends EndpointMonitorElement {
|
|
|
64
64
|
lang_campaigns_label = 'Campaigns';
|
|
65
65
|
|
|
66
66
|
@property({ type: String })
|
|
67
|
-
lang_empty = 'No events
|
|
67
|
+
lang_empty = 'No upcoming events';
|
|
68
|
+
|
|
69
|
+
@property({ type: String })
|
|
70
|
+
lang_empty_help =
|
|
71
|
+
'Events appear here when a contact joins a campaign. Scheduled flows and messages will also show up here.';
|
|
72
|
+
|
|
73
|
+
@property({ type: String })
|
|
74
|
+
lang_campaigns_link = 'View campaigns';
|
|
68
75
|
|
|
69
76
|
@property({ type: String })
|
|
70
77
|
lang_projected_info =
|
|
@@ -101,11 +108,44 @@ export class ContactEvents extends EndpointMonitorElement {
|
|
|
101
108
|
display: block;
|
|
102
109
|
}
|
|
103
110
|
|
|
111
|
+
/* empty state follows the list design system: centered icon, a short
|
|
112
|
+
title, muted explanatory copy, and a single call-to-action link */
|
|
104
113
|
.empty {
|
|
105
|
-
|
|
114
|
+
display: flex;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
align-items: center;
|
|
106
117
|
text-align: center;
|
|
118
|
+
padding: 7em 1em 4em;
|
|
107
119
|
color: var(--text-color);
|
|
108
|
-
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.empty temba-icon {
|
|
123
|
+
margin-bottom: 0.75em;
|
|
124
|
+
--icon-color: var(--text-3, #7b8593);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.empty-title {
|
|
128
|
+
font-weight: 600;
|
|
129
|
+
margin-bottom: 0.4em;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.empty-help {
|
|
133
|
+
font-size: 0.875em;
|
|
134
|
+
line-height: 1.5;
|
|
135
|
+
max-width: 22em;
|
|
136
|
+
margin-bottom: 1em;
|
|
137
|
+
color: var(--text-3, #7b8593);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.empty-link {
|
|
141
|
+
font-size: 0.875em;
|
|
142
|
+
font-weight: 500;
|
|
143
|
+
color: var(--color-link-primary);
|
|
144
|
+
text-decoration: none;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.empty-link:hover {
|
|
148
|
+
text-decoration: underline;
|
|
109
149
|
}
|
|
110
150
|
|
|
111
151
|
/* row of campaign pills the contact is currently a member of */
|
|
@@ -128,7 +168,8 @@ export class ContactEvents extends EndpointMonitorElement {
|
|
|
128
168
|
}
|
|
129
169
|
|
|
130
170
|
/* each pill is colored with its campaign's hue - background, border
|
|
131
|
-
and text all derived from --pill-hue.
|
|
171
|
+
and text all derived from --pill-hue. clickable links to the
|
|
172
|
+
campaign's read page */
|
|
132
173
|
.campaign-pill {
|
|
133
174
|
display: inline-flex;
|
|
134
175
|
align-items: center;
|
|
@@ -146,6 +187,21 @@ export class ContactEvents extends EndpointMonitorElement {
|
|
|
146
187
|
border: 1px solid
|
|
147
188
|
color-mix(in srgb, var(--pill-hue) 25%, var(--color-widget-bg, #fff));
|
|
148
189
|
color: var(--pill-hue);
|
|
190
|
+
cursor: pointer;
|
|
191
|
+
transition: background 100ms ease-in-out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.campaign-pill:hover {
|
|
195
|
+
background: color-mix(
|
|
196
|
+
in srgb,
|
|
197
|
+
var(--pill-hue) 22%,
|
|
198
|
+
var(--color-widget-bg, #fff)
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.campaign-pill:focus-visible {
|
|
203
|
+
outline: 2px solid var(--pill-hue);
|
|
204
|
+
outline-offset: 1px;
|
|
149
205
|
}
|
|
150
206
|
|
|
151
207
|
/* status-badge dot leading each campaign pill, in the same hue */
|
|
@@ -424,7 +480,7 @@ export class ContactEvents extends EndpointMonitorElement {
|
|
|
424
480
|
const requestedContact = this.contact;
|
|
425
481
|
try {
|
|
426
482
|
const response = await this.store.getUrl(
|
|
427
|
-
`/contact/
|
|
483
|
+
`/contact/timeline/${encodeURIComponent(this.contact)}/`,
|
|
428
484
|
{ force: true }
|
|
429
485
|
);
|
|
430
486
|
if (this.contact !== requestedContact) {
|
|
@@ -570,7 +626,7 @@ export class ContactEvents extends EndpointMonitorElement {
|
|
|
570
626
|
// capture the contact at request time so a paged response that returns
|
|
571
627
|
// after the user has switched contacts can't append onto the new timeline
|
|
572
628
|
const requestedContact = this.contact;
|
|
573
|
-
const url = `/contact/
|
|
629
|
+
const url = `/contact/timeline/${encodeURIComponent(
|
|
574
630
|
this.contact
|
|
575
631
|
)}/?before=${encodeURIComponent(this.nextBefore)}`;
|
|
576
632
|
|
|
@@ -609,7 +665,7 @@ export class ContactEvents extends EndpointMonitorElement {
|
|
|
609
665
|
|
|
610
666
|
this.loadingMoreFuture = true;
|
|
611
667
|
const requestedContact = this.contact;
|
|
612
|
-
const url = `/contact/
|
|
668
|
+
const url = `/contact/timeline/${encodeURIComponent(
|
|
613
669
|
this.contact
|
|
614
670
|
)}/?after=${encodeURIComponent(this.nextAfter)}`;
|
|
615
671
|
|
|
@@ -710,6 +766,13 @@ export class ContactEvents extends EndpointMonitorElement {
|
|
|
710
766
|
html`<div
|
|
711
767
|
class="campaign-pill"
|
|
712
768
|
style="--pill-hue:${this.getCampaignColor(campaign.uuid)}"
|
|
769
|
+
role="button"
|
|
770
|
+
tabindex="0"
|
|
771
|
+
@click=${(e: Event) => this.handlePillClicked(e, campaign)}
|
|
772
|
+
@keydown=${(e: KeyboardEvent) =>
|
|
773
|
+
this.handleActivationKey(e, () =>
|
|
774
|
+
this.handlePillClicked(e, campaign)
|
|
775
|
+
)}
|
|
713
776
|
>
|
|
714
777
|
<span class="campaign-dot"></span>${campaign.name}
|
|
715
778
|
</div>`
|
|
@@ -742,7 +805,14 @@ export class ContactEvents extends EndpointMonitorElement {
|
|
|
742
805
|
pastDescending.length === 0
|
|
743
806
|
) {
|
|
744
807
|
return html`<div class="empty">
|
|
745
|
-
<slot name="empty"
|
|
808
|
+
<slot name="empty">
|
|
809
|
+
<temba-icon name=${Icon.schedule} size="2"></temba-icon>
|
|
810
|
+
<div class="empty-title">${this.lang_empty}</div>
|
|
811
|
+
<div class="empty-help">${this.lang_empty_help}</div>
|
|
812
|
+
<a class="empty-link" href="/campaign/" onclick="goto(event)"
|
|
813
|
+
>${this.lang_campaigns_link}</a
|
|
814
|
+
>
|
|
815
|
+
</slot>
|
|
746
816
|
</div>`;
|
|
747
817
|
}
|
|
748
818
|
|
package/temba-modules.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { ContactFields } from './src/live/ContactFields';
|
|
|
31
31
|
import { ContactFieldEditor } from './src/live/ContactFieldEditor';
|
|
32
32
|
|
|
33
33
|
import { ContactBadges } from './src/live/ContactBadges';
|
|
34
|
-
import {
|
|
34
|
+
import { ContactTimeline } from './src/live/ContactTimeline';
|
|
35
35
|
import { TembaSlider } from './src/form/TembaSlider';
|
|
36
36
|
import { RunList } from './src/list/RunList';
|
|
37
37
|
import { FlowStoreElement } from './src/store/FlowStoreElement';
|
|
@@ -149,7 +149,7 @@ addCustomElement('temba-dropdown', Dropdown);
|
|
|
149
149
|
addCustomElement('temba-tabs', TabPane);
|
|
150
150
|
addCustomElement('temba-tab', Tab);
|
|
151
151
|
addCustomElement('temba-contact-badges', ContactBadges);
|
|
152
|
-
addCustomElement('temba-contact-
|
|
152
|
+
addCustomElement('temba-contact-timeline', ContactTimeline);
|
|
153
153
|
addCustomElement('temba-slider', TembaSlider);
|
|
154
154
|
addCustomElement('temba-content-menu', ContentMenu);
|
|
155
155
|
addCustomElement('temba-compose', Compose);
|