@nyaruka/temba-components 0.26.9 → 0.27.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 (71) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/{d0cc86be.js → e7b04ba3.js} +686 -420
  3. package/dist/index.js +686 -420
  4. package/dist/static/icons/symbol-defs.svg +35 -4
  5. package/dist/sw.js +1 -1
  6. package/dist/sw.js.map +1 -1
  7. package/dist/templates/components-body.html +1 -1
  8. package/dist/templates/components-head.html +1 -1
  9. package/out-tsc/src/contacts/ContactBadges.js +97 -0
  10. package/out-tsc/src/contacts/ContactBadges.js.map +1 -0
  11. package/out-tsc/src/contacts/ContactFieldEditor.js +53 -41
  12. package/out-tsc/src/contacts/ContactFieldEditor.js.map +1 -1
  13. package/out-tsc/src/contacts/ContactFields.js +91 -33
  14. package/out-tsc/src/contacts/ContactFields.js.map +1 -1
  15. package/out-tsc/src/contacts/ContactPending.js +238 -0
  16. package/out-tsc/src/contacts/ContactPending.js.map +1 -0
  17. package/out-tsc/src/contacts/ContactStoreElement.js +3 -0
  18. package/out-tsc/src/contacts/ContactStoreElement.js.map +1 -1
  19. package/out-tsc/src/contacts/events.js +1 -1
  20. package/out-tsc/src/contacts/events.js.map +1 -1
  21. package/out-tsc/src/interfaces.js +12 -0
  22. package/out-tsc/src/interfaces.js.map +1 -1
  23. package/out-tsc/src/label/Label.js +25 -0
  24. package/out-tsc/src/label/Label.js.map +1 -1
  25. package/out-tsc/src/list/TembaMenu.js +8 -6
  26. package/out-tsc/src/list/TembaMenu.js.map +1 -1
  27. package/out-tsc/src/store/Store.js +79 -0
  28. package/out-tsc/src/store/Store.js.map +1 -1
  29. package/out-tsc/src/tabpane/Tab.js +14 -1
  30. package/out-tsc/src/tabpane/Tab.js.map +1 -1
  31. package/out-tsc/src/tabpane/TabPane.js +35 -0
  32. package/out-tsc/src/tabpane/TabPane.js.map +1 -1
  33. package/out-tsc/src/textinput/TextInput.js +2 -2
  34. package/out-tsc/src/textinput/TextInput.js.map +1 -1
  35. package/out-tsc/src/utils/index.js +3 -3
  36. package/out-tsc/src/utils/index.js.map +1 -1
  37. package/out-tsc/src/vectoricon/VectorIcon.js +13 -2
  38. package/out-tsc/src/vectoricon/VectorIcon.js.map +1 -1
  39. package/out-tsc/temba-modules.js +8 -6
  40. package/out-tsc/temba-modules.js.map +1 -1
  41. package/package.json +3 -1
  42. package/src/contacts/ContactBadges.ts +104 -0
  43. package/src/contacts/ContactFieldEditor.ts +55 -41
  44. package/src/contacts/ContactFields.ts +105 -36
  45. package/src/contacts/ContactPending.ts +236 -0
  46. package/src/contacts/ContactStoreElement.ts +4 -0
  47. package/src/contacts/events.ts +1 -1
  48. package/src/interfaces.ts +34 -1
  49. package/src/label/Label.ts +25 -0
  50. package/src/list/TembaMenu.ts +7 -5
  51. package/src/store/Store.ts +104 -1
  52. package/src/tabpane/Tab.ts +14 -1
  53. package/src/tabpane/TabPane.ts +36 -0
  54. package/src/textinput/TextInput.ts +2 -2
  55. package/src/utils/index.ts +10 -10
  56. package/src/vectoricon/VectorIcon.ts +15 -2
  57. package/static/css/temba-components.css +3 -0
  58. package/static/icons/Read Me.txt +1 -1
  59. package/static/icons/SVG/bookmark-filled.svg +5 -0
  60. package/static/icons/SVG/bookmark.svg +1 -1
  61. package/static/icons/SVG/external-link1.svg +5 -0
  62. package/static/icons/SVG/globe.svg +5 -0
  63. package/static/icons/SVG/language.svg +5 -0
  64. package/static/icons/SVG/search.svg +5 -0
  65. package/static/icons/demo-external-svg.html +218 -165
  66. package/static/icons/demo-files/demo.css +6 -3
  67. package/static/icons/demo.html +253 -169
  68. package/static/icons/selection.json +318 -184
  69. package/static/icons/style.css +4 -0
  70. package/static/icons/symbol-defs.svg +35 -4
  71. package/temba-modules.ts +9 -6
@@ -1,40 +1,35 @@
1
- import { css, html, TemplateResult } from 'lit';
1
+ import { css, html, PropertyValueMap, TemplateResult } from 'lit';
2
2
  import { property } from 'lit/decorators';
3
- import { ContactField } from '../interfaces';
4
3
  import { postJSON } from '../utils';
5
4
  import { ContactFieldEditor } from './ContactFieldEditor';
6
5
  import { ContactStoreElement } from './ContactStoreElement';
6
+ import { Checkbox } from '../checkbox/Checkbox';
7
+
8
+ const MIN_FOR_FILTER = 10;
7
9
 
8
10
  export class ContactFields extends ContactStoreElement {
9
11
  static get styles() {
10
12
  return css`
11
13
  :host {
12
- display: flex;
13
- flex-wrap: wrap;
14
- flex-shrink: 1;
14
+ --curvature-widget: 0px;
15
+ border-radius: 6px;
15
16
  }
16
17
 
17
18
  .field {
18
19
  display: flex;
19
20
  margin: 0.3em 0.3em;
20
21
  box-shadow: 0 0 0.2em rgba(0, 0, 0, 0.15);
21
- border-radius: var(--curvature);
22
+ border-radius: 0px;
22
23
  align-items: center;
23
24
  overflow: hidden;
24
25
  }
25
26
 
26
- .field.set {
27
- background: #fff;
28
- }
29
-
30
- .field.unset {
31
- opacity: 0.4;
32
- }
33
-
34
- .field.unset .label {
27
+ .show-all .unset {
28
+ display: block;
35
29
  }
36
30
 
37
- .field:hover {
31
+ .unset {
32
+ display: none;
38
33
  }
39
34
 
40
35
  .field:hover {
@@ -45,8 +40,8 @@ export class ContactFields extends ContactStoreElement {
45
40
 
46
41
  .label {
47
42
  padding: 0.25em 1em;
48
- border-top-left-radius: var(--curvature);
49
- border-bottom-left-radius: var(--curvature);
43
+ border-top-left-radius: 0px;
44
+ border-bottom-left-radius: 0px;
50
45
  color: #777;
51
46
  font-size: 0.9em;
52
47
  font-weight: 400;
@@ -60,53 +55,127 @@ export class ContactFields extends ContactStoreElement {
60
55
  overflow: hidden;
61
56
  text-overflow: ellipsis;
62
57
  padding: 0.25em 1em;
63
- border-top-right-radius: var(--curvature);
64
- border-bottom-right-radius: var(--curvature);
58
+ border-top-right-radius: 0px;
59
+ border-bottom-right-radius: 0px;
65
60
  font-size: 0.9em;
66
61
  }
67
62
 
68
63
  temba-contact-field {
69
- margin: 0.3em;
70
- min-width: 320px;
71
- flex-grow: 1;
64
+ }
65
+
66
+ .footer {
67
+ margin-bottom: 0;
68
+ display: flex;
69
+ background: #fff;
70
+ align-items: center;
71
+ margin-top: 0.5em;
72
72
  }
73
73
  `;
74
74
  }
75
75
 
76
+ @property({ type: Boolean })
77
+ pinned: boolean;
78
+
79
+ @property({ type: Boolean })
80
+ system: boolean;
81
+
76
82
  @property({ type: Boolean })
77
83
  dirty: boolean;
78
84
 
85
+ @property({ type: Boolean })
86
+ showAll: boolean;
87
+
79
88
  connectedCallback(): void {
80
89
  super.connectedCallback();
81
90
  this.handleFieldChanged = this.handleFieldChanged.bind(this);
82
91
  }
83
92
 
93
+ protected updated(
94
+ changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
95
+ ): void {
96
+ super.updated(changes);
97
+ if (changes.has('data')) {
98
+ if (Object.keys(this.data.fields).length <= MIN_FOR_FILTER) {
99
+ this.showAll = true;
100
+ }
101
+ }
102
+ }
103
+
84
104
  public handleFieldChanged(evt: InputEvent) {
85
105
  const field = evt.currentTarget as ContactFieldEditor;
86
- this.data.fields[field.key] = field.value;
106
+ const value = field.value;
107
+ this.data.fields[field.key] = value;
87
108
  postJSON('/api/v2/contacts.json?uuid=' + this.data.uuid, {
88
- fields: { [field.key]: field.value },
89
- }).then(() => {
90
- this.refresh();
109
+ fields: { [field.key]: value },
110
+ }).then((response: any) => {
111
+ this.updateStoreContact(response.json);
91
112
  });
92
113
  }
93
114
 
115
+ public handleToggle(evt: Event) {
116
+ const checkbox = evt.currentTarget as Checkbox;
117
+ this.showAll = checkbox.checked;
118
+ }
119
+
94
120
  public render(): TemplateResult {
95
- const pinned = this.store.getPinnedFields();
121
+ if (this.data) {
122
+ const fieldsToShow = Object.entries(this.data.fields)
123
+ .filter((entry: [string, string]) => {
124
+ return (
125
+ (this.pinned && this.store.getContactField(entry[0]).pinned) ||
126
+ (!this.pinned && !this.store.getContactField(entry[0]).pinned)
127
+ );
128
+ })
129
+ .sort((a: [string, string], b: [string, string]) => {
130
+ const [ak] = a;
131
+ const [bk] = b;
132
+ const priority =
133
+ this.store.getContactField(bk).priority -
134
+ this.store.getContactField(ak).priority;
135
+ if (priority !== 0) {
136
+ return priority;
137
+ }
138
+
139
+ return ak.localeCompare(bk);
140
+ });
141
+
142
+ if (fieldsToShow.length == 0) {
143
+ return html`<slot name="empty"></slot>`;
144
+ }
96
145
 
97
- const fields = pinned.map((field: ContactField) => {
98
- if (this.data) {
99
- const value = this.data.fields[field.key];
146
+ const fields = fieldsToShow.map((entry: [string, string]) => {
147
+ const [k, v] = entry;
148
+ const field = this.store.getContactField(k);
100
149
  return html`<temba-contact-field
150
+ class=${v ? 'set' : 'unset'}
101
151
  key=${field.key}
102
152
  name=${field.label}
103
- value=${value}
153
+ value=${v}
104
154
  type=${field.value_type}
105
155
  @change=${this.handleFieldChanged}
106
156
  ></temba-contact-field>`;
107
- }
108
- });
109
-
110
- return html`${this.data ? html` ${fields} ` : null}`;
157
+ });
158
+
159
+ return html`
160
+ <div class="${this.showAll || this.pinned ? 'show-all' : ''}">
161
+ ${fields}
162
+ </div>
163
+
164
+ ${!this.pinned && Object.keys(this.data.fields).length >= MIN_FOR_FILTER
165
+ ? html`<div class="footer">
166
+ <div style="flex-grow: 1"></div>
167
+ <div>
168
+ <temba-checkbox
169
+ ?checked=${this.showAll}
170
+ @change=${this.handleToggle}
171
+ label="Show All"
172
+ />
173
+ </div>
174
+ </div>`
175
+ : null}
176
+ `;
177
+ }
178
+
179
+ return null;
111
180
  }
112
181
  }
@@ -0,0 +1,236 @@
1
+ import { css, html, PropertyValueMap, TemplateResult } from 'lit';
2
+ import { property } from 'lit/decorators';
3
+ import { ScheduledEvent, ScheduledEventType } from '../interfaces';
4
+ import { StoreElement } from '../store/StoreElement';
5
+
6
+ const ICONS = {
7
+ [ScheduledEventType.CampaignEvent]: 'campaign',
8
+ [ScheduledEventType.ScheduledBroadcast]: 'message-square',
9
+ [ScheduledEventType.ScheduledTrigger]: 'radio',
10
+ };
11
+
12
+ export class ContactPending extends StoreElement {
13
+ @property({ type: String })
14
+ contact: string;
15
+
16
+ @property({ type: Object, attribute: false })
17
+ data: ScheduledEvent[];
18
+
19
+ @property({ type: String })
20
+ lang_weekly = 'Weekly';
21
+
22
+ @property({ type: String })
23
+ lang_daily = 'Daily';
24
+
25
+ @property({ type: String })
26
+ lang_once = 'Once';
27
+
28
+ REPEAT_PERIOD = {
29
+ O: this.lang_once,
30
+ D: this.lang_daily,
31
+ W: this.lang_weekly,
32
+ };
33
+
34
+ static get styles() {
35
+ return css`
36
+ :host {
37
+ --text-color: rgba(50, 50, 50, 0.65);
38
+ }
39
+
40
+ a,
41
+ .linked {
42
+ color: var(--color-link-primary);
43
+ cursor: pointer;
44
+ }
45
+
46
+ a:hover,
47
+ .linked:hover {
48
+ text-decoration: underline;
49
+ color: var(--color-link-primary-hover);
50
+ }
51
+
52
+ .type {
53
+ background: rgba(0, 0, 0, 0.02);
54
+ padding: 1em;
55
+ display: flex;
56
+ align-self: stretch;
57
+ --icon-color: rgba(50, 50, 50, 0.25);
58
+ border-top-left-radius: var(--curvature);
59
+ border-bottom-left-radius: var(--curvature);
60
+ }
61
+
62
+ .details {
63
+ display: flex;
64
+ flex-direction: column;
65
+ padding: 0.5em 1em;
66
+ flex-grow: 1;
67
+ }
68
+
69
+ .campaign {
70
+ display: flex;
71
+ color: var(--text-color);
72
+ --icon-color: var(--text-color);
73
+ align-self: center;
74
+ white-space: nowrap;
75
+ }
76
+
77
+ .message {
78
+ // font-style: italic;
79
+ display: -webkit-box;
80
+ -webkit-line-clamp: 2;
81
+ -webkit-box-orient: vertical;
82
+ overflow: hidden;
83
+ padding: 0.1em;
84
+ }
85
+
86
+ .event {
87
+ margin-bottom: 0.5em;
88
+ // background: rgba(0,0,0,.02);
89
+ border-radius: var(--curvature);
90
+ display: flex;
91
+ flex-direction: row;
92
+ align-items: center;
93
+ box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.055),
94
+ 0 0 0px 1px rgba(0, 0, 0, 0.02);
95
+ }
96
+
97
+ .time {
98
+ white-space: nowrap;
99
+ background: rgba(0, 0, 0, 0.02);
100
+ border-top-right-radius: var(--curvature);
101
+ border-bottom-right-radius: var(--curvature);
102
+ display: flex;
103
+ align-self: stretch;
104
+ padding: 0 1em;
105
+ min-width: 5em;
106
+ }
107
+
108
+ .duration {
109
+ align-self: center;
110
+ flex-grow: 1;
111
+ text-align: center;
112
+ }
113
+
114
+ .flow {
115
+ display: inline-block;
116
+ }
117
+
118
+ temba-tip {
119
+ cursor: default;
120
+ }
121
+
122
+ .scheduled-by {
123
+ font-size: 0.85em;
124
+ display: flex;
125
+ color: var(--text-color);
126
+ --icon-color: var(--text-color);
127
+ }
128
+
129
+ .scheduled-by temba-icon {
130
+ margin-right: 0.25em;
131
+ }
132
+
133
+ .campaign_event .scheduled-by:hover {
134
+ color: var(--color-link-primary);
135
+ --icon-color: var(--color-link-primary);
136
+ cursor: pointer;
137
+ }
138
+
139
+ .scheduled-by .name {
140
+ flex-grow: 1;
141
+ }
142
+ `;
143
+ }
144
+
145
+ protected updated(
146
+ changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
147
+ ): void {
148
+ super.updated(changes);
149
+ if (changes.has('contact')) {
150
+ if (this.contact) {
151
+ this.url = `/contact/scheduled/${this.contact}/`;
152
+ } else {
153
+ this.url = null;
154
+ }
155
+ }
156
+ }
157
+
158
+ public renderEvent(event: ScheduledEvent) {
159
+ return html`
160
+ <div class="event ${event.type}">
161
+ <div class="type">
162
+ <temba-icon
163
+ size="2"
164
+ name="${event.message ? 'message-square' : 'flow'}"
165
+ ></temba-icon>
166
+ </div>
167
+
168
+ <div class="details">
169
+ <div>
170
+ ${event.flow
171
+ ? html`
172
+ <div
173
+ class="flow linked"
174
+ href="/flow/editor/${event.flow.uuid}/"
175
+ onclick="goto(event)"
176
+ >
177
+ ${event.flow.name}
178
+ </div>
179
+ `
180
+ : null}
181
+ ${event.message
182
+ ? html` <div class="message">${event.message}</div> `
183
+ : null}
184
+ </div>
185
+
186
+ <div class="scheduled-by">
187
+ ${event.campaign
188
+ ? html`<div
189
+ style="display:flex"
190
+ href="/campaign/read/${event.campaign.uuid}/"
191
+ onclick="goto(event, this)"
192
+ >
193
+ <temba-icon name="campaign"></temba-icon>
194
+ <div class="name">${event.campaign.name}</div>
195
+ </div>`
196
+ : html`
197
+ ${event.type === ScheduledEventType.ScheduledTrigger
198
+ ? html`<temba-icon
199
+ name="${ICONS[event.type]}"
200
+ ></temba-icon>`
201
+ : null}
202
+ <div class="name">
203
+ ${this.REPEAT_PERIOD[event.repeat_period]}
204
+ </div>
205
+ `}
206
+ </div>
207
+ </div>
208
+
209
+ <div class="time">
210
+ <div class="duration">
211
+ <temba-tip
212
+ text=${this.store.formatDate(event.scheduled)}
213
+ position="left"
214
+ >
215
+ ${this.store.getShortDuration(event.scheduled)}
216
+ </temba-tip>
217
+ </div>
218
+ </div>
219
+ </div>
220
+ `;
221
+ }
222
+
223
+ public render(): TemplateResult {
224
+ if (this.data) {
225
+ if (this.data.length > 0) {
226
+ return html`
227
+ ${this.data.map(event => {
228
+ return this.renderEvent(event);
229
+ })}
230
+ `;
231
+ } else {
232
+ return html`<slot name="empty"></slot>`;
233
+ }
234
+ }
235
+ }
236
+ }
@@ -36,6 +36,10 @@ export class ContactStoreElement extends StoreElement {
36
36
  return null;
37
37
  }
38
38
 
39
+ public updateStoreContact(value: any) {
40
+ this.store.updateCache(`/api/v2/contacts.json?uuid=${this.contact}`, value);
41
+ }
42
+
39
43
  protected updated(
40
44
  changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
41
45
  ): void {
@@ -685,7 +685,7 @@ export const renderAttachment = (attachment: string): TemplateResult => {
685
685
  style="border-radius:var(--curvature);box-shadow:0px 0px 12px 0px rgba(0,0,0,.1), 0px 0px 2px 0px rgba(0,0,0,.15);"
686
686
  max-width="400px"
687
687
  height="auto"
688
- controls="controls"
688
+ controls
689
689
  >
690
690
  <source src="${url}" type="video/mp4" />
691
691
  </video> `;
package/src/interfaces.ts CHANGED
@@ -1,3 +1,34 @@
1
+ export interface Workspace {
2
+ uuid: string;
3
+ name: string;
4
+ country: string;
5
+ languages: string[];
6
+ timezone: string;
7
+ date_style: DateStyle;
8
+ anon: boolean;
9
+ }
10
+
11
+ export enum DateStyle {
12
+ DayFirst = 'day_first',
13
+ MonthFirst = 'month_first',
14
+ YearFirst = 'year_first',
15
+ }
16
+
17
+ export enum ScheduledEventType {
18
+ CampaignEvent = 'campaign_event',
19
+ ScheduledBroadcast = 'scheduled_broadcast',
20
+ ScheduledTrigger = 'scheduled_trigger',
21
+ }
22
+
23
+ export interface ScheduledEvent {
24
+ type: ScheduledEventType;
25
+ scheduled: string;
26
+ repeat_period: string;
27
+ campaign?: ObjectReference;
28
+ flow?: ObjectReference;
29
+ message?: string;
30
+ }
31
+
1
32
  export interface User {
2
33
  id?: number;
3
34
  first_name?: string;
@@ -84,13 +115,15 @@ export interface Contact {
84
115
  stopped: boolean;
85
116
  blocked: boolean;
86
117
  urns: string[];
87
- lang: string;
118
+ language?: string;
88
119
  fields: { [key: string]: string };
89
120
  groups: Group[];
90
121
  modified_on: string;
91
122
  created_on: string;
92
123
  last_seen_on: string;
124
+ status: string;
93
125
 
126
+ flow?: ObjectReference;
94
127
  last_msg?: Msg;
95
128
  direction?: string;
96
129
  ticket: {
@@ -50,6 +50,27 @@ export default class Label extends LitElement {
50
50
  text-shadow: none;
51
51
  }
52
52
 
53
+ .tertiary {
54
+ background: var(--color-label-tertiary);
55
+ color: var(--color-label-tertiary-text);
56
+ --icon-color: var(--color-label-tertiary-text);
57
+ text-shadow: none;
58
+ }
59
+
60
+ .tertiary {
61
+ background: var(--color-label-tertiary);
62
+ color: var(--color-label-tertiary-text);
63
+ --icon-color: var(--color-label-tertiary-text);
64
+ text-shadow: none;
65
+ }
66
+
67
+ .tertiary {
68
+ background: var(--color-label-tertiary);
69
+ color: var(--color-label-tertiary-text);
70
+ --icon-color: var(--color-label-tertiary-text);
71
+ text-shadow: none;
72
+ }
73
+
53
74
  .light {
54
75
  background: var(--color-overlay-light);
55
76
  color: var(--color-overlay-light-text);
@@ -83,6 +104,9 @@ export default class Label extends LitElement {
83
104
  @property({ type: Boolean })
84
105
  secondary: boolean;
85
106
 
107
+ @property({ type: Boolean })
108
+ tertiary: boolean;
109
+
86
110
  @property({ type: Boolean })
87
111
  light: boolean;
88
112
 
@@ -116,6 +140,7 @@ export default class Label extends LitElement {
116
140
  clickable: this.clickable,
117
141
  primary: this.primary,
118
142
  secondary: this.secondary,
143
+ tertiary: this.tertiary,
119
144
  light: this.light,
120
145
  dark: this.dark,
121
146
  shadow: this.shadow,
@@ -503,11 +503,13 @@ export class TembaMenu extends RapidElement {
503
503
  // go up the tree until we find an endpoint
504
504
  const item = this.getMenuItemForSelection(path);
505
505
 
506
- if (item.endpoint) {
507
- this.loadItems(item, false);
508
- } else {
509
- path.pop();
510
- this.refresh(path);
506
+ if (item) {
507
+ if (item.endpoint) {
508
+ this.loadItems(item, false);
509
+ } else {
510
+ path.pop();
511
+ this.refresh(path);
512
+ }
511
513
  }
512
514
  }
513
515