@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,1109 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { split_by_run_result } from '../../src/flow/nodes/split_by_run_result';
3
+ import { Node } from '../../src/store/flow-definition';
4
+ import { NodeTest } from '../NodeHelper';
5
+ import { zustand } from '../../src/store/AppState';
6
+
7
+ /**
8
+ * Test suite for the split_by_run_result node configuration.
9
+ */
10
+ describe('split_by_run_result node config', () => {
11
+ const helper = new NodeTest(split_by_run_result, 'split_by_run_result');
12
+
13
+ // Setup mock flow results in the store before each test
14
+ beforeEach(() => {
15
+ // Mock the store with flow results
16
+ zustand.setState({
17
+ flowInfo: {
18
+ results: [
19
+ {
20
+ key: 'favorite_color',
21
+ name: 'Favorite Color',
22
+ categories: ['Red', 'Blue', 'Green'],
23
+ node_uuids: ['node-1']
24
+ },
25
+ {
26
+ key: 'age',
27
+ name: 'Age',
28
+ categories: ['Adult', 'Teen', 'Child'],
29
+ node_uuids: ['node-2']
30
+ }
31
+ ],
32
+ dependencies: [],
33
+ counts: { nodes: 0, languages: 0 },
34
+ locals: []
35
+ }
36
+ } as any);
37
+ });
38
+
39
+ describe('basic properties', () => {
40
+ helper.testBasicProperties();
41
+
42
+ it('has correct name', () => {
43
+ expect(split_by_run_result.name).to.equal('Split by Result');
44
+ });
45
+
46
+ it('has correct type', () => {
47
+ expect(split_by_run_result.type).to.equal('split_by_run_result');
48
+ });
49
+
50
+ it('has correct dialog size', () => {
51
+ expect(split_by_run_result.dialogSize).to.equal('large');
52
+ });
53
+ });
54
+
55
+ describe('toFormData', () => {
56
+ it('should transform node with rules to form data correctly', () => {
57
+ const node: Node = {
58
+ uuid: 'test-node-uuid',
59
+ actions: [],
60
+ router: {
61
+ type: 'switch',
62
+ operand: '@results.favorite_color',
63
+ cases: [
64
+ {
65
+ uuid: 'case-1',
66
+ type: 'has_phrase',
67
+ arguments: ['red'],
68
+ category_uuid: 'cat-1'
69
+ },
70
+ {
71
+ uuid: 'case-2',
72
+ type: 'has_any_word',
73
+ arguments: ['blue', 'azure'],
74
+ category_uuid: 'cat-2'
75
+ }
76
+ ],
77
+ categories: [
78
+ { uuid: 'cat-1', name: 'Red', exit_uuid: 'exit-1' },
79
+ { uuid: 'cat-2', name: 'Blue', exit_uuid: 'exit-2' },
80
+ { uuid: 'cat-other', name: 'Other', exit_uuid: 'exit-other' }
81
+ ],
82
+ default_category_uuid: 'cat-other',
83
+ result_name: 'color_category'
84
+ },
85
+ exits: [
86
+ { uuid: 'exit-1', destination_uuid: null },
87
+ { uuid: 'exit-2', destination_uuid: null },
88
+ { uuid: 'exit-other', destination_uuid: null }
89
+ ]
90
+ };
91
+
92
+ const nodeUI = {
93
+ config: {
94
+ operand: {
95
+ value: 'favorite_color',
96
+ name: 'Favorite Color'
97
+ }
98
+ }
99
+ };
100
+
101
+ const formData = split_by_run_result.toFormData!(node, nodeUI);
102
+
103
+ expect(formData.uuid).to.equal('test-node-uuid');
104
+ expect(formData.result).to.be.an('array');
105
+ expect(formData.result[0].value).to.equal('favorite_color');
106
+ expect(formData.result[0].name).to.equal('Favorite Color');
107
+ expect(formData.rules).to.have.lengthOf(2);
108
+
109
+ // Check first rule
110
+ expect(formData.rules[0].operator.value).to.equal('has_phrase');
111
+ expect(formData.rules[0].value1).to.equal('red');
112
+ expect(formData.rules[0].category).to.equal('Red');
113
+
114
+ // Check second rule
115
+ expect(formData.rules[1].operator.value).to.equal('has_any_word');
116
+ expect(formData.rules[1].value1).to.equal('blue azure');
117
+ expect(formData.rules[1].category).to.equal('Blue');
118
+
119
+ expect(formData.result_name).to.equal('color_category');
120
+ });
121
+
122
+ it('should transform node with no rules to form data correctly', () => {
123
+ const node: Node = {
124
+ uuid: 'test-node-uuid',
125
+ actions: [],
126
+ router: {
127
+ type: 'switch',
128
+ operand: '@results.age',
129
+ cases: [],
130
+ categories: [
131
+ {
132
+ uuid: 'cat-all',
133
+ name: 'All Responses',
134
+ exit_uuid: 'exit-all'
135
+ }
136
+ ],
137
+ default_category_uuid: 'cat-all'
138
+ },
139
+ exits: [{ uuid: 'exit-all', destination_uuid: null }]
140
+ };
141
+
142
+ const nodeUI = {
143
+ config: {
144
+ operand: {
145
+ value: 'age',
146
+ name: 'Age'
147
+ }
148
+ }
149
+ };
150
+
151
+ const formData = split_by_run_result.toFormData!(node, nodeUI);
152
+
153
+ expect(formData.uuid).to.equal('test-node-uuid');
154
+ expect(formData.result).to.be.an('array');
155
+ expect(formData.result[0].value).to.equal('age');
156
+ expect(formData.result[0].name).to.equal('Age');
157
+ expect(formData.rules).to.have.lengthOf(0);
158
+ expect(formData.result_name).to.equal('');
159
+ });
160
+
161
+ it('should handle missing nodeUI config gracefully', () => {
162
+ const node: Node = {
163
+ uuid: 'test-node-uuid',
164
+ actions: [],
165
+ router: {
166
+ type: 'switch',
167
+ operand: '@results.favorite_color',
168
+ cases: [],
169
+ categories: [
170
+ {
171
+ uuid: 'cat-all',
172
+ name: 'All Responses',
173
+ exit_uuid: 'exit-all'
174
+ }
175
+ ],
176
+ default_category_uuid: 'cat-all'
177
+ },
178
+ exits: [{ uuid: 'exit-all', destination_uuid: null }]
179
+ };
180
+
181
+ const formData = split_by_run_result.toFormData!(node);
182
+
183
+ expect(formData.uuid).to.equal('test-node-uuid');
184
+ expect(formData.result).to.be.an('array');
185
+ expect(formData.result).to.have.lengthOf(0);
186
+ });
187
+
188
+ it('should transform node with two-operand operators correctly', () => {
189
+ const node: Node = {
190
+ uuid: 'test-node-uuid',
191
+ actions: [],
192
+ router: {
193
+ type: 'switch',
194
+ operand: '@results.age',
195
+ cases: [
196
+ {
197
+ uuid: 'case-1',
198
+ type: 'has_number_between',
199
+ arguments: ['13', '17'],
200
+ category_uuid: 'cat-1'
201
+ }
202
+ ],
203
+ categories: [
204
+ { uuid: 'cat-1', name: 'Teen', exit_uuid: 'exit-1' },
205
+ { uuid: 'cat-other', name: 'Other', exit_uuid: 'exit-other' }
206
+ ],
207
+ default_category_uuid: 'cat-other'
208
+ },
209
+ exits: [
210
+ { uuid: 'exit-1', destination_uuid: null },
211
+ { uuid: 'exit-other', destination_uuid: null }
212
+ ]
213
+ };
214
+
215
+ const nodeUI = {
216
+ config: {
217
+ operand: {
218
+ value: 'age',
219
+ name: 'Age'
220
+ }
221
+ }
222
+ };
223
+
224
+ const formData = split_by_run_result.toFormData!(node, nodeUI);
225
+
226
+ expect(formData.rules).to.have.lengthOf(1);
227
+ expect(formData.rules[0].operator.value).to.equal('has_number_between');
228
+ expect(formData.rules[0].value1).to.equal('13');
229
+ expect(formData.rules[0].value2).to.equal('17');
230
+ expect(formData.rules[0].category).to.equal('Teen');
231
+ });
232
+
233
+ it('should transform node with zero-operand operators correctly', () => {
234
+ const node: Node = {
235
+ uuid: 'test-node-uuid',
236
+ actions: [],
237
+ router: {
238
+ type: 'switch',
239
+ operand: '@results.age',
240
+ cases: [
241
+ {
242
+ uuid: 'case-1',
243
+ type: 'has_number',
244
+ arguments: [],
245
+ category_uuid: 'cat-1'
246
+ }
247
+ ],
248
+ categories: [
249
+ { uuid: 'cat-1', name: 'Has Number', exit_uuid: 'exit-1' },
250
+ { uuid: 'cat-other', name: 'Other', exit_uuid: 'exit-other' }
251
+ ],
252
+ default_category_uuid: 'cat-other'
253
+ },
254
+ exits: [
255
+ { uuid: 'exit-1', destination_uuid: null },
256
+ { uuid: 'exit-other', destination_uuid: null }
257
+ ]
258
+ };
259
+
260
+ const nodeUI = {
261
+ config: {
262
+ operand: {
263
+ value: 'age',
264
+ name: 'Age'
265
+ }
266
+ }
267
+ };
268
+
269
+ const formData = split_by_run_result.toFormData!(node, nodeUI);
270
+
271
+ expect(formData.rules).to.have.lengthOf(1);
272
+ expect(formData.rules[0].operator.value).to.equal('has_number');
273
+ expect(formData.rules[0].value1).to.equal('');
274
+ expect(formData.rules[0].value2).to.equal('');
275
+ });
276
+ });
277
+
278
+ describe('fromFormData', () => {
279
+ it('should transform form data with rules to node correctly', () => {
280
+ const formData = {
281
+ uuid: 'test-node-uuid',
282
+ result: [{ value: 'favorite_color', name: 'Favorite Color' }],
283
+ rules: [
284
+ {
285
+ operator: { value: 'has_phrase', name: 'has the phrase' },
286
+ value1: 'red',
287
+ value2: '',
288
+ category: 'Red'
289
+ },
290
+ {
291
+ operator: { value: 'has_any_word', name: 'has any of the words' },
292
+ value1: 'blue azure',
293
+ value2: '',
294
+ category: 'Blue'
295
+ }
296
+ ],
297
+ result_name: 'color_category'
298
+ };
299
+
300
+ const originalNode: Node = {
301
+ uuid: 'test-node-uuid',
302
+ actions: [],
303
+ router: {
304
+ type: 'switch',
305
+ operand: '@results.old_result',
306
+ cases: [],
307
+ categories: [],
308
+ default_category_uuid: ''
309
+ },
310
+ exits: []
311
+ };
312
+
313
+ const resultNode = split_by_run_result.fromFormData!(
314
+ formData,
315
+ originalNode
316
+ );
317
+
318
+ expect(resultNode.uuid).to.equal('test-node-uuid');
319
+ expect(resultNode.router).to.exist;
320
+ expect(resultNode.router!.type).to.equal('switch');
321
+ expect(resultNode.router!.operand).to.equal('@results.favorite_color');
322
+ expect(resultNode.router!.result_name).to.equal('color_category');
323
+
324
+ // Verify cases
325
+ expect(resultNode.router!.cases).to.have.lengthOf(2);
326
+ expect(resultNode.router!.cases![0].type).to.equal('has_phrase');
327
+ expect(resultNode.router!.cases![0].arguments).to.deep.equal(['red']);
328
+ expect(resultNode.router!.cases![1].type).to.equal('has_any_word');
329
+ expect(resultNode.router!.cases![1].arguments).to.deep.equal([
330
+ 'blue',
331
+ 'azure'
332
+ ]);
333
+
334
+ // Verify categories
335
+ expect(resultNode.router!.categories).to.have.lengthOf(3); // Red, Blue, Other
336
+ expect(resultNode.router!.categories[0].name).to.equal('Red');
337
+ expect(resultNode.router!.categories[1].name).to.equal('Blue');
338
+ expect(resultNode.router!.categories[2].name).to.equal('Other');
339
+
340
+ // Verify exits
341
+ expect(resultNode.exits).to.have.lengthOf(3);
342
+ });
343
+
344
+ it('should transform form data without rules to node correctly', () => {
345
+ const formData = {
346
+ uuid: 'test-node-uuid',
347
+ result: [{ value: 'age', name: 'Age' }],
348
+ rules: [],
349
+ result_name: ''
350
+ };
351
+
352
+ const originalNode: Node = {
353
+ uuid: 'test-node-uuid',
354
+ actions: [],
355
+ router: {
356
+ type: 'switch',
357
+ operand: '@results.old_result',
358
+ cases: [],
359
+ categories: [],
360
+ default_category_uuid: ''
361
+ },
362
+ exits: []
363
+ };
364
+
365
+ const resultNode = split_by_run_result.fromFormData!(
366
+ formData,
367
+ originalNode
368
+ );
369
+
370
+ expect(resultNode.uuid).to.equal('test-node-uuid');
371
+ expect(resultNode.router).to.exist;
372
+ expect(resultNode.router!.type).to.equal('switch');
373
+ expect(resultNode.router!.operand).to.equal('@results.age');
374
+ expect(resultNode.router!.result_name).to.be.undefined;
375
+
376
+ // Should have only the default 'All Responses' category (no rules means "All Responses")
377
+ expect(resultNode.router!.categories).to.have.lengthOf(1);
378
+ expect(resultNode.router!.categories[0].name).to.equal('All Responses');
379
+
380
+ // Should have one exit
381
+ expect(resultNode.exits).to.have.lengthOf(1);
382
+ });
383
+
384
+ it('should not set result_name if empty', () => {
385
+ const formData = {
386
+ uuid: 'test-node-uuid',
387
+ result: [{ value: 'favorite_color', name: 'Favorite Color' }],
388
+ rules: [],
389
+ result_name: ''
390
+ };
391
+
392
+ const originalNode: Node = {
393
+ uuid: 'test-node-uuid',
394
+ actions: [],
395
+ router: {
396
+ type: 'switch',
397
+ operand: '@results.old_result',
398
+ cases: [],
399
+ categories: [],
400
+ default_category_uuid: ''
401
+ },
402
+ exits: []
403
+ };
404
+
405
+ const resultNode = split_by_run_result.fromFormData!(
406
+ formData,
407
+ originalNode
408
+ );
409
+
410
+ expect(resultNode.router!.result_name).to.be.undefined;
411
+ });
412
+
413
+ it('should preserve existing UUIDs when updating categories', () => {
414
+ const formData = {
415
+ uuid: 'test-node-uuid',
416
+ result: [{ value: 'favorite_color', name: 'Favorite Color' }],
417
+ rules: [
418
+ {
419
+ operator: { value: 'has_phrase', name: 'has the phrase' },
420
+ value1: 'red',
421
+ value2: '',
422
+ category: 'Red'
423
+ }
424
+ ],
425
+ result_name: ''
426
+ };
427
+
428
+ const originalNode: Node = {
429
+ uuid: 'test-node-uuid',
430
+ actions: [],
431
+ router: {
432
+ type: 'switch',
433
+ operand: '@results.favorite_color',
434
+ cases: [
435
+ {
436
+ uuid: 'existing-case-uuid',
437
+ type: 'has_phrase',
438
+ arguments: ['red'],
439
+ category_uuid: 'existing-cat-uuid'
440
+ }
441
+ ],
442
+ categories: [
443
+ {
444
+ uuid: 'existing-cat-uuid',
445
+ name: 'Red',
446
+ exit_uuid: 'existing-exit-uuid'
447
+ },
448
+ {
449
+ uuid: 'existing-other-uuid',
450
+ name: 'Other',
451
+ exit_uuid: 'existing-other-exit-uuid'
452
+ }
453
+ ],
454
+ default_category_uuid: 'existing-other-uuid'
455
+ },
456
+ exits: [
457
+ { uuid: 'existing-exit-uuid', destination_uuid: 'some-node' },
458
+ { uuid: 'existing-other-exit-uuid', destination_uuid: null }
459
+ ]
460
+ };
461
+
462
+ const resultNode = split_by_run_result.fromFormData!(
463
+ formData,
464
+ originalNode
465
+ );
466
+
467
+ // Should preserve existing category UUIDs
468
+ const redCategory = resultNode.router!.categories.find(
469
+ (cat) => cat.name === 'Red'
470
+ );
471
+ expect(redCategory!.uuid).to.equal('existing-cat-uuid');
472
+ expect(redCategory!.exit_uuid).to.equal('existing-exit-uuid');
473
+
474
+ const otherCategory = resultNode.router!.categories.find(
475
+ (cat) => cat.name === 'Other'
476
+ );
477
+ expect(otherCategory!.uuid).to.equal('existing-other-uuid');
478
+ expect(otherCategory!.exit_uuid).to.equal('existing-other-exit-uuid');
479
+
480
+ // Should preserve exit destinations
481
+ const redExit = resultNode.exits.find(
482
+ (exit) => exit.uuid === 'existing-exit-uuid'
483
+ );
484
+ expect(redExit!.destination_uuid).to.equal('some-node');
485
+ });
486
+
487
+ it('should handle form data without result selection', () => {
488
+ const formData = {
489
+ uuid: 'test-node-uuid',
490
+ result: [],
491
+ rules: [],
492
+ result_name: ''
493
+ };
494
+
495
+ const originalNode: Node = {
496
+ uuid: 'test-node-uuid',
497
+ actions: [],
498
+ router: {
499
+ type: 'switch',
500
+ operand: '@results.old_result',
501
+ cases: [],
502
+ categories: [],
503
+ default_category_uuid: ''
504
+ },
505
+ exits: []
506
+ };
507
+
508
+ const resultNode = split_by_run_result.fromFormData!(
509
+ formData,
510
+ originalNode
511
+ );
512
+
513
+ // Should return the original node unchanged
514
+ expect(resultNode).to.equal(originalNode);
515
+ });
516
+
517
+ it('should handle incomplete rules gracefully', () => {
518
+ const formData = {
519
+ uuid: 'test-node-uuid',
520
+ result: [{ value: 'age', name: 'Age' }],
521
+ rules: [
522
+ {
523
+ operator: { value: 'has_number_gte', name: 'has a number above' },
524
+ value1: '18',
525
+ value2: '',
526
+ category: 'Adult'
527
+ },
528
+ {
529
+ operator: { value: 'has_number_lt', name: 'has a number below' },
530
+ value1: '', // Missing value1
531
+ value2: '',
532
+ category: 'Child'
533
+ },
534
+ {
535
+ operator: {
536
+ value: 'has_number_between',
537
+ name: 'has a number between'
538
+ },
539
+ value1: '10',
540
+ value2: '', // Missing value2 for 2-operand operator
541
+ category: 'Teen'
542
+ }
543
+ ],
544
+ result_name: ''
545
+ };
546
+
547
+ const originalNode: Node = {
548
+ uuid: 'test-node-uuid',
549
+ actions: [],
550
+ router: {
551
+ type: 'switch',
552
+ operand: '@results.age',
553
+ cases: [],
554
+ categories: [],
555
+ default_category_uuid: ''
556
+ },
557
+ exits: []
558
+ };
559
+
560
+ const resultNode = split_by_run_result.fromFormData!(
561
+ formData,
562
+ originalNode
563
+ );
564
+
565
+ // Only the first complete rule should be included
566
+ expect(resultNode.router!.cases).to.have.lengthOf(1);
567
+ expect(resultNode.router!.cases![0].type).to.equal('has_number_gte');
568
+
569
+ // Should have Adult category and Other
570
+ expect(resultNode.router!.categories).to.have.lengthOf(2);
571
+ expect(resultNode.router!.categories[0].name).to.equal('Adult');
572
+ expect(resultNode.router!.categories[1].name).to.equal('Other');
573
+ });
574
+
575
+ it('should handle two-operand operators correctly', () => {
576
+ const formData = {
577
+ uuid: 'test-node-uuid',
578
+ result: [{ value: 'age', name: 'Age' }],
579
+ rules: [
580
+ {
581
+ operator: {
582
+ value: 'has_number_between',
583
+ name: 'has a number between'
584
+ },
585
+ value1: '13',
586
+ value2: '17',
587
+ category: 'Teen'
588
+ }
589
+ ],
590
+ result_name: ''
591
+ };
592
+
593
+ const originalNode: Node = {
594
+ uuid: 'test-node-uuid',
595
+ actions: [],
596
+ router: {
597
+ type: 'switch',
598
+ operand: '@results.age',
599
+ cases: [],
600
+ categories: [],
601
+ default_category_uuid: ''
602
+ },
603
+ exits: []
604
+ };
605
+
606
+ const resultNode = split_by_run_result.fromFormData!(
607
+ formData,
608
+ originalNode
609
+ );
610
+
611
+ expect(resultNode.router!.cases).to.have.lengthOf(1);
612
+ expect(resultNode.router!.cases![0].type).to.equal('has_number_between');
613
+ expect(resultNode.router!.cases![0].arguments).to.deep.equal([
614
+ '13',
615
+ '17'
616
+ ]);
617
+ });
618
+ });
619
+
620
+ describe('validation', () => {
621
+ it('should validate that a result is required', () => {
622
+ const formData = {
623
+ result: [],
624
+ rules: [],
625
+ result_name: ''
626
+ };
627
+
628
+ const validation = split_by_run_result.validate!(formData);
629
+
630
+ expect(validation.valid).to.be.false;
631
+ expect(validation.errors).to.have.property('result');
632
+ expect(validation.errors.result).to.equal('A flow result is required');
633
+ });
634
+
635
+ it('should pass validation with valid result', () => {
636
+ const formData = {
637
+ result: [{ value: 'favorite_color', name: 'Favorite Color' }],
638
+ rules: [],
639
+ result_name: ''
640
+ };
641
+
642
+ const validation = split_by_run_result.validate!(formData);
643
+
644
+ expect(validation.valid).to.be.true;
645
+ expect(validation.errors).to.deep.equal({});
646
+ });
647
+
648
+ it('should pass validation with result and rules', () => {
649
+ const formData = {
650
+ result: [{ value: 'age', name: 'Age' }],
651
+ rules: [
652
+ {
653
+ operator: { value: 'has_number_gte', name: 'has a number above' },
654
+ value1: '18',
655
+ value2: '',
656
+ category: 'Adult'
657
+ }
658
+ ],
659
+ result_name: 'age_category'
660
+ };
661
+
662
+ const validation = split_by_run_result.validate!(formData);
663
+
664
+ expect(validation.valid).to.be.true;
665
+ expect(validation.errors).to.deep.equal({});
666
+ });
667
+ });
668
+
669
+ describe('delimiter functionality', () => {
670
+ describe('toFormData', () => {
671
+ it('should extract delimiter configuration when enabled', () => {
672
+ const node: Node = {
673
+ uuid: 'test-node-uuid',
674
+ actions: [],
675
+ router: {
676
+ type: 'switch',
677
+ operand: '@(field(results.favorite_color, 2, "."))',
678
+ cases: [
679
+ {
680
+ uuid: 'case-1',
681
+ type: 'has_phrase',
682
+ arguments: ['red'],
683
+ category_uuid: 'cat-1'
684
+ }
685
+ ],
686
+ categories: [
687
+ { uuid: 'cat-1', name: 'Red', exit_uuid: 'exit-1' },
688
+ { uuid: 'cat-other', name: 'Other', exit_uuid: 'exit-other' }
689
+ ],
690
+ default_category_uuid: 'cat-other',
691
+ result_name: 'color_category'
692
+ },
693
+ exits: [
694
+ { uuid: 'exit-1', destination_uuid: null },
695
+ { uuid: 'exit-other', destination_uuid: null }
696
+ ]
697
+ };
698
+
699
+ const nodeUI = {
700
+ config: {
701
+ operand: {
702
+ value: 'favorite_color',
703
+ name: 'Favorite Color'
704
+ },
705
+ index: 2,
706
+ delimiter: '.'
707
+ }
708
+ };
709
+
710
+ const formData = split_by_run_result.toFormData!(node, nodeUI);
711
+
712
+ expect(formData.delimit_by).to.be.an('array');
713
+ expect(formData.delimit_by[0].value).to.equal('.');
714
+ expect(formData.delimit_by[0].name).to.equal('Delimited by periods');
715
+ expect(formData.delimit_index).to.be.an('array');
716
+ expect(formData.delimit_index[0].value).to.equal('2');
717
+ expect(formData.delimit_index[0].name).to.equal('third result');
718
+ });
719
+
720
+ it('should handle delimiter not enabled', () => {
721
+ const node: Node = {
722
+ uuid: 'test-node-uuid',
723
+ actions: [],
724
+ router: {
725
+ type: 'switch',
726
+ operand: '@results.favorite_color',
727
+ cases: [],
728
+ categories: [
729
+ {
730
+ uuid: 'cat-all',
731
+ name: 'All Responses',
732
+ exit_uuid: 'exit-all'
733
+ }
734
+ ],
735
+ default_category_uuid: 'cat-all'
736
+ },
737
+ exits: [{ uuid: 'exit-all', destination_uuid: null }]
738
+ };
739
+
740
+ const nodeUI = {
741
+ config: {
742
+ operand: {
743
+ value: 'favorite_color',
744
+ name: 'Favorite Color'
745
+ }
746
+ }
747
+ };
748
+
749
+ const formData = split_by_run_result.toFormData!(node, nodeUI);
750
+
751
+ expect(formData.delimit_by).to.be.an('array');
752
+ expect(formData.delimit_by[0].value).to.equal('');
753
+ expect(formData.delimit_by[0].name).to.equal("Don't delimit result");
754
+ expect(formData.delimit_index).to.be.an('array');
755
+ expect(formData.delimit_index[0].value).to.equal('0');
756
+ });
757
+ });
758
+
759
+ describe('fromFormData', () => {
760
+ it('should generate operand with field() function when delimiter enabled', () => {
761
+ const formData = {
762
+ uuid: 'test-node-uuid',
763
+ result: [{ value: 'favorite_color', name: 'Favorite Color' }],
764
+ delimit_by: [{ value: '+', name: 'plusses' }],
765
+ delimit_index: [{ value: '1', name: 'second' }],
766
+ rules: [
767
+ {
768
+ operator: { value: 'has_phrase', name: 'has the phrase' },
769
+ value1: 'red',
770
+ value2: '',
771
+ category: 'Red'
772
+ }
773
+ ],
774
+ result_name: 'color_category'
775
+ };
776
+
777
+ const originalNode: Node = {
778
+ uuid: 'test-node-uuid',
779
+ actions: [],
780
+ router: {
781
+ type: 'switch',
782
+ operand: '@results.old_result',
783
+ cases: [],
784
+ categories: [],
785
+ default_category_uuid: ''
786
+ },
787
+ exits: []
788
+ };
789
+
790
+ const resultNode = split_by_run_result.fromFormData!(
791
+ formData,
792
+ originalNode
793
+ );
794
+
795
+ expect(resultNode.router!.operand).to.equal(
796
+ '@(field(results.favorite_color, 1, "+"))'
797
+ );
798
+ });
799
+
800
+ it('should generate standard operand when delimiter not enabled', () => {
801
+ const formData = {
802
+ uuid: 'test-node-uuid',
803
+ result: [{ value: 'favorite_color', name: 'Favorite Color' }],
804
+ delimit_by: [{ value: '', name: "Don't delimit" }],
805
+ delimit_index: [{ value: '0', name: 'first' }],
806
+ rules: [],
807
+ result_name: ''
808
+ };
809
+
810
+ const originalNode: Node = {
811
+ uuid: 'test-node-uuid',
812
+ actions: [],
813
+ router: {
814
+ type: 'switch',
815
+ operand: '@results.old_result',
816
+ cases: [],
817
+ categories: [],
818
+ default_category_uuid: ''
819
+ },
820
+ exits: []
821
+ };
822
+
823
+ const resultNode = split_by_run_result.fromFormData!(
824
+ formData,
825
+ originalNode
826
+ );
827
+
828
+ expect(resultNode.router!.operand).to.equal('@results.favorite_color');
829
+ });
830
+
831
+ it('should handle different field numbers correctly', () => {
832
+ const formData = {
833
+ uuid: 'test-node-uuid',
834
+ result: [{ value: 'age', name: 'Age' }],
835
+ delimit_by: [{ value: '.', name: 'periods' }],
836
+ delimit_index: [{ value: '9', name: 'tenth' }],
837
+ rules: [],
838
+ result_name: ''
839
+ };
840
+
841
+ const originalNode: Node = {
842
+ uuid: 'test-node-uuid',
843
+ actions: [],
844
+ router: {
845
+ type: 'switch',
846
+ operand: '@results.age',
847
+ cases: [],
848
+ categories: [],
849
+ default_category_uuid: ''
850
+ },
851
+ exits: []
852
+ };
853
+
854
+ const resultNode = split_by_run_result.fromFormData!(
855
+ formData,
856
+ originalNode
857
+ );
858
+
859
+ expect(resultNode.router!.operand).to.equal(
860
+ '@(field(results.age, 9, "."))'
861
+ );
862
+ });
863
+
864
+ it('should use default values when delimiter fields missing', () => {
865
+ const formData = {
866
+ uuid: 'test-node-uuid',
867
+ result: [{ value: 'favorite_color', name: 'Favorite Color' }],
868
+ delimit_by: [{ value: ' ', name: 'spaces' }],
869
+ rules: [],
870
+ result_name: ''
871
+ };
872
+
873
+ const originalNode: Node = {
874
+ uuid: 'test-node-uuid',
875
+ actions: [],
876
+ router: {
877
+ type: 'switch',
878
+ operand: '@results.old_result',
879
+ cases: [],
880
+ categories: [],
881
+ default_category_uuid: ''
882
+ },
883
+ exits: []
884
+ };
885
+
886
+ const resultNode = split_by_run_result.fromFormData!(
887
+ formData,
888
+ originalNode
889
+ );
890
+
891
+ expect(resultNode.router!.operand).to.equal(
892
+ '@(field(results.favorite_color, 0, " "))'
893
+ );
894
+ });
895
+ });
896
+
897
+ describe('toUIConfig', () => {
898
+ it('should persist delimiter configuration when enabled', () => {
899
+ const formData = {
900
+ result: [{ value: 'favorite_color', name: 'Favorite Color' }],
901
+ delimit_by: [{ value: '+', name: 'plusses' }],
902
+ delimit_index: [{ value: '3', name: 'fourth' }],
903
+ rules: [],
904
+ result_name: ''
905
+ };
906
+
907
+ const config = split_by_run_result.toUIConfig!(formData);
908
+
909
+ expect(config.operand).to.exist;
910
+ expect(config.operand.id).to.equal('favorite_color');
911
+ expect(config.index).to.equal(3);
912
+ expect(config.delimiter).to.equal('+');
913
+ });
914
+
915
+ it('should not include delimiter config when not enabled', () => {
916
+ const formData = {
917
+ result: [{ value: 'favorite_color', name: 'Favorite Color' }],
918
+ delimit_by: [{ value: '', name: "Don't delimit" }],
919
+ delimit_index: [{ value: '0', name: 'first' }],
920
+ rules: [],
921
+ result_name: ''
922
+ };
923
+
924
+ const config = split_by_run_result.toUIConfig!(formData);
925
+
926
+ expect(config.operand).to.exist;
927
+ expect(config.operand.id).to.equal('favorite_color');
928
+ expect(config.index).to.be.undefined;
929
+ expect(config.delimiter).to.be.undefined;
930
+ });
931
+ });
932
+
933
+ describe('round-trip tests', () => {
934
+ it('should preserve delimiter configuration through toFormData and fromFormData', () => {
935
+ const originalNode: Node = {
936
+ uuid: 'test-node-uuid',
937
+ actions: [],
938
+ router: {
939
+ type: 'switch',
940
+ operand: '@(field(results.favorite_color, 5, "."))',
941
+ cases: [
942
+ {
943
+ uuid: 'case-1',
944
+ type: 'has_phrase',
945
+ arguments: ['red'],
946
+ category_uuid: 'cat-1'
947
+ }
948
+ ],
949
+ categories: [
950
+ { uuid: 'cat-1', name: 'Red', exit_uuid: 'exit-1' },
951
+ { uuid: 'cat-other', name: 'Other', exit_uuid: 'exit-other' }
952
+ ],
953
+ default_category_uuid: 'cat-other',
954
+ result_name: 'color_category'
955
+ },
956
+ exits: [
957
+ { uuid: 'exit-1', destination_uuid: null },
958
+ { uuid: 'exit-other', destination_uuid: null }
959
+ ]
960
+ };
961
+
962
+ const nodeUI = {
963
+ config: {
964
+ operand: {
965
+ value: 'favorite_color',
966
+ name: 'Favorite Color'
967
+ },
968
+ index: 5,
969
+ delimiter: '.'
970
+ }
971
+ };
972
+
973
+ // Convert to form data
974
+ const formData = split_by_run_result.toFormData!(originalNode, nodeUI);
975
+
976
+ // Convert back to node
977
+ const resultNode = split_by_run_result.fromFormData!(
978
+ formData,
979
+ originalNode
980
+ );
981
+
982
+ // Verify operand is preserved
983
+ expect(resultNode.router!.operand).to.equal(
984
+ '@(field(results.favorite_color, 5, "."))'
985
+ );
986
+ });
987
+
988
+ it('should handle toggling delimiter on and off', () => {
989
+ const originalNode: Node = {
990
+ uuid: 'test-node-uuid',
991
+ actions: [],
992
+ router: {
993
+ type: 'switch',
994
+ operand: '@results.favorite_color',
995
+ cases: [],
996
+ categories: [
997
+ {
998
+ uuid: 'cat-all',
999
+ name: 'All Responses',
1000
+ exit_uuid: 'exit-all'
1001
+ }
1002
+ ],
1003
+ default_category_uuid: 'cat-all'
1004
+ },
1005
+ exits: [{ uuid: 'exit-all', destination_uuid: null }]
1006
+ };
1007
+
1008
+ // Start with delimiter disabled
1009
+ const nodeUI = {
1010
+ config: {
1011
+ operand: {
1012
+ value: 'favorite_color',
1013
+ name: 'Favorite Color'
1014
+ }
1015
+ }
1016
+ };
1017
+
1018
+ // Convert to form data
1019
+ const formData = split_by_run_result.toFormData!(originalNode, nodeUI);
1020
+ expect(formData.delimit_by[0].value).to.equal('');
1021
+
1022
+ // Enable delimiter
1023
+ formData.delimit_by = [{ value: '+', name: 'plusses' }];
1024
+ formData.delimit_index = [{ value: '2', name: 'third' }];
1025
+
1026
+ // Convert to node
1027
+ const resultNode = split_by_run_result.fromFormData!(
1028
+ formData,
1029
+ originalNode
1030
+ );
1031
+
1032
+ expect(resultNode.router!.operand).to.equal(
1033
+ '@(field(results.favorite_color, 2, "+"))'
1034
+ );
1035
+
1036
+ // Get UI config and verify it persists delimiter settings
1037
+ const config = split_by_run_result.toUIConfig!(formData);
1038
+ expect(config.index).to.equal(2);
1039
+ expect(config.delimiter).to.equal('+');
1040
+ });
1041
+
1042
+ it('should properly remove delimiter when switching from delimited to non-delimited', () => {
1043
+ // Start with a node that has delimiter enabled
1044
+ const nodeWithDelimiter: Node = {
1045
+ uuid: 'test-node-uuid',
1046
+ actions: [],
1047
+ router: {
1048
+ type: 'switch',
1049
+ operand: '@(field(results.favorite_color, 2, "."))',
1050
+ cases: [],
1051
+ categories: [
1052
+ {
1053
+ uuid: 'cat-all',
1054
+ name: 'All Responses',
1055
+ exit_uuid: 'exit-all'
1056
+ }
1057
+ ],
1058
+ default_category_uuid: 'cat-all'
1059
+ },
1060
+ exits: [{ uuid: 'exit-all', destination_uuid: null }]
1061
+ };
1062
+
1063
+ const nodeUI = {
1064
+ config: {
1065
+ operand: {
1066
+ value: 'favorite_color',
1067
+ name: 'Favorite Color'
1068
+ }
1069
+ }
1070
+ };
1071
+
1072
+ // Convert to form data - should show delimiter is enabled
1073
+ const formData = split_by_run_result.toFormData!(
1074
+ nodeWithDelimiter,
1075
+ nodeUI
1076
+ );
1077
+ expect(formData.delimit_by[0].value).to.equal('.');
1078
+ expect(formData.delimit_index[0].value).to.equal('2');
1079
+
1080
+ // Now remove the delimiter by selecting "Don't delimit result"
1081
+ formData.delimit_by = [{ value: '', name: "Don't delimit result" }];
1082
+
1083
+ // Convert back to node
1084
+ const resultNode = split_by_run_result.fromFormData!(
1085
+ formData,
1086
+ nodeWithDelimiter
1087
+ );
1088
+
1089
+ // Operand should now be standard without field() function
1090
+ expect(resultNode.router!.operand).to.equal('@results.favorite_color');
1091
+
1092
+ // Get UI config - should NOT have index or delimiter
1093
+ const config = split_by_run_result.toUIConfig!(formData);
1094
+ expect(config.index).to.be.undefined;
1095
+ expect(config.delimiter).to.be.undefined;
1096
+
1097
+ // Re-open the dialog - toFormData should show no delimiter
1098
+ const formDataReopened = split_by_run_result.toFormData!(
1099
+ resultNode,
1100
+ nodeUI
1101
+ );
1102
+ expect(formDataReopened.delimit_by[0].value).to.equal('');
1103
+ expect(formDataReopened.delimit_by[0].name).to.equal(
1104
+ "Don't delimit result"
1105
+ );
1106
+ });
1107
+ });
1108
+ });
1109
+ });