@nyaruka/temba-components 0.129.10 → 0.130.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 (316) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/demo/components/flow/example.html +5 -1
  3. package/demo/data/flows/sample-flow.json +217 -113
  4. package/dist/temba-components.js +310 -356
  5. package/dist/temba-components.js.map +1 -1
  6. package/out-tsc/src/events.js.map +1 -1
  7. package/out-tsc/src/flow/CanvasNode.js +3 -35
  8. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  9. package/out-tsc/src/flow/Editor.js +9 -6
  10. package/out-tsc/src/flow/Editor.js.map +1 -1
  11. package/out-tsc/src/flow/NodeEditor.js +44 -11
  12. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  13. package/out-tsc/src/flow/actions/add_contact_groups.js +1 -1
  14. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
  15. package/out-tsc/src/flow/actions/add_contact_urn.js +1 -1
  16. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  17. package/out-tsc/src/flow/actions/add_input_labels.js +1 -1
  18. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  19. package/out-tsc/src/flow/actions/remove_contact_groups.js +1 -1
  20. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
  21. package/out-tsc/src/flow/actions/send_email.js +9 -0
  22. package/out-tsc/src/flow/actions/send_email.js.map +1 -1
  23. package/out-tsc/src/flow/actions/send_msg.js +7 -8
  24. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  25. package/out-tsc/src/flow/actions/set_contact_channel.js +25 -4
  26. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  27. package/out-tsc/src/flow/actions/set_contact_field.js +51 -1
  28. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  29. package/out-tsc/src/flow/actions/set_contact_language.js +70 -2
  30. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  31. package/out-tsc/src/flow/actions/set_contact_name.js +27 -2
  32. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  33. package/out-tsc/src/flow/actions/set_contact_status.js +32 -2
  34. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  35. package/out-tsc/src/flow/actions/set_run_result.js +13 -11
  36. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  37. package/out-tsc/src/flow/actions/split_by_expression_example.js +4 -4
  38. package/out-tsc/src/flow/actions/split_by_expression_example.js.map +1 -1
  39. package/out-tsc/src/flow/config.js +2 -8
  40. package/out-tsc/src/flow/config.js.map +1 -1
  41. package/out-tsc/src/flow/forms/index.js +2 -0
  42. package/out-tsc/src/flow/forms/index.js.map +1 -0
  43. package/out-tsc/src/flow/nodes/split_by_llm.js +101 -0
  44. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -0
  45. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +4 -89
  46. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  47. package/out-tsc/src/flow/nodes/split_by_random.js +117 -0
  48. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
  49. package/out-tsc/src/flow/nodes/split_by_subflow.js +123 -3
  50. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  51. package/out-tsc/src/flow/nodes/split_by_ticket.js +114 -13
  52. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -1
  53. package/out-tsc/src/flow/nodes/split_by_webhook.js +158 -12
  54. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
  55. package/out-tsc/src/flow/types.js.map +1 -1
  56. package/out-tsc/src/form/ArrayEditor.js +9 -25
  57. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  58. package/out-tsc/src/form/FieldRenderer.js +6 -64
  59. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  60. package/out-tsc/src/form/select/Select.js +29 -58
  61. package/out-tsc/src/form/select/Select.js.map +1 -1
  62. package/out-tsc/src/live/ContactChat.js +48 -66
  63. package/out-tsc/src/live/ContactChat.js.map +1 -1
  64. package/out-tsc/src/utils.js +118 -0
  65. package/out-tsc/src/utils.js.map +1 -1
  66. package/out-tsc/test/nodes/split_by_llm.test.js +174 -0
  67. package/out-tsc/test/nodes/split_by_llm.test.js.map +1 -0
  68. package/out-tsc/test/nodes/split_by_random.test.js +0 -6
  69. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -1
  70. package/out-tsc/test/temba-field-renderer.test.js +6 -3
  71. package/out-tsc/test/temba-field-renderer.test.js.map +1 -1
  72. package/out-tsc/test/utils.test.js +18 -0
  73. package/out-tsc/test/utils.test.js.map +1 -1
  74. package/package.json +1 -1
  75. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  76. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  77. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  78. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  79. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  80. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  81. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  82. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  83. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  84. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  85. package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
  86. package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
  87. package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
  88. package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
  89. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  90. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  91. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  92. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  93. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  94. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  95. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  96. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  97. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  98. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  99. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  100. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  101. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  102. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  103. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  104. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  105. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  106. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  107. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  108. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  109. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  110. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  111. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  112. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  113. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  114. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  115. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  116. package/screenshots/truth/checkbox/checkbox-label-background-hover.png +0 -0
  117. package/screenshots/truth/checkbox/checkbox-no-label-no-background-hover.png +0 -0
  118. package/screenshots/truth/checkbox/checkbox-whitespace-label-no-background-hover.png +0 -0
  119. package/screenshots/truth/checkbox/checkbox-with-help-text.png +0 -0
  120. package/screenshots/truth/checkbox/checked.png +0 -0
  121. package/screenshots/truth/checkbox/default.png +0 -0
  122. package/screenshots/truth/colorpicker/default.png +0 -0
  123. package/screenshots/truth/colorpicker/focused.png +0 -0
  124. package/screenshots/truth/colorpicker/initialized.png +0 -0
  125. package/screenshots/truth/colorpicker/selected.png +0 -0
  126. package/screenshots/truth/compose/attachments-tab.png +0 -0
  127. package/screenshots/truth/compose/attachments-with-files.png +0 -0
  128. package/screenshots/truth/compose/intial-text.png +0 -0
  129. package/screenshots/truth/compose/no-counter.png +0 -0
  130. package/screenshots/truth/compose/wraps-text-and-spaces.png +0 -0
  131. package/screenshots/truth/compose/wraps-text-and-url.png +0 -0
  132. package/screenshots/truth/compose/wraps-text-no-spaces.png +0 -0
  133. package/screenshots/truth/contacts/chat-failure.png +0 -0
  134. package/screenshots/truth/contacts/chat-for-active-contact.png +0 -0
  135. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  136. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  137. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  138. package/screenshots/truth/counter/summary.png +0 -0
  139. package/screenshots/truth/counter/text.png +0 -0
  140. package/screenshots/truth/counter/unicode-variables.png +0 -0
  141. package/screenshots/truth/counter/unicode.png +0 -0
  142. package/screenshots/truth/counter/variable.png +0 -0
  143. package/screenshots/truth/datepicker/date-truncated-time.png +0 -0
  144. package/screenshots/truth/datepicker/date.png +0 -0
  145. package/screenshots/truth/datepicker/initial-timezone.png +0 -0
  146. package/screenshots/truth/datepicker/range-picker-editing-start.png +0 -0
  147. package/screenshots/truth/datepicker/updated-keyboard-date.png +0 -0
  148. package/screenshots/truth/dialog/focused.png +0 -0
  149. package/screenshots/truth/dropdown/right-edge-collision.png +0 -0
  150. package/screenshots/truth/editor/router.png +0 -0
  151. package/screenshots/truth/editor/send_msg.png +0 -0
  152. package/screenshots/truth/editor/set_contact_language.png +0 -0
  153. package/screenshots/truth/editor/set_contact_name.png +0 -0
  154. package/screenshots/truth/editor/set_run_result.png +0 -0
  155. package/screenshots/truth/editor/wait.png +0 -0
  156. package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
  157. package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
  158. package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
  159. package/screenshots/truth/field-renderer/context-comparison.png +0 -0
  160. package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
  161. package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
  162. package/screenshots/truth/field-renderer/select-multi.png +0 -0
  163. package/screenshots/truth/field-renderer/select-no-label.png +0 -0
  164. package/screenshots/truth/field-renderer/select-with-label.png +0 -0
  165. package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
  166. package/screenshots/truth/field-renderer/text-no-label.png +0 -0
  167. package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
  168. package/screenshots/truth/field-renderer/text-with-label.png +0 -0
  169. package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
  170. package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
  171. package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
  172. package/screenshots/truth/list/fields-dragging.png +0 -0
  173. package/screenshots/truth/list/fields-filtered.png +0 -0
  174. package/screenshots/truth/list/fields-hovered.png +0 -0
  175. package/screenshots/truth/list/fields.png +0 -0
  176. package/screenshots/truth/list/items-selected.png +0 -0
  177. package/screenshots/truth/list/items-updated.png +0 -0
  178. package/screenshots/truth/list/items.png +0 -0
  179. package/screenshots/truth/menu/menu-focused-with items.png +0 -0
  180. package/screenshots/truth/menu/menu-refresh-1.png +0 -0
  181. package/screenshots/truth/menu/menu-refresh-2.png +0 -0
  182. package/screenshots/truth/menu/menu-submenu.png +0 -0
  183. package/screenshots/truth/menu/menu-tasks-nextup.png +0 -0
  184. package/screenshots/truth/menu/menu-tasks.png +0 -0
  185. package/screenshots/truth/message-editor/autogrow-initial-content.png +0 -0
  186. package/screenshots/truth/message-editor/default.png +0 -0
  187. package/screenshots/truth/message-editor/drag-highlight.png +0 -0
  188. package/screenshots/truth/message-editor/filtered-attachments.png +0 -0
  189. package/screenshots/truth/message-editor/with-completion.png +0 -0
  190. package/screenshots/truth/message-editor/with-properties.png +0 -0
  191. package/screenshots/truth/modax/form.png +0 -0
  192. package/screenshots/truth/modax/simple.png +0 -0
  193. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  194. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  195. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  196. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  197. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  198. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  199. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  200. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  201. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  202. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  203. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  204. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  205. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  206. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  207. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  208. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  209. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  210. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  211. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  212. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  213. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  214. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  215. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  216. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  217. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  218. package/screenshots/truth/omnibox/selected.png +0 -0
  219. package/screenshots/truth/options/block.png +0 -0
  220. package/screenshots/truth/run-list/basic.png +0 -0
  221. package/screenshots/truth/select/disabled-multi-selection.png +0 -0
  222. package/screenshots/truth/select/disabled-selection.png +0 -0
  223. package/screenshots/truth/select/disabled.png +0 -0
  224. package/screenshots/truth/select/embedded.png +0 -0
  225. package/screenshots/truth/select/empty-options.png +0 -0
  226. package/screenshots/truth/select/expression-selected.png +0 -0
  227. package/screenshots/truth/select/expressions.png +0 -0
  228. package/screenshots/truth/select/functions.png +0 -0
  229. package/screenshots/truth/select/local-options.png +0 -0
  230. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  231. package/screenshots/truth/select/multiple-initial-values.png +0 -0
  232. package/screenshots/truth/select/remote-options.png +0 -0
  233. package/screenshots/truth/select/search-enabled.png +0 -0
  234. package/screenshots/truth/select/search-multi-no-matches.png +0 -0
  235. package/screenshots/truth/select/search-selected-focus.png +0 -0
  236. package/screenshots/truth/select/search-selected.png +0 -0
  237. package/screenshots/truth/select/search-with-selected.png +0 -0
  238. package/screenshots/truth/select/searching.png +0 -0
  239. package/screenshots/truth/select/selected-multi-maxitems-reached.png +0 -0
  240. package/screenshots/truth/select/selected-multi.png +0 -0
  241. package/screenshots/truth/select/selected-single.png +0 -0
  242. package/screenshots/truth/select/selection-clearable.png +0 -0
  243. package/screenshots/truth/select/static-initial-value.png +0 -0
  244. package/screenshots/truth/select/static-initial-via-selected.png +0 -0
  245. package/screenshots/truth/select/truncated-selection.png +0 -0
  246. package/screenshots/truth/select/with-placeholder.png +0 -0
  247. package/screenshots/truth/select/without-placeholder.png +0 -0
  248. package/screenshots/truth/slider/update-slider-on-circle-dragged.png +0 -0
  249. package/screenshots/truth/templates/default.png +0 -0
  250. package/screenshots/truth/templates/unapproved.png +0 -0
  251. package/screenshots/truth/textinput/autogrow-initial.png +0 -0
  252. package/screenshots/truth/textinput/input-disabled.png +0 -0
  253. package/screenshots/truth/textinput/input-focused.png +0 -0
  254. package/screenshots/truth/textinput/input-form.png +0 -0
  255. package/screenshots/truth/textinput/input-inserted.png +0 -0
  256. package/screenshots/truth/textinput/input-placeholder.png +0 -0
  257. package/screenshots/truth/textinput/input-updated.png +0 -0
  258. package/screenshots/truth/textinput/input.png +0 -0
  259. package/screenshots/truth/textinput/textarea-focused.png +0 -0
  260. package/screenshots/truth/textinput/textarea.png +0 -0
  261. package/src/events.ts +4 -2
  262. package/src/flow/CanvasNode.ts +2 -39
  263. package/src/flow/Editor.ts +6 -3
  264. package/src/flow/NodeEditor.ts +54 -13
  265. package/src/flow/actions/add_contact_groups.ts +1 -1
  266. package/src/flow/actions/add_contact_urn.ts +1 -1
  267. package/src/flow/actions/add_input_labels.ts +1 -1
  268. package/src/flow/actions/remove_contact_groups.ts +1 -1
  269. package/src/flow/actions/send_email.ts +11 -1
  270. package/src/flow/actions/send_msg.ts +20 -11
  271. package/src/flow/actions/set_contact_channel.ts +28 -5
  272. package/src/flow/actions/set_contact_field.ts +56 -2
  273. package/src/flow/actions/set_contact_language.ts +74 -3
  274. package/src/flow/actions/set_contact_name.ts +31 -3
  275. package/src/flow/actions/set_contact_status.ts +36 -3
  276. package/src/flow/actions/set_run_result.ts +13 -15
  277. package/src/flow/actions/split_by_expression_example.ts +4 -4
  278. package/src/flow/config.ts +2 -8
  279. package/src/flow/forms/index.ts +1 -0
  280. package/src/flow/nodes/split_by_llm.ts +119 -0
  281. package/src/flow/nodes/split_by_llm_categorize.ts +13 -116
  282. package/src/flow/nodes/split_by_random.ts +148 -0
  283. package/src/flow/nodes/split_by_subflow.ts +153 -3
  284. package/src/flow/nodes/split_by_ticket.ts +134 -12
  285. package/src/flow/nodes/split_by_webhook.ts +185 -12
  286. package/src/flow/types.ts +2 -1
  287. package/src/form/ArrayEditor.ts +6 -20
  288. package/src/form/FieldRenderer.ts +6 -65
  289. package/src/form/select/Select.ts +34 -66
  290. package/src/live/ContactChat.ts +56 -58
  291. package/src/store/flow-definition.d.ts +8 -2
  292. package/src/utils.ts +196 -0
  293. package/static/api/fields.json +93 -1208
  294. package/static/api/workspace.json +23 -0
  295. package/test/nodes/split_by_llm.test.ts +232 -0
  296. package/test/nodes/split_by_random.test.ts +0 -7
  297. package/test/temba-field-renderer.test.ts +26 -13
  298. package/test/utils.test.ts +20 -0
  299. package/test-assets/style.css +36 -234
  300. package/web-dev-server.config.mjs +28 -0
  301. package/web-test-runner.config.mjs +38 -1
  302. package/out-tsc/src/flow/actions/call_llm.js +0 -64
  303. package/out-tsc/src/flow/actions/call_llm.js.map +0 -1
  304. package/out-tsc/src/flow/actions/call_webhook.js +0 -131
  305. package/out-tsc/src/flow/actions/call_webhook.js.map +0 -1
  306. package/out-tsc/src/flow/actions/enter_flow.js +0 -14
  307. package/out-tsc/src/flow/actions/enter_flow.js.map +0 -1
  308. package/out-tsc/src/flow/actions/open_ticket.js +0 -73
  309. package/out-tsc/src/flow/actions/open_ticket.js.map +0 -1
  310. package/out-tsc/test/actions/call_llm.test.js +0 -103
  311. package/out-tsc/test/actions/call_llm.test.js.map +0 -1
  312. package/src/flow/actions/call_llm.ts +0 -66
  313. package/src/flow/actions/call_webhook.ts +0 -143
  314. package/src/flow/actions/enter_flow.ts +0 -15
  315. package/src/flow/actions/open_ticket.ts +0 -83
  316. package/test/actions/call_llm.test.ts +0 -137
@@ -1,9 +1,157 @@
1
1
  import { COLORS, NodeConfig } from '../types';
2
+ import { Node, Category, Exit } from '../../store/flow-definition.d';
3
+ import { generateUUID } from '../../utils';
4
+
5
+ // Helper function to create a random router with categories
6
+ const createRandomRouter = (
7
+ userCategories: string[],
8
+ existingCategories: Category[] = [],
9
+ existingExits: Exit[] = []
10
+ ) => {
11
+ const categories: Category[] = [];
12
+ const exits: Exit[] = [];
13
+
14
+ // Create categories and exits for user-defined buckets
15
+ userCategories.forEach((categoryName) => {
16
+ // Try to find existing category by name
17
+ const existingCategory = existingCategories.find(
18
+ (cat) => cat.name === categoryName
19
+ );
20
+ const existingExit = existingCategory
21
+ ? existingExits.find((exit) => exit.uuid === existingCategory.exit_uuid)
22
+ : null;
23
+
24
+ const exitUuid = existingExit?.uuid || generateUUID();
25
+ const categoryUuid = existingCategory?.uuid || generateUUID();
26
+
27
+ categories.push({
28
+ uuid: categoryUuid,
29
+ name: categoryName,
30
+ exit_uuid: exitUuid
31
+ });
32
+
33
+ exits.push({
34
+ uuid: exitUuid,
35
+ destination_uuid: existingExit?.destination_uuid || null
36
+ });
37
+ });
38
+
39
+ return {
40
+ router: {
41
+ type: 'random' as const,
42
+ categories: categories
43
+ },
44
+ exits: exits
45
+ };
46
+ };
2
47
 
3
48
  export const split_by_random: NodeConfig = {
4
49
  type: 'split_by_random',
5
50
  name: 'Split by Random',
6
51
  color: COLORS.split,
52
+ form: {
53
+ categories: {
54
+ type: 'array',
55
+ label: 'Buckets',
56
+ helpText: 'Define the buckets to randomly split contacts into',
57
+ required: true,
58
+ itemLabel: 'Bucket',
59
+ minItems: 2,
60
+ maxItems: 10,
61
+ isEmptyItem: (item: any) => {
62
+ return !item.name || item.name.trim() === '';
63
+ },
64
+ itemConfig: {
65
+ name: {
66
+ type: 'text',
67
+ placeholder: 'Bucket name',
68
+ required: true
69
+ }
70
+ }
71
+ }
72
+ },
73
+ layout: ['categories'],
74
+ validate: (formData: any) => {
75
+ const errors: { [key: string]: string } = {};
76
+
77
+ // Check for duplicate category names
78
+ if (formData.categories && Array.isArray(formData.categories)) {
79
+ const categories = formData.categories.filter(
80
+ (item: any) => item?.name && item.name.trim() !== ''
81
+ );
82
+
83
+ // Ensure minimum buckets
84
+ if (categories.length < 2) {
85
+ errors.categories = 'At least 2 buckets are required for random split';
86
+ }
87
+
88
+ // Find all categories that have duplicates (case-insensitive)
89
+ const duplicateCategories = [];
90
+ const lowerCaseMap = new Map();
91
+
92
+ // First pass: map lowercase names to all original cases
93
+ categories.forEach((category) => {
94
+ const lowerName = category.name.trim().toLowerCase();
95
+ if (!lowerCaseMap.has(lowerName)) {
96
+ lowerCaseMap.set(lowerName, []);
97
+ }
98
+ lowerCaseMap.get(lowerName).push(category.name.trim());
99
+ });
100
+
101
+ // Second pass: collect all names that appear more than once
102
+ lowerCaseMap.forEach((originalNames) => {
103
+ if (originalNames.length > 1) {
104
+ duplicateCategories.push(...originalNames);
105
+ }
106
+ });
107
+
108
+ if (duplicateCategories.length > 0) {
109
+ const uniqueDuplicates = [...new Set(duplicateCategories)];
110
+ errors.categories = `Duplicate bucket names found: ${uniqueDuplicates.join(
111
+ ', '
112
+ )}`;
113
+ }
114
+ }
115
+
116
+ return {
117
+ valid: Object.keys(errors).length === 0,
118
+ errors
119
+ };
120
+ },
121
+ toFormData: (node: Node) => {
122
+ // Extract categories from the existing node structure
123
+ const categories =
124
+ node.router?.categories?.map((cat) => ({ name: cat.name })) || [];
125
+
126
+ return {
127
+ uuid: node.uuid,
128
+ categories: categories
129
+ };
130
+ },
131
+ fromFormData: (formData: any, originalNode: Node): Node => {
132
+ // Get user categories
133
+ const userCategories = (formData.categories || [])
134
+ .filter((item: any) => item?.name?.trim())
135
+ .map((item: any) => item.name.trim());
136
+
137
+ // Create router and exits using existing data when possible
138
+ const existingCategories = originalNode.router?.categories || [];
139
+ const existingExits = originalNode.exits || [];
140
+
141
+ const { router, exits } = createRandomRouter(
142
+ userCategories,
143
+ existingCategories,
144
+ existingExits
145
+ );
146
+
147
+ // Return the complete node
148
+ return {
149
+ uuid: originalNode.uuid,
150
+ actions: originalNode.actions || [],
151
+ router: router,
152
+ exits: exits
153
+ };
154
+ },
7
155
  router: {
8
156
  type: 'random'
9
157
  }
@@ -1,9 +1,159 @@
1
- import { enter_flow } from '../actions/enter_flow';
2
1
  import { COLORS, NodeConfig } from '../types';
2
+ import { Node } from '../../store/flow-definition';
3
+ import { generateUUID } from '../../utils';
4
+ import { html } from 'lit';
3
5
 
4
6
  export const split_by_subflow: NodeConfig = {
5
7
  type: 'split_by_subflow',
6
- name: 'Split by Subflow',
8
+ name: 'Enter a Flow',
7
9
  color: COLORS.execute,
8
- action: enter_flow
10
+ form: {
11
+ flow: {
12
+ type: 'select',
13
+ required: true,
14
+ placeholder: 'Select a flow...',
15
+ helpText:
16
+ 'Once the subflow is complete or expires, the contact will return here',
17
+ endpoint: '/api/v2/flows.json',
18
+ valueKey: 'uuid',
19
+ nameKey: 'name'
20
+ }
21
+ },
22
+ layout: ['flow'],
23
+ render: (node: Node) => {
24
+ const enterFlowAction = node.actions?.find(
25
+ (action) => action.type === 'enter_flow'
26
+ ) as any;
27
+ return html`
28
+ <div class="body">
29
+ ${enterFlowAction?.flow?.name || 'Configure subflow'}
30
+ </div>
31
+ `;
32
+ },
33
+ toFormData: (node: Node) => {
34
+ // Extract data from the existing node structure
35
+ const enterFlowAction = node.actions?.find(
36
+ (action) => action.type === 'enter_flow'
37
+ ) as any;
38
+
39
+ return {
40
+ uuid: node.uuid,
41
+ flow: enterFlowAction?.flow
42
+ ? [{ uuid: enterFlowAction.flow.uuid, name: enterFlowAction.flow.name }]
43
+ : []
44
+ };
45
+ },
46
+ fromFormData: (formData: any, originalNode: Node): Node => {
47
+ // Get flow selection
48
+ const flowSelection =
49
+ Array.isArray(formData.flow) && formData.flow.length > 0
50
+ ? formData.flow[0]
51
+ : null;
52
+
53
+ // Find existing enter_flow action to preserve its UUID
54
+ const existingEnterFlowAction = originalNode.actions?.find(
55
+ (action) => action.type === 'enter_flow'
56
+ );
57
+ const enterFlowUuid = existingEnterFlowAction?.uuid || generateUUID();
58
+
59
+ // Create enter_flow action
60
+ const enterFlowAction: any = {
61
+ type: 'enter_flow',
62
+ uuid: enterFlowUuid,
63
+ flow: flowSelection
64
+ ? {
65
+ uuid: flowSelection.uuid || flowSelection.value,
66
+ name: flowSelection.name
67
+ }
68
+ : { uuid: '', name: '' }
69
+ };
70
+
71
+ // Create categories and exits for Complete and Expired
72
+ const existingCategories = originalNode.router?.categories || [];
73
+ const existingExits = originalNode.exits || [];
74
+ const existingCases = originalNode.router?.cases || [];
75
+
76
+ // Find existing Complete category
77
+ const existingCompleteCategory = existingCategories.find(
78
+ (cat) => cat.name === 'Complete'
79
+ );
80
+ const existingCompleteExit = existingCompleteCategory
81
+ ? existingExits.find(
82
+ (exit) => exit.uuid === existingCompleteCategory.exit_uuid
83
+ )
84
+ : null;
85
+ const existingCompleteCase = existingCompleteCategory
86
+ ? existingCases.find(
87
+ (case_) => case_.category_uuid === existingCompleteCategory.uuid
88
+ )
89
+ : null;
90
+
91
+ const completeCategoryUuid =
92
+ existingCompleteCategory?.uuid || generateUUID();
93
+ const completeExitUuid = existingCompleteExit?.uuid || generateUUID();
94
+ const completeCaseUuid = existingCompleteCase?.uuid || generateUUID();
95
+
96
+ // Find existing Expired category
97
+ const existingExpiredCategory = existingCategories.find(
98
+ (cat) => cat.name === 'Expired'
99
+ );
100
+ const existingExpiredExit = existingExpiredCategory
101
+ ? existingExits.find(
102
+ (exit) => exit.uuid === existingExpiredCategory.exit_uuid
103
+ )
104
+ : null;
105
+
106
+ const expiredCategoryUuid = existingExpiredCategory?.uuid || generateUUID();
107
+ const expiredExitUuid = existingExpiredExit?.uuid || generateUUID();
108
+
109
+ const categories = [
110
+ {
111
+ uuid: completeCategoryUuid,
112
+ name: 'Complete',
113
+ exit_uuid: completeExitUuid
114
+ },
115
+ {
116
+ uuid: expiredCategoryUuid,
117
+ name: 'Expired',
118
+ exit_uuid: expiredExitUuid
119
+ }
120
+ ];
121
+
122
+ const exits = [
123
+ {
124
+ uuid: completeExitUuid,
125
+ destination_uuid: existingCompleteExit?.destination_uuid || null
126
+ },
127
+ {
128
+ uuid: expiredExitUuid,
129
+ destination_uuid: existingExpiredExit?.destination_uuid || null
130
+ }
131
+ ];
132
+
133
+ const cases = [
134
+ {
135
+ uuid: completeCaseUuid,
136
+ type: 'has_only_text',
137
+ arguments: ['completed'],
138
+ category_uuid: completeCategoryUuid
139
+ }
140
+ ];
141
+
142
+ // Create the router
143
+ const router = {
144
+ type: 'switch' as const,
145
+ categories: categories,
146
+ default_category_uuid: expiredCategoryUuid,
147
+ operand: '@child.status',
148
+ cases: cases
149
+ };
150
+
151
+ // Return the complete node
152
+ return {
153
+ uuid: originalNode.uuid,
154
+ actions: [enterFlowAction],
155
+ router: router,
156
+ exits: exits
157
+ };
158
+ }
9
159
  };
@@ -1,19 +1,141 @@
1
- import { open_ticket } from '../actions/open_ticket';
2
- import { NodeConfig } from '../types';
1
+ import { COLORS, NodeConfig } from '../types';
2
+ import { Node, OpenTicket } from '../../store/flow-definition';
3
+ import { generateUUID, createSuccessFailureRouter } from '../../utils';
4
+ import { html } from 'lit';
3
5
 
4
6
  export const split_by_ticket: NodeConfig = {
5
7
  type: 'split_by_ticket',
6
- action: open_ticket,
7
- router: {
8
- type: 'switch',
9
- defaultCategory: 'Failure',
10
- operand: '@locals._new_ticket',
11
- rules: [
8
+ name: 'Open Ticket',
9
+ color: COLORS.create,
10
+ form: {
11
+ topic: {
12
+ type: 'select',
13
+ label: 'Topic',
14
+ required: true,
15
+ placeholder: 'Select a topic',
16
+ options: [],
17
+ endpoint: '/api/v2/topics.json',
18
+ valueKey: 'uuid',
19
+ nameKey: 'name',
20
+ maxWidth: '200px'
21
+ },
22
+ assignee: {
23
+ type: 'select',
24
+ label: 'Assignee',
25
+ required: false,
26
+ placeholder: 'Select an agent (optional)',
27
+ endpoint: '/api/v2/users.json',
28
+ valueKey: 'uuid',
29
+ getName: (item: {
30
+ first_name?: string;
31
+ last_name?: string;
32
+ name?: string;
33
+ }) => {
34
+ return item.name || [item.first_name, item.last_name].join(' ');
35
+ },
36
+ clearable: true
37
+ },
38
+ note: {
39
+ type: 'textarea',
40
+ label: 'Note',
41
+ required: false,
42
+ placeholder: 'Enter a note for the ticket (optional)',
43
+ minHeight: 100
44
+ }
45
+ },
46
+ layout: [{ type: 'row', items: ['topic', 'assignee'] }, 'note'],
47
+ render: (node: Node) => {
48
+ const openTicketAction = node.actions?.find(
49
+ (action) => action.type === 'open_ticket'
50
+ ) as OpenTicket;
51
+ return html`
52
+ <div class="body">
53
+ ${openTicketAction?.topic?.name || 'Configure ticket'}
54
+ </div>
55
+ `;
56
+ },
57
+ toFormData: (node: Node) => {
58
+ // Extract data from the existing node structure
59
+ const openTicketAction = node.actions?.find(
60
+ (action) => action.type === 'open_ticket'
61
+ ) as any;
62
+
63
+ return {
64
+ uuid: node.uuid,
65
+ topic: openTicketAction?.topic
66
+ ? [
67
+ {
68
+ uuid: openTicketAction.topic.uuid,
69
+ name: openTicketAction.topic.name
70
+ }
71
+ ]
72
+ : [],
73
+ assignee: openTicketAction?.assignee
74
+ ? [
75
+ {
76
+ uuid: openTicketAction.assignee.uuid,
77
+ name: openTicketAction.assignee.name
78
+ }
79
+ ]
80
+ : [],
81
+ note: openTicketAction?.note || ''
82
+ };
83
+ },
84
+ fromFormData: (formData: any, originalNode: Node): Node => {
85
+ // Find existing open_ticket action to preserve its UUID
86
+ const existingOpenTicketAction = originalNode.actions?.find(
87
+ (action) => action.type === 'open_ticket'
88
+ );
89
+ const openTicketUuid = existingOpenTicketAction?.uuid || generateUUID();
90
+
91
+ // Create open_ticket action
92
+ const openTicketAction: OpenTicket = {
93
+ type: 'open_ticket',
94
+ uuid: openTicketUuid,
95
+ topic:
96
+ formData.topic && formData.topic.length > 0
97
+ ? {
98
+ uuid: formData.topic[0].uuid,
99
+ name: formData.topic[0].name
100
+ }
101
+ : undefined,
102
+ assignee:
103
+ formData.assignee && formData.assignee.length > 0
104
+ ? {
105
+ uuid: formData.assignee[0].uuid,
106
+ name:
107
+ formData.assignee[0].name ||
108
+ [
109
+ formData.assignee[0].first_name,
110
+ formData.assignee[0].last_name
111
+ ].join(' ')
112
+ }
113
+ : undefined,
114
+ note: formData.note || ''
115
+ };
116
+
117
+ // Create categories and exits for Success and Failure
118
+ const existingCategories = originalNode.router?.categories || [];
119
+ const existingExits = originalNode.exits || [];
120
+ const existingCases = originalNode.router?.cases || [];
121
+
122
+ const { router, exits } = createSuccessFailureRouter(
123
+ '@locals._new_ticket',
12
124
  {
13
125
  type: 'has_text',
14
- arguments: [],
15
- categoryName: 'Success'
16
- }
17
- ]
126
+ arguments: []
127
+ },
128
+ existingCategories,
129
+ existingExits,
130
+ existingCases
131
+ );
132
+
133
+ // Return the complete node
134
+ return {
135
+ uuid: originalNode.uuid,
136
+ actions: [openTicketAction],
137
+ router: router,
138
+ exits: exits
139
+ };
18
140
  }
19
141
  };
@@ -1,19 +1,192 @@
1
- import { call_webhook } from '../actions/call_webhook';
2
- import { NodeConfig } from '../types';
1
+ import { COLORS, NodeConfig } from '../types';
2
+ import { CallWebhook, Node } from '../../store/flow-definition';
3
+ import { generateUUID, createSuccessFailureRouter } from '../../utils';
4
+ import { html } from 'lit';
5
+
6
+ const defaultPost = `@(json(object(
7
+ "contact", object(
8
+ "uuid", contact.uuid,
9
+ "name", contact.name,
10
+ "urn", contact.urn
11
+ ),
12
+ "flow", object(
13
+ "uuid", run.flow.uuid,
14
+ "name", run.flow.name
15
+ ),
16
+ "results", foreach_value(results, extract_object, "value", "category")
17
+ )))`;
3
18
 
4
19
  export const split_by_webhook: NodeConfig = {
5
20
  type: 'split_by_webhook',
6
- action: call_webhook,
7
- router: {
8
- type: 'switch',
9
- defaultCategory: 'Failure',
10
- operand: '@webhook.status',
11
- rules: [
21
+ name: 'Call Webhook',
22
+ color: COLORS.call,
23
+ form: {
24
+ method: {
25
+ type: 'select',
26
+ required: true,
27
+ options: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH'],
28
+ maxWidth: '120px',
29
+ searchable: false
30
+ },
31
+ url: {
32
+ type: 'text',
33
+ required: true,
34
+ evaluated: true,
35
+ placeholder: 'https://example.com/webhook'
36
+ },
37
+ headers: {
38
+ type: 'key-value',
39
+ sortable: true,
40
+ keyPlaceholder: 'Header name',
41
+ valuePlaceholder: 'Header value',
42
+ minRows: 0
43
+ },
44
+ body: {
45
+ type: 'textarea',
46
+ evaluated: true,
47
+ placeholder: 'Request body content (JSON, XML, etc.)',
48
+ minHeight: 200,
49
+ dependsOn: ['method'],
50
+ computeValue: (
51
+ values: Record<string, any>,
52
+ currentValue: any,
53
+ originalValues?: Record<string, any>
54
+ ) => {
55
+ // Check if method is POST (handle both string and select object formats)
56
+ const method =
57
+ Array.isArray(values.method) && values.method.length > 0
58
+ ? values.method[0].value || values.method[0].name
59
+ : values.method;
60
+
61
+ if (method === 'POST') {
62
+ // For POST, provide the template if body is empty or was never set by user
63
+ if (!currentValue || currentValue.trim() === '') {
64
+ return defaultPost;
65
+ }
66
+ } else {
67
+ // For non-POST methods, clear the body if it was auto-generated or empty
68
+ // Check if the original body was empty (user never specified a body)
69
+ const originalBody = originalValues?.body || '';
70
+ const isOriginallyEmpty = !originalBody || originalBody.trim() === '';
71
+
72
+ // Clear if: originally empty, contains default template, or is currently empty
73
+ if (
74
+ isOriginallyEmpty ||
75
+ !currentValue ||
76
+ currentValue.trim() === '' ||
77
+ currentValue.trim() === defaultPost.trim()
78
+ ) {
79
+ return '';
80
+ }
81
+ }
82
+
83
+ return currentValue; // Keep existing value if user has customized it
84
+ }
85
+ }
86
+ },
87
+ layout: [
88
+ // Row with method and URL side by side
89
+ { type: 'row', items: ['method', 'url'] },
90
+ // Advanced group with nested layouts
91
+ {
92
+ type: 'group',
93
+ label: 'Headers',
94
+ items: ['headers'],
95
+ collapsible: true,
96
+ collapsed: true,
97
+ helpText: 'Configure authentication or custom headers',
98
+ getGroupValueCount: (formData: any) => {
99
+ return formData.headers?.length || 0;
100
+ }
101
+ },
102
+ {
103
+ type: 'group',
104
+ label: 'Body',
105
+ items: ['body'],
106
+ collapsible: true,
107
+ collapsed: true,
108
+ helpText: 'Configure the request payload',
109
+ getGroupValueCount: (formData: any) => {
110
+ return !!(
111
+ formData.body &&
112
+ formData.body.trim() !== '' &&
113
+ formData.body !== defaultPost
114
+ );
115
+ }
116
+ }
117
+ ],
118
+ render: (node: Node) => {
119
+ const callWebhookAction = node.actions?.find(
120
+ (action) => action.type === 'call_webhook'
121
+ ) as CallWebhook;
122
+ return html`
123
+ <div
124
+ class="body"
125
+ style="word-wrap: break-word; overflow-wrap: break-word; hyphens: auto;"
126
+ >
127
+ ${callWebhookAction?.url || 'Configure webhook'}
128
+ </div>
129
+ `;
130
+ },
131
+ toFormData: (node: Node) => {
132
+ // Extract data from the existing node structure
133
+ const callWebhookAction = node.actions?.find(
134
+ (action) => action.type === 'call_webhook'
135
+ ) as any;
136
+
137
+ return {
138
+ uuid: node.uuid,
139
+ method: callWebhookAction?.method || 'GET',
140
+ url: callWebhookAction?.url || '',
141
+ headers: callWebhookAction?.headers || [],
142
+ body: callWebhookAction?.body || ''
143
+ };
144
+ },
145
+ fromFormData: (formData: any, originalNode: Node): Node => {
146
+ // Get method selection
147
+ const methodSelection =
148
+ Array.isArray(formData.method) && formData.method.length > 0
149
+ ? formData.method[0]
150
+ : { value: 'GET', name: 'GET' };
151
+
152
+ // Find existing call_webhook action to preserve its UUID
153
+ const existingCallWebhookAction = originalNode.actions?.find(
154
+ (action) => action.type === 'call_webhook'
155
+ );
156
+ const callWebhookUuid = existingCallWebhookAction?.uuid || generateUUID();
157
+
158
+ // Create call_webhook action
159
+ const callWebhookAction: CallWebhook = {
160
+ type: 'call_webhook',
161
+ uuid: callWebhookUuid,
162
+ method: methodSelection.value,
163
+ url: formData.url || '',
164
+ headers: formData.headers || [],
165
+ body: formData.body || ''
166
+ };
167
+
168
+ // Create categories and exits for Success and Failure
169
+ const existingCategories = originalNode.router?.categories || [];
170
+ const existingExits = originalNode.exits || [];
171
+ const existingCases = originalNode.router?.cases || [];
172
+
173
+ const { router, exits } = createSuccessFailureRouter(
174
+ '@webhook.status',
12
175
  {
13
176
  type: 'has_number_between',
14
- arguments: ['200', '299'],
15
- categoryName: 'Success'
16
- }
17
- ]
177
+ arguments: ['200', '299']
178
+ },
179
+ existingCategories,
180
+ existingExits,
181
+ existingCases
182
+ );
183
+
184
+ // Return the complete node
185
+ return {
186
+ uuid: originalNode.uuid,
187
+ actions: [callWebhookAction],
188
+ router: router,
189
+ exits: exits
190
+ };
18
191
  }
19
192
  };
package/src/flow/types.ts CHANGED
@@ -143,7 +143,7 @@ export interface TextareaFieldConfig extends BaseFieldConfig {
143
143
 
144
144
  export interface SelectFieldConfig extends BaseFieldConfig {
145
145
  type: 'select';
146
- options?: string[] | { value: string; label: string }[];
146
+ options?: string[] | { value: string; name: string }[];
147
147
  multi?: boolean;
148
148
  clearable?: boolean;
149
149
  searchable?: boolean;
@@ -158,6 +158,7 @@ export interface SelectFieldConfig extends BaseFieldConfig {
158
158
  flavor?: 'small' | 'large';
159
159
  createArbitraryOption?: (input: string, options: any[]) => any;
160
160
  allowCreate?: boolean;
161
+ getDynamicOptions?: () => Array<{ value: string; name: string }>;
161
162
  }
162
163
 
163
164
  export interface KeyValueFieldConfig extends BaseFieldConfig {