@nyaruka/temba-components 0.129.3 → 0.129.5

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 (304) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.github/workflows/build.yml +135 -3
  3. package/CHANGELOG.md +19 -0
  4. package/demo/data/flows/sample-flow.json +110 -87
  5. package/demo/field-config-demo.html +135 -0
  6. package/dist/temba-components.js +1257 -675
  7. package/dist/temba-components.js.map +1 -1
  8. package/docs/ActionEditor-Migration.md +118 -0
  9. package/out-tsc/src/events.js.map +1 -1
  10. package/out-tsc/src/flow/{EditorNode.js → CanvasNode.js} +345 -42
  11. package/out-tsc/src/flow/CanvasNode.js.map +1 -0
  12. package/out-tsc/src/flow/Editor.js +107 -3
  13. package/out-tsc/src/flow/Editor.js.map +1 -1
  14. package/out-tsc/src/flow/NodeEditor.js +1211 -0
  15. package/out-tsc/src/flow/NodeEditor.js.map +1 -0
  16. package/out-tsc/src/flow/Plumber.js +0 -6
  17. package/out-tsc/src/flow/Plumber.js.map +1 -1
  18. package/out-tsc/src/flow/actions/add_contact_groups.js +40 -0
  19. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -0
  20. package/out-tsc/src/flow/actions/add_contact_urn.js +16 -0
  21. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -0
  22. package/out-tsc/src/flow/actions/add_input_labels.js +11 -0
  23. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -0
  24. package/out-tsc/src/flow/actions/call_classifier.js +11 -0
  25. package/out-tsc/src/flow/actions/call_classifier.js.map +1 -0
  26. package/out-tsc/src/flow/actions/call_llm.js +11 -0
  27. package/out-tsc/src/flow/actions/call_llm.js.map +1 -0
  28. package/out-tsc/src/flow/actions/call_resthook.js +11 -0
  29. package/out-tsc/src/flow/actions/call_resthook.js.map +1 -0
  30. package/out-tsc/src/flow/actions/call_webhook.js +122 -0
  31. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -0
  32. package/out-tsc/src/flow/actions/enter_flow.js +14 -0
  33. package/out-tsc/src/flow/actions/enter_flow.js.map +1 -0
  34. package/out-tsc/src/flow/actions/open_ticket.js +11 -0
  35. package/out-tsc/src/flow/actions/open_ticket.js.map +1 -0
  36. package/out-tsc/src/flow/actions/play_audio.js +11 -0
  37. package/out-tsc/src/flow/actions/play_audio.js.map +1 -0
  38. package/out-tsc/src/flow/actions/remove_contact_groups.js +62 -0
  39. package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -0
  40. package/out-tsc/src/flow/actions/request_optin.js +11 -0
  41. package/out-tsc/src/flow/actions/request_optin.js.map +1 -0
  42. package/out-tsc/src/flow/actions/say_msg.js +11 -0
  43. package/out-tsc/src/flow/actions/say_msg.js.map +1 -0
  44. package/out-tsc/src/flow/actions/send_broadcast.js +33 -0
  45. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -0
  46. package/out-tsc/src/flow/actions/send_email.js +56 -0
  47. package/out-tsc/src/flow/actions/send_email.js.map +1 -0
  48. package/out-tsc/src/flow/actions/send_msg.js +55 -0
  49. package/out-tsc/src/flow/actions/send_msg.js.map +1 -0
  50. package/out-tsc/src/flow/actions/set_contact_channel.js +12 -0
  51. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -0
  52. package/out-tsc/src/flow/actions/set_contact_field.js +12 -0
  53. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -0
  54. package/out-tsc/src/flow/actions/set_contact_language.js +10 -0
  55. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -0
  56. package/out-tsc/src/flow/actions/set_contact_name.js +10 -0
  57. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -0
  58. package/out-tsc/src/flow/actions/set_contact_status.js +10 -0
  59. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -0
  60. package/out-tsc/src/flow/actions/set_run_result.js +10 -0
  61. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -0
  62. package/out-tsc/src/flow/actions/split_by_expression_example.js +77 -0
  63. package/out-tsc/src/flow/actions/split_by_expression_example.js.map +1 -0
  64. package/out-tsc/src/flow/actions/start_session.js +11 -0
  65. package/out-tsc/src/flow/actions/start_session.js.map +1 -0
  66. package/out-tsc/src/flow/actions/transfer_airtime.js +11 -0
  67. package/out-tsc/src/flow/actions/transfer_airtime.js.map +1 -0
  68. package/out-tsc/src/flow/config.js +88 -193
  69. package/out-tsc/src/flow/config.js.map +1 -1
  70. package/out-tsc/src/flow/nodes/execute_actions.js +4 -0
  71. package/out-tsc/src/flow/nodes/execute_actions.js.map +1 -0
  72. package/out-tsc/src/flow/nodes/split_by_airtime.js +9 -0
  73. package/out-tsc/src/flow/nodes/split_by_airtime.js.map +1 -0
  74. package/out-tsc/src/flow/nodes/split_by_contact_field.js +7 -0
  75. package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -0
  76. package/out-tsc/src/flow/nodes/split_by_expression.js +7 -0
  77. package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -0
  78. package/out-tsc/src/flow/nodes/split_by_groups.js +7 -0
  79. package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -0
  80. package/out-tsc/src/flow/nodes/split_by_random.js +10 -0
  81. package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -0
  82. package/out-tsc/src/flow/nodes/split_by_run_result.js +7 -0
  83. package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -0
  84. package/out-tsc/src/flow/nodes/split_by_scheme.js +7 -0
  85. package/out-tsc/src/flow/nodes/split_by_scheme.js.map +1 -0
  86. package/out-tsc/src/flow/nodes/split_by_subflow.js +9 -0
  87. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -0
  88. package/out-tsc/src/flow/nodes/split_by_webhook.js +18 -0
  89. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -0
  90. package/out-tsc/src/flow/nodes/wait_for_audio.js +7 -0
  91. package/out-tsc/src/flow/nodes/wait_for_audio.js.map +1 -0
  92. package/out-tsc/src/flow/nodes/wait_for_digits.js +7 -0
  93. package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -0
  94. package/out-tsc/src/flow/nodes/wait_for_image.js +7 -0
  95. package/out-tsc/src/flow/nodes/wait_for_image.js.map +1 -0
  96. package/out-tsc/src/flow/nodes/wait_for_location.js +7 -0
  97. package/out-tsc/src/flow/nodes/wait_for_location.js.map +1 -0
  98. package/out-tsc/src/flow/nodes/wait_for_menu.js +7 -0
  99. package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -0
  100. package/out-tsc/src/flow/nodes/wait_for_response.js +7 -0
  101. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -0
  102. package/out-tsc/src/flow/nodes/wait_for_video.js +7 -0
  103. package/out-tsc/src/flow/nodes/wait_for_video.js.map +1 -0
  104. package/out-tsc/src/flow/types.js +79 -0
  105. package/out-tsc/src/flow/types.js.map +1 -0
  106. package/out-tsc/src/flow/utils.js +65 -0
  107. package/out-tsc/src/flow/utils.js.map +1 -0
  108. package/out-tsc/src/form/ArrayEditor.js +199 -0
  109. package/out-tsc/src/form/ArrayEditor.js.map +1 -0
  110. package/out-tsc/src/form/BaseListEditor.js +128 -0
  111. package/out-tsc/src/form/BaseListEditor.js.map +1 -0
  112. package/out-tsc/src/form/Checkbox.js +17 -2
  113. package/out-tsc/src/form/Checkbox.js.map +1 -1
  114. package/out-tsc/src/form/Completion.js +6 -0
  115. package/out-tsc/src/form/Completion.js.map +1 -1
  116. package/out-tsc/src/form/FormField.js +110 -11
  117. package/out-tsc/src/form/FormField.js.map +1 -1
  118. package/out-tsc/src/form/KeyValueEditor.js +223 -0
  119. package/out-tsc/src/form/KeyValueEditor.js.map +1 -0
  120. package/out-tsc/src/form/select/Select.js +92 -32
  121. package/out-tsc/src/form/select/Select.js.map +1 -1
  122. package/out-tsc/src/interfaces.js +6 -0
  123. package/out-tsc/src/interfaces.js.map +1 -1
  124. package/out-tsc/src/live/ContactChat.js +2 -76
  125. package/out-tsc/src/live/ContactChat.js.map +1 -1
  126. package/out-tsc/temba-modules.js +9 -2
  127. package/out-tsc/temba-modules.js.map +1 -1
  128. package/out-tsc/test/ActionHelper.js +116 -0
  129. package/out-tsc/test/ActionHelper.js.map +1 -0
  130. package/out-tsc/test/actions/add_contact_groups.test.js +66 -0
  131. package/out-tsc/test/actions/add_contact_groups.test.js.map +1 -0
  132. package/out-tsc/test/actions/remove_contact_groups.test.js +226 -0
  133. package/out-tsc/test/actions/remove_contact_groups.test.js.map +1 -0
  134. package/out-tsc/test/actions/send_email.test.js +160 -0
  135. package/out-tsc/test/actions/send_email.test.js.map +1 -0
  136. package/out-tsc/test/actions/send_msg.test.js +95 -0
  137. package/out-tsc/test/actions/send_msg.test.js.map +1 -0
  138. package/out-tsc/test/temba-action-editing-integration.test.js +183 -0
  139. package/out-tsc/test/temba-action-editing-integration.test.js.map +1 -0
  140. package/out-tsc/test/temba-checkbox.test.js +1 -1
  141. package/out-tsc/test/temba-checkbox.test.js.map +1 -1
  142. package/out-tsc/test/temba-field-config.test.js +133 -0
  143. package/out-tsc/test/temba-field-config.test.js.map +1 -0
  144. package/out-tsc/test/temba-flow-editor-node.test.js +14 -14
  145. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  146. package/out-tsc/test/temba-node-editor.test.js +283 -0
  147. package/out-tsc/test/temba-node-editor.test.js.map +1 -0
  148. package/out-tsc/test/temba-select.test.js +158 -0
  149. package/out-tsc/test/temba-select.test.js.map +1 -1
  150. package/package.json +1 -1
  151. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  152. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  153. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  154. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  155. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  156. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  157. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  158. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  159. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  160. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  161. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  162. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  163. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  164. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  165. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  166. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  167. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  168. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  169. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  170. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  171. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  172. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  173. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  174. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  175. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  176. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  177. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  178. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  179. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  180. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  181. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  182. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  183. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  184. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  185. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  186. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  187. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  188. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  189. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  190. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  191. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  192. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  193. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  194. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  195. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  196. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  197. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  198. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  199. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  200. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  201. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  202. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  203. package/screenshots/truth/editor/router.png +0 -0
  204. package/screenshots/truth/editor/send_msg.png +0 -0
  205. package/screenshots/truth/editor/set_contact_language.png +0 -0
  206. package/screenshots/truth/editor/set_contact_name.png +0 -0
  207. package/screenshots/truth/editor/set_run_result.png +0 -0
  208. package/screenshots/truth/editor/wait.png +0 -0
  209. package/screenshots/truth/formfield/markdown-errors.png +0 -0
  210. package/screenshots/truth/formfield/plain-text-errors.png +0 -0
  211. package/screenshots/truth/formfield/widget-only-markdown-errors.png +0 -0
  212. package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
  213. package/src/events.ts +1 -40
  214. package/src/flow/{EditorNode.ts → CanvasNode.ts} +424 -48
  215. package/src/flow/Editor.ts +140 -4
  216. package/src/flow/NodeEditor.ts +1454 -0
  217. package/src/flow/Plumber.ts +0 -9
  218. package/src/flow/actions/add_contact_groups.ts +42 -0
  219. package/src/flow/actions/add_contact_urn.ts +17 -0
  220. package/src/flow/actions/add_input_labels.ts +12 -0
  221. package/src/flow/actions/call_classifier.ts +12 -0
  222. package/src/flow/actions/call_llm.ts +12 -0
  223. package/src/flow/actions/call_resthook.ts +12 -0
  224. package/src/flow/actions/call_webhook.ts +133 -0
  225. package/src/flow/actions/enter_flow.ts +15 -0
  226. package/src/flow/actions/open_ticket.ts +12 -0
  227. package/src/flow/actions/play_audio.ts +12 -0
  228. package/src/flow/actions/remove_contact_groups.ts +66 -0
  229. package/src/flow/actions/request_optin.ts +12 -0
  230. package/src/flow/actions/say_msg.ts +12 -0
  231. package/src/flow/actions/send_broadcast.ts +35 -0
  232. package/src/flow/actions/send_email.ts +60 -0
  233. package/src/flow/actions/send_msg.ts +58 -0
  234. package/src/flow/actions/set_contact_channel.ts +13 -0
  235. package/src/flow/actions/set_contact_field.ts +13 -0
  236. package/src/flow/actions/set_contact_language.ts +11 -0
  237. package/src/flow/actions/set_contact_name.ts +11 -0
  238. package/src/flow/actions/set_contact_status.ts +11 -0
  239. package/src/flow/actions/set_run_result.ts +11 -0
  240. package/src/flow/actions/split_by_expression_example.ts +88 -0
  241. package/src/flow/actions/start_session.ts +12 -0
  242. package/src/flow/actions/transfer_airtime.ts +12 -0
  243. package/src/flow/config.ts +93 -232
  244. package/src/flow/nodes/execute_actions.ts +5 -0
  245. package/src/flow/nodes/split_by_airtime.ts +9 -0
  246. package/src/flow/nodes/split_by_contact_field.ts +7 -0
  247. package/src/flow/nodes/split_by_expression.ts +7 -0
  248. package/src/flow/nodes/split_by_groups.ts +7 -0
  249. package/src/flow/nodes/split_by_random.ts +10 -0
  250. package/src/flow/nodes/split_by_run_result.ts +7 -0
  251. package/src/flow/nodes/split_by_scheme.ts +7 -0
  252. package/src/flow/nodes/split_by_subflow.ts +9 -0
  253. package/src/flow/nodes/split_by_webhook.ts +19 -0
  254. package/src/flow/nodes/wait_for_audio.ts +7 -0
  255. package/src/flow/nodes/wait_for_digits.ts +7 -0
  256. package/src/flow/nodes/wait_for_image.ts +7 -0
  257. package/src/flow/nodes/wait_for_location.ts +7 -0
  258. package/src/flow/nodes/wait_for_menu.ts +7 -0
  259. package/src/flow/nodes/wait_for_response.ts +7 -0
  260. package/src/flow/nodes/wait_for_video.ts +7 -0
  261. package/src/flow/types.ts +352 -0
  262. package/src/flow/utils.ts +76 -0
  263. package/src/form/ArrayEditor.ts +240 -0
  264. package/src/form/BaseListEditor.ts +177 -0
  265. package/src/form/Checkbox.ts +22 -3
  266. package/src/form/Completion.ts +6 -0
  267. package/src/form/FormField.ts +115 -11
  268. package/src/form/KeyValueEditor.ts +251 -0
  269. package/src/form/select/Select.ts +105 -32
  270. package/src/interfaces.ts +7 -2
  271. package/src/live/ContactChat.ts +3 -97
  272. package/src/store/flow-definition.d.ts +6 -1
  273. package/static/api/contacts.json +30 -0
  274. package/static/api/groups.json +4 -426
  275. package/static/api/locations.json +24 -0
  276. package/static/api/media.json +5 -0
  277. package/static/api/optins.json +16 -0
  278. package/static/api/orgs.json +13 -0
  279. package/static/api/topics.json +21 -0
  280. package/static/api/users.json +26 -0
  281. package/static/css/temba-components.css +3 -6
  282. package/temba-modules.ts +9 -2
  283. package/test/ActionHelper.ts +142 -0
  284. package/test/actions/add_contact_groups.test.ts +89 -0
  285. package/test/actions/remove_contact_groups.test.ts +265 -0
  286. package/test/actions/send_email.test.ts +214 -0
  287. package/test/actions/send_msg.test.ts +130 -0
  288. package/test/temba-action-editing-integration.test.ts +240 -0
  289. package/test/temba-checkbox.test.ts +1 -1
  290. package/test/temba-field-config.test.ts +152 -0
  291. package/test/temba-flow-editor-node.test.ts +18 -18
  292. package/test/temba-node-editor.test.ts +353 -0
  293. package/test/temba-select.test.ts +234 -0
  294. package/test-assets/contacts/history.json +11 -33
  295. package/web-dev-server.config.mjs +34 -0
  296. package/.github/workflows/coverage.yml +0 -80
  297. package/demo/sticky-note-demo.html +0 -155
  298. package/out-tsc/src/flow/EditorNode.js.map +0 -1
  299. package/out-tsc/src/flow/render.js +0 -358
  300. package/out-tsc/src/flow/render.js.map +0 -1
  301. package/out-tsc/test/temba-flow-render.test.js +0 -794
  302. package/out-tsc/test/temba-flow-render.test.js.map +0 -1
  303. package/src/flow/render.ts +0 -443
  304. package/test/temba-flow-render.test.ts +0 -1003
@@ -0,0 +1,1211 @@
1
+ import { __decorate } from "tslib";
2
+ import { html, css } from 'lit';
3
+ import { property, state } from 'lit/decorators.js';
4
+ import { RapidElement } from '../RapidElement';
5
+ import { NODE_CONFIG, ACTION_CONFIG } from './config';
6
+ import { CustomEventType } from '../interfaces';
7
+ import { generateUUID } from '../utils';
8
+ export class NodeEditor extends RapidElement {
9
+ constructor() {
10
+ super(...arguments);
11
+ this.isOpen = false;
12
+ this.formData = {};
13
+ this.originalFormData = {};
14
+ this.errors = {};
15
+ this.groupCollapseState = {};
16
+ }
17
+ static get styles() {
18
+ return css `
19
+ .node-editor-form {
20
+ padding: 20px;
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: 15px;
24
+ min-width: 400px;
25
+ padding-bottom: 40px;
26
+ }
27
+
28
+ .form-field {
29
+ display: flex;
30
+ flex-direction: column;
31
+ }
32
+
33
+ .form-field label {
34
+ font-weight: 500;
35
+ margin-bottom: 6px;
36
+ color: #333;
37
+ font-size: 14px;
38
+ }
39
+
40
+ .field-errors {
41
+ color: var(--color-error, tomato);
42
+ font-size: 12px;
43
+ margin-left: 5px;
44
+ margin-top: 15px;
45
+ }
46
+
47
+ .form-actions {
48
+ display: flex;
49
+ gap: 10px;
50
+ justify-content: flex-end;
51
+ margin-top: 20px;
52
+ }
53
+
54
+ .action-section {
55
+ border: 1px solid #e0e0e0;
56
+ border-radius: 4px;
57
+ padding: 15px;
58
+ margin: 10px 0;
59
+ }
60
+
61
+ .action-section h3 {
62
+ margin: 0 0 10px 0;
63
+ color: #333;
64
+ font-size: 14px;
65
+ font-weight: 600;
66
+ }
67
+
68
+ .router-section {
69
+ border: 1px solid #e0e0e0;
70
+ border-radius: 4px;
71
+ padding: 15px;
72
+ margin: 10px 0;
73
+ }
74
+
75
+ .router-section h3 {
76
+ margin: 0 0 10px 0;
77
+ color: #333;
78
+ font-size: 14px;
79
+ font-weight: 600;
80
+ }
81
+
82
+ .form-row {
83
+ display: grid;
84
+ gap: 1rem;
85
+ align-items: end;
86
+ }
87
+
88
+ .form-group {
89
+ border: 1px solid #e0e0e0;
90
+ border-radius: 6px;
91
+ overflow: hidden;
92
+ }
93
+
94
+ .form-group.has-errors {
95
+ border-color: var(--color-error, tomato);
96
+ }
97
+
98
+ .form-group-header {
99
+ background: #f8f9fa;
100
+ padding: 12px 15px;
101
+ border-bottom: 1px solid #e0e0e0;
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: space-between;
105
+ cursor: pointer;
106
+ user-select: none;
107
+ }
108
+
109
+ .form-group-header.collapsible:hover {
110
+ background: #f1f3f4;
111
+ }
112
+
113
+ .form-group-info {
114
+ flex: 1;
115
+ }
116
+
117
+ .form-group-title {
118
+ font-weight: 500;
119
+ color: #333;
120
+ font-size: 14px;
121
+ display: flex;
122
+ }
123
+
124
+ .form-group-help {
125
+ font-size: 12px;
126
+ color: #666;
127
+ margin-top: 2px;
128
+ }
129
+
130
+ .form-group-toggle {
131
+ color: #666;
132
+ transition: transform 0.3s ease;
133
+ display: flex;
134
+ align-items: center;
135
+ }
136
+
137
+ .form-group-toggle.collapsed {
138
+ transform: rotate(-90deg);
139
+ }
140
+
141
+ .form-group-content {
142
+ padding: 15px;
143
+ display: flex;
144
+ flex-direction: column;
145
+ gap: 15px;
146
+ overflow: hidden;
147
+ transition: all 0.3s ease;
148
+ max-height: 1000px; /* Large enough to accommodate most content */
149
+ opacity: 1;
150
+ }
151
+
152
+ .form-group-content.collapsed {
153
+ max-height: 0;
154
+ padding-top: 0;
155
+ padding-bottom: 0;
156
+ opacity: 0;
157
+ }
158
+
159
+ .group-toggle-icon {
160
+ color: #666;
161
+ transition: transform 0.3s ease;
162
+ cursor: pointer;
163
+ transform: rotate(0deg);
164
+ }
165
+
166
+ .group-toggle-icon.expanded {
167
+ transform: rotate(90deg);
168
+ }
169
+
170
+ .group-toggle-icon.collapsed {
171
+ transform: rotate(0deg);
172
+ }
173
+
174
+ .group-toggle-icon:hover {
175
+ color: #333;
176
+ }
177
+
178
+ .group-error-icon {
179
+ color: var(--color-error, tomato);
180
+ margin-right: 8px;
181
+ }
182
+ `;
183
+ }
184
+ connectedCallback() {
185
+ super.connectedCallback();
186
+ this.initializeFormData();
187
+ }
188
+ updated(changedProperties) {
189
+ super.updated(changedProperties);
190
+ if (changedProperties.has('node') || changedProperties.has('action')) {
191
+ if (this.node || this.action) {
192
+ this.openDialog();
193
+ }
194
+ else {
195
+ this.isOpen = false;
196
+ }
197
+ }
198
+ }
199
+ openDialog() {
200
+ this.initializeFormData();
201
+ this.errors = {};
202
+ this.isOpen = true;
203
+ }
204
+ closeDialog() {
205
+ this.isOpen = false;
206
+ this.formData = {};
207
+ this.errors = {};
208
+ this.groupCollapseState = {};
209
+ }
210
+ initializeFormData() {
211
+ if (this.action) {
212
+ // Action editing mode - use action config
213
+ const actionConfig = ACTION_CONFIG[this.action.type];
214
+ if (actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.toFormData) {
215
+ this.formData = actionConfig.toFormData(this.action);
216
+ }
217
+ else {
218
+ this.formData = { ...this.action };
219
+ // Apply smart transformations for select fields that expect {name, value} format
220
+ this.applySmartSelectTransformations(actionConfig);
221
+ }
222
+ // Convert Record objects to array format for key-value editors
223
+ this.processFormDataForEditing();
224
+ // Store a copy of the original form data for computed field comparisons
225
+ this.originalFormData = JSON.parse(JSON.stringify(this.formData));
226
+ }
227
+ else if (this.node) {
228
+ // Node editing mode - use node config
229
+ const nodeConfig = this.getNodeConfig();
230
+ if (nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.toFormData) {
231
+ this.formData = nodeConfig.toFormData(this.node);
232
+ }
233
+ else {
234
+ this.formData = { ...this.node };
235
+ }
236
+ // Convert Record objects to array format for key-value editors
237
+ this.processFormDataForEditing();
238
+ // Store a copy of the original form data for computed field comparisons
239
+ this.originalFormData = JSON.parse(JSON.stringify(this.formData));
240
+ }
241
+ // enforce immutability of formData
242
+ Object.keys(this.formData).forEach((key) => {
243
+ const value = this.formData[key];
244
+ if (Array.isArray(value)) {
245
+ this.formData[key] = [...value];
246
+ }
247
+ else if (value && typeof value === 'object') {
248
+ // If it's an object, ensure we don't mutate the original
249
+ this.formData[key] = { ...value };
250
+ }
251
+ });
252
+ }
253
+ processFormDataForEditing() {
254
+ const processed = { ...this.formData };
255
+ // Convert Record objects to key-value arrays for key-value editors
256
+ Object.keys(processed).forEach((key) => {
257
+ const value = processed[key];
258
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
259
+ // Check if this field should be a key-value editor
260
+ const isKeyValueField = this.isKeyValueField(key);
261
+ if (isKeyValueField) {
262
+ // Convert Record to array format
263
+ processed[key] = Object.entries(value).map(([k, v]) => ({
264
+ key: k,
265
+ value: v
266
+ }));
267
+ }
268
+ }
269
+ });
270
+ this.formData = processed;
271
+ }
272
+ applySmartSelectTransformations(actionConfig) {
273
+ if (!actionConfig)
274
+ return;
275
+ const fields = actionConfig.form;
276
+ if (!fields)
277
+ return;
278
+ Object.entries(fields).forEach(([fieldName, fieldConfig]) => {
279
+ if (this.shouldApplySmartSelectTransformation(fieldName, fieldConfig)) {
280
+ const value = this.formData[fieldName];
281
+ if (Array.isArray(value) &&
282
+ value.length > 0 &&
283
+ typeof value[0] === 'string') {
284
+ // Transform string array to select options format
285
+ this.formData[fieldName] = value.map((item) => ({
286
+ name: item,
287
+ value: item
288
+ }));
289
+ }
290
+ }
291
+ });
292
+ }
293
+ shouldApplySmartSelectTransformation(fieldName, fieldConfig) {
294
+ var _a;
295
+ const selectConfig = fieldConfig;
296
+ return ((fieldConfig.type === 'select' &&
297
+ (selectConfig.multi || selectConfig.tags) &&
298
+ // Don't transform if already has explicit transformations
299
+ !this.action) ||
300
+ !((_a = ACTION_CONFIG[this.action.type]) === null || _a === void 0 ? void 0 : _a.toFormData));
301
+ }
302
+ isKeyValueField(fieldName) {
303
+ var _a;
304
+ // Check if this field is configured as a key-value type
305
+ if (this.action) {
306
+ const actionConfig = ACTION_CONFIG[this.action.type];
307
+ const fields = actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.form;
308
+ return ((_a = fields === null || fields === void 0 ? void 0 : fields[fieldName]) === null || _a === void 0 ? void 0 : _a.type) === 'key-value';
309
+ }
310
+ return false;
311
+ }
312
+ getNodeConfig() {
313
+ if (!this.nodeUI)
314
+ return null;
315
+ // Get node config based on the nodeUI's type
316
+ return this.nodeUI.type ? NODE_CONFIG[this.nodeUI.type] : null;
317
+ }
318
+ getHeaderColor() {
319
+ if (this.action) {
320
+ // Action editing mode
321
+ const actionConfig = ACTION_CONFIG[this.action.type];
322
+ return (actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.color) || '#666666';
323
+ }
324
+ else if (this.node) {
325
+ // Node editing mode
326
+ const nodeConfig = this.getNodeConfig();
327
+ return (nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.color) || '#666666';
328
+ }
329
+ return '#666666';
330
+ }
331
+ handleDialogButtonClick(event) {
332
+ const button = event.detail.button;
333
+ if (button.name === 'Save') {
334
+ this.handleSave();
335
+ }
336
+ else if (button.name === 'Cancel') {
337
+ this.handleCancel();
338
+ }
339
+ }
340
+ handleSave() {
341
+ // Validate the form
342
+ const validation = this.validateForm();
343
+ if (!validation.valid) {
344
+ this.errors = validation.errors;
345
+ // Expand any groups that contain validation errors
346
+ this.expandGroupsWithErrors(validation.errors);
347
+ return;
348
+ }
349
+ // Process form data to convert key-value arrays to Records before saving
350
+ const processedFormData = this.processFormDataForSave();
351
+ // Determine whether to use node or action saving based on context
352
+ // If we have a node with a router, always use node saving (even if action is set)
353
+ // because router configuration is handled at the node level
354
+ if (this.node && this.node.router) {
355
+ // Node editing mode with router - use formDataToNode
356
+ const updatedNode = this.formDataToNode(processedFormData);
357
+ this.fireCustomEvent(CustomEventType.NodeSaved, {
358
+ node: updatedNode
359
+ });
360
+ }
361
+ else if (this.action) {
362
+ // Pure action editing mode (no router)
363
+ const updatedAction = this.formDataToAction(processedFormData);
364
+ this.fireCustomEvent(CustomEventType.ActionSaved, {
365
+ action: updatedAction
366
+ });
367
+ }
368
+ else if (this.node) {
369
+ // Node editing mode without router
370
+ const updatedNode = this.formDataToNode(processedFormData);
371
+ this.fireCustomEvent(CustomEventType.NodeSaved, {
372
+ node: updatedNode
373
+ });
374
+ }
375
+ }
376
+ processFormDataForSave() {
377
+ const processed = { ...this.formData };
378
+ // Convert key-value arrays to Records
379
+ Object.keys(processed).forEach((key) => {
380
+ const value = processed[key];
381
+ if (Array.isArray(value) &&
382
+ value.length > 0 &&
383
+ typeof value[0] === 'object' &&
384
+ 'key' in value[0] &&
385
+ 'value' in value[0]) {
386
+ // This is a key-value array, convert to Record
387
+ const record = {};
388
+ value.forEach(({ key: k, value: v }) => {
389
+ if (k.trim() !== '' || v.trim() !== '') {
390
+ record[k] = v;
391
+ }
392
+ });
393
+ processed[key] = record;
394
+ }
395
+ else if (Array.isArray(value) && value.length === 0) {
396
+ // Empty key-value array should become empty object
397
+ const isKeyValueField = this.isKeyValueField(key);
398
+ if (isKeyValueField) {
399
+ processed[key] = {};
400
+ }
401
+ }
402
+ });
403
+ return processed;
404
+ }
405
+ handleCancel() {
406
+ this.fireCustomEvent(CustomEventType.NodeEditCancelled, {});
407
+ }
408
+ validateForm() {
409
+ const errors = {};
410
+ if (this.action) {
411
+ // Action validation using fields configuration
412
+ const actionConfig = ACTION_CONFIG[this.action.type];
413
+ // Check if new field configuration system is available
414
+ if (actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.form) {
415
+ Object.entries(actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.form).forEach(([fieldName, fieldConfig]) => {
416
+ const value = this.formData[fieldName];
417
+ // Check required fields
418
+ if (fieldConfig.required &&
419
+ (!value || (Array.isArray(value) && value.length === 0))) {
420
+ errors[fieldName] = `${fieldConfig.label || fieldName} is required`;
421
+ }
422
+ // Check minLength for text fields
423
+ if (typeof value === 'string' &&
424
+ fieldConfig.minLength &&
425
+ value.length < fieldConfig.minLength) {
426
+ errors[fieldName] = `${fieldConfig.label || fieldName} must be at least ${fieldConfig.minLength} characters`;
427
+ }
428
+ // Check maxLength for text fields
429
+ if (typeof value === 'string' &&
430
+ fieldConfig.maxLength &&
431
+ value.length > fieldConfig.maxLength) {
432
+ errors[fieldName] = `${fieldConfig.label || fieldName} must be no more than ${fieldConfig.maxLength} characters`;
433
+ }
434
+ });
435
+ }
436
+ // Run custom validation if available
437
+ if (actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.validate) {
438
+ // Convert form data back to action for validation
439
+ let actionForValidation;
440
+ if (actionConfig.fromFormData) {
441
+ actionForValidation = actionConfig.fromFormData(this.formData);
442
+ }
443
+ else {
444
+ actionForValidation = { ...this.action, ...this.formData };
445
+ }
446
+ const customValidation = actionConfig.validate(actionForValidation);
447
+ Object.assign(errors, customValidation.errors);
448
+ }
449
+ }
450
+ else if (this.node) {
451
+ // Node validation
452
+ const nodeConfig = this.getNodeConfig();
453
+ // Check required fields from node properties
454
+ if (nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.properties) {
455
+ Object.entries(nodeConfig.properties).forEach(([fieldName, fieldConfig]) => {
456
+ const value = this.formData[fieldName];
457
+ // Check required fields
458
+ if (fieldConfig.required &&
459
+ (!value || (Array.isArray(value) && value.length === 0))) {
460
+ errors[fieldName] = `${fieldConfig.label || fieldName} is required`;
461
+ }
462
+ });
463
+ }
464
+ }
465
+ // Validate key-value fields for unique keys
466
+ this.validateKeyValueUniqueness(errors);
467
+ return {
468
+ valid: Object.keys(errors).length === 0,
469
+ errors
470
+ };
471
+ }
472
+ validateKeyValueUniqueness(errors) {
473
+ // The individual key-value editors will show validation errors on duplicate keys and empty keys with values
474
+ // We just need to prevent form submission when there are validation issues
475
+ Object.entries(this.formData).forEach(([fieldName, value]) => {
476
+ if (Array.isArray(value) &&
477
+ value.length > 0 &&
478
+ typeof value[0] === 'object' &&
479
+ 'key' in value[0] &&
480
+ 'value' in value[0]) {
481
+ // This is a key-value array
482
+ let hasValidationErrors = false;
483
+ // Check for empty keys with values
484
+ value.forEach(({ key, value: itemValue }) => {
485
+ if (key.trim() === '' && itemValue.trim() !== '') {
486
+ hasValidationErrors = true;
487
+ }
488
+ });
489
+ // Check for duplicate keys (only non-empty ones)
490
+ const keys = value
491
+ .filter(({ key }) => key.trim() !== '') // Only check non-empty keys
492
+ .map(({ key }) => key.trim());
493
+ const uniqueKeys = new Set(keys);
494
+ if (keys.length !== uniqueKeys.size) {
495
+ hasValidationErrors = true;
496
+ }
497
+ if (hasValidationErrors) {
498
+ errors[fieldName] = `Please resolve validation errors to continue`;
499
+ }
500
+ }
501
+ });
502
+ }
503
+ formDataToNode(formData = this.formData) {
504
+ var _a;
505
+ if (!this.node)
506
+ throw new Error('No node to update');
507
+ let updatedNode = { ...this.node };
508
+ // Handle actions using action config transformations if available
509
+ if (this.node.actions && this.node.actions.length > 0) {
510
+ updatedNode.actions = this.node.actions.map((action) => {
511
+ // If we're editing a specific action, only transform that one
512
+ if (this.action && action.uuid === this.action.uuid) {
513
+ const actionConfig = ACTION_CONFIG[action.type];
514
+ if (actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.fromFormData) {
515
+ // Use action-specific form data transformation
516
+ return actionConfig.fromFormData(formData);
517
+ }
518
+ else {
519
+ // Default transformation - merge form data with original action
520
+ return { ...action, ...formData };
521
+ }
522
+ }
523
+ else {
524
+ // Keep other actions unchanged
525
+ return action;
526
+ }
527
+ });
528
+ }
529
+ // Handle router configuration using node config
530
+ if (this.node.router) {
531
+ const nodeConfig = this.getNodeConfig();
532
+ if (nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.fromFormData) {
533
+ // Use node-specific form data transformation
534
+ updatedNode = nodeConfig.fromFormData(formData, updatedNode);
535
+ }
536
+ else {
537
+ // Default router handling
538
+ updatedNode.router = { ...this.node.router };
539
+ // Apply form data to router fields if they exist
540
+ if (formData.result_name !== undefined) {
541
+ updatedNode.router.result_name = formData.result_name;
542
+ }
543
+ // Handle preconfigured rules from node config
544
+ if ((_a = nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.router) === null || _a === void 0 ? void 0 : _a.rules) {
545
+ // Build a complete new set of categories and exits based on node config
546
+ const existingCategories = updatedNode.router.categories || [];
547
+ const existingExits = updatedNode.exits || [];
548
+ const newCategories = [];
549
+ const newExits = [];
550
+ // Group rules by category name to handle multiple rules pointing to the same category
551
+ const categoryNameToRules = new Map();
552
+ nodeConfig.router.rules.forEach((rule) => {
553
+ if (!categoryNameToRules.has(rule.categoryName)) {
554
+ categoryNameToRules.set(rule.categoryName, []);
555
+ }
556
+ categoryNameToRules.get(rule.categoryName).push(rule);
557
+ });
558
+ // Create categories for all unique category names
559
+ categoryNameToRules.forEach((rules, categoryName) => {
560
+ // Check if category already exists to preserve its UUID and exit_uuid
561
+ const existingCategory = existingCategories.find((cat) => cat.name === categoryName);
562
+ if (existingCategory) {
563
+ // Preserve existing category and its associated exit
564
+ newCategories.push(existingCategory);
565
+ const associatedExit = existingExits.find((exit) => exit.uuid === existingCategory.exit_uuid);
566
+ if (associatedExit) {
567
+ newExits.push(associatedExit);
568
+ }
569
+ }
570
+ else {
571
+ // Create new category and exit
572
+ const categoryUuid = generateUUID();
573
+ const exitUuid = generateUUID();
574
+ newCategories.push({
575
+ uuid: categoryUuid,
576
+ name: categoryName,
577
+ exit_uuid: exitUuid
578
+ });
579
+ newExits.push({
580
+ uuid: exitUuid,
581
+ destination_uuid: null
582
+ });
583
+ }
584
+ });
585
+ // Add default category if specified
586
+ if (nodeConfig.router.defaultCategory) {
587
+ // Check if default category already exists in our new list
588
+ const existingDefault = newCategories.find((cat) => cat.name === nodeConfig.router.defaultCategory);
589
+ if (!existingDefault) {
590
+ // Check if it exists in the original categories
591
+ const originalDefault = existingCategories.find((cat) => cat.name === nodeConfig.router.defaultCategory);
592
+ if (originalDefault) {
593
+ // Preserve existing default category and its exit
594
+ newCategories.push(originalDefault);
595
+ const associatedExit = existingExits.find((exit) => exit.uuid === originalDefault.exit_uuid);
596
+ if (associatedExit) {
597
+ newExits.push(associatedExit);
598
+ }
599
+ }
600
+ else {
601
+ // Create new default category and exit
602
+ const categoryUuid = generateUUID();
603
+ const exitUuid = generateUUID();
604
+ newCategories.push({
605
+ uuid: categoryUuid,
606
+ name: nodeConfig.router.defaultCategory,
607
+ exit_uuid: exitUuid
608
+ });
609
+ newExits.push({
610
+ uuid: exitUuid,
611
+ destination_uuid: null
612
+ });
613
+ }
614
+ }
615
+ }
616
+ // Replace the entire categories and exits lists with our complete new sets
617
+ updatedNode.router.categories = newCategories;
618
+ updatedNode.exits = newExits;
619
+ }
620
+ }
621
+ }
622
+ else {
623
+ // If no router, just apply form data to node properties
624
+ Object.keys(formData).forEach((key) => {
625
+ if (key !== 'uuid' &&
626
+ key !== 'actions' &&
627
+ key !== 'exits' &&
628
+ key !== 'router') {
629
+ updatedNode[key] = formData[key];
630
+ }
631
+ });
632
+ }
633
+ return updatedNode;
634
+ }
635
+ formDataToAction(formData = this.formData) {
636
+ if (!this.action)
637
+ throw new Error('No action to update');
638
+ // Use action config transformation if available
639
+ const actionConfig = ACTION_CONFIG[this.action.type];
640
+ if (actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.fromFormData) {
641
+ return actionConfig.fromFormData(formData);
642
+ }
643
+ else {
644
+ // Apply smart select transformations in reverse and provide default 1:1 mapping
645
+ const processedFormData = this.reverseSmartSelectTransformations(formData, actionConfig);
646
+ return { ...this.action, ...processedFormData };
647
+ }
648
+ }
649
+ reverseSmartSelectTransformations(formData, actionConfig) {
650
+ if (!actionConfig || !actionConfig.form)
651
+ return formData;
652
+ const processed = { ...formData };
653
+ Object.entries(actionConfig.form).forEach(([fieldName, fieldConfig]) => {
654
+ if (this.shouldApplySmartSelectTransformation(fieldName, fieldConfig)) {
655
+ const value = processed[fieldName];
656
+ if (Array.isArray(value) &&
657
+ value.length > 0 &&
658
+ typeof value[0] === 'object' &&
659
+ 'value' in value[0]) {
660
+ // Transform select options format back to string array
661
+ processed[fieldName] = value.map((item) => item.value || item.name || item);
662
+ }
663
+ }
664
+ });
665
+ return processed;
666
+ }
667
+ handleFormFieldChange(propertyName, event) {
668
+ const target = event.target;
669
+ let value;
670
+ // Handle different component types like ActionEditor does
671
+ if (target.tagName === 'TEMBA-CHECKBOX') {
672
+ value = target.checked;
673
+ }
674
+ else if (target.tagName === 'TEMBA-SELECT' &&
675
+ (target.multi || target.emails || target.tags)) {
676
+ value = target.values || [];
677
+ }
678
+ else if (target.values !== undefined) {
679
+ value = target.values;
680
+ }
681
+ else {
682
+ value = target.value;
683
+ }
684
+ this.formData = {
685
+ ...this.formData,
686
+ [propertyName]: value
687
+ };
688
+ // Clear any existing error for this field
689
+ if (this.errors[propertyName]) {
690
+ const newErrors = { ...this.errors };
691
+ delete newErrors[propertyName];
692
+ this.errors = newErrors;
693
+ }
694
+ // Check for computed values in dependent fields
695
+ this.updateComputedFields(propertyName);
696
+ // Trigger re-render to handle conditional field visibility
697
+ this.requestUpdate();
698
+ }
699
+ updateComputedFields(changedFieldName) {
700
+ if (!this.action)
701
+ return;
702
+ const config = ACTION_CONFIG[this.action.type];
703
+ if (!(config === null || config === void 0 ? void 0 : config.form))
704
+ return;
705
+ // Check all fields to see if any depend on the changed field
706
+ Object.entries(config.form).forEach(([fieldName, fieldConfig]) => {
707
+ var _a;
708
+ if ((_a = fieldConfig.dependsOn) === null || _a === void 0 ? void 0 : _a.includes(changedFieldName)) {
709
+ if (fieldConfig.computeValue) {
710
+ const currentValue = this.formData[fieldName];
711
+ const computedValue = fieldConfig.computeValue(this.formData, currentValue, this.originalFormData);
712
+ // Update the form data with the computed value
713
+ this.formData = {
714
+ ...this.formData,
715
+ [fieldName]: computedValue
716
+ };
717
+ }
718
+ }
719
+ });
720
+ }
721
+ renderNewField(fieldName, config, value) {
722
+ var _a;
723
+ // Check visibility condition
724
+ if ((_a = config.conditions) === null || _a === void 0 ? void 0 : _a.visible) {
725
+ try {
726
+ const isVisible = config.conditions.visible(this.formData);
727
+ if (!isVisible) {
728
+ return html ``;
729
+ }
730
+ }
731
+ catch (error) {
732
+ console.error(`Error checking visibility for ${fieldName}:`, error);
733
+ // If there's an error, show the field by default
734
+ }
735
+ }
736
+ const errors = this.errors[fieldName] ? [this.errors[fieldName]] : [];
737
+ // Build container style with maxWidth if specified
738
+ const containerStyle = config.maxWidth
739
+ ? `max-width: ${config.maxWidth};`
740
+ : '';
741
+ const fieldContent = this.renderFieldContent(fieldName, config, value, errors);
742
+ // Wrap in container with style if maxWidth is specified
743
+ if (containerStyle) {
744
+ return html `<div style="${containerStyle}">${fieldContent}</div>`;
745
+ }
746
+ return fieldContent;
747
+ }
748
+ renderFieldContent(fieldName, config, value, errors) {
749
+ var _a;
750
+ switch (config.type) {
751
+ case 'text':
752
+ return html `<temba-textinput
753
+ name="${fieldName}"
754
+ label="${config.label}"
755
+ ?required="${config.required}"
756
+ .errors="${errors}"
757
+ .value="${value || ''}"
758
+ placeholder="${config.placeholder || ''}"
759
+ .helpText="${config.helpText || ''}"
760
+ @input="${(e) => this.handleFormFieldChange(fieldName, e)}"
761
+ ></temba-textinput>`;
762
+ case 'textarea': {
763
+ const textareaConfig = config;
764
+ const minHeightStyle = textareaConfig.minHeight
765
+ ? `--textarea-min-height: ${textareaConfig.minHeight}px;`
766
+ : '';
767
+ if (config.evaluated) {
768
+ return html `<temba-completion
769
+ name="${fieldName}"
770
+ label="${config.label}"
771
+ ?required="${config.required}"
772
+ .errors="${errors}"
773
+ .value="${value || ''}"
774
+ placeholder="${config.placeholder || ''}"
775
+ textarea
776
+ expressions="session"
777
+ style="${minHeightStyle}"
778
+ .helpText="${config.helpText || ''}"
779
+ @input="${(e) => this.handleFormFieldChange(fieldName, e)}"
780
+ ></temba-completion>`;
781
+ }
782
+ else {
783
+ return html `<temba-textinput
784
+ name="${fieldName}"
785
+ label="${config.label}"
786
+ ?required="${config.required}"
787
+ .errors="${errors}"
788
+ .value="${value || ''}"
789
+ placeholder="${config.placeholder || ''}"
790
+ textarea
791
+ .rows="${textareaConfig.rows || 3}"
792
+ style="${minHeightStyle}"
793
+ .helpText="${config.helpText || ''}"
794
+ @input="${(e) => this.handleFormFieldChange(fieldName, e)}"
795
+ ></temba-textinput>`;
796
+ }
797
+ }
798
+ case 'select': {
799
+ const selectConfig = config;
800
+ return html `<temba-select
801
+ name="${fieldName}"
802
+ label="${config.label}"
803
+ ?required="${config.required}"
804
+ .errors="${errors}"
805
+ .values="${value || (selectConfig.multi ? [] : '')}"
806
+ ?multi="${selectConfig.multi}"
807
+ ?searchable="${selectConfig.searchable}"
808
+ ?tags="${selectConfig.tags}"
809
+ ?emails="${selectConfig.emails}"
810
+ placeholder="${selectConfig.placeholder || ''}"
811
+ maxItems="${selectConfig.maxItems || 0}"
812
+ valueKey="${selectConfig.valueKey || 'value'}"
813
+ nameKey="${selectConfig.nameKey || 'name'}"
814
+ endpoint="${selectConfig.endpoint || ''}"
815
+ .helpText="${config.helpText || ''}"
816
+ @change="${(e) => this.handleFormFieldChange(fieldName, e)}"
817
+ >
818
+ ${(_a = selectConfig.options) === null || _a === void 0 ? void 0 : _a.map((option) => {
819
+ if (typeof option === 'string') {
820
+ return html `<temba-option
821
+ name="${option}"
822
+ value="${option}"
823
+ ></temba-option>`;
824
+ }
825
+ else {
826
+ return html `<temba-option
827
+ name="${option.label || option.name}"
828
+ value="${option.value}"
829
+ ></temba-option>`;
830
+ }
831
+ })}
832
+ </temba-select>`;
833
+ }
834
+ case 'key-value':
835
+ return html `<div class="form-field">
836
+ <label>${config.label}${config.required ? ' *' : ''}</label>
837
+ <temba-key-value-editor
838
+ name="${fieldName}"
839
+ .value="${value || []}"
840
+ .sortable="${config.sortable}"
841
+ .keyPlaceholder="${config.keyPlaceholder || 'Key'}"
842
+ .valuePlaceholder="${config.valuePlaceholder || 'Value'}"
843
+ .minRows="${config.minRows || 0}"
844
+ @change="${(e) => {
845
+ if (e.detail) {
846
+ this.handleNewFieldChange(fieldName, e.detail.value);
847
+ }
848
+ }}"
849
+ ></temba-key-value-editor>
850
+ ${errors.length
851
+ ? html `<div class="field-errors">${errors.join(', ')}</div>`
852
+ : ''}
853
+ </div>`;
854
+ case 'array':
855
+ return html `<div class="form-field">
856
+ <label>${config.label}${config.required ? ' *' : ''}</label>
857
+ <temba-array-editor
858
+ .value="${value || []}"
859
+ .itemConfig="${config.itemConfig}"
860
+ .sortable="${config.sortable}"
861
+ .itemLabel="${config.itemLabel || 'Item'}"
862
+ .minItems="${config.minItems || 0}"
863
+ .onItemChange="${config.onItemChange}"
864
+ @change="${(e) => this.handleNewFieldChange(fieldName, e.detail.value)}"
865
+ ></temba-array-editor>
866
+ ${errors.length
867
+ ? html `<div class="field-errors">${errors.join(', ')}</div>`
868
+ : ''}
869
+ </div>`;
870
+ case 'checkbox': {
871
+ const checkboxConfig = config;
872
+ return html `<div class="form-field">
873
+ <temba-checkbox
874
+ name="${fieldName}"
875
+ label="${config.label}"
876
+ .helpText="${config.helpText || ''}"
877
+ ?required="${config.required}"
878
+ .errors="${errors}"
879
+ ?checked="${value || false}"
880
+ size="${checkboxConfig.size || 1.2}"
881
+ animateChange="${checkboxConfig.animateChange || 'pulse'}"
882
+ @change="${(e) => this.handleFormFieldChange(fieldName, e)}"
883
+ ></temba-checkbox>
884
+ ${errors.length
885
+ ? html `<div class="field-errors">${errors.join(', ')}</div>`
886
+ : ''}
887
+ </div>`;
888
+ }
889
+ default:
890
+ return html `<div>Unsupported field type: ${config.type}</div>`;
891
+ }
892
+ }
893
+ handleGroupToggle(groupLabel) {
894
+ this.groupCollapseState = {
895
+ ...this.groupCollapseState,
896
+ [groupLabel]: !this.groupCollapseState[groupLabel]
897
+ };
898
+ }
899
+ expandGroupsWithErrors(errors) {
900
+ if (!this.action)
901
+ return;
902
+ const config = ACTION_CONFIG[this.action.type];
903
+ if (!(config === null || config === void 0 ? void 0 : config.layout))
904
+ return;
905
+ const errorFields = new Set(Object.keys(errors));
906
+ this.expandGroupsWithErrorsRecursive(config.layout, errorFields);
907
+ }
908
+ expandGroupsWithErrorsRecursive(items, errorFields) {
909
+ items.forEach((item) => {
910
+ if (typeof item === 'object' && item.type === 'group') {
911
+ const fieldsInGroup = this.collectFieldsFromItems(item.items);
912
+ const groupHasErrors = fieldsInGroup.some((fieldName) => errorFields.has(fieldName));
913
+ if (groupHasErrors) {
914
+ // Expand this group
915
+ this.groupCollapseState = {
916
+ ...this.groupCollapseState,
917
+ [item.label]: false
918
+ };
919
+ }
920
+ // Recursively check nested items
921
+ this.expandGroupsWithErrorsRecursive(item.items, errorFields);
922
+ }
923
+ else if (typeof item === 'object' && item.type === 'row') {
924
+ // Recursively check items in rows
925
+ this.expandGroupsWithErrorsRecursive(item.items, errorFields);
926
+ }
927
+ });
928
+ }
929
+ renderLayoutItem(item, config, renderedFields) {
930
+ if (typeof item === 'string') {
931
+ // String shorthand for field
932
+ return this.renderLayoutItem({ type: 'field', field: item }, config, renderedFields);
933
+ }
934
+ switch (item.type) {
935
+ case 'field':
936
+ if (config.form[item.field] && !renderedFields.has(item.field)) {
937
+ renderedFields.add(item.field);
938
+ return this.renderNewField(item.field, config.form[item.field], this.formData[item.field]);
939
+ }
940
+ return html ``;
941
+ case 'row':
942
+ return this.renderRow(item, config, renderedFields);
943
+ case 'group':
944
+ return this.renderGroup(item, config, renderedFields);
945
+ default:
946
+ return html ``;
947
+ }
948
+ }
949
+ renderRow(rowConfig, config, renderedFields) {
950
+ const { items, gap = '1rem' } = rowConfig;
951
+ // Collect all fields from this row for width calculations
952
+ const fieldsInRow = this.collectFieldsFromItems(items);
953
+ const validFields = fieldsInRow.filter((fieldName) => { var _a; return (_a = config.form) === null || _a === void 0 ? void 0 : _a[fieldName]; });
954
+ if (validFields.length === 0) {
955
+ return html ``;
956
+ }
957
+ // Calculate grid template columns based on field maxWidth constraints
958
+ const columns = validFields.map((fieldName) => {
959
+ const fieldConfig = config.form[fieldName];
960
+ return fieldConfig.maxWidth || '1fr';
961
+ });
962
+ return html `
963
+ <div
964
+ class="form-row"
965
+ style="display: grid; grid-template-columns: ${columns.join(' ')}; gap: ${gap};"
966
+ >
967
+ ${items.map((item) => this.renderLayoutItem(item, config, renderedFields))}
968
+ </div>
969
+ `;
970
+ }
971
+ renderGroup(groupConfig, config, renderedFields) {
972
+ var _a;
973
+ const { label, items, collapsible = false, collapsed = false, helpText } = groupConfig;
974
+ // Initialize collapse state if not set
975
+ if (collapsible && !(label in this.groupCollapseState)) {
976
+ this.groupCollapseState = {
977
+ ...this.groupCollapseState,
978
+ [label]: collapsed
979
+ };
980
+ }
981
+ const isCollapsed = collapsible
982
+ ? (_a = this.groupCollapseState[label]) !== null && _a !== void 0 ? _a : collapsed
983
+ : false;
984
+ // Check if any field in this group has errors
985
+ const fieldsInGroup = this.collectFieldsFromItems(items);
986
+ const groupHasErrors = fieldsInGroup.some((fieldName) => this.errors[fieldName]);
987
+ return html `
988
+ <div
989
+ class="form-group ${collapsible ? 'collapsible' : ''} ${groupHasErrors
990
+ ? 'has-errors'
991
+ : ''}"
992
+ >
993
+ <div
994
+ class="form-group-header ${collapsible ? 'clickable' : ''}"
995
+ @click=${collapsible
996
+ ? () => this.handleGroupToggle(label)
997
+ : undefined}
998
+ >
999
+ <div class="form-group-info">
1000
+ <div class="form-group-title">${label}</div>
1001
+ ${helpText
1002
+ ? html `<div class="form-group-help">${helpText}</div>`
1003
+ : ''}
1004
+ </div>
1005
+ ${groupHasErrors
1006
+ ? html `<temba-icon
1007
+ name="alert_warning"
1008
+ class="group-error-icon"
1009
+ size="1.5"
1010
+ ></temba-icon>`
1011
+ : ''}
1012
+ ${collapsible && !groupHasErrors
1013
+ ? html `<temba-icon
1014
+ name="arrow_right"
1015
+ size="1.5"
1016
+ class="group-toggle-icon ${isCollapsed
1017
+ ? 'collapsed'
1018
+ : 'expanded'}"
1019
+ ></temba-icon>`
1020
+ : ''}
1021
+ </div>
1022
+ <div
1023
+ class="form-group-content ${isCollapsed ? 'collapsed' : 'expanded'}"
1024
+ >
1025
+ ${items.map((item) => this.renderLayoutItem(item, config, renderedFields))}
1026
+ </div>
1027
+ </div>
1028
+ `;
1029
+ }
1030
+ collectFieldsFromItems(items) {
1031
+ const fields = [];
1032
+ items.forEach((item) => {
1033
+ if (typeof item === 'string') {
1034
+ fields.push(item);
1035
+ }
1036
+ else if (item.type === 'field') {
1037
+ fields.push(item.field);
1038
+ }
1039
+ else if (item.type === 'row') {
1040
+ fields.push(...this.collectFieldsFromItems(item.items));
1041
+ }
1042
+ else if (item.type === 'group') {
1043
+ fields.push(...this.collectFieldsFromItems(item.items));
1044
+ }
1045
+ });
1046
+ return fields;
1047
+ }
1048
+ renderFieldRow(rowConfig, config) {
1049
+ // This method is deprecated - use renderRow instead
1050
+ return this.renderRow(rowConfig, config, new Set());
1051
+ }
1052
+ renderFieldGroup(groupConfig, config) {
1053
+ // This method is deprecated - use renderGroup instead
1054
+ return this.renderGroup(groupConfig, config, new Set());
1055
+ }
1056
+ handleNewFieldChange(fieldName, value) {
1057
+ this.formData = {
1058
+ ...this.formData,
1059
+ [fieldName]: value
1060
+ };
1061
+ // Clear any existing error for this field
1062
+ if (this.errors[fieldName]) {
1063
+ const newErrors = { ...this.errors };
1064
+ delete newErrors[fieldName];
1065
+ this.errors = newErrors;
1066
+ }
1067
+ // Trigger re-render
1068
+ this.requestUpdate();
1069
+ }
1070
+ renderFields() {
1071
+ if (!this.action) {
1072
+ return html ` <div>No action selected</div> `;
1073
+ }
1074
+ const config = ACTION_CONFIG[this.action.type];
1075
+ if (!config) {
1076
+ return html ` <div>No configuration available for this action</div> `;
1077
+ }
1078
+ // Use the new fields configuration system
1079
+ if (config.form) {
1080
+ // If layout is specified, use it
1081
+ if (config.layout) {
1082
+ const renderedFields = new Set();
1083
+ return html `
1084
+ ${config.layout.map((item) => this.renderLayoutItem(item, config, renderedFields))}
1085
+ ${
1086
+ /* Render any fields not explicitly placed in layout */
1087
+ Object.entries(config.form).map(([fieldName, fieldConfig]) => {
1088
+ if (!renderedFields.has(fieldName)) {
1089
+ return this.renderNewField(fieldName, fieldConfig, this.formData[fieldName]);
1090
+ }
1091
+ return html ``;
1092
+ })}
1093
+ `;
1094
+ }
1095
+ else {
1096
+ // Default rendering without layout
1097
+ return html `
1098
+ ${Object.entries(config.form).map(([fieldName, fieldConfig]) => this.renderNewField(fieldName, fieldConfig, this.formData[fieldName]))}
1099
+ `;
1100
+ }
1101
+ }
1102
+ return html ` <div>No form configuration available</div> `;
1103
+ }
1104
+ renderActionSection() {
1105
+ if (!this.node || this.node.actions.length === 0) {
1106
+ return html ``;
1107
+ }
1108
+ const nodeConfig = this.getNodeConfig();
1109
+ // If node has an action config, defer to ActionEditor
1110
+ if (nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.action) {
1111
+ const action = this.node.actions[0]; // Assume single action for now
1112
+ return html `
1113
+ <div class="action-section">
1114
+ <h3>Action Configuration</h3>
1115
+ <div class="action-preview">
1116
+ <p><strong>Type:</strong> ${action.type}</p>
1117
+ <p><em>Action details will be editable here</em></p>
1118
+ </div>
1119
+ </div>
1120
+ `;
1121
+ }
1122
+ return html ``;
1123
+ }
1124
+ renderRouterSection() {
1125
+ var _a;
1126
+ if (!((_a = this.node) === null || _a === void 0 ? void 0 : _a.router)) {
1127
+ return html ``;
1128
+ }
1129
+ const nodeConfig = this.getNodeConfig();
1130
+ return html `
1131
+ <div class="router-section">
1132
+ <h3>Router Configuration</h3>
1133
+ ${(nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.router)
1134
+ ? this.renderRouterConfig()
1135
+ : html `<p>Basic router (no advanced configuration)</p>`}
1136
+ </div>
1137
+ `;
1138
+ }
1139
+ renderRouterConfig() {
1140
+ const nodeConfig = this.getNodeConfig();
1141
+ if (!(nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.router))
1142
+ return html ``;
1143
+ // Render router configuration based on node config
1144
+ // This is where you'd render rule and category editors
1145
+ return html `
1146
+ <div class="router-config">
1147
+ <p><strong>Type:</strong> ${nodeConfig.router.type}</p>
1148
+ ${nodeConfig.router.rules
1149
+ ? html `
1150
+ <div class="rules-section">
1151
+ <h4>Rules</h4>
1152
+ <!-- Future: Render rule editor based on nodeConfig.router.rules -->
1153
+ <p><em>Rule editing will be implemented here</em></p>
1154
+ </div>
1155
+ `
1156
+ : ''}
1157
+ </div>
1158
+ `;
1159
+ }
1160
+ render() {
1161
+ var _a, _b;
1162
+ if (!this.isOpen) {
1163
+ return html ``;
1164
+ }
1165
+ const headerColor = this.getHeaderColor();
1166
+ const nodeConfig = this.getNodeConfig();
1167
+ const actionConfig = ACTION_CONFIG[(_a = this.action) === null || _a === void 0 ? void 0 : _a.type];
1168
+ return html `
1169
+ <temba-dialog
1170
+ header="${(actionConfig === null || actionConfig === void 0 ? void 0 : actionConfig.name) || (nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.name) || 'Edit'}"
1171
+ .open="${this.isOpen}"
1172
+ @temba-button-clicked=${this.handleDialogButtonClick}
1173
+ primaryButtonName="Save"
1174
+ cancelButtonName="Cancel"
1175
+ style="--header-bg: ${headerColor}"
1176
+ >
1177
+ <div class="node-editor-form">
1178
+ ${this.renderFields()}
1179
+ ${((_b = nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.router) === null || _b === void 0 ? void 0 : _b.configurable)
1180
+ ? this.renderRouterSection()
1181
+ : null}
1182
+ </div>
1183
+ </temba-dialog>
1184
+ `;
1185
+ }
1186
+ }
1187
+ __decorate([
1188
+ property({ type: Object })
1189
+ ], NodeEditor.prototype, "action", void 0);
1190
+ __decorate([
1191
+ property({ type: Object })
1192
+ ], NodeEditor.prototype, "node", void 0);
1193
+ __decorate([
1194
+ property({ type: Object })
1195
+ ], NodeEditor.prototype, "nodeUI", void 0);
1196
+ __decorate([
1197
+ property({ type: Boolean })
1198
+ ], NodeEditor.prototype, "isOpen", void 0);
1199
+ __decorate([
1200
+ state()
1201
+ ], NodeEditor.prototype, "formData", void 0);
1202
+ __decorate([
1203
+ state()
1204
+ ], NodeEditor.prototype, "originalFormData", void 0);
1205
+ __decorate([
1206
+ state()
1207
+ ], NodeEditor.prototype, "errors", void 0);
1208
+ __decorate([
1209
+ state()
1210
+ ], NodeEditor.prototype, "groupCollapseState", void 0);
1211
+ //# sourceMappingURL=NodeEditor.js.map