@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
@@ -1,11 +1,21 @@
1
- import { COLORS, NodeConfig } from '../types';
1
+ import { SPLIT_GROUPS, FormData, NodeConfig, FlowTypes } from '../types';
2
2
  import { Node, Category, Exit, Case } from '../../store/flow-definition';
3
- import { generateUUID } from '../../utils';
3
+ import { generateUUID, createRulesRouter } from '../../utils';
4
4
  import {
5
5
  getWaitForResponseOperators,
6
6
  operatorsToSelectOptions,
7
7
  getOperatorConfig
8
8
  } from '../operators';
9
+ import {
10
+ resultNameField,
11
+ categoriesToLocalizationFormData,
12
+ localizationFormDataToCategories
13
+ } from './shared';
14
+ import {
15
+ createRulesArrayConfig,
16
+ extractUserRules,
17
+ casesToFormRules
18
+ } from './shared-rules';
9
19
 
10
20
  const TIMEOUT_OPTIONS = [
11
21
  { value: '60', name: '1 minute' },
@@ -28,318 +38,23 @@ const TIMEOUT_OPTIONS = [
28
38
  { value: '604800', name: '1 week' }
29
39
  ];
30
40
 
31
- // Helper function to check if a category is a system category
32
- const isSystemCategory = (categoryName: string): boolean => {
33
- return ['No Response', 'Other', 'All Responses', 'Timeout'].includes(
34
- categoryName
35
- );
36
- };
37
-
38
- // Helper function to check if a UUID belongs to a system category
39
- const isSystemCategoryUuid = (
40
- uuid: string,
41
- categories: Category[]
42
- ): boolean => {
43
- const category = categories.find((cat) => cat.uuid === uuid);
44
- return category ? isSystemCategory(category.name) : false;
45
- };
46
-
47
- // Helper function to generate default category name based on operator and operands
48
- const generateDefaultCategoryName = (
49
- operator: string,
50
- value1?: string,
51
- value2?: string
52
- ): string => {
53
- const operatorConfig = getOperatorConfig(operator);
54
- if (!operatorConfig) return '';
55
-
56
- // Fixed category names (no operands)
57
- if (operatorConfig.operands === 0) {
58
- return operatorConfig.categoryName || '';
59
- }
60
-
61
- // Dynamic category names based on operands
62
- const cleanValue1 = (value1 || '').trim();
63
- const cleanValue2 = (value2 || '').trim();
64
-
65
- // Helper to capitalize first letter
66
- const capitalize = (str: string) => {
67
- if (!str) return '';
68
- return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
69
- };
70
-
71
- // Handle different operator types
72
- switch (operator) {
73
- // Word/phrase operators - capitalize first letter of value
74
- case 'has_any_word':
75
- case 'has_all_words':
76
- case 'has_phrase':
77
- case 'has_only_phrase':
78
- case 'has_beginning':
79
- return cleanValue1 ? capitalize(cleanValue1) : '';
80
-
81
- // Pattern operators - show as-is
82
- case 'has_pattern':
83
- return cleanValue1;
84
-
85
- // Number comparison operators - include symbol
86
- case 'has_number_eq':
87
- return cleanValue1 ? `= ${cleanValue1}` : '';
88
- case 'has_number_lt':
89
- return cleanValue1 ? `< ${cleanValue1}` : '';
90
- case 'has_number_lte':
91
- return cleanValue1 ? `≤ ${cleanValue1}` : '';
92
- case 'has_number_gt':
93
- return cleanValue1 ? `> ${cleanValue1}` : '';
94
- case 'has_number_gte':
95
- return cleanValue1 ? `≥ ${cleanValue1}` : '';
96
-
97
- // Number between - range format
98
- case 'has_number_between':
99
- if (cleanValue1 && cleanValue2) {
100
- return `${cleanValue1} - ${cleanValue2}`;
101
- }
102
- return '';
103
-
104
- // Date operators - format with relative expressions
105
- case 'has_date_lt':
106
- case 'has_date_lte':
107
- if (cleanValue1) {
108
- // Parse relative date expression (e.g., "today + 5" or "today - 3")
109
- const match = cleanValue1.match(/^(today)\s*([+-])\s*(\d+)$/i);
110
- if (match) {
111
- const [, base, operator, days] = match;
112
- const dayWord = days === '1' ? 'day' : 'days';
113
- return `Before ${base} ${operator} ${days} ${dayWord}`;
114
- }
115
- // Fallback for other date formats
116
- return `Before ${cleanValue1}`;
117
- }
118
- return '';
119
-
120
- case 'has_date_gt':
121
- case 'has_date_gte':
122
- if (cleanValue1) {
123
- // Parse relative date expression
124
- const match = cleanValue1.match(/^(today)\s*([+-])\s*(\d+)$/i);
125
- if (match) {
126
- const [, base, operator, days] = match;
127
- const dayWord = days === '1' ? 'day' : 'days';
128
- return `After ${base} ${operator} ${days} ${dayWord}`;
129
- }
130
- // Fallback for other date formats
131
- return `After ${cleanValue1}`;
132
- }
133
- return '';
134
-
135
- case 'has_date_eq':
136
- if (cleanValue1) {
137
- // Parse relative date expression
138
- const match = cleanValue1.match(/^(today)\s*([+-])\s*(\d+)$/i);
139
- if (match) {
140
- const [, base, operator, days] = match;
141
- const dayWord = days === '1' ? 'day' : 'days';
142
- return `${base} ${operator} ${days} ${dayWord}`;
143
- }
144
- return cleanValue1;
145
- }
146
- return '';
147
-
148
- default:
149
- // Fallback - capitalize first value
150
- return cleanValue1 ? capitalize(cleanValue1) : '';
151
- }
152
- };
153
-
154
41
  // Helper function to create a wait_for_response router with user rules
42
+ // This is a thin wrapper around createRulesRouter that adds the No Response category for timeouts
155
43
  const createWaitForResponseRouter = (
156
44
  userRules: any[],
157
45
  existingCategories: Category[] = [],
158
46
  existingExits: Exit[] = [],
159
47
  existingCases: Case[] = []
160
48
  ) => {
161
- const categories: Category[] = [];
162
- const exits: Exit[] = [];
163
- const cases: Case[] = [];
164
-
165
- // Filter existing categories to get only user-defined rules (exclude system categories)
166
- const existingUserCategories = existingCategories.filter(
167
- (cat) => !isSystemCategory(cat.name)
49
+ const { router, exits } = createRulesRouter(
50
+ '@input.text',
51
+ userRules,
52
+ getOperatorConfig,
53
+ existingCategories,
54
+ existingExits,
55
+ existingCases
168
56
  );
169
57
 
170
- // Track categories as we create them (case-insensitive lookup)
171
- const createdCategories = new Map<
172
- string,
173
- { uuid: string; name: string; exit_uuid: string }
174
- >();
175
-
176
- // Process rules in their original order to preserve rule order
177
- userRules.forEach((rule, ruleIndex) => {
178
- const categoryKey = rule.category.trim().toLowerCase();
179
- const categoryName = rule.category.trim(); // Use original casing
180
-
181
- let categoryInfo = createdCategories.get(categoryKey);
182
-
183
- if (!categoryInfo) {
184
- // First time seeing this category - create it
185
-
186
- // Smart category matching: try by name first, then fall back to position
187
- let existingCategory = existingUserCategories.find(
188
- (cat) => cat.name.toLowerCase() === categoryKey
189
- );
190
-
191
- // If no match by name, try by position (for category rename scenarios)
192
- const categoryCreationOrder = Array.from(createdCategories.keys()).length;
193
- if (
194
- !existingCategory &&
195
- categoryCreationOrder < existingUserCategories.length
196
- ) {
197
- const candidateCategory = existingUserCategories[categoryCreationOrder];
198
- // Double-check that this candidate is not a system category UUID
199
- if (
200
- candidateCategory &&
201
- !isSystemCategoryUuid(candidateCategory.uuid, existingCategories)
202
- ) {
203
- existingCategory = candidateCategory;
204
- }
205
- }
206
-
207
- const existingExit = existingCategory
208
- ? existingExits.find((exit) => exit.uuid === existingCategory.exit_uuid)
209
- : null;
210
-
211
- // Generate UUIDs, ensuring we don't reuse system category UUIDs
212
- let exitUuid = existingExit?.uuid || generateUUID();
213
- let categoryUuid = existingCategory?.uuid || generateUUID();
214
-
215
- // Additional safety check: if somehow we got a system category UUID, generate new ones
216
- if (isSystemCategoryUuid(categoryUuid, existingCategories)) {
217
- categoryUuid = generateUUID();
218
- exitUuid = generateUUID();
219
- }
220
-
221
- categoryInfo = {
222
- uuid: categoryUuid,
223
- name: categoryName,
224
- exit_uuid: exitUuid
225
- };
226
-
227
- createdCategories.set(categoryKey, categoryInfo);
228
-
229
- // Add category and exit
230
- categories.push({
231
- uuid: categoryUuid,
232
- name: categoryName,
233
- exit_uuid: exitUuid
234
- });
235
-
236
- exits.push({
237
- uuid: exitUuid,
238
- destination_uuid: existingExit?.destination_uuid || null
239
- });
240
- }
241
-
242
- // Create case for this rule
243
- let existingCase = existingCases[ruleIndex];
244
-
245
- // If we can't find by position, try to find by matching rule content
246
- if (!existingCase && existingCases.length > 0) {
247
- existingCase = existingCases.find((case_) => {
248
- // Find the category for this case
249
- const caseCategory = existingCategories.find(
250
- (cat) => cat.uuid === case_.category_uuid
251
- );
252
-
253
- // Match by operator type and category name
254
- return (
255
- case_.type === rule.operator &&
256
- caseCategory?.name.toLowerCase() === categoryKey
257
- );
258
- });
259
- }
260
-
261
- const caseUuid = existingCase?.uuid || generateUUID();
262
-
263
- // Parse rule value based on operator configuration
264
- const operatorConfig = getOperatorConfig(rule.operator);
265
- let arguments_: string[] = [];
266
-
267
- if (operatorConfig) {
268
- if (operatorConfig.operands === 0) {
269
- // No operands needed
270
- arguments_ = [];
271
- } else if (operatorConfig.operands === 2) {
272
- // Split value for two operands (e.g., "1 10" for between)
273
- arguments_ = rule.value.split(' ').filter((arg: string) => arg.trim());
274
- } else {
275
- // Single operand - but split words for operators that expect multiple words
276
- if (rule.value && rule.value.trim()) {
277
- // Split on spaces and filter out empty strings
278
- arguments_ = rule.value
279
- .trim()
280
- .split(/\s+/)
281
- .filter((arg: string) => arg.length > 0);
282
- } else {
283
- arguments_ = [];
284
- }
285
- }
286
- } else {
287
- // Fallback for unknown operators - split on spaces if value exists
288
- if (rule.value && rule.value.trim()) {
289
- arguments_ = rule.value
290
- .trim()
291
- .split(/\s+/)
292
- .filter((arg: string) => arg.length > 0);
293
- } else {
294
- arguments_ = [];
295
- }
296
- }
297
-
298
- cases.push({
299
- uuid: caseUuid,
300
- type: rule.operator,
301
- arguments: arguments_,
302
- category_uuid: categoryInfo.uuid
303
- });
304
- });
305
-
306
- // Add default category (always present)
307
- // Name is "Other" if there are user rules, "All Responses" if there are no user rules
308
- const defaultCategoryName = userRules.length > 0 ? 'Other' : 'All Responses';
309
-
310
- // Try to find existing default category by name (prefer exact match)
311
- let existingDefaultCategory = existingCategories.find(
312
- (cat) => cat.name === defaultCategoryName
313
- );
314
-
315
- // If no exact match, try to find the other possible default category name
316
- if (!existingDefaultCategory) {
317
- const alternateName = userRules.length > 0 ? 'All Responses' : 'Other';
318
- existingDefaultCategory = existingCategories.find(
319
- (cat) => cat.name === alternateName
320
- );
321
- }
322
-
323
- const existingDefaultExit = existingDefaultCategory
324
- ? existingExits.find(
325
- (exit) => exit.uuid === existingDefaultCategory.exit_uuid
326
- )
327
- : null;
328
-
329
- const defaultExitUuid = existingDefaultExit?.uuid || generateUUID();
330
- const defaultCategoryUuid = existingDefaultCategory?.uuid || generateUUID();
331
-
332
- categories.push({
333
- uuid: defaultCategoryUuid,
334
- name: defaultCategoryName,
335
- exit_uuid: defaultExitUuid
336
- });
337
-
338
- exits.push({
339
- uuid: defaultExitUuid,
340
- destination_uuid: existingDefaultExit?.destination_uuid || null
341
- });
342
-
343
58
  // Add "No Response" category last (if it exists in the original)
344
59
  const existingNoResponseCategory = existingCategories.find(
345
60
  (cat) => cat.name === 'No Response' || cat.name === 'Timeout'
@@ -351,253 +66,28 @@ const createWaitForResponseRouter = (
351
66
  );
352
67
 
353
68
  if (existingNoResponseExit) {
354
- categories.push(existingNoResponseCategory);
355
- exits.push(existingNoResponseExit);
69
+ router.categories.push(existingNoResponseCategory);
70
+ exits.push({
71
+ uuid: existingNoResponseExit.uuid,
72
+ destination_uuid: existingNoResponseExit.destination_uuid || null
73
+ });
356
74
  }
357
75
  }
358
76
 
359
- // Find the default category (either "Other" or "All Responses")
360
- const defaultCategory = categories.find(
361
- (cat) => cat.name === 'Other' || cat.name === 'All Responses'
362
- );
363
-
364
- return {
365
- router: {
366
- type: 'switch' as const,
367
- categories: categories,
368
- default_category_uuid: defaultCategory?.uuid,
369
- operand: '@input.text',
370
- cases: cases
371
- },
372
- exits: exits
373
- };
77
+ return { router, exits };
374
78
  };
375
79
 
376
80
  export const wait_for_response: NodeConfig = {
377
81
  type: 'wait_for_response',
378
82
  name: 'Wait for Response',
379
- color: COLORS.wait,
83
+ group: SPLIT_GROUPS.wait,
84
+ flowTypes: [FlowTypes.MESSAGE],
380
85
  dialogSize: 'large',
381
86
  form: {
382
- rules: {
383
- type: 'array',
384
- helpText: 'Define rules to categorize responses',
385
- itemLabel: 'Rule',
386
- minItems: 0,
387
- maxItems: 100,
388
- sortable: true,
389
- maintainEmptyItem: true, // Explicitly enable empty item maintenance
390
- isEmptyItem: (item: any) => {
391
- // Helper function to get operator value from various formats
392
- const getOperatorValue = (operator: any): string => {
393
- if (typeof operator === 'string') {
394
- return operator.trim();
395
- } else if (Array.isArray(operator) && operator.length > 0) {
396
- // Handle array format: [{value: "has_any_word", name: "..."}]
397
- const firstOperator = operator[0];
398
- if (
399
- firstOperator &&
400
- typeof firstOperator === 'object' &&
401
- firstOperator.value
402
- ) {
403
- return firstOperator.value.trim();
404
- }
405
- } else if (
406
- operator &&
407
- typeof operator === 'object' &&
408
- operator.value
409
- ) {
410
- // Handle object format: {value: "has_any_word", name: "..."}
411
- return operator.value.trim();
412
- }
413
- return '';
414
- };
415
-
416
- // Check if operator and category are provided
417
- const operatorValue = getOperatorValue(item.operator);
418
- if (!operatorValue || !item.category || item.category.trim() === '') {
419
- return true;
420
- }
421
-
422
- // Check if value is required based on operator configuration
423
- const operatorConfig = getOperatorConfig(operatorValue);
424
- if (operatorConfig && operatorConfig.operands === 1) {
425
- // value1 is required for this operator
426
- return !item.value1 || item.value1.trim() === '';
427
- } else if (operatorConfig && operatorConfig.operands === 2) {
428
- // Both value1 and value2 are required for this operator
429
- return (
430
- !item.value1 ||
431
- item.value1.trim() === '' ||
432
- !item.value2 ||
433
- item.value2.trim() === ''
434
- );
435
- }
436
-
437
- // No value required for this operator
438
- return false;
439
- },
440
- onItemChange: (
441
- itemIndex: number,
442
- field: string,
443
- value: any,
444
- allItems: any[]
445
- ) => {
446
- const updatedItems = [...allItems];
447
- const item = { ...updatedItems[itemIndex] };
448
-
449
- // Helper to get operator value from various formats
450
- const getOperatorValue = (operator: any): string => {
451
- if (typeof operator === 'string') {
452
- return operator.trim();
453
- } else if (Array.isArray(operator) && operator.length > 0) {
454
- const firstOperator = operator[0];
455
- if (
456
- firstOperator &&
457
- typeof firstOperator === 'object' &&
458
- firstOperator.value
459
- ) {
460
- return firstOperator.value.trim();
461
- }
462
- } else if (
463
- operator &&
464
- typeof operator === 'object' &&
465
- operator.value
466
- ) {
467
- return operator.value.trim();
468
- }
469
- return '';
470
- };
471
-
472
- // Update the changed field
473
- item[field] = value;
474
-
475
- // Get operator values (before and after the change)
476
- const oldItem = allItems[itemIndex] || {};
477
- const oldOperatorValue =
478
- field === 'operator'
479
- ? getOperatorValue(oldItem.operator)
480
- : getOperatorValue(item.operator);
481
- const newOperatorValue = getOperatorValue(item.operator);
482
-
483
- // Calculate what the default category name should be before the change
484
- const oldDefaultCategory = generateDefaultCategoryName(
485
- oldOperatorValue,
486
- field === 'value1' ? oldItem.value1 : item.value1,
487
- field === 'value2' ? oldItem.value2 : item.value2
488
- );
489
-
490
- // Calculate what the new default category name should be after the change
491
- const newDefaultCategory = generateDefaultCategoryName(
492
- newOperatorValue,
493
- item.value1,
494
- item.value2
495
- );
496
-
497
- // Determine if we should auto-update the category
498
- const shouldUpdateCategory =
499
- // Category is empty
500
- !item.category ||
501
- item.category.trim() === '' ||
502
- // Category matches the old default (user hasn't customized it)
503
- item.category === oldDefaultCategory;
504
-
505
- // Auto-populate or update category if conditions are met
506
- if (shouldUpdateCategory && newDefaultCategory) {
507
- item.category = newDefaultCategory;
508
- }
509
-
510
- updatedItems[itemIndex] = item;
511
- return updatedItems;
512
- },
513
- itemConfig: {
514
- operator: {
515
- type: 'select',
516
- required: true,
517
- multi: false, // Explicitly set as single-select
518
- options: operatorsToSelectOptions(getWaitForResponseOperators()),
519
- flavor: 'xsmall',
520
- width: '200px'
521
- },
522
- value1: {
523
- type: 'text',
524
- flavor: 'xsmall',
525
- conditions: {
526
- visible: (formData: Record<string, any>) => {
527
- // Helper function to get operator value from various formats
528
- const getOperatorValue = (operator: any): string => {
529
- if (typeof operator === 'string') {
530
- return operator.trim();
531
- } else if (Array.isArray(operator) && operator.length > 0) {
532
- const firstOperator = operator[0];
533
- if (
534
- firstOperator &&
535
- typeof firstOperator === 'object' &&
536
- firstOperator.value
537
- ) {
538
- return firstOperator.value.trim();
539
- }
540
- } else if (
541
- operator &&
542
- typeof operator === 'object' &&
543
- operator.value
544
- ) {
545
- return operator.value.trim();
546
- }
547
- return '';
548
- };
549
-
550
- // Show value1 field for operators that require 1 or 2 operands
551
- const operatorValue = getOperatorValue(formData.operator);
552
- const operatorConfig = getOperatorConfig(operatorValue);
553
- return operatorConfig ? operatorConfig.operands >= 1 : true;
554
- }
555
- }
556
- },
557
- value2: {
558
- type: 'text',
559
- flavor: 'xsmall',
560
- conditions: {
561
- visible: (formData: Record<string, any>) => {
562
- // Helper function to get operator value from various formats
563
- const getOperatorValue = (operator: any): string => {
564
- if (typeof operator === 'string') {
565
- return operator.trim();
566
- } else if (Array.isArray(operator) && operator.length > 0) {
567
- const firstOperator = operator[0];
568
- if (
569
- firstOperator &&
570
- typeof firstOperator === 'object' &&
571
- firstOperator.value
572
- ) {
573
- return firstOperator.value.trim();
574
- }
575
- } else if (
576
- operator &&
577
- typeof operator === 'object' &&
578
- operator.value
579
- ) {
580
- return operator.value.trim();
581
- }
582
- return '';
583
- };
584
-
585
- // Show value2 field only if operator requires exactly 2 operands
586
- const operatorValue = getOperatorValue(formData.operator);
587
- const operatorConfig = getOperatorConfig(operatorValue);
588
- return operatorConfig ? operatorConfig.operands === 2 : false;
589
- }
590
- }
591
- },
592
- category: {
593
- type: 'text',
594
- placeholder: 'Category',
595
- required: true,
596
- maxWidth: '120px',
597
- flavor: 'xsmall'
598
- }
599
- }
600
- },
87
+ rules: createRulesArrayConfig(
88
+ operatorsToSelectOptions(getWaitForResponseOperators()),
89
+ 'Define rules to categorize responses'
90
+ ),
601
91
  timeout_enabled: {
602
92
  type: 'checkbox',
603
93
  label: (formData: Record<string, any>) => {
@@ -611,21 +101,17 @@ export const wait_for_response: NodeConfig = {
611
101
  type: 'select',
612
102
  placeholder: '5 minutes',
613
103
  multi: false,
614
- maxWidth: '150px',
104
+ maxWidth: '100px',
615
105
  flavor: 'xsmall',
616
106
  options: TIMEOUT_OPTIONS,
107
+
617
108
  conditions: {
618
109
  visible: (formData: Record<string, any>) => {
619
110
  return formData.timeout_enabled === true;
620
111
  }
621
112
  }
622
113
  },
623
- result_name: {
624
- type: 'text',
625
- label: 'Result Name',
626
- helpText: 'The name to save the response as',
627
- placeholder: 'response'
628
- }
114
+ result_name: resultNameField
629
115
  },
630
116
  layout: ['rules', 'result_name'],
631
117
  gutter: [
@@ -635,7 +121,7 @@ export const wait_for_response: NodeConfig = {
635
121
  gap: '0.5rem'
636
122
  }
637
123
  ],
638
- validate: (_formData: any) => {
124
+ validate: (_formData: FormData) => {
639
125
  const errors: { [key: string]: string } = {};
640
126
 
641
127
  // No validation needed - allow multiple rules to use same category name
@@ -647,52 +133,8 @@ export const wait_for_response: NodeConfig = {
647
133
  };
648
134
  },
649
135
  toFormData: (node: Node) => {
650
- // Extract rules from router cases
651
- const rules = [];
652
- if (node.router?.cases && node.router?.categories) {
653
- node.router.cases.forEach((case_) => {
654
- // Find the category for this case
655
- const category = node.router!.categories.find(
656
- (cat) => cat.uuid === case_.category_uuid
657
- );
658
-
659
- // Skip system categories
660
- if (category && !isSystemCategory(category.name)) {
661
- // Handle different operator types
662
- const operatorConfig = getOperatorConfig(case_.type);
663
- const operatorDisplayName = operatorConfig
664
- ? operatorConfig.name
665
- : case_.type;
666
- let value1 = '';
667
- let value2 = '';
668
-
669
- if (operatorConfig && operatorConfig.operands === 0) {
670
- // No value needed for operators like has_text, has_number
671
- value1 = '';
672
- value2 = '';
673
- } else if (operatorConfig && operatorConfig.operands === 1) {
674
- // Single value for operators like has_number_lt - use value1
675
- value1 = case_.arguments.join(' ');
676
- value2 = '';
677
- } else if (operatorConfig && operatorConfig.operands === 2) {
678
- // Two separate values for operators like has_number_between
679
- value1 = case_.arguments[0] || '';
680
- value2 = case_.arguments[1] || '';
681
- } else {
682
- // Fallback: use first argument for unknown operators
683
- value1 = case_.arguments.join(' ');
684
- value2 = '';
685
- }
686
-
687
- rules.push({
688
- operator: { value: case_.type, name: operatorDisplayName },
689
- value1: value1,
690
- value2: value2,
691
- category: category.name
692
- });
693
- }
694
- });
695
- }
136
+ // Extract rules from router cases using shared function
137
+ const rules = casesToFormRules(node);
696
138
 
697
139
  // Extract timeout configuration
698
140
  const timeoutSeconds = node.router?.wait?.timeout?.seconds;
@@ -709,88 +151,12 @@ export const wait_for_response: NodeConfig = {
709
151
  rules: rules,
710
152
  timeout_enabled: !!timeoutSeconds,
711
153
  timeout_duration: timeoutOption,
712
- result_name: node.router?.result_name || 'response'
154
+ result_name: node.router?.result_name || ''
713
155
  };
714
156
  },
715
- fromFormData: (formData: any, originalNode: Node): Node => {
716
- // Helper function to get operator value from various formats
717
- const getOperatorValue = (operator: any): string => {
718
- if (typeof operator === 'string') {
719
- return operator.trim();
720
- } else if (Array.isArray(operator) && operator.length > 0) {
721
- // Handle array format: [{value: "has_any_word", name: "..."}]
722
- const firstOperator = operator[0];
723
- if (
724
- firstOperator &&
725
- typeof firstOperator === 'object' &&
726
- firstOperator.value
727
- ) {
728
- return firstOperator.value.trim();
729
- }
730
- } else if (operator && typeof operator === 'object' && operator.value) {
731
- // Handle object format: {value: "has_any_word", name: "..."}
732
- return operator.value.trim();
733
- }
734
- return '';
735
- };
736
-
737
- // Get user rules
738
- const userRules = (formData.rules || [])
739
- .filter((rule: any) => {
740
- // Always need operator and category
741
- const operatorValue = getOperatorValue(rule?.operator);
742
- if (
743
- !operatorValue ||
744
- !rule?.category ||
745
- operatorValue === '' ||
746
- rule.category.trim() === ''
747
- ) {
748
- return false;
749
- }
750
-
751
- // Check if value is required based on operator
752
- const operatorConfig = getOperatorConfig(operatorValue);
753
- if (operatorConfig && operatorConfig.operands === 1) {
754
- // value1 is required for this operator
755
- return rule?.value1 && rule.value1.trim() !== '';
756
- } else if (operatorConfig && operatorConfig.operands === 2) {
757
- // Both value1 and value2 are required for this operator
758
- return (
759
- rule?.value1 &&
760
- rule.value1.trim() !== '' &&
761
- rule?.value2 &&
762
- rule.value2.trim() !== ''
763
- );
764
- }
765
-
766
- // No value required for this operator
767
- return true;
768
- })
769
- .map((rule: any) => {
770
- const operatorValue = getOperatorValue(rule.operator);
771
- const operatorConfig = getOperatorConfig(operatorValue);
772
-
773
- let value = '';
774
-
775
- if (operatorConfig && operatorConfig.operands === 1) {
776
- // Single value from value1
777
- value = rule.value1 ? rule.value1.trim() : '';
778
- } else if (operatorConfig && operatorConfig.operands === 2) {
779
- // Two values - combine them with space
780
- const val1 = rule.value1 ? rule.value1.trim() : '';
781
- const val2 = rule.value2 ? rule.value2.trim() : '';
782
- value = `${val1} ${val2}`.trim();
783
- } else {
784
- // No value needed for 0-operand operators
785
- value = '';
786
- }
787
-
788
- return {
789
- operator: operatorValue,
790
- value: value,
791
- category: rule.category.trim()
792
- };
793
- });
157
+ fromFormData: (formData: FormData, originalNode: Node): Node => {
158
+ // Get user rules using shared extraction function
159
+ const userRules = extractUserRules(formData);
794
160
 
795
161
  // If no user rules, clear cases but preserve other router config
796
162
  if (userRules.length === 0) {
@@ -844,10 +210,14 @@ export const wait_for_response: NodeConfig = {
844
210
 
845
211
  const router: any = {
846
212
  ...noRulesRouter,
847
- result_name: formData.result_name || 'response',
848
213
  cases: [] // Clear all cases when no rules
849
214
  };
850
215
 
216
+ // Only set result_name if provided
217
+ if (formData.result_name && formData.result_name.trim() !== '') {
218
+ router.result_name = formData.result_name.trim();
219
+ }
220
+
851
221
  // Build wait configuration based on form data
852
222
  const waitConfig: any = {
853
223
  type: 'msg'
@@ -923,10 +293,14 @@ export const wait_for_response: NodeConfig = {
923
293
 
924
294
  // Build final router with wait configuration and result_name
925
295
  const finalRouter: any = {
926
- ...router,
927
- result_name: formData.result_name || 'response'
296
+ ...router
928
297
  };
929
298
 
299
+ // Only set result_name if provided
300
+ if (formData.result_name && formData.result_name.trim() !== '') {
301
+ finalRouter.result_name = formData.result_name.trim();
302
+ }
303
+
930
304
  // Build wait configuration based on form data
931
305
  const waitConfig: any = {
932
306
  type: 'msg'
@@ -1021,5 +395,10 @@ export const wait_for_response: NodeConfig = {
1021
395
  router: finalRouter,
1022
396
  exits: exits
1023
397
  };
1024
- }
398
+ },
399
+
400
+ // Localization support for categories
401
+ localizable: 'categories',
402
+ toLocalizationFormData: categoriesToLocalizationFormData,
403
+ fromLocalizationFormData: localizationFormDataToCategories
1025
404
  };