@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/ContactList.ts
CHANGED
|
@@ -10,18 +10,23 @@ const FIELD_PREFIX = 'field:';
|
|
|
10
10
|
/**
|
|
11
11
|
* Contact CRUDL list — drop-in replacement for the rapidpro
|
|
12
12
|
* `contacts/contact_list.html` table. Each row carries a contact
|
|
13
|
-
* silhouette as the leading icon,
|
|
14
|
-
* last-seen
|
|
13
|
+
* silhouette as the leading icon, then the system columns (name,
|
|
14
|
+
* URN, last-seen) followed by the workspace's featured fields.
|
|
15
|
+
* Name + Last-seen are sortable.
|
|
15
16
|
*
|
|
16
|
-
* Featured contact fields
|
|
17
|
-
* columns
|
|
17
|
+
* Featured contact fields render as extra columns after the system
|
|
18
|
+
* columns. The component fetches them from
|
|
18
19
|
* {@link ContactList.fieldsEndpoint} on connect; cells read each
|
|
19
|
-
* contact's value out of `item.fields[<key>]`.
|
|
20
|
+
* contact's value out of `item.fields[<key>]`. Date/time fields
|
|
21
|
+
* render as a relative duration, matching the Last-seen column.
|
|
20
22
|
*/
|
|
21
23
|
export class ContactList extends ContentList<Contact> {
|
|
22
24
|
static get styles() {
|
|
23
25
|
return css`
|
|
24
26
|
${ContentList.styles}
|
|
27
|
+
/* The contact name is the one cell that carries a slightly
|
|
28
|
+
heavier weight — every other cell stays at the regular
|
|
29
|
+
table weight so values don't read as emphasised. */
|
|
25
30
|
.contact-name {
|
|
26
31
|
color: inherit;
|
|
27
32
|
font-weight: var(--w-medium);
|
|
@@ -33,23 +38,6 @@ export class ContactList extends ContentList<Contact> {
|
|
|
33
38
|
color: var(--text-3);
|
|
34
39
|
font-size: 12.5px;
|
|
35
40
|
}
|
|
36
|
-
/* Featured-field values are concrete data (someone's age,
|
|
37
|
-
their state, etc.) — bold text, not a pill. Truncated
|
|
38
|
-
with a title tooltip so long entries don't blow up the
|
|
39
|
-
row width. */
|
|
40
|
-
.field-value {
|
|
41
|
-
font-weight: var(--w-semibold);
|
|
42
|
-
overflow: hidden;
|
|
43
|
-
text-overflow: ellipsis;
|
|
44
|
-
white-space: nowrap;
|
|
45
|
-
display: block;
|
|
46
|
-
}
|
|
47
|
-
.group-list {
|
|
48
|
-
display: flex;
|
|
49
|
-
align-items: center;
|
|
50
|
-
gap: 4px;
|
|
51
|
-
flex-wrap: wrap;
|
|
52
|
-
}
|
|
53
41
|
`;
|
|
54
42
|
}
|
|
55
43
|
|
|
@@ -129,28 +117,59 @@ export class ContactList extends ContentList<Contact> {
|
|
|
129
117
|
}
|
|
130
118
|
}
|
|
131
119
|
|
|
132
|
-
/** Columns: name, urn,
|
|
120
|
+
/** Columns: name, urn, the featured fields, then last seen.
|
|
121
|
+
*
|
|
122
|
+
* Name + URN lead and Last-seen trails, with the workspace's
|
|
123
|
+
* custom fields filling the middle. Every column sizes to its
|
|
124
|
+
* content between min/max bounds — none are hard-fixed — so the
|
|
125
|
+
* table stays compact and overflows into a horizontal scroll only
|
|
126
|
+
* when the field set is genuinely wide. Name + URN are pinned to
|
|
127
|
+
* the left edge and Last-seen to the right, so identity and
|
|
128
|
+
* recency stay anchored while the fields scroll between them.
|
|
129
|
+
*
|
|
130
|
+
* There is deliberately no group-membership column — contacts
|
|
131
|
+
* routinely belong to dozens of groups, so a groups cell is
|
|
132
|
+
* noise rather than signal in a list view. */
|
|
133
133
|
private buildColumns(): ContentListColumn[] {
|
|
134
|
+
// Custom-field columns are all left-aligned for simplicity, and
|
|
135
|
+
// set no minWidth — their natural floor is the column header
|
|
136
|
+
// label, which the table's auto layout already honours; maxWidth
|
|
137
|
+
// just caps a runaway value.
|
|
134
138
|
const fieldColumns: ContentListColumn[] = (this.featuredFields || []).map(
|
|
135
139
|
(f: any) => ({
|
|
136
140
|
key: FIELD_PREFIX + f.key,
|
|
137
141
|
label: f.name || f.label || f.key,
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
sortable: true,
|
|
143
|
+
maxWidth: '200px'
|
|
140
144
|
})
|
|
141
145
|
);
|
|
146
|
+
// Name + URN are the pinned identity columns and are not
|
|
147
|
+
// sortable — every other column (last-seen and the custom
|
|
148
|
+
// fields) is.
|
|
142
149
|
return [
|
|
143
|
-
{
|
|
144
|
-
|
|
150
|
+
{
|
|
151
|
+
key: 'name',
|
|
152
|
+
label: 'Name',
|
|
153
|
+
minWidth: '150px',
|
|
154
|
+
maxWidth: '260px',
|
|
155
|
+
pinned: true
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
key: 'urn',
|
|
159
|
+
label: 'URN',
|
|
160
|
+
minWidth: '120px',
|
|
161
|
+
maxWidth: '190px',
|
|
162
|
+
pinned: true
|
|
163
|
+
},
|
|
145
164
|
...fieldColumns,
|
|
146
|
-
{ key: 'groups', label: 'Groups', grow: 1 },
|
|
147
165
|
{
|
|
148
166
|
key: 'last_seen_on',
|
|
149
167
|
label: 'Last seen',
|
|
150
168
|
sortable: true,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
align: 'right'
|
|
169
|
+
minWidth: '96px',
|
|
170
|
+
maxWidth: '150px',
|
|
171
|
+
align: 'right',
|
|
172
|
+
pinned: 'right'
|
|
154
173
|
}
|
|
155
174
|
];
|
|
156
175
|
}
|
|
@@ -170,10 +189,14 @@ export class ContactList extends ContentList<Contact> {
|
|
|
170
189
|
if (column.key.startsWith(FIELD_PREFIX)) {
|
|
171
190
|
const fieldKey = column.key.substring(FIELD_PREFIX.length);
|
|
172
191
|
const raw = item.fields?.[fieldKey];
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
192
|
+
if (raw == null || raw === '') return '';
|
|
193
|
+
// Date/time fields render as a relative duration, matching the
|
|
194
|
+
// Last-seen column — never a raw timestamp string.
|
|
195
|
+
if (this.isDateField(fieldKey)) {
|
|
196
|
+
return html`<temba-date value=${raw} display="duration"></temba-date>`;
|
|
197
|
+
}
|
|
198
|
+
const value = String(raw);
|
|
199
|
+
return html`<span title=${value}>${value}</span>`;
|
|
177
200
|
}
|
|
178
201
|
switch (column.key) {
|
|
179
202
|
case 'name':
|
|
@@ -184,8 +207,6 @@ export class ContactList extends ContentList<Contact> {
|
|
|
184
207
|
return html`<span class="contact-urn"
|
|
185
208
|
>${this.primaryUrn(item) || ''}</span
|
|
186
209
|
>`;
|
|
187
|
-
case 'groups':
|
|
188
|
-
return this.renderGroups(item);
|
|
189
210
|
case 'last_seen_on':
|
|
190
211
|
return item.last_seen_on
|
|
191
212
|
? html`<temba-date
|
|
@@ -198,6 +219,16 @@ export class ContactList extends ContentList<Contact> {
|
|
|
198
219
|
}
|
|
199
220
|
}
|
|
200
221
|
|
|
222
|
+
/** True when a featured field stores a date/time value — those
|
|
223
|
+
* cells render via temba-date instead of as plain text. */
|
|
224
|
+
private isDateField(fieldKey: string): boolean {
|
|
225
|
+
const field = (this.featuredFields || []).find(
|
|
226
|
+
(f: any) => f.key === fieldKey
|
|
227
|
+
);
|
|
228
|
+
const type = field?.value_type;
|
|
229
|
+
return type === 'datetime' || type === 'date';
|
|
230
|
+
}
|
|
231
|
+
|
|
201
232
|
private primaryUrn(item: Contact): string {
|
|
202
233
|
const i = item as any;
|
|
203
234
|
if (i.urn) return i.urn;
|
|
@@ -207,19 +238,4 @@ export class ContactList extends ContentList<Contact> {
|
|
|
207
238
|
}
|
|
208
239
|
return '';
|
|
209
240
|
}
|
|
210
|
-
|
|
211
|
-
private renderGroups(item: Contact): TemplateResult {
|
|
212
|
-
const groups = item.groups || [];
|
|
213
|
-
if (groups.length === 0) return html``;
|
|
214
|
-
return html`
|
|
215
|
-
<div class="group-list">
|
|
216
|
-
${groups.map(
|
|
217
|
-
(g: any) =>
|
|
218
|
-
html`<temba-label type="group" icon=${Icon.group}
|
|
219
|
-
>${g.name}</temba-label
|
|
220
|
-
>`
|
|
221
|
-
)}
|
|
222
|
-
</div>
|
|
223
|
-
`;
|
|
224
|
-
}
|
|
225
241
|
}
|