@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
@@ -1,13 +1,72 @@
1
1
  import { html } from 'lit-html';
2
- import { ActionConfig, ACTION_GROUPS, FlowTypes } from '../types';
2
+ import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
3
3
  import { Node, PlayAudio } from '../../store/flow-definition';
4
4
 
5
5
  export const play_audio: ActionConfig = {
6
- name: 'Play Audio',
6
+ name: 'Play Recording',
7
7
  group: ACTION_GROUPS.send,
8
8
  flowTypes: [FlowTypes.VOICE],
9
- render: (_node: Node, _action: PlayAudio) => {
10
- // This will need to be implemented based on the actual render logic
11
- return html`<div>Play Audio</div>`;
9
+ render: (_node: Node, action: PlayAudio) => {
10
+ return html`
11
+ <div style="display: flex; align-items: center; gap: 0.3em;">
12
+ <temba-icon name="recording" size="1"></temba-icon>
13
+ <div
14
+ style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;"
15
+ title="${action.audio_url || ''}"
16
+ >
17
+ ${action.audio_url || ''}
18
+ </div>
19
+ </div>
20
+ `;
21
+ },
22
+ form: {
23
+ audio_url: {
24
+ type: 'text',
25
+ label: 'Recording URL',
26
+ required: true,
27
+ evaluated: true
28
+ }
29
+ },
30
+ layout: ['audio_url'],
31
+ toFormData: (action: PlayAudio) => {
32
+ return {
33
+ uuid: action.uuid,
34
+ audio_url: action.audio_url || ''
35
+ };
36
+ },
37
+ fromFormData: (data: FormData) => {
38
+ return {
39
+ uuid: data.uuid,
40
+ type: 'play_audio',
41
+ audio_url: (data.audio_url || '').trim()
42
+ } as PlayAudio;
43
+ },
44
+ localizable: ['audio_url'],
45
+ toLocalizationFormData: (
46
+ action: PlayAudio,
47
+ localization: Record<string, any>
48
+ ) => {
49
+ const formData: FormData = {
50
+ uuid: action.uuid
51
+ };
52
+
53
+ if (localization.audio_url && Array.isArray(localization.audio_url)) {
54
+ formData.audio_url = localization.audio_url[0] || '';
55
+ } else {
56
+ formData.audio_url = '';
57
+ }
58
+
59
+ return formData;
60
+ },
61
+ fromLocalizationFormData: (formData: FormData, action: PlayAudio) => {
62
+ const localization: Record<string, any> = {};
63
+
64
+ if (formData.audio_url && formData.audio_url.trim() !== '') {
65
+ if (formData.audio_url !== action.audio_url) {
66
+ localization.audio_url = [formData.audio_url];
67
+ }
68
+ }
69
+
70
+ return localization;
12
71
  }
13
72
  };
@@ -69,7 +69,12 @@ export const remove_contact_groups: ActionConfig = {
69
69
  return {
70
70
  uuid: formData.uuid,
71
71
  type: 'remove_contact_groups',
72
- groups: formData.all_groups ? [] : formData.groups || [],
72
+ groups: formData.all_groups
73
+ ? []
74
+ : (formData.groups || []).map((g: any) => ({
75
+ uuid: g.uuid,
76
+ name: g.name
77
+ })),
73
78
  all_groups: formData.all_groups || false
74
79
  };
75
80
  }
@@ -1,13 +1,103 @@
1
1
  import { html } from 'lit-html';
2
- import { ActionConfig, ACTION_GROUPS, FlowTypes } from '../types';
2
+ import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
3
+ import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
3
4
  import { Node, SayMsg } from '../../store/flow-definition';
5
+ import { renderAudioPlayer } from './audio-player';
4
6
 
5
7
  export const say_msg: ActionConfig = {
6
8
  name: 'Say Message',
7
9
  group: ACTION_GROUPS.send,
8
10
  flowTypes: [FlowTypes.VOICE],
9
- render: (_node: Node, _action: SayMsg) => {
10
- // This will need to be implemented based on the actual render logic
11
- return html`<div>Say Message</div>`;
11
+ render: (_node: Node, action: SayMsg) => {
12
+ const text = (action.text || '').replace(/\n/g, '<br>');
13
+ return html`
14
+ ${unsafeHTML(text)}
15
+ ${action.audio_url
16
+ ? html`<div style="margin-top: 0.5em;">
17
+ ${renderAudioPlayer(action.audio_url)}
18
+ </div>`
19
+ : null}
20
+ `;
21
+ },
22
+ form: {
23
+ text: {
24
+ type: 'textarea',
25
+ label: 'Message',
26
+ required: true,
27
+ evaluated: true,
28
+ placeholder: 'Enter message to speak...',
29
+ minHeight: 80
30
+ },
31
+ audio_url: {
32
+ type: 'media',
33
+ label: 'Recording',
34
+ required: false,
35
+ accept: 'audio/*',
36
+ optionalLink: 'Add a recording'
37
+ }
38
+ },
39
+ layout: ['text', 'audio_url'],
40
+ toFormData: (action: SayMsg) => {
41
+ return {
42
+ uuid: action.uuid,
43
+ text: action.text || '',
44
+ audio_url: action.audio_url || ''
45
+ };
46
+ },
47
+ fromFormData: (data: FormData) => {
48
+ const result: any = {
49
+ uuid: data.uuid,
50
+ type: 'say_msg',
51
+ text: data.text || ''
52
+ };
53
+ if (data.audio_url && data.audio_url.trim() !== '') {
54
+ result.audio_url = data.audio_url.trim();
55
+ }
56
+ return result as SayMsg;
57
+ },
58
+ sanitize: (formData: FormData): void => {
59
+ if (formData.text && typeof formData.text === 'string') {
60
+ formData.text = formData.text.trim();
61
+ }
62
+ },
63
+ localizable: ['text', 'audio_url'],
64
+ toLocalizationFormData: (
65
+ action: SayMsg,
66
+ localization: Record<string, any>
67
+ ) => {
68
+ const formData: FormData = {
69
+ uuid: action.uuid
70
+ };
71
+
72
+ if (localization.text && Array.isArray(localization.text)) {
73
+ formData.text = localization.text[0] || '';
74
+ } else {
75
+ formData.text = '';
76
+ }
77
+
78
+ if (localization.audio_url && Array.isArray(localization.audio_url)) {
79
+ formData.audio_url = localization.audio_url[0] || '';
80
+ } else {
81
+ formData.audio_url = '';
82
+ }
83
+
84
+ return formData;
85
+ },
86
+ fromLocalizationFormData: (formData: FormData, action: SayMsg) => {
87
+ const localization: Record<string, any> = {};
88
+
89
+ if (formData.text && formData.text.trim() !== '') {
90
+ if (formData.text !== action.text) {
91
+ localization.text = [formData.text];
92
+ }
93
+ }
94
+
95
+ if (formData.audio_url && formData.audio_url.trim() !== '') {
96
+ if (formData.audio_url !== action.audio_url) {
97
+ localization.audio_url = [formData.audio_url];
98
+ }
99
+ }
100
+
101
+ return localization;
12
102
  }
13
103
  };
@@ -70,8 +70,12 @@ export const send_broadcast: ActionConfig = {
70
70
 
71
71
  fromFormData: (formData: FormData): SendBroadcast => {
72
72
  const recipients = formData.recipients || [];
73
- const contacts = recipients.filter((r: any) => !r.group);
74
- const groups = recipients.filter((r: any) => r.group);
73
+ const contacts = recipients
74
+ .filter((r: any) => !r.group)
75
+ .map((c: any) => ({ uuid: c.uuid, name: c.name }));
76
+ const groups = recipients
77
+ .filter((r: any) => r.group)
78
+ .map((g: any) => ({ uuid: g.uuid, name: g.name }));
75
79
 
76
80
  const result: SendBroadcast = {
77
81
  uuid: formData.uuid,
@@ -1,5 +1,5 @@
1
1
  import { html } from 'lit-html';
2
- import { ActionConfig, ACTION_GROUPS, FlowTypes } from '../types';
2
+ import { ActionConfig, ACTION_GROUPS, FormData, FlowTypes } from '../types';
3
3
  import { Node, SetContactChannel } from '../../store/flow-definition';
4
4
 
5
5
  export const set_contact_channel: ActionConfig = {
@@ -19,6 +19,7 @@ export const set_contact_channel: ActionConfig = {
19
19
  endpoint: '/api/v2/channels.json',
20
20
  valueKey: 'uuid',
21
21
  nameKey: 'name',
22
+ placeholder: 'Select channel',
22
23
  helpText: 'Select the channel to set for the contact'
23
24
  }
24
25
  },
@@ -27,5 +28,16 @@ export const set_contact_channel: ActionConfig = {
27
28
  uuid: action.uuid,
28
29
  channel: action.channel ? [action.channel] : null
29
30
  };
31
+ },
32
+ fromFormData: (formData: FormData): SetContactChannel => {
33
+ const channel = formData.channel?.[0];
34
+ return {
35
+ uuid: formData.uuid,
36
+ type: 'set_contact_channel',
37
+ channel: {
38
+ uuid: channel.uuid || channel.value,
39
+ name: channel.name
40
+ }
41
+ };
30
42
  }
31
43
  };
@@ -12,11 +12,13 @@ export const set_contact_status: ActionConfig = {
12
12
  },
13
13
  toFormData: (action: SetContactStatus) => {
14
14
  return {
15
- ...action,
16
- status: {
17
- name: titleCase(action.status || 'active'),
18
- value: action.status || 'active'
19
- }
15
+ uuid: action.uuid,
16
+ status: [
17
+ {
18
+ name: titleCase(action.status || 'active'),
19
+ value: action.status || 'active'
20
+ }
21
+ ]
20
22
  };
21
23
  },
22
24
  fromFormData: (formData: FormData): SetContactStatus => {
@@ -186,7 +186,10 @@ export const start_session: ActionConfig = {
186
186
  } = {
187
187
  uuid: formData.uuid,
188
188
  type: 'start_session',
189
- flow: formData.flow[0],
189
+ flow: {
190
+ uuid: formData.flow[0].uuid || formData.flow[0].value,
191
+ name: formData.flow[0].name
192
+ },
190
193
  groups: [],
191
194
  contacts: []
192
195
  };
@@ -202,8 +205,12 @@ export const start_session: ActionConfig = {
202
205
  } else {
203
206
  // Manual selection - separate contacts and groups
204
207
  const recipients = formData.recipients || [];
205
- action.contacts = recipients.filter((r: any) => !r.group);
206
- action.groups = recipients.filter((r: any) => r.group);
208
+ action.contacts = recipients
209
+ .filter((r: any) => !r.group)
210
+ .map((c: any) => ({ uuid: c.uuid, name: c.name }));
211
+ action.groups = recipients
212
+ .filter((r: any) => r.group)
213
+ .map((g: any) => ({ uuid: g.uuid, name: g.name }));
207
214
  }
208
215
 
209
216
  // Add exclusions if set
@@ -20,6 +20,7 @@ import { remove_contact_groups } from './actions/remove_contact_groups';
20
20
  import { request_optin } from './actions/request_optin';
21
21
  import { say_msg } from './actions/say_msg';
22
22
  import { play_audio } from './actions/play_audio';
23
+ import { enter_flow } from './actions/enter_flow';
23
24
 
24
25
  // Import all node configurations
25
26
  import { execute_actions } from './nodes/execute_actions';
@@ -31,11 +32,14 @@ import { split_by_random } from './nodes/split_by_random';
31
32
  import { split_by_run_result } from './nodes/split_by_run_result';
32
33
  import { split_by_scheme } from './nodes/split_by_scheme';
33
34
  import { split_by_subflow } from './nodes/split_by_subflow';
35
+ import { terminal } from './nodes/terminal';
34
36
  import { split_by_ticket } from './nodes/split_by_ticket';
35
37
  import { split_by_webhook } from './nodes/split_by_webhook';
36
38
  import { split_by_resthook } from './nodes/split_by_resthook';
37
39
  import { split_by_llm } from './nodes/split_by_llm';
38
40
  import { split_by_llm_categorize } from './nodes/split_by_llm_categorize';
41
+ import { wait_for_audio } from './nodes/wait_for_audio';
42
+ import { wait_for_dial } from './nodes/wait_for_dial';
39
43
  import { wait_for_digits } from './nodes/wait_for_digits';
40
44
  import { wait_for_menu } from './nodes/wait_for_menu';
41
45
  import { wait_for_response } from './nodes/wait_for_response';
@@ -59,7 +63,8 @@ export const ACTION_CONFIG: {
59
63
  set_contact_status,
60
64
  add_contact_urn,
61
65
  add_input_labels,
62
- request_optin
66
+ request_optin,
67
+ enter_flow
63
68
  });
64
69
 
65
70
  // Helper to register a config and its aliases
@@ -96,8 +101,11 @@ export const NODE_CONFIG: {
96
101
  split_by_ticket,
97
102
  split_by_webhook,
98
103
  split_by_resthook,
99
- wait_for_digits,
100
104
  wait_for_menu,
105
+ wait_for_digits,
106
+ wait_for_audio,
107
+ wait_for_dial,
101
108
  wait_for_response,
102
- split_by_airtime
109
+ split_by_airtime,
110
+ terminal // Temporary: legacy support for terminal nodes (see AppState.ts)
103
111
  });
@@ -132,7 +132,7 @@ export const createRulesItemConfig = () => ({
132
132
  multi: false,
133
133
  options: [], // Will be set by the caller
134
134
  flavor: 'xsmall' as const,
135
- width: '200px'
135
+ width: '220px'
136
136
  },
137
137
  value1: {
138
138
  type: 'text' as const,
@@ -38,10 +38,10 @@ const getOperandForField = (field: any): string => {
38
38
  }
39
39
  // For system properties, use the id
40
40
  if (field.type === 'property') {
41
- return `@contact.${field.id}`;
41
+ return `@contact.${field.id || field.value}`;
42
42
  }
43
- // Fallback to key
44
- return `@fields.${field.key}`;
43
+ // For custom fields, use key with fallbacks
44
+ return `@fields.${field.key || field.id || field.value}`;
45
45
  };
46
46
 
47
47
  export const split_by_contact_field: NodeConfig = {
@@ -79,7 +79,7 @@ export const split_by_contact_field: NodeConfig = {
79
79
  },
80
80
  rules: createRulesArrayConfig(
81
81
  operatorsToSelectOptions(getWaitForResponseOperators()),
82
- 'Define rules to split the contact field into categories'
82
+ ''
83
83
  ),
84
84
  result_name: resultNameField
85
85
  },
@@ -98,7 +98,18 @@ export const split_by_contact_field: NodeConfig = {
98
98
  },
99
99
  toFormData: (node: Node, nodeUI?: any) => {
100
100
  // Get the field from the UI config operand (source of truth)
101
- const field = nodeUI?.config?.operand || CONTACT_PROPERTIES.name;
101
+ const operand = nodeUI?.config?.operand || CONTACT_PROPERTIES.name;
102
+
103
+ // Normalize the field object to include properties expected by
104
+ // the Select component (valueKey: 'key') and getOperandForField.
105
+ // toUIConfig saves only { id, name, type }, so we need to add
106
+ // 'key' for custom fields and 'value' for static option matching.
107
+ const field = { ...operand };
108
+ if (field.type === 'field') {
109
+ if (!field.key) field.key = field.id;
110
+ } else {
111
+ if (!field.value) field.value = field.id;
112
+ }
102
113
 
103
114
  // Extract rules from router cases using shared function
104
115
  const rules = casesToFormRules(node);
@@ -34,7 +34,7 @@ export const split_by_expression: NodeConfig = {
34
34
  },
35
35
  rules: createRulesArrayConfig(
36
36
  operatorsToSelectOptions(getWaitForResponseOperators()),
37
- 'Define rules to categorize the expression result'
37
+ ''
38
38
  ),
39
39
  result_name: resultNameField
40
40
  },
@@ -34,7 +34,6 @@ export const split_by_llm_categorize: NodeConfig = {
34
34
  },
35
35
  categories: {
36
36
  type: 'array',
37
- label: 'Categories',
38
37
  helpText: 'Define the categories for classification',
39
38
  required: true,
40
39
  sortable: true,
@@ -57,7 +57,6 @@ export const split_by_random: NodeConfig = {
57
57
  form: {
58
58
  categories: {
59
59
  type: 'array',
60
- label: 'Buckets',
61
60
  helpText: 'Define the buckets to randomly split contacts into',
62
61
  required: true,
63
62
  itemLabel: 'Bucket',
@@ -105,7 +105,7 @@ export const split_by_run_result: NodeConfig = {
105
105
  },
106
106
  rules: createRulesArrayConfig(
107
107
  operatorsToSelectOptions(getWaitForResponseOperators()),
108
- 'Define rules to categorize the result'
108
+ ''
109
109
  ),
110
110
  result_name: resultNameField
111
111
  },
@@ -135,7 +135,12 @@ export const split_by_run_result: NodeConfig = {
135
135
  },
136
136
  toFormData: (node: Node, nodeUI?: any) => {
137
137
  // Get the result from the UI config operand (source of truth)
138
- const result = nodeUI?.config?.operand;
138
+ // Normalize: toUIConfig saves { id, name, type } but the Select component
139
+ // (valueKey: 'value') and fromFormData expect a 'value' property.
140
+ const savedOperand = nodeUI?.config?.operand;
141
+ const result = savedOperand
142
+ ? { ...savedOperand, value: savedOperand.value || savedOperand.id }
143
+ : null;
139
144
 
140
145
  // Extract rules from router cases using shared function
141
146
  const rules = casesToFormRules(node);
@@ -180,6 +185,7 @@ export const split_by_run_result: NodeConfig = {
180
185
 
181
186
  // Build operand based on whether delimiter is selected
182
187
  let operand: string;
188
+ const resultKey = selectedResult.value || selectedResult.id;
183
189
  const delimitBy = formData.delimit_by?.[0]?.value;
184
190
  const hasDelimiter = delimitBy !== undefined && delimitBy !== '';
185
191
 
@@ -188,10 +194,10 @@ export const split_by_run_result: NodeConfig = {
188
194
  const delimitIndex = formData.delimit_index?.[0]?.value ?? '0';
189
195
 
190
196
  // Build operand with field() function
191
- operand = `@(field(results.${selectedResult.value}, ${delimitIndex}, "${delimitBy}"))`;
197
+ operand = `@(field(results.${resultKey}, ${delimitIndex}, "${delimitBy}"))`;
192
198
  } else {
193
199
  // Standard operand without delimiter
194
- operand = `@results.${selectedResult.value}`;
200
+ operand = `@results.${resultKey}`;
195
201
  }
196
202
 
197
203
  // Get user rules using shared extraction function
@@ -0,0 +1,9 @@
1
+ // Temporary: Legacy support for terminal nodes (nodes with a terminal action
2
+ // like enter_flow with terminal: true). This node type and its reclassification
3
+ // logic in AppState.ts can be removed once we stop supporting terminal nodes.
4
+
5
+ import { NodeConfig } from '../types';
6
+
7
+ export const terminal: NodeConfig = {
8
+ type: 'terminal'
9
+ };
@@ -0,0 +1,88 @@
1
+ import { SPLIT_GROUPS, FormData, NodeConfig, FlowTypes } from '../types';
2
+ import { Node, Category, Exit } from '../../store/flow-definition';
3
+ import { generateUUID } from '../../utils';
4
+ import {
5
+ categoriesToLocalizationFormData,
6
+ localizationFormDataToCategories
7
+ } from './shared';
8
+
9
+ export const wait_for_audio: NodeConfig = {
10
+ type: 'wait_for_audio',
11
+ name: 'Make Recording',
12
+ group: SPLIT_GROUPS.wait,
13
+ flowTypes: [FlowTypes.VOICE],
14
+ form: {
15
+ result_name: {
16
+ type: 'text',
17
+ label: 'Result Name',
18
+ required: false,
19
+ placeholder: '(optional)',
20
+ helpText: 'The name to use to reference this result in the flow'
21
+ }
22
+ },
23
+ layout: ['result_name'],
24
+ toFormData: (node: Node) => {
25
+ return {
26
+ uuid: node.uuid,
27
+ result_name: node.router?.result_name || ''
28
+ };
29
+ },
30
+ fromFormData: (formData: FormData, originalNode: Node): Node => {
31
+ // Preserve or create "All Responses" category
32
+ const existingCategories = originalNode.router?.categories || [];
33
+ const existingExits = originalNode.exits || [];
34
+
35
+ let allResponsesCategory = existingCategories.find(
36
+ (cat: Category) => cat.name === 'All Responses'
37
+ );
38
+
39
+ let allResponsesExit: Exit;
40
+
41
+ if (allResponsesCategory) {
42
+ allResponsesExit = existingExits.find(
43
+ (exit: Exit) => exit.uuid === allResponsesCategory!.exit_uuid
44
+ ) || {
45
+ uuid: allResponsesCategory.exit_uuid,
46
+ destination_uuid: null
47
+ };
48
+ } else {
49
+ const exitUuid = generateUUID();
50
+ allResponsesCategory = {
51
+ uuid: generateUUID(),
52
+ name: 'All Responses',
53
+ exit_uuid: exitUuid
54
+ };
55
+ allResponsesExit = {
56
+ uuid: exitUuid,
57
+ destination_uuid: null
58
+ };
59
+ }
60
+
61
+ const router: any = {
62
+ type: 'switch',
63
+ operand: '@input',
64
+ default_category_uuid: allResponsesCategory.uuid,
65
+ cases: [],
66
+ categories: [allResponsesCategory],
67
+ wait: {
68
+ type: 'msg',
69
+ hint: {
70
+ type: 'audio'
71
+ }
72
+ }
73
+ };
74
+
75
+ if (formData.result_name && formData.result_name.trim() !== '') {
76
+ router.result_name = formData.result_name.trim();
77
+ }
78
+
79
+ return {
80
+ ...originalNode,
81
+ router,
82
+ exits: [allResponsesExit]
83
+ };
84
+ },
85
+ localizable: 'categories',
86
+ toLocalizationFormData: categoriesToLocalizationFormData,
87
+ fromLocalizationFormData: localizationFormDataToCategories
88
+ };