@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
@@ -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
+ });