@nyaruka/temba-components 0.129.7 → 0.129.8

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 (127) 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 +8 -3
  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 +42 -26
  10. package/dist/temba-components.js +506 -218
  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/flow/NodeEditor.js +245 -22
  16. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  17. package/out-tsc/src/flow/actions/call_webhook.js +26 -17
  18. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
  19. package/out-tsc/src/flow/actions/send_msg.js +147 -6
  20. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  21. package/out-tsc/src/flow/types.js.map +1 -1
  22. package/out-tsc/src/form/ArrayEditor.js +111 -38
  23. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  24. package/out-tsc/src/form/BaseListEditor.js +19 -4
  25. package/out-tsc/src/form/BaseListEditor.js.map +1 -1
  26. package/out-tsc/src/form/FormField.js +1 -1
  27. package/out-tsc/src/form/FormField.js.map +1 -1
  28. package/out-tsc/src/form/KeyValueEditor.js +1 -1
  29. package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
  30. package/out-tsc/src/form/MediaPicker.js +13 -1
  31. package/out-tsc/src/form/MediaPicker.js.map +1 -1
  32. package/out-tsc/src/form/MessageEditor.js +422 -0
  33. package/out-tsc/src/form/MessageEditor.js.map +1 -0
  34. package/out-tsc/src/form/TextInput.js +12 -5
  35. package/out-tsc/src/form/TextInput.js.map +1 -1
  36. package/out-tsc/src/form/select/Select.js +4 -4
  37. package/out-tsc/src/form/select/Select.js.map +1 -1
  38. package/out-tsc/src/live/ContactChat.js +27 -2
  39. package/out-tsc/src/live/ContactChat.js.map +1 -1
  40. package/out-tsc/temba-modules.js +2 -0
  41. package/out-tsc/temba-modules.js.map +1 -1
  42. package/out-tsc/test/temba-field-config.test.js +4 -2
  43. package/out-tsc/test/temba-field-config.test.js.map +1 -1
  44. package/out-tsc/test/temba-message-editor.test.js +194 -0
  45. package/out-tsc/test/temba-message-editor.test.js.map +1 -0
  46. package/out-tsc/test/temba-node-editor.test.js +71 -0
  47. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  48. package/out-tsc/test/temba-select.test.js +1 -1
  49. package/out-tsc/test/temba-select.test.js.map +1 -1
  50. package/out-tsc/test/temba-textinput.test.js +16 -0
  51. package/out-tsc/test/temba-textinput.test.js.map +1 -1
  52. package/out-tsc/test/temba-webchat.test.js +4 -0
  53. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  54. package/out-tsc/test/utils.test.js +2 -8
  55. package/out-tsc/test/utils.test.js.map +1 -1
  56. package/package.json +7 -4
  57. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  58. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  59. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  60. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  61. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  62. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  63. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  64. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  65. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  66. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  67. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  68. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  69. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  70. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  71. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  72. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  73. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  74. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  75. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  76. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  77. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  78. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  79. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  80. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  81. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  82. package/screenshots/truth/editor/send_msg.png +0 -0
  83. package/screenshots/truth/editor/set_contact_language.png +0 -0
  84. package/screenshots/truth/editor/set_contact_name.png +0 -0
  85. package/screenshots/truth/editor/set_run_result.png +0 -0
  86. package/screenshots/truth/formfield/markdown-errors.png +0 -0
  87. package/screenshots/truth/formfield/no-errors.png +0 -0
  88. package/screenshots/truth/formfield/plain-text-errors.png +0 -0
  89. package/screenshots/truth/message-editor/autogrow-initial-content.png +0 -0
  90. package/screenshots/truth/message-editor/default.png +0 -0
  91. package/screenshots/truth/message-editor/drag-highlight.png +0 -0
  92. package/screenshots/truth/message-editor/filtered-attachments.png +0 -0
  93. package/screenshots/truth/message-editor/with-completion.png +0 -0
  94. package/screenshots/truth/message-editor/with-properties.png +0 -0
  95. package/screenshots/truth/textinput/autogrow-initial.png +0 -0
  96. package/screenshots/truth/textinput/input-form.png +0 -0
  97. package/src/display/Thumbnail.ts +2 -1
  98. package/src/events.ts +5 -0
  99. package/src/flow/NodeEditor.ts +269 -23
  100. package/src/flow/actions/call_webhook.ts +28 -18
  101. package/src/flow/actions/send_msg.ts +170 -6
  102. package/src/flow/types.ts +21 -2
  103. package/src/form/ArrayEditor.ts +120 -42
  104. package/src/form/BaseListEditor.ts +22 -6
  105. package/src/form/FormField.ts +1 -1
  106. package/src/form/KeyValueEditor.ts +1 -1
  107. package/src/form/MediaPicker.ts +13 -1
  108. package/src/form/MessageEditor.ts +449 -0
  109. package/src/form/TextInput.ts +15 -7
  110. package/src/form/select/Select.ts +4 -4
  111. package/src/live/ContactChat.ts +30 -4
  112. package/static/css/temba-components.css +2 -0
  113. package/static/mr/docs/en-us/editor.json +2588 -0
  114. package/stress-test.js +138 -0
  115. package/temba-modules.ts +2 -0
  116. package/test/temba-field-config.test.ts +4 -2
  117. package/test/temba-message-editor.test.ts +300 -0
  118. package/test/temba-node-editor.test.ts +94 -0
  119. package/test/temba-select.test.ts +1 -1
  120. package/test/temba-textinput.test.ts +26 -0
  121. package/test/temba-webchat.test.ts +5 -0
  122. package/test/utils.test.ts +2 -13
  123. package/test-assets/contacts/history.json +19 -0
  124. package/test-assets/style.css +2 -0
  125. package/web-dev-mock.mjs +433 -0
  126. package/web-dev-server.config.mjs +51 -5
  127. package/web-test-runner.config.mjs +9 -4
@@ -28,17 +28,21 @@ export const send_msg = {
28
28
  },
29
29
  form: {
30
30
  text: {
31
- type: 'textarea',
32
- label: 'Message Text',
33
- helpText: 'Enter the message to send. You can use expressions like @contact.name',
31
+ type: 'message-editor',
32
+ label: 'Message',
33
+ helpText: 'Enter the message to send with optional attachments. You can use expressions like @contact.name',
34
34
  required: true,
35
35
  evaluated: true,
36
- minHeight: 175
36
+ placeholder: 'Type your message here...',
37
+ maxAttachments: 10,
38
+ accept: '',
39
+ endpoint: '/api/v2/media.json',
40
+ counter: 'temba-charcount',
41
+ gsm: true,
42
+ autogrow: true
37
43
  },
38
44
  quick_replies: {
39
45
  type: 'select',
40
- label: 'Quick Replies',
41
- helpText: 'Add quick reply options for this message',
42
46
  options: [],
43
47
  multi: true,
44
48
  tags: true,
@@ -46,6 +50,123 @@ export const send_msg = {
46
50
  placeholder: 'Add quick replies...',
47
51
  maxItems: 10,
48
52
  evaluated: true
53
+ },
54
+ runtime_attachments: {
55
+ type: 'array',
56
+ helpText: 'Add dynamic attachments using expressions',
57
+ itemLabel: 'Attachment',
58
+ maxItems: 10,
59
+ isEmptyItem: (item) => {
60
+ return !item.expression || item.expression.trim() === '';
61
+ },
62
+ itemConfig: {
63
+ type: {
64
+ type: 'select',
65
+ options: [
66
+ { value: 'image', label: 'Image' },
67
+ { value: 'audio', label: 'Audio' },
68
+ { value: 'video', label: 'Video' },
69
+ { value: 'document', label: 'Document' }
70
+ ],
71
+ required: true,
72
+ searchable: false
73
+ },
74
+ expression: {
75
+ type: 'text',
76
+ placeholder: 'Expression (e.g. @contact.photo)',
77
+ required: true,
78
+ evaluated: true
79
+ }
80
+ }
81
+ }
82
+ },
83
+ layout: [
84
+ 'text',
85
+ {
86
+ type: 'group',
87
+ label: 'Quick Replies',
88
+ items: ['quick_replies'],
89
+ collapsible: true,
90
+ collapsed: (formData) => {
91
+ // Collapse only if there are no quick replies
92
+ return !formData.quick_replies || formData.quick_replies.length === 0;
93
+ },
94
+ getGroupValueCount: (formData) => {
95
+ var _a;
96
+ return ((_a = formData.quick_replies) === null || _a === void 0 ? void 0 : _a.length) || 0;
97
+ }
98
+ },
99
+ {
100
+ type: 'group',
101
+ label: 'Runtime Attachments',
102
+ items: ['runtime_attachments'],
103
+ collapsible: true,
104
+ collapsed: true,
105
+ helpText: 'Add dynamic attachments that are evaluated at runtime',
106
+ getGroupValueCount: (formData) => {
107
+ var _a;
108
+ return (((_a = formData.runtime_attachments) === null || _a === void 0 ? void 0 : _a.filter((item) => item && item.expression && item.expression.trim() !== '').length) || 0);
109
+ }
110
+ }
111
+ ],
112
+ toFormData: (action) => {
113
+ // Extract runtime attachments from the text field attachments
114
+ const runtimeAttachments = [];
115
+ const staticAttachments = [];
116
+ if (action.attachments && Array.isArray(action.attachments)) {
117
+ action.attachments.forEach((attachment) => {
118
+ if (typeof attachment === 'string' && attachment.includes(':')) {
119
+ const colonIndex = attachment.indexOf(':');
120
+ const contentType = attachment.substring(0, colonIndex);
121
+ const value = attachment.substring(colonIndex + 1);
122
+ if (!contentType.includes('/')) {
123
+ // This is a runtime attachment
124
+ runtimeAttachments.push({
125
+ type: contentType,
126
+ expression: value
127
+ });
128
+ }
129
+ else {
130
+ // This is a static attachment
131
+ staticAttachments.push(attachment);
132
+ }
133
+ }
134
+ });
135
+ }
136
+ return {
137
+ uuid: action.uuid,
138
+ text: action.text || '',
139
+ attachments: staticAttachments,
140
+ runtime_attachments: runtimeAttachments,
141
+ quick_replies: (action.quick_replies || []).map((reply) => ({
142
+ name: reply,
143
+ value: reply
144
+ }))
145
+ };
146
+ },
147
+ fromFormData: (data) => {
148
+ const result = {
149
+ uuid: data.uuid,
150
+ type: 'send_msg',
151
+ text: data.text || '',
152
+ attachments: [],
153
+ quick_replies: (data.quick_replies || []).map((reply) => typeof reply === 'string' ? reply : reply.value || reply.name || reply)
154
+ };
155
+ // Combine static attachments from text field with runtime attachments
156
+ const staticAttachments = data.attachments || [];
157
+ const runtimeAttachments = (data.runtime_attachments || [])
158
+ .filter((item) => item && item.type && item.expression) // Filter out invalid items
159
+ .map((item) => `${item.type}:${item.expression}`);
160
+ result.attachments = [...staticAttachments, ...runtimeAttachments];
161
+ // Remove quick_replies if empty to match original format
162
+ if (result.quick_replies.length === 0) {
163
+ delete result.quick_replies;
164
+ }
165
+ return result;
166
+ },
167
+ sanitize: (formData) => {
168
+ if (formData.text && typeof formData.text === 'string') {
169
+ formData.text = formData.text.trim();
49
170
  }
50
171
  },
51
172
  validate: (action) => {
@@ -53,6 +174,26 @@ export const send_msg = {
53
174
  if (!action.text || action.text.trim() === '') {
54
175
  errors.text = 'Message text is required';
55
176
  }
177
+ const attachments = action.attachments || [];
178
+ if (attachments.length > 10) {
179
+ const staticAttachments = attachments.filter((attachment) => typeof attachment === 'string' &&
180
+ attachment.substring(0, attachment.indexOf(':')).includes('/'));
181
+ const runtimeAttachments = attachments.filter((attachment) => typeof attachment === 'string' &&
182
+ !attachment.substring(0, attachment.indexOf(':')).includes('/'));
183
+ if (runtimeAttachments.length > 0) {
184
+ errors.runtime_attachments =
185
+ 'Each message can only have up to 10 attachments';
186
+ }
187
+ if (staticAttachments.length > 0) {
188
+ const message = 'Each message can only have up to 10 total attachments';
189
+ if (errors.text) {
190
+ errors.text += ` ${message}`;
191
+ }
192
+ else {
193
+ errors.text = message;
194
+ }
195
+ }
196
+ }
56
197
  return {
57
198
  valid: Object.keys(errors).length === 0,
58
199
  errors
@@ -1 +1 @@
1
- {"version":3,"file":"send_msg.js","sourceRoot":"","sources":["../../../../src/flow/actions/send_msg.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAChE,OAAO,EAAgB,MAAM,EAAoB,MAAM,UAAU,CAAC;AAGlE,MAAM,CAAC,MAAM,QAAQ,GAAiB;IACpC,IAAI,EAAE,cAAc;IACpB,KAAK,EAAE,MAAM,CAAC,IAAI;IAClB,MAAM,EAAE,CAAC,KAAW,EAAE,MAAe,EAAE,EAAE;;QACvC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAChD,OAAO,IAAI,CAAA;QACP,UAAU,CAAC,IAAI,CAAC;QAChB,CAAA,MAAA,MAAM,CAAC,aAAa,0CAAE,MAAM,IAAG,CAAC;YAChC,CAAC,CAAC,IAAI,CAAA;cACA,MAAM,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;gBACnC,OAAO,IAAI,CAAA,4BAA4B,KAAK,QAAQ,CAAC;YACvD,CAAC,CAAC;cACA,MAAM,CAAC,QAAQ;gBACf,CAAC,CAAC,IAAI,CAAA;;;;mDAI+B,MAAM,CAAC,QAAQ,CAAC,IAAI;uBAChD;gBACT,CAAC,CAAC,IAAI;iBACH;YACT,CAAC,CAAC,IAAI;KACT,CAAC;IACJ,CAAC;IACD,IAAI,EAAE;QACJ,IAAI,EAAE;YACJ,IAAI,EAAE,UAAU;YAChB,KAAK,EAAE,cAAc;YACrB,QAAQ,EACN,uEAAuE;YACzE,QAAQ,EAAE,IAAI;YACd,SAAS,EAAE,IAAI;YACf,SAAS,EAAE,GAAG;SACf;QACD,aAAa,EAAE;YACb,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,eAAe;YACtB,QAAQ,EAAE,0CAA0C;YACpD,OAAO,EAAE,EAAE;YACX,KAAK,EAAE,IAAI;YACX,IAAI,EAAE,IAAI;YACV,UAAU,EAAE,IAAI;YAChB,WAAW,EAAE,sBAAsB;YACnC,QAAQ,EAAE,EAAE;YACZ,SAAS,EAAE,IAAI;SAChB;KACF;IACD,QAAQ,EAAE,CAAC,MAAe,EAAoB,EAAE;QAC9C,MAAM,MAAM,GAA8B,EAAE,CAAC;QAE7C,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC9C,MAAM,CAAC,IAAI,GAAG,0BAA0B,CAAC;QAC3C,CAAC;QAED,OAAO;YACL,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC;YACvC,MAAM;SACP,CAAC;IACJ,CAAC;CACF,CAAC","sourcesContent":["import { html } from 'lit-html';\nimport { unsafeHTML } from 'lit-html/directives/unsafe-html.js';\nimport { ActionConfig, COLORS, ValidationResult } from '../types';\nimport { Node, SendMsg } from '../../store/flow-definition';\n\nexport const send_msg: ActionConfig = {\n name: 'Send Message',\n color: COLORS.send,\n render: (_node: Node, action: SendMsg) => {\n const text = action.text.replace(/\\n/g, '<br>');\n return html`\n ${unsafeHTML(text)}\n ${action.quick_replies?.length > 0\n ? html`<div class=\"quick-replies\">\n ${action.quick_replies.map((reply) => {\n return html`<div class=\"quick-reply\">${reply}</div>`;\n })}\n ${action.template\n ? html`<div\n style=\"border: 1px solid var(--color-widget-border);padding: 0.5em;margin-top: 1em;border-radius: var(--curvature); display:flex;background: rgba(0,0,0,.03);\"\n >\n <temba-icon name=\"channel_wac\"></temba-icon>\n <div style=\"margin-left:0.5em\">${action.template.name}</div>\n </div>`\n : null}\n </div>`\n : null}\n `;\n },\n form: {\n text: {\n type: 'textarea',\n label: 'Message Text',\n helpText:\n 'Enter the message to send. You can use expressions like @contact.name',\n required: true,\n evaluated: true,\n minHeight: 175\n },\n quick_replies: {\n type: 'select',\n label: 'Quick Replies',\n helpText: 'Add quick reply options for this message',\n options: [],\n multi: true,\n tags: true,\n searchable: true,\n placeholder: 'Add quick replies...',\n maxItems: 10,\n evaluated: true\n }\n },\n validate: (action: SendMsg): ValidationResult => {\n const errors: { [key: string]: string } = {};\n\n if (!action.text || action.text.trim() === '') {\n errors.text = 'Message text is required';\n }\n\n return {\n valid: Object.keys(errors).length === 0,\n errors\n };\n }\n};\n"]}
1
+ {"version":3,"file":"send_msg.js","sourceRoot":"","sources":["../../../../src/flow/actions/send_msg.ts"],"names":[],"mappings":"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","sourcesContent":["import { html } from 'lit-html';\nimport { unsafeHTML } from 'lit-html/directives/unsafe-html.js';\nimport { ActionConfig, COLORS, ValidationResult } from '../types';\nimport { Node, SendMsg } from '../../store/flow-definition';\n\nexport const send_msg: ActionConfig = {\n name: 'Send Message',\n color: COLORS.send,\n render: (_node: Node, action: SendMsg) => {\n const text = action.text.replace(/\\n/g, '<br>');\n return html`\n ${unsafeHTML(text)}\n ${action.quick_replies?.length > 0\n ? html`<div class=\"quick-replies\">\n ${action.quick_replies.map((reply) => {\n return html`<div class=\"quick-reply\">${reply}</div>`;\n })}\n ${action.template\n ? html`<div\n style=\"border: 1px solid var(--color-widget-border);padding: 0.5em;margin-top: 1em;border-radius: var(--curvature); display:flex;background: rgba(0,0,0,.03);\"\n >\n <temba-icon name=\"channel_wac\"></temba-icon>\n <div style=\"margin-left:0.5em\">${action.template.name}</div>\n </div>`\n : null}\n </div>`\n : null}\n `;\n },\n form: {\n text: {\n type: 'message-editor',\n label: 'Message',\n helpText:\n 'Enter the message to send with optional attachments. You can use expressions like @contact.name',\n required: true,\n evaluated: true,\n placeholder: 'Type your message here...',\n maxAttachments: 10,\n accept: '',\n endpoint: '/api/v2/media.json',\n counter: 'temba-charcount',\n gsm: true,\n autogrow: true\n },\n quick_replies: {\n type: 'select',\n options: [],\n multi: true,\n tags: true,\n searchable: true,\n placeholder: 'Add quick replies...',\n maxItems: 10,\n evaluated: true\n },\n runtime_attachments: {\n type: 'array',\n helpText: 'Add dynamic attachments using expressions',\n itemLabel: 'Attachment',\n maxItems: 10,\n isEmptyItem: (item: any) => {\n return !item.expression || item.expression.trim() === '';\n },\n itemConfig: {\n type: {\n type: 'select',\n options: [\n { value: 'image', label: 'Image' },\n { value: 'audio', label: 'Audio' },\n { value: 'video', label: 'Video' },\n { value: 'document', label: 'Document' }\n ],\n required: true,\n searchable: false\n },\n expression: {\n type: 'text',\n placeholder: 'Expression (e.g. @contact.photo)',\n required: true,\n evaluated: true\n }\n }\n }\n },\n layout: [\n 'text',\n {\n type: 'group',\n label: 'Quick Replies',\n items: ['quick_replies'],\n collapsible: true,\n collapsed: (formData: any) => {\n // Collapse only if there are no quick replies\n return !formData.quick_replies || formData.quick_replies.length === 0;\n },\n getGroupValueCount: (formData: any) => {\n return formData.quick_replies?.length || 0;\n }\n },\n {\n type: 'group',\n label: 'Runtime Attachments',\n items: ['runtime_attachments'],\n collapsible: true,\n collapsed: true,\n helpText: 'Add dynamic attachments that are evaluated at runtime',\n getGroupValueCount: (formData: any) => {\n return (\n formData.runtime_attachments?.filter(\n (item: any) =>\n item && item.expression && item.expression.trim() !== ''\n ).length || 0\n );\n }\n }\n ],\n toFormData: (action: SendMsg) => {\n // Extract runtime attachments from the text field attachments\n const runtimeAttachments: { type: string; expression: string }[] = [];\n const staticAttachments: string[] = [];\n\n if (action.attachments && Array.isArray(action.attachments)) {\n action.attachments.forEach((attachment) => {\n if (typeof attachment === 'string' && attachment.includes(':')) {\n const colonIndex = attachment.indexOf(':');\n const contentType = attachment.substring(0, colonIndex);\n const value = attachment.substring(colonIndex + 1);\n\n if (!contentType.includes('/')) {\n // This is a runtime attachment\n runtimeAttachments.push({\n type: contentType,\n expression: value\n });\n } else {\n // This is a static attachment\n staticAttachments.push(attachment);\n }\n }\n });\n }\n\n return {\n uuid: action.uuid,\n text: action.text || '',\n attachments: staticAttachments,\n runtime_attachments: runtimeAttachments,\n quick_replies: (action.quick_replies || []).map((reply) => ({\n name: reply,\n value: reply\n }))\n };\n },\n fromFormData: (data: Record<string, any>) => {\n const result = {\n uuid: data.uuid,\n type: 'send_msg',\n text: data.text || '',\n attachments: [],\n quick_replies: (data.quick_replies || []).map((reply: any) =>\n typeof reply === 'string' ? reply : reply.value || reply.name || reply\n )\n };\n\n // Combine static attachments from text field with runtime attachments\n const staticAttachments = data.attachments || [];\n const runtimeAttachments = (data.runtime_attachments || [])\n .filter((item: any) => item && item.type && item.expression) // Filter out invalid items\n .map(\n (item: { type: string; expression: string }) =>\n `${item.type}:${item.expression}`\n );\n\n result.attachments = [...staticAttachments, ...runtimeAttachments];\n\n // Remove quick_replies if empty to match original format\n if (result.quick_replies.length === 0) {\n delete (result as any).quick_replies;\n }\n\n return result as SendMsg;\n },\n sanitize: (formData: any): void => {\n if (formData.text && typeof formData.text === 'string') {\n formData.text = formData.text.trim();\n }\n },\n validate: (action: SendMsg): ValidationResult => {\n const errors: { [key: string]: string } = {};\n\n if (!action.text || action.text.trim() === '') {\n errors.text = 'Message text is required';\n }\n\n const attachments = action.attachments || [];\n if (attachments.length > 10) {\n const staticAttachments = attachments.filter(\n (attachment) =>\n typeof attachment === 'string' &&\n attachment.substring(0, attachment.indexOf(':')).includes('/')\n );\n\n const runtimeAttachments = attachments.filter(\n (attachment) =>\n typeof attachment === 'string' &&\n !attachment.substring(0, attachment.indexOf(':')).includes('/')\n );\n\n if (runtimeAttachments.length > 0) {\n errors.runtime_attachments =\n 'Each message can only have up to 10 attachments';\n }\n\n if (staticAttachments.length > 0) {\n const message = 'Each message can only have up to 10 total attachments';\n if (errors.text) {\n errors.text += ` ${message}`;\n } else {\n errors.text = message;\n }\n }\n }\n\n return {\n valid: Object.keys(errors).length === 0,\n errors\n };\n }\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/flow/types.ts"],"names":[],"mappings":"AAiQA,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,SAAS,EAAE,SAAS;IACpB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,SAAS;IAChB,OAAO,EAAE,SAAS;IAClB,IAAI,EAAE,SAAS;IACf,GAAG,EAAE,SAAS;IACd,MAAM,EAAE,SAAS;CAClB,CAAC;AAEF,iCAAiC;AACjC,MAAM,UAAU,mBAAmB,CAAC,KAAU;IAC5C,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,cAAc,CAAC,CAAC,+BAA+B;IACxD,CAAC;IACD,sDAAsD;IACtD,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,wBAAwB,CAAC,KAAU;IACjD,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO;YACL,MAAM,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE;SACnC,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO;YACL,MAAM,EAAE;gBACN,IAAI,EAAE,iBAAiB;gBACvB,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;aAC/B;SACF,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;YACrD,OAAO;gBACL,MAAM,EAAE;oBACN,IAAI,EAAE,cAAc;oBACpB,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;iBACxC;aACF,CAAC;QACJ,CAAC;QACD,OAAO;YACL,MAAM,EAAE;gBACN,IAAI,EAAE,cAAc;gBACpB,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;aAC5B;SACF,CAAC;IACJ,CAAC;IACD,OAAO;QACL,MAAM,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE;KACpC,CAAC;AACJ,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,iBAAiB,CAC/B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,iBAAiB,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,kBAAkB,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,cAAc,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,gBAAgB,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,cAAc,CAAC;AACxC,CAAC","sourcesContent":["import { TemplateResult } from 'lit-html';\nimport { Action } from '../store/flow-definition';\n\nexport interface ValidationResult {\n valid: boolean;\n errors: { [key: string]: string };\n}\n\n// Component attribute interfaces - these define what's allowed for each component type\nexport interface TextInputAttributes {\n type?: 'text' | 'email' | 'number' | 'url' | 'tel';\n placeholder?: string;\n clearable?: boolean;\n maxlength?: number;\n gsm?: boolean;\n autogrow?: boolean;\n textarea?: boolean;\n submitOnEnter?: boolean;\n}\n\nexport interface CompletionAttributes {\n placeholder?: string;\n clearable?: boolean;\n maxlength?: number;\n gsm?: boolean;\n autogrow?: boolean;\n textarea?: boolean;\n expressions?: string;\n counter?: string;\n minHeight?: number;\n}\n\nexport interface SelectAttributes {\n placeholder?: string;\n multi?: boolean;\n searchable?: boolean;\n tags?: boolean;\n emails?: boolean;\n clearable?: boolean;\n endpoint?: string;\n valueKey?: string;\n nameKey?: string;\n queryParam?: string;\n maxItems?: number;\n maxItemsText?: string;\n expressions?: string;\n options?: Array<{ name: string; value: any }>;\n sorted?: boolean;\n allowCreate?: boolean;\n jsonValue?: boolean;\n spaceSelect?: boolean;\n infoText?: string;\n}\n\nexport interface CheckboxAttributes {\n label?: string;\n size?: number;\n disabled?: boolean;\n animateChange?: string;\n}\n\nexport interface SliderAttributes {\n min?: number;\n max?: number;\n range?: boolean;\n}\n\n// Widget configuration using discriminated union for type safety\nexport type WidgetConfig =\n | { type: 'temba-textinput'; attributes?: TextInputAttributes }\n | { type: 'temba-completion'; attributes?: CompletionAttributes }\n | { type: 'temba-select'; attributes?: SelectAttributes }\n | { type: 'temba-checkbox'; attributes?: CheckboxAttributes }\n | { type: 'temba-slider'; attributes?: SliderAttributes }\n | { type: string; attributes?: { [key: string]: any } }; // Generic fallback\n\n// Property configuration with the clean structure you want\nexport interface PropertyConfig {\n // Form field metadata\n label?: string;\n helpText?: string;\n required?: boolean;\n maxLength?: number;\n minLength?: number;\n pattern?: string;\n\n // Widget configuration\n widget: WidgetConfig;\n\n // Conditional behavior based on other field values\n conditions?: {\n // When to show this field\n visible?: (formData: any) => boolean;\n\n // When this field is disabled\n disabled?: (formData: any) => boolean;\n };\n}\n\nexport interface NodeConfig {\n type: string;\n name?: string;\n color?: string;\n action?: ActionConfig;\n router?: {\n type: 'switch' | 'random';\n defaultCategory?: string;\n operand?: string;\n configurable?: boolean; // can the rules be configured in the UI\n rules?: {\n type: 'has_number_between' | 'has_string' | 'has_value' | 'has_not_value';\n arguments: string[];\n categoryName: string;\n }[];\n };\n properties?: { [key: string]: PropertyConfig };\n toFormData?: (node: any) => any;\n fromFormData?: (formData: any, originalNode: any) => any;\n}\n\n// New field configuration system for generic form generation\nexport interface BaseFieldConfig {\n label?: string;\n required?: boolean;\n evaluated?: boolean; // if this field supports expression evaluation\n dependsOn?: string[]; // fields this field depends on\n computeValue?: (\n values: Record<string, any>,\n currentValue: any,\n originalValues?: Record<string, any>\n ) => any;\n\n // Validation properties\n minLength?: number;\n maxLength?: number;\n pattern?: string;\n helpText?: string;\n\n // Layout properties\n maxWidth?: string; // CSS max-width value (e.g., '200px', '50%', '10rem')\n\n // Conditional rendering\n conditions?: {\n visible?: (formData: Record<string, any>) => boolean;\n disabled?: (formData: Record<string, any>) => boolean;\n };\n}\n\nexport interface TextFieldConfig extends BaseFieldConfig {\n type: 'text';\n placeholder?: string;\n}\n\nexport interface TextareaFieldConfig extends BaseFieldConfig {\n type: 'textarea';\n placeholder?: string;\n rows?: number;\n minHeight?: number;\n}\n\nexport interface SelectFieldConfig extends BaseFieldConfig {\n type: 'select';\n options: string[] | { value: string; label: string }[];\n multi?: boolean;\n searchable?: boolean;\n tags?: boolean;\n placeholder?: string;\n maxItems?: number;\n valueKey?: string;\n nameKey?: string;\n endpoint?: string;\n emails?: boolean;\n}\n\nexport interface KeyValueFieldConfig extends BaseFieldConfig {\n type: 'key-value';\n sortable?: boolean;\n keyPlaceholder?: string;\n valuePlaceholder?: string;\n minRows?: number;\n}\n\nexport interface ArrayFieldConfig extends BaseFieldConfig {\n type: 'array';\n itemConfig: Record<string, FieldConfig>;\n sortable?: boolean;\n minItems?: number;\n maxItems?: number;\n itemLabel?: string;\n onItemChange?: (\n itemIndex: number,\n field: string,\n value: any,\n allItems: any[]\n ) => any[];\n}\n\nexport interface CheckboxFieldConfig extends BaseFieldConfig {\n type: 'checkbox';\n size?: number;\n animateChange?: string;\n}\n\nexport type FieldConfig =\n | TextFieldConfig\n | TextareaFieldConfig\n | SelectFieldConfig\n | KeyValueFieldConfig\n | ArrayFieldConfig\n | CheckboxFieldConfig;\n\n// Layout configurations for better form organization\n// Recursive layout system - any layout item can contain other layout items\n\nexport interface FieldItemConfig {\n type: 'field';\n field: string; // field name to render\n}\n\nexport interface RowLayoutConfig {\n type: 'row';\n items: LayoutItem[]; // can contain fields, groups, or other rows\n gap?: string; // CSS gap value, defaults to '1rem'\n}\n\nexport interface GroupLayoutConfig {\n type: 'group';\n label: string;\n items: LayoutItem[]; // can contain fields, rows, or other groups\n collapsible?: boolean;\n collapsed?: boolean; // initial state if collapsible\n helpText?: string;\n}\n\nexport type LayoutItem =\n | FieldItemConfig\n | RowLayoutConfig\n | GroupLayoutConfig\n | string; // string is shorthand for field\n\nexport interface ActionConfig {\n name: string;\n color: string;\n evaluated?: string[];\n render?: (node: any, action: any) => TemplateResult;\n\n form?: Record<string, FieldConfig>;\n layout?: LayoutItem[]; // optional layout configuration - array of layout items\n\n // Action editor configuration (legacy)\n // Form-level transformations\n toFormData?: (action: Action) => any;\n fromFormData?: (formData: any) => Action;\n\n validate?: (action: Action) => ValidationResult;\n}\n\nexport const COLORS = {\n send: '#3498db',\n update: '#01c1af',\n broadcast: '#8e5ea7',\n call: '#e68628',\n create: '#df419f',\n save: '#1a777c',\n split: '#aaaaaa',\n execute: '#666666',\n wait: '#4d7dad',\n add: '#309c42',\n remove: '#e74c3c'\n};\n\n// Default property type mappings\nexport function getDefaultComponent(value: any): WidgetConfig['type'] {\n if (typeof value === 'boolean') {\n return 'temba-checkbox';\n }\n if (typeof value === 'number') {\n return 'temba-textinput';\n }\n if (Array.isArray(value)) {\n return 'temba-select'; // For arrays, use multi-select\n }\n // Default to text input for strings and unknown types\n return 'temba-textinput';\n}\n\n// Get component properties for default mappings with proper typing\nexport function getDefaultComponentProps(value: any): PropertyConfig {\n if (typeof value === 'boolean') {\n return {\n widget: { type: 'temba-checkbox' }\n };\n }\n if (typeof value === 'number') {\n return {\n widget: {\n type: 'temba-textinput',\n attributes: { type: 'number' }\n }\n };\n }\n if (Array.isArray(value)) {\n if (value.length > 0 && typeof value[0] === 'string') {\n return {\n widget: {\n type: 'temba-select',\n attributes: { multi: true, tags: true }\n }\n };\n }\n return {\n widget: {\n type: 'temba-select',\n attributes: { multi: true }\n }\n };\n }\n return {\n widget: { type: 'temba-textinput' }\n };\n}\n\n// Type guard functions for working with WidgetConfig\nexport function isTextInputWidget(\n config: WidgetConfig\n): config is { type: 'temba-textinput'; attributes?: TextInputAttributes } {\n return config.type === 'temba-textinput';\n}\n\nexport function isCompletionWidget(\n config: WidgetConfig\n): config is { type: 'temba-completion'; attributes?: CompletionAttributes } {\n return config.type === 'temba-completion';\n}\n\nexport function isSelectWidget(\n config: WidgetConfig\n): config is { type: 'temba-select'; attributes?: SelectAttributes } {\n return config.type === 'temba-select';\n}\n\nexport function isCheckboxWidget(\n config: WidgetConfig\n): config is { type: 'temba-checkbox'; attributes?: CheckboxAttributes } {\n return config.type === 'temba-checkbox';\n}\n\nexport function isSliderWidget(\n config: WidgetConfig\n): config is { type: 'slider'; attributes?: SliderAttributes } {\n return config.type === 'temba-slider';\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/flow/types.ts"],"names":[],"mappings":"AAoRA,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,SAAS,EAAE,SAAS;IACpB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,SAAS;IAChB,OAAO,EAAE,SAAS;IAClB,IAAI,EAAE,SAAS;IACf,GAAG,EAAE,SAAS;IACd,MAAM,EAAE,SAAS;CAClB,CAAC;AAEF,iCAAiC;AACjC,MAAM,UAAU,mBAAmB,CAAC,KAAU;IAC5C,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,gBAAgB,CAAC;IAC1B,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,cAAc,CAAC,CAAC,+BAA+B;IACxD,CAAC;IACD,sDAAsD;IACtD,OAAO,iBAAiB,CAAC;AAC3B,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,wBAAwB,CAAC,KAAU;IACjD,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO;YACL,MAAM,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE;SACnC,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO;YACL,MAAM,EAAE;gBACN,IAAI,EAAE,iBAAiB;gBACvB,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;aAC/B;SACF,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ,EAAE,CAAC;YACrD,OAAO;gBACL,MAAM,EAAE;oBACN,IAAI,EAAE,cAAc;oBACpB,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE;iBACxC;aACF,CAAC;QACJ,CAAC;QACD,OAAO;YACL,MAAM,EAAE;gBACN,IAAI,EAAE,cAAc;gBACpB,UAAU,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;aAC5B;SACF,CAAC;IACJ,CAAC;IACD,OAAO;QACL,MAAM,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE;KACpC,CAAC;AACJ,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,iBAAiB,CAC/B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,iBAAiB,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,kBAAkB,CAChC,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,kBAAkB,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,cAAc,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,gBAAgB,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,MAAoB;IAEpB,OAAO,MAAM,CAAC,IAAI,KAAK,cAAc,CAAC;AACxC,CAAC","sourcesContent":["import { TemplateResult } from 'lit-html';\nimport { Action } from '../store/flow-definition';\n\nexport interface ValidationResult {\n valid: boolean;\n errors: { [key: string]: string };\n}\n\n// Component attribute interfaces - these define what's allowed for each component type\nexport interface TextInputAttributes {\n type?: 'text' | 'email' | 'number' | 'url' | 'tel';\n placeholder?: string;\n clearable?: boolean;\n maxlength?: number;\n gsm?: boolean;\n autogrow?: boolean;\n textarea?: boolean;\n submitOnEnter?: boolean;\n}\n\nexport interface CompletionAttributes {\n placeholder?: string;\n clearable?: boolean;\n maxlength?: number;\n gsm?: boolean;\n autogrow?: boolean;\n textarea?: boolean;\n expressions?: string;\n counter?: string;\n minHeight?: number;\n}\n\nexport interface SelectAttributes {\n placeholder?: string;\n multi?: boolean;\n searchable?: boolean;\n tags?: boolean;\n emails?: boolean;\n clearable?: boolean;\n endpoint?: string;\n valueKey?: string;\n nameKey?: string;\n queryParam?: string;\n maxItems?: number;\n maxItemsText?: string;\n expressions?: string;\n options?: Array<{ name: string; value: any }>;\n sorted?: boolean;\n allowCreate?: boolean;\n jsonValue?: boolean;\n spaceSelect?: boolean;\n infoText?: string;\n}\n\nexport interface CheckboxAttributes {\n label?: string;\n size?: number;\n disabled?: boolean;\n animateChange?: string;\n}\n\nexport interface SliderAttributes {\n min?: number;\n max?: number;\n range?: boolean;\n}\n\n// Widget configuration using discriminated union for type safety\nexport type WidgetConfig =\n | { type: 'temba-textinput'; attributes?: TextInputAttributes }\n | { type: 'temba-completion'; attributes?: CompletionAttributes }\n | { type: 'temba-select'; attributes?: SelectAttributes }\n | { type: 'temba-checkbox'; attributes?: CheckboxAttributes }\n | { type: 'temba-slider'; attributes?: SliderAttributes }\n | { type: string; attributes?: { [key: string]: any } }; // Generic fallback\n\n// Property configuration with the clean structure you want\nexport interface PropertyConfig {\n // Form field metadata\n label?: string;\n helpText?: string;\n required?: boolean;\n maxLength?: number;\n minLength?: number;\n pattern?: string;\n\n // Widget configuration\n widget: WidgetConfig;\n\n // Conditional behavior based on other field values\n conditions?: {\n // When to show this field\n visible?: (formData: any) => boolean;\n\n // When this field is disabled\n disabled?: (formData: any) => boolean;\n };\n}\n\nexport interface NodeConfig {\n type: string;\n name?: string;\n color?: string;\n action?: ActionConfig;\n router?: {\n type: 'switch' | 'random';\n defaultCategory?: string;\n operand?: string;\n configurable?: boolean; // can the rules be configured in the UI\n rules?: {\n type: 'has_number_between' | 'has_string' | 'has_value' | 'has_not_value';\n arguments: string[];\n categoryName: string;\n }[];\n };\n properties?: { [key: string]: PropertyConfig };\n toFormData?: (node: any) => any;\n fromFormData?: (formData: any, originalNode: any) => any;\n}\n\n// New field configuration system for generic form generation\nexport interface BaseFieldConfig {\n label?: string;\n required?: boolean;\n evaluated?: boolean; // if this field supports expression evaluation\n dependsOn?: string[]; // fields this field depends on\n computeValue?: (\n values: Record<string, any>,\n currentValue: any,\n originalValues?: Record<string, any>\n ) => any;\n\n // Validation properties\n minLength?: number;\n maxLength?: number;\n pattern?: string;\n helpText?: string;\n\n // Layout properties\n maxWidth?: string; // CSS max-width value (e.g., '200px', '50%', '10rem')\n\n // Conditional rendering\n conditions?: {\n visible?: (formData: Record<string, any>) => boolean;\n disabled?: (formData: Record<string, any>) => boolean;\n };\n}\n\nexport interface TextFieldConfig extends BaseFieldConfig {\n type: 'text';\n placeholder?: string;\n}\n\nexport interface TextareaFieldConfig extends BaseFieldConfig {\n type: 'textarea';\n placeholder?: string;\n rows?: number;\n minHeight?: number;\n}\n\nexport interface SelectFieldConfig extends BaseFieldConfig {\n type: 'select';\n options: string[] | { value: string; label: string }[];\n multi?: boolean;\n clearable?: boolean;\n searchable?: boolean;\n tags?: boolean;\n placeholder?: string;\n maxItems?: number;\n valueKey?: string;\n nameKey?: string;\n endpoint?: string;\n emails?: boolean;\n flavor?: 'small' | 'large';\n}\n\nexport interface KeyValueFieldConfig extends BaseFieldConfig {\n type: 'key-value';\n sortable?: boolean;\n keyPlaceholder?: string;\n valuePlaceholder?: string;\n minRows?: number;\n}\n\nexport interface ArrayFieldConfig extends BaseFieldConfig {\n type: 'array';\n itemConfig: Record<string, FieldConfig>;\n sortable?: boolean;\n minItems?: number;\n maxItems?: number;\n itemLabel?: string;\n onItemChange?: (\n itemIndex: number,\n field: string,\n value: any,\n allItems: any[]\n ) => any[];\n isEmptyItem?: (item: any) => boolean;\n}\n\nexport interface CheckboxFieldConfig extends BaseFieldConfig {\n type: 'checkbox';\n size?: number;\n animateChange?: string;\n}\n\nexport interface MessageEditorFieldConfig extends BaseFieldConfig {\n type: 'message-editor';\n placeholder?: string;\n minHeight?: number;\n maxAttachments?: number;\n accept?: string;\n endpoint?: string;\n counter?: string;\n gsm?: boolean;\n autogrow?: boolean;\n disableCompletion?: boolean;\n}\n\nexport type FieldConfig =\n | TextFieldConfig\n | TextareaFieldConfig\n | SelectFieldConfig\n | KeyValueFieldConfig\n | ArrayFieldConfig\n | CheckboxFieldConfig\n | MessageEditorFieldConfig;\n\n// Layout configurations for better form organization\n// Recursive layout system - any layout item can contain other layout items\n\nexport interface FieldItemConfig {\n type: 'field';\n field: string; // field name to render\n}\n\nexport interface RowLayoutConfig {\n type: 'row';\n items: LayoutItem[]; // can contain fields, groups, or other rows\n gap?: string; // CSS gap value, defaults to '1rem'\n}\n\nexport interface GroupLayoutConfig {\n type: 'group';\n label: string;\n items: LayoutItem[]; // can contain fields, rows, or other groups\n collapsible?: boolean;\n collapsed?: boolean | ((formData: any) => boolean); // initial state if collapsible - can be a function\n helpText?: string;\n getGroupValueCount?: (formData: any) => number; // optional function to get count for bubble display\n}\n\nexport type LayoutItem =\n | FieldItemConfig\n | RowLayoutConfig\n | GroupLayoutConfig\n | string; // string is shorthand for field\n\nexport interface ActionConfig {\n name: string;\n color: string;\n evaluated?: string[];\n render?: (node: any, action: any) => TemplateResult;\n\n form?: Record<string, FieldConfig>;\n layout?: LayoutItem[]; // optional layout configuration - array of layout items\n\n // Action editor configuration (legacy)\n // Form-level transformations\n sanitize?: (formData: any) => any;\n toFormData?: (action: Action) => any;\n fromFormData?: (formData: any) => Action;\n\n validate?: (action: Action) => ValidationResult;\n}\n\nexport const COLORS = {\n send: '#3498db',\n update: '#01c1af',\n broadcast: '#8e5ea7',\n call: '#e68628',\n create: '#df419f',\n save: '#1a777c',\n split: '#aaaaaa',\n execute: '#666666',\n wait: '#4d7dad',\n add: '#309c42',\n remove: '#e74c3c'\n};\n\n// Default property type mappings\nexport function getDefaultComponent(value: any): WidgetConfig['type'] {\n if (typeof value === 'boolean') {\n return 'temba-checkbox';\n }\n if (typeof value === 'number') {\n return 'temba-textinput';\n }\n if (Array.isArray(value)) {\n return 'temba-select'; // For arrays, use multi-select\n }\n // Default to text input for strings and unknown types\n return 'temba-textinput';\n}\n\n// Get component properties for default mappings with proper typing\nexport function getDefaultComponentProps(value: any): PropertyConfig {\n if (typeof value === 'boolean') {\n return {\n widget: { type: 'temba-checkbox' }\n };\n }\n if (typeof value === 'number') {\n return {\n widget: {\n type: 'temba-textinput',\n attributes: { type: 'number' }\n }\n };\n }\n if (Array.isArray(value)) {\n if (value.length > 0 && typeof value[0] === 'string') {\n return {\n widget: {\n type: 'temba-select',\n attributes: { multi: true, tags: true }\n }\n };\n }\n return {\n widget: {\n type: 'temba-select',\n attributes: { multi: true }\n }\n };\n }\n return {\n widget: { type: 'temba-textinput' }\n };\n}\n\n// Type guard functions for working with WidgetConfig\nexport function isTextInputWidget(\n config: WidgetConfig\n): config is { type: 'temba-textinput'; attributes?: TextInputAttributes } {\n return config.type === 'temba-textinput';\n}\n\nexport function isCompletionWidget(\n config: WidgetConfig\n): config is { type: 'temba-completion'; attributes?: CompletionAttributes } {\n return config.type === 'temba-completion';\n}\n\nexport function isSelectWidget(\n config: WidgetConfig\n): config is { type: 'temba-select'; attributes?: SelectAttributes } {\n return config.type === 'temba-select';\n}\n\nexport function isCheckboxWidget(\n config: WidgetConfig\n): config is { type: 'temba-checkbox'; attributes?: CheckboxAttributes } {\n return config.type === 'temba-checkbox';\n}\n\nexport function isSliderWidget(\n config: WidgetConfig\n): config is { type: 'slider'; attributes?: SliderAttributes } {\n return config.type === 'temba-slider';\n}\n"]}
@@ -7,6 +7,7 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
7
7
  super();
8
8
  this.itemConfig = {};
9
9
  this.itemLabel = 'Item';
10
+ this.maintainEmptyItem = true; // Enable by default for better UX
10
11
  this._items = [];
11
12
  }
12
13
  // External API
@@ -19,7 +20,25 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
19
20
  }
20
21
  // Implement abstract methods
21
22
  isEmptyItem(item) {
22
- return Object.values(item).every((value) => value === undefined || value === null || value === '');
23
+ // Use configurable function if provided
24
+ if (this.isEmptyItemFn) {
25
+ return this.isEmptyItemFn(item);
26
+ }
27
+ // Default behavior: check if all values are empty
28
+ const values = Object.values(item);
29
+ if (values.length === 0) {
30
+ return true;
31
+ }
32
+ return values.every((value) => value === undefined || value === null || value === '');
33
+ }
34
+ // Override cleanItems to be more permissive for form data
35
+ cleanItems(items) {
36
+ // For runtime attachments, keep items that have at least one non-empty field
37
+ return items.filter((item) => {
38
+ const values = Object.values(item);
39
+ return (values.length > 0 &&
40
+ values.some((value) => value !== undefined && value !== null && value !== ''));
41
+ });
23
42
  }
24
43
  createEmptyItem() {
25
44
  return {};
@@ -44,9 +63,17 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
44
63
  if (config.computeValue) {
45
64
  return config.computeValue(item, currentValue);
46
65
  }
66
+ // For select fields, ensure we return the right type
67
+ if (config.type === 'select') {
68
+ const selectConfig = config;
69
+ if (currentValue === undefined || currentValue === null) {
70
+ return selectConfig.multi ? [] : '';
71
+ }
72
+ }
47
73
  return currentValue;
48
74
  }
49
75
  renderField(itemIndex, fieldName, config) {
76
+ var _a;
50
77
  const computedValue = this.computeFieldValue(itemIndex, fieldName, config);
51
78
  switch (config.type) {
52
79
  case 'text':
@@ -63,12 +90,64 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
63
90
  .rows=${config.rows || 3}
64
91
  @change=${(e) => this.handleFieldChange(itemIndex, fieldName, e.target.value)}
65
92
  ></temba-textinput>`;
66
- case 'select':
93
+ case 'select': {
94
+ const selectConfig = config;
95
+ const fieldValue = this.computeFieldValue(itemIndex, fieldName, config);
67
96
  return html `<temba-select
68
- .value=${computedValue || ''}
69
- .options=${config.options}
70
- @change=${(e) => this.handleFieldChange(itemIndex, fieldName, e.target.value)}
71
- ></temba-select>`;
97
+ class="form-control"
98
+ ?clearable="${selectConfig.clearable || false}"
99
+ ?searchable="${selectConfig.searchable || false}"
100
+ ?tags="${selectConfig.tags || false}"
101
+ ?multi="${selectConfig.multi || false}"
102
+ ?emails="${selectConfig.emails || false}"
103
+ placeholder="${selectConfig.placeholder || ''}"
104
+ maxItems="${selectConfig.maxItems || 0}"
105
+ valueKey="${selectConfig.valueKey || 'value'}"
106
+ nameKey="${selectConfig.nameKey || 'name'}"
107
+ endpoint="${selectConfig.endpoint || ''}"
108
+ value="${fieldValue || ''}"
109
+ flavor="small"
110
+ @change="${(e) => {
111
+ const target = e.target;
112
+ let value;
113
+ // For temba-select, extract the correct value
114
+ if (target.tagName === 'TEMBA-SELECT') {
115
+ if (target.multi || target.emails || target.tags) {
116
+ value = target.values || [];
117
+ }
118
+ else {
119
+ // Single select: extract value from first selected option
120
+ const values = target.values || [];
121
+ value =
122
+ values.length > 0 && values[0]
123
+ ? values[0].value !== undefined
124
+ ? values[0].value
125
+ : values[0]
126
+ : '';
127
+ }
128
+ }
129
+ else {
130
+ value = target.value;
131
+ }
132
+ this.handleFieldChange(itemIndex, fieldName, value);
133
+ }}"
134
+ >
135
+ ${(_a = selectConfig.options) === null || _a === void 0 ? void 0 : _a.map((option) => {
136
+ if (typeof option === 'string') {
137
+ return html `<temba-option
138
+ name="${option}"
139
+ value="${option}"
140
+ ></temba-option>`;
141
+ }
142
+ else {
143
+ return html `<temba-option
144
+ name="${option.label || option.name}"
145
+ value="${option.value}"
146
+ ></temba-option>`;
147
+ }
148
+ })}
149
+ </temba-select>`;
150
+ }
72
151
  default:
73
152
  return html `<span>Unsupported field type: ${config.type}</span>`;
74
153
  }
@@ -77,27 +156,23 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
77
156
  const canRemove = this.canRemoveItem(index);
78
157
  return html `
79
158
  <div class="array-item">
80
- <div class="item-header">
81
- <span class="item-title">${this.itemLabel} ${index + 1}</span>
159
+ <div class="item-fields">
160
+ ${Object.entries(this.itemConfig).map(([fieldName, config]) => html `
161
+ <div class="field">
162
+ ${this.renderField(index, fieldName, config)}
163
+ </div>
164
+ `)}
82
165
  ${canRemove
83
166
  ? html `
84
167
  <button
85
168
  @click=${() => this.removeItem(index)}
86
169
  class="remove-btn"
87
170
  >
88
- Remove
171
+ <temba-icon name="x"></temba-icon>
89
172
  </button>
90
173
  `
91
174
  : ''}
92
175
  </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
176
  </div>
102
177
  `;
103
178
  }
@@ -114,27 +189,15 @@ let TembaArrayEditor = class TembaArrayEditor extends BaseListEditor {
114
189
  };
115
190
  TembaArrayEditor.styles = css `
116
191
  .array-editor {
117
- border: 1px solid #e0e0e0;
118
- border-radius: 6px;
119
- padding: 16px;
120
- background: #fafafa;
121
192
  }
122
193
 
123
194
  .array-item {
124
- border: 1px solid #d0d0d0;
125
- border-radius: 4px;
126
- padding: 16px;
127
- margin-bottom: 12px;
128
- background: white;
129
195
  }
130
196
 
131
197
  .item-header {
132
198
  display: flex;
133
199
  justify-content: space-between;
134
200
  align-items: center;
135
- margin-bottom: 12px;
136
- padding-bottom: 8px;
137
- border-bottom: 1px solid #eee;
138
201
  }
139
202
 
140
203
  .item-title {
@@ -143,8 +206,17 @@ TembaArrayEditor.styles = css `
143
206
  }
144
207
 
145
208
  .item-fields {
146
- display: grid;
209
+ display: flex;
147
210
  gap: 12px;
211
+ align-items: center;
212
+ }
213
+
214
+ .field {
215
+ flex: 1;
216
+ }
217
+
218
+ .field:first-child {
219
+ flex: 0 0 140px; /* Fixed width for type dropdown */
148
220
  }
149
221
 
150
222
  .field label {
@@ -157,7 +229,7 @@ TembaArrayEditor.styles = css `
157
229
 
158
230
  .add-btn,
159
231
  .remove-btn {
160
- padding: 8px 16px;
232
+ padding: 8px;
161
233
  border: 1px solid #ccc;
162
234
  border-radius: 4px;
163
235
  background: white;
@@ -171,13 +243,8 @@ TembaArrayEditor.styles = css `
171
243
  }
172
244
 
173
245
  .remove-btn {
174
- background: #fff5f5;
175
- border-color: #fecaca;
176
- color: #dc2626;
177
- }
178
-
179
- .remove-btn:hover {
180
- background: #fef2f2;
246
+ background: #fefefe;
247
+ color: #999;
181
248
  }
182
249
  `;
183
250
  __decorate([
@@ -189,6 +256,12 @@ __decorate([
189
256
  __decorate([
190
257
  property({ type: Function })
191
258
  ], TembaArrayEditor.prototype, "onItemChange", void 0);
259
+ __decorate([
260
+ property({ type: Function })
261
+ ], TembaArrayEditor.prototype, "isEmptyItemFn", void 0);
262
+ __decorate([
263
+ property({ type: Boolean })
264
+ ], TembaArrayEditor.prototype, "maintainEmptyItem", void 0);
192
265
  __decorate([
193
266
  property({ type: Array })
194
267
  ], 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';\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 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 const selectConfig = config as SelectFieldConfig;\n const fieldValue = this.computeFieldValue(itemIndex, fieldName, config);\n\n return html`<temba-select\n class=\"form-control\"\n ?clearable=\"${selectConfig.clearable || false}\"\n ?searchable=\"${selectConfig.searchable || false}\"\n ?tags=\"${selectConfig.tags || false}\"\n ?multi=\"${selectConfig.multi || false}\"\n ?emails=\"${selectConfig.emails || false}\"\n placeholder=\"${selectConfig.placeholder || ''}\"\n maxItems=\"${selectConfig.maxItems || 0}\"\n valueKey=\"${selectConfig.valueKey || 'value'}\"\n nameKey=\"${selectConfig.nameKey || 'name'}\"\n endpoint=\"${selectConfig.endpoint || ''}\"\n value=\"${fieldValue || ''}\"\n flavor=\"small\"\n @change=\"${(e: Event) => {\n const target = e.target as any;\n let value: any;\n\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\n this.handleFieldChange(itemIndex, fieldName, value);\n }}\"\n >\n ${selectConfig.options?.map((option: any) => {\n if (typeof option === 'string') {\n return html`<temba-option\n name=\"${option}\"\n value=\"${option}\"\n ></temba-option>`;\n } else {\n return html`<temba-option\n name=\"${option.label || option.name}\"\n value=\"${option.value}\"\n ></temba-option>`;\n }\n })}\n </temba-select>`;\n }\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-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:first-child {\n flex: 0 0 140px; /* Fixed width for type dropdown */\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,