@nyaruka/temba-components 0.129.7 → 0.129.9

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 (269) hide show
  1. package/.devcontainer/Dockerfile +11 -4
  2. package/.devcontainer/devcontainer.json +3 -2
  3. package/.github/workflows/build.yml +4 -14
  4. package/CHANGELOG.md +29 -0
  5. package/demo/components/flow/example.html +1 -1
  6. package/demo/components/message-editor/example.html +125 -0
  7. package/demo/components/textinput/completion.html +1 -0
  8. package/demo/data/flows/food-order.json +12 -21
  9. package/demo/data/flows/sample-flow.json +210 -104
  10. package/dist/temba-components.js +715 -364
  11. package/dist/temba-components.js.map +1 -1
  12. package/out-tsc/src/display/Thumbnail.js +2 -1
  13. package/out-tsc/src/display/Thumbnail.js.map +1 -1
  14. package/out-tsc/src/events.js.map +1 -1
  15. package/out-tsc/src/excellent/helpers.js +2 -2
  16. package/out-tsc/src/excellent/helpers.js.map +1 -1
  17. package/out-tsc/src/flow/CanvasNode.js +25 -7
  18. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  19. package/out-tsc/src/flow/Editor.js +11 -1
  20. package/out-tsc/src/flow/Editor.js.map +1 -1
  21. package/out-tsc/src/flow/NodeEditor.js +342 -276
  22. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  23. package/out-tsc/src/flow/actions/add_input_labels.js +40 -0
  24. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  25. package/out-tsc/src/flow/actions/call_llm.js +56 -3
  26. package/out-tsc/src/flow/actions/call_llm.js.map +1 -1
  27. package/out-tsc/src/flow/actions/call_webhook.js +26 -17
  28. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
  29. package/out-tsc/src/flow/actions/open_ticket.js +65 -3
  30. package/out-tsc/src/flow/actions/open_ticket.js.map +1 -1
  31. package/out-tsc/src/flow/actions/send_msg.js +147 -6
  32. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  33. package/out-tsc/src/flow/actions/set_run_result.js +75 -0
  34. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  35. package/out-tsc/src/flow/config.js +4 -0
  36. package/out-tsc/src/flow/config.js.map +1 -1
  37. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +227 -0
  38. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -0
  39. package/out-tsc/src/flow/nodes/split_by_ticket.js +18 -0
  40. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -0
  41. package/out-tsc/src/flow/nodes/wait_for_response.js +27 -1
  42. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  43. package/out-tsc/src/flow/types.js +0 -65
  44. package/out-tsc/src/flow/types.js.map +1 -1
  45. package/out-tsc/src/form/ArrayEditor.js +87 -57
  46. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  47. package/out-tsc/src/form/BaseListEditor.js +19 -4
  48. package/out-tsc/src/form/BaseListEditor.js.map +1 -1
  49. package/out-tsc/src/form/FieldRenderer.js +305 -0
  50. package/out-tsc/src/form/FieldRenderer.js.map +1 -0
  51. package/out-tsc/src/form/FormField.js +4 -4
  52. package/out-tsc/src/form/FormField.js.map +1 -1
  53. package/out-tsc/src/form/KeyValueEditor.js +1 -1
  54. package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
  55. package/out-tsc/src/form/MediaPicker.js +13 -1
  56. package/out-tsc/src/form/MediaPicker.js.map +1 -1
  57. package/out-tsc/src/form/MessageEditor.js +422 -0
  58. package/out-tsc/src/form/MessageEditor.js.map +1 -0
  59. package/out-tsc/src/form/TextInput.js +13 -6
  60. package/out-tsc/src/form/TextInput.js.map +1 -1
  61. package/out-tsc/src/form/select/Select.js +52 -24
  62. package/out-tsc/src/form/select/Select.js.map +1 -1
  63. package/out-tsc/src/live/ContactChat.js +66 -15
  64. package/out-tsc/src/live/ContactChat.js.map +1 -1
  65. package/out-tsc/src/markdown.js +13 -11
  66. package/out-tsc/src/markdown.js.map +1 -1
  67. package/out-tsc/temba-modules.js +2 -0
  68. package/out-tsc/temba-modules.js.map +1 -1
  69. package/out-tsc/test/ActionHelper.js +2 -0
  70. package/out-tsc/test/ActionHelper.js.map +1 -1
  71. package/out-tsc/test/NodeHelper.js +148 -0
  72. package/out-tsc/test/NodeHelper.js.map +1 -0
  73. package/out-tsc/test/actions/call_llm.test.js +103 -0
  74. package/out-tsc/test/actions/call_llm.test.js.map +1 -0
  75. package/out-tsc/test/nodes/split_by_llm_categorize.test.js +532 -0
  76. package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -0
  77. package/out-tsc/test/nodes/split_by_random.test.js +150 -0
  78. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -0
  79. package/out-tsc/test/nodes/wait_for_digits.test.js +150 -0
  80. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -0
  81. package/out-tsc/test/nodes/wait_for_response.test.js +171 -0
  82. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -0
  83. package/out-tsc/test/temba-add-input-labels.test.js +70 -0
  84. package/out-tsc/test/temba-add-input-labels.test.js.map +1 -0
  85. package/out-tsc/test/temba-field-config.test.js +4 -2
  86. package/out-tsc/test/temba-field-config.test.js.map +1 -1
  87. package/out-tsc/test/temba-field-renderer.test.js +296 -0
  88. package/out-tsc/test/temba-field-renderer.test.js.map +1 -0
  89. package/out-tsc/test/temba-markdown.test.js +1 -1
  90. package/out-tsc/test/temba-markdown.test.js.map +1 -1
  91. package/out-tsc/test/temba-message-editor.test.js +194 -0
  92. package/out-tsc/test/temba-message-editor.test.js.map +1 -0
  93. package/out-tsc/test/temba-node-editor.test.js +471 -0
  94. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  95. package/out-tsc/test/temba-select.test.js +7 -4
  96. package/out-tsc/test/temba-select.test.js.map +1 -1
  97. package/out-tsc/test/temba-textinput.test.js +16 -0
  98. package/out-tsc/test/temba-textinput.test.js.map +1 -1
  99. package/out-tsc/test/temba-webchat.test.js +5 -1
  100. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  101. package/out-tsc/test/utils.test.js +2 -8
  102. package/out-tsc/test/utils.test.js.map +1 -1
  103. package/package.json +7 -4
  104. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  105. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  106. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  107. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  108. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  109. package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
  110. package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
  111. package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
  112. package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
  113. package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
  114. package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
  115. package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
  116. package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
  117. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  118. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  119. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  120. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  121. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  122. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  123. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  124. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  125. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  126. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  127. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  128. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  129. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  130. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  131. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  132. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  133. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  134. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  135. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  136. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  137. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  138. package/screenshots/truth/editor/router.png +0 -0
  139. package/screenshots/truth/editor/send_msg.png +0 -0
  140. package/screenshots/truth/editor/set_contact_language.png +0 -0
  141. package/screenshots/truth/editor/set_contact_name.png +0 -0
  142. package/screenshots/truth/editor/set_run_result.png +0 -0
  143. package/screenshots/truth/editor/wait.png +0 -0
  144. package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
  145. package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
  146. package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
  147. package/screenshots/truth/field-renderer/context-comparison.png +0 -0
  148. package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
  149. package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
  150. package/screenshots/truth/field-renderer/select-multi.png +0 -0
  151. package/screenshots/truth/field-renderer/select-no-label.png +0 -0
  152. package/screenshots/truth/field-renderer/select-with-label.png +0 -0
  153. package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
  154. package/screenshots/truth/field-renderer/text-no-label.png +0 -0
  155. package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
  156. package/screenshots/truth/field-renderer/text-with-label.png +0 -0
  157. package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
  158. package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
  159. package/screenshots/truth/formfield/markdown-errors.png +0 -0
  160. package/screenshots/truth/formfield/no-errors.png +0 -0
  161. package/screenshots/truth/formfield/plain-text-errors.png +0 -0
  162. package/screenshots/truth/message-editor/autogrow-initial-content.png +0 -0
  163. package/screenshots/truth/message-editor/default.png +0 -0
  164. package/screenshots/truth/message-editor/drag-highlight.png +0 -0
  165. package/screenshots/truth/message-editor/filtered-attachments.png +0 -0
  166. package/screenshots/truth/message-editor/with-completion.png +0 -0
  167. package/screenshots/truth/message-editor/with-properties.png +0 -0
  168. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  169. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  170. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  171. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  172. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  173. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  174. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  175. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  176. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  177. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  178. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  179. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  180. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  181. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  182. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  183. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  184. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  185. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  186. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  187. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  188. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  189. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  190. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  191. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  192. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  193. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  194. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  195. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  196. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  197. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  198. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  199. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  200. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  201. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  202. package/screenshots/truth/omnibox/selected.png +0 -0
  203. package/screenshots/truth/select/functions.png +0 -0
  204. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  205. package/screenshots/truth/select/search-enabled.png +0 -0
  206. package/screenshots/truth/textinput/autogrow-initial.png +0 -0
  207. package/screenshots/truth/textinput/input-form.png +0 -0
  208. package/src/display/Thumbnail.ts +2 -1
  209. package/src/events.ts +13 -1
  210. package/src/excellent/helpers.ts +2 -2
  211. package/src/flow/CanvasNode.ts +22 -1
  212. package/src/flow/Editor.ts +12 -1
  213. package/src/flow/NodeEditor.ts +412 -354
  214. package/src/flow/actions/add_input_labels.ts +45 -0
  215. package/src/flow/actions/call_llm.ts +57 -3
  216. package/src/flow/actions/call_webhook.ts +28 -18
  217. package/src/flow/actions/open_ticket.ts +74 -3
  218. package/src/flow/actions/send_msg.ts +170 -6
  219. package/src/flow/actions/set_run_result.ts +83 -0
  220. package/src/flow/config.ts +4 -0
  221. package/src/flow/nodes/split_by_llm_categorize.ts +277 -0
  222. package/src/flow/nodes/split_by_ticket.ts +19 -0
  223. package/src/flow/nodes/wait_for_response.ts +28 -1
  224. package/src/flow/types.ts +46 -128
  225. package/src/form/ArrayEditor.ts +96 -66
  226. package/src/form/BaseListEditor.ts +22 -6
  227. package/src/form/FieldRenderer.ts +465 -0
  228. package/src/form/FormField.ts +4 -4
  229. package/src/form/KeyValueEditor.ts +1 -1
  230. package/src/form/MediaPicker.ts +13 -1
  231. package/src/form/MessageEditor.ts +449 -0
  232. package/src/form/TextInput.ts +16 -8
  233. package/src/form/select/Select.ts +55 -24
  234. package/src/live/ContactChat.ts +69 -19
  235. package/src/markdown.ts +19 -11
  236. package/src/store/flow-definition.d.ts +5 -2
  237. package/static/api/labels.json +31 -0
  238. package/static/api/topics.json +24 -9
  239. package/static/api/users.json +35 -16
  240. package/static/css/temba-components.css +5 -3
  241. package/static/mr/docs/en-us/editor.json +2588 -0
  242. package/stress-test.js +143 -0
  243. package/temba-modules.ts +2 -0
  244. package/test/ActionHelper.ts +2 -0
  245. package/test/NodeHelper.ts +184 -0
  246. package/test/actions/call_llm.test.ts +137 -0
  247. package/test/nodes/README.md +78 -0
  248. package/test/nodes/split_by_llm_categorize.test.ts +698 -0
  249. package/test/nodes/split_by_random.test.ts +177 -0
  250. package/test/nodes/wait_for_digits.test.ts +176 -0
  251. package/test/nodes/wait_for_response.test.ts +206 -0
  252. package/test/temba-add-input-labels.test.ts +87 -0
  253. package/test/temba-field-config.test.ts +4 -2
  254. package/test/temba-field-renderer.test.ts +482 -0
  255. package/test/temba-markdown.test.ts +1 -1
  256. package/test/temba-message-editor.test.ts +300 -0
  257. package/test/temba-node-editor.test.ts +590 -0
  258. package/test/temba-select.test.ts +7 -7
  259. package/test/temba-textinput.test.ts +26 -0
  260. package/test/temba-webchat.test.ts +6 -1
  261. package/test/utils.test.ts +2 -13
  262. package/test-assets/contacts/history.json +19 -0
  263. package/test-assets/select/llms.json +18 -0
  264. package/test-assets/style.css +2 -0
  265. package/web-dev-mock.mjs +523 -0
  266. package/web-dev-server.config.mjs +74 -6
  267. package/web-test-runner.config.mjs +9 -4
  268. package/test/temba-flow-editor.test.ts.backup +0 -563
  269. package/test/temba-utils-index.test.ts.backup +0 -1737
@@ -0,0 +1,465 @@
1
+ import { html, TemplateResult } from 'lit';
2
+ import {
3
+ FieldConfig,
4
+ TextFieldConfig,
5
+ TextareaFieldConfig,
6
+ SelectFieldConfig,
7
+ CheckboxFieldConfig,
8
+ MessageEditorFieldConfig,
9
+ KeyValueFieldConfig,
10
+ ArrayFieldConfig
11
+ } from '../flow/types';
12
+
13
+ /**
14
+ * FieldRenderer provides a consistent way to render field configurations
15
+ * into web components across different contexts (NodeEditor, ArrayEditor, etc.)
16
+ */
17
+ export class FieldRenderer {
18
+ /**
19
+ * Renders a field based on its configuration
20
+ * @param fieldName - The name of the field
21
+ * @param config - The field configuration
22
+ * @param value - The current value of the field
23
+ * @param context - Additional context for rendering
24
+ * @returns A TemplateResult for the rendered field
25
+ */
26
+ static renderField(
27
+ fieldName: string,
28
+ config: FieldConfig,
29
+ value: any,
30
+ context: FieldRenderContext = {}
31
+ ): TemplateResult {
32
+ /*const {
33
+ errors = [],
34
+ onChange,
35
+ showLabel = true,
36
+ flavor,
37
+ extraClasses = '',
38
+ style = ''
39
+ } = context;*/
40
+ switch (config.type) {
41
+ case 'text':
42
+ return FieldRenderer.renderTextInput(fieldName, config, value, context);
43
+
44
+ case 'textarea':
45
+ return FieldRenderer.renderTextarea(
46
+ fieldName,
47
+ config as TextareaFieldConfig,
48
+ value,
49
+ context
50
+ );
51
+
52
+ case 'select':
53
+ return FieldRenderer.renderSelect(
54
+ fieldName,
55
+ config as SelectFieldConfig,
56
+ value,
57
+ context
58
+ );
59
+
60
+ case 'checkbox':
61
+ return FieldRenderer.renderCheckbox(
62
+ fieldName,
63
+ config as CheckboxFieldConfig,
64
+ value,
65
+ context
66
+ );
67
+
68
+ case 'key-value':
69
+ return FieldRenderer.renderKeyValue(
70
+ fieldName,
71
+ config as KeyValueFieldConfig,
72
+ value,
73
+ context
74
+ );
75
+
76
+ case 'array':
77
+ return FieldRenderer.renderArray(
78
+ fieldName,
79
+ config as ArrayFieldConfig,
80
+ value,
81
+ context
82
+ );
83
+
84
+ case 'message-editor':
85
+ return FieldRenderer.renderMessageEditor(
86
+ fieldName,
87
+ config as MessageEditorFieldConfig,
88
+ value,
89
+ context
90
+ );
91
+
92
+ default:
93
+ return html`<div>Unsupported field type: ${(config as any).type}</div>`;
94
+ }
95
+ }
96
+
97
+ private static renderTextInput(
98
+ fieldName: string,
99
+ config: TextFieldConfig,
100
+ value: any,
101
+ context: FieldRenderContext
102
+ ): TemplateResult {
103
+ const {
104
+ errors = [],
105
+ onChange,
106
+ showLabel = true,
107
+ extraClasses,
108
+ style
109
+ } = context;
110
+
111
+ // If field supports expression evaluation, use temba-completion
112
+ if (config.evaluated) {
113
+ return html`<temba-completion
114
+ name="${fieldName}"
115
+ label="${showLabel ? config.label : ''}"
116
+ ?required="${config.required}"
117
+ .errors="${errors}"
118
+ .value="${value || ''}"
119
+ placeholder="${config.placeholder || ''}"
120
+ expressions="session"
121
+ .helpText="${config.helpText || ''}"
122
+ class="${extraClasses}"
123
+ style="${style}"
124
+ @input="${onChange || (() => {})}"
125
+ ></temba-completion>`;
126
+ }
127
+
128
+ return html`<temba-textinput
129
+ name="${fieldName}"
130
+ label="${showLabel ? config.label : ''}"
131
+ ?required="${config.required}"
132
+ .errors="${errors}"
133
+ .value="${value || ''}"
134
+ placeholder="${config.placeholder || ''}"
135
+ .helpText="${config.helpText || ''}"
136
+ class="${extraClasses}"
137
+ style="${style}"
138
+ @input="${onChange || (() => {})}"
139
+ ></temba-textinput>`;
140
+ }
141
+
142
+ private static renderTextarea(
143
+ fieldName: string,
144
+ config: TextareaFieldConfig,
145
+ value: any,
146
+ context: FieldRenderContext
147
+ ): TemplateResult {
148
+ const {
149
+ errors = [],
150
+ onChange,
151
+ showLabel = true,
152
+ extraClasses,
153
+ style
154
+ } = context;
155
+
156
+ const minHeightStyle = config.minHeight
157
+ ? `--textarea-min-height: ${config.minHeight}px;`
158
+ : '';
159
+ const combinedStyle = `${minHeightStyle}${style}`;
160
+
161
+ // If field supports expression evaluation, use temba-completion
162
+ if (config.evaluated) {
163
+ return html`<temba-completion
164
+ name="${fieldName}"
165
+ label="${showLabel ? config.label : ''}"
166
+ ?required="${config.required}"
167
+ .errors="${errors}"
168
+ .value="${value || ''}"
169
+ placeholder="${config.placeholder || ''}"
170
+ textarea
171
+ expressions="session"
172
+ .helpText="${config.helpText || ''}"
173
+ class="${extraClasses}"
174
+ style="${combinedStyle}"
175
+ @input="${onChange || (() => {})}"
176
+ ></temba-completion>`;
177
+ }
178
+
179
+ return html`<temba-textinput
180
+ name="${fieldName}"
181
+ label="${showLabel ? config.label : ''}"
182
+ ?required="${config.required}"
183
+ .errors="${errors}"
184
+ .value="${value || ''}"
185
+ placeholder="${config.placeholder || ''}"
186
+ textarea
187
+ .rows="${config.rows || 3}"
188
+ .helpText="${config.helpText || ''}"
189
+ class="${extraClasses}"
190
+ style="${combinedStyle}"
191
+ @input="${onChange || (() => {})}"
192
+ ></temba-textinput>`;
193
+ }
194
+
195
+ private static renderSelect(
196
+ fieldName: string,
197
+ config: SelectFieldConfig,
198
+ value: any,
199
+ context: FieldRenderContext
200
+ ): TemplateResult {
201
+ const {
202
+ errors = [],
203
+ onChange,
204
+ showLabel = true,
205
+ flavor,
206
+ extraClasses,
207
+ style
208
+ } = context;
209
+
210
+ // Ensure proper value handling for multi vs single select
211
+ const normalizedValue = (() => {
212
+ if (config.multi) {
213
+ // Multi-select: ensure we have an array and convert strings to option objects
214
+ const valueArray = Array.isArray(value) ? value : value ? [value] : [];
215
+ return valueArray.map((val) => {
216
+ if (typeof val === 'string') {
217
+ // Convert string values to option objects
218
+ return { name: val, value: val };
219
+ }
220
+ return val;
221
+ });
222
+ } else {
223
+ // Single select: use the value as-is
224
+ return value || '';
225
+ }
226
+ })();
227
+
228
+ if (typeof normalizedValue === 'string') {
229
+ return html`<temba-select
230
+ name="${fieldName}"
231
+ ?required="${config.required}"
232
+ .errors="${errors}"
233
+ value="${config.multi ? '' : normalizedValue}"
234
+ .values="${config.multi ? normalizedValue : undefined}"
235
+ ?multi="${config.multi}"
236
+ ?searchable="${config.searchable}"
237
+ ?tags="${config.tags}"
238
+ ?emails="${config.emails}"
239
+ ?clearable="${config.clearable || false}"
240
+ label="${showLabel ? config.label : ''}"
241
+ placeholder="${config.placeholder || ''}"
242
+ maxItems="${config.maxItems || 0}"
243
+ valueKey="${config.valueKey || 'value'}"
244
+ nameKey="${config.nameKey || 'name'}"
245
+ endpoint="${config.endpoint || ''}"
246
+ .helpText="${config.helpText || ''}"
247
+ flavor="${flavor || config.flavor || 'small'}"
248
+ class="${extraClasses}"
249
+ style="${style}"
250
+ .getName=${config.getName}
251
+ .createArbitraryOption=${config.createArbitraryOption}
252
+ ?allowCreate="${config.allowCreate || false}"
253
+ @change="${onChange || (() => {})}"
254
+ >
255
+ ${config.options?.map((option: any) => {
256
+ if (typeof option === 'string') {
257
+ return html`<temba-option
258
+ name="${option}"
259
+ value="${option}"
260
+ ></temba-option>`;
261
+ } else {
262
+ return html`<temba-option
263
+ name="${option.label || option.name}"
264
+ value="${option.value}"
265
+ ></temba-option>`;
266
+ }
267
+ })}
268
+ </temba-select>`;
269
+ }
270
+
271
+ return html`<temba-select
272
+ name="${fieldName}"
273
+ label="${showLabel ? config.label : ''}"
274
+ ?required="${config.required}"
275
+ .errors="${errors}"
276
+ .values="${normalizedValue}"
277
+ ?multi="${config.multi}"
278
+ ?searchable="${config.searchable}"
279
+ ?tags="${config.tags}"
280
+ ?emails="${config.emails}"
281
+ ?clearable="${config.clearable || false}"
282
+ placeholder="${config.placeholder || ''}"
283
+ maxItems="${config.maxItems || 0}"
284
+ valueKey="${config.valueKey || 'value'}"
285
+ nameKey="${config.nameKey || 'name'}"
286
+ endpoint="${config.endpoint || ''}"
287
+ .helpText="${config.helpText || ''}"
288
+ flavor="${flavor || config.flavor || 'small'}"
289
+ class="${extraClasses}"
290
+ style="${style}"
291
+ .getName=${config.getName}
292
+ .createArbitraryOption=${config.createArbitraryOption}
293
+ ?allowCreate="${config.allowCreate || false}"
294
+ @change="${onChange || (() => {})}"
295
+ >
296
+ ${config.options?.map((option: any) => {
297
+ if (typeof option === 'string') {
298
+ return html`<temba-option
299
+ name="${option}"
300
+ value="${option}"
301
+ ></temba-option>`;
302
+ } else {
303
+ return html`<temba-option
304
+ name="${option.label || option.name}"
305
+ value="${option.value}"
306
+ ></temba-option>`;
307
+ }
308
+ })}
309
+ </temba-select>`;
310
+ }
311
+
312
+ private static renderCheckbox(
313
+ fieldName: string,
314
+ config: CheckboxFieldConfig,
315
+ value: any,
316
+ context: FieldRenderContext
317
+ ): TemplateResult {
318
+ const { errors = [], onChange, extraClasses, style } = context;
319
+
320
+ return html`<div class="form-field">
321
+ <temba-checkbox
322
+ name="${fieldName}"
323
+ label="${config.label}"
324
+ .helpText="${config.helpText || ''}"
325
+ ?required="${config.required}"
326
+ .errors="${errors}"
327
+ ?checked="${value || false}"
328
+ size="${config.size || 1.2}"
329
+ animateChange="${config.animateChange || 'pulse'}"
330
+ class="${extraClasses}"
331
+ style="${style}"
332
+ @change="${onChange || (() => {})}"
333
+ ></temba-checkbox>
334
+ ${errors.length
335
+ ? html`<div class="field-errors">${errors.join(', ')}</div>`
336
+ : ''}
337
+ </div>`;
338
+ }
339
+
340
+ private static renderKeyValue(
341
+ fieldName: string,
342
+ config: KeyValueFieldConfig,
343
+ value: any,
344
+ context: FieldRenderContext
345
+ ): TemplateResult {
346
+ const {
347
+ errors = [],
348
+ onChange,
349
+ showLabel = true,
350
+ extraClasses,
351
+ style
352
+ } = context;
353
+
354
+ return html`<div class="form-field">
355
+ ${showLabel ? html`<label>${config.label}</label>` : ''}
356
+ <temba-key-value-editor
357
+ name="${fieldName}"
358
+ .value="${value || []}"
359
+ .sortable="${config.sortable}"
360
+ .keyPlaceholder="${config.keyPlaceholder || 'Key'}"
361
+ .valuePlaceholder="${config.valuePlaceholder || 'Value'}"
362
+ .minRows="${config.minRows || 0}"
363
+ class="${extraClasses}"
364
+ style="${style}"
365
+ @change="${onChange || (() => {})}"
366
+ ></temba-key-value-editor>
367
+ ${errors.length
368
+ ? html`<div class="field-errors">${errors.join(', ')}</div>`
369
+ : ''}
370
+ </div>`;
371
+ }
372
+
373
+ private static renderArray(
374
+ fieldName: string,
375
+ config: ArrayFieldConfig,
376
+ value: any,
377
+ context: FieldRenderContext
378
+ ): TemplateResult {
379
+ const {
380
+ errors = [],
381
+ onChange,
382
+ showLabel = true,
383
+ extraClasses,
384
+ style
385
+ } = context;
386
+
387
+ return html`<div class="form-field">
388
+ ${showLabel ? html`<label>${config.label}</label>` : ''}
389
+ <temba-array-editor
390
+ .value="${value || []}"
391
+ .itemConfig="${config.itemConfig}"
392
+ .sortable="${config.sortable}"
393
+ .itemLabel="${config.itemLabel || 'Item'}"
394
+ .minItems="${config.minItems || 0}"
395
+ .maxItems="${config.maxItems || 0}"
396
+ .onItemChange="${config.onItemChange}"
397
+ .isEmptyItemFn="${config.isEmptyItem}"
398
+ class="${extraClasses}"
399
+ style="${style}"
400
+ @change="${onChange || (() => {})}"
401
+ ></temba-array-editor>
402
+ ${errors.length
403
+ ? html`<div class="field-errors">${errors.join(', ')}</div>`
404
+ : ''}
405
+ </div>`;
406
+ }
407
+
408
+ private static renderMessageEditor(
409
+ fieldName: string,
410
+ config: MessageEditorFieldConfig,
411
+ value: any,
412
+ context: FieldRenderContext
413
+ ): TemplateResult {
414
+ const {
415
+ errors = [],
416
+ onChange,
417
+ showLabel = true,
418
+ extraClasses,
419
+ style,
420
+ additionalData = {}
421
+ } = context;
422
+
423
+ return html`<temba-message-editor
424
+ name="${fieldName}"
425
+ label="${showLabel ? config.label : ''}"
426
+ ?required="${config.required}"
427
+ .errors="${errors}"
428
+ .value="${value || ''}"
429
+ .attachments="${additionalData.attachments || []}"
430
+ placeholder="${config.placeholder || ''}"
431
+ .helpText="${config.helpText || ''}"
432
+ ?autogrow="${config.autogrow}"
433
+ ?gsm="${config.gsm}"
434
+ ?disableCompletion="${config.disableCompletion}"
435
+ counter="${config.counter || ''}"
436
+ accept="${config.accept || ''}"
437
+ endpoint="${config.endpoint || ''}"
438
+ max-attachments="${config.maxAttachments || 3}"
439
+ minHeight="${config.minHeight || 60}"
440
+ class="${extraClasses}"
441
+ style="${style}"
442
+ @change="${onChange || (() => {})}"
443
+ ></temba-message-editor>`;
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Context object for field rendering that provides additional options
449
+ */
450
+ export interface FieldRenderContext {
451
+ /** Array of error messages for the field */
452
+ errors?: string[];
453
+ /** Change event handler */
454
+ onChange?: (event: Event) => void;
455
+ /** Whether to show the field label */
456
+ showLabel?: boolean;
457
+ /** Flavor for components that support it (like temba-select) */
458
+ flavor?: string;
459
+ /** Additional CSS classes to apply */
460
+ extraClasses?: string;
461
+ /** Additional CSS styles to apply */
462
+ style?: string;
463
+ /** Additional data needed for specific field types */
464
+ additionalData?: Record<string, any>;
465
+ }
@@ -1,6 +1,6 @@
1
1
  import { TemplateResult, html, css, LitElement } from 'lit';
2
2
  import { property } from 'lit/decorators.js';
3
- import { renderMarkdown } from '../markdown';
3
+ import { renderMarkdownInline } from '../markdown';
4
4
 
5
5
  /**
6
6
  * A small wrapper to display labels and help text in a smartmin style.
@@ -21,7 +21,7 @@ export class FormField extends LitElement {
21
21
  font-size: var(--label-size);
22
22
  letter-spacing: 0.05em;
23
23
  line-height: normal;
24
- color: #777;
24
+ color: var(--color-label, #777);
25
25
  }
26
26
 
27
27
  .help-text {
@@ -196,7 +196,7 @@ export class FormField extends LitElement {
196
196
  const errors = hasErrors
197
197
  ? this.errors.map((error: string) => {
198
198
  return html`
199
- <div class="alert-error">${renderMarkdown(error)}</div>
199
+ <div class="alert-error">${renderMarkdownInline(error)}</div>
200
200
  `;
201
201
  })
202
202
  : [];
@@ -228,7 +228,7 @@ export class FormField extends LitElement {
228
228
  ${this.helpText && this.helpText !== 'None'
229
229
  ? html`
230
230
  <div class="help-text ${this.helpAlways ? 'help-always' : null}">
231
- ${this.helpText}
231
+ ${renderMarkdownInline(this.helpText)}
232
232
  </div>
233
233
  `
234
234
  : null}
@@ -216,8 +216,8 @@ export class KeyValueEditor extends BaseListEditor<KeyValueItem> {
216
216
  .row {
217
217
  display: grid;
218
218
  grid-template-columns: 1fr 1fr auto;
219
- gap: 8px;
220
219
  align-items: center;
220
+ column-gap: 6px;
221
221
  }
222
222
 
223
223
  .remove-btn {
@@ -29,7 +29,6 @@ export class MediaPicker extends RapidElement {
29
29
  }
30
30
 
31
31
  .highlight .drop-mask {
32
- background: rgba(210, 243, 184, 0.8);
33
32
  }
34
33
 
35
34
  .drop-mask > div {
@@ -126,6 +125,9 @@ export class MediaPicker extends RapidElement {
126
125
  @property({ type: Boolean })
127
126
  pendingDrop: boolean;
128
127
 
128
+ @property({ type: Boolean })
129
+ ignoreDrops = false;
130
+
129
131
  @property({ type: String })
130
132
  icon = Icon.add;
131
133
 
@@ -180,6 +182,9 @@ export class MediaPicker extends RapidElement {
180
182
  }
181
183
 
182
184
  private handleDrop(evt: DragEvent): void {
185
+ if (this.ignoreDrops) {
186
+ return;
187
+ }
183
188
  this.unhighlight(evt);
184
189
  if (this.canAcceptAttachments()) {
185
190
  this.uploadFiles(this.getAcceptableFiles(evt));
@@ -191,6 +196,10 @@ export class MediaPicker extends RapidElement {
191
196
  }
192
197
 
193
198
  private highlight(evt: DragEvent): void {
199
+ if (this.ignoreDrops) {
200
+ return;
201
+ }
202
+
194
203
  evt.preventDefault();
195
204
  evt.stopPropagation();
196
205
  if (this.canAcceptAttachments()) {
@@ -199,6 +208,9 @@ export class MediaPicker extends RapidElement {
199
208
  }
200
209
 
201
210
  private unhighlight(evt: DragEvent): void {
211
+ if (this.ignoreDrops) {
212
+ return;
213
+ }
202
214
  evt.preventDefault();
203
215
  evt.stopPropagation();
204
216
  this.pendingDrop = false;