@nyaruka/temba-components 0.129.3 → 0.129.5

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 (304) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.github/workflows/build.yml +135 -3
  3. package/CHANGELOG.md +19 -0
  4. package/demo/data/flows/sample-flow.json +110 -87
  5. package/demo/field-config-demo.html +135 -0
  6. package/dist/temba-components.js +1257 -675
  7. package/dist/temba-components.js.map +1 -1
  8. package/docs/ActionEditor-Migration.md +118 -0
  9. package/out-tsc/src/events.js.map +1 -1
  10. package/out-tsc/src/flow/{EditorNode.js → CanvasNode.js} +345 -42
  11. package/out-tsc/src/flow/CanvasNode.js.map +1 -0
  12. package/out-tsc/src/flow/Editor.js +107 -3
  13. package/out-tsc/src/flow/Editor.js.map +1 -1
  14. package/out-tsc/src/flow/NodeEditor.js +1211 -0
  15. package/out-tsc/src/flow/NodeEditor.js.map +1 -0
  16. package/out-tsc/src/flow/Plumber.js +0 -6
  17. package/out-tsc/src/flow/Plumber.js.map +1 -1
  18. package/out-tsc/src/flow/actions/add_contact_groups.js +40 -0
  19. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -0
  20. package/out-tsc/src/flow/actions/add_contact_urn.js +16 -0
  21. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -0
  22. package/out-tsc/src/flow/actions/add_input_labels.js +11 -0
  23. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -0
  24. package/out-tsc/src/flow/actions/call_classifier.js +11 -0
  25. package/out-tsc/src/flow/actions/call_classifier.js.map +1 -0
  26. package/out-tsc/src/flow/actions/call_llm.js +11 -0
  27. package/out-tsc/src/flow/actions/call_llm.js.map +1 -0
  28. package/out-tsc/src/flow/actions/call_resthook.js +11 -0
  29. package/out-tsc/src/flow/actions/call_resthook.js.map +1 -0
  30. package/out-tsc/src/flow/actions/call_webhook.js +122 -0
  31. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -0
  32. package/out-tsc/src/flow/actions/enter_flow.js +14 -0
  33. package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
  34. package/out-tsc/src/flow/actions/open_ticket.js +11 -0
  35. package/out-tsc/src/flow/actions/open_ticket.js.map +1 -0
  36. package/out-tsc/src/flow/actions/play_audio.js +11 -0
  37. package/out-tsc/src/flow/actions/play_audio.js.map +1 -0
  38. package/out-tsc/src/flow/actions/remove_contact_groups.js +62 -0
  39. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -0
  40. package/out-tsc/src/flow/actions/request_optin.js +11 -0
  41. package/out-tsc/src/flow/actions/request_optin.js.map +1 -0
  42. package/out-tsc/src/flow/actions/say_msg.js +11 -0
  43. package/out-tsc/src/flow/actions/say_msg.js.map +1 -0
  44. package/out-tsc/src/flow/actions/send_broadcast.js +33 -0
  45. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -0
  46. package/out-tsc/src/flow/actions/send_email.js +56 -0
  47. package/out-tsc/src/flow/actions/send_email.js.map +1 -0
  48. package/out-tsc/src/flow/actions/send_msg.js +55 -0
  49. package/out-tsc/src/flow/actions/send_msg.js.map +1 -0
  50. package/out-tsc/src/flow/actions/set_contact_channel.js +12 -0
  51. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -0
  52. package/out-tsc/src/flow/actions/set_contact_field.js +12 -0
  53. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -0
  54. package/out-tsc/src/flow/actions/set_contact_language.js +10 -0
  55. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -0
  56. package/out-tsc/src/flow/actions/set_contact_name.js +10 -0
  57. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -0
  58. package/out-tsc/src/flow/actions/set_contact_status.js +10 -0
  59. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -0
  60. package/out-tsc/src/flow/actions/set_run_result.js +10 -0
  61. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -0
  62. package/out-tsc/src/flow/actions/split_by_expression_example.js +77 -0
  63. package/out-tsc/src/flow/actions/split_by_expression_example.js.map +1 -0
  64. package/out-tsc/src/flow/actions/start_session.js +11 -0
  65. package/out-tsc/src/flow/actions/start_session.js.map +1 -0
  66. package/out-tsc/src/flow/actions/transfer_airtime.js +11 -0
  67. package/out-tsc/src/flow/actions/transfer_airtime.js.map +1 -0
  68. package/out-tsc/src/flow/config.js +88 -193
  69. package/out-tsc/src/flow/config.js.map +1 -1
  70. package/out-tsc/src/flow/nodes/execute_actions.js +4 -0
  71. package/out-tsc/src/flow/nodes/execute_actions.js.map +1 -0
  72. package/out-tsc/src/flow/nodes/split_by_airtime.js +9 -0
  73. package/out-tsc/src/flow/nodes/split_by_airtime.js.map +1 -0
  74. package/out-tsc/src/flow/nodes/split_by_contact_field.js +7 -0
  75. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -0
  76. package/out-tsc/src/flow/nodes/split_by_expression.js +7 -0
  77. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -0
  78. package/out-tsc/src/flow/nodes/split_by_groups.js +7 -0
  79. package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -0
  80. package/out-tsc/src/flow/nodes/split_by_random.js +10 -0
  81. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -0
  82. package/out-tsc/src/flow/nodes/split_by_run_result.js +7 -0
  83. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -0
  84. package/out-tsc/src/flow/nodes/split_by_scheme.js +7 -0
  85. package/out-tsc/src/flow/nodes/split_by_scheme.js.map +1 -0
  86. package/out-tsc/src/flow/nodes/split_by_subflow.js +9 -0
  87. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -0
  88. package/out-tsc/src/flow/nodes/split_by_webhook.js +18 -0
  89. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -0
  90. package/out-tsc/src/flow/nodes/wait_for_audio.js +7 -0
  91. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
  92. package/out-tsc/src/flow/nodes/wait_for_digits.js +7 -0
  93. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -0
  94. package/out-tsc/src/flow/nodes/wait_for_image.js +7 -0
  95. package/out-tsc/src/flow/nodes/wait_for_image.js.map +1 -0
  96. package/out-tsc/src/flow/nodes/wait_for_location.js +7 -0
  97. package/out-tsc/src/flow/nodes/wait_for_location.js.map +1 -0
  98. package/out-tsc/src/flow/nodes/wait_for_menu.js +7 -0
  99. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -0
  100. package/out-tsc/src/flow/nodes/wait_for_response.js +7 -0
  101. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -0
  102. package/out-tsc/src/flow/nodes/wait_for_video.js +7 -0
  103. package/out-tsc/src/flow/nodes/wait_for_video.js.map +1 -0
  104. package/out-tsc/src/flow/types.js +79 -0
  105. package/out-tsc/src/flow/types.js.map +1 -0
  106. package/out-tsc/src/flow/utils.js +65 -0
  107. package/out-tsc/src/flow/utils.js.map +1 -0
  108. package/out-tsc/src/form/ArrayEditor.js +199 -0
  109. package/out-tsc/src/form/ArrayEditor.js.map +1 -0
  110. package/out-tsc/src/form/BaseListEditor.js +128 -0
  111. package/out-tsc/src/form/BaseListEditor.js.map +1 -0
  112. package/out-tsc/src/form/Checkbox.js +17 -2
  113. package/out-tsc/src/form/Checkbox.js.map +1 -1
  114. package/out-tsc/src/form/Completion.js +6 -0
  115. package/out-tsc/src/form/Completion.js.map +1 -1
  116. package/out-tsc/src/form/FormField.js +110 -11
  117. package/out-tsc/src/form/FormField.js.map +1 -1
  118. package/out-tsc/src/form/KeyValueEditor.js +223 -0
  119. package/out-tsc/src/form/KeyValueEditor.js.map +1 -0
  120. package/out-tsc/src/form/select/Select.js +92 -32
  121. package/out-tsc/src/form/select/Select.js.map +1 -1
  122. package/out-tsc/src/interfaces.js +6 -0
  123. package/out-tsc/src/interfaces.js.map +1 -1
  124. package/out-tsc/src/live/ContactChat.js +2 -76
  125. package/out-tsc/src/live/ContactChat.js.map +1 -1
  126. package/out-tsc/temba-modules.js +9 -2
  127. package/out-tsc/temba-modules.js.map +1 -1
  128. package/out-tsc/test/ActionHelper.js +116 -0
  129. package/out-tsc/test/ActionHelper.js.map +1 -0
  130. package/out-tsc/test/actions/add_contact_groups.test.js +66 -0
  131. package/out-tsc/test/actions/add_contact_groups.test.js.map +1 -0
  132. package/out-tsc/test/actions/remove_contact_groups.test.js +226 -0
  133. package/out-tsc/test/actions/remove_contact_groups.test.js.map +1 -0
  134. package/out-tsc/test/actions/send_email.test.js +160 -0
  135. package/out-tsc/test/actions/send_email.test.js.map +1 -0
  136. package/out-tsc/test/actions/send_msg.test.js +95 -0
  137. package/out-tsc/test/actions/send_msg.test.js.map +1 -0
  138. package/out-tsc/test/temba-action-editing-integration.test.js +183 -0
  139. package/out-tsc/test/temba-action-editing-integration.test.js.map +1 -0
  140. package/out-tsc/test/temba-checkbox.test.js +1 -1
  141. package/out-tsc/test/temba-checkbox.test.js.map +1 -1
  142. package/out-tsc/test/temba-field-config.test.js +133 -0
  143. package/out-tsc/test/temba-field-config.test.js.map +1 -0
  144. package/out-tsc/test/temba-flow-editor-node.test.js +14 -14
  145. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  146. package/out-tsc/test/temba-node-editor.test.js +283 -0
  147. package/out-tsc/test/temba-node-editor.test.js.map +1 -0
  148. package/out-tsc/test/temba-select.test.js +158 -0
  149. package/out-tsc/test/temba-select.test.js.map +1 -1
  150. package/package.json +1 -1
  151. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  152. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  153. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  154. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  155. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  156. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  157. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  158. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  159. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  160. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  161. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  162. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  163. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  164. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  165. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  166. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  167. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  168. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  169. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  170. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  171. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  172. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  173. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  174. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  175. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  176. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  177. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  178. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  179. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  180. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  181. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  182. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  183. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  184. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  185. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  186. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  187. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  188. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  189. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  190. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  191. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  192. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  193. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  194. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  195. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  196. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  197. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  198. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  199. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  200. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  201. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  202. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  203. package/screenshots/truth/editor/router.png +0 -0
  204. package/screenshots/truth/editor/send_msg.png +0 -0
  205. package/screenshots/truth/editor/set_contact_language.png +0 -0
  206. package/screenshots/truth/editor/set_contact_name.png +0 -0
  207. package/screenshots/truth/editor/set_run_result.png +0 -0
  208. package/screenshots/truth/editor/wait.png +0 -0
  209. package/screenshots/truth/formfield/markdown-errors.png +0 -0
  210. package/screenshots/truth/formfield/plain-text-errors.png +0 -0
  211. package/screenshots/truth/formfield/widget-only-markdown-errors.png +0 -0
  212. package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
  213. package/src/events.ts +1 -40
  214. package/src/flow/{EditorNode.ts → CanvasNode.ts} +424 -48
  215. package/src/flow/Editor.ts +140 -4
  216. package/src/flow/NodeEditor.ts +1454 -0
  217. package/src/flow/Plumber.ts +0 -9
  218. package/src/flow/actions/add_contact_groups.ts +42 -0
  219. package/src/flow/actions/add_contact_urn.ts +17 -0
  220. package/src/flow/actions/add_input_labels.ts +12 -0
  221. package/src/flow/actions/call_classifier.ts +12 -0
  222. package/src/flow/actions/call_llm.ts +12 -0
  223. package/src/flow/actions/call_resthook.ts +12 -0
  224. package/src/flow/actions/call_webhook.ts +133 -0
  225. package/src/flow/actions/enter_flow.ts +15 -0
  226. package/src/flow/actions/open_ticket.ts +12 -0
  227. package/src/flow/actions/play_audio.ts +12 -0
  228. package/src/flow/actions/remove_contact_groups.ts +66 -0
  229. package/src/flow/actions/request_optin.ts +12 -0
  230. package/src/flow/actions/say_msg.ts +12 -0
  231. package/src/flow/actions/send_broadcast.ts +35 -0
  232. package/src/flow/actions/send_email.ts +60 -0
  233. package/src/flow/actions/send_msg.ts +58 -0
  234. package/src/flow/actions/set_contact_channel.ts +13 -0
  235. package/src/flow/actions/set_contact_field.ts +13 -0
  236. package/src/flow/actions/set_contact_language.ts +11 -0
  237. package/src/flow/actions/set_contact_name.ts +11 -0
  238. package/src/flow/actions/set_contact_status.ts +11 -0
  239. package/src/flow/actions/set_run_result.ts +11 -0
  240. package/src/flow/actions/split_by_expression_example.ts +88 -0
  241. package/src/flow/actions/start_session.ts +12 -0
  242. package/src/flow/actions/transfer_airtime.ts +12 -0
  243. package/src/flow/config.ts +93 -232
  244. package/src/flow/nodes/execute_actions.ts +5 -0
  245. package/src/flow/nodes/split_by_airtime.ts +9 -0
  246. package/src/flow/nodes/split_by_contact_field.ts +7 -0
  247. package/src/flow/nodes/split_by_expression.ts +7 -0
  248. package/src/flow/nodes/split_by_groups.ts +7 -0
  249. package/src/flow/nodes/split_by_random.ts +10 -0
  250. package/src/flow/nodes/split_by_run_result.ts +7 -0
  251. package/src/flow/nodes/split_by_scheme.ts +7 -0
  252. package/src/flow/nodes/split_by_subflow.ts +9 -0
  253. package/src/flow/nodes/split_by_webhook.ts +19 -0
  254. package/src/flow/nodes/wait_for_audio.ts +7 -0
  255. package/src/flow/nodes/wait_for_digits.ts +7 -0
  256. package/src/flow/nodes/wait_for_image.ts +7 -0
  257. package/src/flow/nodes/wait_for_location.ts +7 -0
  258. package/src/flow/nodes/wait_for_menu.ts +7 -0
  259. package/src/flow/nodes/wait_for_response.ts +7 -0
  260. package/src/flow/nodes/wait_for_video.ts +7 -0
  261. package/src/flow/types.ts +352 -0
  262. package/src/flow/utils.ts +76 -0
  263. package/src/form/ArrayEditor.ts +240 -0
  264. package/src/form/BaseListEditor.ts +177 -0
  265. package/src/form/Checkbox.ts +22 -3
  266. package/src/form/Completion.ts +6 -0
  267. package/src/form/FormField.ts +115 -11
  268. package/src/form/KeyValueEditor.ts +251 -0
  269. package/src/form/select/Select.ts +105 -32
  270. package/src/interfaces.ts +7 -2
  271. package/src/live/ContactChat.ts +3 -97
  272. package/src/store/flow-definition.d.ts +6 -1
  273. package/static/api/contacts.json +30 -0
  274. package/static/api/groups.json +4 -426
  275. package/static/api/locations.json +24 -0
  276. package/static/api/media.json +5 -0
  277. package/static/api/optins.json +16 -0
  278. package/static/api/orgs.json +13 -0
  279. package/static/api/topics.json +21 -0
  280. package/static/api/users.json +26 -0
  281. package/static/css/temba-components.css +3 -6
  282. package/temba-modules.ts +9 -2
  283. package/test/ActionHelper.ts +142 -0
  284. package/test/actions/add_contact_groups.test.ts +89 -0
  285. package/test/actions/remove_contact_groups.test.ts +265 -0
  286. package/test/actions/send_email.test.ts +214 -0
  287. package/test/actions/send_msg.test.ts +130 -0
  288. package/test/temba-action-editing-integration.test.ts +240 -0
  289. package/test/temba-checkbox.test.ts +1 -1
  290. package/test/temba-field-config.test.ts +152 -0
  291. package/test/temba-flow-editor-node.test.ts +18 -18
  292. package/test/temba-node-editor.test.ts +353 -0
  293. package/test/temba-select.test.ts +234 -0
  294. package/test-assets/contacts/history.json +11 -33
  295. package/web-dev-server.config.mjs +34 -0
  296. package/.github/workflows/coverage.yml +0 -80
  297. package/demo/sticky-note-demo.html +0 -155
  298. package/out-tsc/src/flow/EditorNode.js.map +0 -1
  299. package/out-tsc/src/flow/render.js +0 -358
  300. package/out-tsc/src/flow/render.js.map +0 -1
  301. package/out-tsc/test/temba-flow-render.test.js +0 -794
  302. package/out-tsc/test/temba-flow-render.test.js.map +0 -1
  303. package/src/flow/render.ts +0 -443
  304. package/test/temba-flow-render.test.ts +0 -1003
@@ -1,5 +1,6 @@
1
1
  import { css, html, PropertyValueMap, TemplateResult } from 'lit';
2
- import { EDITOR_CONFIG, UIConfig } from './config';
2
+ import { repeat } from 'lit/directives/repeat.js';
3
+ import { ACTION_CONFIG, ActionConfig, NODE_CONFIG, NodeConfig } from './config';
3
4
  import { Action, Exit, Node, NodeUI, Router } from '../store/flow-definition';
4
5
  import { property } from 'lit/decorators.js';
5
6
  import { RapidElement } from '../RapidElement';
@@ -8,7 +9,9 @@ import { Plumber } from './Plumber';
8
9
  import { getStore } from '../store/Store';
9
10
  import { CustomEventType } from '../interfaces';
10
11
 
11
- export class EditorNode extends RapidElement {
12
+ const DRAG_THRESHOLD = 5;
13
+
14
+ export class CanvasNode extends RapidElement {
12
15
  createRenderRoot() {
13
16
  return this;
14
17
  }
@@ -34,6 +37,15 @@ export class EditorNode extends RapidElement {
34
37
  // Set of action UUIDs that are in the removing state
35
38
  private actionRemovingState: Set<string> = new Set();
36
39
 
40
+ // Track action click state to distinguish from drag
41
+ private actionClickStartPos: { x: number; y: number } | null = null;
42
+ private pendingActionClick: { action: Action; event: MouseEvent } | null =
43
+ null;
44
+
45
+ // Track node click state to distinguish from drag
46
+ private nodeClickStartPos: { x: number; y: number } | null = null;
47
+ private pendingNodeClick: { event: MouseEvent } | null = null;
48
+
37
49
  static get styles() {
38
50
  return css`
39
51
 
@@ -49,6 +61,11 @@ export class EditorNode extends RapidElement {
49
61
 
50
62
  }
51
63
 
64
+ /* Cap width for execute_actions nodes */
65
+ .node.execute-actions {
66
+ max-width: 200px;
67
+ }
68
+
52
69
  .node:hover {
53
70
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
54
71
  }
@@ -60,8 +77,8 @@ export class EditorNode extends RapidElement {
60
77
  }
61
78
 
62
79
  .action {
63
- max-width: 200px;
64
80
  position: relative;
81
+ font-size: 13px;
65
82
  }
66
83
 
67
84
  .action .remove-button {
@@ -83,18 +100,40 @@ export class EditorNode extends RapidElement {
83
100
  z-index: 10;
84
101
  }
85
102
 
86
- .action:hover .remove-button {
103
+ .action:hover .remove-button,
104
+ .router:hover .remove-button {
87
105
  display: flex;
88
106
  }
89
107
 
90
- .action.removing .title {
108
+ .action.removing .title,
109
+ .router .title.removing {
91
110
  background-color: var(--color-error, #dc3545) !important;
92
111
  }
93
112
 
94
- .action.removing .title .name {
113
+ .action.removing .title .name,
114
+ .router .title.removing .name {
95
115
  color: white;
96
116
  }
97
117
 
118
+ .router .remove-button {
119
+ position: absolute;
120
+ top: 5px;
121
+ right: 5px;
122
+ width: 16px;
123
+ height: 16px;
124
+ border-radius: 50%;
125
+ background: var(--color-error, #dc3545);
126
+ color: white;
127
+ border: none;
128
+ cursor: pointer;
129
+ display: none;
130
+ align-items: center;
131
+ justify-content: center;
132
+ font-size: 10px;
133
+ line-height: 1;
134
+ z-index: 10;
135
+ }
136
+
98
137
  .action.sortable {
99
138
  display: flex;
100
139
  align-items: stretch;
@@ -104,10 +143,17 @@ export class EditorNode extends RapidElement {
104
143
  flex-grow: 1;
105
144
  display: flex;
106
145
  flex-direction: column;
146
+ min-width: 0; /* Allow flex item to shrink below its content size */
147
+ overflow: hidden;
107
148
  }
108
149
 
109
150
  .action .body {
110
- padding: 1em;
151
+ padding: 0.75em;
152
+ word-wrap: break-word;
153
+ overflow-wrap: break-word;
154
+ hyphens: auto;
155
+ white-space: normal;
156
+ overflow: hidden;
111
157
  }
112
158
 
113
159
  .action .drag-handle {
@@ -183,6 +229,10 @@ export class EditorNode extends RapidElement {
183
229
  .category .title {
184
230
  font-weight: normal;
185
231
  font-size: 1em;
232
+ max-width: 150px;
233
+ white-space: nowrap;
234
+ overflow: hidden;
235
+ text-overflow: ellipsis;
186
236
  }
187
237
 
188
238
  .router .body {
@@ -292,10 +342,14 @@ export class EditorNode extends RapidElement {
292
342
  ): void {
293
343
  super.updated(changes);
294
344
  if (changes.has('node')) {
295
- // make our initial connections
296
- if (changes.get('node') === undefined) {
345
+ // Only proceed if plumber is available (for tests that don't set it up)
346
+ if (this.plumber) {
347
+ this.plumber.removeNodeConnections(this.node.uuid);
348
+ // make our initial connections
297
349
  for (const exit of this.node.exits) {
298
350
  if (!exit.destination_uuid) {
351
+ // if we have no destination, then we are a source
352
+ // so make our source endpoint
299
353
  this.plumber.makeSource(exit.uuid);
300
354
  } else {
301
355
  this.plumber.connectIds(
@@ -305,17 +359,21 @@ export class EditorNode extends RapidElement {
305
359
  );
306
360
  }
307
361
  }
362
+
363
+ this.plumber.revalidate([this.node.uuid]);
308
364
  }
309
365
 
310
366
  const ele = this.parentElement;
311
- const rect = ele.getBoundingClientRect();
312
-
313
- getStore()
314
- ?.getState()
315
- .expandCanvas(
316
- this.ui.position.left + rect.width,
317
- this.ui.position.top + rect.height
318
- );
367
+ if (ele) {
368
+ const rect = ele.getBoundingClientRect();
369
+
370
+ getStore()
371
+ ?.getState()
372
+ .expandCanvas(
373
+ this.ui.position.left + rect.width,
374
+ this.ui.position.top + rect.height
375
+ );
376
+ }
319
377
  }
320
378
  }
321
379
 
@@ -480,6 +538,55 @@ export class EditorNode extends RapidElement {
480
538
  }
481
539
  }
482
540
 
541
+ private handleNodeRemoveClick(event: MouseEvent) {
542
+ event.preventDefault();
543
+ event.stopPropagation();
544
+
545
+ const nodeId = this.node.uuid;
546
+
547
+ // If the node is already in removing state, perform the removal
548
+ if (this.actionRemovingState.has(nodeId)) {
549
+ this.removeNode();
550
+ return;
551
+ }
552
+
553
+ // Start removal UI state
554
+ this.actionRemovingState.add(nodeId);
555
+ this.requestUpdate();
556
+
557
+ // Clear any existing timeout for this node
558
+ if (this.actionRemovalTimeouts.has(nodeId)) {
559
+ clearTimeout(this.actionRemovalTimeouts.get(nodeId));
560
+ }
561
+
562
+ // Set timeout to reset UI if user doesn't click
563
+ const timeoutId = window.setTimeout(() => {
564
+ this.actionRemovingState.delete(nodeId);
565
+ this.actionRemovalTimeouts.delete(nodeId);
566
+ this.requestUpdate();
567
+ }, 1000); // 1 second as per requirements
568
+
569
+ this.actionRemovalTimeouts.set(nodeId, timeoutId);
570
+ }
571
+
572
+ private removeNode() {
573
+ const nodeId = this.node.uuid;
574
+
575
+ // Clear the UI state
576
+ this.actionRemovingState.delete(nodeId);
577
+
578
+ // Clear any timeout
579
+ if (this.actionRemovalTimeouts.has(nodeId)) {
580
+ clearTimeout(this.actionRemovalTimeouts.get(nodeId));
581
+ this.actionRemovalTimeouts.delete(nodeId);
582
+ }
583
+
584
+ // Fire the node deleted event
585
+ this.fireCustomEvent(CustomEventType.NodeDeleted, {
586
+ uuid: this.node.uuid
587
+ });
588
+ }
589
+
483
590
  private handleActionOrderChanged(event: CustomEvent) {
484
591
  const [fromIdx, toIdx] = event.detail.swap;
485
592
 
@@ -498,7 +605,197 @@ export class EditorNode extends RapidElement {
498
605
  .updateNode(this.node.uuid, { ...this.node, actions: newActions });
499
606
  }
500
607
 
501
- private renderTitle(config: UIConfig, isRemoving: boolean = false) {
608
+ private handleActionMouseDown(event: MouseEvent, action: Action): void {
609
+ // Don't handle clicks on the remove button or when action is in removing state
610
+ const target = event.target as HTMLElement;
611
+ if (
612
+ target.closest('.remove-button') ||
613
+ this.actionRemovingState.has(action.uuid)
614
+ ) {
615
+ return;
616
+ }
617
+
618
+ // Store the starting position and action for later comparison
619
+ // Don't prevent default - let the Editor's drag system work normally
620
+ this.actionClickStartPos = { x: event.clientX, y: event.clientY };
621
+ this.pendingActionClick = { action, event };
622
+ }
623
+
624
+ private handleActionMouseUp(event: MouseEvent, action: Action): void {
625
+ // Don't handle if we don't have a pending click or if it's not the same action
626
+ if (
627
+ !this.pendingActionClick ||
628
+ this.pendingActionClick.action.uuid !== action.uuid
629
+ ) {
630
+ this.actionClickStartPos = null;
631
+ this.pendingActionClick = null;
632
+ return;
633
+ }
634
+
635
+ // Don't handle clicks on the remove button or when action is in removing state
636
+ const target = event.target as HTMLElement;
637
+ if (
638
+ target.closest('.remove-button') ||
639
+ this.actionRemovingState.has(action.uuid)
640
+ ) {
641
+ this.actionClickStartPos = null;
642
+ this.pendingActionClick = null;
643
+ return;
644
+ }
645
+
646
+ // Check if the mouse moved beyond the drag threshold
647
+ if (this.actionClickStartPos) {
648
+ const deltaX = event.clientX - this.actionClickStartPos.x;
649
+ const deltaY = event.clientY - this.actionClickStartPos.y;
650
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
651
+
652
+ // Check if the Editor is currently in dragging mode
653
+ const editor = this.closest('temba-flow-editor') as any;
654
+ const editorWasDragging = editor?.dragging;
655
+
656
+ // Only fire the action edit event if we haven't dragged beyond the threshold
657
+ // AND either there's no Editor parent (test case) or the Editor didn't drag the node
658
+ if (distance <= DRAG_THRESHOLD && (!editor || !editorWasDragging)) {
659
+ // Fire event to request action editing
660
+ this.fireCustomEvent(CustomEventType.ActionEditRequested, {
661
+ action,
662
+ nodeUuid: this.node.uuid
663
+ });
664
+ }
665
+ }
666
+
667
+ // Clean up
668
+ this.actionClickStartPos = null;
669
+ this.pendingActionClick = null;
670
+ }
671
+
672
+ private handleActionClick(event: MouseEvent, action: Action): void {
673
+ // This method is kept for backward compatibility but should not be used
674
+ // The new mousedown/mouseup approach handles click vs drag properly
675
+ event.preventDefault();
676
+ event.stopPropagation();
677
+
678
+ // Don't handle clicks on the remove button or when action is in removing state
679
+ const target = event.target as HTMLElement;
680
+ if (
681
+ target.closest('.remove-button') ||
682
+ this.actionRemovingState.has(action.uuid)
683
+ ) {
684
+ return;
685
+ }
686
+
687
+ // Fire event to request action editing
688
+ this.fireCustomEvent(CustomEventType.ActionEditRequested, {
689
+ action,
690
+ nodeUuid: this.node.uuid
691
+ });
692
+ }
693
+
694
+ private handleNodeEditClick(event: MouseEvent): void {
695
+ event.preventDefault();
696
+ event.stopPropagation();
697
+
698
+ // Don't handle clicks on the remove button or when node is in removing state
699
+ const target = event.target as HTMLElement;
700
+ if (
701
+ target.closest('.remove-button') ||
702
+ this.actionRemovingState.has(this.node.uuid)
703
+ ) {
704
+ return;
705
+ }
706
+
707
+ // Fire node edit requested event if the node has a router
708
+ if (this.node.router) {
709
+ // If router node has exactly one action, open the action editor directly
710
+ if (this.node.actions && this.node.actions.length === 1) {
711
+ this.fireCustomEvent(CustomEventType.ActionEditRequested, {
712
+ action: this.node.actions[0],
713
+ nodeUuid: this.node.uuid
714
+ });
715
+ } else {
716
+ // Otherwise open the node editor as before
717
+ this.fireCustomEvent(CustomEventType.NodeEditRequested, {
718
+ node: this.node,
719
+ nodeUI: this.ui
720
+ });
721
+ }
722
+ }
723
+ }
724
+
725
+ private handleNodeMouseDown(event: MouseEvent): void {
726
+ // Don't handle clicks on the remove button or when node is in removing state
727
+ const target = event.target as HTMLElement;
728
+ if (
729
+ target.closest('.remove-button') ||
730
+ this.actionRemovingState.has(this.node.uuid)
731
+ ) {
732
+ return;
733
+ }
734
+
735
+ // Store the starting position for later comparison
736
+ // Don't prevent default - let the Editor's drag system work normally
737
+ this.nodeClickStartPos = { x: event.clientX, y: event.clientY };
738
+ this.pendingNodeClick = { event };
739
+ }
740
+
741
+ private handleNodeMouseUp(event: MouseEvent): void {
742
+ // Don't handle if we don't have a pending click
743
+ if (!this.pendingNodeClick) {
744
+ this.nodeClickStartPos = null;
745
+ this.pendingNodeClick = null;
746
+ return;
747
+ }
748
+
749
+ // Don't handle clicks on the remove button or when node is in removing state
750
+ const target = event.target as HTMLElement;
751
+ if (
752
+ target.closest('.remove-button') ||
753
+ this.actionRemovingState.has(this.node.uuid)
754
+ ) {
755
+ this.nodeClickStartPos = null;
756
+ this.pendingNodeClick = null;
757
+ return;
758
+ }
759
+
760
+ // Check if the mouse moved beyond the drag threshold
761
+ if (this.nodeClickStartPos) {
762
+ const deltaX = event.clientX - this.nodeClickStartPos.x;
763
+ const deltaY = event.clientY - this.nodeClickStartPos.y;
764
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
765
+
766
+ // Check if the Editor is currently in dragging mode
767
+ const editor = this.closest('temba-flow-editor') as any;
768
+ const editorWasDragging = editor?.dragging;
769
+
770
+ // Only fire the node edit event if we haven't dragged beyond the threshold
771
+ // AND either there's no Editor parent (test case) or the Editor didn't drag the node
772
+ if (distance <= 5 && (!editor || !editorWasDragging)) {
773
+ // Using literal 5 instead of DRAG_THRESHOLD since it's not imported
774
+ // Fire event to request node editing if the node has a router
775
+ if (this.node.router) {
776
+ // If router node has exactly one action, open the action editor directly
777
+ if (this.node.actions && this.node.actions.length === 1) {
778
+ this.fireCustomEvent(CustomEventType.ActionEditRequested, {
779
+ action: this.node.actions[0],
780
+ nodeUuid: this.node.uuid
781
+ });
782
+ } else {
783
+ // Otherwise open the node editor as before
784
+ this.fireCustomEvent(CustomEventType.NodeEditRequested, {
785
+ node: this.node,
786
+ nodeUI: this.ui
787
+ });
788
+ }
789
+ }
790
+ }
791
+ }
792
+
793
+ // Clean up
794
+ this.nodeClickStartPos = null;
795
+ this.pendingNodeClick = null;
796
+ }
797
+
798
+ private renderTitle(config: ActionConfig, isRemoving: boolean = false) {
502
799
  return html`<div class="title" style="background:${config.color}">
503
800
  ${this.node?.actions?.length > 1
504
801
  ? html`<temba-icon class="drag-handle" name="sort"></temba-icon>`
@@ -508,8 +805,17 @@ export class EditorNode extends RapidElement {
508
805
  </div>`;
509
806
  }
510
807
 
808
+ private renderNodeTitle(config: NodeConfig, isRemoving: boolean = false) {
809
+ return html`<div
810
+ class="title ${isRemoving ? 'removing' : ''}"
811
+ style="background:${config.color}"
812
+ >
813
+ <div class="name">${isRemoving ? 'Remove?' : config.name}</div>
814
+ </div>`;
815
+ }
816
+
511
817
  private renderAction(node: Node, action: Action, index: number) {
512
- const config = EDITOR_CONFIG[action.type];
818
+ const config = ACTION_CONFIG[action.type];
513
819
  const isRemoving = this.actionRemovingState.has(action.uuid);
514
820
 
515
821
  if (config) {
@@ -525,7 +831,12 @@ export class EditorNode extends RapidElement {
525
831
  >
526
832
 
527
833
  </button>
528
- <div class="action-content">
834
+ <div
835
+ class="action-content"
836
+ @mousedown=${(e: MouseEvent) => this.handleActionMouseDown(e, action)}
837
+ @mouseup=${(e: MouseEvent) => this.handleActionMouseUp(e, action)}
838
+ style="cursor: pointer;"
839
+ >
529
840
  ${this.renderTitle(config, isRemoving)}
530
841
  <div class="body">
531
842
  ${config.render
@@ -552,12 +863,39 @@ export class EditorNode extends RapidElement {
552
863
  }
553
864
 
554
865
  private renderRouter(router: Router, ui: NodeUI) {
555
- const config = EDITOR_CONFIG[ui.type];
556
- if (config) {
557
- return html`<div class="router">
558
- ${this.renderTitle(config, false)}
866
+ const nodeConfig = NODE_CONFIG[ui.type];
867
+ if (nodeConfig) {
868
+ // For tests that call renderRouter directly without setting this.node
869
+ const hasActions = this.node ? this.node.actions.length > 0 : false;
870
+ const isRemoving =
871
+ this.node &&
872
+ this.node.actions.length === 0 &&
873
+ this.actionRemovingState.has(this.node.uuid);
874
+
875
+ return html`<div class="router" style="position: relative;">
876
+ ${!hasActions
877
+ ? html` <button
878
+ class="remove-button"
879
+ @click=${(e: MouseEvent) => this.handleNodeRemoveClick(e)}
880
+ title="Remove node"
881
+ >
882
+
883
+ </button>
884
+ <div
885
+ @mousedown=${(e: MouseEvent) => this.handleNodeMouseDown(e)}
886
+ @mouseup=${(e: MouseEvent) => this.handleNodeMouseUp(e)}
887
+ style="cursor: pointer;"
888
+ >
889
+ ${this.renderNodeTitle(nodeConfig, isRemoving)}
890
+ </div>`
891
+ : ''}
559
892
  ${router.result_name
560
- ? html`<div class="body">
893
+ ? html`<div
894
+ class="body"
895
+ @mousedown=${(e: MouseEvent) => this.handleNodeMouseDown(e)}
896
+ @mouseup=${(e: MouseEvent) => this.handleNodeMouseUp(e)}
897
+ style="cursor: pointer;"
898
+ >
561
899
  Save as
562
900
  <div class="result-name">${router.result_name}</div>
563
901
  </div>`
@@ -570,18 +908,28 @@ export class EditorNode extends RapidElement {
570
908
  if (!node.router || !node.router.categories) {
571
909
  return null;
572
910
  }
573
- const categories = node.router.categories.map((category) => {
574
- const exit = node.exits.find(
575
- (exit: Exit) => exit.uuid == category.exit_uuid
576
- );
577
-
578
- return html`<div class="category">
579
- <div class="title">${category.name}</div>
580
- ${this.renderExit(exit)}
581
- </div>`;
582
- });
583
911
 
584
- return html`<div class="categories">${categories}</div>`;
912
+ return html`<div class="categories">
913
+ ${repeat(
914
+ node.router.categories,
915
+ (category) => category.uuid,
916
+ (category) => {
917
+ const exit = node.exits.find(
918
+ (exit: Exit) => exit.uuid == category.exit_uuid
919
+ );
920
+
921
+ return html`<div
922
+ class="category"
923
+ @mousedown=${(e: MouseEvent) => this.handleNodeMouseDown(e)}
924
+ @mouseup=${(e: MouseEvent) => this.handleNodeMouseUp(e)}
925
+ style="cursor: pointer;"
926
+ >
927
+ <div class="title">${category.name}</div>
928
+ ${this.renderExit(exit)}
929
+ </div>`;
930
+ }
931
+ )}
932
+ </div>`;
585
933
  }
586
934
 
587
935
  private renderExit(exit: Exit): TemplateResult {
@@ -603,29 +951,57 @@ export class EditorNode extends RapidElement {
603
951
  return html`<div class="node">Loading...</div>`;
604
952
  }
605
953
 
954
+ const nodeConfig = NODE_CONFIG[this.ui.type];
955
+
606
956
  return html`
607
957
  <div
608
958
  id="${this.node.uuid}"
609
- class="node"
959
+ class="node ${this.ui.type === 'execute_actions'
960
+ ? 'execute-actions'
961
+ : ''}"
610
962
  style="left:${this.ui.position.left}px;top:${this.ui.position.top}px"
611
963
  >
612
964
  ${this.node.actions.length > 0
613
- ? html`<temba-sortable-list
614
- dragHandle="drag-handle"
615
- @temba-order-changed="${this.handleActionOrderChanged}"
616
- >
617
- ${this.node.actions.map((actionSpec, index) => {
618
- return this.renderAction(this.node, actionSpec, index);
619
- })}
620
- </temba-sortable-list>`
965
+ ? this.ui.type === 'execute_actions'
966
+ ? html`<temba-sortable-list
967
+ dragHandle="drag-handle"
968
+ @temba-order-changed="${this.handleActionOrderChanged}"
969
+ >
970
+ ${repeat(
971
+ this.node.actions,
972
+ (action) => action.uuid,
973
+ (action, index) => this.renderAction(this.node, action, index)
974
+ )}
975
+ </temba-sortable-list>`
976
+ : html`${repeat(
977
+ this.node.actions,
978
+ (action) => action.uuid,
979
+ (action, index) => this.renderAction(this.node, action, index)
980
+ )}`
981
+ : !this.node.router && nodeConfig && nodeConfig.name
982
+ ? html`<div class="router" style="position: relative;">
983
+ <button
984
+ class="remove-button"
985
+ @click=${(e: MouseEvent) => this.handleNodeRemoveClick(e)}
986
+ title="Remove node"
987
+ >
988
+
989
+ </button>
990
+ ${this.renderNodeTitle(
991
+ nodeConfig,
992
+ this.actionRemovingState.has(this.node.uuid)
993
+ )}
994
+ </div>`
621
995
  : ''}
622
996
  ${this.node.router
623
997
  ? html` ${this.renderRouter(this.node.router, this.ui)}
624
998
  ${this.renderCategories(this.node)}`
625
999
  : html`<div class="action-exits">
626
- ${this.node.exits.map((exit) => {
627
- return this.renderExit(exit);
628
- })}
1000
+ ${repeat(
1001
+ this.node.exits,
1002
+ (exit) => exit.uuid,
1003
+ (exit) => this.renderExit(exit)
1004
+ )}
629
1005
  </div>`}
630
1006
  </div>
631
1007
  `;