@nyaruka/temba-components 0.129.3 → 0.129.4

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 +18 -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 +1200 -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 +77 -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 +85 -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 +1443 -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 +89 -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 +127 -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,130 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { send_msg } from '../../src/flow/actions/send_msg';
3
+ import { SendMsg } from '../../src/store/flow-definition';
4
+ import { ActionTest } from '../ActionHelper';
5
+
6
+ /**
7
+ * Test suite for the send_msg action configuration.
8
+ */
9
+ describe('send_msg action config', () => {
10
+ const helper = new ActionTest(send_msg, 'send_msg');
11
+
12
+ describe('basic properties', () => {
13
+ helper.testBasicProperties();
14
+
15
+ it('has correct name', () => {
16
+ expect(send_msg.name).to.equal('Send Message');
17
+ });
18
+ });
19
+
20
+ describe('action scenarios', () => {
21
+ helper.testAction(
22
+ {
23
+ uuid: 'test-action-1',
24
+ type: 'send_msg',
25
+ text: 'Hello world!',
26
+ quick_replies: []
27
+ } as SendMsg,
28
+ 'simple-text'
29
+ );
30
+
31
+ helper.testAction(
32
+ {
33
+ uuid: 'test-action-2',
34
+ type: 'send_msg',
35
+ text: 'Hello\nworld!\nHow are you?',
36
+ quick_replies: []
37
+ } as SendMsg,
38
+ 'text-with-linebreaks'
39
+ );
40
+
41
+ helper.testAction(
42
+ {
43
+ uuid: 'test-action-3',
44
+ type: 'send_msg',
45
+ text: 'Choose an option:',
46
+ quick_replies: ['Yes', 'No', 'Maybe']
47
+ } as SendMsg,
48
+ 'text-with-quick-replies'
49
+ );
50
+
51
+ helper.testAction(
52
+ {
53
+ uuid: 'test-action-4',
54
+ type: 'send_msg',
55
+ text: 'Rate our service:',
56
+ quick_replies: [
57
+ '⭐',
58
+ '⭐⭐',
59
+ '⭐⭐⭐',
60
+ '⭐⭐⭐⭐',
61
+ '⭐⭐⭐⭐⭐',
62
+ 'Not applicable'
63
+ ]
64
+ } as SendMsg,
65
+ 'text-with-many-quick-replies'
66
+ );
67
+
68
+ helper.testAction(
69
+ {
70
+ uuid: 'test-action-5',
71
+ type: 'send_msg',
72
+ text: 'Welcome to our service!\n\nPlease choose from the following options:\n- Option A: Basic plan\n- Option B: Premium plan\n- Option C: Enterprise plan',
73
+ quick_replies: ['Basic', 'Premium', 'Enterprise']
74
+ } as SendMsg,
75
+ 'multiline-text-with-replies'
76
+ );
77
+
78
+ helper.testAction(
79
+ {
80
+ uuid: 'test-action-6',
81
+ type: 'send_msg',
82
+ text: 'Which department would you like to contact?',
83
+ quick_replies: [
84
+ 'Customer Support Department',
85
+ 'Technical Support Team',
86
+ 'Billing and Accounts Department',
87
+ 'Sales and Marketing Division'
88
+ ]
89
+ } as SendMsg,
90
+ 'long-quick-replies'
91
+ );
92
+
93
+ helper.testAction(
94
+ {
95
+ uuid: 'test-action-7',
96
+ type: 'send_msg',
97
+ text: 'This action definition is missing quick_replies altogether.'
98
+ } as SendMsg,
99
+ 'text-without-quick-replies'
100
+ );
101
+ });
102
+
103
+ describe('validation edge cases', () => {
104
+ it('fails validation for empty text', () => {
105
+ const action: SendMsg = {
106
+ uuid: 'test-action',
107
+ type: 'send_msg',
108
+ text: '',
109
+ quick_replies: []
110
+ };
111
+
112
+ const result = send_msg.validate(action);
113
+ expect(result.valid).to.be.false;
114
+ expect(result.errors.text).to.equal('Message text is required');
115
+ });
116
+
117
+ it('fails validation for whitespace-only text', () => {
118
+ const action: SendMsg = {
119
+ uuid: 'test-action',
120
+ type: 'send_msg',
121
+ text: ' \n\t ',
122
+ quick_replies: []
123
+ };
124
+
125
+ const result = send_msg.validate(action);
126
+ expect(result.valid).to.be.false;
127
+ expect(result.errors.text).to.equal('Message text is required');
128
+ });
129
+ });
130
+ });
@@ -0,0 +1,240 @@
1
+ import { expect, fixture, html } from '@open-wc/testing';
2
+ import { CanvasNode } from '../src/flow/CanvasNode';
3
+ import { SendMsg, Node } from '../src/store/flow-definition';
4
+ import { CustomEventType } from '../src/interfaces';
5
+ import '../temba-modules';
6
+
7
+ describe('Action Editing Integration', () => {
8
+ it('should handle complete action editing workflow', async () => {
9
+ // Create a test node with a send_msg action
10
+ const testNode: Node = {
11
+ uuid: 'test-node',
12
+ actions: [
13
+ {
14
+ type: 'send_msg',
15
+ uuid: 'test-action',
16
+ text: 'Hello world',
17
+ quick_replies: []
18
+ } as SendMsg
19
+ ],
20
+ exits: []
21
+ };
22
+
23
+ // Create EditorNode
24
+ const editorNode: CanvasNode = await fixture(html`
25
+ <temba-flow-node
26
+ .node=${testNode}
27
+ .ui=${{ position: { left: 0, top: 0 } }}
28
+ ></temba-flow-node>
29
+ `);
30
+
31
+ await editorNode.updateComplete;
32
+
33
+ // Verify action is rendered
34
+ const actionElement = editorNode.querySelector('.action');
35
+ expect(actionElement).to.exist;
36
+
37
+ // Set up event listener to catch ActionEditRequested event
38
+ let editRequestedEvent: CustomEvent | null = null;
39
+ editorNode.addEventListener(
40
+ CustomEventType.ActionEditRequested,
41
+ (event: CustomEvent) => {
42
+ editRequestedEvent = event;
43
+ }
44
+ );
45
+
46
+ // Simulate clicking on the action content (not the remove button)
47
+ const actionContent = editorNode.querySelector(
48
+ '.action-content'
49
+ ) as HTMLElement;
50
+ expect(actionContent).to.exist;
51
+
52
+ // Simulate a click (mousedown followed by mouseup at same position)
53
+ const mouseDownEvent = new MouseEvent('mousedown', {
54
+ clientX: 100,
55
+ clientY: 100,
56
+ bubbles: true
57
+ });
58
+ const mouseUpEvent = new MouseEvent('mouseup', {
59
+ clientX: 100,
60
+ clientY: 100,
61
+ bubbles: true
62
+ });
63
+
64
+ actionContent.dispatchEvent(mouseDownEvent);
65
+ actionContent.dispatchEvent(mouseUpEvent);
66
+
67
+ // Verify event was fired
68
+ expect(editRequestedEvent).to.exist;
69
+ expect(editRequestedEvent!.detail.action).to.deep.equal(
70
+ testNode.actions[0]
71
+ );
72
+ expect(editRequestedEvent!.detail.nodeUuid).to.equal('test-node');
73
+ });
74
+
75
+ it('should ignore clicks on remove button', async () => {
76
+ // Create a test node with a send_msg action
77
+ const testNode: Node = {
78
+ uuid: 'test-node',
79
+ actions: [
80
+ {
81
+ type: 'send_msg',
82
+ uuid: 'test-action',
83
+ text: 'Hello world',
84
+ quick_replies: []
85
+ } as SendMsg
86
+ ],
87
+ exits: []
88
+ };
89
+
90
+ // Create EditorNode
91
+ const editorNode: CanvasNode = await fixture(html`
92
+ <temba-flow-node
93
+ .node=${testNode}
94
+ .ui=${{ position: { left: 0, top: 0 } }}
95
+ ></temba-flow-node>
96
+ `);
97
+
98
+ await editorNode.updateComplete;
99
+
100
+ // Set up event listener to catch ActionEditRequested event
101
+ let editRequestedEvent: CustomEvent | null = null;
102
+ editorNode.addEventListener(
103
+ CustomEventType.ActionEditRequested,
104
+ (event: CustomEvent) => {
105
+ editRequestedEvent = event;
106
+ }
107
+ );
108
+
109
+ // Simulate clicking on the remove button
110
+ const removeButton = editorNode.querySelector(
111
+ '.remove-button'
112
+ ) as HTMLElement;
113
+ expect(removeButton).to.exist;
114
+
115
+ removeButton.click();
116
+
117
+ // Verify NO ActionEditRequested event was fired (only remove action handling)
118
+ expect(editRequestedEvent).to.be.null;
119
+ });
120
+
121
+ it('should not open action editor when dragging beyond threshold', async () => {
122
+ // Create a test node with a send_msg action
123
+ const testNode: Node = {
124
+ uuid: 'test-node',
125
+ actions: [
126
+ {
127
+ type: 'send_msg',
128
+ uuid: 'test-action',
129
+ text: 'Hello world',
130
+ quick_replies: []
131
+ } as SendMsg
132
+ ],
133
+ exits: []
134
+ };
135
+
136
+ // Create EditorNode
137
+ const editorNode: CanvasNode = await fixture(html`
138
+ <temba-flow-node
139
+ .node=${testNode}
140
+ .ui=${{ position: { left: 0, top: 0 } }}
141
+ ></temba-flow-node>
142
+ `);
143
+
144
+ await editorNode.updateComplete;
145
+
146
+ // Set up event listener to catch ActionEditRequested event
147
+ let editRequestedEvent: CustomEvent | null = null;
148
+ editorNode.addEventListener(
149
+ CustomEventType.ActionEditRequested,
150
+ (event: CustomEvent) => {
151
+ editRequestedEvent = event;
152
+ }
153
+ );
154
+
155
+ // Simulate a drag operation (mousedown followed by mouseup at different position beyond threshold)
156
+ const actionContent = editorNode.querySelector(
157
+ '.action-content'
158
+ ) as HTMLElement;
159
+ expect(actionContent).to.exist;
160
+
161
+ const mouseDownEvent = new MouseEvent('mousedown', {
162
+ clientX: 100,
163
+ clientY: 100,
164
+ bubbles: true
165
+ });
166
+ const mouseUpEvent = new MouseEvent('mouseup', {
167
+ clientX: 110, // 10 pixels away, beyond the 5 pixel threshold
168
+ clientY: 100,
169
+ bubbles: true
170
+ });
171
+
172
+ actionContent.dispatchEvent(mouseDownEvent);
173
+ actionContent.dispatchEvent(mouseUpEvent);
174
+
175
+ // Verify NO ActionEditRequested event was fired because we dragged beyond threshold
176
+ expect(editRequestedEvent).to.be.null;
177
+ });
178
+
179
+ it('should open action editor when clicking within threshold', async () => {
180
+ // Create a test node with a send_msg action
181
+ const testNode: Node = {
182
+ uuid: 'test-node',
183
+ actions: [
184
+ {
185
+ type: 'send_msg',
186
+ uuid: 'test-action',
187
+ text: 'Hello world',
188
+ quick_replies: []
189
+ } as SendMsg
190
+ ],
191
+ exits: []
192
+ };
193
+
194
+ // Create EditorNode
195
+ const editorNode: CanvasNode = await fixture(html`
196
+ <temba-flow-node
197
+ .node=${testNode}
198
+ .ui=${{ position: { left: 0, top: 0 } }}
199
+ ></temba-flow-node>
200
+ `);
201
+
202
+ await editorNode.updateComplete;
203
+
204
+ // Set up event listener to catch ActionEditRequested event
205
+ let editRequestedEvent: CustomEvent | null = null;
206
+ editorNode.addEventListener(
207
+ CustomEventType.ActionEditRequested,
208
+ (event: CustomEvent) => {
209
+ editRequestedEvent = event;
210
+ }
211
+ );
212
+
213
+ // Simulate a small movement (mousedown followed by mouseup at position within threshold)
214
+ const actionContent = editorNode.querySelector(
215
+ '.action-content'
216
+ ) as HTMLElement;
217
+ expect(actionContent).to.exist;
218
+
219
+ const mouseDownEvent = new MouseEvent('mousedown', {
220
+ clientX: 100,
221
+ clientY: 100,
222
+ bubbles: true
223
+ });
224
+ const mouseUpEvent = new MouseEvent('mouseup', {
225
+ clientX: 103, // 3 pixels away, within the 5 pixel threshold
226
+ clientY: 102, // 2 pixels away
227
+ bubbles: true
228
+ });
229
+
230
+ actionContent.dispatchEvent(mouseDownEvent);
231
+ actionContent.dispatchEvent(mouseUpEvent);
232
+
233
+ // Verify ActionEditRequested event was fired because we stayed within threshold
234
+ expect(editRequestedEvent).to.exist;
235
+ expect(editRequestedEvent!.detail.action).to.deep.equal(
236
+ testNode.actions[0]
237
+ );
238
+ expect(editRequestedEvent!.detail.nodeUuid).to.equal('test-node');
239
+ });
240
+ });
@@ -71,7 +71,7 @@ describe('temba-checkbox', () => {
71
71
  const el: Checkbox = await fixture(html`
72
72
  <temba-checkbox name="My Checkbox"></temba-checkbox>
73
73
  `);
74
- expect(el.label).to.equal(null);
74
+ expect(el.label).to.equal(undefined);
75
75
  //the ".wrapper.label" style results in the background hover effect
76
76
  const wrapperDivEl = el.shadowRoot.querySelector(
77
77
  'div.wrapper.label'
@@ -0,0 +1,152 @@
1
+ import { html, fixture, expect } from '@open-wc/testing';
2
+ import '../src/form/KeyValueEditor';
3
+ import '../src/form/ArrayEditor';
4
+
5
+ describe('Field Configuration System', () => {
6
+ describe('KeyValueEditor', () => {
7
+ it('should render with empty value and always show one empty row', async () => {
8
+ const el = await fixture(html`
9
+ <temba-key-value-editor></temba-key-value-editor>
10
+ `);
11
+
12
+ expect(el).to.exist;
13
+ // Should always have at least one row (empty row)
14
+ const rows = el.shadowRoot?.querySelectorAll('.row');
15
+ expect(rows?.length).to.equal(1);
16
+ // Should not have add button anymore
17
+ expect(el.shadowRoot?.querySelector('.add-btn')).to.not.exist;
18
+ });
19
+
20
+ it('should render with initial values and maintain empty row', async () => {
21
+ const initialValue = {
22
+ 'Content-Type': 'application/json',
23
+ Authorization: 'Bearer token123'
24
+ };
25
+
26
+ const el = await fixture(html`
27
+ <temba-key-value-editor .value=${initialValue}></temba-key-value-editor>
28
+ `);
29
+
30
+ expect(el).to.exist;
31
+ // Should have 2 data rows + 1 empty row = 3 total
32
+ const rows = el.shadowRoot?.querySelectorAll('.row');
33
+ expect(rows?.length).to.equal(3);
34
+ });
35
+
36
+ it('should emit clean values without empty rows', async () => {
37
+ const el = await fixture(html`
38
+ <temba-key-value-editor></temba-key-value-editor>
39
+ `);
40
+
41
+ let changeEvent: any = null;
42
+ el.addEventListener('change', (e) => {
43
+ changeEvent = e;
44
+ });
45
+
46
+ // Trigger a field change that should cause an update
47
+ const keyInput = el.shadowRoot?.querySelector('temba-textinput') as any;
48
+ if (keyInput) {
49
+ keyInput.value = 'test-key';
50
+ keyInput.dispatchEvent(new Event('change'));
51
+ }
52
+
53
+ await (el as any).updateComplete;
54
+
55
+ expect(changeEvent).to.exist;
56
+ // Should emit the array format with key-value pairs
57
+ expect(changeEvent.detail.value).to.be.an('array');
58
+ expect(changeEvent.detail.value).to.deep.include({
59
+ key: 'test-key',
60
+ value: ''
61
+ });
62
+ });
63
+
64
+ it('should hide remove button for empty rows', async () => {
65
+ const initialValue = {
66
+ 'Content-Type': 'application/json'
67
+ };
68
+
69
+ const el = await fixture(html`
70
+ <temba-key-value-editor .value=${initialValue}></temba-key-value-editor>
71
+ `);
72
+
73
+ expect(el).to.exist;
74
+ const rows = el.shadowRoot?.querySelectorAll('.row');
75
+ expect(rows?.length).to.equal(2); // 1 data row + 1 empty row
76
+
77
+ // First row (with data) should have remove button
78
+ const firstRowRemoveBtn = rows?.[0]?.querySelector('.remove-btn');
79
+ expect(firstRowRemoveBtn).to.exist;
80
+
81
+ // Second row (empty) should have spacer instead of remove button
82
+ const secondRowRemoveBtn = rows?.[1]?.querySelector('.remove-btn');
83
+ const secondRowSpacer = rows?.[1]?.querySelector('.remove-btn-spacer');
84
+ expect(secondRowRemoveBtn).to.not.exist;
85
+ expect(secondRowSpacer).to.exist;
86
+ });
87
+
88
+ it('should show remove button when empty row gets content', async () => {
89
+ const el = await fixture(html`
90
+ <temba-key-value-editor></temba-key-value-editor>
91
+ `);
92
+
93
+ // Initially should have no remove button (empty row)
94
+ let rows = el.shadowRoot?.querySelectorAll('.row');
95
+ expect(rows?.[0]?.querySelector('.remove-btn')).to.not.exist;
96
+ expect(rows?.[0]?.querySelector('.remove-btn-spacer')).to.exist;
97
+
98
+ // Simulate adding content by setting value and triggering update
99
+ (el as any).value = { 'test-key': '' };
100
+ (el as any).requestUpdate();
101
+ await (el as any).updateComplete;
102
+
103
+ // Now should have remove button for the row with content
104
+ rows = el.shadowRoot?.querySelectorAll('.row');
105
+ expect(rows?.length).to.equal(2); // row with content + empty row
106
+ expect(rows?.[0]?.querySelector('.remove-btn')).to.exist;
107
+ expect(rows?.[1]?.querySelector('.remove-btn-spacer')).to.exist;
108
+ });
109
+ });
110
+
111
+ describe('ArrayEditor', () => {
112
+ it('should render with empty array', async () => {
113
+ const itemConfig = {
114
+ name: { type: 'text', label: 'Name', required: true },
115
+ value: { type: 'text', label: 'Value' }
116
+ };
117
+
118
+ const el = await fixture(html`
119
+ <temba-array-editor .itemConfig=${itemConfig}></temba-array-editor>
120
+ `);
121
+
122
+ await (el as any).updateComplete;
123
+
124
+ expect(el).to.exist;
125
+ expect(el.shadowRoot?.querySelector('.add-btn')).to.exist;
126
+ });
127
+
128
+ it('should render with initial items', async () => {
129
+ const itemConfig = {
130
+ operator: { type: 'text', label: 'Operator' },
131
+ value: { type: 'text', label: 'Value' }
132
+ };
133
+
134
+ const initialValue = [
135
+ { operator: 'equals', value: 'test' },
136
+ { operator: 'contains', value: 'example' }
137
+ ];
138
+
139
+ const el = await fixture(html`
140
+ <temba-array-editor
141
+ .value=${initialValue}
142
+ .itemConfig=${itemConfig}
143
+ itemLabel="Rule"
144
+ ></temba-array-editor>
145
+ `);
146
+
147
+ expect(el).to.exist;
148
+ const items = el.shadowRoot?.querySelectorAll('.array-item');
149
+ expect(items?.length).to.equal(2);
150
+ });
151
+ });
152
+ });
@@ -1,6 +1,6 @@
1
1
  import '../temba-modules';
2
2
  import { html, fixture, expect } from '@open-wc/testing';
3
- import { EditorNode } from '../src/flow/EditorNode';
3
+ import { CanvasNode } from '../src/flow/CanvasNode';
4
4
  import {
5
5
  Node,
6
6
  NodeUI,
@@ -12,7 +12,7 @@ import { stub, restore, useFakeTimers } from 'sinon';
12
12
  import { CustomEventType } from '../src/interfaces';
13
13
 
14
14
  describe('EditorNode', () => {
15
- let editorNode: EditorNode;
15
+ let editorNode: CanvasNode;
16
16
  let mockPlumber: any;
17
17
 
18
18
  beforeEach(async () => {
@@ -30,14 +30,14 @@ describe('EditorNode', () => {
30
30
 
31
31
  describe('basic functionality', () => {
32
32
  it('creates render root as element itself', () => {
33
- const editorNode = new EditorNode();
33
+ const editorNode = new CanvasNode();
34
34
  expect(editorNode.createRenderRoot()).to.equal(editorNode);
35
35
  });
36
36
  });
37
37
 
38
38
  describe('renderAction', () => {
39
39
  beforeEach(() => {
40
- editorNode = new EditorNode();
40
+ editorNode = new CanvasNode();
41
41
  });
42
42
 
43
43
  it('renders action with known config', () => {
@@ -77,7 +77,7 @@ describe('EditorNode', () => {
77
77
 
78
78
  describe('renderRouter', () => {
79
79
  beforeEach(() => {
80
- editorNode = new EditorNode();
80
+ editorNode = new CanvasNode();
81
81
  });
82
82
 
83
83
  it('renders router with result name', () => {
@@ -129,7 +129,7 @@ describe('EditorNode', () => {
129
129
 
130
130
  describe('renderCategories', () => {
131
131
  beforeEach(() => {
132
- editorNode = new EditorNode();
132
+ editorNode = new CanvasNode();
133
133
  });
134
134
 
135
135
  it('returns null when no router', () => {
@@ -179,7 +179,7 @@ describe('EditorNode', () => {
179
179
 
180
180
  describe('renderExit', () => {
181
181
  beforeEach(() => {
182
- editorNode = new EditorNode();
182
+ editorNode = new CanvasNode();
183
183
  });
184
184
 
185
185
  it('renders exit with connected class when destination exists', async () => {
@@ -214,7 +214,7 @@ describe('EditorNode', () => {
214
214
 
215
215
  describe('renderTitle', () => {
216
216
  beforeEach(() => {
217
- editorNode = new EditorNode();
217
+ editorNode = new CanvasNode();
218
218
  });
219
219
 
220
220
  it('renders title with config color and name', async () => {
@@ -235,7 +235,7 @@ describe('EditorNode', () => {
235
235
 
236
236
  describe('updated lifecycle', () => {
237
237
  it('handles updated without node changes', () => {
238
- editorNode = new EditorNode();
238
+ editorNode = new CanvasNode();
239
239
  (editorNode as any).plumber = mockPlumber;
240
240
 
241
241
  const changes = new Map();
@@ -250,12 +250,12 @@ describe('EditorNode', () => {
250
250
  });
251
251
 
252
252
  it('verifies updated method exists', () => {
253
- editorNode = new EditorNode();
253
+ editorNode = new CanvasNode();
254
254
  expect(typeof (editorNode as any).updated).to.equal('function');
255
255
  });
256
256
 
257
257
  it('processes node changes and calls plumber methods', () => {
258
- editorNode = new EditorNode();
258
+ editorNode = new CanvasNode();
259
259
  (editorNode as any).plumber = mockPlumber;
260
260
 
261
261
  const mockNode: Node = {
@@ -321,7 +321,7 @@ describe('EditorNode', () => {
321
321
  };
322
322
 
323
323
  // Test individual render methods work
324
- editorNode = new EditorNode();
324
+ editorNode = new CanvasNode();
325
325
 
326
326
  // Test renderAction
327
327
  const actionResult = (editorNode as any).renderAction(
@@ -343,10 +343,10 @@ describe('EditorNode', () => {
343
343
  });
344
344
 
345
345
  describe('drag and drop functionality', () => {
346
- let editorNode: EditorNode;
346
+ let editorNode: CanvasNode;
347
347
 
348
348
  beforeEach(() => {
349
- editorNode = new EditorNode();
349
+ editorNode = new CanvasNode();
350
350
  });
351
351
 
352
352
  it('renders actions with sortable class and proper IDs', async () => {
@@ -414,7 +414,7 @@ describe('EditorNode', () => {
414
414
  exits: []
415
415
  };
416
416
 
417
- let editorNode: EditorNode = await fixture(
417
+ let editorNode: CanvasNode = await fixture(
418
418
  html`<temba-flow-node
419
419
  .node=${mockNode}
420
420
  .ui=${{ position: { left: 0, top: 0 } }}
@@ -681,7 +681,7 @@ describe('EditorNode', () => {
681
681
 
682
682
  (window as any).getStore = () => mockStore;
683
683
 
684
- editorNode = new EditorNode();
684
+ editorNode = new CanvasNode();
685
685
  (editorNode as any).plumber = mockPlumber;
686
686
  });
687
687
 
@@ -814,12 +814,12 @@ describe('EditorNode', () => {
814
814
  });
815
815
 
816
816
  describe('action removal', () => {
817
- let editorNode: EditorNode;
817
+ let editorNode: CanvasNode;
818
818
  let mockPlumber: any;
819
819
  let getStoreStub: any;
820
820
 
821
821
  beforeEach(() => {
822
- editorNode = new EditorNode();
822
+ editorNode = new CanvasNode();
823
823
 
824
824
  // Mock plumber
825
825
  mockPlumber = {