@nyaruka/temba-components 0.131.0 → 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 (430) hide show
  1. package/.github/workflows/publish.yml +4 -1
  2. package/CHANGELOG.md +67 -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 +1156 -619
  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/ContactSearch.js +1 -1
  110. package/out-tsc/src/form/ContactSearch.js.map +1 -1
  111. package/out-tsc/src/form/FieldRenderer.js +2 -4
  112. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  113. package/out-tsc/src/interfaces.js +3 -0
  114. package/out-tsc/src/interfaces.js.map +1 -1
  115. package/out-tsc/src/list/SortableList.js +98 -33
  116. package/out-tsc/src/list/SortableList.js.map +1 -1
  117. package/out-tsc/src/live/ContactChat.js +15 -18
  118. package/out-tsc/src/live/ContactChat.js.map +1 -1
  119. package/out-tsc/src/store/AppState.js +53 -0
  120. package/out-tsc/src/store/AppState.js.map +1 -1
  121. package/out-tsc/src/utils.js +254 -13
  122. package/out-tsc/src/utils.js.map +1 -1
  123. package/out-tsc/temba-modules.js +4 -0
  124. package/out-tsc/temba-modules.js.map +1 -1
  125. package/out-tsc/test/ActionHelper.js +3 -3
  126. package/out-tsc/test/ActionHelper.js.map +1 -1
  127. package/out-tsc/test/NodeHelper.js +6 -3
  128. package/out-tsc/test/NodeHelper.js.map +1 -1
  129. package/out-tsc/test/actions/add_contact_urn.test.js +202 -0
  130. package/out-tsc/test/actions/add_contact_urn.test.js.map +1 -0
  131. package/out-tsc/test/actions/send_broadcast.test.js +148 -0
  132. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -0
  133. package/out-tsc/test/actions/send_email.test.js +17 -23
  134. package/out-tsc/test/actions/send_email.test.js.map +1 -1
  135. package/out-tsc/test/actions/send_msg.test.js +33 -15
  136. package/out-tsc/test/actions/send_msg.test.js.map +1 -1
  137. package/out-tsc/test/actions/start_session.test.js +116 -0
  138. package/out-tsc/test/actions/start_session.test.js.map +1 -0
  139. package/out-tsc/test/nodes/split_by_airtime.test.js +604 -0
  140. package/out-tsc/test/nodes/split_by_airtime.test.js.map +1 -0
  141. package/out-tsc/test/nodes/split_by_contact_field.test.js +387 -0
  142. package/out-tsc/test/nodes/split_by_contact_field.test.js.map +1 -0
  143. package/out-tsc/test/nodes/split_by_expression.test.js +614 -0
  144. package/out-tsc/test/nodes/split_by_expression.test.js.map +1 -0
  145. package/out-tsc/test/nodes/split_by_random.test.js +3 -3
  146. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -1
  147. package/out-tsc/test/nodes/split_by_resthook.test.js +337 -0
  148. package/out-tsc/test/nodes/split_by_resthook.test.js.map +1 -0
  149. package/out-tsc/test/nodes/split_by_run_result.test.js +920 -0
  150. package/out-tsc/test/nodes/split_by_run_result.test.js.map +1 -0
  151. package/out-tsc/test/nodes/split_by_scheme.test.js +399 -0
  152. package/out-tsc/test/nodes/split_by_scheme.test.js.map +1 -0
  153. package/out-tsc/test/nodes/split_by_subflow.test.js +333 -0
  154. package/out-tsc/test/nodes/split_by_subflow.test.js.map +1 -0
  155. package/out-tsc/test/nodes/wait_for_digits.test.js +2 -2
  156. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  157. package/out-tsc/test/nodes/wait_for_response.test.js +2 -1
  158. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -1
  159. package/out-tsc/test/temba-action-drag-between-nodes.test.js +252 -0
  160. package/out-tsc/test/temba-action-drag-between-nodes.test.js.map +1 -0
  161. package/out-tsc/test/temba-canvas-menu.test.js +122 -0
  162. package/out-tsc/test/temba-canvas-menu.test.js.map +1 -0
  163. package/out-tsc/test/temba-flow-editor-node.test.js +85 -2
  164. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  165. package/out-tsc/test/temba-flow-editor.test.js +7 -8
  166. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  167. package/out-tsc/test/temba-node-editor.test.js +3 -1
  168. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  169. package/out-tsc/test/temba-node-type-selector.test.js +115 -0
  170. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -0
  171. package/out-tsc/test/temba-omnibox.test.js +2 -1
  172. package/out-tsc/test/temba-omnibox.test.js.map +1 -1
  173. package/out-tsc/test/temba-sortable-list.test.js +51 -0
  174. package/out-tsc/test/temba-sortable-list.test.js.map +1 -1
  175. package/out-tsc/test/temba-utils-index.test.js +1 -27
  176. package/out-tsc/test/temba-utils-index.test.js.map +1 -1
  177. package/out-tsc/test/utils.test.js +2 -0
  178. package/out-tsc/test/utils.test.js.map +1 -1
  179. package/package.json +2 -1
  180. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  181. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  182. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  183. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  184. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  185. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  186. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  187. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  188. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  189. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  190. package/screenshots/truth/actions/add_contact_urn/editor/expression-facebook.png +0 -0
  191. package/screenshots/truth/actions/add_contact_urn/editor/expression-phone.png +0 -0
  192. package/screenshots/truth/actions/add_contact_urn/editor/facebook-id.png +0 -0
  193. package/screenshots/truth/actions/add_contact_urn/editor/instagram-handle.png +0 -0
  194. package/screenshots/truth/actions/add_contact_urn/editor/line-id.png +0 -0
  195. package/screenshots/truth/actions/add_contact_urn/editor/phone-number.png +0 -0
  196. package/screenshots/truth/actions/add_contact_urn/editor/telegram-id.png +0 -0
  197. package/screenshots/truth/actions/add_contact_urn/editor/viber-id.png +0 -0
  198. package/screenshots/truth/actions/add_contact_urn/editor/wechat-id.png +0 -0
  199. package/screenshots/truth/actions/add_contact_urn/editor/whatsapp.png +0 -0
  200. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  201. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  202. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  203. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  204. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  205. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  206. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  207. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  208. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  209. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  210. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  211. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  212. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  213. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  214. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  215. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  216. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  217. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  218. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  219. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  220. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  221. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  222. package/screenshots/truth/actions/send_broadcast/editor/contacts-only.png +0 -0
  223. package/screenshots/truth/actions/send_broadcast/editor/groups-and-contacts.png +0 -0
  224. package/screenshots/truth/actions/send_broadcast/editor/groups-only.png +0 -0
  225. package/screenshots/truth/actions/send_broadcast/editor/many-groups.png +0 -0
  226. package/screenshots/truth/actions/send_broadcast/editor/multiline-text.png +0 -0
  227. package/screenshots/truth/actions/send_broadcast/editor/with-attachments.png +0 -0
  228. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  229. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  230. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  231. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  232. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  233. package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
  234. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  235. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  236. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  237. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  238. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  239. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  240. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  241. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  242. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  243. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  244. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  245. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  246. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  247. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  248. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  249. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  250. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  251. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  252. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  253. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  254. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  255. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  256. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  257. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  258. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  259. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  260. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  261. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  262. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  263. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  264. package/screenshots/truth/actions/start_session/editor/contact-query.png +0 -0
  265. package/screenshots/truth/actions/start_session/editor/contacts-only.png +0 -0
  266. package/screenshots/truth/actions/start_session/editor/create-contact.png +0 -0
  267. package/screenshots/truth/actions/start_session/editor/groups-and-contacts.png +0 -0
  268. package/screenshots/truth/actions/start_session/editor/groups-only.png +0 -0
  269. package/screenshots/truth/actions/start_session/editor/many-recipients.png +0 -0
  270. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  271. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  272. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  273. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  274. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  275. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  276. package/screenshots/truth/canvas-menu/open.png +0 -0
  277. package/screenshots/truth/editor/router.png +0 -0
  278. package/screenshots/truth/editor/wait.png +0 -0
  279. package/screenshots/truth/list/fields-dragging.png +0 -0
  280. package/screenshots/truth/list/sortable-dragging.png +0 -0
  281. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  282. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  283. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  284. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  285. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  286. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  287. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  288. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  289. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  290. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  291. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  292. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  293. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  294. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  295. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  296. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  297. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  298. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  299. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  300. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  301. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  302. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  303. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  304. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  305. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  306. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  307. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  308. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  309. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  310. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  311. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  312. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  313. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  314. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  315. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  316. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  317. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  318. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  319. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  320. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  321. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  322. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  323. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  324. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  325. package/src/Icons.ts +4 -1
  326. package/src/events.ts +2 -6
  327. package/src/flow/CanvasMenu.ts +217 -0
  328. package/src/flow/CanvasNode.ts +408 -10
  329. package/src/flow/Editor.ts +683 -44
  330. package/src/flow/NodeEditor.ts +304 -125
  331. package/src/flow/NodeTypeSelector.ts +592 -0
  332. package/src/flow/actions/add_contact_groups.ts +4 -4
  333. package/src/flow/actions/add_contact_urn.ts +76 -4
  334. package/src/flow/actions/add_input_labels.ts +4 -4
  335. package/src/flow/actions/play_audio.ts +2 -2
  336. package/src/flow/actions/remove_contact_groups.ts +14 -6
  337. package/src/flow/actions/request_optin.ts +2 -2
  338. package/src/flow/actions/say_msg.ts +2 -2
  339. package/src/flow/actions/send_broadcast.ts +85 -23
  340. package/src/flow/actions/send_email.ts +10 -6
  341. package/src/flow/actions/send_msg.ts +22 -32
  342. package/src/flow/actions/set_contact_channel.ts +5 -11
  343. package/src/flow/actions/set_contact_field.ts +20 -25
  344. package/src/flow/actions/set_contact_language.ts +9 -4
  345. package/src/flow/actions/set_contact_name.ts +3 -15
  346. package/src/flow/actions/set_contact_status.ts +3 -3
  347. package/src/flow/actions/set_run_result.ts +4 -4
  348. package/src/flow/actions/start_session.ts +208 -6
  349. package/src/flow/config.ts +13 -15
  350. package/src/flow/currencies.ts +51 -0
  351. package/src/flow/nodes/shared-rules.ts +301 -0
  352. package/src/flow/nodes/shared.ts +18 -0
  353. package/src/flow/nodes/split_by_airtime.ts +238 -5
  354. package/src/flow/nodes/split_by_contact_field.ts +185 -3
  355. package/src/flow/nodes/split_by_expression.ts +94 -2
  356. package/src/flow/nodes/split_by_groups.ts +15 -10
  357. package/src/flow/nodes/split_by_intent.ts +7 -0
  358. package/src/flow/nodes/split_by_llm.ts +4 -3
  359. package/src/flow/nodes/split_by_llm_categorize.ts +4 -4
  360. package/src/flow/nodes/split_by_random.ts +5 -5
  361. package/src/flow/nodes/split_by_resthook.ts +130 -0
  362. package/src/flow/nodes/split_by_run_result.ts +249 -3
  363. package/src/flow/nodes/split_by_scheme.ts +192 -2
  364. package/src/flow/nodes/split_by_subflow.ts +6 -4
  365. package/src/flow/nodes/split_by_ticket.ts +4 -3
  366. package/src/flow/nodes/split_by_webhook.ts +6 -5
  367. package/src/flow/nodes/wait_for_audio.ts +2 -2
  368. package/src/flow/nodes/wait_for_digits.ts +2 -2
  369. package/src/flow/nodes/wait_for_image.ts +2 -2
  370. package/src/flow/nodes/wait_for_location.ts +2 -2
  371. package/src/flow/nodes/wait_for_menu.ts +2 -2
  372. package/src/flow/nodes/wait_for_response.ts +48 -679
  373. package/src/flow/nodes/wait_for_video.ts +2 -2
  374. package/src/flow/types.ts +109 -23
  375. package/src/flow/utils.ts +108 -14
  376. package/src/form/ContactSearch.ts +1 -1
  377. package/src/form/FieldRenderer.ts +2 -4
  378. package/src/interfaces.ts +3 -0
  379. package/src/list/SortableList.ts +109 -34
  380. package/src/live/ContactChat.ts +15 -18
  381. package/src/store/AppState.ts +69 -0
  382. package/src/store/flow-definition.d.ts +2 -5
  383. package/src/utils.ts +332 -12
  384. package/static/api/channels.json +46 -0
  385. package/static/api/resthooks.json +31 -0
  386. package/static/svg/index.svg +1 -1
  387. package/static/svg/work/traced/lightning-02.svg +1 -0
  388. package/static/svg/work/used/lightning-02.svg +3 -0
  389. package/temba-modules.ts +4 -0
  390. package/test/ActionHelper.ts +3 -3
  391. package/test/NodeHelper.ts +6 -3
  392. package/test/actions/add_contact_urn.test.ts +287 -0
  393. package/test/actions/send_broadcast.test.ts +190 -0
  394. package/test/actions/send_email.test.ts +17 -23
  395. package/test/actions/send_msg.test.ts +39 -15
  396. package/test/actions/start_session.test.ts +151 -0
  397. package/test/nodes/split_by_airtime.test.ts +673 -0
  398. package/test/nodes/split_by_contact_field.test.ts +451 -0
  399. package/test/nodes/split_by_expression.test.ts +751 -0
  400. package/test/nodes/split_by_random.test.ts +3 -3
  401. package/test/nodes/split_by_resthook.test.ts +398 -0
  402. package/test/nodes/split_by_run_result.test.ts +1109 -0
  403. package/test/nodes/split_by_scheme.test.ts +486 -0
  404. package/test/nodes/split_by_subflow.test.ts +381 -0
  405. package/test/nodes/wait_for_digits.test.ts +2 -2
  406. package/test/nodes/wait_for_response.test.ts +2 -1
  407. package/test/temba-action-drag-between-nodes.test.ts +301 -0
  408. package/test/temba-canvas-menu.test.ts +156 -0
  409. package/test/temba-flow-editor-node.test.ts +102 -2
  410. package/test/temba-flow-editor.test.ts +7 -8
  411. package/test/temba-node-editor.test.ts +3 -1
  412. package/test/temba-node-type-selector.test.ts +152 -0
  413. package/test/temba-omnibox.test.ts +2 -1
  414. package/test/temba-sortable-list.test.ts +69 -0
  415. package/test/temba-utils-index.test.ts +0 -35
  416. package/test/utils.test.ts +2 -0
  417. package/test-assets/contacts/history.json +14 -20
  418. package/web-dev-server.config.mjs +3 -1
  419. package/out-tsc/src/flow/actions/call_classifier.js +0 -11
  420. package/out-tsc/src/flow/actions/call_classifier.js.map +0 -1
  421. package/out-tsc/src/flow/actions/call_resthook.js +0 -11
  422. package/out-tsc/src/flow/actions/call_resthook.js.map +0 -1
  423. package/out-tsc/src/flow/actions/split_by_expression_example.js +0 -77
  424. package/out-tsc/src/flow/actions/split_by_expression_example.js.map +0 -1
  425. package/out-tsc/src/flow/actions/transfer_airtime.js +0 -11
  426. package/out-tsc/src/flow/actions/transfer_airtime.js.map +0 -1
  427. package/src/flow/actions/call_classifier.ts +0 -12
  428. package/src/flow/actions/call_resthook.ts +0 -12
  429. package/src/flow/actions/split_by_expression_example.ts +0 -88
  430. package/src/flow/actions/transfer_airtime.ts +0 -12
@@ -0,0 +1,381 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { split_by_subflow } from '../../src/flow/nodes/split_by_subflow';
3
+ import { Node } from '../../src/store/flow-definition';
4
+ import { NodeTest } from '../NodeHelper';
5
+
6
+ /**
7
+ * Test suite for the split_by_subflow node configuration.
8
+ */
9
+ describe('split_by_subflow node config', () => {
10
+ const helper = new NodeTest(split_by_subflow, 'split_by_subflow');
11
+
12
+ describe('basic properties', () => {
13
+ helper.testBasicProperties();
14
+
15
+ it('has correct name', () => {
16
+ expect(split_by_subflow.name).to.equal('Enter a Flow');
17
+ });
18
+
19
+ it('has correct type', () => {
20
+ expect(split_by_subflow.type).to.equal('split_by_subflow');
21
+ });
22
+
23
+ it('has showAsAction set to true', () => {
24
+ expect(split_by_subflow.showAsAction).to.be.true;
25
+ });
26
+
27
+ it('has form configuration', () => {
28
+ expect(split_by_subflow.form).to.exist;
29
+ expect(split_by_subflow.form.flow).to.exist;
30
+ expect(split_by_subflow.form.flow.type).to.equal('select');
31
+ expect(split_by_subflow.form.flow.required).to.be.true;
32
+ });
33
+
34
+ it('has layout configuration', () => {
35
+ expect(split_by_subflow.layout).to.exist;
36
+ expect(split_by_subflow.layout).to.deep.equal(['flow']);
37
+ });
38
+ });
39
+
40
+ describe('toFormData', () => {
41
+ it('extracts data from node with enter_flow action', () => {
42
+ const node: Node = {
43
+ uuid: 'test-node',
44
+ actions: [
45
+ {
46
+ uuid: 'test-action',
47
+ type: 'enter_flow',
48
+ flow: { uuid: 'flow-123', name: 'Registration Flow' }
49
+ } as any
50
+ ],
51
+ router: {
52
+ type: 'switch',
53
+ operand: '@child.status',
54
+ cases: [
55
+ {
56
+ uuid: 'case-1',
57
+ type: 'has_only_text',
58
+ arguments: ['completed'],
59
+ category_uuid: 'cat-1'
60
+ }
61
+ ],
62
+ categories: [
63
+ { uuid: 'cat-1', name: 'Complete', exit_uuid: 'exit-1' },
64
+ { uuid: 'cat-2', name: 'Expired', exit_uuid: 'exit-2' }
65
+ ],
66
+ default_category_uuid: 'cat-2'
67
+ },
68
+ exits: [
69
+ { uuid: 'exit-1', destination_uuid: null },
70
+ { uuid: 'exit-2', destination_uuid: null }
71
+ ]
72
+ };
73
+
74
+ const formData = split_by_subflow.toFormData(node);
75
+
76
+ expect(formData.uuid).to.equal('test-node');
77
+ expect(formData.flow).to.be.an('array');
78
+ expect(formData.flow).to.have.lengthOf(1);
79
+ expect(formData.flow[0]).to.deep.equal({
80
+ uuid: 'flow-123',
81
+ name: 'Registration Flow'
82
+ });
83
+ });
84
+
85
+ it('handles empty node', () => {
86
+ const node: Node = {
87
+ uuid: 'test-node',
88
+ actions: [],
89
+ exits: []
90
+ };
91
+
92
+ const formData = split_by_subflow.toFormData(node);
93
+
94
+ expect(formData.uuid).to.equal('test-node');
95
+ expect(formData.flow).to.be.an('array');
96
+ expect(formData.flow).to.have.lengthOf(0);
97
+ });
98
+ });
99
+
100
+ describe('fromFormData', () => {
101
+ it('creates node with enter_flow action and router from form data', () => {
102
+ const formData = {
103
+ uuid: 'test-node',
104
+ flow: [{ uuid: 'flow-123', name: 'Registration Flow' }]
105
+ };
106
+
107
+ const originalNode: Node = {
108
+ uuid: 'test-node',
109
+ actions: [],
110
+ exits: []
111
+ };
112
+
113
+ const node = split_by_subflow.fromFormData(formData, originalNode);
114
+
115
+ // Check node structure
116
+ expect(node.uuid).to.equal('test-node');
117
+ expect(node.actions).to.have.length(1);
118
+ expect(node.actions[0].type).to.equal('enter_flow');
119
+
120
+ // Check enter_flow action has properly formatted flow
121
+ const enterFlowAction = node.actions[0] as any;
122
+ expect(enterFlowAction.flow).to.exist;
123
+ expect(enterFlowAction.flow.uuid).to.equal('flow-123');
124
+ expect(enterFlowAction.flow.name).to.equal('Registration Flow');
125
+
126
+ // Check router
127
+ expect(node.router).to.exist;
128
+ expect(node.router.type).to.equal('switch');
129
+ expect(node.router.operand).to.equal('@child.status');
130
+ expect(node.router.categories).to.have.length(2);
131
+ expect(node.router.categories[0].name).to.equal('Complete');
132
+ expect(node.router.categories[1].name).to.equal('Expired');
133
+
134
+ // Check exits
135
+ expect(node.exits).to.have.length(2);
136
+ });
137
+
138
+ it('handles empty flow selection', () => {
139
+ const formData = {
140
+ uuid: 'test-node',
141
+ flow: []
142
+ };
143
+
144
+ const originalNode: Node = {
145
+ uuid: 'test-node',
146
+ actions: [],
147
+ exits: []
148
+ };
149
+
150
+ const node = split_by_subflow.fromFormData(formData, originalNode);
151
+
152
+ // Should still create the node structure
153
+ expect(node.actions).to.have.length(1);
154
+ const enterFlowAction = node.actions[0] as any;
155
+ expect(enterFlowAction.flow.uuid).to.equal('');
156
+ expect(enterFlowAction.flow.name).to.equal('');
157
+ });
158
+
159
+ it('preserves UUIDs when editing existing node', () => {
160
+ const existingNode: Node = {
161
+ uuid: 'test-node',
162
+ actions: [
163
+ {
164
+ uuid: 'existing-action-uuid',
165
+ type: 'enter_flow',
166
+ flow: { uuid: 'flow-old', name: 'Old Flow' }
167
+ } as any
168
+ ],
169
+ router: {
170
+ type: 'switch',
171
+ operand: '@child.status',
172
+ cases: [
173
+ {
174
+ uuid: 'existing-case-uuid',
175
+ type: 'has_only_text',
176
+ arguments: ['completed'],
177
+ category_uuid: 'existing-cat-1'
178
+ }
179
+ ],
180
+ categories: [
181
+ {
182
+ uuid: 'existing-cat-1',
183
+ name: 'Complete',
184
+ exit_uuid: 'existing-exit-1'
185
+ },
186
+ {
187
+ uuid: 'existing-cat-2',
188
+ name: 'Expired',
189
+ exit_uuid: 'existing-exit-2'
190
+ }
191
+ ],
192
+ default_category_uuid: 'existing-cat-2'
193
+ },
194
+ exits: [
195
+ { uuid: 'existing-exit-1', destination_uuid: null },
196
+ { uuid: 'existing-exit-2', destination_uuid: null }
197
+ ]
198
+ };
199
+
200
+ const formData = {
201
+ uuid: 'test-node',
202
+ flow: [{ uuid: 'flow-new', name: 'New Flow' }]
203
+ };
204
+
205
+ const node = split_by_subflow.fromFormData(formData, existingNode);
206
+
207
+ // Should preserve action UUID
208
+ expect(node.actions[0].uuid).to.equal('existing-action-uuid');
209
+
210
+ // Should preserve router structure UUIDs
211
+ expect(node.router.categories[0].uuid).to.equal('existing-cat-1');
212
+ expect(node.router.categories[1].uuid).to.equal('existing-cat-2');
213
+ expect(node.exits[0].uuid).to.equal('existing-exit-1');
214
+ expect(node.exits[1].uuid).to.equal('existing-exit-2');
215
+
216
+ // But should update the flow
217
+ const enterFlowAction = node.actions[0] as any;
218
+ expect(enterFlowAction.flow.uuid).to.equal('flow-new');
219
+ expect(enterFlowAction.flow.name).to.equal('New Flow');
220
+ });
221
+ });
222
+
223
+ describe('round-trip transformation', () => {
224
+ it('should preserve node structure through toFormData -> fromFormData', () => {
225
+ const originalNode: Node = {
226
+ uuid: 'test-node-uuid',
227
+ actions: [
228
+ {
229
+ uuid: 'action-uuid',
230
+ type: 'enter_flow',
231
+ flow: { uuid: 'flow-456', name: 'My Subflow' }
232
+ } as any
233
+ ],
234
+ router: {
235
+ type: 'switch',
236
+ operand: '@child.status',
237
+ cases: [
238
+ {
239
+ uuid: 'case-uuid',
240
+ type: 'has_only_text',
241
+ arguments: ['completed'],
242
+ category_uuid: 'cat-complete'
243
+ }
244
+ ],
245
+ categories: [
246
+ {
247
+ uuid: 'cat-complete',
248
+ name: 'Complete',
249
+ exit_uuid: 'exit-complete'
250
+ },
251
+ {
252
+ uuid: 'cat-expired',
253
+ name: 'Expired',
254
+ exit_uuid: 'exit-expired'
255
+ }
256
+ ],
257
+ default_category_uuid: 'cat-expired'
258
+ },
259
+ exits: [
260
+ { uuid: 'exit-complete', destination_uuid: null },
261
+ { uuid: 'exit-expired', destination_uuid: null }
262
+ ]
263
+ };
264
+
265
+ // Convert to form data
266
+ const formData = split_by_subflow.toFormData(originalNode);
267
+
268
+ // Convert back to node
269
+ const resultNode = split_by_subflow.fromFormData(formData, originalNode);
270
+
271
+ // Should match the original structure
272
+ expect(resultNode).to.deep.equal(originalNode);
273
+ });
274
+ });
275
+
276
+ describe('NodeEditor integration', () => {
277
+ it('should properly use node fromFormData when formDataToNode is called', async () => {
278
+ // This test simulates the bug where node.fromFormData should be used
279
+ // to create the entire node (including actions), not action.fromFormData
280
+
281
+ const { fixture, html } = await import('@open-wc/testing');
282
+ await import('../../temba-modules');
283
+
284
+ const originalNode: Node = {
285
+ uuid: 'test-node-uuid',
286
+ actions: [
287
+ {
288
+ uuid: 'action-uuid',
289
+ type: 'enter_flow',
290
+ flow: { uuid: 'flow-old', name: 'Old Flow' }
291
+ } as any
292
+ ],
293
+ router: {
294
+ type: 'switch',
295
+ operand: '@child.status',
296
+ cases: [
297
+ {
298
+ uuid: 'case-uuid',
299
+ type: 'has_only_text',
300
+ arguments: ['completed'],
301
+ category_uuid: 'cat-complete'
302
+ }
303
+ ],
304
+ categories: [
305
+ {
306
+ uuid: 'cat-complete',
307
+ name: 'Complete',
308
+ exit_uuid: 'exit-complete'
309
+ },
310
+ {
311
+ uuid: 'cat-expired',
312
+ name: 'Expired',
313
+ exit_uuid: 'exit-expired'
314
+ }
315
+ ],
316
+ default_category_uuid: 'cat-expired'
317
+ },
318
+ exits: [
319
+ { uuid: 'exit-complete', destination_uuid: null },
320
+ { uuid: 'exit-expired', destination_uuid: null }
321
+ ]
322
+ };
323
+
324
+ const nodeUI = {
325
+ type: 'split_by_subflow',
326
+ position: { left: 50, top: 50 }
327
+ };
328
+
329
+ // Create the node editor
330
+ const nodeEditor = (await fixture(html`
331
+ <temba-node-editor
332
+ .node=${originalNode}
333
+ .nodeUI=${nodeUI}
334
+ .action=${originalNode.actions[0]}
335
+ .isOpen=${true}
336
+ ></temba-node-editor>
337
+ `)) as any;
338
+
339
+ await nodeEditor.updateComplete;
340
+ await new Promise((resolve) => setTimeout(resolve, 200));
341
+ await nodeEditor.updateComplete;
342
+
343
+ // Get the initial formData to verify toFormData was called
344
+ const initialFormData = nodeEditor.formData;
345
+ expect(initialFormData.flow).to.exist;
346
+ expect(initialFormData.flow).to.have.lengthOf(1);
347
+ expect(initialFormData.flow[0].uuid).to.equal('flow-old');
348
+
349
+ // Simulate changing the flow selection
350
+ const newFormData = {
351
+ ...initialFormData,
352
+ flow: [{ uuid: 'flow-new', name: 'New Flow' }]
353
+ };
354
+
355
+ // Call formDataToNode directly to test the transformation
356
+ const resultNode = nodeEditor.formDataToNode(newFormData);
357
+
358
+ // Verify that the result node has the new flow properly formatted
359
+ expect(resultNode).to.exist;
360
+ expect(resultNode.actions).to.have.length(1);
361
+
362
+ const enterFlowAction = resultNode.actions[0] as any;
363
+ expect(enterFlowAction.type).to.equal('enter_flow');
364
+
365
+ // This is the key assertion - the flow should be properly formatted
366
+ // as created by node's fromFormData, not by action's fromFormData
367
+ expect(enterFlowAction.flow).to.exist;
368
+ expect(enterFlowAction.flow.uuid).to.equal('flow-new');
369
+ expect(enterFlowAction.flow.name).to.equal('New Flow');
370
+
371
+ // The flow should NOT have extra properties that might come from the select component
372
+ expect(enterFlowAction.flow).to.not.have.property('value');
373
+
374
+ // Verify router structure was also created properly by node's fromFormData
375
+ expect(resultNode.router).to.exist;
376
+ expect(resultNode.router.type).to.equal('switch');
377
+ expect(resultNode.router.categories).to.have.length(2);
378
+ expect(resultNode.exits).to.have.length(2);
379
+ });
380
+ });
381
+ });
@@ -20,8 +20,8 @@ describe('wait_for_digits node config', () => {
20
20
  expect(wait_for_digits.type).to.equal('wait_for_digits');
21
21
  });
22
22
 
23
- it('has correct color', () => {
24
- expect(wait_for_digits.color).to.exist;
23
+ it('has correct group', () => {
24
+ expect(wait_for_digits.group).to.exist;
25
25
  });
26
26
 
27
27
  it('is a simple node config without form or layout', () => {
@@ -375,7 +375,8 @@ describe('wait_for_response node config', () => {
375
375
  const result = wait_for_response.fromFormData!(formData, originalNode);
376
376
 
377
377
  expect(result.uuid).to.equal('test-node');
378
- expect(result.router?.result_name).to.equal('response');
378
+ // When no result_name is provided, it should not be set
379
+ expect(result.router?.result_name).to.be.undefined;
379
380
  });
380
381
 
381
382
  it('handles operators with no operands correctly', () => {
@@ -0,0 +1,301 @@
1
+ import { expect, fixture, html } from '@open-wc/testing';
2
+ import { CanvasNode } from '../src/flow/CanvasNode';
3
+ import { Node, NodeUI, SendMsg } from '../src/store/flow-definition';
4
+ import '../temba-modules';
5
+
6
+ describe('Drag actions between nodes', () => {
7
+ let node1: Node;
8
+ let node1UI: NodeUI;
9
+ let node2: Node;
10
+ let node2UI: NodeUI;
11
+
12
+ beforeEach(() => {
13
+ // Create test nodes
14
+ node1 = {
15
+ uuid: 'node-1',
16
+ actions: [
17
+ {
18
+ type: 'send_msg',
19
+ uuid: 'action-1',
20
+ text: 'First message',
21
+ quick_replies: []
22
+ } as SendMsg,
23
+ {
24
+ type: 'send_msg',
25
+ uuid: 'action-2',
26
+ text: 'Second message',
27
+ quick_replies: []
28
+ } as SendMsg
29
+ ],
30
+ exits: [{ uuid: 'exit-1', destination_uuid: null }]
31
+ };
32
+
33
+ node1UI = {
34
+ position: { left: 100, top: 100 },
35
+ type: 'execute_actions',
36
+ config: {}
37
+ };
38
+
39
+ node2 = {
40
+ uuid: 'node-2',
41
+ actions: [
42
+ {
43
+ type: 'send_msg',
44
+ uuid: 'action-3',
45
+ text: 'Third message',
46
+ quick_replies: []
47
+ } as SendMsg
48
+ ],
49
+ exits: [{ uuid: 'exit-2', destination_uuid: null }]
50
+ };
51
+
52
+ node2UI = {
53
+ position: { left: 400, top: 100 },
54
+ type: 'execute_actions',
55
+ config: {}
56
+ };
57
+ });
58
+
59
+ it('should render execute_actions node with sortable list', async () => {
60
+ const node1Element = await fixture<CanvasNode>(html`
61
+ <temba-flow-node .node=${node1} .ui=${node1UI}></temba-flow-node>
62
+ `);
63
+
64
+ await node1Element.updateComplete;
65
+
66
+ // Verify node renders with sortable list
67
+ const sortableList = node1Element.querySelector('temba-sortable-list');
68
+ expect(sortableList).to.exist;
69
+
70
+ // Verify actions are rendered
71
+ const actions = node1Element.querySelectorAll('.action.sortable');
72
+ expect(actions.length).to.equal(2);
73
+ });
74
+
75
+ it('should show placeholder in target node during drag', async () => {
76
+ const node2Element = await fixture<CanvasNode>(html`
77
+ <temba-flow-node .node=${node2} .ui=${node2UI}></temba-flow-node>
78
+ `);
79
+
80
+ await node2Element.updateComplete;
81
+
82
+ // Simulate action-drag-over event from Editor
83
+ const dragOverEvent = new CustomEvent('action-drag-over', {
84
+ detail: {
85
+ action: node1.actions[0],
86
+ sourceNodeUuid: 'node-1',
87
+ actionIndex: 0,
88
+ mouseY: 150
89
+ },
90
+ bubbles: false
91
+ });
92
+
93
+ node2Element.dispatchEvent(dragOverEvent);
94
+ await node2Element.updateComplete;
95
+
96
+ // Check that placeholder is rendered
97
+ const placeholder = node2Element.querySelector('.drop-placeholder');
98
+ expect(placeholder).to.exist;
99
+ });
100
+
101
+ it('should handle drag-over event and store external drag info', async () => {
102
+ const node2Element = await fixture<CanvasNode>(html`
103
+ <temba-flow-node .node=${node2} .ui=${node2UI}></temba-flow-node>
104
+ `);
105
+
106
+ await node2Element.updateComplete;
107
+
108
+ // Simulate drag over
109
+ const dragOverEvent = new CustomEvent('action-drag-over', {
110
+ detail: {
111
+ action: node1.actions[0],
112
+ sourceNodeUuid: 'node-1',
113
+ actionIndex: 0,
114
+ mouseY: 150
115
+ },
116
+ bubbles: false
117
+ });
118
+ node2Element.dispatchEvent(dragOverEvent);
119
+ await node2Element.updateComplete;
120
+
121
+ // Verify that external drag info is stored (check internal state)
122
+ const externalDragInfo = (node2Element as any).externalDragInfo;
123
+ expect(externalDragInfo).to.exist;
124
+ expect(externalDragInfo.action.uuid).to.equal('action-1');
125
+ expect(externalDragInfo.sourceNodeUuid).to.equal('node-1');
126
+ });
127
+
128
+ it('should calculate correct drop index based on mouse position', async () => {
129
+ const node2Element = await fixture<CanvasNode>(html`
130
+ <temba-flow-node .node=${node2} .ui=${node2UI}></temba-flow-node>
131
+ `);
132
+
133
+ await node2Element.updateComplete;
134
+
135
+ // Get action element bounds to calculate positions
136
+ const actionElement = node2Element.querySelector(
137
+ '.action.sortable'
138
+ ) as HTMLElement;
139
+ expect(actionElement).to.exist;
140
+
141
+ const rect = actionElement.getBoundingClientRect();
142
+ const topY = rect.top + 5; // Near top of first action
143
+ const bottomY = rect.bottom + 5; // Below first action
144
+
145
+ // Drag over at top
146
+ const dragOverEventTop = new CustomEvent('action-drag-over', {
147
+ detail: {
148
+ action: node1.actions[0],
149
+ sourceNodeUuid: 'node-1',
150
+ actionIndex: 0,
151
+ mouseY: topY
152
+ },
153
+ bubbles: false
154
+ });
155
+ node2Element.dispatchEvent(dragOverEventTop);
156
+ await node2Element.updateComplete;
157
+
158
+ // Check drop index is at beginning
159
+ let externalDragInfo = (node2Element as any).externalDragInfo;
160
+ expect(externalDragInfo.dropIndex).to.equal(0);
161
+
162
+ // Drag over at bottom
163
+ const dragOverEventBottom = new CustomEvent('action-drag-over', {
164
+ detail: {
165
+ action: node1.actions[0],
166
+ sourceNodeUuid: 'node-1',
167
+ actionIndex: 0,
168
+ mouseY: bottomY
169
+ },
170
+ bubbles: false
171
+ });
172
+ node2Element.dispatchEvent(dragOverEventBottom);
173
+ await node2Element.updateComplete;
174
+
175
+ // Check drop index is at end
176
+ externalDragInfo = (node2Element as any).externalDragInfo;
177
+ expect(externalDragInfo.dropIndex).to.equal(1);
178
+ });
179
+
180
+ it('should not accept drops from the same node', async () => {
181
+ const node1Element = await fixture<CanvasNode>(html`
182
+ <temba-flow-node .node=${node1} .ui=${node1UI}></temba-flow-node>
183
+ `);
184
+
185
+ await node1Element.updateComplete;
186
+
187
+ // Try to drop action from node-1 onto node-1
188
+ const dragOverEvent = new CustomEvent('action-drag-over', {
189
+ detail: {
190
+ action: node1.actions[0],
191
+ sourceNodeUuid: 'node-1', // Same as target
192
+ actionIndex: 0,
193
+ mouseY: 150
194
+ },
195
+ bubbles: false
196
+ });
197
+ node1Element.dispatchEvent(dragOverEvent);
198
+ await node1Element.updateComplete;
199
+
200
+ // Verify external drag info was NOT stored (rejected due to same source)
201
+ const externalDragInfo = (node1Element as any).externalDragInfo;
202
+ expect(externalDragInfo).to.be.null;
203
+ });
204
+
205
+ it('should clear external drag state and hide placeholder', async () => {
206
+ const node2Element = await fixture<CanvasNode>(html`
207
+ <temba-flow-node .node=${node2} .ui=${node2UI}></temba-flow-node>
208
+ `);
209
+
210
+ await node2Element.updateComplete;
211
+
212
+ // Simulate drag over
213
+ const dragOverEvent = new CustomEvent('action-drag-over', {
214
+ detail: {
215
+ action: node1.actions[0],
216
+ sourceNodeUuid: 'node-1',
217
+ actionIndex: 0,
218
+ mouseY: 150
219
+ },
220
+ bubbles: false
221
+ });
222
+ node2Element.dispatchEvent(dragOverEvent);
223
+ await node2Element.updateComplete;
224
+
225
+ // Verify placeholder exists
226
+ let placeholder = node2Element.querySelector('.drop-placeholder');
227
+ expect(placeholder).to.exist;
228
+
229
+ // Clear the external drag state (simulating drag leaving the node)
230
+ (node2Element as any).externalDragInfo = null;
231
+ // Trigger re-render
232
+ node2Element.requestUpdate();
233
+ await node2Element.updateComplete;
234
+
235
+ // Verify placeholder is gone
236
+ placeholder = node2Element.querySelector('.drop-placeholder');
237
+ expect(placeholder).to.not.exist;
238
+ });
239
+
240
+ it('should have sortable list for internal drag support', async () => {
241
+ const node1Element = await fixture<CanvasNode>(html`
242
+ <temba-flow-node .node=${node1} .ui=${node1UI}></temba-flow-node>
243
+ `);
244
+
245
+ await node1Element.updateComplete;
246
+
247
+ // Verify sortable list exists for internal drag (reordering within same node)
248
+ const sortableList = node1Element.querySelector('temba-sortable-list');
249
+ expect(sortableList).to.exist;
250
+
251
+ // Verify it has external drag enabled
252
+ expect(sortableList.hasAttribute('externaldrag')).to.be.true;
253
+ });
254
+
255
+ it('should only accept drops on execute_actions nodes', async () => {
256
+ const routerNodeUI: NodeUI = {
257
+ position: { left: 100, top: 100 },
258
+ type: 'split_by_expression', // Not execute_actions
259
+ config: {}
260
+ };
261
+
262
+ const routerNode: Node = {
263
+ uuid: 'router-node',
264
+ actions: [],
265
+ exits: [{ uuid: 'exit-1', destination_uuid: null }],
266
+ router: {
267
+ type: 'switch',
268
+ operand: '@input',
269
+ cases: [],
270
+ categories: [],
271
+ default_category_uuid: 'cat-1'
272
+ }
273
+ };
274
+
275
+ const routerElement = await fixture<CanvasNode>(html`
276
+ <temba-flow-node
277
+ .node=${routerNode}
278
+ .ui=${routerNodeUI}
279
+ ></temba-flow-node>
280
+ `);
281
+
282
+ await routerElement.updateComplete;
283
+
284
+ // Try to drag over a non-execute_actions node
285
+ const dragOverEvent = new CustomEvent('action-drag-over', {
286
+ detail: {
287
+ action: node1.actions[0],
288
+ sourceNodeUuid: 'node-1',
289
+ actionIndex: 0,
290
+ mouseY: 150
291
+ },
292
+ bubbles: false
293
+ });
294
+ routerElement.dispatchEvent(dragOverEvent);
295
+ await routerElement.updateComplete;
296
+
297
+ // Verify external drag info was NOT stored (rejected due to wrong node type)
298
+ const externalDragInfo = (routerElement as any).externalDragInfo;
299
+ expect(externalDragInfo).to.be.null;
300
+ });
301
+ });