@nyaruka/temba-components 0.129.8 → 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 (203) hide show
  1. package/CHANGELOG.md +27 -3
  2. package/demo/data/flows/sample-flow.json +186 -96
  3. package/dist/temba-components.js +414 -351
  4. package/dist/temba-components.js.map +1 -1
  5. package/out-tsc/src/events.js.map +1 -1
  6. package/out-tsc/src/excellent/helpers.js +2 -2
  7. package/out-tsc/src/excellent/helpers.js.map +1 -1
  8. package/out-tsc/src/flow/CanvasNode.js +25 -7
  9. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  10. package/out-tsc/src/flow/Editor.js +11 -1
  11. package/out-tsc/src/flow/Editor.js.map +1 -1
  12. package/out-tsc/src/flow/NodeEditor.js +133 -290
  13. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  14. package/out-tsc/src/flow/actions/add_input_labels.js +40 -0
  15. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  16. package/out-tsc/src/flow/actions/call_llm.js +56 -3
  17. package/out-tsc/src/flow/actions/call_llm.js.map +1 -1
  18. package/out-tsc/src/flow/actions/call_webhook.js +1 -1
  19. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
  20. package/out-tsc/src/flow/actions/open_ticket.js +65 -3
  21. package/out-tsc/src/flow/actions/open_ticket.js.map +1 -1
  22. package/out-tsc/src/flow/actions/set_run_result.js +75 -0
  23. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  24. package/out-tsc/src/flow/config.js +4 -0
  25. package/out-tsc/src/flow/config.js.map +1 -1
  26. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +227 -0
  27. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -0
  28. package/out-tsc/src/flow/nodes/split_by_ticket.js +18 -0
  29. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -0
  30. package/out-tsc/src/flow/nodes/wait_for_response.js +27 -1
  31. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  32. package/out-tsc/src/flow/types.js +0 -65
  33. package/out-tsc/src/flow/types.js.map +1 -1
  34. package/out-tsc/src/form/ArrayEditor.js +18 -61
  35. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  36. package/out-tsc/src/form/FieldRenderer.js +305 -0
  37. package/out-tsc/src/form/FieldRenderer.js.map +1 -0
  38. package/out-tsc/src/form/FormField.js +3 -3
  39. package/out-tsc/src/form/FormField.js.map +1 -1
  40. package/out-tsc/src/form/TextInput.js +1 -1
  41. package/out-tsc/src/form/TextInput.js.map +1 -1
  42. package/out-tsc/src/form/select/Select.js +48 -20
  43. package/out-tsc/src/form/select/Select.js.map +1 -1
  44. package/out-tsc/src/live/ContactChat.js +39 -13
  45. package/out-tsc/src/live/ContactChat.js.map +1 -1
  46. package/out-tsc/src/markdown.js +13 -11
  47. package/out-tsc/src/markdown.js.map +1 -1
  48. package/out-tsc/test/ActionHelper.js +2 -0
  49. package/out-tsc/test/ActionHelper.js.map +1 -1
  50. package/out-tsc/test/NodeHelper.js +148 -0
  51. package/out-tsc/test/NodeHelper.js.map +1 -0
  52. package/out-tsc/test/actions/call_llm.test.js +103 -0
  53. package/out-tsc/test/actions/call_llm.test.js.map +1 -0
  54. package/out-tsc/test/nodes/split_by_llm_categorize.test.js +532 -0
  55. package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -0
  56. package/out-tsc/test/nodes/split_by_random.test.js +150 -0
  57. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -0
  58. package/out-tsc/test/nodes/wait_for_digits.test.js +150 -0
  59. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -0
  60. package/out-tsc/test/nodes/wait_for_response.test.js +171 -0
  61. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -0
  62. package/out-tsc/test/temba-add-input-labels.test.js +70 -0
  63. package/out-tsc/test/temba-add-input-labels.test.js.map +1 -0
  64. package/out-tsc/test/temba-field-renderer.test.js +296 -0
  65. package/out-tsc/test/temba-field-renderer.test.js.map +1 -0
  66. package/out-tsc/test/temba-markdown.test.js +1 -1
  67. package/out-tsc/test/temba-markdown.test.js.map +1 -1
  68. package/out-tsc/test/temba-node-editor.test.js +400 -0
  69. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  70. package/out-tsc/test/temba-select.test.js +6 -3
  71. package/out-tsc/test/temba-select.test.js.map +1 -1
  72. package/out-tsc/test/temba-webchat.test.js +1 -1
  73. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  74. package/package.json +1 -1
  75. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  76. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  77. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  78. package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
  79. package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
  80. package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
  81. package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
  82. package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
  83. package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
  84. package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
  85. package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
  86. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  87. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  88. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  89. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  90. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  91. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  92. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  93. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  94. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  95. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  96. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  97. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  98. package/screenshots/truth/editor/router.png +0 -0
  99. package/screenshots/truth/editor/send_msg.png +0 -0
  100. package/screenshots/truth/editor/set_contact_language.png +0 -0
  101. package/screenshots/truth/editor/set_contact_name.png +0 -0
  102. package/screenshots/truth/editor/set_run_result.png +0 -0
  103. package/screenshots/truth/editor/wait.png +0 -0
  104. package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
  105. package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
  106. package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
  107. package/screenshots/truth/field-renderer/context-comparison.png +0 -0
  108. package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
  109. package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
  110. package/screenshots/truth/field-renderer/select-multi.png +0 -0
  111. package/screenshots/truth/field-renderer/select-no-label.png +0 -0
  112. package/screenshots/truth/field-renderer/select-with-label.png +0 -0
  113. package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
  114. package/screenshots/truth/field-renderer/text-no-label.png +0 -0
  115. package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
  116. package/screenshots/truth/field-renderer/text-with-label.png +0 -0
  117. package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
  118. package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
  119. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  120. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  121. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  122. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  123. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  124. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  125. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  126. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  127. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  128. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  129. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  130. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  131. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  132. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  133. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  134. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  135. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  136. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  137. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  138. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  139. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  140. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  141. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  142. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  143. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  144. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  145. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  146. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  147. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  148. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  149. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  150. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  151. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  152. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  153. package/screenshots/truth/omnibox/selected.png +0 -0
  154. package/screenshots/truth/select/functions.png +0 -0
  155. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  156. package/screenshots/truth/select/search-enabled.png +0 -0
  157. package/src/events.ts +8 -1
  158. package/src/excellent/helpers.ts +2 -2
  159. package/src/flow/CanvasNode.ts +22 -1
  160. package/src/flow/Editor.ts +12 -1
  161. package/src/flow/NodeEditor.ts +186 -374
  162. package/src/flow/actions/add_input_labels.ts +45 -0
  163. package/src/flow/actions/call_llm.ts +57 -3
  164. package/src/flow/actions/call_webhook.ts +1 -1
  165. package/src/flow/actions/open_ticket.ts +74 -3
  166. package/src/flow/actions/set_run_result.ts +83 -0
  167. package/src/flow/config.ts +4 -0
  168. package/src/flow/nodes/split_by_llm_categorize.ts +277 -0
  169. package/src/flow/nodes/split_by_ticket.ts +19 -0
  170. package/src/flow/nodes/wait_for_response.ts +28 -1
  171. package/src/flow/types.ts +26 -127
  172. package/src/form/ArrayEditor.ts +34 -82
  173. package/src/form/FieldRenderer.ts +465 -0
  174. package/src/form/FormField.ts +3 -3
  175. package/src/form/TextInput.ts +1 -1
  176. package/src/form/select/Select.ts +51 -20
  177. package/src/live/ContactChat.ts +39 -15
  178. package/src/markdown.ts +19 -11
  179. package/src/store/flow-definition.d.ts +5 -2
  180. package/static/api/labels.json +31 -0
  181. package/static/api/topics.json +24 -9
  182. package/static/api/users.json +35 -16
  183. package/static/css/temba-components.css +3 -3
  184. package/stress-test.js +18 -13
  185. package/test/ActionHelper.ts +2 -0
  186. package/test/NodeHelper.ts +184 -0
  187. package/test/actions/call_llm.test.ts +137 -0
  188. package/test/nodes/README.md +78 -0
  189. package/test/nodes/split_by_llm_categorize.test.ts +698 -0
  190. package/test/nodes/split_by_random.test.ts +177 -0
  191. package/test/nodes/wait_for_digits.test.ts +176 -0
  192. package/test/nodes/wait_for_response.test.ts +206 -0
  193. package/test/temba-add-input-labels.test.ts +87 -0
  194. package/test/temba-field-renderer.test.ts +482 -0
  195. package/test/temba-markdown.test.ts +1 -1
  196. package/test/temba-node-editor.test.ts +496 -0
  197. package/test/temba-select.test.ts +6 -6
  198. package/test/temba-webchat.test.ts +1 -1
  199. package/test-assets/select/llms.json +18 -0
  200. package/web-dev-mock.mjs +96 -6
  201. package/web-dev-server.config.mjs +29 -7
  202. package/test/temba-flow-editor.test.ts.backup +0 -563
  203. 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.
@@ -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}
@@ -193,7 +193,7 @@ export class TextInput extends FormElement {
193
193
 
194
194
  this.inputElement = this.shadowRoot.querySelector('.textinput');
195
195
 
196
- if (changes.has('counter')) {
196
+ if (changes.has('counter') && this.counter && this.counter.trim()) {
197
197
  let root = this.getParentModax() as any;
198
198
  if (root) {
199
199
  root = root.shadowRoot;
@@ -222,6 +222,7 @@ export class Select<T extends SelectOption> extends FormElement {
222
222
 
223
223
  .multi temba-sortable-list {
224
224
  margin: 0 !important;
225
+ flex-grow: 1;
225
226
  }
226
227
 
227
228
  input {
@@ -255,6 +256,12 @@ export class Select<T extends SelectOption> extends FormElement {
255
256
 
256
257
  .multi .input-wrapper {
257
258
  margin-left: 2px !important;
259
+ margin-right: 2px !important;
260
+ margin-top: 2px;
261
+ margin-bottom: 2px;
262
+ flex-shrink: 0;
263
+ min-width: 100px;
264
+ align-self: center;
258
265
  }
259
266
 
260
267
  .input-wrapper:focus-within .placeholder {
@@ -290,11 +297,6 @@ export class Select<T extends SelectOption> extends FormElement {
290
297
  box-shadow: none !important;
291
298
  }
292
299
 
293
- .multi .input-wrapper {
294
- flex-shrink: 0;
295
- min-width: 100px;
296
- }
297
-
298
300
  .input-wrapper .searchbox {
299
301
  }
300
302
 
@@ -310,6 +312,17 @@ export class Select<T extends SelectOption> extends FormElement {
310
312
  margin-left: 6px;
311
313
  }
312
314
 
315
+ .empty .placeholder {
316
+ display: block;
317
+ }
318
+
319
+ .multi .placeholder {
320
+ display: block;
321
+ margin: 2px 2px;
322
+ padding: 2px 8px;
323
+ align-self: center;
324
+ }
325
+
313
326
  .footer {
314
327
  padding: 5px 10px;
315
328
  background: var(--color-primary-light);
@@ -322,7 +335,7 @@ export class Select<T extends SelectOption> extends FormElement {
322
335
  .small {
323
336
  --temba-select-selected-padding: 6px;
324
337
  --temba-select-selected-line-height: 12px;
325
- --temba-select-selected-font-size: 12px;
338
+ --temba-select-selected-font-size: 14px;
326
339
  --temba-select-min-height: 2.28em;
327
340
  }
328
341
 
@@ -476,15 +489,26 @@ export class Select<T extends SelectOption> extends FormElement {
476
489
  @property({ type: String, attribute: 'info_text' })
477
490
  infoText = '';
478
491
 
492
+ // Override the setter to ensure values is always an array
479
493
  @property({ type: Array })
480
- values: T[] = [];
494
+ set values(newValues: any) {
495
+ this._values = Array.isArray(newValues) ? newValues : [];
496
+ this.requestUpdate('values');
497
+ }
498
+
499
+ get values(): T[] {
500
+ return this._values || [];
501
+ }
502
+
503
+ private _values: T[] = [];
481
504
 
482
505
  @property({ type: Object })
483
506
  selection: any;
484
507
 
485
508
  @property({ attribute: false })
486
- getName: (option: any) => string = (option: any) =>
487
- option[this.nameKey || 'name'];
509
+ getName: (option: any) => string = (option: any) => {
510
+ return option[this.nameKey || 'name'];
511
+ };
488
512
 
489
513
  @property({ attribute: false })
490
514
  isMatch: (option: any, q: string) => boolean = this.isMatchDefault;
@@ -545,7 +569,7 @@ export class Select<T extends SelectOption> extends FormElement {
545
569
  private alphaSort = (a: any, b: any) => {
546
570
  // by default, all endpoint values are sorted by name
547
571
  if (this.endpoint) {
548
- return this.getName(a).localeCompare(this.getName(b));
572
+ return this.getNameInternal(a).localeCompare(this.getNameInternal(b));
549
573
  }
550
574
  return 0;
551
575
  };
@@ -573,7 +597,7 @@ export class Select<T extends SelectOption> extends FormElement {
573
597
  }
574
598
 
575
599
  public isMatchDefault(option: T, q: string) {
576
- const name = this.getName(option) || '';
600
+ const name = this.getNameInternal(option) || '';
577
601
  return name.toLowerCase().indexOf(q) > -1;
578
602
  }
579
603
 
@@ -963,7 +987,9 @@ export class Select<T extends SelectOption> extends FormElement {
963
987
  }
964
988
 
965
989
  protected getNameInternal: (option: T) => string = (option: T) => {
966
- return this.getName(option);
990
+ return this.getName
991
+ ? this.getName(option)
992
+ : option[this.nameKey || 'name'] || '';
967
993
  };
968
994
 
969
995
  private getOptionsDefault(response: WebResponse): any[] {
@@ -1493,7 +1519,7 @@ export class Select<T extends SelectOption> extends FormElement {
1493
1519
  name="${icon}"
1494
1520
  style="margin-right:0.5em;"
1495
1521
  ></temba-icon>`
1496
- : null}<span>${this.getName(option)}</span>
1522
+ : null}<span>${this.getNameInternal(option)}</span>
1497
1523
  </div>
1498
1524
  `;
1499
1525
  }
@@ -1636,9 +1662,13 @@ export class Select<T extends SelectOption> extends FormElement {
1636
1662
 
1637
1663
  public render(): TemplateResult {
1638
1664
  const placeholder = this.values.length === 0 ? this.placeholder : '';
1639
- const placeholderDiv = html`
1640
- <div class="placeholder">${placeholder}</div>
1641
- `;
1665
+
1666
+ // Single unified placeholder - shows when empty and (not focused OR not searchable)
1667
+ const shouldShowPlaceholder =
1668
+ this.values.length === 0 && (!this.focused || !this.searchable);
1669
+ const placeholderElement = shouldShowPlaceholder
1670
+ ? html`<div class="placeholder">${placeholder}</div>`
1671
+ : null;
1642
1672
 
1643
1673
  const clear =
1644
1674
  this.clearable && this.values.length > 0 && !this.isMultiMode
@@ -1683,10 +1713,9 @@ export class Select<T extends SelectOption> extends FormElement {
1683
1713
  .value=${this.input}
1684
1714
  />
1685
1715
  <div id="anchor" style=${styleMap(anchorStyles)}></div>
1686
- ${placeholderDiv}
1687
1716
  </div>
1688
1717
  `
1689
- : placeholderDiv;
1718
+ : null;
1690
1719
 
1691
1720
  const items = html`${!this.isMultiMode && !this.resolving ? input : null}
1692
1721
  ${this.isMultiMode && this.values.length > 1
@@ -1760,9 +1789,10 @@ export class Select<T extends SelectOption> extends FormElement {
1760
1789
  </div>
1761
1790
  `
1762
1791
  )}
1792
+ ${this.searchable && this.focused ? input : null}
1763
1793
  </temba-sortable-list>
1764
1794
  `
1765
- : this.values.map(
1795
+ : html`${this.values.map(
1766
1796
  (selected: any, index: number) => html`
1767
1797
  <div
1768
1798
  class="selected-item ${index === this.selectedIndex
@@ -1811,7 +1841,8 @@ export class Select<T extends SelectOption> extends FormElement {
1811
1841
  </div>
1812
1842
  `
1813
1843
  )}
1814
- ${this.isMultiMode ? input : null}`;
1844
+ ${this.isMultiMode && this.searchable && this.focused ? input : null}
1845
+ ${placeholderElement}`}`;
1815
1846
 
1816
1847
  return html`
1817
1848