@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.
@@ -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, name + URN, group pills, and a
14
- * last-seen duration. Name + Last-seen are sortable.
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 from the workspace render as extra
17
- * columns between URN and Groups. The component fetches them from
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, <featured fields>, groups, last seen. */
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
- width: '110px',
139
- grow: 0
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
- { key: 'name', label: 'Name', sortable: true, grow: 2 },
144
- { key: 'urn', label: 'URN', width: '150px', grow: 0 },
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
- width: '110px',
152
- grow: 0,
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
- const value = raw == null || raw === '' ? '' : String(raw);
174
- return value
175
- ? html`<span class="field-value" title=${value}>${value}</span>`
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
  }