@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
@@ -20,9 +20,10 @@ import {
20
20
  } from './types';
21
21
  import { CustomEventType } from '../interfaces';
22
22
  import { generateUUID } from '../utils';
23
+ import { formatIssueMessage } from './utils';
23
24
  import { FieldRenderer } from '../form/FieldRenderer';
24
25
  import { renderMarkdownInline } from '../markdown';
25
- import { AppState, fromStore, zustand } from '../store/AppState';
26
+ import { AppState, FlowIssue, fromStore, zustand } from '../store/AppState';
26
27
  import { getStore } from '../store/Store';
27
28
 
28
29
  export class NodeEditor extends RapidElement {
@@ -55,6 +56,19 @@ export class NodeEditor extends RapidElement {
55
56
  margin-top: 15px;
56
57
  }
57
58
 
59
+ .issue-warning {
60
+ display: flex;
61
+ align-items: center;
62
+ gap: 8px;
63
+ color: var(--color-error, tomato);
64
+ font-size: 13px;
65
+ cursor: pointer;
66
+ }
67
+
68
+ .issue-warning:hover .issue-text {
69
+ text-decoration: underline;
70
+ }
71
+
58
72
  .form-actions {
59
73
  display: flex;
60
74
  gap: 10px;
@@ -112,12 +126,29 @@ export class NodeEditor extends RapidElement {
112
126
  color: var(--color-label, #777);
113
127
  }
114
128
 
129
+ .form-row-inline-label {
130
+ font-size: 20px;
131
+ font-weight: 300;
132
+ color: #ccc;
133
+ min-width: 20px;
134
+ text-align: center;
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: center;
138
+ user-select: none;
139
+ }
140
+
115
141
  .form-row-help {
116
142
  font-size: 12px;
117
143
  color: #666;
118
144
  margin-top: 6px;
119
145
  }
120
146
 
147
+ .form-text {
148
+ color: #666;
149
+ font-size: 13px;
150
+ }
151
+
121
152
  .form-group {
122
153
  border: 1px solid #e0e0e0;
123
154
  border-radius: 6px;
@@ -381,6 +412,9 @@ export class NodeEditor extends RapidElement {
381
412
  @property({ type: Object })
382
413
  nodeUI?: NodeUI;
383
414
 
415
+ @property({ attribute: false })
416
+ dialogOrigin?: { x: number; y: number };
417
+
384
418
  @property({ type: Boolean })
385
419
  isOpen: boolean = false;
386
420
 
@@ -411,6 +445,12 @@ export class NodeEditor extends RapidElement {
411
445
  @fromStore(zustand, (state: AppState) => state.flowDefinition)
412
446
  private flowDefinition!: FlowDefinition;
413
447
 
448
+ @fromStore(zustand, (state: AppState) => state.issuesByNode)
449
+ private issuesByNode!: Map<string, FlowIssue[]>;
450
+
451
+ @fromStore(zustand, (state: AppState) => state.issuesByAction)
452
+ private issuesByAction!: Map<string, FlowIssue[]>;
453
+
414
454
  connectedCallback(): void {
415
455
  super.connectedCallback();
416
456
  this.initializeFormData();
@@ -447,7 +487,13 @@ export class NodeEditor extends RapidElement {
447
487
  private initializeFormData(): void {
448
488
  const nodeConfig = this.getNodeConfig();
449
489
 
450
- if ((!nodeConfig || nodeConfig.type === 'execute_actions') && this.action) {
490
+ // Temporary: terminal nodes defer to action configs, same as execute_actions
491
+ if (
492
+ (!nodeConfig ||
493
+ nodeConfig.type === 'execute_actions' ||
494
+ nodeConfig.type === 'terminal') &&
495
+ this.action
496
+ ) {
451
497
  // Action editing mode - use action config
452
498
  const actionConfig = ACTION_CONFIG[this.action.type];
453
499
 
@@ -597,8 +643,13 @@ export class NodeEditor extends RapidElement {
597
643
  if (this.node && this.nodeUI) {
598
644
  const nodeConfig = this.getNodeConfig();
599
645
 
600
- // For execute_actions nodes, defer to action editing if an action is selected
601
- if (this.nodeUI.type === 'execute_actions' && this.action) {
646
+ // Temporary: terminal nodes defer to action configs for editing, same as execute_actions
647
+ // For execute_actions/terminal nodes, defer to action editing if an action is selected
648
+ if (
649
+ (this.nodeUI.type === 'execute_actions' ||
650
+ this.nodeUI.type === ('terminal' as any)) &&
651
+ this.action
652
+ ) {
602
653
  return ACTION_CONFIG[this.action.type] || null;
603
654
  }
604
655
 
@@ -1462,6 +1513,11 @@ export class NodeEditor extends RapidElement {
1462
1513
  } else if (fieldName && config.type === 'message-editor') {
1463
1514
  // Special handling for message editor
1464
1515
  this.handleMessageEditorChange(fieldName, e);
1516
+ } else if (fieldName && config.type === 'media') {
1517
+ // Extract URL from media picker's attachment
1518
+ const picker = e.target as any;
1519
+ const url = picker.attachments?.[0]?.url || '';
1520
+ this.handleNewFieldChange(fieldName, url);
1465
1521
  } else {
1466
1522
  // Default handling for most field types
1467
1523
  this.handleFormFieldChange(fieldName, e);
@@ -1575,6 +1631,12 @@ export class NodeEditor extends RapidElement {
1575
1631
  case 'group':
1576
1632
  return this.renderGroup(item, config, renderedFields);
1577
1633
 
1634
+ case 'spacer':
1635
+ return html``;
1636
+
1637
+ case 'text':
1638
+ return html`<div class="form-text">${item.text}</div>`;
1639
+
1578
1640
  default:
1579
1641
  return html``;
1580
1642
  }
@@ -1585,7 +1647,14 @@ export class NodeEditor extends RapidElement {
1585
1647
  config: ActionConfig | NodeConfig,
1586
1648
  renderedFields: Set<string>
1587
1649
  ): TemplateResult {
1588
- const { items, gap = '1rem', label, helpText } = rowConfig;
1650
+ const {
1651
+ items,
1652
+ gap = '1rem',
1653
+ label,
1654
+ helpText,
1655
+ inlineLabels,
1656
+ marginBottom
1657
+ } = rowConfig;
1589
1658
 
1590
1659
  // Collect all fields from this row for width calculations
1591
1660
  const fieldsInRow = this.collectFieldsFromItems(items);
@@ -1619,8 +1688,18 @@ export class NodeEditor extends RapidElement {
1619
1688
  });
1620
1689
 
1621
1690
  const rowContent = html`
1622
- <div class="form-row" style="display: flex; gap: ${gap};">
1691
+ <div
1692
+ class="form-row"
1693
+ style="display: flex; gap: ${gap};${marginBottom
1694
+ ? ` margin-bottom: ${marginBottom};`
1695
+ : ''}"
1696
+ >
1623
1697
  ${items.map((item) => {
1698
+ // Spacer items render as empty flex children
1699
+ if (typeof item !== 'string' && item.type === 'spacer') {
1700
+ return html`<div style="flex: 1 1 0;"></div>`;
1701
+ }
1702
+
1624
1703
  // Get the field name from the item
1625
1704
  const fieldName =
1626
1705
  typeof item === 'string'
@@ -1641,10 +1720,22 @@ export class NodeEditor extends RapidElement {
1641
1720
  renderedFields
1642
1721
  );
1643
1722
 
1723
+ // When inlineLabels is provided, render the label inline to the left
1724
+ const inlineLabel =
1725
+ inlineLabels && fieldName ? inlineLabels[fieldName] : null;
1726
+
1644
1727
  // Wrap in a div with flex style if we have a flex style
1645
- return flexStyle
1646
- ? html`<div style="${flexStyle}">${itemContent}</div>`
1647
- : itemContent;
1728
+ if (flexStyle) {
1729
+ return inlineLabel
1730
+ ? html`<div
1731
+ style="${flexStyle} display: flex; align-items: center; gap: 0.35rem;"
1732
+ >
1733
+ <span class="form-row-inline-label">${inlineLabel}</span>
1734
+ <div style="flex: 1 1 0; min-width: 0;">${itemContent}</div>
1735
+ </div>`
1736
+ : html`<div style="${flexStyle}">${itemContent}</div>`;
1737
+ }
1738
+ return itemContent;
1648
1739
  })}
1649
1740
  </div>
1650
1741
  `;
@@ -2038,6 +2129,40 @@ export class NodeEditor extends RapidElement {
2038
2129
  `;
2039
2130
  }
2040
2131
 
2132
+ private handleIssueClick(issue: FlowIssue): void {
2133
+ this.fireCustomEvent(CustomEventType.ShowIssue, { issue });
2134
+ }
2135
+
2136
+ private renderIssueWarnings(): TemplateResult | string {
2137
+ const issues: FlowIssue[] = [];
2138
+
2139
+ // Check for action-level issues
2140
+ if (this.action && this.issuesByAction?.has(this.action.uuid)) {
2141
+ issues.push(...this.issuesByAction.get(this.action.uuid));
2142
+ }
2143
+
2144
+ // Check for node-level issues (issues without action_uuid)
2145
+ if (this.node && this.issuesByNode?.has(this.node.uuid)) {
2146
+ issues.push(...this.issuesByNode.get(this.node.uuid));
2147
+ }
2148
+
2149
+ if (issues.length === 0) return '';
2150
+
2151
+ return html`
2152
+ ${issues.map(
2153
+ (issue) => html`
2154
+ <div
2155
+ class="issue-warning"
2156
+ @click=${() => this.handleIssueClick(issue)}
2157
+ >
2158
+ <temba-icon name="alert_warning" size="1.2"></temba-icon>
2159
+ <span class="issue-text">${formatIssueMessage(issue)}</span>
2160
+ </div>
2161
+ `
2162
+ )}
2163
+ `;
2164
+ }
2165
+
2041
2166
  render(): TemplateResult {
2042
2167
  if (!this.isOpen) {
2043
2168
  return html``;
@@ -2061,6 +2186,8 @@ export class NodeEditor extends RapidElement {
2061
2186
  <temba-dialog
2062
2187
  header="${header}"
2063
2188
  .open="${this.isOpen}"
2189
+ .originX=${this.dialogOrigin?.x ?? null}
2190
+ .originY=${this.dialogOrigin?.y ?? null}
2064
2191
  @temba-button-clicked=${this.handleDialogButtonClick}
2065
2192
  primaryButtonName="Save"
2066
2193
  cancelButtonName="Cancel"
@@ -2072,6 +2199,7 @@ export class NodeEditor extends RapidElement {
2072
2199
  ${this.getNodeConfig()?.router?.configurable
2073
2200
  ? this.renderRouterSection()
2074
2201
  : null}
2202
+ ${this.renderIssueWarnings()}
2075
2203
  </div>
2076
2204
 
2077
2205
  <div slot="gutter">${this.renderGutter()}</div>
@@ -12,6 +12,7 @@ interface ConnectionEndpoints {
12
12
  targetX: number;
13
13
  targetY: number;
14
14
  targetFace: TargetFace;
15
+ jogYOffset: number;
15
16
  }
16
17
 
17
18
  interface ConnectionInfo {
@@ -47,7 +48,8 @@ export function calculateFlowchartPath(
47
48
  stubStart = 20,
48
49
  stubEnd = 10,
49
50
  cornerRadius = 5,
50
- targetFace: TargetFace = 'top'
51
+ targetFace: TargetFace = 'top',
52
+ jogYOffset = 0
51
53
  ): string {
52
54
  const r = cornerRadius;
53
55
 
@@ -66,7 +68,11 @@ export function calculateFlowchartPath(
66
68
  // jogY is the horizontal level — must be above entryY so the
67
69
  // final approach into the node is always downward (no backtracking).
68
70
  const dirX = targetX > sourceX ? 1 : -1;
69
- const jogY = Math.max(sourceY + r, Math.min(exitY, entryY - r));
71
+ const baseJogY = Math.max(sourceY + r, Math.min(exitY, entryY - r));
72
+ const jogY = Math.max(
73
+ sourceY + r,
74
+ Math.min(baseJogY + jogYOffset, entryY - r - 3)
75
+ );
70
76
 
71
77
  // Corner 1: vertical→horizontal at jogY
72
78
  const r1 = Math.min(r, jogY - sourceY);
@@ -321,11 +327,17 @@ export class Plumber {
321
327
  });
322
328
  this.pendingConnections = [];
323
329
 
324
- // Repaint all connections that share a target with newly created ones
325
- // so anchor distribution is correct after the full batch is processed
330
+ // Repaint all connections that share a target or source with newly
331
+ // created ones so anchor distribution and jogY offsets are correct
326
332
  if (createdTargets.size > 0) {
327
- this.connections.forEach((conn, exitId) => {
333
+ const createdScopes = new Set<string>();
334
+ this.connections.forEach((conn) => {
328
335
  if (createdTargets.has(conn.toId)) {
336
+ createdScopes.add(conn.scope);
337
+ }
338
+ });
339
+ this.connections.forEach((conn, exitId) => {
340
+ if (createdTargets.has(conn.toId) || createdScopes.has(conn.scope)) {
329
341
  this.updateConnectionSVG(exitId);
330
342
  }
331
343
  });
@@ -371,7 +383,8 @@ export class Plumber {
371
383
 
372
384
  private getConnectionEndpoints(
373
385
  fromId: string,
374
- toId: string
386
+ toId: string,
387
+ scope?: string
375
388
  ): ConnectionEndpoints | null {
376
389
  const fromEl = document.getElementById(fromId);
377
390
  const toEl = document.getElementById(toId);
@@ -476,7 +489,57 @@ export class Plumber {
476
489
  : targetTop + margin + (span * (index + 0.5)) / count;
477
490
  }
478
491
 
479
- return { sourceX, sourceY, targetX, targetY, targetFace };
492
+ // Compute jogYOffset: stagger horizontal segments for sibling exits
493
+ // from the same source node so paths don't overlap
494
+ let jogYOffset = 0;
495
+ if (targetFace === 'top' && scope) {
496
+ const SIBLING_SPACING = 8;
497
+ const MAX_SPREAD = 50;
498
+
499
+ const siblings: { fromId: string; targetX: number }[] = [];
500
+ this.connections.forEach((conn) => {
501
+ if (conn.scope === scope && conn.toId !== toId) {
502
+ const connFromEl = document.getElementById(conn.fromId);
503
+ const connToEl = document.getElementById(conn.toId);
504
+ if (connFromEl && connToEl) {
505
+ const connFromRect = connFromEl.getBoundingClientRect();
506
+ const connToRect = connToEl.getBoundingClientRect();
507
+ const connSourceX =
508
+ connFromRect.left + connFromRect.width / 2 - canvasRect.left;
509
+ const connSourceY = connFromRect.bottom - canvasRect.top;
510
+ const connFace = this.determineTargetFace(
511
+ connSourceX,
512
+ connSourceY,
513
+ connToRect,
514
+ canvasRect
515
+ );
516
+ if (connFace === 'top') {
517
+ const connTargetLeft = connToRect.left - canvasRect.left;
518
+ const connTargetW = connToRect.width;
519
+ siblings.push({
520
+ fromId: conn.fromId,
521
+ targetX: connTargetLeft + connTargetW / 2
522
+ });
523
+ }
524
+ }
525
+ }
526
+ });
527
+
528
+ if (siblings.length > 0) {
529
+ siblings.push({ fromId, targetX });
530
+ siblings.sort((a, b) => a.targetX - b.targetX);
531
+ const idx = siblings.findIndex((s) => s.fromId === fromId);
532
+ const sibCount = siblings.length;
533
+ const rawSpread = (sibCount - 1) * SIBLING_SPACING;
534
+ const spacing =
535
+ rawSpread > MAX_SPREAD
536
+ ? MAX_SPREAD / (sibCount - 1)
537
+ : SIBLING_SPACING;
538
+ jogYOffset = (idx - (sibCount - 1) / 2) * spacing;
539
+ }
540
+ }
541
+
542
+ return { sourceX, sourceY, targetX, targetY, targetFace, jogYOffset };
480
543
  }
481
544
 
482
545
  // --- SVG creation and management ---
@@ -535,7 +598,8 @@ export class Plumber {
535
598
  sourceY: number,
536
599
  targetX: number,
537
600
  targetY: number,
538
- targetFace: TargetFace = 'top'
601
+ targetFace: TargetFace = 'top',
602
+ jogYOffset = 0
539
603
  ) {
540
604
  const aw = ARROW_HALF_WIDTH;
541
605
  const al = ARROW_LENGTH;
@@ -562,7 +626,8 @@ export class Plumber {
562
626
  EXIT_STUB,
563
627
  effectiveStub,
564
628
  5,
565
- targetFace
629
+ targetFace,
630
+ jogYOffset
566
631
  );
567
632
  pathEl.setAttribute('d', d);
568
633
 
@@ -596,7 +661,7 @@ export class Plumber {
596
661
  scope: string,
597
662
  toId: string
598
663
  ): boolean {
599
- const endpoints = this.getConnectionEndpoints(exitId, toId);
664
+ const endpoints = this.getConnectionEndpoints(exitId, toId, scope);
600
665
  if (!endpoints) return false;
601
666
 
602
667
  const { svgEl, pathEl, arrowEl } = this.createSVGElement();
@@ -607,7 +672,8 @@ export class Plumber {
607
672
  endpoints.sourceY,
608
673
  endpoints.targetX,
609
674
  endpoints.targetY,
610
- endpoints.targetFace
675
+ endpoints.targetFace,
676
+ endpoints.jogYOffset
611
677
  );
612
678
  this.canvas.appendChild(svgEl);
613
679
 
@@ -674,7 +740,11 @@ export class Plumber {
674
740
  const conn = this.connections.get(exitId);
675
741
  if (!conn) return;
676
742
 
677
- const endpoints = this.getConnectionEndpoints(conn.fromId, conn.toId);
743
+ const endpoints = this.getConnectionEndpoints(
744
+ conn.fromId,
745
+ conn.toId,
746
+ conn.scope
747
+ );
678
748
  if (!endpoints) return;
679
749
 
680
750
  this.updateSVGPath(
@@ -684,7 +754,8 @@ export class Plumber {
684
754
  endpoints.sourceY,
685
755
  endpoints.targetX,
686
756
  endpoints.targetY,
687
- endpoints.targetFace
757
+ endpoints.targetFace,
758
+ endpoints.jogYOffset
688
759
  );
689
760
  this.updateOverlayPosition(exitId);
690
761
  }
@@ -901,7 +972,11 @@ export class Plumber {
901
972
  const conn = this.connections.get(exitId);
902
973
  if (!overlayEl || !conn) return;
903
974
 
904
- const endpoints = this.getConnectionEndpoints(conn.fromId, conn.toId);
975
+ const endpoints = this.getConnectionEndpoints(
976
+ conn.fromId,
977
+ conn.toId,
978
+ conn.scope
979
+ );
905
980
  if (!endpoints) return;
906
981
 
907
982
  overlayEl.style.position = 'absolute';
@@ -305,7 +305,7 @@ export class StickyNote extends RapidElement {
305
305
 
306
306
  private handleBodyBlur(event: FocusEvent): void {
307
307
  const target = event.target as HTMLElement;
308
- const newBody = target.textContent || '';
308
+ const newBody = target.innerText || '';
309
309
 
310
310
  if (this.data && newBody !== this.data.body) {
311
311
  getStore()
@@ -328,7 +328,17 @@ export class StickyNote extends RapidElement {
328
328
  event.stopPropagation();
329
329
  }
330
330
 
331
- private handleKeyDown(event: KeyboardEvent): void {
331
+ private handleTitleKeyDown(event: KeyboardEvent): void {
332
+ if (event.key === 'Enter') {
333
+ event.preventDefault();
334
+ (event.target as HTMLElement).blur();
335
+ }
336
+ if (event.key === 'Escape') {
337
+ (event.target as HTMLElement).blur();
338
+ }
339
+ }
340
+
341
+ private handleBodyKeyDown(event: KeyboardEvent): void {
332
342
  if (event.key === 'Enter' && !event.shiftKey) {
333
343
  event.preventDefault();
334
344
  (event.target as HTMLElement).blur();
@@ -386,7 +396,7 @@ export class StickyNote extends RapidElement {
386
396
  class="sticky-title"
387
397
  contenteditable="${!this.isTranslating}"
388
398
  @blur="${this.handleTitleBlur}"
389
- @keydown="${this.handleKeyDown}"
399
+ @keydown="${this.handleTitleKeyDown}"
390
400
  @mousedown="${this.handleContentMouseDown}"
391
401
  .textContent="${this.data.title}"
392
402
  ></div>
@@ -396,7 +406,7 @@ export class StickyNote extends RapidElement {
396
406
  class="sticky-body"
397
407
  contenteditable="${!this.isTranslating}"
398
408
  @blur="${this.handleBodyBlur}"
399
- @keydown="${this.handleKeyDown}"
409
+ @keydown="${this.handleBodyKeyDown}"
400
410
  @mousedown="${this.handleContentMouseDown}"
401
411
  .textContent="${this.data.body}"
402
412
  ></div>
@@ -52,7 +52,10 @@ export const add_contact_groups: ActionConfig = {
52
52
  return {
53
53
  uuid: formData.uuid,
54
54
  type: 'add_contact_groups',
55
- groups: formData.groups || []
55
+ groups: (formData.groups || []).map((g: any) => ({
56
+ uuid: g.uuid,
57
+ name: g.name
58
+ }))
56
59
  };
57
60
  }
58
61
  };
@@ -53,7 +53,10 @@ export const add_input_labels: ActionConfig = {
53
53
  return {
54
54
  uuid: formData.uuid,
55
55
  type: 'add_input_labels',
56
- labels: formData.labels || []
56
+ labels: (formData.labels || []).map((l: any) => ({
57
+ uuid: l.uuid,
58
+ name: l.name
59
+ }))
57
60
  };
58
61
  }
59
62
  };
@@ -0,0 +1,127 @@
1
+ import { html, TemplateResult } from 'lit-html';
2
+
3
+ // SVG paths for play and pause icons
4
+ const PLAY_SVG = html`<svg
5
+ viewBox="0 0 24 24"
6
+ width="16"
7
+ height="16"
8
+ fill="currentColor"
9
+ >
10
+ <polygon points="6,3 20,12 6,21" />
11
+ </svg>`;
12
+
13
+ // Track active audio so only one plays at a time
14
+ let activeAudio: HTMLAudioElement | null = null;
15
+ let activeContainer: HTMLElement | null = null;
16
+
17
+ function stopActive() {
18
+ if (activeAudio) {
19
+ activeAudio.pause();
20
+ activeAudio.currentTime = 0;
21
+ if (activeContainer) {
22
+ resetPlayer(activeContainer);
23
+ }
24
+ activeAudio = null;
25
+ activeContainer = null;
26
+ }
27
+ }
28
+
29
+ function resetPlayer(container: HTMLElement) {
30
+ const btn = container.querySelector('.audio-play-btn') as HTMLElement;
31
+ const progress = container.querySelector('.audio-progress') as HTMLElement;
32
+ if (btn)
33
+ btn.innerHTML =
34
+ '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><polygon points="6,3 20,12 6,21"/></svg>';
35
+ if (progress) progress.style.width = '0%';
36
+ }
37
+
38
+ function handlePlayClick(e: MouseEvent) {
39
+ e.stopPropagation();
40
+ e.preventDefault();
41
+
42
+ const container = (e.currentTarget as HTMLElement).closest(
43
+ '.audio-player'
44
+ ) as HTMLElement;
45
+ if (!container) return;
46
+
47
+ const url = container.dataset.url;
48
+ if (!url) return;
49
+
50
+ const btn = container.querySelector('.audio-play-btn') as HTMLElement;
51
+ const progress = container.querySelector('.audio-progress') as HTMLElement;
52
+
53
+ // If this is already playing, pause it
54
+ if (activeAudio && activeContainer === container && !activeAudio.paused) {
55
+ activeAudio.pause();
56
+ btn.innerHTML =
57
+ '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><polygon points="6,3 20,12 6,21"/></svg>';
58
+ return;
59
+ }
60
+
61
+ // Stop any other playing audio
62
+ stopActive();
63
+
64
+ const audio = new Audio(url);
65
+ activeAudio = audio;
66
+ activeContainer = container;
67
+
68
+ btn.innerHTML =
69
+ '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><rect x="5" y="3" width="4" height="18"/><rect x="15" y="3" width="4" height="18"/></svg>';
70
+
71
+ audio.addEventListener('timeupdate', () => {
72
+ if (audio.duration && progress) {
73
+ const pct = (audio.currentTime / audio.duration) * 100;
74
+ progress.style.width = `${pct}%`;
75
+ }
76
+ });
77
+
78
+ audio.addEventListener('ended', () => {
79
+ resetPlayer(container);
80
+ activeAudio = null;
81
+ activeContainer = null;
82
+ });
83
+
84
+ audio.addEventListener('error', () => {
85
+ resetPlayer(container);
86
+ activeAudio = null;
87
+ activeContainer = null;
88
+ });
89
+
90
+ audio.play().catch(() => {
91
+ resetPlayer(container);
92
+ activeAudio = null;
93
+ activeContainer = null;
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Renders an inline audio player with play/pause button and progress bar.
99
+ * Used on canvas nodes for play_audio and say_msg actions.
100
+ */
101
+ export function renderAudioPlayer(audioUrl: string): TemplateResult {
102
+ return html`
103
+ <div
104
+ class="audio-player"
105
+ data-url="${audioUrl}"
106
+ style="display: flex; align-items: center; gap: 0.4em; cursor: default;"
107
+ @mousedown=${(e: MouseEvent) => e.stopPropagation()}
108
+ @mouseup=${(e: MouseEvent) => e.stopPropagation()}
109
+ >
110
+ <div
111
+ class="audio-play-btn"
112
+ @click=${handlePlayClick}
113
+ style="cursor: pointer; color: #666; display: flex; align-items: center; flex-shrink: 0;"
114
+ >
115
+ ${PLAY_SVG}
116
+ </div>
117
+ <div
118
+ style="flex: 1; height: 4px; background: #e0e0e0; border-radius: 2px; overflow: hidden; min-width: 40px;"
119
+ >
120
+ <div
121
+ class="audio-progress"
122
+ style="width: 0%; height: 100%; background: var(--color-primary, #2387ca); border-radius: 2px; transition: width 0.2s linear;"
123
+ ></div>
124
+ </div>
125
+ </div>
126
+ `;
127
+ }
@@ -0,0 +1,44 @@
1
+ import { html } from 'lit-html';
2
+ import { ActionConfig, ACTION_GROUPS, FlowTypes } from '../types';
3
+ import { Node, EnterFlow } from '../../store/flow-definition';
4
+ import { renderNamedObjects } from '../utils';
5
+
6
+ export const enter_flow: ActionConfig = {
7
+ name: 'Enter a Flow',
8
+ group: ACTION_GROUPS.trigger,
9
+ hideFromActions: true,
10
+ flowTypes: [FlowTypes.VOICE, FlowTypes.MESSAGE, FlowTypes.BACKGROUND],
11
+ render: (_node: Node, action: EnterFlow) => {
12
+ return html`${renderNamedObjects([action.flow], 'flow')}`;
13
+ },
14
+ toFormData: (action: EnterFlow) => {
15
+ return {
16
+ uuid: action.uuid,
17
+ flow: action.flow ? [action.flow] : []
18
+ };
19
+ },
20
+ form: {
21
+ flow: {
22
+ type: 'select',
23
+ required: true,
24
+ placeholder: 'Select a flow...',
25
+ helpText: 'The contact will enter this flow and not return',
26
+ endpoint: '/api/v2/flows.json',
27
+ valueKey: 'uuid',
28
+ nameKey: 'name'
29
+ }
30
+ },
31
+ layout: ['flow'],
32
+ fromFormData: (formData: any): EnterFlow => {
33
+ const selected = formData.flow[0];
34
+ return {
35
+ uuid: formData.uuid,
36
+ type: 'enter_flow',
37
+ terminal: true,
38
+ flow: {
39
+ uuid: selected.uuid || selected.value,
40
+ name: selected.name
41
+ }
42
+ };
43
+ }
44
+ };