@nyaruka/temba-components 0.131.1 → 0.131.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (427) hide show
  1. package/.github/workflows/publish.yml +4 -1
  2. package/CHANGELOG.md +61 -1
  3. package/demo/data/flows/food-order.json +2 -2
  4. package/demo/data/flows/sample-flow.json +74 -125
  5. package/dist/static/svg/index.svg +1 -1
  6. package/dist/temba-components.js +1155 -618
  7. package/dist/temba-components.js.map +1 -1
  8. package/out-tsc/src/Icons.js +4 -1
  9. package/out-tsc/src/Icons.js.map +1 -1
  10. package/out-tsc/src/events.js.map +1 -1
  11. package/out-tsc/src/flow/CanvasMenu.js +200 -0
  12. package/out-tsc/src/flow/CanvasMenu.js.map +1 -0
  13. package/out-tsc/src/flow/CanvasNode.js +327 -19
  14. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  15. package/out-tsc/src/flow/Editor.js +562 -66
  16. package/out-tsc/src/flow/Editor.js.map +1 -1
  17. package/out-tsc/src/flow/NodeEditor.js +240 -93
  18. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  19. package/out-tsc/src/flow/NodeTypeSelector.js +499 -0
  20. package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -0
  21. package/out-tsc/src/flow/actions/add_contact_groups.js +3 -3
  22. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
  23. package/out-tsc/src/flow/actions/add_contact_urn.js +62 -4
  24. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  25. package/out-tsc/src/flow/actions/add_input_labels.js +3 -3
  26. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  27. package/out-tsc/src/flow/actions/play_audio.js +2 -2
  28. package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
  29. package/out-tsc/src/flow/actions/remove_contact_groups.js +6 -5
  30. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
  31. package/out-tsc/src/flow/actions/request_optin.js +2 -2
  32. package/out-tsc/src/flow/actions/request_optin.js.map +1 -1
  33. package/out-tsc/src/flow/actions/say_msg.js +2 -2
  34. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  35. package/out-tsc/src/flow/actions/send_broadcast.js +76 -23
  36. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  37. package/out-tsc/src/flow/actions/send_email.js +4 -5
  38. package/out-tsc/src/flow/actions/send_email.js.map +1 -1
  39. package/out-tsc/src/flow/actions/send_msg.js +9 -19
  40. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  41. package/out-tsc/src/flow/actions/set_contact_channel.js +5 -9
  42. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  43. package/out-tsc/src/flow/actions/set_contact_field.js +19 -20
  44. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  45. package/out-tsc/src/flow/actions/set_contact_language.js +2 -2
  46. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  47. package/out-tsc/src/flow/actions/set_contact_name.js +2 -12
  48. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  49. package/out-tsc/src/flow/actions/set_contact_status.js +2 -2
  50. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  51. package/out-tsc/src/flow/actions/set_run_result.js +3 -3
  52. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  53. package/out-tsc/src/flow/actions/start_session.js +180 -6
  54. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  55. package/out-tsc/src/flow/config.js +11 -15
  56. package/out-tsc/src/flow/config.js.map +1 -1
  57. package/out-tsc/src/flow/currencies.js +45 -0
  58. package/out-tsc/src/flow/currencies.js.map +1 -0
  59. package/out-tsc/src/flow/nodes/shared-rules.js +257 -0
  60. package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -0
  61. package/out-tsc/src/flow/nodes/shared.js +17 -0
  62. package/out-tsc/src/flow/nodes/shared.js.map +1 -0
  63. package/out-tsc/src/flow/nodes/split_by_airtime.js +205 -5
  64. package/out-tsc/src/flow/nodes/split_by_airtime.js.map +1 -1
  65. package/out-tsc/src/flow/nodes/split_by_contact_field.js +147 -3
  66. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -1
  67. package/out-tsc/src/flow/nodes/split_by_expression.js +68 -2
  68. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -1
  69. package/out-tsc/src/flow/nodes/split_by_groups.js +12 -9
  70. package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -1
  71. package/out-tsc/src/flow/nodes/split_by_intent.js +7 -0
  72. package/out-tsc/src/flow/nodes/split_by_intent.js.map +1 -0
  73. package/out-tsc/src/flow/nodes/split_by_llm.js +3 -2
  74. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
  75. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +2 -2
  76. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  77. package/out-tsc/src/flow/nodes/split_by_random.js +3 -3
  78. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
  79. package/out-tsc/src/flow/nodes/split_by_resthook.js +108 -0
  80. package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -0
  81. package/out-tsc/src/flow/nodes/split_by_run_result.js +206 -3
  82. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
  83. package/out-tsc/src/flow/nodes/split_by_scheme.js +153 -2
  84. package/out-tsc/src/flow/nodes/split_by_scheme.js.map +1 -1
  85. package/out-tsc/src/flow/nodes/split_by_subflow.js +6 -4
  86. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  87. package/out-tsc/src/flow/nodes/split_by_ticket.js +3 -2
  88. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -1
  89. package/out-tsc/src/flow/nodes/split_by_webhook.js +3 -2
  90. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
  91. package/out-tsc/src/flow/nodes/wait_for_audio.js +2 -2
  92. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -1
  93. package/out-tsc/src/flow/nodes/wait_for_digits.js +2 -2
  94. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
  95. package/out-tsc/src/flow/nodes/wait_for_image.js +2 -2
  96. package/out-tsc/src/flow/nodes/wait_for_image.js.map +1 -1
  97. package/out-tsc/src/flow/nodes/wait_for_location.js +2 -2
  98. package/out-tsc/src/flow/nodes/wait_for_location.js.map +1 -1
  99. package/out-tsc/src/flow/nodes/wait_for_menu.js +2 -2
  100. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
  101. package/out-tsc/src/flow/nodes/wait_for_response.js +32 -567
  102. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  103. package/out-tsc/src/flow/nodes/wait_for_video.js +2 -2
  104. package/out-tsc/src/flow/nodes/wait_for_video.js.map +1 -1
  105. package/out-tsc/src/flow/types.js +71 -12
  106. package/out-tsc/src/flow/types.js.map +1 -1
  107. package/out-tsc/src/flow/utils.js +101 -14
  108. package/out-tsc/src/flow/utils.js.map +1 -1
  109. package/out-tsc/src/form/FieldRenderer.js +2 -4
  110. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  111. package/out-tsc/src/interfaces.js +3 -0
  112. package/out-tsc/src/interfaces.js.map +1 -1
  113. package/out-tsc/src/list/SortableList.js +98 -33
  114. package/out-tsc/src/list/SortableList.js.map +1 -1
  115. package/out-tsc/src/live/ContactChat.js +15 -18
  116. package/out-tsc/src/live/ContactChat.js.map +1 -1
  117. package/out-tsc/src/store/AppState.js +53 -0
  118. package/out-tsc/src/store/AppState.js.map +1 -1
  119. package/out-tsc/src/utils.js +254 -13
  120. package/out-tsc/src/utils.js.map +1 -1
  121. package/out-tsc/temba-modules.js +4 -0
  122. package/out-tsc/temba-modules.js.map +1 -1
  123. package/out-tsc/test/ActionHelper.js +3 -3
  124. package/out-tsc/test/ActionHelper.js.map +1 -1
  125. package/out-tsc/test/NodeHelper.js +6 -3
  126. package/out-tsc/test/NodeHelper.js.map +1 -1
  127. package/out-tsc/test/actions/add_contact_urn.test.js +202 -0
  128. package/out-tsc/test/actions/add_contact_urn.test.js.map +1 -0
  129. package/out-tsc/test/actions/send_broadcast.test.js +148 -0
  130. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -0
  131. package/out-tsc/test/actions/send_email.test.js +17 -23
  132. package/out-tsc/test/actions/send_email.test.js.map +1 -1
  133. package/out-tsc/test/actions/send_msg.test.js +33 -15
  134. package/out-tsc/test/actions/send_msg.test.js.map +1 -1
  135. package/out-tsc/test/actions/start_session.test.js +116 -0
  136. package/out-tsc/test/actions/start_session.test.js.map +1 -0
  137. package/out-tsc/test/nodes/split_by_airtime.test.js +604 -0
  138. package/out-tsc/test/nodes/split_by_airtime.test.js.map +1 -0
  139. package/out-tsc/test/nodes/split_by_contact_field.test.js +387 -0
  140. package/out-tsc/test/nodes/split_by_contact_field.test.js.map +1 -0
  141. package/out-tsc/test/nodes/split_by_expression.test.js +614 -0
  142. package/out-tsc/test/nodes/split_by_expression.test.js.map +1 -0
  143. package/out-tsc/test/nodes/split_by_random.test.js +3 -3
  144. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -1
  145. package/out-tsc/test/nodes/split_by_resthook.test.js +337 -0
  146. package/out-tsc/test/nodes/split_by_resthook.test.js.map +1 -0
  147. package/out-tsc/test/nodes/split_by_run_result.test.js +920 -0
  148. package/out-tsc/test/nodes/split_by_run_result.test.js.map +1 -0
  149. package/out-tsc/test/nodes/split_by_scheme.test.js +399 -0
  150. package/out-tsc/test/nodes/split_by_scheme.test.js.map +1 -0
  151. package/out-tsc/test/nodes/split_by_subflow.test.js +333 -0
  152. package/out-tsc/test/nodes/split_by_subflow.test.js.map +1 -0
  153. package/out-tsc/test/nodes/wait_for_digits.test.js +2 -2
  154. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  155. package/out-tsc/test/nodes/wait_for_response.test.js +2 -1
  156. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -1
  157. package/out-tsc/test/temba-action-drag-between-nodes.test.js +252 -0
  158. package/out-tsc/test/temba-action-drag-between-nodes.test.js.map +1 -0
  159. package/out-tsc/test/temba-canvas-menu.test.js +122 -0
  160. package/out-tsc/test/temba-canvas-menu.test.js.map +1 -0
  161. package/out-tsc/test/temba-flow-editor-node.test.js +85 -2
  162. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  163. package/out-tsc/test/temba-flow-editor.test.js +7 -8
  164. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  165. package/out-tsc/test/temba-node-editor.test.js +3 -1
  166. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  167. package/out-tsc/test/temba-node-type-selector.test.js +115 -0
  168. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -0
  169. package/out-tsc/test/temba-omnibox.test.js +2 -1
  170. package/out-tsc/test/temba-omnibox.test.js.map +1 -1
  171. package/out-tsc/test/temba-sortable-list.test.js +51 -0
  172. package/out-tsc/test/temba-sortable-list.test.js.map +1 -1
  173. package/out-tsc/test/temba-utils-index.test.js +1 -27
  174. package/out-tsc/test/temba-utils-index.test.js.map +1 -1
  175. package/out-tsc/test/utils.test.js +2 -0
  176. package/out-tsc/test/utils.test.js.map +1 -1
  177. package/package.json +2 -1
  178. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  179. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  180. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  181. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  182. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  183. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  184. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  185. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  186. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  187. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  188. package/screenshots/truth/actions/add_contact_urn/editor/expression-facebook.png +0 -0
  189. package/screenshots/truth/actions/add_contact_urn/editor/expression-phone.png +0 -0
  190. package/screenshots/truth/actions/add_contact_urn/editor/facebook-id.png +0 -0
  191. package/screenshots/truth/actions/add_contact_urn/editor/instagram-handle.png +0 -0
  192. package/screenshots/truth/actions/add_contact_urn/editor/line-id.png +0 -0
  193. package/screenshots/truth/actions/add_contact_urn/editor/phone-number.png +0 -0
  194. package/screenshots/truth/actions/add_contact_urn/editor/telegram-id.png +0 -0
  195. package/screenshots/truth/actions/add_contact_urn/editor/viber-id.png +0 -0
  196. package/screenshots/truth/actions/add_contact_urn/editor/wechat-id.png +0 -0
  197. package/screenshots/truth/actions/add_contact_urn/editor/whatsapp.png +0 -0
  198. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  199. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  200. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  201. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  202. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  203. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  204. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  205. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  206. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  207. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  208. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  209. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  210. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  211. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  212. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  213. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  214. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  215. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  216. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  217. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  218. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  219. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  220. package/screenshots/truth/actions/send_broadcast/editor/contacts-only.png +0 -0
  221. package/screenshots/truth/actions/send_broadcast/editor/groups-and-contacts.png +0 -0
  222. package/screenshots/truth/actions/send_broadcast/editor/groups-only.png +0 -0
  223. package/screenshots/truth/actions/send_broadcast/editor/many-groups.png +0 -0
  224. package/screenshots/truth/actions/send_broadcast/editor/multiline-text.png +0 -0
  225. package/screenshots/truth/actions/send_broadcast/editor/with-attachments.png +0 -0
  226. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  227. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  228. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  229. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  230. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  231. package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
  232. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  233. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  234. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  235. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  236. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  237. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  238. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  239. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  240. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  241. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  242. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  243. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  244. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  245. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  246. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  247. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  248. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  249. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  250. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  251. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  252. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  253. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  254. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  255. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  256. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  257. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  258. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  259. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  260. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  261. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  262. package/screenshots/truth/actions/start_session/editor/contact-query.png +0 -0
  263. package/screenshots/truth/actions/start_session/editor/contacts-only.png +0 -0
  264. package/screenshots/truth/actions/start_session/editor/create-contact.png +0 -0
  265. package/screenshots/truth/actions/start_session/editor/groups-and-contacts.png +0 -0
  266. package/screenshots/truth/actions/start_session/editor/groups-only.png +0 -0
  267. package/screenshots/truth/actions/start_session/editor/many-recipients.png +0 -0
  268. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  269. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  270. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  271. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  272. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  273. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  274. package/screenshots/truth/canvas-menu/open.png +0 -0
  275. package/screenshots/truth/editor/router.png +0 -0
  276. package/screenshots/truth/editor/wait.png +0 -0
  277. package/screenshots/truth/list/fields-dragging.png +0 -0
  278. package/screenshots/truth/list/sortable-dragging.png +0 -0
  279. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  280. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  281. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  282. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  283. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  284. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  285. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  286. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  287. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  288. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  289. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  290. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  291. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  292. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  293. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  294. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  295. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  296. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  297. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  298. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  299. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  300. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  301. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  302. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  303. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  304. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  305. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  306. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  307. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  308. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  309. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  310. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  311. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  312. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  313. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  314. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  315. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  316. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  317. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  318. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  319. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  320. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  321. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  322. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  323. package/src/Icons.ts +4 -1
  324. package/src/events.ts +2 -6
  325. package/src/flow/CanvasMenu.ts +217 -0
  326. package/src/flow/CanvasNode.ts +408 -10
  327. package/src/flow/Editor.ts +683 -44
  328. package/src/flow/NodeEditor.ts +304 -125
  329. package/src/flow/NodeTypeSelector.ts +592 -0
  330. package/src/flow/actions/add_contact_groups.ts +4 -4
  331. package/src/flow/actions/add_contact_urn.ts +76 -4
  332. package/src/flow/actions/add_input_labels.ts +4 -4
  333. package/src/flow/actions/play_audio.ts +2 -2
  334. package/src/flow/actions/remove_contact_groups.ts +14 -6
  335. package/src/flow/actions/request_optin.ts +2 -2
  336. package/src/flow/actions/say_msg.ts +2 -2
  337. package/src/flow/actions/send_broadcast.ts +85 -23
  338. package/src/flow/actions/send_email.ts +10 -6
  339. package/src/flow/actions/send_msg.ts +22 -32
  340. package/src/flow/actions/set_contact_channel.ts +5 -11
  341. package/src/flow/actions/set_contact_field.ts +20 -25
  342. package/src/flow/actions/set_contact_language.ts +9 -4
  343. package/src/flow/actions/set_contact_name.ts +3 -15
  344. package/src/flow/actions/set_contact_status.ts +3 -3
  345. package/src/flow/actions/set_run_result.ts +4 -4
  346. package/src/flow/actions/start_session.ts +208 -6
  347. package/src/flow/config.ts +13 -15
  348. package/src/flow/currencies.ts +51 -0
  349. package/src/flow/nodes/shared-rules.ts +301 -0
  350. package/src/flow/nodes/shared.ts +18 -0
  351. package/src/flow/nodes/split_by_airtime.ts +238 -5
  352. package/src/flow/nodes/split_by_contact_field.ts +185 -3
  353. package/src/flow/nodes/split_by_expression.ts +94 -2
  354. package/src/flow/nodes/split_by_groups.ts +15 -10
  355. package/src/flow/nodes/split_by_intent.ts +7 -0
  356. package/src/flow/nodes/split_by_llm.ts +4 -3
  357. package/src/flow/nodes/split_by_llm_categorize.ts +4 -4
  358. package/src/flow/nodes/split_by_random.ts +5 -5
  359. package/src/flow/nodes/split_by_resthook.ts +130 -0
  360. package/src/flow/nodes/split_by_run_result.ts +249 -3
  361. package/src/flow/nodes/split_by_scheme.ts +192 -2
  362. package/src/flow/nodes/split_by_subflow.ts +6 -4
  363. package/src/flow/nodes/split_by_ticket.ts +4 -3
  364. package/src/flow/nodes/split_by_webhook.ts +6 -5
  365. package/src/flow/nodes/wait_for_audio.ts +2 -2
  366. package/src/flow/nodes/wait_for_digits.ts +2 -2
  367. package/src/flow/nodes/wait_for_image.ts +2 -2
  368. package/src/flow/nodes/wait_for_location.ts +2 -2
  369. package/src/flow/nodes/wait_for_menu.ts +2 -2
  370. package/src/flow/nodes/wait_for_response.ts +48 -679
  371. package/src/flow/nodes/wait_for_video.ts +2 -2
  372. package/src/flow/types.ts +109 -23
  373. package/src/flow/utils.ts +108 -14
  374. package/src/form/FieldRenderer.ts +2 -4
  375. package/src/interfaces.ts +3 -0
  376. package/src/list/SortableList.ts +109 -34
  377. package/src/live/ContactChat.ts +15 -18
  378. package/src/store/AppState.ts +69 -0
  379. package/src/store/flow-definition.d.ts +2 -5
  380. package/src/utils.ts +332 -12
  381. package/static/api/channels.json +46 -0
  382. package/static/api/resthooks.json +31 -0
  383. package/static/svg/index.svg +1 -1
  384. package/static/svg/work/traced/lightning-02.svg +1 -0
  385. package/static/svg/work/used/lightning-02.svg +3 -0
  386. package/temba-modules.ts +4 -0
  387. package/test/ActionHelper.ts +3 -3
  388. package/test/NodeHelper.ts +6 -3
  389. package/test/actions/add_contact_urn.test.ts +287 -0
  390. package/test/actions/send_broadcast.test.ts +190 -0
  391. package/test/actions/send_email.test.ts +17 -23
  392. package/test/actions/send_msg.test.ts +39 -15
  393. package/test/actions/start_session.test.ts +151 -0
  394. package/test/nodes/split_by_airtime.test.ts +673 -0
  395. package/test/nodes/split_by_contact_field.test.ts +451 -0
  396. package/test/nodes/split_by_expression.test.ts +751 -0
  397. package/test/nodes/split_by_random.test.ts +3 -3
  398. package/test/nodes/split_by_resthook.test.ts +398 -0
  399. package/test/nodes/split_by_run_result.test.ts +1109 -0
  400. package/test/nodes/split_by_scheme.test.ts +486 -0
  401. package/test/nodes/split_by_subflow.test.ts +381 -0
  402. package/test/nodes/wait_for_digits.test.ts +2 -2
  403. package/test/nodes/wait_for_response.test.ts +2 -1
  404. package/test/temba-action-drag-between-nodes.test.ts +301 -0
  405. package/test/temba-canvas-menu.test.ts +156 -0
  406. package/test/temba-flow-editor-node.test.ts +102 -2
  407. package/test/temba-flow-editor.test.ts +7 -8
  408. package/test/temba-node-editor.test.ts +3 -1
  409. package/test/temba-node-type-selector.test.ts +152 -0
  410. package/test/temba-omnibox.test.ts +2 -1
  411. package/test/temba-sortable-list.test.ts +69 -0
  412. package/test/temba-utils-index.test.ts +0 -35
  413. package/test/utils.test.ts +2 -0
  414. package/test-assets/contacts/history.json +14 -20
  415. package/web-dev-server.config.mjs +3 -1
  416. package/out-tsc/src/flow/actions/call_classifier.js +0 -11
  417. package/out-tsc/src/flow/actions/call_classifier.js.map +0 -1
  418. package/out-tsc/src/flow/actions/call_resthook.js +0 -11
  419. package/out-tsc/src/flow/actions/call_resthook.js.map +0 -1
  420. package/out-tsc/src/flow/actions/split_by_expression_example.js +0 -77
  421. package/out-tsc/src/flow/actions/split_by_expression_example.js.map +0 -1
  422. package/out-tsc/src/flow/actions/transfer_airtime.js +0 -11
  423. package/out-tsc/src/flow/actions/transfer_airtime.js.map +0 -1
  424. package/src/flow/actions/call_classifier.ts +0 -12
  425. package/src/flow/actions/call_resthook.ts +0 -12
  426. package/src/flow/actions/split_by_expression_example.ts +0 -88
  427. package/src/flow/actions/transfer_airtime.ts +0 -12
@@ -13,11 +13,16 @@ import { AppState, fromStore, zustand } from '../store/AppState';
13
13
  import { RapidElement } from '../RapidElement';
14
14
  import { repeat } from 'lit-html/directives/repeat.js';
15
15
  import { CustomEventType } from '../interfaces';
16
+ import { generateUUID } from '../utils';
17
+ import { ACTION_CONFIG, NODE_CONFIG } from './config';
18
+ import { ACTION_GROUP_METADATA } from './types';
16
19
 
17
20
  import { Plumber } from './Plumber';
18
21
  import { CanvasNode } from './CanvasNode';
19
22
  import { Dialog } from '../layout/Dialog';
20
23
  import { Connection } from '@jsplumb/browser-ui';
24
+ import { CanvasMenu, CanvasMenuSelection } from './CanvasMenu';
25
+ import { NodeTypeSelector, NodeTypeSelection } from './NodeTypeSelector';
21
26
 
22
27
  export function snapToGrid(value: number): number {
23
28
  const snapped = Math.round(value / 20) * 20;
@@ -55,6 +60,11 @@ export interface SelectionBox {
55
60
 
56
61
  const DRAG_THRESHOLD = 5;
57
62
 
63
+ // Offset for positioning dropped action node relative to mouse cursor
64
+ // Keep small to make drop location close to cursor position
65
+ const DROP_PREVIEW_OFFSET_X = 20;
66
+ const DROP_PREVIEW_OFFSET_Y = 20;
67
+
58
68
  export class Editor extends RapidElement {
59
69
  // unfortunately, jsplumb requires that we be in light DOM
60
70
  createRenderRoot() {
@@ -129,6 +139,31 @@ export class Editor extends RapidElement {
129
139
  @state()
130
140
  private editingAction: Action | null = null;
131
141
 
142
+ @state()
143
+ private isCreatingNewNode = false;
144
+
145
+ @state()
146
+ private pendingNodePosition: FlowPosition | null = null;
147
+
148
+ // Canvas drop state for dragging actions to canvas
149
+ @state()
150
+ private canvasDropPreview: {
151
+ action: Action;
152
+ nodeUuid: string;
153
+ actionIndex: number;
154
+ position: FlowPosition;
155
+ actionHeight: number;
156
+ } | null = null;
157
+ @state()
158
+ private addActionToNodeUuid: string | null = null;
159
+
160
+ // Track target node for action drag
161
+ @state()
162
+ private actionDragTargetNodeUuid: string | null = null;
163
+
164
+ // Track previous target node to clear placeholder when moving between nodes
165
+ private previousActionDragTargetNodeUuid: string | null = null;
166
+
132
167
  private canvasMouseDown = false;
133
168
 
134
169
  // Bound event handlers to maintain proper 'this' context
@@ -136,7 +171,7 @@ export class Editor extends RapidElement {
136
171
  private boundMouseUp = this.handleMouseUp.bind(this);
137
172
  private boundGlobalMouseDown = this.handleGlobalMouseDown.bind(this);
138
173
  private boundKeyDown = this.handleKeyDown.bind(this);
139
- private boundCanvasDoubleClick = this.handleCanvasDoubleClick.bind(this);
174
+ private boundCanvasContextMenu = this.handleCanvasContextMenu.bind(this);
140
175
 
141
176
  static get styles() {
142
177
  return css`
@@ -412,7 +447,7 @@ export class Editor extends RapidElement {
412
447
 
413
448
  const canvas = this.querySelector('#canvas');
414
449
  if (canvas) {
415
- canvas.removeEventListener('dblclick', this.boundCanvasDoubleClick);
450
+ canvas.removeEventListener('contextmenu', this.boundCanvasContextMenu);
416
451
  }
417
452
  }
418
453
 
@@ -424,7 +459,7 @@ export class Editor extends RapidElement {
424
459
 
425
460
  const canvas = this.querySelector('#canvas');
426
461
  if (canvas) {
427
- canvas.addEventListener('dblclick', this.boundCanvasDoubleClick);
462
+ canvas.addEventListener('contextmenu', this.boundCanvasContextMenu);
428
463
  }
429
464
 
430
465
  // Listen for action edit requests from flow nodes
@@ -433,11 +468,44 @@ export class Editor extends RapidElement {
433
468
  this.handleActionEditRequested.bind(this)
434
469
  );
435
470
 
471
+ // Listen for add action requests from flow nodes
472
+ this.addEventListener(
473
+ CustomEventType.AddActionRequested,
474
+ this.handleAddActionRequested.bind(this)
475
+ );
476
+
436
477
  // Listen for node edit requests from flow nodes
437
478
  this.addEventListener(
438
479
  CustomEventType.NodeEditRequested,
439
480
  this.handleNodeEditRequested.bind(this)
440
481
  );
482
+
483
+ // Listen for canvas menu selections
484
+ this.addEventListener(CustomEventType.Selection, (event: CustomEvent) => {
485
+ const target = event.target as HTMLElement;
486
+ if (target.tagName === 'TEMBA-CANVAS-MENU') {
487
+ this.handleCanvasMenuSelection(event);
488
+ } else if (target.tagName === 'TEMBA-NODE-TYPE-SELECTOR') {
489
+ this.handleNodeTypeSelection(event);
490
+ }
491
+ });
492
+
493
+ // Listen for action drag events from nodes
494
+ this.addEventListener(
495
+ CustomEventType.DragExternal,
496
+ this.handleActionDragExternal.bind(this)
497
+ );
498
+
499
+ this.addEventListener(
500
+ CustomEventType.DragInternal,
501
+ this.handleActionDragInternal.bind(this)
502
+ );
503
+
504
+ this.addEventListener(CustomEventType.DragStop, (event: CustomEvent) => {
505
+ if (event.detail.isExternal) {
506
+ this.handleActionDropExternal(event);
507
+ }
508
+ });
441
509
  }
442
510
 
443
511
  private getPosition(uuid: string, type: 'node' | 'sticky'): FlowPosition {
@@ -730,6 +798,50 @@ export class Editor extends RapidElement {
730
798
  ></div>`;
731
799
  }
732
800
 
801
+ private renderCanvasDropPreview(): TemplateResult | string {
802
+ if (!this.canvasDropPreview) return '';
803
+
804
+ const { action, position } = this.canvasDropPreview;
805
+ const actionConfig = ACTION_CONFIG[action.type];
806
+
807
+ if (!actionConfig) return '';
808
+
809
+ return html`<div
810
+ class="canvas-drop-preview"
811
+ style="position: absolute; left: ${position.left}px; top: ${position.top}px; opacity: 0.6; pointer-events: none; z-index: 10000;"
812
+ >
813
+ <div
814
+ class="node execute-actions"
815
+ style="outline: 3px dashed var(--color-primary, #3b82f6); outline-offset: 2px; border-radius: var(--curvature);"
816
+ >
817
+ <div class="action sortable ${action.type}">
818
+ <div class="action-content">
819
+ <div
820
+ class="cn-title"
821
+ style="background: ${actionConfig.group
822
+ ? ACTION_GROUP_METADATA[actionConfig.group]?.color
823
+ : '#aaaaaa'}"
824
+ >
825
+ <div class="title-spacer"></div>
826
+ <div class="name">${actionConfig.name}</div>
827
+ <div class="title-spacer"></div>
828
+ </div>
829
+ <div class="body">
830
+ ${actionConfig.render
831
+ ? actionConfig.render({ actions: [action] } as any, action)
832
+ : html`<pre>${action.type}</pre>`}
833
+ </div>
834
+ </div>
835
+ </div>
836
+ <div class="action-exits">
837
+ <div class="exit-wrapper">
838
+ <div class="exit"></div>
839
+ </div>
840
+ </div>
841
+ </div>
842
+ </div>`;
843
+ }
844
+
733
845
  private handleMouseMove(event: MouseEvent): void {
734
846
  // Handle selection box drawing
735
847
  if (this.canvasMouseDown && !this.isMouseDown) {
@@ -943,13 +1055,17 @@ export class Editor extends RapidElement {
943
1055
  store.getState().expandCanvas(maxWidth, maxHeight);
944
1056
  }
945
1057
 
946
- private handleCanvasDoubleClick(event: MouseEvent): void {
947
- // Check if we double-clicked on empty canvas space
1058
+ private handleCanvasContextMenu(event: MouseEvent): void {
1059
+ // Check if we right-clicked on empty canvas space
948
1060
  const target = event.target as HTMLElement;
949
1061
  if (target.id !== 'canvas') {
950
1062
  return;
951
1063
  }
952
1064
 
1065
+ // Prevent the default browser context menu
1066
+ event.preventDefault();
1067
+ event.stopPropagation();
1068
+
953
1069
  // Get canvas position
954
1070
  const canvas = this.querySelector('#canvas');
955
1071
  if (!canvas) {
@@ -964,15 +1080,141 @@ export class Editor extends RapidElement {
964
1080
  const snappedLeft = snapToGrid(relativeX);
965
1081
  const snappedTop = snapToGrid(relativeY);
966
1082
 
967
- // Create new sticky note
1083
+ // Show the canvas menu at the mouse position (use viewport coordinates)
1084
+ const canvasMenu = this.querySelector('temba-canvas-menu') as CanvasMenu;
1085
+ if (canvasMenu) {
1086
+ canvasMenu.show(event.clientX, event.clientY, {
1087
+ x: snappedLeft,
1088
+ y: snappedTop
1089
+ });
1090
+ }
1091
+ }
1092
+
1093
+ private handleCanvasMenuSelection(event: CustomEvent): void {
1094
+ const selection = event.detail as CanvasMenuSelection;
968
1095
  const store = getStore();
969
- store.getState().createStickyNote({
970
- left: snappedLeft,
971
- top: snappedTop
972
- });
973
1096
 
974
- event.preventDefault();
975
- event.stopPropagation();
1097
+ if (selection.action === 'sticky') {
1098
+ // Create new sticky note
1099
+ store.getState().createStickyNote({
1100
+ left: selection.position.x,
1101
+ top: selection.position.y
1102
+ });
1103
+ } else {
1104
+ // Show node type selector
1105
+ const selector = this.querySelector(
1106
+ 'temba-node-type-selector'
1107
+ ) as NodeTypeSelector;
1108
+ if (selector) {
1109
+ selector.show(selection.action, selection.position);
1110
+ }
1111
+ }
1112
+ }
1113
+
1114
+ private handleNodeTypeSelection(event: CustomEvent): void {
1115
+ const selection = event.detail as NodeTypeSelection;
1116
+
1117
+ // Check if we're adding an action to an existing node
1118
+ if (this.addActionToNodeUuid) {
1119
+ // Find the existing node
1120
+ const node = this.definition.nodes.find(
1121
+ (n) => n.uuid === this.addActionToNodeUuid
1122
+ );
1123
+ const nodeUI = this.definition._ui.nodes[this.addActionToNodeUuid];
1124
+
1125
+ if (node && nodeUI) {
1126
+ // Create a new action to add to the existing node
1127
+ const actionUuid = generateUUID();
1128
+ this.editingAction = {
1129
+ uuid: actionUuid,
1130
+ type: selection.nodeType as any
1131
+ } as Action;
1132
+
1133
+ // Set the editing node to the existing node (not creating new)
1134
+ this.editingNode = node;
1135
+ this.editingNodeUI = nodeUI;
1136
+ this.isCreatingNewNode = false;
1137
+
1138
+ // Clear the addActionToNodeUuid flag
1139
+ this.addActionToNodeUuid = null;
1140
+
1141
+ return;
1142
+ }
1143
+
1144
+ // If we couldn't find the node, clear the flag and continue with normal flow
1145
+ this.addActionToNodeUuid = null;
1146
+ }
1147
+
1148
+ // Create a temporary node structure for editing (not added to store yet)
1149
+ const nodeUuid = generateUUID();
1150
+
1151
+ // Determine if this is an action type or a node type
1152
+ // Actions need to be wrapped in an execute_actions node
1153
+ const isActionType = selection.nodeType in ACTION_CONFIG;
1154
+ const nodeType = isActionType ? 'execute_actions' : selection.nodeType;
1155
+
1156
+ // For nodes with routers, initialize an empty router to ensure fromFormData works correctly
1157
+ const nodeConfig = NODE_CONFIG[nodeType];
1158
+ const hasRouter =
1159
+ nodeConfig?.form &&
1160
+ Object.keys(nodeConfig.form).some(
1161
+ (key) =>
1162
+ ['rules', 'categories', 'cases'].includes(key) ||
1163
+ nodeConfig.form[key]?.type === 'array'
1164
+ );
1165
+
1166
+ const tempNode: Node = {
1167
+ uuid: nodeUuid,
1168
+ actions: [],
1169
+ exits: hasRouter
1170
+ ? [] // Router-based nodes will generate their own exits
1171
+ : [
1172
+ {
1173
+ uuid: generateUUID(),
1174
+ destination_uuid: null
1175
+ }
1176
+ ]
1177
+ };
1178
+
1179
+ if (hasRouter) {
1180
+ // This node uses a router - initialize it with empty structure
1181
+ tempNode.router = {
1182
+ type: 'switch',
1183
+ categories: [],
1184
+ cases: [],
1185
+ operand: '@input.text',
1186
+ default_category_uuid: undefined
1187
+ };
1188
+ }
1189
+
1190
+ const tempNodeUI: NodeUI = {
1191
+ position: {
1192
+ left: selection.position.x,
1193
+ top: selection.position.y
1194
+ },
1195
+ type: nodeType as any,
1196
+ config: {}
1197
+ };
1198
+
1199
+ // Mark that we're creating a new node and store the position
1200
+ this.isCreatingNewNode = true;
1201
+ this.pendingNodePosition = {
1202
+ left: selection.position.x,
1203
+ top: selection.position.y
1204
+ };
1205
+
1206
+ // Open the node editor with the temporary node
1207
+ this.editingNode = tempNode;
1208
+ this.editingNodeUI = tempNodeUI;
1209
+
1210
+ // If this is an action type, we also need to set up an editing action
1211
+ if (isActionType) {
1212
+ const actionUuid = generateUUID();
1213
+ this.editingAction = {
1214
+ uuid: actionUuid,
1215
+ type: selection.nodeType as any
1216
+ } as Action;
1217
+ }
976
1218
  }
977
1219
 
978
1220
  private handleActionEditRequested(event: CustomEvent): void {
@@ -988,6 +1230,38 @@ export class Editor extends RapidElement {
988
1230
  this.editingNodeUI = this.definition._ui.nodes[nodeUuid];
989
1231
  }
990
1232
  }
1233
+
1234
+ private handleAddActionRequested(event: CustomEvent): void {
1235
+ // Get the node where we want to add the action
1236
+ const nodeUuid = event.detail.nodeUuid;
1237
+ const node = this.definition.nodes.find((n) => n.uuid === nodeUuid);
1238
+
1239
+ if (!node) {
1240
+ return;
1241
+ }
1242
+
1243
+ // Get the node's position to place the selector near it
1244
+ const nodeUI = this.definition._ui.nodes[nodeUuid];
1245
+ if (!nodeUI) {
1246
+ return;
1247
+ }
1248
+
1249
+ // Show the node type selector in action mode, excluding branching actions
1250
+ const selector = this.querySelector(
1251
+ 'temba-node-type-selector'
1252
+ ) as NodeTypeSelector;
1253
+ if (selector) {
1254
+ // Show the selector near the node, using a mode that excludes branching actions
1255
+ selector.show('action-no-branching', {
1256
+ x: nodeUI.position.left,
1257
+ y: nodeUI.position.top
1258
+ });
1259
+
1260
+ // Store the node UUID so we know which node to add the action to
1261
+ this.addActionToNodeUuid = nodeUuid;
1262
+ }
1263
+ }
1264
+
991
1265
  private handleNodeEditRequested(event: CustomEvent): void {
992
1266
  this.editingNode = event.detail.node;
993
1267
  this.editingNodeUI = event.detail.nodeUI;
@@ -995,21 +1269,60 @@ export class Editor extends RapidElement {
995
1269
 
996
1270
  private handleActionSaved(updatedAction: Action): void {
997
1271
  if (this.editingNode && this.editingAction) {
998
- // Update the specific action in the node
999
- const updatedActions = this.editingNode.actions.map((action) =>
1000
- action.uuid === this.editingAction.uuid ? updatedAction : action
1272
+ let updatedActions: Action[];
1273
+
1274
+ // Check if this action already exists in the node
1275
+ const existingActionIndex = this.editingNode.actions.findIndex(
1276
+ (action) => action.uuid === this.editingAction.uuid
1001
1277
  );
1278
+
1279
+ if (existingActionIndex >= 0) {
1280
+ // Update existing action
1281
+ updatedActions = this.editingNode.actions.map((action) =>
1282
+ action.uuid === this.editingAction.uuid ? updatedAction : action
1283
+ );
1284
+ } else {
1285
+ // Add new action
1286
+ updatedActions = [...this.editingNode.actions, updatedAction];
1287
+ }
1288
+
1002
1289
  const updatedNode = { ...this.editingNode, actions: updatedActions };
1003
1290
 
1004
- // Update the node in the store
1005
- getStore()?.getState().updateNode(this.editingNode.uuid, updatedNode);
1291
+ // Check if we're creating a new node or updating an existing one
1292
+ if (this.isCreatingNewNode) {
1293
+ // This is a new node with a new action - add it to the store
1294
+ const store = getStore();
1006
1295
 
1007
- // Repaint jsplumb connections in case node size changed
1008
- if (this.plumber) {
1009
- // Use requestAnimationFrame to ensure DOM has been updated first
1010
- requestAnimationFrame(() => {
1011
- this.plumber.repaintEverything();
1012
- });
1296
+ const nodeUI: NodeUI = {
1297
+ position: this.pendingNodePosition || { left: 0, top: 0 },
1298
+ type: this.editingNodeUI?.type,
1299
+ config: {}
1300
+ };
1301
+
1302
+ // Add the node to the store
1303
+ store.getState().addNode(updatedNode, nodeUI);
1304
+
1305
+ // Reset the creation flags
1306
+ this.isCreatingNewNode = false;
1307
+ this.pendingNodePosition = null;
1308
+
1309
+ // Repaint jsplumb connections
1310
+ if (this.plumber) {
1311
+ requestAnimationFrame(() => {
1312
+ this.plumber.repaintEverything();
1313
+ });
1314
+ }
1315
+ } else {
1316
+ // Update existing node in the store
1317
+ getStore()?.getState().updateNode(this.editingNode.uuid, updatedNode);
1318
+
1319
+ // Repaint jsplumb connections in case node size changed
1320
+ if (this.plumber) {
1321
+ // Use requestAnimationFrame to ensure DOM has been updated first
1322
+ requestAnimationFrame(() => {
1323
+ this.plumber.repaintEverything();
1324
+ });
1325
+ }
1013
1326
  }
1014
1327
  }
1015
1328
  this.closeNodeEditor();
@@ -1022,32 +1335,64 @@ export class Editor extends RapidElement {
1022
1335
  }
1023
1336
 
1024
1337
  private handleActionEditCanceled(): void {
1338
+ // If we were creating a new node, just discard it
1339
+ if (this.isCreatingNewNode) {
1340
+ this.isCreatingNewNode = false;
1341
+ this.pendingNodePosition = null;
1342
+ }
1025
1343
  this.closeNodeEditor();
1026
1344
  }
1027
1345
 
1028
- private handleNodeSaved(updatedNode: Node): void {
1346
+ private handleNodeSaved(
1347
+ updatedNode: Node,
1348
+ uiConfig?: Record<string, any>
1349
+ ): void {
1029
1350
  if (this.editingNode) {
1030
- // Clean up jsPlumb connections for removed exits before updating the node
1031
- if (this.plumber) {
1032
- const oldExits = this.editingNode.exits || [];
1033
- const newExits = updatedNode.exits || [];
1351
+ if (this.isCreatingNewNode) {
1352
+ // This is a new node - add it to the store for the first time
1353
+ const store = getStore();
1354
+
1355
+ const nodeUI: NodeUI = {
1356
+ position: this.pendingNodePosition || { left: 0, top: 0 },
1357
+ type: this.editingNodeUI?.type,
1358
+ config: uiConfig || {}
1359
+ };
1034
1360
 
1035
- // Find exits that were removed
1036
- const removedExits = oldExits.filter(
1037
- (oldExit) =>
1038
- !newExits.find((newExit) => newExit.uuid === oldExit.uuid)
1039
- );
1361
+ // Add the node to the store
1362
+ store.getState().addNode(updatedNode, nodeUI);
1040
1363
 
1041
- // Remove jsPlumb connections for removed exits
1042
- removedExits.forEach((exit) => {
1043
- this.plumber.removeExitConnection(exit.uuid);
1044
- });
1045
- }
1364
+ // Reset the creation flags
1365
+ this.isCreatingNewNode = false;
1366
+ this.pendingNodePosition = null;
1367
+ } else {
1368
+ // This is an existing node - update it
1369
+ // Clean up jsPlumb connections for removed exits before updating the node
1370
+ if (this.plumber) {
1371
+ const oldExits = this.editingNode.exits || [];
1372
+ const newExits = updatedNode.exits || [];
1373
+
1374
+ // Find exits that were removed
1375
+ const removedExits = oldExits.filter(
1376
+ (oldExit) =>
1377
+ !newExits.find((newExit) => newExit.uuid === oldExit.uuid)
1378
+ );
1379
+
1380
+ // Remove jsPlumb connections for removed exits
1381
+ removedExits.forEach((exit) => {
1382
+ this.plumber.removeExitConnection(exit.uuid);
1383
+ });
1384
+ }
1046
1385
 
1047
- this.plumber.revalidate([updatedNode.uuid]);
1386
+ this.plumber.revalidate([updatedNode.uuid]);
1048
1387
 
1049
- // Update the node in the store
1050
- getStore()?.getState().updateNode(this.editingNode.uuid, updatedNode);
1388
+ // Update the node in the store
1389
+ getStore()?.getState().updateNode(this.editingNode.uuid, updatedNode);
1390
+
1391
+ // Update the UI config if provided
1392
+ if (uiConfig) {
1393
+ getStore()?.getState().updateNodeUIConfig(updatedNode.uuid, uiConfig);
1394
+ }
1395
+ }
1051
1396
 
1052
1397
  // Repaint jsplumb connections in case node size changed
1053
1398
  if (this.plumber) {
@@ -1061,9 +1406,299 @@ export class Editor extends RapidElement {
1061
1406
  }
1062
1407
 
1063
1408
  private handleNodeEditCanceled(): void {
1409
+ // If we were creating a new node, just discard it
1410
+ if (this.isCreatingNewNode) {
1411
+ this.isCreatingNewNode = false;
1412
+ this.pendingNodePosition = null;
1413
+ }
1064
1414
  this.closeNodeEditor();
1065
1415
  }
1066
1416
 
1417
+ private getNodeAtPosition(mouseX: number, mouseY: number): string | null {
1418
+ // Get all node elements
1419
+ const nodeElements = this.querySelectorAll('temba-flow-node');
1420
+
1421
+ for (const nodeElement of Array.from(nodeElements)) {
1422
+ const rect = nodeElement.getBoundingClientRect();
1423
+
1424
+ if (
1425
+ mouseX >= rect.left &&
1426
+ mouseX <= rect.right &&
1427
+ mouseY >= rect.top &&
1428
+ mouseY <= rect.bottom
1429
+ ) {
1430
+ return nodeElement.getAttribute('data-node-uuid');
1431
+ }
1432
+ }
1433
+
1434
+ return null;
1435
+ }
1436
+
1437
+ private calculateCanvasDropPosition(
1438
+ mouseX: number,
1439
+ mouseY: number,
1440
+ applyGridSnapping: boolean = true
1441
+ ): FlowPosition {
1442
+ // calculate the position on the canvas
1443
+ const canvas = this.querySelector('#canvas');
1444
+ if (!canvas) return { left: 0, top: 0 };
1445
+
1446
+ const canvasRect = canvas.getBoundingClientRect();
1447
+
1448
+ // calculate position relative to canvas
1449
+ // canvasRect gives us the canvas position in the viewport, which already accounts for scroll
1450
+ // so we just need mouseX/Y - canvasRect.left/top to get position within canvas
1451
+ const left = mouseX - canvasRect.left - DROP_PREVIEW_OFFSET_X;
1452
+ const top = mouseY - canvasRect.top - DROP_PREVIEW_OFFSET_Y;
1453
+
1454
+ // Apply grid snapping only if requested (for final drop position)
1455
+ if (applyGridSnapping) {
1456
+ return {
1457
+ left: snapToGrid(left),
1458
+ top: snapToGrid(top)
1459
+ };
1460
+ }
1461
+
1462
+ return { left, top };
1463
+ }
1464
+
1465
+ private handleActionDragExternal(event: CustomEvent): void {
1466
+ const {
1467
+ action,
1468
+ nodeUuid,
1469
+ actionIndex,
1470
+ mouseX,
1471
+ mouseY,
1472
+ actionHeight = 60
1473
+ } = event.detail;
1474
+
1475
+ // Check if mouse is over another execute_actions node
1476
+ const targetNode = this.getNodeAtPosition(mouseX, mouseY);
1477
+
1478
+ if (targetNode && targetNode !== nodeUuid) {
1479
+ const targetNodeUI = this.definition._ui.nodes[targetNode];
1480
+ const targetNodeDef = this.definition.nodes.find(
1481
+ (n) => n.uuid === targetNode
1482
+ );
1483
+
1484
+ // Only allow dropping on execute_actions nodes, and not the source node
1485
+ if (targetNodeUI?.type === 'execute_actions' && targetNodeDef) {
1486
+ // If we moved to a different target node, clear the previous one's placeholder
1487
+ if (
1488
+ this.previousActionDragTargetNodeUuid &&
1489
+ this.previousActionDragTargetNodeUuid !== targetNode
1490
+ ) {
1491
+ const previousElement = this.querySelector(
1492
+ `temba-flow-node[data-node-uuid="${this.previousActionDragTargetNodeUuid}"]`
1493
+ );
1494
+ if (previousElement) {
1495
+ previousElement.dispatchEvent(
1496
+ new CustomEvent('action-drag-leave', {
1497
+ detail: {},
1498
+ bubbles: false
1499
+ })
1500
+ );
1501
+ }
1502
+ }
1503
+
1504
+ // Update target node for drop handling
1505
+ this.actionDragTargetNodeUuid = targetNode;
1506
+ this.previousActionDragTargetNodeUuid = targetNode;
1507
+
1508
+ // Hide canvas preview when over a valid target
1509
+ this.canvasDropPreview = null;
1510
+
1511
+ // Tell source node to show ghost (we're over a valid target)
1512
+ const sourceElement = this.querySelector(
1513
+ `temba-flow-node[data-node-uuid="${nodeUuid}"]`
1514
+ );
1515
+ if (sourceElement) {
1516
+ sourceElement.dispatchEvent(
1517
+ new CustomEvent('action-show-ghost', {
1518
+ detail: {},
1519
+ bubbles: false
1520
+ })
1521
+ );
1522
+ }
1523
+
1524
+ // Notify the target node about the drag
1525
+ const targetElement = this.querySelector(
1526
+ `temba-flow-node[data-node-uuid="${targetNode}"]`
1527
+ );
1528
+ if (targetElement) {
1529
+ targetElement.dispatchEvent(
1530
+ new CustomEvent('action-drag-over', {
1531
+ detail: {
1532
+ action,
1533
+ sourceNodeUuid: nodeUuid,
1534
+ actionIndex,
1535
+ mouseX,
1536
+ mouseY,
1537
+ actionHeight
1538
+ },
1539
+ bubbles: false
1540
+ })
1541
+ );
1542
+ }
1543
+
1544
+ this.requestUpdate();
1545
+ return;
1546
+ }
1547
+ }
1548
+
1549
+ // Not over a valid target node, clear any previous target's placeholder
1550
+ if (this.previousActionDragTargetNodeUuid) {
1551
+ const previousElement = this.querySelector(
1552
+ `temba-flow-node[data-node-uuid="${this.previousActionDragTargetNodeUuid}"]`
1553
+ );
1554
+ if (previousElement) {
1555
+ previousElement.dispatchEvent(
1556
+ new CustomEvent('action-drag-leave', {
1557
+ detail: {},
1558
+ bubbles: false
1559
+ })
1560
+ );
1561
+ }
1562
+ this.previousActionDragTargetNodeUuid = null;
1563
+ }
1564
+
1565
+ this.actionDragTargetNodeUuid = null;
1566
+
1567
+ // Tell source node to hide ghost (we're not over a valid target)
1568
+ const sourceElement = this.querySelector(
1569
+ `temba-flow-node[data-node-uuid="${nodeUuid}"]`
1570
+ );
1571
+ if (sourceElement) {
1572
+ sourceElement.dispatchEvent(
1573
+ new CustomEvent('action-hide-ghost', {
1574
+ detail: {},
1575
+ bubbles: false
1576
+ })
1577
+ );
1578
+ }
1579
+
1580
+ // Don't snap to grid for preview - let it follow cursor smoothly
1581
+ const position = this.calculateCanvasDropPosition(mouseX, mouseY, false);
1582
+
1583
+ this.canvasDropPreview = {
1584
+ action,
1585
+ nodeUuid,
1586
+ actionIndex,
1587
+ position,
1588
+ actionHeight
1589
+ };
1590
+
1591
+ // Force re-render to update preview position
1592
+ this.requestUpdate();
1593
+ }
1594
+
1595
+ private handleActionDragInternal(_event: CustomEvent): void {
1596
+ // Clear any previous target's placeholder when returning to internal drag
1597
+ if (this.previousActionDragTargetNodeUuid) {
1598
+ const previousElement = this.querySelector(
1599
+ `temba-flow-node[data-node-uuid="${this.previousActionDragTargetNodeUuid}"]`
1600
+ );
1601
+ if (previousElement) {
1602
+ previousElement.dispatchEvent(
1603
+ new CustomEvent('action-drag-leave', {
1604
+ detail: {},
1605
+ bubbles: false
1606
+ })
1607
+ );
1608
+ }
1609
+ this.previousActionDragTargetNodeUuid = null;
1610
+ }
1611
+
1612
+ this.canvasDropPreview = null;
1613
+ this.actionDragTargetNodeUuid = null;
1614
+ }
1615
+
1616
+ private handleActionDropExternal(event: CustomEvent): void {
1617
+ const { action, nodeUuid, actionIndex, mouseX, mouseY } = event.detail;
1618
+
1619
+ // Check if we're dropping on an existing execute_actions node
1620
+ const targetNodeUuid = this.actionDragTargetNodeUuid;
1621
+
1622
+ if (targetNodeUuid && targetNodeUuid !== nodeUuid) {
1623
+ // Dropping on another node - notify the target node to handle the drop
1624
+ const targetElement = this.querySelector(
1625
+ `temba-flow-node[data-node-uuid="${targetNodeUuid}"]`
1626
+ );
1627
+ if (targetElement) {
1628
+ targetElement.dispatchEvent(
1629
+ new CustomEvent('action-drop', {
1630
+ detail: {
1631
+ action,
1632
+ sourceNodeUuid: nodeUuid,
1633
+ actionIndex,
1634
+ mouseX,
1635
+ mouseY
1636
+ },
1637
+ bubbles: false
1638
+ })
1639
+ );
1640
+ }
1641
+
1642
+ // Clear state
1643
+ this.canvasDropPreview = null;
1644
+ this.actionDragTargetNodeUuid = null;
1645
+ return;
1646
+ }
1647
+
1648
+ // Not dropping on another node, create a new one on canvas
1649
+ // Snap to grid for the final drop position
1650
+ const position = this.calculateCanvasDropPosition(mouseX, mouseY, true);
1651
+
1652
+ // remove the action from the original node
1653
+ const originalNode = this.definition.nodes.find((n) => n.uuid === nodeUuid);
1654
+ if (!originalNode) return;
1655
+
1656
+ const updatedActions = originalNode.actions.filter(
1657
+ (_a, idx) => idx !== actionIndex
1658
+ );
1659
+
1660
+ // if no actions remain, delete the node
1661
+ if (updatedActions.length === 0) {
1662
+ getStore()?.getState().removeNodes([nodeUuid]);
1663
+ } else {
1664
+ // update the node
1665
+ const updatedNode = { ...originalNode, actions: updatedActions };
1666
+ getStore()?.getState().updateNode(nodeUuid, updatedNode);
1667
+ }
1668
+
1669
+ // create a new execute_actions node with the dropped action
1670
+ const newNode: Node = {
1671
+ uuid: generateUUID(),
1672
+ actions: [action],
1673
+ exits: [
1674
+ {
1675
+ uuid: generateUUID(),
1676
+ destination_uuid: null
1677
+ }
1678
+ ]
1679
+ };
1680
+
1681
+ const newNodeUI: NodeUI = {
1682
+ position,
1683
+ type: 'execute_actions',
1684
+ config: {}
1685
+ };
1686
+
1687
+ // add the new node
1688
+ getStore()?.getState().addNode(newNode, newNodeUI);
1689
+
1690
+ // clear the preview
1691
+ this.canvasDropPreview = null;
1692
+ this.actionDragTargetNodeUuid = null;
1693
+
1694
+ // repaint connections
1695
+ if (this.plumber) {
1696
+ requestAnimationFrame(() => {
1697
+ this.plumber.repaintEverything();
1698
+ });
1699
+ }
1700
+ }
1701
+
1067
1702
  public render(): TemplateResult {
1068
1703
  // we have to embed our own style since we are in light DOM
1069
1704
  const style = html`<style>
@@ -1104,6 +1739,7 @@ export class Editor extends RapidElement {
1104
1739
  : ''}"
1105
1740
  @mousedown=${this.handleMouseDown.bind(this)}
1106
1741
  uuid=${node.uuid}
1742
+ data-node-uuid=${node.uuid}
1107
1743
  style="left:${position.left}px; top:${position.top}px"
1108
1744
  .plumber=${this.plumber}
1109
1745
  .node=${node}
@@ -1137,7 +1773,7 @@ export class Editor extends RapidElement {
1137
1773
  ></temba-sticky-note>`;
1138
1774
  }
1139
1775
  )}
1140
- ${this.renderSelectionBox()}
1776
+ ${this.renderSelectionBox()} ${this.renderCanvasDropPreview()}
1141
1777
  </div>
1142
1778
  </div>
1143
1779
  </div>
@@ -1148,11 +1784,14 @@ export class Editor extends RapidElement {
1148
1784
  .nodeUI=${this.editingNodeUI}
1149
1785
  .action=${this.editingAction}
1150
1786
  @temba-node-saved=${(e: CustomEvent) =>
1151
- this.handleNodeSaved(e.detail.node)}
1787
+ this.handleNodeSaved(e.detail.node, e.detail.uiConfig)}
1152
1788
  @temba-action-saved=${(e: CustomEvent) =>
1153
1789
  this.handleActionSaved(e.detail.action)}
1154
1790
  @temba-node-edit-cancelled=${this.handleNodeEditCanceled}
1155
1791
  ></temba-node-editor>`
1156
- : ''} `;
1792
+ : ''}
1793
+
1794
+ <temba-canvas-menu></temba-canvas-menu>
1795
+ <temba-node-type-selector></temba-node-type-selector> `;
1157
1796
  }
1158
1797
  }