@nyaruka/temba-components 0.131.1 → 0.131.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (485) hide show
  1. package/.github/workflows/publish.yml +4 -1
  2. package/CHANGELOG.md +75 -1
  3. package/demo/components/floating-tabs/example.html +400 -0
  4. package/demo/components/flow/index.html +1 -1
  5. package/demo/data/flows/food-order.json +2 -2
  6. package/demo/data/flows/sample-flow.json +113 -125
  7. package/demo/data/flows/voicemail.json +613 -0
  8. package/demo/index.html +6 -0
  9. package/dist/locales/es.js +5 -5
  10. package/dist/locales/es.js.map +1 -1
  11. package/dist/locales/fr.js +5 -5
  12. package/dist/locales/fr.js.map +1 -1
  13. package/dist/locales/locale-codes.js +11 -2
  14. package/dist/locales/locale-codes.js.map +1 -1
  15. package/dist/locales/pt.js +5 -5
  16. package/dist/locales/pt.js.map +1 -1
  17. package/dist/static/svg/index.svg +1 -1
  18. package/dist/temba-components.js +1773 -662
  19. package/dist/temba-components.js.map +1 -1
  20. package/out-tsc/src/Icons.js +4 -1
  21. package/out-tsc/src/Icons.js.map +1 -1
  22. package/out-tsc/src/display/FloatingTab.js +167 -0
  23. package/out-tsc/src/display/FloatingTab.js.map +1 -0
  24. package/out-tsc/src/display/ProgressBar.js +22 -2
  25. package/out-tsc/src/display/ProgressBar.js.map +1 -1
  26. package/out-tsc/src/events.js.map +1 -1
  27. package/out-tsc/src/flow/CanvasMenu.js +200 -0
  28. package/out-tsc/src/flow/CanvasMenu.js.map +1 -0
  29. package/out-tsc/src/flow/CanvasNode.js +489 -47
  30. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  31. package/out-tsc/src/flow/Editor.js +1417 -67
  32. package/out-tsc/src/flow/Editor.js.map +1 -1
  33. package/out-tsc/src/flow/NodeEditor.js +479 -112
  34. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  35. package/out-tsc/src/flow/NodeTypeSelector.js +540 -0
  36. package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -0
  37. package/out-tsc/src/flow/StickyNote.js +12 -3
  38. package/out-tsc/src/flow/StickyNote.js.map +1 -1
  39. package/out-tsc/src/flow/actions/add_contact_groups.js +4 -3
  40. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
  41. package/out-tsc/src/flow/actions/add_contact_urn.js +63 -4
  42. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  43. package/out-tsc/src/flow/actions/add_input_labels.js +4 -3
  44. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  45. package/out-tsc/src/flow/actions/play_audio.js +3 -2
  46. package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
  47. package/out-tsc/src/flow/actions/remove_contact_groups.js +7 -5
  48. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
  49. package/out-tsc/src/flow/actions/request_optin.js +3 -2
  50. package/out-tsc/src/flow/actions/request_optin.js.map +1 -1
  51. package/out-tsc/src/flow/actions/say_msg.js +3 -2
  52. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  53. package/out-tsc/src/flow/actions/send_broadcast.js +77 -23
  54. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  55. package/out-tsc/src/flow/actions/send_email.js +5 -5
  56. package/out-tsc/src/flow/actions/send_email.js.map +1 -1
  57. package/out-tsc/src/flow/actions/send_msg.js +101 -21
  58. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  59. package/out-tsc/src/flow/actions/set_contact_channel.js +6 -9
  60. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  61. package/out-tsc/src/flow/actions/set_contact_field.js +20 -20
  62. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  63. package/out-tsc/src/flow/actions/set_contact_language.js +3 -2
  64. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  65. package/out-tsc/src/flow/actions/set_contact_name.js +3 -12
  66. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  67. package/out-tsc/src/flow/actions/set_contact_status.js +3 -2
  68. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  69. package/out-tsc/src/flow/actions/set_run_result.js +4 -3
  70. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  71. package/out-tsc/src/flow/actions/start_session.js +181 -6
  72. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  73. package/out-tsc/src/flow/config.js +11 -23
  74. package/out-tsc/src/flow/config.js.map +1 -1
  75. package/out-tsc/src/flow/currencies.js +45 -0
  76. package/out-tsc/src/flow/currencies.js.map +1 -0
  77. package/out-tsc/src/flow/nodes/shared-rules.js +257 -0
  78. package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -0
  79. package/out-tsc/src/flow/nodes/shared.js +71 -0
  80. package/out-tsc/src/flow/nodes/shared.js.map +1 -0
  81. package/out-tsc/src/flow/nodes/split_by_airtime.js +211 -5
  82. package/out-tsc/src/flow/nodes/split_by_airtime.js.map +1 -1
  83. package/out-tsc/src/flow/nodes/split_by_contact_field.js +152 -3
  84. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -1
  85. package/out-tsc/src/flow/nodes/split_by_expression.js +73 -2
  86. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -1
  87. package/out-tsc/src/flow/nodes/split_by_groups.js +18 -10
  88. package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -1
  89. package/out-tsc/src/flow/nodes/split_by_intent.js +8 -0
  90. package/out-tsc/src/flow/nodes/split_by_intent.js.map +1 -0
  91. package/out-tsc/src/flow/nodes/split_by_llm.js +11 -3
  92. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
  93. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +10 -3
  94. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  95. package/out-tsc/src/flow/nodes/split_by_random.js +10 -4
  96. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
  97. package/out-tsc/src/flow/nodes/split_by_resthook.js +113 -0
  98. package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -0
  99. package/out-tsc/src/flow/nodes/split_by_run_result.js +211 -3
  100. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
  101. package/out-tsc/src/flow/nodes/split_by_scheme.js +158 -2
  102. package/out-tsc/src/flow/nodes/split_by_scheme.js.map +1 -1
  103. package/out-tsc/src/flow/nodes/split_by_subflow.js +13 -5
  104. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  105. package/out-tsc/src/flow/nodes/split_by_ticket.js +10 -3
  106. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -1
  107. package/out-tsc/src/flow/nodes/split_by_webhook.js +10 -3
  108. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
  109. package/out-tsc/src/flow/nodes/wait_for_digits.js +3 -2
  110. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
  111. package/out-tsc/src/flow/nodes/wait_for_menu.js +3 -2
  112. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
  113. package/out-tsc/src/flow/nodes/wait_for_response.js +38 -568
  114. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  115. package/out-tsc/src/flow/types.js +86 -12
  116. package/out-tsc/src/flow/types.js.map +1 -1
  117. package/out-tsc/src/flow/utils.js +101 -14
  118. package/out-tsc/src/flow/utils.js.map +1 -1
  119. package/out-tsc/src/form/FieldRenderer.js +2 -4
  120. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  121. package/out-tsc/src/interfaces.js +3 -0
  122. package/out-tsc/src/interfaces.js.map +1 -1
  123. package/out-tsc/src/layout/FloatingWindow.js +346 -0
  124. package/out-tsc/src/layout/FloatingWindow.js.map +1 -0
  125. package/out-tsc/src/list/SortableList.js +98 -33
  126. package/out-tsc/src/list/SortableList.js.map +1 -1
  127. package/out-tsc/src/live/ContactChat.js +6 -25
  128. package/out-tsc/src/live/ContactChat.js.map +1 -1
  129. package/out-tsc/src/locales/es.js +5 -5
  130. package/out-tsc/src/locales/es.js.map +1 -1
  131. package/out-tsc/src/locales/fr.js +5 -5
  132. package/out-tsc/src/locales/fr.js.map +1 -1
  133. package/out-tsc/src/locales/locale-codes.js +11 -2
  134. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  135. package/out-tsc/src/locales/pt.js +5 -5
  136. package/out-tsc/src/locales/pt.js.map +1 -1
  137. package/out-tsc/src/store/AppState.js +120 -0
  138. package/out-tsc/src/store/AppState.js.map +1 -1
  139. package/out-tsc/src/utils.js +254 -13
  140. package/out-tsc/src/utils.js.map +1 -1
  141. package/out-tsc/temba-modules.js +8 -0
  142. package/out-tsc/temba-modules.js.map +1 -1
  143. package/out-tsc/test/ActionHelper.js +3 -3
  144. package/out-tsc/test/ActionHelper.js.map +1 -1
  145. package/out-tsc/test/NodeHelper.js +6 -3
  146. package/out-tsc/test/NodeHelper.js.map +1 -1
  147. package/out-tsc/test/actions/add_contact_urn.test.js +202 -0
  148. package/out-tsc/test/actions/add_contact_urn.test.js.map +1 -0
  149. package/out-tsc/test/actions/send_broadcast.test.js +148 -0
  150. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -0
  151. package/out-tsc/test/actions/send_email.test.js +17 -23
  152. package/out-tsc/test/actions/send_email.test.js.map +1 -1
  153. package/out-tsc/test/actions/send_msg.test.js +33 -15
  154. package/out-tsc/test/actions/send_msg.test.js.map +1 -1
  155. package/out-tsc/test/actions/start_session.test.js +116 -0
  156. package/out-tsc/test/actions/start_session.test.js.map +1 -0
  157. package/out-tsc/test/nodes/split_by_airtime.test.js +604 -0
  158. package/out-tsc/test/nodes/split_by_airtime.test.js.map +1 -0
  159. package/out-tsc/test/nodes/split_by_contact_field.test.js +387 -0
  160. package/out-tsc/test/nodes/split_by_contact_field.test.js.map +1 -0
  161. package/out-tsc/test/nodes/split_by_expression.test.js +614 -0
  162. package/out-tsc/test/nodes/split_by_expression.test.js.map +1 -0
  163. package/out-tsc/test/nodes/split_by_random.test.js +3 -3
  164. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -1
  165. package/out-tsc/test/nodes/split_by_resthook.test.js +337 -0
  166. package/out-tsc/test/nodes/split_by_resthook.test.js.map +1 -0
  167. package/out-tsc/test/nodes/split_by_run_result.test.js +920 -0
  168. package/out-tsc/test/nodes/split_by_run_result.test.js.map +1 -0
  169. package/out-tsc/test/nodes/split_by_scheme.test.js +399 -0
  170. package/out-tsc/test/nodes/split_by_scheme.test.js.map +1 -0
  171. package/out-tsc/test/nodes/split_by_subflow.test.js +333 -0
  172. package/out-tsc/test/nodes/split_by_subflow.test.js.map +1 -0
  173. package/out-tsc/test/nodes/wait_for_digits.test.js +2 -2
  174. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  175. package/out-tsc/test/nodes/wait_for_response.test.js +2 -1
  176. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -1
  177. package/out-tsc/test/temba-action-drag-between-nodes.test.js +252 -0
  178. package/out-tsc/test/temba-action-drag-between-nodes.test.js.map +1 -0
  179. package/out-tsc/test/temba-canvas-menu.test.js +122 -0
  180. package/out-tsc/test/temba-canvas-menu.test.js.map +1 -0
  181. package/out-tsc/test/temba-floating-tab.test.js +91 -0
  182. package/out-tsc/test/temba-floating-tab.test.js.map +1 -0
  183. package/out-tsc/test/temba-floating-window.test.js +301 -0
  184. package/out-tsc/test/temba-floating-window.test.js.map +1 -0
  185. package/out-tsc/test/temba-flow-editor-node.test.js +202 -2
  186. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  187. package/out-tsc/test/temba-flow-editor.test.js +7 -8
  188. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  189. package/out-tsc/test/temba-localization.test.js +471 -0
  190. package/out-tsc/test/temba-localization.test.js.map +1 -0
  191. package/out-tsc/test/temba-node-editor.test.js +3 -1
  192. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  193. package/out-tsc/test/temba-node-type-selector.test.js +265 -0
  194. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -0
  195. package/out-tsc/test/temba-omnibox.test.js +2 -1
  196. package/out-tsc/test/temba-omnibox.test.js.map +1 -1
  197. package/out-tsc/test/temba-sortable-list.test.js +51 -0
  198. package/out-tsc/test/temba-sortable-list.test.js.map +1 -1
  199. package/out-tsc/test/temba-utils-index.test.js +1 -27
  200. package/out-tsc/test/temba-utils-index.test.js.map +1 -1
  201. package/out-tsc/test/utils.test.js +20 -0
  202. package/out-tsc/test/utils.test.js.map +1 -1
  203. package/package.json +2 -1
  204. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  205. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  206. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  207. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  208. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  209. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  210. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  211. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  212. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  213. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  214. package/screenshots/truth/actions/add_contact_urn/editor/expression-facebook.png +0 -0
  215. package/screenshots/truth/actions/add_contact_urn/editor/expression-phone.png +0 -0
  216. package/screenshots/truth/actions/add_contact_urn/editor/facebook-id.png +0 -0
  217. package/screenshots/truth/actions/add_contact_urn/editor/instagram-handle.png +0 -0
  218. package/screenshots/truth/actions/add_contact_urn/editor/line-id.png +0 -0
  219. package/screenshots/truth/actions/add_contact_urn/editor/phone-number.png +0 -0
  220. package/screenshots/truth/actions/add_contact_urn/editor/telegram-id.png +0 -0
  221. package/screenshots/truth/actions/add_contact_urn/editor/viber-id.png +0 -0
  222. package/screenshots/truth/actions/add_contact_urn/editor/wechat-id.png +0 -0
  223. package/screenshots/truth/actions/add_contact_urn/editor/whatsapp.png +0 -0
  224. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  225. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  226. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  227. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  228. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  229. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  230. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  231. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  232. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  233. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  234. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  235. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  236. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  237. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  238. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  239. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  240. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  241. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  242. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  243. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  244. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  245. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  246. package/screenshots/truth/actions/send_broadcast/editor/contacts-only.png +0 -0
  247. package/screenshots/truth/actions/send_broadcast/editor/groups-and-contacts.png +0 -0
  248. package/screenshots/truth/actions/send_broadcast/editor/groups-only.png +0 -0
  249. package/screenshots/truth/actions/send_broadcast/editor/many-groups.png +0 -0
  250. package/screenshots/truth/actions/send_broadcast/editor/multiline-text.png +0 -0
  251. package/screenshots/truth/actions/send_broadcast/editor/with-attachments.png +0 -0
  252. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  253. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  254. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  255. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  256. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  257. package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
  258. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  259. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  260. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  261. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  262. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  263. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  264. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  265. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  266. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  267. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  268. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  269. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  270. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  271. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  272. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  273. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  274. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  275. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  276. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  277. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  278. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  279. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  280. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  281. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  282. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  283. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  284. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  285. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  286. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  287. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  288. package/screenshots/truth/actions/start_session/editor/contact-query.png +0 -0
  289. package/screenshots/truth/actions/start_session/editor/contacts-only.png +0 -0
  290. package/screenshots/truth/actions/start_session/editor/create-contact.png +0 -0
  291. package/screenshots/truth/actions/start_session/editor/groups-and-contacts.png +0 -0
  292. package/screenshots/truth/actions/start_session/editor/groups-only.png +0 -0
  293. package/screenshots/truth/actions/start_session/editor/many-recipients.png +0 -0
  294. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  295. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  296. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  297. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  298. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  299. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  300. package/screenshots/truth/canvas-menu/open.png +0 -0
  301. package/screenshots/truth/editor/router.png +0 -0
  302. package/screenshots/truth/editor/wait.png +0 -0
  303. package/screenshots/truth/floating-tab/default.png +0 -0
  304. package/screenshots/truth/floating-tab/gray.png +0 -0
  305. package/screenshots/truth/floating-tab/green.png +0 -0
  306. package/screenshots/truth/floating-tab/hidden.png +0 -0
  307. package/screenshots/truth/floating-tab/hover.png +0 -0
  308. package/screenshots/truth/floating-tab/purple.png +0 -0
  309. package/screenshots/truth/floating-window/chromeless.png +0 -0
  310. package/screenshots/truth/floating-window/custom-size.png +0 -0
  311. package/screenshots/truth/floating-window/default.png +0 -0
  312. package/screenshots/truth/floating-window/with-header.png +0 -0
  313. package/screenshots/truth/list/fields-dragging.png +0 -0
  314. package/screenshots/truth/list/sortable-dragging.png +0 -0
  315. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  316. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  317. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  318. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  319. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  320. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  321. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  322. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  323. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  324. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  325. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  326. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  327. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  328. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  329. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  330. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  331. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  332. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  333. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  334. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  335. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  336. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  337. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  338. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  339. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  340. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  341. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  342. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  343. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  344. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  345. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  346. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  347. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  348. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  349. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  350. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  351. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  352. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  353. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  354. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  355. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  356. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  357. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  358. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  359. package/src/Icons.ts +4 -1
  360. package/src/display/FloatingTab.ts +174 -0
  361. package/src/display/ProgressBar.ts +22 -2
  362. package/src/events.ts +2 -8
  363. package/src/flow/CanvasMenu.ts +217 -0
  364. package/src/flow/CanvasNode.ts +596 -40
  365. package/src/flow/Editor.ts +1721 -45
  366. package/src/flow/NodeEditor.ts +621 -144
  367. package/src/flow/NodeTypeSelector.ts +636 -0
  368. package/src/flow/StickyNote.ts +12 -3
  369. package/src/flow/actions/add_contact_groups.ts +5 -4
  370. package/src/flow/actions/add_contact_urn.ts +78 -4
  371. package/src/flow/actions/add_input_labels.ts +5 -4
  372. package/src/flow/actions/play_audio.ts +3 -2
  373. package/src/flow/actions/remove_contact_groups.ts +16 -6
  374. package/src/flow/actions/request_optin.ts +3 -2
  375. package/src/flow/actions/say_msg.ts +3 -2
  376. package/src/flow/actions/send_broadcast.ts +86 -23
  377. package/src/flow/actions/send_email.ts +12 -6
  378. package/src/flow/actions/send_msg.ts +155 -34
  379. package/src/flow/actions/set_contact_channel.ts +6 -11
  380. package/src/flow/actions/set_contact_field.ts +21 -25
  381. package/src/flow/actions/set_contact_language.ts +11 -4
  382. package/src/flow/actions/set_contact_name.ts +4 -15
  383. package/src/flow/actions/set_contact_status.ts +4 -3
  384. package/src/flow/actions/set_run_result.ts +5 -4
  385. package/src/flow/actions/start_session.ts +210 -6
  386. package/src/flow/config.ts +11 -23
  387. package/src/flow/currencies.ts +51 -0
  388. package/src/flow/nodes/shared-rules.ts +301 -0
  389. package/src/flow/nodes/shared.ts +87 -0
  390. package/src/flow/nodes/split_by_airtime.ts +255 -5
  391. package/src/flow/nodes/split_by_contact_field.ts +195 -3
  392. package/src/flow/nodes/split_by_expression.ts +104 -2
  393. package/src/flow/nodes/split_by_groups.ts +26 -11
  394. package/src/flow/nodes/split_by_intent.ts +8 -0
  395. package/src/flow/nodes/split_by_llm.ts +22 -4
  396. package/src/flow/nodes/split_by_llm_categorize.ts +22 -5
  397. package/src/flow/nodes/split_by_random.ts +16 -6
  398. package/src/flow/nodes/split_by_resthook.ts +140 -0
  399. package/src/flow/nodes/split_by_run_result.ts +259 -3
  400. package/src/flow/nodes/split_by_scheme.ts +202 -2
  401. package/src/flow/nodes/split_by_subflow.ts +17 -5
  402. package/src/flow/nodes/split_by_ticket.ts +15 -4
  403. package/src/flow/nodes/split_by_webhook.ts +17 -6
  404. package/src/flow/nodes/wait_for_digits.ts +3 -2
  405. package/src/flow/nodes/wait_for_menu.ts +3 -2
  406. package/src/flow/nodes/wait_for_response.ts +59 -680
  407. package/src/flow/types.ts +156 -23
  408. package/src/flow/utils.ts +108 -14
  409. package/src/form/FieldRenderer.ts +2 -4
  410. package/src/interfaces.ts +3 -0
  411. package/src/layout/FloatingWindow.ts +386 -0
  412. package/src/list/SortableList.ts +109 -34
  413. package/src/live/ContactChat.ts +7 -25
  414. package/src/locales/es.ts +18 -13
  415. package/src/locales/fr.ts +18 -13
  416. package/src/locales/locale-codes.ts +11 -2
  417. package/src/locales/pt.ts +18 -13
  418. package/src/store/AppState.ts +173 -0
  419. package/src/store/flow-definition.d.ts +2 -5
  420. package/src/utils.ts +332 -12
  421. package/static/api/channels.json +46 -0
  422. package/static/api/llms.json +18 -0
  423. package/static/api/resthooks.json +31 -0
  424. package/static/svg/index.svg +1 -1
  425. package/static/svg/work/traced/lightning-02.svg +1 -0
  426. package/static/svg/work/used/lightning-02.svg +3 -0
  427. package/temba-modules.ts +8 -0
  428. package/test/ActionHelper.ts +3 -3
  429. package/test/NodeHelper.ts +6 -3
  430. package/test/actions/add_contact_urn.test.ts +287 -0
  431. package/test/actions/send_broadcast.test.ts +190 -0
  432. package/test/actions/send_email.test.ts +17 -23
  433. package/test/actions/send_msg.test.ts +39 -15
  434. package/test/actions/start_session.test.ts +151 -0
  435. package/test/nodes/split_by_airtime.test.ts +673 -0
  436. package/test/nodes/split_by_contact_field.test.ts +451 -0
  437. package/test/nodes/split_by_expression.test.ts +751 -0
  438. package/test/nodes/split_by_random.test.ts +3 -3
  439. package/test/nodes/split_by_resthook.test.ts +398 -0
  440. package/test/nodes/split_by_run_result.test.ts +1109 -0
  441. package/test/nodes/split_by_scheme.test.ts +486 -0
  442. package/test/nodes/split_by_subflow.test.ts +381 -0
  443. package/test/nodes/wait_for_digits.test.ts +2 -2
  444. package/test/nodes/wait_for_response.test.ts +2 -1
  445. package/test/temba-action-drag-between-nodes.test.ts +301 -0
  446. package/test/temba-canvas-menu.test.ts +156 -0
  447. package/test/temba-floating-tab.test.ts +110 -0
  448. package/test/temba-floating-window.test.ts +477 -0
  449. package/test/temba-flow-editor-node.test.ts +246 -2
  450. package/test/temba-flow-editor.test.ts +7 -8
  451. package/test/temba-localization.test.ts +611 -0
  452. package/test/temba-node-editor.test.ts +3 -1
  453. package/test/temba-node-type-selector.test.ts +355 -0
  454. package/test/temba-omnibox.test.ts +2 -1
  455. package/test/temba-sortable-list.test.ts +69 -0
  456. package/test/temba-utils-index.test.ts +0 -35
  457. package/test/utils.test.ts +22 -0
  458. package/test-assets/contacts/history.json +14 -21
  459. package/test-assets/select/llms.json +2 -2
  460. package/web-dev-server.config.mjs +49 -1
  461. package/web-test-runner.config.mjs +0 -1
  462. package/out-tsc/src/flow/actions/call_classifier.js +0 -11
  463. package/out-tsc/src/flow/actions/call_classifier.js.map +0 -1
  464. package/out-tsc/src/flow/actions/call_resthook.js +0 -11
  465. package/out-tsc/src/flow/actions/call_resthook.js.map +0 -1
  466. package/out-tsc/src/flow/actions/split_by_expression_example.js +0 -77
  467. package/out-tsc/src/flow/actions/split_by_expression_example.js.map +0 -1
  468. package/out-tsc/src/flow/actions/transfer_airtime.js +0 -11
  469. package/out-tsc/src/flow/actions/transfer_airtime.js.map +0 -1
  470. package/out-tsc/src/flow/nodes/wait_for_audio.js +0 -7
  471. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +0 -1
  472. package/out-tsc/src/flow/nodes/wait_for_image.js +0 -7
  473. package/out-tsc/src/flow/nodes/wait_for_image.js.map +0 -1
  474. package/out-tsc/src/flow/nodes/wait_for_location.js +0 -7
  475. package/out-tsc/src/flow/nodes/wait_for_location.js.map +0 -1
  476. package/out-tsc/src/flow/nodes/wait_for_video.js +0 -7
  477. package/out-tsc/src/flow/nodes/wait_for_video.js.map +0 -1
  478. package/src/flow/actions/call_classifier.ts +0 -12
  479. package/src/flow/actions/call_resthook.ts +0 -12
  480. package/src/flow/actions/split_by_expression_example.ts +0 -88
  481. package/src/flow/actions/transfer_airtime.ts +0 -12
  482. package/src/flow/nodes/wait_for_audio.ts +0 -7
  483. package/src/flow/nodes/wait_for_image.ts +0 -7
  484. package/src/flow/nodes/wait_for_location.ts +0 -7
  485. package/src/flow/nodes/wait_for_video.ts +0 -7
@@ -7,6 +7,9 @@ import { fromStore, zustand } from '../store/AppState';
7
7
  import { RapidElement } from '../RapidElement';
8
8
  import { repeat } from 'lit-html/directives/repeat.js';
9
9
  import { CustomEventType } from '../interfaces';
10
+ import { generateUUID, postJSON } from '../utils';
11
+ import { ACTION_CONFIG, NODE_CONFIG } from './config';
12
+ import { ACTION_GROUP_METADATA } from './types';
10
13
  import { Plumber } from './Plumber';
11
14
  import { CanvasNode } from './CanvasNode';
12
15
  export function snapToGrid(value) {
@@ -24,6 +27,11 @@ export function findNodeForExit(definition, exitUuid) {
24
27
  }
25
28
  const SAVE_QUIET_TIME = 500;
26
29
  const DRAG_THRESHOLD = 5;
30
+ const AUTO_TRANSLATE_MODELS_ENDPOINT = '/api/internal/llms.json';
31
+ // Offset for positioning dropped action node relative to mouse cursor
32
+ // Keep small to make drop location close to cursor position
33
+ const DROP_PREVIEW_OFFSET_X = 20;
34
+ const DROP_PREVIEW_OFFSET_Y = 20;
27
35
  export class Editor extends RapidElement {
28
36
  // unfortunately, jsplumb requires that we be in light DOM
29
37
  createRenderRoot() {
@@ -33,6 +41,18 @@ export class Editor extends RapidElement {
33
41
  get dragging() {
34
42
  return this.isDragging;
35
43
  }
44
+ getAvailableLanguages() {
45
+ var _b, _c;
46
+ // Use languages from flow definition if available, otherwise use defaults
47
+ if (((_c = (_b = this.definition) === null || _b === void 0 ? void 0 : _b._ui) === null || _c === void 0 ? void 0 : _c.languages) &&
48
+ this.definition._ui.languages.length > 0) {
49
+ return this.definition._ui.languages.map((lang) => ({
50
+ code: typeof lang === 'string' ? lang : lang.iso || lang.code,
51
+ name: typeof lang === 'string' ? lang : lang.name
52
+ }));
53
+ }
54
+ return this.DEFAULT_LANGUAGES;
55
+ }
36
56
  static get styles() {
37
57
  return css `
38
58
  #editor {
@@ -176,12 +196,193 @@ export class Editor extends RapidElement {
176
196
  .jtk-floating-endpoint {
177
197
  pointer-events: none;
178
198
  }
199
+
200
+ .localization-window-content {
201
+ display: flex;
202
+ flex-direction: column;
203
+ gap: 16px;
204
+ height: 100%;
205
+ }
206
+
207
+ .localization-header {
208
+ font-size: 13px;
209
+ color: #4b5563;
210
+ line-height: 1.4;
211
+ }
212
+
213
+ .localization-language-select {
214
+ --color-widget-border: #d1d5db;
215
+ --color-widget-background: #fff;
216
+ }
217
+
218
+ .localization-language-row {
219
+ display: flex;
220
+ align-items: flex-end;
221
+ gap: 12px;
222
+ }
223
+
224
+ .localization-language-row temba-select {
225
+ flex: 1;
226
+ }
227
+
228
+ .localization-progress {
229
+ margin-top: auto;
230
+ display: flex;
231
+ flex-direction: column;
232
+ gap: 8px;
233
+ }
234
+
235
+ .localization-progress-bar-row {
236
+ display: flex;
237
+ align-items: center;
238
+ gap: 8px;
239
+ }
240
+
241
+ .localization-progress-trigger {
242
+ flex: 1;
243
+ border-radius: 6px;
244
+ cursor: pointer;
245
+ display: flex;
246
+ align-items: center;
247
+ }
248
+
249
+ .localization-progress-trigger:focus-visible {
250
+ outline: 2px solid #94a3b8;
251
+ outline-offset: 2px;
252
+ }
253
+
254
+ .localization-progress-trigger temba-progress {
255
+ flex: 1;
256
+ }
257
+
258
+ .localization-progress h5 {
259
+ margin: 0;
260
+ font-size: 13px;
261
+ font-weight: 600;
262
+ color: #374151;
263
+ }
264
+
265
+ .localization-progress-summary {
266
+ font-size: 12px;
267
+ color: #6b7280;
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 6px;
271
+ min-height: 20px;
272
+ }
273
+
274
+ .translation-settings-toggle {
275
+ display: inline-flex;
276
+ align-items: center;
277
+ gap: 6px;
278
+ background: transparent;
279
+ border: none;
280
+ color: #6b7280;
281
+ font-size: 12px;
282
+ font-weight: 600;
283
+ cursor: pointer;
284
+ padding: 4px;
285
+ border-radius: 4px;
286
+ }
287
+
288
+ .translation-settings-label {
289
+ font-size: 12px;
290
+ color: #6b7280;
291
+ }
292
+
293
+ .translation-settings-toggle:focus-visible {
294
+ outline: 2px solid #94a3b8;
295
+ outline-offset: 2px;
296
+ }
297
+
298
+ .translation-settings-arrow {
299
+ width: 8px;
300
+ height: 8px;
301
+ border-right: 2px solid currentColor;
302
+ border-bottom: 2px solid currentColor;
303
+ transform: rotate(-45deg);
304
+ transition: transform 0.2s ease;
305
+ margin-left: 2px;
306
+ }
307
+
308
+ .translation-settings-arrow.expanded {
309
+ transform: rotate(45deg);
310
+ }
311
+
312
+ .translation-settings {
313
+ }
314
+
315
+ .translation-settings-row {
316
+ display: flex;
317
+ align-items: center;
318
+ justify-content: space-between;
319
+ }
320
+
321
+ .translation-settings-row temba-checkbox {
322
+ width: 100%;
323
+ }
324
+
325
+ .auto-translate-button {
326
+ background: var(--color-primary-dark);
327
+ border: none;
328
+ color: #fff;
329
+ padding: 10px 12px;
330
+ border-radius: var(--curvature);
331
+ font-size: 12px;
332
+ font-weight: 600;
333
+ cursor: pointer;
334
+ transition: opacity 0.2s ease;
335
+ }
336
+
337
+ .auto-translate-button[disabled] {
338
+ opacity: 0.5;
339
+ cursor: not-allowed;
340
+ }
341
+
342
+ .auto-translate-error {
343
+ font-size: 12px;
344
+ color: #b91c1c;
345
+ }
346
+
347
+ .auto-translate-dialog-content {
348
+ padding: 20px;
349
+ display: flex;
350
+ flex-direction: column;
351
+ gap: 12px;
352
+ font-size: 14px;
353
+ color: #374151;
354
+ }
355
+
356
+ .auto-translate-dialog-content p {
357
+ margin: 0;
358
+ }
359
+
360
+ .auto-translate-loading {
361
+ display: flex;
362
+ align-items: center;
363
+ gap: 8px;
364
+ font-size: 13px;
365
+ color: #6b7280;
366
+ }
367
+
368
+ .auto-translate-empty {
369
+ font-size: 13px;
370
+ color: #6b7280;
371
+ }
372
+
373
+ .localization-empty {
374
+ font-size: 13px;
375
+ color: #9ca3af;
376
+ white-space: nowrap;
377
+ }
179
378
  `;
180
379
  }
181
380
  constructor() {
182
381
  super();
183
382
  // timer for debounced saving
184
383
  this.saveTimer = null;
384
+ this.flowType = 'message';
385
+ this.features = [];
185
386
  // Drag state
186
387
  this.isDragging = false;
187
388
  this.isMouseDown = false;
@@ -196,17 +397,42 @@ export class Editor extends RapidElement {
196
397
  this.sourceId = null;
197
398
  this.dragFromNodeId = null;
198
399
  this.isValidTarget = true;
400
+ this.localizationWindowHidden = true;
401
+ this.translationFilters = {
402
+ categories: false
403
+ };
404
+ this.translationSettingsExpanded = false;
405
+ this.autoTranslateDialogOpen = false;
406
+ this.autoTranslating = false;
407
+ this.autoTranslateModel = null;
408
+ this.autoTranslateError = null;
409
+ this.translationCache = new Map();
199
410
  // NodeEditor state - handles both node and action editing
200
411
  this.editingNode = null;
201
412
  this.editingNodeUI = null;
202
413
  this.editingAction = null;
414
+ this.isCreatingNewNode = false;
415
+ this.pendingNodePosition = null;
416
+ // Canvas drop state for dragging actions to canvas
417
+ this.canvasDropPreview = null;
418
+ this.addActionToNodeUuid = null;
419
+ // Track target node for action drag
420
+ this.actionDragTargetNodeUuid = null;
421
+ // Track previous target node to clear placeholder when moving between nodes
422
+ this.previousActionDragTargetNodeUuid = null;
203
423
  this.canvasMouseDown = false;
424
+ // Default languages if not specified in flow definition
425
+ this.DEFAULT_LANGUAGES = [
426
+ { code: 'eng', name: 'English' },
427
+ { code: 'fra', name: 'French' },
428
+ { code: 'esp', name: 'Spanish' }
429
+ ];
204
430
  // Bound event handlers to maintain proper 'this' context
205
431
  this.boundMouseMove = this.handleMouseMove.bind(this);
206
432
  this.boundMouseUp = this.handleMouseUp.bind(this);
207
433
  this.boundGlobalMouseDown = this.handleGlobalMouseDown.bind(this);
208
434
  this.boundKeyDown = this.handleKeyDown.bind(this);
209
- this.boundCanvasDoubleClick = this.handleCanvasDoubleClick.bind(this);
435
+ this.boundCanvasContextMenu = this.handleCanvasContextMenu.bind(this);
210
436
  }
211
437
  firstUpdated(changes) {
212
438
  super.firstUpdated(changes);
@@ -248,18 +474,54 @@ export class Editor extends RapidElement {
248
474
  this.isValidTarget = true;
249
475
  }
250
476
  updated(changes) {
477
+ var _b, _c, _d;
251
478
  super.updated(changes);
252
479
  if (changes.has('canvasSize')) {
253
480
  // console.log('Setting canvas size', this.canvasSize);
254
481
  }
255
482
  if (changes.has('definition')) {
256
483
  this.updateCanvasSize();
484
+ // Set flowType from the loaded definition
485
+ if ((_b = this.definition) === null || _b === void 0 ? void 0 : _b.type) {
486
+ this.flowType = this.getFlowTypeFromDefinition(this.definition.type);
487
+ }
488
+ const filters = ((_d = (_c = this.definition) === null || _c === void 0 ? void 0 : _c._ui) === null || _d === void 0 ? void 0 : _d.translation_filters) || {
489
+ categories: false
490
+ };
491
+ const normalizedFilters = {
492
+ categories: !!filters.categories
493
+ };
494
+ if (this.translationFilters.categories !== normalizedFilters.categories) {
495
+ this.translationFilters = normalizedFilters;
496
+ }
497
+ this.translationCache.clear();
257
498
  }
258
499
  if (changes.has('dirtyDate')) {
259
500
  if (this.dirtyDate) {
260
501
  this.debouncedSave();
261
502
  }
262
503
  }
504
+ if (changes.has('languageCode')) {
505
+ this.translationCache.clear();
506
+ }
507
+ }
508
+ /**
509
+ * Map FlowDefinition type to Editor flowType
510
+ * FlowDefinition uses: 'messaging', 'messaging_background', 'messaging_offline', 'voice'
511
+ * Editor uses: 'message', 'voice', 'background'
512
+ */
513
+ getFlowTypeFromDefinition(definitionType) {
514
+ if (definitionType === 'voice') {
515
+ return 'voice';
516
+ }
517
+ else if (definitionType === 'messaging_background' ||
518
+ definitionType === 'messaging_offline') {
519
+ return 'background';
520
+ }
521
+ else {
522
+ // 'messaging' or any other messaging type defaults to 'message'
523
+ return 'message';
524
+ }
263
525
  }
264
526
  debouncedSave() {
265
527
  // Clear any existing timer
@@ -293,6 +555,15 @@ export class Editor extends RapidElement {
293
555
  });
294
556
  getStore().getState().setDirtyDate(null);
295
557
  }
558
+ handleLanguageChange(languageCode) {
559
+ zustand.getState().setLanguageCode(languageCode);
560
+ // Repaint connections after language change since node sizes can change
561
+ if (this.plumber) {
562
+ requestAnimationFrame(() => {
563
+ this.plumber.repaintEverything();
564
+ });
565
+ }
566
+ }
296
567
  disconnectedCallback() {
297
568
  super.disconnectedCallback();
298
569
  if (this.saveTimer !== null) {
@@ -305,7 +576,7 @@ export class Editor extends RapidElement {
305
576
  document.removeEventListener('keydown', this.boundKeyDown);
306
577
  const canvas = this.querySelector('#canvas');
307
578
  if (canvas) {
308
- canvas.removeEventListener('dblclick', this.boundCanvasDoubleClick);
579
+ canvas.removeEventListener('contextmenu', this.boundCanvasContextMenu);
309
580
  }
310
581
  }
311
582
  setupGlobalEventListeners() {
@@ -315,20 +586,40 @@ export class Editor extends RapidElement {
315
586
  document.addEventListener('keydown', this.boundKeyDown);
316
587
  const canvas = this.querySelector('#canvas');
317
588
  if (canvas) {
318
- canvas.addEventListener('dblclick', this.boundCanvasDoubleClick);
589
+ canvas.addEventListener('contextmenu', this.boundCanvasContextMenu);
319
590
  }
320
591
  // Listen for action edit requests from flow nodes
321
592
  this.addEventListener(CustomEventType.ActionEditRequested, this.handleActionEditRequested.bind(this));
593
+ // Listen for add action requests from flow nodes
594
+ this.addEventListener(CustomEventType.AddActionRequested, this.handleAddActionRequested.bind(this));
322
595
  // Listen for node edit requests from flow nodes
323
596
  this.addEventListener(CustomEventType.NodeEditRequested, this.handleNodeEditRequested.bind(this));
597
+ // Listen for canvas menu selections
598
+ this.addEventListener(CustomEventType.Selection, (event) => {
599
+ const target = event.target;
600
+ if (target.tagName === 'TEMBA-CANVAS-MENU') {
601
+ this.handleCanvasMenuSelection(event);
602
+ }
603
+ else if (target.tagName === 'TEMBA-NODE-TYPE-SELECTOR') {
604
+ this.handleNodeTypeSelection(event);
605
+ }
606
+ });
607
+ // Listen for action drag events from nodes
608
+ this.addEventListener(CustomEventType.DragExternal, this.handleActionDragExternal.bind(this));
609
+ this.addEventListener(CustomEventType.DragInternal, this.handleActionDragInternal.bind(this));
610
+ this.addEventListener(CustomEventType.DragStop, (event) => {
611
+ if (event.detail.isExternal) {
612
+ this.handleActionDropExternal(event);
613
+ }
614
+ });
324
615
  }
325
616
  getPosition(uuid, type) {
326
- var _a, _b, _c;
617
+ var _b, _c, _d;
327
618
  if (type === 'node') {
328
- return (_a = this.definition._ui.nodes[uuid]) === null || _a === void 0 ? void 0 : _a.position;
619
+ return (_b = this.definition._ui.nodes[uuid]) === null || _b === void 0 ? void 0 : _b.position;
329
620
  }
330
621
  else {
331
- return (_c = (_b = this.definition._ui.stickies) === null || _b === void 0 ? void 0 : _b[uuid]) === null || _c === void 0 ? void 0 : _c.position;
622
+ return (_d = (_c = this.definition._ui.stickies) === null || _c === void 0 ? void 0 : _c[uuid]) === null || _d === void 0 ? void 0 : _d.position;
332
623
  }
333
624
  }
334
625
  handleMouseDown(event) {
@@ -370,12 +661,12 @@ export class Editor extends RapidElement {
370
661
  event.stopPropagation();
371
662
  }
372
663
  handleGlobalMouseDown(event) {
373
- var _a;
664
+ var _b;
374
665
  // ignore right clicks
375
666
  if (event.button !== 0)
376
667
  return;
377
668
  // Check if the click is within our canvas
378
- const canvasRect = (_a = this.querySelector('#grid')) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
669
+ const canvasRect = (_b = this.querySelector('#grid')) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect();
379
670
  if (!canvasRect)
380
671
  return;
381
672
  const isWithinCanvas = event.clientX >= canvasRect.left &&
@@ -395,14 +686,14 @@ export class Editor extends RapidElement {
395
686
  this.handleCanvasMouseDown(event);
396
687
  }
397
688
  handleCanvasMouseDown(event) {
398
- var _a;
689
+ var _b;
399
690
  const target = event.target;
400
691
  if (target.id === 'canvas' || target.id === 'grid') {
401
692
  // Ignore clicks on exits
402
693
  // Start selection box
403
694
  this.canvasMouseDown = true;
404
695
  this.dragStartPos = { x: event.clientX, y: event.clientY };
405
- const canvasRect = (_a = this.querySelector('#canvas')) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
696
+ const canvasRect = (_b = this.querySelector('#canvas')) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect();
406
697
  if (canvasRect) {
407
698
  // Clear current selection
408
699
  this.selectedItems.clear();
@@ -466,16 +757,16 @@ export class Editor extends RapidElement {
466
757
  deleteSelectedItems() {
467
758
  const nodes = Array.from(this.selectedItems).filter((uuid) => this.definition.nodes.some((node) => node.uuid === uuid));
468
759
  this.deleteNodes(Array.from(nodes));
469
- const stickies = Array.from(this.selectedItems).filter((uuid) => { var _a, _b; return (_b = (_a = this.definition._ui) === null || _a === void 0 ? void 0 : _a.stickies) === null || _b === void 0 ? void 0 : _b[uuid]; });
760
+ const stickies = Array.from(this.selectedItems).filter((uuid) => { var _b, _c; return (_c = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.stickies) === null || _c === void 0 ? void 0 : _c[uuid]; });
470
761
  getStore().getState().removeStickyNotes(stickies);
471
762
  // Clear selection
472
763
  this.selectedItems.clear();
473
764
  }
474
765
  updateSelectionBox(event) {
475
- var _a;
766
+ var _b;
476
767
  if (!this.selectionBox || !this.canvasMouseDown)
477
768
  return;
478
- const canvasRect = (_a = this.querySelector('#canvas')) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
769
+ const canvasRect = (_b = this.querySelector('#canvas')) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect();
479
770
  if (!canvasRect)
480
771
  return;
481
772
  const relativeX = event.clientX - canvasRect.left;
@@ -489,7 +780,7 @@ export class Editor extends RapidElement {
489
780
  this.updateSelectedItemsFromBox();
490
781
  }
491
782
  updateSelectedItemsFromBox() {
492
- var _a, _b, _c;
783
+ var _b, _c, _d;
493
784
  if (!this.selectionBox)
494
785
  return;
495
786
  const newSelection = new Set();
@@ -498,14 +789,14 @@ export class Editor extends RapidElement {
498
789
  const boxRight = Math.max(this.selectionBox.startX, this.selectionBox.endX);
499
790
  const boxBottom = Math.max(this.selectionBox.startY, this.selectionBox.endY);
500
791
  // Check nodes
501
- (_a = this.definition) === null || _a === void 0 ? void 0 : _a.nodes.forEach((node) => {
502
- var _a, _b, _c;
792
+ (_b = this.definition) === null || _b === void 0 ? void 0 : _b.nodes.forEach((node) => {
793
+ var _b, _c, _d;
503
794
  const nodeElement = this.querySelector(`[id="${node.uuid}"]`);
504
795
  if (nodeElement) {
505
- const position = (_b = (_a = this.definition._ui) === null || _a === void 0 ? void 0 : _a.nodes[node.uuid]) === null || _b === void 0 ? void 0 : _b.position;
796
+ const position = (_c = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.nodes[node.uuid]) === null || _c === void 0 ? void 0 : _c.position;
506
797
  if (position) {
507
798
  const rect = nodeElement.getBoundingClientRect();
508
- const canvasRect = (_c = this.querySelector('#canvas')) === null || _c === void 0 ? void 0 : _c.getBoundingClientRect();
799
+ const canvasRect = (_d = this.querySelector('#canvas')) === null || _d === void 0 ? void 0 : _d.getBoundingClientRect();
509
800
  if (canvasRect) {
510
801
  const nodeLeft = position.left;
511
802
  const nodeTop = position.top;
@@ -523,7 +814,7 @@ export class Editor extends RapidElement {
523
814
  }
524
815
  });
525
816
  // Check sticky notes
526
- const stickies = ((_c = (_b = this.definition) === null || _b === void 0 ? void 0 : _b._ui) === null || _c === void 0 ? void 0 : _c.stickies) || {};
817
+ const stickies = ((_d = (_c = this.definition) === null || _c === void 0 ? void 0 : _c._ui) === null || _d === void 0 ? void 0 : _d.stickies) || {};
527
818
  Object.entries(stickies).forEach(([uuid, sticky]) => {
528
819
  if (sticky.position) {
529
820
  const stickyElement = this.querySelector(`temba-sticky-note[uuid="${uuid}"]`);
@@ -561,6 +852,49 @@ export class Editor extends RapidElement {
561
852
  style="left: ${left}px; top: ${top}px; width: ${width}px; height: ${height}px;"
562
853
  ></div>`;
563
854
  }
855
+ renderCanvasDropPreview() {
856
+ var _b;
857
+ if (!this.canvasDropPreview)
858
+ return '';
859
+ const { action, position } = this.canvasDropPreview;
860
+ const actionConfig = ACTION_CONFIG[action.type];
861
+ if (!actionConfig)
862
+ return '';
863
+ return html `<div
864
+ class="canvas-drop-preview"
865
+ style="position: absolute; left: ${position.left}px; top: ${position.top}px; opacity: 0.6; pointer-events: none; z-index: 10000;"
866
+ >
867
+ <div
868
+ class="node execute-actions"
869
+ style="outline: 3px dashed var(--color-primary, #3b82f6); outline-offset: 2px; border-radius: var(--curvature);"
870
+ >
871
+ <div class="action sortable ${action.type}">
872
+ <div class="action-content">
873
+ <div
874
+ class="cn-title"
875
+ style="background: ${actionConfig.group
876
+ ? (_b = ACTION_GROUP_METADATA[actionConfig.group]) === null || _b === void 0 ? void 0 : _b.color
877
+ : '#aaaaaa'}"
878
+ >
879
+ <div class="title-spacer"></div>
880
+ <div class="name">${actionConfig.name}</div>
881
+ <div class="title-spacer"></div>
882
+ </div>
883
+ <div class="body">
884
+ ${actionConfig.render
885
+ ? actionConfig.render({ actions: [action] }, action)
886
+ : html `<pre>${action.type}</pre>`}
887
+ </div>
888
+ </div>
889
+ </div>
890
+ <div class="action-exits">
891
+ <div class="exit-wrapper">
892
+ <div class="exit"></div>
893
+ </div>
894
+ </div>
895
+ </div>
896
+ </div>`;
897
+ }
564
898
  handleMouseMove(event) {
565
899
  // Handle selection box drawing
566
900
  if (this.canvasMouseDown && !this.isMouseDown) {
@@ -693,7 +1027,7 @@ export class Editor extends RapidElement {
693
1027
  this.canvasMouseDown = false;
694
1028
  }
695
1029
  updateCanvasSize() {
696
- var _a;
1030
+ var _b;
697
1031
  if (!this.definition)
698
1032
  return;
699
1033
  const store = getStore();
@@ -715,7 +1049,7 @@ export class Editor extends RapidElement {
715
1049
  }
716
1050
  });
717
1051
  // Check sticky note positions
718
- const stickies = ((_a = this.definition._ui) === null || _a === void 0 ? void 0 : _a.stickies) || {};
1052
+ const stickies = ((_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.stickies) || {};
719
1053
  Object.entries(stickies).forEach(([uuid, sticky]) => {
720
1054
  if (sticky.position) {
721
1055
  const stickyElement = this.querySelector(`temba-sticky-note[uuid="${uuid}"]`);
@@ -738,12 +1072,15 @@ export class Editor extends RapidElement {
738
1072
  // Update canvas size in store
739
1073
  store.getState().expandCanvas(maxWidth, maxHeight);
740
1074
  }
741
- handleCanvasDoubleClick(event) {
742
- // Check if we double-clicked on empty canvas space
1075
+ handleCanvasContextMenu(event) {
1076
+ // Check if we right-clicked on empty canvas space
743
1077
  const target = event.target;
744
1078
  if (target.id !== 'canvas') {
745
1079
  return;
746
1080
  }
1081
+ // Prevent the default browser context menu
1082
+ event.preventDefault();
1083
+ event.stopPropagation();
747
1084
  // Get canvas position
748
1085
  const canvas = this.querySelector('#canvas');
749
1086
  if (!canvas) {
@@ -755,14 +1092,119 @@ export class Editor extends RapidElement {
755
1092
  // Snap position to grid
756
1093
  const snappedLeft = snapToGrid(relativeX);
757
1094
  const snappedTop = snapToGrid(relativeY);
758
- // Create new sticky note
1095
+ // Show the canvas menu at the mouse position (use viewport coordinates)
1096
+ const canvasMenu = this.querySelector('temba-canvas-menu');
1097
+ if (canvasMenu) {
1098
+ canvasMenu.show(event.clientX, event.clientY, {
1099
+ x: snappedLeft,
1100
+ y: snappedTop
1101
+ });
1102
+ }
1103
+ }
1104
+ handleCanvasMenuSelection(event) {
1105
+ const selection = event.detail;
759
1106
  const store = getStore();
760
- store.getState().createStickyNote({
761
- left: snappedLeft,
762
- top: snappedTop
763
- });
764
- event.preventDefault();
765
- event.stopPropagation();
1107
+ if (selection.action === 'sticky') {
1108
+ // Create new sticky note
1109
+ store.getState().createStickyNote({
1110
+ left: selection.position.x,
1111
+ top: selection.position.y
1112
+ });
1113
+ }
1114
+ else {
1115
+ // Show node type selector
1116
+ const selector = this.querySelector('temba-node-type-selector');
1117
+ if (selector) {
1118
+ selector.show(selection.action, selection.position);
1119
+ }
1120
+ }
1121
+ }
1122
+ handleNodeTypeSelection(event) {
1123
+ const selection = event.detail;
1124
+ // Check if we're adding an action to an existing node
1125
+ if (this.addActionToNodeUuid) {
1126
+ // Find the existing node
1127
+ const node = this.definition.nodes.find((n) => n.uuid === this.addActionToNodeUuid);
1128
+ const nodeUI = this.definition._ui.nodes[this.addActionToNodeUuid];
1129
+ if (node && nodeUI) {
1130
+ // Create a new action to add to the existing node
1131
+ const actionUuid = generateUUID();
1132
+ this.editingAction = {
1133
+ uuid: actionUuid,
1134
+ type: selection.nodeType
1135
+ };
1136
+ // Set the editing node to the existing node (not creating new)
1137
+ this.editingNode = node;
1138
+ this.editingNodeUI = nodeUI;
1139
+ this.isCreatingNewNode = false;
1140
+ // Clear the addActionToNodeUuid flag
1141
+ this.addActionToNodeUuid = null;
1142
+ return;
1143
+ }
1144
+ // If we couldn't find the node, clear the flag and continue with normal flow
1145
+ this.addActionToNodeUuid = null;
1146
+ }
1147
+ // Create a temporary node structure for editing (not added to store yet)
1148
+ const nodeUuid = generateUUID();
1149
+ // Determine if this is an action type or a node type
1150
+ // Actions need to be wrapped in an execute_actions node
1151
+ const isActionType = selection.nodeType in ACTION_CONFIG;
1152
+ const nodeType = isActionType ? 'execute_actions' : selection.nodeType;
1153
+ // For nodes with routers, initialize an empty router to ensure fromFormData works correctly
1154
+ const nodeConfig = NODE_CONFIG[nodeType];
1155
+ const hasRouter = (nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.form) &&
1156
+ Object.keys(nodeConfig.form).some((key) => {
1157
+ var _b;
1158
+ return ['rules', 'categories', 'cases'].includes(key) ||
1159
+ ((_b = nodeConfig.form[key]) === null || _b === void 0 ? void 0 : _b.type) === 'array';
1160
+ });
1161
+ const tempNode = {
1162
+ uuid: nodeUuid,
1163
+ actions: [],
1164
+ exits: hasRouter
1165
+ ? [] // Router-based nodes will generate their own exits
1166
+ : [
1167
+ {
1168
+ uuid: generateUUID(),
1169
+ destination_uuid: null
1170
+ }
1171
+ ]
1172
+ };
1173
+ if (hasRouter) {
1174
+ // This node uses a router - initialize it with empty structure
1175
+ tempNode.router = {
1176
+ type: 'switch',
1177
+ categories: [],
1178
+ cases: [],
1179
+ operand: '@input.text',
1180
+ default_category_uuid: undefined
1181
+ };
1182
+ }
1183
+ const tempNodeUI = {
1184
+ position: {
1185
+ left: selection.position.x,
1186
+ top: selection.position.y
1187
+ },
1188
+ type: nodeType,
1189
+ config: {}
1190
+ };
1191
+ // Mark that we're creating a new node and store the position
1192
+ this.isCreatingNewNode = true;
1193
+ this.pendingNodePosition = {
1194
+ left: selection.position.x,
1195
+ top: selection.position.y
1196
+ };
1197
+ // Open the node editor with the temporary node
1198
+ this.editingNode = tempNode;
1199
+ this.editingNodeUI = tempNodeUI;
1200
+ // If this is an action type, we also need to set up an editing action
1201
+ if (isActionType) {
1202
+ const actionUuid = generateUUID();
1203
+ this.editingAction = {
1204
+ uuid: actionUuid,
1205
+ type: selection.nodeType
1206
+ };
1207
+ }
766
1208
  }
767
1209
  handleActionEditRequested(event) {
768
1210
  // For action editing, we set the action and find the corresponding node
@@ -775,24 +1217,80 @@ export class Editor extends RapidElement {
775
1217
  this.editingNodeUI = this.definition._ui.nodes[nodeUuid];
776
1218
  }
777
1219
  }
1220
+ handleAddActionRequested(event) {
1221
+ // Get the node where we want to add the action
1222
+ const nodeUuid = event.detail.nodeUuid;
1223
+ const node = this.definition.nodes.find((n) => n.uuid === nodeUuid);
1224
+ if (!node) {
1225
+ return;
1226
+ }
1227
+ // Get the node's position to place the selector near it
1228
+ const nodeUI = this.definition._ui.nodes[nodeUuid];
1229
+ if (!nodeUI) {
1230
+ return;
1231
+ }
1232
+ // Show the node type selector in action mode, excluding branching actions
1233
+ const selector = this.querySelector('temba-node-type-selector');
1234
+ if (selector) {
1235
+ // Show the selector near the node, using a mode that excludes branching actions
1236
+ selector.show('action-no-branching', {
1237
+ x: nodeUI.position.left,
1238
+ y: nodeUI.position.top
1239
+ });
1240
+ // Store the node UUID so we know which node to add the action to
1241
+ this.addActionToNodeUuid = nodeUuid;
1242
+ }
1243
+ }
778
1244
  handleNodeEditRequested(event) {
779
1245
  this.editingNode = event.detail.node;
780
1246
  this.editingNodeUI = event.detail.nodeUI;
781
1247
  }
782
1248
  handleActionSaved(updatedAction) {
783
- var _a;
1249
+ var _b, _c;
784
1250
  if (this.editingNode && this.editingAction) {
785
- // Update the specific action in the node
786
- const updatedActions = this.editingNode.actions.map((action) => action.uuid === this.editingAction.uuid ? updatedAction : action);
1251
+ let updatedActions;
1252
+ // Check if this action already exists in the node
1253
+ const existingActionIndex = this.editingNode.actions.findIndex((action) => action.uuid === this.editingAction.uuid);
1254
+ if (existingActionIndex >= 0) {
1255
+ // Update existing action
1256
+ updatedActions = this.editingNode.actions.map((action) => action.uuid === this.editingAction.uuid ? updatedAction : action);
1257
+ }
1258
+ else {
1259
+ // Add new action
1260
+ updatedActions = [...this.editingNode.actions, updatedAction];
1261
+ }
787
1262
  const updatedNode = { ...this.editingNode, actions: updatedActions };
788
- // Update the node in the store
789
- (_a = getStore()) === null || _a === void 0 ? void 0 : _a.getState().updateNode(this.editingNode.uuid, updatedNode);
790
- // Repaint jsplumb connections in case node size changed
791
- if (this.plumber) {
792
- // Use requestAnimationFrame to ensure DOM has been updated first
793
- requestAnimationFrame(() => {
794
- this.plumber.repaintEverything();
795
- });
1263
+ // Check if we're creating a new node or updating an existing one
1264
+ if (this.isCreatingNewNode) {
1265
+ // This is a new node with a new action - add it to the store
1266
+ const store = getStore();
1267
+ const nodeUI = {
1268
+ position: this.pendingNodePosition || { left: 0, top: 0 },
1269
+ type: (_b = this.editingNodeUI) === null || _b === void 0 ? void 0 : _b.type,
1270
+ config: {}
1271
+ };
1272
+ // Add the node to the store
1273
+ store.getState().addNode(updatedNode, nodeUI);
1274
+ // Reset the creation flags
1275
+ this.isCreatingNewNode = false;
1276
+ this.pendingNodePosition = null;
1277
+ // Repaint jsplumb connections
1278
+ if (this.plumber) {
1279
+ requestAnimationFrame(() => {
1280
+ this.plumber.repaintEverything();
1281
+ });
1282
+ }
1283
+ }
1284
+ else {
1285
+ // Update existing node in the store
1286
+ (_c = getStore()) === null || _c === void 0 ? void 0 : _c.getState().updateNode(this.editingNode.uuid, updatedNode);
1287
+ // Repaint jsplumb connections in case node size changed
1288
+ if (this.plumber) {
1289
+ // Use requestAnimationFrame to ensure DOM has been updated first
1290
+ requestAnimationFrame(() => {
1291
+ this.plumber.repaintEverything();
1292
+ });
1293
+ }
796
1294
  }
797
1295
  }
798
1296
  this.closeNodeEditor();
@@ -803,25 +1301,51 @@ export class Editor extends RapidElement {
803
1301
  this.editingAction = null;
804
1302
  }
805
1303
  handleActionEditCanceled() {
1304
+ // If we were creating a new node, just discard it
1305
+ if (this.isCreatingNewNode) {
1306
+ this.isCreatingNewNode = false;
1307
+ this.pendingNodePosition = null;
1308
+ }
806
1309
  this.closeNodeEditor();
807
1310
  }
808
- handleNodeSaved(updatedNode) {
809
- var _a;
1311
+ handleNodeSaved(updatedNode, uiConfig) {
1312
+ var _b, _c, _d;
810
1313
  if (this.editingNode) {
811
- // Clean up jsPlumb connections for removed exits before updating the node
812
- if (this.plumber) {
813
- const oldExits = this.editingNode.exits || [];
814
- const newExits = updatedNode.exits || [];
815
- // Find exits that were removed
816
- const removedExits = oldExits.filter((oldExit) => !newExits.find((newExit) => newExit.uuid === oldExit.uuid));
817
- // Remove jsPlumb connections for removed exits
818
- removedExits.forEach((exit) => {
819
- this.plumber.removeExitConnection(exit.uuid);
820
- });
1314
+ if (this.isCreatingNewNode) {
1315
+ // This is a new node - add it to the store for the first time
1316
+ const store = getStore();
1317
+ const nodeUI = {
1318
+ position: this.pendingNodePosition || { left: 0, top: 0 },
1319
+ type: (_b = this.editingNodeUI) === null || _b === void 0 ? void 0 : _b.type,
1320
+ config: uiConfig || {}
1321
+ };
1322
+ // Add the node to the store
1323
+ store.getState().addNode(updatedNode, nodeUI);
1324
+ // Reset the creation flags
1325
+ this.isCreatingNewNode = false;
1326
+ this.pendingNodePosition = null;
1327
+ }
1328
+ else {
1329
+ // This is an existing node - update it
1330
+ // Clean up jsPlumb connections for removed exits before updating the node
1331
+ if (this.plumber) {
1332
+ const oldExits = this.editingNode.exits || [];
1333
+ const newExits = updatedNode.exits || [];
1334
+ // Find exits that were removed
1335
+ const removedExits = oldExits.filter((oldExit) => !newExits.find((newExit) => newExit.uuid === oldExit.uuid));
1336
+ // Remove jsPlumb connections for removed exits
1337
+ removedExits.forEach((exit) => {
1338
+ this.plumber.removeExitConnection(exit.uuid);
1339
+ });
1340
+ }
1341
+ this.plumber.revalidate([updatedNode.uuid]);
1342
+ // Update the node in the store
1343
+ (_c = getStore()) === null || _c === void 0 ? void 0 : _c.getState().updateNode(this.editingNode.uuid, updatedNode);
1344
+ // Update the UI config if provided
1345
+ if (uiConfig) {
1346
+ (_d = getStore()) === null || _d === void 0 ? void 0 : _d.getState().updateNodeUIConfig(updatedNode.uuid, uiConfig);
1347
+ }
821
1348
  }
822
- this.plumber.revalidate([updatedNode.uuid]);
823
- // Update the node in the store
824
- (_a = getStore()) === null || _a === void 0 ? void 0 : _a.getState().updateNode(this.editingNode.uuid, updatedNode);
825
1349
  // Repaint jsplumb connections in case node size changed
826
1350
  if (this.plumber) {
827
1351
  // Use requestAnimationFrame to ensure DOM has been updated first
@@ -833,17 +1357,787 @@ export class Editor extends RapidElement {
833
1357
  this.closeNodeEditor();
834
1358
  }
835
1359
  handleNodeEditCanceled() {
1360
+ // If we were creating a new node, just discard it
1361
+ if (this.isCreatingNewNode) {
1362
+ this.isCreatingNewNode = false;
1363
+ this.pendingNodePosition = null;
1364
+ }
836
1365
  this.closeNodeEditor();
837
1366
  }
1367
+ getNodeAtPosition(mouseX, mouseY) {
1368
+ // Get all node elements
1369
+ const nodeElements = this.querySelectorAll('temba-flow-node');
1370
+ for (const nodeElement of Array.from(nodeElements)) {
1371
+ const rect = nodeElement.getBoundingClientRect();
1372
+ if (mouseX >= rect.left &&
1373
+ mouseX <= rect.right &&
1374
+ mouseY >= rect.top &&
1375
+ mouseY <= rect.bottom) {
1376
+ return nodeElement.getAttribute('data-node-uuid');
1377
+ }
1378
+ }
1379
+ return null;
1380
+ }
1381
+ calculateCanvasDropPosition(mouseX, mouseY, applyGridSnapping = true) {
1382
+ // calculate the position on the canvas
1383
+ const canvas = this.querySelector('#canvas');
1384
+ if (!canvas)
1385
+ return { left: 0, top: 0 };
1386
+ const canvasRect = canvas.getBoundingClientRect();
1387
+ // calculate position relative to canvas
1388
+ // canvasRect gives us the canvas position in the viewport, which already accounts for scroll
1389
+ // so we just need mouseX/Y - canvasRect.left/top to get position within canvas
1390
+ const left = mouseX - canvasRect.left - DROP_PREVIEW_OFFSET_X;
1391
+ const top = mouseY - canvasRect.top - DROP_PREVIEW_OFFSET_Y;
1392
+ // Apply grid snapping only if requested (for final drop position)
1393
+ if (applyGridSnapping) {
1394
+ return {
1395
+ left: snapToGrid(left),
1396
+ top: snapToGrid(top)
1397
+ };
1398
+ }
1399
+ return { left, top };
1400
+ }
1401
+ handleActionDragExternal(event) {
1402
+ const { action, nodeUuid, actionIndex, mouseX, mouseY, actionHeight = 60 } = event.detail;
1403
+ // Check if mouse is over another execute_actions node
1404
+ const targetNode = this.getNodeAtPosition(mouseX, mouseY);
1405
+ if (targetNode && targetNode !== nodeUuid) {
1406
+ const targetNodeUI = this.definition._ui.nodes[targetNode];
1407
+ const targetNodeDef = this.definition.nodes.find((n) => n.uuid === targetNode);
1408
+ // Only allow dropping on execute_actions nodes, and not the source node
1409
+ if ((targetNodeUI === null || targetNodeUI === void 0 ? void 0 : targetNodeUI.type) === 'execute_actions' && targetNodeDef) {
1410
+ // If we moved to a different target node, clear the previous one's placeholder
1411
+ if (this.previousActionDragTargetNodeUuid &&
1412
+ this.previousActionDragTargetNodeUuid !== targetNode) {
1413
+ const previousElement = this.querySelector(`temba-flow-node[data-node-uuid="${this.previousActionDragTargetNodeUuid}"]`);
1414
+ if (previousElement) {
1415
+ previousElement.dispatchEvent(new CustomEvent('action-drag-leave', {
1416
+ detail: {},
1417
+ bubbles: false
1418
+ }));
1419
+ }
1420
+ }
1421
+ // Update target node for drop handling
1422
+ this.actionDragTargetNodeUuid = targetNode;
1423
+ this.previousActionDragTargetNodeUuid = targetNode;
1424
+ // Hide canvas preview when over a valid target
1425
+ this.canvasDropPreview = null;
1426
+ // Tell source node to show ghost (we're over a valid target)
1427
+ const sourceElement = this.querySelector(`temba-flow-node[data-node-uuid="${nodeUuid}"]`);
1428
+ if (sourceElement) {
1429
+ sourceElement.dispatchEvent(new CustomEvent('action-show-ghost', {
1430
+ detail: {},
1431
+ bubbles: false
1432
+ }));
1433
+ }
1434
+ // Notify the target node about the drag
1435
+ const targetElement = this.querySelector(`temba-flow-node[data-node-uuid="${targetNode}"]`);
1436
+ if (targetElement) {
1437
+ targetElement.dispatchEvent(new CustomEvent('action-drag-over', {
1438
+ detail: {
1439
+ action,
1440
+ sourceNodeUuid: nodeUuid,
1441
+ actionIndex,
1442
+ mouseX,
1443
+ mouseY,
1444
+ actionHeight
1445
+ },
1446
+ bubbles: false
1447
+ }));
1448
+ }
1449
+ this.requestUpdate();
1450
+ return;
1451
+ }
1452
+ }
1453
+ // Not over a valid target node, clear any previous target's placeholder
1454
+ if (this.previousActionDragTargetNodeUuid) {
1455
+ const previousElement = this.querySelector(`temba-flow-node[data-node-uuid="${this.previousActionDragTargetNodeUuid}"]`);
1456
+ if (previousElement) {
1457
+ previousElement.dispatchEvent(new CustomEvent('action-drag-leave', {
1458
+ detail: {},
1459
+ bubbles: false
1460
+ }));
1461
+ }
1462
+ this.previousActionDragTargetNodeUuid = null;
1463
+ }
1464
+ this.actionDragTargetNodeUuid = null;
1465
+ // Tell source node to hide ghost (we're not over a valid target)
1466
+ const sourceElement = this.querySelector(`temba-flow-node[data-node-uuid="${nodeUuid}"]`);
1467
+ if (sourceElement) {
1468
+ sourceElement.dispatchEvent(new CustomEvent('action-hide-ghost', {
1469
+ detail: {},
1470
+ bubbles: false
1471
+ }));
1472
+ }
1473
+ // Don't snap to grid for preview - let it follow cursor smoothly
1474
+ const position = this.calculateCanvasDropPosition(mouseX, mouseY, false);
1475
+ this.canvasDropPreview = {
1476
+ action,
1477
+ nodeUuid,
1478
+ actionIndex,
1479
+ position,
1480
+ actionHeight
1481
+ };
1482
+ // Force re-render to update preview position
1483
+ this.requestUpdate();
1484
+ }
1485
+ handleActionDragInternal(_event) {
1486
+ // Clear any previous target's placeholder when returning to internal drag
1487
+ if (this.previousActionDragTargetNodeUuid) {
1488
+ const previousElement = this.querySelector(`temba-flow-node[data-node-uuid="${this.previousActionDragTargetNodeUuid}"]`);
1489
+ if (previousElement) {
1490
+ previousElement.dispatchEvent(new CustomEvent('action-drag-leave', {
1491
+ detail: {},
1492
+ bubbles: false
1493
+ }));
1494
+ }
1495
+ this.previousActionDragTargetNodeUuid = null;
1496
+ }
1497
+ this.canvasDropPreview = null;
1498
+ this.actionDragTargetNodeUuid = null;
1499
+ }
1500
+ handleActionDropExternal(event) {
1501
+ var _b, _c, _d;
1502
+ const { action, nodeUuid, actionIndex, mouseX, mouseY } = event.detail;
1503
+ // Check if we're dropping on an existing execute_actions node
1504
+ const targetNodeUuid = this.actionDragTargetNodeUuid;
1505
+ if (targetNodeUuid && targetNodeUuid !== nodeUuid) {
1506
+ // Dropping on another node - notify the target node to handle the drop
1507
+ const targetElement = this.querySelector(`temba-flow-node[data-node-uuid="${targetNodeUuid}"]`);
1508
+ if (targetElement) {
1509
+ targetElement.dispatchEvent(new CustomEvent('action-drop', {
1510
+ detail: {
1511
+ action,
1512
+ sourceNodeUuid: nodeUuid,
1513
+ actionIndex,
1514
+ mouseX,
1515
+ mouseY
1516
+ },
1517
+ bubbles: false
1518
+ }));
1519
+ }
1520
+ // Clear state
1521
+ this.canvasDropPreview = null;
1522
+ this.actionDragTargetNodeUuid = null;
1523
+ return;
1524
+ }
1525
+ // Not dropping on another node, create a new one on canvas
1526
+ // Snap to grid for the final drop position
1527
+ const position = this.calculateCanvasDropPosition(mouseX, mouseY, true);
1528
+ // remove the action from the original node
1529
+ const originalNode = this.definition.nodes.find((n) => n.uuid === nodeUuid);
1530
+ if (!originalNode)
1531
+ return;
1532
+ const updatedActions = originalNode.actions.filter((_a, idx) => idx !== actionIndex);
1533
+ // if no actions remain, delete the node
1534
+ if (updatedActions.length === 0) {
1535
+ (_b = getStore()) === null || _b === void 0 ? void 0 : _b.getState().removeNodes([nodeUuid]);
1536
+ }
1537
+ else {
1538
+ // update the node
1539
+ const updatedNode = { ...originalNode, actions: updatedActions };
1540
+ (_c = getStore()) === null || _c === void 0 ? void 0 : _c.getState().updateNode(nodeUuid, updatedNode);
1541
+ }
1542
+ // create a new execute_actions node with the dropped action
1543
+ const newNode = {
1544
+ uuid: generateUUID(),
1545
+ actions: [action],
1546
+ exits: [
1547
+ {
1548
+ uuid: generateUUID(),
1549
+ destination_uuid: null
1550
+ }
1551
+ ]
1552
+ };
1553
+ const newNodeUI = {
1554
+ position,
1555
+ type: 'execute_actions',
1556
+ config: {}
1557
+ };
1558
+ // add the new node
1559
+ (_d = getStore()) === null || _d === void 0 ? void 0 : _d.getState().addNode(newNode, newNodeUI);
1560
+ // clear the preview
1561
+ this.canvasDropPreview = null;
1562
+ this.actionDragTargetNodeUuid = null;
1563
+ // repaint connections
1564
+ if (this.plumber) {
1565
+ requestAnimationFrame(() => {
1566
+ this.plumber.repaintEverything();
1567
+ });
1568
+ }
1569
+ }
1570
+ getLocalizationLanguages() {
1571
+ if (!this.definition) {
1572
+ return [];
1573
+ }
1574
+ const baseLanguage = this.definition.language;
1575
+ return this.getAvailableLanguages().filter((lang) => lang.code !== baseLanguage);
1576
+ }
1577
+ getLocalizationProgress(languageCode) {
1578
+ if (!this.definition ||
1579
+ !languageCode ||
1580
+ languageCode === this.definition.language) {
1581
+ return { total: 0, localized: 0 };
1582
+ }
1583
+ const bundles = this.buildTranslationBundles(this.translationFilters.categories, languageCode);
1584
+ return this.getTranslationCounts(bundles);
1585
+ }
1586
+ getLanguageLocalization(languageCode) {
1587
+ var _b;
1588
+ if (!((_b = this.definition) === null || _b === void 0 ? void 0 : _b.localization)) {
1589
+ return {};
1590
+ }
1591
+ return this.definition.localization[languageCode] || {};
1592
+ }
1593
+ buildTranslationBundles(includeCategories, languageCode = this.languageCode) {
1594
+ if (!this.definition ||
1595
+ !languageCode ||
1596
+ languageCode === this.definition.language) {
1597
+ return [];
1598
+ }
1599
+ const languageLocalization = this.getLanguageLocalization(languageCode);
1600
+ const bundles = [];
1601
+ this.definition.nodes.forEach((node) => {
1602
+ var _b, _c, _d, _e;
1603
+ node.actions.forEach((action) => {
1604
+ const config = ACTION_CONFIG[action.type];
1605
+ if (!(config === null || config === void 0 ? void 0 : config.localizable) || config.localizable.length === 0) {
1606
+ return;
1607
+ }
1608
+ // For send_msg actions, only count 'text' for progress tracking
1609
+ // (quick_replies and attachments are still localizable but don't count toward progress)
1610
+ const localizableKeys = action.type === 'send_msg'
1611
+ ? config.localizable.filter((key) => key === 'text')
1612
+ : config.localizable;
1613
+ const translations = this.findTranslations('property', action.uuid, localizableKeys, action, languageLocalization);
1614
+ if (translations.length > 0) {
1615
+ bundles.push({
1616
+ nodeUuid: node.uuid,
1617
+ actionUuid: action.uuid,
1618
+ translations
1619
+ });
1620
+ }
1621
+ });
1622
+ if (!includeCategories) {
1623
+ return;
1624
+ }
1625
+ const nodeUI = (_c = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.nodes) === null || _c === void 0 ? void 0 : _c[node.uuid];
1626
+ const nodeType = nodeUI === null || nodeUI === void 0 ? void 0 : nodeUI.type;
1627
+ if (!nodeType) {
1628
+ return;
1629
+ }
1630
+ const nodeConfig = NODE_CONFIG[nodeType];
1631
+ if ((nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.localizable) === 'categories' &&
1632
+ ((_e = (_d = node.router) === null || _d === void 0 ? void 0 : _d.categories) === null || _e === void 0 ? void 0 : _e.length)) {
1633
+ const categoryTranslations = node.router.categories.flatMap((category) => this.findTranslations('category', category.uuid, ['name'], category, languageLocalization));
1634
+ if (categoryTranslations.length > 0) {
1635
+ bundles.push({
1636
+ nodeUuid: node.uuid,
1637
+ translations: categoryTranslations
1638
+ });
1639
+ }
1640
+ }
1641
+ });
1642
+ return bundles;
1643
+ }
1644
+ findTranslations(type, uuid, localizeableKeys, source, localization) {
1645
+ const translations = [];
1646
+ localizeableKeys.forEach((attribute) => {
1647
+ if (attribute === 'quick_replies') {
1648
+ return;
1649
+ }
1650
+ const pathSegments = attribute.split('.');
1651
+ let from = source;
1652
+ let to = [];
1653
+ while (pathSegments.length > 0 && from) {
1654
+ if (from.uuid) {
1655
+ to = localization[from.uuid];
1656
+ }
1657
+ const path = pathSegments.shift();
1658
+ if (!path) {
1659
+ break;
1660
+ }
1661
+ if (to) {
1662
+ to = to[path];
1663
+ }
1664
+ from = from[path];
1665
+ }
1666
+ if (!from) {
1667
+ return;
1668
+ }
1669
+ const fromValue = this.formatTranslationValue(from);
1670
+ if (!fromValue) {
1671
+ return;
1672
+ }
1673
+ const toValue = to ? this.formatTranslationValue(to) : null;
1674
+ translations.push({
1675
+ uuid,
1676
+ type,
1677
+ attribute,
1678
+ from: fromValue,
1679
+ to: toValue
1680
+ });
1681
+ });
1682
+ return translations;
1683
+ }
1684
+ formatTranslationValue(value) {
1685
+ if (value === null || value === undefined) {
1686
+ return null;
1687
+ }
1688
+ if (Array.isArray(value)) {
1689
+ const normalized = value
1690
+ .map((entry) => this.formatTranslationValue(entry))
1691
+ .filter((entry) => !!entry);
1692
+ return normalized.length > 0 ? normalized.join(', ') : null;
1693
+ }
1694
+ if (typeof value === 'object') {
1695
+ if ('name' in value && value.name) {
1696
+ return String(value.name);
1697
+ }
1698
+ if ('arguments' in value && Array.isArray(value.arguments)) {
1699
+ return value.arguments.join(' ');
1700
+ }
1701
+ return null;
1702
+ }
1703
+ if (typeof value === 'number') {
1704
+ return value.toString();
1705
+ }
1706
+ if (typeof value === 'string') {
1707
+ const trimmed = value.trim();
1708
+ return trimmed.length > 0 ? trimmed : null;
1709
+ }
1710
+ return null;
1711
+ }
1712
+ getTranslationCounts(bundles) {
1713
+ return bundles.reduce((counts, bundle) => {
1714
+ bundle.translations.forEach((translation) => {
1715
+ counts.total += 1;
1716
+ if (translation.to && translation.to.trim().length > 0) {
1717
+ counts.localized += 1;
1718
+ }
1719
+ });
1720
+ return counts;
1721
+ }, { total: 0, localized: 0 });
1722
+ }
1723
+ handleLocalizationTabClick() {
1724
+ const languages = this.getLocalizationLanguages();
1725
+ if (!languages.length) {
1726
+ return;
1727
+ }
1728
+ this.localizationWindowHidden = false;
1729
+ const alreadySelected = languages.some((lang) => lang.code === this.languageCode);
1730
+ if (!alreadySelected) {
1731
+ this.handleLanguageChange(languages[0].code);
1732
+ }
1733
+ }
1734
+ handleLocalizationLanguageSelect(languageCode) {
1735
+ if (languageCode === this.languageCode) {
1736
+ return;
1737
+ }
1738
+ this.handleLanguageChange(languageCode);
1739
+ }
1740
+ handleLocalizationLanguageSelectChange(event) {
1741
+ var _b, _c;
1742
+ const select = event.target;
1743
+ const nextValue = (_c = (_b = select === null || select === void 0 ? void 0 : select.values) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.value;
1744
+ if (nextValue) {
1745
+ this.handleLocalizationLanguageSelect(nextValue);
1746
+ }
1747
+ }
1748
+ handleLocalizationWindowClosed() {
1749
+ var _b;
1750
+ this.localizationWindowHidden = true;
1751
+ const baseLanguage = (_b = this.definition) === null || _b === void 0 ? void 0 : _b.language;
1752
+ if (baseLanguage && this.languageCode !== baseLanguage) {
1753
+ this.handleLanguageChange(baseLanguage);
1754
+ }
1755
+ }
1756
+ toggleTranslationSettings() {
1757
+ this.translationSettingsExpanded = !this.translationSettingsExpanded;
1758
+ }
1759
+ handleLocalizationProgressToggleClick(event) {
1760
+ const target = event.target;
1761
+ if (target.closest('.translation-settings-toggle')) {
1762
+ return;
1763
+ }
1764
+ this.toggleTranslationSettings();
1765
+ }
1766
+ handleLocalizationProgressToggleKeydown(event) {
1767
+ if (event.key === 'Enter' || event.key === ' ') {
1768
+ event.preventDefault();
1769
+ this.toggleTranslationSettings();
1770
+ }
1771
+ }
1772
+ handleIncludeCategoriesChange(event) {
1773
+ var _b, _c;
1774
+ const checkbox = event.target;
1775
+ const categories = (_b = checkbox === null || checkbox === void 0 ? void 0 : checkbox.checked) !== null && _b !== void 0 ? _b : false;
1776
+ this.translationFilters = { categories };
1777
+ (_c = getStore()) === null || _c === void 0 ? void 0 : _c.getState().setTranslationFilters({ categories });
1778
+ this.requestUpdate();
1779
+ }
1780
+ async handleAutoTranslateClick(event) {
1781
+ event.preventDefault();
1782
+ event.stopPropagation();
1783
+ if (this.autoTranslating) {
1784
+ this.autoTranslating = false;
1785
+ return;
1786
+ }
1787
+ this.autoTranslateDialogOpen = true;
1788
+ }
1789
+ handleAutoTranslateDialogButton(event) {
1790
+ var _b;
1791
+ const button = (_b = event.detail) === null || _b === void 0 ? void 0 : _b.button;
1792
+ if (!button) {
1793
+ return;
1794
+ }
1795
+ if (button.name === 'Translate') {
1796
+ if (!this.autoTranslateModel) {
1797
+ return;
1798
+ }
1799
+ this.autoTranslateDialogOpen = false;
1800
+ this.autoTranslateError = null;
1801
+ this.autoTranslating = true;
1802
+ this.runAutoTranslation().catch((error) => {
1803
+ console.error('Auto translation failed', error);
1804
+ this.autoTranslateError = 'Auto translation failed. Please try again.';
1805
+ this.autoTranslating = false;
1806
+ });
1807
+ }
1808
+ else if (button.name === 'Cancel' || button.name === 'Close') {
1809
+ this.autoTranslateDialogOpen = false;
1810
+ }
1811
+ }
1812
+ handleAutoTranslateModelChange(event) {
1813
+ var _b;
1814
+ const select = event.target;
1815
+ const nextModel = ((_b = select === null || select === void 0 ? void 0 : select.values) === null || _b === void 0 ? void 0 : _b[0]) || null;
1816
+ this.autoTranslateModel = nextModel;
1817
+ }
1818
+ shouldTranslateValue(text) {
1819
+ if (!text) {
1820
+ return false;
1821
+ }
1822
+ const trimmed = text.trim();
1823
+ if (trimmed.length <= 1) {
1824
+ return false;
1825
+ }
1826
+ if (/^\d+$/.test(trimmed)) {
1827
+ return false;
1828
+ }
1829
+ return true;
1830
+ }
1831
+ async requestAutoTranslation(text) {
1832
+ var _b, _c;
1833
+ if (!this.autoTranslateModel || !this.definition) {
1834
+ return null;
1835
+ }
1836
+ const payload = {
1837
+ text,
1838
+ lang: {
1839
+ from: this.definition.language,
1840
+ to: this.languageCode
1841
+ }
1842
+ };
1843
+ const response = await postJSON(`/llm/translate/${this.autoTranslateModel.uuid}/`, payload);
1844
+ if ((response === null || response === void 0 ? void 0 : response.status) === 200) {
1845
+ const result = ((_b = response.json) === null || _b === void 0 ? void 0 : _b.result) || ((_c = response.json) === null || _c === void 0 ? void 0 : _c.text);
1846
+ return result ? String(result) : null;
1847
+ }
1848
+ throw new Error('Auto translation request failed');
1849
+ }
1850
+ applyLocalizationUpdates(updates, autoTranslated = false) {
1851
+ if (!updates.length || !this.definition) {
1852
+ return;
1853
+ }
1854
+ const store = getStore();
1855
+ if (!store) {
1856
+ return;
1857
+ }
1858
+ updates.forEach(({ uuid, translations }) => {
1859
+ var _b, _c;
1860
+ const normalized = Object.entries(translations).reduce((acc, [key, value]) => {
1861
+ if (!value) {
1862
+ return acc;
1863
+ }
1864
+ acc[key] = Array.isArray(value) ? value : [value];
1865
+ return acc;
1866
+ }, {});
1867
+ const existing = ((_c = (_b = this.definition.localization) === null || _b === void 0 ? void 0 : _b[this.languageCode]) === null || _c === void 0 ? void 0 : _c[uuid]) || {};
1868
+ const merged = { ...existing, ...normalized };
1869
+ store.getState().updateLocalization(this.languageCode, uuid, merged);
1870
+ if (autoTranslated) {
1871
+ zustand
1872
+ .getState()
1873
+ .markAutoTranslated(this.languageCode, uuid, Object.keys(translations));
1874
+ }
1875
+ });
1876
+ }
1877
+ async runAutoTranslation() {
1878
+ if (!this.definition ||
1879
+ this.languageCode === this.definition.language ||
1880
+ !this.autoTranslateModel) {
1881
+ this.autoTranslating = false;
1882
+ return;
1883
+ }
1884
+ const bundles = this.buildTranslationBundles(this.translationFilters.categories);
1885
+ for (const bundle of bundles) {
1886
+ if (!this.autoTranslating) {
1887
+ break;
1888
+ }
1889
+ const untranslated = bundle.translations.filter((translation) => !translation.to || translation.to.trim().length === 0);
1890
+ if (untranslated.length === 0) {
1891
+ continue;
1892
+ }
1893
+ const updates = [];
1894
+ for (const translation of untranslated) {
1895
+ if (!this.autoTranslating) {
1896
+ break;
1897
+ }
1898
+ if (!this.shouldTranslateValue(translation.from)) {
1899
+ continue;
1900
+ }
1901
+ const cached = this.translationCache.get(translation.from);
1902
+ if (cached) {
1903
+ updates.push({
1904
+ uuid: translation.uuid,
1905
+ translations: { [translation.attribute]: cached }
1906
+ });
1907
+ continue;
1908
+ }
1909
+ try {
1910
+ const result = await this.requestAutoTranslation(translation.from);
1911
+ if (result) {
1912
+ this.translationCache.set(translation.from, result);
1913
+ updates.push({
1914
+ uuid: translation.uuid,
1915
+ translations: { [translation.attribute]: result }
1916
+ });
1917
+ }
1918
+ }
1919
+ catch (error) {
1920
+ console.error('Auto translation request failed', error);
1921
+ this.autoTranslateError =
1922
+ 'Auto translation failed. Please try again.';
1923
+ this.autoTranslating = false;
1924
+ break;
1925
+ }
1926
+ }
1927
+ if (updates.length > 0) {
1928
+ this.applyLocalizationUpdates(updates, true);
1929
+ }
1930
+ if (!this.autoTranslating) {
1931
+ break;
1932
+ }
1933
+ }
1934
+ this.autoTranslating = false;
1935
+ }
1936
+ renderLocalizationWindow() {
1937
+ var _b, _c, _d;
1938
+ const languages = this.getLocalizationLanguages();
1939
+ if (!languages.length) {
1940
+ return html ``;
1941
+ }
1942
+ const baseLanguage = (_b = this.definition) === null || _b === void 0 ? void 0 : _b.language;
1943
+ const availableLanguages = this.getAvailableLanguages();
1944
+ const baseName = ((_c = availableLanguages.find((lang) => lang.code === baseLanguage)) === null || _c === void 0 ? void 0 : _c.name) ||
1945
+ 'Base Language';
1946
+ const activeLanguageCode = languages.some((lang) => lang.code === this.languageCode)
1947
+ ? this.languageCode
1948
+ : (_d = languages[0]) === null || _d === void 0 ? void 0 : _d.code;
1949
+ const activeLanguage = activeLanguageCode
1950
+ ? languages.find((lang) => lang.code === activeLanguageCode)
1951
+ : null;
1952
+ const progress = this.getLocalizationProgress(activeLanguageCode || '');
1953
+ const includeCategories = this.translationFilters.categories;
1954
+ const settingsPanelId = 'translation-settings-panel';
1955
+ const remainingTranslations = Math.max(progress.total - progress.localized, 0);
1956
+ const hasTranslations = progress.total > 0;
1957
+ const hasPendingTranslations = remainingTranslations > 0;
1958
+ const autoTranslateButtonLabel = this.autoTranslating
1959
+ ? 'Stop Auto Translate'
1960
+ : 'Auto Translate';
1961
+ const autoTranslateButtonDisabled = !this.autoTranslating && !hasTranslations;
1962
+ return html `
1963
+ <temba-floating-window
1964
+ id="localization-window"
1965
+ header="Translations"
1966
+ .width=${360}
1967
+ .maxHeight=${600}
1968
+ .top=${20}
1969
+ color="#6b7280"
1970
+ .hidden=${this.localizationWindowHidden}
1971
+ @temba-dialog-hidden=${this.handleLocalizationWindowClosed}
1972
+ >
1973
+ <div class="localization-window-content">
1974
+ <div class="localization-header">
1975
+ Translate from <strong>${baseName}</strong> to the languages below.
1976
+ Closing this window returns you to editing in ${baseName}.
1977
+ </div>
1978
+ <div class="localization-language-row">
1979
+ <temba-select
1980
+ flavor="small"
1981
+ class="localization-language-select"
1982
+ .values=${activeLanguage
1983
+ ? [{ name: activeLanguage.name, value: activeLanguage.code }]
1984
+ : []}
1985
+ @change=${this.handleLocalizationLanguageSelectChange}
1986
+ >
1987
+ ${languages.map((lang) => html `<temba-option
1988
+ value="${lang.code}"
1989
+ name="${lang.name}"
1990
+ ></temba-option>`)}
1991
+ </temba-select>
1992
+ <button
1993
+ class="auto-translate-button"
1994
+ type="button"
1995
+ ?disabled=${autoTranslateButtonDisabled}
1996
+ @click=${this.handleAutoTranslateClick}
1997
+ >
1998
+ ${autoTranslateButtonLabel}
1999
+ </button>
2000
+ </div>
2001
+ <div class="localization-progress">
2002
+ <div class="localization-progress-summary">
2003
+ ${this.autoTranslating
2004
+ ? html `<temba-loading units="3" size="8"></temba-loading>
2005
+ <span>Auto translating remaining text…</span>`
2006
+ : !hasTranslations
2007
+ ? html `<span>
2008
+ Add content or enable more options to start translating.
2009
+ </span>`
2010
+ : hasPendingTranslations
2011
+ ? html `<span>
2012
+ ${progress.localized} of ${progress.total} items translated
2013
+ </span>`
2014
+ : html `<span>All items are translated.</span>`}
2015
+ </div>
2016
+ ${this.autoTranslateError
2017
+ ? html `<div class="auto-translate-error">
2018
+ ${this.autoTranslateError}
2019
+ </div>`
2020
+ : ''}
2021
+ <div class="localization-progress-bar-row">
2022
+ <div
2023
+ class="localization-progress-trigger"
2024
+ role="button"
2025
+ tabindex="0"
2026
+ aria-expanded="${this.translationSettingsExpanded}"
2027
+ aria-controls="${settingsPanelId}"
2028
+ @click=${this.handleLocalizationProgressToggleClick}
2029
+ @keydown=${this.handleLocalizationProgressToggleKeydown}
2030
+ >
2031
+ <temba-progress
2032
+ .current=${progress.localized}
2033
+ .total=${Math.max(progress.total, 1)}
2034
+ .animated=${false}
2035
+ ></temba-progress>
2036
+ </div>
2037
+ <button
2038
+ class="translation-settings-toggle"
2039
+ type="button"
2040
+ @click=${this.toggleTranslationSettings}
2041
+ aria-expanded="${this.translationSettingsExpanded}"
2042
+ aria-controls="${settingsPanelId}"
2043
+ >
2044
+ <span
2045
+ class="translation-settings-arrow ${this
2046
+ .translationSettingsExpanded
2047
+ ? 'expanded'
2048
+ : ''}"
2049
+ ></span>
2050
+ </button>
2051
+ </div>
2052
+ ${this.translationSettingsExpanded
2053
+ ? html `<div id="${settingsPanelId}" class="translation-settings">
2054
+ <div class="translation-settings-row">
2055
+ <temba-checkbox
2056
+ name="include-categories"
2057
+ label="Include categories"
2058
+ ?checked=${includeCategories}
2059
+ style="--checkbox-padding:5px; border-radius:var(--curvature);"
2060
+ @change=${this.handleIncludeCategoriesChange}
2061
+ ></temba-checkbox>
2062
+ </div>
2063
+ </div>`
2064
+ : ''}
2065
+ </div>
2066
+ </div>
2067
+ </temba-floating-window>
2068
+ `;
2069
+ }
2070
+ renderAutoTranslateDialog() {
2071
+ if (!this.autoTranslateDialogOpen) {
2072
+ return html ``;
2073
+ }
2074
+ const selectedModel = this.autoTranslateModel
2075
+ ? [this.autoTranslateModel]
2076
+ : [];
2077
+ const disableTranslate = !this.autoTranslateModel;
2078
+ return html `
2079
+ <temba-dialog
2080
+ header="Auto translate"
2081
+ .open=${this.autoTranslateDialogOpen}
2082
+ primaryButtonName="Translate"
2083
+ cancelButtonName="Cancel"
2084
+ size="small"
2085
+ .disabled=${disableTranslate}
2086
+ @temba-button-clicked=${this.handleAutoTranslateDialogButton}
2087
+ >
2088
+ <div class="auto-translate-dialog-content">
2089
+ <p>
2090
+ We'll send any untranslated text to the selected AI model and save
2091
+ the responses automatically.
2092
+ </p>
2093
+ <div class="auto-translate-models">
2094
+ <temba-select
2095
+ class="auto-translate-model-select"
2096
+ endpoint="${AUTO_TRANSLATE_MODELS_ENDPOINT}"
2097
+ .valueKey=${'uuid'}
2098
+ .values=${selectedModel}
2099
+ ?searchable=${true}
2100
+ ?clearable=${true}
2101
+ placeholder="Select an AI model"
2102
+ @change=${this.handleAutoTranslateModelChange}
2103
+ ></temba-select>
2104
+ </div>
2105
+ <p>Only text without translations will be sent.</p>
2106
+ ${this.autoTranslateError
2107
+ ? html `<div class="auto-translate-error">
2108
+ ${this.autoTranslateError}
2109
+ </div>`
2110
+ : ''}
2111
+ </div>
2112
+ </temba-dialog>
2113
+ `;
2114
+ }
2115
+ renderLocalizationTab() {
2116
+ const languages = this.getLocalizationLanguages();
2117
+ if (!languages.length) {
2118
+ return html ``;
2119
+ }
2120
+ return html `
2121
+ <temba-floating-tab
2122
+ id="localization-tab"
2123
+ icon="language"
2124
+ label="Translate Flow"
2125
+ color="#6b7280"
2126
+ .hidden=${!this.localizationWindowHidden}
2127
+ @temba-button-clicked=${this.handleLocalizationTabClick}
2128
+ ></temba-floating-tab>
2129
+ `;
2130
+ }
838
2131
  render() {
839
- var _a, _b;
2132
+ var _b, _c;
840
2133
  // we have to embed our own style since we are in light DOM
841
2134
  const style = html `<style>
842
2135
  ${unsafeCSS(Editor.styles.cssText)}
843
2136
  ${unsafeCSS(CanvasNode.styles.cssText)}
844
2137
  </style>`;
845
- const stickies = ((_b = (_a = this.definition) === null || _a === void 0 ? void 0 : _a._ui) === null || _b === void 0 ? void 0 : _b.stickies) || {};
846
- return html `${style}
2138
+ const stickies = ((_c = (_b = this.definition) === null || _b === void 0 ? void 0 : _b._ui) === null || _c === void 0 ? void 0 : _c.stickies) || {};
2139
+ return html `${style} ${this.renderLocalizationWindow()}
2140
+ ${this.renderAutoTranslateDialog()}
847
2141
  <div id="editor">
848
2142
  <div
849
2143
  id="grid"
@@ -853,13 +2147,13 @@ export class Editor extends RapidElement {
853
2147
  <div id="canvas">
854
2148
  ${this.definition
855
2149
  ? repeat(this.definition.nodes, (node) => node.uuid, (node) => {
856
- var _a, _b, _c;
857
- const position = ((_b = (_a = this.definition._ui) === null || _a === void 0 ? void 0 : _a.nodes[node.uuid]) === null || _b === void 0 ? void 0 : _b.position) || {
2150
+ var _b, _c, _d;
2151
+ const position = ((_c = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.nodes[node.uuid]) === null || _c === void 0 ? void 0 : _c.position) || {
858
2152
  left: 0,
859
2153
  top: 0
860
2154
  };
861
2155
  const dragging = this.isDragging &&
862
- ((_c = this.currentDragItem) === null || _c === void 0 ? void 0 : _c.uuid) === node.uuid;
2156
+ ((_d = this.currentDragItem) === null || _d === void 0 ? void 0 : _d.uuid) === node.uuid;
863
2157
  const selected = this.selectedItems.has(node.uuid);
864
2158
  return html `<temba-flow-node
865
2159
  class="draggable ${dragging ? 'dragging' : ''} ${selected
@@ -867,6 +2161,7 @@ export class Editor extends RapidElement {
867
2161
  : ''}"
868
2162
  @mousedown=${this.handleMouseDown.bind(this)}
869
2163
  uuid=${node.uuid}
2164
+ data-node-uuid=${node.uuid}
870
2165
  style="left:${position.left}px; top:${position.top}px"
871
2166
  .plumber=${this.plumber}
872
2167
  .node=${node}
@@ -878,9 +2173,9 @@ export class Editor extends RapidElement {
878
2173
  })
879
2174
  : html `<temba-loading></temba-loading>`}
880
2175
  ${repeat(Object.entries(stickies), ([uuid]) => uuid, ([uuid, sticky]) => {
881
- var _a;
2176
+ var _b;
882
2177
  const position = sticky.position || { left: 0, top: 0 };
883
- const dragging = this.isDragging && ((_a = this.currentDragItem) === null || _a === void 0 ? void 0 : _a.uuid) === uuid;
2178
+ const dragging = this.isDragging && ((_b = this.currentDragItem) === null || _b === void 0 ? void 0 : _b.uuid) === uuid;
884
2179
  const selected = this.selectedItems.has(uuid);
885
2180
  return html `<temba-sticky-note
886
2181
  class="draggable ${dragging ? 'dragging' : ''} ${selected
@@ -895,7 +2190,7 @@ export class Editor extends RapidElement {
895
2190
  .selected=${selected}
896
2191
  ></temba-sticky-note>`;
897
2192
  })}
898
- ${this.renderSelectionBox()}
2193
+ ${this.renderSelectionBox()} ${this.renderCanvasDropPreview()}
899
2194
  </div>
900
2195
  </div>
901
2196
  </div>
@@ -905,11 +2200,18 @@ export class Editor extends RapidElement {
905
2200
  .node=${this.editingNode}
906
2201
  .nodeUI=${this.editingNodeUI}
907
2202
  .action=${this.editingAction}
908
- @temba-node-saved=${(e) => this.handleNodeSaved(e.detail.node)}
2203
+ @temba-node-saved=${(e) => this.handleNodeSaved(e.detail.node, e.detail.uiConfig)}
909
2204
  @temba-action-saved=${(e) => this.handleActionSaved(e.detail.action)}
910
2205
  @temba-node-edit-cancelled=${this.handleNodeEditCanceled}
911
2206
  ></temba-node-editor>`
912
- : ''} `;
2207
+ : ''}
2208
+
2209
+ <temba-canvas-menu></temba-canvas-menu>
2210
+ <temba-node-type-selector
2211
+ .flowType=${this.flowType}
2212
+ .features=${this.features}
2213
+ ></temba-node-type-selector>
2214
+ ${this.renderLocalizationTab()} `;
913
2215
  }
914
2216
  }
915
2217
  __decorate([
@@ -918,6 +2220,12 @@ __decorate([
918
2220
  __decorate([
919
2221
  property({ type: String })
920
2222
  ], Editor.prototype, "version", void 0);
2223
+ __decorate([
2224
+ property({ type: String })
2225
+ ], Editor.prototype, "flowType", void 0);
2226
+ __decorate([
2227
+ property({ type: Array })
2228
+ ], Editor.prototype, "features", void 0);
921
2229
  __decorate([
922
2230
  fromStore(zustand, (state) => state.flowDefinition)
923
2231
  ], Editor.prototype, "definition", void 0);
@@ -927,6 +2235,12 @@ __decorate([
927
2235
  __decorate([
928
2236
  fromStore(zustand, (state) => state.dirtyDate)
929
2237
  ], Editor.prototype, "dirtyDate", void 0);
2238
+ __decorate([
2239
+ fromStore(zustand, (state) => state.languageCode)
2240
+ ], Editor.prototype, "languageCode", void 0);
2241
+ __decorate([
2242
+ fromStore(zustand, (state) => state.isTranslating)
2243
+ ], Editor.prototype, "isTranslating", void 0);
930
2244
  __decorate([
931
2245
  state()
932
2246
  ], Editor.prototype, "isDragging", void 0);
@@ -954,6 +2268,27 @@ __decorate([
954
2268
  __decorate([
955
2269
  state()
956
2270
  ], Editor.prototype, "isValidTarget", void 0);
2271
+ __decorate([
2272
+ state()
2273
+ ], Editor.prototype, "localizationWindowHidden", void 0);
2274
+ __decorate([
2275
+ state()
2276
+ ], Editor.prototype, "translationFilters", void 0);
2277
+ __decorate([
2278
+ state()
2279
+ ], Editor.prototype, "translationSettingsExpanded", void 0);
2280
+ __decorate([
2281
+ state()
2282
+ ], Editor.prototype, "autoTranslateDialogOpen", void 0);
2283
+ __decorate([
2284
+ state()
2285
+ ], Editor.prototype, "autoTranslating", void 0);
2286
+ __decorate([
2287
+ state()
2288
+ ], Editor.prototype, "autoTranslateModel", void 0);
2289
+ __decorate([
2290
+ state()
2291
+ ], Editor.prototype, "autoTranslateError", void 0);
957
2292
  __decorate([
958
2293
  state()
959
2294
  ], Editor.prototype, "editingNode", void 0);
@@ -963,4 +2298,19 @@ __decorate([
963
2298
  __decorate([
964
2299
  state()
965
2300
  ], Editor.prototype, "editingAction", void 0);
2301
+ __decorate([
2302
+ state()
2303
+ ], Editor.prototype, "isCreatingNewNode", void 0);
2304
+ __decorate([
2305
+ state()
2306
+ ], Editor.prototype, "pendingNodePosition", void 0);
2307
+ __decorate([
2308
+ state()
2309
+ ], Editor.prototype, "canvasDropPreview", void 0);
2310
+ __decorate([
2311
+ state()
2312
+ ], Editor.prototype, "addActionToNodeUuid", void 0);
2313
+ __decorate([
2314
+ state()
2315
+ ], Editor.prototype, "actionDragTargetNodeUuid", void 0);
966
2316
  //# sourceMappingURL=Editor.js.map