@nyaruka/temba-components 0.131.1 → 0.131.2

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 (427) hide show
  1. package/.github/workflows/publish.yml +4 -1
  2. package/CHANGELOG.md +61 -1
  3. package/demo/data/flows/food-order.json +2 -2
  4. package/demo/data/flows/sample-flow.json +74 -125
  5. package/dist/static/svg/index.svg +1 -1
  6. package/dist/temba-components.js +1155 -618
  7. package/dist/temba-components.js.map +1 -1
  8. package/out-tsc/src/Icons.js +4 -1
  9. package/out-tsc/src/Icons.js.map +1 -1
  10. package/out-tsc/src/events.js.map +1 -1
  11. package/out-tsc/src/flow/CanvasMenu.js +200 -0
  12. package/out-tsc/src/flow/CanvasMenu.js.map +1 -0
  13. package/out-tsc/src/flow/CanvasNode.js +327 -19
  14. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  15. package/out-tsc/src/flow/Editor.js +562 -66
  16. package/out-tsc/src/flow/Editor.js.map +1 -1
  17. package/out-tsc/src/flow/NodeEditor.js +240 -93
  18. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  19. package/out-tsc/src/flow/NodeTypeSelector.js +499 -0
  20. package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -0
  21. package/out-tsc/src/flow/actions/add_contact_groups.js +3 -3
  22. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
  23. package/out-tsc/src/flow/actions/add_contact_urn.js +62 -4
  24. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  25. package/out-tsc/src/flow/actions/add_input_labels.js +3 -3
  26. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  27. package/out-tsc/src/flow/actions/play_audio.js +2 -2
  28. package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
  29. package/out-tsc/src/flow/actions/remove_contact_groups.js +6 -5
  30. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
  31. package/out-tsc/src/flow/actions/request_optin.js +2 -2
  32. package/out-tsc/src/flow/actions/request_optin.js.map +1 -1
  33. package/out-tsc/src/flow/actions/say_msg.js +2 -2
  34. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  35. package/out-tsc/src/flow/actions/send_broadcast.js +76 -23
  36. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  37. package/out-tsc/src/flow/actions/send_email.js +4 -5
  38. package/out-tsc/src/flow/actions/send_email.js.map +1 -1
  39. package/out-tsc/src/flow/actions/send_msg.js +9 -19
  40. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  41. package/out-tsc/src/flow/actions/set_contact_channel.js +5 -9
  42. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  43. package/out-tsc/src/flow/actions/set_contact_field.js +19 -20
  44. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  45. package/out-tsc/src/flow/actions/set_contact_language.js +2 -2
  46. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  47. package/out-tsc/src/flow/actions/set_contact_name.js +2 -12
  48. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  49. package/out-tsc/src/flow/actions/set_contact_status.js +2 -2
  50. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  51. package/out-tsc/src/flow/actions/set_run_result.js +3 -3
  52. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  53. package/out-tsc/src/flow/actions/start_session.js +180 -6
  54. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  55. package/out-tsc/src/flow/config.js +11 -15
  56. package/out-tsc/src/flow/config.js.map +1 -1
  57. package/out-tsc/src/flow/currencies.js +45 -0
  58. package/out-tsc/src/flow/currencies.js.map +1 -0
  59. package/out-tsc/src/flow/nodes/shared-rules.js +257 -0
  60. package/out-tsc/src/flow/nodes/shared-rules.js.map +1 -0
  61. package/out-tsc/src/flow/nodes/shared.js +17 -0
  62. package/out-tsc/src/flow/nodes/shared.js.map +1 -0
  63. package/out-tsc/src/flow/nodes/split_by_airtime.js +205 -5
  64. package/out-tsc/src/flow/nodes/split_by_airtime.js.map +1 -1
  65. package/out-tsc/src/flow/nodes/split_by_contact_field.js +147 -3
  66. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -1
  67. package/out-tsc/src/flow/nodes/split_by_expression.js +68 -2
  68. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -1
  69. package/out-tsc/src/flow/nodes/split_by_groups.js +12 -9
  70. package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -1
  71. package/out-tsc/src/flow/nodes/split_by_intent.js +7 -0
  72. package/out-tsc/src/flow/nodes/split_by_intent.js.map +1 -0
  73. package/out-tsc/src/flow/nodes/split_by_llm.js +3 -2
  74. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
  75. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +2 -2
  76. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  77. package/out-tsc/src/flow/nodes/split_by_random.js +3 -3
  78. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
  79. package/out-tsc/src/flow/nodes/split_by_resthook.js +108 -0
  80. package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -0
  81. package/out-tsc/src/flow/nodes/split_by_run_result.js +206 -3
  82. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
  83. package/out-tsc/src/flow/nodes/split_by_scheme.js +153 -2
  84. package/out-tsc/src/flow/nodes/split_by_scheme.js.map +1 -1
  85. package/out-tsc/src/flow/nodes/split_by_subflow.js +6 -4
  86. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  87. package/out-tsc/src/flow/nodes/split_by_ticket.js +3 -2
  88. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -1
  89. package/out-tsc/src/flow/nodes/split_by_webhook.js +3 -2
  90. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
  91. package/out-tsc/src/flow/nodes/wait_for_audio.js +2 -2
  92. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -1
  93. package/out-tsc/src/flow/nodes/wait_for_digits.js +2 -2
  94. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
  95. package/out-tsc/src/flow/nodes/wait_for_image.js +2 -2
  96. package/out-tsc/src/flow/nodes/wait_for_image.js.map +1 -1
  97. package/out-tsc/src/flow/nodes/wait_for_location.js +2 -2
  98. package/out-tsc/src/flow/nodes/wait_for_location.js.map +1 -1
  99. package/out-tsc/src/flow/nodes/wait_for_menu.js +2 -2
  100. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
  101. package/out-tsc/src/flow/nodes/wait_for_response.js +32 -567
  102. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  103. package/out-tsc/src/flow/nodes/wait_for_video.js +2 -2
  104. package/out-tsc/src/flow/nodes/wait_for_video.js.map +1 -1
  105. package/out-tsc/src/flow/types.js +71 -12
  106. package/out-tsc/src/flow/types.js.map +1 -1
  107. package/out-tsc/src/flow/utils.js +101 -14
  108. package/out-tsc/src/flow/utils.js.map +1 -1
  109. package/out-tsc/src/form/FieldRenderer.js +2 -4
  110. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  111. package/out-tsc/src/interfaces.js +3 -0
  112. package/out-tsc/src/interfaces.js.map +1 -1
  113. package/out-tsc/src/list/SortableList.js +98 -33
  114. package/out-tsc/src/list/SortableList.js.map +1 -1
  115. package/out-tsc/src/live/ContactChat.js +15 -18
  116. package/out-tsc/src/live/ContactChat.js.map +1 -1
  117. package/out-tsc/src/store/AppState.js +53 -0
  118. package/out-tsc/src/store/AppState.js.map +1 -1
  119. package/out-tsc/src/utils.js +254 -13
  120. package/out-tsc/src/utils.js.map +1 -1
  121. package/out-tsc/temba-modules.js +4 -0
  122. package/out-tsc/temba-modules.js.map +1 -1
  123. package/out-tsc/test/ActionHelper.js +3 -3
  124. package/out-tsc/test/ActionHelper.js.map +1 -1
  125. package/out-tsc/test/NodeHelper.js +6 -3
  126. package/out-tsc/test/NodeHelper.js.map +1 -1
  127. package/out-tsc/test/actions/add_contact_urn.test.js +202 -0
  128. package/out-tsc/test/actions/add_contact_urn.test.js.map +1 -0
  129. package/out-tsc/test/actions/send_broadcast.test.js +148 -0
  130. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -0
  131. package/out-tsc/test/actions/send_email.test.js +17 -23
  132. package/out-tsc/test/actions/send_email.test.js.map +1 -1
  133. package/out-tsc/test/actions/send_msg.test.js +33 -15
  134. package/out-tsc/test/actions/send_msg.test.js.map +1 -1
  135. package/out-tsc/test/actions/start_session.test.js +116 -0
  136. package/out-tsc/test/actions/start_session.test.js.map +1 -0
  137. package/out-tsc/test/nodes/split_by_airtime.test.js +604 -0
  138. package/out-tsc/test/nodes/split_by_airtime.test.js.map +1 -0
  139. package/out-tsc/test/nodes/split_by_contact_field.test.js +387 -0
  140. package/out-tsc/test/nodes/split_by_contact_field.test.js.map +1 -0
  141. package/out-tsc/test/nodes/split_by_expression.test.js +614 -0
  142. package/out-tsc/test/nodes/split_by_expression.test.js.map +1 -0
  143. package/out-tsc/test/nodes/split_by_random.test.js +3 -3
  144. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -1
  145. package/out-tsc/test/nodes/split_by_resthook.test.js +337 -0
  146. package/out-tsc/test/nodes/split_by_resthook.test.js.map +1 -0
  147. package/out-tsc/test/nodes/split_by_run_result.test.js +920 -0
  148. package/out-tsc/test/nodes/split_by_run_result.test.js.map +1 -0
  149. package/out-tsc/test/nodes/split_by_scheme.test.js +399 -0
  150. package/out-tsc/test/nodes/split_by_scheme.test.js.map +1 -0
  151. package/out-tsc/test/nodes/split_by_subflow.test.js +333 -0
  152. package/out-tsc/test/nodes/split_by_subflow.test.js.map +1 -0
  153. package/out-tsc/test/nodes/wait_for_digits.test.js +2 -2
  154. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  155. package/out-tsc/test/nodes/wait_for_response.test.js +2 -1
  156. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -1
  157. package/out-tsc/test/temba-action-drag-between-nodes.test.js +252 -0
  158. package/out-tsc/test/temba-action-drag-between-nodes.test.js.map +1 -0
  159. package/out-tsc/test/temba-canvas-menu.test.js +122 -0
  160. package/out-tsc/test/temba-canvas-menu.test.js.map +1 -0
  161. package/out-tsc/test/temba-flow-editor-node.test.js +85 -2
  162. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  163. package/out-tsc/test/temba-flow-editor.test.js +7 -8
  164. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  165. package/out-tsc/test/temba-node-editor.test.js +3 -1
  166. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  167. package/out-tsc/test/temba-node-type-selector.test.js +115 -0
  168. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -0
  169. package/out-tsc/test/temba-omnibox.test.js +2 -1
  170. package/out-tsc/test/temba-omnibox.test.js.map +1 -1
  171. package/out-tsc/test/temba-sortable-list.test.js +51 -0
  172. package/out-tsc/test/temba-sortable-list.test.js.map +1 -1
  173. package/out-tsc/test/temba-utils-index.test.js +1 -27
  174. package/out-tsc/test/temba-utils-index.test.js.map +1 -1
  175. package/out-tsc/test/utils.test.js +2 -0
  176. package/out-tsc/test/utils.test.js.map +1 -1
  177. package/package.json +2 -1
  178. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  179. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  180. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  181. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  182. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  183. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  184. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  185. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  186. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  187. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  188. package/screenshots/truth/actions/add_contact_urn/editor/expression-facebook.png +0 -0
  189. package/screenshots/truth/actions/add_contact_urn/editor/expression-phone.png +0 -0
  190. package/screenshots/truth/actions/add_contact_urn/editor/facebook-id.png +0 -0
  191. package/screenshots/truth/actions/add_contact_urn/editor/instagram-handle.png +0 -0
  192. package/screenshots/truth/actions/add_contact_urn/editor/line-id.png +0 -0
  193. package/screenshots/truth/actions/add_contact_urn/editor/phone-number.png +0 -0
  194. package/screenshots/truth/actions/add_contact_urn/editor/telegram-id.png +0 -0
  195. package/screenshots/truth/actions/add_contact_urn/editor/viber-id.png +0 -0
  196. package/screenshots/truth/actions/add_contact_urn/editor/wechat-id.png +0 -0
  197. package/screenshots/truth/actions/add_contact_urn/editor/whatsapp.png +0 -0
  198. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  199. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  200. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  201. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  202. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  203. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  204. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  205. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  206. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  207. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  208. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  209. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  210. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  211. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  212. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  213. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  214. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  215. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  216. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  217. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  218. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  219. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  220. package/screenshots/truth/actions/send_broadcast/editor/contacts-only.png +0 -0
  221. package/screenshots/truth/actions/send_broadcast/editor/groups-and-contacts.png +0 -0
  222. package/screenshots/truth/actions/send_broadcast/editor/groups-only.png +0 -0
  223. package/screenshots/truth/actions/send_broadcast/editor/many-groups.png +0 -0
  224. package/screenshots/truth/actions/send_broadcast/editor/multiline-text.png +0 -0
  225. package/screenshots/truth/actions/send_broadcast/editor/with-attachments.png +0 -0
  226. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  227. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  228. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  229. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  230. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  231. package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
  232. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  233. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  234. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  235. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  236. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  237. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  238. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  239. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  240. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  241. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  242. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  243. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  244. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  245. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  246. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  247. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  248. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  249. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  250. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  251. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  252. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  253. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  254. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  255. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  256. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  257. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  258. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  259. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  260. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  261. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  262. package/screenshots/truth/actions/start_session/editor/contact-query.png +0 -0
  263. package/screenshots/truth/actions/start_session/editor/contacts-only.png +0 -0
  264. package/screenshots/truth/actions/start_session/editor/create-contact.png +0 -0
  265. package/screenshots/truth/actions/start_session/editor/groups-and-contacts.png +0 -0
  266. package/screenshots/truth/actions/start_session/editor/groups-only.png +0 -0
  267. package/screenshots/truth/actions/start_session/editor/many-recipients.png +0 -0
  268. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  269. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  270. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  271. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  272. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  273. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  274. package/screenshots/truth/canvas-menu/open.png +0 -0
  275. package/screenshots/truth/editor/router.png +0 -0
  276. package/screenshots/truth/editor/wait.png +0 -0
  277. package/screenshots/truth/list/fields-dragging.png +0 -0
  278. package/screenshots/truth/list/sortable-dragging.png +0 -0
  279. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  280. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  281. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  282. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  283. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  284. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  285. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  286. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  287. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  288. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  289. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  290. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  291. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  292. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  293. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  294. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  295. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  296. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  297. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  298. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  299. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  300. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  301. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  302. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  303. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  304. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  305. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  306. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  307. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  308. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  309. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  310. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  311. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  312. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  313. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  314. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  315. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  316. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  317. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  318. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  319. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  320. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  321. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  322. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  323. package/src/Icons.ts +4 -1
  324. package/src/events.ts +2 -6
  325. package/src/flow/CanvasMenu.ts +217 -0
  326. package/src/flow/CanvasNode.ts +408 -10
  327. package/src/flow/Editor.ts +683 -44
  328. package/src/flow/NodeEditor.ts +304 -125
  329. package/src/flow/NodeTypeSelector.ts +592 -0
  330. package/src/flow/actions/add_contact_groups.ts +4 -4
  331. package/src/flow/actions/add_contact_urn.ts +76 -4
  332. package/src/flow/actions/add_input_labels.ts +4 -4
  333. package/src/flow/actions/play_audio.ts +2 -2
  334. package/src/flow/actions/remove_contact_groups.ts +14 -6
  335. package/src/flow/actions/request_optin.ts +2 -2
  336. package/src/flow/actions/say_msg.ts +2 -2
  337. package/src/flow/actions/send_broadcast.ts +85 -23
  338. package/src/flow/actions/send_email.ts +10 -6
  339. package/src/flow/actions/send_msg.ts +22 -32
  340. package/src/flow/actions/set_contact_channel.ts +5 -11
  341. package/src/flow/actions/set_contact_field.ts +20 -25
  342. package/src/flow/actions/set_contact_language.ts +9 -4
  343. package/src/flow/actions/set_contact_name.ts +3 -15
  344. package/src/flow/actions/set_contact_status.ts +3 -3
  345. package/src/flow/actions/set_run_result.ts +4 -4
  346. package/src/flow/actions/start_session.ts +208 -6
  347. package/src/flow/config.ts +13 -15
  348. package/src/flow/currencies.ts +51 -0
  349. package/src/flow/nodes/shared-rules.ts +301 -0
  350. package/src/flow/nodes/shared.ts +18 -0
  351. package/src/flow/nodes/split_by_airtime.ts +238 -5
  352. package/src/flow/nodes/split_by_contact_field.ts +185 -3
  353. package/src/flow/nodes/split_by_expression.ts +94 -2
  354. package/src/flow/nodes/split_by_groups.ts +15 -10
  355. package/src/flow/nodes/split_by_intent.ts +7 -0
  356. package/src/flow/nodes/split_by_llm.ts +4 -3
  357. package/src/flow/nodes/split_by_llm_categorize.ts +4 -4
  358. package/src/flow/nodes/split_by_random.ts +5 -5
  359. package/src/flow/nodes/split_by_resthook.ts +130 -0
  360. package/src/flow/nodes/split_by_run_result.ts +249 -3
  361. package/src/flow/nodes/split_by_scheme.ts +192 -2
  362. package/src/flow/nodes/split_by_subflow.ts +6 -4
  363. package/src/flow/nodes/split_by_ticket.ts +4 -3
  364. package/src/flow/nodes/split_by_webhook.ts +6 -5
  365. package/src/flow/nodes/wait_for_audio.ts +2 -2
  366. package/src/flow/nodes/wait_for_digits.ts +2 -2
  367. package/src/flow/nodes/wait_for_image.ts +2 -2
  368. package/src/flow/nodes/wait_for_location.ts +2 -2
  369. package/src/flow/nodes/wait_for_menu.ts +2 -2
  370. package/src/flow/nodes/wait_for_response.ts +48 -679
  371. package/src/flow/nodes/wait_for_video.ts +2 -2
  372. package/src/flow/types.ts +109 -23
  373. package/src/flow/utils.ts +108 -14
  374. package/src/form/FieldRenderer.ts +2 -4
  375. package/src/interfaces.ts +3 -0
  376. package/src/list/SortableList.ts +109 -34
  377. package/src/live/ContactChat.ts +15 -18
  378. package/src/store/AppState.ts +69 -0
  379. package/src/store/flow-definition.d.ts +2 -5
  380. package/src/utils.ts +332 -12
  381. package/static/api/channels.json +46 -0
  382. package/static/api/resthooks.json +31 -0
  383. package/static/svg/index.svg +1 -1
  384. package/static/svg/work/traced/lightning-02.svg +1 -0
  385. package/static/svg/work/used/lightning-02.svg +3 -0
  386. package/temba-modules.ts +4 -0
  387. package/test/ActionHelper.ts +3 -3
  388. package/test/NodeHelper.ts +6 -3
  389. package/test/actions/add_contact_urn.test.ts +287 -0
  390. package/test/actions/send_broadcast.test.ts +190 -0
  391. package/test/actions/send_email.test.ts +17 -23
  392. package/test/actions/send_msg.test.ts +39 -15
  393. package/test/actions/start_session.test.ts +151 -0
  394. package/test/nodes/split_by_airtime.test.ts +673 -0
  395. package/test/nodes/split_by_contact_field.test.ts +451 -0
  396. package/test/nodes/split_by_expression.test.ts +751 -0
  397. package/test/nodes/split_by_random.test.ts +3 -3
  398. package/test/nodes/split_by_resthook.test.ts +398 -0
  399. package/test/nodes/split_by_run_result.test.ts +1109 -0
  400. package/test/nodes/split_by_scheme.test.ts +486 -0
  401. package/test/nodes/split_by_subflow.test.ts +381 -0
  402. package/test/nodes/wait_for_digits.test.ts +2 -2
  403. package/test/nodes/wait_for_response.test.ts +2 -1
  404. package/test/temba-action-drag-between-nodes.test.ts +301 -0
  405. package/test/temba-canvas-menu.test.ts +156 -0
  406. package/test/temba-flow-editor-node.test.ts +102 -2
  407. package/test/temba-flow-editor.test.ts +7 -8
  408. package/test/temba-node-editor.test.ts +3 -1
  409. package/test/temba-node-type-selector.test.ts +152 -0
  410. package/test/temba-omnibox.test.ts +2 -1
  411. package/test/temba-sortable-list.test.ts +69 -0
  412. package/test/temba-utils-index.test.ts +0 -35
  413. package/test/utils.test.ts +2 -0
  414. package/test-assets/contacts/history.json +14 -20
  415. package/web-dev-server.config.mjs +3 -1
  416. package/out-tsc/src/flow/actions/call_classifier.js +0 -11
  417. package/out-tsc/src/flow/actions/call_classifier.js.map +0 -1
  418. package/out-tsc/src/flow/actions/call_resthook.js +0 -11
  419. package/out-tsc/src/flow/actions/call_resthook.js.map +0 -1
  420. package/out-tsc/src/flow/actions/split_by_expression_example.js +0 -77
  421. package/out-tsc/src/flow/actions/split_by_expression_example.js.map +0 -1
  422. package/out-tsc/src/flow/actions/transfer_airtime.js +0 -11
  423. package/out-tsc/src/flow/actions/transfer_airtime.js.map +0 -1
  424. package/src/flow/actions/call_classifier.ts +0 -12
  425. package/src/flow/actions/call_resthook.ts +0 -12
  426. package/src/flow/actions/split_by_expression_example.ts +0 -88
  427. package/src/flow/actions/transfer_airtime.ts +0 -12
@@ -10,7 +10,14 @@ import {
10
10
  FieldConfig,
11
11
  ActionConfig
12
12
  } from './config';
13
- import { LayoutItem, RowLayoutConfig, GroupLayoutConfig } from './types';
13
+ import {
14
+ LayoutItem,
15
+ RowLayoutConfig,
16
+ GroupLayoutConfig,
17
+ FormData,
18
+ ACTION_GROUP_METADATA,
19
+ SPLIT_GROUP_METADATA
20
+ } from './types';
14
21
  import { CustomEventType } from '../interfaces';
15
22
  import { generateUUID } from '../utils';
16
23
  import { FieldRenderer } from '../form/FieldRenderer';
@@ -25,7 +32,6 @@ export class NodeEditor extends RapidElement {
25
32
  flex-direction: column;
26
33
  gap: 15px;
27
34
  min-width: 400px;
28
- padding-bottom: 40px;
29
35
 
30
36
  --color-bubble-bg: rgba(var(--primary-rgb), 0.7);
31
37
  --color-bubble-border: rgba(0, 0, 0, 0.2);
@@ -88,6 +94,28 @@ export class NodeEditor extends RapidElement {
88
94
  align-items: center;
89
95
  }
90
96
 
97
+ .form-row-wrapper {
98
+ display: flex;
99
+ flex-direction: column;
100
+ }
101
+
102
+ .form-row-label {
103
+ margin-bottom: 5px;
104
+ margin-left: 4px;
105
+ display: block;
106
+ font-weight: 400;
107
+ font-size: var(--label-size);
108
+ letter-spacing: 0.05em;
109
+ line-height: normal;
110
+ color: var(--color-label, #777);
111
+ }
112
+
113
+ .form-row-help {
114
+ font-size: 12px;
115
+ color: #666;
116
+ margin-top: 6px;
117
+ }
118
+
91
119
  .form-group {
92
120
  border: 1px solid #e0e0e0;
93
121
  border-radius: 6px;
@@ -276,6 +304,21 @@ export class NodeEditor extends RapidElement {
276
304
  .gutter-fields temba-select {
277
305
  min-width: 120px;
278
306
  }
307
+
308
+ .optional-field-link {
309
+ margin: 10px 0;
310
+ }
311
+
312
+ .optional-field-link a {
313
+ color: var(--color-link-primary, #0066cc);
314
+ text-decoration: none;
315
+ font-size: 13px;
316
+ cursor: pointer;
317
+ }
318
+
319
+ .optional-field-link a:hover {
320
+ text-decoration: underline;
321
+ }
279
322
  `;
280
323
  }
281
324
 
@@ -292,10 +335,10 @@ export class NodeEditor extends RapidElement {
292
335
  isOpen: boolean = false;
293
336
 
294
337
  @state()
295
- private formData: any = {};
338
+ private formData: FormData = {};
296
339
 
297
340
  @state()
298
- private originalFormData: any = {};
341
+ private originalFormData: FormData = {};
299
342
 
300
343
  @state()
301
344
  private errors: { [key: string]: string } = {};
@@ -306,6 +349,9 @@ export class NodeEditor extends RapidElement {
306
349
  @state()
307
350
  private groupHoverState: { [key: string]: boolean } = {};
308
351
 
352
+ @state()
353
+ private revealedOptionalFields: Set<string> = new Set();
354
+
309
355
  connectedCallback(): void {
310
356
  super.connectedCallback();
311
357
  this.initializeFormData();
@@ -361,7 +407,7 @@ export class NodeEditor extends RapidElement {
361
407
  // Node editing mode - use node config
362
408
  const nodeConfig = this.getNodeConfig();
363
409
  if (nodeConfig?.toFormData) {
364
- this.formData = nodeConfig.toFormData(this.node);
410
+ this.formData = nodeConfig.toFormData(this.node, this.nodeUI);
365
411
  } else {
366
412
  this.formData = { ...this.node };
367
413
  }
@@ -488,7 +534,10 @@ export class NodeEditor extends RapidElement {
488
534
 
489
535
  private getHeaderColor(): string {
490
536
  const config = this.getConfig();
491
- return config?.color || '#666666';
537
+ return config?.group
538
+ ? ACTION_GROUP_METADATA[config.group]?.color ||
539
+ SPLIT_GROUP_METADATA[config.group]?.color
540
+ : '#aaaaaa';
492
541
  }
493
542
 
494
543
  private handleDialogButtonClick(event: CustomEvent): void {
@@ -522,8 +571,16 @@ export class NodeEditor extends RapidElement {
522
571
  if (this.node && this.node.router) {
523
572
  // Node editing mode with router - use formDataToNode
524
573
  const updatedNode = this.formDataToNode(processedFormData);
574
+
575
+ // Generate UI config if the node config provides a toUIConfig function
576
+ const nodeConfig = this.getNodeConfig();
577
+ const uiConfig = nodeConfig?.toUIConfig
578
+ ? nodeConfig.toUIConfig(processedFormData)
579
+ : undefined;
580
+
525
581
  this.fireCustomEvent(CustomEventType.NodeSaved, {
526
- node: updatedNode
582
+ node: updatedNode,
583
+ uiConfig
527
584
  });
528
585
  } else if (this.action) {
529
586
  // Pure action editing mode (no router)
@@ -534,13 +591,21 @@ export class NodeEditor extends RapidElement {
534
591
  } else if (this.node) {
535
592
  // Node editing mode without router
536
593
  const updatedNode = this.formDataToNode(processedFormData);
594
+
595
+ // Generate UI config if the node config provides a toUIConfig function
596
+ const nodeConfig = this.getNodeConfig();
597
+ const uiConfig = nodeConfig?.toUIConfig
598
+ ? nodeConfig.toUIConfig(processedFormData)
599
+ : undefined;
600
+
537
601
  this.fireCustomEvent(CustomEventType.NodeSaved, {
538
- node: updatedNode
602
+ node: updatedNode,
603
+ uiConfig
539
604
  });
540
605
  }
541
606
  }
542
607
 
543
- private processFormDataForSave(): any {
608
+ private processFormDataForSave(): FormData {
544
609
  const processed = { ...this.formData };
545
610
 
546
611
  // Convert key-value arrays to Records
@@ -743,12 +808,21 @@ export class NodeEditor extends RapidElement {
743
808
  });
744
809
  }
745
810
 
746
- private formDataToNode(formData: any = this.formData): Node {
811
+ private formDataToNode(formData: FormData = this.formData): Node {
747
812
  if (!this.node) throw new Error('No node to update');
748
813
  let updatedNode: Node = { ...this.node };
749
814
 
815
+ // Check if node config has fromFormData - if so, it handles the entire transformation
816
+ const nodeConfig = this.getNodeConfig();
817
+ const nodeHasFromFormData = nodeConfig?.fromFormData !== undefined;
818
+
750
819
  // Handle actions using action config transformations if available
751
- if (this.node.actions && this.node.actions.length > 0) {
820
+ // Skip this if the node has its own fromFormData (which handles actions itself)
821
+ if (
822
+ !nodeHasFromFormData &&
823
+ this.node.actions &&
824
+ this.node.actions.length > 0
825
+ ) {
752
826
  updatedNode.actions = this.node.actions.map((action) => {
753
827
  // If we're editing a specific action, only transform that one
754
828
  if (this.action && action.uuid === this.action.uuid) {
@@ -768,66 +842,105 @@ export class NodeEditor extends RapidElement {
768
842
  }
769
843
 
770
844
  // Handle router configuration using node config
771
- if (this.node.router) {
772
- const nodeConfig = this.getNodeConfig();
773
-
774
- if (nodeConfig?.fromFormData) {
775
- // Use node-specific form data transformation
776
- updatedNode = nodeConfig.fromFormData(formData, updatedNode);
777
- } else {
778
- // Default router handling
779
- updatedNode.router = { ...this.node.router };
845
+ if (nodeHasFromFormData) {
846
+ // Use node-specific form data transformation
847
+ // When a node has fromFormData, it's responsible for creating the entire
848
+ // node structure including actions and router (regardless of whether router exists yet)
849
+ updatedNode = nodeConfig.fromFormData!(formData, updatedNode);
850
+ } else if (this.node.router) {
851
+ // Default router handling when no nodeConfig.fromFormData
852
+ updatedNode.router = { ...this.node.router };
853
+
854
+ // Apply form data to router fields if they exist
855
+ if (formData.result_name !== undefined) {
856
+ updatedNode.router.result_name = formData.result_name;
857
+ }
858
+
859
+ // Handle preconfigured rules from node config
860
+ if (nodeConfig?.router?.rules) {
861
+ // Build a complete new set of categories and exits based on node config
862
+ const existingCategories = updatedNode.router.categories || [];
863
+ const existingExits = updatedNode.exits || [];
864
+
865
+ const newCategories: any[] = [];
866
+ const newExits: any[] = [];
867
+
868
+ // Group rules by category name to handle multiple rules pointing to the same category
869
+ const categoryNameToRules = new Map<
870
+ string,
871
+ typeof nodeConfig.router.rules
872
+ >();
873
+ nodeConfig.router.rules.forEach((rule) => {
874
+ if (!categoryNameToRules.has(rule.categoryName)) {
875
+ categoryNameToRules.set(rule.categoryName, []);
876
+ }
877
+ categoryNameToRules.get(rule.categoryName)!.push(rule);
878
+ });
780
879
 
781
- // Apply form data to router fields if they exist
782
- if (formData.result_name !== undefined) {
783
- updatedNode.router.result_name = formData.result_name;
784
- }
880
+ // Create categories for all unique category names
881
+ categoryNameToRules.forEach((rules, categoryName) => {
882
+ // Check if category already exists to preserve its UUID and exit_uuid
883
+ const existingCategory = existingCategories.find(
884
+ (cat) => cat.name === categoryName
885
+ );
785
886
 
786
- // Handle preconfigured rules from node config
787
- if (nodeConfig?.router?.rules) {
788
- // Build a complete new set of categories and exits based on node config
789
- const existingCategories = updatedNode.router.categories || [];
790
- const existingExits = updatedNode.exits || [];
791
-
792
- const newCategories: any[] = [];
793
- const newExits: any[] = [];
794
-
795
- // Group rules by category name to handle multiple rules pointing to the same category
796
- const categoryNameToRules = new Map<
797
- string,
798
- typeof nodeConfig.router.rules
799
- >();
800
- nodeConfig.router.rules.forEach((rule) => {
801
- if (!categoryNameToRules.has(rule.categoryName)) {
802
- categoryNameToRules.set(rule.categoryName, []);
887
+ if (existingCategory) {
888
+ // Preserve existing category and its associated exit
889
+ newCategories.push(existingCategory);
890
+ const associatedExit = existingExits.find(
891
+ (exit) => exit.uuid === existingCategory.exit_uuid
892
+ );
893
+ if (associatedExit) {
894
+ newExits.push(associatedExit);
803
895
  }
804
- categoryNameToRules.get(rule.categoryName)!.push(rule);
805
- });
896
+ } else {
897
+ // Create new category and exit
898
+ const categoryUuid = generateUUID();
899
+ const exitUuid = generateUUID();
900
+
901
+ newCategories.push({
902
+ uuid: categoryUuid,
903
+ name: categoryName,
904
+ exit_uuid: exitUuid
905
+ });
906
+
907
+ newExits.push({
908
+ uuid: exitUuid,
909
+ destination_uuid: null
910
+ });
911
+ }
912
+ });
913
+
914
+ // Add default category if specified
915
+ if (nodeConfig.router.defaultCategory) {
916
+ // Check if default category already exists in our new list
917
+ const existingDefault = newCategories.find(
918
+ (cat) => cat.name === nodeConfig.router.defaultCategory
919
+ );
806
920
 
807
- // Create categories for all unique category names
808
- categoryNameToRules.forEach((rules, categoryName) => {
809
- // Check if category already exists to preserve its UUID and exit_uuid
810
- const existingCategory = existingCategories.find(
811
- (cat) => cat.name === categoryName
921
+ if (!existingDefault) {
922
+ // Check if it exists in the original categories
923
+ const originalDefault = existingCategories.find(
924
+ (cat) => cat.name === nodeConfig.router.defaultCategory
812
925
  );
813
926
 
814
- if (existingCategory) {
815
- // Preserve existing category and its associated exit
816
- newCategories.push(existingCategory);
927
+ if (originalDefault) {
928
+ // Preserve existing default category and its exit
929
+ newCategories.push(originalDefault);
817
930
  const associatedExit = existingExits.find(
818
- (exit) => exit.uuid === existingCategory.exit_uuid
931
+ (exit) => exit.uuid === originalDefault.exit_uuid
819
932
  );
820
933
  if (associatedExit) {
821
934
  newExits.push(associatedExit);
822
935
  }
823
936
  } else {
824
- // Create new category and exit
937
+ // Create new default category and exit
825
938
  const categoryUuid = generateUUID();
826
939
  const exitUuid = generateUUID();
827
940
 
828
941
  newCategories.push({
829
942
  uuid: categoryUuid,
830
- name: categoryName,
943
+ name: nodeConfig.router.defaultCategory,
831
944
  exit_uuid: exitUuid
832
945
  });
833
946
 
@@ -836,53 +949,12 @@ export class NodeEditor extends RapidElement {
836
949
  destination_uuid: null
837
950
  });
838
951
  }
839
- });
840
-
841
- // Add default category if specified
842
- if (nodeConfig.router.defaultCategory) {
843
- // Check if default category already exists in our new list
844
- const existingDefault = newCategories.find(
845
- (cat) => cat.name === nodeConfig.router.defaultCategory
846
- );
847
-
848
- if (!existingDefault) {
849
- // Check if it exists in the original categories
850
- const originalDefault = existingCategories.find(
851
- (cat) => cat.name === nodeConfig.router.defaultCategory
852
- );
853
-
854
- if (originalDefault) {
855
- // Preserve existing default category and its exit
856
- newCategories.push(originalDefault);
857
- const associatedExit = existingExits.find(
858
- (exit) => exit.uuid === originalDefault.exit_uuid
859
- );
860
- if (associatedExit) {
861
- newExits.push(associatedExit);
862
- }
863
- } else {
864
- // Create new default category and exit
865
- const categoryUuid = generateUUID();
866
- const exitUuid = generateUUID();
867
-
868
- newCategories.push({
869
- uuid: categoryUuid,
870
- name: nodeConfig.router.defaultCategory,
871
- exit_uuid: exitUuid
872
- });
873
-
874
- newExits.push({
875
- uuid: exitUuid,
876
- destination_uuid: null
877
- });
878
- }
879
- }
880
952
  }
881
-
882
- // Replace the entire categories and exits lists with our complete new sets
883
- updatedNode.router.categories = newCategories;
884
- updatedNode.exits = newExits;
885
953
  }
954
+
955
+ // Replace the entire categories and exits lists with our complete new sets
956
+ updatedNode.router.categories = newCategories;
957
+ updatedNode.exits = newExits;
886
958
  }
887
959
  } else {
888
960
  // If no router, just apply form data to node properties
@@ -901,7 +973,7 @@ export class NodeEditor extends RapidElement {
901
973
  return updatedNode;
902
974
  }
903
975
 
904
- private formDataToAction(formData: any = this.formData): Action {
976
+ private formDataToAction(formData: FormData = this.formData): Action {
905
977
  if (!this.action) throw new Error('No action to update');
906
978
 
907
979
  // Use action config transformation if available
@@ -1009,23 +1081,31 @@ export class NodeEditor extends RapidElement {
1009
1081
  });
1010
1082
  }
1011
1083
 
1012
- private renderNewField(
1013
- fieldName: string,
1014
- config: FieldConfig,
1015
- value: any
1016
- ): TemplateResult {
1017
- // Check visibility condition
1084
+ /**
1085
+ * Helper method to check if a field is visible based on its conditions
1086
+ */
1087
+ private isFieldVisible(fieldName: string, config: FieldConfig): boolean {
1018
1088
  if (config.conditions?.visible) {
1019
1089
  try {
1020
- const isVisible = config.conditions.visible(this.formData);
1021
- if (!isVisible) {
1022
- return html``;
1023
- }
1090
+ return config.conditions.visible(this.formData);
1024
1091
  } catch (error) {
1025
1092
  console.error(`Error checking visibility for ${fieldName}:`, error);
1026
1093
  // If there's an error, show the field by default
1094
+ return true;
1027
1095
  }
1028
1096
  }
1097
+ return true;
1098
+ }
1099
+
1100
+ private renderNewField(
1101
+ fieldName: string,
1102
+ config: FieldConfig,
1103
+ value: any
1104
+ ): TemplateResult {
1105
+ // Check visibility condition
1106
+ if (!this.isFieldVisible(fieldName, config)) {
1107
+ return html``;
1108
+ }
1029
1109
 
1030
1110
  const errors = this.errors[fieldName] ? [this.errors[fieldName]] : [];
1031
1111
 
@@ -1049,6 +1129,43 @@ export class NodeEditor extends RapidElement {
1049
1129
  return fieldContent;
1050
1130
  }
1051
1131
 
1132
+ private renderOptionalField(
1133
+ fieldName: string,
1134
+ config: FieldConfig,
1135
+ value: any
1136
+ ): TemplateResult {
1137
+ // If the field has a value or has been revealed, show it
1138
+ const hasValue = value && value.toString().trim() !== '';
1139
+ const isRevealed = this.revealedOptionalFields.has(fieldName);
1140
+
1141
+ if (hasValue || isRevealed) {
1142
+ // Render the field normally
1143
+ return this.renderNewField(fieldName, config, value);
1144
+ }
1145
+
1146
+ // Show the "Save as..." link
1147
+ return html`
1148
+ <div class="optional-field-link">
1149
+ <a
1150
+ href="#"
1151
+ @click="${(e: Event) => {
1152
+ e.preventDefault();
1153
+ this.revealOptionalField(fieldName);
1154
+ }}"
1155
+ >
1156
+ ${config.optionalLink}
1157
+ </a>
1158
+ </div>
1159
+ `;
1160
+ }
1161
+
1162
+ private revealOptionalField(fieldName: string): void {
1163
+ this.revealedOptionalFields = new Set([
1164
+ ...this.revealedOptionalFields,
1165
+ fieldName
1166
+ ]);
1167
+ }
1168
+
1052
1169
  private renderFieldContent(
1053
1170
  fieldName: string,
1054
1171
  config: FieldConfig,
@@ -1160,9 +1277,20 @@ export class NodeEditor extends RapidElement {
1160
1277
  case 'field':
1161
1278
  if (config.form![item.field] && !renderedFields.has(item.field)) {
1162
1279
  renderedFields.add(item.field);
1280
+ const fieldConfig = config.form![item.field] as FieldConfig;
1281
+
1282
+ // Handle optional link fields
1283
+ if (fieldConfig.optionalLink) {
1284
+ return this.renderOptionalField(
1285
+ item.field,
1286
+ fieldConfig,
1287
+ this.formData[item.field]
1288
+ );
1289
+ }
1290
+
1163
1291
  return this.renderNewField(
1164
1292
  item.field,
1165
- config.form![item.field] as FieldConfig,
1293
+ fieldConfig,
1166
1294
  this.formData[item.field]
1167
1295
  );
1168
1296
  }
@@ -1184,7 +1312,7 @@ export class NodeEditor extends RapidElement {
1184
1312
  config: ActionConfig | NodeConfig,
1185
1313
  renderedFields: Set<string>
1186
1314
  ): TemplateResult {
1187
- const { items, gap = '1rem' } = rowConfig;
1315
+ const { items, gap = '1rem', label, helpText } = rowConfig;
1188
1316
 
1189
1317
  // Collect all fields from this row for width calculations
1190
1318
  const fieldsInRow = this.collectFieldsFromItems(items);
@@ -1192,26 +1320,77 @@ export class NodeEditor extends RapidElement {
1192
1320
  (fieldName) => config.form?.[fieldName]
1193
1321
  );
1194
1322
 
1195
- if (validFields.length === 0) {
1323
+ // Filter for visible fields only to handle conditional visibility
1324
+ const visibleFields = validFields.filter((fieldName) => {
1325
+ const fieldConfig = config.form![fieldName];
1326
+ return this.isFieldVisible(fieldName, fieldConfig);
1327
+ });
1328
+
1329
+ if (visibleFields.length === 0) {
1196
1330
  return html``;
1197
1331
  }
1198
1332
 
1199
- // Calculate grid template columns based on field maxWidth constraints
1200
- const columns = validFields.map((fieldName) => {
1333
+ // Build a map of field flex styles
1334
+ // Fields with maxWidth get flex: 0 0 {maxWidth} (fixed)
1335
+ // Fields without maxWidth get flex: 1 1 0 (grow to fill space)
1336
+ const fieldFlexStyles = new Map<string, string>();
1337
+ visibleFields.forEach((fieldName) => {
1201
1338
  const fieldConfig = config.form![fieldName];
1202
- return fieldConfig.maxWidth || '1fr';
1339
+ if (fieldConfig.maxWidth) {
1340
+ // Fixed width field: no grow, no shrink, basis = maxWidth
1341
+ fieldFlexStyles.set(fieldName, `flex: 0 0 ${fieldConfig.maxWidth};`);
1342
+ } else {
1343
+ // Flexible field: grow to fill remaining space
1344
+ fieldFlexStyles.set(fieldName, `flex: 1 1 0;`);
1345
+ }
1203
1346
  });
1204
1347
 
1348
+ const rowContent = html`
1349
+ <div class="form-row" style="display: flex; gap: ${gap};">
1350
+ ${items.map((item) => {
1351
+ // Get the field name from the item
1352
+ const fieldName =
1353
+ typeof item === 'string'
1354
+ ? item
1355
+ : item.type === 'field'
1356
+ ? item.field
1357
+ : null;
1358
+
1359
+ // Get flex style for this field if it's a visible field
1360
+ const flexStyle =
1361
+ fieldName && fieldFlexStyles.has(fieldName)
1362
+ ? fieldFlexStyles.get(fieldName)
1363
+ : '';
1364
+
1365
+ const itemContent = this.renderLayoutItem(
1366
+ item,
1367
+ config,
1368
+ renderedFields
1369
+ );
1370
+
1371
+ // Wrap in a div with flex style if we have a flex style
1372
+ return flexStyle
1373
+ ? html`<div style="${flexStyle}">${itemContent}</div>`
1374
+ : itemContent;
1375
+ })}
1376
+ </div>
1377
+ `;
1378
+
1379
+ // If no label or helpText, return just the row content
1380
+ if (!label && !helpText) {
1381
+ return rowContent;
1382
+ }
1383
+
1384
+ // Otherwise, wrap with label on top, content, then helpText below (matching field pattern)
1205
1385
  return html`
1206
- <div
1207
- class="form-row"
1208
- style="display: grid; grid-template-columns: ${columns.join(
1209
- ' '
1210
- )}; gap: ${gap};"
1211
- >
1212
- ${items.map((item) =>
1213
- this.renderLayoutItem(item, config, renderedFields)
1214
- )}
1386
+ <div class="form-row-wrapper">
1387
+ ${label ? html`<label class="form-row-label">${label}</label>` : ''}
1388
+ ${rowContent}
1389
+ ${helpText
1390
+ ? html`<div class="form-row-help">
1391
+ ${renderMarkdownInline(helpText)}
1392
+ </div>`
1393
+ : ''}
1215
1394
  </div>
1216
1395
  `;
1217
1396
  }