@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.
Files changed (131) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/temba-components.js +825 -654
  3. package/dist/temba-components.js.map +1 -1
  4. package/out-tsc/src/Icons.js +1 -0
  5. package/out-tsc/src/Icons.js.map +1 -1
  6. package/out-tsc/src/flow/CanvasMenu.js +30 -35
  7. package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
  8. package/out-tsc/src/flow/CanvasNode.js +13 -8
  9. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  10. package/out-tsc/src/flow/Editor.js +18 -5
  11. package/out-tsc/src/flow/Editor.js.map +1 -1
  12. package/out-tsc/src/flow/NodeEditor.js +346 -10
  13. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  14. package/out-tsc/src/flow/NodeTypeSelector.js +2 -0
  15. package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
  16. package/out-tsc/src/flow/Plumber.js +3 -1
  17. package/out-tsc/src/flow/Plumber.js.map +1 -1
  18. package/out-tsc/src/flow/actions/add_contact_urn.js +2 -6
  19. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  20. package/out-tsc/src/flow/actions/enter_flow.js +2 -2
  21. package/out-tsc/src/flow/actions/enter_flow.js.map +1 -1
  22. package/out-tsc/src/flow/actions/say_msg.js +2 -1
  23. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  24. package/out-tsc/src/flow/actions/send_broadcast.js +2 -6
  25. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  26. package/out-tsc/src/flow/actions/send_email.js +2 -6
  27. package/out-tsc/src/flow/actions/send_email.js.map +1 -1
  28. package/out-tsc/src/flow/actions/send_msg.js +52 -35
  29. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  30. package/out-tsc/src/flow/actions/set_contact_channel.js +2 -1
  31. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  32. package/out-tsc/src/flow/actions/set_contact_field.js +4 -5
  33. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  34. package/out-tsc/src/flow/actions/set_contact_language.js +3 -3
  35. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  36. package/out-tsc/src/flow/actions/set_contact_name.js +2 -1
  37. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  38. package/out-tsc/src/flow/actions/set_contact_status.js +2 -1
  39. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  40. package/out-tsc/src/flow/actions/set_run_result.js +3 -3
  41. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  42. package/out-tsc/src/flow/actions/start_session.js +2 -2
  43. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  44. package/out-tsc/src/flow/nodes/split_by_llm.js +4 -5
  45. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
  46. package/out-tsc/src/flow/nodes/split_by_resthook.js +3 -8
  47. package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -1
  48. package/out-tsc/src/flow/nodes/split_by_subflow.js +2 -2
  49. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  50. package/out-tsc/src/flow/nodes/split_by_webhook.js +25 -33
  51. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
  52. package/out-tsc/src/flow/nodes/wait_for_response.js +1 -0
  53. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  54. package/out-tsc/src/flow/types.js.map +1 -1
  55. package/out-tsc/src/flow/utils.js +68 -0
  56. package/out-tsc/src/flow/utils.js.map +1 -1
  57. package/out-tsc/src/form/FieldRenderer.js +17 -2
  58. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  59. package/out-tsc/src/interfaces.js +1 -0
  60. package/out-tsc/src/interfaces.js.map +1 -1
  61. package/out-tsc/src/simulator/Simulator.js +1 -1
  62. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  63. package/out-tsc/test/temba-canvas-menu.test.js +13 -9
  64. package/out-tsc/test/temba-canvas-menu.test.js.map +1 -1
  65. package/out-tsc/test/temba-flow-reflow.test.js.map +1 -1
  66. package/out-tsc/test/temba-node-editor.test.js +9 -10
  67. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  68. package/out-tsc/test/temba-node-type-selector.test.js +3 -3
  69. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
  70. package/out-tsc/test/temba-simulator.test.js +2 -2
  71. package/out-tsc/test/temba-simulator.test.js.map +1 -1
  72. package/package.json +1 -1
  73. package/screenshots/truth/actions/enter_flow/render/basic-flow.png +0 -0
  74. package/screenshots/truth/actions/enter_flow/render/long-flow-name.png +0 -0
  75. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  76. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  77. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  78. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  79. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  80. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  81. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  82. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  83. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  84. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  85. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  86. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  87. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  88. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  89. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  90. package/screenshots/truth/canvas-menu/open.png +0 -0
  91. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  92. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  93. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  94. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  95. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  96. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  97. package/src/Icons.ts +1 -0
  98. package/src/flow/CanvasMenu.ts +38 -39
  99. package/src/flow/CanvasNode.ts +16 -8
  100. package/src/flow/Editor.ts +33 -6
  101. package/src/flow/NodeEditor.ts +373 -10
  102. package/src/flow/NodeTypeSelector.ts +2 -0
  103. package/src/flow/Plumber.ts +3 -1
  104. package/src/flow/actions/add_contact_urn.ts +5 -6
  105. package/src/flow/actions/enter_flow.ts +2 -2
  106. package/src/flow/actions/say_msg.ts +2 -1
  107. package/src/flow/actions/send_broadcast.ts +2 -6
  108. package/src/flow/actions/send_email.ts +2 -6
  109. package/src/flow/actions/send_msg.ts +56 -38
  110. package/src/flow/actions/set_contact_channel.ts +5 -1
  111. package/src/flow/actions/set_contact_field.ts +10 -5
  112. package/src/flow/actions/set_contact_language.ts +6 -3
  113. package/src/flow/actions/set_contact_name.ts +5 -1
  114. package/src/flow/actions/set_contact_status.ts +5 -1
  115. package/src/flow/actions/set_run_result.ts +6 -3
  116. package/src/flow/actions/start_session.ts +2 -2
  117. package/src/flow/nodes/split_by_llm.ts +5 -5
  118. package/src/flow/nodes/split_by_resthook.ts +3 -8
  119. package/src/flow/nodes/split_by_subflow.ts +2 -2
  120. package/src/flow/nodes/split_by_webhook.ts +26 -34
  121. package/src/flow/nodes/wait_for_response.ts +1 -0
  122. package/src/flow/types.ts +25 -2
  123. package/src/flow/utils.ts +81 -1
  124. package/src/form/FieldRenderer.ts +32 -3
  125. package/src/interfaces.ts +1 -0
  126. package/src/simulator/Simulator.ts +1 -1
  127. package/test/temba-canvas-menu.test.ts +13 -9
  128. package/test/temba-flow-reflow.test.ts +4 -2
  129. package/test/temba-node-editor.test.ts +9 -10
  130. package/test/temba-node-type-selector.test.ts +3 -3
  131. package/test/temba-simulator.test.ts +2 -2
@@ -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
- const newCollapsedState = collapsed(this.formData);
1266
-
1267
- // Only update if the state has changed to avoid unnecessary re-renders
1268
- if (this.groupCollapseState[label] !== newCollapsedState) {
1269
- this.groupCollapseState = {
1270
- ...this.groupCollapseState,
1271
- [label]: newCollapsedState
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
  })
@@ -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 html`<div
20
- style="word-wrap: break-word; overflow-wrap: break-word; hyphens: auto;"
21
- >
22
- Add ${friendlyScheme} <strong>${action.path}</strong>
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 { renderNamedObjects } from '../utils';
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`${renderNamedObjects([action.flow], 'flow')}`;
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
- <div
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
- <div
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
  },