@nyaruka/temba-components 0.131.1 → 0.131.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (485) hide show
  1. package/.github/workflows/publish.yml +4 -1
  2. package/CHANGELOG.md +75 -1
  3. package/demo/components/floating-tabs/example.html +400 -0
  4. package/demo/components/flow/index.html +1 -1
  5. package/demo/data/flows/food-order.json +2 -2
  6. package/demo/data/flows/sample-flow.json +113 -125
  7. package/demo/data/flows/voicemail.json +613 -0
  8. package/demo/index.html +6 -0
  9. package/dist/locales/es.js +5 -5
  10. package/dist/locales/es.js.map +1 -1
  11. package/dist/locales/fr.js +5 -5
  12. package/dist/locales/fr.js.map +1 -1
  13. package/dist/locales/locale-codes.js +11 -2
  14. package/dist/locales/locale-codes.js.map +1 -1
  15. package/dist/locales/pt.js +5 -5
  16. package/dist/locales/pt.js.map +1 -1
  17. package/dist/static/svg/index.svg +1 -1
  18. package/dist/temba-components.js +1773 -662
  19. package/dist/temba-components.js.map +1 -1
  20. package/out-tsc/src/Icons.js +4 -1
  21. package/out-tsc/src/Icons.js.map +1 -1
  22. package/out-tsc/src/display/FloatingTab.js +167 -0
  23. package/out-tsc/src/display/FloatingTab.js.map +1 -0
  24. package/out-tsc/src/display/ProgressBar.js +22 -2
  25. package/out-tsc/src/display/ProgressBar.js.map +1 -1
  26. package/out-tsc/src/events.js.map +1 -1
  27. package/out-tsc/src/flow/CanvasMenu.js +200 -0
  28. package/out-tsc/src/flow/CanvasMenu.js.map +1 -0
  29. package/out-tsc/src/flow/CanvasNode.js +489 -47
  30. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  31. package/out-tsc/src/flow/Editor.js +1417 -67
  32. package/out-tsc/src/flow/Editor.js.map +1 -1
  33. package/out-tsc/src/flow/NodeEditor.js +479 -112
  34. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  35. package/out-tsc/src/flow/NodeTypeSelector.js +540 -0
  36. package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -0
  37. package/out-tsc/src/flow/StickyNote.js +12 -3
  38. package/out-tsc/src/flow/StickyNote.js.map +1 -1
  39. package/out-tsc/src/flow/actions/add_contact_groups.js +4 -3
  40. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
  41. package/out-tsc/src/flow/actions/add_contact_urn.js +63 -4
  42. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  43. package/out-tsc/src/flow/actions/add_input_labels.js +4 -3
  44. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  45. package/out-tsc/src/flow/actions/play_audio.js +3 -2
  46. package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
  47. package/out-tsc/src/flow/actions/remove_contact_groups.js +7 -5
  48. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
  49. package/out-tsc/src/flow/actions/request_optin.js +3 -2
  50. package/out-tsc/src/flow/actions/request_optin.js.map +1 -1
  51. package/out-tsc/src/flow/actions/say_msg.js +3 -2
  52. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  53. package/out-tsc/src/flow/actions/send_broadcast.js +77 -23
  54. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  55. package/out-tsc/src/flow/actions/send_email.js +5 -5
  56. package/out-tsc/src/flow/actions/send_email.js.map +1 -1
  57. package/out-tsc/src/flow/actions/send_msg.js +101 -21
  58. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  59. package/out-tsc/src/flow/actions/set_contact_channel.js +6 -9
  60. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  61. package/out-tsc/src/flow/actions/set_contact_field.js +20 -20
  62. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  63. package/out-tsc/src/flow/actions/set_contact_language.js +3 -2
  64. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  65. package/out-tsc/src/flow/actions/set_contact_name.js +3 -12
  66. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  67. package/out-tsc/src/flow/actions/set_contact_status.js +3 -2
  68. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  69. package/out-tsc/src/flow/actions/set_run_result.js +4 -3
  70. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  71. package/out-tsc/src/flow/actions/start_session.js +181 -6
  72. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  73. package/out-tsc/src/flow/config.js +11 -23
  74. package/out-tsc/src/flow/config.js.map +1 -1
  75. package/out-tsc/src/flow/currencies.js +45 -0
  76. package/out-tsc/src/flow/currencies.js.map +1 -0
  77. package/out-tsc/src/flow/nodes/shared-rules.js +257 -0
  78. package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -0
  79. package/out-tsc/src/flow/nodes/shared.js +71 -0
  80. package/out-tsc/src/flow/nodes/shared.js.map +1 -0
  81. package/out-tsc/src/flow/nodes/split_by_airtime.js +211 -5
  82. package/out-tsc/src/flow/nodes/split_by_airtime.js.map +1 -1
  83. package/out-tsc/src/flow/nodes/split_by_contact_field.js +152 -3
  84. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -1
  85. package/out-tsc/src/flow/nodes/split_by_expression.js +73 -2
  86. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -1
  87. package/out-tsc/src/flow/nodes/split_by_groups.js +18 -10
  88. package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -1
  89. package/out-tsc/src/flow/nodes/split_by_intent.js +8 -0
  90. package/out-tsc/src/flow/nodes/split_by_intent.js.map +1 -0
  91. package/out-tsc/src/flow/nodes/split_by_llm.js +11 -3
  92. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
  93. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +10 -3
  94. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  95. package/out-tsc/src/flow/nodes/split_by_random.js +10 -4
  96. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
  97. package/out-tsc/src/flow/nodes/split_by_resthook.js +113 -0
  98. package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -0
  99. package/out-tsc/src/flow/nodes/split_by_run_result.js +211 -3
  100. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
  101. package/out-tsc/src/flow/nodes/split_by_scheme.js +158 -2
  102. package/out-tsc/src/flow/nodes/split_by_scheme.js.map +1 -1
  103. package/out-tsc/src/flow/nodes/split_by_subflow.js +13 -5
  104. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  105. package/out-tsc/src/flow/nodes/split_by_ticket.js +10 -3
  106. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -1
  107. package/out-tsc/src/flow/nodes/split_by_webhook.js +10 -3
  108. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
  109. package/out-tsc/src/flow/nodes/wait_for_digits.js +3 -2
  110. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
  111. package/out-tsc/src/flow/nodes/wait_for_menu.js +3 -2
  112. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
  113. package/out-tsc/src/flow/nodes/wait_for_response.js +38 -568
  114. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  115. package/out-tsc/src/flow/types.js +86 -12
  116. package/out-tsc/src/flow/types.js.map +1 -1
  117. package/out-tsc/src/flow/utils.js +101 -14
  118. package/out-tsc/src/flow/utils.js.map +1 -1
  119. package/out-tsc/src/form/FieldRenderer.js +2 -4
  120. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  121. package/out-tsc/src/interfaces.js +3 -0
  122. package/out-tsc/src/interfaces.js.map +1 -1
  123. package/out-tsc/src/layout/FloatingWindow.js +346 -0
  124. package/out-tsc/src/layout/FloatingWindow.js.map +1 -0
  125. package/out-tsc/src/list/SortableList.js +98 -33
  126. package/out-tsc/src/list/SortableList.js.map +1 -1
  127. package/out-tsc/src/live/ContactChat.js +6 -25
  128. package/out-tsc/src/live/ContactChat.js.map +1 -1
  129. package/out-tsc/src/locales/es.js +5 -5
  130. package/out-tsc/src/locales/es.js.map +1 -1
  131. package/out-tsc/src/locales/fr.js +5 -5
  132. package/out-tsc/src/locales/fr.js.map +1 -1
  133. package/out-tsc/src/locales/locale-codes.js +11 -2
  134. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  135. package/out-tsc/src/locales/pt.js +5 -5
  136. package/out-tsc/src/locales/pt.js.map +1 -1
  137. package/out-tsc/src/store/AppState.js +120 -0
  138. package/out-tsc/src/store/AppState.js.map +1 -1
  139. package/out-tsc/src/utils.js +254 -13
  140. package/out-tsc/src/utils.js.map +1 -1
  141. package/out-tsc/temba-modules.js +8 -0
  142. package/out-tsc/temba-modules.js.map +1 -1
  143. package/out-tsc/test/ActionHelper.js +3 -3
  144. package/out-tsc/test/ActionHelper.js.map +1 -1
  145. package/out-tsc/test/NodeHelper.js +6 -3
  146. package/out-tsc/test/NodeHelper.js.map +1 -1
  147. package/out-tsc/test/actions/add_contact_urn.test.js +202 -0
  148. package/out-tsc/test/actions/add_contact_urn.test.js.map +1 -0
  149. package/out-tsc/test/actions/send_broadcast.test.js +148 -0
  150. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -0
  151. package/out-tsc/test/actions/send_email.test.js +17 -23
  152. package/out-tsc/test/actions/send_email.test.js.map +1 -1
  153. package/out-tsc/test/actions/send_msg.test.js +33 -15
  154. package/out-tsc/test/actions/send_msg.test.js.map +1 -1
  155. package/out-tsc/test/actions/start_session.test.js +116 -0
  156. package/out-tsc/test/actions/start_session.test.js.map +1 -0
  157. package/out-tsc/test/nodes/split_by_airtime.test.js +604 -0
  158. package/out-tsc/test/nodes/split_by_airtime.test.js.map +1 -0
  159. package/out-tsc/test/nodes/split_by_contact_field.test.js +387 -0
  160. package/out-tsc/test/nodes/split_by_contact_field.test.js.map +1 -0
  161. package/out-tsc/test/nodes/split_by_expression.test.js +614 -0
  162. package/out-tsc/test/nodes/split_by_expression.test.js.map +1 -0
  163. package/out-tsc/test/nodes/split_by_random.test.js +3 -3
  164. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -1
  165. package/out-tsc/test/nodes/split_by_resthook.test.js +337 -0
  166. package/out-tsc/test/nodes/split_by_resthook.test.js.map +1 -0
  167. package/out-tsc/test/nodes/split_by_run_result.test.js +920 -0
  168. package/out-tsc/test/nodes/split_by_run_result.test.js.map +1 -0
  169. package/out-tsc/test/nodes/split_by_scheme.test.js +399 -0
  170. package/out-tsc/test/nodes/split_by_scheme.test.js.map +1 -0
  171. package/out-tsc/test/nodes/split_by_subflow.test.js +333 -0
  172. package/out-tsc/test/nodes/split_by_subflow.test.js.map +1 -0
  173. package/out-tsc/test/nodes/wait_for_digits.test.js +2 -2
  174. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  175. package/out-tsc/test/nodes/wait_for_response.test.js +2 -1
  176. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -1
  177. package/out-tsc/test/temba-action-drag-between-nodes.test.js +252 -0
  178. package/out-tsc/test/temba-action-drag-between-nodes.test.js.map +1 -0
  179. package/out-tsc/test/temba-canvas-menu.test.js +122 -0
  180. package/out-tsc/test/temba-canvas-menu.test.js.map +1 -0
  181. package/out-tsc/test/temba-floating-tab.test.js +91 -0
  182. package/out-tsc/test/temba-floating-tab.test.js.map +1 -0
  183. package/out-tsc/test/temba-floating-window.test.js +301 -0
  184. package/out-tsc/test/temba-floating-window.test.js.map +1 -0
  185. package/out-tsc/test/temba-flow-editor-node.test.js +202 -2
  186. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  187. package/out-tsc/test/temba-flow-editor.test.js +7 -8
  188. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  189. package/out-tsc/test/temba-localization.test.js +471 -0
  190. package/out-tsc/test/temba-localization.test.js.map +1 -0
  191. package/out-tsc/test/temba-node-editor.test.js +3 -1
  192. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  193. package/out-tsc/test/temba-node-type-selector.test.js +265 -0
  194. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -0
  195. package/out-tsc/test/temba-omnibox.test.js +2 -1
  196. package/out-tsc/test/temba-omnibox.test.js.map +1 -1
  197. package/out-tsc/test/temba-sortable-list.test.js +51 -0
  198. package/out-tsc/test/temba-sortable-list.test.js.map +1 -1
  199. package/out-tsc/test/temba-utils-index.test.js +1 -27
  200. package/out-tsc/test/temba-utils-index.test.js.map +1 -1
  201. package/out-tsc/test/utils.test.js +20 -0
  202. package/out-tsc/test/utils.test.js.map +1 -1
  203. package/package.json +2 -1
  204. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  205. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  206. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  207. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  208. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  209. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  210. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  211. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  212. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  213. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  214. package/screenshots/truth/actions/add_contact_urn/editor/expression-facebook.png +0 -0
  215. package/screenshots/truth/actions/add_contact_urn/editor/expression-phone.png +0 -0
  216. package/screenshots/truth/actions/add_contact_urn/editor/facebook-id.png +0 -0
  217. package/screenshots/truth/actions/add_contact_urn/editor/instagram-handle.png +0 -0
  218. package/screenshots/truth/actions/add_contact_urn/editor/line-id.png +0 -0
  219. package/screenshots/truth/actions/add_contact_urn/editor/phone-number.png +0 -0
  220. package/screenshots/truth/actions/add_contact_urn/editor/telegram-id.png +0 -0
  221. package/screenshots/truth/actions/add_contact_urn/editor/viber-id.png +0 -0
  222. package/screenshots/truth/actions/add_contact_urn/editor/wechat-id.png +0 -0
  223. package/screenshots/truth/actions/add_contact_urn/editor/whatsapp.png +0 -0
  224. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  225. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  226. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  227. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  228. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  229. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  230. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  231. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  232. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  233. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  234. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  235. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  236. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  237. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  238. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  239. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  240. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  241. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  242. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  243. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  244. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  245. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  246. package/screenshots/truth/actions/send_broadcast/editor/contacts-only.png +0 -0
  247. package/screenshots/truth/actions/send_broadcast/editor/groups-and-contacts.png +0 -0
  248. package/screenshots/truth/actions/send_broadcast/editor/groups-only.png +0 -0
  249. package/screenshots/truth/actions/send_broadcast/editor/many-groups.png +0 -0
  250. package/screenshots/truth/actions/send_broadcast/editor/multiline-text.png +0 -0
  251. package/screenshots/truth/actions/send_broadcast/editor/with-attachments.png +0 -0
  252. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  253. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  254. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  255. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  256. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  257. package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
  258. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  259. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  260. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  261. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  262. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  263. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  264. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  265. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  266. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  267. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  268. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  269. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  270. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  271. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  272. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  273. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  274. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  275. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  276. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  277. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  278. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  279. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  280. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  281. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  282. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  283. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  284. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  285. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  286. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  287. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  288. package/screenshots/truth/actions/start_session/editor/contact-query.png +0 -0
  289. package/screenshots/truth/actions/start_session/editor/contacts-only.png +0 -0
  290. package/screenshots/truth/actions/start_session/editor/create-contact.png +0 -0
  291. package/screenshots/truth/actions/start_session/editor/groups-and-contacts.png +0 -0
  292. package/screenshots/truth/actions/start_session/editor/groups-only.png +0 -0
  293. package/screenshots/truth/actions/start_session/editor/many-recipients.png +0 -0
  294. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  295. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  296. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  297. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  298. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  299. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  300. package/screenshots/truth/canvas-menu/open.png +0 -0
  301. package/screenshots/truth/editor/router.png +0 -0
  302. package/screenshots/truth/editor/wait.png +0 -0
  303. package/screenshots/truth/floating-tab/default.png +0 -0
  304. package/screenshots/truth/floating-tab/gray.png +0 -0
  305. package/screenshots/truth/floating-tab/green.png +0 -0
  306. package/screenshots/truth/floating-tab/hidden.png +0 -0
  307. package/screenshots/truth/floating-tab/hover.png +0 -0
  308. package/screenshots/truth/floating-tab/purple.png +0 -0
  309. package/screenshots/truth/floating-window/chromeless.png +0 -0
  310. package/screenshots/truth/floating-window/custom-size.png +0 -0
  311. package/screenshots/truth/floating-window/default.png +0 -0
  312. package/screenshots/truth/floating-window/with-header.png +0 -0
  313. package/screenshots/truth/list/fields-dragging.png +0 -0
  314. package/screenshots/truth/list/sortable-dragging.png +0 -0
  315. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  316. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  317. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  318. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  319. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  320. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  321. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  322. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  323. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  324. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  325. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  326. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  327. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  328. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  329. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  330. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  331. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  332. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  333. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  334. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  335. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  336. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  337. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  338. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  339. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  340. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  341. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  342. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  343. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  344. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  345. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  346. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  347. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  348. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  349. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  350. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  351. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  352. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  353. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  354. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  355. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  356. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  357. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  358. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  359. package/src/Icons.ts +4 -1
  360. package/src/display/FloatingTab.ts +174 -0
  361. package/src/display/ProgressBar.ts +22 -2
  362. package/src/events.ts +2 -8
  363. package/src/flow/CanvasMenu.ts +217 -0
  364. package/src/flow/CanvasNode.ts +596 -40
  365. package/src/flow/Editor.ts +1721 -45
  366. package/src/flow/NodeEditor.ts +621 -144
  367. package/src/flow/NodeTypeSelector.ts +636 -0
  368. package/src/flow/StickyNote.ts +12 -3
  369. package/src/flow/actions/add_contact_groups.ts +5 -4
  370. package/src/flow/actions/add_contact_urn.ts +78 -4
  371. package/src/flow/actions/add_input_labels.ts +5 -4
  372. package/src/flow/actions/play_audio.ts +3 -2
  373. package/src/flow/actions/remove_contact_groups.ts +16 -6
  374. package/src/flow/actions/request_optin.ts +3 -2
  375. package/src/flow/actions/say_msg.ts +3 -2
  376. package/src/flow/actions/send_broadcast.ts +86 -23
  377. package/src/flow/actions/send_email.ts +12 -6
  378. package/src/flow/actions/send_msg.ts +155 -34
  379. package/src/flow/actions/set_contact_channel.ts +6 -11
  380. package/src/flow/actions/set_contact_field.ts +21 -25
  381. package/src/flow/actions/set_contact_language.ts +11 -4
  382. package/src/flow/actions/set_contact_name.ts +4 -15
  383. package/src/flow/actions/set_contact_status.ts +4 -3
  384. package/src/flow/actions/set_run_result.ts +5 -4
  385. package/src/flow/actions/start_session.ts +210 -6
  386. package/src/flow/config.ts +11 -23
  387. package/src/flow/currencies.ts +51 -0
  388. package/src/flow/nodes/shared-rules.ts +301 -0
  389. package/src/flow/nodes/shared.ts +87 -0
  390. package/src/flow/nodes/split_by_airtime.ts +255 -5
  391. package/src/flow/nodes/split_by_contact_field.ts +195 -3
  392. package/src/flow/nodes/split_by_expression.ts +104 -2
  393. package/src/flow/nodes/split_by_groups.ts +26 -11
  394. package/src/flow/nodes/split_by_intent.ts +8 -0
  395. package/src/flow/nodes/split_by_llm.ts +22 -4
  396. package/src/flow/nodes/split_by_llm_categorize.ts +22 -5
  397. package/src/flow/nodes/split_by_random.ts +16 -6
  398. package/src/flow/nodes/split_by_resthook.ts +140 -0
  399. package/src/flow/nodes/split_by_run_result.ts +259 -3
  400. package/src/flow/nodes/split_by_scheme.ts +202 -2
  401. package/src/flow/nodes/split_by_subflow.ts +17 -5
  402. package/src/flow/nodes/split_by_ticket.ts +15 -4
  403. package/src/flow/nodes/split_by_webhook.ts +17 -6
  404. package/src/flow/nodes/wait_for_digits.ts +3 -2
  405. package/src/flow/nodes/wait_for_menu.ts +3 -2
  406. package/src/flow/nodes/wait_for_response.ts +59 -680
  407. package/src/flow/types.ts +156 -23
  408. package/src/flow/utils.ts +108 -14
  409. package/src/form/FieldRenderer.ts +2 -4
  410. package/src/interfaces.ts +3 -0
  411. package/src/layout/FloatingWindow.ts +386 -0
  412. package/src/list/SortableList.ts +109 -34
  413. package/src/live/ContactChat.ts +7 -25
  414. package/src/locales/es.ts +18 -13
  415. package/src/locales/fr.ts +18 -13
  416. package/src/locales/locale-codes.ts +11 -2
  417. package/src/locales/pt.ts +18 -13
  418. package/src/store/AppState.ts +173 -0
  419. package/src/store/flow-definition.d.ts +2 -5
  420. package/src/utils.ts +332 -12
  421. package/static/api/channels.json +46 -0
  422. package/static/api/llms.json +18 -0
  423. package/static/api/resthooks.json +31 -0
  424. package/static/svg/index.svg +1 -1
  425. package/static/svg/work/traced/lightning-02.svg +1 -0
  426. package/static/svg/work/used/lightning-02.svg +3 -0
  427. package/temba-modules.ts +8 -0
  428. package/test/ActionHelper.ts +3 -3
  429. package/test/NodeHelper.ts +6 -3
  430. package/test/actions/add_contact_urn.test.ts +287 -0
  431. package/test/actions/send_broadcast.test.ts +190 -0
  432. package/test/actions/send_email.test.ts +17 -23
  433. package/test/actions/send_msg.test.ts +39 -15
  434. package/test/actions/start_session.test.ts +151 -0
  435. package/test/nodes/split_by_airtime.test.ts +673 -0
  436. package/test/nodes/split_by_contact_field.test.ts +451 -0
  437. package/test/nodes/split_by_expression.test.ts +751 -0
  438. package/test/nodes/split_by_random.test.ts +3 -3
  439. package/test/nodes/split_by_resthook.test.ts +398 -0
  440. package/test/nodes/split_by_run_result.test.ts +1109 -0
  441. package/test/nodes/split_by_scheme.test.ts +486 -0
  442. package/test/nodes/split_by_subflow.test.ts +381 -0
  443. package/test/nodes/wait_for_digits.test.ts +2 -2
  444. package/test/nodes/wait_for_response.test.ts +2 -1
  445. package/test/temba-action-drag-between-nodes.test.ts +301 -0
  446. package/test/temba-canvas-menu.test.ts +156 -0
  447. package/test/temba-floating-tab.test.ts +110 -0
  448. package/test/temba-floating-window.test.ts +477 -0
  449. package/test/temba-flow-editor-node.test.ts +246 -2
  450. package/test/temba-flow-editor.test.ts +7 -8
  451. package/test/temba-localization.test.ts +611 -0
  452. package/test/temba-node-editor.test.ts +3 -1
  453. package/test/temba-node-type-selector.test.ts +355 -0
  454. package/test/temba-omnibox.test.ts +2 -1
  455. package/test/temba-sortable-list.test.ts +69 -0
  456. package/test/temba-utils-index.test.ts +0 -35
  457. package/test/utils.test.ts +22 -0
  458. package/test-assets/contacts/history.json +14 -21
  459. package/test-assets/select/llms.json +2 -2
  460. package/web-dev-server.config.mjs +49 -1
  461. package/web-test-runner.config.mjs +0 -1
  462. package/out-tsc/src/flow/actions/call_classifier.js +0 -11
  463. package/out-tsc/src/flow/actions/call_classifier.js.map +0 -1
  464. package/out-tsc/src/flow/actions/call_resthook.js +0 -11
  465. package/out-tsc/src/flow/actions/call_resthook.js.map +0 -1
  466. package/out-tsc/src/flow/actions/split_by_expression_example.js +0 -77
  467. package/out-tsc/src/flow/actions/split_by_expression_example.js.map +0 -1
  468. package/out-tsc/src/flow/actions/transfer_airtime.js +0 -11
  469. package/out-tsc/src/flow/actions/transfer_airtime.js.map +0 -1
  470. package/out-tsc/src/flow/nodes/wait_for_audio.js +0 -7
  471. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +0 -1
  472. package/out-tsc/src/flow/nodes/wait_for_image.js +0 -7
  473. package/out-tsc/src/flow/nodes/wait_for_image.js.map +0 -1
  474. package/out-tsc/src/flow/nodes/wait_for_location.js +0 -7
  475. package/out-tsc/src/flow/nodes/wait_for_location.js.map +0 -1
  476. package/out-tsc/src/flow/nodes/wait_for_video.js +0 -7
  477. package/out-tsc/src/flow/nodes/wait_for_video.js.map +0 -1
  478. package/src/flow/actions/call_classifier.ts +0 -12
  479. package/src/flow/actions/call_resthook.ts +0 -12
  480. package/src/flow/actions/split_by_expression_example.ts +0 -88
  481. package/src/flow/actions/transfer_airtime.ts +0 -12
  482. package/src/flow/nodes/wait_for_audio.ts +0 -7
  483. package/src/flow/nodes/wait_for_image.ts +0 -7
  484. package/src/flow/nodes/wait_for_location.ts +0 -7
  485. package/src/flow/nodes/wait_for_video.ts +0 -7
@@ -0,0 +1,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
+ });