@nyaruka/temba-components 0.129.3 → 0.129.4

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 (304) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.github/workflows/build.yml +135 -3
  3. package/CHANGELOG.md +18 -0
  4. package/demo/data/flows/sample-flow.json +110 -87
  5. package/demo/field-config-demo.html +135 -0
  6. package/dist/temba-components.js +1257 -675
  7. package/dist/temba-components.js.map +1 -1
  8. package/docs/ActionEditor-Migration.md +118 -0
  9. package/out-tsc/src/events.js.map +1 -1
  10. package/out-tsc/src/flow/{EditorNode.js → CanvasNode.js} +345 -42
  11. package/out-tsc/src/flow/CanvasNode.js.map +1 -0
  12. package/out-tsc/src/flow/Editor.js +107 -3
  13. package/out-tsc/src/flow/Editor.js.map +1 -1
  14. package/out-tsc/src/flow/NodeEditor.js +1200 -0
  15. package/out-tsc/src/flow/NodeEditor.js.map +1 -0
  16. package/out-tsc/src/flow/Plumber.js +0 -6
  17. package/out-tsc/src/flow/Plumber.js.map +1 -1
  18. package/out-tsc/src/flow/actions/add_contact_groups.js +40 -0
  19. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -0
  20. package/out-tsc/src/flow/actions/add_contact_urn.js +16 -0
  21. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -0
  22. package/out-tsc/src/flow/actions/add_input_labels.js +11 -0
  23. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -0
  24. package/out-tsc/src/flow/actions/call_classifier.js +11 -0
  25. package/out-tsc/src/flow/actions/call_classifier.js.map +1 -0
  26. package/out-tsc/src/flow/actions/call_llm.js +11 -0
  27. package/out-tsc/src/flow/actions/call_llm.js.map +1 -0
  28. package/out-tsc/src/flow/actions/call_resthook.js +11 -0
  29. package/out-tsc/src/flow/actions/call_resthook.js.map +1 -0
  30. package/out-tsc/src/flow/actions/call_webhook.js +122 -0
  31. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -0
  32. package/out-tsc/src/flow/actions/enter_flow.js +14 -0
  33. package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
  34. package/out-tsc/src/flow/actions/open_ticket.js +11 -0
  35. package/out-tsc/src/flow/actions/open_ticket.js.map +1 -0
  36. package/out-tsc/src/flow/actions/play_audio.js +11 -0
  37. package/out-tsc/src/flow/actions/play_audio.js.map +1 -0
  38. package/out-tsc/src/flow/actions/remove_contact_groups.js +62 -0
  39. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -0
  40. package/out-tsc/src/flow/actions/request_optin.js +11 -0
  41. package/out-tsc/src/flow/actions/request_optin.js.map +1 -0
  42. package/out-tsc/src/flow/actions/say_msg.js +11 -0
  43. package/out-tsc/src/flow/actions/say_msg.js.map +1 -0
  44. package/out-tsc/src/flow/actions/send_broadcast.js +33 -0
  45. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -0
  46. package/out-tsc/src/flow/actions/send_email.js +56 -0
  47. package/out-tsc/src/flow/actions/send_email.js.map +1 -0
  48. package/out-tsc/src/flow/actions/send_msg.js +55 -0
  49. package/out-tsc/src/flow/actions/send_msg.js.map +1 -0
  50. package/out-tsc/src/flow/actions/set_contact_channel.js +12 -0
  51. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -0
  52. package/out-tsc/src/flow/actions/set_contact_field.js +12 -0
  53. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -0
  54. package/out-tsc/src/flow/actions/set_contact_language.js +10 -0
  55. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -0
  56. package/out-tsc/src/flow/actions/set_contact_name.js +10 -0
  57. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -0
  58. package/out-tsc/src/flow/actions/set_contact_status.js +10 -0
  59. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -0
  60. package/out-tsc/src/flow/actions/set_run_result.js +10 -0
  61. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -0
  62. package/out-tsc/src/flow/actions/split_by_expression_example.js +77 -0
  63. package/out-tsc/src/flow/actions/split_by_expression_example.js.map +1 -0
  64. package/out-tsc/src/flow/actions/start_session.js +11 -0
  65. package/out-tsc/src/flow/actions/start_session.js.map +1 -0
  66. package/out-tsc/src/flow/actions/transfer_airtime.js +11 -0
  67. package/out-tsc/src/flow/actions/transfer_airtime.js.map +1 -0
  68. package/out-tsc/src/flow/config.js +88 -193
  69. package/out-tsc/src/flow/config.js.map +1 -1
  70. package/out-tsc/src/flow/nodes/execute_actions.js +4 -0
  71. package/out-tsc/src/flow/nodes/execute_actions.js.map +1 -0
  72. package/out-tsc/src/flow/nodes/split_by_airtime.js +9 -0
  73. package/out-tsc/src/flow/nodes/split_by_airtime.js.map +1 -0
  74. package/out-tsc/src/flow/nodes/split_by_contact_field.js +7 -0
  75. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -0
  76. package/out-tsc/src/flow/nodes/split_by_expression.js +7 -0
  77. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -0
  78. package/out-tsc/src/flow/nodes/split_by_groups.js +7 -0
  79. package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -0
  80. package/out-tsc/src/flow/nodes/split_by_random.js +10 -0
  81. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -0
  82. package/out-tsc/src/flow/nodes/split_by_run_result.js +7 -0
  83. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -0
  84. package/out-tsc/src/flow/nodes/split_by_scheme.js +7 -0
  85. package/out-tsc/src/flow/nodes/split_by_scheme.js.map +1 -0
  86. package/out-tsc/src/flow/nodes/split_by_subflow.js +9 -0
  87. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -0
  88. package/out-tsc/src/flow/nodes/split_by_webhook.js +18 -0
  89. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -0
  90. package/out-tsc/src/flow/nodes/wait_for_audio.js +7 -0
  91. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
  92. package/out-tsc/src/flow/nodes/wait_for_digits.js +7 -0
  93. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -0
  94. package/out-tsc/src/flow/nodes/wait_for_image.js +7 -0
  95. package/out-tsc/src/flow/nodes/wait_for_image.js.map +1 -0
  96. package/out-tsc/src/flow/nodes/wait_for_location.js +7 -0
  97. package/out-tsc/src/flow/nodes/wait_for_location.js.map +1 -0
  98. package/out-tsc/src/flow/nodes/wait_for_menu.js +7 -0
  99. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -0
  100. package/out-tsc/src/flow/nodes/wait_for_response.js +7 -0
  101. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -0
  102. package/out-tsc/src/flow/nodes/wait_for_video.js +7 -0
  103. package/out-tsc/src/flow/nodes/wait_for_video.js.map +1 -0
  104. package/out-tsc/src/flow/types.js +79 -0
  105. package/out-tsc/src/flow/types.js.map +1 -0
  106. package/out-tsc/src/flow/utils.js +65 -0
  107. package/out-tsc/src/flow/utils.js.map +1 -0
  108. package/out-tsc/src/form/ArrayEditor.js +199 -0
  109. package/out-tsc/src/form/ArrayEditor.js.map +1 -0
  110. package/out-tsc/src/form/BaseListEditor.js +128 -0
  111. package/out-tsc/src/form/BaseListEditor.js.map +1 -0
  112. package/out-tsc/src/form/Checkbox.js +17 -2
  113. package/out-tsc/src/form/Checkbox.js.map +1 -1
  114. package/out-tsc/src/form/Completion.js +6 -0
  115. package/out-tsc/src/form/Completion.js.map +1 -1
  116. package/out-tsc/src/form/FormField.js +110 -11
  117. package/out-tsc/src/form/FormField.js.map +1 -1
  118. package/out-tsc/src/form/KeyValueEditor.js +223 -0
  119. package/out-tsc/src/form/KeyValueEditor.js.map +1 -0
  120. package/out-tsc/src/form/select/Select.js +77 -32
  121. package/out-tsc/src/form/select/Select.js.map +1 -1
  122. package/out-tsc/src/interfaces.js +6 -0
  123. package/out-tsc/src/interfaces.js.map +1 -1
  124. package/out-tsc/src/live/ContactChat.js +2 -76
  125. package/out-tsc/src/live/ContactChat.js.map +1 -1
  126. package/out-tsc/temba-modules.js +9 -2
  127. package/out-tsc/temba-modules.js.map +1 -1
  128. package/out-tsc/test/ActionHelper.js +116 -0
  129. package/out-tsc/test/ActionHelper.js.map +1 -0
  130. package/out-tsc/test/actions/add_contact_groups.test.js +66 -0
  131. package/out-tsc/test/actions/add_contact_groups.test.js.map +1 -0
  132. package/out-tsc/test/actions/remove_contact_groups.test.js +226 -0
  133. package/out-tsc/test/actions/remove_contact_groups.test.js.map +1 -0
  134. package/out-tsc/test/actions/send_email.test.js +160 -0
  135. package/out-tsc/test/actions/send_email.test.js.map +1 -0
  136. package/out-tsc/test/actions/send_msg.test.js +95 -0
  137. package/out-tsc/test/actions/send_msg.test.js.map +1 -0
  138. package/out-tsc/test/temba-action-editing-integration.test.js +183 -0
  139. package/out-tsc/test/temba-action-editing-integration.test.js.map +1 -0
  140. package/out-tsc/test/temba-checkbox.test.js +1 -1
  141. package/out-tsc/test/temba-checkbox.test.js.map +1 -1
  142. package/out-tsc/test/temba-field-config.test.js +133 -0
  143. package/out-tsc/test/temba-field-config.test.js.map +1 -0
  144. package/out-tsc/test/temba-flow-editor-node.test.js +14 -14
  145. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  146. package/out-tsc/test/temba-node-editor.test.js +283 -0
  147. package/out-tsc/test/temba-node-editor.test.js.map +1 -0
  148. package/out-tsc/test/temba-select.test.js +85 -0
  149. package/out-tsc/test/temba-select.test.js.map +1 -1
  150. package/package.json +1 -1
  151. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  152. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  153. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  154. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  155. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  156. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  157. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  158. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  159. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  160. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  161. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  162. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  163. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  164. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  165. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  166. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  167. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  168. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  169. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  170. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  171. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  172. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  173. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  174. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  175. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  176. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  177. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  178. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  179. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  180. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  181. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  182. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  183. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  184. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  185. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  186. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  187. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  188. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  189. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  190. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  191. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  192. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  193. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  194. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  195. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  196. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  197. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  198. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  199. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  200. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  201. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  202. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  203. package/screenshots/truth/editor/router.png +0 -0
  204. package/screenshots/truth/editor/send_msg.png +0 -0
  205. package/screenshots/truth/editor/set_contact_language.png +0 -0
  206. package/screenshots/truth/editor/set_contact_name.png +0 -0
  207. package/screenshots/truth/editor/set_run_result.png +0 -0
  208. package/screenshots/truth/editor/wait.png +0 -0
  209. package/screenshots/truth/formfield/markdown-errors.png +0 -0
  210. package/screenshots/truth/formfield/plain-text-errors.png +0 -0
  211. package/screenshots/truth/formfield/widget-only-markdown-errors.png +0 -0
  212. package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
  213. package/src/events.ts +1 -40
  214. package/src/flow/{EditorNode.ts → CanvasNode.ts} +424 -48
  215. package/src/flow/Editor.ts +140 -4
  216. package/src/flow/NodeEditor.ts +1443 -0
  217. package/src/flow/Plumber.ts +0 -9
  218. package/src/flow/actions/add_contact_groups.ts +42 -0
  219. package/src/flow/actions/add_contact_urn.ts +17 -0
  220. package/src/flow/actions/add_input_labels.ts +12 -0
  221. package/src/flow/actions/call_classifier.ts +12 -0
  222. package/src/flow/actions/call_llm.ts +12 -0
  223. package/src/flow/actions/call_resthook.ts +12 -0
  224. package/src/flow/actions/call_webhook.ts +133 -0
  225. package/src/flow/actions/enter_flow.ts +15 -0
  226. package/src/flow/actions/open_ticket.ts +12 -0
  227. package/src/flow/actions/play_audio.ts +12 -0
  228. package/src/flow/actions/remove_contact_groups.ts +66 -0
  229. package/src/flow/actions/request_optin.ts +12 -0
  230. package/src/flow/actions/say_msg.ts +12 -0
  231. package/src/flow/actions/send_broadcast.ts +35 -0
  232. package/src/flow/actions/send_email.ts +60 -0
  233. package/src/flow/actions/send_msg.ts +58 -0
  234. package/src/flow/actions/set_contact_channel.ts +13 -0
  235. package/src/flow/actions/set_contact_field.ts +13 -0
  236. package/src/flow/actions/set_contact_language.ts +11 -0
  237. package/src/flow/actions/set_contact_name.ts +11 -0
  238. package/src/flow/actions/set_contact_status.ts +11 -0
  239. package/src/flow/actions/set_run_result.ts +11 -0
  240. package/src/flow/actions/split_by_expression_example.ts +88 -0
  241. package/src/flow/actions/start_session.ts +12 -0
  242. package/src/flow/actions/transfer_airtime.ts +12 -0
  243. package/src/flow/config.ts +93 -232
  244. package/src/flow/nodes/execute_actions.ts +5 -0
  245. package/src/flow/nodes/split_by_airtime.ts +9 -0
  246. package/src/flow/nodes/split_by_contact_field.ts +7 -0
  247. package/src/flow/nodes/split_by_expression.ts +7 -0
  248. package/src/flow/nodes/split_by_groups.ts +7 -0
  249. package/src/flow/nodes/split_by_random.ts +10 -0
  250. package/src/flow/nodes/split_by_run_result.ts +7 -0
  251. package/src/flow/nodes/split_by_scheme.ts +7 -0
  252. package/src/flow/nodes/split_by_subflow.ts +9 -0
  253. package/src/flow/nodes/split_by_webhook.ts +19 -0
  254. package/src/flow/nodes/wait_for_audio.ts +7 -0
  255. package/src/flow/nodes/wait_for_digits.ts +7 -0
  256. package/src/flow/nodes/wait_for_image.ts +7 -0
  257. package/src/flow/nodes/wait_for_location.ts +7 -0
  258. package/src/flow/nodes/wait_for_menu.ts +7 -0
  259. package/src/flow/nodes/wait_for_response.ts +7 -0
  260. package/src/flow/nodes/wait_for_video.ts +7 -0
  261. package/src/flow/types.ts +352 -0
  262. package/src/flow/utils.ts +76 -0
  263. package/src/form/ArrayEditor.ts +240 -0
  264. package/src/form/BaseListEditor.ts +177 -0
  265. package/src/form/Checkbox.ts +22 -3
  266. package/src/form/Completion.ts +6 -0
  267. package/src/form/FormField.ts +115 -11
  268. package/src/form/KeyValueEditor.ts +251 -0
  269. package/src/form/select/Select.ts +89 -32
  270. package/src/interfaces.ts +7 -2
  271. package/src/live/ContactChat.ts +3 -97
  272. package/src/store/flow-definition.d.ts +6 -1
  273. package/static/api/contacts.json +30 -0
  274. package/static/api/groups.json +4 -426
  275. package/static/api/locations.json +24 -0
  276. package/static/api/media.json +5 -0
  277. package/static/api/optins.json +16 -0
  278. package/static/api/orgs.json +13 -0
  279. package/static/api/topics.json +21 -0
  280. package/static/api/users.json +26 -0
  281. package/static/css/temba-components.css +3 -6
  282. package/temba-modules.ts +9 -2
  283. package/test/ActionHelper.ts +142 -0
  284. package/test/actions/add_contact_groups.test.ts +89 -0
  285. package/test/actions/remove_contact_groups.test.ts +265 -0
  286. package/test/actions/send_email.test.ts +214 -0
  287. package/test/actions/send_msg.test.ts +130 -0
  288. package/test/temba-action-editing-integration.test.ts +240 -0
  289. package/test/temba-checkbox.test.ts +1 -1
  290. package/test/temba-field-config.test.ts +152 -0
  291. package/test/temba-flow-editor-node.test.ts +18 -18
  292. package/test/temba-node-editor.test.ts +353 -0
  293. package/test/temba-select.test.ts +127 -0
  294. package/test-assets/contacts/history.json +11 -33
  295. package/web-dev-server.config.mjs +34 -0
  296. package/.github/workflows/coverage.yml +0 -80
  297. package/demo/sticky-note-demo.html +0 -155
  298. package/out-tsc/src/flow/EditorNode.js.map +0 -1
  299. package/out-tsc/src/flow/render.js +0 -358
  300. package/out-tsc/src/flow/render.js.map +0 -1
  301. package/out-tsc/test/temba-flow-render.test.js +0 -794
  302. package/out-tsc/test/temba-flow-render.test.js.map +0 -1
  303. package/src/flow/render.ts +0 -443
  304. package/test/temba-flow-render.test.ts +0 -1003
@@ -0,0 +1,118 @@
1
+ # ActionEditor Form Data Abstraction Migration Guide
2
+
3
+ ## Overview
4
+
5
+ The ActionEditor now supports form-level data transformations that provide a higher level of abstraction than the previous per-property approach. This allows for flexible mapping between action properties and form fields.
6
+
7
+ ## Key Changes
8
+
9
+ ### 1. Action-Level Transformations
10
+ Instead of per-property `toFormValue`/`fromFormValue`, you can now define these at the action level:
11
+
12
+ ```typescript
13
+ export const myAction: UIConfig = {
14
+ // Transform entire action to form data
15
+ toFormValue: (action: MyAction) => ({
16
+ // Form data can combine multiple action properties
17
+ contact_info: `${action.name} <${action.email}>`,
18
+ selected_options: action.options.map(opt => ({ name: opt, value: opt }))
19
+ }),
20
+
21
+ // Transform form data back to action
22
+ fromFormValue: (formData: any): MyAction => {
23
+ // Parse the combined contact_info back to separate fields
24
+ const [name, email] = parseContactInfo(formData.contact_info);
25
+ return {
26
+ ...action,
27
+ name,
28
+ email,
29
+ options: formData.selected_options.map(opt => opt.value)
30
+ };
31
+ }
32
+ }
33
+ ```
34
+
35
+ ### 2. Form Configuration (replaces properties)
36
+ Use `form` instead of `properties`, with keys matching your form data structure:
37
+
38
+ ```typescript
39
+ export const myAction: UIConfig = {
40
+ // OLD: properties keys matched action properties
41
+ properties: {
42
+ name: { /* config */ },
43
+ email: { /* config */ }
44
+ },
45
+
46
+ // NEW: form keys match form data structure
47
+ form: {
48
+ contact_info: { /* combines name + email */ },
49
+ selected_options: { /* transforms options array */ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Migration Examples
55
+
56
+ ### Before (per-property transformations)
57
+ ```typescript
58
+ export const send_msg: UIConfig = {
59
+ properties: {
60
+ quick_replies: {
61
+ toFormValue: (actionValue: string[]) =>
62
+ actionValue.map(text => ({ name: text, value: text })),
63
+ fromFormValue: (formValue: Array<{name: string, value: string}>) =>
64
+ formValue.map(item => item.value)
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ ### After (form-level transformations)
71
+ ```typescript
72
+ export const send_msg_new: UIConfig = {
73
+ toFormValue: (action: SendMsg) => ({
74
+ text: action.text,
75
+ quick_replies: action.quick_replies.map(text => ({ name: text, value: text }))
76
+ }),
77
+
78
+ fromFormValue: (formData: any): SendMsg => ({
79
+ ...formData,
80
+ type: 'send_msg',
81
+ quick_replies: formData.quick_replies.map(item => item.value || item.name)
82
+ }),
83
+
84
+ form: {
85
+ text: { /* config */ },
86
+ quick_replies: { /* config */ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ ## Benefits
92
+
93
+ 1. **Flexible field mapping**: Form fields don't need to match action properties 1:1
94
+ 2. **Data consolidation**: Combine multiple action properties into single form fields
95
+ 3. **Cleaner separation**: Form structure is independent of action structure
96
+ 4. **Backward compatibility**: Existing configs continue to work unchanged
97
+
98
+ ## Backward Compatibility
99
+
100
+ The ActionEditor automatically falls back to the old per-property approach when form-level transformations aren't provided:
101
+
102
+ - Uses `properties` if `form` is not defined
103
+ - Applies per-property `toFormValue`/`fromFormValue` if action-level ones aren't provided
104
+ - Maintains 1:1 mapping when no transformations are specified
105
+
106
+ ## Testing
107
+
108
+ Test both transformation directions:
109
+
110
+ ```typescript
111
+ it('should transform action to form data', async () => {
112
+ // Set action and verify formData structure
113
+ });
114
+
115
+ it('should transform form data back to action on save', async () => {
116
+ // Modify formData and verify saved action structure
117
+ });
118
+ ```
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/events.ts"],"names":[],"mappings":"","sourcesContent":["import { Msg, ObjectReference, User } from './interfaces';\n\nexport interface EventGroup {\n type: string;\n events: ContactEvent[];\n open: boolean;\n}\n\nexport interface ContactEvent {\n type: string;\n created_on: string;\n created_by?: User;\n}\n\nexport interface ChannelEvent extends ContactEvent {\n channel_event_type: string;\n duration: number;\n\n event: {\n type: string;\n channel: { uuid: string; name: string };\n duration?: number;\n optin?: {\n uuid: string;\n name: string;\n };\n };\n}\n\nexport interface ContactLanguageChangedEvent extends ContactEvent {\n language: string;\n step_uuid: string;\n session_uuid: string;\n}\n\nexport interface OptinRequestedEvent extends ContactEvent {\n optin: {\n uuid: string;\n name: string;\n };\n}\n\nexport interface MsgEvent extends ContactEvent {\n msg: Msg;\n status: string;\n failed_reason?: string;\n failed_reason_display?: string;\n logs_url: string;\n msg_type: string;\n recipient_count?: number;\n created_by?: User;\n optin?: ObjectReference;\n}\n\nexport interface FlowEvent extends ContactEvent {\n flow: ObjectReference;\n status: string;\n}\n\nexport interface EmailSentEvent extends ContactEvent {\n to: string[];\n subject: string;\n body: string;\n}\n\nexport interface URNsChangedEvent extends ContactEvent {\n urns: string[];\n}\n\nexport interface TicketEvent extends ContactEvent {\n note?: string;\n assignee?: User;\n ticket: {\n uuid: string;\n topic?: ObjectReference;\n closed_on?: string;\n opened_on?: string;\n };\n topic?: ObjectReference;\n created_by?: User;\n}\n\nexport interface LabelsAddedEvent extends ContactEvent {\n labels: ObjectReference[];\n}\n\nexport interface NameChangedEvent extends ContactEvent {\n name: string;\n}\n\nexport interface UpdateFieldEvent extends ContactEvent {\n field: { key: string; name: string };\n value: { text: string };\n}\n\nexport interface ErrorMessageEvent extends ContactEvent {\n text: string;\n}\n\nexport interface UpdateResultEvent extends ContactEvent {\n name: string;\n value: string;\n category: string;\n input: string;\n}\n\nexport interface ContactGroupsEvent extends ContactEvent {\n groups_added: ObjectReference[];\n groups_removed: ObjectReference[];\n}\n\nexport interface WebhookEvent extends ContactEvent {\n status: string;\n status_code: number;\n elapsed_ms: number;\n logs_url: string;\n url: string;\n}\n\nexport interface AirtimeTransferredEvent extends ContactEvent {\n sender: string;\n recipient: string;\n currency: string;\n desired_amount: string;\n actual_amount: string;\n logs_url: string;\n}\n\nexport type CallStartedEvent = ContactEvent;\nexport interface CampaignFiredEvent extends ContactEvent {\n campaign: { uuid: string; id: number; name: string };\n campaign_event: {\n id: number;\n offset_display: string;\n relative_to: { key: string; name: string };\n };\n fired_result: string;\n}\n\nexport interface ContactHistoryPage {\n has_older: boolean;\n recent_only: boolean;\n next_before: number;\n next_after: number;\n start_date: Date;\n events: ContactEvent[];\n}\n"]}
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/events.ts"],"names":[],"mappings":"","sourcesContent":["import { Msg, ObjectReference, User } from './interfaces';\n\nexport interface EventGroup {\n type: string;\n events: ContactEvent[];\n open: boolean;\n}\n\nexport interface ContactEvent {\n uuid?: string;\n type: string;\n created_on: string;\n created_by?: User;\n}\n\nexport interface ChannelEvent extends ContactEvent {\n channel_event_type: string;\n duration: number;\n\n event: {\n type: string;\n channel: { uuid: string; name: string };\n duration?: number;\n optin?: {\n uuid: string;\n name: string;\n };\n };\n}\n\nexport interface ContactLanguageChangedEvent extends ContactEvent {\n language: string;\n step_uuid: string;\n session_uuid: string;\n}\n\nexport interface OptinRequestedEvent extends ContactEvent {\n optin: {\n uuid: string;\n name: string;\n };\n}\n\nexport interface MsgEvent extends ContactEvent {\n msg: Msg;\n status: string;\n failed_reason?: string;\n failed_reason_display?: string;\n logs_url: string;\n recipient_count?: number;\n created_by?: User;\n optin?: ObjectReference;\n}\n\nexport interface FlowEvent extends ContactEvent {\n flow: ObjectReference;\n status: string;\n}\n\nexport interface URNsChangedEvent extends ContactEvent {\n urns: string[];\n}\n\nexport interface TicketEvent extends ContactEvent {\n note?: string;\n assignee?: User;\n ticket: {\n uuid: string;\n topic?: ObjectReference;\n closed_on?: string;\n opened_on?: string;\n };\n topic?: ObjectReference;\n created_by?: User;\n}\n\nexport interface NameChangedEvent extends ContactEvent {\n name: string;\n}\n\nexport interface UpdateFieldEvent extends ContactEvent {\n field: { key: string; name: string };\n value: { text: string };\n}\n\nexport interface ContactGroupsEvent extends ContactEvent {\n groups_added: ObjectReference[];\n groups_removed: ObjectReference[];\n}\n\nexport interface AirtimeTransferredEvent extends ContactEvent {\n sender: string;\n recipient: string;\n currency: string;\n desired_amount: string;\n actual_amount: string;\n}\n\nexport type CallStartedEvent = ContactEvent;\n\nexport interface ContactHistoryPage {\n has_older: boolean;\n recent_only: boolean;\n next_before: number;\n next_after: number;\n start_date: Date;\n events: ContactEvent[];\n}\n"]}
@@ -1,12 +1,14 @@
1
1
  import { __decorate } from "tslib";
2
2
  import { css, html } from 'lit';
3
- import { EDITOR_CONFIG } from './config';
3
+ import { repeat } from 'lit/directives/repeat.js';
4
+ import { ACTION_CONFIG, NODE_CONFIG } from './config';
4
5
  import { property } from 'lit/decorators.js';
5
6
  import { RapidElement } from '../RapidElement';
6
7
  import { getClasses } from '../utils';
7
8
  import { getStore } from '../store/Store';
8
9
  import { CustomEventType } from '../interfaces';
9
- export class EditorNode extends RapidElement {
10
+ const DRAG_THRESHOLD = 5;
11
+ export class CanvasNode extends RapidElement {
10
12
  createRenderRoot() {
11
13
  return this;
12
14
  }
@@ -25,6 +27,11 @@ export class EditorNode extends RapidElement {
25
27
 
26
28
  }
27
29
 
30
+ /* Cap width for execute_actions nodes */
31
+ .node.execute-actions {
32
+ max-width: 200px;
33
+ }
34
+
28
35
  .node:hover {
29
36
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
30
37
  }
@@ -36,8 +43,8 @@ export class EditorNode extends RapidElement {
36
43
  }
37
44
 
38
45
  .action {
39
- max-width: 200px;
40
46
  position: relative;
47
+ font-size: 13px;
41
48
  }
42
49
 
43
50
  .action .remove-button {
@@ -59,18 +66,40 @@ export class EditorNode extends RapidElement {
59
66
  z-index: 10;
60
67
  }
61
68
 
62
- .action:hover .remove-button {
69
+ .action:hover .remove-button,
70
+ .router:hover .remove-button {
63
71
  display: flex;
64
72
  }
65
73
 
66
- .action.removing .title {
74
+ .action.removing .title,
75
+ .router .title.removing {
67
76
  background-color: var(--color-error, #dc3545) !important;
68
77
  }
69
78
 
70
- .action.removing .title .name {
79
+ .action.removing .title .name,
80
+ .router .title.removing .name {
71
81
  color: white;
72
82
  }
73
83
 
84
+ .router .remove-button {
85
+ position: absolute;
86
+ top: 5px;
87
+ right: 5px;
88
+ width: 16px;
89
+ height: 16px;
90
+ border-radius: 50%;
91
+ background: var(--color-error, #dc3545);
92
+ color: white;
93
+ border: none;
94
+ cursor: pointer;
95
+ display: none;
96
+ align-items: center;
97
+ justify-content: center;
98
+ font-size: 10px;
99
+ line-height: 1;
100
+ z-index: 10;
101
+ }
102
+
74
103
  .action.sortable {
75
104
  display: flex;
76
105
  align-items: stretch;
@@ -80,10 +109,17 @@ export class EditorNode extends RapidElement {
80
109
  flex-grow: 1;
81
110
  display: flex;
82
111
  flex-direction: column;
112
+ min-width: 0; /* Allow flex item to shrink below its content size */
113
+ overflow: hidden;
83
114
  }
84
115
 
85
116
  .action .body {
86
- padding: 1em;
117
+ padding: 0.75em;
118
+ word-wrap: break-word;
119
+ overflow-wrap: break-word;
120
+ hyphens: auto;
121
+ white-space: normal;
122
+ overflow: hidden;
87
123
  }
88
124
 
89
125
  .action .drag-handle {
@@ -159,6 +195,10 @@ export class EditorNode extends RapidElement {
159
195
  .category .title {
160
196
  font-weight: normal;
161
197
  font-size: 1em;
198
+ max-width: 150px;
199
+ white-space: nowrap;
200
+ overflow: hidden;
201
+ text-overflow: ellipsis;
162
202
  }
163
203
 
164
204
  .router .body {
@@ -267,26 +307,39 @@ export class EditorNode extends RapidElement {
267
307
  this.actionRemovalTimeouts = new Map();
268
308
  // Set of action UUIDs that are in the removing state
269
309
  this.actionRemovingState = new Set();
310
+ // Track action click state to distinguish from drag
311
+ this.actionClickStartPos = null;
312
+ this.pendingActionClick = null;
313
+ // Track node click state to distinguish from drag
314
+ this.nodeClickStartPos = null;
315
+ this.pendingNodeClick = null;
270
316
  this.handleActionOrderChanged = this.handleActionOrderChanged.bind(this);
271
317
  }
272
318
  updated(changes) {
273
319
  var _a;
274
320
  super.updated(changes);
275
321
  if (changes.has('node')) {
276
- // make our initial connections
277
- if (changes.get('node') === undefined) {
322
+ // Only proceed if plumber is available (for tests that don't set it up)
323
+ if (this.plumber) {
324
+ this.plumber.removeNodeConnections(this.node.uuid);
325
+ // make our initial connections
278
326
  for (const exit of this.node.exits) {
279
327
  if (!exit.destination_uuid) {
328
+ // if we have no destination, then we are a source
329
+ // so make our source endpoint
280
330
  this.plumber.makeSource(exit.uuid);
281
331
  }
282
332
  else {
283
333
  this.plumber.connectIds(this.node.uuid, exit.uuid, exit.destination_uuid);
284
334
  }
285
335
  }
336
+ this.plumber.revalidate([this.node.uuid]);
286
337
  }
287
338
  const ele = this.parentElement;
288
- const rect = ele.getBoundingClientRect();
289
- (_a = getStore()) === null || _a === void 0 ? void 0 : _a.getState().expandCanvas(this.ui.position.left + rect.width, this.ui.position.top + rect.height);
339
+ if (ele) {
340
+ const rect = ele.getBoundingClientRect();
341
+ (_a = getStore()) === null || _a === void 0 ? void 0 : _a.getState().expandCanvas(this.ui.position.left + rect.width, this.ui.position.top + rect.height);
342
+ }
290
343
  }
291
344
  }
292
345
  disconnectedCallback() {
@@ -412,6 +465,44 @@ export class EditorNode extends RapidElement {
412
465
  this.requestUpdate();
413
466
  }
414
467
  }
468
+ handleNodeRemoveClick(event) {
469
+ event.preventDefault();
470
+ event.stopPropagation();
471
+ const nodeId = this.node.uuid;
472
+ // If the node is already in removing state, perform the removal
473
+ if (this.actionRemovingState.has(nodeId)) {
474
+ this.removeNode();
475
+ return;
476
+ }
477
+ // Start removal UI state
478
+ this.actionRemovingState.add(nodeId);
479
+ this.requestUpdate();
480
+ // Clear any existing timeout for this node
481
+ if (this.actionRemovalTimeouts.has(nodeId)) {
482
+ clearTimeout(this.actionRemovalTimeouts.get(nodeId));
483
+ }
484
+ // Set timeout to reset UI if user doesn't click
485
+ const timeoutId = window.setTimeout(() => {
486
+ this.actionRemovingState.delete(nodeId);
487
+ this.actionRemovalTimeouts.delete(nodeId);
488
+ this.requestUpdate();
489
+ }, 1000); // 1 second as per requirements
490
+ this.actionRemovalTimeouts.set(nodeId, timeoutId);
491
+ }
492
+ removeNode() {
493
+ const nodeId = this.node.uuid;
494
+ // Clear the UI state
495
+ this.actionRemovingState.delete(nodeId);
496
+ // Clear any timeout
497
+ if (this.actionRemovalTimeouts.has(nodeId)) {
498
+ clearTimeout(this.actionRemovalTimeouts.get(nodeId));
499
+ this.actionRemovalTimeouts.delete(nodeId);
500
+ }
501
+ // Fire the node deleted event
502
+ this.fireCustomEvent(CustomEventType.NodeDeleted, {
503
+ uuid: this.node.uuid
504
+ });
505
+ }
415
506
  handleActionOrderChanged(event) {
416
507
  var _a;
417
508
  const [fromIdx, toIdx] = event.detail.swap;
@@ -425,6 +516,162 @@ export class EditorNode extends RapidElement {
425
516
  this.node = { ...this.node, actions: newActions };
426
517
  (_a = getStore()) === null || _a === void 0 ? void 0 : _a.getState().updateNode(this.node.uuid, { ...this.node, actions: newActions });
427
518
  }
519
+ handleActionMouseDown(event, action) {
520
+ // Don't handle clicks on the remove button or when action is in removing state
521
+ const target = event.target;
522
+ if (target.closest('.remove-button') ||
523
+ this.actionRemovingState.has(action.uuid)) {
524
+ return;
525
+ }
526
+ // Store the starting position and action for later comparison
527
+ // Don't prevent default - let the Editor's drag system work normally
528
+ this.actionClickStartPos = { x: event.clientX, y: event.clientY };
529
+ this.pendingActionClick = { action, event };
530
+ }
531
+ handleActionMouseUp(event, action) {
532
+ // Don't handle if we don't have a pending click or if it's not the same action
533
+ if (!this.pendingActionClick ||
534
+ this.pendingActionClick.action.uuid !== action.uuid) {
535
+ this.actionClickStartPos = null;
536
+ this.pendingActionClick = null;
537
+ return;
538
+ }
539
+ // Don't handle clicks on the remove button or when action is in removing state
540
+ const target = event.target;
541
+ if (target.closest('.remove-button') ||
542
+ this.actionRemovingState.has(action.uuid)) {
543
+ this.actionClickStartPos = null;
544
+ this.pendingActionClick = null;
545
+ return;
546
+ }
547
+ // Check if the mouse moved beyond the drag threshold
548
+ if (this.actionClickStartPos) {
549
+ const deltaX = event.clientX - this.actionClickStartPos.x;
550
+ const deltaY = event.clientY - this.actionClickStartPos.y;
551
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
552
+ // Check if the Editor is currently in dragging mode
553
+ const editor = this.closest('temba-flow-editor');
554
+ const editorWasDragging = editor === null || editor === void 0 ? void 0 : editor.dragging;
555
+ // Only fire the action edit event if we haven't dragged beyond the threshold
556
+ // AND either there's no Editor parent (test case) or the Editor didn't drag the node
557
+ if (distance <= DRAG_THRESHOLD && (!editor || !editorWasDragging)) {
558
+ // Fire event to request action editing
559
+ this.fireCustomEvent(CustomEventType.ActionEditRequested, {
560
+ action,
561
+ nodeUuid: this.node.uuid
562
+ });
563
+ }
564
+ }
565
+ // Clean up
566
+ this.actionClickStartPos = null;
567
+ this.pendingActionClick = null;
568
+ }
569
+ handleActionClick(event, action) {
570
+ // This method is kept for backward compatibility but should not be used
571
+ // The new mousedown/mouseup approach handles click vs drag properly
572
+ event.preventDefault();
573
+ event.stopPropagation();
574
+ // Don't handle clicks on the remove button or when action is in removing state
575
+ const target = event.target;
576
+ if (target.closest('.remove-button') ||
577
+ this.actionRemovingState.has(action.uuid)) {
578
+ return;
579
+ }
580
+ // Fire event to request action editing
581
+ this.fireCustomEvent(CustomEventType.ActionEditRequested, {
582
+ action,
583
+ nodeUuid: this.node.uuid
584
+ });
585
+ }
586
+ handleNodeEditClick(event) {
587
+ event.preventDefault();
588
+ event.stopPropagation();
589
+ // Don't handle clicks on the remove button or when node is in removing state
590
+ const target = event.target;
591
+ if (target.closest('.remove-button') ||
592
+ this.actionRemovingState.has(this.node.uuid)) {
593
+ return;
594
+ }
595
+ // Fire node edit requested event if the node has a router
596
+ if (this.node.router) {
597
+ // If router node has exactly one action, open the action editor directly
598
+ if (this.node.actions && this.node.actions.length === 1) {
599
+ this.fireCustomEvent(CustomEventType.ActionEditRequested, {
600
+ action: this.node.actions[0],
601
+ nodeUuid: this.node.uuid
602
+ });
603
+ }
604
+ else {
605
+ // Otherwise open the node editor as before
606
+ this.fireCustomEvent(CustomEventType.NodeEditRequested, {
607
+ node: this.node,
608
+ nodeUI: this.ui
609
+ });
610
+ }
611
+ }
612
+ }
613
+ handleNodeMouseDown(event) {
614
+ // Don't handle clicks on the remove button or when node is in removing state
615
+ const target = event.target;
616
+ if (target.closest('.remove-button') ||
617
+ this.actionRemovingState.has(this.node.uuid)) {
618
+ return;
619
+ }
620
+ // Store the starting position for later comparison
621
+ // Don't prevent default - let the Editor's drag system work normally
622
+ this.nodeClickStartPos = { x: event.clientX, y: event.clientY };
623
+ this.pendingNodeClick = { event };
624
+ }
625
+ handleNodeMouseUp(event) {
626
+ // Don't handle if we don't have a pending click
627
+ if (!this.pendingNodeClick) {
628
+ this.nodeClickStartPos = null;
629
+ this.pendingNodeClick = null;
630
+ return;
631
+ }
632
+ // Don't handle clicks on the remove button or when node is in removing state
633
+ const target = event.target;
634
+ if (target.closest('.remove-button') ||
635
+ this.actionRemovingState.has(this.node.uuid)) {
636
+ this.nodeClickStartPos = null;
637
+ this.pendingNodeClick = null;
638
+ return;
639
+ }
640
+ // Check if the mouse moved beyond the drag threshold
641
+ if (this.nodeClickStartPos) {
642
+ const deltaX = event.clientX - this.nodeClickStartPos.x;
643
+ const deltaY = event.clientY - this.nodeClickStartPos.y;
644
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
645
+ // Check if the Editor is currently in dragging mode
646
+ const editor = this.closest('temba-flow-editor');
647
+ const editorWasDragging = editor === null || editor === void 0 ? void 0 : editor.dragging;
648
+ // Only fire the node edit event if we haven't dragged beyond the threshold
649
+ // AND either there's no Editor parent (test case) or the Editor didn't drag the node
650
+ if (distance <= 5 && (!editor || !editorWasDragging)) {
651
+ // Using literal 5 instead of DRAG_THRESHOLD since it's not imported
652
+ // Fire event to request node editing if the node has a router
653
+ if (this.node.router) {
654
+ // If router node has exactly one action, open the action editor directly
655
+ if (this.node.actions && this.node.actions.length === 1) {
656
+ this.fireCustomEvent(CustomEventType.ActionEditRequested, {
657
+ action: this.node.actions[0],
658
+ nodeUuid: this.node.uuid
659
+ });
660
+ }
661
+ else {
662
+ // Otherwise open the node editor as before
663
+ this.fireCustomEvent(CustomEventType.NodeEditRequested, {
664
+ node: this.node,
665
+ nodeUI: this.ui
666
+ });
667
+ }
668
+ }
669
+ }
670
+ }
671
+ // Clean up
672
+ this.nodeClickStartPos = null;
673
+ this.pendingNodeClick = null;
674
+ }
428
675
  renderTitle(config, isRemoving = false) {
429
676
  var _a, _b;
430
677
  return html `<div class="title" style="background:${config.color}">
@@ -435,8 +682,16 @@ export class EditorNode extends RapidElement {
435
682
  <div class="name">${isRemoving ? 'Remove?' : config.name}</div>
436
683
  </div>`;
437
684
  }
685
+ renderNodeTitle(config, isRemoving = false) {
686
+ return html `<div
687
+ class="title ${isRemoving ? 'removing' : ''}"
688
+ style="background:${config.color}"
689
+ >
690
+ <div class="name">${isRemoving ? 'Remove?' : config.name}</div>
691
+ </div>`;
692
+ }
438
693
  renderAction(node, action, index) {
439
- const config = EDITOR_CONFIG[action.type];
694
+ const config = ACTION_CONFIG[action.type];
440
695
  const isRemoving = this.actionRemovingState.has(action.uuid);
441
696
  if (config) {
442
697
  return html `<div
@@ -450,7 +705,12 @@ export class EditorNode extends RapidElement {
450
705
  >
451
706
 
452
707
  </button>
453
- <div class="action-content">
708
+ <div
709
+ class="action-content"
710
+ @mousedown=${(e) => this.handleActionMouseDown(e, action)}
711
+ @mouseup=${(e) => this.handleActionMouseUp(e, action)}
712
+ style="cursor: pointer;"
713
+ >
454
714
  ${this.renderTitle(config, isRemoving)}
455
715
  <div class="body">
456
716
  ${config.render
@@ -475,12 +735,37 @@ export class EditorNode extends RapidElement {
475
735
  </div>`;
476
736
  }
477
737
  renderRouter(router, ui) {
478
- const config = EDITOR_CONFIG[ui.type];
479
- if (config) {
480
- return html `<div class="router">
481
- ${this.renderTitle(config, false)}
738
+ const nodeConfig = NODE_CONFIG[ui.type];
739
+ if (nodeConfig) {
740
+ // For tests that call renderRouter directly without setting this.node
741
+ const hasActions = this.node ? this.node.actions.length > 0 : false;
742
+ const isRemoving = this.node &&
743
+ this.node.actions.length === 0 &&
744
+ this.actionRemovingState.has(this.node.uuid);
745
+ return html `<div class="router" style="position: relative;">
746
+ ${!hasActions
747
+ ? html ` <button
748
+ class="remove-button"
749
+ @click=${(e) => this.handleNodeRemoveClick(e)}
750
+ title="Remove node"
751
+ >
752
+
753
+ </button>
754
+ <div
755
+ @mousedown=${(e) => this.handleNodeMouseDown(e)}
756
+ @mouseup=${(e) => this.handleNodeMouseUp(e)}
757
+ style="cursor: pointer;"
758
+ >
759
+ ${this.renderNodeTitle(nodeConfig, isRemoving)}
760
+ </div>`
761
+ : ''}
482
762
  ${router.result_name
483
- ? html `<div class="body">
763
+ ? html `<div
764
+ class="body"
765
+ @mousedown=${(e) => this.handleNodeMouseDown(e)}
766
+ @mouseup=${(e) => this.handleNodeMouseUp(e)}
767
+ style="cursor: pointer;"
768
+ >
484
769
  Save as
485
770
  <div class="result-name">${router.result_name}</div>
486
771
  </div>`
@@ -492,14 +777,20 @@ export class EditorNode extends RapidElement {
492
777
  if (!node.router || !node.router.categories) {
493
778
  return null;
494
779
  }
495
- const categories = node.router.categories.map((category) => {
780
+ return html `<div class="categories">
781
+ ${repeat(node.router.categories, (category) => category.uuid, (category) => {
496
782
  const exit = node.exits.find((exit) => exit.uuid == category.exit_uuid);
497
- return html `<div class="category">
498
- <div class="title">${category.name}</div>
499
- ${this.renderExit(exit)}
500
- </div>`;
501
- });
502
- return html `<div class="categories">${categories}</div>`;
783
+ return html `<div
784
+ class="category"
785
+ @mousedown=${(e) => this.handleNodeMouseDown(e)}
786
+ @mouseup=${(e) => this.handleNodeMouseUp(e)}
787
+ style="cursor: pointer;"
788
+ >
789
+ <div class="title">${category.name}</div>
790
+ ${this.renderExit(exit)}
791
+ </div>`;
792
+ })}
793
+ </div>`;
503
794
  }
504
795
  renderExit(exit) {
505
796
  return html `<div class="exit-wrapper">
@@ -518,29 +809,41 @@ export class EditorNode extends RapidElement {
518
809
  if (!this.node || !this.ui) {
519
810
  return html `<div class="node">Loading...</div>`;
520
811
  }
812
+ const nodeConfig = NODE_CONFIG[this.ui.type];
521
813
  return html `
522
814
  <div
523
815
  id="${this.node.uuid}"
524
- class="node"
816
+ class="node ${this.ui.type === 'execute_actions'
817
+ ? 'execute-actions'
818
+ : ''}"
525
819
  style="left:${this.ui.position.left}px;top:${this.ui.position.top}px"
526
820
  >
527
821
  ${this.node.actions.length > 0
528
- ? html `<temba-sortable-list
529
- dragHandle="drag-handle"
530
- @temba-order-changed="${this.handleActionOrderChanged}"
531
- >
532
- ${this.node.actions.map((actionSpec, index) => {
533
- return this.renderAction(this.node, actionSpec, index);
534
- })}
535
- </temba-sortable-list>`
536
- : ''}
822
+ ? this.ui.type === 'execute_actions'
823
+ ? html `<temba-sortable-list
824
+ dragHandle="drag-handle"
825
+ @temba-order-changed="${this.handleActionOrderChanged}"
826
+ >
827
+ ${repeat(this.node.actions, (action) => action.uuid, (action, index) => this.renderAction(this.node, action, index))}
828
+ </temba-sortable-list>`
829
+ : html `${repeat(this.node.actions, (action) => action.uuid, (action, index) => this.renderAction(this.node, action, index))}`
830
+ : !this.node.router && nodeConfig && nodeConfig.name
831
+ ? html `<div class="router" style="position: relative;">
832
+ <button
833
+ class="remove-button"
834
+ @click=${(e) => this.handleNodeRemoveClick(e)}
835
+ title="Remove node"
836
+ >
837
+
838
+ </button>
839
+ ${this.renderNodeTitle(nodeConfig, this.actionRemovingState.has(this.node.uuid))}
840
+ </div>`
841
+ : ''}
537
842
  ${this.node.router
538
843
  ? html ` ${this.renderRouter(this.node.router, this.ui)}
539
844
  ${this.renderCategories(this.node)}`
540
845
  : html `<div class="action-exits">
541
- ${this.node.exits.map((exit) => {
542
- return this.renderExit(exit);
543
- })}
846
+ ${repeat(this.node.exits, (exit) => exit.uuid, (exit) => this.renderExit(exit))}
544
847
  </div>`}
545
848
  </div>
546
849
  `;
@@ -548,11 +851,11 @@ export class EditorNode extends RapidElement {
548
851
  }
549
852
  __decorate([
550
853
  property({ type: Object })
551
- ], EditorNode.prototype, "plumber", void 0);
854
+ ], CanvasNode.prototype, "plumber", void 0);
552
855
  __decorate([
553
856
  property({ type: Object })
554
- ], EditorNode.prototype, "node", void 0);
857
+ ], CanvasNode.prototype, "node", void 0);
555
858
  __decorate([
556
859
  property({ type: Object })
557
- ], EditorNode.prototype, "ui", void 0);
558
- //# sourceMappingURL=EditorNode.js.map
860
+ ], CanvasNode.prototype, "ui", void 0);
861
+ //# sourceMappingURL=CanvasNode.js.map