@nyaruka/temba-components 0.131.0 → 0.131.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (430) hide show
  1. package/.github/workflows/publish.yml +4 -1
  2. package/CHANGELOG.md +67 -1
  3. package/demo/data/flows/food-order.json +2 -2
  4. package/demo/data/flows/sample-flow.json +74 -125
  5. package/dist/static/svg/index.svg +1 -1
  6. package/dist/temba-components.js +1156 -619
  7. package/dist/temba-components.js.map +1 -1
  8. package/out-tsc/src/Icons.js +4 -1
  9. package/out-tsc/src/Icons.js.map +1 -1
  10. package/out-tsc/src/events.js.map +1 -1
  11. package/out-tsc/src/flow/CanvasMenu.js +200 -0
  12. package/out-tsc/src/flow/CanvasMenu.js.map +1 -0
  13. package/out-tsc/src/flow/CanvasNode.js +327 -19
  14. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  15. package/out-tsc/src/flow/Editor.js +562 -66
  16. package/out-tsc/src/flow/Editor.js.map +1 -1
  17. package/out-tsc/src/flow/NodeEditor.js +240 -93
  18. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  19. package/out-tsc/src/flow/NodeTypeSelector.js +499 -0
  20. package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -0
  21. package/out-tsc/src/flow/actions/add_contact_groups.js +3 -3
  22. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
  23. package/out-tsc/src/flow/actions/add_contact_urn.js +62 -4
  24. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  25. package/out-tsc/src/flow/actions/add_input_labels.js +3 -3
  26. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  27. package/out-tsc/src/flow/actions/play_audio.js +2 -2
  28. package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
  29. package/out-tsc/src/flow/actions/remove_contact_groups.js +6 -5
  30. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
  31. package/out-tsc/src/flow/actions/request_optin.js +2 -2
  32. package/out-tsc/src/flow/actions/request_optin.js.map +1 -1
  33. package/out-tsc/src/flow/actions/say_msg.js +2 -2
  34. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  35. package/out-tsc/src/flow/actions/send_broadcast.js +76 -23
  36. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  37. package/out-tsc/src/flow/actions/send_email.js +4 -5
  38. package/out-tsc/src/flow/actions/send_email.js.map +1 -1
  39. package/out-tsc/src/flow/actions/send_msg.js +9 -19
  40. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  41. package/out-tsc/src/flow/actions/set_contact_channel.js +5 -9
  42. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  43. package/out-tsc/src/flow/actions/set_contact_field.js +19 -20
  44. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  45. package/out-tsc/src/flow/actions/set_contact_language.js +2 -2
  46. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  47. package/out-tsc/src/flow/actions/set_contact_name.js +2 -12
  48. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  49. package/out-tsc/src/flow/actions/set_contact_status.js +2 -2
  50. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  51. package/out-tsc/src/flow/actions/set_run_result.js +3 -3
  52. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  53. package/out-tsc/src/flow/actions/start_session.js +180 -6
  54. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  55. package/out-tsc/src/flow/config.js +11 -15
  56. package/out-tsc/src/flow/config.js.map +1 -1
  57. package/out-tsc/src/flow/currencies.js +45 -0
  58. package/out-tsc/src/flow/currencies.js.map +1 -0
  59. package/out-tsc/src/flow/nodes/shared-rules.js +257 -0
  60. package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -0
  61. package/out-tsc/src/flow/nodes/shared.js +17 -0
  62. package/out-tsc/src/flow/nodes/shared.js.map +1 -0
  63. package/out-tsc/src/flow/nodes/split_by_airtime.js +205 -5
  64. package/out-tsc/src/flow/nodes/split_by_airtime.js.map +1 -1
  65. package/out-tsc/src/flow/nodes/split_by_contact_field.js +147 -3
  66. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -1
  67. package/out-tsc/src/flow/nodes/split_by_expression.js +68 -2
  68. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -1
  69. package/out-tsc/src/flow/nodes/split_by_groups.js +12 -9
  70. package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -1
  71. package/out-tsc/src/flow/nodes/split_by_intent.js +7 -0
  72. package/out-tsc/src/flow/nodes/split_by_intent.js.map +1 -0
  73. package/out-tsc/src/flow/nodes/split_by_llm.js +3 -2
  74. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
  75. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +2 -2
  76. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  77. package/out-tsc/src/flow/nodes/split_by_random.js +3 -3
  78. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
  79. package/out-tsc/src/flow/nodes/split_by_resthook.js +108 -0
  80. package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -0
  81. package/out-tsc/src/flow/nodes/split_by_run_result.js +206 -3
  82. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
  83. package/out-tsc/src/flow/nodes/split_by_scheme.js +153 -2
  84. package/out-tsc/src/flow/nodes/split_by_scheme.js.map +1 -1
  85. package/out-tsc/src/flow/nodes/split_by_subflow.js +6 -4
  86. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  87. package/out-tsc/src/flow/nodes/split_by_ticket.js +3 -2
  88. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -1
  89. package/out-tsc/src/flow/nodes/split_by_webhook.js +3 -2
  90. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
  91. package/out-tsc/src/flow/nodes/wait_for_audio.js +2 -2
  92. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -1
  93. package/out-tsc/src/flow/nodes/wait_for_digits.js +2 -2
  94. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
  95. package/out-tsc/src/flow/nodes/wait_for_image.js +2 -2
  96. package/out-tsc/src/flow/nodes/wait_for_image.js.map +1 -1
  97. package/out-tsc/src/flow/nodes/wait_for_location.js +2 -2
  98. package/out-tsc/src/flow/nodes/wait_for_location.js.map +1 -1
  99. package/out-tsc/src/flow/nodes/wait_for_menu.js +2 -2
  100. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
  101. package/out-tsc/src/flow/nodes/wait_for_response.js +32 -567
  102. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  103. package/out-tsc/src/flow/nodes/wait_for_video.js +2 -2
  104. package/out-tsc/src/flow/nodes/wait_for_video.js.map +1 -1
  105. package/out-tsc/src/flow/types.js +71 -12
  106. package/out-tsc/src/flow/types.js.map +1 -1
  107. package/out-tsc/src/flow/utils.js +101 -14
  108. package/out-tsc/src/flow/utils.js.map +1 -1
  109. package/out-tsc/src/form/ContactSearch.js +1 -1
  110. package/out-tsc/src/form/ContactSearch.js.map +1 -1
  111. package/out-tsc/src/form/FieldRenderer.js +2 -4
  112. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  113. package/out-tsc/src/interfaces.js +3 -0
  114. package/out-tsc/src/interfaces.js.map +1 -1
  115. package/out-tsc/src/list/SortableList.js +98 -33
  116. package/out-tsc/src/list/SortableList.js.map +1 -1
  117. package/out-tsc/src/live/ContactChat.js +15 -18
  118. package/out-tsc/src/live/ContactChat.js.map +1 -1
  119. package/out-tsc/src/store/AppState.js +53 -0
  120. package/out-tsc/src/store/AppState.js.map +1 -1
  121. package/out-tsc/src/utils.js +254 -13
  122. package/out-tsc/src/utils.js.map +1 -1
  123. package/out-tsc/temba-modules.js +4 -0
  124. package/out-tsc/temba-modules.js.map +1 -1
  125. package/out-tsc/test/ActionHelper.js +3 -3
  126. package/out-tsc/test/ActionHelper.js.map +1 -1
  127. package/out-tsc/test/NodeHelper.js +6 -3
  128. package/out-tsc/test/NodeHelper.js.map +1 -1
  129. package/out-tsc/test/actions/add_contact_urn.test.js +202 -0
  130. package/out-tsc/test/actions/add_contact_urn.test.js.map +1 -0
  131. package/out-tsc/test/actions/send_broadcast.test.js +148 -0
  132. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -0
  133. package/out-tsc/test/actions/send_email.test.js +17 -23
  134. package/out-tsc/test/actions/send_email.test.js.map +1 -1
  135. package/out-tsc/test/actions/send_msg.test.js +33 -15
  136. package/out-tsc/test/actions/send_msg.test.js.map +1 -1
  137. package/out-tsc/test/actions/start_session.test.js +116 -0
  138. package/out-tsc/test/actions/start_session.test.js.map +1 -0
  139. package/out-tsc/test/nodes/split_by_airtime.test.js +604 -0
  140. package/out-tsc/test/nodes/split_by_airtime.test.js.map +1 -0
  141. package/out-tsc/test/nodes/split_by_contact_field.test.js +387 -0
  142. package/out-tsc/test/nodes/split_by_contact_field.test.js.map +1 -0
  143. package/out-tsc/test/nodes/split_by_expression.test.js +614 -0
  144. package/out-tsc/test/nodes/split_by_expression.test.js.map +1 -0
  145. package/out-tsc/test/nodes/split_by_random.test.js +3 -3
  146. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -1
  147. package/out-tsc/test/nodes/split_by_resthook.test.js +337 -0
  148. package/out-tsc/test/nodes/split_by_resthook.test.js.map +1 -0
  149. package/out-tsc/test/nodes/split_by_run_result.test.js +920 -0
  150. package/out-tsc/test/nodes/split_by_run_result.test.js.map +1 -0
  151. package/out-tsc/test/nodes/split_by_scheme.test.js +399 -0
  152. package/out-tsc/test/nodes/split_by_scheme.test.js.map +1 -0
  153. package/out-tsc/test/nodes/split_by_subflow.test.js +333 -0
  154. package/out-tsc/test/nodes/split_by_subflow.test.js.map +1 -0
  155. package/out-tsc/test/nodes/wait_for_digits.test.js +2 -2
  156. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  157. package/out-tsc/test/nodes/wait_for_response.test.js +2 -1
  158. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -1
  159. package/out-tsc/test/temba-action-drag-between-nodes.test.js +252 -0
  160. package/out-tsc/test/temba-action-drag-between-nodes.test.js.map +1 -0
  161. package/out-tsc/test/temba-canvas-menu.test.js +122 -0
  162. package/out-tsc/test/temba-canvas-menu.test.js.map +1 -0
  163. package/out-tsc/test/temba-flow-editor-node.test.js +85 -2
  164. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  165. package/out-tsc/test/temba-flow-editor.test.js +7 -8
  166. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  167. package/out-tsc/test/temba-node-editor.test.js +3 -1
  168. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  169. package/out-tsc/test/temba-node-type-selector.test.js +115 -0
  170. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -0
  171. package/out-tsc/test/temba-omnibox.test.js +2 -1
  172. package/out-tsc/test/temba-omnibox.test.js.map +1 -1
  173. package/out-tsc/test/temba-sortable-list.test.js +51 -0
  174. package/out-tsc/test/temba-sortable-list.test.js.map +1 -1
  175. package/out-tsc/test/temba-utils-index.test.js +1 -27
  176. package/out-tsc/test/temba-utils-index.test.js.map +1 -1
  177. package/out-tsc/test/utils.test.js +2 -0
  178. package/out-tsc/test/utils.test.js.map +1 -1
  179. package/package.json +2 -1
  180. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  181. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  182. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  183. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  184. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  185. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  186. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  187. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  188. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  189. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  190. package/screenshots/truth/actions/add_contact_urn/editor/expression-facebook.png +0 -0
  191. package/screenshots/truth/actions/add_contact_urn/editor/expression-phone.png +0 -0
  192. package/screenshots/truth/actions/add_contact_urn/editor/facebook-id.png +0 -0
  193. package/screenshots/truth/actions/add_contact_urn/editor/instagram-handle.png +0 -0
  194. package/screenshots/truth/actions/add_contact_urn/editor/line-id.png +0 -0
  195. package/screenshots/truth/actions/add_contact_urn/editor/phone-number.png +0 -0
  196. package/screenshots/truth/actions/add_contact_urn/editor/telegram-id.png +0 -0
  197. package/screenshots/truth/actions/add_contact_urn/editor/viber-id.png +0 -0
  198. package/screenshots/truth/actions/add_contact_urn/editor/wechat-id.png +0 -0
  199. package/screenshots/truth/actions/add_contact_urn/editor/whatsapp.png +0 -0
  200. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  201. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  202. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  203. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  204. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  205. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  206. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  207. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  208. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  209. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  210. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  211. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  212. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  213. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  214. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  215. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  216. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  217. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  218. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  219. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  220. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  221. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  222. package/screenshots/truth/actions/send_broadcast/editor/contacts-only.png +0 -0
  223. package/screenshots/truth/actions/send_broadcast/editor/groups-and-contacts.png +0 -0
  224. package/screenshots/truth/actions/send_broadcast/editor/groups-only.png +0 -0
  225. package/screenshots/truth/actions/send_broadcast/editor/many-groups.png +0 -0
  226. package/screenshots/truth/actions/send_broadcast/editor/multiline-text.png +0 -0
  227. package/screenshots/truth/actions/send_broadcast/editor/with-attachments.png +0 -0
  228. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  229. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  230. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  231. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  232. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  233. package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
  234. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  235. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  236. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  237. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  238. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  239. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  240. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  241. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  242. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  243. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  244. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  245. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  246. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  247. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  248. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  249. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  250. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  251. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  252. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  253. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  254. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  255. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  256. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  257. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  258. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  259. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  260. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  261. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  262. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  263. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  264. package/screenshots/truth/actions/start_session/editor/contact-query.png +0 -0
  265. package/screenshots/truth/actions/start_session/editor/contacts-only.png +0 -0
  266. package/screenshots/truth/actions/start_session/editor/create-contact.png +0 -0
  267. package/screenshots/truth/actions/start_session/editor/groups-and-contacts.png +0 -0
  268. package/screenshots/truth/actions/start_session/editor/groups-only.png +0 -0
  269. package/screenshots/truth/actions/start_session/editor/many-recipients.png +0 -0
  270. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  271. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  272. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  273. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  274. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  275. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  276. package/screenshots/truth/canvas-menu/open.png +0 -0
  277. package/screenshots/truth/editor/router.png +0 -0
  278. package/screenshots/truth/editor/wait.png +0 -0
  279. package/screenshots/truth/list/fields-dragging.png +0 -0
  280. package/screenshots/truth/list/sortable-dragging.png +0 -0
  281. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  282. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  283. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  284. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  285. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  286. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  287. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  288. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  289. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  290. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  291. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  292. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  293. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  294. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  295. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  296. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  297. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  298. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  299. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  300. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  301. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  302. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  303. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  304. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  305. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  306. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  307. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  308. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  309. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  310. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  311. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  312. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  313. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  314. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  315. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  316. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  317. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  318. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  319. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  320. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  321. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  322. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  323. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  324. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  325. package/src/Icons.ts +4 -1
  326. package/src/events.ts +2 -6
  327. package/src/flow/CanvasMenu.ts +217 -0
  328. package/src/flow/CanvasNode.ts +408 -10
  329. package/src/flow/Editor.ts +683 -44
  330. package/src/flow/NodeEditor.ts +304 -125
  331. package/src/flow/NodeTypeSelector.ts +592 -0
  332. package/src/flow/actions/add_contact_groups.ts +4 -4
  333. package/src/flow/actions/add_contact_urn.ts +76 -4
  334. package/src/flow/actions/add_input_labels.ts +4 -4
  335. package/src/flow/actions/play_audio.ts +2 -2
  336. package/src/flow/actions/remove_contact_groups.ts +14 -6
  337. package/src/flow/actions/request_optin.ts +2 -2
  338. package/src/flow/actions/say_msg.ts +2 -2
  339. package/src/flow/actions/send_broadcast.ts +85 -23
  340. package/src/flow/actions/send_email.ts +10 -6
  341. package/src/flow/actions/send_msg.ts +22 -32
  342. package/src/flow/actions/set_contact_channel.ts +5 -11
  343. package/src/flow/actions/set_contact_field.ts +20 -25
  344. package/src/flow/actions/set_contact_language.ts +9 -4
  345. package/src/flow/actions/set_contact_name.ts +3 -15
  346. package/src/flow/actions/set_contact_status.ts +3 -3
  347. package/src/flow/actions/set_run_result.ts +4 -4
  348. package/src/flow/actions/start_session.ts +208 -6
  349. package/src/flow/config.ts +13 -15
  350. package/src/flow/currencies.ts +51 -0
  351. package/src/flow/nodes/shared-rules.ts +301 -0
  352. package/src/flow/nodes/shared.ts +18 -0
  353. package/src/flow/nodes/split_by_airtime.ts +238 -5
  354. package/src/flow/nodes/split_by_contact_field.ts +185 -3
  355. package/src/flow/nodes/split_by_expression.ts +94 -2
  356. package/src/flow/nodes/split_by_groups.ts +15 -10
  357. package/src/flow/nodes/split_by_intent.ts +7 -0
  358. package/src/flow/nodes/split_by_llm.ts +4 -3
  359. package/src/flow/nodes/split_by_llm_categorize.ts +4 -4
  360. package/src/flow/nodes/split_by_random.ts +5 -5
  361. package/src/flow/nodes/split_by_resthook.ts +130 -0
  362. package/src/flow/nodes/split_by_run_result.ts +249 -3
  363. package/src/flow/nodes/split_by_scheme.ts +192 -2
  364. package/src/flow/nodes/split_by_subflow.ts +6 -4
  365. package/src/flow/nodes/split_by_ticket.ts +4 -3
  366. package/src/flow/nodes/split_by_webhook.ts +6 -5
  367. package/src/flow/nodes/wait_for_audio.ts +2 -2
  368. package/src/flow/nodes/wait_for_digits.ts +2 -2
  369. package/src/flow/nodes/wait_for_image.ts +2 -2
  370. package/src/flow/nodes/wait_for_location.ts +2 -2
  371. package/src/flow/nodes/wait_for_menu.ts +2 -2
  372. package/src/flow/nodes/wait_for_response.ts +48 -679
  373. package/src/flow/nodes/wait_for_video.ts +2 -2
  374. package/src/flow/types.ts +109 -23
  375. package/src/flow/utils.ts +108 -14
  376. package/src/form/ContactSearch.ts +1 -1
  377. package/src/form/FieldRenderer.ts +2 -4
  378. package/src/interfaces.ts +3 -0
  379. package/src/list/SortableList.ts +109 -34
  380. package/src/live/ContactChat.ts +15 -18
  381. package/src/store/AppState.ts +69 -0
  382. package/src/store/flow-definition.d.ts +2 -5
  383. package/src/utils.ts +332 -12
  384. package/static/api/channels.json +46 -0
  385. package/static/api/resthooks.json +31 -0
  386. package/static/svg/index.svg +1 -1
  387. package/static/svg/work/traced/lightning-02.svg +1 -0
  388. package/static/svg/work/used/lightning-02.svg +3 -0
  389. package/temba-modules.ts +4 -0
  390. package/test/ActionHelper.ts +3 -3
  391. package/test/NodeHelper.ts +6 -3
  392. package/test/actions/add_contact_urn.test.ts +287 -0
  393. package/test/actions/send_broadcast.test.ts +190 -0
  394. package/test/actions/send_email.test.ts +17 -23
  395. package/test/actions/send_msg.test.ts +39 -15
  396. package/test/actions/start_session.test.ts +151 -0
  397. package/test/nodes/split_by_airtime.test.ts +673 -0
  398. package/test/nodes/split_by_contact_field.test.ts +451 -0
  399. package/test/nodes/split_by_expression.test.ts +751 -0
  400. package/test/nodes/split_by_random.test.ts +3 -3
  401. package/test/nodes/split_by_resthook.test.ts +398 -0
  402. package/test/nodes/split_by_run_result.test.ts +1109 -0
  403. package/test/nodes/split_by_scheme.test.ts +486 -0
  404. package/test/nodes/split_by_subflow.test.ts +381 -0
  405. package/test/nodes/wait_for_digits.test.ts +2 -2
  406. package/test/nodes/wait_for_response.test.ts +2 -1
  407. package/test/temba-action-drag-between-nodes.test.ts +301 -0
  408. package/test/temba-canvas-menu.test.ts +156 -0
  409. package/test/temba-flow-editor-node.test.ts +102 -2
  410. package/test/temba-flow-editor.test.ts +7 -8
  411. package/test/temba-node-editor.test.ts +3 -1
  412. package/test/temba-node-type-selector.test.ts +152 -0
  413. package/test/temba-omnibox.test.ts +2 -1
  414. package/test/temba-sortable-list.test.ts +69 -0
  415. package/test/temba-utils-index.test.ts +0 -35
  416. package/test/utils.test.ts +2 -0
  417. package/test-assets/contacts/history.json +14 -20
  418. package/web-dev-server.config.mjs +3 -1
  419. package/out-tsc/src/flow/actions/call_classifier.js +0 -11
  420. package/out-tsc/src/flow/actions/call_classifier.js.map +0 -1
  421. package/out-tsc/src/flow/actions/call_resthook.js +0 -11
  422. package/out-tsc/src/flow/actions/call_resthook.js.map +0 -1
  423. package/out-tsc/src/flow/actions/split_by_expression_example.js +0 -77
  424. package/out-tsc/src/flow/actions/split_by_expression_example.js.map +0 -1
  425. package/out-tsc/src/flow/actions/transfer_airtime.js +0 -11
  426. package/out-tsc/src/flow/actions/transfer_airtime.js.map +0 -1
  427. package/src/flow/actions/call_classifier.ts +0 -12
  428. package/src/flow/actions/call_resthook.ts +0 -12
  429. package/src/flow/actions/split_by_expression_example.ts +0 -88
  430. package/src/flow/actions/transfer_airtime.ts +0 -12
@@ -0,0 +1,751 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { split_by_expression } from '../../src/flow/nodes/split_by_expression';
3
+ import { Node } from '../../src/store/flow-definition';
4
+ import { NodeTest } from '../NodeHelper';
5
+
6
+ /**
7
+ * Test suite for the split_by_expression node configuration.
8
+ */
9
+ describe('split_by_expression node config', () => {
10
+ const helper = new NodeTest(split_by_expression, 'split_by_expression');
11
+
12
+ describe('basic properties', () => {
13
+ helper.testBasicProperties();
14
+
15
+ it('has correct name', () => {
16
+ expect(split_by_expression.name).to.equal('Split by Expression');
17
+ });
18
+
19
+ it('has correct type', () => {
20
+ expect(split_by_expression.type).to.equal('split_by_expression');
21
+ });
22
+ });
23
+
24
+ describe('toFormData', () => {
25
+ it('should transform node with rules to form data correctly', () => {
26
+ const node: Node = {
27
+ uuid: 'test-node-uuid',
28
+ actions: [],
29
+ router: {
30
+ type: 'switch',
31
+ operand: '@fields.age',
32
+ cases: [
33
+ {
34
+ uuid: 'case-1',
35
+ type: 'has_number_gte',
36
+ arguments: ['18'],
37
+ category_uuid: 'cat-1'
38
+ },
39
+ {
40
+ uuid: 'case-2',
41
+ type: 'has_number_between',
42
+ arguments: ['13', '17'],
43
+ category_uuid: 'cat-2'
44
+ },
45
+ {
46
+ uuid: 'case-3',
47
+ type: 'has_number_lt',
48
+ arguments: ['13'],
49
+ category_uuid: 'cat-3'
50
+ }
51
+ ],
52
+ categories: [
53
+ { uuid: 'cat-1', name: 'Adult', exit_uuid: 'exit-1' },
54
+ { uuid: 'cat-2', name: 'Teen', exit_uuid: 'exit-2' },
55
+ { uuid: 'cat-3', name: 'Child', exit_uuid: 'exit-3' },
56
+ { uuid: 'cat-other', name: 'Other', exit_uuid: 'exit-other' }
57
+ ],
58
+ default_category_uuid: 'cat-other'
59
+ },
60
+ exits: [
61
+ { uuid: 'exit-1', destination_uuid: null },
62
+ { uuid: 'exit-2', destination_uuid: null },
63
+ { uuid: 'exit-3', destination_uuid: null },
64
+ { uuid: 'exit-other', destination_uuid: null }
65
+ ]
66
+ };
67
+
68
+ const formData = split_by_expression.toFormData!(node);
69
+
70
+ expect(formData.uuid).to.equal('test-node-uuid');
71
+ expect(formData.operand).to.equal('@fields.age');
72
+ expect(formData.rules).to.have.lengthOf(3);
73
+
74
+ // Check first rule
75
+ expect(formData.rules[0].operator.value).to.equal('has_number_gte');
76
+ expect(formData.rules[0].value1).to.equal('18');
77
+ expect(formData.rules[0].category).to.equal('Adult');
78
+
79
+ // Check second rule (two operands)
80
+ expect(formData.rules[1].operator.value).to.equal('has_number_between');
81
+ expect(formData.rules[1].value1).to.equal('13');
82
+ expect(formData.rules[1].value2).to.equal('17');
83
+ expect(formData.rules[1].category).to.equal('Teen');
84
+
85
+ // Check third rule
86
+ expect(formData.rules[2].operator.value).to.equal('has_number_lt');
87
+ expect(formData.rules[2].value1).to.equal('13');
88
+ expect(formData.rules[2].category).to.equal('Child');
89
+ });
90
+
91
+ it('should transform node with no rules to form data correctly', () => {
92
+ const node: Node = {
93
+ uuid: 'test-node-uuid',
94
+ actions: [],
95
+ router: {
96
+ type: 'switch',
97
+ operand: '@input.text',
98
+ cases: [],
99
+ categories: [
100
+ {
101
+ uuid: 'cat-all',
102
+ name: 'All Responses',
103
+ exit_uuid: 'exit-all'
104
+ }
105
+ ],
106
+ default_category_uuid: 'cat-all'
107
+ },
108
+ exits: [{ uuid: 'exit-all', destination_uuid: null }]
109
+ };
110
+
111
+ const formData = split_by_expression.toFormData!(node);
112
+
113
+ expect(formData.uuid).to.equal('test-node-uuid');
114
+ expect(formData.operand).to.equal('@input.text');
115
+ expect(formData.rules).to.have.lengthOf(0);
116
+ });
117
+
118
+ it('should handle result_name in toFormData', () => {
119
+ const node: Node = {
120
+ uuid: 'test-node-uuid',
121
+ actions: [],
122
+ router: {
123
+ type: 'switch',
124
+ operand: '@fields.color',
125
+ result_name: 'Color Choice',
126
+ cases: [],
127
+ categories: [
128
+ {
129
+ uuid: 'cat-all',
130
+ name: 'All Responses',
131
+ exit_uuid: 'exit-all'
132
+ }
133
+ ],
134
+ default_category_uuid: 'cat-all'
135
+ },
136
+ exits: [{ uuid: 'exit-all', destination_uuid: null }]
137
+ };
138
+
139
+ const formData = split_by_expression.toFormData!(node);
140
+
141
+ expect(formData.result_name).to.equal('Color Choice');
142
+ });
143
+
144
+ it('should handle zero-operand operators', () => {
145
+ const node: Node = {
146
+ uuid: 'test-node-uuid',
147
+ actions: [],
148
+ router: {
149
+ type: 'switch',
150
+ operand: '@input.text',
151
+ cases: [
152
+ {
153
+ uuid: 'case-1',
154
+ type: 'has_number',
155
+ arguments: [],
156
+ category_uuid: 'cat-1'
157
+ }
158
+ ],
159
+ categories: [
160
+ { uuid: 'cat-1', name: 'Has Number', exit_uuid: 'exit-1' },
161
+ { uuid: 'cat-other', name: 'Other', exit_uuid: 'exit-other' }
162
+ ],
163
+ default_category_uuid: 'cat-other'
164
+ },
165
+ exits: [
166
+ { uuid: 'exit-1', destination_uuid: null },
167
+ { uuid: 'exit-other', destination_uuid: null }
168
+ ]
169
+ };
170
+
171
+ const formData = split_by_expression.toFormData!(node);
172
+
173
+ expect(formData.rules).to.have.lengthOf(1);
174
+ expect(formData.rules[0].operator.value).to.equal('has_number');
175
+ expect(formData.rules[0].value1).to.equal('');
176
+ expect(formData.rules[0].value2).to.equal('');
177
+ });
178
+ });
179
+
180
+ describe('fromFormData', () => {
181
+ it('should transform form data with rules to node correctly', () => {
182
+ const formData = {
183
+ uuid: 'test-node-uuid',
184
+ operand: '@fields.age',
185
+ rules: [
186
+ {
187
+ operator: {
188
+ value: 'has_number_gte',
189
+ name: 'has a number at or above'
190
+ },
191
+ value1: '18',
192
+ value2: '',
193
+ category: 'Adult'
194
+ },
195
+ {
196
+ operator: {
197
+ value: 'has_number_between',
198
+ name: 'has a number between'
199
+ },
200
+ value1: '13',
201
+ value2: '17',
202
+ category: 'Teen'
203
+ }
204
+ ],
205
+ result_name: ''
206
+ };
207
+
208
+ const originalNode: Node = {
209
+ uuid: 'test-node-uuid',
210
+ actions: [],
211
+ router: {
212
+ type: 'switch',
213
+ operand: '@input.text',
214
+ cases: [],
215
+ categories: [],
216
+ default_category_uuid: ''
217
+ },
218
+ exits: []
219
+ };
220
+
221
+ const resultNode = split_by_expression.fromFormData!(
222
+ formData,
223
+ originalNode
224
+ );
225
+
226
+ expect(resultNode.uuid).to.equal('test-node-uuid');
227
+ expect(resultNode.router).to.exist;
228
+ expect(resultNode.router!.operand).to.equal('@fields.age');
229
+ expect(resultNode.router!.type).to.equal('switch');
230
+ expect(resultNode.router!.cases).to.have.lengthOf(2);
231
+ expect(resultNode.router!.categories).to.have.lengthOf(3); // 2 rules + Other
232
+
233
+ // Check first case
234
+ const case1 = resultNode.router!.cases![0];
235
+ expect(case1.type).to.equal('has_number_gte');
236
+ expect(case1.arguments).to.deep.equal(['18']);
237
+
238
+ // Check second case (should split into two arguments)
239
+ const case2 = resultNode.router!.cases![1];
240
+ expect(case2.type).to.equal('has_number_between');
241
+ expect(case2.arguments).to.deep.equal(['13', '17']);
242
+
243
+ // Check categories
244
+ const adultCategory = resultNode.router!.categories!.find(
245
+ (cat) => cat.name === 'Adult'
246
+ );
247
+ expect(adultCategory).to.exist;
248
+
249
+ const teenCategory = resultNode.router!.categories!.find(
250
+ (cat) => cat.name === 'Teen'
251
+ );
252
+ expect(teenCategory).to.exist;
253
+
254
+ const otherCategory = resultNode.router!.categories!.find(
255
+ (cat) => cat.name === 'Other'
256
+ );
257
+ expect(otherCategory).to.exist;
258
+
259
+ // Check exits match categories
260
+ expect(resultNode.exits).to.have.lengthOf(3);
261
+ });
262
+
263
+ it('should handle empty rules', () => {
264
+ const formData = {
265
+ uuid: 'test-node-uuid',
266
+ operand: '@fields.status',
267
+ rules: [],
268
+ result_name: ''
269
+ };
270
+
271
+ const originalNode: Node = {
272
+ uuid: 'test-node-uuid',
273
+ actions: [],
274
+ router: {
275
+ type: 'switch',
276
+ operand: '@input.text',
277
+ cases: [],
278
+ categories: [],
279
+ default_category_uuid: ''
280
+ },
281
+ exits: []
282
+ };
283
+
284
+ const resultNode = split_by_expression.fromFormData!(
285
+ formData,
286
+ originalNode
287
+ );
288
+
289
+ expect(resultNode.router).to.exist;
290
+ expect(resultNode.router!.operand).to.equal('@fields.status');
291
+ expect(resultNode.router!.cases).to.have.lengthOf(0);
292
+ expect(resultNode.router!.categories).to.have.lengthOf(1);
293
+ expect(resultNode.router!.categories![0].name).to.equal('All Responses');
294
+ });
295
+
296
+ it('should preserve UUIDs when editing existing node', () => {
297
+ const existingNode: Node = {
298
+ uuid: 'test-node-uuid',
299
+ actions: [],
300
+ router: {
301
+ type: 'switch',
302
+ operand: '@fields.age',
303
+ cases: [
304
+ {
305
+ uuid: 'existing-case-1',
306
+ type: 'has_number_gte',
307
+ arguments: ['18'],
308
+ category_uuid: 'existing-cat-1'
309
+ }
310
+ ],
311
+ categories: [
312
+ {
313
+ uuid: 'existing-cat-1',
314
+ name: 'Adult',
315
+ exit_uuid: 'existing-exit-1'
316
+ },
317
+ {
318
+ uuid: 'existing-cat-other',
319
+ name: 'Other',
320
+ exit_uuid: 'existing-exit-other'
321
+ }
322
+ ],
323
+ default_category_uuid: 'existing-cat-other'
324
+ },
325
+ exits: [
326
+ { uuid: 'existing-exit-1', destination_uuid: 'next-node-1' },
327
+ { uuid: 'existing-exit-other', destination_uuid: 'next-node-2' }
328
+ ]
329
+ };
330
+
331
+ const formData = {
332
+ uuid: 'test-node-uuid',
333
+ operand: '@fields.age',
334
+ rules: [
335
+ {
336
+ operator: {
337
+ value: 'has_number_gte',
338
+ name: 'has a number at or above'
339
+ },
340
+ value1: '18',
341
+ value2: '',
342
+ category: 'Adult'
343
+ }
344
+ ],
345
+ result_name: ''
346
+ };
347
+
348
+ const resultNode = split_by_expression.fromFormData!(
349
+ formData,
350
+ existingNode
351
+ );
352
+
353
+ // Check that UUIDs are preserved
354
+ const adultCategory = resultNode.router!.categories!.find(
355
+ (cat) => cat.name === 'Adult'
356
+ );
357
+ expect(adultCategory!.uuid).to.equal('existing-cat-1');
358
+ expect(adultCategory!.exit_uuid).to.equal('existing-exit-1');
359
+
360
+ // Check that destination_uuid is preserved
361
+ const adultExit = resultNode.exits!.find(
362
+ (exit) => exit.uuid === 'existing-exit-1'
363
+ );
364
+ expect(adultExit!.destination_uuid).to.equal('next-node-1');
365
+
366
+ // Check Other category UUID is preserved
367
+ const otherCategory = resultNode.router!.categories!.find(
368
+ (cat) => cat.name === 'Other'
369
+ );
370
+ expect(otherCategory!.uuid).to.equal('existing-cat-other');
371
+ });
372
+
373
+ it('should handle multiple rules with same category', () => {
374
+ const formData = {
375
+ uuid: 'test-node-uuid',
376
+ operand: '@fields.color',
377
+ rules: [
378
+ {
379
+ operator: { value: 'has_any_word', name: 'has any of the words' },
380
+ value1: 'red',
381
+ value2: '',
382
+ category: 'Warm'
383
+ },
384
+ {
385
+ operator: { value: 'has_any_word', name: 'has any of the words' },
386
+ value1: 'orange',
387
+ value2: '',
388
+ category: 'Warm'
389
+ },
390
+ {
391
+ operator: { value: 'has_any_word', name: 'has any of the words' },
392
+ value1: 'blue',
393
+ value2: '',
394
+ category: 'Cool'
395
+ }
396
+ ],
397
+ result_name: ''
398
+ };
399
+
400
+ const originalNode: Node = {
401
+ uuid: 'test-node-uuid',
402
+ actions: [],
403
+ router: {
404
+ type: 'switch',
405
+ operand: '@input.text',
406
+ cases: [],
407
+ categories: [],
408
+ default_category_uuid: ''
409
+ },
410
+ exits: []
411
+ };
412
+
413
+ const resultNode = split_by_expression.fromFormData!(
414
+ formData,
415
+ originalNode
416
+ );
417
+
418
+ // Should have 3 cases but only 3 categories (Warm, Cool, Other)
419
+ expect(resultNode.router!.cases).to.have.lengthOf(3);
420
+ expect(resultNode.router!.categories).to.have.lengthOf(3);
421
+
422
+ // Check that both "Warm" rules point to the same category
423
+ const warmCategory = resultNode.router!.categories!.find(
424
+ (cat) => cat.name === 'Warm'
425
+ );
426
+ expect(warmCategory).to.exist;
427
+
428
+ const warmCases = resultNode.router!.cases!.filter(
429
+ (case_) => case_.category_uuid === warmCategory!.uuid
430
+ );
431
+ expect(warmCases).to.have.lengthOf(2);
432
+
433
+ // Should only have 3 exits (Warm, Cool, Other)
434
+ expect(resultNode.exits).to.have.lengthOf(3);
435
+ });
436
+
437
+ it('should handle result_name in fromFormData', () => {
438
+ const formData = {
439
+ uuid: 'test-node-uuid',
440
+ operand: '@fields.priority',
441
+ rules: [],
442
+ result_name: 'Priority Level'
443
+ };
444
+
445
+ const originalNode: Node = {
446
+ uuid: 'test-node-uuid',
447
+ actions: [],
448
+ router: {
449
+ type: 'switch',
450
+ operand: '@input.text',
451
+ cases: [],
452
+ categories: [],
453
+ default_category_uuid: ''
454
+ },
455
+ exits: []
456
+ };
457
+
458
+ const resultNode = split_by_expression.fromFormData!(
459
+ formData,
460
+ originalNode
461
+ );
462
+
463
+ expect(resultNode.router!.result_name).to.equal('Priority Level');
464
+ });
465
+
466
+ it('should not set result_name if empty', () => {
467
+ const formData = {
468
+ uuid: 'test-node-uuid',
469
+ operand: '@fields.status',
470
+ rules: [],
471
+ result_name: ''
472
+ };
473
+
474
+ const originalNode: Node = {
475
+ uuid: 'test-node-uuid',
476
+ actions: [],
477
+ router: {
478
+ type: 'switch',
479
+ operand: '@input.text',
480
+ cases: [],
481
+ categories: [],
482
+ default_category_uuid: ''
483
+ },
484
+ exits: []
485
+ };
486
+
487
+ const resultNode = split_by_expression.fromFormData!(
488
+ formData,
489
+ originalNode
490
+ );
491
+
492
+ expect(resultNode.router!.result_name).to.be.undefined;
493
+ });
494
+
495
+ it('should handle zero-operand operators in fromFormData', () => {
496
+ const formData = {
497
+ uuid: 'test-node-uuid',
498
+ operand: '@input.text',
499
+ rules: [
500
+ {
501
+ operator: { value: 'has_number', name: 'has a number' },
502
+ value1: '',
503
+ value2: '',
504
+ category: 'Has Number'
505
+ }
506
+ ],
507
+ result_name: ''
508
+ };
509
+
510
+ const originalNode: Node = {
511
+ uuid: 'test-node-uuid',
512
+ actions: [],
513
+ router: {
514
+ type: 'switch',
515
+ operand: '@input.text',
516
+ cases: [],
517
+ categories: [],
518
+ default_category_uuid: ''
519
+ },
520
+ exits: []
521
+ };
522
+
523
+ const resultNode = split_by_expression.fromFormData!(
524
+ formData,
525
+ originalNode
526
+ );
527
+
528
+ expect(resultNode.router!.cases).to.have.lengthOf(1);
529
+ expect(resultNode.router!.cases![0].type).to.equal('has_number');
530
+ expect(resultNode.router!.cases![0].arguments).to.deep.equal([]);
531
+ });
532
+
533
+ it('should filter out incomplete rules', () => {
534
+ const formData = {
535
+ uuid: 'test-node-uuid',
536
+ operand: '@fields.age',
537
+ rules: [
538
+ {
539
+ operator: {
540
+ value: 'has_number_gte',
541
+ name: 'has a number at or above'
542
+ },
543
+ value1: '18',
544
+ value2: '',
545
+ category: 'Adult'
546
+ },
547
+ {
548
+ operator: { value: 'has_number_lt', name: 'has a number below' },
549
+ value1: '', // Missing value1
550
+ value2: '',
551
+ category: 'Child'
552
+ },
553
+ {
554
+ operator: {
555
+ value: 'has_number_between',
556
+ name: 'has a number between'
557
+ },
558
+ value1: '10',
559
+ value2: '', // Missing value2 for 2-operand operator
560
+ category: 'Teen'
561
+ }
562
+ ],
563
+ result_name: ''
564
+ };
565
+
566
+ const originalNode: Node = {
567
+ uuid: 'test-node-uuid',
568
+ actions: [],
569
+ router: {
570
+ type: 'switch',
571
+ operand: '@input.text',
572
+ cases: [],
573
+ categories: [],
574
+ default_category_uuid: ''
575
+ },
576
+ exits: []
577
+ };
578
+
579
+ const resultNode = split_by_expression.fromFormData!(
580
+ formData,
581
+ originalNode
582
+ );
583
+
584
+ // Only the first complete rule should be included
585
+ expect(resultNode.router!.cases).to.have.lengthOf(1);
586
+ expect(resultNode.router!.cases![0].type).to.equal('has_number_gte');
587
+ expect(resultNode.router!.categories).to.have.lengthOf(2); // Adult + Other
588
+ });
589
+
590
+ it('should handle string operator format', () => {
591
+ const formData = {
592
+ uuid: 'test-node-uuid',
593
+ operand: '@fields.name',
594
+ rules: [
595
+ {
596
+ operator: 'has_any_word', // String format instead of object
597
+ value1: 'john',
598
+ value2: '',
599
+ category: 'Name'
600
+ }
601
+ ],
602
+ result_name: ''
603
+ };
604
+
605
+ const originalNode: Node = {
606
+ uuid: 'test-node-uuid',
607
+ actions: [],
608
+ router: {
609
+ type: 'switch',
610
+ operand: '@input.text',
611
+ cases: [],
612
+ categories: [],
613
+ default_category_uuid: ''
614
+ },
615
+ exits: []
616
+ };
617
+
618
+ const resultNode = split_by_expression.fromFormData!(
619
+ formData,
620
+ originalNode
621
+ );
622
+
623
+ expect(resultNode.router!.cases).to.have.lengthOf(1);
624
+ expect(resultNode.router!.cases![0].type).to.equal('has_any_word');
625
+ expect(resultNode.router!.cases![0].arguments).to.deep.equal(['john']);
626
+ });
627
+
628
+ it('should default operand to @input.text if not provided', () => {
629
+ const formData = {
630
+ uuid: 'test-node-uuid',
631
+ operand: '', // Empty operand
632
+ rules: [],
633
+ result_name: ''
634
+ };
635
+
636
+ const originalNode: Node = {
637
+ uuid: 'test-node-uuid',
638
+ actions: [],
639
+ router: {
640
+ type: 'switch',
641
+ operand: '@input.text',
642
+ cases: [],
643
+ categories: [],
644
+ default_category_uuid: ''
645
+ },
646
+ exits: []
647
+ };
648
+
649
+ const resultNode = split_by_expression.fromFormData!(
650
+ formData,
651
+ originalNode
652
+ );
653
+
654
+ expect(resultNode.router!.operand).to.equal('@input.text');
655
+ });
656
+ });
657
+
658
+ describe('validate', () => {
659
+ it('should validate that operand is required', () => {
660
+ const formData = {
661
+ operand: '',
662
+ rules: []
663
+ };
664
+
665
+ const result = split_by_expression.validate!(formData);
666
+
667
+ expect(result.valid).to.be.false;
668
+ expect(result.errors.operand).to.exist;
669
+ });
670
+
671
+ it('should validate successfully with operand', () => {
672
+ const formData = {
673
+ operand: '@fields.age',
674
+ rules: []
675
+ };
676
+
677
+ const result = split_by_expression.validate!(formData);
678
+
679
+ expect(result.valid).to.be.true;
680
+ expect(Object.keys(result.errors)).to.have.lengthOf(0);
681
+ });
682
+ });
683
+
684
+ describe('round-trip transformation', () => {
685
+ it('should preserve node structure through toFormData -> fromFormData', () => {
686
+ const originalNode: Node = {
687
+ uuid: 'test-node-uuid',
688
+ actions: [],
689
+ router: {
690
+ type: 'switch',
691
+ operand: '@fields.temperature',
692
+ result_name: 'Temp Category',
693
+ cases: [
694
+ {
695
+ uuid: 'case-hot',
696
+ type: 'has_number_gt',
697
+ arguments: ['30'],
698
+ category_uuid: 'cat-hot'
699
+ },
700
+ {
701
+ uuid: 'case-cold',
702
+ type: 'has_number_lt',
703
+ arguments: ['10'],
704
+ category_uuid: 'cat-cold'
705
+ }
706
+ ],
707
+ categories: [
708
+ { uuid: 'cat-hot', name: 'Hot', exit_uuid: 'exit-hot' },
709
+ { uuid: 'cat-cold', name: 'Cold', exit_uuid: 'exit-cold' },
710
+ { uuid: 'cat-other', name: 'Other', exit_uuid: 'exit-other' }
711
+ ],
712
+ default_category_uuid: 'cat-other'
713
+ },
714
+ exits: [
715
+ { uuid: 'exit-hot', destination_uuid: 'next-1' },
716
+ { uuid: 'exit-cold', destination_uuid: 'next-2' },
717
+ { uuid: 'exit-other', destination_uuid: 'next-3' }
718
+ ]
719
+ };
720
+
721
+ const formData = split_by_expression.toFormData!(originalNode);
722
+ const resultNode = split_by_expression.fromFormData!(
723
+ formData,
724
+ originalNode
725
+ );
726
+
727
+ // Check basic structure
728
+ expect(resultNode.uuid).to.equal(originalNode.uuid);
729
+ expect(resultNode.router!.operand).to.equal(originalNode.router!.operand);
730
+ expect(resultNode.router!.result_name).to.equal(
731
+ originalNode.router!.result_name
732
+ );
733
+
734
+ // Check that all UUIDs are preserved
735
+ expect(resultNode.router!.categories).to.have.lengthOf(3);
736
+ expect(resultNode.exits).to.have.lengthOf(3);
737
+
738
+ const hotCategory = resultNode.router!.categories!.find(
739
+ (cat) => cat.name === 'Hot'
740
+ );
741
+ expect(hotCategory!.uuid).to.equal('cat-hot');
742
+ expect(hotCategory!.exit_uuid).to.equal('exit-hot');
743
+
744
+ // Check destinations are preserved
745
+ const hotExit = resultNode.exits!.find(
746
+ (exit) => exit.uuid === 'exit-hot'
747
+ );
748
+ expect(hotExit!.destination_uuid).to.equal('next-1');
749
+ });
750
+ });
751
+ });