@nyaruka/temba-components 0.35.0 → 0.35.1

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 (48) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/{30636513.js → ba7fe497.js} +63 -57
  3. package/dist/index.js +63 -57
  4. package/dist/sw.js +1 -1
  5. package/dist/sw.js.map +1 -1
  6. package/dist/templates/components-body.html +1 -1
  7. package/dist/templates/components-head.html +1 -1
  8. package/out-tsc/src/contacts/ContactPending.js +30 -35
  9. package/out-tsc/src/contacts/ContactPending.js.map +1 -1
  10. package/out-tsc/src/contacts/events.js +24 -10
  11. package/out-tsc/src/contacts/events.js.map +1 -1
  12. package/out-tsc/src/date/TembaDate.js +37 -11
  13. package/out-tsc/src/date/TembaDate.js.map +1 -1
  14. package/out-tsc/src/list/RunList.js +17 -16
  15. package/out-tsc/src/list/RunList.js.map +1 -1
  16. package/out-tsc/src/store/Store.js +12 -26
  17. package/out-tsc/src/store/Store.js.map +1 -1
  18. package/out-tsc/src/vectoricon/index.js +3 -0
  19. package/out-tsc/src/vectoricon/index.js.map +1 -1
  20. package/out-tsc/test/temba-contact-history.test.js +5 -8
  21. package/out-tsc/test/temba-contact-history.test.js.map +1 -1
  22. package/out-tsc/test/temba-date.test.js +3 -9
  23. package/out-tsc/test/temba-date.test.js.map +1 -1
  24. package/out-tsc/test/utils.test.js +9 -0
  25. package/out-tsc/test/utils.test.js.map +1 -1
  26. package/package.json +1 -1
  27. package/screenshots/truth/contacts/contact-active-default.png +0 -0
  28. package/screenshots/truth/contacts/contact-active-show-chat-history.png +0 -0
  29. package/screenshots/truth/contacts/contact-active-show-chat-msg.png +0 -0
  30. package/screenshots/truth/contacts/contact-archived-hide-chat-msg.png +0 -0
  31. package/screenshots/truth/contacts/contact-archived-show-chat-history.png +0 -0
  32. package/screenshots/truth/contacts/contact-blocked-hide-chat-msg.png +0 -0
  33. package/screenshots/truth/contacts/contact-blocked-show-chat-history.png +0 -0
  34. package/screenshots/truth/contacts/contact-stopped-hide-chat-msg.png +0 -0
  35. package/screenshots/truth/contacts/contact-stopped-show-chat-history.png +0 -0
  36. package/screenshots/truth/contacts/fields-updated.png +0 -0
  37. package/screenshots/truth/contacts/history-expanded.png +0 -0
  38. package/screenshots/truth/contacts/history.png +0 -0
  39. package/screenshots/truth/date/duration.png +0 -0
  40. package/src/contacts/ContactPending.ts +38 -34
  41. package/src/contacts/events.ts +24 -10
  42. package/src/date/TembaDate.ts +40 -11
  43. package/src/list/RunList.ts +17 -28
  44. package/src/store/Store.ts +13 -37
  45. package/src/vectoricon/index.ts +3 -0
  46. package/test/temba-contact-history.test.ts +7 -7
  47. package/test/temba-date.test.ts +3 -8
  48. package/test/utils.test.ts +10 -1
Binary file
@@ -1,6 +1,10 @@
1
1
  import { css, html, PropertyValueMap, TemplateResult } from 'lit';
2
2
  import { property } from 'lit/decorators';
3
- import { ScheduledEvent, ScheduledEventType } from '../interfaces';
3
+ import {
4
+ CustomEventType,
5
+ ScheduledEvent,
6
+ ScheduledEventType,
7
+ } from '../interfaces';
4
8
  import { StoreElement } from '../store/StoreElement';
5
9
  import { Icon } from '../vectoricon';
6
10
 
@@ -92,6 +96,12 @@ export class ContactPending extends StoreElement {
92
96
  0 0 0px 1px rgba(0, 0, 0, 0.02);
93
97
  }
94
98
 
99
+ .event:hover {
100
+ cursor: pointer;
101
+ box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.055),
102
+ 0 0 0px 2px var(--color-link-primary);
103
+ }
104
+
95
105
  .time {
96
106
  white-space: nowrap;
97
107
  background: rgba(0, 0, 0, 0.02);
@@ -128,18 +138,17 @@ export class ContactPending extends StoreElement {
128
138
  margin-right: 0.25em;
129
139
  }
130
140
 
131
- .campaign_event .scheduled-by:hover {
132
- color: var(--color-link-primary);
133
- --icon-color: var(--color-link-primary);
134
- cursor: pointer;
135
- }
136
-
137
141
  .scheduled-by .name {
138
142
  flex-grow: 1;
139
143
  }
140
144
  `;
141
145
  }
142
146
 
147
+ constructor() {
148
+ super();
149
+ this.handleEventClicked = this.handleEventClicked.bind(this);
150
+ }
151
+
143
152
  protected updated(
144
153
  changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
145
154
  ): void {
@@ -153,52 +162,47 @@ export class ContactPending extends StoreElement {
153
162
  }
154
163
  }
155
164
 
156
- public renderEvent(event: ScheduledEvent) {
165
+ public handleEventClicked(event: ScheduledEvent) {
166
+ this.fireCustomEvent(CustomEventType.Selection, event);
167
+ }
168
+
169
+ public renderEvent(scheduledEvent: ScheduledEvent) {
157
170
  return html`
158
- <div class="event ${event.type}">
171
+ <div
172
+ class="event ${scheduledEvent.type}"
173
+ @click="${() => this.handleEventClicked(scheduledEvent)}"
174
+ >
159
175
  <div class="type">
160
176
  <temba-icon
161
177
  size="2"
162
- name="${event.message ? Icon.message : Icon.flow}"
178
+ name="${scheduledEvent.message ? Icon.message : Icon.flow}"
163
179
  ></temba-icon>
164
180
  </div>
165
181
 
166
182
  <div class="details">
167
183
  <div>
168
- ${event.flow
169
- ? html`
170
- <div
171
- class="flow linked"
172
- href="/flow/editor/${event.flow.uuid}/"
173
- onclick="goto(event)"
174
- >
175
- ${event.flow.name}
176
- </div>
177
- `
184
+ ${scheduledEvent.flow
185
+ ? html` Start ${scheduledEvent.flow.name}`
178
186
  : null}
179
- ${event.message
180
- ? html` <div class="message">${event.message}</div> `
187
+ ${scheduledEvent.message
188
+ ? html` <div class="message">${scheduledEvent.message}</div> `
181
189
  : null}
182
190
  </div>
183
191
 
184
192
  <div class="scheduled-by">
185
- ${event.campaign
186
- ? html`<div
187
- style="display:flex"
188
- href="/campaign/read/${event.campaign.uuid}/"
189
- onclick="goto(event, this)"
190
- >
193
+ ${scheduledEvent.campaign
194
+ ? html`<div style="display:flex">
191
195
  <temba-icon name="${Icon.campaign}"></temba-icon>
192
- <div class="name">${event.campaign.name}</div>
196
+ <div class="name">${scheduledEvent.campaign.name}</div>
193
197
  </div>`
194
198
  : html`
195
- ${event.type === ScheduledEventType.ScheduledTrigger
199
+ ${scheduledEvent.type === ScheduledEventType.ScheduledTrigger
196
200
  ? html`<temba-icon
197
- name="${ICONS[event.type]}"
201
+ name="${ICONS[scheduledEvent.type]}"
198
202
  ></temba-icon>`
199
203
  : null}
200
204
  <div class="name">
201
- ${this.REPEAT_PERIOD[event.repeat_period]}
205
+ ${this.REPEAT_PERIOD[scheduledEvent.repeat_period]}
202
206
  </div>
203
207
  `}
204
208
  </div>
@@ -207,10 +211,10 @@ export class ContactPending extends StoreElement {
207
211
  <div class="time">
208
212
  <div class="duration">
209
213
  <temba-tip
210
- text=${this.store.formatDate(event.scheduled)}
214
+ text=${this.store.formatDate(scheduledEvent.scheduled)}
211
215
  position="left"
212
216
  >
213
- ${this.store.getShortDurationFromIso(event.scheduled)}
217
+ ${this.store.getShortDurationFromIso(scheduledEvent.scheduled)}
214
218
  </temba-tip>
215
219
  </div>
216
220
  </div>
@@ -239,8 +239,8 @@ export const getEventStyles = () => {
239
239
  .tickets .note-summary {
240
240
  display: flex;
241
241
  flex-direction: row;
242
- line-height: 0.5;
243
- font-size: 80%;
242
+ font-size: 85%;
243
+ margin-top: -0.5em;
244
244
  color: rgba(0, 0, 0, 0.6);
245
245
  padding: 8px 3px;
246
246
  }
@@ -310,12 +310,11 @@ export const getEventStyles = () => {
310
310
 
311
311
  .msg-summary {
312
312
  display: flex;
313
- line-height: 0.5;
314
-
315
- font-size: 80%;
313
+ font-size: 85%;
316
314
  color: rgba(0, 0, 0, 0.6);
317
315
  padding: 6px 3px;
318
316
  margin-bottom: 0.5em;
317
+ margin-top: -0.5em;
319
318
  }
320
319
 
321
320
  .msg-summary temba-icon.log {
@@ -776,7 +775,11 @@ export const renderMsgEvent = (
776
775
  <div class="separator">•</div>`);
777
776
  }
778
777
  summary.push(
779
- html`<div class="time">${timeSince(new Date(event.created_on))}</div>`
778
+ html`<temba-date
779
+ class="time"
780
+ value="${event.created_on}"
781
+ display="duration"
782
+ ></temba-date>`
780
783
  );
781
784
 
782
785
  return html`<div style="display:flex;align-items:flex-start">
@@ -938,7 +941,11 @@ export const renderNoteCreated = (
938
941
  <div class="description">${event.note}</div>
939
942
  <div class="note-summary">
940
943
  <div style="flex-grow:1"></div>
941
- <div class="time">${timeSince(new Date(event.created_on))}</div>
944
+ <temba-date
945
+ class="time"
946
+ value="${event.created_on}"
947
+ display="duration"
948
+ ></temba-date>
942
949
  </div>
943
950
  </div>
944
951
  <div style="margin-left:0.8em;margin-top:0.3em;font-size:0.8em">
@@ -986,14 +993,17 @@ export const renderTicketAction = (
986
993
  ${getDisplayName(event.created_by)} ${action} this ticket
987
994
  </div>
988
995
  <div class="subtext" style="justify-content:center">
989
- ${timeSince(reopened, { hideRecentText: true, suffix: ' ago' })}
996
+ <temba-date
997
+ class="time"
998
+ value="${reopened}"
999
+ display="duration"
1000
+ ></temba-date>
990
1001
  </div>
991
1002
  </div>
992
1003
  `;
993
1004
  };
994
1005
 
995
1006
  export const renderTicketAssigned = (event: TicketEvent): TemplateResult => {
996
- const created = new Date(event.created_on);
997
1007
  return html`
998
1008
  <div class="assigned active">
999
1009
  <div style="text-align:center">
@@ -1005,7 +1015,11 @@ export const renderTicketAssigned = (event: TicketEvent): TemplateResult => {
1005
1015
  : html`${getDisplayName(event.created_by)} unassigned this ticket`}
1006
1016
  </div>
1007
1017
  <div class="subtext" style="justify-content:center">
1008
- ${timeSince(created, { hideRecentText: true, suffix: ' ago' })}
1018
+ <temba-date
1019
+ class="time"
1020
+ value="${event.created_on}"
1021
+ display="duration"
1022
+ ></temba-date>
1009
1023
  </div>
1010
1024
  </div>
1011
1025
  `;
@@ -7,7 +7,11 @@ import { DateTime } from 'luxon';
7
7
  export const Display = {
8
8
  date: DateTime.DATE_SHORT,
9
9
  datetime: DateTime.DATETIME_SHORT,
10
+ time: DateTime.TIME_SIMPLE,
11
+ timedate: 'timedate',
10
12
  duration: 'duration',
13
+ relative: 'relative',
14
+ day: 'LLL d',
11
15
  };
12
16
 
13
17
  export class TembaDate extends RapidElement {
@@ -48,19 +52,44 @@ export class TembaDate extends RapidElement {
48
52
  }
49
53
 
50
54
  public render(): TemplateResult {
51
- if (this.datetime) {
52
- if (this.display === Display.duration) {
53
- return html`<div class="date">
54
- ${this.store.getShortDuration(this.datetime)}
55
- </div>`;
55
+ if (this.datetime && this.store) {
56
+ this.datetime.setLocale(this.store.getLocale());
57
+
58
+ let formatted = '';
59
+ if (this.display === Display.timedate) {
60
+ const hours = Math.abs(
61
+ this.datetime.diffNow().milliseconds / 1000 / 60 / 60
62
+ );
63
+ if (hours < 24) {
64
+ formatted = this.datetime.toLocaleString(Display.time);
65
+ } else if (hours < 24 * 365) {
66
+ formatted = this.datetime.toFormat(Display.day);
67
+ } else {
68
+ formatted = this.datetime.toLocaleString(Display.date);
69
+ }
70
+ } else if (this.display === Display.relative) {
71
+ const minutes = Math.abs(
72
+ this.datetime.diffNow().milliseconds / 1000 / 60
73
+ );
74
+ if (minutes < 1) {
75
+ return html`<div class="date">just now</div>`;
76
+ }
77
+
78
+ formatted = this.store.getShortDuration(this.datetime);
79
+ } else if (this.display === Display.duration) {
80
+ const minutes = Math.abs(
81
+ this.datetime.diffNow().milliseconds / 1000 / 60
82
+ );
83
+ if (minutes < 1) {
84
+ return html`<div class="date">just now</div>`;
85
+ }
86
+ formatted = this.store.getShortDuration(this.datetime);
87
+ } else if (this.display === Display.day) {
88
+ formatted = this.datetime.toLocaleString(Display.day);
56
89
  } else {
57
- return html`
58
- <div class="date">
59
- ${this.datetime.toLocaleString(Display[this.display])}
60
- </div>
61
- `;
90
+ formatted = this.datetime.toLocaleString(Display[this.display]);
62
91
  }
92
+ return html`<div class="date">${formatted}</div>`;
63
93
  }
64
- return null;
65
94
  }
66
95
  }
@@ -141,7 +141,7 @@ export class RunList extends TembaList {
141
141
  </div>
142
142
 
143
143
  <div style="flex-shrink:1">
144
- ${this.store.getShortDurationFromIso(run.modified_on)}
144
+ <temba-date value="${run.modified_on}" display="duration" />
145
145
  </div>
146
146
  ${this.getIcon(run)}
147
147
  </div>
@@ -227,7 +227,6 @@ export class RunList extends TembaList {
227
227
  return null;
228
228
  }
229
229
 
230
- const exitType = this.selectedRun.exit_type;
231
230
  const resultKeys = Object.keys(this.selectedRun.values);
232
231
 
233
232
  return html` <div
@@ -251,35 +250,25 @@ export class RunList extends TembaList {
251
250
  >
252
251
  ${this.selectedRun.exit_type
253
252
  ? html`
254
- ${this.getIcon(this.selectedRun)}
255
- <div style="margin-left:0.5em;flex-grow:1">
256
- ${capitalize(this.selectedRun.exit_type)}
257
- ${exitType == 'completed'
258
- ? html` in
259
- ${this.store.getShortDurationFromIso(
260
- this.selectedRun.created_on,
261
- this.selectedRun.exited_on,
262
- true
263
- )}`
264
- : null}
265
- ${exitType == 'interrupted' || exitType == 'expired'
266
- ? html` after
267
- ${this.store.getShortDurationFromIso(
268
- this.selectedRun.created_on,
269
- this.selectedRun.exited_on,
270
- true
271
- )}`
272
- : null}
253
+ <div style="margin-left:2em;flex-grow:1;display:flex">
254
+ ${this.getIcon(this.selectedRun)}
255
+ <div style="margin-left:0.5em">
256
+ ${capitalize(this.selectedRun.exit_type)}&nbsp;
257
+ </div>
258
+ <temba-date
259
+ value="${this.selectedRun.exited_on}"
260
+ compare="${this.selectedRun.created_on}"
261
+ display="duration"
262
+ />
273
263
  </div>
274
264
  `
275
265
  : html`${this.getIcon(this.selectedRun)}
276
- <div style="margin-left:0.5em;flex-grow:1">
277
- Active for
278
- ${this.store.getShortDurationFromIso(
279
- this.selectedRun.created_on,
280
- null,
281
- true
282
- )}
266
+ <div style="margin-left:1.5em;flex-grow:1;display:flex">
267
+ <div>Started&nbsp;</div>
268
+ <temba-date
269
+ value="${this.selectedRun.created_on}"
270
+ display="duration"
271
+ />
283
272
  </div>`}
284
273
  </div>
285
274
  </div>
@@ -74,6 +74,10 @@ export class Store extends RapidElement {
74
74
 
75
75
  private cache: any;
76
76
 
77
+ public getLocale() {
78
+ return this.locale[0];
79
+ }
80
+
77
81
  public reset() {
78
82
  this.cache = Lru(this.max, this.ttl);
79
83
 
@@ -192,41 +196,17 @@ export class Store extends RapidElement {
192
196
  });
193
197
  }
194
198
 
195
- public getShortDuration(
196
- scheduled: DateTime,
197
- compareDate: DateTime = null,
198
- showSeconds = false
199
- ) {
199
+ public getShortDuration(scheduled: DateTime, compareDate: DateTime = null) {
200
200
  const now = compareDate || DateTime.now();
201
- const duration = scheduled.diff(now).valueOf();
202
-
203
- if (showSeconds) {
204
- return this.humanizer.humanize(duration, {
205
- language: this.getLanguageCode(),
206
- largest: 1,
207
- round: true,
208
- });
209
- }
210
-
211
- if (Math.abs(duration) < 60000) {
212
- return 'just now';
213
- }
214
-
215
- return this.humanizer.humanize(duration, {
216
- language: this.getLanguageCode(),
217
- largest: 1,
218
- round: false,
219
- });
201
+ return scheduled
202
+ .setLocale(this.locale[0])
203
+ .toRelative({ base: now, style: 'long' });
220
204
  }
221
205
 
222
- public getShortDurationFromIso(
223
- isoDateA: string,
224
- isoDateB: string = null,
225
- showSeconds = false
226
- ) {
206
+ public getShortDurationFromIso(isoDateA: string, isoDateB: string = null) {
227
207
  const scheduled = DateTime.fromISO(isoDateA);
228
208
  const now = isoDateB ? DateTime.fromISO(isoDateB) : DateTime.now();
229
- return this.getShortDuration(scheduled, now, showSeconds);
209
+ return this.getShortDuration(scheduled, now);
230
210
  }
231
211
 
232
212
  public setKeyedAssets(name: string, values: string[]): void {
@@ -278,13 +258,9 @@ export class Store extends RapidElement {
278
258
  }
279
259
 
280
260
  public formatDate(dateString: string) {
281
- return new Date(dateString).toLocaleString(this.locale, {
282
- year: 'numeric',
283
- month: 'long',
284
- day: 'numeric',
285
- hour: 'numeric',
286
- minute: 'numeric',
287
- });
261
+ return DateTime.fromISO(dateString)
262
+ .setLocale(this.getLocale())
263
+ .toLocaleString(DateTime.DATETIME_SHORT);
288
264
  }
289
265
 
290
266
  public postUrl(
@@ -1,4 +1,5 @@
1
1
  export enum Icon {
2
+ analytics = 'bar-chart-01',
2
3
  account = 'user-01',
3
4
  active = 'play',
4
5
  add_note = 'file-02',
@@ -55,9 +56,11 @@ export enum Icon {
55
56
  org_new = 'stars-02',
56
57
  org_suspended = 'slash-circle-01',
57
58
  org_verified = 'check-verified-02',
59
+ overview = 'pie-chart-01',
58
60
  featured = 'star-01',
59
61
  resthooks = 'share-07',
60
62
  restore = 'play',
63
+ runs = 'rows-03',
61
64
  search = 'search-refraction',
62
65
  select_open = 'chevron-down',
63
66
  select_clear = 'x',
@@ -1,12 +1,12 @@
1
1
  import { fixture, assert, expect } from '@open-wc/testing';
2
- import * as sinon from 'sinon';
3
2
  import { ContactHistory } from '../src/contacts/ContactHistory';
4
- import { stubbable } from '../src/utils';
5
3
  import {
6
4
  assertScreenshot,
7
5
  getClip,
8
6
  getHTML,
7
+ loadStore,
9
8
  mockGET,
9
+ mockNow,
10
10
  } from '../test/utils.test';
11
11
  import './utils.test';
12
12
 
@@ -41,12 +41,10 @@ const getHistoryClip = (ele: ContactHistory) => {
41
41
  };
42
42
 
43
43
  // stub our current date for consistent screenshots
44
- sinon.stub(stubbable, 'getCurrentDate').callsFake(() => {
45
- return new Date('2021-03-31T00:00:00.000-00:00');
46
- });
44
+ mockNow('2021-03-31T00:31:00.000-00:00');
47
45
 
48
46
  describe('temba-contact-history', () => {
49
- beforeEach(() => {
47
+ beforeEach(async () => {
50
48
  mockGET(
51
49
  /\/contact\/history\/contact-dave-active\/.*/,
52
50
  '/test-assets/contacts/history.json'
@@ -56,6 +54,8 @@ describe('temba-contact-history', () => {
56
54
  /\/api\/v2\/tickets\.json\?contact=contact-dave-active/,
57
55
  '/test-assets/api/tickets.json'
58
56
  );
57
+
58
+ await loadStore();
59
59
  });
60
60
 
61
61
  it('can be created', async () => {
@@ -76,7 +76,7 @@ describe('temba-contact-history', () => {
76
76
  const events = history.shadowRoot.querySelector('.events');
77
77
  const top = events.scrollHeight - events.getBoundingClientRect().height;
78
78
 
79
- expect(top).to.equal(520);
79
+ expect(top).to.equal(549);
80
80
 
81
81
  // make sure we actually scrolled to there
82
82
  expect(events.scrollTop).to.equal(top);
@@ -1,12 +1,11 @@
1
- import * as sinon from 'sinon';
2
1
  import { TembaDate } from '../src/date/TembaDate';
3
2
  import {
4
3
  assertScreenshot,
5
4
  getClip,
6
5
  getComponent,
7
6
  loadStore,
7
+ mockNow,
8
8
  } from './utils.test';
9
- import { DateTime } from 'luxon';
10
9
  import { expect } from '@open-wc/testing';
11
10
 
12
11
  const TAG = 'temba-date';
@@ -16,11 +15,7 @@ export const getDate = async (attrs: any = {}) => {
16
15
  return (await getComponent(TAG, attrs)) as TembaDate;
17
16
  };
18
17
 
19
- // mock the current time
20
- const now = DateTime.fromISO('2022-12-02T21:00:00.000000-07:00');
21
- sinon.replace(DateTime, 'now', () => {
22
- return now;
23
- });
18
+ mockNow('2022-12-02T21:00:00.000000-07:00');
24
19
 
25
20
  describe('temba-date', () => {
26
21
  beforeEach(() => {
@@ -47,7 +42,7 @@ describe('temba-date', () => {
47
42
  ).innerText;
48
43
 
49
44
  await assertScreenshot('date/duration', getClip(date));
50
- expect(dateString).to.equal('44 years');
45
+ expect(dateString).to.equal('44 years ago');
51
46
  });
52
47
 
53
48
  it('renders datetime', async () => {
@@ -1,5 +1,6 @@
1
1
  import '../temba-modules';
2
-
2
+ import { DateTime } from 'luxon';
3
+ import * as sinon from 'sinon';
3
4
  interface Clip {
4
5
  x: number;
5
6
  y: number;
@@ -228,3 +229,11 @@ export const loadStore = async () => {
228
229
 
229
230
  return store;
230
231
  };
232
+
233
+ export const mockNow = (isodate: string) => {
234
+ const now = DateTime.fromISO(isodate);
235
+ // mock the current time
236
+ sinon.replace(DateTime, 'now', () => {
237
+ return now;
238
+ });
239
+ };