@nyaruka/temba-components 0.142.1 → 0.142.2
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 +11 -0
- package/dist/temba-components.js +825 -654
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/Icons.js +1 -0
- package/out-tsc/src/Icons.js.map +1 -1
- package/out-tsc/src/flow/CanvasMenu.js +30 -35
- package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +13 -8
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +18 -5
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +346 -10
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/NodeTypeSelector.js +2 -0
- package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +3 -1
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/flow/actions/add_contact_urn.js +2 -6
- package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
- package/out-tsc/src/flow/actions/enter_flow.js +2 -2
- package/out-tsc/src/flow/actions/enter_flow.js.map +1 -1
- package/out-tsc/src/flow/actions/say_msg.js +2 -1
- package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
- package/out-tsc/src/flow/actions/send_broadcast.js +2 -6
- package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
- package/out-tsc/src/flow/actions/send_email.js +2 -6
- package/out-tsc/src/flow/actions/send_email.js.map +1 -1
- package/out-tsc/src/flow/actions/send_msg.js +52 -35
- package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_channel.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_field.js +4 -5
- package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_language.js +3 -3
- package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_name.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_status.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
- package/out-tsc/src/flow/actions/set_run_result.js +3 -3
- package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
- package/out-tsc/src/flow/actions/start_session.js +2 -2
- package/out-tsc/src/flow/actions/start_session.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_llm.js +4 -5
- package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_resthook.js +3 -8
- package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_subflow.js +2 -2
- package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_webhook.js +25 -33
- package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_response.js +1 -0
- package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/flow/utils.js +68 -0
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/form/FieldRenderer.js +17 -2
- package/out-tsc/src/form/FieldRenderer.js.map +1 -1
- package/out-tsc/src/interfaces.js +1 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +1 -1
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/test/temba-canvas-menu.test.js +13 -9
- package/out-tsc/test/temba-canvas-menu.test.js.map +1 -1
- package/out-tsc/test/temba-flow-reflow.test.js.map +1 -1
- package/out-tsc/test/temba-node-editor.test.js +9 -10
- package/out-tsc/test/temba-node-editor.test.js.map +1 -1
- package/out-tsc/test/temba-node-type-selector.test.js +3 -3
- package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
- package/out-tsc/test/temba-simulator.test.js +2 -2
- package/out-tsc/test/temba-simulator.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/enter_flow/render/basic-flow.png +0 -0
- package/screenshots/truth/actions/enter_flow/render/long-flow-name.png +0 -0
- package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
- package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
- package/screenshots/truth/canvas-menu/open.png +0 -0
- package/screenshots/truth/node-type-selector/action-mode.png +0 -0
- package/screenshots/truth/node-type-selector/split-mode.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
- package/src/Icons.ts +1 -0
- package/src/flow/CanvasMenu.ts +38 -39
- package/src/flow/CanvasNode.ts +16 -8
- package/src/flow/Editor.ts +33 -6
- package/src/flow/NodeEditor.ts +373 -10
- package/src/flow/NodeTypeSelector.ts +2 -0
- package/src/flow/Plumber.ts +3 -1
- package/src/flow/actions/add_contact_urn.ts +5 -6
- package/src/flow/actions/enter_flow.ts +2 -2
- package/src/flow/actions/say_msg.ts +2 -1
- package/src/flow/actions/send_broadcast.ts +2 -6
- package/src/flow/actions/send_email.ts +2 -6
- package/src/flow/actions/send_msg.ts +56 -38
- package/src/flow/actions/set_contact_channel.ts +5 -1
- package/src/flow/actions/set_contact_field.ts +10 -5
- package/src/flow/actions/set_contact_language.ts +6 -3
- package/src/flow/actions/set_contact_name.ts +5 -1
- package/src/flow/actions/set_contact_status.ts +5 -1
- package/src/flow/actions/set_run_result.ts +6 -3
- package/src/flow/actions/start_session.ts +2 -2
- package/src/flow/nodes/split_by_llm.ts +5 -5
- package/src/flow/nodes/split_by_resthook.ts +3 -8
- package/src/flow/nodes/split_by_subflow.ts +2 -2
- package/src/flow/nodes/split_by_webhook.ts +26 -34
- package/src/flow/nodes/wait_for_response.ts +1 -0
- package/src/flow/types.ts +25 -2
- package/src/flow/utils.ts +81 -1
- package/src/form/FieldRenderer.ts +32 -3
- package/src/interfaces.ts +1 -0
- package/src/simulator/Simulator.ts +1 -1
- package/test/temba-canvas-menu.test.ts +13 -9
- package/test/temba-flow-reflow.test.ts +4 -2
- package/test/temba-node-editor.test.ts +9 -10
- package/test/temba-node-type-selector.test.ts +3 -3
- package/test/temba-simulator.test.ts +2 -2
package/src/flow/NodeEditor.ts
CHANGED
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
LayoutItem,
|
|
15
15
|
RowLayoutConfig,
|
|
16
16
|
GroupLayoutConfig,
|
|
17
|
+
AccordionLayoutConfig,
|
|
18
|
+
AccordionSection,
|
|
17
19
|
FormData,
|
|
18
20
|
ACTION_GROUP_METADATA,
|
|
19
21
|
SPLIT_GROUP_METADATA
|
|
@@ -155,6 +157,25 @@ export class NodeEditor extends RapidElement {
|
|
|
155
157
|
overflow: hidden;
|
|
156
158
|
}
|
|
157
159
|
|
|
160
|
+
.form-group.no-border {
|
|
161
|
+
border: none;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.form-group.no-border > .form-group-header {
|
|
165
|
+
background: none;
|
|
166
|
+
border-bottom: none;
|
|
167
|
+
padding-left: 11px; /* 1px border + 10px padding to align with bordered groups */
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.form-group.no-border > .form-group-header:hover {
|
|
171
|
+
background: none;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.form-group.no-border > .form-group-content {
|
|
175
|
+
padding-left: 0;
|
|
176
|
+
padding-right: 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
158
179
|
.form-group.has-errors {
|
|
159
180
|
border-color: var(--color-error, tomato);
|
|
160
181
|
}
|
|
@@ -319,6 +340,135 @@ export class NodeEditor extends RapidElement {
|
|
|
319
340
|
align-items: center;
|
|
320
341
|
}
|
|
321
342
|
|
|
343
|
+
/* Accordion styles */
|
|
344
|
+
.accordion {
|
|
345
|
+
border: 1px solid #e0e0e0;
|
|
346
|
+
border-radius: 6px;
|
|
347
|
+
overflow: hidden;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.accordion-section {
|
|
351
|
+
border-bottom: 1px solid #e0e0e0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.accordion-section:last-child {
|
|
355
|
+
border-bottom: none;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.accordion-header {
|
|
359
|
+
display: flex;
|
|
360
|
+
align-items: center;
|
|
361
|
+
justify-content: space-between;
|
|
362
|
+
padding: 6px 10px;
|
|
363
|
+
cursor: pointer;
|
|
364
|
+
user-select: none;
|
|
365
|
+
background: #f8f9fa;
|
|
366
|
+
transition: background 0.15s ease;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.accordion-header:hover {
|
|
370
|
+
background: #f0f1f2;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.accordion-section.expanded > .accordion-header {
|
|
374
|
+
border-bottom: 1px solid #e0e0e0;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.accordion-title {
|
|
378
|
+
font-weight: 500;
|
|
379
|
+
font-size: 13px;
|
|
380
|
+
color: var(--color-label, #777);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.accordion-toggle-container {
|
|
384
|
+
position: relative;
|
|
385
|
+
display: flex;
|
|
386
|
+
align-items: center;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.accordion-toggle-icon {
|
|
390
|
+
color: #999;
|
|
391
|
+
transition:
|
|
392
|
+
transform 0.2s ease,
|
|
393
|
+
opacity 0.3s ease;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.accordion-toggle-icon.expanded {
|
|
397
|
+
transform: rotate(90deg);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.accordion-toggle-icon.faded {
|
|
401
|
+
opacity: 0;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.accordion-count-bubble {
|
|
405
|
+
border-radius: 50%;
|
|
406
|
+
display: flex;
|
|
407
|
+
align-items: center;
|
|
408
|
+
justify-content: center;
|
|
409
|
+
font-size: 10px;
|
|
410
|
+
font-weight: 600;
|
|
411
|
+
padding: 3px;
|
|
412
|
+
min-width: 10px;
|
|
413
|
+
min-height: 10px;
|
|
414
|
+
position: absolute;
|
|
415
|
+
top: 50%;
|
|
416
|
+
left: 50%;
|
|
417
|
+
transform: translate(-50%, -50%);
|
|
418
|
+
line-height: 0px;
|
|
419
|
+
opacity: 1;
|
|
420
|
+
transition: opacity 0.3s ease;
|
|
421
|
+
background: var(--color-bubble-bg, #fff);
|
|
422
|
+
border: 1px solid var(--color-bubble-border, #777);
|
|
423
|
+
color: var(--color-bubble-text, #000);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.accordion-count-bubble.hidden {
|
|
427
|
+
opacity: 0;
|
|
428
|
+
pointer-events: none;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.accordion-checkmark-icon {
|
|
432
|
+
position: absolute;
|
|
433
|
+
top: 50%;
|
|
434
|
+
left: 50%;
|
|
435
|
+
transform: translate(-50%, -50%);
|
|
436
|
+
opacity: 1;
|
|
437
|
+
transition: opacity 0.3s ease;
|
|
438
|
+
border-radius: 50%;
|
|
439
|
+
color: var(--color-bubble-text, #000);
|
|
440
|
+
background: var(--color-bubble-bg, #fff);
|
|
441
|
+
border: 1px solid var(--color-bubble-border, #777);
|
|
442
|
+
padding: 0.15em;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.accordion-checkmark-icon.hidden {
|
|
446
|
+
opacity: 0;
|
|
447
|
+
pointer-events: none;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.accordion-content {
|
|
451
|
+
padding: 8px 10px;
|
|
452
|
+
display: flex;
|
|
453
|
+
flex-direction: column;
|
|
454
|
+
gap: 8px;
|
|
455
|
+
overflow: hidden;
|
|
456
|
+
transition: all 0.2s ease-in-out;
|
|
457
|
+
opacity: 1;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.accordion-content.collapsed {
|
|
461
|
+
max-height: 0;
|
|
462
|
+
padding-top: 0;
|
|
463
|
+
padding-bottom: 0;
|
|
464
|
+
opacity: 0;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.accordion-error-icon {
|
|
468
|
+
color: var(--color-error, tomato);
|
|
469
|
+
margin-right: 6px;
|
|
470
|
+
}
|
|
471
|
+
|
|
322
472
|
.gutter-fields {
|
|
323
473
|
display: flex;
|
|
324
474
|
flex-direction: column;
|
|
@@ -1261,15 +1411,20 @@ export class NodeEditor extends RapidElement {
|
|
|
1261
1411
|
const { label, collapsed, collapsible } = item;
|
|
1262
1412
|
|
|
1263
1413
|
// Only update if the group is collapsible and has a function-based collapsed property
|
|
1414
|
+
// Skip reveal groups that have been expanded — they are one-way
|
|
1264
1415
|
if (collapsible && typeof collapsed === 'function') {
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1416
|
+
if (item.reveal && this.groupCollapseState[label] === false) {
|
|
1417
|
+
// Reveal group was manually expanded — don't re-collapse
|
|
1418
|
+
} else {
|
|
1419
|
+
const newCollapsedState = collapsed(this.formData);
|
|
1420
|
+
|
|
1421
|
+
// Only update if the state has changed to avoid unnecessary re-renders
|
|
1422
|
+
if (this.groupCollapseState[label] !== newCollapsedState) {
|
|
1423
|
+
this.groupCollapseState = {
|
|
1424
|
+
...this.groupCollapseState,
|
|
1425
|
+
[label]: newCollapsedState
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1273
1428
|
}
|
|
1274
1429
|
}
|
|
1275
1430
|
|
|
@@ -1278,6 +1433,21 @@ export class NodeEditor extends RapidElement {
|
|
|
1278
1433
|
} else if (typeof item === 'object' && item.type === 'row') {
|
|
1279
1434
|
// Recursively check items in rows
|
|
1280
1435
|
this.updateGroupCollapseStatesRecursive(item.items);
|
|
1436
|
+
} else if (typeof item === 'object' && item.type === 'accordion') {
|
|
1437
|
+
// Check each accordion section
|
|
1438
|
+
item.sections.forEach((section) => {
|
|
1439
|
+
const stateKey = `accordion:${section.label}`;
|
|
1440
|
+
if (typeof section.collapsed === 'function') {
|
|
1441
|
+
const newCollapsedState = section.collapsed(this.formData);
|
|
1442
|
+
if (this.groupCollapseState[stateKey] !== newCollapsedState) {
|
|
1443
|
+
this.groupCollapseState = {
|
|
1444
|
+
...this.groupCollapseState,
|
|
1445
|
+
[stateKey]: newCollapsedState
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
this.updateGroupCollapseStatesRecursive(section.items);
|
|
1450
|
+
});
|
|
1281
1451
|
}
|
|
1282
1452
|
});
|
|
1283
1453
|
}
|
|
@@ -1518,6 +1688,9 @@ export class NodeEditor extends RapidElement {
|
|
|
1518
1688
|
const picker = e.target as any;
|
|
1519
1689
|
const url = picker.attachments?.[0]?.url || '';
|
|
1520
1690
|
this.handleNewFieldChange(fieldName, url);
|
|
1691
|
+
} else if (fieldName && config.type === 'template-editor') {
|
|
1692
|
+
// Special handling for template editor (manages template + template_variables)
|
|
1693
|
+
this.handleTemplateEditorChange(fieldName, e);
|
|
1521
1694
|
} else {
|
|
1522
1695
|
// Default handling for most field types
|
|
1523
1696
|
this.handleFormFieldChange(fieldName, e);
|
|
@@ -1526,7 +1699,8 @@ export class NodeEditor extends RapidElement {
|
|
|
1526
1699
|
showLabel: true,
|
|
1527
1700
|
formData: this.formData,
|
|
1528
1701
|
additionalData: {
|
|
1529
|
-
attachments: this.formData.attachments || []
|
|
1702
|
+
attachments: this.formData.attachments || [],
|
|
1703
|
+
template_variables: this.formData.template_variables || []
|
|
1530
1704
|
}
|
|
1531
1705
|
});
|
|
1532
1706
|
}
|
|
@@ -1584,6 +1758,21 @@ export class NodeEditor extends RapidElement {
|
|
|
1584
1758
|
} else if (typeof item === 'object' && item.type === 'row') {
|
|
1585
1759
|
// Recursively check items in rows
|
|
1586
1760
|
this.expandGroupsWithErrorsRecursive(item.items, errorFields);
|
|
1761
|
+
} else if (typeof item === 'object' && item.type === 'accordion') {
|
|
1762
|
+
// Check each accordion section for errors
|
|
1763
|
+
item.sections.forEach((section) => {
|
|
1764
|
+
const fieldsInSection = this.collectFieldsFromItems(section.items);
|
|
1765
|
+
const sectionHasErrors = fieldsInSection.some((fieldName) =>
|
|
1766
|
+
errorFields.has(fieldName)
|
|
1767
|
+
);
|
|
1768
|
+
if (sectionHasErrors) {
|
|
1769
|
+
this.groupCollapseState = {
|
|
1770
|
+
...this.groupCollapseState,
|
|
1771
|
+
[`accordion:${section.label}`]: false
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
this.expandGroupsWithErrorsRecursive(section.items, errorFields);
|
|
1775
|
+
});
|
|
1587
1776
|
}
|
|
1588
1777
|
});
|
|
1589
1778
|
}
|
|
@@ -1631,6 +1820,9 @@ export class NodeEditor extends RapidElement {
|
|
|
1631
1820
|
case 'group':
|
|
1632
1821
|
return this.renderGroup(item, config, renderedFields);
|
|
1633
1822
|
|
|
1823
|
+
case 'accordion':
|
|
1824
|
+
return this.renderAccordion(item, config, renderedFields);
|
|
1825
|
+
|
|
1634
1826
|
case 'spacer':
|
|
1635
1827
|
return html``;
|
|
1636
1828
|
|
|
@@ -1771,6 +1963,8 @@ export class NodeEditor extends RapidElement {
|
|
|
1771
1963
|
collapsed = false,
|
|
1772
1964
|
helpText,
|
|
1773
1965
|
contentPadding,
|
|
1966
|
+
bordered = true,
|
|
1967
|
+
reveal = false,
|
|
1774
1968
|
getGroupValueCount
|
|
1775
1969
|
} = groupConfig;
|
|
1776
1970
|
|
|
@@ -1791,6 +1985,13 @@ export class NodeEditor extends RapidElement {
|
|
|
1791
1985
|
(typeof collapsed === 'function' ? collapsed(this.formData) : collapsed)
|
|
1792
1986
|
: false;
|
|
1793
1987
|
|
|
1988
|
+
// Reveal mode: once expanded, render items directly without any group wrapper
|
|
1989
|
+
if (reveal && !isCollapsed) {
|
|
1990
|
+
return html`${items.map((item) =>
|
|
1991
|
+
this.renderLayoutItem(item, config, renderedFields)
|
|
1992
|
+
)}`;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1794
1995
|
// Check if any field in this group has errors
|
|
1795
1996
|
const fieldsInGroup = this.collectFieldsFromItems(items);
|
|
1796
1997
|
const groupHasErrors = fieldsInGroup.some(
|
|
@@ -1832,7 +2033,7 @@ export class NodeEditor extends RapidElement {
|
|
|
1832
2033
|
? 'has-errors'
|
|
1833
2034
|
: ''} ${isCollapsed ? 'collapsed' : 'expanded'} ${hasValue
|
|
1834
2035
|
? 'has-bubble'
|
|
1835
|
-
: ''}"
|
|
2036
|
+
: ''} ${!bordered ? 'no-border' : ''}"
|
|
1836
2037
|
>
|
|
1837
2038
|
<div
|
|
1838
2039
|
class="form-group-header ${collapsible ? 'clickable' : ''}"
|
|
@@ -1900,6 +2101,148 @@ export class NodeEditor extends RapidElement {
|
|
|
1900
2101
|
`;
|
|
1901
2102
|
}
|
|
1902
2103
|
|
|
2104
|
+
private renderAccordion(
|
|
2105
|
+
accordionConfig: AccordionLayoutConfig,
|
|
2106
|
+
config: ActionConfig | NodeConfig,
|
|
2107
|
+
renderedFields: Set<string>
|
|
2108
|
+
): TemplateResult {
|
|
2109
|
+
const { sections, multi = false } = accordionConfig;
|
|
2110
|
+
|
|
2111
|
+
return html`
|
|
2112
|
+
<div class="accordion">
|
|
2113
|
+
${sections.map((section) => {
|
|
2114
|
+
const { label, collapsed = true, getValueCount } = section;
|
|
2115
|
+
const stateKey = `accordion:${label}`;
|
|
2116
|
+
|
|
2117
|
+
// Initialize collapse state if not set
|
|
2118
|
+
if (!(stateKey in this.groupCollapseState)) {
|
|
2119
|
+
const initialCollapsed =
|
|
2120
|
+
typeof collapsed === 'function'
|
|
2121
|
+
? collapsed(this.formData)
|
|
2122
|
+
: collapsed;
|
|
2123
|
+
this.groupCollapseState = {
|
|
2124
|
+
...this.groupCollapseState,
|
|
2125
|
+
[stateKey]: initialCollapsed
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
const isCollapsed = this.groupCollapseState[stateKey] ?? true;
|
|
2130
|
+
const isHovered = this.groupHoverState[stateKey] ?? false;
|
|
2131
|
+
|
|
2132
|
+
// Check for errors in this section
|
|
2133
|
+
const fieldsInSection = this.collectFieldsFromItems(section.items);
|
|
2134
|
+
const sectionHasErrors = fieldsInSection.some(
|
|
2135
|
+
(fieldName) => this.errors[fieldName]
|
|
2136
|
+
);
|
|
2137
|
+
|
|
2138
|
+
// Value count / checkmark display
|
|
2139
|
+
let valueCount = 0;
|
|
2140
|
+
let showBubble = false;
|
|
2141
|
+
let showCheckmark = false;
|
|
2142
|
+
let hasValue = false;
|
|
2143
|
+
|
|
2144
|
+
if (getValueCount) {
|
|
2145
|
+
try {
|
|
2146
|
+
const result = getValueCount(this.formData);
|
|
2147
|
+
if (typeof result === 'boolean') {
|
|
2148
|
+
hasValue = result;
|
|
2149
|
+
showCheckmark = result && isCollapsed && !isHovered;
|
|
2150
|
+
} else if (typeof result === 'number') {
|
|
2151
|
+
valueCount = result;
|
|
2152
|
+
hasValue = valueCount > 0;
|
|
2153
|
+
showBubble = valueCount > 0 && isCollapsed && !isHovered;
|
|
2154
|
+
}
|
|
2155
|
+
} catch (error) {
|
|
2156
|
+
// ignore
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
return html`
|
|
2161
|
+
<div
|
|
2162
|
+
class="accordion-section ${isCollapsed
|
|
2163
|
+
? 'collapsed'
|
|
2164
|
+
: 'expanded'} ${hasValue ? 'has-value' : ''}"
|
|
2165
|
+
>
|
|
2166
|
+
<div
|
|
2167
|
+
class="accordion-header"
|
|
2168
|
+
@click=${() =>
|
|
2169
|
+
this.handleAccordionToggle(stateKey, sections, multi)}
|
|
2170
|
+
@mouseenter=${() => this.handleGroupMouseEnter(stateKey)}
|
|
2171
|
+
@mouseleave=${() => this.handleGroupMouseLeave(stateKey)}
|
|
2172
|
+
>
|
|
2173
|
+
<div class="accordion-title">${label}</div>
|
|
2174
|
+
${sectionHasErrors
|
|
2175
|
+
? html`<temba-icon
|
|
2176
|
+
name="alert_warning"
|
|
2177
|
+
class="accordion-error-icon"
|
|
2178
|
+
size="1.2"
|
|
2179
|
+
></temba-icon>`
|
|
2180
|
+
: html`<div class="accordion-toggle-container">
|
|
2181
|
+
<temba-icon
|
|
2182
|
+
name="arrow_right"
|
|
2183
|
+
size="1.2"
|
|
2184
|
+
class="accordion-toggle-icon ${isCollapsed
|
|
2185
|
+
? 'collapsed'
|
|
2186
|
+
: 'expanded'} ${showBubble || showCheckmark
|
|
2187
|
+
? 'faded'
|
|
2188
|
+
: ''}"
|
|
2189
|
+
></temba-icon>
|
|
2190
|
+
${showCheckmark
|
|
2191
|
+
? html`<temba-icon
|
|
2192
|
+
name="check"
|
|
2193
|
+
size="0.8"
|
|
2194
|
+
class="accordion-checkmark-icon"
|
|
2195
|
+
></temba-icon>`
|
|
2196
|
+
: showBubble
|
|
2197
|
+
? html`<div
|
|
2198
|
+
class="accordion-count-bubble ${!showBubble
|
|
2199
|
+
? 'hidden'
|
|
2200
|
+
: ''}"
|
|
2201
|
+
>
|
|
2202
|
+
${valueCount}
|
|
2203
|
+
</div>`
|
|
2204
|
+
: ''}
|
|
2205
|
+
</div>`}
|
|
2206
|
+
</div>
|
|
2207
|
+
<div
|
|
2208
|
+
class="accordion-content ${isCollapsed ? 'collapsed' : 'expanded'}"
|
|
2209
|
+
>
|
|
2210
|
+
${section.items.map((item) =>
|
|
2211
|
+
this.renderLayoutItem(item, config, renderedFields)
|
|
2212
|
+
)}
|
|
2213
|
+
</div>
|
|
2214
|
+
</div>
|
|
2215
|
+
`;
|
|
2216
|
+
})}
|
|
2217
|
+
</div>
|
|
2218
|
+
`;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
private handleAccordionToggle(
|
|
2222
|
+
stateKey: string,
|
|
2223
|
+
sections: AccordionSection[],
|
|
2224
|
+
multi: boolean
|
|
2225
|
+
): void {
|
|
2226
|
+
const isCurrentlyCollapsed = this.groupCollapseState[stateKey] ?? true;
|
|
2227
|
+
|
|
2228
|
+
if (multi) {
|
|
2229
|
+
// Multi mode: just toggle this section
|
|
2230
|
+
this.groupCollapseState = {
|
|
2231
|
+
...this.groupCollapseState,
|
|
2232
|
+
[stateKey]: !isCurrentlyCollapsed
|
|
2233
|
+
};
|
|
2234
|
+
} else {
|
|
2235
|
+
// Single mode: collapse all other sections, toggle this one
|
|
2236
|
+
const newState = { ...this.groupCollapseState };
|
|
2237
|
+
sections.forEach((section) => {
|
|
2238
|
+
const key = `accordion:${section.label}`;
|
|
2239
|
+
newState[key] = true; // collapse all
|
|
2240
|
+
});
|
|
2241
|
+
newState[stateKey] = !isCurrentlyCollapsed; // toggle clicked
|
|
2242
|
+
this.groupCollapseState = newState;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
|
|
1903
2246
|
private collectFieldsFromItems(items: LayoutItem[]): string[] {
|
|
1904
2247
|
const fields: string[] = [];
|
|
1905
2248
|
|
|
@@ -1912,6 +2255,10 @@ export class NodeEditor extends RapidElement {
|
|
|
1912
2255
|
fields.push(...this.collectFieldsFromItems(item.items));
|
|
1913
2256
|
} else if (item.type === 'group') {
|
|
1914
2257
|
fields.push(...this.collectFieldsFromItems(item.items));
|
|
2258
|
+
} else if (item.type === 'accordion') {
|
|
2259
|
+
item.sections.forEach((section) => {
|
|
2260
|
+
fields.push(...this.collectFieldsFromItems(section.items));
|
|
2261
|
+
});
|
|
1915
2262
|
}
|
|
1916
2263
|
});
|
|
1917
2264
|
|
|
@@ -1953,6 +2300,22 @@ export class NodeEditor extends RapidElement {
|
|
|
1953
2300
|
// Trigger re-render
|
|
1954
2301
|
this.requestUpdate();
|
|
1955
2302
|
}
|
|
2303
|
+
private handleTemplateEditorChange(fieldName: string, event: Event): void {
|
|
2304
|
+
const customEvent = event as CustomEvent;
|
|
2305
|
+
const detail = customEvent.detail;
|
|
2306
|
+
|
|
2307
|
+
this.formData = {
|
|
2308
|
+
...this.formData,
|
|
2309
|
+
[fieldName]: detail.template
|
|
2310
|
+
? { uuid: detail.template.uuid, name: detail.template.name }
|
|
2311
|
+
: null,
|
|
2312
|
+
template_variables: detail.variables || []
|
|
2313
|
+
};
|
|
2314
|
+
|
|
2315
|
+
this.updateGroupCollapseStates();
|
|
2316
|
+
this.requestUpdate();
|
|
2317
|
+
}
|
|
2318
|
+
|
|
1956
2319
|
private handleMessageEditorChange(fieldName: string, event: Event): void {
|
|
1957
2320
|
const target = event.target as any;
|
|
1958
2321
|
|
|
@@ -451,12 +451,14 @@ export class NodeTypeSelector extends RapidElement {
|
|
|
451
451
|
.filter(([type, config]) => {
|
|
452
452
|
// exclude execute_actions (it's the default action-only node)
|
|
453
453
|
// exclude nodes that have showAsAction=true (they appear in action mode)
|
|
454
|
+
// exclude nodes that have hideFromSplits=true (promoted to context menu)
|
|
454
455
|
// exclude aliases (type won't match config.type for aliases)
|
|
455
456
|
return (
|
|
456
457
|
type !== 'execute_actions' &&
|
|
457
458
|
type === config.type &&
|
|
458
459
|
config.name &&
|
|
459
460
|
!config.showAsAction &&
|
|
461
|
+
!config.hideFromSplits &&
|
|
460
462
|
this.isConfigAvailable(config)
|
|
461
463
|
);
|
|
462
464
|
})
|
package/src/flow/Plumber.ts
CHANGED
|
@@ -1087,7 +1087,9 @@ export class Plumber {
|
|
|
1087
1087
|
const contactUuid = target.getAttribute('data-uuid');
|
|
1088
1088
|
if (contactUuid) {
|
|
1089
1089
|
this.editor.fireCustomEvent('temba-contact-clicked', {
|
|
1090
|
-
uuid: contactUuid
|
|
1090
|
+
uuid: contactUuid,
|
|
1091
|
+
metaKey: e.metaKey,
|
|
1092
|
+
ctrlKey: e.ctrlKey
|
|
1091
1093
|
});
|
|
1092
1094
|
}
|
|
1093
1095
|
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
FlowTypes
|
|
8
8
|
} from '../types';
|
|
9
9
|
import { Node, AddContactUrn } from '../../store/flow-definition';
|
|
10
|
-
import { SCHEMES } from '../utils';
|
|
10
|
+
import { SCHEMES, renderClamped } from '../utils';
|
|
11
11
|
|
|
12
12
|
export const add_contact_urn: ActionConfig = {
|
|
13
13
|
name: 'Add URN',
|
|
@@ -16,11 +16,10 @@ export const add_contact_urn: ActionConfig = {
|
|
|
16
16
|
render: (_node: Node, action: AddContactUrn) => {
|
|
17
17
|
const schemeObj = SCHEMES.find((s) => s.scheme === action.scheme);
|
|
18
18
|
const friendlyScheme = schemeObj?.path || action.scheme;
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
</div>`;
|
|
19
|
+
return renderClamped(
|
|
20
|
+
html`Add ${friendlyScheme} <strong>${action.path}</strong>`,
|
|
21
|
+
`Add ${friendlyScheme} ${action.path}`
|
|
22
|
+
);
|
|
24
23
|
},
|
|
25
24
|
|
|
26
25
|
toFormData: (action: AddContactUrn) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { html } from 'lit-html';
|
|
2
2
|
import { ActionConfig, ACTION_GROUPS, FlowTypes } from '../types';
|
|
3
3
|
import { Node, EnterFlow } from '../../store/flow-definition';
|
|
4
|
-
import {
|
|
4
|
+
import { renderFlowLinks } from '../utils';
|
|
5
5
|
|
|
6
6
|
export const enter_flow: ActionConfig = {
|
|
7
7
|
name: 'Enter a Flow',
|
|
@@ -9,7 +9,7 @@ export const enter_flow: ActionConfig = {
|
|
|
9
9
|
hideFromActions: true,
|
|
10
10
|
flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
|
|
11
11
|
render: (_node: Node, action: EnterFlow) => {
|
|
12
|
-
return html`${
|
|
12
|
+
return html`${renderFlowLinks([action.flow], 'flow')}`;
|
|
13
13
|
},
|
|
14
14
|
toFormData: (action: EnterFlow) => {
|
|
15
15
|
return {
|
|
@@ -3,6 +3,7 @@ import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
|
|
|
3
3
|
import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
|
|
4
4
|
import { Node, SayMsg } from '../../store/flow-definition';
|
|
5
5
|
import { renderAudioPlayer } from './audio-player';
|
|
6
|
+
import { renderClamped } from '../utils';
|
|
6
7
|
|
|
7
8
|
export const say_msg: ActionConfig = {
|
|
8
9
|
name: 'Say Message',
|
|
@@ -11,7 +12,7 @@ export const say_msg: ActionConfig = {
|
|
|
11
12
|
render: (_node: Node, action: SayMsg) => {
|
|
12
13
|
const text = (action.text || '').replace(/\n/g, '<br>');
|
|
13
14
|
return html`
|
|
14
|
-
${unsafeHTML(text)}
|
|
15
|
+
${renderClamped(html`${unsafeHTML(text)}`, action.text || '')}
|
|
15
16
|
${action.audio_url
|
|
16
17
|
? html`<div style="margin-top: 0.5em;">
|
|
17
18
|
${renderAudioPlayer(action.audio_url)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { html } from 'lit-html';
|
|
2
2
|
import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
|
|
3
3
|
import { Node, SendBroadcast } from '../../store/flow-definition';
|
|
4
|
-
import { renderStringList } from '../utils';
|
|
4
|
+
import { renderStringList, renderClamped } from '../utils';
|
|
5
5
|
import { Icon } from '../../Icons';
|
|
6
6
|
|
|
7
7
|
export const send_broadcast: ActionConfig = {
|
|
@@ -17,11 +17,7 @@ export const send_broadcast: ActionConfig = {
|
|
|
17
17
|
return html`<div>
|
|
18
18
|
<div>${renderStringList(recipients, Icon.contacts)}</div>
|
|
19
19
|
<div style="margin-top: 0.5em">
|
|
20
|
-
|
|
21
|
-
style="word-wrap: break-word; overflow-wrap: break-word; hyphens: auto;"
|
|
22
|
-
>
|
|
23
|
-
${action.text}
|
|
24
|
-
</div>
|
|
20
|
+
${renderClamped(action.text, action.text)}
|
|
25
21
|
</div>
|
|
26
22
|
</div>`;
|
|
27
23
|
},
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
FlowTypes
|
|
8
8
|
} from '../types';
|
|
9
9
|
import { Node, SendEmail } from '../../store/flow-definition';
|
|
10
|
-
import { renderStringList } from '../utils';
|
|
10
|
+
import { renderStringList, renderClamped } from '../utils';
|
|
11
11
|
import { Icon } from '../../Icons';
|
|
12
12
|
|
|
13
13
|
export const send_email: ActionConfig = {
|
|
@@ -18,11 +18,7 @@ export const send_email: ActionConfig = {
|
|
|
18
18
|
return html`<div>
|
|
19
19
|
<div>${renderStringList(action.addresses, Icon.email)}</div>
|
|
20
20
|
<div style="margin-top: 0.5em">
|
|
21
|
-
|
|
22
|
-
style="word-wrap: break-word; overflow-wrap: break-word; hyphens: auto;"
|
|
23
|
-
>
|
|
24
|
-
${action.subject}
|
|
25
|
-
</div>
|
|
21
|
+
${renderClamped(action.subject, action.subject)}
|
|
26
22
|
</div>
|
|
27
23
|
</div>`;
|
|
28
24
|
},
|