@nyaruka/temba-components 0.129.7 → 0.129.9

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 (269) hide show
  1. package/.devcontainer/Dockerfile +11 -4
  2. package/.devcontainer/devcontainer.json +3 -2
  3. package/.github/workflows/build.yml +4 -14
  4. package/CHANGELOG.md +29 -0
  5. package/demo/components/flow/example.html +1 -1
  6. package/demo/components/message-editor/example.html +125 -0
  7. package/demo/components/textinput/completion.html +1 -0
  8. package/demo/data/flows/food-order.json +12 -21
  9. package/demo/data/flows/sample-flow.json +210 -104
  10. package/dist/temba-components.js +715 -364
  11. package/dist/temba-components.js.map +1 -1
  12. package/out-tsc/src/display/Thumbnail.js +2 -1
  13. package/out-tsc/src/display/Thumbnail.js.map +1 -1
  14. package/out-tsc/src/events.js.map +1 -1
  15. package/out-tsc/src/excellent/helpers.js +2 -2
  16. package/out-tsc/src/excellent/helpers.js.map +1 -1
  17. package/out-tsc/src/flow/CanvasNode.js +25 -7
  18. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  19. package/out-tsc/src/flow/Editor.js +11 -1
  20. package/out-tsc/src/flow/Editor.js.map +1 -1
  21. package/out-tsc/src/flow/NodeEditor.js +342 -276
  22. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  23. package/out-tsc/src/flow/actions/add_input_labels.js +40 -0
  24. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  25. package/out-tsc/src/flow/actions/call_llm.js +56 -3
  26. package/out-tsc/src/flow/actions/call_llm.js.map +1 -1
  27. package/out-tsc/src/flow/actions/call_webhook.js +26 -17
  28. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
  29. package/out-tsc/src/flow/actions/open_ticket.js +65 -3
  30. package/out-tsc/src/flow/actions/open_ticket.js.map +1 -1
  31. package/out-tsc/src/flow/actions/send_msg.js +147 -6
  32. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  33. package/out-tsc/src/flow/actions/set_run_result.js +75 -0
  34. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  35. package/out-tsc/src/flow/config.js +4 -0
  36. package/out-tsc/src/flow/config.js.map +1 -1
  37. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +227 -0
  38. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -0
  39. package/out-tsc/src/flow/nodes/split_by_ticket.js +18 -0
  40. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -0
  41. package/out-tsc/src/flow/nodes/wait_for_response.js +27 -1
  42. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  43. package/out-tsc/src/flow/types.js +0 -65
  44. package/out-tsc/src/flow/types.js.map +1 -1
  45. package/out-tsc/src/form/ArrayEditor.js +87 -57
  46. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  47. package/out-tsc/src/form/BaseListEditor.js +19 -4
  48. package/out-tsc/src/form/BaseListEditor.js.map +1 -1
  49. package/out-tsc/src/form/FieldRenderer.js +305 -0
  50. package/out-tsc/src/form/FieldRenderer.js.map +1 -0
  51. package/out-tsc/src/form/FormField.js +4 -4
  52. package/out-tsc/src/form/FormField.js.map +1 -1
  53. package/out-tsc/src/form/KeyValueEditor.js +1 -1
  54. package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
  55. package/out-tsc/src/form/MediaPicker.js +13 -1
  56. package/out-tsc/src/form/MediaPicker.js.map +1 -1
  57. package/out-tsc/src/form/MessageEditor.js +422 -0
  58. package/out-tsc/src/form/MessageEditor.js.map +1 -0
  59. package/out-tsc/src/form/TextInput.js +13 -6
  60. package/out-tsc/src/form/TextInput.js.map +1 -1
  61. package/out-tsc/src/form/select/Select.js +52 -24
  62. package/out-tsc/src/form/select/Select.js.map +1 -1
  63. package/out-tsc/src/live/ContactChat.js +66 -15
  64. package/out-tsc/src/live/ContactChat.js.map +1 -1
  65. package/out-tsc/src/markdown.js +13 -11
  66. package/out-tsc/src/markdown.js.map +1 -1
  67. package/out-tsc/temba-modules.js +2 -0
  68. package/out-tsc/temba-modules.js.map +1 -1
  69. package/out-tsc/test/ActionHelper.js +2 -0
  70. package/out-tsc/test/ActionHelper.js.map +1 -1
  71. package/out-tsc/test/NodeHelper.js +148 -0
  72. package/out-tsc/test/NodeHelper.js.map +1 -0
  73. package/out-tsc/test/actions/call_llm.test.js +103 -0
  74. package/out-tsc/test/actions/call_llm.test.js.map +1 -0
  75. package/out-tsc/test/nodes/split_by_llm_categorize.test.js +532 -0
  76. package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -0
  77. package/out-tsc/test/nodes/split_by_random.test.js +150 -0
  78. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -0
  79. package/out-tsc/test/nodes/wait_for_digits.test.js +150 -0
  80. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -0
  81. package/out-tsc/test/nodes/wait_for_response.test.js +171 -0
  82. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -0
  83. package/out-tsc/test/temba-add-input-labels.test.js +70 -0
  84. package/out-tsc/test/temba-add-input-labels.test.js.map +1 -0
  85. package/out-tsc/test/temba-field-config.test.js +4 -2
  86. package/out-tsc/test/temba-field-config.test.js.map +1 -1
  87. package/out-tsc/test/temba-field-renderer.test.js +296 -0
  88. package/out-tsc/test/temba-field-renderer.test.js.map +1 -0
  89. package/out-tsc/test/temba-markdown.test.js +1 -1
  90. package/out-tsc/test/temba-markdown.test.js.map +1 -1
  91. package/out-tsc/test/temba-message-editor.test.js +194 -0
  92. package/out-tsc/test/temba-message-editor.test.js.map +1 -0
  93. package/out-tsc/test/temba-node-editor.test.js +471 -0
  94. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  95. package/out-tsc/test/temba-select.test.js +7 -4
  96. package/out-tsc/test/temba-select.test.js.map +1 -1
  97. package/out-tsc/test/temba-textinput.test.js +16 -0
  98. package/out-tsc/test/temba-textinput.test.js.map +1 -1
  99. package/out-tsc/test/temba-webchat.test.js +5 -1
  100. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  101. package/out-tsc/test/utils.test.js +2 -8
  102. package/out-tsc/test/utils.test.js.map +1 -1
  103. package/package.json +7 -4
  104. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  105. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  106. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  107. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  108. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  109. package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
  110. package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
  111. package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
  112. package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
  113. package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
  114. package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
  115. package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
  116. package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
  117. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  118. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  119. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  120. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  121. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  122. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  123. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  124. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  125. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  126. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  127. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  128. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  129. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  130. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  131. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  132. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  133. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  134. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  135. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  136. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  137. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  138. package/screenshots/truth/editor/router.png +0 -0
  139. package/screenshots/truth/editor/send_msg.png +0 -0
  140. package/screenshots/truth/editor/set_contact_language.png +0 -0
  141. package/screenshots/truth/editor/set_contact_name.png +0 -0
  142. package/screenshots/truth/editor/set_run_result.png +0 -0
  143. package/screenshots/truth/editor/wait.png +0 -0
  144. package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
  145. package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
  146. package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
  147. package/screenshots/truth/field-renderer/context-comparison.png +0 -0
  148. package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
  149. package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
  150. package/screenshots/truth/field-renderer/select-multi.png +0 -0
  151. package/screenshots/truth/field-renderer/select-no-label.png +0 -0
  152. package/screenshots/truth/field-renderer/select-with-label.png +0 -0
  153. package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
  154. package/screenshots/truth/field-renderer/text-no-label.png +0 -0
  155. package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
  156. package/screenshots/truth/field-renderer/text-with-label.png +0 -0
  157. package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
  158. package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
  159. package/screenshots/truth/formfield/markdown-errors.png +0 -0
  160. package/screenshots/truth/formfield/no-errors.png +0 -0
  161. package/screenshots/truth/formfield/plain-text-errors.png +0 -0
  162. package/screenshots/truth/message-editor/autogrow-initial-content.png +0 -0
  163. package/screenshots/truth/message-editor/default.png +0 -0
  164. package/screenshots/truth/message-editor/drag-highlight.png +0 -0
  165. package/screenshots/truth/message-editor/filtered-attachments.png +0 -0
  166. package/screenshots/truth/message-editor/with-completion.png +0 -0
  167. package/screenshots/truth/message-editor/with-properties.png +0 -0
  168. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  169. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  170. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  171. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  172. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  173. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  174. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  175. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  176. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  177. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  178. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  179. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  180. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  181. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  182. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  183. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  184. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  185. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  186. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  187. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  188. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  189. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  190. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  191. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  192. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  193. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  194. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  195. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  196. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  197. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  198. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  199. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  200. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  201. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  202. package/screenshots/truth/omnibox/selected.png +0 -0
  203. package/screenshots/truth/select/functions.png +0 -0
  204. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  205. package/screenshots/truth/select/search-enabled.png +0 -0
  206. package/screenshots/truth/textinput/autogrow-initial.png +0 -0
  207. package/screenshots/truth/textinput/input-form.png +0 -0
  208. package/src/display/Thumbnail.ts +2 -1
  209. package/src/events.ts +13 -1
  210. package/src/excellent/helpers.ts +2 -2
  211. package/src/flow/CanvasNode.ts +22 -1
  212. package/src/flow/Editor.ts +12 -1
  213. package/src/flow/NodeEditor.ts +412 -354
  214. package/src/flow/actions/add_input_labels.ts +45 -0
  215. package/src/flow/actions/call_llm.ts +57 -3
  216. package/src/flow/actions/call_webhook.ts +28 -18
  217. package/src/flow/actions/open_ticket.ts +74 -3
  218. package/src/flow/actions/send_msg.ts +170 -6
  219. package/src/flow/actions/set_run_result.ts +83 -0
  220. package/src/flow/config.ts +4 -0
  221. package/src/flow/nodes/split_by_llm_categorize.ts +277 -0
  222. package/src/flow/nodes/split_by_ticket.ts +19 -0
  223. package/src/flow/nodes/wait_for_response.ts +28 -1
  224. package/src/flow/types.ts +46 -128
  225. package/src/form/ArrayEditor.ts +96 -66
  226. package/src/form/BaseListEditor.ts +22 -6
  227. package/src/form/FieldRenderer.ts +465 -0
  228. package/src/form/FormField.ts +4 -4
  229. package/src/form/KeyValueEditor.ts +1 -1
  230. package/src/form/MediaPicker.ts +13 -1
  231. package/src/form/MessageEditor.ts +449 -0
  232. package/src/form/TextInput.ts +16 -8
  233. package/src/form/select/Select.ts +55 -24
  234. package/src/live/ContactChat.ts +69 -19
  235. package/src/markdown.ts +19 -11
  236. package/src/store/flow-definition.d.ts +5 -2
  237. package/static/api/labels.json +31 -0
  238. package/static/api/topics.json +24 -9
  239. package/static/api/users.json +35 -16
  240. package/static/css/temba-components.css +5 -3
  241. package/static/mr/docs/en-us/editor.json +2588 -0
  242. package/stress-test.js +143 -0
  243. package/temba-modules.ts +2 -0
  244. package/test/ActionHelper.ts +2 -0
  245. package/test/NodeHelper.ts +184 -0
  246. package/test/actions/call_llm.test.ts +137 -0
  247. package/test/nodes/README.md +78 -0
  248. package/test/nodes/split_by_llm_categorize.test.ts +698 -0
  249. package/test/nodes/split_by_random.test.ts +177 -0
  250. package/test/nodes/wait_for_digits.test.ts +176 -0
  251. package/test/nodes/wait_for_response.test.ts +206 -0
  252. package/test/temba-add-input-labels.test.ts +87 -0
  253. package/test/temba-field-config.test.ts +4 -2
  254. package/test/temba-field-renderer.test.ts +482 -0
  255. package/test/temba-markdown.test.ts +1 -1
  256. package/test/temba-message-editor.test.ts +300 -0
  257. package/test/temba-node-editor.test.ts +590 -0
  258. package/test/temba-select.test.ts +7 -7
  259. package/test/temba-textinput.test.ts +26 -0
  260. package/test/temba-webchat.test.ts +6 -1
  261. package/test/utils.test.ts +2 -13
  262. package/test-assets/contacts/history.json +19 -0
  263. package/test-assets/select/llms.json +18 -0
  264. package/test-assets/style.css +2 -0
  265. package/web-dev-mock.mjs +523 -0
  266. package/web-dev-server.config.mjs +74 -6
  267. package/web-test-runner.config.mjs +9 -4
  268. package/test/temba-flow-editor.test.ts.backup +0 -563
  269. package/test/temba-utils-index.test.ts.backup +0 -1737
@@ -10,16 +10,11 @@ import {
10
10
  FieldConfig,
11
11
  ActionConfig
12
12
  } from './config';
13
- import {
14
- SelectFieldConfig,
15
- CheckboxFieldConfig,
16
- TextareaFieldConfig,
17
- LayoutItem,
18
- RowLayoutConfig,
19
- GroupLayoutConfig
20
- } from './types';
13
+ import { LayoutItem, RowLayoutConfig, GroupLayoutConfig } from './types';
21
14
  import { CustomEventType } from '../interfaces';
22
15
  import { generateUUID } from '../utils';
16
+ import { FieldRenderer } from '../form/FieldRenderer';
17
+ import { renderMarkdownInline } from '../markdown';
23
18
 
24
19
  export class NodeEditor extends RapidElement {
25
20
  static get styles() {
@@ -31,6 +26,10 @@ export class NodeEditor extends RapidElement {
31
26
  gap: 15px;
32
27
  min-width: 400px;
33
28
  padding-bottom: 40px;
29
+
30
+ --color-bubble-bg: rgba(var(--primary-rgb), 0.7);
31
+ --color-bubble-border: rgba(0, 0, 0, 0.2);
32
+ --color-bubble-text: #fff;
34
33
  }
35
34
 
36
35
  .form-field {
@@ -39,10 +38,6 @@ export class NodeEditor extends RapidElement {
39
38
  }
40
39
 
41
40
  .form-field label {
42
- font-weight: 500;
43
- margin-bottom: 6px;
44
- color: #333;
45
- font-size: 14px;
46
41
  }
47
42
 
48
43
  .field-errors {
@@ -103,10 +98,16 @@ export class NodeEditor extends RapidElement {
103
98
  border-color: var(--color-error, tomato);
104
99
  }
105
100
 
101
+ .form-group.has-bubble {
102
+ border-width: 2px;
103
+ border-color: rgba(var(--primary-rgb), 0.5);
104
+ }
105
+
106
106
  .form-group-header {
107
107
  background: #f8f9fa;
108
- padding: 12px 15px;
108
+ padding: 8px 10px;
109
109
  border-bottom: 1px solid #e0e0e0;
110
+
110
111
  display: flex;
111
112
  align-items: center;
112
113
  justify-content: space-between;
@@ -114,6 +115,18 @@ export class NodeEditor extends RapidElement {
114
115
  user-select: none;
115
116
  }
116
117
 
118
+ .form-group.has-bubble .form-group-header {
119
+ background: rgba(var(--primary-rgb), 0.1);
120
+ }
121
+
122
+ .collapsed .form-group-header {
123
+ border: none;
124
+ }
125
+
126
+ .form-group-header:hover {
127
+ background: rgba(0, 0, 0, 0.05);
128
+ }
129
+
117
130
  .form-group-header.collapsible:hover {
118
131
  background: #f1f3f4;
119
132
  }
@@ -124,7 +137,7 @@ export class NodeEditor extends RapidElement {
124
137
 
125
138
  .form-group-title {
126
139
  font-weight: 500;
127
- color: #333;
140
+ color: var(--color-label, #777);
128
141
  font-size: 14px;
129
142
  display: flex;
130
143
  }
@@ -147,13 +160,13 @@ export class NodeEditor extends RapidElement {
147
160
  }
148
161
 
149
162
  .form-group-content {
150
- padding: 15px;
163
+ padding: 6px;
151
164
  display: flex;
152
165
  flex-direction: column;
153
166
  gap: 15px;
154
167
  overflow: hidden;
155
- transition: all 0.3s ease;
156
- max-height: 1000px; /* Large enough to accommodate most content */
168
+ transition: all 0.2s ease-in-out;
169
+
157
170
  opacity: 1;
158
171
  }
159
172
 
@@ -166,9 +179,14 @@ export class NodeEditor extends RapidElement {
166
179
 
167
180
  .group-toggle-icon {
168
181
  color: #666;
169
- transition: transform 0.3s ease;
182
+ transition: transform 0.3s ease, opacity 0.3s ease;
170
183
  cursor: pointer;
171
184
  transform: rotate(0deg);
185
+ opacity: 1;
186
+ }
187
+
188
+ .group-toggle-icon.faded {
189
+ opacity: 0;
172
190
  }
173
191
 
174
192
  .group-toggle-icon.expanded {
@@ -187,6 +205,58 @@ export class NodeEditor extends RapidElement {
187
205
  color: var(--color-error, tomato);
188
206
  margin-right: 8px;
189
207
  }
208
+
209
+ .group-count-bubble {
210
+ border-radius: 50%;
211
+ display: flex;
212
+ align-items: center;
213
+ justify-content: center;
214
+ font-size: 11px;
215
+ font-weight: 600;
216
+ padding: 4px;
217
+ min-width: 12px;
218
+ min-height: 12px;
219
+ position: absolute;
220
+ top: 50%;
221
+ left: 50%;
222
+ transform: translate(-50%, -50%);
223
+ line-height: 0px;
224
+ opacity: 1;
225
+ transition: opacity 0.3s ease;
226
+ background: var(--color-bubble-bg, #fff);
227
+ border: 1px solid var(--color-bubble-border, #777);
228
+ color: var(--color-bubble-text, #000);
229
+ }
230
+
231
+ .group-count-bubble.hidden {
232
+ opacity: 0;
233
+ pointer-events: none;
234
+ }
235
+
236
+ .group-checkmark-icon {
237
+ position: absolute;
238
+ top: 50%;
239
+ left: 50%;
240
+ transform: translate(-50%, -50%);
241
+ opacity: 1;
242
+ transition: opacity 0.3s ease;
243
+ border-radius: 50%;
244
+ color: var(--color-bubble-text, #000);
245
+ background: var(--color-bubble-bg, #fff);
246
+ border: 1px solid var(--color-bubble-border, #777);
247
+ padding: 0.2em;
248
+ }
249
+
250
+ .group-checkmark-icon.hidden {
251
+ opacity: 0;
252
+ pointer-events: none;
253
+ }
254
+
255
+ .group-toggle-container {
256
+ position: relative;
257
+ display: flex;
258
+ align-items: center;
259
+ }
190
260
  `;
191
261
  }
192
262
 
@@ -214,6 +284,9 @@ export class NodeEditor extends RapidElement {
214
284
  @state()
215
285
  private groupCollapseState: { [key: string]: boolean } = {};
216
286
 
287
+ @state()
288
+ private groupHoverState: { [key: string]: boolean } = {};
289
+
217
290
  connectedCallback(): void {
218
291
  super.connectedCallback();
219
292
  this.initializeFormData();
@@ -221,10 +294,21 @@ export class NodeEditor extends RapidElement {
221
294
 
222
295
  updated(changedProperties: Map<string | number | symbol, unknown>): void {
223
296
  super.updated(changedProperties);
224
- if (changedProperties.has('node') || changedProperties.has('action')) {
225
- if (this.node || this.action) {
297
+ if (
298
+ changedProperties.has('node') ||
299
+ changedProperties.has('action') ||
300
+ changedProperties.has('nodeUI')
301
+ ) {
302
+ // For action editing, we only need the action
303
+ if (this.action && (!this.node || !this.nodeUI)) {
226
304
  this.openDialog();
227
- } else {
305
+ }
306
+ // For node editing, we need both node and nodeUI
307
+ else if (this.node && this.nodeUI) {
308
+ this.openDialog();
309
+ }
310
+ // If we don't have the required data, close the dialog
311
+ else if (!this.action && (!this.node || !this.nodeUI)) {
228
312
  this.isOpen = false;
229
313
  }
230
314
  }
@@ -241,10 +325,13 @@ export class NodeEditor extends RapidElement {
241
325
  this.formData = {};
242
326
  this.errors = {};
243
327
  this.groupCollapseState = {};
328
+ this.groupHoverState = {};
244
329
  }
245
330
 
246
331
  private initializeFormData(): void {
247
- if (this.action) {
332
+ const nodeConfig = this.getNodeConfig();
333
+
334
+ if ((!nodeConfig || nodeConfig.type === 'execute_actions') && this.action) {
248
335
  // Action editing mode - use action config
249
336
  const actionConfig = ACTION_CONFIG[this.action.type];
250
337
 
@@ -252,8 +339,6 @@ export class NodeEditor extends RapidElement {
252
339
  this.formData = actionConfig.toFormData(this.action);
253
340
  } else {
254
341
  this.formData = { ...this.action };
255
- // Apply smart transformations for select fields that expect {name, value} format
256
- this.applySmartSelectTransformations(actionConfig);
257
342
  }
258
343
 
259
344
  // Convert Record objects to array format for key-value editors
@@ -311,52 +396,35 @@ export class NodeEditor extends RapidElement {
311
396
  this.formData = processed;
312
397
  }
313
398
 
314
- private applySmartSelectTransformations(actionConfig: ActionConfig): void {
315
- if (!actionConfig) return;
399
+ private isKeyValueField(fieldName: string): boolean {
400
+ // Check if this field is configured as a key-value type
401
+ const config = this.getConfig();
402
+ const fields = config?.form;
403
+ return fields?.[fieldName]?.type === 'key-value';
404
+ }
316
405
 
317
- const fields = actionConfig.form;
318
- if (!fields) return;
406
+ private getConfig(): ActionConfig | NodeConfig | null {
407
+ // If we have a node and nodeUI, check if we should use node config
408
+ if (this.node && this.nodeUI) {
409
+ const nodeConfig = this.getNodeConfig();
319
410
 
320
- Object.entries(fields).forEach(([fieldName, fieldConfig]) => {
321
- if (this.shouldApplySmartSelectTransformation(fieldName, fieldConfig)) {
322
- const value = this.formData[fieldName];
323
- if (
324
- Array.isArray(value) &&
325
- value.length > 0 &&
326
- typeof value[0] === 'string'
327
- ) {
328
- // Transform string array to select options format
329
- this.formData[fieldName] = value.map((item: string) => ({
330
- name: item,
331
- value: item
332
- }));
333
- }
411
+ // For execute_actions nodes, defer to action editing if an action is selected
412
+ if (this.nodeUI.type === 'execute_actions' && this.action) {
413
+ return ACTION_CONFIG[this.action.type] || null;
334
414
  }
335
- });
336
- }
337
415
 
338
- private shouldApplySmartSelectTransformation(
339
- fieldName: string,
340
- fieldConfig: any
341
- ): boolean {
342
- const selectConfig = fieldConfig as SelectFieldConfig;
343
- return (
344
- (fieldConfig.type === 'select' &&
345
- (selectConfig.multi || selectConfig.tags) &&
346
- // Don't transform if already has explicit transformations
347
- !this.action) ||
348
- !ACTION_CONFIG[this.action.type]?.toFormData
349
- );
350
- }
416
+ // For all other nodes with a config, use the node config
417
+ if (nodeConfig) {
418
+ return nodeConfig;
419
+ }
420
+ }
351
421
 
352
- private isKeyValueField(fieldName: string): boolean {
353
- // Check if this field is configured as a key-value type
422
+ // Fall back to action config if no node config or for pure action editing
354
423
  if (this.action) {
355
- const actionConfig = ACTION_CONFIG[this.action.type];
356
- const fields = actionConfig?.form;
357
- return fields?.[fieldName]?.type === 'key-value';
424
+ return ACTION_CONFIG[this.action.type] || null;
358
425
  }
359
- return false;
426
+
427
+ return null;
360
428
  }
361
429
 
362
430
  private getNodeConfig(): NodeConfig | null {
@@ -366,16 +434,8 @@ export class NodeEditor extends RapidElement {
366
434
  }
367
435
 
368
436
  private getHeaderColor(): string {
369
- if (this.action) {
370
- // Action editing mode
371
- const actionConfig = ACTION_CONFIG[this.action.type];
372
- return actionConfig?.color || '#666666';
373
- } else if (this.node) {
374
- // Node editing mode
375
- const nodeConfig = this.getNodeConfig();
376
- return nodeConfig?.color || '#666666';
377
- }
378
- return '#666666';
437
+ const config = this.getConfig();
438
+ return config?.color || '#666666';
379
439
  }
380
440
 
381
441
  private handleDialogButtonClick(event: CustomEvent): void {
@@ -466,89 +526,70 @@ export class NodeEditor extends RapidElement {
466
526
 
467
527
  private validateForm(): ValidationResult {
468
528
  const errors: { [key: string]: string } = {};
529
+ const config = this.getConfig();
469
530
 
470
- if (this.action) {
471
- // Action validation using fields configuration
472
- const actionConfig = ACTION_CONFIG[this.action.type];
473
-
531
+ if (config) {
474
532
  // Check if new field configuration system is available
475
- if (actionConfig?.form) {
476
- Object.entries(actionConfig?.form).forEach(
477
- ([fieldName, fieldConfig]) => {
478
- const value = this.formData[fieldName];
479
-
480
- // Check required fields
481
- if (
482
- (fieldConfig as any).required &&
483
- (!value || (Array.isArray(value) && value.length === 0))
484
- ) {
485
- errors[fieldName] = `${
486
- (fieldConfig as any).label || fieldName
487
- } is required`;
488
- }
533
+ if (config.form) {
534
+ Object.entries(config.form).forEach(([fieldName, fieldConfig]) => {
535
+ const value = this.formData[fieldName];
536
+
537
+ // Check required fields
538
+ if (
539
+ (fieldConfig as any).required &&
540
+ (!value || (Array.isArray(value) && value.length === 0))
541
+ ) {
542
+ errors[fieldName] = `${
543
+ (fieldConfig as any).label || fieldName
544
+ } is required.`;
545
+ }
489
546
 
490
- // Check minLength for text fields
491
- if (
492
- typeof value === 'string' &&
493
- (fieldConfig as any).minLength &&
494
- value.length < (fieldConfig as any).minLength
495
- ) {
496
- errors[fieldName] = `${
497
- (fieldConfig as any).label || fieldName
498
- } must be at least ${(fieldConfig as any).minLength} characters`;
499
- }
547
+ // Check minLength for text fields
548
+ if (
549
+ typeof value === 'string' &&
550
+ (fieldConfig as any).minLength &&
551
+ value.length < (fieldConfig as any).minLength
552
+ ) {
553
+ errors[fieldName] = `${
554
+ (fieldConfig as any).label || fieldName
555
+ } must be at least ${(fieldConfig as any).minLength} characters`;
556
+ }
500
557
 
501
- // Check maxLength for text fields
502
- if (
503
- typeof value === 'string' &&
504
- (fieldConfig as any).maxLength &&
505
- value.length > (fieldConfig as any).maxLength
506
- ) {
507
- errors[fieldName] = `${
508
- (fieldConfig as any).label || fieldName
509
- } must be no more than ${
510
- (fieldConfig as any).maxLength
511
- } characters`;
512
- }
558
+ // Check maxLength for text fields
559
+ if (
560
+ typeof value === 'string' &&
561
+ (fieldConfig as any).maxLength &&
562
+ value.length > (fieldConfig as any).maxLength
563
+ ) {
564
+ errors[fieldName] = `${
565
+ (fieldConfig as any).label || fieldName
566
+ } must be no more than ${
567
+ (fieldConfig as any).maxLength
568
+ } characters`;
513
569
  }
514
- );
570
+ });
515
571
  }
516
572
 
573
+ // Universal validation for category arrays to check for reserved names
574
+ this.validateCategoryNames(errors);
575
+
517
576
  // Run custom validation if available
518
- if (actionConfig?.validate) {
519
- // Convert form data back to action for validation
520
- let actionForValidation: Action;
521
- if (actionConfig.fromFormData) {
522
- actionForValidation = actionConfig.fromFormData(this.formData);
523
- } else {
524
- actionForValidation = { ...this.action, ...this.formData } as Action;
577
+ if (config.validate) {
578
+ if (config.sanitize) {
579
+ config.sanitize(this.formData);
525
580
  }
526
581
 
527
- const customValidation = actionConfig.validate(actionForValidation);
582
+ let customValidation;
583
+ if (this.action) {
584
+ customValidation = config.validate({
585
+ ...this.action,
586
+ ...this.formData
587
+ });
588
+ } else {
589
+ customValidation = config.validate(this.formData);
590
+ }
528
591
  Object.assign(errors, customValidation.errors);
529
592
  }
530
- } else if (this.node) {
531
- // Node validation
532
- const nodeConfig = this.getNodeConfig();
533
-
534
- // Check required fields from node properties
535
- if (nodeConfig?.properties) {
536
- Object.entries(nodeConfig.properties).forEach(
537
- ([fieldName, fieldConfig]) => {
538
- const value = this.formData[fieldName];
539
-
540
- // Check required fields
541
- if (
542
- fieldConfig.required &&
543
- (!value || (Array.isArray(value) && value.length === 0))
544
- ) {
545
- errors[fieldName] = `${
546
- fieldConfig.label || fieldName
547
- } is required`;
548
- }
549
- }
550
- );
551
- }
552
593
  }
553
594
 
554
595
  // Validate key-value fields for unique keys
@@ -599,6 +640,44 @@ export class NodeEditor extends RapidElement {
599
640
  });
600
641
  }
601
642
 
643
+ private validateCategoryNames(errors: { [key: string]: string }): void {
644
+ // Universal validation for category names across all node types
645
+ // Prevents use of reserved category names that have special meaning in the system
646
+ // Define reserved category names (case-insensitive)
647
+ const reservedNames = [
648
+ 'other',
649
+ 'failure',
650
+ 'success',
651
+ 'all responses',
652
+ 'no response'
653
+ ];
654
+
655
+ // Check all form fields for category arrays
656
+ Object.entries(this.formData).forEach(([fieldName, value]) => {
657
+ if (Array.isArray(value) && fieldName === 'categories') {
658
+ const categories = value.filter(
659
+ (item: any) => item?.name && item.name.trim() !== ''
660
+ );
661
+
662
+ // Check for reserved names
663
+ const reservedUsed = categories
664
+ .filter((item: any) => {
665
+ const lowerName = item.name.trim().toLowerCase();
666
+ return reservedNames.includes(lowerName);
667
+ })
668
+ .map((item: any) => item.name.trim()); // Preserve original case
669
+
670
+ if (reservedUsed.length > 0) {
671
+ errors[
672
+ fieldName
673
+ ] = `Reserved category names cannot be used: ${reservedUsed.join(
674
+ ', '
675
+ )}`;
676
+ }
677
+ }
678
+ });
679
+ }
680
+
602
681
  private formDataToNode(formData: any = this.formData): Node {
603
682
  if (!this.node) throw new Error('No node to update');
604
683
  let updatedNode: Node = { ...this.node };
@@ -765,42 +844,11 @@ export class NodeEditor extends RapidElement {
765
844
  if (actionConfig?.fromFormData) {
766
845
  return actionConfig.fromFormData(formData);
767
846
  } else {
768
- // Apply smart select transformations in reverse and provide default 1:1 mapping
769
- const processedFormData = this.reverseSmartSelectTransformations(
770
- formData,
771
- actionConfig
772
- );
773
- return { ...this.action, ...processedFormData };
847
+ // Default 1:1 mapping
848
+ return { ...this.action, ...formData };
774
849
  }
775
850
  }
776
851
 
777
- private reverseSmartSelectTransformations(
778
- formData: any,
779
- actionConfig: ActionConfig
780
- ): any {
781
- if (!actionConfig || !actionConfig.form) return formData;
782
- const processed = { ...formData };
783
-
784
- Object.entries(actionConfig.form).forEach(([fieldName, fieldConfig]) => {
785
- if (this.shouldApplySmartSelectTransformation(fieldName, fieldConfig)) {
786
- const value = processed[fieldName];
787
- if (
788
- Array.isArray(value) &&
789
- value.length > 0 &&
790
- typeof value[0] === 'object' &&
791
- 'value' in value[0]
792
- ) {
793
- // Transform select options format back to string array
794
- processed[fieldName] = value.map(
795
- (item: any) => item.value || item.name || item
796
- );
797
- }
798
- }
799
- });
800
-
801
- return processed;
802
- }
803
-
804
852
  private handleFormFieldChange(propertyName: string, event: Event): void {
805
853
  const target = event.target as any;
806
854
  let value: any;
@@ -834,14 +882,49 @@ export class NodeEditor extends RapidElement {
834
882
  // Check for computed values in dependent fields
835
883
  this.updateComputedFields(propertyName);
836
884
 
885
+ // Re-evaluate group collapse states that depend on form data
886
+ this.updateGroupCollapseStates();
887
+
837
888
  // Trigger re-render to handle conditional field visibility
838
889
  this.requestUpdate();
839
890
  }
840
891
 
841
- private updateComputedFields(changedFieldName: string): void {
842
- if (!this.action) return;
892
+ private updateGroupCollapseStates(): void {
893
+ const config = this.getConfig();
894
+ if (!config?.layout) return;
843
895
 
844
- const config = ACTION_CONFIG[this.action.type];
896
+ this.updateGroupCollapseStatesRecursive(config.layout);
897
+ }
898
+
899
+ private updateGroupCollapseStatesRecursive(items: LayoutItem[]): void {
900
+ items.forEach((item) => {
901
+ if (typeof item === 'object' && item.type === 'group') {
902
+ const { label, collapsed, collapsible } = item;
903
+
904
+ // Only update if the group is collapsible and has a function-based collapsed property
905
+ if (collapsible && typeof collapsed === 'function') {
906
+ const newCollapsedState = collapsed(this.formData);
907
+
908
+ // Only update if the state has changed to avoid unnecessary re-renders
909
+ if (this.groupCollapseState[label] !== newCollapsedState) {
910
+ this.groupCollapseState = {
911
+ ...this.groupCollapseState,
912
+ [label]: newCollapsedState
913
+ };
914
+ }
915
+ }
916
+
917
+ // Recursively check nested items
918
+ this.updateGroupCollapseStatesRecursive(item.items);
919
+ } else if (typeof item === 'object' && item.type === 'row') {
920
+ // Recursively check items in rows
921
+ this.updateGroupCollapseStatesRecursive(item.items);
922
+ }
923
+ });
924
+ }
925
+
926
+ private updateComputedFields(changedFieldName: string): void {
927
+ const config = this.getConfig();
845
928
  if (!config?.form) return;
846
929
 
847
930
  // Check all fields to see if any depend on the changed field
@@ -912,154 +995,33 @@ export class NodeEditor extends RapidElement {
912
995
  value: any,
913
996
  errors: string[]
914
997
  ): TemplateResult {
915
- switch (config.type) {
916
- case 'text':
917
- return html`<temba-textinput
918
- name="${fieldName}"
919
- label="${config.label}"
920
- ?required="${config.required}"
921
- .errors="${errors}"
922
- .value="${value || ''}"
923
- placeholder="${config.placeholder || ''}"
924
- .helpText="${config.helpText || ''}"
925
- @input="${(e: Event) => this.handleFormFieldChange(fieldName, e)}"
926
- ></temba-textinput>`;
927
-
928
- case 'textarea': {
929
- const textareaConfig = config as TextareaFieldConfig;
930
- const minHeightStyle = textareaConfig.minHeight
931
- ? `--textarea-min-height: ${textareaConfig.minHeight}px;`
932
- : '';
933
-
934
- if (config.evaluated) {
935
- return html`<temba-completion
936
- name="${fieldName}"
937
- label="${config.label}"
938
- ?required="${config.required}"
939
- .errors="${errors}"
940
- .value="${value || ''}"
941
- placeholder="${config.placeholder || ''}"
942
- textarea
943
- expressions="session"
944
- style="${minHeightStyle}"
945
- .helpText="${config.helpText || ''}"
946
- @input="${(e: Event) => this.handleFormFieldChange(fieldName, e)}"
947
- ></temba-completion>`;
998
+ // Use FieldRenderer for consistent field rendering
999
+ return FieldRenderer.renderField(fieldName, config, value, {
1000
+ errors,
1001
+ onChange: (e: Event) => {
1002
+ // Handle different change event types
1003
+ if (fieldName && config.type === 'key-value') {
1004
+ // Special handling for key-value editor
1005
+ const customEvent = e as CustomEvent;
1006
+ if (customEvent.detail) {
1007
+ this.handleNewFieldChange(fieldName, customEvent.detail.value);
1008
+ }
1009
+ } else if (fieldName && config.type === 'array') {
1010
+ // Special handling for array editor
1011
+ this.handleNewFieldChange(fieldName, (e.target as any).value);
1012
+ } else if (fieldName && config.type === 'message-editor') {
1013
+ // Special handling for message editor
1014
+ this.handleMessageEditorChange(fieldName, e);
948
1015
  } else {
949
- return html`<temba-textinput
950
- name="${fieldName}"
951
- label="${config.label}"
952
- ?required="${config.required}"
953
- .errors="${errors}"
954
- .value="${value || ''}"
955
- placeholder="${config.placeholder || ''}"
956
- textarea
957
- .rows="${textareaConfig.rows || 3}"
958
- style="${minHeightStyle}"
959
- .helpText="${config.helpText || ''}"
960
- @input="${(e: Event) => this.handleFormFieldChange(fieldName, e)}"
961
- ></temba-textinput>`;
1016
+ // Default handling for most field types
1017
+ this.handleFormFieldChange(fieldName, e);
962
1018
  }
1019
+ },
1020
+ showLabel: true,
1021
+ additionalData: {
1022
+ attachments: this.formData.attachments || []
963
1023
  }
964
-
965
- case 'select': {
966
- const selectConfig = config as SelectFieldConfig;
967
- return html`<temba-select
968
- name="${fieldName}"
969
- label="${config.label}"
970
- ?required="${config.required}"
971
- .errors="${errors}"
972
- .values="${value || (selectConfig.multi ? [] : '')}"
973
- ?multi="${selectConfig.multi}"
974
- ?searchable="${selectConfig.searchable}"
975
- ?tags="${selectConfig.tags}"
976
- ?emails="${selectConfig.emails}"
977
- placeholder="${selectConfig.placeholder || ''}"
978
- maxItems="${selectConfig.maxItems || 0}"
979
- valueKey="${selectConfig.valueKey || 'value'}"
980
- nameKey="${selectConfig.nameKey || 'name'}"
981
- endpoint="${selectConfig.endpoint || ''}"
982
- .helpText="${config.helpText || ''}"
983
- @change="${(e: Event) => this.handleFormFieldChange(fieldName, e)}"
984
- >
985
- ${selectConfig.options?.map((option: any) => {
986
- if (typeof option === 'string') {
987
- return html`<temba-option
988
- name="${option}"
989
- value="${option}"
990
- ></temba-option>`;
991
- } else {
992
- return html`<temba-option
993
- name="${option.label || option.name}"
994
- value="${option.value}"
995
- ></temba-option>`;
996
- }
997
- })}
998
- </temba-select>`;
999
- }
1000
-
1001
- case 'key-value':
1002
- return html`<div class="form-field">
1003
- <label>${config.label}${config.required ? ' *' : ''}</label>
1004
- <temba-key-value-editor
1005
- name="${fieldName}"
1006
- .value="${value || []}"
1007
- .sortable="${config.sortable}"
1008
- .keyPlaceholder="${config.keyPlaceholder || 'Key'}"
1009
- .valuePlaceholder="${config.valuePlaceholder || 'Value'}"
1010
- .minRows="${config.minRows || 0}"
1011
- @change="${(e: CustomEvent) => {
1012
- if (e.detail) {
1013
- this.handleNewFieldChange(fieldName, e.detail.value);
1014
- }
1015
- }}"
1016
- ></temba-key-value-editor>
1017
- ${errors.length
1018
- ? html`<div class="field-errors">${errors.join(', ')}</div>`
1019
- : ''}
1020
- </div>`;
1021
-
1022
- case 'array':
1023
- return html`<div class="form-field">
1024
- <label>${config.label}${config.required ? ' *' : ''}</label>
1025
- <temba-array-editor
1026
- .value="${value || []}"
1027
- .itemConfig="${config.itemConfig}"
1028
- .sortable="${config.sortable}"
1029
- .itemLabel="${config.itemLabel || 'Item'}"
1030
- .minItems="${config.minItems || 0}"
1031
- .onItemChange="${config.onItemChange}"
1032
- @change="${(e: CustomEvent) =>
1033
- this.handleNewFieldChange(fieldName, e.detail.value)}"
1034
- ></temba-array-editor>
1035
- ${errors.length
1036
- ? html`<div class="field-errors">${errors.join(', ')}</div>`
1037
- : ''}
1038
- </div>`;
1039
-
1040
- case 'checkbox': {
1041
- const checkboxConfig = config as CheckboxFieldConfig;
1042
- return html`<div class="form-field">
1043
- <temba-checkbox
1044
- name="${fieldName}"
1045
- label="${config.label}"
1046
- .helpText="${config.helpText || ''}"
1047
- ?required="${config.required}"
1048
- .errors="${errors}"
1049
- ?checked="${value || false}"
1050
- size="${checkboxConfig.size || 1.2}"
1051
- animateChange="${checkboxConfig.animateChange || 'pulse'}"
1052
- @change="${(e: Event) => this.handleFormFieldChange(fieldName, e)}"
1053
- ></temba-checkbox>
1054
- ${errors.length
1055
- ? html`<div class="field-errors">${errors.join(', ')}</div>`
1056
- : ''}
1057
- </div>`;
1058
- }
1059
-
1060
- default:
1061
- return html`<div>Unsupported field type: ${(config as any).type}</div>`;
1062
- }
1024
+ });
1063
1025
  }
1064
1026
 
1065
1027
  private handleGroupToggle(groupLabel: string): void {
@@ -1069,10 +1031,22 @@ export class NodeEditor extends RapidElement {
1069
1031
  };
1070
1032
  }
1071
1033
 
1072
- private expandGroupsWithErrors(errors: { [key: string]: string }): void {
1073
- if (!this.action) return;
1034
+ private handleGroupMouseEnter(groupLabel: string): void {
1035
+ this.groupHoverState = {
1036
+ ...this.groupHoverState,
1037
+ [groupLabel]: true
1038
+ };
1039
+ }
1040
+
1041
+ private handleGroupMouseLeave(groupLabel: string): void {
1042
+ this.groupHoverState = {
1043
+ ...this.groupHoverState,
1044
+ [groupLabel]: false
1045
+ };
1046
+ }
1074
1047
 
1075
- const config = ACTION_CONFIG[this.action.type];
1048
+ private expandGroupsWithErrors(errors: { [key: string]: string }): void {
1049
+ const config = this.getConfig();
1076
1050
  if (!config?.layout) return;
1077
1051
 
1078
1052
  const errorFields = new Set(Object.keys(errors));
@@ -1109,7 +1083,7 @@ export class NodeEditor extends RapidElement {
1109
1083
 
1110
1084
  private renderLayoutItem(
1111
1085
  item: LayoutItem,
1112
- config: ActionConfig,
1086
+ config: ActionConfig | NodeConfig,
1113
1087
  renderedFields: Set<string>
1114
1088
  ): TemplateResult {
1115
1089
  if (typeof item === 'string') {
@@ -1146,7 +1120,7 @@ export class NodeEditor extends RapidElement {
1146
1120
 
1147
1121
  private renderRow(
1148
1122
  rowConfig: RowLayoutConfig,
1149
- config: ActionConfig,
1123
+ config: ActionConfig | NodeConfig,
1150
1124
  renderedFields: Set<string>
1151
1125
  ): TemplateResult {
1152
1126
  const { items, gap = '1rem' } = rowConfig;
@@ -1183,7 +1157,7 @@ export class NodeEditor extends RapidElement {
1183
1157
 
1184
1158
  private renderGroup(
1185
1159
  groupConfig: GroupLayoutConfig,
1186
- config: ActionConfig,
1160
+ config: ActionConfig | NodeConfig,
1187
1161
  renderedFields: Set<string>
1188
1162
  ): TemplateResult {
1189
1163
  const {
@@ -1191,19 +1165,25 @@ export class NodeEditor extends RapidElement {
1191
1165
  items,
1192
1166
  collapsible = false,
1193
1167
  collapsed = false,
1194
- helpText
1168
+ helpText,
1169
+ getGroupValueCount
1195
1170
  } = groupConfig;
1196
1171
 
1197
1172
  // Initialize collapse state if not set
1198
1173
  if (collapsible && !(label in this.groupCollapseState)) {
1174
+ // Evaluate collapsed property - can be boolean or function
1175
+ const initialCollapsed =
1176
+ typeof collapsed === 'function' ? collapsed(this.formData) : collapsed;
1177
+
1199
1178
  this.groupCollapseState = {
1200
1179
  ...this.groupCollapseState,
1201
- [label]: collapsed
1180
+ [label]: initialCollapsed
1202
1181
  };
1203
1182
  }
1204
1183
 
1205
1184
  const isCollapsed = collapsible
1206
- ? this.groupCollapseState[label] ?? collapsed
1185
+ ? this.groupCollapseState[label] ??
1186
+ (typeof collapsed === 'function' ? collapsed(this.formData) : collapsed)
1207
1187
  : false;
1208
1188
 
1209
1189
  // Check if any field in this group has errors
@@ -1212,10 +1192,41 @@ export class NodeEditor extends RapidElement {
1212
1192
  (fieldName) => this.errors[fieldName]
1213
1193
  );
1214
1194
 
1195
+ // Calculate count for bubble display
1196
+ let valueCount = 0;
1197
+ let showBubble = false;
1198
+ let showCheckmark = false;
1199
+ let hasValue = false;
1200
+ const isHovered = this.groupHoverState[label] ?? false;
1201
+
1202
+ if (getGroupValueCount && collapsible) {
1203
+ try {
1204
+ const result = getGroupValueCount(this.formData);
1205
+
1206
+ if (typeof result === 'boolean') {
1207
+ // Boolean result - show checkmark when true
1208
+ showCheckmark = result && isCollapsed && !isHovered;
1209
+ hasValue = result;
1210
+ } else if (typeof result === 'number') {
1211
+ // Numeric result - show count bubble
1212
+ valueCount = result;
1213
+ showBubble = valueCount > 0 && isCollapsed && !isHovered;
1214
+ hasValue = valueCount > 0;
1215
+ }
1216
+ } catch (error) {
1217
+ console.error(
1218
+ `Error calculating group value count for ${label}:`,
1219
+ error
1220
+ );
1221
+ }
1222
+ }
1223
+
1215
1224
  return html`
1216
1225
  <div
1217
1226
  class="form-group ${collapsible ? 'collapsible' : ''} ${groupHasErrors
1218
1227
  ? 'has-errors'
1228
+ : ''} ${isCollapsed ? 'collapsed' : 'expanded'} ${hasValue
1229
+ ? 'has-bubble'
1219
1230
  : ''}"
1220
1231
  >
1221
1232
  <div
@@ -1223,11 +1234,19 @@ export class NodeEditor extends RapidElement {
1223
1234
  @click=${collapsible
1224
1235
  ? () => this.handleGroupToggle(label)
1225
1236
  : undefined}
1237
+ @mouseenter=${collapsible
1238
+ ? () => this.handleGroupMouseEnter(label)
1239
+ : undefined}
1240
+ @mouseleave=${collapsible
1241
+ ? () => this.handleGroupMouseLeave(label)
1242
+ : undefined}
1226
1243
  >
1227
1244
  <div class="form-group-info">
1228
1245
  <div class="form-group-title">${label}</div>
1229
1246
  ${helpText
1230
- ? html`<div class="form-group-help">${helpText}</div>`
1247
+ ? html`<div class="form-group-help">
1248
+ ${renderMarkdownInline(helpText)}
1249
+ </div>`
1231
1250
  : ''}
1232
1251
  </div>
1233
1252
  ${groupHasErrors
@@ -1238,13 +1257,28 @@ export class NodeEditor extends RapidElement {
1238
1257
  ></temba-icon>`
1239
1258
  : ''}
1240
1259
  ${collapsible && !groupHasErrors
1241
- ? html`<temba-icon
1242
- name="arrow_right"
1243
- size="1.5"
1244
- class="group-toggle-icon ${isCollapsed
1245
- ? 'collapsed'
1246
- : 'expanded'}"
1247
- ></temba-icon>`
1260
+ ? html`<div class="group-toggle-container">
1261
+ <temba-icon
1262
+ name="arrow_right"
1263
+ size="1.5"
1264
+ class="group-toggle-icon ${isCollapsed
1265
+ ? 'collapsed'
1266
+ : 'expanded'} ${showBubble || showCheckmark ? 'faded' : ''}"
1267
+ ></temba-icon>
1268
+ ${showCheckmark
1269
+ ? html`<temba-icon
1270
+ name="check"
1271
+ size="1"
1272
+ class="group-checkmark-icon"
1273
+ ></temba-icon>`
1274
+ : showBubble
1275
+ ? html`<div
1276
+ class="group-count-bubble ${!showBubble ? 'hidden' : ''}"
1277
+ >
1278
+ ${valueCount}
1279
+ </div>`
1280
+ : ''}
1281
+ </div>`
1248
1282
  : ''}
1249
1283
  </div>
1250
1284
  <div
@@ -1278,7 +1312,7 @@ export class NodeEditor extends RapidElement {
1278
1312
 
1279
1313
  private renderFieldRow(
1280
1314
  rowConfig: RowLayoutConfig,
1281
- config: ActionConfig
1315
+ config: ActionConfig | NodeConfig
1282
1316
  ): TemplateResult {
1283
1317
  // This method is deprecated - use renderRow instead
1284
1318
  return this.renderRow(rowConfig, config, new Set());
@@ -1286,7 +1320,7 @@ export class NodeEditor extends RapidElement {
1286
1320
 
1287
1321
  private renderFieldGroup(
1288
1322
  groupConfig: GroupLayoutConfig,
1289
- config: ActionConfig
1323
+ config: ActionConfig | NodeConfig
1290
1324
  ): TemplateResult {
1291
1325
  // This method is deprecated - use renderGroup instead
1292
1326
  return this.renderGroup(groupConfig, config, new Set());
@@ -1305,18 +1339,38 @@ export class NodeEditor extends RapidElement {
1305
1339
  this.errors = newErrors;
1306
1340
  }
1307
1341
 
1342
+ // Re-evaluate group collapse states that depend on form data
1343
+ this.updateGroupCollapseStates();
1344
+
1308
1345
  // Trigger re-render
1309
1346
  this.requestUpdate();
1310
1347
  }
1348
+ private handleMessageEditorChange(fieldName: string, event: Event): void {
1349
+ const target = event.target as any;
1311
1350
 
1312
- private renderFields(): TemplateResult {
1313
- if (!this.action) {
1314
- return html` <div>No action selected</div> `;
1351
+ // Update both text and attachments from the message editor
1352
+ this.formData = {
1353
+ ...this.formData,
1354
+ [fieldName]: target.value,
1355
+ attachments: target.attachments || []
1356
+ };
1357
+
1358
+ // Clear any existing errors for both fields
1359
+ if (this.errors[fieldName]) {
1360
+ const newErrors = { ...this.errors };
1361
+ delete newErrors[fieldName];
1362
+ delete newErrors.attachments;
1363
+ this.errors = newErrors;
1315
1364
  }
1316
1365
 
1317
- const config = ACTION_CONFIG[this.action.type];
1366
+ // Trigger re-render
1367
+ this.requestUpdate();
1368
+ }
1369
+
1370
+ private renderFields(): TemplateResult {
1371
+ const config = this.getConfig();
1318
1372
  if (!config) {
1319
- return html` <div>No configuration available for this action</div> `;
1373
+ return html` <div>No configuration available</div> `;
1320
1374
  }
1321
1375
 
1322
1376
  // Use the new fields configuration system
@@ -1357,7 +1411,12 @@ export class NodeEditor extends RapidElement {
1357
1411
  }
1358
1412
  }
1359
1413
 
1360
- return html` <div>No form configuration available</div> `;
1414
+ // Fallback for configs without form configuration
1415
+ if (this.action) {
1416
+ return html` <div>No form configuration available for this action</div> `;
1417
+ } else {
1418
+ return html` <div>No form configuration available for this node</div> `;
1419
+ }
1361
1420
  }
1362
1421
 
1363
1422
  private renderActionSection(): TemplateResult {
@@ -1430,12 +1489,11 @@ export class NodeEditor extends RapidElement {
1430
1489
  }
1431
1490
 
1432
1491
  const headerColor = this.getHeaderColor();
1433
- const nodeConfig = this.getNodeConfig();
1434
- const actionConfig = ACTION_CONFIG[this.action?.type];
1492
+ const config = this.getConfig();
1435
1493
 
1436
1494
  return html`
1437
1495
  <temba-dialog
1438
- header="${actionConfig?.name || nodeConfig?.name || 'Edit'}"
1496
+ header="${config?.name || 'Edit'}"
1439
1497
  .open="${this.isOpen}"
1440
1498
  @temba-button-clicked=${this.handleDialogButtonClick}
1441
1499
  primaryButtonName="Save"
@@ -1444,7 +1502,7 @@ export class NodeEditor extends RapidElement {
1444
1502
  >
1445
1503
  <div class="node-editor-form">
1446
1504
  ${this.renderFields()}
1447
- ${nodeConfig?.router?.configurable
1505
+ ${this.getNodeConfig()?.router?.configurable
1448
1506
  ? this.renderRouterSection()
1449
1507
  : null}
1450
1508
  </div>