@nyaruka/temba-components 0.158.1 → 0.159.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 +12 -0
- package/dist/static/svg/index.svg +1 -1
- package/dist/temba-components.js +1458 -600
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -2
- package/src/Icons.ts +8 -1
- package/src/display/Button.ts +24 -14
- package/src/display/Thumbnail.ts +1 -1
- package/src/flow/nodes/split_by_resthook.ts +3 -3
- package/src/interfaces.ts +46 -2
- package/src/layout/PageHeader.ts +338 -0
- package/src/list/ContactList.ts +68 -52
- package/src/list/ContentList.ts +1461 -346
- package/src/list/FlowList.ts +20 -26
- package/src/list/MsgList.ts +169 -71
- package/src/live/ContactEvents.ts +880 -0
- package/src/styles/designTokens.ts +5 -2
- package/src/styles/pillVariants.ts +21 -6
- package/static/css/design-system.css +769 -0
- package/static/css/temba-components.css +16 -77
- package/static/svg/index.svg +1 -1
- package/static/svg/work/traced/chevron-down-double.svg +1 -0
- package/static/svg/work/traced/chevron-up-double.svg +1 -0
- package/static/svg/work/used/chevron-down-double.svg +3 -0
- package/static/svg/work/used/chevron-up-double.svg +3 -0
- package/temba-modules.ts +4 -2
- package/web-dev-server.config.mjs +9 -0
- package/src/live/ContactPending.ts +0 -247
package/src/list/FlowList.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { css, html, TemplateResult } from 'lit';
|
|
2
2
|
import { ContentList, ContentListColumn } from './ContentList';
|
|
3
3
|
import { Icon } from '../Icons';
|
|
4
|
+
import { Flow, ObjectReference } from '../interfaces';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Flow CRUDL list — drop-in replacement for the rapidpro
|
|
7
8
|
* `flows/flow_list.html` table. Each row's leading icon reflects
|
|
8
9
|
* the flow type (messaging / voice / background / surveyor).
|
|
9
|
-
* Columns: name, runs, ongoing, completion bar,
|
|
10
|
+
* Columns: name, status, runs, ongoing, completion bar, activity
|
|
11
|
+
* sparkline.
|
|
10
12
|
*/
|
|
11
|
-
export class FlowList extends ContentList<
|
|
13
|
+
export class FlowList extends ContentList<Flow> {
|
|
12
14
|
static get styles() {
|
|
13
15
|
return css`
|
|
14
16
|
${ContentList.styles}
|
|
@@ -83,43 +85,35 @@ export class FlowList extends ContentList<any> {
|
|
|
83
85
|
this.emptyMessage = 'No flows';
|
|
84
86
|
this.searchPlaceholder = 'Search flows';
|
|
85
87
|
this.columns = [
|
|
86
|
-
{ key: 'name', label: 'Name', sortable: true, grow: 3 },
|
|
87
88
|
{
|
|
88
|
-
key: '
|
|
89
|
-
label: '
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
key: 'name',
|
|
90
|
+
label: 'Name',
|
|
91
|
+
sortable: true,
|
|
92
|
+
width: '280px',
|
|
93
|
+
pinned: true
|
|
92
94
|
},
|
|
95
|
+
{ key: 'status', label: 'Status', width: '110px' },
|
|
93
96
|
{
|
|
94
97
|
key: 'runs',
|
|
95
98
|
label: 'Runs',
|
|
96
99
|
sortable: true,
|
|
97
|
-
width: '
|
|
98
|
-
grow: 0,
|
|
100
|
+
width: '90px',
|
|
99
101
|
align: 'right'
|
|
100
102
|
},
|
|
101
103
|
{
|
|
102
104
|
key: 'ongoing',
|
|
103
105
|
label: 'Ongoing',
|
|
104
106
|
sortable: true,
|
|
105
|
-
width: '
|
|
106
|
-
grow: 0,
|
|
107
|
+
width: '90px',
|
|
107
108
|
align: 'right'
|
|
108
109
|
},
|
|
109
110
|
{
|
|
110
111
|
key: 'completion',
|
|
111
112
|
label: 'Completion',
|
|
112
|
-
width: '
|
|
113
|
-
grow: 0,
|
|
113
|
+
width: '130px',
|
|
114
114
|
align: 'right'
|
|
115
115
|
},
|
|
116
|
-
{
|
|
117
|
-
key: 'activity',
|
|
118
|
-
label: 'Activity',
|
|
119
|
-
width: '120px',
|
|
120
|
-
grow: 0,
|
|
121
|
-
align: 'right'
|
|
122
|
-
}
|
|
116
|
+
{ key: 'activity', label: 'Activity', width: '120px', align: 'right' }
|
|
123
117
|
];
|
|
124
118
|
this.bulkActions = [
|
|
125
119
|
{
|
|
@@ -133,7 +127,7 @@ export class FlowList extends ContentList<any> {
|
|
|
133
127
|
];
|
|
134
128
|
}
|
|
135
129
|
|
|
136
|
-
protected getRowIcon(item:
|
|
130
|
+
protected getRowIcon(item: Flow): string | null {
|
|
137
131
|
switch (item?.type) {
|
|
138
132
|
case 'voice':
|
|
139
133
|
case 'ivr':
|
|
@@ -147,12 +141,12 @@ export class FlowList extends ContentList<any> {
|
|
|
147
141
|
}
|
|
148
142
|
}
|
|
149
143
|
|
|
150
|
-
protected getRowHref(item:
|
|
144
|
+
protected getRowHref(item: Flow): string | null {
|
|
151
145
|
return item?.uuid ? `/flow/editor/${item.uuid}/` : null;
|
|
152
146
|
}
|
|
153
147
|
|
|
154
148
|
protected renderCell(
|
|
155
|
-
item:
|
|
149
|
+
item: Flow,
|
|
156
150
|
column: ContentListColumn
|
|
157
151
|
): TemplateResult | string {
|
|
158
152
|
switch (column.key) {
|
|
@@ -170,7 +164,7 @@ export class FlowList extends ContentList<any> {
|
|
|
170
164
|
${labels.length > 0
|
|
171
165
|
? html`<span class="flow-labels">
|
|
172
166
|
${labels.map(
|
|
173
|
-
(l:
|
|
167
|
+
(l: ObjectReference) =>
|
|
174
168
|
html`<temba-label type="label" icon=${Icon.label}
|
|
175
169
|
>${l.name}</temba-label
|
|
176
170
|
>`
|
|
@@ -196,7 +190,7 @@ export class FlowList extends ContentList<any> {
|
|
|
196
190
|
}
|
|
197
191
|
}
|
|
198
192
|
|
|
199
|
-
private renderCompletion(item:
|
|
193
|
+
private renderCompletion(item: Flow): TemplateResult {
|
|
200
194
|
const value = typeof item.completion === 'number' ? item.completion : 0;
|
|
201
195
|
const pct = Math.round(value * 100);
|
|
202
196
|
return html`
|
|
@@ -242,7 +236,7 @@ export class FlowList extends ContentList<any> {
|
|
|
242
236
|
`;
|
|
243
237
|
}
|
|
244
238
|
|
|
245
|
-
private renderFlowStatus(item:
|
|
239
|
+
private renderFlowStatus(item: Flow): TemplateResult {
|
|
246
240
|
const status = (item.status || 'active').toString().toLowerCase();
|
|
247
241
|
const kind = status === 'archived' ? 'archived' : 'active';
|
|
248
242
|
const label = status === 'archived' ? 'Archived' : 'Active';
|
package/src/list/MsgList.ts
CHANGED
|
@@ -1,48 +1,94 @@
|
|
|
1
1
|
import { css, html, TemplateResult } from 'lit';
|
|
2
2
|
import { ContentList, ContentListColumn } from './ContentList';
|
|
3
3
|
import { Icon } from '../Icons';
|
|
4
|
+
import { Msg } from '../interfaces';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Message CRUDL list — drop-in replacement for the rapidpro
|
|
7
|
-
* `msgs/msg_list.html` table. Reverse-chronological;
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* `msgs/msg_list.html` table. Reverse-chronological; the message
|
|
9
|
+
* cell carries the body text with its attachment thumbnails right
|
|
10
|
+
* after it and the flow / label pills pushed to the trailing edge,
|
|
11
|
+
* with a duration timestamp closing the row.
|
|
10
12
|
*/
|
|
11
|
-
export class MsgList extends ContentList<
|
|
13
|
+
export class MsgList extends ContentList<Msg> {
|
|
12
14
|
static get styles() {
|
|
13
15
|
return css`
|
|
14
16
|
${ContentList.styles}
|
|
15
|
-
|
|
17
|
+
/* The message cell holds the body text with its attachment
|
|
18
|
+
thumbnails right after it, and the flow / label pills pushed
|
|
19
|
+
to the trailing edge. The text sizes to content and
|
|
20
|
+
ellipsizes when squeezed — resolved per row, so a busy row
|
|
21
|
+
doesn't widen a column for all of them. */
|
|
22
|
+
.msg-cell {
|
|
16
23
|
display: flex;
|
|
17
24
|
align-items: center;
|
|
18
|
-
gap:
|
|
19
|
-
flex-wrap: wrap;
|
|
20
|
-
font-size: 12px;
|
|
21
|
-
color: var(--text-3);
|
|
25
|
+
gap: 12px;
|
|
22
26
|
}
|
|
23
27
|
.msg-text {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
flex: 0 1 auto;
|
|
29
|
+
/* A small floor so the message keeps a few words even on a
|
|
30
|
+
row whose meta is wide — the message yields most of its
|
|
31
|
+
width to the attachments and pills before they clip. */
|
|
32
|
+
min-width: 80px;
|
|
27
33
|
overflow: hidden;
|
|
28
34
|
text-overflow: ellipsis;
|
|
29
35
|
white-space: nowrap;
|
|
30
36
|
}
|
|
31
|
-
.
|
|
37
|
+
/* Attachment thumbnails sit immediately after the text. */
|
|
38
|
+
.msg-attachments {
|
|
39
|
+
flex: 0 0 auto;
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 6px;
|
|
43
|
+
}
|
|
44
|
+
/* Flow + label pills, pushed to the trailing edge of the cell. */
|
|
45
|
+
.cell-pills {
|
|
46
|
+
flex: 0 0 auto;
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
gap: 6px;
|
|
50
|
+
margin-left: auto;
|
|
51
|
+
}
|
|
52
|
+
/* Attachment thumbnails are sized well below the 44px row so
|
|
53
|
+
they never grow its height — a small square preview rather
|
|
54
|
+
than the chat history's full-size thumbnail. --thumb-icon-
|
|
55
|
+
padding shrinks temba-thumbnail's non-image fallback (the
|
|
56
|
+
document/video icon box) to match. The max-height clamp is
|
|
57
|
+
a backstop for any type that still carries its own larger
|
|
58
|
+
intrinsic size (e.g. a location map). */
|
|
59
|
+
.msg-thumb {
|
|
60
|
+
--thumb-size: 24px;
|
|
61
|
+
--thumb-padding: 2px;
|
|
62
|
+
--thumb-icon-padding: 4px;
|
|
63
|
+
max-height: 36px;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
}
|
|
66
|
+
/* Sent cell — the date with an optional channel-log icon to
|
|
67
|
+
its right. The cell stays right-aligned (the column's own
|
|
68
|
+
alignment) and uses a flex row so the icon sits flush
|
|
69
|
+
against the date with a small gap. */
|
|
70
|
+
.sent-cell {
|
|
32
71
|
display: flex;
|
|
33
|
-
|
|
34
|
-
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: flex-end;
|
|
74
|
+
gap: 6px;
|
|
35
75
|
}
|
|
36
|
-
|
|
76
|
+
/* Channel-log icon link inside the sent cell. */
|
|
77
|
+
.msg-log {
|
|
78
|
+
flex: 0 0 auto;
|
|
37
79
|
display: inline-flex;
|
|
38
80
|
align-items: center;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
border-radius: 999px;
|
|
42
|
-
background: var(--sunken);
|
|
81
|
+
padding: 2px;
|
|
82
|
+
border-radius: var(--r-sm);
|
|
43
83
|
color: var(--text-3);
|
|
44
|
-
|
|
45
|
-
|
|
84
|
+
text-decoration: none;
|
|
85
|
+
}
|
|
86
|
+
.msg-log:hover {
|
|
87
|
+
background: var(--sunken);
|
|
88
|
+
color: var(--text-1);
|
|
89
|
+
}
|
|
90
|
+
.msg-log temba-icon {
|
|
91
|
+
--icon-color: currentColor;
|
|
46
92
|
}
|
|
47
93
|
`;
|
|
48
94
|
}
|
|
@@ -52,15 +98,28 @@ export class MsgList extends ContentList<any> {
|
|
|
52
98
|
this.valueKey = 'id';
|
|
53
99
|
this.emptyMessage = 'No messages';
|
|
54
100
|
this.searchPlaceholder = 'Search messages';
|
|
101
|
+
// Messages page 100 at a time, matching rapidpro's msg list.
|
|
102
|
+
this.pageSize = 100;
|
|
103
|
+
// Fixed layout so a long message ellipsis-truncates within its
|
|
104
|
+
// column instead of stretching the table; minTableWidth lets the
|
|
105
|
+
// list scroll horizontally once the container is too narrow to
|
|
106
|
+
// keep the columns usable, rather than clipping anything.
|
|
107
|
+
this.fixedLayout = true;
|
|
108
|
+
this.minTableWidth = '640px';
|
|
55
109
|
this.columns = [
|
|
56
|
-
{
|
|
57
|
-
|
|
110
|
+
{
|
|
111
|
+
key: 'contact',
|
|
112
|
+
label: 'Contact',
|
|
113
|
+
width: '130px',
|
|
114
|
+
pinned: true
|
|
115
|
+
},
|
|
116
|
+
{ key: 'text', label: 'Message', grow: true },
|
|
58
117
|
{
|
|
59
118
|
key: 'created_on',
|
|
60
119
|
label: 'Sent',
|
|
61
|
-
width: '
|
|
62
|
-
|
|
63
|
-
|
|
120
|
+
width: '120px',
|
|
121
|
+
align: 'right',
|
|
122
|
+
pinned: 'right'
|
|
64
123
|
}
|
|
65
124
|
];
|
|
66
125
|
this.bulkActions = [
|
|
@@ -76,68 +135,107 @@ export class MsgList extends ContentList<any> {
|
|
|
76
135
|
}
|
|
77
136
|
|
|
78
137
|
protected renderCell(
|
|
79
|
-
item:
|
|
138
|
+
item: Msg,
|
|
80
139
|
column: ContentListColumn
|
|
81
140
|
): TemplateResult | string {
|
|
82
141
|
switch (column.key) {
|
|
83
142
|
case 'contact': {
|
|
84
143
|
const contact = item.contact || {};
|
|
85
|
-
return
|
|
86
|
-
>${contact.name || contact.urn || ''}</span
|
|
87
|
-
>`;
|
|
144
|
+
return contact.name || contact.urn || '';
|
|
88
145
|
}
|
|
89
146
|
case 'text':
|
|
90
|
-
return this.
|
|
147
|
+
return this.renderMessageCell(item);
|
|
91
148
|
case 'created_on':
|
|
92
|
-
return
|
|
93
|
-
value=${item.created_on}
|
|
94
|
-
display="duration"
|
|
95
|
-
></temba-date>`;
|
|
149
|
+
return this.renderSentCell(item);
|
|
96
150
|
default:
|
|
97
151
|
return super.renderCell(item, column);
|
|
98
152
|
}
|
|
99
153
|
}
|
|
100
154
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
155
|
+
/** The message cell — body text, its attachment thumbnails, then
|
|
156
|
+
* the trailing flow / label pills. Each piece sizes to content, so
|
|
157
|
+
* the split is resolved independently for every row. */
|
|
158
|
+
private renderMessageCell(item: Msg): TemplateResult {
|
|
159
|
+
return html`
|
|
160
|
+
<div class="msg-cell">
|
|
161
|
+
<span class="msg-text">${this.renderMessageText(item)}</span>
|
|
162
|
+
${this.renderAttachments(item)}${this.renderPills(item)}
|
|
163
|
+
</div>
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
105
166
|
|
|
167
|
+
/** The sent cell — duration timestamp with an optional channel-log
|
|
168
|
+
* icon to its right. The icon is rendered when the server includes
|
|
169
|
+
* a logs_url on the row (permission- and retention-gated
|
|
170
|
+
* server-side). stopPropagation keeps the row's contact navigation
|
|
171
|
+
* from also firing when the icon is clicked. */
|
|
172
|
+
private renderSentCell(item: Msg): TemplateResult | string {
|
|
173
|
+
if (!item.created_on) return '';
|
|
106
174
|
return html`
|
|
107
|
-
<div class="
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
</span>
|
|
111
|
-
${labels.length || attachments.length || item.flow || isOptin
|
|
175
|
+
<div class="sent-cell">
|
|
176
|
+
<temba-date value=${item.created_on} display="duration"></temba-date>
|
|
177
|
+
${item.logs_url && this.isSafeHref(item.logs_url)
|
|
112
178
|
? html`
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
></temba-icon>
|
|
122
|
-
attachment
|
|
123
|
-
</span>
|
|
124
|
-
`
|
|
125
|
-
)}
|
|
126
|
-
${item.flow
|
|
127
|
-
? html`<temba-label type="flow" icon=${Icon.flow}
|
|
128
|
-
>${item.flow.name}</temba-label
|
|
129
|
-
>`
|
|
130
|
-
: null}
|
|
131
|
-
${labels.map(
|
|
132
|
-
(l: any) => html`
|
|
133
|
-
<temba-label type="label" icon=${Icon.label}
|
|
134
|
-
>${l.name}</temba-label
|
|
135
|
-
>
|
|
136
|
-
`
|
|
137
|
-
)}
|
|
138
|
-
</div>
|
|
179
|
+
<a
|
|
180
|
+
class="msg-log"
|
|
181
|
+
href=${item.logs_url}
|
|
182
|
+
@click=${(e: MouseEvent) => e.stopPropagation()}
|
|
183
|
+
aria-label="Channel log"
|
|
184
|
+
>
|
|
185
|
+
<temba-icon name=${Icon.log} size="0.95"></temba-icon>
|
|
186
|
+
</a>
|
|
139
187
|
`
|
|
188
|
+
: ''}
|
|
189
|
+
</div>
|
|
190
|
+
`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The message body — plain text, or an opt-in pill when an
|
|
194
|
+
* opt-in request carries no text of its own. */
|
|
195
|
+
private renderMessageText(item: Msg): TemplateResult | string {
|
|
196
|
+
const isOptin = item.msg_type === 'optin' || item.type === 'optin';
|
|
197
|
+
if (!item.text && isOptin) {
|
|
198
|
+
return this.renderStatusPill('pending', 'opt-in request');
|
|
199
|
+
}
|
|
200
|
+
return item.text || '';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Attachment thumbnails for a row, sitting immediately after the
|
|
204
|
+
* message text, or '' when the row carries none. */
|
|
205
|
+
private renderAttachments(item: Msg): TemplateResult | string {
|
|
206
|
+
const attachments = item.attachments || [];
|
|
207
|
+
if (!attachments.length) return '';
|
|
208
|
+
return html`
|
|
209
|
+
<div class="msg-attachments">
|
|
210
|
+
${attachments.map(
|
|
211
|
+
(a) => html`
|
|
212
|
+
<temba-thumbnail
|
|
213
|
+
class="msg-thumb"
|
|
214
|
+
attachment=${a}
|
|
215
|
+
></temba-thumbnail>
|
|
216
|
+
`
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Flow + label pills for a row, pushed to the trailing edge of
|
|
223
|
+
* the message cell, or '' when the row carries none. */
|
|
224
|
+
private renderPills(item: Msg): TemplateResult | string {
|
|
225
|
+
const labels = item.labels || [];
|
|
226
|
+
if (!item.flow && !labels.length) return '';
|
|
227
|
+
return html`
|
|
228
|
+
<div class="cell-pills">
|
|
229
|
+
${item.flow
|
|
230
|
+
? html`<temba-label type="flow" icon=${Icon.flow}
|
|
231
|
+
>${item.flow.name}</temba-label
|
|
232
|
+
>`
|
|
140
233
|
: null}
|
|
234
|
+
${labels.map(
|
|
235
|
+
(l) => html`
|
|
236
|
+
<temba-label type="label" icon=${Icon.label}>${l.name}</temba-label>
|
|
237
|
+
`
|
|
238
|
+
)}
|
|
141
239
|
</div>
|
|
142
240
|
`;
|
|
143
241
|
}
|