@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
@@ -5,6 +5,8 @@ import { RapidElement } from '../RapidElement';
5
5
  import { NODE_CONFIG, ACTION_CONFIG } from './config';
6
6
  import { CustomEventType } from '../interfaces';
7
7
  import { generateUUID } from '../utils';
8
+ import { FieldRenderer } from '../form/FieldRenderer';
9
+ import { renderMarkdownInline } from '../markdown';
8
10
  export class NodeEditor extends RapidElement {
9
11
  constructor() {
10
12
  super(...arguments);
@@ -13,6 +15,7 @@ export class NodeEditor extends RapidElement {
13
15
  this.originalFormData = {};
14
16
  this.errors = {};
15
17
  this.groupCollapseState = {};
18
+ this.groupHoverState = {};
16
19
  }
17
20
  static get styles() {
18
21
  return css `
@@ -23,6 +26,10 @@ export class NodeEditor extends RapidElement {
23
26
  gap: 15px;
24
27
  min-width: 400px;
25
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;
26
33
  }
27
34
 
28
35
  .form-field {
@@ -31,10 +38,6 @@ export class NodeEditor extends RapidElement {
31
38
  }
32
39
 
33
40
  .form-field label {
34
- font-weight: 500;
35
- margin-bottom: 6px;
36
- color: #333;
37
- font-size: 14px;
38
41
  }
39
42
 
40
43
  .field-errors {
@@ -95,10 +98,16 @@ export class NodeEditor extends RapidElement {
95
98
  border-color: var(--color-error, tomato);
96
99
  }
97
100
 
101
+ .form-group.has-bubble {
102
+ border-width: 2px;
103
+ border-color: rgba(var(--primary-rgb), 0.5);
104
+ }
105
+
98
106
  .form-group-header {
99
107
  background: #f8f9fa;
100
- padding: 12px 15px;
108
+ padding: 8px 10px;
101
109
  border-bottom: 1px solid #e0e0e0;
110
+
102
111
  display: flex;
103
112
  align-items: center;
104
113
  justify-content: space-between;
@@ -106,6 +115,18 @@ export class NodeEditor extends RapidElement {
106
115
  user-select: none;
107
116
  }
108
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
+
109
130
  .form-group-header.collapsible:hover {
110
131
  background: #f1f3f4;
111
132
  }
@@ -116,7 +137,7 @@ export class NodeEditor extends RapidElement {
116
137
 
117
138
  .form-group-title {
118
139
  font-weight: 500;
119
- color: #333;
140
+ color: var(--color-label, #777);
120
141
  font-size: 14px;
121
142
  display: flex;
122
143
  }
@@ -139,13 +160,13 @@ export class NodeEditor extends RapidElement {
139
160
  }
140
161
 
141
162
  .form-group-content {
142
- padding: 15px;
163
+ padding: 6px;
143
164
  display: flex;
144
165
  flex-direction: column;
145
166
  gap: 15px;
146
167
  overflow: hidden;
147
- transition: all 0.3s ease;
148
- max-height: 1000px; /* Large enough to accommodate most content */
168
+ transition: all 0.2s ease-in-out;
169
+
149
170
  opacity: 1;
150
171
  }
151
172
 
@@ -158,9 +179,14 @@ export class NodeEditor extends RapidElement {
158
179
 
159
180
  .group-toggle-icon {
160
181
  color: #666;
161
- transition: transform 0.3s ease;
182
+ transition: transform 0.3s ease, opacity 0.3s ease;
162
183
  cursor: pointer;
163
184
  transform: rotate(0deg);
185
+ opacity: 1;
186
+ }
187
+
188
+ .group-toggle-icon.faded {
189
+ opacity: 0;
164
190
  }
165
191
 
166
192
  .group-toggle-icon.expanded {
@@ -179,6 +205,58 @@ export class NodeEditor extends RapidElement {
179
205
  color: var(--color-error, tomato);
180
206
  margin-right: 8px;
181
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
+ }
182
260
  `;
183
261
  }
184
262
  connectedCallback() {
@@ -187,11 +265,19 @@ export class NodeEditor extends RapidElement {
187
265
  }
188
266
  updated(changedProperties) {
189
267
  super.updated(changedProperties);
190
- if (changedProperties.has('node') || changedProperties.has('action')) {
191
- if (this.node || this.action) {
268
+ if (changedProperties.has('node') ||
269
+ changedProperties.has('action') ||
270
+ changedProperties.has('nodeUI')) {
271
+ // For action editing, we only need the action
272
+ if (this.action && (!this.node || !this.nodeUI)) {
192
273
  this.openDialog();
193
274
  }
194
- else {
275
+ // For node editing, we need both node and nodeUI
276
+ else if (this.node && this.nodeUI) {
277
+ this.openDialog();
278
+ }
279
+ // If we don't have the required data, close the dialog
280
+ else if (!this.action && (!this.node || !this.nodeUI)) {
195
281
  this.isOpen = false;
196
282
  }
197
283
  }
@@ -206,9 +292,11 @@ export class NodeEditor extends RapidElement {
206
292
  this.formData = {};
207
293
  this.errors = {};
208
294
  this.groupCollapseState = {};
295
+ this.groupHoverState = {};
209
296
  }
210
297
  initializeFormData() {
211
- if (this.action) {
298
+ const nodeConfig = this.getNodeConfig();
299
+ if ((!nodeConfig || nodeConfig.type === 'execute_actions') && this.action) {
212
300
  // Action editing mode - use action config
213
301
  const actionConfig = ACTION_CONFIG[this.action.type];
214
302
  if (actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.toFormData) {
@@ -216,8 +304,6 @@ export class NodeEditor extends RapidElement {
216
304
  }
217
305
  else {
218
306
  this.formData = { ...this.action };
219
- // Apply smart transformations for select fields that expect {name, value} format
220
- this.applySmartSelectTransformations(actionConfig);
221
307
  }
222
308
  // Convert Record objects to array format for key-value editors
223
309
  this.processFormDataForEditing();
@@ -269,45 +355,31 @@ export class NodeEditor extends RapidElement {
269
355
  });
270
356
  this.formData = processed;
271
357
  }
272
- applySmartSelectTransformations(actionConfig) {
273
- if (!actionConfig)
274
- return;
275
- const fields = actionConfig.form;
276
- if (!fields)
277
- return;
278
- Object.entries(fields).forEach(([fieldName, fieldConfig]) => {
279
- if (this.shouldApplySmartSelectTransformation(fieldName, fieldConfig)) {
280
- const value = this.formData[fieldName];
281
- if (Array.isArray(value) &&
282
- value.length > 0 &&
283
- typeof value[0] === 'string') {
284
- // Transform string array to select options format
285
- this.formData[fieldName] = value.map((item) => ({
286
- name: item,
287
- value: item
288
- }));
289
- }
290
- }
291
- });
292
- }
293
- shouldApplySmartSelectTransformation(fieldName, fieldConfig) {
294
- var _a;
295
- const selectConfig = fieldConfig;
296
- return ((fieldConfig.type === 'select' &&
297
- (selectConfig.multi || selectConfig.tags) &&
298
- // Don't transform if already has explicit transformations
299
- !this.action) ||
300
- !((_a = ACTION_CONFIG[this.action.type]) === null || _a === void 0 ? void 0 : _a.toFormData));
301
- }
302
358
  isKeyValueField(fieldName) {
303
359
  var _a;
304
360
  // Check if this field is configured as a key-value type
361
+ const config = this.getConfig();
362
+ const fields = config === null || config === void 0 ? void 0 : config.form;
363
+ return ((_a = fields === null || fields === void 0 ? void 0 : fields[fieldName]) === null || _a === void 0 ? void 0 : _a.type) === 'key-value';
364
+ }
365
+ getConfig() {
366
+ // If we have a node and nodeUI, check if we should use node config
367
+ if (this.node && this.nodeUI) {
368
+ const nodeConfig = this.getNodeConfig();
369
+ // For execute_actions nodes, defer to action editing if an action is selected
370
+ if (this.nodeUI.type === 'execute_actions' && this.action) {
371
+ return ACTION_CONFIG[this.action.type] || null;
372
+ }
373
+ // For all other nodes with a config, use the node config
374
+ if (nodeConfig) {
375
+ return nodeConfig;
376
+ }
377
+ }
378
+ // Fall back to action config if no node config or for pure action editing
305
379
  if (this.action) {
306
- const actionConfig = ACTION_CONFIG[this.action.type];
307
- const fields = actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.form;
308
- return ((_a = fields === null || fields === void 0 ? void 0 : fields[fieldName]) === null || _a === void 0 ? void 0 : _a.type) === 'key-value';
380
+ return ACTION_CONFIG[this.action.type] || null;
309
381
  }
310
- return false;
382
+ return null;
311
383
  }
312
384
  getNodeConfig() {
313
385
  if (!this.nodeUI)
@@ -316,17 +388,8 @@ export class NodeEditor extends RapidElement {
316
388
  return this.nodeUI.type ? NODE_CONFIG[this.nodeUI.type] : null;
317
389
  }
318
390
  getHeaderColor() {
319
- if (this.action) {
320
- // Action editing mode
321
- const actionConfig = ACTION_CONFIG[this.action.type];
322
- return (actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.color) || '#666666';
323
- }
324
- else if (this.node) {
325
- // Node editing mode
326
- const nodeConfig = this.getNodeConfig();
327
- return (nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.color) || '#666666';
328
- }
329
- return '#666666';
391
+ const config = this.getConfig();
392
+ return (config === null || config === void 0 ? void 0 : config.color) || '#666666';
330
393
  }
331
394
  handleDialogButtonClick(event) {
332
395
  const button = event.detail.button;
@@ -407,17 +470,16 @@ export class NodeEditor extends RapidElement {
407
470
  }
408
471
  validateForm() {
409
472
  const errors = {};
410
- if (this.action) {
411
- // Action validation using fields configuration
412
- const actionConfig = ACTION_CONFIG[this.action.type];
473
+ const config = this.getConfig();
474
+ if (config) {
413
475
  // Check if new field configuration system is available
414
- if (actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.form) {
415
- Object.entries(actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.form).forEach(([fieldName, fieldConfig]) => {
476
+ if (config.form) {
477
+ Object.entries(config.form).forEach(([fieldName, fieldConfig]) => {
416
478
  const value = this.formData[fieldName];
417
479
  // Check required fields
418
480
  if (fieldConfig.required &&
419
481
  (!value || (Array.isArray(value) && value.length === 0))) {
420
- errors[fieldName] = `${fieldConfig.label || fieldName} is required`;
482
+ errors[fieldName] = `${fieldConfig.label || fieldName} is required.`;
421
483
  }
422
484
  // Check minLength for text fields
423
485
  if (typeof value === 'string' &&
@@ -433,35 +495,26 @@ export class NodeEditor extends RapidElement {
433
495
  }
434
496
  });
435
497
  }
498
+ // Universal validation for category arrays to check for reserved names
499
+ this.validateCategoryNames(errors);
436
500
  // Run custom validation if available
437
- if (actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.validate) {
438
- // Convert form data back to action for validation
439
- let actionForValidation;
440
- if (actionConfig.fromFormData) {
441
- actionForValidation = actionConfig.fromFormData(this.formData);
501
+ if (config.validate) {
502
+ if (config.sanitize) {
503
+ config.sanitize(this.formData);
504
+ }
505
+ let customValidation;
506
+ if (this.action) {
507
+ customValidation = config.validate({
508
+ ...this.action,
509
+ ...this.formData
510
+ });
442
511
  }
443
512
  else {
444
- actionForValidation = { ...this.action, ...this.formData };
513
+ customValidation = config.validate(this.formData);
445
514
  }
446
- const customValidation = actionConfig.validate(actionForValidation);
447
515
  Object.assign(errors, customValidation.errors);
448
516
  }
449
517
  }
450
- else if (this.node) {
451
- // Node validation
452
- const nodeConfig = this.getNodeConfig();
453
- // Check required fields from node properties
454
- if (nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.properties) {
455
- Object.entries(nodeConfig.properties).forEach(([fieldName, fieldConfig]) => {
456
- const value = this.formData[fieldName];
457
- // Check required fields
458
- if (fieldConfig.required &&
459
- (!value || (Array.isArray(value) && value.length === 0))) {
460
- errors[fieldName] = `${fieldConfig.label || fieldName} is required`;
461
- }
462
- });
463
- }
464
- }
465
518
  // Validate key-value fields for unique keys
466
519
  this.validateKeyValueUniqueness(errors);
467
520
  return {
@@ -500,6 +553,34 @@ export class NodeEditor extends RapidElement {
500
553
  }
501
554
  });
502
555
  }
556
+ validateCategoryNames(errors) {
557
+ // Universal validation for category names across all node types
558
+ // Prevents use of reserved category names that have special meaning in the system
559
+ // Define reserved category names (case-insensitive)
560
+ const reservedNames = [
561
+ 'other',
562
+ 'failure',
563
+ 'success',
564
+ 'all responses',
565
+ 'no response'
566
+ ];
567
+ // Check all form fields for category arrays
568
+ Object.entries(this.formData).forEach(([fieldName, value]) => {
569
+ if (Array.isArray(value) && fieldName === 'categories') {
570
+ const categories = value.filter((item) => (item === null || item === void 0 ? void 0 : item.name) && item.name.trim() !== '');
571
+ // Check for reserved names
572
+ const reservedUsed = categories
573
+ .filter((item) => {
574
+ const lowerName = item.name.trim().toLowerCase();
575
+ return reservedNames.includes(lowerName);
576
+ })
577
+ .map((item) => item.name.trim()); // Preserve original case
578
+ if (reservedUsed.length > 0) {
579
+ errors[fieldName] = `Reserved category names cannot be used: ${reservedUsed.join(', ')}`;
580
+ }
581
+ }
582
+ });
583
+ }
503
584
  formDataToNode(formData = this.formData) {
504
585
  var _a;
505
586
  if (!this.node)
@@ -641,29 +722,10 @@ export class NodeEditor extends RapidElement {
641
722
  return actionConfig.fromFormData(formData);
642
723
  }
643
724
  else {
644
- // Apply smart select transformations in reverse and provide default 1:1 mapping
645
- const processedFormData = this.reverseSmartSelectTransformations(formData, actionConfig);
646
- return { ...this.action, ...processedFormData };
725
+ // Default 1:1 mapping
726
+ return { ...this.action, ...formData };
647
727
  }
648
728
  }
649
- reverseSmartSelectTransformations(formData, actionConfig) {
650
- if (!actionConfig || !actionConfig.form)
651
- return formData;
652
- const processed = { ...formData };
653
- Object.entries(actionConfig.form).forEach(([fieldName, fieldConfig]) => {
654
- if (this.shouldApplySmartSelectTransformation(fieldName, fieldConfig)) {
655
- const value = processed[fieldName];
656
- if (Array.isArray(value) &&
657
- value.length > 0 &&
658
- typeof value[0] === 'object' &&
659
- 'value' in value[0]) {
660
- // Transform select options format back to string array
661
- processed[fieldName] = value.map((item) => item.value || item.name || item);
662
- }
663
- }
664
- });
665
- return processed;
666
- }
667
729
  handleFormFieldChange(propertyName, event) {
668
730
  const target = event.target;
669
731
  let value;
@@ -693,13 +755,43 @@ export class NodeEditor extends RapidElement {
693
755
  }
694
756
  // Check for computed values in dependent fields
695
757
  this.updateComputedFields(propertyName);
758
+ // Re-evaluate group collapse states that depend on form data
759
+ this.updateGroupCollapseStates();
696
760
  // Trigger re-render to handle conditional field visibility
697
761
  this.requestUpdate();
698
762
  }
699
- updateComputedFields(changedFieldName) {
700
- if (!this.action)
763
+ updateGroupCollapseStates() {
764
+ const config = this.getConfig();
765
+ if (!(config === null || config === void 0 ? void 0 : config.layout))
701
766
  return;
702
- const config = ACTION_CONFIG[this.action.type];
767
+ this.updateGroupCollapseStatesRecursive(config.layout);
768
+ }
769
+ updateGroupCollapseStatesRecursive(items) {
770
+ items.forEach((item) => {
771
+ if (typeof item === 'object' && item.type === 'group') {
772
+ const { label, collapsed, collapsible } = item;
773
+ // Only update if the group is collapsible and has a function-based collapsed property
774
+ if (collapsible && typeof collapsed === 'function') {
775
+ const newCollapsedState = collapsed(this.formData);
776
+ // Only update if the state has changed to avoid unnecessary re-renders
777
+ if (this.groupCollapseState[label] !== newCollapsedState) {
778
+ this.groupCollapseState = {
779
+ ...this.groupCollapseState,
780
+ [label]: newCollapsedState
781
+ };
782
+ }
783
+ }
784
+ // Recursively check nested items
785
+ this.updateGroupCollapseStatesRecursive(item.items);
786
+ }
787
+ else if (typeof item === 'object' && item.type === 'row') {
788
+ // Recursively check items in rows
789
+ this.updateGroupCollapseStatesRecursive(item.items);
790
+ }
791
+ });
792
+ }
793
+ updateComputedFields(changedFieldName) {
794
+ const config = this.getConfig();
703
795
  if (!(config === null || config === void 0 ? void 0 : config.form))
704
796
  return;
705
797
  // Check all fields to see if any depend on the changed field
@@ -746,149 +838,36 @@ export class NodeEditor extends RapidElement {
746
838
  return fieldContent;
747
839
  }
748
840
  renderFieldContent(fieldName, config, value, errors) {
749
- var _a;
750
- switch (config.type) {
751
- case 'text':
752
- return html `<temba-textinput
753
- name="${fieldName}"
754
- label="${config.label}"
755
- ?required="${config.required}"
756
- .errors="${errors}"
757
- .value="${value || ''}"
758
- placeholder="${config.placeholder || ''}"
759
- .helpText="${config.helpText || ''}"
760
- @input="${(e) => this.handleFormFieldChange(fieldName, e)}"
761
- ></temba-textinput>`;
762
- case 'textarea': {
763
- const textareaConfig = config;
764
- const minHeightStyle = textareaConfig.minHeight
765
- ? `--textarea-min-height: ${textareaConfig.minHeight}px;`
766
- : '';
767
- if (config.evaluated) {
768
- return html `<temba-completion
769
- name="${fieldName}"
770
- label="${config.label}"
771
- ?required="${config.required}"
772
- .errors="${errors}"
773
- .value="${value || ''}"
774
- placeholder="${config.placeholder || ''}"
775
- textarea
776
- expressions="session"
777
- style="${minHeightStyle}"
778
- .helpText="${config.helpText || ''}"
779
- @input="${(e) => this.handleFormFieldChange(fieldName, e)}"
780
- ></temba-completion>`;
841
+ // Use FieldRenderer for consistent field rendering
842
+ return FieldRenderer.renderField(fieldName, config, value, {
843
+ errors,
844
+ onChange: (e) => {
845
+ // Handle different change event types
846
+ if (fieldName && config.type === 'key-value') {
847
+ // Special handling for key-value editor
848
+ const customEvent = e;
849
+ if (customEvent.detail) {
850
+ this.handleNewFieldChange(fieldName, customEvent.detail.value);
851
+ }
852
+ }
853
+ else if (fieldName && config.type === 'array') {
854
+ // Special handling for array editor
855
+ this.handleNewFieldChange(fieldName, e.target.value);
856
+ }
857
+ else if (fieldName && config.type === 'message-editor') {
858
+ // Special handling for message editor
859
+ this.handleMessageEditorChange(fieldName, e);
781
860
  }
782
861
  else {
783
- return html `<temba-textinput
784
- name="${fieldName}"
785
- label="${config.label}"
786
- ?required="${config.required}"
787
- .errors="${errors}"
788
- .value="${value || ''}"
789
- placeholder="${config.placeholder || ''}"
790
- textarea
791
- .rows="${textareaConfig.rows || 3}"
792
- style="${minHeightStyle}"
793
- .helpText="${config.helpText || ''}"
794
- @input="${(e) => this.handleFormFieldChange(fieldName, e)}"
795
- ></temba-textinput>`;
862
+ // Default handling for most field types
863
+ this.handleFormFieldChange(fieldName, e);
796
864
  }
865
+ },
866
+ showLabel: true,
867
+ additionalData: {
868
+ attachments: this.formData.attachments || []
797
869
  }
798
- case 'select': {
799
- const selectConfig = config;
800
- return html `<temba-select
801
- name="${fieldName}"
802
- label="${config.label}"
803
- ?required="${config.required}"
804
- .errors="${errors}"
805
- .values="${value || (selectConfig.multi ? [] : '')}"
806
- ?multi="${selectConfig.multi}"
807
- ?searchable="${selectConfig.searchable}"
808
- ?tags="${selectConfig.tags}"
809
- ?emails="${selectConfig.emails}"
810
- placeholder="${selectConfig.placeholder || ''}"
811
- maxItems="${selectConfig.maxItems || 0}"
812
- valueKey="${selectConfig.valueKey || 'value'}"
813
- nameKey="${selectConfig.nameKey || 'name'}"
814
- endpoint="${selectConfig.endpoint || ''}"
815
- .helpText="${config.helpText || ''}"
816
- @change="${(e) => this.handleFormFieldChange(fieldName, e)}"
817
- >
818
- ${(_a = selectConfig.options) === null || _a === void 0 ? void 0 : _a.map((option) => {
819
- if (typeof option === 'string') {
820
- return html `<temba-option
821
- name="${option}"
822
- value="${option}"
823
- ></temba-option>`;
824
- }
825
- else {
826
- return html `<temba-option
827
- name="${option.label || option.name}"
828
- value="${option.value}"
829
- ></temba-option>`;
830
- }
831
- })}
832
- </temba-select>`;
833
- }
834
- case 'key-value':
835
- return html `<div class="form-field">
836
- <label>${config.label}${config.required ? ' *' : ''}</label>
837
- <temba-key-value-editor
838
- name="${fieldName}"
839
- .value="${value || []}"
840
- .sortable="${config.sortable}"
841
- .keyPlaceholder="${config.keyPlaceholder || 'Key'}"
842
- .valuePlaceholder="${config.valuePlaceholder || 'Value'}"
843
- .minRows="${config.minRows || 0}"
844
- @change="${(e) => {
845
- if (e.detail) {
846
- this.handleNewFieldChange(fieldName, e.detail.value);
847
- }
848
- }}"
849
- ></temba-key-value-editor>
850
- ${errors.length
851
- ? html `<div class="field-errors">${errors.join(', ')}</div>`
852
- : ''}
853
- </div>`;
854
- case 'array':
855
- return html `<div class="form-field">
856
- <label>${config.label}${config.required ? ' *' : ''}</label>
857
- <temba-array-editor
858
- .value="${value || []}"
859
- .itemConfig="${config.itemConfig}"
860
- .sortable="${config.sortable}"
861
- .itemLabel="${config.itemLabel || 'Item'}"
862
- .minItems="${config.minItems || 0}"
863
- .onItemChange="${config.onItemChange}"
864
- @change="${(e) => this.handleNewFieldChange(fieldName, e.detail.value)}"
865
- ></temba-array-editor>
866
- ${errors.length
867
- ? html `<div class="field-errors">${errors.join(', ')}</div>`
868
- : ''}
869
- </div>`;
870
- case 'checkbox': {
871
- const checkboxConfig = config;
872
- return html `<div class="form-field">
873
- <temba-checkbox
874
- name="${fieldName}"
875
- label="${config.label}"
876
- .helpText="${config.helpText || ''}"
877
- ?required="${config.required}"
878
- .errors="${errors}"
879
- ?checked="${value || false}"
880
- size="${checkboxConfig.size || 1.2}"
881
- animateChange="${checkboxConfig.animateChange || 'pulse'}"
882
- @change="${(e) => this.handleFormFieldChange(fieldName, e)}"
883
- ></temba-checkbox>
884
- ${errors.length
885
- ? html `<div class="field-errors">${errors.join(', ')}</div>`
886
- : ''}
887
- </div>`;
888
- }
889
- default:
890
- return html `<div>Unsupported field type: ${config.type}</div>`;
891
- }
870
+ });
892
871
  }
893
872
  handleGroupToggle(groupLabel) {
894
873
  this.groupCollapseState = {
@@ -896,10 +875,20 @@ export class NodeEditor extends RapidElement {
896
875
  [groupLabel]: !this.groupCollapseState[groupLabel]
897
876
  };
898
877
  }
878
+ handleGroupMouseEnter(groupLabel) {
879
+ this.groupHoverState = {
880
+ ...this.groupHoverState,
881
+ [groupLabel]: true
882
+ };
883
+ }
884
+ handleGroupMouseLeave(groupLabel) {
885
+ this.groupHoverState = {
886
+ ...this.groupHoverState,
887
+ [groupLabel]: false
888
+ };
889
+ }
899
890
  expandGroupsWithErrors(errors) {
900
- if (!this.action)
901
- return;
902
- const config = ACTION_CONFIG[this.action.type];
891
+ const config = this.getConfig();
903
892
  if (!(config === null || config === void 0 ? void 0 : config.layout))
904
893
  return;
905
894
  const errorFields = new Set(Object.keys(errors));
@@ -969,25 +958,54 @@ export class NodeEditor extends RapidElement {
969
958
  `;
970
959
  }
971
960
  renderGroup(groupConfig, config, renderedFields) {
972
- var _a;
973
- const { label, items, collapsible = false, collapsed = false, helpText } = groupConfig;
961
+ var _a, _b;
962
+ const { label, items, collapsible = false, collapsed = false, helpText, getGroupValueCount } = groupConfig;
974
963
  // Initialize collapse state if not set
975
964
  if (collapsible && !(label in this.groupCollapseState)) {
965
+ // Evaluate collapsed property - can be boolean or function
966
+ const initialCollapsed = typeof collapsed === 'function' ? collapsed(this.formData) : collapsed;
976
967
  this.groupCollapseState = {
977
968
  ...this.groupCollapseState,
978
- [label]: collapsed
969
+ [label]: initialCollapsed
979
970
  };
980
971
  }
981
972
  const isCollapsed = collapsible
982
- ? (_a = this.groupCollapseState[label]) !== null && _a !== void 0 ? _a : collapsed
973
+ ? (_a = this.groupCollapseState[label]) !== null && _a !== void 0 ? _a : (typeof collapsed === 'function' ? collapsed(this.formData) : collapsed)
983
974
  : false;
984
975
  // Check if any field in this group has errors
985
976
  const fieldsInGroup = this.collectFieldsFromItems(items);
986
977
  const groupHasErrors = fieldsInGroup.some((fieldName) => this.errors[fieldName]);
978
+ // Calculate count for bubble display
979
+ let valueCount = 0;
980
+ let showBubble = false;
981
+ let showCheckmark = false;
982
+ let hasValue = false;
983
+ const isHovered = (_b = this.groupHoverState[label]) !== null && _b !== void 0 ? _b : false;
984
+ if (getGroupValueCount && collapsible) {
985
+ try {
986
+ const result = getGroupValueCount(this.formData);
987
+ if (typeof result === 'boolean') {
988
+ // Boolean result - show checkmark when true
989
+ showCheckmark = result && isCollapsed && !isHovered;
990
+ hasValue = result;
991
+ }
992
+ else if (typeof result === 'number') {
993
+ // Numeric result - show count bubble
994
+ valueCount = result;
995
+ showBubble = valueCount > 0 && isCollapsed && !isHovered;
996
+ hasValue = valueCount > 0;
997
+ }
998
+ }
999
+ catch (error) {
1000
+ console.error(`Error calculating group value count for ${label}:`, error);
1001
+ }
1002
+ }
987
1003
  return html `
988
1004
  <div
989
1005
  class="form-group ${collapsible ? 'collapsible' : ''} ${groupHasErrors
990
1006
  ? 'has-errors'
1007
+ : ''} ${isCollapsed ? 'collapsed' : 'expanded'} ${hasValue
1008
+ ? 'has-bubble'
991
1009
  : ''}"
992
1010
  >
993
1011
  <div
@@ -995,11 +1013,19 @@ export class NodeEditor extends RapidElement {
995
1013
  @click=${collapsible
996
1014
  ? () => this.handleGroupToggle(label)
997
1015
  : undefined}
1016
+ @mouseenter=${collapsible
1017
+ ? () => this.handleGroupMouseEnter(label)
1018
+ : undefined}
1019
+ @mouseleave=${collapsible
1020
+ ? () => this.handleGroupMouseLeave(label)
1021
+ : undefined}
998
1022
  >
999
1023
  <div class="form-group-info">
1000
1024
  <div class="form-group-title">${label}</div>
1001
1025
  ${helpText
1002
- ? html `<div class="form-group-help">${helpText}</div>`
1026
+ ? html `<div class="form-group-help">
1027
+ ${renderMarkdownInline(helpText)}
1028
+ </div>`
1003
1029
  : ''}
1004
1030
  </div>
1005
1031
  ${groupHasErrors
@@ -1010,13 +1036,28 @@ export class NodeEditor extends RapidElement {
1010
1036
  ></temba-icon>`
1011
1037
  : ''}
1012
1038
  ${collapsible && !groupHasErrors
1013
- ? html `<temba-icon
1014
- name="arrow_right"
1015
- size="1.5"
1016
- class="group-toggle-icon ${isCollapsed
1039
+ ? html `<div class="group-toggle-container">
1040
+ <temba-icon
1041
+ name="arrow_right"
1042
+ size="1.5"
1043
+ class="group-toggle-icon ${isCollapsed
1017
1044
  ? 'collapsed'
1018
- : 'expanded'}"
1019
- ></temba-icon>`
1045
+ : 'expanded'} ${showBubble || showCheckmark ? 'faded' : ''}"
1046
+ ></temba-icon>
1047
+ ${showCheckmark
1048
+ ? html `<temba-icon
1049
+ name="check"
1050
+ size="1"
1051
+ class="group-checkmark-icon"
1052
+ ></temba-icon>`
1053
+ : showBubble
1054
+ ? html `<div
1055
+ class="group-count-bubble ${!showBubble ? 'hidden' : ''}"
1056
+ >
1057
+ ${valueCount}
1058
+ </div>`
1059
+ : ''}
1060
+ </div>`
1020
1061
  : ''}
1021
1062
  </div>
1022
1063
  <div
@@ -1064,16 +1105,33 @@ export class NodeEditor extends RapidElement {
1064
1105
  delete newErrors[fieldName];
1065
1106
  this.errors = newErrors;
1066
1107
  }
1108
+ // Re-evaluate group collapse states that depend on form data
1109
+ this.updateGroupCollapseStates();
1067
1110
  // Trigger re-render
1068
1111
  this.requestUpdate();
1069
1112
  }
1070
- renderFields() {
1071
- if (!this.action) {
1072
- return html ` <div>No action selected</div> `;
1113
+ handleMessageEditorChange(fieldName, event) {
1114
+ const target = event.target;
1115
+ // Update both text and attachments from the message editor
1116
+ this.formData = {
1117
+ ...this.formData,
1118
+ [fieldName]: target.value,
1119
+ attachments: target.attachments || []
1120
+ };
1121
+ // Clear any existing errors for both fields
1122
+ if (this.errors[fieldName]) {
1123
+ const newErrors = { ...this.errors };
1124
+ delete newErrors[fieldName];
1125
+ delete newErrors.attachments;
1126
+ this.errors = newErrors;
1073
1127
  }
1074
- const config = ACTION_CONFIG[this.action.type];
1128
+ // Trigger re-render
1129
+ this.requestUpdate();
1130
+ }
1131
+ renderFields() {
1132
+ const config = this.getConfig();
1075
1133
  if (!config) {
1076
- return html ` <div>No configuration available for this action</div> `;
1134
+ return html ` <div>No configuration available</div> `;
1077
1135
  }
1078
1136
  // Use the new fields configuration system
1079
1137
  if (config.form) {
@@ -1099,7 +1157,13 @@ export class NodeEditor extends RapidElement {
1099
1157
  `;
1100
1158
  }
1101
1159
  }
1102
- return html ` <div>No form configuration available</div> `;
1160
+ // Fallback for configs without form configuration
1161
+ if (this.action) {
1162
+ return html ` <div>No form configuration available for this action</div> `;
1163
+ }
1164
+ else {
1165
+ return html ` <div>No form configuration available for this node</div> `;
1166
+ }
1103
1167
  }
1104
1168
  renderActionSection() {
1105
1169
  if (!this.node || this.node.actions.length === 0) {
@@ -1163,11 +1227,10 @@ export class NodeEditor extends RapidElement {
1163
1227
  return html ``;
1164
1228
  }
1165
1229
  const headerColor = this.getHeaderColor();
1166
- const nodeConfig = this.getNodeConfig();
1167
- const actionConfig = ACTION_CONFIG[(_a = this.action) === null || _a === void 0 ? void 0 : _a.type];
1230
+ const config = this.getConfig();
1168
1231
  return html `
1169
1232
  <temba-dialog
1170
- header="${(actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.name) || (nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.name) || 'Edit'}"
1233
+ header="${(config === null || config === void 0 ? void 0 : config.name) || 'Edit'}"
1171
1234
  .open="${this.isOpen}"
1172
1235
  @temba-button-clicked=${this.handleDialogButtonClick}
1173
1236
  primaryButtonName="Save"
@@ -1176,7 +1239,7 @@ export class NodeEditor extends RapidElement {
1176
1239
  >
1177
1240
  <div class="node-editor-form">
1178
1241
  ${this.renderFields()}
1179
- ${((_b = nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.router) === null || _b === void 0 ? void 0 : _b.configurable)
1242
+ ${((_b = (_a = this.getNodeConfig()) === null || _a === void 0 ? void 0 : _a.router) === null || _b === void 0 ? void 0 : _b.configurable)
1180
1243
  ? this.renderRouterSection()
1181
1244
  : null}
1182
1245
  </div>
@@ -1208,4 +1271,7 @@ __decorate([
1208
1271
  __decorate([
1209
1272
  state()
1210
1273
  ], NodeEditor.prototype, "groupCollapseState", void 0);
1274
+ __decorate([
1275
+ state()
1276
+ ], NodeEditor.prototype, "groupHoverState", void 0);
1211
1277
  //# sourceMappingURL=NodeEditor.js.map