@nyaruka/temba-components 0.131.1 → 0.131.3

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 (485) hide show
  1. package/.github/workflows/publish.yml +4 -1
  2. package/CHANGELOG.md +75 -1
  3. package/demo/components/floating-tabs/example.html +400 -0
  4. package/demo/components/flow/index.html +1 -1
  5. package/demo/data/flows/food-order.json +2 -2
  6. package/demo/data/flows/sample-flow.json +113 -125
  7. package/demo/data/flows/voicemail.json +613 -0
  8. package/demo/index.html +6 -0
  9. package/dist/locales/es.js +5 -5
  10. package/dist/locales/es.js.map +1 -1
  11. package/dist/locales/fr.js +5 -5
  12. package/dist/locales/fr.js.map +1 -1
  13. package/dist/locales/locale-codes.js +11 -2
  14. package/dist/locales/locale-codes.js.map +1 -1
  15. package/dist/locales/pt.js +5 -5
  16. package/dist/locales/pt.js.map +1 -1
  17. package/dist/static/svg/index.svg +1 -1
  18. package/dist/temba-components.js +1773 -662
  19. package/dist/temba-components.js.map +1 -1
  20. package/out-tsc/src/Icons.js +4 -1
  21. package/out-tsc/src/Icons.js.map +1 -1
  22. package/out-tsc/src/display/FloatingTab.js +167 -0
  23. package/out-tsc/src/display/FloatingTab.js.map +1 -0
  24. package/out-tsc/src/display/ProgressBar.js +22 -2
  25. package/out-tsc/src/display/ProgressBar.js.map +1 -1
  26. package/out-tsc/src/events.js.map +1 -1
  27. package/out-tsc/src/flow/CanvasMenu.js +200 -0
  28. package/out-tsc/src/flow/CanvasMenu.js.map +1 -0
  29. package/out-tsc/src/flow/CanvasNode.js +489 -47
  30. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  31. package/out-tsc/src/flow/Editor.js +1417 -67
  32. package/out-tsc/src/flow/Editor.js.map +1 -1
  33. package/out-tsc/src/flow/NodeEditor.js +479 -112
  34. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  35. package/out-tsc/src/flow/NodeTypeSelector.js +540 -0
  36. package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -0
  37. package/out-tsc/src/flow/StickyNote.js +12 -3
  38. package/out-tsc/src/flow/StickyNote.js.map +1 -1
  39. package/out-tsc/src/flow/actions/add_contact_groups.js +4 -3
  40. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
  41. package/out-tsc/src/flow/actions/add_contact_urn.js +63 -4
  42. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  43. package/out-tsc/src/flow/actions/add_input_labels.js +4 -3
  44. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  45. package/out-tsc/src/flow/actions/play_audio.js +3 -2
  46. package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
  47. package/out-tsc/src/flow/actions/remove_contact_groups.js +7 -5
  48. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
  49. package/out-tsc/src/flow/actions/request_optin.js +3 -2
  50. package/out-tsc/src/flow/actions/request_optin.js.map +1 -1
  51. package/out-tsc/src/flow/actions/say_msg.js +3 -2
  52. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  53. package/out-tsc/src/flow/actions/send_broadcast.js +77 -23
  54. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  55. package/out-tsc/src/flow/actions/send_email.js +5 -5
  56. package/out-tsc/src/flow/actions/send_email.js.map +1 -1
  57. package/out-tsc/src/flow/actions/send_msg.js +101 -21
  58. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  59. package/out-tsc/src/flow/actions/set_contact_channel.js +6 -9
  60. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  61. package/out-tsc/src/flow/actions/set_contact_field.js +20 -20
  62. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  63. package/out-tsc/src/flow/actions/set_contact_language.js +3 -2
  64. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  65. package/out-tsc/src/flow/actions/set_contact_name.js +3 -12
  66. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  67. package/out-tsc/src/flow/actions/set_contact_status.js +3 -2
  68. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  69. package/out-tsc/src/flow/actions/set_run_result.js +4 -3
  70. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  71. package/out-tsc/src/flow/actions/start_session.js +181 -6
  72. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  73. package/out-tsc/src/flow/config.js +11 -23
  74. package/out-tsc/src/flow/config.js.map +1 -1
  75. package/out-tsc/src/flow/currencies.js +45 -0
  76. package/out-tsc/src/flow/currencies.js.map +1 -0
  77. package/out-tsc/src/flow/nodes/shared-rules.js +257 -0
  78. package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -0
  79. package/out-tsc/src/flow/nodes/shared.js +71 -0
  80. package/out-tsc/src/flow/nodes/shared.js.map +1 -0
  81. package/out-tsc/src/flow/nodes/split_by_airtime.js +211 -5
  82. package/out-tsc/src/flow/nodes/split_by_airtime.js.map +1 -1
  83. package/out-tsc/src/flow/nodes/split_by_contact_field.js +152 -3
  84. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -1
  85. package/out-tsc/src/flow/nodes/split_by_expression.js +73 -2
  86. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -1
  87. package/out-tsc/src/flow/nodes/split_by_groups.js +18 -10
  88. package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -1
  89. package/out-tsc/src/flow/nodes/split_by_intent.js +8 -0
  90. package/out-tsc/src/flow/nodes/split_by_intent.js.map +1 -0
  91. package/out-tsc/src/flow/nodes/split_by_llm.js +11 -3
  92. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
  93. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +10 -3
  94. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  95. package/out-tsc/src/flow/nodes/split_by_random.js +10 -4
  96. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
  97. package/out-tsc/src/flow/nodes/split_by_resthook.js +113 -0
  98. package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -0
  99. package/out-tsc/src/flow/nodes/split_by_run_result.js +211 -3
  100. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
  101. package/out-tsc/src/flow/nodes/split_by_scheme.js +158 -2
  102. package/out-tsc/src/flow/nodes/split_by_scheme.js.map +1 -1
  103. package/out-tsc/src/flow/nodes/split_by_subflow.js +13 -5
  104. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  105. package/out-tsc/src/flow/nodes/split_by_ticket.js +10 -3
  106. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -1
  107. package/out-tsc/src/flow/nodes/split_by_webhook.js +10 -3
  108. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
  109. package/out-tsc/src/flow/nodes/wait_for_digits.js +3 -2
  110. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
  111. package/out-tsc/src/flow/nodes/wait_for_menu.js +3 -2
  112. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
  113. package/out-tsc/src/flow/nodes/wait_for_response.js +38 -568
  114. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  115. package/out-tsc/src/flow/types.js +86 -12
  116. package/out-tsc/src/flow/types.js.map +1 -1
  117. package/out-tsc/src/flow/utils.js +101 -14
  118. package/out-tsc/src/flow/utils.js.map +1 -1
  119. package/out-tsc/src/form/FieldRenderer.js +2 -4
  120. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  121. package/out-tsc/src/interfaces.js +3 -0
  122. package/out-tsc/src/interfaces.js.map +1 -1
  123. package/out-tsc/src/layout/FloatingWindow.js +346 -0
  124. package/out-tsc/src/layout/FloatingWindow.js.map +1 -0
  125. package/out-tsc/src/list/SortableList.js +98 -33
  126. package/out-tsc/src/list/SortableList.js.map +1 -1
  127. package/out-tsc/src/live/ContactChat.js +6 -25
  128. package/out-tsc/src/live/ContactChat.js.map +1 -1
  129. package/out-tsc/src/locales/es.js +5 -5
  130. package/out-tsc/src/locales/es.js.map +1 -1
  131. package/out-tsc/src/locales/fr.js +5 -5
  132. package/out-tsc/src/locales/fr.js.map +1 -1
  133. package/out-tsc/src/locales/locale-codes.js +11 -2
  134. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  135. package/out-tsc/src/locales/pt.js +5 -5
  136. package/out-tsc/src/locales/pt.js.map +1 -1
  137. package/out-tsc/src/store/AppState.js +120 -0
  138. package/out-tsc/src/store/AppState.js.map +1 -1
  139. package/out-tsc/src/utils.js +254 -13
  140. package/out-tsc/src/utils.js.map +1 -1
  141. package/out-tsc/temba-modules.js +8 -0
  142. package/out-tsc/temba-modules.js.map +1 -1
  143. package/out-tsc/test/ActionHelper.js +3 -3
  144. package/out-tsc/test/ActionHelper.js.map +1 -1
  145. package/out-tsc/test/NodeHelper.js +6 -3
  146. package/out-tsc/test/NodeHelper.js.map +1 -1
  147. package/out-tsc/test/actions/add_contact_urn.test.js +202 -0
  148. package/out-tsc/test/actions/add_contact_urn.test.js.map +1 -0
  149. package/out-tsc/test/actions/send_broadcast.test.js +148 -0
  150. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -0
  151. package/out-tsc/test/actions/send_email.test.js +17 -23
  152. package/out-tsc/test/actions/send_email.test.js.map +1 -1
  153. package/out-tsc/test/actions/send_msg.test.js +33 -15
  154. package/out-tsc/test/actions/send_msg.test.js.map +1 -1
  155. package/out-tsc/test/actions/start_session.test.js +116 -0
  156. package/out-tsc/test/actions/start_session.test.js.map +1 -0
  157. package/out-tsc/test/nodes/split_by_airtime.test.js +604 -0
  158. package/out-tsc/test/nodes/split_by_airtime.test.js.map +1 -0
  159. package/out-tsc/test/nodes/split_by_contact_field.test.js +387 -0
  160. package/out-tsc/test/nodes/split_by_contact_field.test.js.map +1 -0
  161. package/out-tsc/test/nodes/split_by_expression.test.js +614 -0
  162. package/out-tsc/test/nodes/split_by_expression.test.js.map +1 -0
  163. package/out-tsc/test/nodes/split_by_random.test.js +3 -3
  164. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -1
  165. package/out-tsc/test/nodes/split_by_resthook.test.js +337 -0
  166. package/out-tsc/test/nodes/split_by_resthook.test.js.map +1 -0
  167. package/out-tsc/test/nodes/split_by_run_result.test.js +920 -0
  168. package/out-tsc/test/nodes/split_by_run_result.test.js.map +1 -0
  169. package/out-tsc/test/nodes/split_by_scheme.test.js +399 -0
  170. package/out-tsc/test/nodes/split_by_scheme.test.js.map +1 -0
  171. package/out-tsc/test/nodes/split_by_subflow.test.js +333 -0
  172. package/out-tsc/test/nodes/split_by_subflow.test.js.map +1 -0
  173. package/out-tsc/test/nodes/wait_for_digits.test.js +2 -2
  174. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  175. package/out-tsc/test/nodes/wait_for_response.test.js +2 -1
  176. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -1
  177. package/out-tsc/test/temba-action-drag-between-nodes.test.js +252 -0
  178. package/out-tsc/test/temba-action-drag-between-nodes.test.js.map +1 -0
  179. package/out-tsc/test/temba-canvas-menu.test.js +122 -0
  180. package/out-tsc/test/temba-canvas-menu.test.js.map +1 -0
  181. package/out-tsc/test/temba-floating-tab.test.js +91 -0
  182. package/out-tsc/test/temba-floating-tab.test.js.map +1 -0
  183. package/out-tsc/test/temba-floating-window.test.js +301 -0
  184. package/out-tsc/test/temba-floating-window.test.js.map +1 -0
  185. package/out-tsc/test/temba-flow-editor-node.test.js +202 -2
  186. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  187. package/out-tsc/test/temba-flow-editor.test.js +7 -8
  188. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  189. package/out-tsc/test/temba-localization.test.js +471 -0
  190. package/out-tsc/test/temba-localization.test.js.map +1 -0
  191. package/out-tsc/test/temba-node-editor.test.js +3 -1
  192. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  193. package/out-tsc/test/temba-node-type-selector.test.js +265 -0
  194. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -0
  195. package/out-tsc/test/temba-omnibox.test.js +2 -1
  196. package/out-tsc/test/temba-omnibox.test.js.map +1 -1
  197. package/out-tsc/test/temba-sortable-list.test.js +51 -0
  198. package/out-tsc/test/temba-sortable-list.test.js.map +1 -1
  199. package/out-tsc/test/temba-utils-index.test.js +1 -27
  200. package/out-tsc/test/temba-utils-index.test.js.map +1 -1
  201. package/out-tsc/test/utils.test.js +20 -0
  202. package/out-tsc/test/utils.test.js.map +1 -1
  203. package/package.json +2 -1
  204. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  205. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  206. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  207. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  208. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  209. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  210. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  211. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  212. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  213. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  214. package/screenshots/truth/actions/add_contact_urn/editor/expression-facebook.png +0 -0
  215. package/screenshots/truth/actions/add_contact_urn/editor/expression-phone.png +0 -0
  216. package/screenshots/truth/actions/add_contact_urn/editor/facebook-id.png +0 -0
  217. package/screenshots/truth/actions/add_contact_urn/editor/instagram-handle.png +0 -0
  218. package/screenshots/truth/actions/add_contact_urn/editor/line-id.png +0 -0
  219. package/screenshots/truth/actions/add_contact_urn/editor/phone-number.png +0 -0
  220. package/screenshots/truth/actions/add_contact_urn/editor/telegram-id.png +0 -0
  221. package/screenshots/truth/actions/add_contact_urn/editor/viber-id.png +0 -0
  222. package/screenshots/truth/actions/add_contact_urn/editor/wechat-id.png +0 -0
  223. package/screenshots/truth/actions/add_contact_urn/editor/whatsapp.png +0 -0
  224. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  225. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  226. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  227. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  228. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  229. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  230. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  231. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  232. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  233. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  234. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  235. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  236. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  237. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  238. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  239. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  240. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  241. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  242. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  243. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  244. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  245. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  246. package/screenshots/truth/actions/send_broadcast/editor/contacts-only.png +0 -0
  247. package/screenshots/truth/actions/send_broadcast/editor/groups-and-contacts.png +0 -0
  248. package/screenshots/truth/actions/send_broadcast/editor/groups-only.png +0 -0
  249. package/screenshots/truth/actions/send_broadcast/editor/many-groups.png +0 -0
  250. package/screenshots/truth/actions/send_broadcast/editor/multiline-text.png +0 -0
  251. package/screenshots/truth/actions/send_broadcast/editor/with-attachments.png +0 -0
  252. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  253. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  254. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  255. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  256. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  257. package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
  258. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  259. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  260. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  261. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  262. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  263. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  264. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  265. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  266. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  267. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  268. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  269. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  270. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  271. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  272. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  273. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  274. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  275. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  276. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  277. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  278. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  279. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  280. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  281. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  282. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  283. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  284. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  285. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  286. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  287. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  288. package/screenshots/truth/actions/start_session/editor/contact-query.png +0 -0
  289. package/screenshots/truth/actions/start_session/editor/contacts-only.png +0 -0
  290. package/screenshots/truth/actions/start_session/editor/create-contact.png +0 -0
  291. package/screenshots/truth/actions/start_session/editor/groups-and-contacts.png +0 -0
  292. package/screenshots/truth/actions/start_session/editor/groups-only.png +0 -0
  293. package/screenshots/truth/actions/start_session/editor/many-recipients.png +0 -0
  294. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  295. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  296. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  297. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  298. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  299. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  300. package/screenshots/truth/canvas-menu/open.png +0 -0
  301. package/screenshots/truth/editor/router.png +0 -0
  302. package/screenshots/truth/editor/wait.png +0 -0
  303. package/screenshots/truth/floating-tab/default.png +0 -0
  304. package/screenshots/truth/floating-tab/gray.png +0 -0
  305. package/screenshots/truth/floating-tab/green.png +0 -0
  306. package/screenshots/truth/floating-tab/hidden.png +0 -0
  307. package/screenshots/truth/floating-tab/hover.png +0 -0
  308. package/screenshots/truth/floating-tab/purple.png +0 -0
  309. package/screenshots/truth/floating-window/chromeless.png +0 -0
  310. package/screenshots/truth/floating-window/custom-size.png +0 -0
  311. package/screenshots/truth/floating-window/default.png +0 -0
  312. package/screenshots/truth/floating-window/with-header.png +0 -0
  313. package/screenshots/truth/list/fields-dragging.png +0 -0
  314. package/screenshots/truth/list/sortable-dragging.png +0 -0
  315. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  316. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  317. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  318. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  319. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  320. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  321. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  322. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  323. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  324. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  325. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  326. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  327. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  328. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  329. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  330. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  331. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  332. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  333. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  334. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  335. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  336. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  337. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  338. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  339. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  340. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  341. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  342. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  343. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  344. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  345. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  346. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  347. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  348. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  349. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  350. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  351. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  352. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  353. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  354. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  355. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  356. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  357. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  358. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  359. package/src/Icons.ts +4 -1
  360. package/src/display/FloatingTab.ts +174 -0
  361. package/src/display/ProgressBar.ts +22 -2
  362. package/src/events.ts +2 -8
  363. package/src/flow/CanvasMenu.ts +217 -0
  364. package/src/flow/CanvasNode.ts +596 -40
  365. package/src/flow/Editor.ts +1721 -45
  366. package/src/flow/NodeEditor.ts +621 -144
  367. package/src/flow/NodeTypeSelector.ts +636 -0
  368. package/src/flow/StickyNote.ts +12 -3
  369. package/src/flow/actions/add_contact_groups.ts +5 -4
  370. package/src/flow/actions/add_contact_urn.ts +78 -4
  371. package/src/flow/actions/add_input_labels.ts +5 -4
  372. package/src/flow/actions/play_audio.ts +3 -2
  373. package/src/flow/actions/remove_contact_groups.ts +16 -6
  374. package/src/flow/actions/request_optin.ts +3 -2
  375. package/src/flow/actions/say_msg.ts +3 -2
  376. package/src/flow/actions/send_broadcast.ts +86 -23
  377. package/src/flow/actions/send_email.ts +12 -6
  378. package/src/flow/actions/send_msg.ts +155 -34
  379. package/src/flow/actions/set_contact_channel.ts +6 -11
  380. package/src/flow/actions/set_contact_field.ts +21 -25
  381. package/src/flow/actions/set_contact_language.ts +11 -4
  382. package/src/flow/actions/set_contact_name.ts +4 -15
  383. package/src/flow/actions/set_contact_status.ts +4 -3
  384. package/src/flow/actions/set_run_result.ts +5 -4
  385. package/src/flow/actions/start_session.ts +210 -6
  386. package/src/flow/config.ts +11 -23
  387. package/src/flow/currencies.ts +51 -0
  388. package/src/flow/nodes/shared-rules.ts +301 -0
  389. package/src/flow/nodes/shared.ts +87 -0
  390. package/src/flow/nodes/split_by_airtime.ts +255 -5
  391. package/src/flow/nodes/split_by_contact_field.ts +195 -3
  392. package/src/flow/nodes/split_by_expression.ts +104 -2
  393. package/src/flow/nodes/split_by_groups.ts +26 -11
  394. package/src/flow/nodes/split_by_intent.ts +8 -0
  395. package/src/flow/nodes/split_by_llm.ts +22 -4
  396. package/src/flow/nodes/split_by_llm_categorize.ts +22 -5
  397. package/src/flow/nodes/split_by_random.ts +16 -6
  398. package/src/flow/nodes/split_by_resthook.ts +140 -0
  399. package/src/flow/nodes/split_by_run_result.ts +259 -3
  400. package/src/flow/nodes/split_by_scheme.ts +202 -2
  401. package/src/flow/nodes/split_by_subflow.ts +17 -5
  402. package/src/flow/nodes/split_by_ticket.ts +15 -4
  403. package/src/flow/nodes/split_by_webhook.ts +17 -6
  404. package/src/flow/nodes/wait_for_digits.ts +3 -2
  405. package/src/flow/nodes/wait_for_menu.ts +3 -2
  406. package/src/flow/nodes/wait_for_response.ts +59 -680
  407. package/src/flow/types.ts +156 -23
  408. package/src/flow/utils.ts +108 -14
  409. package/src/form/FieldRenderer.ts +2 -4
  410. package/src/interfaces.ts +3 -0
  411. package/src/layout/FloatingWindow.ts +386 -0
  412. package/src/list/SortableList.ts +109 -34
  413. package/src/live/ContactChat.ts +7 -25
  414. package/src/locales/es.ts +18 -13
  415. package/src/locales/fr.ts +18 -13
  416. package/src/locales/locale-codes.ts +11 -2
  417. package/src/locales/pt.ts +18 -13
  418. package/src/store/AppState.ts +173 -0
  419. package/src/store/flow-definition.d.ts +2 -5
  420. package/src/utils.ts +332 -12
  421. package/static/api/channels.json +46 -0
  422. package/static/api/llms.json +18 -0
  423. package/static/api/resthooks.json +31 -0
  424. package/static/svg/index.svg +1 -1
  425. package/static/svg/work/traced/lightning-02.svg +1 -0
  426. package/static/svg/work/used/lightning-02.svg +3 -0
  427. package/temba-modules.ts +8 -0
  428. package/test/ActionHelper.ts +3 -3
  429. package/test/NodeHelper.ts +6 -3
  430. package/test/actions/add_contact_urn.test.ts +287 -0
  431. package/test/actions/send_broadcast.test.ts +190 -0
  432. package/test/actions/send_email.test.ts +17 -23
  433. package/test/actions/send_msg.test.ts +39 -15
  434. package/test/actions/start_session.test.ts +151 -0
  435. package/test/nodes/split_by_airtime.test.ts +673 -0
  436. package/test/nodes/split_by_contact_field.test.ts +451 -0
  437. package/test/nodes/split_by_expression.test.ts +751 -0
  438. package/test/nodes/split_by_random.test.ts +3 -3
  439. package/test/nodes/split_by_resthook.test.ts +398 -0
  440. package/test/nodes/split_by_run_result.test.ts +1109 -0
  441. package/test/nodes/split_by_scheme.test.ts +486 -0
  442. package/test/nodes/split_by_subflow.test.ts +381 -0
  443. package/test/nodes/wait_for_digits.test.ts +2 -2
  444. package/test/nodes/wait_for_response.test.ts +2 -1
  445. package/test/temba-action-drag-between-nodes.test.ts +301 -0
  446. package/test/temba-canvas-menu.test.ts +156 -0
  447. package/test/temba-floating-tab.test.ts +110 -0
  448. package/test/temba-floating-window.test.ts +477 -0
  449. package/test/temba-flow-editor-node.test.ts +246 -2
  450. package/test/temba-flow-editor.test.ts +7 -8
  451. package/test/temba-localization.test.ts +611 -0
  452. package/test/temba-node-editor.test.ts +3 -1
  453. package/test/temba-node-type-selector.test.ts +355 -0
  454. package/test/temba-omnibox.test.ts +2 -1
  455. package/test/temba-sortable-list.test.ts +69 -0
  456. package/test/temba-utils-index.test.ts +0 -35
  457. package/test/utils.test.ts +22 -0
  458. package/test-assets/contacts/history.json +14 -21
  459. package/test-assets/select/llms.json +2 -2
  460. package/web-dev-server.config.mjs +49 -1
  461. package/web-test-runner.config.mjs +0 -1
  462. package/out-tsc/src/flow/actions/call_classifier.js +0 -11
  463. package/out-tsc/src/flow/actions/call_classifier.js.map +0 -1
  464. package/out-tsc/src/flow/actions/call_resthook.js +0 -11
  465. package/out-tsc/src/flow/actions/call_resthook.js.map +0 -1
  466. package/out-tsc/src/flow/actions/split_by_expression_example.js +0 -77
  467. package/out-tsc/src/flow/actions/split_by_expression_example.js.map +0 -1
  468. package/out-tsc/src/flow/actions/transfer_airtime.js +0 -11
  469. package/out-tsc/src/flow/actions/transfer_airtime.js.map +0 -1
  470. package/out-tsc/src/flow/nodes/wait_for_audio.js +0 -7
  471. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +0 -1
  472. package/out-tsc/src/flow/nodes/wait_for_image.js +0 -7
  473. package/out-tsc/src/flow/nodes/wait_for_image.js.map +0 -1
  474. package/out-tsc/src/flow/nodes/wait_for_location.js +0 -7
  475. package/out-tsc/src/flow/nodes/wait_for_location.js.map +0 -1
  476. package/out-tsc/src/flow/nodes/wait_for_video.js +0 -7
  477. package/out-tsc/src/flow/nodes/wait_for_video.js.map +0 -1
  478. package/src/flow/actions/call_classifier.ts +0 -12
  479. package/src/flow/actions/call_resthook.ts +0 -12
  480. package/src/flow/actions/split_by_expression_example.ts +0 -88
  481. package/src/flow/actions/transfer_airtime.ts +0 -12
  482. package/src/flow/nodes/wait_for_audio.ts +0 -7
  483. package/src/flow/nodes/wait_for_image.ts +0 -7
  484. package/src/flow/nodes/wait_for_location.ts +0 -7
  485. package/src/flow/nodes/wait_for_video.ts +0 -7
@@ -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,87 @@
1
+ import { FormData, TextFieldConfig } from '../types';
2
+ import { Node } from '../../store/flow-definition';
3
+
4
+ /**
5
+ * Shared result_name field configuration for router nodes.
6
+ * This provides a consistent "Save as..." optional field interface across all splits.
7
+ *
8
+ * The field is hidden by default and revealed via a "Save as..." link.
9
+ * Once revealed, it cannot be hidden again (the link disappears).
10
+ * If the field already has a value, it's shown immediately without the link.
11
+ */
12
+ export const resultNameField: TextFieldConfig = {
13
+ type: 'text',
14
+ label: 'Result Name',
15
+ required: false,
16
+ placeholder: '(optional)',
17
+ helpText: 'The name to use to reference this result in the flow',
18
+ optionalLink: 'Save result as...'
19
+ };
20
+
21
+ /**
22
+ * Shared category localization functions for router nodes.
23
+ * These provide a consistent way to localize category names across all router types.
24
+ */
25
+
26
+ /**
27
+ * Converts a node's categories to localization form data.
28
+ * @param node - The node containing categories to localize
29
+ * @param localization - The existing localization data for this language
30
+ * @returns Form data with category UUIDs mapped to original and localized names
31
+ */
32
+ export function categoriesToLocalizationFormData(
33
+ node: Node,
34
+ localization: Record<string, any>
35
+ ): FormData {
36
+ const categories = node.router?.categories || [];
37
+ const localizationData: Record<string, any> = {};
38
+
39
+ categories.forEach((category: any) => {
40
+ const categoryUuid = category.uuid;
41
+ const categoryLocalization = localization[categoryUuid];
42
+
43
+ localizationData[categoryUuid] = {
44
+ originalName: category.name,
45
+ localizedName:
46
+ categoryLocalization && categoryLocalization.name
47
+ ? Array.isArray(categoryLocalization.name)
48
+ ? categoryLocalization.name[0] || ''
49
+ : categoryLocalization.name
50
+ : ''
51
+ };
52
+ });
53
+
54
+ return {
55
+ categories: localizationData
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Converts localization form data back to the localization structure.
61
+ * @param formData - The form data containing category localizations
62
+ * @param _node - The original node (reserved for future validation)
63
+ * @returns Record mapping category UUIDs to their localization data
64
+ */
65
+ export function localizationFormDataToCategories(
66
+ formData: FormData,
67
+ _node: Node
68
+ ): Record<string, any> {
69
+ const localizationData: Record<string, any> = {};
70
+
71
+ if (formData.categories) {
72
+ Object.keys(formData.categories).forEach((categoryUuid) => {
73
+ const categoryData = formData.categories[categoryUuid];
74
+ const localizedName = categoryData.localizedName?.trim() || '';
75
+ const originalName = categoryData.originalName?.trim() || '';
76
+
77
+ // Only save if localized name is different from original and not empty
78
+ if (localizedName && localizedName !== originalName) {
79
+ localizationData[categoryUuid] = {
80
+ name: [localizedName]
81
+ };
82
+ }
83
+ });
84
+ }
85
+
86
+ return localizationData;
87
+ }
@@ -1,9 +1,259 @@
1
- import { transfer_airtime } from '../actions/transfer_airtime';
2
- import { COLORS, NodeConfig } from '../types';
1
+ import {
2
+ ACTION_GROUPS,
3
+ FormData,
4
+ NodeConfig,
5
+ FlowTypes,
6
+ Features
7
+ } from '../types';
8
+ import { TransferAirtime, Node } from '../../store/flow-definition';
9
+ import { generateUUID, createSuccessFailureRouter } from '../../utils';
10
+ import { html } from 'lit';
11
+ import { CURRENCY_OPTIONS, CURRENCIES } from '../currencies';
12
+ import {
13
+ resultNameField,
14
+ categoriesToLocalizationFormData,
15
+ localizationFormDataToCategories
16
+ } from './shared';
3
17
 
4
18
  export const split_by_airtime: NodeConfig = {
5
19
  type: 'split_by_airtime',
6
- name: 'Split by Airtime Transfer',
7
- color: COLORS.send,
8
- action: transfer_airtime
20
+ name: 'Send Airtime',
21
+ group: ACTION_GROUPS.services,
22
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
23
+ features: [Features.AIRTIME],
24
+ showAsAction: true,
25
+ form: {
26
+ amounts: {
27
+ type: 'array',
28
+ label: 'Airtime Amounts',
29
+ helpText: 'Define the currencies and amounts to transfer',
30
+ required: true,
31
+ itemLabel: 'Amount',
32
+ sortable: false,
33
+ minItems: 1,
34
+ maxItems: 10,
35
+ isEmptyItem: (item: any) => {
36
+ return !item.currency || !item.amount || item.amount.trim() === '';
37
+ },
38
+ itemConfig: {
39
+ currency: {
40
+ type: 'select',
41
+ placeholder: 'Select a currency',
42
+ required: true,
43
+ options: CURRENCY_OPTIONS,
44
+ searchable: true,
45
+ multi: false,
46
+ width: '200px'
47
+ },
48
+ amount: {
49
+ type: 'text',
50
+ placeholder: 'Amount',
51
+ required: true
52
+ }
53
+ }
54
+ },
55
+ result_name: resultNameField
56
+ },
57
+ layout: ['amounts', 'result_name'],
58
+ validate: (formData: FormData) => {
59
+ const errors: { [key: string]: string } = {};
60
+
61
+ // Validate that we have at least one amount
62
+ if (formData.amounts && Array.isArray(formData.amounts)) {
63
+ const validAmounts = formData.amounts.filter(
64
+ (item: any) =>
65
+ item?.currency && item?.amount && item.amount.trim() !== ''
66
+ );
67
+
68
+ if (validAmounts.length === 0) {
69
+ errors.amounts = 'At least one currency and amount is required';
70
+ return {
71
+ valid: false,
72
+ errors
73
+ };
74
+ }
75
+
76
+ // Check for duplicate currencies
77
+ const currencies = new Set();
78
+ const duplicates: string[] = [];
79
+
80
+ validAmounts.forEach((item: any) => {
81
+ // Extract currency code from selection
82
+ const currencyCode =
83
+ Array.isArray(item.currency) && item.currency.length > 0
84
+ ? item.currency[0].value
85
+ : typeof item.currency === 'string'
86
+ ? item.currency
87
+ : item.currency?.value;
88
+
89
+ if (currencies.has(currencyCode)) {
90
+ duplicates.push(currencyCode);
91
+ } else {
92
+ currencies.add(currencyCode);
93
+ }
94
+ });
95
+
96
+ if (duplicates.length > 0) {
97
+ errors.amounts = `Duplicate currencies found: ${duplicates.join(', ')}`;
98
+ }
99
+
100
+ // Validate amounts are numeric
101
+ for (const item of validAmounts) {
102
+ const amount = item.amount.trim();
103
+ if (isNaN(Number(amount)) || Number(amount) <= 0) {
104
+ errors.amounts = 'All amounts must be valid positive numbers';
105
+ return {
106
+ valid: false,
107
+ errors
108
+ };
109
+ }
110
+ }
111
+ } else {
112
+ errors.amounts = 'At least one currency and amount is required';
113
+ }
114
+
115
+ return {
116
+ valid: Object.keys(errors).length === 0,
117
+ errors
118
+ };
119
+ },
120
+ render: (node: Node) => {
121
+ const transferAirtimeAction = node.actions?.find(
122
+ (action) => action.type === 'transfer_airtime'
123
+ ) as TransferAirtime;
124
+
125
+ if (!transferAirtimeAction || !transferAirtimeAction.amounts) {
126
+ return html`<div class="body">Configure airtime transfer</div>`;
127
+ }
128
+
129
+ const amounts = transferAirtimeAction.amounts;
130
+ const currencies = Object.keys(amounts);
131
+
132
+ if (currencies.length === 0) {
133
+ return html`<div class="body">Configure airtime transfer</div>`;
134
+ }
135
+
136
+ // Display the first currency amount, with indicator if there are more
137
+ const firstCurrency = currencies[0];
138
+ const firstAmount = amounts[firstCurrency];
139
+ const moreCount = currencies.length - 1;
140
+
141
+ return html`
142
+ <div class="body">
143
+ ${firstCurrency}
144
+ ${firstAmount}${moreCount > 0
145
+ ? html` <span style="color: #999;">+${moreCount} more</span>`
146
+ : ''}
147
+ </div>
148
+ `;
149
+ },
150
+ toFormData: (node: Node) => {
151
+ // Extract data from the existing node structure
152
+ const transferAirtimeAction = node.actions?.find(
153
+ (action) => action.type === 'transfer_airtime'
154
+ ) as TransferAirtime;
155
+
156
+ const amounts: any[] = [];
157
+ if (transferAirtimeAction && transferAirtimeAction.amounts) {
158
+ Object.entries(transferAirtimeAction.amounts).forEach(
159
+ ([currency, amount]) => {
160
+ amounts.push({
161
+ currency: [
162
+ {
163
+ value: currency,
164
+ name: CURRENCIES[currency]?.name
165
+ ? `${CURRENCIES[currency].name} (${currency})`
166
+ : currency
167
+ }
168
+ ],
169
+ amount: String(amount)
170
+ });
171
+ }
172
+ );
173
+ }
174
+
175
+ return {
176
+ uuid: node.uuid,
177
+ amounts: amounts,
178
+ result_name: node.router?.result_name || ''
179
+ };
180
+ },
181
+ fromFormData: (formData: FormData, originalNode: Node): Node => {
182
+ // Get user amounts and convert to amounts object
183
+ const amountsObject: Record<string, number> = {};
184
+
185
+ if (formData.amounts && Array.isArray(formData.amounts)) {
186
+ formData.amounts.forEach((item: any) => {
187
+ if (!item?.currency || !item?.amount || item.amount.trim() === '') {
188
+ return;
189
+ }
190
+
191
+ // Extract currency code from selection (handle both array and object formats)
192
+ let currencyCode: string;
193
+ if (Array.isArray(item.currency) && item.currency.length > 0) {
194
+ currencyCode = item.currency[0].value;
195
+ } else if (typeof item.currency === 'string') {
196
+ currencyCode = item.currency;
197
+ } else if (item.currency?.value) {
198
+ currencyCode = item.currency.value;
199
+ } else {
200
+ return;
201
+ }
202
+
203
+ const amount = parseFloat(item.amount.trim());
204
+ if (!isNaN(amount) && amount > 0) {
205
+ amountsObject[currencyCode] = amount;
206
+ }
207
+ });
208
+ }
209
+
210
+ // Find existing transfer_airtime action to preserve its UUID
211
+ const existingTransferAirtimeAction = originalNode.actions?.find(
212
+ (action) => action.type === 'transfer_airtime'
213
+ );
214
+ const transferAirtimeUuid =
215
+ existingTransferAirtimeAction?.uuid || generateUUID();
216
+
217
+ // Create transfer_airtime action
218
+ const transferAirtimeAction: TransferAirtime = {
219
+ type: 'transfer_airtime',
220
+ uuid: transferAirtimeUuid,
221
+ amounts: amountsObject
222
+ };
223
+
224
+ // Create categories and exits for Success and Failure
225
+ const existingCategories = originalNode.router?.categories || [];
226
+ const existingExits = originalNode.exits || [];
227
+ const existingCases = originalNode.router?.cases || [];
228
+
229
+ const { router, exits } = createSuccessFailureRouter(
230
+ '@locals._new_transfer',
231
+ {
232
+ type: 'has_text',
233
+ arguments: []
234
+ },
235
+ existingCategories,
236
+ existingExits,
237
+ existingCases
238
+ );
239
+
240
+ // Add result_name if provided
241
+ const finalRouter: any = { ...router };
242
+ if (formData.result_name && formData.result_name.trim() !== '') {
243
+ finalRouter.result_name = formData.result_name.trim();
244
+ }
245
+
246
+ // Return the complete node
247
+ return {
248
+ uuid: originalNode.uuid,
249
+ actions: [transferAirtimeAction],
250
+ router: finalRouter,
251
+ exits: exits
252
+ };
253
+ },
254
+
255
+ // Localization support for categories
256
+ localizable: 'categories',
257
+ toLocalizationFormData: categoriesToLocalizationFormData,
258
+ fromLocalizationFormData: localizationFormDataToCategories
9
259
  };