@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.
- package/.devcontainer/Dockerfile +11 -4
- package/.devcontainer/devcontainer.json +3 -2
- package/.github/workflows/build.yml +4 -14
- package/CHANGELOG.md +8 -3
- package/demo/components/flow/example.html +1 -1
- package/demo/components/message-editor/example.html +125 -0
- package/demo/components/textinput/completion.html +1 -0
- package/demo/data/flows/food-order.json +12 -21
- package/demo/data/flows/sample-flow.json +42 -26
- package/dist/temba-components.js +506 -218
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Thumbnail.js +2 -1
- package/out-tsc/src/display/Thumbnail.js.map +1 -1
- package/out-tsc/src/events.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +245 -22
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/actions/call_webhook.js +26 -17
- package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
- package/out-tsc/src/flow/actions/send_msg.js +147 -6
- package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +111 -38
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/BaseListEditor.js +19 -4
- package/out-tsc/src/form/BaseListEditor.js.map +1 -1
- package/out-tsc/src/form/FormField.js +1 -1
- package/out-tsc/src/form/FormField.js.map +1 -1
- package/out-tsc/src/form/KeyValueEditor.js +1 -1
- package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
- package/out-tsc/src/form/MediaPicker.js +13 -1
- package/out-tsc/src/form/MediaPicker.js.map +1 -1
- package/out-tsc/src/form/MessageEditor.js +422 -0
- package/out-tsc/src/form/MessageEditor.js.map +1 -0
- package/out-tsc/src/form/TextInput.js +12 -5
- package/out-tsc/src/form/TextInput.js.map +1 -1
- package/out-tsc/src/form/select/Select.js +4 -4
- package/out-tsc/src/form/select/Select.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +27 -2
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/temba-modules.js +2 -0
- package/out-tsc/temba-modules.js.map +1 -1
- package/out-tsc/test/temba-field-config.test.js +4 -2
- package/out-tsc/test/temba-field-config.test.js.map +1 -1
- package/out-tsc/test/temba-message-editor.test.js +194 -0
- package/out-tsc/test/temba-message-editor.test.js.map +1 -0
- package/out-tsc/test/temba-node-editor.test.js +71 -0
- package/out-tsc/test/temba-node-editor.test.js.map +1 -1
- package/out-tsc/test/temba-select.test.js +1 -1
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/out-tsc/test/temba-textinput.test.js +16 -0
- package/out-tsc/test/temba-textinput.test.js.map +1 -1
- package/out-tsc/test/temba-webchat.test.js +4 -0
- package/out-tsc/test/temba-webchat.test.js.map +1 -1
- package/out-tsc/test/utils.test.js +2 -8
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +7 -4
- package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
- package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
- package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
- package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
- package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
- package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
- package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
- package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
- package/screenshots/truth/editor/send_msg.png +0 -0
- package/screenshots/truth/editor/set_contact_language.png +0 -0
- package/screenshots/truth/editor/set_contact_name.png +0 -0
- package/screenshots/truth/editor/set_run_result.png +0 -0
- package/screenshots/truth/formfield/markdown-errors.png +0 -0
- package/screenshots/truth/formfield/no-errors.png +0 -0
- package/screenshots/truth/formfield/plain-text-errors.png +0 -0
- package/screenshots/truth/message-editor/autogrow-initial-content.png +0 -0
- package/screenshots/truth/message-editor/default.png +0 -0
- package/screenshots/truth/message-editor/drag-highlight.png +0 -0
- package/screenshots/truth/message-editor/filtered-attachments.png +0 -0
- package/screenshots/truth/message-editor/with-completion.png +0 -0
- package/screenshots/truth/message-editor/with-properties.png +0 -0
- package/screenshots/truth/textinput/autogrow-initial.png +0 -0
- package/screenshots/truth/textinput/input-form.png +0 -0
- package/src/display/Thumbnail.ts +2 -1
- package/src/events.ts +5 -0
- package/src/flow/NodeEditor.ts +269 -23
- package/src/flow/actions/call_webhook.ts +28 -18
- package/src/flow/actions/send_msg.ts +170 -6
- package/src/flow/types.ts +21 -2
- package/src/form/ArrayEditor.ts +120 -42
- package/src/form/BaseListEditor.ts +22 -6
- package/src/form/FormField.ts +1 -1
- package/src/form/KeyValueEditor.ts +1 -1
- package/src/form/MediaPicker.ts +13 -1
- package/src/form/MessageEditor.ts +449 -0
- package/src/form/TextInput.ts +15 -7
- package/src/form/select/Select.ts +4 -4
- package/src/live/ContactChat.ts +30 -4
- package/static/css/temba-components.css +2 -0
- package/static/mr/docs/en-us/editor.json +2588 -0
- package/stress-test.js +138 -0
- package/temba-modules.ts +2 -0
- package/test/temba-field-config.test.ts +4 -2
- package/test/temba-message-editor.test.ts +300 -0
- package/test/temba-node-editor.test.ts +94 -0
- package/test/temba-select.test.ts +1 -1
- package/test/temba-textinput.test.ts +26 -0
- package/test/temba-webchat.test.ts +5 -0
- package/test/utils.test.ts +2 -13
- package/test-assets/contacts/history.json +19 -0
- package/test-assets/style.css +2 -0
- package/web-dev-mock.mjs +433 -0
- package/web-dev-server.config.mjs +51 -5
- 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: '
|
|
32
|
-
label: 'Message
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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-
|
|
81
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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: #
|
|
175
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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="
|
|
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
|
|
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,
|