@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.
- package/CHANGELOG.md +16 -0
- package/dist/temba-components.js +1305 -530
- package/dist/temba-components.js.map +1 -1
- package/orca/setup.sh +81 -0
- package/orca.yaml +3 -0
- package/package.json +1 -1
- package/src/display/Dropdown.ts +17 -5
- package/src/events/eventRenderers.ts +4 -9
- package/src/flow/CanvasNode.ts +14 -6
- package/src/flow/DragManager.ts +4 -2
- package/src/flow/utils.ts +1 -0
- package/src/form/DatePicker.ts +2 -1
- package/src/interfaces.ts +4 -1
- package/src/layout/Tab.ts +0 -15
- package/src/layout/TabPane.ts +76 -164
- package/src/list/ContactList.ts +225 -0
- package/src/list/ContentList.ts +1298 -0
- package/src/list/FlowList.ts +251 -0
- package/src/list/MsgList.ts +144 -0
- package/src/live/ContactChat.ts +6 -2
- package/src/live/ContactDetails.ts +40 -35
- package/src/live/ContactFieldEditor.ts +35 -55
- package/src/live/ContactFields.ts +1 -2
- package/src/live/ContactNotepad.ts +9 -1
- package/src/live/ContactPending.ts +1 -0
- package/src/styles/designTokens.ts +2 -0
- package/static/api/flow-labels.json +31 -0
- package/static/css/temba-components.css +2 -0
- package/temba-modules.ts +8 -0
- package/web-dev-server.config.mjs +156 -0
|
@@ -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 {
|
|
@@ -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) => {
|