@nyaruka/temba-components 0.140.0 → 0.141.1

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 (297) hide show
  1. package/.lintstagedrc.js +10 -0
  2. package/CHANGELOG.md +22 -0
  3. package/dist/locales/es.js +5 -5
  4. package/dist/locales/es.js.map +1 -1
  5. package/dist/locales/fr.js +5 -5
  6. package/dist/locales/fr.js.map +1 -1
  7. package/dist/locales/locale-codes.js +11 -2
  8. package/dist/locales/locale-codes.js.map +1 -1
  9. package/dist/locales/pt.js +5 -5
  10. package/dist/locales/pt.js.map +1 -1
  11. package/dist/temba-components.js +263 -156
  12. package/dist/temba-components.js.map +1 -1
  13. package/out-tsc/src/display/FloatingTab.js +1 -1
  14. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  15. package/out-tsc/src/flow/CanvasNode.js +1 -1
  16. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  17. package/out-tsc/src/flow/Editor.js +239 -43
  18. package/out-tsc/src/flow/Editor.js.map +1 -1
  19. package/out-tsc/src/flow/Plumber.js +61 -14
  20. package/out-tsc/src/flow/Plumber.js.map +1 -1
  21. package/out-tsc/src/flow/actions/add_contact_groups.js +4 -1
  22. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
  23. package/out-tsc/src/flow/actions/add_input_labels.js +4 -1
  24. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  25. package/out-tsc/src/flow/actions/remove_contact_groups.js +6 -1
  26. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
  27. package/out-tsc/src/flow/actions/send_broadcast.js +6 -2
  28. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  29. package/out-tsc/src/flow/actions/set_contact_channel.js +13 -0
  30. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  31. package/out-tsc/src/flow/actions/set_contact_status.js +7 -5
  32. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  33. package/out-tsc/src/flow/actions/start_session.js +10 -3
  34. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  35. package/out-tsc/src/flow/nodes/split_by_contact_field.js +18 -5
  36. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -1
  37. package/out-tsc/src/flow/nodes/split_by_expression.js +1 -1
  38. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -1
  39. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +0 -1
  40. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  41. package/out-tsc/src/flow/nodes/split_by_random.js +0 -1
  42. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
  43. package/out-tsc/src/flow/nodes/split_by_run_result.js +10 -4
  44. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
  45. package/out-tsc/src/flow/nodes/wait_for_digits.js +1 -1
  46. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
  47. package/out-tsc/src/flow/nodes/wait_for_response.js +1 -1
  48. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  49. package/out-tsc/src/form/FieldRenderer.js +7 -0
  50. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  51. package/out-tsc/src/layout/Dialog.js +0 -1
  52. package/out-tsc/src/layout/Dialog.js.map +1 -1
  53. package/out-tsc/src/layout/Modax.js +20 -2
  54. package/out-tsc/src/layout/Modax.js.map +1 -1
  55. package/out-tsc/src/list/ContentMenu.js +14 -1
  56. package/out-tsc/src/list/ContentMenu.js.map +1 -1
  57. package/out-tsc/src/live/ContactChat.js +10 -1
  58. package/out-tsc/src/live/ContactChat.js.map +1 -1
  59. package/out-tsc/src/live/TembaChart.js.map +1 -1
  60. package/out-tsc/src/locales/es.js +5 -5
  61. package/out-tsc/src/locales/es.js.map +1 -1
  62. package/out-tsc/src/locales/fr.js +5 -5
  63. package/out-tsc/src/locales/fr.js.map +1 -1
  64. package/out-tsc/src/locales/locale-codes.js +11 -2
  65. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  66. package/out-tsc/src/locales/pt.js +5 -5
  67. package/out-tsc/src/locales/pt.js.map +1 -1
  68. package/out-tsc/src/simulator/Simulator.js +11 -0
  69. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  70. package/out-tsc/src/store/AppState.js +13 -0
  71. package/out-tsc/src/store/AppState.js.map +1 -1
  72. package/out-tsc/src/version.js +9 -0
  73. package/out-tsc/src/version.js.map +1 -0
  74. package/out-tsc/test/actions/add_contact_groups.test.js +35 -0
  75. package/out-tsc/test/actions/add_contact_groups.test.js.map +1 -1
  76. package/out-tsc/test/actions/add_input_labels.test.js +53 -0
  77. package/out-tsc/test/actions/add_input_labels.test.js.map +1 -0
  78. package/out-tsc/test/actions/enter_flow.test.js +71 -0
  79. package/out-tsc/test/actions/enter_flow.test.js.map +1 -0
  80. package/out-tsc/test/actions/remove_contact_groups.test.js +24 -0
  81. package/out-tsc/test/actions/remove_contact_groups.test.js.map +1 -1
  82. package/out-tsc/test/actions/send_broadcast.test.js +41 -0
  83. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
  84. package/out-tsc/test/actions/set_contact_channel.test.js +67 -0
  85. package/out-tsc/test/actions/set_contact_channel.test.js.map +1 -0
  86. package/out-tsc/test/actions/set_contact_field.test.js +52 -0
  87. package/out-tsc/test/actions/set_contact_field.test.js.map +1 -0
  88. package/out-tsc/test/actions/set_contact_language.test.js +39 -0
  89. package/out-tsc/test/actions/set_contact_language.test.js.map +1 -0
  90. package/out-tsc/test/actions/set_contact_name.test.js +28 -0
  91. package/out-tsc/test/actions/set_contact_name.test.js.map +1 -0
  92. package/out-tsc/test/actions/set_contact_status.test.js +44 -0
  93. package/out-tsc/test/actions/set_contact_status.test.js.map +1 -0
  94. package/out-tsc/test/actions/set_run_result.test.js +47 -0
  95. package/out-tsc/test/actions/set_run_result.test.js.map +1 -0
  96. package/out-tsc/test/actions/start_session.test.js +76 -0
  97. package/out-tsc/test/actions/start_session.test.js.map +1 -1
  98. package/out-tsc/test/nodes/split_by_contact_field.test.js +50 -0
  99. package/out-tsc/test/nodes/split_by_contact_field.test.js.map +1 -1
  100. package/out-tsc/test/nodes/split_by_run_result.test.js +82 -0
  101. package/out-tsc/test/nodes/split_by_run_result.test.js.map +1 -1
  102. package/out-tsc/test/nodes/split_by_ticket.test.js +139 -0
  103. package/out-tsc/test/nodes/split_by_ticket.test.js.map +1 -0
  104. package/out-tsc/test/nodes/split_by_webhook.test.js +111 -0
  105. package/out-tsc/test/nodes/split_by_webhook.test.js.map +1 -0
  106. package/out-tsc/test/temba-contact-chat.test.js +12 -0
  107. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  108. package/out-tsc/test/temba-flow-editor.test.js +206 -0
  109. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  110. package/out-tsc/test/temba-flow-plumber.test.js +19 -0
  111. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  112. package/out-tsc/test/temba-select.test.js +4 -1
  113. package/out-tsc/test/temba-select.test.js.map +1 -1
  114. package/out-tsc/test/utils.test.js +4 -2
  115. package/out-tsc/test/utils.test.js.map +1 -1
  116. package/package.json +3 -9
  117. package/rollup.components.mjs +7 -1
  118. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  119. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  120. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  121. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  122. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  123. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  124. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  125. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  126. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  127. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  128. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  129. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  130. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  131. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  132. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  133. package/screenshots/truth/actions/add_input_labels/editor/multiple-labels.png +0 -0
  134. package/screenshots/truth/actions/add_input_labels/editor/single-label.png +0 -0
  135. package/screenshots/truth/actions/add_input_labels/render/multiple-labels.png +0 -0
  136. package/screenshots/truth/actions/add_input_labels/render/single-label.png +0 -0
  137. package/screenshots/truth/actions/enter_flow/editor/basic-flow.png +0 -0
  138. package/screenshots/truth/actions/enter_flow/editor/long-flow-name.png +0 -0
  139. package/screenshots/truth/actions/enter_flow/render/basic-flow.png +0 -0
  140. package/screenshots/truth/actions/enter_flow/render/long-flow-name.png +0 -0
  141. package/screenshots/truth/actions/play_audio/render/expression-url.png +0 -0
  142. package/screenshots/truth/actions/play_audio/render/static-url.png +0 -0
  143. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  144. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  145. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  146. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  147. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  148. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  149. package/screenshots/truth/actions/say_msg/render/multiline-text.png +0 -0
  150. package/screenshots/truth/actions/say_msg/render/simple-text.png +0 -0
  151. package/screenshots/truth/actions/say_msg/render/text-with-audio-url.png +0 -0
  152. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  153. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  154. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  155. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  156. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  157. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  158. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  159. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  160. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  161. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  162. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  163. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  164. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  165. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  166. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  167. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  168. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  169. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  170. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  171. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  172. package/screenshots/truth/actions/set_contact_channel/editor/sms-channel.png +0 -0
  173. package/screenshots/truth/actions/set_contact_channel/editor/whatsapp-channel.png +0 -0
  174. package/screenshots/truth/actions/set_contact_channel/render/sms-channel.png +0 -0
  175. package/screenshots/truth/actions/set_contact_channel/render/whatsapp-channel.png +0 -0
  176. package/screenshots/truth/actions/set_contact_field/editor/clear-value.png +0 -0
  177. package/screenshots/truth/actions/set_contact_field/editor/set-value.png +0 -0
  178. package/screenshots/truth/actions/set_contact_field/render/clear-value.png +0 -0
  179. package/screenshots/truth/actions/set_contact_field/render/set-value.png +0 -0
  180. package/screenshots/truth/actions/set_contact_language/editor/english.png +0 -0
  181. package/screenshots/truth/actions/set_contact_language/editor/french.png +0 -0
  182. package/screenshots/truth/actions/set_contact_language/render/english.png +0 -0
  183. package/screenshots/truth/actions/set_contact_language/render/french.png +0 -0
  184. package/screenshots/truth/actions/set_contact_name/editor/expression-name.png +0 -0
  185. package/screenshots/truth/actions/set_contact_name/editor/static-name.png +0 -0
  186. package/screenshots/truth/actions/set_contact_name/render/expression-name.png +0 -0
  187. package/screenshots/truth/actions/set_contact_name/render/static-name.png +0 -0
  188. package/screenshots/truth/actions/set_contact_status/editor/active.png +0 -0
  189. package/screenshots/truth/actions/set_contact_status/editor/archived.png +0 -0
  190. package/screenshots/truth/actions/set_contact_status/editor/blocked.png +0 -0
  191. package/screenshots/truth/actions/set_contact_status/render/active.png +0 -0
  192. package/screenshots/truth/actions/set_contact_status/render/archived.png +0 -0
  193. package/screenshots/truth/actions/set_contact_status/render/blocked.png +0 -0
  194. package/screenshots/truth/actions/set_run_result/editor/expression-value.png +0 -0
  195. package/screenshots/truth/actions/set_run_result/editor/with-category.png +0 -0
  196. package/screenshots/truth/actions/set_run_result/render/expression-value.png +0 -0
  197. package/screenshots/truth/actions/set_run_result/render/with-category.png +0 -0
  198. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  199. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  200. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  201. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  202. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  203. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  204. package/screenshots/truth/editor/wait.png +0 -0
  205. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  206. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  207. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  208. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  209. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  210. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  211. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  212. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  213. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  214. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  215. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  216. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  217. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  218. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  219. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  220. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  221. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  222. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  223. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  224. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  225. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  226. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  227. package/screenshots/truth/nodes/wait_for_audio/render/basic-audio-wait.png +0 -0
  228. package/screenshots/truth/nodes/wait_for_dial/render/basic-dial.png +0 -0
  229. package/screenshots/truth/nodes/wait_for_dial/render/dial-with-limits.png +0 -0
  230. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  231. package/screenshots/truth/nodes/wait_for_digits/editor/digits-with-rules.png +0 -0
  232. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  233. package/screenshots/truth/nodes/wait_for_digits/render/digits-with-rules.png +0 -0
  234. package/screenshots/truth/nodes/wait_for_menu/render/menu-with-digits.png +0 -0
  235. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  236. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  237. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  238. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  239. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  240. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  241. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  242. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  243. package/src/display/FloatingTab.ts +1 -1
  244. package/src/flow/CanvasNode.ts +1 -1
  245. package/src/flow/Editor.ts +299 -88
  246. package/src/flow/Plumber.ts +89 -14
  247. package/src/flow/actions/add_contact_groups.ts +4 -1
  248. package/src/flow/actions/add_input_labels.ts +4 -1
  249. package/src/flow/actions/remove_contact_groups.ts +6 -1
  250. package/src/flow/actions/send_broadcast.ts +6 -2
  251. package/src/flow/actions/set_contact_channel.ts +13 -1
  252. package/src/flow/actions/set_contact_status.ts +7 -5
  253. package/src/flow/actions/start_session.ts +10 -3
  254. package/src/flow/nodes/split_by_contact_field.ts +16 -5
  255. package/src/flow/nodes/split_by_expression.ts +1 -1
  256. package/src/flow/nodes/split_by_llm_categorize.ts +0 -1
  257. package/src/flow/nodes/split_by_random.ts +0 -1
  258. package/src/flow/nodes/split_by_run_result.ts +10 -4
  259. package/src/flow/nodes/wait_for_digits.ts +2 -1
  260. package/src/flow/nodes/wait_for_response.ts +1 -1
  261. package/src/form/FieldRenderer.ts +7 -0
  262. package/src/layout/Dialog.ts +0 -1
  263. package/src/layout/Modax.ts +19 -2
  264. package/src/list/ContentMenu.ts +15 -1
  265. package/src/live/ContactChat.ts +10 -1
  266. package/src/live/TembaChart.ts +1 -1
  267. package/src/locales/es.ts +18 -13
  268. package/src/locales/fr.ts +18 -13
  269. package/src/locales/locale-codes.ts +11 -2
  270. package/src/locales/pt.ts +18 -13
  271. package/src/simulator/Simulator.ts +12 -0
  272. package/src/store/AppState.ts +15 -0
  273. package/src/store/flow-definition.d.ts +1 -0
  274. package/src/version.ts +10 -0
  275. package/test/actions/add_contact_groups.test.ts +38 -0
  276. package/test/actions/add_input_labels.test.ts +67 -0
  277. package/test/actions/enter_flow.test.ts +88 -0
  278. package/test/actions/remove_contact_groups.test.ts +29 -0
  279. package/test/actions/send_broadcast.test.ts +44 -0
  280. package/test/actions/set_contact_channel.test.ts +88 -0
  281. package/test/actions/set_contact_field.test.ts +68 -0
  282. package/test/actions/set_contact_language.test.ts +55 -0
  283. package/test/actions/set_contact_name.test.ts +39 -0
  284. package/test/actions/set_contact_status.test.ts +64 -0
  285. package/test/actions/set_run_result.test.ts +61 -0
  286. package/test/actions/start_session.test.ts +82 -0
  287. package/test/nodes/split_by_contact_field.test.ts +59 -0
  288. package/test/nodes/split_by_run_result.test.ts +100 -0
  289. package/test/nodes/split_by_ticket.test.ts +157 -0
  290. package/test/nodes/split_by_webhook.test.ts +131 -0
  291. package/test/temba-contact-chat.test.ts +17 -0
  292. package/test/temba-flow-editor.test.ts +264 -0
  293. package/test/temba-flow-plumber.test.ts +62 -0
  294. package/test/temba-select.test.ts +6 -1
  295. package/test/utils.test.ts +4 -2
  296. package/web-dev-server.config.mjs +5 -1
  297. package/web-test-runner.config.mjs +4 -1
@@ -19,7 +19,14 @@ import {
19
19
  import { RapidElement } from '../RapidElement';
20
20
  import { repeat } from 'lit-html/directives/repeat.js';
21
21
  import { CustomEventType, Workspace } from '../interfaces';
22
- import { generateUUID, postJSON, fetchResults, getClasses } from '../utils';
22
+ import {
23
+ generateUUID,
24
+ postJSON,
25
+ fetchResults,
26
+ getClasses,
27
+ WebResponse
28
+ } from '../utils';
29
+ import { TEMBA_COMPONENTS_VERSION } from '../version';
23
30
  import {
24
31
  formatIssueMessage,
25
32
  getNodeBounds,
@@ -72,7 +79,7 @@ export function findNodeForExit(
72
79
  return null;
73
80
  }
74
81
 
75
- const SAVE_QUIET_TIME = 500;
82
+ const SAVE_QUIET_TIME = 2000;
76
83
 
77
84
  export interface DraggableItem {
78
85
  uuid: string;
@@ -260,6 +267,12 @@ export class Editor extends RapidElement {
260
267
  @state()
261
268
  private isLoadingRevisions = false;
262
269
 
270
+ @state()
271
+ private isSaving = false;
272
+
273
+ @state()
274
+ private saveError: string | null = null;
275
+
263
276
  private preRevertState: {
264
277
  definition: FlowDefinition;
265
278
  dirtyDate: Date | null;
@@ -361,6 +374,13 @@ export class Editor extends RapidElement {
361
374
 
362
375
  static get styles() {
363
376
  return css`
377
+ #editor-container {
378
+ position: relative;
379
+ flex: 1;
380
+ display: flex;
381
+ min-height: 0;
382
+ }
383
+
364
384
  #editor {
365
385
  overflow: scroll;
366
386
  flex: 1;
@@ -839,6 +859,71 @@ export class Editor extends RapidElement {
839
859
  color: tomato;
840
860
  flex-shrink: 0;
841
861
  }
862
+
863
+ .empty-flow {
864
+ position: sticky;
865
+ top: 80px;
866
+ left: 0;
867
+ right: 0;
868
+ height: 0;
869
+ display: flex;
870
+ justify-content: center;
871
+ pointer-events: none;
872
+ z-index: 50;
873
+ }
874
+
875
+ .empty-flow-content {
876
+ display: flex;
877
+ flex-direction: column;
878
+ align-items: center;
879
+ gap: 16px;
880
+ text-align: center;
881
+ pointer-events: auto;
882
+ }
883
+
884
+ .empty-flow-title {
885
+ font-size: 18px;
886
+ font-weight: 600;
887
+ color: #374151;
888
+ }
889
+
890
+ .empty-flow-description {
891
+ font-size: 14px;
892
+ color: #6b7280;
893
+ max-width: 320px;
894
+ line-height: 1.5;
895
+ }
896
+
897
+ .empty-flow-button {
898
+ background: var(--color-primary-dark);
899
+ border: none;
900
+ color: #fff;
901
+ padding: 10px 20px;
902
+ border-radius: var(--curvature);
903
+ font-size: 14px;
904
+ font-weight: 600;
905
+ cursor: pointer;
906
+ transition: opacity 0.2s ease;
907
+ }
908
+
909
+ .empty-flow-button:hover {
910
+ opacity: 0.9;
911
+ }
912
+
913
+ .save-indicator {
914
+ position: absolute;
915
+ top: 8px;
916
+ right: 16px;
917
+ padding: 6px 10px;
918
+ z-index: 10000;
919
+ pointer-events: none;
920
+ opacity: 0;
921
+ transition: opacity 0.15s ease-in-out;
922
+ }
923
+
924
+ .save-indicator.visible {
925
+ opacity: 1;
926
+ }
842
927
  `;
843
928
  }
844
929
 
@@ -854,6 +939,7 @@ export class Editor extends RapidElement {
854
939
  this.setupGlobalEventListeners();
855
940
  if (changes.has('flow')) {
856
941
  getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
942
+ this.fetchRevisions();
857
943
  }
858
944
 
859
945
  this.plumber.on('connection:drag', (connection: any) => {
@@ -1018,10 +1104,16 @@ export class Editor extends RapidElement {
1018
1104
 
1019
1105
  if (changes.has('dirtyDate')) {
1020
1106
  if (this.dirtyDate) {
1107
+ this.isSaving = true;
1021
1108
  this.debouncedSave();
1022
1109
  }
1023
1110
  }
1024
1111
 
1112
+ if (changes.has('saveError') && this.saveError) {
1113
+ this.showSaveErrorDialog(this.saveError);
1114
+ this.saveError = null;
1115
+ }
1116
+
1025
1117
  if (changes.has('languageCode')) {
1026
1118
  this.translationCache.clear();
1027
1119
  }
@@ -1065,12 +1157,30 @@ export class Editor extends RapidElement {
1065
1157
  }, SAVE_QUIET_TIME);
1066
1158
  }
1067
1159
 
1160
+ private definitionForSave(definition: FlowDefinition): FlowDefinition {
1161
+ return {
1162
+ ...definition,
1163
+ _ui: {
1164
+ ...definition._ui,
1165
+ editor: TEMBA_COMPONENTS_VERSION
1166
+ }
1167
+ };
1168
+ }
1169
+
1068
1170
  private saveChanges(definitionOverride?: FlowDefinition): Promise<void> {
1069
- const definition = definitionOverride || this.definition;
1070
- // post the flow definition to the server
1171
+ const definition = this.definitionForSave(
1172
+ definitionOverride || this.definition
1173
+ );
1174
+ this.isSaving = true;
1175
+
1071
1176
  return getStore()
1072
1177
  .postJSON(`/flow/revisions/${this.flow}/`, definition)
1073
1178
  .then((response) => {
1179
+ if (response.status < 200 || response.status >= 300) {
1180
+ this.saveError = this.extractErrorMessage(response);
1181
+ return;
1182
+ }
1183
+
1074
1184
  // Update flow info and revision with the response data
1075
1185
  if (response.json) {
1076
1186
  const state = getStore().getState();
@@ -1083,17 +1193,58 @@ export class Editor extends RapidElement {
1083
1193
  state.setRevision(response.json.revision.revision);
1084
1194
  }
1085
1195
 
1086
- // if the revisions window is open, refresh the list
1087
- if (!this.revisionsWindowHidden) {
1088
- this.fetchRevisions();
1089
- }
1196
+ // Refresh revisions list so the tab visibility stays up to date
1197
+ this.fetchRevisions();
1090
1198
  }
1199
+
1200
+ getStore().getState().setDirtyDate(null);
1091
1201
  })
1092
1202
  .catch((error) => {
1093
1203
  console.error('Failed to save flow:', error);
1204
+ if (error instanceof Response) {
1205
+ this.saveError = `Server error (${error.status}). Your changes have not been saved.`;
1206
+ } else {
1207
+ this.saveError =
1208
+ 'Unable to reach the server. Please check your connection and try again.';
1209
+ }
1210
+ })
1211
+ .finally(() => {
1212
+ this.isSaving = false;
1094
1213
  });
1214
+ }
1095
1215
 
1096
- getStore().getState().setDirtyDate(null);
1216
+ private extractErrorMessage(response: WebResponse): string {
1217
+ if (response.json) {
1218
+ if (typeof response.json.detail === 'string') {
1219
+ return response.json.detail;
1220
+ }
1221
+ if (typeof response.json.error === 'string') {
1222
+ return response.json.error;
1223
+ }
1224
+ if (typeof response.json.description === 'string') {
1225
+ return response.json.description;
1226
+ }
1227
+ }
1228
+ return `Save failed with status ${response.status}.`;
1229
+ }
1230
+
1231
+ private showSaveErrorDialog(message: string): void {
1232
+ const dialog = document.createElement('temba-dialog') as Dialog;
1233
+ dialog.header = 'Save Failed';
1234
+ dialog.primaryButtonName = '';
1235
+ dialog.cancelButtonName = 'Dismiss';
1236
+
1237
+ const content = document.createElement('div');
1238
+ content.style.cssText = 'padding: 20px; font-size: 14px; line-height: 1.5;';
1239
+ content.textContent = message;
1240
+ dialog.appendChild(content);
1241
+
1242
+ document.body.appendChild(dialog);
1243
+ dialog.open = true;
1244
+
1245
+ dialog.addEventListener('temba-dialog-hidden', () => {
1246
+ document.body.removeChild(dialog);
1247
+ });
1097
1248
  }
1098
1249
 
1099
1250
  private startActivityFetching(): void {
@@ -1129,6 +1280,11 @@ export class Editor extends RapidElement {
1129
1280
  }
1130
1281
  const state = store.getState();
1131
1282
  state.fetchActivity(activityEndpoint).then(() => {
1283
+ // Guard against responses arriving after the editor is disconnected
1284
+ if (!this.isConnected) {
1285
+ return;
1286
+ }
1287
+
1132
1288
  // Schedule next fetch with exponential backoff (max 5 minutes)
1133
1289
  this.activityInterval = Math.min(60000 * 5, this.activityInterval + 100);
1134
1290
 
@@ -1165,6 +1321,10 @@ export class Editor extends RapidElement {
1165
1321
  if (canvas) {
1166
1322
  canvas.removeEventListener('contextmenu', this.boundCanvasContextMenu);
1167
1323
  }
1324
+
1325
+ // Clear all flow-specific data from the store so stale data
1326
+ // isn't briefly visible when a different flow is opened.
1327
+ zustand.getState().clearFlowData();
1168
1328
  }
1169
1329
 
1170
1330
  private setupGlobalEventListeners(): void {
@@ -2023,6 +2183,28 @@ export class Editor extends RapidElement {
2023
2183
  }
2024
2184
  }
2025
2185
 
2186
+ private handleEmptyFlowClick(event: MouseEvent): void {
2187
+ const editor = this.querySelector('#editor') as HTMLElement;
2188
+ if (!editor) return;
2189
+
2190
+ // Scroll to top-left
2191
+ editor.scrollTo({ left: 0, top: 0, behavior: 'smooth' });
2192
+
2193
+ // Place node at top-left of the canvas
2194
+ const nodeLeft = 0;
2195
+ const nodeTop = 0;
2196
+
2197
+ const canvasMenu = this.querySelector('temba-canvas-menu') as CanvasMenu;
2198
+ if (canvasMenu) {
2199
+ const button = event.currentTarget as HTMLElement;
2200
+ const rect = button.getBoundingClientRect();
2201
+ const menuWidth = 265;
2202
+ const menuX = rect.left + rect.width / 2 - menuWidth / 2;
2203
+ const menuY = rect.bottom + 8;
2204
+ canvasMenu.show(menuX, menuY, { x: nodeLeft, y: nodeTop }, false);
2205
+ }
2206
+ }
2207
+
2026
2208
  private handleCanvasMenuSelection(event: CustomEvent): void {
2027
2209
  const selection = event.detail as CanvasMenuSelection;
2028
2210
  const store = getStore();
@@ -3424,7 +3606,7 @@ export class Editor extends RapidElement {
3424
3606
  }
3425
3607
 
3426
3608
  private renderIssuesTab(): TemplateResult | string {
3427
- if (!this.flowIssues?.length) return '';
3609
+ if (!this.flowIssues?.length || !this.revisionsWindowHidden) return '';
3428
3610
  return html`
3429
3611
  <temba-floating-tab
3430
3612
  id="issues-tab"
@@ -3469,6 +3651,7 @@ export class Editor extends RapidElement {
3469
3651
  }
3470
3652
 
3471
3653
  private renderRevisionsTab(): TemplateResult | string {
3654
+ if (this.revisions.length <= 1) return '';
3472
3655
  return html`
3473
3656
  <temba-floating-tab
3474
3657
  id="revisions-tab"
@@ -3762,6 +3945,8 @@ export class Editor extends RapidElement {
3762
3945
  }
3763
3946
 
3764
3947
  private renderLocalizationTab(): TemplateResult | string {
3948
+ if (!this.revisionsWindowHidden) return '';
3949
+ if (this.definition?.nodes.length === 0) return '';
3765
3950
  const languages = this.getLocalizationLanguages();
3766
3951
  if (!languages.length) {
3767
3952
  return html``;
@@ -3834,88 +4019,114 @@ export class Editor extends RapidElement {
3834
4019
  return html`${style} ${this.renderIssuesWindow()}
3835
4020
  ${this.renderRevisionsWindow()} ${this.renderLocalizationWindow()}
3836
4021
  ${this.renderAutoTranslateDialog()}
3837
- <div id="editor">
3838
- <div
3839
- id="grid"
3840
- class="${this.viewingRevision ? 'viewing-revision' : ''}"
3841
- style="min-width:100%;width:${this.canvasSize.width}px; height:${this
3842
- .canvasSize.height}px"
3843
- >
4022
+ <div id="editor-container">
4023
+ <div id="editor">
4024
+ ${this.definition &&
4025
+ this.definition.nodes.length === 0 &&
4026
+ !this.isReadOnly()
4027
+ ? html`<div class="empty-flow">
4028
+ <div class="empty-flow-content">
4029
+ <div class="empty-flow-title">This flow is empty</div>
4030
+ <div class="empty-flow-description">
4031
+ Get started by adding your first action or split to define
4032
+ how this flow will work.
4033
+ </div>
4034
+ <button
4035
+ class="empty-flow-button"
4036
+ @click=${this.handleEmptyFlowClick}
4037
+ >
4038
+ Add first step
4039
+ </button>
4040
+ </div>
4041
+ </div>`
4042
+ : ''}
3844
4043
  <div
3845
- id="canvas"
3846
- class="${getClasses({
3847
- 'viewing-revision': !!this.viewingRevision,
3848
- 'read-only-connections':
3849
- !!this.viewingRevision || this.isTranslating
3850
- })}"
4044
+ id="grid"
4045
+ class="${this.viewingRevision ? 'viewing-revision' : ''}"
4046
+ style="min-width:100%;width:${this.canvasSize
4047
+ .width}px; height:${this.canvasSize.height}px"
3851
4048
  >
3852
- ${this.definition
3853
- ? repeat(
3854
- [...this.definition.nodes].sort((a, b) =>
3855
- a.uuid.localeCompare(b.uuid)
3856
- ),
3857
- (node) => node.uuid,
3858
- (node) => {
3859
- const position = this.definition._ui?.nodes[node.uuid]
3860
- ?.position || {
3861
- left: 0,
3862
- top: 0
3863
- };
3864
-
3865
- const dragging =
3866
- this.isDragging &&
3867
- this.currentDragItem?.uuid === node.uuid;
3868
-
3869
- const selected = this.selectedItems.has(node.uuid);
3870
-
3871
- // first node is the flow start (nodes are sorted by position)
3872
- const isFlowStart =
3873
- this.definition.nodes.length > 0 &&
3874
- this.definition.nodes[0].uuid === node.uuid;
3875
-
3876
- return html`<temba-flow-node
3877
- class="draggable ${dragging ? 'dragging' : ''} ${selected
3878
- ? 'selected'
3879
- : ''} ${isFlowStart ? 'flow-start' : ''}"
3880
- @mousedown=${this.handleMouseDown.bind(this)}
3881
- uuid=${node.uuid}
3882
- data-node-uuid=${node.uuid}
3883
- style="left:${position.left}px; top:${position.top}px;transition: all 0.2s ease-in-out;"
3884
- .plumber=${this.plumber}
3885
- .node=${node}
3886
- .ui=${this.definition._ui.nodes[node.uuid]}
3887
- @temba-node-deleted=${(event) => {
3888
- this.deleteNodes([event.detail.uuid]);
3889
- }}
3890
- ></temba-flow-node>`;
3891
- }
3892
- )
3893
- : html`<temba-loading></temba-loading>`}
3894
- ${repeat(
3895
- Object.entries(stickies),
3896
- ([uuid]) => uuid,
3897
- ([uuid, sticky]) => {
3898
- const position = sticky.position || { left: 0, top: 0 };
3899
- const dragging =
3900
- this.isDragging && this.currentDragItem?.uuid === uuid;
3901
- const selected = this.selectedItems.has(uuid);
3902
- return html`<temba-sticky-note
3903
- class="draggable ${dragging ? 'dragging' : ''} ${selected
3904
- ? 'selected'
3905
- : ''}"
3906
- @mousedown=${this.handleMouseDown.bind(this)}
3907
- style="left:${position.left}px; top:${position.top}px;"
3908
- uuid=${uuid}
3909
- .data=${sticky}
3910
- .dragging=${dragging}
3911
- .selected=${selected}
3912
- ></temba-sticky-note>`;
3913
- }
3914
- )}
3915
- ${this.renderSelectionBox()} ${this.renderCanvasDropPreview()}
3916
- ${this.renderConnectionPlaceholder()}
4049
+ <div
4050
+ id="canvas"
4051
+ class="${getClasses({
4052
+ 'viewing-revision': !!this.viewingRevision,
4053
+ 'read-only-connections':
4054
+ !!this.viewingRevision || this.isTranslating
4055
+ })}"
4056
+ >
4057
+ ${this.definition
4058
+ ? repeat(
4059
+ [...this.definition.nodes].sort((a, b) =>
4060
+ a.uuid.localeCompare(b.uuid)
4061
+ ),
4062
+ (node) => node.uuid,
4063
+ (node) => {
4064
+ const position = this.definition._ui?.nodes[node.uuid]
4065
+ ?.position || {
4066
+ left: 0,
4067
+ top: 0
4068
+ };
4069
+
4070
+ const dragging =
4071
+ this.isDragging &&
4072
+ this.currentDragItem?.uuid === node.uuid;
4073
+
4074
+ const selected = this.selectedItems.has(node.uuid);
4075
+
4076
+ // first node is the flow start (nodes are sorted by position)
4077
+ const isFlowStart =
4078
+ this.definition.nodes.length > 0 &&
4079
+ this.definition.nodes[0].uuid === node.uuid;
4080
+
4081
+ return html`<temba-flow-node
4082
+ class="draggable ${dragging
4083
+ ? 'dragging'
4084
+ : ''} ${selected ? 'selected' : ''} ${isFlowStart
4085
+ ? 'flow-start'
4086
+ : ''}"
4087
+ @mousedown=${this.handleMouseDown.bind(this)}
4088
+ uuid=${node.uuid}
4089
+ data-node-uuid=${node.uuid}
4090
+ style="left:${position.left}px; top:${position.top}px;transition: all 0.2s ease-in-out;"
4091
+ .plumber=${this.plumber}
4092
+ .node=${node}
4093
+ .ui=${this.definition._ui.nodes[node.uuid]}
4094
+ @temba-node-deleted=${(event) => {
4095
+ this.deleteNodes([event.detail.uuid]);
4096
+ }}
4097
+ ></temba-flow-node>`;
4098
+ }
4099
+ )
4100
+ : html`<temba-loading></temba-loading>`}
4101
+ ${repeat(
4102
+ Object.entries(stickies),
4103
+ ([uuid]) => uuid,
4104
+ ([uuid, sticky]) => {
4105
+ const position = sticky.position || { left: 0, top: 0 };
4106
+ const dragging =
4107
+ this.isDragging && this.currentDragItem?.uuid === uuid;
4108
+ const selected = this.selectedItems.has(uuid);
4109
+ return html`<temba-sticky-note
4110
+ class="draggable ${dragging ? 'dragging' : ''} ${selected
4111
+ ? 'selected'
4112
+ : ''}"
4113
+ @mousedown=${this.handleMouseDown.bind(this)}
4114
+ style="left:${position.left}px; top:${position.top}px;"
4115
+ uuid=${uuid}
4116
+ .data=${sticky}
4117
+ .dragging=${dragging}
4118
+ .selected=${selected}
4119
+ ></temba-sticky-note>`;
4120
+ }
4121
+ )}
4122
+ ${this.renderSelectionBox()} ${this.renderCanvasDropPreview()}
4123
+ ${this.renderConnectionPlaceholder()}
4124
+ </div>
3917
4125
  </div>
3918
4126
  </div>
4127
+ <div class="save-indicator ${this.isSaving ? 'visible' : ''}">
4128
+ <temba-loading units="3" size="8"></temba-loading>
4129
+ </div>
3919
4130
  </div>
3920
4131
 
3921
4132
  ${this.editingNode || this.editingAction