@nyaruka/temba-components 0.129.3 → 0.129.4

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 +18 -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 +1200 -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 +77 -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 +85 -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 +1443 -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 +89 -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 +127 -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,177 @@
1
+ import { LitElement, TemplateResult, html } from 'lit';
2
+ import { property } from 'lit/decorators.js';
3
+
4
+ export interface ListItem {
5
+ [key: string]: any;
6
+ }
7
+
8
+ export interface ListEditorConfig {
9
+ // Determines if empty items should be automatically maintained
10
+ maintainEmptyItem?: boolean;
11
+ // Function to check if an item is considered empty
12
+ isEmptyItem?: (item: ListItem) => boolean;
13
+ // Function to create a new empty item
14
+ createEmptyItem?: () => ListItem;
15
+ // Function to clean items before emitting (e.g., filter out empty items)
16
+ cleanItems?: (items: ListItem[]) => ListItem[];
17
+ // Minimum number of items to maintain
18
+ minItems?: number;
19
+ // Maximum number of items allowed
20
+ maxItems?: number;
21
+ }
22
+
23
+ export abstract class BaseListEditor<
24
+ T extends ListItem = ListItem
25
+ > extends LitElement {
26
+ @property({ attribute: false })
27
+ protected _items: T[] = [];
28
+
29
+ @property({ type: Number })
30
+ minItems = 0;
31
+
32
+ @property({ type: Number })
33
+ maxItems?: number;
34
+
35
+ @property({ type: Boolean })
36
+ maintainEmptyItem = false;
37
+
38
+ // Abstract methods that must be implemented by subclasses
39
+ abstract isEmptyItem(item: T): boolean;
40
+ abstract createEmptyItem(): T;
41
+ abstract renderItem(item: T, index: number): TemplateResult;
42
+
43
+ // Optional methods that subclasses can override
44
+ protected getContainerClass(): string {
45
+ return 'base-list-editor';
46
+ }
47
+
48
+ protected renderAddButton(): TemplateResult {
49
+ return html`
50
+ <button class="add-btn" @click=${() => this.addItem()}>Add Item</button>
51
+ `;
52
+ }
53
+
54
+ protected shouldShowAddButton(): boolean {
55
+ return (
56
+ !this.maintainEmptyItem &&
57
+ (!this.maxItems || this._items.length < this.maxItems)
58
+ );
59
+ }
60
+
61
+ render(): TemplateResult {
62
+ const items = this.displayItems;
63
+
64
+ return html`
65
+ <div class=${this.getContainerClass()}>
66
+ <div
67
+ class="list-items"
68
+ style="gap: 8px; display: grid; grid-template-columns: 1fr;"
69
+ >
70
+ ${items.map((item, index) => this.renderItem(item, index))}
71
+ </div>
72
+ ${this.shouldShowAddButton() ? this.renderAddButton() : ''}
73
+ </div>
74
+ `;
75
+ }
76
+
77
+ // Optional method for cleaning items before emission (can return any type)
78
+ protected cleanItems(items: T[]): any {
79
+ if (!this.maintainEmptyItem) {
80
+ return items;
81
+ }
82
+ // Filter out empty items for the emitted value
83
+ return items.filter((item) => !this.isEmptyItem(item));
84
+ }
85
+
86
+ // Get the items to display (may include empty items for UI)
87
+ protected get displayItems(): T[] {
88
+ const items = [...this._items];
89
+
90
+ if (this.maintainEmptyItem) {
91
+ const hasEmptyItem = items.some((item) => this.isEmptyItem(item));
92
+ if (!hasEmptyItem) {
93
+ items.push(this.createEmptyItem());
94
+ }
95
+ }
96
+
97
+ return items;
98
+ }
99
+
100
+ // Handle changes to an item
101
+ protected handleItemChange(index: number, newItem: T) {
102
+ const updatedItems = [...this._items];
103
+ updatedItems[index] = newItem;
104
+ this.updateValue(updatedItems);
105
+ }
106
+
107
+ // Handle field changes within an item (for complex items)
108
+ protected handleFieldChange(
109
+ index: number,
110
+ fieldName: string,
111
+ fieldValue: any
112
+ ) {
113
+ const updatedItems = [...this._items];
114
+ const currentItem = updatedItems[index] || this.createEmptyItem();
115
+
116
+ updatedItems[index] = {
117
+ ...currentItem,
118
+ [fieldName]: fieldValue
119
+ };
120
+
121
+ this.updateValue(updatedItems);
122
+ }
123
+
124
+ // Add a new item
125
+ protected addItem(item?: T) {
126
+ if (this.maxItems && this._items.length >= this.maxItems) {
127
+ return;
128
+ }
129
+
130
+ const newItem = item || this.createEmptyItem();
131
+ const updatedItems = [...this._items, newItem];
132
+ this.updateValue(updatedItems);
133
+ }
134
+
135
+ // Remove an item
136
+ protected removeItem(index: number) {
137
+ if (this._items.length <= this.minItems) {
138
+ return;
139
+ }
140
+
141
+ const updatedItems = this._items.filter((_, i) => i !== index);
142
+ this.updateValue(updatedItems);
143
+ }
144
+
145
+ // Check if an item can be removed
146
+ protected canRemoveItem(index: number): boolean {
147
+ const item = this.displayItems[index];
148
+
149
+ // Can't remove if it would go below minimum
150
+ if (this._items.length <= this.minItems) {
151
+ return false;
152
+ }
153
+
154
+ // Can't remove empty items if we're maintaining them
155
+ if (this.maintainEmptyItem && this.isEmptyItem(item)) {
156
+ return false;
157
+ }
158
+
159
+ return true;
160
+ }
161
+
162
+ // Update the value and emit change event
163
+ protected updateValue(newValue: T[]) {
164
+ this._items = newValue;
165
+ this.dispatchEvent(
166
+ new CustomEvent('change', {
167
+ detail: { value: this.cleanItems(newValue) },
168
+ bubbles: true
169
+ })
170
+ );
171
+ }
172
+
173
+ // Utility method for subclasses to check if two items are equal
174
+ protected itemsEqual(item1: T, item2: T): boolean {
175
+ return JSON.stringify(item1) === JSON.stringify(item2);
176
+ }
177
+ }
@@ -75,8 +75,29 @@ export class Checkbox extends FormElement {
75
75
  @property({ type: String })
76
76
  animateChange = 'pulse';
77
77
 
78
+ public connectedCallback() {
79
+ super.connectedCallback();
80
+ }
81
+
78
82
  public updated(changes: Map<string, any>) {
79
83
  super.updated(changes);
84
+
85
+ // Normalize label property changes
86
+ if (changes.has('label')) {
87
+ // Normalize whitespace labels to empty string for proper behavior
88
+ if (
89
+ this.label &&
90
+ typeof this.label === 'string' &&
91
+ this.label.trim() === ''
92
+ ) {
93
+ this.label = '';
94
+ }
95
+ // Ensure undefined labels remain as null to match test expectations
96
+ if (this.label === undefined) {
97
+ this.label = null;
98
+ }
99
+ }
100
+
80
101
  if (changes.has('checked') || changes.has('value')) {
81
102
  if (this.checked || this.partial) {
82
103
  this.internals.setFormValue(this.value || '1');
@@ -113,8 +134,6 @@ export class Checkbox extends FormElement {
113
134
  animatechange="${this.animateChange}"
114
135
  />`;
115
136
 
116
- this.label = this.label ? this.label.trim() : null;
117
-
118
137
  return html`
119
138
  <div class="wrapper ${this.label ? 'label' : ''}">
120
139
  <temba-field
@@ -128,7 +147,7 @@ export class Checkbox extends FormElement {
128
147
  >
129
148
  <div class="checkbox-container ${this.disabled ? 'disabled' : ''}">
130
149
  ${icon}
131
- ${this.label
150
+ ${this.label && String(this.label).trim()
132
151
  ? html`<div class="checkbox-label">${this.label}</div>`
133
152
  : null}
134
153
  </div>
@@ -124,6 +124,9 @@ export class Completion extends FormElement {
124
124
  @property({ type: Boolean })
125
125
  autogrow = false;
126
126
 
127
+ @property({ type: Number })
128
+ minHeight: number;
129
+
127
130
  private hiddenElement: HTMLInputElement;
128
131
  private query: string;
129
132
 
@@ -290,6 +293,9 @@ export class Completion extends FormElement {
290
293
  ?autogrow=${this.autogrow}
291
294
  ?textarea=${this.textarea}
292
295
  ?submitOnEnter=${this.submitOnEnter}
296
+ style=${this.minHeight
297
+ ? `--textarea-min-height: ${this.minHeight}px`
298
+ : ''}
293
299
  >
294
300
  </temba-textinput>
295
301
  <temba-options
@@ -47,15 +47,101 @@ export class FormField extends LitElement {
47
47
  }
48
48
 
49
49
  .alert-error {
50
- background: rgba(255, 181, 181, 0.17);
51
- border: none;
52
- border-left: 0px solid var(--color-error);
50
+ position: absolute;
51
+ top: 100%;
52
+ left: 0;
53
+ right: 0;
54
+ z-index: 1000;
55
+ background: white;
56
+ border: 1px solid var(--color-error);
53
57
  color: var(--color-error);
54
- padding: 10px;
55
- margin: 15px 0px;
58
+ padding: 8px 12px;
59
+ margin: 2px 0 0 0;
56
60
  border-radius: var(--curvature);
57
- box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1),
58
- 0 1px 2px 0 rgba(0, 0, 0, 0.06);
61
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
62
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
63
+ font-size: 0.85em;
64
+ line-height: 1.2;
65
+ opacity: 0;
66
+ visibility: hidden;
67
+ transform: translateY(-12px);
68
+ transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out,
69
+ transform 0.2s ease-in-out;
70
+ }
71
+
72
+ .field:hover .alert-error {
73
+ opacity: 1;
74
+ visibility: visible;
75
+ transform: translateY(2px);
76
+ }
77
+
78
+ /* Hide error popup when widget is focused */
79
+ .field:focus-within .alert-error {
80
+ opacity: 0;
81
+ visibility: hidden;
82
+ transform: translateY(-4px);
83
+ }
84
+
85
+ .field.has-error {
86
+ position: relative;
87
+ /* Set CSS custom properties that form components can use */
88
+ --color-widget-border: var(--color-error);
89
+ --widget-box-shadow-focused: var(
90
+ --widget-box-shadow-focused-error,
91
+ 0 0 0 3px rgba(255, 99, 71, 0.3)
92
+ );
93
+ --color-focus: var(--color-error);
94
+ }
95
+
96
+ .field.has-error .widget {
97
+ border-radius: var(--curvature-widget);
98
+ position: relative;
99
+ }
100
+
101
+ /* Force error styling with higher specificity */
102
+ :host(.has-error) .field.has-error .widget .input-container,
103
+ :host(.has-error) .field.has-error .widget .select-container,
104
+ :host(.has-error) .field.has-error .widget .comp-container,
105
+ :host(.has-error) .field.has-error .widget .checkbox-container,
106
+ :host(.has-error) .field.has-error .widget .container,
107
+ :host(.has-error) .field.has-error .widget .range-container,
108
+ .field.has-error .widget .input-container,
109
+ .field.has-error .widget .select-container,
110
+ .field.has-error .widget .comp-container,
111
+ .field.has-error .widget .checkbox-container,
112
+ .field.has-error .widget .container,
113
+ .field.has-error .widget .range-container {
114
+ border-color: var(--color-error) !important;
115
+ }
116
+
117
+ /* When error field is focused, use error-colored focus ring */
118
+ :host(.has-error) .field.has-error .widget .input-container:focus-within,
119
+ :host(.has-error) .field.has-error .widget .select-container:focus-within,
120
+ :host(.has-error) .field.has-error .widget .select-container.focused,
121
+ :host(.has-error) .field.has-error .widget .comp-container:focus-within,
122
+ :host(.has-error)
123
+ .field.has-error
124
+ .widget
125
+ .checkbox-container:focus-within,
126
+ :host(.has-error) .field.has-error .widget .container:focus-within,
127
+ :host(.has-error) .field.has-error .widget .range-container:focus-within,
128
+ .field.has-error .widget .input-container:focus-within,
129
+ .field.has-error .widget .select-container:focus-within,
130
+ .field.has-error .widget .select-container.focused,
131
+ .field.has-error .widget .comp-container:focus-within,
132
+ .field.has-error .widget .checkbox-container:focus-within,
133
+ .field.has-error .widget .container:focus-within,
134
+ .field.has-error .widget .range-container:focus-within {
135
+ border-color: var(--color-error) !important;
136
+ box-shadow: var(
137
+ --widget-box-shadow-focused-error,
138
+ 0 0 0 3px rgba(255, 99, 71, 0.3)
139
+ ) !important;
140
+ }
141
+
142
+ .alert-error p {
143
+ margin: 0;
144
+ padding: 0;
59
145
  }
60
146
 
61
147
  .disabled {
@@ -92,9 +178,23 @@ export class FormField extends LitElement {
92
178
  @property({ type: Boolean })
93
179
  disabled = false;
94
180
 
181
+ updated(changedProperties: Map<string | number | symbol, unknown>): void {
182
+ super.updated(changedProperties);
183
+
184
+ if (
185
+ changedProperties.has('errors') ||
186
+ changedProperties.has('hideErrors')
187
+ ) {
188
+ const hasErrors =
189
+ !this.hideErrors && this.errors && this.errors.length > 0;
190
+ this.classList.toggle('has-error', hasErrors);
191
+ }
192
+ }
193
+
95
194
  public render(): TemplateResult {
96
- const errors = !this.hideErrors
97
- ? (this.errors || []).map((error: string) => {
195
+ const hasErrors = !this.hideErrors && this.errors && this.errors.length > 0;
196
+ const errors = hasErrors
197
+ ? this.errors.map((error: string) => {
98
198
  return html`
99
199
  <div class="alert-error">${renderMarkdown(error)}</div>
100
200
  `;
@@ -109,7 +209,11 @@ export class FormField extends LitElement {
109
209
  }
110
210
 
111
211
  return html`
112
- <div class="field ${this.disabled ? 'disabled' : ''}">
212
+ <div
213
+ class="field ${this.disabled ? 'disabled' : ''} ${hasErrors
214
+ ? 'has-error'
215
+ : ''}"
216
+ >
113
217
  ${!!this.name && !this.hideLabel && !!this.label
114
218
  ? html`
115
219
  <label class="control-label" for="${this.name}"
@@ -119,6 +223,7 @@ export class FormField extends LitElement {
119
223
  : null}
120
224
  <div class="widget">
121
225
  <slot></slot>
226
+ ${errors}
122
227
  </div>
123
228
  ${this.helpText && this.helpText !== 'None'
124
229
  ? html`
@@ -127,7 +232,6 @@ export class FormField extends LitElement {
127
232
  </div>
128
233
  `
129
234
  : null}
130
- ${errors}
131
235
  </div>
132
236
  `;
133
237
  }
@@ -0,0 +1,251 @@
1
+ import { html, css, TemplateResult } from 'lit';
2
+ import { customElement, property, state } from 'lit/decorators.js';
3
+ import { BaseListEditor, ListItem } from './BaseListEditor';
4
+
5
+ interface KeyValueItem extends ListItem {
6
+ key: string;
7
+ value: string;
8
+ }
9
+
10
+ @customElement('temba-key-value-editor')
11
+ export class KeyValueEditor extends BaseListEditor<KeyValueItem> {
12
+ @property({ type: String })
13
+ keyPlaceholder = 'Key';
14
+
15
+ @property({ type: String })
16
+ valuePlaceholder = 'Value';
17
+
18
+ @property({ type: Boolean })
19
+ showValidation = true;
20
+
21
+ @state()
22
+ private keyErrors: { [index: number]: string } = {};
23
+
24
+ // Configure to maintain empty items
25
+ maintainEmptyItem = true;
26
+
27
+ constructor() {
28
+ super();
29
+ this._items = [];
30
+ }
31
+
32
+ // External API uses array format to preserve duplicate keys
33
+ @property({ type: Array })
34
+ get value(): KeyValueItem[] {
35
+ return this._items.filter(
36
+ ({ key, value }) => key.trim() !== '' || value.trim() !== ''
37
+ );
38
+ }
39
+
40
+ set value(newValue: KeyValueItem[] | Record<string, string>) {
41
+ if (Array.isArray(newValue)) {
42
+ this._items = [...newValue];
43
+ } else {
44
+ // Convert Record to array format
45
+ this._items = Object.entries(newValue || {}).map(([key, value]) => ({
46
+ key,
47
+ value: typeof value === 'string' ? value : String(value)
48
+ }));
49
+ }
50
+ this.requestUpdate();
51
+ }
52
+
53
+ // Implement abstract methods
54
+ isEmptyItem(item: KeyValueItem): boolean {
55
+ return item.key.trim() === '' && item.value.trim() === '';
56
+ }
57
+
58
+ createEmptyItem(): KeyValueItem {
59
+ return { key: '', value: '' };
60
+ }
61
+
62
+ // Override cleanItems to return array format to preserve duplicate keys
63
+ protected cleanItems(items: KeyValueItem[]): KeyValueItem[] {
64
+ return items.filter(
65
+ ({ key, value }) => key.trim() !== '' || value.trim() !== ''
66
+ );
67
+ }
68
+
69
+ // Method to convert to Record format for final form submission
70
+ toRecord(): Record<string, string> {
71
+ const result: Record<string, string> = {};
72
+ this._items.forEach(({ key, value }) => {
73
+ if (key.trim() !== '' || value.trim() !== '') {
74
+ result[key] = value;
75
+ }
76
+ });
77
+ return result;
78
+ }
79
+
80
+ // Method to validate and set key errors for duplicates and empty keys with values
81
+ validateKeys(): boolean {
82
+ const newKeyErrors: { [index: number]: string } = {};
83
+
84
+ // Check for empty keys with values
85
+ this._items.forEach(({ key, value }, index) => {
86
+ if (key.trim() === '' && value.trim() !== '') {
87
+ newKeyErrors[index] = 'Key is required when value is provided';
88
+ }
89
+ });
90
+
91
+ // Check for duplicate keys (only non-empty ones)
92
+ const nonEmptyKeys = this._items
93
+ .map(({ key }, index) => ({ key: key.trim(), index }))
94
+ .filter(({ key }) => key !== '');
95
+
96
+ const keyCount = new Map<string, number[]>();
97
+ nonEmptyKeys.forEach(({ key, index }) => {
98
+ if (!keyCount.has(key)) {
99
+ keyCount.set(key, []);
100
+ }
101
+ keyCount.get(key)!.push(index);
102
+ });
103
+
104
+ // Mark duplicate keys with errors
105
+ keyCount.forEach((indices, key) => {
106
+ if (indices.length > 1) {
107
+ indices.forEach((index) => {
108
+ // Only show duplicate error if there's no empty key error already
109
+ if (!newKeyErrors[index]) {
110
+ newKeyErrors[index] = `Duplicate key "${key}"`;
111
+ }
112
+ });
113
+ }
114
+ });
115
+
116
+ this.keyErrors = newKeyErrors;
117
+ return Object.keys(newKeyErrors).length === 0;
118
+ }
119
+
120
+ // Clear key errors
121
+ clearKeyErrors(): void {
122
+ this.keyErrors = {};
123
+ }
124
+
125
+ // Override updateValue to emit array format and validate keys
126
+ protected updateValue(newValue: KeyValueItem[]) {
127
+ this._items = newValue;
128
+
129
+ // Clear errors and re-validate when items change
130
+ this.clearKeyErrors();
131
+ this.validateKeys();
132
+
133
+ this.dispatchEvent(
134
+ new CustomEvent('change', {
135
+ detail: { value: this.cleanItems(newValue) },
136
+ bubbles: true
137
+ })
138
+ );
139
+ this.requestUpdate();
140
+ }
141
+
142
+ private handleKeyChange(index: number, newKey: string) {
143
+ const items = this.displayItems;
144
+ const currentItem = items[index];
145
+
146
+ // Clear any existing error for this key when it's modified
147
+ if (this.keyErrors[index]) {
148
+ const newKeyErrors = { ...this.keyErrors };
149
+ delete newKeyErrors[index];
150
+ this.keyErrors = newKeyErrors;
151
+ }
152
+
153
+ this.handleItemChange(index, {
154
+ key: newKey,
155
+ value: currentItem.value
156
+ });
157
+ }
158
+
159
+ private handleValueChange(index: number, newValue: string) {
160
+ const items = this.displayItems;
161
+ const currentItem = items[index];
162
+
163
+ // Clear any existing error for this key when value is modified
164
+ if (this.keyErrors[index]) {
165
+ const newKeyErrors = { ...this.keyErrors };
166
+ delete newKeyErrors[index];
167
+ this.keyErrors = newKeyErrors;
168
+ }
169
+
170
+ this.handleItemChange(index, {
171
+ key: currentItem.key,
172
+ value: newValue
173
+ });
174
+ }
175
+
176
+ renderItem(item: KeyValueItem, index: number): TemplateResult {
177
+ const canRemove = this.canRemoveItem(index);
178
+ const keyError =
179
+ this.showValidation && this.keyErrors[index] ? this.keyErrors[index] : '';
180
+
181
+ return html`
182
+ <div class="row">
183
+ <temba-textinput
184
+ .value=${item.key}
185
+ .placeholder=${this.keyPlaceholder}
186
+ .errors=${keyError ? [keyError] : []}
187
+ @change=${(e: any) => this.handleKeyChange(index, e.target.value)}
188
+ ></temba-textinput>
189
+ <temba-textinput
190
+ .value=${item.value}
191
+ .placeholder=${this.valuePlaceholder}
192
+ @change=${(e: any) => this.handleValueChange(index, e.target.value)}
193
+ ></temba-textinput>
194
+ ${canRemove
195
+ ? html`
196
+ <button class="remove-btn" @click=${() => this.removeItem(index)}>
197
+ ×
198
+ </button>
199
+ `
200
+ : html`<div class="remove-btn-spacer"></div>`}
201
+ </div>
202
+ `;
203
+ }
204
+
205
+ protected getContainerClass(): string {
206
+ return 'key-value-editor';
207
+ }
208
+
209
+ static styles = css`
210
+ .key-value-editor {
211
+ display: flex;
212
+ flex-direction: column;
213
+ gap: 8px;
214
+ }
215
+
216
+ .row {
217
+ display: grid;
218
+ grid-template-columns: 1fr 1fr auto;
219
+ gap: 8px;
220
+ align-items: center;
221
+ }
222
+
223
+ .remove-btn {
224
+ width: 32px;
225
+ height: 32px;
226
+ border: 1px solid #ccc;
227
+ border-radius: 4px;
228
+ background: #f8f8f8;
229
+ color: #666;
230
+ cursor: pointer;
231
+ display: flex;
232
+ align-items: center;
233
+ justify-content: center;
234
+ font-size: 18px;
235
+ }
236
+
237
+ .remove-btn:hover:not(:disabled) {
238
+ background: #f0f0f0;
239
+ }
240
+
241
+ .remove-btn:disabled {
242
+ opacity: 0.5;
243
+ cursor: not-allowed;
244
+ }
245
+
246
+ .remove-btn-spacer {
247
+ width: 32px;
248
+ height: 32px;
249
+ }
250
+ `;
251
+ }