@nyaruka/temba-components 0.140.0 → 0.141.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. package/.lintstagedrc.js +10 -0
  2. package/CHANGELOG.md +22 -0
  3. package/dist/locales/es.js +5 -5
  4. package/dist/locales/es.js.map +1 -1
  5. package/dist/locales/fr.js +5 -5
  6. package/dist/locales/fr.js.map +1 -1
  7. package/dist/locales/locale-codes.js +11 -2
  8. package/dist/locales/locale-codes.js.map +1 -1
  9. package/dist/locales/pt.js +5 -5
  10. package/dist/locales/pt.js.map +1 -1
  11. package/dist/temba-components.js +263 -156
  12. package/dist/temba-components.js.map +1 -1
  13. package/out-tsc/src/display/FloatingTab.js +1 -1
  14. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  15. package/out-tsc/src/flow/CanvasNode.js +1 -1
  16. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  17. package/out-tsc/src/flow/Editor.js +239 -43
  18. package/out-tsc/src/flow/Editor.js.map +1 -1
  19. package/out-tsc/src/flow/Plumber.js +61 -14
  20. package/out-tsc/src/flow/Plumber.js.map +1 -1
  21. package/out-tsc/src/flow/actions/add_contact_groups.js +4 -1
  22. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
  23. package/out-tsc/src/flow/actions/add_input_labels.js +4 -1
  24. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  25. package/out-tsc/src/flow/actions/remove_contact_groups.js +6 -1
  26. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
  27. package/out-tsc/src/flow/actions/send_broadcast.js +6 -2
  28. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  29. package/out-tsc/src/flow/actions/set_contact_channel.js +13 -0
  30. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  31. package/out-tsc/src/flow/actions/set_contact_status.js +7 -5
  32. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  33. package/out-tsc/src/flow/actions/start_session.js +10 -3
  34. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  35. package/out-tsc/src/flow/nodes/split_by_contact_field.js +18 -5
  36. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -1
  37. package/out-tsc/src/flow/nodes/split_by_expression.js +1 -1
  38. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -1
  39. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +0 -1
  40. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  41. package/out-tsc/src/flow/nodes/split_by_random.js +0 -1
  42. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
  43. package/out-tsc/src/flow/nodes/split_by_run_result.js +10 -4
  44. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
  45. package/out-tsc/src/flow/nodes/wait_for_digits.js +1 -1
  46. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
  47. package/out-tsc/src/flow/nodes/wait_for_response.js +1 -1
  48. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  49. package/out-tsc/src/form/FieldRenderer.js +7 -0
  50. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  51. package/out-tsc/src/layout/Dialog.js +0 -1
  52. package/out-tsc/src/layout/Dialog.js.map +1 -1
  53. package/out-tsc/src/layout/Modax.js +20 -2
  54. package/out-tsc/src/layout/Modax.js.map +1 -1
  55. package/out-tsc/src/list/ContentMenu.js +14 -1
  56. package/out-tsc/src/list/ContentMenu.js.map +1 -1
  57. package/out-tsc/src/live/ContactChat.js +10 -1
  58. package/out-tsc/src/live/ContactChat.js.map +1 -1
  59. package/out-tsc/src/live/TembaChart.js.map +1 -1
  60. package/out-tsc/src/locales/es.js +5 -5
  61. package/out-tsc/src/locales/es.js.map +1 -1
  62. package/out-tsc/src/locales/fr.js +5 -5
  63. package/out-tsc/src/locales/fr.js.map +1 -1
  64. package/out-tsc/src/locales/locale-codes.js +11 -2
  65. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  66. package/out-tsc/src/locales/pt.js +5 -5
  67. package/out-tsc/src/locales/pt.js.map +1 -1
  68. package/out-tsc/src/simulator/Simulator.js +11 -0
  69. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  70. package/out-tsc/src/store/AppState.js +13 -0
  71. package/out-tsc/src/store/AppState.js.map +1 -1
  72. package/out-tsc/src/version.js +9 -0
  73. package/out-tsc/src/version.js.map +1 -0
  74. package/out-tsc/test/actions/add_contact_groups.test.js +35 -0
  75. package/out-tsc/test/actions/add_contact_groups.test.js.map +1 -1
  76. package/out-tsc/test/actions/add_input_labels.test.js +53 -0
  77. package/out-tsc/test/actions/add_input_labels.test.js.map +1 -0
  78. package/out-tsc/test/actions/enter_flow.test.js +71 -0
  79. package/out-tsc/test/actions/enter_flow.test.js.map +1 -0
  80. package/out-tsc/test/actions/remove_contact_groups.test.js +24 -0
  81. package/out-tsc/test/actions/remove_contact_groups.test.js.map +1 -1
  82. package/out-tsc/test/actions/send_broadcast.test.js +41 -0
  83. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
  84. package/out-tsc/test/actions/set_contact_channel.test.js +67 -0
  85. package/out-tsc/test/actions/set_contact_channel.test.js.map +1 -0
  86. package/out-tsc/test/actions/set_contact_field.test.js +52 -0
  87. package/out-tsc/test/actions/set_contact_field.test.js.map +1 -0
  88. package/out-tsc/test/actions/set_contact_language.test.js +39 -0
  89. package/out-tsc/test/actions/set_contact_language.test.js.map +1 -0
  90. package/out-tsc/test/actions/set_contact_name.test.js +28 -0
  91. package/out-tsc/test/actions/set_contact_name.test.js.map +1 -0
  92. package/out-tsc/test/actions/set_contact_status.test.js +44 -0
  93. package/out-tsc/test/actions/set_contact_status.test.js.map +1 -0
  94. package/out-tsc/test/actions/set_run_result.test.js +47 -0
  95. package/out-tsc/test/actions/set_run_result.test.js.map +1 -0
  96. package/out-tsc/test/actions/start_session.test.js +76 -0
  97. package/out-tsc/test/actions/start_session.test.js.map +1 -1
  98. package/out-tsc/test/nodes/split_by_contact_field.test.js +50 -0
  99. package/out-tsc/test/nodes/split_by_contact_field.test.js.map +1 -1
  100. package/out-tsc/test/nodes/split_by_run_result.test.js +82 -0
  101. package/out-tsc/test/nodes/split_by_run_result.test.js.map +1 -1
  102. package/out-tsc/test/nodes/split_by_ticket.test.js +139 -0
  103. package/out-tsc/test/nodes/split_by_ticket.test.js.map +1 -0
  104. package/out-tsc/test/nodes/split_by_webhook.test.js +111 -0
  105. package/out-tsc/test/nodes/split_by_webhook.test.js.map +1 -0
  106. package/out-tsc/test/temba-contact-chat.test.js +12 -0
  107. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  108. package/out-tsc/test/temba-flow-editor.test.js +206 -0
  109. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  110. package/out-tsc/test/temba-flow-plumber.test.js +19 -0
  111. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  112. package/out-tsc/test/temba-select.test.js +4 -1
  113. package/out-tsc/test/temba-select.test.js.map +1 -1
  114. package/out-tsc/test/utils.test.js +4 -2
  115. package/out-tsc/test/utils.test.js.map +1 -1
  116. package/package.json +3 -9
  117. package/rollup.components.mjs +7 -1
  118. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  119. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  120. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  121. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  122. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  123. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  124. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  125. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  126. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  127. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  128. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  129. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  130. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  131. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  132. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  133. package/screenshots/truth/actions/add_input_labels/editor/multiple-labels.png +0 -0
  134. package/screenshots/truth/actions/add_input_labels/editor/single-label.png +0 -0
  135. package/screenshots/truth/actions/add_input_labels/render/multiple-labels.png +0 -0
  136. package/screenshots/truth/actions/add_input_labels/render/single-label.png +0 -0
  137. package/screenshots/truth/actions/enter_flow/editor/basic-flow.png +0 -0
  138. package/screenshots/truth/actions/enter_flow/editor/long-flow-name.png +0 -0
  139. package/screenshots/truth/actions/enter_flow/render/basic-flow.png +0 -0
  140. package/screenshots/truth/actions/enter_flow/render/long-flow-name.png +0 -0
  141. package/screenshots/truth/actions/play_audio/render/expression-url.png +0 -0
  142. package/screenshots/truth/actions/play_audio/render/static-url.png +0 -0
  143. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  144. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  145. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  146. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  147. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  148. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  149. package/screenshots/truth/actions/say_msg/render/multiline-text.png +0 -0
  150. package/screenshots/truth/actions/say_msg/render/simple-text.png +0 -0
  151. package/screenshots/truth/actions/say_msg/render/text-with-audio-url.png +0 -0
  152. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  153. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  154. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  155. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  156. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  157. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  158. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  159. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  160. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  161. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  162. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  163. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  164. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  165. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  166. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  167. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  168. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  169. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  170. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  171. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  172. package/screenshots/truth/actions/set_contact_channel/editor/sms-channel.png +0 -0
  173. package/screenshots/truth/actions/set_contact_channel/editor/whatsapp-channel.png +0 -0
  174. package/screenshots/truth/actions/set_contact_channel/render/sms-channel.png +0 -0
  175. package/screenshots/truth/actions/set_contact_channel/render/whatsapp-channel.png +0 -0
  176. package/screenshots/truth/actions/set_contact_field/editor/clear-value.png +0 -0
  177. package/screenshots/truth/actions/set_contact_field/editor/set-value.png +0 -0
  178. package/screenshots/truth/actions/set_contact_field/render/clear-value.png +0 -0
  179. package/screenshots/truth/actions/set_contact_field/render/set-value.png +0 -0
  180. package/screenshots/truth/actions/set_contact_language/editor/english.png +0 -0
  181. package/screenshots/truth/actions/set_contact_language/editor/french.png +0 -0
  182. package/screenshots/truth/actions/set_contact_language/render/english.png +0 -0
  183. package/screenshots/truth/actions/set_contact_language/render/french.png +0 -0
  184. package/screenshots/truth/actions/set_contact_name/editor/expression-name.png +0 -0
  185. package/screenshots/truth/actions/set_contact_name/editor/static-name.png +0 -0
  186. package/screenshots/truth/actions/set_contact_name/render/expression-name.png +0 -0
  187. package/screenshots/truth/actions/set_contact_name/render/static-name.png +0 -0
  188. package/screenshots/truth/actions/set_contact_status/editor/active.png +0 -0
  189. package/screenshots/truth/actions/set_contact_status/editor/archived.png +0 -0
  190. package/screenshots/truth/actions/set_contact_status/editor/blocked.png +0 -0
  191. package/screenshots/truth/actions/set_contact_status/render/active.png +0 -0
  192. package/screenshots/truth/actions/set_contact_status/render/archived.png +0 -0
  193. package/screenshots/truth/actions/set_contact_status/render/blocked.png +0 -0
  194. package/screenshots/truth/actions/set_run_result/editor/expression-value.png +0 -0
  195. package/screenshots/truth/actions/set_run_result/editor/with-category.png +0 -0
  196. package/screenshots/truth/actions/set_run_result/render/expression-value.png +0 -0
  197. package/screenshots/truth/actions/set_run_result/render/with-category.png +0 -0
  198. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  199. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  200. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  201. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  202. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  203. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  204. package/screenshots/truth/editor/wait.png +0 -0
  205. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  206. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  207. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  208. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  209. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  210. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  211. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  212. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  213. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  214. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  215. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  216. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  217. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  218. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  219. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  220. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  221. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  222. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  223. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  224. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  225. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  226. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  227. package/screenshots/truth/nodes/wait_for_audio/render/basic-audio-wait.png +0 -0
  228. package/screenshots/truth/nodes/wait_for_dial/render/basic-dial.png +0 -0
  229. package/screenshots/truth/nodes/wait_for_dial/render/dial-with-limits.png +0 -0
  230. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  231. package/screenshots/truth/nodes/wait_for_digits/editor/digits-with-rules.png +0 -0
  232. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  233. package/screenshots/truth/nodes/wait_for_digits/render/digits-with-rules.png +0 -0
  234. package/screenshots/truth/nodes/wait_for_menu/render/menu-with-digits.png +0 -0
  235. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  236. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  237. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  238. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  239. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  240. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  241. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  242. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  243. package/src/display/FloatingTab.ts +1 -1
  244. package/src/flow/CanvasNode.ts +1 -1
  245. package/src/flow/Editor.ts +299 -88
  246. package/src/flow/Plumber.ts +89 -14
  247. package/src/flow/actions/add_contact_groups.ts +4 -1
  248. package/src/flow/actions/add_input_labels.ts +4 -1
  249. package/src/flow/actions/remove_contact_groups.ts +6 -1
  250. package/src/flow/actions/send_broadcast.ts +6 -2
  251. package/src/flow/actions/set_contact_channel.ts +13 -1
  252. package/src/flow/actions/set_contact_status.ts +7 -5
  253. package/src/flow/actions/start_session.ts +10 -3
  254. package/src/flow/nodes/split_by_contact_field.ts +16 -5
  255. package/src/flow/nodes/split_by_expression.ts +1 -1
  256. package/src/flow/nodes/split_by_llm_categorize.ts +0 -1
  257. package/src/flow/nodes/split_by_random.ts +0 -1
  258. package/src/flow/nodes/split_by_run_result.ts +10 -4
  259. package/src/flow/nodes/wait_for_digits.ts +2 -1
  260. package/src/flow/nodes/wait_for_response.ts +1 -1
  261. package/src/form/FieldRenderer.ts +7 -0
  262. package/src/layout/Dialog.ts +0 -1
  263. package/src/layout/Modax.ts +19 -2
  264. package/src/list/ContentMenu.ts +15 -1
  265. package/src/live/ContactChat.ts +10 -1
  266. package/src/live/TembaChart.ts +1 -1
  267. package/src/locales/es.ts +18 -13
  268. package/src/locales/fr.ts +18 -13
  269. package/src/locales/locale-codes.ts +11 -2
  270. package/src/locales/pt.ts +18 -13
  271. package/src/simulator/Simulator.ts +12 -0
  272. package/src/store/AppState.ts +15 -0
  273. package/src/store/flow-definition.d.ts +1 -0
  274. package/src/version.ts +10 -0
  275. package/test/actions/add_contact_groups.test.ts +38 -0
  276. package/test/actions/add_input_labels.test.ts +67 -0
  277. package/test/actions/enter_flow.test.ts +88 -0
  278. package/test/actions/remove_contact_groups.test.ts +29 -0
  279. package/test/actions/send_broadcast.test.ts +44 -0
  280. package/test/actions/set_contact_channel.test.ts +88 -0
  281. package/test/actions/set_contact_field.test.ts +68 -0
  282. package/test/actions/set_contact_language.test.ts +55 -0
  283. package/test/actions/set_contact_name.test.ts +39 -0
  284. package/test/actions/set_contact_status.test.ts +64 -0
  285. package/test/actions/set_run_result.test.ts +61 -0
  286. package/test/actions/start_session.test.ts +82 -0
  287. package/test/nodes/split_by_contact_field.test.ts +59 -0
  288. package/test/nodes/split_by_run_result.test.ts +100 -0
  289. package/test/nodes/split_by_ticket.test.ts +157 -0
  290. package/test/nodes/split_by_webhook.test.ts +131 -0
  291. package/test/temba-contact-chat.test.ts +17 -0
  292. package/test/temba-flow-editor.test.ts +264 -0
  293. package/test/temba-flow-plumber.test.ts +62 -0
  294. package/test/temba-select.test.ts +6 -1
  295. package/test/utils.test.ts +4 -2
  296. package/web-dev-server.config.mjs +5 -1
  297. package/web-test-runner.config.mjs +4 -1
@@ -0,0 +1,64 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { set_contact_status } from '../../src/flow/actions/set_contact_status';
3
+ import { SetContactStatus } from '../../src/store/flow-definition';
4
+ import { ActionTest } from '../ActionHelper';
5
+
6
+ /**
7
+ * Test suite for the set_contact_status action configuration.
8
+ */
9
+ describe('set_contact_status action config', () => {
10
+ const helper = new ActionTest(set_contact_status, 'set_contact_status');
11
+
12
+ describe('basic properties', () => {
13
+ helper.testBasicProperties();
14
+
15
+ it('has correct name', () => {
16
+ expect(set_contact_status.name).to.equal('Update Status');
17
+ });
18
+ });
19
+
20
+ describe('action scenarios', () => {
21
+ helper.testAction(
22
+ {
23
+ uuid: 'test-action-1',
24
+ type: 'set_contact_status',
25
+ status: 'active'
26
+ } as SetContactStatus,
27
+ 'active'
28
+ );
29
+
30
+ helper.testAction(
31
+ {
32
+ uuid: 'test-action-2',
33
+ type: 'set_contact_status',
34
+ status: 'blocked'
35
+ } as SetContactStatus,
36
+ 'blocked'
37
+ );
38
+
39
+ helper.testAction(
40
+ {
41
+ uuid: 'test-action-3',
42
+ type: 'set_contact_status',
43
+ status: 'archived'
44
+ } as SetContactStatus,
45
+ 'archived'
46
+ );
47
+ });
48
+
49
+ describe('round-trip', () => {
50
+ it('should extract status value from select option', () => {
51
+ const formData = {
52
+ uuid: 'test-uuid',
53
+ status: [{ value: 'stopped', name: 'Stopped' }]
54
+ };
55
+
56
+ const action = set_contact_status.fromFormData(
57
+ formData
58
+ ) as SetContactStatus;
59
+
60
+ expect(action.status).to.equal('stopped');
61
+ expect(action.type).to.equal('set_contact_status');
62
+ });
63
+ });
64
+ });
@@ -0,0 +1,61 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { set_run_result } from '../../src/flow/actions/set_run_result';
3
+ import { SetRunResult } from '../../src/store/flow-definition';
4
+ import { ActionTest } from '../ActionHelper';
5
+
6
+ /**
7
+ * Test suite for the set_run_result action configuration.
8
+ */
9
+ describe('set_run_result action config', () => {
10
+ const helper = new ActionTest(set_run_result, 'set_run_result');
11
+
12
+ describe('basic properties', () => {
13
+ helper.testBasicProperties();
14
+
15
+ it('has correct name', () => {
16
+ expect(set_run_result.name).to.equal('Save Flow Result');
17
+ });
18
+ });
19
+
20
+ describe('action scenarios', () => {
21
+ helper.testAction(
22
+ {
23
+ uuid: 'test-action-1',
24
+ type: 'set_run_result',
25
+ name: 'Score',
26
+ value: '100',
27
+ category: 'High'
28
+ } as SetRunResult,
29
+ 'with-category'
30
+ );
31
+
32
+ helper.testAction(
33
+ {
34
+ uuid: 'test-action-2',
35
+ type: 'set_run_result',
36
+ name: 'Response',
37
+ value: '@input.text',
38
+ category: ''
39
+ } as SetRunResult,
40
+ 'expression-value'
41
+ );
42
+ });
43
+
44
+ describe('round-trip', () => {
45
+ it('should extract name from select option array', () => {
46
+ const formData = {
47
+ uuid: 'test-uuid',
48
+ name: [{ value: 'Score', name: 'Score' }],
49
+ value: '42',
50
+ category: 'Medium'
51
+ };
52
+
53
+ const action = set_run_result.fromFormData(formData) as SetRunResult;
54
+
55
+ expect(action.name).to.equal('Score');
56
+ expect(action.value).to.equal('42');
57
+ expect(action.category).to.equal('Medium');
58
+ expect(action.type).to.equal('set_run_result');
59
+ });
60
+ });
61
+ });
@@ -148,4 +148,86 @@ describe('start_session action config', () => {
148
148
  expect(result.valid).to.be.true;
149
149
  expect(Object.keys(result.errors).length).to.equal(0);
150
150
  });
151
+
152
+ describe('metadata stripping', () => {
153
+ it('should strip superfluous API metadata from flow', () => {
154
+ const formData = {
155
+ uuid: 'test-uuid',
156
+ flow: [
157
+ {
158
+ uuid: 'flow-1',
159
+ name: 'Registration Flow',
160
+ type: 'message',
161
+ archived: false,
162
+ labels: [],
163
+ expires: 720,
164
+ runs: {
165
+ active: 0,
166
+ waiting: 5,
167
+ completed: 100,
168
+ interrupted: 2,
169
+ expired: 1,
170
+ failed: 0
171
+ },
172
+ results: [],
173
+ parent_refs: [],
174
+ created_on: '2024-01-01T00:00:00.000Z',
175
+ modified_on: '2024-06-15T12:00:00.000Z'
176
+ }
177
+ ],
178
+ startType: [{ value: 'create', name: 'Create a new contact' }]
179
+ };
180
+
181
+ const action = start_session.fromFormData(formData) as StartSession;
182
+
183
+ expect(action.flow).to.deep.equal({
184
+ uuid: 'flow-1',
185
+ name: 'Registration Flow'
186
+ });
187
+ });
188
+
189
+ it('should strip superfluous API metadata from contacts and groups', () => {
190
+ const formData = {
191
+ uuid: 'test-uuid',
192
+ flow: [{ uuid: 'flow-1', name: 'Test Flow' }],
193
+ startType: [{ value: 'manual', name: 'Select recipients manually' }],
194
+ recipients: [
195
+ {
196
+ uuid: 'contact-1',
197
+ name: 'Alice',
198
+ status: 'active',
199
+ language: 'eng',
200
+ urns: ['tel:+250788123456'],
201
+ groups: [{ uuid: 'g-1', name: 'G1' }],
202
+ fields: { age: '30' },
203
+ created_on: '2024-01-01T00:00:00.000Z',
204
+ modified_on: '2024-06-15T12:00:00.000Z',
205
+ last_seen_on: '2024-06-14T10:00:00.000Z'
206
+ },
207
+ {
208
+ uuid: 'group-1',
209
+ name: 'VIP',
210
+ group: true,
211
+ query: 'status = vip',
212
+ status: 'ready',
213
+ count: 42,
214
+ system: false
215
+ }
216
+ ]
217
+ };
218
+
219
+ const action = start_session.fromFormData(formData) as StartSession;
220
+
221
+ expect(action.contacts).to.have.lengthOf(1);
222
+ expect(action.contacts[0]).to.deep.equal({
223
+ uuid: 'contact-1',
224
+ name: 'Alice'
225
+ });
226
+ expect(action.groups).to.have.lengthOf(1);
227
+ expect(action.groups[0]).to.deep.equal({
228
+ uuid: 'group-1',
229
+ name: 'VIP'
230
+ });
231
+ });
232
+ });
151
233
  });
@@ -448,4 +448,63 @@ describe('split_by_contact_field', () => {
448
448
  expect(uiConfig.operand.name).to.equal('Facebook');
449
449
  expect(uiConfig.operand.type).to.equal('scheme');
450
450
  });
451
+
452
+ it('should handle round-trip for custom field without re-selecting', () => {
453
+ // Reproduces the bug: toUIConfig saves { id, name, type } without 'key',
454
+ // then toFormData loads it, and fromFormData must still produce a valid operand.
455
+ const originalNode: Node = {
456
+ uuid: 'test-node-uuid',
457
+ actions: [],
458
+ router: {
459
+ type: 'switch',
460
+ cases: [
461
+ {
462
+ uuid: 'case-1',
463
+ type: 'has_text',
464
+ arguments: ['red'],
465
+ category_uuid: 'cat-1'
466
+ }
467
+ ],
468
+ categories: [
469
+ { uuid: 'cat-1', name: 'Red', exit_uuid: 'exit-1' },
470
+ { uuid: 'cat-other', name: 'Other', exit_uuid: 'exit-other' }
471
+ ],
472
+ default_category_uuid: 'cat-other',
473
+ operand: '@fields.favorite_color',
474
+ result_name: ''
475
+ },
476
+ exits: [
477
+ { uuid: 'exit-1', destination_uuid: null },
478
+ { uuid: 'exit-other', destination_uuid: null }
479
+ ]
480
+ };
481
+
482
+ // nodeUI as saved by toUIConfig - note: NO 'key' property, only 'id'
483
+ const nodeUI = {
484
+ type: 'split_by_contact_field',
485
+ position: { left: 0, top: 0 },
486
+ config: {
487
+ operand: {
488
+ id: 'favorite_color',
489
+ name: 'Favorite Color',
490
+ type: 'field'
491
+ }
492
+ }
493
+ };
494
+
495
+ // Step 1: toFormData (opening the editor)
496
+ const formData = split_by_contact_field.toFormData!(originalNode, nodeUI);
497
+
498
+ // The field should have 'key' normalized from 'id'
499
+ expect(formData.field[0].key).to.equal('favorite_color');
500
+
501
+ // Step 2: fromFormData (saving without re-selecting the field)
502
+ const updatedNode = split_by_contact_field.fromFormData!(
503
+ formData,
504
+ originalNode
505
+ );
506
+
507
+ // The operand must NOT be @fields.undefined
508
+ expect(updatedNode.router!.operand).to.equal('@fields.favorite_color');
509
+ });
451
510
  });
@@ -1136,6 +1136,106 @@ describe('split_by_run_result node config', () => {
1136
1136
  });
1137
1137
  });
1138
1138
 
1139
+ describe('toFormData normalizes operand from toUIConfig', () => {
1140
+ it('should handle round-trip when operand has only id (no value)', () => {
1141
+ // Reproduces the bug: toUIConfig saves { id, name, type } without 'value',
1142
+ // then toFormData loads it, and fromFormData must still produce a valid operand.
1143
+ const originalNode: Node = {
1144
+ uuid: 'test-node-uuid',
1145
+ actions: [],
1146
+ router: {
1147
+ type: 'switch',
1148
+ operand: '@results.favorite_color',
1149
+ cases: [
1150
+ {
1151
+ uuid: 'case-1',
1152
+ type: 'has_phrase',
1153
+ arguments: ['red'],
1154
+ category_uuid: 'cat-1'
1155
+ }
1156
+ ],
1157
+ categories: [
1158
+ { uuid: 'cat-1', name: 'Red', exit_uuid: 'exit-1' },
1159
+ { uuid: 'cat-other', name: 'Other', exit_uuid: 'exit-other' }
1160
+ ],
1161
+ default_category_uuid: 'cat-other',
1162
+ result_name: ''
1163
+ },
1164
+ exits: [
1165
+ { uuid: 'exit-1', destination_uuid: null },
1166
+ { uuid: 'exit-other', destination_uuid: null }
1167
+ ]
1168
+ };
1169
+
1170
+ // nodeUI as saved by toUIConfig - note: NO 'value' property, only 'id'
1171
+ const nodeUI = {
1172
+ config: {
1173
+ operand: {
1174
+ id: 'favorite_color',
1175
+ name: 'Favorite Color',
1176
+ type: 'result'
1177
+ }
1178
+ }
1179
+ };
1180
+
1181
+ // Step 1: toFormData (opening the editor)
1182
+ const formData = split_by_run_result.toFormData!(originalNode, nodeUI);
1183
+
1184
+ // The result should have 'value' normalized from 'id'
1185
+ expect(formData.result[0].value).to.equal('favorite_color');
1186
+
1187
+ // Step 2: fromFormData (saving without re-selecting the result)
1188
+ const updatedNode = split_by_run_result.fromFormData!(
1189
+ formData,
1190
+ originalNode
1191
+ );
1192
+
1193
+ // The operand must NOT be @results.undefined
1194
+ expect(updatedNode.router!.operand).to.equal('@results.favorite_color');
1195
+ });
1196
+
1197
+ it('should handle round-trip with delimiter when operand has only id', () => {
1198
+ const originalNode: Node = {
1199
+ uuid: 'test-node-uuid',
1200
+ actions: [],
1201
+ router: {
1202
+ type: 'switch',
1203
+ operand: '@(field(results.favorite_color, 2, "."))',
1204
+ cases: [],
1205
+ categories: [
1206
+ { uuid: 'cat-other', name: 'Other', exit_uuid: 'exit-other' }
1207
+ ],
1208
+ default_category_uuid: 'cat-other',
1209
+ result_name: ''
1210
+ },
1211
+ exits: [{ uuid: 'exit-other', destination_uuid: null }]
1212
+ };
1213
+
1214
+ const nodeUI = {
1215
+ config: {
1216
+ operand: {
1217
+ id: 'favorite_color',
1218
+ name: 'Favorite Color',
1219
+ type: 'result'
1220
+ },
1221
+ index: 2,
1222
+ delimiter: '.'
1223
+ }
1224
+ };
1225
+
1226
+ const formData = split_by_run_result.toFormData!(originalNode, nodeUI);
1227
+ expect(formData.result[0].value).to.equal('favorite_color');
1228
+
1229
+ const updatedNode = split_by_run_result.fromFormData!(
1230
+ formData,
1231
+ originalNode
1232
+ );
1233
+ expect(updatedNode.router!.operand).to.equal(
1234
+ '@(field(results.favorite_color, 2, "."))'
1235
+ );
1236
+ });
1237
+ });
1238
+
1139
1239
  describe('backwards compatibility', () => {
1140
1240
  it('should support split_by_run_result_delimited type from old flows', () => {
1141
1241
  // Verify that split_by_run_result_delimited points to the same config as split_by_run_result
@@ -0,0 +1,157 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { split_by_ticket } from '../../src/flow/nodes/split_by_ticket';
3
+ import { Node, OpenTicket } from '../../src/store/flow-definition';
4
+ import { NodeTest } from '../NodeHelper';
5
+
6
+ /**
7
+ * Test suite for the split_by_ticket node configuration.
8
+ */
9
+ describe('split_by_ticket node config', () => {
10
+ const helper = new NodeTest(split_by_ticket, 'split_by_ticket');
11
+
12
+ describe('basic properties', () => {
13
+ helper.testBasicProperties();
14
+
15
+ it('has correct type and name', () => {
16
+ expect(split_by_ticket.type).to.equal('split_by_ticket');
17
+ expect(split_by_ticket.name).to.equal('Open Ticket');
18
+ });
19
+ });
20
+
21
+ describe('round-trip transformation', () => {
22
+ it('should transform from flow definition to form data', () => {
23
+ const node: Node = {
24
+ uuid: 'test-node',
25
+ actions: [
26
+ {
27
+ type: 'open_ticket',
28
+ uuid: 'action-1',
29
+ topic: { uuid: 'topic-1', name: 'General' },
30
+ assignee: { uuid: 'user-1', name: 'Alice' },
31
+ note: 'Test note'
32
+ } as OpenTicket
33
+ ],
34
+ exits: []
35
+ };
36
+
37
+ const formData = split_by_ticket.toFormData!(node);
38
+
39
+ expect(formData.uuid).to.equal('test-node');
40
+ expect(formData.topic).to.have.lengthOf(1);
41
+ expect(formData.topic[0]).to.deep.equal({
42
+ uuid: 'topic-1',
43
+ name: 'General'
44
+ });
45
+ expect(formData.assignee).to.have.lengthOf(1);
46
+ expect(formData.assignee[0]).to.deep.equal({
47
+ uuid: 'user-1',
48
+ name: 'Alice'
49
+ });
50
+ expect(formData.note).to.equal('Test note');
51
+ });
52
+
53
+ it('should transform from form data to flow definition', () => {
54
+ const originalNode: Node = {
55
+ uuid: 'test-node',
56
+ actions: [],
57
+ exits: []
58
+ };
59
+
60
+ const formData = {
61
+ uuid: 'test-node',
62
+ topic: [{ uuid: 'topic-1', name: 'General' }],
63
+ assignee: [{ uuid: 'user-1', name: 'Alice' }],
64
+ note: 'Test note'
65
+ };
66
+
67
+ const resultNode = split_by_ticket.fromFormData!(formData, originalNode);
68
+
69
+ expect(resultNode.uuid).to.equal('test-node');
70
+ expect(resultNode.actions).to.have.lengthOf(1);
71
+ expect(resultNode.actions![0].type).to.equal('open_ticket');
72
+ expect((resultNode.actions![0] as any).topic).to.deep.equal({
73
+ uuid: 'topic-1',
74
+ name: 'General'
75
+ });
76
+ expect((resultNode.actions![0] as any).assignee).to.deep.equal({
77
+ uuid: 'user-1',
78
+ name: 'Alice'
79
+ });
80
+ });
81
+
82
+ it('should strip superfluous API metadata from topic and assignee', () => {
83
+ const originalNode: Node = {
84
+ uuid: 'test-node',
85
+ actions: [],
86
+ exits: []
87
+ };
88
+
89
+ const formData = {
90
+ uuid: 'test-node',
91
+ topic: [
92
+ {
93
+ uuid: 'topic-1',
94
+ name: 'General',
95
+ created_on: '2024-01-01T00:00:00.000Z',
96
+ modified_on: '2024-06-15T12:00:00.000Z'
97
+ }
98
+ ],
99
+ assignee: [
100
+ {
101
+ uuid: 'user-1',
102
+ name: 'Alice Johnson',
103
+ email: 'alice@example.com',
104
+ first_name: 'Alice',
105
+ last_name: 'Johnson',
106
+ role: 'agent',
107
+ created_on: '2024-01-01T00:00:00.000Z'
108
+ }
109
+ ],
110
+ note: ''
111
+ };
112
+
113
+ const resultNode = split_by_ticket.fromFormData!(formData, originalNode);
114
+
115
+ const action = resultNode.actions![0] as any;
116
+ expect(action.topic).to.deep.equal({
117
+ uuid: 'topic-1',
118
+ name: 'General'
119
+ });
120
+ expect(action.assignee).to.deep.equal({
121
+ uuid: 'user-1',
122
+ name: 'Alice Johnson'
123
+ });
124
+ });
125
+
126
+ it('should handle assignee with only first_name/last_name (no name)', () => {
127
+ const originalNode: Node = {
128
+ uuid: 'test-node',
129
+ actions: [],
130
+ exits: []
131
+ };
132
+
133
+ const formData = {
134
+ uuid: 'test-node',
135
+ topic: [{ uuid: 'topic-1', name: 'General' }],
136
+ assignee: [
137
+ {
138
+ uuid: 'user-1',
139
+ first_name: 'Bob',
140
+ last_name: 'Smith',
141
+ email: 'bob@example.com',
142
+ role: 'agent'
143
+ }
144
+ ],
145
+ note: ''
146
+ };
147
+
148
+ const resultNode = split_by_ticket.fromFormData!(formData, originalNode);
149
+
150
+ const action = resultNode.actions![0] as any;
151
+ expect(action.assignee).to.deep.equal({
152
+ uuid: 'user-1',
153
+ name: 'Bob Smith'
154
+ });
155
+ });
156
+ });
157
+ });
@@ -0,0 +1,131 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { split_by_webhook } from '../../src/flow/nodes/split_by_webhook';
3
+ import { Node, CallWebhook } from '../../src/store/flow-definition';
4
+ import { NodeTest } from '../NodeHelper';
5
+
6
+ /**
7
+ * Test suite for the split_by_webhook node configuration.
8
+ */
9
+ describe('split_by_webhook node config', () => {
10
+ const helper = new NodeTest(split_by_webhook, 'split_by_webhook');
11
+
12
+ describe('basic properties', () => {
13
+ helper.testBasicProperties();
14
+
15
+ it('has correct type and name', () => {
16
+ expect(split_by_webhook.type).to.equal('split_by_webhook');
17
+ expect(split_by_webhook.name).to.equal('Call Webhook');
18
+ });
19
+ });
20
+
21
+ describe('round-trip transformation', () => {
22
+ it('should transform from flow definition to form data', () => {
23
+ const node: Node = {
24
+ uuid: 'test-node',
25
+ actions: [
26
+ {
27
+ type: 'call_webhook',
28
+ uuid: 'action-1',
29
+ method: 'POST',
30
+ url: 'https://example.com/webhook',
31
+ headers: { Authorization: 'Bearer token123' },
32
+ body: '{"key": "value"}'
33
+ } as CallWebhook
34
+ ],
35
+ exits: []
36
+ };
37
+
38
+ const formData = split_by_webhook.toFormData!(node);
39
+
40
+ expect(formData.uuid).to.equal('test-node');
41
+ expect(formData.method).to.equal('POST');
42
+ expect(formData.url).to.equal('https://example.com/webhook');
43
+ expect(formData.headers).to.deep.equal({
44
+ Authorization: 'Bearer token123'
45
+ });
46
+ expect(formData.body).to.equal('{"key": "value"}');
47
+ });
48
+
49
+ it('should transform from form data to flow definition (GET)', () => {
50
+ const originalNode: Node = {
51
+ uuid: 'test-node',
52
+ actions: [],
53
+ exits: []
54
+ };
55
+
56
+ const formData = {
57
+ uuid: 'test-node',
58
+ method: [{ value: 'GET', name: 'GET' }],
59
+ url: 'https://example.com/api',
60
+ headers: [],
61
+ body: ''
62
+ };
63
+
64
+ const resultNode = split_by_webhook.fromFormData!(formData, originalNode);
65
+
66
+ expect(resultNode.uuid).to.equal('test-node');
67
+ expect(resultNode.actions).to.have.lengthOf(1);
68
+ expect(resultNode.actions![0].type).to.equal('call_webhook');
69
+
70
+ const action = resultNode.actions![0] as any;
71
+ expect(action.method).to.equal('GET');
72
+ expect(action.url).to.equal('https://example.com/api');
73
+
74
+ // Should have Success/Failure router
75
+ expect(resultNode.router).to.exist;
76
+ expect(resultNode.router!.type).to.equal('switch');
77
+ expect(resultNode.exits).to.have.lengthOf(2);
78
+ });
79
+
80
+ it('should transform from form data to flow definition (POST)', () => {
81
+ const originalNode: Node = {
82
+ uuid: 'test-node',
83
+ actions: [],
84
+ exits: []
85
+ };
86
+
87
+ const formData = {
88
+ uuid: 'test-node',
89
+ method: [{ value: 'POST', name: 'POST' }],
90
+ url: 'https://example.com/webhook',
91
+ headers: [{ name: 'Content-Type', value: 'application/json' }],
92
+ body: '{"data": "@contact.name"}'
93
+ };
94
+
95
+ const resultNode = split_by_webhook.fromFormData!(formData, originalNode);
96
+
97
+ const action = resultNode.actions![0] as any;
98
+ expect(action.method).to.equal('POST');
99
+ expect(action.url).to.equal('https://example.com/webhook');
100
+ expect(action.headers).to.have.lengthOf(1);
101
+ expect(action.body).to.equal('{"data": "@contact.name"}');
102
+ });
103
+
104
+ it('should preserve action UUID on re-save', () => {
105
+ const originalNode: Node = {
106
+ uuid: 'test-node',
107
+ actions: [
108
+ {
109
+ type: 'call_webhook',
110
+ uuid: 'existing-action-uuid',
111
+ method: 'GET',
112
+ url: 'https://example.com/old'
113
+ } as CallWebhook
114
+ ],
115
+ exits: []
116
+ };
117
+
118
+ const formData = {
119
+ uuid: 'test-node',
120
+ method: [{ value: 'GET', name: 'GET' }],
121
+ url: 'https://example.com/new',
122
+ headers: [],
123
+ body: ''
124
+ };
125
+
126
+ const resultNode = split_by_webhook.fromFormData!(formData, originalNode);
127
+
128
+ expect(resultNode.actions![0].uuid).to.equal('existing-action-uuid');
129
+ });
130
+ });
131
+ });
@@ -116,6 +116,23 @@ describe('temba-contact-chat', () => {
116
116
  await assertScreenshot('contacts/chat-for-stopped-contact', getClip(chat));
117
117
  });
118
118
 
119
+ it('keeps flow footer from blocking scrollbar drag interactions', async () => {
120
+ await loadStore();
121
+ const chat: ContactChat = await getContactChat({
122
+ contact: 'contact-dave-active'
123
+ });
124
+
125
+ const flowFooter = chat.shadowRoot.querySelector(
126
+ '.flow-footer'
127
+ ) as HTMLElement;
128
+ const inFlow = flowFooter.querySelector('.in-flow') as HTMLElement;
129
+
130
+ expect(flowFooter).to.exist;
131
+ expect(inFlow).to.exist;
132
+ expect(getComputedStyle(flowFooter).pointerEvents).to.equal('none');
133
+ expect(getComputedStyle(inFlow).pointerEvents).to.equal('auto');
134
+ });
135
+
119
136
  it('sends text without attachments', async () => {
120
137
  // we are a StoreElement, so load a store first
121
138
  await loadStore();