@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,156 @@
1
+ import { expect, assert } from '@open-wc/testing';
2
+ import { CanvasMenu } from '../src/flow/CanvasMenu';
3
+ import { assertScreenshot, getClip, getComponent } from './utils.test';
4
+
5
+ describe('temba-canvas-menu', () => {
6
+ const createCanvasMenu = async () => {
7
+ const component = (await getComponent(
8
+ 'temba-canvas-menu',
9
+ {},
10
+ '',
11
+ 250,
12
+ 250
13
+ )) as CanvasMenu;
14
+ await component.updateComplete;
15
+ return component;
16
+ };
17
+
18
+ it('can be created', async () => {
19
+ const menu = await createCanvasMenu();
20
+ assert.instanceOf(menu, CanvasMenu);
21
+ expect(menu.open).to.be.false;
22
+ });
23
+
24
+ it('is not visible when closed', async () => {
25
+ const menu = await createCanvasMenu();
26
+ expect(menu.open).to.be.false;
27
+
28
+ // verify no menu is rendered
29
+ const menuElement = menu.shadowRoot?.querySelector('.menu');
30
+ expect(menuElement).to.be.null;
31
+ });
32
+
33
+ it('shows menu when opened', async () => {
34
+ const menu = await createCanvasMenu();
35
+
36
+ // open the menu
37
+ menu.show(100, 100, { x: 50, y: 50 });
38
+ await menu.updateComplete;
39
+
40
+ expect(menu.open).to.be.true;
41
+ expect(menu.x).to.equal(100);
42
+ expect(menu.y).to.equal(100);
43
+
44
+ // verify menu is rendered
45
+ const menuElement = menu.shadowRoot?.querySelector('.menu');
46
+ expect(menuElement).to.not.be.null;
47
+
48
+ await assertScreenshot('canvas-menu/open', getClip(menu));
49
+ });
50
+
51
+ it('has three menu items', async () => {
52
+ const menu = await createCanvasMenu();
53
+ menu.show(100, 100, { x: 50, y: 50 });
54
+ await menu.updateComplete;
55
+
56
+ const menuItems = menu.shadowRoot?.querySelectorAll('.menu-item');
57
+ expect(menuItems?.length).to.equal(3);
58
+
59
+ // check menu item titles
60
+ const titles = Array.from(menuItems || []).map(
61
+ (item) => item.querySelector('.menu-item-title')?.textContent
62
+ );
63
+ expect(titles).to.deep.equal([
64
+ 'Add Action',
65
+ 'Add Split',
66
+ 'Add Sticky Note'
67
+ ]);
68
+ });
69
+
70
+ it('closes when close() is called', async () => {
71
+ const menu = await createCanvasMenu();
72
+ menu.show(100, 100, { x: 50, y: 50 });
73
+ await menu.updateComplete;
74
+
75
+ expect(menu.open).to.be.true;
76
+
77
+ menu.close();
78
+ await menu.updateComplete;
79
+
80
+ expect(menu.open).to.be.false;
81
+ });
82
+
83
+ it('fires selection event when menu item is clicked', async () => {
84
+ const menu = await createCanvasMenu();
85
+ menu.show(100, 100, { x: 50, y: 50 });
86
+ await menu.updateComplete;
87
+
88
+ let selectionFired = false;
89
+ let selectionDetail = null;
90
+
91
+ menu.addEventListener('temba-selection', (event: any) => {
92
+ selectionFired = true;
93
+ selectionDetail = event.detail;
94
+ });
95
+
96
+ // click on sticky note option (now the third item)
97
+ const menuItems = menu.shadowRoot?.querySelectorAll('.menu-item');
98
+ const stickyItem = menuItems?.[2] as HTMLElement;
99
+ stickyItem.click();
100
+ await menu.updateComplete;
101
+
102
+ expect(selectionFired).to.be.true;
103
+ expect(selectionDetail).to.deep.equal({
104
+ action: 'sticky',
105
+ position: { x: 50, y: 50 }
106
+ });
107
+ expect(menu.open).to.be.false;
108
+ });
109
+
110
+ it('adjusts position to stay within viewport bounds', async () => {
111
+ const menu = await createCanvasMenu();
112
+
113
+ // open menu at position that would go off screen
114
+ const viewportWidth = window.innerWidth;
115
+ const viewportHeight = window.innerHeight;
116
+ const margin = 10; // matches the margin in CanvasMenu
117
+
118
+ // position that would go off the right and bottom edges
119
+ menu.show(viewportWidth - 50, viewportHeight - 50, {
120
+ x: 100,
121
+ y: 100
122
+ });
123
+ await menu.updateComplete;
124
+
125
+ // wait for position adjustment
126
+ await new Promise((resolve) => setTimeout(resolve, 100));
127
+ await menu.updateComplete;
128
+
129
+ const menuElement = menu.shadowRoot?.querySelector('.menu') as HTMLElement;
130
+ expect(menuElement).to.not.be.null;
131
+
132
+ const menuRect = menuElement.getBoundingClientRect();
133
+
134
+ // verify menu stays within viewport with margin
135
+ expect(menuRect.right).to.be.at.most(viewportWidth - margin);
136
+ expect(menuRect.bottom).to.be.at.most(viewportHeight - margin);
137
+
138
+ // verify click position is preserved (not adjusted)
139
+ let selectionFired = false;
140
+ let selectionDetail = null;
141
+
142
+ menu.addEventListener('temba-selection', (event: any) => {
143
+ selectionFired = true;
144
+ selectionDetail = event.detail;
145
+ });
146
+
147
+ const menuItems = menu.shadowRoot?.querySelectorAll('.menu-item');
148
+ const actionItem = menuItems?.[0] as HTMLElement;
149
+ actionItem.click();
150
+ await menu.updateComplete;
151
+
152
+ expect(selectionFired).to.be.true;
153
+ // click position should remain unchanged
154
+ expect(selectionDetail.position).to.deep.equal({ x: 100, y: 100 });
155
+ });
156
+ });
@@ -10,6 +10,7 @@ import {
10
10
  } from '../src/store/flow-definition.d';
11
11
  import { stub, restore, useFakeTimers } from 'sinon';
12
12
  import { CustomEventType } from '../src/interfaces';
13
+ import { ACTION_GROUPS } from '../src/flow/types';
13
14
 
14
15
  describe('EditorNode', () => {
15
16
  let editorNode: CanvasNode;
@@ -220,7 +221,7 @@ describe('EditorNode', () => {
220
221
  it('renders title with config color and name', async () => {
221
222
  const config = {
222
223
  name: 'Test Action',
223
- color: '#ff0000'
224
+ group: ACTION_GROUPS.send // Uses 'send' group which has color '#3498db'
224
225
  };
225
226
 
226
227
  const mockAction: Action = {
@@ -236,7 +237,8 @@ describe('EditorNode', () => {
236
237
 
237
238
  const nameElement = title?.querySelector('.name');
238
239
  expect(nameElement?.textContent?.trim()).to.equal('Test Action');
239
- expect(title?.getAttribute('style')).to.contain('background:#ff0000');
240
+ // The 'send' group has color '#3498db' from ACTION_GROUP_METADATA
241
+ expect(title?.getAttribute('style')).to.contain('background:#3498db');
240
242
  });
241
243
  });
242
244
 
@@ -1199,4 +1201,102 @@ describe('EditorNode', () => {
1199
1201
  // This sequence ensures JSPlumb visuals stay in sync with the flow definition
1200
1202
  });
1201
1203
  });
1204
+
1205
+ describe('add action button', () => {
1206
+ beforeEach(async () => {
1207
+ editorNode = new CanvasNode();
1208
+ editorNode['plumber'] = mockPlumber;
1209
+ });
1210
+
1211
+ it('handleAddActionClick fires AddActionRequested event', () => {
1212
+ const mockNode: Node = {
1213
+ uuid: 'test-node',
1214
+ actions: [
1215
+ {
1216
+ type: 'send_msg',
1217
+ uuid: 'action-1',
1218
+ text: 'Hello',
1219
+ quick_replies: []
1220
+ } as any
1221
+ ],
1222
+ exits: [{ uuid: 'exit-1', destination_uuid: null }]
1223
+ };
1224
+
1225
+ editorNode['node'] = mockNode;
1226
+
1227
+ let eventFired = false;
1228
+ let eventDetail: any = null;
1229
+
1230
+ editorNode.addEventListener(CustomEventType.AddActionRequested, (e) => {
1231
+ eventFired = true;
1232
+ eventDetail = (e as CustomEvent).detail;
1233
+ });
1234
+
1235
+ const mockEvent = {
1236
+ preventDefault: stub(),
1237
+ stopPropagation: stub()
1238
+ } as any;
1239
+
1240
+ // Call the add action click handler
1241
+ (editorNode as any).handleAddActionClick(mockEvent);
1242
+
1243
+ // Verify the event was fired with correct detail
1244
+ expect(eventFired).to.be.true;
1245
+ expect(eventDetail).to.exist;
1246
+ expect(eventDetail.nodeUuid).to.equal('test-node');
1247
+
1248
+ // Verify event handlers were called
1249
+ expect(mockEvent.preventDefault).to.have.been.called;
1250
+ expect(mockEvent.stopPropagation).to.have.been.called;
1251
+ });
1252
+
1253
+ it('renders add action button for execute_actions nodes', () => {
1254
+ const mockNode: Node = {
1255
+ uuid: 'test-node',
1256
+ actions: [
1257
+ {
1258
+ type: 'send_msg',
1259
+ uuid: 'action-1',
1260
+ text: 'Hello',
1261
+ quick_replies: []
1262
+ } as any
1263
+ ],
1264
+ exits: [{ uuid: 'exit-1', destination_uuid: null }]
1265
+ };
1266
+
1267
+ const mockUI: NodeUI = {
1268
+ position: { left: 0, top: 0 },
1269
+ type: 'execute_actions'
1270
+ };
1271
+
1272
+ editorNode['node'] = mockNode;
1273
+ editorNode['ui'] = mockUI;
1274
+
1275
+ const rendered = editorNode.render();
1276
+ expect(rendered).to.exist;
1277
+ });
1278
+
1279
+ it('renders correctly for non-execute_actions nodes', () => {
1280
+ const mockNode: Node = {
1281
+ uuid: 'test-node',
1282
+ actions: [],
1283
+ exits: [{ uuid: 'exit-1', destination_uuid: null }],
1284
+ router: {
1285
+ type: 'switch',
1286
+ categories: []
1287
+ }
1288
+ };
1289
+
1290
+ const mockUI: NodeUI = {
1291
+ position: { left: 0, top: 0 },
1292
+ type: 'wait_for_response'
1293
+ };
1294
+
1295
+ editorNode['node'] = mockNode;
1296
+ editorNode['ui'] = mockUI;
1297
+
1298
+ const rendered = editorNode.render();
1299
+ expect(rendered).to.exist;
1300
+ });
1301
+ });
1202
1302
  });
@@ -412,12 +412,12 @@ describe('Editor', () => {
412
412
  });
413
413
  });
414
414
 
415
- describe('double-click functionality', () => {
415
+ describe('canvas menu functionality', () => {
416
416
  afterEach(() => {
417
417
  restore();
418
418
  });
419
419
 
420
- it('creates sticky note on canvas double-click', async () => {
420
+ it('has context menu handler set up', async () => {
421
421
  editor = await fixture(html`
422
422
  <temba-flow-editor>
423
423
  <div id="grid">
@@ -436,10 +436,9 @@ describe('Editor', () => {
436
436
  const canvas = editor.querySelector('#canvas') as HTMLElement;
437
437
  expect(canvas).to.exist;
438
438
 
439
- // Check that the double-click event listener is set up
440
- // We'll test this by checking if the boundCanvasDoubleClick method exists
441
- expect((editor as any).boundCanvasDoubleClick).to.exist;
442
- expect(typeof (editor as any).boundCanvasDoubleClick).to.equal(
439
+ // Check that the context menu event listener is set up
440
+ expect((editor as any).boundCanvasContextMenu).to.exist;
441
+ expect(typeof (editor as any).boundCanvasContextMenu).to.equal(
443
442
  'function'
444
443
  );
445
444
 
@@ -456,8 +455,8 @@ describe('Editor', () => {
456
455
  expect(snapToGrid(-10)).to.equal(0); // Should not go negative
457
456
  });
458
457
 
459
- it('tests double-click handler logic independently', () => {
460
- // Test the logic that would be in handleCanvasDoubleClick without the problematic getStore call
458
+ it('tests context menu handler logic independently', () => {
459
+ // Test the logic that would be in handleCanvasContextMenu
461
460
 
462
461
  // Mock event with canvas target
463
462
  const canvasTarget = { id: 'canvas' };
@@ -21,7 +21,9 @@ const assertDialogScreenshot = async (
21
21
  const clip = getClip(dialog);
22
22
  // Adjust width to show full dialog with proper padding
23
23
  const dialogRect = dialog.getBoundingClientRect();
24
- clip.width = dialogRect.width + 20; // 10px padding on each side
24
+ // Use scrollWidth to get the actual content width (including overflow)
25
+ const contentWidth = Math.max(dialogRect.width, dialog.scrollWidth);
26
+ clip.width = contentWidth + 20; // 10px padding on each side
25
27
  await assertScreenshot(screenshotName, clip);
26
28
  };
27
29
 
@@ -0,0 +1,152 @@
1
+ import { expect, assert } from '@open-wc/testing';
2
+ import { NodeTypeSelector } from '../src/flow/NodeTypeSelector';
3
+ import { assertScreenshot, getClip, getComponent } from './utils.test';
4
+
5
+ describe('temba-node-type-selector', () => {
6
+ const createSelector = async () => {
7
+ const component = (await getComponent(
8
+ 'temba-node-type-selector',
9
+ {},
10
+ '',
11
+ 700,
12
+ 600
13
+ )) as NodeTypeSelector;
14
+ await component.updateComplete;
15
+ return component;
16
+ };
17
+
18
+ it('can be created', async () => {
19
+ const selector = await createSelector();
20
+ assert.instanceOf(selector, NodeTypeSelector);
21
+ expect(selector.open).to.be.false;
22
+ });
23
+
24
+ it('is not visible when closed', async () => {
25
+ const selector = await createSelector();
26
+ expect(selector.open).to.be.false;
27
+
28
+ // component should not be in DOM when closed
29
+ expect(selector.hasAttribute('open')).to.be.false;
30
+ });
31
+
32
+ it('shows dialog when opened in action mode', async () => {
33
+ const selector = await createSelector();
34
+
35
+ selector.show('action', { x: 100, y: 100 });
36
+ await selector.updateComplete;
37
+
38
+ expect(selector.open).to.be.true;
39
+ expect(selector.mode).to.equal('action');
40
+ expect(selector.hasAttribute('open')).to.be.true;
41
+
42
+ const dialog = selector.shadowRoot?.querySelector('.dialog') as HTMLElement;
43
+ await assertScreenshot('node-type-selector/action-mode', getClip(dialog));
44
+ });
45
+
46
+ it('shows dialog when opened in split mode', async () => {
47
+ const selector = await createSelector();
48
+
49
+ selector.show('split', { x: 100, y: 100 });
50
+ await selector.updateComplete;
51
+
52
+ expect(selector.open).to.be.true;
53
+ expect(selector.mode).to.equal('split');
54
+
55
+ const dialog = selector.shadowRoot?.querySelector('.dialog') as HTMLElement;
56
+ await assertScreenshot('node-type-selector/split-mode', getClip(dialog));
57
+ });
58
+
59
+ it('displays action types in action mode', async () => {
60
+ const selector = await createSelector();
61
+ selector.show('action', { x: 100, y: 100 });
62
+ await selector.updateComplete;
63
+
64
+ const title = selector.shadowRoot?.querySelector('.header h2');
65
+ expect(title?.textContent).to.equal('Select an Action');
66
+
67
+ // verify we have node items
68
+ const nodeItems = selector.shadowRoot?.querySelectorAll('.node-item');
69
+ expect(nodeItems?.length).to.be.greaterThan(0);
70
+ });
71
+
72
+ it('displays split types in split mode', async () => {
73
+ const selector = await createSelector();
74
+ selector.show('split', { x: 100, y: 100 });
75
+ await selector.updateComplete;
76
+
77
+ const title = selector.shadowRoot?.querySelector('.header h2');
78
+ expect(title?.textContent).to.equal('Select a Split');
79
+
80
+ // verify we have node items
81
+ const nodeItems = selector.shadowRoot?.querySelectorAll('.node-item');
82
+ expect(nodeItems?.length).to.be.greaterThan(0);
83
+ });
84
+
85
+ it('closes when close() is called', async () => {
86
+ const selector = await createSelector();
87
+ selector.show('action', { x: 100, y: 100 });
88
+ await selector.updateComplete;
89
+
90
+ expect(selector.open).to.be.true;
91
+
92
+ selector.close();
93
+ await selector.updateComplete;
94
+
95
+ expect(selector.open).to.be.false;
96
+ });
97
+
98
+ it('closes when overlay is clicked', async () => {
99
+ const selector = await createSelector();
100
+ selector.show('action', { x: 100, y: 100 });
101
+ await selector.updateComplete;
102
+
103
+ const overlay = selector.shadowRoot?.querySelector(
104
+ '.overlay'
105
+ ) as HTMLElement;
106
+ overlay.click();
107
+ await selector.updateComplete;
108
+
109
+ expect(selector.open).to.be.false;
110
+ });
111
+
112
+ it('closes when cancel button is clicked', async () => {
113
+ const selector = await createSelector();
114
+ selector.show('action', { x: 100, y: 100 });
115
+ await selector.updateComplete;
116
+
117
+ const cancelButton = selector.shadowRoot?.querySelector(
118
+ 'temba-button'
119
+ ) as HTMLElement;
120
+ cancelButton.click();
121
+ await selector.updateComplete;
122
+
123
+ expect(selector.open).to.be.false;
124
+ });
125
+
126
+ it('fires selection event when node type is clicked', async () => {
127
+ const selector = await createSelector();
128
+ selector.show('action', { x: 100, y: 100 });
129
+ await selector.updateComplete;
130
+
131
+ let selectionFired = false;
132
+ let selectionDetail = null;
133
+
134
+ selector.addEventListener('temba-selection', (event: any) => {
135
+ selectionFired = true;
136
+ selectionDetail = event.detail;
137
+ });
138
+
139
+ // click on first node item
140
+ const firstNodeItem = selector.shadowRoot?.querySelector(
141
+ '.node-item'
142
+ ) as HTMLElement;
143
+ firstNodeItem.click();
144
+ await selector.updateComplete;
145
+
146
+ expect(selectionFired).to.be.true;
147
+ expect(selectionDetail).to.have.property('nodeType');
148
+ expect(selectionDetail).to.have.property('position');
149
+ expect(selectionDetail.position).to.deep.equal({ x: 100, y: 100 });
150
+ expect(selector.open).to.be.false;
151
+ });
152
+ });
@@ -49,7 +49,8 @@ describe('temba-omnibox', () => {
49
49
  assert.instanceOf(omnibox, Omnibox);
50
50
  });
51
51
 
52
- it('fires change events on selection', async () => {
52
+ // TODO: make this pass reliably on CI
53
+ xit('fires change events on selection', async () => {
53
54
  const omnibox: Omnibox = await createOmnibox(clock, {
54
55
  endpoint: '/test-assets/select/omnibox.json'
55
56
  });
@@ -148,4 +148,73 @@ describe('temba-sortable-list', () => {
148
148
  const changeEvent = await updated;
149
149
  expect(changeEvent.type).to.equal('change');
150
150
  });
151
+
152
+ it('detects external drag when dragging outside container', async () => {
153
+ const list: SortableList = await createSorter(BORING_LIST);
154
+ list.externalDrag = true;
155
+ await list.updateComplete;
156
+
157
+ const bounds = list.getBoundingClientRect();
158
+
159
+ // track external drag events
160
+ let externalDragFired = false;
161
+ let internalDragFired = false;
162
+
163
+ list.addEventListener(CustomEventType.DragExternal, () => {
164
+ externalDragFired = true;
165
+ });
166
+
167
+ list.addEventListener(CustomEventType.DragInternal, () => {
168
+ internalDragFired = true;
169
+ });
170
+
171
+ // start dragging an item
172
+ await moveMouse(bounds.left + 20, bounds.bottom - 10);
173
+ await mouseDown();
174
+
175
+ // drag outside the container (far to the right)
176
+ await moveMouse(bounds.right + 100, bounds.top + 10);
177
+ clock.runAll();
178
+
179
+ // should have fired external drag event
180
+ expect(externalDragFired).to.be.true;
181
+
182
+ // drag back inside
183
+ externalDragFired = false; // reset
184
+ await moveMouse(bounds.left + 20, bounds.top + 10);
185
+ clock.runAll();
186
+
187
+ // should have fired internal drag event
188
+ expect(internalDragFired).to.be.true;
189
+
190
+ // clean up
191
+ await mouseUp();
192
+ clock.runAll();
193
+ });
194
+
195
+ it('fires DragStop with isExternal=true when dropped outside container', async () => {
196
+ const list: SortableList = await createSorter(BORING_LIST);
197
+ list.externalDrag = true;
198
+ await list.updateComplete;
199
+
200
+ const bounds = list.getBoundingClientRect();
201
+
202
+ // start dragging an item
203
+ await moveMouse(bounds.left + 20, bounds.bottom - 10);
204
+ await mouseDown();
205
+
206
+ // drag outside the container
207
+ await moveMouse(bounds.right + 100, bounds.top + 10);
208
+ clock.runAll();
209
+
210
+ // listen for drag stop event
211
+ const dragStop = oneEvent(list, CustomEventType.DragStop, false);
212
+
213
+ // drop outside
214
+ await mouseUp();
215
+ clock.runAll();
216
+
217
+ const dragStopEvent = await dragStop;
218
+ expect(dragStopEvent.detail.isExternal).to.be.true;
219
+ });
151
220
  });
@@ -35,7 +35,6 @@ import {
35
35
  stubbable,
36
36
  renderAvatar,
37
37
  fillTemplate,
38
- spreadAttributes,
39
38
  getUrl,
40
39
  postUrl,
41
40
  postJSON,
@@ -1098,40 +1097,6 @@ describe('utils/index', () => {
1098
1097
  });
1099
1098
  });
1100
1099
 
1101
- describe('spreadAttributes', () => {
1102
- it('spreads regular attributes', () => {
1103
- const attrs = { id: 'test', class: 'example' };
1104
- const result = spreadAttributes(attrs);
1105
-
1106
- expect(Array.isArray(result)).to.be.true;
1107
- expect(result.length).to.equal(2);
1108
- });
1109
-
1110
- it('handles event attributes with @', () => {
1111
- const attrs = { '@click': 'handleClick' };
1112
- const result = spreadAttributes(attrs);
1113
-
1114
- expect(Array.isArray(result)).to.be.true;
1115
- expect(result.length).to.equal(1);
1116
- });
1117
-
1118
- it('handles property attributes with .', () => {
1119
- const attrs = { '.value': 'test' };
1120
- const result = spreadAttributes(attrs);
1121
-
1122
- expect(Array.isArray(result)).to.be.true;
1123
- expect(result.length).to.equal(1);
1124
- });
1125
-
1126
- it('handles mixed attribute types', () => {
1127
- const attrs = { id: 'test', '@click': 'handler', '.prop': 'value' };
1128
- const result = spreadAttributes(attrs);
1129
-
1130
- expect(Array.isArray(result)).to.be.true;
1131
- expect(result.length).to.equal(3);
1132
- });
1133
- });
1134
-
1135
1100
  describe('renderAvatar', () => {
1136
1101
  it('renders avatar with name only', () => {
1137
1102
  const result = renderAvatar({ name: 'John Doe' });
@@ -330,6 +330,7 @@ export const clickOption = async (
330
330
  );
331
331
  if (!existingOption) {
332
332
  try {
333
+ // Increased wait time to handle slower CI environments
333
334
  await waitForCondition(
334
335
  () => {
335
336
  const option = options.shadowRoot?.querySelector(
@@ -376,6 +377,7 @@ export const openSelect = async (clock: any, select: Select<SelectOption>) => {
376
377
  if (hasEndpoint) {
377
378
  try {
378
379
  // Wait for options to be properly rendered and visible (but only for endpoint selects)
380
+ // Increased max attempts to handle slower CI environments
379
381
  await waitForCondition(
380
382
  () => {
381
383
  const options = select.shadowRoot.querySelector(