@nyaruka/temba-components 0.129.6 → 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 +25 -1
- package/demo/components/flow/example.html +9 -2
- package/demo/components/flow/index.html +206 -0
- package/demo/components/message-editor/example.html +125 -0
- package/demo/components/textinput/completion.html +1 -0
- package/demo/data/flows/food-order.json +132 -0
- package/demo/data/flows/sample-flow.json +40 -24
- package/demo/index.html +1 -1
- package/dist/temba-components.js +518 -220
- 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/CanvasNode.js +10 -2
- package/out-tsc/src/flow/CanvasNode.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/StickyNote.js +1 -1
- package/out-tsc/src/flow/StickyNote.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_email.js +1 -2
- package/out-tsc/src/flow/actions/send_email.js.map +1 -1
- package/out-tsc/src/flow/actions/send_msg.js +155 -7
- 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 +29 -4
- 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/add_contact_groups/render/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/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/remove_contact_groups/render/cleanup-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/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_email/render/complex-business-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
- package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/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/actions/send_msg/render/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/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/sticky-note/blue-color.png +0 -0
- package/screenshots/truth/sticky-note/blue.png +0 -0
- package/screenshots/truth/sticky-note/color-picker-expanded.png +0 -0
- package/screenshots/truth/sticky-note/default.png +0 -0
- package/screenshots/truth/sticky-note/gray-color.png +0 -0
- package/screenshots/truth/sticky-note/gray.png +0 -0
- package/screenshots/truth/sticky-note/green-color.png +0 -0
- package/screenshots/truth/sticky-note/green.png +0 -0
- package/screenshots/truth/sticky-note/pink-color.png +0 -0
- package/screenshots/truth/sticky-note/pink.png +0 -0
- package/screenshots/truth/sticky-note/yellow-color.png +0 -0
- package/screenshots/truth/sticky-note/yellow.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 +6 -2
- package/src/flow/CanvasNode.ts +10 -2
- package/src/flow/NodeEditor.ts +269 -23
- package/src/flow/StickyNote.ts +1 -1
- package/src/flow/actions/call_webhook.ts +28 -18
- package/src/flow/actions/send_email.ts +1 -2
- package/src/flow/actions/send_msg.ts +178 -7
- 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 +32 -6
- package/src/store/flow-definition.d.ts +25 -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 +20 -2
- package/test-assets/style.css +2 -0
- package/web-dev-mock.mjs +433 -0
- package/web-dev-server.config.mjs +71 -6
- package/web-test-runner.config.mjs +9 -4
|
@@ -2,6 +2,19 @@ import { html } from 'lit-html';
|
|
|
2
2
|
import { ActionConfig, COLORS } from '../types';
|
|
3
3
|
import { Node, CallWebhook } from '../../store/flow-definition';
|
|
4
4
|
|
|
5
|
+
const defaultPost = `@(json(object(
|
|
6
|
+
"contact", object(
|
|
7
|
+
"uuid", contact.uuid,
|
|
8
|
+
"name", contact.name,
|
|
9
|
+
"urn", contact.urn
|
|
10
|
+
),
|
|
11
|
+
"flow", object(
|
|
12
|
+
"uuid", run.flow.uuid,
|
|
13
|
+
"name", run.flow.name
|
|
14
|
+
),
|
|
15
|
+
"results", foreach_value(results, extract_object, "value", "category")
|
|
16
|
+
)))`;
|
|
17
|
+
|
|
5
18
|
export const call_webhook: ActionConfig = {
|
|
6
19
|
name: 'Call Webhook',
|
|
7
20
|
color: COLORS.call,
|
|
@@ -19,7 +32,7 @@ export const call_webhook: ActionConfig = {
|
|
|
19
32
|
required: true,
|
|
20
33
|
options: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH'],
|
|
21
34
|
maxWidth: '120px',
|
|
22
|
-
searchable:
|
|
35
|
+
searchable: false
|
|
23
36
|
},
|
|
24
37
|
url: {
|
|
25
38
|
type: 'text',
|
|
@@ -51,23 +64,10 @@ export const call_webhook: ActionConfig = {
|
|
|
51
64
|
? values.method[0].value || values.method[0].name
|
|
52
65
|
: values.method;
|
|
53
66
|
|
|
54
|
-
const defaultTemplate = `@(json(object(
|
|
55
|
-
"contact", object(
|
|
56
|
-
"uuid", contact.uuid,
|
|
57
|
-
"name", contact.name,
|
|
58
|
-
"urn", contact.urn
|
|
59
|
-
),
|
|
60
|
-
"flow", object(
|
|
61
|
-
"uuid", run.flow.uuid,
|
|
62
|
-
"name", run.flow.name
|
|
63
|
-
),
|
|
64
|
-
"results", foreach_value(results, extract_object, "value", "category")
|
|
65
|
-
)))`;
|
|
66
|
-
|
|
67
67
|
if (method === 'POST') {
|
|
68
68
|
// For POST, provide the template if body is empty or was never set by user
|
|
69
69
|
if (!currentValue || currentValue.trim() === '') {
|
|
70
|
-
return
|
|
70
|
+
return defaultPost;
|
|
71
71
|
}
|
|
72
72
|
} else {
|
|
73
73
|
// For non-POST methods, clear the body if it was auto-generated or empty
|
|
@@ -80,7 +80,7 @@ export const call_webhook: ActionConfig = {
|
|
|
80
80
|
isOriginallyEmpty ||
|
|
81
81
|
!currentValue ||
|
|
82
82
|
currentValue.trim() === '' ||
|
|
83
|
-
currentValue.trim() ===
|
|
83
|
+
currentValue.trim() === defaultPost.trim()
|
|
84
84
|
) {
|
|
85
85
|
return '';
|
|
86
86
|
}
|
|
@@ -100,7 +100,10 @@ export const call_webhook: ActionConfig = {
|
|
|
100
100
|
items: ['headers'],
|
|
101
101
|
collapsible: true,
|
|
102
102
|
collapsed: true,
|
|
103
|
-
helpText: 'Configure authentication or custom headers'
|
|
103
|
+
helpText: 'Configure authentication or custom headers',
|
|
104
|
+
getGroupValueCount: (formData: any) => {
|
|
105
|
+
return formData.headers?.length + 10 || 0;
|
|
106
|
+
}
|
|
104
107
|
},
|
|
105
108
|
{
|
|
106
109
|
type: 'group',
|
|
@@ -108,7 +111,14 @@ export const call_webhook: ActionConfig = {
|
|
|
108
111
|
items: ['body'],
|
|
109
112
|
collapsible: true,
|
|
110
113
|
collapsed: true,
|
|
111
|
-
helpText: 'Configure the request payload'
|
|
114
|
+
helpText: 'Configure the request payload',
|
|
115
|
+
getGroupValueCount: (formData: any) => {
|
|
116
|
+
return !!(
|
|
117
|
+
formData.body &&
|
|
118
|
+
formData.body.trim() !== '' &&
|
|
119
|
+
formData.body !== defaultPost
|
|
120
|
+
);
|
|
121
|
+
}
|
|
112
122
|
}
|
|
113
123
|
],
|
|
114
124
|
toFormData: (action: CallWebhook) => {
|
|
@@ -15,25 +15,36 @@ export const send_msg: ActionConfig = {
|
|
|
15
15
|
${action.quick_replies.map((reply) => {
|
|
16
16
|
return html`<div class="quick-reply">${reply}</div>`;
|
|
17
17
|
})}
|
|
18
|
+
${action.template
|
|
19
|
+
? html`<div
|
|
20
|
+
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);"
|
|
21
|
+
>
|
|
22
|
+
<temba-icon name="channel_wac"></temba-icon>
|
|
23
|
+
<div style="margin-left:0.5em">${action.template.name}</div>
|
|
24
|
+
</div>`
|
|
25
|
+
: null}
|
|
18
26
|
</div>`
|
|
19
27
|
: null}
|
|
20
28
|
`;
|
|
21
29
|
},
|
|
22
30
|
form: {
|
|
23
31
|
text: {
|
|
24
|
-
type: '
|
|
25
|
-
label: 'Message
|
|
32
|
+
type: 'message-editor',
|
|
33
|
+
label: 'Message',
|
|
26
34
|
helpText:
|
|
27
|
-
'Enter the message to send. You can use expressions like @contact.name',
|
|
35
|
+
'Enter the message to send with optional attachments. You can use expressions like @contact.name',
|
|
28
36
|
required: true,
|
|
29
37
|
evaluated: true,
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
placeholder: 'Type your message here...',
|
|
39
|
+
maxAttachments: 10,
|
|
40
|
+
accept: '',
|
|
41
|
+
endpoint: '/api/v2/media.json',
|
|
42
|
+
counter: 'temba-charcount',
|
|
43
|
+
gsm: true,
|
|
44
|
+
autogrow: true
|
|
32
45
|
},
|
|
33
46
|
quick_replies: {
|
|
34
47
|
type: 'select',
|
|
35
|
-
label: 'Quick Replies',
|
|
36
|
-
helpText: 'Add quick reply options for this message',
|
|
37
48
|
options: [],
|
|
38
49
|
multi: true,
|
|
39
50
|
tags: true,
|
|
@@ -41,6 +52,137 @@ export const send_msg: ActionConfig = {
|
|
|
41
52
|
placeholder: 'Add quick replies...',
|
|
42
53
|
maxItems: 10,
|
|
43
54
|
evaluated: true
|
|
55
|
+
},
|
|
56
|
+
runtime_attachments: {
|
|
57
|
+
type: 'array',
|
|
58
|
+
helpText: 'Add dynamic attachments using expressions',
|
|
59
|
+
itemLabel: 'Attachment',
|
|
60
|
+
maxItems: 10,
|
|
61
|
+
isEmptyItem: (item: any) => {
|
|
62
|
+
return !item.expression || item.expression.trim() === '';
|
|
63
|
+
},
|
|
64
|
+
itemConfig: {
|
|
65
|
+
type: {
|
|
66
|
+
type: 'select',
|
|
67
|
+
options: [
|
|
68
|
+
{ value: 'image', label: 'Image' },
|
|
69
|
+
{ value: 'audio', label: 'Audio' },
|
|
70
|
+
{ value: 'video', label: 'Video' },
|
|
71
|
+
{ value: 'document', label: 'Document' }
|
|
72
|
+
],
|
|
73
|
+
required: true,
|
|
74
|
+
searchable: false
|
|
75
|
+
},
|
|
76
|
+
expression: {
|
|
77
|
+
type: 'text',
|
|
78
|
+
placeholder: 'Expression (e.g. @contact.photo)',
|
|
79
|
+
required: true,
|
|
80
|
+
evaluated: true
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
layout: [
|
|
86
|
+
'text',
|
|
87
|
+
{
|
|
88
|
+
type: 'group',
|
|
89
|
+
label: 'Quick Replies',
|
|
90
|
+
items: ['quick_replies'],
|
|
91
|
+
collapsible: true,
|
|
92
|
+
collapsed: (formData: any) => {
|
|
93
|
+
// Collapse only if there are no quick replies
|
|
94
|
+
return !formData.quick_replies || formData.quick_replies.length === 0;
|
|
95
|
+
},
|
|
96
|
+
getGroupValueCount: (formData: any) => {
|
|
97
|
+
return formData.quick_replies?.length || 0;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
type: 'group',
|
|
102
|
+
label: 'Runtime Attachments',
|
|
103
|
+
items: ['runtime_attachments'],
|
|
104
|
+
collapsible: true,
|
|
105
|
+
collapsed: true,
|
|
106
|
+
helpText: 'Add dynamic attachments that are evaluated at runtime',
|
|
107
|
+
getGroupValueCount: (formData: any) => {
|
|
108
|
+
return (
|
|
109
|
+
formData.runtime_attachments?.filter(
|
|
110
|
+
(item: any) =>
|
|
111
|
+
item && item.expression && item.expression.trim() !== ''
|
|
112
|
+
).length || 0
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
toFormData: (action: SendMsg) => {
|
|
118
|
+
// Extract runtime attachments from the text field attachments
|
|
119
|
+
const runtimeAttachments: { type: string; expression: string }[] = [];
|
|
120
|
+
const staticAttachments: string[] = [];
|
|
121
|
+
|
|
122
|
+
if (action.attachments && Array.isArray(action.attachments)) {
|
|
123
|
+
action.attachments.forEach((attachment) => {
|
|
124
|
+
if (typeof attachment === 'string' && attachment.includes(':')) {
|
|
125
|
+
const colonIndex = attachment.indexOf(':');
|
|
126
|
+
const contentType = attachment.substring(0, colonIndex);
|
|
127
|
+
const value = attachment.substring(colonIndex + 1);
|
|
128
|
+
|
|
129
|
+
if (!contentType.includes('/')) {
|
|
130
|
+
// This is a runtime attachment
|
|
131
|
+
runtimeAttachments.push({
|
|
132
|
+
type: contentType,
|
|
133
|
+
expression: value
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
// This is a static attachment
|
|
137
|
+
staticAttachments.push(attachment);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
uuid: action.uuid,
|
|
145
|
+
text: action.text || '',
|
|
146
|
+
attachments: staticAttachments,
|
|
147
|
+
runtime_attachments: runtimeAttachments,
|
|
148
|
+
quick_replies: (action.quick_replies || []).map((reply) => ({
|
|
149
|
+
name: reply,
|
|
150
|
+
value: reply
|
|
151
|
+
}))
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
fromFormData: (data: Record<string, any>) => {
|
|
155
|
+
const result = {
|
|
156
|
+
uuid: data.uuid,
|
|
157
|
+
type: 'send_msg',
|
|
158
|
+
text: data.text || '',
|
|
159
|
+
attachments: [],
|
|
160
|
+
quick_replies: (data.quick_replies || []).map((reply: any) =>
|
|
161
|
+
typeof reply === 'string' ? reply : reply.value || reply.name || reply
|
|
162
|
+
)
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Combine static attachments from text field with runtime attachments
|
|
166
|
+
const staticAttachments = data.attachments || [];
|
|
167
|
+
const runtimeAttachments = (data.runtime_attachments || [])
|
|
168
|
+
.filter((item: any) => item && item.type && item.expression) // Filter out invalid items
|
|
169
|
+
.map(
|
|
170
|
+
(item: { type: string; expression: string }) =>
|
|
171
|
+
`${item.type}:${item.expression}`
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
result.attachments = [...staticAttachments, ...runtimeAttachments];
|
|
175
|
+
|
|
176
|
+
// Remove quick_replies if empty to match original format
|
|
177
|
+
if (result.quick_replies.length === 0) {
|
|
178
|
+
delete (result as any).quick_replies;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return result as SendMsg;
|
|
182
|
+
},
|
|
183
|
+
sanitize: (formData: any): void => {
|
|
184
|
+
if (formData.text && typeof formData.text === 'string') {
|
|
185
|
+
formData.text = formData.text.trim();
|
|
44
186
|
}
|
|
45
187
|
},
|
|
46
188
|
validate: (action: SendMsg): ValidationResult => {
|
|
@@ -50,6 +192,35 @@ export const send_msg: ActionConfig = {
|
|
|
50
192
|
errors.text = 'Message text is required';
|
|
51
193
|
}
|
|
52
194
|
|
|
195
|
+
const attachments = action.attachments || [];
|
|
196
|
+
if (attachments.length > 10) {
|
|
197
|
+
const staticAttachments = attachments.filter(
|
|
198
|
+
(attachment) =>
|
|
199
|
+
typeof attachment === 'string' &&
|
|
200
|
+
attachment.substring(0, attachment.indexOf(':')).includes('/')
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const runtimeAttachments = attachments.filter(
|
|
204
|
+
(attachment) =>
|
|
205
|
+
typeof attachment === 'string' &&
|
|
206
|
+
!attachment.substring(0, attachment.indexOf(':')).includes('/')
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (runtimeAttachments.length > 0) {
|
|
210
|
+
errors.runtime_attachments =
|
|
211
|
+
'Each message can only have up to 10 attachments';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (staticAttachments.length > 0) {
|
|
215
|
+
const message = 'Each message can only have up to 10 total attachments';
|
|
216
|
+
if (errors.text) {
|
|
217
|
+
errors.text += ` ${message}`;
|
|
218
|
+
} else {
|
|
219
|
+
errors.text = message;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
53
224
|
return {
|
|
54
225
|
valid: Object.keys(errors).length === 0,
|
|
55
226
|
errors
|
package/src/flow/types.ts
CHANGED
|
@@ -162,6 +162,7 @@ export interface SelectFieldConfig extends BaseFieldConfig {
|
|
|
162
162
|
type: 'select';
|
|
163
163
|
options: string[] | { value: string; label: string }[];
|
|
164
164
|
multi?: boolean;
|
|
165
|
+
clearable?: boolean;
|
|
165
166
|
searchable?: boolean;
|
|
166
167
|
tags?: boolean;
|
|
167
168
|
placeholder?: string;
|
|
@@ -170,6 +171,7 @@ export interface SelectFieldConfig extends BaseFieldConfig {
|
|
|
170
171
|
nameKey?: string;
|
|
171
172
|
endpoint?: string;
|
|
172
173
|
emails?: boolean;
|
|
174
|
+
flavor?: 'small' | 'large';
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
export interface KeyValueFieldConfig extends BaseFieldConfig {
|
|
@@ -193,6 +195,7 @@ export interface ArrayFieldConfig extends BaseFieldConfig {
|
|
|
193
195
|
value: any,
|
|
194
196
|
allItems: any[]
|
|
195
197
|
) => any[];
|
|
198
|
+
isEmptyItem?: (item: any) => boolean;
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
export interface CheckboxFieldConfig extends BaseFieldConfig {
|
|
@@ -201,13 +204,27 @@ export interface CheckboxFieldConfig extends BaseFieldConfig {
|
|
|
201
204
|
animateChange?: string;
|
|
202
205
|
}
|
|
203
206
|
|
|
207
|
+
export interface MessageEditorFieldConfig extends BaseFieldConfig {
|
|
208
|
+
type: 'message-editor';
|
|
209
|
+
placeholder?: string;
|
|
210
|
+
minHeight?: number;
|
|
211
|
+
maxAttachments?: number;
|
|
212
|
+
accept?: string;
|
|
213
|
+
endpoint?: string;
|
|
214
|
+
counter?: string;
|
|
215
|
+
gsm?: boolean;
|
|
216
|
+
autogrow?: boolean;
|
|
217
|
+
disableCompletion?: boolean;
|
|
218
|
+
}
|
|
219
|
+
|
|
204
220
|
export type FieldConfig =
|
|
205
221
|
| TextFieldConfig
|
|
206
222
|
| TextareaFieldConfig
|
|
207
223
|
| SelectFieldConfig
|
|
208
224
|
| KeyValueFieldConfig
|
|
209
225
|
| ArrayFieldConfig
|
|
210
|
-
| CheckboxFieldConfig
|
|
226
|
+
| CheckboxFieldConfig
|
|
227
|
+
| MessageEditorFieldConfig;
|
|
211
228
|
|
|
212
229
|
// Layout configurations for better form organization
|
|
213
230
|
// Recursive layout system - any layout item can contain other layout items
|
|
@@ -228,8 +245,9 @@ export interface GroupLayoutConfig {
|
|
|
228
245
|
label: string;
|
|
229
246
|
items: LayoutItem[]; // can contain fields, rows, or other groups
|
|
230
247
|
collapsible?: boolean;
|
|
231
|
-
collapsed?: boolean; // initial state if collapsible
|
|
248
|
+
collapsed?: boolean | ((formData: any) => boolean); // initial state if collapsible - can be a function
|
|
232
249
|
helpText?: string;
|
|
250
|
+
getGroupValueCount?: (formData: any) => number; // optional function to get count for bubble display
|
|
233
251
|
}
|
|
234
252
|
|
|
235
253
|
export type LayoutItem =
|
|
@@ -249,6 +267,7 @@ export interface ActionConfig {
|
|
|
249
267
|
|
|
250
268
|
// Action editor configuration (legacy)
|
|
251
269
|
// Form-level transformations
|
|
270
|
+
sanitize?: (formData: any) => any;
|
|
252
271
|
toFormData?: (action: Action) => any;
|
|
253
272
|
fromFormData?: (formData: any) => Action;
|
|
254
273
|
|
package/src/form/ArrayEditor.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { html, css, TemplateResult } from 'lit';
|
|
2
2
|
import { customElement, property } from 'lit/decorators.js';
|
|
3
|
-
import { FieldConfig } from '../flow/types';
|
|
3
|
+
import { FieldConfig, SelectFieldConfig } from '../flow/types';
|
|
4
4
|
import { BaseListEditor, ListItem } from './BaseListEditor';
|
|
5
5
|
|
|
6
6
|
@customElement('temba-array-editor')
|
|
@@ -19,6 +19,12 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
|
|
|
19
19
|
allItems: any[]
|
|
20
20
|
) => any[];
|
|
21
21
|
|
|
22
|
+
@property({ type: Function })
|
|
23
|
+
isEmptyItemFn?: (item: any) => boolean;
|
|
24
|
+
|
|
25
|
+
@property({ type: Boolean })
|
|
26
|
+
maintainEmptyItem = true; // Enable by default for better UX
|
|
27
|
+
|
|
22
28
|
constructor() {
|
|
23
29
|
super();
|
|
24
30
|
this._items = [];
|
|
@@ -37,11 +43,36 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
|
|
|
37
43
|
|
|
38
44
|
// Implement abstract methods
|
|
39
45
|
isEmptyItem(item: ListItem): boolean {
|
|
40
|
-
|
|
46
|
+
// Use configurable function if provided
|
|
47
|
+
if (this.isEmptyItemFn) {
|
|
48
|
+
return this.isEmptyItemFn(item);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Default behavior: check if all values are empty
|
|
52
|
+
const values = Object.values(item);
|
|
53
|
+
if (values.length === 0) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return values.every(
|
|
41
58
|
(value) => value === undefined || value === null || value === ''
|
|
42
59
|
);
|
|
43
60
|
}
|
|
44
61
|
|
|
62
|
+
// Override cleanItems to be more permissive for form data
|
|
63
|
+
protected cleanItems(items: ListItem[]): any {
|
|
64
|
+
// For runtime attachments, keep items that have at least one non-empty field
|
|
65
|
+
return items.filter((item) => {
|
|
66
|
+
const values = Object.values(item);
|
|
67
|
+
return (
|
|
68
|
+
values.length > 0 &&
|
|
69
|
+
values.some(
|
|
70
|
+
(value) => value !== undefined && value !== null && value !== ''
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
45
76
|
createEmptyItem(): ListItem {
|
|
46
77
|
return {};
|
|
47
78
|
}
|
|
@@ -83,6 +114,14 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
|
|
|
83
114
|
return config.computeValue(item, currentValue);
|
|
84
115
|
}
|
|
85
116
|
|
|
117
|
+
// For select fields, ensure we return the right type
|
|
118
|
+
if (config.type === 'select') {
|
|
119
|
+
const selectConfig = config as SelectFieldConfig;
|
|
120
|
+
if (currentValue === undefined || currentValue === null) {
|
|
121
|
+
return selectConfig.multi ? [] : '';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
86
125
|
return currentValue;
|
|
87
126
|
}
|
|
88
127
|
|
|
@@ -112,13 +151,64 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
|
|
|
112
151
|
this.handleFieldChange(itemIndex, fieldName, e.target.value)}
|
|
113
152
|
></temba-textinput>`;
|
|
114
153
|
|
|
115
|
-
case 'select':
|
|
154
|
+
case 'select': {
|
|
155
|
+
const selectConfig = config as SelectFieldConfig;
|
|
156
|
+
const fieldValue = this.computeFieldValue(itemIndex, fieldName, config);
|
|
157
|
+
|
|
116
158
|
return html`<temba-select
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
159
|
+
class="form-control"
|
|
160
|
+
?clearable="${selectConfig.clearable || false}"
|
|
161
|
+
?searchable="${selectConfig.searchable || false}"
|
|
162
|
+
?tags="${selectConfig.tags || false}"
|
|
163
|
+
?multi="${selectConfig.multi || false}"
|
|
164
|
+
?emails="${selectConfig.emails || false}"
|
|
165
|
+
placeholder="${selectConfig.placeholder || ''}"
|
|
166
|
+
maxItems="${selectConfig.maxItems || 0}"
|
|
167
|
+
valueKey="${selectConfig.valueKey || 'value'}"
|
|
168
|
+
nameKey="${selectConfig.nameKey || 'name'}"
|
|
169
|
+
endpoint="${selectConfig.endpoint || ''}"
|
|
170
|
+
value="${fieldValue || ''}"
|
|
171
|
+
flavor="small"
|
|
172
|
+
@change="${(e: Event) => {
|
|
173
|
+
const target = e.target as any;
|
|
174
|
+
let value: any;
|
|
175
|
+
|
|
176
|
+
// For temba-select, extract the correct value
|
|
177
|
+
if (target.tagName === 'TEMBA-SELECT') {
|
|
178
|
+
if (target.multi || target.emails || target.tags) {
|
|
179
|
+
value = target.values || [];
|
|
180
|
+
} else {
|
|
181
|
+
// Single select: extract value from first selected option
|
|
182
|
+
const values = target.values || [];
|
|
183
|
+
value =
|
|
184
|
+
values.length > 0 && values[0]
|
|
185
|
+
? values[0].value !== undefined
|
|
186
|
+
? values[0].value
|
|
187
|
+
: values[0]
|
|
188
|
+
: '';
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
value = target.value;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.handleFieldChange(itemIndex, fieldName, value);
|
|
195
|
+
}}"
|
|
196
|
+
>
|
|
197
|
+
${selectConfig.options?.map((option: any) => {
|
|
198
|
+
if (typeof option === 'string') {
|
|
199
|
+
return html`<temba-option
|
|
200
|
+
name="${option}"
|
|
201
|
+
value="${option}"
|
|
202
|
+
></temba-option>`;
|
|
203
|
+
} else {
|
|
204
|
+
return html`<temba-option
|
|
205
|
+
name="${option.label || option.name}"
|
|
206
|
+
value="${option.value}"
|
|
207
|
+
></temba-option>`;
|
|
208
|
+
}
|
|
209
|
+
})}
|
|
210
|
+
</temba-select>`;
|
|
211
|
+
}
|
|
122
212
|
|
|
123
213
|
default:
|
|
124
214
|
return html`<span>Unsupported field type: ${config.type}</span>`;
|
|
@@ -130,29 +220,25 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
|
|
|
130
220
|
|
|
131
221
|
return html`
|
|
132
222
|
<div class="array-item">
|
|
133
|
-
<div class="item-
|
|
134
|
-
|
|
223
|
+
<div class="item-fields">
|
|
224
|
+
${Object.entries(this.itemConfig).map(
|
|
225
|
+
([fieldName, config]) => html`
|
|
226
|
+
<div class="field">
|
|
227
|
+
${this.renderField(index, fieldName, config)}
|
|
228
|
+
</div>
|
|
229
|
+
`
|
|
230
|
+
)}
|
|
135
231
|
${canRemove
|
|
136
232
|
? html`
|
|
137
233
|
<button
|
|
138
234
|
@click=${() => this.removeItem(index)}
|
|
139
235
|
class="remove-btn"
|
|
140
236
|
>
|
|
141
|
-
|
|
237
|
+
<temba-icon name="x"></temba-icon>
|
|
142
238
|
</button>
|
|
143
239
|
`
|
|
144
240
|
: ''}
|
|
145
241
|
</div>
|
|
146
|
-
<div class="item-fields">
|
|
147
|
-
${Object.entries(this.itemConfig).map(
|
|
148
|
-
([fieldName, config]) => html`
|
|
149
|
-
<div class="field">
|
|
150
|
-
<label>${config.label}${config.required ? ' *' : ''}</label>
|
|
151
|
-
${this.renderField(index, fieldName, config)}
|
|
152
|
-
</div>
|
|
153
|
-
`
|
|
154
|
-
)}
|
|
155
|
-
</div>
|
|
156
242
|
</div>
|
|
157
243
|
`;
|
|
158
244
|
}
|
|
@@ -171,27 +257,15 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
|
|
|
171
257
|
|
|
172
258
|
static styles = css`
|
|
173
259
|
.array-editor {
|
|
174
|
-
border: 1px solid #e0e0e0;
|
|
175
|
-
border-radius: 6px;
|
|
176
|
-
padding: 16px;
|
|
177
|
-
background: #fafafa;
|
|
178
260
|
}
|
|
179
261
|
|
|
180
262
|
.array-item {
|
|
181
|
-
border: 1px solid #d0d0d0;
|
|
182
|
-
border-radius: 4px;
|
|
183
|
-
padding: 16px;
|
|
184
|
-
margin-bottom: 12px;
|
|
185
|
-
background: white;
|
|
186
263
|
}
|
|
187
264
|
|
|
188
265
|
.item-header {
|
|
189
266
|
display: flex;
|
|
190
267
|
justify-content: space-between;
|
|
191
268
|
align-items: center;
|
|
192
|
-
margin-bottom: 12px;
|
|
193
|
-
padding-bottom: 8px;
|
|
194
|
-
border-bottom: 1px solid #eee;
|
|
195
269
|
}
|
|
196
270
|
|
|
197
271
|
.item-title {
|
|
@@ -200,8 +274,17 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
|
|
|
200
274
|
}
|
|
201
275
|
|
|
202
276
|
.item-fields {
|
|
203
|
-
display:
|
|
277
|
+
display: flex;
|
|
204
278
|
gap: 12px;
|
|
279
|
+
align-items: center;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.field {
|
|
283
|
+
flex: 1;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.field:first-child {
|
|
287
|
+
flex: 0 0 140px; /* Fixed width for type dropdown */
|
|
205
288
|
}
|
|
206
289
|
|
|
207
290
|
.field label {
|
|
@@ -214,7 +297,7 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
|
|
|
214
297
|
|
|
215
298
|
.add-btn,
|
|
216
299
|
.remove-btn {
|
|
217
|
-
padding: 8px
|
|
300
|
+
padding: 8px;
|
|
218
301
|
border: 1px solid #ccc;
|
|
219
302
|
border-radius: 4px;
|
|
220
303
|
background: white;
|
|
@@ -228,13 +311,8 @@ export class TembaArrayEditor extends BaseListEditor<ListItem> {
|
|
|
228
311
|
}
|
|
229
312
|
|
|
230
313
|
.remove-btn {
|
|
231
|
-
background: #
|
|
232
|
-
|
|
233
|
-
color: #dc2626;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
.remove-btn:hover {
|
|
237
|
-
background: #fef2f2;
|
|
314
|
+
background: #fefefe;
|
|
315
|
+
color: #999;
|
|
238
316
|
}
|
|
239
317
|
`;
|
|
240
318
|
}
|
|
@@ -52,10 +52,12 @@ export abstract class BaseListEditor<
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
protected shouldShowAddButton(): boolean {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
// Never show add button when maintaining empty items (auto-add behavior)
|
|
56
|
+
if (this.maintainEmptyItem) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return !this.maxItems || this._items.length < this.maxItems;
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
render(): TemplateResult {
|
|
@@ -65,7 +67,7 @@ export abstract class BaseListEditor<
|
|
|
65
67
|
<div class=${this.getContainerClass()}>
|
|
66
68
|
<div
|
|
67
69
|
class="list-items"
|
|
68
|
-
style="
|
|
70
|
+
style="display: grid; grid-template-columns: 1fr; gap: 8px;"
|
|
69
71
|
>
|
|
70
72
|
${items.map((item, index) => this.renderItem(item, index))}
|
|
71
73
|
</div>
|
|
@@ -89,7 +91,8 @@ export abstract class BaseListEditor<
|
|
|
89
91
|
|
|
90
92
|
if (this.maintainEmptyItem) {
|
|
91
93
|
const hasEmptyItem = items.some((item) => this.isEmptyItem(item));
|
|
92
|
-
if
|
|
94
|
+
// Only add empty item if we haven't reached maxItems and don't already have an empty item
|
|
95
|
+
if (!hasEmptyItem && (!this.maxItems || items.length < this.maxItems)) {
|
|
93
96
|
items.push(this.createEmptyItem());
|
|
94
97
|
}
|
|
95
98
|
}
|
|
@@ -111,6 +114,19 @@ export abstract class BaseListEditor<
|
|
|
111
114
|
fieldValue: any
|
|
112
115
|
) {
|
|
113
116
|
const updatedItems = [...this._items];
|
|
117
|
+
|
|
118
|
+
// If editing beyond the current array (auto-generated empty row), check maxItems
|
|
119
|
+
if (index >= this._items.length) {
|
|
120
|
+
if (this.maxItems && this._items.length >= this.maxItems) {
|
|
121
|
+
// Don't allow adding new items if we've reached maxItems
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Extend the array to include the new item
|
|
125
|
+
while (updatedItems.length <= index) {
|
|
126
|
+
updatedItems.push(this.createEmptyItem());
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
114
130
|
const currentItem = updatedItems[index] || this.createEmptyItem();
|
|
115
131
|
|
|
116
132
|
updatedItems[index] = {
|