@nyaruka/temba-components 0.142.1 → 0.142.3

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 (140) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/temba-components.js +953 -708
  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 +38 -38
  7. package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
  8. package/out-tsc/src/flow/CanvasNode.js +171 -17
  9. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  10. package/out-tsc/src/flow/Editor.js +491 -22
  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 +92 -28
  17. package/out-tsc/src/flow/Plumber.js.map +1 -1
  18. package/out-tsc/src/flow/StickyNote.js +63 -3
  19. package/out-tsc/src/flow/StickyNote.js.map +1 -1
  20. package/out-tsc/src/flow/actions/add_contact_urn.js +2 -6
  21. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  22. package/out-tsc/src/flow/actions/enter_flow.js +2 -2
  23. package/out-tsc/src/flow/actions/enter_flow.js.map +1 -1
  24. package/out-tsc/src/flow/actions/say_msg.js +2 -1
  25. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  26. package/out-tsc/src/flow/actions/send_broadcast.js +2 -6
  27. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  28. package/out-tsc/src/flow/actions/send_email.js +2 -6
  29. package/out-tsc/src/flow/actions/send_email.js.map +1 -1
  30. package/out-tsc/src/flow/actions/send_msg.js +55 -35
  31. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  32. package/out-tsc/src/flow/actions/set_contact_channel.js +2 -1
  33. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  34. package/out-tsc/src/flow/actions/set_contact_field.js +4 -5
  35. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  36. package/out-tsc/src/flow/actions/set_contact_language.js +3 -3
  37. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  38. package/out-tsc/src/flow/actions/set_contact_name.js +2 -1
  39. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  40. package/out-tsc/src/flow/actions/set_contact_status.js +2 -1
  41. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  42. package/out-tsc/src/flow/actions/set_run_result.js +3 -3
  43. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  44. package/out-tsc/src/flow/actions/start_session.js +2 -2
  45. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  46. package/out-tsc/src/flow/nodes/split_by_llm.js +4 -5
  47. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
  48. package/out-tsc/src/flow/nodes/split_by_resthook.js +3 -8
  49. package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -1
  50. package/out-tsc/src/flow/nodes/split_by_subflow.js +4 -2
  51. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  52. package/out-tsc/src/flow/nodes/split_by_webhook.js +25 -33
  53. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
  54. package/out-tsc/src/flow/nodes/wait_for_response.js +1 -0
  55. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  56. package/out-tsc/src/flow/types.js.map +1 -1
  57. package/out-tsc/src/flow/utils.js +66 -0
  58. package/out-tsc/src/flow/utils.js.map +1 -1
  59. package/out-tsc/src/form/FieldRenderer.js +17 -2
  60. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  61. package/out-tsc/src/interfaces.js +1 -0
  62. package/out-tsc/src/interfaces.js.map +1 -1
  63. package/out-tsc/src/list/SortableList.js +104 -43
  64. package/out-tsc/src/list/SortableList.js.map +1 -1
  65. package/out-tsc/src/simulator/Simulator.js +6 -2
  66. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  67. package/out-tsc/test/temba-canvas-menu.test.js +13 -9
  68. package/out-tsc/test/temba-canvas-menu.test.js.map +1 -1
  69. package/out-tsc/test/temba-flow-reflow.test.js.map +1 -1
  70. package/out-tsc/test/temba-node-editor.test.js +9 -10
  71. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  72. package/out-tsc/test/temba-node-type-selector.test.js +3 -3
  73. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
  74. package/out-tsc/test/temba-simulator.test.js +2 -2
  75. package/out-tsc/test/temba-simulator.test.js.map +1 -1
  76. package/package.json +1 -1
  77. package/screenshots/truth/actions/enter_flow/render/basic-flow.png +0 -0
  78. package/screenshots/truth/actions/enter_flow/render/long-flow-name.png +0 -0
  79. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  80. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  81. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  82. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  83. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  84. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  85. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  86. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  87. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  88. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  89. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  90. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  91. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  92. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  93. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  94. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  95. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  96. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  97. package/screenshots/truth/canvas-menu/open.png +0 -0
  98. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  99. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  100. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  101. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  102. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  103. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  104. package/src/Icons.ts +1 -0
  105. package/src/flow/CanvasMenu.ts +50 -43
  106. package/src/flow/CanvasNode.ts +201 -17
  107. package/src/flow/Editor.ts +585 -25
  108. package/src/flow/NodeEditor.ts +373 -10
  109. package/src/flow/NodeTypeSelector.ts +2 -0
  110. package/src/flow/Plumber.ts +104 -37
  111. package/src/flow/StickyNote.ts +76 -4
  112. package/src/flow/actions/add_contact_urn.ts +5 -6
  113. package/src/flow/actions/enter_flow.ts +2 -2
  114. package/src/flow/actions/say_msg.ts +2 -1
  115. package/src/flow/actions/send_broadcast.ts +2 -6
  116. package/src/flow/actions/send_email.ts +2 -6
  117. package/src/flow/actions/send_msg.ts +59 -38
  118. package/src/flow/actions/set_contact_channel.ts +5 -1
  119. package/src/flow/actions/set_contact_field.ts +10 -5
  120. package/src/flow/actions/set_contact_language.ts +6 -3
  121. package/src/flow/actions/set_contact_name.ts +5 -1
  122. package/src/flow/actions/set_contact_status.ts +5 -1
  123. package/src/flow/actions/set_run_result.ts +6 -3
  124. package/src/flow/actions/start_session.ts +2 -2
  125. package/src/flow/nodes/split_by_llm.ts +5 -5
  126. package/src/flow/nodes/split_by_resthook.ts +3 -8
  127. package/src/flow/nodes/split_by_subflow.ts +4 -2
  128. package/src/flow/nodes/split_by_webhook.ts +26 -34
  129. package/src/flow/nodes/wait_for_response.ts +1 -0
  130. package/src/flow/types.ts +25 -2
  131. package/src/flow/utils.ts +79 -1
  132. package/src/form/FieldRenderer.ts +32 -3
  133. package/src/interfaces.ts +1 -0
  134. package/src/list/SortableList.ts +117 -47
  135. package/src/simulator/Simulator.ts +6 -2
  136. package/test/temba-canvas-menu.test.ts +13 -9
  137. package/test/temba-flow-reflow.test.ts +4 -2
  138. package/test/temba-node-editor.test.ts +9 -10
  139. package/test/temba-node-type-selector.test.ts +3 -3
  140. 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,133 @@ 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: transform 0.2s ease, opacity 0.3s ease;
392
+ }
393
+
394
+ .accordion-toggle-icon.expanded {
395
+ transform: rotate(90deg);
396
+ }
397
+
398
+ .accordion-toggle-icon.faded {
399
+ opacity: 0;
400
+ }
401
+
402
+ .accordion-count-bubble {
403
+ border-radius: 50%;
404
+ display: flex;
405
+ align-items: center;
406
+ justify-content: center;
407
+ font-size: 10px;
408
+ font-weight: 600;
409
+ padding: 3px;
410
+ min-width: 10px;
411
+ min-height: 10px;
412
+ position: absolute;
413
+ top: 50%;
414
+ left: 50%;
415
+ transform: translate(-50%, -50%);
416
+ line-height: 0px;
417
+ opacity: 1;
418
+ transition: opacity 0.3s ease;
419
+ background: var(--color-bubble-bg, #fff);
420
+ border: 1px solid var(--color-bubble-border, #777);
421
+ color: var(--color-bubble-text, #000);
422
+ }
423
+
424
+ .accordion-count-bubble.hidden {
425
+ opacity: 0;
426
+ pointer-events: none;
427
+ }
428
+
429
+ .accordion-checkmark-icon {
430
+ position: absolute;
431
+ top: 50%;
432
+ left: 50%;
433
+ transform: translate(-50%, -50%);
434
+ opacity: 1;
435
+ transition: opacity 0.3s ease;
436
+ border-radius: 50%;
437
+ color: var(--color-bubble-text, #000);
438
+ background: var(--color-bubble-bg, #fff);
439
+ border: 1px solid var(--color-bubble-border, #777);
440
+ padding: 0.15em;
441
+ }
442
+
443
+ .accordion-checkmark-icon.hidden {
444
+ opacity: 0;
445
+ pointer-events: none;
446
+ }
447
+
448
+ .accordion-content {
449
+ padding: 8px 10px;
450
+ display: flex;
451
+ flex-direction: column;
452
+ gap: 8px;
453
+ overflow: hidden;
454
+ transition: all 0.2s ease-in-out;
455
+ opacity: 1;
456
+ }
457
+
458
+ .accordion-content.collapsed {
459
+ max-height: 0;
460
+ padding-top: 0;
461
+ padding-bottom: 0;
462
+ opacity: 0;
463
+ }
464
+
465
+ .accordion-error-icon {
466
+ color: var(--color-error, tomato);
467
+ margin-right: 6px;
468
+ }
469
+
322
470
  .gutter-fields {
323
471
  display: flex;
324
472
  flex-direction: column;
@@ -1261,15 +1409,20 @@ export class NodeEditor extends RapidElement {
1261
1409
  const { label, collapsed, collapsible } = item;
1262
1410
 
1263
1411
  // Only update if the group is collapsible and has a function-based collapsed property
1412
+ // Skip reveal groups that have been expanded — they are one-way
1264
1413
  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
- };
1414
+ if (item.reveal && this.groupCollapseState[label] === false) {
1415
+ // Reveal group was manually expanded — don't re-collapse
1416
+ } else {
1417
+ const newCollapsedState = collapsed(this.formData);
1418
+
1419
+ // Only update if the state has changed to avoid unnecessary re-renders
1420
+ if (this.groupCollapseState[label] !== newCollapsedState) {
1421
+ this.groupCollapseState = {
1422
+ ...this.groupCollapseState,
1423
+ [label]: newCollapsedState
1424
+ };
1425
+ }
1273
1426
  }
1274
1427
  }
1275
1428
 
@@ -1278,6 +1431,21 @@ export class NodeEditor extends RapidElement {
1278
1431
  } else if (typeof item === 'object' && item.type === 'row') {
1279
1432
  // Recursively check items in rows
1280
1433
  this.updateGroupCollapseStatesRecursive(item.items);
1434
+ } else if (typeof item === 'object' && item.type === 'accordion') {
1435
+ // Check each accordion section
1436
+ item.sections.forEach((section) => {
1437
+ const stateKey = `accordion:${section.label}`;
1438
+ if (typeof section.collapsed === 'function') {
1439
+ const newCollapsedState = section.collapsed(this.formData);
1440
+ if (this.groupCollapseState[stateKey] !== newCollapsedState) {
1441
+ this.groupCollapseState = {
1442
+ ...this.groupCollapseState,
1443
+ [stateKey]: newCollapsedState
1444
+ };
1445
+ }
1446
+ }
1447
+ this.updateGroupCollapseStatesRecursive(section.items);
1448
+ });
1281
1449
  }
1282
1450
  });
1283
1451
  }
@@ -1518,6 +1686,9 @@ export class NodeEditor extends RapidElement {
1518
1686
  const picker = e.target as any;
1519
1687
  const url = picker.attachments?.[0]?.url || '';
1520
1688
  this.handleNewFieldChange(fieldName, url);
1689
+ } else if (fieldName && config.type === 'template-editor') {
1690
+ // Special handling for template editor (manages template + template_variables)
1691
+ this.handleTemplateEditorChange(fieldName, e);
1521
1692
  } else {
1522
1693
  // Default handling for most field types
1523
1694
  this.handleFormFieldChange(fieldName, e);
@@ -1526,7 +1697,8 @@ export class NodeEditor extends RapidElement {
1526
1697
  showLabel: true,
1527
1698
  formData: this.formData,
1528
1699
  additionalData: {
1529
- attachments: this.formData.attachments || []
1700
+ attachments: this.formData.attachments || [],
1701
+ template_variables: this.formData.template_variables || []
1530
1702
  }
1531
1703
  });
1532
1704
  }
@@ -1584,6 +1756,21 @@ export class NodeEditor extends RapidElement {
1584
1756
  } else if (typeof item === 'object' && item.type === 'row') {
1585
1757
  // Recursively check items in rows
1586
1758
  this.expandGroupsWithErrorsRecursive(item.items, errorFields);
1759
+ } else if (typeof item === 'object' && item.type === 'accordion') {
1760
+ // Check each accordion section for errors
1761
+ item.sections.forEach((section) => {
1762
+ const fieldsInSection = this.collectFieldsFromItems(section.items);
1763
+ const sectionHasErrors = fieldsInSection.some((fieldName) =>
1764
+ errorFields.has(fieldName)
1765
+ );
1766
+ if (sectionHasErrors) {
1767
+ this.groupCollapseState = {
1768
+ ...this.groupCollapseState,
1769
+ [`accordion:${section.label}`]: false
1770
+ };
1771
+ }
1772
+ this.expandGroupsWithErrorsRecursive(section.items, errorFields);
1773
+ });
1587
1774
  }
1588
1775
  });
1589
1776
  }
@@ -1631,6 +1818,9 @@ export class NodeEditor extends RapidElement {
1631
1818
  case 'group':
1632
1819
  return this.renderGroup(item, config, renderedFields);
1633
1820
 
1821
+ case 'accordion':
1822
+ return this.renderAccordion(item, config, renderedFields);
1823
+
1634
1824
  case 'spacer':
1635
1825
  return html``;
1636
1826
 
@@ -1771,6 +1961,8 @@ export class NodeEditor extends RapidElement {
1771
1961
  collapsed = false,
1772
1962
  helpText,
1773
1963
  contentPadding,
1964
+ bordered = true,
1965
+ reveal = false,
1774
1966
  getGroupValueCount
1775
1967
  } = groupConfig;
1776
1968
 
@@ -1791,6 +1983,13 @@ export class NodeEditor extends RapidElement {
1791
1983
  (typeof collapsed === 'function' ? collapsed(this.formData) : collapsed)
1792
1984
  : false;
1793
1985
 
1986
+ // Reveal mode: once expanded, render items directly without any group wrapper
1987
+ if (reveal && !isCollapsed) {
1988
+ return html`${items.map((item) =>
1989
+ this.renderLayoutItem(item, config, renderedFields)
1990
+ )}`;
1991
+ }
1992
+
1794
1993
  // Check if any field in this group has errors
1795
1994
  const fieldsInGroup = this.collectFieldsFromItems(items);
1796
1995
  const groupHasErrors = fieldsInGroup.some(
@@ -1832,7 +2031,7 @@ export class NodeEditor extends RapidElement {
1832
2031
  ? 'has-errors'
1833
2032
  : ''} ${isCollapsed ? 'collapsed' : 'expanded'} ${hasValue
1834
2033
  ? 'has-bubble'
1835
- : ''}"
2034
+ : ''} ${!bordered ? 'no-border' : ''}"
1836
2035
  >
1837
2036
  <div
1838
2037
  class="form-group-header ${collapsible ? 'clickable' : ''}"
@@ -1900,6 +2099,150 @@ export class NodeEditor extends RapidElement {
1900
2099
  `;
1901
2100
  }
1902
2101
 
2102
+ private renderAccordion(
2103
+ accordionConfig: AccordionLayoutConfig,
2104
+ config: ActionConfig | NodeConfig,
2105
+ renderedFields: Set<string>
2106
+ ): TemplateResult {
2107
+ const { sections, multi = false } = accordionConfig;
2108
+
2109
+ return html`
2110
+ <div class="accordion">
2111
+ ${sections.map((section) => {
2112
+ const { label, collapsed = true, getValueCount } = section;
2113
+ const stateKey = `accordion:${label}`;
2114
+
2115
+ // Initialize collapse state if not set
2116
+ if (!(stateKey in this.groupCollapseState)) {
2117
+ const initialCollapsed =
2118
+ typeof collapsed === 'function'
2119
+ ? collapsed(this.formData)
2120
+ : collapsed;
2121
+ this.groupCollapseState = {
2122
+ ...this.groupCollapseState,
2123
+ [stateKey]: initialCollapsed
2124
+ };
2125
+ }
2126
+
2127
+ const isCollapsed = this.groupCollapseState[stateKey] ?? true;
2128
+ const isHovered = this.groupHoverState[stateKey] ?? false;
2129
+
2130
+ // Check for errors in this section
2131
+ const fieldsInSection = this.collectFieldsFromItems(section.items);
2132
+ const sectionHasErrors = fieldsInSection.some(
2133
+ (fieldName) => this.errors[fieldName]
2134
+ );
2135
+
2136
+ // Value count / checkmark display
2137
+ let valueCount = 0;
2138
+ let showBubble = false;
2139
+ let showCheckmark = false;
2140
+ let hasValue = false;
2141
+
2142
+ if (getValueCount) {
2143
+ try {
2144
+ const result = getValueCount(this.formData);
2145
+ if (typeof result === 'boolean') {
2146
+ hasValue = result;
2147
+ showCheckmark = result && isCollapsed && !isHovered;
2148
+ } else if (typeof result === 'number') {
2149
+ valueCount = result;
2150
+ hasValue = valueCount > 0;
2151
+ showBubble = valueCount > 0 && isCollapsed && !isHovered;
2152
+ }
2153
+ } catch (error) {
2154
+ // ignore
2155
+ }
2156
+ }
2157
+
2158
+ return html`
2159
+ <div
2160
+ class="accordion-section ${isCollapsed
2161
+ ? 'collapsed'
2162
+ : 'expanded'} ${hasValue ? 'has-value' : ''}"
2163
+ >
2164
+ <div
2165
+ class="accordion-header"
2166
+ @click=${() =>
2167
+ this.handleAccordionToggle(stateKey, sections, multi)}
2168
+ @mouseenter=${() => this.handleGroupMouseEnter(stateKey)}
2169
+ @mouseleave=${() => this.handleGroupMouseLeave(stateKey)}
2170
+ >
2171
+ <div class="accordion-title">${label}</div>
2172
+ ${sectionHasErrors
2173
+ ? html`<temba-icon
2174
+ name="alert_warning"
2175
+ class="accordion-error-icon"
2176
+ size="1.2"
2177
+ ></temba-icon>`
2178
+ : html`<div class="accordion-toggle-container">
2179
+ <temba-icon
2180
+ name="arrow_right"
2181
+ size="1.2"
2182
+ class="accordion-toggle-icon ${isCollapsed
2183
+ ? 'collapsed'
2184
+ : 'expanded'} ${showBubble || showCheckmark
2185
+ ? 'faded'
2186
+ : ''}"
2187
+ ></temba-icon>
2188
+ ${showCheckmark
2189
+ ? html`<temba-icon
2190
+ name="check"
2191
+ size="0.8"
2192
+ class="accordion-checkmark-icon"
2193
+ ></temba-icon>`
2194
+ : showBubble
2195
+ ? html`<div
2196
+ class="accordion-count-bubble ${!showBubble
2197
+ ? 'hidden'
2198
+ : ''}"
2199
+ >
2200
+ ${valueCount}
2201
+ </div>`
2202
+ : ''}
2203
+ </div>`}
2204
+ </div>
2205
+ <div
2206
+ class="accordion-content ${isCollapsed
2207
+ ? 'collapsed'
2208
+ : '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
  })