@nyaruka/temba-components 0.139.0 → 0.141.0

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 (385) hide show
  1. package/.github/workflows/cla.yml +1 -1
  2. package/.github/workflows/copilot-setup-steps.yml +6 -1
  3. package/.lintstagedrc.js +10 -0
  4. package/CHANGELOG.md +32 -0
  5. package/demo/data/flows/sample-flow.json +24 -0
  6. package/dist/locales/es.js +5 -5
  7. package/dist/locales/es.js.map +1 -1
  8. package/dist/locales/fr.js +5 -5
  9. package/dist/locales/fr.js.map +1 -1
  10. package/dist/locales/locale-codes.js +11 -2
  11. package/dist/locales/locale-codes.js.map +1 -1
  12. package/dist/locales/pt.js +5 -5
  13. package/dist/locales/pt.js.map +1 -1
  14. package/dist/temba-components.js +702 -338
  15. package/dist/temba-components.js.map +1 -1
  16. package/out-tsc/src/display/Chat.js +10 -7
  17. package/out-tsc/src/display/Chat.js.map +1 -1
  18. package/out-tsc/src/display/Dropdown.js +3 -1
  19. package/out-tsc/src/display/Dropdown.js.map +1 -1
  20. package/out-tsc/src/display/FloatingTab.js +4 -4
  21. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  22. package/out-tsc/src/display/Thumbnail.js +163 -5
  23. package/out-tsc/src/display/Thumbnail.js.map +1 -1
  24. package/out-tsc/src/flow/CanvasNode.js +65 -23
  25. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  26. package/out-tsc/src/flow/Editor.js +369 -49
  27. package/out-tsc/src/flow/Editor.js.map +1 -1
  28. package/out-tsc/src/flow/NodeEditor.js +118 -10
  29. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  30. package/out-tsc/src/flow/Plumber.js +61 -14
  31. package/out-tsc/src/flow/Plumber.js.map +1 -1
  32. package/out-tsc/src/flow/StickyNote.js +13 -4
  33. package/out-tsc/src/flow/StickyNote.js.map +1 -1
  34. package/out-tsc/src/flow/actions/add_contact_groups.js +4 -1
  35. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
  36. package/out-tsc/src/flow/actions/add_input_labels.js +4 -1
  37. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  38. package/out-tsc/src/flow/actions/audio-player.js +112 -0
  39. package/out-tsc/src/flow/actions/audio-player.js.map +1 -0
  40. package/out-tsc/src/flow/actions/enter_flow.js +43 -0
  41. package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
  42. package/out-tsc/src/flow/actions/play_audio.js +57 -4
  43. package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
  44. package/out-tsc/src/flow/actions/remove_contact_groups.js +6 -1
  45. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
  46. package/out-tsc/src/flow/actions/say_msg.js +86 -3
  47. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  48. package/out-tsc/src/flow/actions/send_broadcast.js +6 -2
  49. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  50. package/out-tsc/src/flow/actions/set_contact_channel.js +13 -0
  51. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  52. package/out-tsc/src/flow/actions/set_contact_status.js +7 -5
  53. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  54. package/out-tsc/src/flow/actions/start_session.js +10 -3
  55. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  56. package/out-tsc/src/flow/config.js +11 -3
  57. package/out-tsc/src/flow/config.js.map +1 -1
  58. package/out-tsc/src/flow/nodes/shared-rules.js +1 -1
  59. package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -1
  60. package/out-tsc/src/flow/nodes/split_by_contact_field.js +18 -5
  61. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -1
  62. package/out-tsc/src/flow/nodes/split_by_expression.js +1 -1
  63. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -1
  64. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +0 -1
  65. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  66. package/out-tsc/src/flow/nodes/split_by_random.js +0 -1
  67. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
  68. package/out-tsc/src/flow/nodes/split_by_run_result.js +10 -4
  69. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
  70. package/out-tsc/src/flow/nodes/terminal.js +7 -0
  71. package/out-tsc/src/flow/nodes/terminal.js.map +1 -0
  72. package/out-tsc/src/flow/nodes/wait_for_audio.js +77 -0
  73. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
  74. package/out-tsc/src/flow/nodes/wait_for_dial.js +151 -0
  75. package/out-tsc/src/flow/nodes/wait_for_dial.js.map +1 -0
  76. package/out-tsc/src/flow/nodes/wait_for_digits.js +61 -1
  77. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
  78. package/out-tsc/src/flow/nodes/wait_for_menu.js +173 -2
  79. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
  80. package/out-tsc/src/flow/nodes/wait_for_response.js +1 -1
  81. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  82. package/out-tsc/src/flow/operators.js +21 -5
  83. package/out-tsc/src/flow/operators.js.map +1 -1
  84. package/out-tsc/src/flow/types.js.map +1 -1
  85. package/out-tsc/src/flow/utils.js +79 -3
  86. package/out-tsc/src/flow/utils.js.map +1 -1
  87. package/out-tsc/src/form/ArrayEditor.js +4 -2
  88. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  89. package/out-tsc/src/form/FieldRenderer.js +56 -0
  90. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  91. package/out-tsc/src/interfaces.js +1 -0
  92. package/out-tsc/src/interfaces.js.map +1 -1
  93. package/out-tsc/src/layout/Dialog.js +51 -7
  94. package/out-tsc/src/layout/Dialog.js.map +1 -1
  95. package/out-tsc/src/layout/Modax.js +20 -2
  96. package/out-tsc/src/layout/Modax.js.map +1 -1
  97. package/out-tsc/src/list/ContentMenu.js +14 -1
  98. package/out-tsc/src/list/ContentMenu.js.map +1 -1
  99. package/out-tsc/src/locales/es.js +5 -5
  100. package/out-tsc/src/locales/es.js.map +1 -1
  101. package/out-tsc/src/locales/fr.js +5 -5
  102. package/out-tsc/src/locales/fr.js.map +1 -1
  103. package/out-tsc/src/locales/locale-codes.js +11 -2
  104. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  105. package/out-tsc/src/locales/pt.js +5 -5
  106. package/out-tsc/src/locales/pt.js.map +1 -1
  107. package/out-tsc/src/simulator/Simulator.js +21 -4
  108. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  109. package/out-tsc/src/store/AppState.js +102 -3
  110. package/out-tsc/src/store/AppState.js.map +1 -1
  111. package/out-tsc/test/actions/add_contact_groups.test.js +35 -0
  112. package/out-tsc/test/actions/add_contact_groups.test.js.map +1 -1
  113. package/out-tsc/test/actions/add_input_labels.test.js +53 -0
  114. package/out-tsc/test/actions/add_input_labels.test.js.map +1 -0
  115. package/out-tsc/test/actions/enter_flow.test.js +71 -0
  116. package/out-tsc/test/actions/enter_flow.test.js.map +1 -0
  117. package/out-tsc/test/actions/play_audio.test.js +118 -0
  118. package/out-tsc/test/actions/play_audio.test.js.map +1 -0
  119. package/out-tsc/test/actions/remove_contact_groups.test.js +24 -0
  120. package/out-tsc/test/actions/remove_contact_groups.test.js.map +1 -1
  121. package/out-tsc/test/actions/say_msg.test.js +158 -0
  122. package/out-tsc/test/actions/say_msg.test.js.map +1 -0
  123. package/out-tsc/test/actions/send_broadcast.test.js +41 -0
  124. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
  125. package/out-tsc/test/actions/set_contact_channel.test.js +67 -0
  126. package/out-tsc/test/actions/set_contact_channel.test.js.map +1 -0
  127. package/out-tsc/test/actions/set_contact_field.test.js +52 -0
  128. package/out-tsc/test/actions/set_contact_field.test.js.map +1 -0
  129. package/out-tsc/test/actions/set_contact_language.test.js +39 -0
  130. package/out-tsc/test/actions/set_contact_language.test.js.map +1 -0
  131. package/out-tsc/test/actions/set_contact_name.test.js +28 -0
  132. package/out-tsc/test/actions/set_contact_name.test.js.map +1 -0
  133. package/out-tsc/test/actions/set_contact_status.test.js +44 -0
  134. package/out-tsc/test/actions/set_contact_status.test.js.map +1 -0
  135. package/out-tsc/test/actions/set_run_result.test.js +47 -0
  136. package/out-tsc/test/actions/set_run_result.test.js.map +1 -0
  137. package/out-tsc/test/actions/start_session.test.js +76 -0
  138. package/out-tsc/test/actions/start_session.test.js.map +1 -1
  139. package/out-tsc/test/nodes/split_by_contact_field.test.js +50 -0
  140. package/out-tsc/test/nodes/split_by_contact_field.test.js.map +1 -1
  141. package/out-tsc/test/nodes/split_by_run_result.test.js +82 -0
  142. package/out-tsc/test/nodes/split_by_run_result.test.js.map +1 -1
  143. package/out-tsc/test/nodes/split_by_ticket.test.js +139 -0
  144. package/out-tsc/test/nodes/split_by_ticket.test.js.map +1 -0
  145. package/out-tsc/test/nodes/split_by_webhook.test.js +111 -0
  146. package/out-tsc/test/nodes/split_by_webhook.test.js.map +1 -0
  147. package/out-tsc/test/nodes/wait_for_audio.test.js +156 -0
  148. package/out-tsc/test/nodes/wait_for_audio.test.js.map +1 -0
  149. package/out-tsc/test/nodes/wait_for_dial.test.js +336 -0
  150. package/out-tsc/test/nodes/wait_for_dial.test.js.map +1 -0
  151. package/out-tsc/test/nodes/wait_for_digits.test.js +198 -84
  152. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  153. package/out-tsc/test/nodes/wait_for_menu.test.js +340 -0
  154. package/out-tsc/test/nodes/wait_for_menu.test.js.map +1 -0
  155. package/out-tsc/test/temba-flow-collision.test.js +261 -6
  156. package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
  157. package/out-tsc/test/temba-flow-editor.test.js +187 -0
  158. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  159. package/out-tsc/test/temba-flow-plumber.test.js +19 -0
  160. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  161. package/out-tsc/test/temba-node-type-selector.test.js +6 -6
  162. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
  163. package/out-tsc/test/temba-select.test.js +4 -1
  164. package/out-tsc/test/temba-select.test.js.map +1 -1
  165. package/out-tsc/test/utils.test.js +4 -2
  166. package/out-tsc/test/utils.test.js.map +1 -1
  167. package/package.json +3 -9
  168. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  169. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  170. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  171. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  172. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  173. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  174. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  175. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  176. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  177. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  178. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  179. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  180. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  181. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  182. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  183. package/screenshots/truth/actions/add_input_labels/editor/multiple-labels.png +0 -0
  184. package/screenshots/truth/actions/add_input_labels/editor/single-label.png +0 -0
  185. package/screenshots/truth/actions/add_input_labels/render/multiple-labels.png +0 -0
  186. package/screenshots/truth/actions/add_input_labels/render/single-label.png +0 -0
  187. package/screenshots/truth/actions/enter_flow/editor/basic-flow.png +0 -0
  188. package/screenshots/truth/actions/enter_flow/editor/long-flow-name.png +0 -0
  189. package/screenshots/truth/actions/enter_flow/render/basic-flow.png +0 -0
  190. package/screenshots/truth/actions/enter_flow/render/long-flow-name.png +0 -0
  191. package/screenshots/truth/actions/play_audio/editor/expression-url.png +0 -0
  192. package/screenshots/truth/actions/play_audio/editor/static-url.png +0 -0
  193. package/screenshots/truth/actions/play_audio/render/expression-url.png +0 -0
  194. package/screenshots/truth/actions/play_audio/render/static-url.png +0 -0
  195. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  196. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  197. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  198. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  199. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  200. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  201. package/screenshots/truth/actions/say_msg/editor/multiline-text.png +0 -0
  202. package/screenshots/truth/actions/say_msg/editor/simple-text.png +0 -0
  203. package/screenshots/truth/actions/say_msg/editor/text-with-audio-url.png +0 -0
  204. package/screenshots/truth/actions/say_msg/render/multiline-text.png +0 -0
  205. package/screenshots/truth/actions/say_msg/render/simple-text.png +0 -0
  206. package/screenshots/truth/actions/say_msg/render/text-with-audio-url.png +0 -0
  207. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  208. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  209. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  210. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  211. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  212. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  213. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  214. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  215. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  216. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  217. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  218. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  219. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  220. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  221. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  222. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  223. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  224. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  225. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  226. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  227. package/screenshots/truth/actions/set_contact_channel/editor/sms-channel.png +0 -0
  228. package/screenshots/truth/actions/set_contact_channel/editor/whatsapp-channel.png +0 -0
  229. package/screenshots/truth/actions/set_contact_channel/render/sms-channel.png +0 -0
  230. package/screenshots/truth/actions/set_contact_channel/render/whatsapp-channel.png +0 -0
  231. package/screenshots/truth/actions/set_contact_field/editor/clear-value.png +0 -0
  232. package/screenshots/truth/actions/set_contact_field/editor/set-value.png +0 -0
  233. package/screenshots/truth/actions/set_contact_field/render/clear-value.png +0 -0
  234. package/screenshots/truth/actions/set_contact_field/render/set-value.png +0 -0
  235. package/screenshots/truth/actions/set_contact_language/editor/english.png +0 -0
  236. package/screenshots/truth/actions/set_contact_language/editor/french.png +0 -0
  237. package/screenshots/truth/actions/set_contact_language/render/english.png +0 -0
  238. package/screenshots/truth/actions/set_contact_language/render/french.png +0 -0
  239. package/screenshots/truth/actions/set_contact_name/editor/expression-name.png +0 -0
  240. package/screenshots/truth/actions/set_contact_name/editor/static-name.png +0 -0
  241. package/screenshots/truth/actions/set_contact_name/render/expression-name.png +0 -0
  242. package/screenshots/truth/actions/set_contact_name/render/static-name.png +0 -0
  243. package/screenshots/truth/actions/set_contact_status/editor/active.png +0 -0
  244. package/screenshots/truth/actions/set_contact_status/editor/archived.png +0 -0
  245. package/screenshots/truth/actions/set_contact_status/editor/blocked.png +0 -0
  246. package/screenshots/truth/actions/set_contact_status/render/active.png +0 -0
  247. package/screenshots/truth/actions/set_contact_status/render/archived.png +0 -0
  248. package/screenshots/truth/actions/set_contact_status/render/blocked.png +0 -0
  249. package/screenshots/truth/actions/set_run_result/editor/expression-value.png +0 -0
  250. package/screenshots/truth/actions/set_run_result/editor/with-category.png +0 -0
  251. package/screenshots/truth/actions/set_run_result/render/expression-value.png +0 -0
  252. package/screenshots/truth/actions/set_run_result/render/with-category.png +0 -0
  253. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  254. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  255. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  256. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  257. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  258. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  259. package/screenshots/truth/editor/router.png +0 -0
  260. package/screenshots/truth/editor/wait.png +0 -0
  261. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  262. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  263. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  264. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  265. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  266. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  267. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  268. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  269. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  270. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  271. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  272. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  273. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  274. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  275. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  276. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  277. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  278. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  279. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  280. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  281. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  282. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  283. package/screenshots/truth/nodes/wait_for_audio/editor/basic-audio-wait.png +0 -0
  284. package/screenshots/truth/nodes/wait_for_audio/render/basic-audio-wait.png +0 -0
  285. package/screenshots/truth/nodes/wait_for_dial/editor/basic-dial.png +0 -0
  286. package/screenshots/truth/nodes/wait_for_dial/editor/dial-with-limits.png +0 -0
  287. package/screenshots/truth/nodes/wait_for_dial/render/basic-dial.png +0 -0
  288. package/screenshots/truth/nodes/wait_for_dial/render/dial-with-limits.png +0 -0
  289. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  290. package/screenshots/truth/nodes/wait_for_digits/editor/digits-with-rules.png +0 -0
  291. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  292. package/screenshots/truth/nodes/wait_for_digits/render/digits-with-rules.png +0 -0
  293. package/screenshots/truth/nodes/wait_for_menu/editor/menu-with-digits.png +0 -0
  294. package/screenshots/truth/nodes/wait_for_menu/render/menu-with-digits.png +0 -0
  295. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  296. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  297. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  298. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  299. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  300. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  301. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  302. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  303. package/src/display/Chat.ts +13 -7
  304. package/src/display/Dropdown.ts +3 -1
  305. package/src/display/FloatingTab.ts +4 -4
  306. package/src/display/Thumbnail.ts +162 -2
  307. package/src/flow/CanvasNode.ts +70 -24
  308. package/src/flow/Editor.ts +440 -99
  309. package/src/flow/NodeEditor.ts +137 -9
  310. package/src/flow/Plumber.ts +89 -14
  311. package/src/flow/StickyNote.ts +14 -4
  312. package/src/flow/actions/add_contact_groups.ts +4 -1
  313. package/src/flow/actions/add_input_labels.ts +4 -1
  314. package/src/flow/actions/audio-player.ts +127 -0
  315. package/src/flow/actions/enter_flow.ts +44 -0
  316. package/src/flow/actions/play_audio.ts +64 -5
  317. package/src/flow/actions/remove_contact_groups.ts +6 -1
  318. package/src/flow/actions/say_msg.ts +94 -4
  319. package/src/flow/actions/send_broadcast.ts +6 -2
  320. package/src/flow/actions/set_contact_channel.ts +13 -1
  321. package/src/flow/actions/set_contact_status.ts +7 -5
  322. package/src/flow/actions/start_session.ts +10 -3
  323. package/src/flow/config.ts +11 -3
  324. package/src/flow/nodes/shared-rules.ts +1 -1
  325. package/src/flow/nodes/split_by_contact_field.ts +16 -5
  326. package/src/flow/nodes/split_by_expression.ts +1 -1
  327. package/src/flow/nodes/split_by_llm_categorize.ts +0 -1
  328. package/src/flow/nodes/split_by_random.ts +0 -1
  329. package/src/flow/nodes/split_by_run_result.ts +10 -4
  330. package/src/flow/nodes/terminal.ts +9 -0
  331. package/src/flow/nodes/wait_for_audio.ts +88 -0
  332. package/src/flow/nodes/wait_for_dial.ts +176 -0
  333. package/src/flow/nodes/wait_for_digits.ts +87 -2
  334. package/src/flow/nodes/wait_for_menu.ts +209 -3
  335. package/src/flow/nodes/wait_for_response.ts +1 -1
  336. package/src/flow/operators.ts +23 -5
  337. package/src/flow/types.ts +23 -1
  338. package/src/flow/utils.ts +82 -3
  339. package/src/form/ArrayEditor.ts +4 -2
  340. package/src/form/FieldRenderer.ts +71 -1
  341. package/src/interfaces.ts +2 -1
  342. package/src/layout/Dialog.ts +52 -7
  343. package/src/layout/Modax.ts +19 -2
  344. package/src/list/ContentMenu.ts +15 -1
  345. package/src/locales/es.ts +18 -13
  346. package/src/locales/fr.ts +18 -13
  347. package/src/locales/locale-codes.ts +11 -2
  348. package/src/locales/pt.ts +18 -13
  349. package/src/simulator/Simulator.ts +25 -4
  350. package/src/store/AppState.ts +120 -1
  351. package/src/store/flow-definition.d.ts +2 -0
  352. package/test/actions/add_contact_groups.test.ts +38 -0
  353. package/test/actions/add_input_labels.test.ts +67 -0
  354. package/test/actions/enter_flow.test.ts +88 -0
  355. package/test/actions/play_audio.test.ts +155 -0
  356. package/test/actions/remove_contact_groups.test.ts +29 -0
  357. package/test/actions/say_msg.test.ts +196 -0
  358. package/test/actions/send_broadcast.test.ts +44 -0
  359. package/test/actions/set_contact_channel.test.ts +88 -0
  360. package/test/actions/set_contact_field.test.ts +68 -0
  361. package/test/actions/set_contact_language.test.ts +55 -0
  362. package/test/actions/set_contact_name.test.ts +39 -0
  363. package/test/actions/set_contact_status.test.ts +64 -0
  364. package/test/actions/set_run_result.test.ts +61 -0
  365. package/test/actions/start_session.test.ts +82 -0
  366. package/test/nodes/split_by_contact_field.test.ts +59 -0
  367. package/test/nodes/split_by_run_result.test.ts +100 -0
  368. package/test/nodes/split_by_ticket.test.ts +157 -0
  369. package/test/nodes/split_by_webhook.test.ts +131 -0
  370. package/test/nodes/wait_for_audio.test.ts +182 -0
  371. package/test/nodes/wait_for_dial.test.ts +382 -0
  372. package/test/nodes/wait_for_digits.test.ts +233 -109
  373. package/test/nodes/wait_for_menu.test.ts +383 -0
  374. package/test/temba-flow-collision.test.ts +286 -6
  375. package/test/temba-flow-editor.test.ts +240 -0
  376. package/test/temba-flow-plumber.test.ts +62 -0
  377. package/test/temba-node-type-selector.test.ts +6 -6
  378. package/test/temba-select.test.ts +6 -1
  379. package/test/utils.test.ts +4 -2
  380. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  381. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  382. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  383. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  384. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  385. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
@@ -11,6 +11,7 @@ import {
11
11
  import { getStore } from '../store/Store';
12
12
  import {
13
13
  AppState,
14
+ FlowIssue,
14
15
  fromStore,
15
16
  zustand,
16
17
  FLOW_SPEC_VERSION
@@ -18,7 +19,20 @@ import {
18
19
  import { RapidElement } from '../RapidElement';
19
20
  import { repeat } from 'lit-html/directives/repeat.js';
20
21
  import { CustomEventType, Workspace } from '../interfaces';
21
- import { generateUUID, postJSON, fetchResults, getClasses } from '../utils';
22
+ import {
23
+ generateUUID,
24
+ postJSON,
25
+ fetchResults,
26
+ getClasses,
27
+ WebResponse
28
+ } from '../utils';
29
+ import {
30
+ formatIssueMessage,
31
+ getNodeBounds,
32
+ calculateReflowPositions,
33
+ NodeBounds,
34
+ snapToGrid
35
+ } from './utils';
22
36
  import { ACTION_CONFIG, NODE_CONFIG } from './config';
23
37
 
24
38
  interface Revision {
@@ -49,12 +63,6 @@ import { Dialog } from '../layout/Dialog';
49
63
 
50
64
  import { CanvasMenu, CanvasMenuSelection } from './CanvasMenu';
51
65
  import { NodeTypeSelector, NodeTypeSelection } from './NodeTypeSelector';
52
- import {
53
- getNodeBounds,
54
- calculateReflowPositions,
55
- NodeBounds,
56
- snapToGrid
57
- } from './utils';
58
66
  import { FloatingWindow } from '../layout/FloatingWindow';
59
67
 
60
68
  export function findNodeForExit(
@@ -70,7 +78,7 @@ export function findNodeForExit(
70
78
  return null;
71
79
  }
72
80
 
73
- const SAVE_QUIET_TIME = 500;
81
+ const SAVE_QUIET_TIME = 2000;
74
82
 
75
83
  export interface DraggableItem {
76
84
  uuid: string;
@@ -173,6 +181,9 @@ export class Editor extends RapidElement {
173
181
  @fromStore(zustand, (state: AppState) => state.getCurrentActivity())
174
182
  private activityData!: any;
175
183
 
184
+ @fromStore(zustand, (state: AppState) => state.flowInfo?.issues || [])
185
+ private flowIssues!: FlowIssue[];
186
+
176
187
  // Drag state
177
188
  @state()
178
189
  private isDragging = false;
@@ -217,6 +228,9 @@ export class Editor extends RapidElement {
217
228
  private connectionSourceX: number | null = null;
218
229
  private connectionSourceY: number | null = null;
219
230
 
231
+ @state()
232
+ private issuesWindowHidden = true;
233
+
220
234
  @state()
221
235
  private localizationWindowHidden = true;
222
236
 
@@ -252,6 +266,12 @@ export class Editor extends RapidElement {
252
266
  @state()
253
267
  private isLoadingRevisions = false;
254
268
 
269
+ @state()
270
+ private isSaving = false;
271
+
272
+ @state()
273
+ private saveError: string | null = null;
274
+
255
275
  private preRevertState: {
256
276
  definition: FlowDefinition;
257
277
  dirtyDate: Date | null;
@@ -269,6 +289,8 @@ export class Editor extends RapidElement {
269
289
  @state()
270
290
  private editingAction: Action | null = null;
271
291
 
292
+ private dialogOrigin: { x: number; y: number } | null = null;
293
+
272
294
  @state()
273
295
  private isCreatingNewNode = false;
274
296
 
@@ -351,12 +373,23 @@ export class Editor extends RapidElement {
351
373
 
352
374
  static get styles() {
353
375
  return css`
376
+ #editor-container {
377
+ position: relative;
378
+ flex: 1;
379
+ display: flex;
380
+ min-height: 0;
381
+ }
382
+
354
383
  #editor {
355
384
  overflow: scroll;
356
385
  flex: 1;
357
386
  -webkit-font-smoothing: antialiased;
358
387
  }
359
388
 
389
+ temba-floating-tab {
390
+ --floating-tab-right: 15px;
391
+ }
392
+
360
393
  #grid {
361
394
  position: relative;
362
395
  background-color: #f9f9f9;
@@ -490,7 +523,7 @@ export class Editor extends RapidElement {
490
523
  font-weight: 600;
491
524
  line-height: 0.9;
492
525
  cursor: pointer;
493
- z-index: 500;
526
+ z-index: 10;
494
527
  pointer-events: auto;
495
528
  white-space: nowrap;
496
529
  user-select: none;
@@ -805,6 +838,91 @@ export class Editor extends RapidElement {
805
838
  color: #9ca3af;
806
839
  white-space: nowrap;
807
840
  }
841
+
842
+ .issue-list-item {
843
+ display: flex;
844
+ align-items: center;
845
+ gap: 8px;
846
+ padding: 8px;
847
+ border-radius: 4px;
848
+ cursor: pointer;
849
+ font-size: 13px;
850
+ color: #333;
851
+ }
852
+
853
+ .issue-list-item:hover {
854
+ background: #fff5f5;
855
+ }
856
+
857
+ .issue-list-item temba-icon {
858
+ color: tomato;
859
+ flex-shrink: 0;
860
+ }
861
+
862
+ .empty-flow {
863
+ position: sticky;
864
+ top: 80px;
865
+ left: 0;
866
+ right: 0;
867
+ height: 0;
868
+ display: flex;
869
+ justify-content: center;
870
+ pointer-events: none;
871
+ z-index: 50;
872
+ }
873
+
874
+ .empty-flow-content {
875
+ display: flex;
876
+ flex-direction: column;
877
+ align-items: center;
878
+ gap: 16px;
879
+ text-align: center;
880
+ pointer-events: auto;
881
+ }
882
+
883
+ .empty-flow-title {
884
+ font-size: 18px;
885
+ font-weight: 600;
886
+ color: #374151;
887
+ }
888
+
889
+ .empty-flow-description {
890
+ font-size: 14px;
891
+ color: #6b7280;
892
+ max-width: 320px;
893
+ line-height: 1.5;
894
+ }
895
+
896
+ .empty-flow-button {
897
+ background: var(--color-primary-dark);
898
+ border: none;
899
+ color: #fff;
900
+ padding: 10px 20px;
901
+ border-radius: var(--curvature);
902
+ font-size: 14px;
903
+ font-weight: 600;
904
+ cursor: pointer;
905
+ transition: opacity 0.2s ease;
906
+ }
907
+
908
+ .empty-flow-button:hover {
909
+ opacity: 0.9;
910
+ }
911
+
912
+ .save-indicator {
913
+ position: absolute;
914
+ top: 8px;
915
+ right: 16px;
916
+ padding: 6px 10px;
917
+ z-index: 10000;
918
+ pointer-events: none;
919
+ opacity: 0;
920
+ transition: opacity 0.15s ease-in-out;
921
+ }
922
+
923
+ .save-indicator.visible {
924
+ opacity: 1;
925
+ }
808
926
  `;
809
927
  }
810
928
 
@@ -820,6 +938,7 @@ export class Editor extends RapidElement {
820
938
  this.setupGlobalEventListeners();
821
939
  if (changes.has('flow')) {
822
940
  getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
941
+ this.fetchRevisions();
823
942
  }
824
943
 
825
944
  this.plumber.on('connection:drag', (connection: any) => {
@@ -984,10 +1103,16 @@ export class Editor extends RapidElement {
984
1103
 
985
1104
  if (changes.has('dirtyDate')) {
986
1105
  if (this.dirtyDate) {
1106
+ this.isSaving = true;
987
1107
  this.debouncedSave();
988
1108
  }
989
1109
  }
990
1110
 
1111
+ if (changes.has('saveError') && this.saveError) {
1112
+ this.showSaveErrorDialog(this.saveError);
1113
+ this.saveError = null;
1114
+ }
1115
+
991
1116
  if (changes.has('languageCode')) {
992
1117
  this.translationCache.clear();
993
1118
  }
@@ -1033,10 +1158,16 @@ export class Editor extends RapidElement {
1033
1158
 
1034
1159
  private saveChanges(definitionOverride?: FlowDefinition): Promise<void> {
1035
1160
  const definition = definitionOverride || this.definition;
1036
- // post the flow definition to the server
1161
+ this.isSaving = true;
1162
+
1037
1163
  return getStore()
1038
1164
  .postJSON(`/flow/revisions/${this.flow}/`, definition)
1039
1165
  .then((response) => {
1166
+ if (response.status < 200 || response.status >= 300) {
1167
+ this.saveError = this.extractErrorMessage(response);
1168
+ return;
1169
+ }
1170
+
1040
1171
  // Update flow info and revision with the response data
1041
1172
  if (response.json) {
1042
1173
  const state = getStore().getState();
@@ -1049,17 +1180,58 @@ export class Editor extends RapidElement {
1049
1180
  state.setRevision(response.json.revision.revision);
1050
1181
  }
1051
1182
 
1052
- // if the revisions window is open, refresh the list
1053
- if (!this.revisionsWindowHidden) {
1054
- this.fetchRevisions();
1055
- }
1183
+ // Refresh revisions list so the tab visibility stays up to date
1184
+ this.fetchRevisions();
1056
1185
  }
1186
+
1187
+ getStore().getState().setDirtyDate(null);
1057
1188
  })
1058
1189
  .catch((error) => {
1059
1190
  console.error('Failed to save flow:', error);
1191
+ if (error instanceof Response) {
1192
+ this.saveError = `Server error (${error.status}). Your changes have not been saved.`;
1193
+ } else {
1194
+ this.saveError =
1195
+ 'Unable to reach the server. Please check your connection and try again.';
1196
+ }
1197
+ })
1198
+ .finally(() => {
1199
+ this.isSaving = false;
1060
1200
  });
1201
+ }
1061
1202
 
1062
- getStore().getState().setDirtyDate(null);
1203
+ private extractErrorMessage(response: WebResponse): string {
1204
+ if (response.json) {
1205
+ if (typeof response.json.detail === 'string') {
1206
+ return response.json.detail;
1207
+ }
1208
+ if (typeof response.json.error === 'string') {
1209
+ return response.json.error;
1210
+ }
1211
+ if (typeof response.json.description === 'string') {
1212
+ return response.json.description;
1213
+ }
1214
+ }
1215
+ return `Save failed with status ${response.status}.`;
1216
+ }
1217
+
1218
+ private showSaveErrorDialog(message: string): void {
1219
+ const dialog = document.createElement('temba-dialog') as Dialog;
1220
+ dialog.header = 'Save Failed';
1221
+ dialog.primaryButtonName = '';
1222
+ dialog.cancelButtonName = 'Dismiss';
1223
+
1224
+ const content = document.createElement('div');
1225
+ content.style.cssText = 'padding: 20px; font-size: 14px; line-height: 1.5;';
1226
+ content.textContent = message;
1227
+ dialog.appendChild(content);
1228
+
1229
+ document.body.appendChild(dialog);
1230
+ dialog.open = true;
1231
+
1232
+ dialog.addEventListener('temba-dialog-hidden', () => {
1233
+ document.body.removeChild(dialog);
1234
+ });
1063
1235
  }
1064
1236
 
1065
1237
  private startActivityFetching(): void {
@@ -1095,6 +1267,11 @@ export class Editor extends RapidElement {
1095
1267
  }
1096
1268
  const state = store.getState();
1097
1269
  state.fetchActivity(activityEndpoint).then(() => {
1270
+ // Guard against responses arriving after the editor is disconnected
1271
+ if (!this.isConnected) {
1272
+ return;
1273
+ }
1274
+
1098
1275
  // Schedule next fetch with exponential backoff (max 5 minutes)
1099
1276
  this.activityInterval = Math.min(60000 * 5, this.activityInterval + 100);
1100
1277
 
@@ -1131,6 +1308,10 @@ export class Editor extends RapidElement {
1131
1308
  if (canvas) {
1132
1309
  canvas.removeEventListener('contextmenu', this.boundCanvasContextMenu);
1133
1310
  }
1311
+
1312
+ // Clear all flow-specific data from the store so stale data
1313
+ // isn't briefly visible when a different flow is opened.
1314
+ zustand.getState().clearFlowData();
1134
1315
  }
1135
1316
 
1136
1317
  private setupGlobalEventListeners(): void {
@@ -1219,6 +1400,7 @@ export class Editor extends RapidElement {
1219
1400
  if (event.button !== 0) return;
1220
1401
 
1221
1402
  if (this.isReadOnly()) return;
1403
+ this.blurActiveContentEditable();
1222
1404
 
1223
1405
  const element = event.currentTarget as HTMLElement;
1224
1406
  // Only start dragging if clicking on the element itself, not on exits or other interactive elements
@@ -1288,8 +1470,22 @@ export class Editor extends RapidElement {
1288
1470
  this.handleCanvasMouseDown(event);
1289
1471
  }
1290
1472
 
1473
+ private blurActiveContentEditable(): void {
1474
+ let active: Element | null = document.activeElement;
1475
+ while (active?.shadowRoot?.activeElement) {
1476
+ active = active.shadowRoot.activeElement;
1477
+ }
1478
+ if (
1479
+ active instanceof HTMLElement &&
1480
+ active.getAttribute('contenteditable') === 'true'
1481
+ ) {
1482
+ active.blur();
1483
+ }
1484
+ }
1485
+
1291
1486
  private handleCanvasMouseDown(event: MouseEvent): void {
1292
1487
  if (this.isReadOnly()) return;
1488
+ this.blurActiveContentEditable();
1293
1489
 
1294
1490
  const target = event.target as HTMLElement;
1295
1491
  if (target.id === 'canvas' || target.id === 'grid') {
@@ -1974,6 +2170,28 @@ export class Editor extends RapidElement {
1974
2170
  }
1975
2171
  }
1976
2172
 
2173
+ private handleEmptyFlowClick(event: MouseEvent): void {
2174
+ const editor = this.querySelector('#editor') as HTMLElement;
2175
+ if (!editor) return;
2176
+
2177
+ // Scroll to top-left
2178
+ editor.scrollTo({ left: 0, top: 0, behavior: 'smooth' });
2179
+
2180
+ // Place node at top-left of the canvas
2181
+ const nodeLeft = 0;
2182
+ const nodeTop = 0;
2183
+
2184
+ const canvasMenu = this.querySelector('temba-canvas-menu') as CanvasMenu;
2185
+ if (canvasMenu) {
2186
+ const button = event.currentTarget as HTMLElement;
2187
+ const rect = button.getBoundingClientRect();
2188
+ const menuWidth = 265;
2189
+ const menuX = rect.left + rect.width / 2 - menuWidth / 2;
2190
+ const menuY = rect.bottom + 8;
2191
+ canvasMenu.show(menuX, menuY, { x: nodeLeft, y: nodeTop }, false);
2192
+ }
2193
+ }
2194
+
1977
2195
  private handleCanvasMenuSelection(event: CustomEvent): void {
1978
2196
  const selection = event.detail as CanvasMenuSelection;
1979
2197
  const store = getStore();
@@ -2160,6 +2378,10 @@ export class Editor extends RapidElement {
2160
2378
  private handleActionEditRequested(event: CustomEvent): void {
2161
2379
  // For action editing, we set the action and find the corresponding node
2162
2380
  this.editingAction = event.detail.action;
2381
+ this.dialogOrigin =
2382
+ event.detail.originX != null
2383
+ ? { x: event.detail.originX, y: event.detail.originY }
2384
+ : null;
2163
2385
 
2164
2386
  // Find the node that contains this action
2165
2387
  const nodeUuid = event.detail.nodeUuid;
@@ -2205,6 +2427,10 @@ export class Editor extends RapidElement {
2205
2427
  private handleNodeEditRequested(event: CustomEvent): void {
2206
2428
  this.editingNode = event.detail.node;
2207
2429
  this.editingNodeUI = event.detail.nodeUI;
2430
+ this.dialogOrigin =
2431
+ event.detail.originX != null
2432
+ ? { x: event.detail.originX, y: event.detail.originY }
2433
+ : null;
2208
2434
  }
2209
2435
 
2210
2436
  private handleNodeDeleted(event: CustomEvent): void {
@@ -2294,6 +2520,7 @@ export class Editor extends RapidElement {
2294
2520
  this.editingNode = null;
2295
2521
  this.editingNodeUI = null;
2296
2522
  this.editingAction = null;
2523
+ this.dialogOrigin = null;
2297
2524
  }
2298
2525
 
2299
2526
  private handleActionEditCanceled(): void {
@@ -2948,6 +3175,7 @@ export class Editor extends RapidElement {
2948
3175
 
2949
3176
  this.localizationWindowHidden = false;
2950
3177
  this.revisionsWindowHidden = true;
3178
+ this.issuesWindowHidden = true;
2951
3179
 
2952
3180
  const alreadySelected = languages.some(
2953
3181
  (lang) => lang.code === this.languageCode
@@ -3210,11 +3438,47 @@ export class Editor extends RapidElement {
3210
3438
  this.autoTranslating = false;
3211
3439
  }
3212
3440
 
3441
+ private handleIssuesTabClick(): void {
3442
+ this.issuesWindowHidden = false;
3443
+ this.revisionsWindowHidden = true;
3444
+ this.localizationWindowHidden = true;
3445
+ }
3446
+
3447
+ private handleIssuesWindowClosed(): void {
3448
+ this.issuesWindowHidden = true;
3449
+ }
3450
+
3451
+ private handleIssueItemClick(issue: FlowIssue): void {
3452
+ const issuesWindow = document.getElementById(
3453
+ 'issues-window'
3454
+ ) as FloatingWindow;
3455
+ issuesWindow?.handleClose();
3456
+ this.issuesWindowHidden = true;
3457
+
3458
+ this.focusNode(issue.node_uuid);
3459
+
3460
+ const node = this.definition.nodes.find((n) => n.uuid === issue.node_uuid);
3461
+ if (!node) return;
3462
+
3463
+ if (issue.action_uuid) {
3464
+ const action = node.actions?.find((a) => a.uuid === issue.action_uuid);
3465
+ if (action) {
3466
+ this.editingAction = action;
3467
+ this.editingNode = node;
3468
+ this.editingNodeUI = this.definition._ui.nodes[issue.node_uuid];
3469
+ }
3470
+ } else {
3471
+ this.editingNode = node;
3472
+ this.editingNodeUI = this.definition._ui.nodes[issue.node_uuid];
3473
+ }
3474
+ }
3475
+
3213
3476
  private handleRevisionsTabClick(): void {
3214
3477
  if (this.revisionsWindowHidden) {
3215
3478
  this.fetchRevisions();
3216
3479
  this.revisionsWindowHidden = false;
3217
- this.localizationWindowHidden = true; // Close other window
3480
+ this.issuesWindowHidden = true;
3481
+ this.localizationWindowHidden = true;
3218
3482
  }
3219
3483
  }
3220
3484
 
@@ -3328,14 +3592,60 @@ export class Editor extends RapidElement {
3328
3592
  getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
3329
3593
  }
3330
3594
 
3595
+ private renderIssuesTab(): TemplateResult | string {
3596
+ if (!this.flowIssues?.length || !this.revisionsWindowHidden) return '';
3597
+ return html`
3598
+ <temba-floating-tab
3599
+ id="issues-tab"
3600
+ icon="alert_warning"
3601
+ label="Flow Issues"
3602
+ color="tomato"
3603
+ order="1"
3604
+ .hidden=${!this.issuesWindowHidden}
3605
+ @temba-button-clicked=${this.handleIssuesTabClick}
3606
+ ></temba-floating-tab>
3607
+ `;
3608
+ }
3609
+
3610
+ private renderIssuesWindow(): TemplateResult | string {
3611
+ if (!this.flowIssues?.length) return '';
3612
+ return html`
3613
+ <temba-floating-window
3614
+ id="issues-window"
3615
+ header="Flow Issues"
3616
+ .width=${360}
3617
+ .maxHeight=${600}
3618
+ .top=${75}
3619
+ color="tomato"
3620
+ .hidden=${this.issuesWindowHidden}
3621
+ @temba-dialog-hidden=${this.handleIssuesWindowClosed}
3622
+ >
3623
+ <div style="display:flex; flex-direction:column; gap:2px;">
3624
+ ${this.flowIssues.map(
3625
+ (issue) => html`
3626
+ <div
3627
+ class="issue-list-item"
3628
+ @click=${() => this.handleIssueItemClick(issue)}
3629
+ >
3630
+ <temba-icon name="alert_warning" size="1.2"></temba-icon>
3631
+ <span>${formatIssueMessage(issue)}</span>
3632
+ </div>
3633
+ `
3634
+ )}
3635
+ </div>
3636
+ </temba-floating-window>
3637
+ `;
3638
+ }
3639
+
3331
3640
  private renderRevisionsTab(): TemplateResult | string {
3641
+ if (this.revisions.length <= 1) return '';
3332
3642
  return html`
3333
3643
  <temba-floating-tab
3334
3644
  id="revisions-tab"
3335
3645
  icon="revisions"
3336
3646
  label="Revisions"
3337
3647
  color="rgb(142, 94, 167)"
3338
- order="1"
3648
+ order="2"
3339
3649
  .hidden=${!this.revisionsWindowHidden && this.localizationWindowHidden}
3340
3650
  @temba-button-clicked=${this.handleRevisionsTabClick}
3341
3651
  ></temba-floating-tab>
@@ -3622,6 +3932,8 @@ export class Editor extends RapidElement {
3622
3932
  }
3623
3933
 
3624
3934
  private renderLocalizationTab(): TemplateResult | string {
3935
+ if (!this.revisionsWindowHidden) return '';
3936
+ if (this.definition?.nodes.length === 0) return '';
3625
3937
  const languages = this.getLocalizationLanguages();
3626
3938
  if (!languages.length) {
3627
3939
  return html``;
@@ -3633,7 +3945,7 @@ export class Editor extends RapidElement {
3633
3945
  icon="language"
3634
3946
  label="Translate Flow"
3635
3947
  color="#6b7280"
3636
- order="2"
3948
+ order="3"
3637
3949
  .hidden=${!this.localizationWindowHidden}
3638
3950
  @temba-button-clicked=${this.handleLocalizationTabClick}
3639
3951
  ></temba-floating-tab>
@@ -3691,90 +4003,117 @@ export class Editor extends RapidElement {
3691
4003
 
3692
4004
  const stickies = this.definition?._ui?.stickies || {};
3693
4005
 
3694
- return html`${style} ${this.renderRevisionsWindow()}
3695
- ${this.renderLocalizationWindow()} ${this.renderAutoTranslateDialog()}
3696
- <div id="editor">
3697
- <div
3698
- id="grid"
3699
- class="${this.viewingRevision ? 'viewing-revision' : ''}"
3700
- style="min-width:100%;width:${this.canvasSize.width}px; height:${this
3701
- .canvasSize.height}px"
3702
- >
4006
+ return html`${style} ${this.renderIssuesWindow()}
4007
+ ${this.renderRevisionsWindow()} ${this.renderLocalizationWindow()}
4008
+ ${this.renderAutoTranslateDialog()}
4009
+ <div id="editor-container">
4010
+ <div id="editor">
4011
+ ${this.definition &&
4012
+ this.definition.nodes.length === 0 &&
4013
+ !this.isReadOnly()
4014
+ ? html`<div class="empty-flow">
4015
+ <div class="empty-flow-content">
4016
+ <div class="empty-flow-title">This flow is empty</div>
4017
+ <div class="empty-flow-description">
4018
+ Get started by adding your first action or split to define
4019
+ how this flow will work.
4020
+ </div>
4021
+ <button
4022
+ class="empty-flow-button"
4023
+ @click=${this.handleEmptyFlowClick}
4024
+ >
4025
+ Add first step
4026
+ </button>
4027
+ </div>
4028
+ </div>`
4029
+ : ''}
3703
4030
  <div
3704
- id="canvas"
3705
- class="${getClasses({
3706
- 'viewing-revision': !!this.viewingRevision,
3707
- 'read-only-connections':
3708
- !!this.viewingRevision || this.isTranslating
3709
- })}"
4031
+ id="grid"
4032
+ class="${this.viewingRevision ? 'viewing-revision' : ''}"
4033
+ style="min-width:100%;width:${this.canvasSize
4034
+ .width}px; height:${this.canvasSize.height}px"
3710
4035
  >
3711
- ${this.definition
3712
- ? repeat(
3713
- [...this.definition.nodes].sort((a, b) =>
3714
- a.uuid.localeCompare(b.uuid)
3715
- ),
3716
- (node) => node.uuid,
3717
- (node) => {
3718
- const position = this.definition._ui?.nodes[node.uuid]
3719
- ?.position || {
3720
- left: 0,
3721
- top: 0
3722
- };
3723
-
3724
- const dragging =
3725
- this.isDragging &&
3726
- this.currentDragItem?.uuid === node.uuid;
3727
-
3728
- const selected = this.selectedItems.has(node.uuid);
3729
-
3730
- // first node is the flow start (nodes are sorted by position)
3731
- const isFlowStart =
3732
- this.definition.nodes.length > 0 &&
3733
- this.definition.nodes[0].uuid === node.uuid;
3734
-
3735
- return html`<temba-flow-node
3736
- class="draggable ${dragging ? 'dragging' : ''} ${selected
3737
- ? 'selected'
3738
- : ''} ${isFlowStart ? 'flow-start' : ''}"
3739
- @mousedown=${this.handleMouseDown.bind(this)}
3740
- uuid=${node.uuid}
3741
- data-node-uuid=${node.uuid}
3742
- style="left:${position.left}px; top:${position.top}px;transition: all 0.2s ease-in-out;"
3743
- .plumber=${this.plumber}
3744
- .node=${node}
3745
- .ui=${this.definition._ui.nodes[node.uuid]}
3746
- @temba-node-deleted=${(event) => {
3747
- this.deleteNodes([event.detail.uuid]);
3748
- }}
3749
- ></temba-flow-node>`;
3750
- }
3751
- )
3752
- : html`<temba-loading></temba-loading>`}
3753
- ${repeat(
3754
- Object.entries(stickies),
3755
- ([uuid]) => uuid,
3756
- ([uuid, sticky]) => {
3757
- const position = sticky.position || { left: 0, top: 0 };
3758
- const dragging =
3759
- this.isDragging && this.currentDragItem?.uuid === uuid;
3760
- const selected = this.selectedItems.has(uuid);
3761
- return html`<temba-sticky-note
3762
- class="draggable ${dragging ? 'dragging' : ''} ${selected
3763
- ? 'selected'
3764
- : ''}"
3765
- @mousedown=${this.handleMouseDown.bind(this)}
3766
- style="left:${position.left}px; top:${position.top}px;"
3767
- uuid=${uuid}
3768
- .data=${sticky}
3769
- .dragging=${dragging}
3770
- .selected=${selected}
3771
- ></temba-sticky-note>`;
3772
- }
3773
- )}
3774
- ${this.renderSelectionBox()} ${this.renderCanvasDropPreview()}
3775
- ${this.renderConnectionPlaceholder()}
4036
+ <div
4037
+ id="canvas"
4038
+ class="${getClasses({
4039
+ 'viewing-revision': !!this.viewingRevision,
4040
+ 'read-only-connections':
4041
+ !!this.viewingRevision || this.isTranslating
4042
+ })}"
4043
+ >
4044
+ ${this.definition
4045
+ ? repeat(
4046
+ [...this.definition.nodes].sort((a, b) =>
4047
+ a.uuid.localeCompare(b.uuid)
4048
+ ),
4049
+ (node) => node.uuid,
4050
+ (node) => {
4051
+ const position = this.definition._ui?.nodes[node.uuid]
4052
+ ?.position || {
4053
+ left: 0,
4054
+ top: 0
4055
+ };
4056
+
4057
+ const dragging =
4058
+ this.isDragging &&
4059
+ this.currentDragItem?.uuid === node.uuid;
4060
+
4061
+ const selected = this.selectedItems.has(node.uuid);
4062
+
4063
+ // first node is the flow start (nodes are sorted by position)
4064
+ const isFlowStart =
4065
+ this.definition.nodes.length > 0 &&
4066
+ this.definition.nodes[0].uuid === node.uuid;
4067
+
4068
+ return html`<temba-flow-node
4069
+ class="draggable ${dragging
4070
+ ? 'dragging'
4071
+ : ''} ${selected ? 'selected' : ''} ${isFlowStart
4072
+ ? 'flow-start'
4073
+ : ''}"
4074
+ @mousedown=${this.handleMouseDown.bind(this)}
4075
+ uuid=${node.uuid}
4076
+ data-node-uuid=${node.uuid}
4077
+ style="left:${position.left}px; top:${position.top}px;transition: all 0.2s ease-in-out;"
4078
+ .plumber=${this.plumber}
4079
+ .node=${node}
4080
+ .ui=${this.definition._ui.nodes[node.uuid]}
4081
+ @temba-node-deleted=${(event) => {
4082
+ this.deleteNodes([event.detail.uuid]);
4083
+ }}
4084
+ ></temba-flow-node>`;
4085
+ }
4086
+ )
4087
+ : html`<temba-loading></temba-loading>`}
4088
+ ${repeat(
4089
+ Object.entries(stickies),
4090
+ ([uuid]) => uuid,
4091
+ ([uuid, sticky]) => {
4092
+ const position = sticky.position || { left: 0, top: 0 };
4093
+ const dragging =
4094
+ this.isDragging && this.currentDragItem?.uuid === uuid;
4095
+ const selected = this.selectedItems.has(uuid);
4096
+ return html`<temba-sticky-note
4097
+ class="draggable ${dragging ? 'dragging' : ''} ${selected
4098
+ ? 'selected'
4099
+ : ''}"
4100
+ @mousedown=${this.handleMouseDown.bind(this)}
4101
+ style="left:${position.left}px; top:${position.top}px;"
4102
+ uuid=${uuid}
4103
+ .data=${sticky}
4104
+ .dragging=${dragging}
4105
+ .selected=${selected}
4106
+ ></temba-sticky-note>`;
4107
+ }
4108
+ )}
4109
+ ${this.renderSelectionBox()} ${this.renderCanvasDropPreview()}
4110
+ ${this.renderConnectionPlaceholder()}
4111
+ </div>
3776
4112
  </div>
3777
4113
  </div>
4114
+ <div class="save-indicator ${this.isSaving ? 'visible' : ''}">
4115
+ <temba-loading units="3" size="8"></temba-loading>
4116
+ </div>
3778
4117
  </div>
3779
4118
 
3780
4119
  ${this.editingNode || this.editingAction
@@ -3782,6 +4121,7 @@ export class Editor extends RapidElement {
3782
4121
  .node=${this.editingNode}
3783
4122
  .nodeUI=${this.editingNodeUI}
3784
4123
  .action=${this.editingAction}
4124
+ .dialogOrigin=${this.dialogOrigin}
3785
4125
  @temba-node-saved=${(e: CustomEvent) =>
3786
4126
  this.handleNodeSaved(e.detail.node, e.detail.uiConfig)}
3787
4127
  @temba-action-saved=${(e: CustomEvent) =>
@@ -3797,6 +4137,7 @@ export class Editor extends RapidElement {
3797
4137
  .features=${this.features}
3798
4138
  ></temba-node-type-selector>`
3799
4139
  : ''}
3800
- ${this.renderRevisionsTab()} ${this.renderLocalizationTab()} `;
4140
+ ${this.renderIssuesTab()} ${this.renderRevisionsTab()}
4141
+ ${this.renderLocalizationTab()} `;
3801
4142
  }
3802
4143
  }