@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
@@ -2,11 +2,13 @@ import { __decorate } from "tslib";
2
2
  import { html, css } from 'lit';
3
3
  import { customElement, property } from 'lit/decorators.js';
4
4
  import { BaseListEditor } from './BaseListEditor';
5
+ import { FieldRenderer } from './FieldRenderer';
5
6
  let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
6
7
  constructor() {
7
8
  super();
8
9
  this.itemConfig = {};
9
10
  this.itemLabel = 'Item';
11
+ this.maintainEmptyItem = true; // Enable by default for better UX
10
12
  this._items = [];
11
13
  }
12
14
  // External API
@@ -19,7 +21,25 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
19
21
  }
20
22
  // Implement abstract methods
21
23
  isEmptyItem(item) {
22
- return Object.values(item).every((value) => value === undefined || value === null || value === '');
24
+ // Use configurable function if provided
25
+ if (this.isEmptyItemFn) {
26
+ return this.isEmptyItemFn(item);
27
+ }
28
+ // Default behavior: check if all values are empty
29
+ const values = Object.values(item);
30
+ if (values.length === 0) {
31
+ return true;
32
+ }
33
+ return values.every((value) => value === undefined || value === null || value === '');
34
+ }
35
+ // Override cleanItems to be more permissive for form data
36
+ cleanItems(items) {
37
+ // For runtime attachments, keep items that have at least one non-empty field
38
+ return items.filter((item) => {
39
+ const values = Object.values(item);
40
+ return (values.length > 0 &&
41
+ values.some((value) => value !== undefined && value !== null && value !== ''));
42
+ });
23
43
  }
24
44
  createEmptyItem() {
25
45
  return {};
@@ -44,60 +64,76 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
44
64
  if (config.computeValue) {
45
65
  return config.computeValue(item, currentValue);
46
66
  }
67
+ // For select fields, ensure we return the right type
68
+ if (config.type === 'select') {
69
+ const selectConfig = config;
70
+ if (currentValue === undefined || currentValue === null) {
71
+ return selectConfig.multi ? [] : '';
72
+ }
73
+ }
47
74
  return currentValue;
48
75
  }
49
76
  renderField(itemIndex, fieldName, config) {
50
77
  const computedValue = this.computeFieldValue(itemIndex, fieldName, config);
51
- switch (config.type) {
52
- case 'text':
53
- return html `<temba-textinput
54
- .value=${computedValue || ''}
55
- .placeholder=${config.placeholder}
56
- @change=${(e) => this.handleFieldChange(itemIndex, fieldName, e.target.value)}
57
- ></temba-textinput>`;
58
- case 'textarea':
59
- return html `<temba-textinput
60
- .value=${computedValue || ''}
61
- .placeholder=${config.placeholder}
62
- textarea
63
- .rows=${config.rows || 3}
64
- @change=${(e) => this.handleFieldChange(itemIndex, fieldName, e.target.value)}
65
- ></temba-textinput>`;
66
- case 'select':
67
- return html `<temba-select
68
- .value=${computedValue || ''}
69
- .options=${config.options}
70
- @change=${(e) => this.handleFieldChange(itemIndex, fieldName, e.target.value)}
71
- ></temba-select>`;
72
- default:
73
- return html `<span>Unsupported field type: ${config.type}</span>`;
74
- }
78
+ // Use FieldRenderer for consistent field rendering
79
+ return FieldRenderer.renderField(fieldName, config, computedValue, {
80
+ showLabel: false, // ArrayEditor doesn't show labels for individual fields
81
+ flavor: 'small', // ArrayEditor uses small flavor
82
+ extraClasses: 'form-control',
83
+ onChange: (e) => {
84
+ let value;
85
+ const target = e.target;
86
+ // Handle different field types and their change events
87
+ if (config.type === 'select') {
88
+ // For temba-select, extract the correct value
89
+ if (target.tagName === 'TEMBA-SELECT') {
90
+ if (target.multi || target.emails || target.tags) {
91
+ value = target.values || [];
92
+ }
93
+ else {
94
+ // Single select: extract value from first selected option
95
+ const values = target.values || [];
96
+ value =
97
+ values.length > 0 && values[0]
98
+ ? values[0].value !== undefined
99
+ ? values[0].value
100
+ : values[0]
101
+ : '';
102
+ }
103
+ }
104
+ else {
105
+ value = target.value;
106
+ }
107
+ }
108
+ else {
109
+ // For other field types, use the target value directly
110
+ value = target.value;
111
+ }
112
+ this.handleFieldChange(itemIndex, fieldName, value);
113
+ }
114
+ });
75
115
  }
76
116
  renderItem(item, index) {
77
117
  const canRemove = this.canRemoveItem(index);
78
118
  return html `
79
119
  <div class="array-item">
80
- <div class="item-header">
81
- <span class="item-title">${this.itemLabel} ${index + 1}</span>
120
+ <div class="item-fields">
121
+ ${Object.entries(this.itemConfig).map(([fieldName, config]) => html `
122
+ <div class="field">
123
+ ${this.renderField(index, fieldName, config)}
124
+ </div>
125
+ `)}
82
126
  ${canRemove
83
127
  ? html `
84
128
  <button
85
129
  @click=${() => this.removeItem(index)}
86
130
  class="remove-btn"
87
131
  >
88
- Remove
132
+ <temba-icon name="x"></temba-icon>
89
133
  </button>
90
134
  `
91
135
  : ''}
92
136
  </div>
93
- <div class="item-fields">
94
- ${Object.entries(this.itemConfig).map(([fieldName, config]) => html `
95
- <div class="field">
96
- <label>${config.label}${config.required ? ' *' : ''}</label>
97
- ${this.renderField(index, fieldName, config)}
98
- </div>
99
- `)}
100
- </div>
101
137
  </div>
102
138
  `;
103
139
  }
@@ -114,27 +150,15 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
114
150
  };
115
151
  TembaArrayEditor.styles = css `
116
152
  .array-editor {
117
- border: 1px solid #e0e0e0;
118
- border-radius: 6px;
119
- padding: 16px;
120
- background: #fafafa;
121
153
  }
122
154
 
123
155
  .array-item {
124
- border: 1px solid #d0d0d0;
125
- border-radius: 4px;
126
- padding: 16px;
127
- margin-bottom: 12px;
128
- background: white;
129
156
  }
130
157
 
131
158
  .item-header {
132
159
  display: flex;
133
160
  justify-content: space-between;
134
161
  align-items: center;
135
- margin-bottom: 12px;
136
- padding-bottom: 8px;
137
- border-bottom: 1px solid #eee;
138
162
  }
139
163
 
140
164
  .item-title {
@@ -143,8 +167,13 @@ TembaArrayEditor.styles = css `
143
167
  }
144
168
 
145
169
  .item-fields {
146
- display: grid;
170
+ display: flex;
147
171
  gap: 12px;
172
+ align-items: center;
173
+ }
174
+
175
+ .field {
176
+ flex: 1;
148
177
  }
149
178
 
150
179
  .field label {
@@ -157,7 +186,7 @@ TembaArrayEditor.styles = css `
157
186
 
158
187
  .add-btn,
159
188
  .remove-btn {
160
- padding: 8px 16px;
189
+ padding: 8px;
161
190
  border: 1px solid #ccc;
162
191
  border-radius: 4px;
163
192
  background: white;
@@ -171,13 +200,8 @@ TembaArrayEditor.styles = css `
171
200
  }
172
201
 
173
202
  .remove-btn {
174
- background: #fff5f5;
175
- border-color: #fecaca;
176
- color: #dc2626;
177
- }
178
-
179
- .remove-btn:hover {
180
- background: #fef2f2;
203
+ background: #fefefe;
204
+ color: #999;
181
205
  }
182
206
  `;
183
207
  __decorate([
@@ -189,6 +213,12 @@ __decorate([
189
213
  __decorate([
190
214
  property({ type: Function })
191
215
  ], TembaArrayEditor.prototype, "onItemChange", void 0);
216
+ __decorate([
217
+ property({ type: Function })
218
+ ], TembaArrayEditor.prototype, "isEmptyItemFn", void 0);
219
+ __decorate([
220
+ property({ type: Boolean })
221
+ ], TembaArrayEditor.prototype, "maintainEmptyItem", void 0);
192
222
  __decorate([
193
223
  property({ type: Array })
194
224
  ], TembaArrayEditor.prototype, "value", null);
@@ -1 +1 @@
1
- {"version":3,"file":"ArrayEditor.js","sourceRoot":"","sources":["../../../src/form/ArrayEditor.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, css, TemplateResult } from 'lit';\nimport { customElement, property } from 'lit/decorators.js';\nimport { FieldConfig } from '../flow/types';\nimport { BaseListEditor, ListItem } from './BaseListEditor';\n\n@customElement('temba-array-editor')\nexport class TembaArrayEditor extends BaseListEditor<ListItem> {\n @property({ type: Object })\n itemConfig: Record<string, FieldConfig> = {};\n\n @property({ type: String })\n itemLabel = 'Item';\n\n @property({ type: Function })\n onItemChange?: (\n itemIndex: number,\n field: string,\n value: any,\n allItems: any[]\n ) => any[];\n\n constructor() {\n super();\n this._items = [];\n }\n\n // External API\n @property({ type: Array })\n get value(): any[] {\n return [...this._items];\n }\n\n set value(newValue: any[]) {\n this._items = newValue || [];\n this.requestUpdate();\n }\n\n // Implement abstract methods\n isEmptyItem(item: ListItem): boolean {\n return Object.values(item).every(\n (value) => value === undefined || value === null || value === ''\n );\n }\n\n createEmptyItem(): ListItem {\n return {};\n }\n\n protected handleFieldChange(\n itemIndex: number,\n fieldName: string,\n newValue: any\n ) {\n let updatedItems: any[];\n\n if (this.onItemChange) {\n updatedItems = this.onItemChange(\n itemIndex,\n fieldName,\n newValue,\n this._items\n );\n } else {\n updatedItems = [...this._items];\n updatedItems[itemIndex] = {\n ...updatedItems[itemIndex],\n [fieldName]: newValue\n };\n }\n\n this.updateValue(updatedItems);\n }\n\n private computeFieldValue(\n itemIndex: number,\n fieldName: string,\n config: FieldConfig\n ): any {\n const item = this._items[itemIndex] || {};\n const currentValue = item[fieldName];\n\n if (config.computeValue) {\n return config.computeValue(item, currentValue);\n }\n\n return currentValue;\n }\n\n private renderField(\n itemIndex: number,\n fieldName: string,\n config: FieldConfig\n ): TemplateResult {\n const computedValue = this.computeFieldValue(itemIndex, fieldName, config);\n\n switch (config.type) {\n case 'text':\n return html`<temba-textinput\n .value=${computedValue || ''}\n .placeholder=${config.placeholder}\n @change=${(e: any) =>\n this.handleFieldChange(itemIndex, fieldName, e.target.value)}\n ></temba-textinput>`;\n\n case 'textarea':\n return html`<temba-textinput\n .value=${computedValue || ''}\n .placeholder=${config.placeholder}\n textarea\n .rows=${config.rows || 3}\n @change=${(e: any) =>\n this.handleFieldChange(itemIndex, fieldName, e.target.value)}\n ></temba-textinput>`;\n\n case 'select':\n return html`<temba-select\n .value=${computedValue || ''}\n .options=${config.options}\n @change=${(e: any) =>\n this.handleFieldChange(itemIndex, fieldName, e.target.value)}\n ></temba-select>`;\n\n default:\n return html`<span>Unsupported field type: ${config.type}</span>`;\n }\n }\n\n renderItem(item: ListItem, index: number): TemplateResult {\n const canRemove = this.canRemoveItem(index);\n\n return html`\n <div class=\"array-item\">\n <div class=\"item-header\">\n <span class=\"item-title\">${this.itemLabel} ${index + 1}</span>\n ${canRemove\n ? html`\n <button\n @click=${() => this.removeItem(index)}\n class=\"remove-btn\"\n >\n Remove\n </button>\n `\n : ''}\n </div>\n <div class=\"item-fields\">\n ${Object.entries(this.itemConfig).map(\n ([fieldName, config]) => html`\n <div class=\"field\">\n <label>${config.label}${config.required ? ' *' : ''}</label>\n ${this.renderField(index, fieldName, config)}\n </div>\n `\n )}\n </div>\n </div>\n `;\n }\n\n protected getContainerClass(): string {\n return 'array-editor';\n }\n\n protected renderAddButton(): TemplateResult {\n return html`\n <button class=\"add-btn\" @click=${() => this.addItem()}>\n Add ${this.itemLabel}\n </button>\n `;\n }\n\n static styles = css`\n .array-editor {\n border: 1px solid #e0e0e0;\n border-radius: 6px;\n padding: 16px;\n background: #fafafa;\n }\n\n .array-item {\n border: 1px solid #d0d0d0;\n border-radius: 4px;\n padding: 16px;\n margin-bottom: 12px;\n background: white;\n }\n\n .item-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 12px;\n padding-bottom: 8px;\n border-bottom: 1px solid #eee;\n }\n\n .item-title {\n font-weight: 600;\n color: #333;\n }\n\n .item-fields {\n display: grid;\n gap: 12px;\n }\n\n .field label {\n display: block;\n margin-bottom: 4px;\n font-weight: 500;\n color: #555;\n font-size: 14px;\n }\n\n .add-btn,\n .remove-btn {\n padding: 8px 16px;\n border: 1px solid #ccc;\n border-radius: 4px;\n background: white;\n cursor: pointer;\n font-size: 14px;\n }\n\n .add-btn:hover,\n .remove-btn:hover {\n background: #f8f8f8;\n }\n\n .remove-btn {\n background: #fff5f5;\n border-color: #fecaca;\n color: #dc2626;\n }\n\n .remove-btn:hover {\n background: #fef2f2;\n }\n `;\n}\n"]}
1
+ {"version":3,"file":"ArrayEditor.js","sourceRoot":"","sources":["../../../src/form/ArrayEditor.ts"],"names":[],"mappings":"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","sourcesContent":["import { html, css, TemplateResult } from 'lit';\nimport { customElement, property } from 'lit/decorators.js';\nimport { FieldConfig, SelectFieldConfig } from '../flow/types';\nimport { BaseListEditor, ListItem } from './BaseListEditor';\nimport { FieldRenderer } from './FieldRenderer';\n\n@customElement('temba-array-editor')\nexport class TembaArrayEditor extends BaseListEditor<ListItem> {\n @property({ type: Object })\n itemConfig: Record<string, FieldConfig> = {};\n\n @property({ type: String })\n itemLabel = 'Item';\n\n @property({ type: Function })\n onItemChange?: (\n itemIndex: number,\n field: string,\n value: any,\n allItems: any[]\n ) => any[];\n\n @property({ type: Function })\n isEmptyItemFn?: (item: any) => boolean;\n\n @property({ type: Boolean })\n maintainEmptyItem = true; // Enable by default for better UX\n\n constructor() {\n super();\n this._items = [];\n }\n\n // External API\n @property({ type: Array })\n get value(): any[] {\n return [...this._items];\n }\n\n set value(newValue: any[]) {\n this._items = newValue || [];\n this.requestUpdate();\n }\n\n // Implement abstract methods\n isEmptyItem(item: ListItem): boolean {\n // Use configurable function if provided\n if (this.isEmptyItemFn) {\n return this.isEmptyItemFn(item);\n }\n\n // Default behavior: check if all values are empty\n const values = Object.values(item);\n if (values.length === 0) {\n return true;\n }\n\n return values.every(\n (value) => value === undefined || value === null || value === ''\n );\n }\n\n // Override cleanItems to be more permissive for form data\n protected cleanItems(items: ListItem[]): any {\n // For runtime attachments, keep items that have at least one non-empty field\n return items.filter((item) => {\n const values = Object.values(item);\n return (\n values.length > 0 &&\n values.some(\n (value) => value !== undefined && value !== null && value !== ''\n )\n );\n });\n }\n\n createEmptyItem(): ListItem {\n return {};\n }\n\n protected handleFieldChange(\n itemIndex: number,\n fieldName: string,\n newValue: any\n ) {\n let updatedItems: any[];\n\n if (this.onItemChange) {\n updatedItems = this.onItemChange(\n itemIndex,\n fieldName,\n newValue,\n this._items\n );\n } else {\n updatedItems = [...this._items];\n updatedItems[itemIndex] = {\n ...updatedItems[itemIndex],\n [fieldName]: newValue\n };\n }\n\n this.updateValue(updatedItems);\n }\n\n private computeFieldValue(\n itemIndex: number,\n fieldName: string,\n config: FieldConfig\n ): any {\n const item = this._items[itemIndex] || {};\n const currentValue = item[fieldName];\n\n if (config.computeValue) {\n return config.computeValue(item, currentValue);\n }\n\n // For select fields, ensure we return the right type\n if (config.type === 'select') {\n const selectConfig = config as SelectFieldConfig;\n if (currentValue === undefined || currentValue === null) {\n return selectConfig.multi ? [] : '';\n }\n }\n\n return currentValue;\n }\n\n private renderField(\n itemIndex: number,\n fieldName: string,\n config: FieldConfig\n ): TemplateResult {\n const computedValue = this.computeFieldValue(itemIndex, fieldName, config);\n\n // Use FieldRenderer for consistent field rendering\n return FieldRenderer.renderField(fieldName, config, computedValue, {\n showLabel: false, // ArrayEditor doesn't show labels for individual fields\n flavor: 'small', // ArrayEditor uses small flavor\n extraClasses: 'form-control',\n onChange: (e: Event) => {\n let value: any;\n const target = e.target as any;\n\n // Handle different field types and their change events\n if (config.type === 'select') {\n // For temba-select, extract the correct value\n if (target.tagName === 'TEMBA-SELECT') {\n if (target.multi || target.emails || target.tags) {\n value = target.values || [];\n } else {\n // Single select: extract value from first selected option\n const values = target.values || [];\n value =\n values.length > 0 && values[0]\n ? values[0].value !== undefined\n ? values[0].value\n : values[0]\n : '';\n }\n } else {\n value = target.value;\n }\n } else {\n // For other field types, use the target value directly\n value = target.value;\n }\n\n this.handleFieldChange(itemIndex, fieldName, value);\n }\n });\n }\n\n renderItem(item: ListItem, index: number): TemplateResult {\n const canRemove = this.canRemoveItem(index);\n\n return html`\n <div class=\"array-item\">\n <div class=\"item-fields\">\n ${Object.entries(this.itemConfig).map(\n ([fieldName, config]) => html`\n <div class=\"field\">\n ${this.renderField(index, fieldName, config)}\n </div>\n `\n )}\n ${canRemove\n ? html`\n <button\n @click=${() => this.removeItem(index)}\n class=\"remove-btn\"\n >\n <temba-icon name=\"x\"></temba-icon>\n </button>\n `\n : ''}\n </div>\n </div>\n `;\n }\n\n protected getContainerClass(): string {\n return 'array-editor';\n }\n\n protected renderAddButton(): TemplateResult {\n return html`\n <button class=\"add-btn\" @click=${() => this.addItem()}>\n Add ${this.itemLabel}\n </button>\n `;\n }\n\n static styles = css`\n .array-editor {\n }\n\n .array-item {\n }\n\n .item-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n\n .item-title {\n font-weight: 600;\n color: #333;\n }\n\n .item-fields {\n display: flex;\n gap: 12px;\n align-items: center;\n }\n\n .field {\n flex: 1;\n }\n\n .field label {\n display: block;\n margin-bottom: 4px;\n font-weight: 500;\n color: #555;\n font-size: 14px;\n }\n\n .add-btn,\n .remove-btn {\n padding: 8px;\n border: 1px solid #ccc;\n border-radius: 4px;\n background: white;\n cursor: pointer;\n font-size: 14px;\n }\n\n .add-btn:hover,\n .remove-btn:hover {\n background: #f8f8f8;\n }\n\n .remove-btn {\n background: #fefefe;\n color: #999;\n }\n `;\n}\n"]}
@@ -18,8 +18,11 @@ export class BaseListEditor extends LitElement {
18
18
  `;
19
19
  }
20
20
  shouldShowAddButton() {
21
- return (!this.maintainEmptyItem &&
22
- (!this.maxItems || this._items.length < this.maxItems));
21
+ // Never show add button when maintaining empty items (auto-add behavior)
22
+ if (this.maintainEmptyItem) {
23
+ return false;
24
+ }
25
+ return !this.maxItems || this._items.length < this.maxItems;
23
26
  }
24
27
  render() {
25
28
  const items = this.displayItems;
@@ -27,7 +30,7 @@ export class BaseListEditor extends LitElement {
27
30
  <div class=${this.getContainerClass()}>
28
31
  <div
29
32
  class="list-items"
30
- style="gap: 8px; display: grid; grid-template-columns: 1fr;"
33
+ style="display: grid; grid-template-columns: 1fr; gap: 8px;"
31
34
  >
32
35
  ${items.map((item, index) => this.renderItem(item, index))}
33
36
  </div>
@@ -48,7 +51,8 @@ export class BaseListEditor extends LitElement {
48
51
  const items = [...this._items];
49
52
  if (this.maintainEmptyItem) {
50
53
  const hasEmptyItem = items.some((item) => this.isEmptyItem(item));
51
- if (!hasEmptyItem) {
54
+ // Only add empty item if we haven't reached maxItems and don't already have an empty item
55
+ if (!hasEmptyItem && (!this.maxItems || items.length < this.maxItems)) {
52
56
  items.push(this.createEmptyItem());
53
57
  }
54
58
  }
@@ -63,6 +67,17 @@ export class BaseListEditor extends LitElement {
63
67
  // Handle field changes within an item (for complex items)
64
68
  handleFieldChange(index, fieldName, fieldValue) {
65
69
  const updatedItems = [...this._items];
70
+ // If editing beyond the current array (auto-generated empty row), check maxItems
71
+ if (index >= this._items.length) {
72
+ if (this.maxItems && this._items.length >= this.maxItems) {
73
+ // Don't allow adding new items if we've reached maxItems
74
+ return;
75
+ }
76
+ // Extend the array to include the new item
77
+ while (updatedItems.length <= index) {
78
+ updatedItems.push(this.createEmptyItem());
79
+ }
80
+ }
66
81
  const currentItem = updatedItems[index] || this.createEmptyItem();
67
82
  updatedItems[index] = {
68
83
  ...currentItem,
@@ -1 +1 @@
1
- {"version":3,"file":"BaseListEditor.js","sourceRoot":"","sources":["../../../src/form/BaseListEditor.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, TemplateResult, html } from 'lit';\nimport { property } from 'lit/decorators.js';\n\nexport interface ListItem {\n [key: string]: any;\n}\n\nexport interface ListEditorConfig {\n // Determines if empty items should be automatically maintained\n maintainEmptyItem?: boolean;\n // Function to check if an item is considered empty\n isEmptyItem?: (item: ListItem) => boolean;\n // Function to create a new empty item\n createEmptyItem?: () => ListItem;\n // Function to clean items before emitting (e.g., filter out empty items)\n cleanItems?: (items: ListItem[]) => ListItem[];\n // Minimum number of items to maintain\n minItems?: number;\n // Maximum number of items allowed\n maxItems?: number;\n}\n\nexport abstract class BaseListEditor<\n T extends ListItem = ListItem\n> extends LitElement {\n @property({ attribute: false })\n protected _items: T[] = [];\n\n @property({ type: Number })\n minItems = 0;\n\n @property({ type: Number })\n maxItems?: number;\n\n @property({ type: Boolean })\n maintainEmptyItem = false;\n\n // Abstract methods that must be implemented by subclasses\n abstract isEmptyItem(item: T): boolean;\n abstract createEmptyItem(): T;\n abstract renderItem(item: T, index: number): TemplateResult;\n\n // Optional methods that subclasses can override\n protected getContainerClass(): string {\n return 'base-list-editor';\n }\n\n protected renderAddButton(): TemplateResult {\n return html`\n <button class=\"add-btn\" @click=${() => this.addItem()}>Add Item</button>\n `;\n }\n\n protected shouldShowAddButton(): boolean {\n return (\n !this.maintainEmptyItem &&\n (!this.maxItems || this._items.length < this.maxItems)\n );\n }\n\n render(): TemplateResult {\n const items = this.displayItems;\n\n return html`\n <div class=${this.getContainerClass()}>\n <div\n class=\"list-items\"\n style=\"gap: 8px; display: grid; grid-template-columns: 1fr;\"\n >\n ${items.map((item, index) => this.renderItem(item, index))}\n </div>\n ${this.shouldShowAddButton() ? this.renderAddButton() : ''}\n </div>\n `;\n }\n\n // Optional method for cleaning items before emission (can return any type)\n protected cleanItems(items: T[]): any {\n if (!this.maintainEmptyItem) {\n return items;\n }\n // Filter out empty items for the emitted value\n return items.filter((item) => !this.isEmptyItem(item));\n }\n\n // Get the items to display (may include empty items for UI)\n protected get displayItems(): T[] {\n const items = [...this._items];\n\n if (this.maintainEmptyItem) {\n const hasEmptyItem = items.some((item) => this.isEmptyItem(item));\n if (!hasEmptyItem) {\n items.push(this.createEmptyItem());\n }\n }\n\n return items;\n }\n\n // Handle changes to an item\n protected handleItemChange(index: number, newItem: T) {\n const updatedItems = [...this._items];\n updatedItems[index] = newItem;\n this.updateValue(updatedItems);\n }\n\n // Handle field changes within an item (for complex items)\n protected handleFieldChange(\n index: number,\n fieldName: string,\n fieldValue: any\n ) {\n const updatedItems = [...this._items];\n const currentItem = updatedItems[index] || this.createEmptyItem();\n\n updatedItems[index] = {\n ...currentItem,\n [fieldName]: fieldValue\n };\n\n this.updateValue(updatedItems);\n }\n\n // Add a new item\n protected addItem(item?: T) {\n if (this.maxItems && this._items.length >= this.maxItems) {\n return;\n }\n\n const newItem = item || this.createEmptyItem();\n const updatedItems = [...this._items, newItem];\n this.updateValue(updatedItems);\n }\n\n // Remove an item\n protected removeItem(index: number) {\n if (this._items.length <= this.minItems) {\n return;\n }\n\n const updatedItems = this._items.filter((_, i) => i !== index);\n this.updateValue(updatedItems);\n }\n\n // Check if an item can be removed\n protected canRemoveItem(index: number): boolean {\n const item = this.displayItems[index];\n\n // Can't remove if it would go below minimum\n if (this._items.length <= this.minItems) {\n return false;\n }\n\n // Can't remove empty items if we're maintaining them\n if (this.maintainEmptyItem && this.isEmptyItem(item)) {\n return false;\n }\n\n return true;\n }\n\n // Update the value and emit change event\n protected updateValue(newValue: T[]) {\n this._items = newValue;\n this.dispatchEvent(\n new CustomEvent('change', {\n detail: { value: this.cleanItems(newValue) },\n bubbles: true\n })\n );\n }\n\n // Utility method for subclasses to check if two items are equal\n protected itemsEqual(item1: T, item2: T): boolean {\n return JSON.stringify(item1) === JSON.stringify(item2);\n }\n}\n"]}
1
+ {"version":3,"file":"BaseListEditor.js","sourceRoot":"","sources":["../../../src/form/BaseListEditor.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, TemplateResult, html } from 'lit';\nimport { property } from 'lit/decorators.js';\n\nexport interface ListItem {\n [key: string]: any;\n}\n\nexport interface ListEditorConfig {\n // Determines if empty items should be automatically maintained\n maintainEmptyItem?: boolean;\n // Function to check if an item is considered empty\n isEmptyItem?: (item: ListItem) => boolean;\n // Function to create a new empty item\n createEmptyItem?: () => ListItem;\n // Function to clean items before emitting (e.g., filter out empty items)\n cleanItems?: (items: ListItem[]) => ListItem[];\n // Minimum number of items to maintain\n minItems?: number;\n // Maximum number of items allowed\n maxItems?: number;\n}\n\nexport abstract class BaseListEditor<\n T extends ListItem = ListItem\n> extends LitElement {\n @property({ attribute: false })\n protected _items: T[] = [];\n\n @property({ type: Number })\n minItems = 0;\n\n @property({ type: Number })\n maxItems?: number;\n\n @property({ type: Boolean })\n maintainEmptyItem = false;\n\n // Abstract methods that must be implemented by subclasses\n abstract isEmptyItem(item: T): boolean;\n abstract createEmptyItem(): T;\n abstract renderItem(item: T, index: number): TemplateResult;\n\n // Optional methods that subclasses can override\n protected getContainerClass(): string {\n return 'base-list-editor';\n }\n\n protected renderAddButton(): TemplateResult {\n return html`\n <button class=\"add-btn\" @click=${() => this.addItem()}>Add Item</button>\n `;\n }\n\n protected shouldShowAddButton(): boolean {\n // Never show add button when maintaining empty items (auto-add behavior)\n if (this.maintainEmptyItem) {\n return false;\n }\n\n return !this.maxItems || this._items.length < this.maxItems;\n }\n\n render(): TemplateResult {\n const items = this.displayItems;\n\n return html`\n <div class=${this.getContainerClass()}>\n <div\n class=\"list-items\"\n style=\"display: grid; grid-template-columns: 1fr; gap: 8px;\"\n >\n ${items.map((item, index) => this.renderItem(item, index))}\n </div>\n ${this.shouldShowAddButton() ? this.renderAddButton() : ''}\n </div>\n `;\n }\n\n // Optional method for cleaning items before emission (can return any type)\n protected cleanItems(items: T[]): any {\n if (!this.maintainEmptyItem) {\n return items;\n }\n // Filter out empty items for the emitted value\n return items.filter((item) => !this.isEmptyItem(item));\n }\n\n // Get the items to display (may include empty items for UI)\n protected get displayItems(): T[] {\n const items = [...this._items];\n\n if (this.maintainEmptyItem) {\n const hasEmptyItem = items.some((item) => this.isEmptyItem(item));\n // Only add empty item if we haven't reached maxItems and don't already have an empty item\n if (!hasEmptyItem && (!this.maxItems || items.length < this.maxItems)) {\n items.push(this.createEmptyItem());\n }\n }\n\n return items;\n }\n\n // Handle changes to an item\n protected handleItemChange(index: number, newItem: T) {\n const updatedItems = [...this._items];\n updatedItems[index] = newItem;\n this.updateValue(updatedItems);\n }\n\n // Handle field changes within an item (for complex items)\n protected handleFieldChange(\n index: number,\n fieldName: string,\n fieldValue: any\n ) {\n const updatedItems = [...this._items];\n\n // If editing beyond the current array (auto-generated empty row), check maxItems\n if (index >= this._items.length) {\n if (this.maxItems && this._items.length >= this.maxItems) {\n // Don't allow adding new items if we've reached maxItems\n return;\n }\n // Extend the array to include the new item\n while (updatedItems.length <= index) {\n updatedItems.push(this.createEmptyItem());\n }\n }\n\n const currentItem = updatedItems[index] || this.createEmptyItem();\n\n updatedItems[index] = {\n ...currentItem,\n [fieldName]: fieldValue\n };\n\n this.updateValue(updatedItems);\n }\n\n // Add a new item\n protected addItem(item?: T) {\n if (this.maxItems && this._items.length >= this.maxItems) {\n return;\n }\n\n const newItem = item || this.createEmptyItem();\n const updatedItems = [...this._items, newItem];\n this.updateValue(updatedItems);\n }\n\n // Remove an item\n protected removeItem(index: number) {\n if (this._items.length <= this.minItems) {\n return;\n }\n\n const updatedItems = this._items.filter((_, i) => i !== index);\n this.updateValue(updatedItems);\n }\n\n // Check if an item can be removed\n protected canRemoveItem(index: number): boolean {\n const item = this.displayItems[index];\n\n // Can't remove if it would go below minimum\n if (this._items.length <= this.minItems) {\n return false;\n }\n\n // Can't remove empty items if we're maintaining them\n if (this.maintainEmptyItem && this.isEmptyItem(item)) {\n return false;\n }\n\n return true;\n }\n\n // Update the value and emit change event\n protected updateValue(newValue: T[]) {\n this._items = newValue;\n this.dispatchEvent(\n new CustomEvent('change', {\n detail: { value: this.cleanItems(newValue) },\n bubbles: true\n })\n );\n }\n\n // Utility method for subclasses to check if two items are equal\n protected itemsEqual(item1: T, item2: T): boolean {\n return JSON.stringify(item1) === JSON.stringify(item2);\n }\n}\n"]}