@nyaruka/temba-components 0.157.1 → 0.158.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.
@@ -3,6 +3,7 @@ import { property } from 'lit/decorators.js';
3
3
  import { ContactStoreElement } from './ContactStoreElement';
4
4
  import { getDisplayName } from './ContactChat';
5
5
  import { ContactNote, CustomEventType } from '../interfaces';
6
+ import { designTokens } from '../styles/designTokens';
6
7
 
7
8
  export class ContactNotepad extends ContactStoreElement {
8
9
  @property({ type: Object, attribute: false })
@@ -14,9 +15,12 @@ export class ContactNotepad extends ContactStoreElement {
14
15
 
15
16
  static get styles() {
16
17
  return css`
18
+ ${designTokens}
19
+
17
20
  :host {
18
21
  height: 100%;
19
22
  display: flex;
23
+ margin-top: var(--gap);
20
24
  }
21
25
 
22
26
  .wrapper {
@@ -24,9 +28,13 @@ export class ContactNotepad extends ContactStoreElement {
24
28
  --color-widget-bg: transparent;
25
29
  --color-widget-bg-focused: transparent;
26
30
  outline: none;
27
- border-radius: var(--curvature);
28
31
  display: flex;
29
32
  flex-direction: column;
33
+ background: var(--surface-note);
34
+ border: 1px solid var(--border-note);
35
+ border-radius: var(--r-sm);
36
+ box-shadow: var(--shadow-2);
37
+ overflow: hidden;
30
38
  }
31
39
 
32
40
  .notepad {
@@ -92,6 +92,7 @@ export class ContactPending extends EndpointMonitorElement {
92
92
  display: flex;
93
93
  flex-direction: row;
94
94
  align-items: center;
95
+ background: var(--surface);
95
96
  box-shadow:
96
97
  0 0 8px 1px rgba(0, 0, 0, 0.055),
97
98
  0 0 0px 1px rgba(0, 0, 0, 0.02);
@@ -32,9 +32,11 @@ export const designTokens = css`
32
32
  /* neutrals */
33
33
  --bg: #f6f7f9;
34
34
  --surface: #ffffff;
35
+ --surface-note: #fff9c2;
35
36
  --sunken: #f1f3f5;
36
37
  --border: #e6e8ec;
37
38
  --border-strong: #d2d6dc;
39
+ --border-note: #ebdf6f;
38
40
  --text-1: #1a1f26;
39
41
  --text-2: #4d5664;
40
42
  --text-3: #7b8593;
@@ -0,0 +1,31 @@
1
+ {
2
+ "next": null,
3
+ "previous": null,
4
+ "results": [
5
+ {
6
+ "uuid": "fl-001",
7
+ "name": "Active Campaigns",
8
+ "count": 12
9
+ },
10
+ {
11
+ "uuid": "fl-002",
12
+ "name": "Surveys",
13
+ "count": 8
14
+ },
15
+ {
16
+ "uuid": "fl-003",
17
+ "name": "Onboarding",
18
+ "count": 4
19
+ },
20
+ {
21
+ "uuid": "fl-004",
22
+ "name": "Internal",
23
+ "count": 2
24
+ },
25
+ {
26
+ "uuid": "fl-005",
27
+ "name": "Archived",
28
+ "count": 6
29
+ }
30
+ ]
31
+ }
@@ -30,9 +30,11 @@
30
30
  /* neutrals */
31
31
  --bg: #F6F7F9;
32
32
  --surface: #FFFFFF;
33
+ --surface-note: #FFF9C2;
33
34
  --sunken: #F1F3F5;
34
35
  --border: #E6E8EC;
35
36
  --border-strong: #D2D6DC;
37
+ --border-note: #EBDF6F;
36
38
  --text-1: #1A1F26;
37
39
  --text-2: #4D5664;
38
40
  --text-3: #7B8593;
package/temba-modules.ts CHANGED
@@ -47,6 +47,10 @@ import { ColorPicker } from './src/form/ColorPicker';
47
47
  import { Resizer } from './src/layout/Resizer';
48
48
  import { Thumbnail } from './src/display/Thumbnail';
49
49
  import { NotificationList } from './src/list/NotificationList';
50
+ import { ContentList } from './src/list/ContentList';
51
+ import { MsgList } from './src/list/MsgList';
52
+ import { ContactList } from './src/list/ContactList';
53
+ import { FlowList } from './src/list/FlowList';
50
54
  import { WebChat } from './src/webchat/WebChat';
51
55
  import { ImagePicker } from './src/form/ImagePicker';
52
56
  import { Mask } from './src/layout/Mask';
@@ -127,6 +131,10 @@ addCustomElement('temba-contact-chat', ContactChat);
127
131
  addCustomElement('temba-contact-details', ContactDetails);
128
132
  addCustomElement('temba-ticket-list', TicketList);
129
133
  addCustomElement('temba-notification-list', NotificationList);
134
+ addCustomElement('temba-content-list', ContentList);
135
+ addCustomElement('temba-msg-list', MsgList);
136
+ addCustomElement('temba-contact-list', ContactList);
137
+ addCustomElement('temba-flow-list', FlowList);
130
138
  addCustomElement('temba-list', TembaList);
131
139
  addCustomElement('temba-sortable-list', SortableList);
132
140
  addCustomElement('temba-run-list', RunList);
@@ -223,6 +223,72 @@ function generateFlowMetadata(flowDefinition) {
223
223
  return generateFlowInfo(flowDefinition);
224
224
  }
225
225
 
226
+ // In-memory state for the content-list demo so the labeling flow
227
+ // (label, refresh, recheck) is exercisable end-to-end. The first
228
+ // GET / POST loads from the on-disk fixture; subsequent ops mutate
229
+ // the in-memory copy. The on-disk fixture is never written — state
230
+ // resets on dev-server restart, which is the right default for a
231
+ // throwaway demo.
232
+ const demoState = {
233
+ messages: null,
234
+ flows: null,
235
+ labels: null,
236
+ flowLabels: null
237
+ };
238
+
239
+ function loadDemoJson(stateKey, filePath) {
240
+ if (demoState[stateKey] === null) {
241
+ demoState[stateKey] = JSON.parse(
242
+ fs.readFileSync(path.resolve(filePath), 'utf-8')
243
+ );
244
+ }
245
+ return demoState[stateKey];
246
+ }
247
+
248
+ /** Filter the in-memory demo items for a content-list endpoint.
249
+ *
250
+ * Supports `?label=<uuid>` (only items carrying that label) so the
251
+ * demo can exercise the filtered-view → label-removed → row-drops-
252
+ * out → recheck-selection lifecycle. */
253
+ function getFilteredDemoItems(data, url) {
254
+ const labelFilter = url.searchParams.get('label');
255
+ let results = data.results;
256
+ if (labelFilter) {
257
+ results = results.filter((item) =>
258
+ (item.labels || []).some((l) => l.uuid === labelFilter)
259
+ );
260
+ }
261
+ return { ...data, count: results.length, results };
262
+ }
263
+
264
+ /** Apply a label-toggle to a list of in-memory items, mirroring
265
+ * smartmin's BulkActionMixin behavior. Body is x-www-form-urlencoded
266
+ * (params: action, objects[], label, add). The `idKey` is what each
267
+ * item is matched against (messages use numeric `id`, flows use
268
+ * string `uuid`). */
269
+ function applyDemoListAction(body, items, labels, idKey) {
270
+ const params = new URLSearchParams(body);
271
+ const action = params.get('action');
272
+ if (action !== 'label') return;
273
+ const labelUuid = params.get('label');
274
+ const add = params.get('add') !== 'false';
275
+ const objectIds = params.getAll('objects');
276
+ const label = (labels.results || []).find((l) => l.uuid === labelUuid);
277
+ if (!label) return;
278
+ objectIds.forEach((idStr) => {
279
+ const lookup = idKey === 'id' ? parseInt(idStr, 10) : idStr;
280
+ const item = items.results.find((i) => i[idKey] === lookup);
281
+ if (!item) return;
282
+ item.labels = item.labels || [];
283
+ const idx = item.labels.findIndex((l) => l.uuid === labelUuid);
284
+ if (add && idx < 0) {
285
+ item.labels.push({ uuid: label.uuid, name: label.name });
286
+ } else if (!add && idx >= 0) {
287
+ item.labels.splice(idx, 1);
288
+ }
289
+ });
290
+ }
291
+
226
292
  export default {
227
293
  nodeResolve: true,
228
294
  plugins: [
@@ -244,6 +310,7 @@ export default {
244
310
  const apiMappings = {
245
311
  '/api/v2/groups.json': 'groups.json',
246
312
  '/api/v2/labels.json': 'labels.json',
313
+ '/api/v2/flow-labels.json': 'flow-labels.json',
247
314
  '/api/v2/fields.json': 'fields.json',
248
315
  '/api/v2/globals.json': 'globals.json',
249
316
  '/api/v2/resthooks.json': 'resthooks.json',
@@ -363,6 +430,95 @@ export default {
363
430
  return;
364
431
  }
365
432
 
433
+ // Serve the content-list demo messages from in-memory state
434
+ // so a labeling POST is reflected on the next refresh. Honors
435
+ // an optional `?label=<uuid>` filter for testing the filtered-
436
+ // view drop-out lifecycle.
437
+ if (
438
+ context.request.method === 'GET' &&
439
+ context.path === '/demo/components/content-list/data/messages.json'
440
+ ) {
441
+ const reqUrl = new URL(context.request.url, 'http://localhost');
442
+ const data = loadDemoJson(
443
+ 'messages',
444
+ './demo/components/content-list/data/messages.json'
445
+ );
446
+ context.contentType = 'application/json';
447
+ context.body = JSON.stringify(getFilteredDemoItems(data, reqUrl));
448
+ return;
449
+ }
450
+
451
+ // Same lifecycle for the flows list — labels carried on
452
+ // each flow live in flows.json; the labels themselves come
453
+ // from /api/v2/flow-labels.json.
454
+ if (
455
+ context.request.method === 'GET' &&
456
+ context.path === '/demo/components/content-list/data/flows.json'
457
+ ) {
458
+ const reqUrl = new URL(context.request.url, 'http://localhost');
459
+ const data = loadDemoJson(
460
+ 'flows',
461
+ './demo/components/content-list/data/flows.json'
462
+ );
463
+ context.contentType = 'application/json';
464
+ context.body = JSON.stringify(getFilteredDemoItems(data, reqUrl));
465
+ return;
466
+ }
467
+
468
+ // Bulk-action POSTs for the content-list demo: mutate the
469
+ // in-memory items so the subsequent refresh actually shows
470
+ // the labeling change. Body is form-urlencoded as sent by
471
+ // ContentList.toggleLabel().
472
+ if (
473
+ context.request.method === 'POST' &&
474
+ context.path === '/demo/components/content-list/list-action'
475
+ ) {
476
+ return new Promise((resolve) => {
477
+ let body = '';
478
+ context.req.on('data', (chunk) => { body += chunk.toString(); });
479
+ context.req.on('end', () => {
480
+ const messages = loadDemoJson(
481
+ 'messages',
482
+ './demo/components/content-list/data/messages.json'
483
+ );
484
+ const labels = loadDemoJson(
485
+ 'labels',
486
+ './static/api/labels.json'
487
+ );
488
+ applyDemoListAction(body, messages, labels, 'id');
489
+ context.contentType = 'application/json';
490
+ context.status = 200;
491
+ context.body = JSON.stringify({ status: 'success' });
492
+ resolve();
493
+ });
494
+ });
495
+ }
496
+
497
+ if (
498
+ context.request.method === 'POST' &&
499
+ context.path === '/demo/components/content-list/flow-list-action'
500
+ ) {
501
+ return new Promise((resolve) => {
502
+ let body = '';
503
+ context.req.on('data', (chunk) => { body += chunk.toString(); });
504
+ context.req.on('end', () => {
505
+ const flows = loadDemoJson(
506
+ 'flows',
507
+ './demo/components/content-list/data/flows.json'
508
+ );
509
+ const labels = loadDemoJson(
510
+ 'flowLabels',
511
+ './static/api/flow-labels.json'
512
+ );
513
+ applyDemoListAction(body, flows, labels, 'uuid');
514
+ context.contentType = 'application/json';
515
+ context.status = 200;
516
+ context.body = JSON.stringify({ status: 'success' });
517
+ resolve();
518
+ });
519
+ });
520
+ }
521
+
366
522
  // Handle contact chat POST (send message) - return a mock event
367
523
  if (context.request.method === 'POST' && context.path.match(/^\/contact\/chat\/[^/]+\/$/)) {
368
524
  return new Promise((resolve) => {