@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
@@ -1,7 +1,8 @@
1
1
  import { html, css, TemplateResult } from 'lit';
2
2
  import { customElement, property } from 'lit/decorators.js';
3
- import { FieldConfig } from '../flow/types';
3
+ import { FieldConfig, SelectFieldConfig } from '../flow/types';
4
4
  import { BaseListEditor, ListItem } from './BaseListEditor';
5
+ import { FieldRenderer } from './FieldRenderer';
5
6
 
6
7
  @customElement('temba-array-editor')
7
8
  export class TembaArrayEditor extends BaseListEditor<ListItem> {
@@ -19,6 +20,12 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
19
20
  allItems: any[]
20
21
  ) => any[];
21
22
 
23
+ @property({ type: Function })
24
+ isEmptyItemFn?: (item: any) => boolean;
25
+
26
+ @property({ type: Boolean })
27
+ maintainEmptyItem = true; // Enable by default for better UX
28
+
22
29
  constructor() {
23
30
  super();
24
31
  this._items = [];
@@ -37,11 +44,36 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
37
44
 
38
45
  // Implement abstract methods
39
46
  isEmptyItem(item: ListItem): boolean {
40
- return Object.values(item).every(
47
+ // Use configurable function if provided
48
+ if (this.isEmptyItemFn) {
49
+ return this.isEmptyItemFn(item);
50
+ }
51
+
52
+ // Default behavior: check if all values are empty
53
+ const values = Object.values(item);
54
+ if (values.length === 0) {
55
+ return true;
56
+ }
57
+
58
+ return values.every(
41
59
  (value) => value === undefined || value === null || value === ''
42
60
  );
43
61
  }
44
62
 
63
+ // Override cleanItems to be more permissive for form data
64
+ protected cleanItems(items: ListItem[]): any {
65
+ // For runtime attachments, keep items that have at least one non-empty field
66
+ return items.filter((item) => {
67
+ const values = Object.values(item);
68
+ return (
69
+ values.length > 0 &&
70
+ values.some(
71
+ (value) => value !== undefined && value !== null && value !== ''
72
+ )
73
+ );
74
+ });
75
+ }
76
+
45
77
  createEmptyItem(): ListItem {
46
78
  return {};
47
79
  }
@@ -83,6 +115,14 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
83
115
  return config.computeValue(item, currentValue);
84
116
  }
85
117
 
118
+ // For select fields, ensure we return the right type
119
+ if (config.type === 'select') {
120
+ const selectConfig = config as SelectFieldConfig;
121
+ if (currentValue === undefined || currentValue === null) {
122
+ return selectConfig.multi ? [] : '';
123
+ }
124
+ }
125
+
86
126
  return currentValue;
87
127
  }
88
128
 
@@ -93,36 +133,42 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
93
133
  ): TemplateResult {
94
134
  const computedValue = this.computeFieldValue(itemIndex, fieldName, config);
95
135
 
96
- switch (config.type) {
97
- case 'text':
98
- return html`<temba-textinput
99
- .value=${computedValue || ''}
100
- .placeholder=${config.placeholder}
101
- @change=${(e: any) =>
102
- this.handleFieldChange(itemIndex, fieldName, e.target.value)}
103
- ></temba-textinput>`;
104
-
105
- case 'textarea':
106
- return html`<temba-textinput
107
- .value=${computedValue || ''}
108
- .placeholder=${config.placeholder}
109
- textarea
110
- .rows=${config.rows || 3}
111
- @change=${(e: any) =>
112
- this.handleFieldChange(itemIndex, fieldName, e.target.value)}
113
- ></temba-textinput>`;
114
-
115
- case 'select':
116
- return html`<temba-select
117
- .value=${computedValue || ''}
118
- .options=${config.options}
119
- @change=${(e: any) =>
120
- this.handleFieldChange(itemIndex, fieldName, e.target.value)}
121
- ></temba-select>`;
122
-
123
- default:
124
- return html`<span>Unsupported field type: ${config.type}</span>`;
125
- }
136
+ // Use FieldRenderer for consistent field rendering
137
+ return FieldRenderer.renderField(fieldName, config, computedValue, {
138
+ showLabel: false, // ArrayEditor doesn't show labels for individual fields
139
+ flavor: 'small', // ArrayEditor uses small flavor
140
+ extraClasses: 'form-control',
141
+ onChange: (e: Event) => {
142
+ let value: any;
143
+ const target = e.target as any;
144
+
145
+ // Handle different field types and their change events
146
+ if (config.type === 'select') {
147
+ // For temba-select, extract the correct value
148
+ if (target.tagName === 'TEMBA-SELECT') {
149
+ if (target.multi || target.emails || target.tags) {
150
+ value = target.values || [];
151
+ } else {
152
+ // Single select: extract value from first selected option
153
+ const values = target.values || [];
154
+ value =
155
+ values.length > 0 && values[0]
156
+ ? values[0].value !== undefined
157
+ ? values[0].value
158
+ : values[0]
159
+ : '';
160
+ }
161
+ } else {
162
+ value = target.value;
163
+ }
164
+ } else {
165
+ // For other field types, use the target value directly
166
+ value = target.value;
167
+ }
168
+
169
+ this.handleFieldChange(itemIndex, fieldName, value);
170
+ }
171
+ });
126
172
  }
127
173
 
128
174
  renderItem(item: ListItem, index: number): TemplateResult {
@@ -130,29 +176,25 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
130
176
 
131
177
  return html`
132
178
  <div class="array-item">
133
- <div class="item-header">
134
- <span class="item-title">${this.itemLabel} ${index + 1}</span>
179
+ <div class="item-fields">
180
+ ${Object.entries(this.itemConfig).map(
181
+ ([fieldName, config]) => html`
182
+ <div class="field">
183
+ ${this.renderField(index, fieldName, config)}
184
+ </div>
185
+ `
186
+ )}
135
187
  ${canRemove
136
188
  ? html`
137
189
  <button
138
190
  @click=${() => this.removeItem(index)}
139
191
  class="remove-btn"
140
192
  >
141
- Remove
193
+ <temba-icon name="x"></temba-icon>
142
194
  </button>
143
195
  `
144
196
  : ''}
145
197
  </div>
146
- <div class="item-fields">
147
- ${Object.entries(this.itemConfig).map(
148
- ([fieldName, config]) => html`
149
- <div class="field">
150
- <label>${config.label}${config.required ? ' *' : ''}</label>
151
- ${this.renderField(index, fieldName, config)}
152
- </div>
153
- `
154
- )}
155
- </div>
156
198
  </div>
157
199
  `;
158
200
  }
@@ -171,27 +213,15 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
171
213
 
172
214
  static styles = css`
173
215
  .array-editor {
174
- border: 1px solid #e0e0e0;
175
- border-radius: 6px;
176
- padding: 16px;
177
- background: #fafafa;
178
216
  }
179
217
 
180
218
  .array-item {
181
- border: 1px solid #d0d0d0;
182
- border-radius: 4px;
183
- padding: 16px;
184
- margin-bottom: 12px;
185
- background: white;
186
219
  }
187
220
 
188
221
  .item-header {
189
222
  display: flex;
190
223
  justify-content: space-between;
191
224
  align-items: center;
192
- margin-bottom: 12px;
193
- padding-bottom: 8px;
194
- border-bottom: 1px solid #eee;
195
225
  }
196
226
 
197
227
  .item-title {
@@ -200,8 +230,13 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
200
230
  }
201
231
 
202
232
  .item-fields {
203
- display: grid;
233
+ display: flex;
204
234
  gap: 12px;
235
+ align-items: center;
236
+ }
237
+
238
+ .field {
239
+ flex: 1;
205
240
  }
206
241
 
207
242
  .field label {
@@ -214,7 +249,7 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
214
249
 
215
250
  .add-btn,
216
251
  .remove-btn {
217
- padding: 8px 16px;
252
+ padding: 8px;
218
253
  border: 1px solid #ccc;
219
254
  border-radius: 4px;
220
255
  background: white;
@@ -228,13 +263,8 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
228
263
  }
229
264
 
230
265
  .remove-btn {
231
- background: #fff5f5;
232
- border-color: #fecaca;
233
- color: #dc2626;
234
- }
235
-
236
- .remove-btn:hover {
237
- background: #fef2f2;
266
+ background: #fefefe;
267
+ color: #999;
238
268
  }
239
269
  `;
240
270
  }
@@ -52,10 +52,12 @@ export abstract class BaseListEditor<
52
52
  }
53
53
 
54
54
  protected shouldShowAddButton(): boolean {
55
- return (
56
- !this.maintainEmptyItem &&
57
- (!this.maxItems || this._items.length < this.maxItems)
58
- );
55
+ // Never show add button when maintaining empty items (auto-add behavior)
56
+ if (this.maintainEmptyItem) {
57
+ return false;
58
+ }
59
+
60
+ return !this.maxItems || this._items.length < this.maxItems;
59
61
  }
60
62
 
61
63
  render(): TemplateResult {
@@ -65,7 +67,7 @@ export abstract class BaseListEditor<
65
67
  <div class=${this.getContainerClass()}>
66
68
  <div
67
69
  class="list-items"
68
- style="gap: 8px; display: grid; grid-template-columns: 1fr;"
70
+ style="display: grid; grid-template-columns: 1fr; gap: 8px;"
69
71
  >
70
72
  ${items.map((item, index) => this.renderItem(item, index))}
71
73
  </div>
@@ -89,7 +91,8 @@ export abstract class BaseListEditor<
89
91
 
90
92
  if (this.maintainEmptyItem) {
91
93
  const hasEmptyItem = items.some((item) => this.isEmptyItem(item));
92
- if (!hasEmptyItem) {
94
+ // Only add empty item if we haven't reached maxItems and don't already have an empty item
95
+ if (!hasEmptyItem && (!this.maxItems || items.length < this.maxItems)) {
93
96
  items.push(this.createEmptyItem());
94
97
  }
95
98
  }
@@ -111,6 +114,19 @@ export abstract class BaseListEditor<
111
114
  fieldValue: any
112
115
  ) {
113
116
  const updatedItems = [...this._items];
117
+
118
+ // If editing beyond the current array (auto-generated empty row), check maxItems
119
+ if (index >= this._items.length) {
120
+ if (this.maxItems && this._items.length >= this.maxItems) {
121
+ // Don't allow adding new items if we've reached maxItems
122
+ return;
123
+ }
124
+ // Extend the array to include the new item
125
+ while (updatedItems.length <= index) {
126
+ updatedItems.push(this.createEmptyItem());
127
+ }
128
+ }
129
+
114
130
  const currentItem = updatedItems[index] || this.createEmptyItem();
115
131
 
116
132
  updatedItems[index] = {