@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
@@ -1,7 +1,7 @@
1
- import { COLORS, NodeConfig } from '../types';
1
+ import { SPLIT_GROUPS, NodeConfig } from '../types';
2
2
 
3
3
  export const wait_for_video: NodeConfig = {
4
4
  type: 'wait_for_video',
5
5
  name: 'Wait for Video',
6
- color: COLORS.wait
6
+ group: SPLIT_GROUPS.wait
7
7
  };
package/src/flow/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { TemplateResult } from 'lit-html';
2
- import { Action, Node } from '../store/flow-definition';
2
+ import { Action, Node, NodeUI } from '../store/flow-definition';
3
3
 
4
4
  export interface ValidationResult {
5
5
  valid: boolean;
@@ -78,9 +78,10 @@ export interface FormConfig {
78
78
  export interface NodeConfig extends FormConfig {
79
79
  type: string;
80
80
  name?: string;
81
- color?: string;
81
+ group?: ActionGroup | SplitGroup; // Nodes can use either when showAsAction is true
82
82
  dialogSize?: 'small' | 'medium' | 'large' | 'xlarge';
83
83
  action?: ActionConfig;
84
+ showAsAction?: boolean; // if true, show in action dialog instead of splits (default: false - nodes show in splits)
84
85
  router?: {
85
86
  type: 'switch' | 'random';
86
87
  defaultCategory?: string;
@@ -98,9 +99,11 @@ export interface NodeConfig extends FormConfig {
98
99
  }[];
99
100
  };
100
101
 
101
- toFormData?: (node: Node) => FormData;
102
+ toFormData?: (node: Node, nodeUI?: any) => FormData;
102
103
  fromFormData?: (formData: FormData, originalNode: Node) => Node;
103
- render?: (node: Node) => TemplateResult;
104
+ toUIConfig?: (formData: FormData) => Record<string, any>;
105
+ render?: (node: Node, nodeUI?: any) => TemplateResult;
106
+ renderTitle?: (node: Node, nodeUI?: NodeUI) => TemplateResult;
104
107
  }
105
108
 
106
109
  // New field configuration system for generic form generation
@@ -130,6 +133,11 @@ export interface BaseFieldConfig {
130
133
  visible?: (formData: Record<string, any>) => boolean;
131
134
  disabled?: (formData: Record<string, any>) => boolean;
132
135
  };
136
+
137
+ // Optional field with reveal link
138
+ // When set, the field is hidden by default and a link with this text is shown
139
+ // Clicking the link reveals the field permanently (can't be hidden again)
140
+ optionalLink?: string;
133
141
  }
134
142
 
135
143
  export interface TextFieldConfig extends BaseFieldConfig {
@@ -231,6 +239,8 @@ export interface RowLayoutConfig {
231
239
  type: 'row';
232
240
  items: LayoutItem[]; // can contain fields, groups, or other rows
233
241
  gap?: string; // CSS gap value, defaults to '1rem'
242
+ label?: string; // optional label for the entire row
243
+ helpText?: string; // optional help text for the entire row
234
244
  }
235
245
 
236
246
  export interface GroupLayoutConfig {
@@ -238,9 +248,9 @@ export interface GroupLayoutConfig {
238
248
  label: string;
239
249
  items: LayoutItem[]; // can contain fields, rows, or other groups
240
250
  collapsible?: boolean;
241
- collapsed?: boolean | ((formData: any) => boolean); // initial state if collapsible - can be a function
251
+ collapsed?: boolean | ((formData: FormData) => boolean); // initial state if collapsible - can be a function
242
252
  helpText?: string;
243
- getGroupValueCount?: (formData: any) => number; // optional function to get count for bubble display
253
+ getGroupValueCount?: (formData: FormData) => number; // optional function to get count for bubble display
244
254
  }
245
255
 
246
256
  export type LayoutItem =
@@ -249,31 +259,107 @@ export type LayoutItem =
249
259
  | GroupLayoutConfig
250
260
  | string; // string is shorthand for field
251
261
 
262
+ /**
263
+ * Action group constants - single source of truth for action categorization
264
+ * Use as const for compile-time type checking
265
+ */
266
+ export const ACTION_GROUPS = {
267
+ send: 'send',
268
+ contacts: 'contacts',
269
+ save: 'save',
270
+ services: 'services',
271
+ broadcast: 'broadcast',
272
+ trigger: 'trigger'
273
+ } as const;
274
+
275
+ /**
276
+ * Split group constants - single source of truth for split categorization
277
+ * Use as const for compile-time type checking
278
+ */
279
+ export const SPLIT_GROUPS = {
280
+ wait: 'wait',
281
+ split: 'split'
282
+ } as const;
283
+
284
+ // Extract types from const objects for compile-time checking
285
+ export type ActionGroup = (typeof ACTION_GROUPS)[keyof typeof ACTION_GROUPS];
286
+ export type SplitGroup = (typeof SPLIT_GROUPS)[keyof typeof SPLIT_GROUPS];
287
+
288
+ /**
289
+ * Metadata for group display
290
+ */
291
+ export interface GroupMetadata {
292
+ color: string;
293
+ title: string;
294
+ description: string;
295
+ }
296
+
297
+ /**
298
+ * Action group metadata - defines display properties for each action group
299
+ * Order in this object determines display order in action selector (top to bottom)
300
+ */
301
+ export const ACTION_GROUP_METADATA: Record<ActionGroup, GroupMetadata> = {
302
+ [ACTION_GROUPS.send]: {
303
+ color: '#3498db',
304
+ title: 'Send',
305
+ description: 'Actions that send messages or content to contacts'
306
+ },
307
+ [ACTION_GROUPS.contacts]: {
308
+ color: '#01c1af',
309
+ title: 'Contact',
310
+ description: 'Actions that update contact information'
311
+ },
312
+ [ACTION_GROUPS.save]: {
313
+ color: '#1a777c',
314
+ title: 'Save',
315
+ description: 'Actions that save or store data'
316
+ },
317
+ [ACTION_GROUPS.services]: {
318
+ color: '#f79035ff',
319
+ title: 'Services',
320
+ description: 'Call external services and APIs'
321
+ },
322
+ [ACTION_GROUPS.broadcast]: {
323
+ color: '#8e5ea7',
324
+ title: 'Other People',
325
+ description: 'Actions that apply to others instead of the contact'
326
+ },
327
+ [ACTION_GROUPS.trigger]: {
328
+ color: '#df419f',
329
+ title: 'Trigger',
330
+ description: 'Actions that trigger other behavior'
331
+ }
332
+ };
333
+
334
+ /**
335
+ * Split group metadata - defines display properties for each split group
336
+ * Order in this object determines display order in split selector (top to bottom)
337
+ */
338
+ export const SPLIT_GROUP_METADATA: Record<SplitGroup, GroupMetadata> = {
339
+ [SPLIT_GROUPS.wait]: {
340
+ color: '#4d7dad',
341
+ title: 'Wait',
342
+ description: 'Wait for user and split on their response'
343
+ },
344
+ [SPLIT_GROUPS.split]: {
345
+ color: '#aaaaaa',
346
+ title: 'Split',
347
+ description: 'Split the flow based on conditions'
348
+ }
349
+ };
350
+
252
351
  export interface ActionConfig extends FormConfig {
253
352
  name: string;
254
- color: string;
353
+ group: ActionGroup;
255
354
  dialogSize?: 'small' | 'medium' | 'large' | 'xlarge';
256
355
  evaluated?: string[];
356
+ hideFromActions?: boolean; // if true, don't show in action dialog (default: false - actions show in actions)
257
357
  render?: (node: any, action: any) => TemplateResult;
258
358
 
259
359
  form?: Record<string, FieldConfig>;
260
360
  layout?: LayoutItem[]; // optional layout configuration - array of layout items
261
361
  gutter?: LayoutItem[]; // fields to render in the dialog gutter (left side of buttons)
262
362
 
263
- toFormData?: (action: Action) => any;
264
- fromFormData?: (formData: any) => Action;
363
+ toFormData?: (action: Action) => FormData;
364
+ fromFormData?: (formData: FormData) => Action;
265
365
  }
266
-
267
- export const COLORS = {
268
- send: '#3498db',
269
- update: '#01c1af',
270
- broadcast: '#8e5ea7',
271
- call: '#e68628',
272
- create: '#df419f',
273
- save: '#1a777c',
274
- split: '#aaaaaa',
275
- execute: '#666666',
276
- wait: '#4d7dad',
277
- add: '#309c42',
278
- remove: '#e74c3c'
279
- };
package/src/flow/utils.ts CHANGED
@@ -60,17 +60,111 @@ export const renderStringList = (items: string[], icon?: string) => {
60
60
  return itemElements;
61
61
  };
62
62
 
63
- // URN scheme mapping for user-friendly display
64
- export const urnSchemeMap: Record<string, string> = {
65
- tel: 'Phone Number',
66
- email: 'Email',
67
- twitter: 'Twitter',
68
- facebook: 'Facebook',
69
- telegram: 'Telegram',
70
- whatsapp: 'WhatsApp',
71
- viber: 'Viber',
72
- line: 'Line',
73
- discord: 'Discord',
74
- slack: 'Slack',
75
- external: 'External ID'
76
- };
63
+ export interface Scheme {
64
+ scheme: string;
65
+ name: string;
66
+ path: string;
67
+ excludeFromSplit?: boolean;
68
+ }
69
+
70
+ export const SCHEMES: Scheme[] = [
71
+ {
72
+ scheme: 'tel',
73
+ name: 'SMS',
74
+ path: 'Phone Number'
75
+ },
76
+ {
77
+ scheme: 'whatsapp',
78
+ name: 'WhatsApp',
79
+ path: 'WhatsApp Number'
80
+ },
81
+ {
82
+ scheme: 'facebook',
83
+ name: 'Facebook',
84
+ path: 'Facebook ID'
85
+ },
86
+ {
87
+ scheme: 'instagram',
88
+ name: 'Instagram',
89
+ path: 'Instagram ID'
90
+ },
91
+ {
92
+ scheme: 'twitterid',
93
+ name: 'Twitter',
94
+ path: 'Twitter ID',
95
+ excludeFromSplit: true
96
+ },
97
+ {
98
+ scheme: 'telegram',
99
+ name: 'Telegram',
100
+ path: 'Telegram ID'
101
+ },
102
+ {
103
+ scheme: 'viber',
104
+ name: 'Viber',
105
+ path: 'Viber ID'
106
+ },
107
+ {
108
+ scheme: 'line',
109
+ name: 'Line',
110
+ path: 'Line ID'
111
+ },
112
+ {
113
+ scheme: 'wechat',
114
+ name: 'WeChat',
115
+ path: 'WeChat ID'
116
+ },
117
+ {
118
+ scheme: 'fcm',
119
+ name: 'Firebase',
120
+ path: 'Firebase ID'
121
+ },
122
+ {
123
+ scheme: 'jiochat',
124
+ name: 'JioChat',
125
+ path: 'JioChat ID'
126
+ },
127
+ {
128
+ scheme: 'freshchat',
129
+ name: 'Freshchat',
130
+ path: 'Freshchat ID'
131
+ },
132
+ {
133
+ scheme: 'mailto',
134
+ name: 'Email',
135
+ path: 'Email Address',
136
+ excludeFromSplit: true
137
+ },
138
+ {
139
+ scheme: 'twitter',
140
+ name: 'Twitter',
141
+ path: 'Twitter Handle',
142
+ excludeFromSplit: true
143
+ },
144
+ {
145
+ scheme: 'vk',
146
+ name: 'VK',
147
+ path: 'VK ID'
148
+ },
149
+ {
150
+ scheme: 'discord',
151
+ name: 'Discord',
152
+ path: 'Discord ID'
153
+ },
154
+ {
155
+ scheme: 'webchat',
156
+ name: 'Webchat',
157
+ path: 'Webchat ID',
158
+ excludeFromSplit: true
159
+ },
160
+ {
161
+ scheme: 'rocketchat',
162
+ name: 'RocketChat',
163
+ path: 'RocketChat ID'
164
+ },
165
+ {
166
+ scheme: 'ext',
167
+ name: 'External',
168
+ path: 'External ID'
169
+ }
170
+ ];
@@ -603,7 +603,7 @@ export class ContactSearch extends FieldElement {
603
603
  >
604
604
  </temba-checkbox>
605
605
 
606
- <div style="margin-left:0.5em">
606
+ <div>
607
607
  ${msg('Have sent a message in the last')}
608
608
  </div>
609
609
 
@@ -1,4 +1,5 @@
1
1
  import { html, TemplateResult } from 'lit';
2
+ import { spread } from '@open-wc/lit-helpers';
2
3
  import {
3
4
  FieldConfig,
4
5
  TextFieldConfig,
@@ -245,10 +246,7 @@ export class FieldRenderer {
245
246
  value="${option}"
246
247
  ></temba-option>`;
247
248
  } else {
248
- return html`<temba-option
249
- name="${option.label || option.name}"
250
- value="${option.value}"
251
- ></temba-option>`;
249
+ return html`<temba-option ${spread(option)}></temba-option>`;
252
250
  }
253
251
  })}
254
252
  </temba-select>`;
package/src/interfaces.ts CHANGED
@@ -278,6 +278,8 @@ export enum CustomEventType {
278
278
  OrderChanged = 'temba-order-changed',
279
279
  DragStart = 'temba-drag-start',
280
280
  DragStop = 'temba-drag-stop',
281
+ DragExternal = 'temba-drag-external',
282
+ DragInternal = 'temba-drag-internal',
281
283
  Resized = 'temba-resized',
282
284
  DetailsChanged = 'temba-details-changed',
283
285
  Error = 'temba-error',
@@ -288,6 +290,7 @@ export enum CustomEventType {
288
290
  DateRangeChanged = 'temba-date-range-changed',
289
291
  NodeDeleted = 'temba-node-deleted',
290
292
  ActionEditRequested = 'temba-action-edit-requested',
293
+ AddActionRequested = 'temba-add-action-requested',
291
294
  ActionSaved = 'temba-action-saved',
292
295
  ActionEditCanceled = 'temba-action-edit-canceled',
293
296
  NodeEditRequested = 'temba-node-edit-requested',
@@ -9,6 +9,10 @@ import { RapidElement } from '../RapidElement';
9
9
 
10
10
  // how far we have to drag before it starts
11
11
  const DRAG_THRESHOLD = 2;
12
+
13
+ // padding around container for external drag detection
14
+ const EXTERNAL_DRAG_PADDING = 50;
15
+
12
16
  export class SortableList extends RapidElement {
13
17
  originalDownDisplay: string;
14
18
  static get styles() {
@@ -65,6 +69,9 @@ export class SortableList extends RapidElement {
65
69
  @property({ type: String })
66
70
  gap: string = '0em';
67
71
 
72
+ @property({ type: Boolean })
73
+ externalDrag: boolean = false;
74
+
68
75
  /**
69
76
  * Optional callback to allow parent components to customize the ghost node.
70
77
  * Called after the ghost node is cloned but before it is appended to the DOM.
@@ -86,6 +93,7 @@ export class SortableList extends RapidElement {
86
93
  dropPlaceholder: HTMLDivElement = null;
87
94
  pendingDropIndex = -1;
88
95
  pendingTargetElement: HTMLElement = null;
96
+ isExternalDrag = false;
89
97
 
90
98
  private clickBlocker: ((e: MouseEvent) => void) | null = null;
91
99
 
@@ -108,6 +116,20 @@ export class SortableList extends RapidElement {
108
116
  return eles;
109
117
  }
110
118
 
119
+ private isMouseOverContainer(mouseX: number, mouseY: number): boolean {
120
+ const container = this.shadowRoot.querySelector('.container');
121
+ if (!container) return false;
122
+
123
+ const rect = container.getBoundingClientRect();
124
+ // add some padding to make it easier to stay within the container
125
+ return (
126
+ mouseX >= rect.left - EXTERNAL_DRAG_PADDING &&
127
+ mouseX <= rect.right + EXTERNAL_DRAG_PADDING &&
128
+ mouseY >= rect.top - EXTERNAL_DRAG_PADDING &&
129
+ mouseY <= rect.bottom + EXTERNAL_DRAG_PADDING
130
+ );
131
+ }
132
+
111
133
  private cloneElementWithState(element: HTMLElement): HTMLElement {
112
134
  // First create a basic clone
113
135
  const clone = element.cloneNode(true) as HTMLElement;
@@ -305,6 +327,8 @@ export class SortableList extends RapidElement {
305
327
  this.dropPlaceholder.style.minHeight = rect.height + 'px';
306
328
  this.dropPlaceholder.style.borderRadius = 'var(--curvature)';
307
329
  this.dropPlaceholder.style.flexShrink = '0';
330
+ this.dropPlaceholder.style.background = '#f3f4f6';
331
+ this.dropPlaceholder.style.border = '2px dashed #d1d5db';
308
332
  }
309
333
 
310
334
  // Insert the placeholder in the correct position in the DOM
@@ -335,10 +359,8 @@ export class SortableList extends RapidElement {
335
359
  this.dropPlaceholder.style.minHeight = rect.height + 'px';
336
360
  this.dropPlaceholder.style.borderRadius = 'var(--curvature)';
337
361
  this.dropPlaceholder.style.flexShrink = '0';
338
- this.dropPlaceholder.style.background =
339
- 'rgba(var(--color-primary-rgb), 0.1)';
340
- this.dropPlaceholder.style.border =
341
- '2px dashed rgba(var(--color-primary-rgb), 0.3)';
362
+ this.dropPlaceholder.style.background = '#f3f4f6';
363
+ this.dropPlaceholder.style.border = '2px dashed #d1d5db';
342
364
 
343
365
  // Insert the placeholder right after the hidden original element
344
366
  this.downEle.insertAdjacentElement('afterend', this.dropPlaceholder);
@@ -440,41 +462,86 @@ export class SortableList extends RapidElement {
440
462
  this.ghostElement.style.left = event.clientX - this.xOffset + 'px';
441
463
  this.ghostElement.style.top = event.clientY - this.yOffset + 'px';
442
464
 
443
- const targetInfo = this.getDropTargetInfo(event.clientX, event.clientY);
444
- if (targetInfo) {
445
- const { element: targetElement, insertAfter } = targetInfo;
446
- const targetIdx = this.getRowIndex(targetElement.id);
465
+ // check if the drag is over the container (only if external dragging is allowed)
466
+ const isOverContainer = this.externalDrag
467
+ ? this.isMouseOverContainer(event.clientX, event.clientY)
468
+ : true; // always consider "over container" if external drag is disabled
447
469
 
448
- // Use the original drag index we captured before moving the element
449
- const originalDragIdx = this.originalDragIndex;
470
+ // detect transition between internal and external drag (only if allowed)
471
+ if (this.externalDrag && !isOverContainer && !this.isExternalDrag) {
472
+ // transitioning to external drag
473
+ this.isExternalDrag = true;
474
+ this.hideDropPlaceholder();
450
475
 
451
- // Calculate where the dragged element will end up in the final array
452
- // targetIdx is the position of target element in current DOM (missing dragged element)
476
+ // hide the ghost element when dragging externally
477
+ if (this.ghostElement) {
478
+ this.ghostElement.style.display = 'none';
479
+ }
453
480
 
454
- let dropIdx;
455
- if (targetIdx < originalDragIdx) {
456
- // Target is before the original drag position - moving backward
457
- dropIdx = insertAfter ? targetIdx + 1 : targetIdx;
458
- } else {
459
- // Target was originally after the drag position - moving forward
460
- // When moving the dragged element forward (i.e., to a higher index), the targetIdx is based on the current DOM,
461
- // which no longer includes the dragged element. This means all elements after the original position have shifted left by one,
462
- // so we need to subtract 1 from targetIdx to get the correct insertion index. If inserting after the target, we use targetIdx as is.
463
- dropIdx = insertAfter ? targetIdx : targetIdx - 1;
481
+ this.fireCustomEvent(CustomEventType.DragExternal, {
482
+ id: this.downEle.id,
483
+ mouseX: event.clientX,
484
+ mouseY: event.clientY
485
+ });
486
+ } else if (this.externalDrag && isOverContainer && this.isExternalDrag) {
487
+ // transitioning back to internal drag
488
+ this.isExternalDrag = false;
489
+
490
+ // show the ghost element again when dragging internally
491
+ if (this.ghostElement) {
492
+ this.ghostElement.style.display = 'block';
464
493
  }
465
494
 
466
- // Store pending drop info but don't fire event yet
467
- this.dropTargetId = targetElement.id;
468
- this.pendingDropIndex = dropIdx;
469
- this.pendingTargetElement = targetElement;
495
+ this.fireCustomEvent(CustomEventType.DragInternal, {
496
+ id: this.downEle.id
497
+ });
498
+ }
499
+
500
+ // only show drop placeholder and calculate drop position if internal drag
501
+ if (!this.isExternalDrag) {
502
+ const targetInfo = this.getDropTargetInfo(event.clientX, event.clientY);
503
+ if (targetInfo) {
504
+ const { element: targetElement, insertAfter } = targetInfo;
505
+ const targetIdx = this.getRowIndex(targetElement.id);
506
+
507
+ // Use the original drag index we captured before moving the element
508
+ const originalDragIdx = this.originalDragIndex;
509
+
510
+ // Calculate where the dragged element will end up in the final array
511
+ // targetIdx is the position of target element in current DOM (missing dragged element)
512
+
513
+ let dropIdx;
514
+ if (targetIdx < originalDragIdx) {
515
+ // Target is before the original drag position - moving backward
516
+ dropIdx = insertAfter ? targetIdx + 1 : targetIdx;
517
+ } else {
518
+ // Target was originally after the drag position - moving forward
519
+ // When moving the dragged element forward (i.e., to a higher index), the targetIdx is based on the current DOM,
520
+ // which no longer includes the dragged element. This means all elements after the original position have shifted left by one,
521
+ // so we need to subtract 1 from targetIdx to get the correct insertion index. If inserting after the target, we use targetIdx as is.
522
+ dropIdx = insertAfter ? targetIdx : targetIdx - 1;
523
+ }
470
524
 
471
- // Show drop placeholder
472
- this.showDropPlaceholder(targetElement, insertAfter);
525
+ // Store pending drop info but don't fire event yet
526
+ this.dropTargetId = targetElement.id;
527
+ this.pendingDropIndex = dropIdx;
528
+ this.pendingTargetElement = targetElement;
529
+
530
+ // Show drop placeholder
531
+ this.showDropPlaceholder(targetElement, insertAfter);
532
+ } else {
533
+ this.hideDropPlaceholder();
534
+ this.dropTargetId = null;
535
+ this.pendingDropIndex = -1;
536
+ this.pendingTargetElement = null;
537
+ }
473
538
  } else {
474
- this.hideDropPlaceholder();
475
- this.dropTargetId = null;
476
- this.pendingDropIndex = -1;
477
- this.pendingTargetElement = null;
539
+ // external drag - continue firing external drag events with updated position
540
+ this.fireCustomEvent(CustomEventType.DragExternal, {
541
+ id: this.downEle.id,
542
+ mouseX: event.clientX,
543
+ mouseY: event.clientY
544
+ });
478
545
  }
479
546
  }
480
547
  }
@@ -498,7 +565,11 @@ export class SortableList extends RapidElement {
498
565
  this.hideDropPlaceholder();
499
566
 
500
567
  // fire the order changed event only when dropped if we have a valid drop position
501
- if (this.pendingDropIndex >= 0 && this.pendingTargetElement) {
568
+ if (
569
+ !this.isExternalDrag &&
570
+ this.pendingDropIndex >= 0 &&
571
+ this.pendingTargetElement
572
+ ) {
502
573
  // Use the original drag index we captured before hiding the element
503
574
  const originalDragIdx = this.originalDragIndex;
504
575
 
@@ -515,7 +586,10 @@ export class SortableList extends RapidElement {
515
586
  }
516
587
 
517
588
  this.fireCustomEvent(CustomEventType.DragStop, {
518
- id: this.draggingId
589
+ id: this.draggingId,
590
+ isExternal: this.isExternalDrag,
591
+ mouseX: evt.clientX,
592
+ mouseY: evt.clientY
519
593
  });
520
594
 
521
595
  this.draggingId = null;
@@ -525,6 +599,7 @@ export class SortableList extends RapidElement {
525
599
  this.originalDragIndex = -1;
526
600
  this.pendingDropIndex = -1;
527
601
  this.pendingTargetElement = null;
602
+ this.isExternalDrag = false;
528
603
 
529
604
  // Clear the ghost reference since we removed it
530
605
  this.ghostElement = null;
@@ -808,16 +808,19 @@ export class ContactChat extends ContactStoreElement {
808
808
  };
809
809
  } else if (event._user) {
810
810
  return event._user;
811
- } else if (event.created_by) {
812
- return {
813
- email: event.created_by.email,
814
- name: `${event.created_by.first_name} ${event.created_by.last_name}`.trim(),
815
- avatar: event.created_by.avatar
816
- };
817
811
  }
818
812
  return null;
819
813
  }
820
814
 
815
+ private isMessageError(status: string | { status: string }): boolean {
816
+ if (typeof status === 'string') {
817
+ return status === 'E' || status === 'F';
818
+ } else if (status && typeof status === 'object' && 'status' in status) {
819
+ return status.status === 'errored' || status.status === 'failed';
820
+ }
821
+ return false;
822
+ }
823
+
821
824
  private createMessages(page: ContactHistoryPage): ChatEvent[] {
822
825
  if (page.events) {
823
826
  let messages = [];
@@ -852,13 +855,9 @@ export class ContactChat extends ContactStoreElement {
852
855
  } else if (
853
856
  event.type === 'msg_created' ||
854
857
  event.type === 'msg_received' ||
855
- event.type === 'ivr_created' ||
856
- event.type === 'broadcast_created'
858
+ event.type === 'ivr_created'
857
859
  ) {
858
860
  const msgEvent = event as MsgEvent;
859
- const status = msgEvent.status || msgEvent._status;
860
- const failedReason =
861
- msgEvent.failed_reason_display || msgEvent._failed_reason;
862
861
 
863
862
  messages.push({
864
863
  id: event.uuid,
@@ -867,7 +866,7 @@ export class ContactChat extends ContactStoreElement {
867
866
  date: new Date(msgEvent.created_on),
868
867
  attachments: msgEvent.msg.attachments,
869
868
  text: msgEvent.msg.text,
870
- sendError: status === 'E' || status === 'F',
869
+ sendError: this.isMessageError(msgEvent._status),
871
870
  popup: html`<div
872
871
  style="display: flex; flex-direction: row; align-items:center; justify-content: space-between;font-size:0.9em;line-height:1em;min-width:10em"
873
872
  >
@@ -882,20 +881,18 @@ export class ContactChat extends ContactStoreElement {
882
881
  ${msgEvent.optin.name}
883
882
  </div>`
884
883
  : null}
885
- ${failedReason
884
+ ${msgEvent._failed_reason
886
885
  ? html`
887
886
  <div
888
887
  style="margin-top:0.2em;margin-right: 0.5em;min-width:10em;max-width:15em;color:var(--color-error);font-size:0.9em"
889
888
  >
890
- ${failedReason}
889
+ ${msgEvent._failed_reason}
891
890
  </div>
892
891
  `
893
892
  : null}
894
893
  </div>
895
- ${msgEvent.logs_url || msgEvent._logs_url
896
- ? html`<a
897
- style="margin-left:0.5em"
898
- href="${msgEvent.logs_url || msgEvent._logs_url}"
894
+ ${msgEvent._logs_url
895
+ ? html`<a style="margin-left:0.5em" href="${msgEvent._logs_url}"
899
896
  ><temba-icon name="log"></temba-icon
900
897
  ></a>`
901
898
  : null}