@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyaruka/temba-components",
3
- "version": "0.159.0",
3
+ "version": "0.159.2",
4
4
  "description": "Web components to support rapidpro and related projects",
5
5
  "author": "Nyaruka <code@nyaruka.coim>",
6
6
  "main": "dist/index.js",
@@ -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
@@ -160,6 +160,7 @@ export interface Msg {
160
160
  export interface ObjectReference {
161
161
  uuid: string;
162
162
  name: string;
163
+ url?: string;
163
164
  }
164
165
 
165
166
  export interface Shortcut {
@@ -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 ContactEvents extends EndpointMonitorElement {
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 for this contact yet.';
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
- padding: 4em 1em;
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
- opacity: 0.55;
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. read-only badges, not links */
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/events/${encodeURIComponent(this.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/events/${encodeURIComponent(
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/events/${encodeURIComponent(
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">${this.lang_empty}</slot>
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 { ContactEvents } from './src/live/ContactEvents';
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-events', ContactEvents);
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);