@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
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { TemplateResult, css, html } from 'lit';
|
|
2
|
+
import { property } from 'lit/decorators.js';
|
|
3
|
+
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
|
4
|
+
import { FormElement } from './FormElement';
|
|
5
|
+
import { Completion } from './Completion';
|
|
6
|
+
import { MediaPicker } from './MediaPicker';
|
|
7
|
+
import { Attachment } from '../interfaces';
|
|
8
|
+
import { getClasses } from '../utils';
|
|
9
|
+
import { Icon } from '../Icons';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* MessageEditor is a composed component that combines temba-completion and temba-media-picker
|
|
13
|
+
* for editing messages with text completion and file attachments
|
|
14
|
+
*/
|
|
15
|
+
export class MessageEditor extends FormElement {
|
|
16
|
+
static get styles() {
|
|
17
|
+
return css`
|
|
18
|
+
:host {
|
|
19
|
+
display: block;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.message-editor-container {
|
|
23
|
+
border: 1px solid var(--color-widget-border);
|
|
24
|
+
border-radius: var(--curvature-widget);
|
|
25
|
+
background: #fff;
|
|
26
|
+
position: relative;
|
|
27
|
+
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.message-editor-container:focus-within {
|
|
31
|
+
border-color: var(--color-focus);
|
|
32
|
+
box-shadow: var(--widget-box-shadow-focused);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.message-editor-container.highlight {
|
|
36
|
+
border-color: rgba(156, 222, 106, 0.88);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Hide the completion field border since we draw our own */
|
|
40
|
+
.message-editor-container temba-completion::part(field) {
|
|
41
|
+
border: none;
|
|
42
|
+
box-shadow: none;
|
|
43
|
+
border-radius: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.message-editor-container temba-completion {
|
|
47
|
+
--widget-box-shadow: none;
|
|
48
|
+
--widget-box-shadow-focused: none;
|
|
49
|
+
--widget-box-shadow-focused: none;
|
|
50
|
+
--color-widget-border: transparent;
|
|
51
|
+
--color-focus: transparent;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.completion-wrapper {
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.media-wrapper {
|
|
58
|
+
padding: 4px 8px;
|
|
59
|
+
background: rgba(0, 0, 0, 0.03);
|
|
60
|
+
border-top: 1px solid var(--color-widget-border);
|
|
61
|
+
border-radius: 0 0 var(--curvature-widget) var(--curvature-widget);
|
|
62
|
+
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.05);
|
|
63
|
+
margin-top: 3px;
|
|
64
|
+
display: none;
|
|
65
|
+
}
|
|
66
|
+
.has-attachments .media-wrapper {
|
|
67
|
+
display: flex;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Override media picker styles to integrate better */
|
|
71
|
+
.media-wrapper temba-media-picker {
|
|
72
|
+
--color-widget-border: transparent;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.media-wrapper .attachments-list {
|
|
76
|
+
padding: 0.2em 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.drop-overlay {
|
|
80
|
+
position: absolute;
|
|
81
|
+
top: 0;
|
|
82
|
+
left: 0;
|
|
83
|
+
right: 0;
|
|
84
|
+
bottom: 0;
|
|
85
|
+
background: rgba(210, 243, 184, 0.5);
|
|
86
|
+
border-radius: var(--curvature-widget);
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
justify-content: center;
|
|
90
|
+
opacity: 0;
|
|
91
|
+
pointer-events: none;
|
|
92
|
+
transition: opacity 0.2s ease-in-out;
|
|
93
|
+
z-index: 10;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.message-editor-container.highlight .drop-overlay {
|
|
97
|
+
opacity: 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.drop-message {
|
|
101
|
+
background: rgba(0, 0, 0, 0.8);
|
|
102
|
+
color: white;
|
|
103
|
+
padding: 12px 24px;
|
|
104
|
+
border-radius: var(--curvature);
|
|
105
|
+
font-weight: 500;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.attachment-icon {
|
|
109
|
+
position: absolute;
|
|
110
|
+
bottom: 4px;
|
|
111
|
+
right: 4px;
|
|
112
|
+
color: var(--color-text-dark);
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
padding: 6px;
|
|
115
|
+
border-radius: var(--curvature);
|
|
116
|
+
transition: background-color 0.2s ease-in-out;
|
|
117
|
+
display: block;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.has-attachments .attachment-icon {
|
|
121
|
+
display: none;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.attachment-icon:hover {
|
|
125
|
+
background-color: rgba(0, 0, 0, 0.05);
|
|
126
|
+
}
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@property({ type: String })
|
|
131
|
+
name = '';
|
|
132
|
+
|
|
133
|
+
@property({ type: String })
|
|
134
|
+
value = '';
|
|
135
|
+
|
|
136
|
+
@property({ type: String })
|
|
137
|
+
placeholder = '';
|
|
138
|
+
|
|
139
|
+
@property({ type: Boolean })
|
|
140
|
+
textarea = true;
|
|
141
|
+
|
|
142
|
+
@property({ type: Boolean })
|
|
143
|
+
autogrow = true;
|
|
144
|
+
|
|
145
|
+
@property({ type: Number })
|
|
146
|
+
minHeight = 60;
|
|
147
|
+
|
|
148
|
+
@property({ type: Number })
|
|
149
|
+
maxLength: number;
|
|
150
|
+
|
|
151
|
+
@property({ type: Boolean })
|
|
152
|
+
session: boolean;
|
|
153
|
+
|
|
154
|
+
@property({ type: Boolean })
|
|
155
|
+
submitOnEnter = false;
|
|
156
|
+
|
|
157
|
+
@property({ type: Boolean })
|
|
158
|
+
gsm: boolean;
|
|
159
|
+
|
|
160
|
+
@property({ type: Boolean })
|
|
161
|
+
disableCompletion: boolean;
|
|
162
|
+
|
|
163
|
+
@property({ type: String })
|
|
164
|
+
counter: string;
|
|
165
|
+
|
|
166
|
+
@property({ type: Array })
|
|
167
|
+
attachments: (Attachment | string)[] = [];
|
|
168
|
+
|
|
169
|
+
@property({ type: String })
|
|
170
|
+
accept = '';
|
|
171
|
+
|
|
172
|
+
@property({ type: Number, attribute: 'max-attachments' })
|
|
173
|
+
maxAttachments = 3;
|
|
174
|
+
|
|
175
|
+
@property({ type: String })
|
|
176
|
+
endpoint = '';
|
|
177
|
+
|
|
178
|
+
@property({ type: Boolean, attribute: false })
|
|
179
|
+
pendingDrop = false;
|
|
180
|
+
|
|
181
|
+
@property({ type: Boolean, attribute: false })
|
|
182
|
+
uploading = false;
|
|
183
|
+
|
|
184
|
+
private completionElement: Completion;
|
|
185
|
+
private mediaPickerElement: MediaPicker;
|
|
186
|
+
|
|
187
|
+
public firstUpdated(changes: Map<string, any>) {
|
|
188
|
+
super.firstUpdated(changes);
|
|
189
|
+
|
|
190
|
+
this.completionElement = this.shadowRoot.querySelector(
|
|
191
|
+
'temba-completion'
|
|
192
|
+
) as Completion;
|
|
193
|
+
|
|
194
|
+
// Get the visible media picker (either in media-wrapper or the hidden one)
|
|
195
|
+
this.mediaPickerElement = this.shadowRoot.querySelector(
|
|
196
|
+
'temba-media-picker'
|
|
197
|
+
) as MediaPicker;
|
|
198
|
+
|
|
199
|
+
// Set up proper attachment filtering and parsing
|
|
200
|
+
this.parseAndFilterAttachments();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Parse attachments and filter out runtime attachments for media picker
|
|
205
|
+
*/
|
|
206
|
+
private parseAndFilterAttachments() {
|
|
207
|
+
if (!this.attachments) return;
|
|
208
|
+
|
|
209
|
+
// Filter out runtime attachments (those without '/' in content type)
|
|
210
|
+
const staticAttachments = this.attachments.filter((attachment) => {
|
|
211
|
+
if (typeof attachment === 'string') {
|
|
212
|
+
const [contentType] = attachment.split(':');
|
|
213
|
+
return contentType.includes('/');
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Convert string attachments to Attachment objects for media picker
|
|
219
|
+
const mediaAttachments = staticAttachments.map((attachment) => {
|
|
220
|
+
if (typeof attachment === 'string') {
|
|
221
|
+
// split into content type and URL
|
|
222
|
+
// e.g. "image/jpeg:http://example.com/image.jpg"
|
|
223
|
+
const colonIndex = attachment.indexOf(':');
|
|
224
|
+
const contentType = attachment.substring(0, colonIndex);
|
|
225
|
+
const url = attachment.substring(colonIndex + 1);
|
|
226
|
+
return {
|
|
227
|
+
content_type: contentType,
|
|
228
|
+
url: url,
|
|
229
|
+
filename: this.getFilenameFromUrl(url),
|
|
230
|
+
size: 0
|
|
231
|
+
} as Attachment;
|
|
232
|
+
}
|
|
233
|
+
return attachment;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (this.mediaPickerElement) {
|
|
237
|
+
this.mediaPickerElement.attachments = mediaAttachments;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check if there are any static attachments (excluding runtime attachments)
|
|
243
|
+
*/
|
|
244
|
+
private hasStaticAttachments(): boolean {
|
|
245
|
+
if (!this.attachments || this.attachments.length === 0) return false;
|
|
246
|
+
|
|
247
|
+
return this.attachments.some((attachment) => {
|
|
248
|
+
if (typeof attachment === 'string') {
|
|
249
|
+
const [contentType] = attachment.split(':');
|
|
250
|
+
return contentType.includes('/');
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private getFilenameFromUrl(url: string): string {
|
|
257
|
+
try {
|
|
258
|
+
const urlObj = new URL(url);
|
|
259
|
+
const pathname = urlObj.pathname;
|
|
260
|
+
return pathname.substring(pathname.lastIndexOf('/') + 1) || 'attachment';
|
|
261
|
+
} catch {
|
|
262
|
+
return 'attachment';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private handleCompletionChange(event: Event) {
|
|
267
|
+
event.stopPropagation();
|
|
268
|
+
const completion = event.target as Completion;
|
|
269
|
+
this.value = completion.value;
|
|
270
|
+
this.fireEvent('change');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private handleMediaChange(event: Event) {
|
|
274
|
+
event.stopPropagation();
|
|
275
|
+
const mediaPicker = event.target as MediaPicker;
|
|
276
|
+
// Convert media picker attachments back to the format expected by the form
|
|
277
|
+
const formattedAttachments = mediaPicker.attachments.map((attachment) => {
|
|
278
|
+
return `${attachment.content_type}:${attachment.url}`;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Merge with any runtime attachments that were filtered out
|
|
282
|
+
const runtimeAttachments = (this.attachments || []).filter((attachment) => {
|
|
283
|
+
if (typeof attachment === 'string') {
|
|
284
|
+
const [contentType] = attachment.split(':');
|
|
285
|
+
return !contentType.includes('/');
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}) as string[];
|
|
289
|
+
|
|
290
|
+
this.attachments = [...runtimeAttachments, ...formattedAttachments];
|
|
291
|
+
this.fireEvent('change');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private handleDragEnter(evt: DragEvent): void {
|
|
295
|
+
this.highlight(evt);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private handleDragOver(evt: DragEvent): void {
|
|
299
|
+
this.highlight(evt);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private handleDragLeave(evt: DragEvent): void {
|
|
303
|
+
this.unhighlight(evt);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private handleDrop(evt: DragEvent): void {
|
|
307
|
+
this.unhighlight(evt);
|
|
308
|
+
|
|
309
|
+
// Forward to media picker
|
|
310
|
+
if (this.mediaPickerElement) {
|
|
311
|
+
const files = [...evt.dataTransfer.files];
|
|
312
|
+
this.mediaPickerElement.uploadFiles(files);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private handleAttachmentIconClick(): void {
|
|
317
|
+
// Trigger the file picker on the media picker
|
|
318
|
+
if (this.mediaPickerElement) {
|
|
319
|
+
const uploadInput = this.mediaPickerElement.shadowRoot.querySelector(
|
|
320
|
+
'#upload-input'
|
|
321
|
+
) as HTMLInputElement;
|
|
322
|
+
if (uploadInput) {
|
|
323
|
+
uploadInput.click();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private highlight(evt: DragEvent): void {
|
|
329
|
+
evt.preventDefault();
|
|
330
|
+
evt.stopPropagation();
|
|
331
|
+
|
|
332
|
+
// Always allow highlight for testing purposes, but in real usage check media picker
|
|
333
|
+
this.pendingDrop = true;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private unhighlight(evt: DragEvent): void {
|
|
337
|
+
evt.preventDefault();
|
|
338
|
+
evt.stopPropagation();
|
|
339
|
+
this.pendingDrop = false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
public updated(changedProperties: Map<string, any>) {
|
|
343
|
+
super.updated(changedProperties);
|
|
344
|
+
|
|
345
|
+
if (changedProperties.has('attachments')) {
|
|
346
|
+
// Re-query media picker since the DOM structure may have changed
|
|
347
|
+
this.mediaPickerElement = this.shadowRoot.querySelector(
|
|
348
|
+
'temba-media-picker'
|
|
349
|
+
) as MediaPicker;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (changedProperties.has('uploading')) {
|
|
353
|
+
this.dispatchEvent(
|
|
354
|
+
new CustomEvent('loading', {
|
|
355
|
+
detail: { loading: this.uploading }
|
|
356
|
+
})
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
public focus() {
|
|
362
|
+
super.focus();
|
|
363
|
+
if (this.completionElement) {
|
|
364
|
+
this.completionElement.focus();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
public click() {
|
|
369
|
+
super.click();
|
|
370
|
+
if (this.completionElement) {
|
|
371
|
+
this.completionElement.click();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
public render(): TemplateResult {
|
|
376
|
+
const hasAttachments = this.hasStaticAttachments();
|
|
377
|
+
|
|
378
|
+
return html`
|
|
379
|
+
<temba-field
|
|
380
|
+
name=${this.name}
|
|
381
|
+
.label=${this.label}
|
|
382
|
+
.helpText=${this.helpText}
|
|
383
|
+
.errors=${this.errors}
|
|
384
|
+
.widgetOnly=${this.widgetOnly}
|
|
385
|
+
>
|
|
386
|
+
<div
|
|
387
|
+
class=${getClasses({
|
|
388
|
+
'message-editor-container': true,
|
|
389
|
+
highlight: this.pendingDrop,
|
|
390
|
+
'has-attachments': hasAttachments
|
|
391
|
+
})}
|
|
392
|
+
@dragenter=${this.handleDragEnter}
|
|
393
|
+
@dragover=${this.handleDragOver}
|
|
394
|
+
@dragleave=${this.handleDragLeave}
|
|
395
|
+
@drop=${this.handleDrop}
|
|
396
|
+
>
|
|
397
|
+
<div class="completion-wrapper">
|
|
398
|
+
<temba-completion
|
|
399
|
+
name=${this.name}
|
|
400
|
+
.value=${this.value}
|
|
401
|
+
placeholder=${this.placeholder}
|
|
402
|
+
?textarea=${this.textarea}
|
|
403
|
+
?autogrow=${this.autogrow}
|
|
404
|
+
?session=${this.session}
|
|
405
|
+
?submitOnEnter=${this.submitOnEnter}
|
|
406
|
+
?gsm=${this.gsm}
|
|
407
|
+
?disableCompletion=${this.disableCompletion}
|
|
408
|
+
maxlength=${ifDefined(this.maxLength)}
|
|
409
|
+
counter=${ifDefined(this.counter)}
|
|
410
|
+
minHeight=${ifDefined(this.minHeight)}
|
|
411
|
+
widgetOnly
|
|
412
|
+
@change=${this.handleCompletionChange}
|
|
413
|
+
></temba-completion>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<div class="media-wrapper ">
|
|
417
|
+
<temba-media-picker
|
|
418
|
+
.accept=${this.accept}
|
|
419
|
+
.max=${this.maxAttachments}
|
|
420
|
+
.endpoint=${this.endpoint}
|
|
421
|
+
@change=${this.handleMediaChange}
|
|
422
|
+
ignoreDrops
|
|
423
|
+
></temba-media-picker>
|
|
424
|
+
</div>
|
|
425
|
+
<temba-icon
|
|
426
|
+
class="attachment-icon"
|
|
427
|
+
name=${Icon.attachment}
|
|
428
|
+
size="1.2"
|
|
429
|
+
@click=${this.handleAttachmentIconClick}
|
|
430
|
+
></temba-icon>
|
|
431
|
+
|
|
432
|
+
<div class="drop-overlay"></div>
|
|
433
|
+
|
|
434
|
+
<!-- Hidden media picker for handling uploads when no attachments are shown -->
|
|
435
|
+
${!hasAttachments
|
|
436
|
+
? html`<temba-media-picker
|
|
437
|
+
style="display: none;"
|
|
438
|
+
.accept=${this.accept}
|
|
439
|
+
.max=${this.maxAttachments}
|
|
440
|
+
.endpoint=${this.endpoint}
|
|
441
|
+
@change=${this.handleMediaChange}
|
|
442
|
+
ignoreDrops
|
|
443
|
+
></temba-media-picker>`
|
|
444
|
+
: ''}
|
|
445
|
+
</div>
|
|
446
|
+
</temba-field>
|
|
447
|
+
`;
|
|
448
|
+
}
|
|
449
|
+
}
|
package/src/form/TextInput.ts
CHANGED
|
@@ -202,7 +202,20 @@ export class TextInput extends FormElement {
|
|
|
202
202
|
root = document;
|
|
203
203
|
}
|
|
204
204
|
this.counterElement = root.querySelector(this.counter);
|
|
205
|
-
this.counterElement
|
|
205
|
+
if (this.counterElement) {
|
|
206
|
+
this.counterElement.text = this.value;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private updateAutogrowSize(): void {
|
|
212
|
+
if (this.textarea && this.autogrow) {
|
|
213
|
+
const autogrow = this.shadowRoot.querySelector(
|
|
214
|
+
'.grow-wrap > div'
|
|
215
|
+
) as HTMLDivElement;
|
|
216
|
+
if (autogrow) {
|
|
217
|
+
autogrow.innerText = this.value + String.fromCharCode(10);
|
|
218
|
+
}
|
|
206
219
|
}
|
|
207
220
|
}
|
|
208
221
|
|
|
@@ -214,12 +227,7 @@ export class TextInput extends FormElement {
|
|
|
214
227
|
this.fireEvent('change');
|
|
215
228
|
}
|
|
216
229
|
|
|
217
|
-
|
|
218
|
-
const autogrow = this.shadowRoot.querySelector(
|
|
219
|
-
'.grow-wrap > div'
|
|
220
|
-
) as HTMLDivElement;
|
|
221
|
-
autogrow.innerText = this.value + String.fromCharCode(10);
|
|
222
|
-
}
|
|
230
|
+
this.updateAutogrowSize();
|
|
223
231
|
|
|
224
232
|
if (this.cursorStart > -1 && this.cursorEnd > -1) {
|
|
225
233
|
this.inputElement.setSelectionRange(this.cursorStart, this.cursorEnd);
|
|
@@ -81,7 +81,7 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
81
81
|
background: rgba(100, 100, 100, 0.05);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
.
|
|
84
|
+
.selected-item.multi .remove-item {
|
|
85
85
|
display: none;
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -320,10 +320,10 @@ export class Select<T extends SelectOption> extends FormElement {
|
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
.small {
|
|
323
|
-
--temba-select-selected-padding:
|
|
324
|
-
--temba-select-selected-line-height:
|
|
323
|
+
--temba-select-selected-padding: 6px;
|
|
324
|
+
--temba-select-selected-line-height: 12px;
|
|
325
325
|
--temba-select-selected-font-size: 12px;
|
|
326
|
-
--
|
|
326
|
+
--temba-select-min-height: 2.28em;
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
.info-text {
|
package/src/live/ContactChat.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
MsgEvent,
|
|
30
30
|
NameChangedEvent,
|
|
31
31
|
OptinRequestedEvent,
|
|
32
|
+
RunEvent,
|
|
32
33
|
TicketEvent,
|
|
33
34
|
UpdateFieldEvent,
|
|
34
35
|
URNsChangedEvent
|
|
@@ -53,9 +54,6 @@ export enum Events {
|
|
|
53
54
|
MESSAGE_RECEIVED = 'msg_received',
|
|
54
55
|
BROADCAST_CREATED = 'broadcast_created',
|
|
55
56
|
IVR_CREATED = 'ivr_created',
|
|
56
|
-
FLOW_ENTERED = 'flow_entered',
|
|
57
|
-
|
|
58
|
-
FLOW_EXITED = 'flow_exited',
|
|
59
57
|
CONTACT_FIELD_CHANGED = 'contact_field_changed',
|
|
60
58
|
CONTACT_GROUPS_CHANGED = 'contact_groups_changed',
|
|
61
59
|
CONTACT_NAME_CHANGED = 'contact_name_changed',
|
|
@@ -71,7 +69,13 @@ export enum Events {
|
|
|
71
69
|
TICKET_OPENED = 'ticket_opened',
|
|
72
70
|
TICKET_REOPENED = 'ticket_reopened',
|
|
73
71
|
TICKET_TOPIC_CHANGED = 'ticket_topic_changed',
|
|
74
|
-
OPTIN_REQUESTED = 'optin_requested'
|
|
72
|
+
OPTIN_REQUESTED = 'optin_requested',
|
|
73
|
+
RUN_STARTED = 'run_started',
|
|
74
|
+
RUN_ENDED = 'run_ended',
|
|
75
|
+
|
|
76
|
+
// deprecated
|
|
77
|
+
FLOW_ENTERED = 'flow_entered',
|
|
78
|
+
FLOW_EXITED = 'flow_exited'
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
const renderInfoList = (singular: string, plural: string, items: any[]) => {
|
|
@@ -126,6 +130,21 @@ const renderFlowEvent = (event: FlowEvent): string => {
|
|
|
126
130
|
return `${verb} [**${event.flow.name}**](/flow/editor/${event.flow.uuid}/)`;
|
|
127
131
|
};
|
|
128
132
|
|
|
133
|
+
const renderRunEvent = (event: RunEvent): string => {
|
|
134
|
+
let verb = 'Started';
|
|
135
|
+
if (event.type === Events.RUN_ENDED) {
|
|
136
|
+
if (event.status === 'completed') {
|
|
137
|
+
verb = 'Completed';
|
|
138
|
+
} else if (event.status === 'expired') {
|
|
139
|
+
verb = 'Expired from';
|
|
140
|
+
} else {
|
|
141
|
+
verb = 'Interrupted';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return `${verb} [**${event.flow.name}**](/flow/editor/${event.flow.uuid}/)`;
|
|
146
|
+
};
|
|
147
|
+
|
|
129
148
|
const renderUpdateEvent = (event: UpdateFieldEvent): string => {
|
|
130
149
|
return event.value
|
|
131
150
|
? `Updated **${event.field.name}** to **${event.value.text}**`
|
|
@@ -638,6 +657,13 @@ export class ContactChat extends ContactStoreElement {
|
|
|
638
657
|
text: renderFlowEvent(event as FlowEvent)
|
|
639
658
|
};
|
|
640
659
|
break;
|
|
660
|
+
case Events.RUN_STARTED:
|
|
661
|
+
case Events.RUN_ENDED:
|
|
662
|
+
message = {
|
|
663
|
+
type: MessageType.Inline,
|
|
664
|
+
text: renderRunEvent(event as RunEvent)
|
|
665
|
+
};
|
|
666
|
+
break;
|
|
641
667
|
case Events.CONTACT_FIELD_CHANGED:
|
|
642
668
|
message = {
|
|
643
669
|
type: MessageType.Inline,
|