@nyaruka/temba-components 0.139.0 → 0.141.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/.github/workflows/cla.yml +1 -1
  2. package/.github/workflows/copilot-setup-steps.yml +6 -1
  3. package/.lintstagedrc.js +10 -0
  4. package/CHANGELOG.md +32 -0
  5. package/demo/data/flows/sample-flow.json +24 -0
  6. package/dist/locales/es.js +5 -5
  7. package/dist/locales/es.js.map +1 -1
  8. package/dist/locales/fr.js +5 -5
  9. package/dist/locales/fr.js.map +1 -1
  10. package/dist/locales/locale-codes.js +11 -2
  11. package/dist/locales/locale-codes.js.map +1 -1
  12. package/dist/locales/pt.js +5 -5
  13. package/dist/locales/pt.js.map +1 -1
  14. package/dist/temba-components.js +702 -338
  15. package/dist/temba-components.js.map +1 -1
  16. package/out-tsc/src/display/Chat.js +10 -7
  17. package/out-tsc/src/display/Chat.js.map +1 -1
  18. package/out-tsc/src/display/Dropdown.js +3 -1
  19. package/out-tsc/src/display/Dropdown.js.map +1 -1
  20. package/out-tsc/src/display/FloatingTab.js +4 -4
  21. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  22. package/out-tsc/src/display/Thumbnail.js +163 -5
  23. package/out-tsc/src/display/Thumbnail.js.map +1 -1
  24. package/out-tsc/src/flow/CanvasNode.js +65 -23
  25. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  26. package/out-tsc/src/flow/Editor.js +369 -49
  27. package/out-tsc/src/flow/Editor.js.map +1 -1
  28. package/out-tsc/src/flow/NodeEditor.js +118 -10
  29. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  30. package/out-tsc/src/flow/Plumber.js +61 -14
  31. package/out-tsc/src/flow/Plumber.js.map +1 -1
  32. package/out-tsc/src/flow/StickyNote.js +13 -4
  33. package/out-tsc/src/flow/StickyNote.js.map +1 -1
  34. package/out-tsc/src/flow/actions/add_contact_groups.js +4 -1
  35. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
  36. package/out-tsc/src/flow/actions/add_input_labels.js +4 -1
  37. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  38. package/out-tsc/src/flow/actions/audio-player.js +112 -0
  39. package/out-tsc/src/flow/actions/audio-player.js.map +1 -0
  40. package/out-tsc/src/flow/actions/enter_flow.js +43 -0
  41. package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
  42. package/out-tsc/src/flow/actions/play_audio.js +57 -4
  43. package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
  44. package/out-tsc/src/flow/actions/remove_contact_groups.js +6 -1
  45. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
  46. package/out-tsc/src/flow/actions/say_msg.js +86 -3
  47. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  48. package/out-tsc/src/flow/actions/send_broadcast.js +6 -2
  49. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  50. package/out-tsc/src/flow/actions/set_contact_channel.js +13 -0
  51. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  52. package/out-tsc/src/flow/actions/set_contact_status.js +7 -5
  53. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  54. package/out-tsc/src/flow/actions/start_session.js +10 -3
  55. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  56. package/out-tsc/src/flow/config.js +11 -3
  57. package/out-tsc/src/flow/config.js.map +1 -1
  58. package/out-tsc/src/flow/nodes/shared-rules.js +1 -1
  59. package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -1
  60. package/out-tsc/src/flow/nodes/split_by_contact_field.js +18 -5
  61. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -1
  62. package/out-tsc/src/flow/nodes/split_by_expression.js +1 -1
  63. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -1
  64. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +0 -1
  65. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  66. package/out-tsc/src/flow/nodes/split_by_random.js +0 -1
  67. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
  68. package/out-tsc/src/flow/nodes/split_by_run_result.js +10 -4
  69. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
  70. package/out-tsc/src/flow/nodes/terminal.js +7 -0
  71. package/out-tsc/src/flow/nodes/terminal.js.map +1 -0
  72. package/out-tsc/src/flow/nodes/wait_for_audio.js +77 -0
  73. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
  74. package/out-tsc/src/flow/nodes/wait_for_dial.js +151 -0
  75. package/out-tsc/src/flow/nodes/wait_for_dial.js.map +1 -0
  76. package/out-tsc/src/flow/nodes/wait_for_digits.js +61 -1
  77. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
  78. package/out-tsc/src/flow/nodes/wait_for_menu.js +173 -2
  79. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
  80. package/out-tsc/src/flow/nodes/wait_for_response.js +1 -1
  81. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  82. package/out-tsc/src/flow/operators.js +21 -5
  83. package/out-tsc/src/flow/operators.js.map +1 -1
  84. package/out-tsc/src/flow/types.js.map +1 -1
  85. package/out-tsc/src/flow/utils.js +79 -3
  86. package/out-tsc/src/flow/utils.js.map +1 -1
  87. package/out-tsc/src/form/ArrayEditor.js +4 -2
  88. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  89. package/out-tsc/src/form/FieldRenderer.js +56 -0
  90. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  91. package/out-tsc/src/interfaces.js +1 -0
  92. package/out-tsc/src/interfaces.js.map +1 -1
  93. package/out-tsc/src/layout/Dialog.js +51 -7
  94. package/out-tsc/src/layout/Dialog.js.map +1 -1
  95. package/out-tsc/src/layout/Modax.js +20 -2
  96. package/out-tsc/src/layout/Modax.js.map +1 -1
  97. package/out-tsc/src/list/ContentMenu.js +14 -1
  98. package/out-tsc/src/list/ContentMenu.js.map +1 -1
  99. package/out-tsc/src/locales/es.js +5 -5
  100. package/out-tsc/src/locales/es.js.map +1 -1
  101. package/out-tsc/src/locales/fr.js +5 -5
  102. package/out-tsc/src/locales/fr.js.map +1 -1
  103. package/out-tsc/src/locales/locale-codes.js +11 -2
  104. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  105. package/out-tsc/src/locales/pt.js +5 -5
  106. package/out-tsc/src/locales/pt.js.map +1 -1
  107. package/out-tsc/src/simulator/Simulator.js +21 -4
  108. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  109. package/out-tsc/src/store/AppState.js +102 -3
  110. package/out-tsc/src/store/AppState.js.map +1 -1
  111. package/out-tsc/test/actions/add_contact_groups.test.js +35 -0
  112. package/out-tsc/test/actions/add_contact_groups.test.js.map +1 -1
  113. package/out-tsc/test/actions/add_input_labels.test.js +53 -0
  114. package/out-tsc/test/actions/add_input_labels.test.js.map +1 -0
  115. package/out-tsc/test/actions/enter_flow.test.js +71 -0
  116. package/out-tsc/test/actions/enter_flow.test.js.map +1 -0
  117. package/out-tsc/test/actions/play_audio.test.js +118 -0
  118. package/out-tsc/test/actions/play_audio.test.js.map +1 -0
  119. package/out-tsc/test/actions/remove_contact_groups.test.js +24 -0
  120. package/out-tsc/test/actions/remove_contact_groups.test.js.map +1 -1
  121. package/out-tsc/test/actions/say_msg.test.js +158 -0
  122. package/out-tsc/test/actions/say_msg.test.js.map +1 -0
  123. package/out-tsc/test/actions/send_broadcast.test.js +41 -0
  124. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
  125. package/out-tsc/test/actions/set_contact_channel.test.js +67 -0
  126. package/out-tsc/test/actions/set_contact_channel.test.js.map +1 -0
  127. package/out-tsc/test/actions/set_contact_field.test.js +52 -0
  128. package/out-tsc/test/actions/set_contact_field.test.js.map +1 -0
  129. package/out-tsc/test/actions/set_contact_language.test.js +39 -0
  130. package/out-tsc/test/actions/set_contact_language.test.js.map +1 -0
  131. package/out-tsc/test/actions/set_contact_name.test.js +28 -0
  132. package/out-tsc/test/actions/set_contact_name.test.js.map +1 -0
  133. package/out-tsc/test/actions/set_contact_status.test.js +44 -0
  134. package/out-tsc/test/actions/set_contact_status.test.js.map +1 -0
  135. package/out-tsc/test/actions/set_run_result.test.js +47 -0
  136. package/out-tsc/test/actions/set_run_result.test.js.map +1 -0
  137. package/out-tsc/test/actions/start_session.test.js +76 -0
  138. package/out-tsc/test/actions/start_session.test.js.map +1 -1
  139. package/out-tsc/test/nodes/split_by_contact_field.test.js +50 -0
  140. package/out-tsc/test/nodes/split_by_contact_field.test.js.map +1 -1
  141. package/out-tsc/test/nodes/split_by_run_result.test.js +82 -0
  142. package/out-tsc/test/nodes/split_by_run_result.test.js.map +1 -1
  143. package/out-tsc/test/nodes/split_by_ticket.test.js +139 -0
  144. package/out-tsc/test/nodes/split_by_ticket.test.js.map +1 -0
  145. package/out-tsc/test/nodes/split_by_webhook.test.js +111 -0
  146. package/out-tsc/test/nodes/split_by_webhook.test.js.map +1 -0
  147. package/out-tsc/test/nodes/wait_for_audio.test.js +156 -0
  148. package/out-tsc/test/nodes/wait_for_audio.test.js.map +1 -0
  149. package/out-tsc/test/nodes/wait_for_dial.test.js +336 -0
  150. package/out-tsc/test/nodes/wait_for_dial.test.js.map +1 -0
  151. package/out-tsc/test/nodes/wait_for_digits.test.js +198 -84
  152. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  153. package/out-tsc/test/nodes/wait_for_menu.test.js +340 -0
  154. package/out-tsc/test/nodes/wait_for_menu.test.js.map +1 -0
  155. package/out-tsc/test/temba-flow-collision.test.js +261 -6
  156. package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
  157. package/out-tsc/test/temba-flow-editor.test.js +187 -0
  158. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  159. package/out-tsc/test/temba-flow-plumber.test.js +19 -0
  160. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  161. package/out-tsc/test/temba-node-type-selector.test.js +6 -6
  162. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
  163. package/out-tsc/test/temba-select.test.js +4 -1
  164. package/out-tsc/test/temba-select.test.js.map +1 -1
  165. package/out-tsc/test/utils.test.js +4 -2
  166. package/out-tsc/test/utils.test.js.map +1 -1
  167. package/package.json +3 -9
  168. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  169. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  170. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  171. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  172. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  173. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  174. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  175. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  176. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  177. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  178. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  179. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  180. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  181. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  182. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  183. package/screenshots/truth/actions/add_input_labels/editor/multiple-labels.png +0 -0
  184. package/screenshots/truth/actions/add_input_labels/editor/single-label.png +0 -0
  185. package/screenshots/truth/actions/add_input_labels/render/multiple-labels.png +0 -0
  186. package/screenshots/truth/actions/add_input_labels/render/single-label.png +0 -0
  187. package/screenshots/truth/actions/enter_flow/editor/basic-flow.png +0 -0
  188. package/screenshots/truth/actions/enter_flow/editor/long-flow-name.png +0 -0
  189. package/screenshots/truth/actions/enter_flow/render/basic-flow.png +0 -0
  190. package/screenshots/truth/actions/enter_flow/render/long-flow-name.png +0 -0
  191. package/screenshots/truth/actions/play_audio/editor/expression-url.png +0 -0
  192. package/screenshots/truth/actions/play_audio/editor/static-url.png +0 -0
  193. package/screenshots/truth/actions/play_audio/render/expression-url.png +0 -0
  194. package/screenshots/truth/actions/play_audio/render/static-url.png +0 -0
  195. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  196. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  197. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  198. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  199. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  200. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  201. package/screenshots/truth/actions/say_msg/editor/multiline-text.png +0 -0
  202. package/screenshots/truth/actions/say_msg/editor/simple-text.png +0 -0
  203. package/screenshots/truth/actions/say_msg/editor/text-with-audio-url.png +0 -0
  204. package/screenshots/truth/actions/say_msg/render/multiline-text.png +0 -0
  205. package/screenshots/truth/actions/say_msg/render/simple-text.png +0 -0
  206. package/screenshots/truth/actions/say_msg/render/text-with-audio-url.png +0 -0
  207. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  208. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  209. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  210. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  211. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  212. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  213. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  214. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  215. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  216. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  217. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  218. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  219. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  220. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  221. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  222. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  223. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  224. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  225. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  226. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  227. package/screenshots/truth/actions/set_contact_channel/editor/sms-channel.png +0 -0
  228. package/screenshots/truth/actions/set_contact_channel/editor/whatsapp-channel.png +0 -0
  229. package/screenshots/truth/actions/set_contact_channel/render/sms-channel.png +0 -0
  230. package/screenshots/truth/actions/set_contact_channel/render/whatsapp-channel.png +0 -0
  231. package/screenshots/truth/actions/set_contact_field/editor/clear-value.png +0 -0
  232. package/screenshots/truth/actions/set_contact_field/editor/set-value.png +0 -0
  233. package/screenshots/truth/actions/set_contact_field/render/clear-value.png +0 -0
  234. package/screenshots/truth/actions/set_contact_field/render/set-value.png +0 -0
  235. package/screenshots/truth/actions/set_contact_language/editor/english.png +0 -0
  236. package/screenshots/truth/actions/set_contact_language/editor/french.png +0 -0
  237. package/screenshots/truth/actions/set_contact_language/render/english.png +0 -0
  238. package/screenshots/truth/actions/set_contact_language/render/french.png +0 -0
  239. package/screenshots/truth/actions/set_contact_name/editor/expression-name.png +0 -0
  240. package/screenshots/truth/actions/set_contact_name/editor/static-name.png +0 -0
  241. package/screenshots/truth/actions/set_contact_name/render/expression-name.png +0 -0
  242. package/screenshots/truth/actions/set_contact_name/render/static-name.png +0 -0
  243. package/screenshots/truth/actions/set_contact_status/editor/active.png +0 -0
  244. package/screenshots/truth/actions/set_contact_status/editor/archived.png +0 -0
  245. package/screenshots/truth/actions/set_contact_status/editor/blocked.png +0 -0
  246. package/screenshots/truth/actions/set_contact_status/render/active.png +0 -0
  247. package/screenshots/truth/actions/set_contact_status/render/archived.png +0 -0
  248. package/screenshots/truth/actions/set_contact_status/render/blocked.png +0 -0
  249. package/screenshots/truth/actions/set_run_result/editor/expression-value.png +0 -0
  250. package/screenshots/truth/actions/set_run_result/editor/with-category.png +0 -0
  251. package/screenshots/truth/actions/set_run_result/render/expression-value.png +0 -0
  252. package/screenshots/truth/actions/set_run_result/render/with-category.png +0 -0
  253. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  254. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  255. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  256. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  257. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  258. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  259. package/screenshots/truth/editor/router.png +0 -0
  260. package/screenshots/truth/editor/wait.png +0 -0
  261. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  262. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  263. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  264. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  265. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  266. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  267. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  268. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  269. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  270. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  271. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  272. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  273. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  274. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  275. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  276. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  277. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  278. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  279. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  280. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  281. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  282. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  283. package/screenshots/truth/nodes/wait_for_audio/editor/basic-audio-wait.png +0 -0
  284. package/screenshots/truth/nodes/wait_for_audio/render/basic-audio-wait.png +0 -0
  285. package/screenshots/truth/nodes/wait_for_dial/editor/basic-dial.png +0 -0
  286. package/screenshots/truth/nodes/wait_for_dial/editor/dial-with-limits.png +0 -0
  287. package/screenshots/truth/nodes/wait_for_dial/render/basic-dial.png +0 -0
  288. package/screenshots/truth/nodes/wait_for_dial/render/dial-with-limits.png +0 -0
  289. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  290. package/screenshots/truth/nodes/wait_for_digits/editor/digits-with-rules.png +0 -0
  291. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  292. package/screenshots/truth/nodes/wait_for_digits/render/digits-with-rules.png +0 -0
  293. package/screenshots/truth/nodes/wait_for_menu/editor/menu-with-digits.png +0 -0
  294. package/screenshots/truth/nodes/wait_for_menu/render/menu-with-digits.png +0 -0
  295. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  296. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  297. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  298. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  299. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  300. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  301. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  302. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  303. package/src/display/Chat.ts +13 -7
  304. package/src/display/Dropdown.ts +3 -1
  305. package/src/display/FloatingTab.ts +4 -4
  306. package/src/display/Thumbnail.ts +162 -2
  307. package/src/flow/CanvasNode.ts +70 -24
  308. package/src/flow/Editor.ts +440 -99
  309. package/src/flow/NodeEditor.ts +137 -9
  310. package/src/flow/Plumber.ts +89 -14
  311. package/src/flow/StickyNote.ts +14 -4
  312. package/src/flow/actions/add_contact_groups.ts +4 -1
  313. package/src/flow/actions/add_input_labels.ts +4 -1
  314. package/src/flow/actions/audio-player.ts +127 -0
  315. package/src/flow/actions/enter_flow.ts +44 -0
  316. package/src/flow/actions/play_audio.ts +64 -5
  317. package/src/flow/actions/remove_contact_groups.ts +6 -1
  318. package/src/flow/actions/say_msg.ts +94 -4
  319. package/src/flow/actions/send_broadcast.ts +6 -2
  320. package/src/flow/actions/set_contact_channel.ts +13 -1
  321. package/src/flow/actions/set_contact_status.ts +7 -5
  322. package/src/flow/actions/start_session.ts +10 -3
  323. package/src/flow/config.ts +11 -3
  324. package/src/flow/nodes/shared-rules.ts +1 -1
  325. package/src/flow/nodes/split_by_contact_field.ts +16 -5
  326. package/src/flow/nodes/split_by_expression.ts +1 -1
  327. package/src/flow/nodes/split_by_llm_categorize.ts +0 -1
  328. package/src/flow/nodes/split_by_random.ts +0 -1
  329. package/src/flow/nodes/split_by_run_result.ts +10 -4
  330. package/src/flow/nodes/terminal.ts +9 -0
  331. package/src/flow/nodes/wait_for_audio.ts +88 -0
  332. package/src/flow/nodes/wait_for_dial.ts +176 -0
  333. package/src/flow/nodes/wait_for_digits.ts +87 -2
  334. package/src/flow/nodes/wait_for_menu.ts +209 -3
  335. package/src/flow/nodes/wait_for_response.ts +1 -1
  336. package/src/flow/operators.ts +23 -5
  337. package/src/flow/types.ts +23 -1
  338. package/src/flow/utils.ts +82 -3
  339. package/src/form/ArrayEditor.ts +4 -2
  340. package/src/form/FieldRenderer.ts +71 -1
  341. package/src/interfaces.ts +2 -1
  342. package/src/layout/Dialog.ts +52 -7
  343. package/src/layout/Modax.ts +19 -2
  344. package/src/list/ContentMenu.ts +15 -1
  345. package/src/locales/es.ts +18 -13
  346. package/src/locales/fr.ts +18 -13
  347. package/src/locales/locale-codes.ts +11 -2
  348. package/src/locales/pt.ts +18 -13
  349. package/src/simulator/Simulator.ts +25 -4
  350. package/src/store/AppState.ts +120 -1
  351. package/src/store/flow-definition.d.ts +2 -0
  352. package/test/actions/add_contact_groups.test.ts +38 -0
  353. package/test/actions/add_input_labels.test.ts +67 -0
  354. package/test/actions/enter_flow.test.ts +88 -0
  355. package/test/actions/play_audio.test.ts +155 -0
  356. package/test/actions/remove_contact_groups.test.ts +29 -0
  357. package/test/actions/say_msg.test.ts +196 -0
  358. package/test/actions/send_broadcast.test.ts +44 -0
  359. package/test/actions/set_contact_channel.test.ts +88 -0
  360. package/test/actions/set_contact_field.test.ts +68 -0
  361. package/test/actions/set_contact_language.test.ts +55 -0
  362. package/test/actions/set_contact_name.test.ts +39 -0
  363. package/test/actions/set_contact_status.test.ts +64 -0
  364. package/test/actions/set_run_result.test.ts +61 -0
  365. package/test/actions/start_session.test.ts +82 -0
  366. package/test/nodes/split_by_contact_field.test.ts +59 -0
  367. package/test/nodes/split_by_run_result.test.ts +100 -0
  368. package/test/nodes/split_by_ticket.test.ts +157 -0
  369. package/test/nodes/split_by_webhook.test.ts +131 -0
  370. package/test/nodes/wait_for_audio.test.ts +182 -0
  371. package/test/nodes/wait_for_dial.test.ts +382 -0
  372. package/test/nodes/wait_for_digits.test.ts +233 -109
  373. package/test/nodes/wait_for_menu.test.ts +383 -0
  374. package/test/temba-flow-collision.test.ts +286 -6
  375. package/test/temba-flow-editor.test.ts +240 -0
  376. package/test/temba-flow-plumber.test.ts +62 -0
  377. package/test/temba-node-type-selector.test.ts +6 -6
  378. package/test/temba-select.test.ts +6 -1
  379. package/test/utils.test.ts +4 -2
  380. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  381. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  382. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  383. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  384. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  385. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
@@ -18,6 +18,59 @@ import { produce } from 'immer';
18
18
  export const FLOW_SPEC_VERSION = '14.3';
19
19
  const CANVAS_PADDING = 800;
20
20
 
21
+ /**
22
+ * Temporary: Reclassify nodes based on whether they contain terminal actions.
23
+ * - execute_actions nodes with a terminal action become "terminal"
24
+ * - terminal nodes that no longer have a terminal action become "execute_actions"
25
+ * This can be removed once we stop supporting terminal nodes.
26
+ */
27
+ function reclassifyTerminalNodes(definition: FlowDefinition): void {
28
+ if (!definition?.nodes || !definition?._ui?.nodes) return;
29
+
30
+ for (const node of definition.nodes) {
31
+ const nodeUI = definition._ui.nodes[node.uuid];
32
+ if (!nodeUI) continue;
33
+
34
+ const hasTerminalAction = node.actions?.some(
35
+ (action) => (action as any).terminal === true
36
+ );
37
+
38
+ if (nodeUI.type === 'execute_actions' && hasTerminalAction) {
39
+ nodeUI.type = 'terminal' as any;
40
+ } else if (nodeUI.type === ('terminal' as any) && !hasTerminalAction) {
41
+ nodeUI.type = 'execute_actions';
42
+ }
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Reclassify wait_for_response nodes that are actually voice-specific wait types.
48
+ * The server stores all voice waits as wait_for_response, but we detect the specific
49
+ * type from the router's wait hint:
50
+ * - hint.type === 'digits' && hint.count === 1 → wait_for_menu
51
+ * - hint.type === 'digits' (no count) → wait_for_digits
52
+ * - hint.type === 'audio' → wait_for_audio
53
+ */
54
+ function reclassifyVoiceWaitNodes(definition: FlowDefinition): void {
55
+ if (!definition?.nodes || !definition?._ui?.nodes) return;
56
+
57
+ for (const node of definition.nodes) {
58
+ const nodeUI = definition._ui.nodes[node.uuid];
59
+ if (!nodeUI || nodeUI.type !== 'wait_for_response') continue;
60
+
61
+ const hint = node.router?.wait?.hint;
62
+ if (!hint) continue;
63
+
64
+ if (hint.type === 'digits' && hint.count === 1) {
65
+ nodeUI.type = 'wait_for_menu' as any;
66
+ } else if (hint.type === 'digits') {
67
+ nodeUI.type = 'wait_for_digits' as any;
68
+ } else if (hint.type === 'audio') {
69
+ nodeUI.type = 'wait_for_audio' as any;
70
+ }
71
+ }
72
+ }
73
+
21
74
  /**
22
75
  * Sorts nodes by their position - first by y (top), then by x (left)
23
76
  */
@@ -65,11 +118,44 @@ export interface Language {
65
118
  name: string;
66
119
  }
67
120
 
121
+ export interface FlowIssue {
122
+ type: string;
123
+ node_uuid: string;
124
+ action_uuid?: string;
125
+ description: string;
126
+ dependency?: {
127
+ key: string;
128
+ name: string;
129
+ type: string;
130
+ };
131
+ }
132
+
68
133
  export interface FlowInfo {
69
134
  results: InfoResult[];
70
135
  dependencies: TypedObjectRef[];
71
136
  counts: { nodes: number; languages: number };
72
137
  locals: string[];
138
+ issues?: FlowIssue[];
139
+ }
140
+
141
+ function buildIssueMaps(issues: FlowIssue[] = []): {
142
+ byNode: Map<string, FlowIssue[]>;
143
+ byAction: Map<string, FlowIssue[]>;
144
+ } {
145
+ const byNode = new Map<string, FlowIssue[]>();
146
+ const byAction = new Map<string, FlowIssue[]>();
147
+ for (const issue of issues) {
148
+ if (issue.action_uuid) {
149
+ const actionList = byAction.get(issue.action_uuid) || [];
150
+ actionList.push(issue);
151
+ byAction.set(issue.action_uuid, actionList);
152
+ } else {
153
+ const nodeList = byNode.get(issue.node_uuid) || [];
154
+ nodeList.push(issue);
155
+ byNode.set(issue.node_uuid, nodeList);
156
+ }
157
+ }
158
+ return { byNode, byAction };
73
159
  }
74
160
 
75
161
  export interface FlowContents {
@@ -100,6 +186,8 @@ export interface Activity {
100
186
  export interface AppState {
101
187
  flowDefinition: FlowDefinition;
102
188
  flowInfo: FlowInfo;
189
+ issuesByNode: Map<string, FlowIssue[]>;
190
+ issuesByAction: Map<string, FlowIssue[]>;
103
191
 
104
192
  languageCode: string;
105
193
  languageNames: { [code: string]: Language };
@@ -157,6 +245,7 @@ export interface AppState {
157
245
  actionUuid: string,
158
246
  localizationData: Record<string, any>
159
247
  ): void;
248
+ clearFlowData: () => void;
160
249
  setTranslationFilters: (filters: { categories: boolean }) => void;
161
250
  markAutoTranslated: (
162
251
  languageCode: string,
@@ -174,6 +263,8 @@ export const zustand = createStore<AppState>()(
174
263
  workspace: null,
175
264
  flowDefinition: null,
176
265
  flowInfo: null,
266
+ issuesByNode: new Map(),
267
+ issuesByAction: new Map(),
177
268
  isTranslating: false,
178
269
  viewingRevision: false,
179
270
  dirtyDate: null,
@@ -201,10 +292,15 @@ export const zustand = createStore<AppState>()(
201
292
  throw new Error('Network response was not ok');
202
293
  }
203
294
  const data = (await response.json()) as FlowContents;
295
+ reclassifyTerminalNodes(data.definition);
296
+ reclassifyVoiceWaitNodes(data.definition);
297
+ const issueMaps = buildIssueMaps(data.info?.issues);
204
298
  set({
205
299
  flowInfo: data.info,
206
300
  flowDefinition: data.definition,
207
- viewingRevision
301
+ viewingRevision,
302
+ issuesByNode: issueMaps.byNode,
303
+ issuesByAction: issueMaps.byAction
208
304
  });
209
305
  },
210
306
 
@@ -285,6 +381,20 @@ export const zustand = createStore<AppState>()(
285
381
  return { name: languageNames[languageCode], code: languageCode };
286
382
  },
287
383
 
384
+ clearFlowData: () => {
385
+ set({
386
+ flowDefinition: null,
387
+ flowInfo: null,
388
+ issuesByNode: new Map(),
389
+ issuesByAction: new Map(),
390
+ activity: null,
391
+ simulatorActivity: null,
392
+ simulatorActive: false,
393
+ dirtyDate: null,
394
+ viewingRevision: false
395
+ });
396
+ },
397
+
288
398
  // todo: eventually we should be doing the fetching
289
399
  setFlowContents: (flow: FlowContents) => {
290
400
  set((state: AppState) => {
@@ -295,10 +405,16 @@ export const zustand = createStore<AppState>()(
295
405
  nodes: [...(flow.definition.nodes || [])]
296
406
  };
297
407
  state.flowInfo = flow.info;
408
+ const issueMaps = buildIssueMaps(flow.info?.issues);
409
+ state.issuesByNode = issueMaps.byNode;
410
+ state.issuesByAction = issueMaps.byAction;
298
411
  // Reset to the flow's default language when loading a new flow
299
412
  state.languageCode = flowLang;
300
413
  state.isTranslating = false;
301
414
 
415
+ reclassifyTerminalNodes(state.flowDefinition);
416
+ reclassifyVoiceWaitNodes(state.flowDefinition);
417
+
302
418
  // Sort nodes by position when loading flow
303
419
  if (state.flowDefinition?.nodes && state.flowDefinition?._ui?.nodes) {
304
420
  sortNodesByPosition(
@@ -312,6 +428,9 @@ export const zustand = createStore<AppState>()(
312
428
  setFlowInfo: (info: FlowInfo) => {
313
429
  set((state: AppState) => {
314
430
  state.flowInfo = info;
431
+ const issueMaps = buildIssueMaps(info?.issues);
432
+ state.issuesByNode = issueMaps.byNode;
433
+ state.issuesByAction = issueMaps.byAction;
315
434
  });
316
435
  },
317
436
 
@@ -28,6 +28,7 @@ export type ActionType =
28
28
  | 'send_email'
29
29
  | 'send_broadcast'
30
30
  | 'enter_flow'
31
+ | 'terminal'
31
32
  | 'start_session'
32
33
  | 'transfer_airtime'
33
34
  | 'split_by_airtime'
@@ -152,6 +153,7 @@ export interface SendBroadcast extends Action {
152
153
 
153
154
  export interface EnterFlow extends Action {
154
155
  flow: NamedObject;
156
+ terminal?: boolean;
155
157
  }
156
158
 
157
159
  export interface StartSession extends Action {
@@ -86,4 +86,42 @@ describe('add_contact_groups action config', () => {
86
86
  'descriptive-group-names'
87
87
  );
88
88
  });
89
+
90
+ describe('metadata stripping', () => {
91
+ it('should strip superfluous API metadata from groups', () => {
92
+ const formData = {
93
+ uuid: 'test-uuid',
94
+ groups: [
95
+ {
96
+ uuid: 'group-1',
97
+ name: 'VIP Customers',
98
+ query: 'status = vip',
99
+ status: 'ready',
100
+ count: 150,
101
+ system: false
102
+ },
103
+ {
104
+ uuid: 'group-2',
105
+ name: 'Beta Testers',
106
+ query: null,
107
+ status: 'ready',
108
+ count: 45,
109
+ system: false
110
+ }
111
+ ]
112
+ };
113
+
114
+ const action = add_contact_groups.fromFormData(formData) as AddToGroup;
115
+
116
+ expect(action.groups).to.have.lengthOf(2);
117
+ expect(action.groups[0]).to.deep.equal({
118
+ uuid: 'group-1',
119
+ name: 'VIP Customers'
120
+ });
121
+ expect(action.groups[1]).to.deep.equal({
122
+ uuid: 'group-2',
123
+ name: 'Beta Testers'
124
+ });
125
+ });
126
+ });
89
127
  });
@@ -0,0 +1,67 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { add_input_labels } from '../../src/flow/actions/add_input_labels';
3
+ import { AddInputLabels } from '../../src/store/flow-definition';
4
+ import { ActionTest } from '../ActionHelper';
5
+
6
+ /**
7
+ * Test suite for the add_input_labels action configuration.
8
+ */
9
+ describe('add_input_labels action config', () => {
10
+ const helper = new ActionTest(add_input_labels, 'add_input_labels');
11
+
12
+ describe('basic properties', () => {
13
+ helper.testBasicProperties();
14
+
15
+ it('has correct name', () => {
16
+ expect(add_input_labels.name).to.equal('Add Input Labels');
17
+ });
18
+ });
19
+
20
+ describe('action scenarios', () => {
21
+ helper.testAction(
22
+ {
23
+ uuid: 'test-action-1',
24
+ type: 'add_input_labels',
25
+ labels: [{ uuid: 'label-1', name: 'Important' }]
26
+ } as AddInputLabels,
27
+ 'single-label'
28
+ );
29
+
30
+ helper.testAction(
31
+ {
32
+ uuid: 'test-action-2',
33
+ type: 'add_input_labels',
34
+ labels: [
35
+ { uuid: 'label-1', name: 'Important' },
36
+ { uuid: 'label-2', name: 'Follow Up' },
37
+ { uuid: 'label-3', name: 'Spam' }
38
+ ]
39
+ } as AddInputLabels,
40
+ 'multiple-labels'
41
+ );
42
+ });
43
+
44
+ describe('metadata stripping', () => {
45
+ it('should strip superfluous API metadata from labels', () => {
46
+ const formData = {
47
+ uuid: 'test-uuid',
48
+ labels: [
49
+ { uuid: 'label-1', name: 'Important', count: 250 },
50
+ { uuid: 'label-2', name: 'Follow Up', count: 42 }
51
+ ]
52
+ };
53
+
54
+ const action = add_input_labels.fromFormData(formData) as AddInputLabels;
55
+
56
+ expect(action.labels).to.have.lengthOf(2);
57
+ expect(action.labels[0]).to.deep.equal({
58
+ uuid: 'label-1',
59
+ name: 'Important'
60
+ });
61
+ expect(action.labels[1]).to.deep.equal({
62
+ uuid: 'label-2',
63
+ name: 'Follow Up'
64
+ });
65
+ });
66
+ });
67
+ });
@@ -0,0 +1,88 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { enter_flow } from '../../src/flow/actions/enter_flow';
3
+ import { EnterFlow } from '../../src/store/flow-definition';
4
+ import { ActionTest } from '../ActionHelper';
5
+
6
+ /**
7
+ * Test suite for the enter_flow action configuration.
8
+ */
9
+ describe('enter_flow action config', () => {
10
+ const helper = new ActionTest(enter_flow, 'enter_flow');
11
+
12
+ describe('basic properties', () => {
13
+ helper.testBasicProperties();
14
+
15
+ it('has correct name', () => {
16
+ expect(enter_flow.name).to.equal('Enter a Flow');
17
+ });
18
+ });
19
+
20
+ describe('action scenarios', () => {
21
+ helper.testAction(
22
+ {
23
+ uuid: 'test-action-1',
24
+ type: 'enter_flow',
25
+ terminal: true,
26
+ flow: { uuid: 'flow-1', name: 'Registration Flow' }
27
+ } as EnterFlow,
28
+ 'basic-flow'
29
+ );
30
+
31
+ helper.testAction(
32
+ {
33
+ uuid: 'test-action-2',
34
+ type: 'enter_flow',
35
+ terminal: true,
36
+ flow: {
37
+ uuid: 'flow-2',
38
+ name: 'Very Long Flow Name That Tests Layout Wrapping'
39
+ }
40
+ } as EnterFlow,
41
+ 'long-flow-name'
42
+ );
43
+ });
44
+
45
+ describe('metadata stripping', () => {
46
+ it('should strip superfluous API metadata from flow', () => {
47
+ const formData = {
48
+ uuid: 'test-uuid',
49
+ flow: [
50
+ {
51
+ uuid: 'flow-1',
52
+ name: 'Registration Flow',
53
+ type: 'message',
54
+ archived: false,
55
+ labels: [],
56
+ expires: 720,
57
+ runs: { active: 0, waiting: 5, completed: 100 },
58
+ results: [],
59
+ parent_refs: [],
60
+ created_on: '2024-01-01T00:00:00.000Z',
61
+ modified_on: '2024-06-15T12:00:00.000Z'
62
+ }
63
+ ]
64
+ };
65
+
66
+ const action = enter_flow.fromFormData(formData) as EnterFlow;
67
+
68
+ expect(action.flow).to.deep.equal({
69
+ uuid: 'flow-1',
70
+ name: 'Registration Flow'
71
+ });
72
+ });
73
+
74
+ it('should handle flow selected via value key', () => {
75
+ const formData = {
76
+ uuid: 'test-uuid',
77
+ flow: [{ value: 'flow-1', name: 'Test Flow' }]
78
+ };
79
+
80
+ const action = enter_flow.fromFormData(formData) as EnterFlow;
81
+
82
+ expect(action.flow).to.deep.equal({
83
+ uuid: 'flow-1',
84
+ name: 'Test Flow'
85
+ });
86
+ });
87
+ });
88
+ });
@@ -0,0 +1,155 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { play_audio } from '../../src/flow/actions/play_audio';
3
+ import { PlayAudio } from '../../src/store/flow-definition';
4
+ import { ActionTest } from '../ActionHelper';
5
+
6
+ /**
7
+ * Test suite for the play_audio action configuration.
8
+ */
9
+ describe('play_audio action config', () => {
10
+ const helper = new ActionTest(play_audio, 'play_audio');
11
+
12
+ describe('basic properties', () => {
13
+ helper.testBasicProperties();
14
+
15
+ it('has correct name', () => {
16
+ expect(play_audio.name).to.equal('Play Recording');
17
+ });
18
+
19
+ it('is voice-only', () => {
20
+ expect(play_audio.flowTypes).to.deep.equal(['voice']);
21
+ });
22
+ });
23
+
24
+ describe('action scenarios', () => {
25
+ helper.testAction(
26
+ {
27
+ uuid: 'test-play-1',
28
+ type: 'play_audio',
29
+ audio_url: '@results.voicemail'
30
+ } as PlayAudio,
31
+ 'expression-url'
32
+ );
33
+
34
+ helper.testAction(
35
+ {
36
+ uuid: 'test-play-2',
37
+ type: 'play_audio',
38
+ audio_url: 'https://example.com/greeting.mp3'
39
+ } as PlayAudio,
40
+ 'static-url'
41
+ );
42
+ });
43
+
44
+ describe('data transformation', () => {
45
+ it('converts action to form data', () => {
46
+ const action: PlayAudio = {
47
+ uuid: 'test-action',
48
+ type: 'play_audio',
49
+ audio_url: '@results.voicemail'
50
+ };
51
+
52
+ const formData = play_audio.toFormData!(action);
53
+ expect(formData.uuid).to.equal('test-action');
54
+ expect(formData.audio_url).to.equal('@results.voicemail');
55
+ });
56
+
57
+ it('handles missing audio_url', () => {
58
+ const action = {
59
+ uuid: 'test-action',
60
+ type: 'play_audio'
61
+ } as PlayAudio;
62
+
63
+ const formData = play_audio.toFormData!(action);
64
+ expect(formData.audio_url).to.equal('');
65
+ });
66
+
67
+ it('converts form data to action', () => {
68
+ const formData = {
69
+ uuid: 'test-action',
70
+ audio_url: '@results.voicemail'
71
+ };
72
+
73
+ const action = play_audio.fromFormData!(formData) as PlayAudio;
74
+ expect(action.uuid).to.equal('test-action');
75
+ expect(action.type).to.equal('play_audio');
76
+ expect(action.audio_url).to.equal('@results.voicemail');
77
+ });
78
+
79
+ it('trims whitespace from audio_url', () => {
80
+ const formData = {
81
+ uuid: 'test-action',
82
+ audio_url: ' @results.voicemail '
83
+ };
84
+
85
+ const action = play_audio.fromFormData!(formData) as PlayAudio;
86
+ expect(action.audio_url).to.equal('@results.voicemail');
87
+ });
88
+ });
89
+
90
+ describe('localization', () => {
91
+ it('converts localization to form data', () => {
92
+ const action: PlayAudio = {
93
+ uuid: 'test-action',
94
+ type: 'play_audio',
95
+ audio_url: '@results.voicemail'
96
+ };
97
+
98
+ const localization = {
99
+ audio_url: ['@results.voicemail_es']
100
+ };
101
+
102
+ const formData = play_audio.toLocalizationFormData!(action, localization);
103
+ expect(formData.audio_url).to.equal('@results.voicemail_es');
104
+ });
105
+
106
+ it('handles missing localization', () => {
107
+ const action: PlayAudio = {
108
+ uuid: 'test-action',
109
+ type: 'play_audio',
110
+ audio_url: '@results.voicemail'
111
+ };
112
+
113
+ const formData = play_audio.toLocalizationFormData!(action, {});
114
+ expect(formData.audio_url).to.equal('');
115
+ });
116
+
117
+ it('converts form data to localization', () => {
118
+ const action: PlayAudio = {
119
+ uuid: 'test-action',
120
+ type: 'play_audio',
121
+ audio_url: '@results.voicemail'
122
+ };
123
+
124
+ const formData = {
125
+ uuid: 'test-action',
126
+ audio_url: '@results.voicemail_es'
127
+ };
128
+
129
+ const localization = play_audio.fromLocalizationFormData!(
130
+ formData,
131
+ action
132
+ );
133
+ expect(localization.audio_url).to.deep.equal(['@results.voicemail_es']);
134
+ });
135
+
136
+ it('omits unchanged localization', () => {
137
+ const action: PlayAudio = {
138
+ uuid: 'test-action',
139
+ type: 'play_audio',
140
+ audio_url: '@results.voicemail'
141
+ };
142
+
143
+ const formData = {
144
+ uuid: 'test-action',
145
+ audio_url: '@results.voicemail' // same as original
146
+ };
147
+
148
+ const localization = play_audio.fromLocalizationFormData!(
149
+ formData,
150
+ action
151
+ );
152
+ expect(localization.audio_url).to.be.undefined;
153
+ });
154
+ });
155
+ });
@@ -262,4 +262,33 @@ describe('remove_contact_groups action config', () => {
262
262
  expect(Object.keys(result.errors)).to.have.length(0);
263
263
  });
264
264
  });
265
+
266
+ describe('metadata stripping', () => {
267
+ it('should strip superfluous API metadata from groups', () => {
268
+ const formData = {
269
+ uuid: 'test-uuid',
270
+ all_groups: false,
271
+ groups: [
272
+ {
273
+ uuid: 'group-1',
274
+ name: 'VIP Customers',
275
+ query: 'status = vip',
276
+ status: 'ready',
277
+ count: 150,
278
+ system: false
279
+ }
280
+ ]
281
+ };
282
+
283
+ const action = remove_contact_groups.fromFormData(
284
+ formData
285
+ ) as RemoveFromGroup;
286
+
287
+ expect(action.groups).to.have.lengthOf(1);
288
+ expect(action.groups[0]).to.deep.equal({
289
+ uuid: 'group-1',
290
+ name: 'VIP Customers'
291
+ });
292
+ });
293
+ });
265
294
  });