@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,301 @@
1
+ import { getOperatorConfig } from '../operators';
2
+ import { generateDefaultCategoryName } from '../../utils';
3
+ import { FormData } from '../types';
4
+
5
+ /**
6
+ * Shared helper function to get operator value from various formats.
7
+ * Handles string, object, and array formats that can come from the form system.
8
+ */
9
+ export const getOperatorValue = (operator: any): string => {
10
+ if (typeof operator === 'string') {
11
+ return operator.trim();
12
+ } else if (Array.isArray(operator) && operator.length > 0) {
13
+ const firstOperator = operator[0];
14
+ if (
15
+ firstOperator &&
16
+ typeof firstOperator === 'object' &&
17
+ firstOperator.value
18
+ ) {
19
+ return firstOperator.value.trim();
20
+ }
21
+ } else if (operator && typeof operator === 'object' && operator.value) {
22
+ return operator.value.trim();
23
+ }
24
+ return '';
25
+ };
26
+
27
+ /**
28
+ * Shared isEmptyItem function for rules array.
29
+ * Determines if a rule item is considered empty and should be filtered out.
30
+ */
31
+ export const isEmptyRuleItem = (item: any): boolean => {
32
+ // Check if operator and category are provided
33
+ const operatorValue = getOperatorValue(item.operator);
34
+ if (!operatorValue || !item.category || item.category.trim() === '') {
35
+ return true;
36
+ }
37
+
38
+ // Check if value is required based on operator configuration
39
+ const operatorConfig = getOperatorConfig(operatorValue);
40
+ if (operatorConfig && operatorConfig.operands === 1) {
41
+ return !item.value1 || item.value1.trim() === '';
42
+ } else if (operatorConfig && operatorConfig.operands === 2) {
43
+ return (
44
+ !item.value1 ||
45
+ item.value1.trim() === '' ||
46
+ !item.value2 ||
47
+ item.value2.trim() === ''
48
+ );
49
+ }
50
+
51
+ return false;
52
+ };
53
+
54
+ /**
55
+ * Shared onItemChange function for rules array.
56
+ * Handles auto-updating category names based on operator and value changes.
57
+ */
58
+ export const createRuleItemChangeHandler = () => {
59
+ return (itemIndex: number, field: string, value: any, allItems: any[]) => {
60
+ const updatedItems = [...allItems];
61
+ const item = { ...updatedItems[itemIndex] };
62
+
63
+ // Update the changed field
64
+ item[field] = value;
65
+
66
+ // Get operator values (before and after the change)
67
+ const oldItem = allItems[itemIndex] || {};
68
+ const oldOperatorValue =
69
+ field === 'operator'
70
+ ? getOperatorValue(oldItem.operator)
71
+ : getOperatorValue(item.operator);
72
+ const newOperatorValue = getOperatorValue(item.operator);
73
+
74
+ // Calculate what the default category name should be before the change
75
+ const oldDefaultCategory = generateDefaultCategoryName(
76
+ oldOperatorValue,
77
+ getOperatorConfig,
78
+ field === 'value1' ? oldItem.value1 : item.value1,
79
+ field === 'value2' ? oldItem.value2 : item.value2
80
+ );
81
+
82
+ // Calculate what the new default category name should be after the change
83
+ const newDefaultCategory = generateDefaultCategoryName(
84
+ newOperatorValue,
85
+ getOperatorConfig,
86
+ item.value1,
87
+ item.value2
88
+ );
89
+
90
+ // Determine if we should auto-update the category
91
+ const shouldUpdateCategory =
92
+ !item.category ||
93
+ item.category.trim() === '' ||
94
+ item.category === oldDefaultCategory;
95
+
96
+ // Auto-populate or update category if conditions are met
97
+ if (shouldUpdateCategory && newDefaultCategory) {
98
+ item.category = newDefaultCategory;
99
+ }
100
+
101
+ updatedItems[itemIndex] = item;
102
+ return updatedItems;
103
+ };
104
+ };
105
+
106
+ /**
107
+ * Shared visibility condition for value1 field.
108
+ */
109
+ export const value1VisibilityCondition = (formData: Record<string, any>) => {
110
+ const operatorValue = getOperatorValue(formData.operator);
111
+ const operatorConfig = getOperatorConfig(operatorValue);
112
+ return operatorConfig ? operatorConfig.operands >= 1 : true;
113
+ };
114
+
115
+ /**
116
+ * Shared visibility condition for value2 field.
117
+ */
118
+ export const value2VisibilityCondition = (formData: Record<string, any>) => {
119
+ const operatorValue = getOperatorValue(formData.operator);
120
+ const operatorConfig = getOperatorConfig(operatorValue);
121
+ return operatorConfig ? operatorConfig.operands === 2 : false;
122
+ };
123
+
124
+ /**
125
+ * Shared item configuration for rules array.
126
+ * This defines the operator, value1, value2, and category fields.
127
+ */
128
+ export const createRulesItemConfig = () => ({
129
+ operator: {
130
+ type: 'select' as const,
131
+ required: true,
132
+ multi: false,
133
+ options: [], // Will be set by the caller
134
+ flavor: 'xsmall' as const,
135
+ width: '200px'
136
+ },
137
+ value1: {
138
+ type: 'text' as const,
139
+ flavor: 'xsmall' as const,
140
+ conditions: {
141
+ visible: value1VisibilityCondition
142
+ }
143
+ },
144
+ value2: {
145
+ type: 'text' as const,
146
+ flavor: 'xsmall' as const,
147
+ conditions: {
148
+ visible: value2VisibilityCondition
149
+ }
150
+ },
151
+ category: {
152
+ type: 'text' as const,
153
+ placeholder: 'Category',
154
+ required: true,
155
+ maxWidth: '120px',
156
+ flavor: 'xsmall' as const
157
+ }
158
+ });
159
+
160
+ /**
161
+ * Shared function to extract rules from form data.
162
+ * Filters and transforms form rules into the format expected by createRulesRouter.
163
+ */
164
+ export const extractUserRules = (formData: FormData) => {
165
+ return (formData.rules || [])
166
+ .filter((rule: any) => {
167
+ const operatorValue = getOperatorValue(rule?.operator);
168
+ if (
169
+ !operatorValue ||
170
+ !rule?.category ||
171
+ operatorValue === '' ||
172
+ rule.category.trim() === ''
173
+ ) {
174
+ return false;
175
+ }
176
+
177
+ const operatorConfig = getOperatorConfig(operatorValue);
178
+ if (operatorConfig && operatorConfig.operands === 1) {
179
+ return rule?.value1 && rule.value1.trim() !== '';
180
+ } else if (operatorConfig && operatorConfig.operands === 2) {
181
+ return (
182
+ rule?.value1 &&
183
+ rule.value1.trim() !== '' &&
184
+ rule?.value2 &&
185
+ rule.value2.trim() !== ''
186
+ );
187
+ }
188
+
189
+ return true;
190
+ })
191
+ .map((rule: any) => {
192
+ const operatorValue = getOperatorValue(rule.operator);
193
+ const operatorConfig = getOperatorConfig(operatorValue);
194
+
195
+ let value = '';
196
+
197
+ if (operatorConfig && operatorConfig.operands === 1) {
198
+ value = rule.value1 ? rule.value1.trim() : '';
199
+ } else if (operatorConfig && operatorConfig.operands === 2) {
200
+ const val1 = rule.value1 ? rule.value1.trim() : '';
201
+ const val2 = rule.value2 ? rule.value2.trim() : '';
202
+ value = `${val1} ${val2}`.trim();
203
+ } else {
204
+ value = '';
205
+ }
206
+
207
+ return {
208
+ operator: operatorValue,
209
+ value: value,
210
+ category: rule.category.trim()
211
+ };
212
+ });
213
+ };
214
+
215
+ /**
216
+ * Shared function to transform router cases to form rules.
217
+ * Converts node router cases back into the form data structure.
218
+ */
219
+ export const casesToFormRules = (node: any) => {
220
+ const rules = [];
221
+ if (node.router?.cases && node.router?.categories) {
222
+ node.router.cases.forEach((case_: any) => {
223
+ // Find the category for this case
224
+ const category = node.router!.categories.find(
225
+ (cat: any) => cat.uuid === case_.category_uuid
226
+ );
227
+
228
+ // Skip system categories
229
+ if (category && !isSystemCategory(category.name)) {
230
+ // Handle different operator types
231
+ const operatorConfig = getOperatorConfig(case_.type);
232
+ const operatorDisplayName = operatorConfig
233
+ ? operatorConfig.name
234
+ : case_.type;
235
+ let value1 = '';
236
+ let value2 = '';
237
+
238
+ if (operatorConfig && operatorConfig.operands === 0) {
239
+ value1 = '';
240
+ value2 = '';
241
+ } else if (operatorConfig && operatorConfig.operands === 1) {
242
+ value1 = case_.arguments.join(' ');
243
+ value2 = '';
244
+ } else if (operatorConfig && operatorConfig.operands === 2) {
245
+ value1 = case_.arguments[0] || '';
246
+ value2 = case_.arguments[1] || '';
247
+ } else {
248
+ value1 = case_.arguments.join(' ');
249
+ value2 = '';
250
+ }
251
+
252
+ rules.push({
253
+ operator: { value: case_.type, name: operatorDisplayName },
254
+ value1: value1,
255
+ value2: value2,
256
+ category: category.name
257
+ });
258
+ }
259
+ });
260
+ }
261
+ return rules;
262
+ };
263
+
264
+ /**
265
+ * Helper to check if a category is a system category
266
+ */
267
+ function isSystemCategory(categoryName: string): boolean {
268
+ return ['No Response', 'Other', 'All Responses', 'Timeout'].includes(
269
+ categoryName
270
+ );
271
+ }
272
+
273
+ /**
274
+ * Creates a complete rules array configuration for forms.
275
+ * This is the shared configuration used by both wait_for_response and split_by_expression.
276
+ *
277
+ * @param operatorOptions - The operator options to use (from operatorsToSelectOptions)
278
+ * @param helpText - The help text to display for the rules array
279
+ * @returns A complete array field configuration object
280
+ */
281
+ export const createRulesArrayConfig = (
282
+ operatorOptions: any[],
283
+ helpText: string = 'Define rules to categorize responses'
284
+ ) => ({
285
+ type: 'array' as const,
286
+ helpText,
287
+ itemLabel: 'Rule',
288
+ minItems: 0,
289
+ maxItems: 100,
290
+ sortable: true,
291
+ maintainEmptyItem: true,
292
+ isEmptyItem: isEmptyRuleItem,
293
+ onItemChange: createRuleItemChangeHandler(),
294
+ itemConfig: {
295
+ ...createRulesItemConfig(),
296
+ operator: {
297
+ ...createRulesItemConfig().operator,
298
+ options: operatorOptions
299
+ }
300
+ }
301
+ });
@@ -0,0 +1,18 @@
1
+ import { TextFieldConfig } from '../types';
2
+
3
+ /**
4
+ * Shared result_name field configuration for router nodes.
5
+ * This provides a consistent "Save as..." optional field interface across all splits.
6
+ *
7
+ * The field is hidden by default and revealed via a "Save as..." link.
8
+ * Once revealed, it cannot be hidden again (the link disappears).
9
+ * If the field already has a value, it's shown immediately without the link.
10
+ */
11
+ export const resultNameField: TextFieldConfig = {
12
+ type: 'text',
13
+ label: 'Result Name',
14
+ required: false,
15
+ placeholder: '(optional)',
16
+ helpText: 'The name to use to reference this result in the flow',
17
+ optionalLink: 'Save result as...'
18
+ };
@@ -1,9 +1,242 @@
1
- import { transfer_airtime } from '../actions/transfer_airtime';
2
- import { COLORS, NodeConfig } from '../types';
1
+ import { ACTION_GROUPS, FormData, NodeConfig } from '../types';
2
+ import { TransferAirtime, Node } from '../../store/flow-definition';
3
+ import { generateUUID, createSuccessFailureRouter } from '../../utils';
4
+ import { html } from 'lit';
5
+ import { CURRENCY_OPTIONS, CURRENCIES } from '../currencies';
6
+ import { resultNameField } from './shared';
3
7
 
4
8
  export const split_by_airtime: NodeConfig = {
5
9
  type: 'split_by_airtime',
6
- name: 'Split by Airtime Transfer',
7
- color: COLORS.send,
8
- action: transfer_airtime
10
+ name: 'Send Airtime',
11
+ group: ACTION_GROUPS.services,
12
+ showAsAction: true,
13
+ form: {
14
+ amounts: {
15
+ type: 'array',
16
+ label: 'Airtime Amounts',
17
+ helpText: 'Define the currencies and amounts to transfer',
18
+ required: true,
19
+ itemLabel: 'Amount',
20
+ sortable: false,
21
+ minItems: 1,
22
+ maxItems: 10,
23
+ isEmptyItem: (item: any) => {
24
+ return !item.currency || !item.amount || item.amount.trim() === '';
25
+ },
26
+ itemConfig: {
27
+ currency: {
28
+ type: 'select',
29
+ placeholder: 'Select a currency',
30
+ required: true,
31
+ options: CURRENCY_OPTIONS,
32
+ searchable: true,
33
+ multi: false,
34
+ width: '200px'
35
+ },
36
+ amount: {
37
+ type: 'text',
38
+ placeholder: 'Amount',
39
+ required: true
40
+ }
41
+ }
42
+ },
43
+ result_name: resultNameField
44
+ },
45
+ layout: ['amounts', 'result_name'],
46
+ validate: (formData: FormData) => {
47
+ const errors: { [key: string]: string } = {};
48
+
49
+ // Validate that we have at least one amount
50
+ if (formData.amounts && Array.isArray(formData.amounts)) {
51
+ const validAmounts = formData.amounts.filter(
52
+ (item: any) =>
53
+ item?.currency && item?.amount && item.amount.trim() !== ''
54
+ );
55
+
56
+ if (validAmounts.length === 0) {
57
+ errors.amounts = 'At least one currency and amount is required';
58
+ return {
59
+ valid: false,
60
+ errors
61
+ };
62
+ }
63
+
64
+ // Check for duplicate currencies
65
+ const currencies = new Set();
66
+ const duplicates: string[] = [];
67
+
68
+ validAmounts.forEach((item: any) => {
69
+ // Extract currency code from selection
70
+ const currencyCode =
71
+ Array.isArray(item.currency) && item.currency.length > 0
72
+ ? item.currency[0].value
73
+ : typeof item.currency === 'string'
74
+ ? item.currency
75
+ : item.currency?.value;
76
+
77
+ if (currencies.has(currencyCode)) {
78
+ duplicates.push(currencyCode);
79
+ } else {
80
+ currencies.add(currencyCode);
81
+ }
82
+ });
83
+
84
+ if (duplicates.length > 0) {
85
+ errors.amounts = `Duplicate currencies found: ${duplicates.join(', ')}`;
86
+ }
87
+
88
+ // Validate amounts are numeric
89
+ for (const item of validAmounts) {
90
+ const amount = item.amount.trim();
91
+ if (isNaN(Number(amount)) || Number(amount) <= 0) {
92
+ errors.amounts = 'All amounts must be valid positive numbers';
93
+ return {
94
+ valid: false,
95
+ errors
96
+ };
97
+ }
98
+ }
99
+ } else {
100
+ errors.amounts = 'At least one currency and amount is required';
101
+ }
102
+
103
+ return {
104
+ valid: Object.keys(errors).length === 0,
105
+ errors
106
+ };
107
+ },
108
+ render: (node: Node) => {
109
+ const transferAirtimeAction = node.actions?.find(
110
+ (action) => action.type === 'transfer_airtime'
111
+ ) as TransferAirtime;
112
+
113
+ if (!transferAirtimeAction || !transferAirtimeAction.amounts) {
114
+ return html`<div class="body">Configure airtime transfer</div>`;
115
+ }
116
+
117
+ const amounts = transferAirtimeAction.amounts;
118
+ const currencies = Object.keys(amounts);
119
+
120
+ if (currencies.length === 0) {
121
+ return html`<div class="body">Configure airtime transfer</div>`;
122
+ }
123
+
124
+ // Display the first currency amount, with indicator if there are more
125
+ const firstCurrency = currencies[0];
126
+ const firstAmount = amounts[firstCurrency];
127
+ const moreCount = currencies.length - 1;
128
+
129
+ return html`
130
+ <div class="body">
131
+ ${firstCurrency}
132
+ ${firstAmount}${moreCount > 0
133
+ ? html` <span style="color: #999;">+${moreCount} more</span>`
134
+ : ''}
135
+ </div>
136
+ `;
137
+ },
138
+ toFormData: (node: Node) => {
139
+ // Extract data from the existing node structure
140
+ const transferAirtimeAction = node.actions?.find(
141
+ (action) => action.type === 'transfer_airtime'
142
+ ) as TransferAirtime;
143
+
144
+ const amounts: any[] = [];
145
+ if (transferAirtimeAction && transferAirtimeAction.amounts) {
146
+ Object.entries(transferAirtimeAction.amounts).forEach(
147
+ ([currency, amount]) => {
148
+ amounts.push({
149
+ currency: [
150
+ {
151
+ value: currency,
152
+ name: CURRENCIES[currency]?.name
153
+ ? `${CURRENCIES[currency].name} (${currency})`
154
+ : currency
155
+ }
156
+ ],
157
+ amount: String(amount)
158
+ });
159
+ }
160
+ );
161
+ }
162
+
163
+ return {
164
+ uuid: node.uuid,
165
+ amounts: amounts,
166
+ result_name: node.router?.result_name || ''
167
+ };
168
+ },
169
+ fromFormData: (formData: FormData, originalNode: Node): Node => {
170
+ // Get user amounts and convert to amounts object
171
+ const amountsObject: Record<string, number> = {};
172
+
173
+ if (formData.amounts && Array.isArray(formData.amounts)) {
174
+ formData.amounts.forEach((item: any) => {
175
+ if (!item?.currency || !item?.amount || item.amount.trim() === '') {
176
+ return;
177
+ }
178
+
179
+ // Extract currency code from selection (handle both array and object formats)
180
+ let currencyCode: string;
181
+ if (Array.isArray(item.currency) && item.currency.length > 0) {
182
+ currencyCode = item.currency[0].value;
183
+ } else if (typeof item.currency === 'string') {
184
+ currencyCode = item.currency;
185
+ } else if (item.currency?.value) {
186
+ currencyCode = item.currency.value;
187
+ } else {
188
+ return;
189
+ }
190
+
191
+ const amount = parseFloat(item.amount.trim());
192
+ if (!isNaN(amount) && amount > 0) {
193
+ amountsObject[currencyCode] = amount;
194
+ }
195
+ });
196
+ }
197
+
198
+ // Find existing transfer_airtime action to preserve its UUID
199
+ const existingTransferAirtimeAction = originalNode.actions?.find(
200
+ (action) => action.type === 'transfer_airtime'
201
+ );
202
+ const transferAirtimeUuid =
203
+ existingTransferAirtimeAction?.uuid || generateUUID();
204
+
205
+ // Create transfer_airtime action
206
+ const transferAirtimeAction: TransferAirtime = {
207
+ type: 'transfer_airtime',
208
+ uuid: transferAirtimeUuid,
209
+ amounts: amountsObject
210
+ };
211
+
212
+ // Create categories and exits for Success and Failure
213
+ const existingCategories = originalNode.router?.categories || [];
214
+ const existingExits = originalNode.exits || [];
215
+ const existingCases = originalNode.router?.cases || [];
216
+
217
+ const { router, exits } = createSuccessFailureRouter(
218
+ '@locals._new_transfer',
219
+ {
220
+ type: 'has_text',
221
+ arguments: []
222
+ },
223
+ existingCategories,
224
+ existingExits,
225
+ existingCases
226
+ );
227
+
228
+ // Add result_name if provided
229
+ const finalRouter: any = { ...router };
230
+ if (formData.result_name && formData.result_name.trim() !== '') {
231
+ finalRouter.result_name = formData.result_name.trim();
232
+ }
233
+
234
+ // Return the complete node
235
+ return {
236
+ uuid: originalNode.uuid,
237
+ actions: [transferAirtimeAction],
238
+ router: finalRouter,
239
+ exits: exits
240
+ };
241
+ }
9
242
  };