@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
@@ -0,0 +1,353 @@
1
+ import '../temba-modules';
2
+ import { html, fixture, expect } from '@open-wc/testing';
3
+ import { assertScreenshot, getClip } from './utils.test';
4
+
5
+ // Define interface for NodeEditor component
6
+ interface NodeEditorElement extends HTMLElement {
7
+ action?: any;
8
+ node?: any;
9
+ nodeUI?: any;
10
+ isOpen?: boolean;
11
+ updateComplete: Promise<boolean>;
12
+ }
13
+
14
+ const assertDialogScreenshot = async (
15
+ el: NodeEditorElement,
16
+ screenshotName: string
17
+ ) => {
18
+ const dialog = el.shadowRoot
19
+ .querySelector('temba-dialog')
20
+ .shadowRoot.querySelector('.dialog-container') as HTMLElement;
21
+ await assertScreenshot(screenshotName, getClip(dialog));
22
+ };
23
+
24
+ describe('temba-node-editor', () => {
25
+ it('can be created', async () => {
26
+ const el = (await fixture(html`
27
+ <temba-node-editor .isOpen=${true}></temba-node-editor>
28
+ `)) as NodeEditorElement;
29
+
30
+ expect(el).to.exist;
31
+ expect(el.tagName).to.equal('TEMBA-NODE-EDITOR');
32
+ });
33
+
34
+ it('renders send_msg action', async () => {
35
+ const action = {
36
+ uuid: 'test-action-uuid',
37
+ type: 'send_msg',
38
+ text: 'Hello world',
39
+ quick_replies: []
40
+ };
41
+
42
+ const el = (await fixture(html`
43
+ <temba-node-editor .action=${action} .isOpen=${true}></temba-node-editor>
44
+ `)) as NodeEditorElement;
45
+
46
+ await el.updateComplete;
47
+ expect(el.shadowRoot).to.not.be.null;
48
+ expect(el.action).to.equal(action);
49
+ });
50
+
51
+ it('renders set_run_result action', async () => {
52
+ const action = {
53
+ uuid: 'test-action-uuid',
54
+ type: 'set_run_result',
55
+ name: 'result_name',
56
+ value: 'result_value'
57
+ };
58
+
59
+ const el = (await fixture(html`
60
+ <temba-node-editor .action=${action} .isOpen=${true}></temba-node-editor>
61
+ `)) as NodeEditorElement;
62
+
63
+ await el.updateComplete;
64
+ expect(el.shadowRoot).to.not.be.null;
65
+ expect(el.action).to.equal(action);
66
+ });
67
+
68
+ it('renders set_contact_field action', async () => {
69
+ const action = {
70
+ uuid: 'test-action-uuid',
71
+ type: 'set_contact_field',
72
+ field: { key: 'age', name: 'Age' },
73
+ value: '25'
74
+ };
75
+
76
+ const el = (await fixture(html`
77
+ <temba-node-editor .action=${action} .isOpen=${true}></temba-node-editor>
78
+ `)) as NodeEditorElement;
79
+
80
+ await el.updateComplete;
81
+ expect(el.shadowRoot).to.not.be.null;
82
+ expect(el.action).to.equal(action);
83
+ });
84
+
85
+ it('renders add_contact_groups action', async () => {
86
+ const action = {
87
+ uuid: 'test-action-uuid',
88
+ type: 'add_contact_groups',
89
+ groups: [{ uuid: 'group-1', name: 'Test Group' }]
90
+ };
91
+
92
+ const el = (await fixture(html`
93
+ <temba-node-editor .action=${action} .isOpen=${true}></temba-node-editor>
94
+ `)) as NodeEditorElement;
95
+
96
+ await el.updateComplete;
97
+ expect(el.shadowRoot).to.not.be.null;
98
+ expect(el.action).to.equal(action);
99
+ });
100
+
101
+ it('renders enter_flow action', async () => {
102
+ const action = {
103
+ uuid: 'test-action-uuid',
104
+ type: 'enter_flow',
105
+ flow: { uuid: 'flow-1', name: 'Sub Flow' }
106
+ };
107
+
108
+ const el = (await fixture(html`
109
+ <temba-node-editor .action=${action} .isOpen=${true}></temba-node-editor>
110
+ `)) as NodeEditorElement;
111
+
112
+ await el.updateComplete;
113
+ expect(el.shadowRoot).to.not.be.null;
114
+ expect(el.action).to.equal(action);
115
+ });
116
+
117
+ it('renders node with router configuration', async () => {
118
+ const node = {
119
+ uuid: 'test-node-uuid',
120
+ actions: [],
121
+ exits: [{ uuid: 'exit-1', name: 'Default' }],
122
+ router: {
123
+ type: 'switch',
124
+ result_name: 'result',
125
+ categories: [{ uuid: 'cat-1', name: 'Category 1', exit_uuid: 'exit-1' }]
126
+ }
127
+ };
128
+
129
+ const nodeUI = {
130
+ type: 'split_by_expression',
131
+ position: { left: 100, top: 100 }
132
+ };
133
+
134
+ const el = (await fixture(html`
135
+ <temba-node-editor
136
+ .node=${node}
137
+ .nodeUI=${nodeUI}
138
+ .isOpen=${true}
139
+ ></temba-node-editor>
140
+ `)) as NodeEditorElement;
141
+
142
+ await el.updateComplete;
143
+ expect(el.shadowRoot).to.not.be.null;
144
+ expect(el.node).to.equal(node);
145
+ expect(el.nodeUI).to.equal(nodeUI);
146
+
147
+ await assertDialogScreenshot(el, 'editor/router');
148
+ });
149
+
150
+ it('renders node with wait configuration', async () => {
151
+ const node = {
152
+ uuid: 'test-node-uuid',
153
+ actions: [],
154
+ exits: [{ uuid: 'exit-1', name: 'Default' }],
155
+ wait: {
156
+ type: 'msg'
157
+ }
158
+ };
159
+
160
+ const nodeUI = {
161
+ type: 'wait_for_response',
162
+ position: { left: 100, top: 100 }
163
+ };
164
+
165
+ const el = (await fixture(html`
166
+ <temba-node-editor
167
+ .node=${node}
168
+ .nodeUI=${nodeUI}
169
+ .isOpen=${true}
170
+ ></temba-node-editor>
171
+ `)) as NodeEditorElement;
172
+
173
+ await el.updateComplete;
174
+ expect(el.shadowRoot).to.not.be.null;
175
+ expect(el.node).to.equal(node);
176
+ expect(el.nodeUI).to.equal(nodeUI);
177
+
178
+ await assertDialogScreenshot(el, 'editor/wait');
179
+ });
180
+
181
+ it('handles different button actions', async () => {
182
+ const action = {
183
+ uuid: 'test-action-uuid',
184
+ type: 'send_msg',
185
+ text: 'Hello world',
186
+ quick_replies: []
187
+ };
188
+
189
+ const el = (await fixture(html`
190
+ <temba-node-editor .action=${action} .isOpen=${true}></temba-node-editor>
191
+ `)) as NodeEditorElement;
192
+
193
+ await el.updateComplete;
194
+
195
+ let saveEventFired = false;
196
+ let cancelEventFired = false;
197
+
198
+ el.addEventListener('temba-action-saved', () => {
199
+ saveEventFired = true;
200
+ });
201
+
202
+ el.addEventListener('temba-node-edit-cancelled', () => {
203
+ cancelEventFired = true;
204
+ });
205
+
206
+ // Get the dialog element inside the node editor
207
+ const dialog = el.shadowRoot!.querySelector('temba-dialog');
208
+ expect(dialog).to.not.be.null;
209
+
210
+ // Test Save button by dispatching event on the dialog
211
+ const saveEvent = new CustomEvent('temba-button-clicked', {
212
+ detail: { button: { name: 'Save' } },
213
+ bubbles: true
214
+ });
215
+ dialog!.dispatchEvent(saveEvent);
216
+ expect(saveEventFired).to.equal(true);
217
+
218
+ // Reset for cancel test
219
+ saveEventFired = false;
220
+
221
+ // Test Cancel button
222
+ const cancelEvent = new CustomEvent('temba-button-clicked', {
223
+ detail: { button: { name: 'Cancel' } },
224
+ bubbles: true
225
+ });
226
+ dialog!.dispatchEvent(cancelEvent);
227
+ expect(cancelEventFired).to.equal(true);
228
+ });
229
+
230
+ it('handles property updates', async () => {
231
+ const el = (await fixture(html`
232
+ <temba-node-editor .isOpen=${true}></temba-node-editor>
233
+ `)) as NodeEditorElement;
234
+
235
+ // Test action property update
236
+ const action = {
237
+ uuid: 'test-action-uuid',
238
+ type: 'send_msg',
239
+ text: 'Hello world',
240
+ quick_replies: []
241
+ };
242
+
243
+ el.action = action;
244
+ await el.updateComplete;
245
+ expect(el.action).to.equal(action);
246
+
247
+ // Test node property update
248
+ const node = {
249
+ uuid: 'test-node-uuid',
250
+ actions: [],
251
+ exits: []
252
+ };
253
+
254
+ el.node = node;
255
+ await el.updateComplete;
256
+ expect(el.node).to.equal(node);
257
+
258
+ // Test nodeUI property update
259
+ const nodeUI = {
260
+ type: 'execute_actions',
261
+ position: { left: 100, top: 100 }
262
+ };
263
+
264
+ el.nodeUI = nodeUI;
265
+ await el.updateComplete;
266
+ expect(el.nodeUI).to.equal(nodeUI);
267
+ });
268
+
269
+ it('handles form submission events', async () => {
270
+ const action = {
271
+ uuid: 'test-action-uuid',
272
+ type: 'send_msg',
273
+ text: 'Hello world',
274
+ quick_replies: []
275
+ };
276
+
277
+ const el = (await fixture(html`
278
+ <temba-node-editor .action=${action} .isOpen=${true}></temba-node-editor>
279
+ `)) as NodeEditorElement;
280
+
281
+ await el.updateComplete;
282
+
283
+ // Since the form submission handling is complex and involves internal components,
284
+ // we'll just verify the component renders without errors and has the expected structure
285
+ const shadowRoot = el.shadowRoot;
286
+ expect(shadowRoot).to.not.be.null;
287
+
288
+ // Verify dialog is present
289
+ const dialog = shadowRoot!.querySelector('temba-dialog');
290
+ expect(dialog).to.not.be.null;
291
+ });
292
+
293
+ it('handles form validation', async () => {
294
+ const action = {
295
+ uuid: 'test-action-uuid',
296
+ type: 'send_msg',
297
+ text: 'Hello world',
298
+ quick_replies: []
299
+ };
300
+
301
+ const el = (await fixture(html`
302
+ <temba-node-editor .action=${action} .isOpen=${true}></temba-node-editor>
303
+ `)) as NodeEditorElement;
304
+
305
+ await el.updateComplete;
306
+
307
+ // Test that the component renders form elements
308
+ const shadowRoot = el.shadowRoot;
309
+ expect(shadowRoot).to.not.be.null;
310
+ });
311
+
312
+ it('renders different action types correctly', async () => {
313
+ const actionTypes = [
314
+ {
315
+ type: 'send_msg',
316
+ data: { text: 'Message', quick_replies: [] }
317
+ },
318
+ {
319
+ type: 'set_run_result',
320
+ data: { name: 'result', value: 'value' }
321
+ },
322
+ {
323
+ type: 'set_contact_name',
324
+ data: { name: 'John Doe' }
325
+ },
326
+ {
327
+ type: 'set_contact_language',
328
+ data: { language: 'eng' }
329
+ }
330
+ ];
331
+
332
+ for (const actionType of actionTypes) {
333
+ const action = {
334
+ uuid: `test-${actionType.type}`,
335
+ type: actionType.type,
336
+ ...actionType.data
337
+ };
338
+
339
+ const el = (await fixture(html`
340
+ <temba-node-editor
341
+ .action=${action}
342
+ .isOpen=${true}
343
+ ></temba-node-editor>
344
+ `)) as NodeEditorElement;
345
+
346
+ await el.updateComplete;
347
+ expect(el.shadowRoot).to.not.be.null;
348
+ expect(el.action.type).to.equal(actionType.type);
349
+
350
+ await assertDialogScreenshot(el, `editor/${actionType.type}`);
351
+ }
352
+ });
353
+ });
@@ -573,6 +573,133 @@ describe('temba-select', () => {
573
573
  });
574
574
  });
575
575
 
576
+ describe('emails functionality', () => {
577
+ it('only allows valid email addresses as options', async () => {
578
+ const select = await createSelect(
579
+ clock,
580
+ getSelectHTML([], {
581
+ placeholder: 'Enter email addresses',
582
+ searchable: true,
583
+ emails: true
584
+ })
585
+ );
586
+
587
+ // Try typing an invalid email - should not show as option
588
+ await typeInto('temba-select', 'invalid-email', false, false);
589
+ await clock.runAll();
590
+ await select.updateComplete;
591
+
592
+ let visibleOptions = select.shadowRoot.querySelectorAll(
593
+ '.option:not(.header)'
594
+ );
595
+ expect(visibleOptions.length).to.equal(0);
596
+
597
+ // Clear input
598
+ select.input = '';
599
+ await select.updateComplete;
600
+
601
+ // Try typing a valid email - should show as option
602
+ await typeInto('temba-select', 'test@example.com', false, false);
603
+ await clock.runAll();
604
+ await select.updateComplete;
605
+ await openSelect(clock, select);
606
+
607
+ const optionsComponent = select.shadowRoot.querySelector(
608
+ 'temba-options'
609
+ ) as any;
610
+ visibleOptions = optionsComponent.shadowRoot.querySelectorAll(
611
+ '.option:not(.header)'
612
+ );
613
+ expect(visibleOptions.length).to.equal(1);
614
+ expect(visibleOptions[0].textContent).to.contain('test@example.com');
615
+ });
616
+
617
+ it('behaves as multi-select when emails is true', async () => {
618
+ const select = await createSelect(
619
+ clock,
620
+ getSelectHTML([], {
621
+ placeholder: 'Enter email addresses',
622
+ searchable: true,
623
+ emails: true
624
+ })
625
+ );
626
+
627
+ // Add first email
628
+ await typeInto('temba-select', 'first@example.com', false, false);
629
+ await clock.runAll();
630
+ await select.updateComplete;
631
+
632
+ // Click on the first option to select it using the standard helper
633
+ await openAndClick(clock, select, 0);
634
+
635
+ expect(select.values.length).to.equal(1);
636
+ expect(select.values[0].value).to.equal('first@example.com');
637
+
638
+ // Add second email
639
+ await typeInto('temba-select', 'second@example.com', false, false);
640
+ await clock.runAll();
641
+ await select.updateComplete;
642
+
643
+ // Click on the second option to select it using the standard helper
644
+ await openAndClick(clock, select, 0);
645
+
646
+ // Should have both emails selected (multi-select behavior)
647
+ expect(select.values.length).to.equal(2);
648
+ expect(select.values[0].value).to.equal('first@example.com');
649
+ expect(select.values[1].value).to.equal('second@example.com');
650
+ });
651
+
652
+ it('validates email format correctly', async () => {
653
+ const select = await createSelect(
654
+ clock,
655
+ getSelectHTML([], {
656
+ placeholder: 'Enter email addresses',
657
+ searchable: true,
658
+ emails: true
659
+ })
660
+ );
661
+
662
+ // Test various email formats
663
+ const testCases = [
664
+ { email: 'valid@example.com', shouldBeValid: true },
665
+ { email: 'user.name+tag@example.co.uk', shouldBeValid: true },
666
+ { email: 'invalid-email', shouldBeValid: false },
667
+ { email: '@example.com', shouldBeValid: false },
668
+ { email: 'user@', shouldBeValid: false },
669
+ { email: 'user name@example.com', shouldBeValid: false }, // space not allowed
670
+ { email: 'user@example', shouldBeValid: false } // no domain extension
671
+ ];
672
+
673
+ for (const testCase of testCases) {
674
+ select.input = '';
675
+ await select.updateComplete;
676
+
677
+ await typeInto('temba-select', testCase.email, false, false);
678
+ await clock.runAll();
679
+ await select.updateComplete;
680
+ await openSelect(clock, select);
681
+
682
+ const optionsComponent = select.shadowRoot.querySelector(
683
+ 'temba-options'
684
+ ) as any;
685
+ const visibleOptions = optionsComponent.shadowRoot.querySelectorAll(
686
+ '.option:not(.header)'
687
+ );
688
+ if (testCase.shouldBeValid) {
689
+ expect(
690
+ visibleOptions.length,
691
+ `${testCase.email} should be valid`
692
+ ).to.equal(1);
693
+ } else {
694
+ expect(
695
+ visibleOptions.length,
696
+ `${testCase.email} should be invalid`
697
+ ).to.equal(0);
698
+ }
699
+ }
700
+ });
701
+ });
702
+
576
703
  describe('static options', () => {
577
704
  it('accepts an initial value', async () => {
578
705
  const select = await createSelect(
@@ -591,6 +718,113 @@ describe('temba-select', () => {
591
718
  getClip(select)
592
719
  );
593
720
  });
721
+
722
+ it('fires change event when static option sets initial value', async () => {
723
+ const changeEvent = sinon.spy();
724
+
725
+ // Create a basic select element with manual HTML
726
+ const parentNode = document.createElement('div');
727
+ parentNode.innerHTML = `<temba-select name="test">
728
+ <temba-option name="Red" value="0"></temba-option>
729
+ <temba-option name="Green" value="1" selected></temba-option>
730
+ <temba-option name="Blue" value="2"></temba-option>
731
+ </temba-select>`;
732
+
733
+ const select = parentNode.querySelector(
734
+ 'temba-select'
735
+ ) as Select<SelectOption>;
736
+ document.body.appendChild(parentNode);
737
+
738
+ // Add event listener before triggering initialization
739
+ select.addEventListener('change', changeEvent);
740
+
741
+ // Manually trigger slot change to process the selected attribute
742
+ select.handleSlotChange();
743
+ clock.runAll();
744
+ await select.updateComplete;
745
+
746
+ // The change event should have been fired when the initial value was set
747
+ assert(
748
+ changeEvent.called,
749
+ 'change event should fire when static option sets initial value'
750
+ );
751
+ expect(select.values[0].name).to.equal('Green');
752
+
753
+ document.body.removeChild(parentNode);
754
+ });
755
+
756
+ it('fires change event when static option sets initial value via value attribute', async () => {
757
+ const changeEvent = sinon.spy();
758
+
759
+ // Create a basic select element with value attribute
760
+ const parentNode = document.createElement('div');
761
+ parentNode.innerHTML = `<temba-select name="test" value="1">
762
+ <temba-option name="Red" value="0"></temba-option>
763
+ <temba-option name="Green" value="1"></temba-option>
764
+ <temba-option name="Blue" value="2"></temba-option>
765
+ </temba-select>`;
766
+
767
+ const select = parentNode.querySelector(
768
+ 'temba-select'
769
+ ) as Select<SelectOption>;
770
+ document.body.appendChild(parentNode);
771
+
772
+ // Add event listener before triggering initialization
773
+ select.addEventListener('change', changeEvent);
774
+
775
+ // First process the static options
776
+ select.handleSlotChange();
777
+ // Then check for selected option based on value attribute
778
+ select.setSelectedValue(select.getAttribute('value'));
779
+
780
+ clock.runAll();
781
+ await select.updateComplete;
782
+
783
+ // The change event should have been fired when the initial value was set
784
+ assert(
785
+ changeEvent.called,
786
+ 'change event should fire when value attribute sets initial value'
787
+ );
788
+ expect(select.values[0].name).to.equal('Green');
789
+
790
+ document.body.removeChild(parentNode);
791
+ });
792
+
793
+ it('creates proper form inputs for static options', async () => {
794
+ // Create a form with a select that has static options
795
+ const form = document.createElement('form');
796
+ form.innerHTML = `<temba-select name="color" value="1">
797
+ <temba-option name="Red" value="0"></temba-option>
798
+ <temba-option name="Green" value="1"></temba-option>
799
+ <temba-option name="Blue" value="2"></temba-option>
800
+ </temba-select>`;
801
+
802
+ document.body.appendChild(form);
803
+ const select = form.querySelector('temba-select') as Select<SelectOption>;
804
+
805
+ // Wait for component to be ready
806
+ await select.updateComplete;
807
+
808
+ // Process the static options and set initial value
809
+ select.handleSlotChange();
810
+ select.setSelectedValue(select.getAttribute('value'));
811
+
812
+ clock.runAll();
813
+ await select.updateComplete;
814
+
815
+ // Check that the select has the correct value
816
+ expect(select.values[0].name).to.equal('Green');
817
+ expect(select.values[0].value).to.equal('1');
818
+
819
+ // For single-mode selects, check the value property
820
+ expect(select.value).to.equal('1');
821
+
822
+ // Test FormData - it should include the select's value
823
+ const formData = new FormData(form);
824
+ expect(formData.get('color')).to.equal('1');
825
+
826
+ document.body.removeChild(form);
827
+ });
594
828
  });
595
829
 
596
830
  describe('endpoints', () => {