@nyaruka/temba-components 0.130.3 → 0.130.5
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/CHANGELOG.md +22 -0
- package/demo/sortable-rules-demo.html +155 -0
- package/dist/temba-components.js +133 -143
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/events.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +13 -7
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/actions/send_msg.js +1 -0
- package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_groups.js +149 -1
- package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +1 -0
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_response.js +81 -75
- package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
- package/out-tsc/src/form/ArrayEditor.js +106 -28
- package/out-tsc/src/form/ArrayEditor.js.map +1 -1
- package/out-tsc/src/form/select/Select.js +21 -25
- package/out-tsc/src/form/select/Select.js.map +1 -1
- package/out-tsc/src/list/SortableList.js +214 -140
- package/out-tsc/src/list/SortableList.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +18 -13
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/test/nodes/split_by_groups.test.js +130 -0
- package/out-tsc/test/nodes/split_by_groups.test.js.map +1 -0
- package/out-tsc/test/nodes/wait_for_response.test.js +149 -0
- package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -1
- package/out-tsc/test/temba-field-config.test.js +56 -0
- package/out-tsc/test/temba-field-config.test.js.map +1 -1
- package/package.json +1 -1
- 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/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/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/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/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/contacts/chat-failure.png +0 -0
- package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
- package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
- package/screenshots/truth/editor/wait.png +0 -0
- package/screenshots/truth/field-renderer/select-with-label.png +0 -0
- package/screenshots/truth/list/fields-dragging.png +0 -0
- package/screenshots/truth/list/sortable-dragging.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
- package/screenshots/truth/select/search-enabled.png +0 -0
- package/screenshots/truth/select/search-selected-focus.png +0 -0
- package/screenshots/truth/select/search-selected.png +0 -0
- package/screenshots/truth/templates/default.png +0 -0
- package/screenshots/truth/templates/unapproved.png +0 -0
- package/src/events.ts +6 -6
- package/src/flow/CanvasNode.ts +15 -13
- package/src/flow/actions/send_msg.ts +1 -0
- package/src/flow/nodes/split_by_groups.ts +190 -1
- package/src/flow/nodes/split_by_llm_categorize.ts +1 -0
- package/src/flow/nodes/wait_for_response.ts +98 -74
- package/src/form/ArrayEditor.ts +112 -28
- package/src/form/select/Select.ts +24 -25
- package/src/list/SortableList.ts +250 -149
- package/src/live/ContactChat.ts +20 -13
- package/test/nodes/split_by_groups.test.ts +165 -0
- package/test/nodes/wait_for_response.test.ts +182 -0
- package/test/temba-field-config.test.ts +69 -0
- package/test-assets/contacts/history.json +37 -35
- package/screenshots/truth/contacts/chat-for-active-contact.png +0 -0
package/src/list/SortableList.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { css, html,
|
|
1
|
+
import { css, html, TemplateResult } from 'lit';
|
|
2
2
|
import { property } from 'lit/decorators.js';
|
|
3
3
|
import { CustomEventType } from '../interfaces';
|
|
4
4
|
import { RapidElement } from '../RapidElement';
|
|
@@ -10,6 +10,7 @@ import { RapidElement } from '../RapidElement';
|
|
|
10
10
|
// how far we have to drag before it starts
|
|
11
11
|
const DRAG_THRESHOLD = 2;
|
|
12
12
|
export class SortableList extends RapidElement {
|
|
13
|
+
originalDownDisplay: string;
|
|
13
14
|
static get styles() {
|
|
14
15
|
return css`
|
|
15
16
|
:host {
|
|
@@ -19,6 +20,8 @@ export class SortableList extends RapidElement {
|
|
|
19
20
|
.container {
|
|
20
21
|
user-select: none;
|
|
21
22
|
position: relative;
|
|
23
|
+
display: grid;
|
|
24
|
+
grid-template-columns: 1fr;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
.container.horizontal {
|
|
@@ -31,58 +34,6 @@ export class SortableList extends RapidElement {
|
|
|
31
34
|
background: var(--color-selection);
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
.dragged-item {
|
|
35
|
-
opacity: 0;
|
|
36
|
-
pointer-events: none;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.sortable {
|
|
40
|
-
transition: all 300ms ease-in-out;
|
|
41
|
-
display: flex;
|
|
42
|
-
padding: 0.4em 0;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.container.horizontal .sortable {
|
|
46
|
-
padding: 0;
|
|
47
|
-
margin-right: 0.25em;
|
|
48
|
-
margin-bottom: 0.25em;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.drop-indicator {
|
|
52
|
-
position: absolute;
|
|
53
|
-
background: var(--color-primary-dark, #1c7cd6);
|
|
54
|
-
z-index: 1000;
|
|
55
|
-
pointer-events: none;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.container.horizontal .drop-indicator {
|
|
59
|
-
width: 2px;
|
|
60
|
-
margin-top: -5px;
|
|
61
|
-
padding-bottom: 10px;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
.container:not(.horizontal) .drop-indicator {
|
|
65
|
-
height: 2px;
|
|
66
|
-
left: 0;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.sortable:hover temba-icon {
|
|
70
|
-
opacity: 1;
|
|
71
|
-
cursor: move;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.ghost {
|
|
75
|
-
position: absolute;
|
|
76
|
-
opacity: 0.7;
|
|
77
|
-
transition: none;
|
|
78
|
-
background: var(--color-background, white);
|
|
79
|
-
border: 1px solid var(--color-primary, #1c7cd6);
|
|
80
|
-
border-radius: 4px;
|
|
81
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
82
|
-
pointer-events: none;
|
|
83
|
-
z-index: 999;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
37
|
.slot {
|
|
87
38
|
flex-grow: 1;
|
|
88
39
|
}
|
|
@@ -111,6 +62,9 @@ export class SortableList extends RapidElement {
|
|
|
111
62
|
@property({ type: String })
|
|
112
63
|
dragHandle: string;
|
|
113
64
|
|
|
65
|
+
@property({ type: String })
|
|
66
|
+
gap: string = '0em';
|
|
67
|
+
|
|
114
68
|
/**
|
|
115
69
|
* Optional callback to allow parent components to customize the ghost node.
|
|
116
70
|
* Called after the ghost node is cloned but before it is appended to the DOM.
|
|
@@ -120,6 +74,8 @@ export class SortableList extends RapidElement {
|
|
|
120
74
|
|
|
121
75
|
ghostElement: HTMLDivElement = null;
|
|
122
76
|
downEle: HTMLDivElement = null;
|
|
77
|
+
originalElementRect: DOMRect = null; // Store original dimensions
|
|
78
|
+
originalDragIndex: number = -1; // Store original index before moving element
|
|
123
79
|
xOffset = 0;
|
|
124
80
|
yOffset = 0;
|
|
125
81
|
yDown = 0;
|
|
@@ -127,7 +83,7 @@ export class SortableList extends RapidElement {
|
|
|
127
83
|
|
|
128
84
|
draggingIdx = -1;
|
|
129
85
|
draggingEle = null;
|
|
130
|
-
|
|
86
|
+
dropPlaceholder: HTMLDivElement = null;
|
|
131
87
|
pendingDropIndex = -1;
|
|
132
88
|
pendingTargetElement: HTMLElement = null;
|
|
133
89
|
|
|
@@ -140,17 +96,135 @@ export class SortableList extends RapidElement {
|
|
|
140
96
|
this.handleMouseDown = this.handleMouseDown.bind(this);
|
|
141
97
|
}
|
|
142
98
|
|
|
143
|
-
protected firstUpdated(
|
|
144
|
-
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
|
145
|
-
): void {
|
|
146
|
-
super.firstUpdated(_changedProperties);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
99
|
private getSortableElements(): Element[] {
|
|
150
|
-
|
|
100
|
+
const eles = this.shadowRoot
|
|
151
101
|
.querySelector('slot')
|
|
152
102
|
.assignedElements()
|
|
153
|
-
.filter(
|
|
103
|
+
.filter(
|
|
104
|
+
(ele) =>
|
|
105
|
+
ele.classList.contains('sortable') &&
|
|
106
|
+
!ele.classList.contains('drop-placeholder')
|
|
107
|
+
);
|
|
108
|
+
return eles;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private cloneElementWithState(element: HTMLElement): HTMLElement {
|
|
112
|
+
// First create a basic clone
|
|
113
|
+
const clone = element.cloneNode(true) as HTMLElement;
|
|
114
|
+
|
|
115
|
+
// Helper function to copy form element values recursively
|
|
116
|
+
const copyFormValues = (original: HTMLElement, cloned: HTMLElement) => {
|
|
117
|
+
try {
|
|
118
|
+
// Copy input values
|
|
119
|
+
const originalInputs = original.querySelectorAll(
|
|
120
|
+
'input, textarea, select'
|
|
121
|
+
);
|
|
122
|
+
const clonedInputs = cloned.querySelectorAll('input, textarea, select');
|
|
123
|
+
|
|
124
|
+
originalInputs.forEach((originalInput, index) => {
|
|
125
|
+
const clonedInput = clonedInputs[index] as
|
|
126
|
+
| HTMLInputElement
|
|
127
|
+
| HTMLTextAreaElement
|
|
128
|
+
| HTMLSelectElement;
|
|
129
|
+
if (clonedInput) {
|
|
130
|
+
if (originalInput instanceof HTMLInputElement) {
|
|
131
|
+
const originalHtmlInput = originalInput as HTMLInputElement;
|
|
132
|
+
const clonedHtmlInput = clonedInput as HTMLInputElement;
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
originalHtmlInput.type === 'checkbox' ||
|
|
136
|
+
originalHtmlInput.type === 'radio'
|
|
137
|
+
) {
|
|
138
|
+
clonedHtmlInput.checked = originalHtmlInput.checked;
|
|
139
|
+
} else {
|
|
140
|
+
clonedHtmlInput.value = originalHtmlInput.value;
|
|
141
|
+
}
|
|
142
|
+
} else if (originalInput instanceof HTMLTextAreaElement) {
|
|
143
|
+
(clonedInput as HTMLTextAreaElement).value = originalInput.value;
|
|
144
|
+
} else if (originalInput instanceof HTMLSelectElement) {
|
|
145
|
+
(clonedInput as HTMLSelectElement).selectedIndex =
|
|
146
|
+
originalInput.selectedIndex;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Copy properties from all custom elements that might have a value property
|
|
152
|
+
const allOriginalElements = Array.from(original.querySelectorAll('*'));
|
|
153
|
+
const allClonedElements = Array.from(cloned.querySelectorAll('*'));
|
|
154
|
+
|
|
155
|
+
allOriginalElements.forEach((originalEl, index) => {
|
|
156
|
+
const clonedEl = allClonedElements[index];
|
|
157
|
+
if (clonedEl && originalEl) {
|
|
158
|
+
// Special handling for temba components
|
|
159
|
+
if (
|
|
160
|
+
originalEl.tagName &&
|
|
161
|
+
originalEl.tagName.toLowerCase().startsWith('temba-')
|
|
162
|
+
) {
|
|
163
|
+
try {
|
|
164
|
+
// Copy common temba component properties
|
|
165
|
+
const tembaProps = [
|
|
166
|
+
'value',
|
|
167
|
+
'values',
|
|
168
|
+
'selectedValue',
|
|
169
|
+
'checked',
|
|
170
|
+
'selected',
|
|
171
|
+
'textContent'
|
|
172
|
+
];
|
|
173
|
+
tembaProps.forEach((prop) => {
|
|
174
|
+
if (
|
|
175
|
+
prop in originalEl &&
|
|
176
|
+
(originalEl as any)[prop] !== undefined
|
|
177
|
+
) {
|
|
178
|
+
(clonedEl as any)[prop] = (originalEl as any)[prop];
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Copy all attributes for temba components to preserve state
|
|
183
|
+
Array.from(originalEl.attributes).forEach((attr) => {
|
|
184
|
+
clonedEl.setAttribute(attr.name, attr.value);
|
|
185
|
+
});
|
|
186
|
+
} catch (e) {
|
|
187
|
+
// Ignore errors when copying temba properties
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
// Try to copy value property for other elements
|
|
191
|
+
try {
|
|
192
|
+
if (
|
|
193
|
+
'value' in originalEl &&
|
|
194
|
+
(originalEl as any).value !== undefined
|
|
195
|
+
) {
|
|
196
|
+
(clonedEl as any).value = (originalEl as any).value;
|
|
197
|
+
}
|
|
198
|
+
} catch (e) {
|
|
199
|
+
// Ignore errors when copying properties
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Copy data attributes that might contain state
|
|
203
|
+
try {
|
|
204
|
+
Array.from(originalEl.attributes).forEach((attr) => {
|
|
205
|
+
if (
|
|
206
|
+
attr.name.startsWith('data-') ||
|
|
207
|
+
attr.name.startsWith('aria-')
|
|
208
|
+
) {
|
|
209
|
+
clonedEl.setAttribute(attr.name, attr.value);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
} catch (e) {
|
|
213
|
+
// Ignore errors when copying attributes
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
} catch (e) {
|
|
219
|
+
// If anything fails, just return the basic clone
|
|
220
|
+
console.warn('Failed to copy form values in cloneElementWithState:', e);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Copy form values for the root element and all descendants
|
|
225
|
+
copyFormValues(element, clone);
|
|
226
|
+
|
|
227
|
+
return clone;
|
|
154
228
|
}
|
|
155
229
|
|
|
156
230
|
public getIds() {
|
|
@@ -208,59 +282,68 @@ export class SortableList extends RapidElement {
|
|
|
208
282
|
}
|
|
209
283
|
}
|
|
210
284
|
|
|
211
|
-
private
|
|
212
|
-
|
|
285
|
+
private showDropPlaceholder(
|
|
286
|
+
targetElement: HTMLElement,
|
|
287
|
+
insertAfter: boolean
|
|
288
|
+
) {
|
|
289
|
+
this.hideDropPlaceholder();
|
|
213
290
|
|
|
214
|
-
if (!targetElement) return;
|
|
291
|
+
if (!targetElement || !this.draggingEle) return;
|
|
215
292
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
this.dropIndicator.className = 'drop-indicator';
|
|
293
|
+
// Don't show placeholder if we're targeting the dragging element itself
|
|
294
|
+
if (targetElement === this.draggingEle) return;
|
|
219
295
|
|
|
220
|
-
|
|
221
|
-
const containerRect = container.getBoundingClientRect();
|
|
296
|
+
this.dropPlaceholder = document.createElement('div');
|
|
222
297
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// Show line before target
|
|
234
|
-
this.dropIndicator.style.left =
|
|
235
|
-
targetRect.left - containerRect.left + 'px';
|
|
236
|
-
}
|
|
237
|
-
} else {
|
|
238
|
-
// For vertical layout, show horizontal line
|
|
239
|
-
this.dropIndicator.style.width = targetRect.width + 'px';
|
|
240
|
-
this.dropIndicator.style.left =
|
|
241
|
-
targetRect.left - containerRect.left + 'px';
|
|
242
|
-
|
|
243
|
-
if (insertAfter) {
|
|
244
|
-
// Show line after target
|
|
245
|
-
this.dropIndicator.style.top =
|
|
246
|
-
targetRect.bottom - containerRect.top + 'px';
|
|
247
|
-
} else {
|
|
248
|
-
// Show line before target
|
|
249
|
-
this.dropIndicator.style.top =
|
|
250
|
-
targetRect.top - containerRect.top + 'px';
|
|
251
|
-
}
|
|
298
|
+
this.dropPlaceholder.className = 'drop-placeholder sortable';
|
|
299
|
+
|
|
300
|
+
// Copy dimensions from the original element (before it was hidden)
|
|
301
|
+
if (this.originalElementRect) {
|
|
302
|
+
const rect = this.originalElementRect;
|
|
303
|
+
this.dropPlaceholder.style.width = rect.width + 'px';
|
|
304
|
+
this.dropPlaceholder.style.height = rect.height + 'px';
|
|
305
|
+
this.dropPlaceholder.style.minHeight = rect.height + 'px';
|
|
306
|
+
this.dropPlaceholder.style.borderRadius = 'var(--curvature)';
|
|
307
|
+
this.dropPlaceholder.style.flexShrink = '0';
|
|
252
308
|
}
|
|
253
309
|
|
|
254
|
-
|
|
310
|
+
// Insert the placeholder in the correct position in the DOM
|
|
311
|
+
if (insertAfter) {
|
|
312
|
+
targetElement.insertAdjacentElement('afterend', this.dropPlaceholder);
|
|
313
|
+
} else {
|
|
314
|
+
targetElement.insertAdjacentElement('beforebegin', this.dropPlaceholder);
|
|
315
|
+
}
|
|
255
316
|
}
|
|
256
317
|
|
|
257
|
-
private
|
|
258
|
-
if (this.
|
|
259
|
-
this.
|
|
260
|
-
this.
|
|
318
|
+
private hideDropPlaceholder() {
|
|
319
|
+
if (this.dropPlaceholder) {
|
|
320
|
+
this.dropPlaceholder.remove();
|
|
321
|
+
this.dropPlaceholder = null;
|
|
261
322
|
}
|
|
262
323
|
}
|
|
263
324
|
|
|
325
|
+
private showInitialPlaceholder() {
|
|
326
|
+
if (!this.downEle || !this.originalElementRect) return;
|
|
327
|
+
|
|
328
|
+
this.dropPlaceholder = document.createElement('div');
|
|
329
|
+
this.dropPlaceholder.className = 'drop-placeholder sortable';
|
|
330
|
+
|
|
331
|
+
// Copy dimensions from the original element
|
|
332
|
+
const rect = this.originalElementRect;
|
|
333
|
+
this.dropPlaceholder.style.width = rect.width + 'px';
|
|
334
|
+
this.dropPlaceholder.style.height = rect.height + 'px';
|
|
335
|
+
this.dropPlaceholder.style.minHeight = rect.height + 'px';
|
|
336
|
+
this.dropPlaceholder.style.borderRadius = 'var(--curvature)';
|
|
337
|
+
this.dropPlaceholder.style.flexShrink = '0';
|
|
338
|
+
this.dropPlaceholder.style.background =
|
|
339
|
+
'rgba(var(--color-primary-rgb), 0.1)';
|
|
340
|
+
this.dropPlaceholder.style.border =
|
|
341
|
+
'2px dashed rgba(var(--color-primary-rgb), 0.3)';
|
|
342
|
+
|
|
343
|
+
// Insert the placeholder right after the hidden original element
|
|
344
|
+
this.downEle.insertAdjacentElement('afterend', this.dropPlaceholder);
|
|
345
|
+
}
|
|
346
|
+
|
|
264
347
|
private handleMouseDown(event: MouseEvent) {
|
|
265
348
|
let ele = event.target as HTMLDivElement;
|
|
266
349
|
|
|
@@ -275,14 +358,14 @@ export class SortableList extends RapidElement {
|
|
|
275
358
|
if (ele) {
|
|
276
359
|
event.preventDefault();
|
|
277
360
|
event.stopPropagation();
|
|
278
|
-
|
|
279
361
|
this.downEle = ele;
|
|
280
362
|
this.draggingId = ele.id;
|
|
281
363
|
this.draggingIdx = this.getRowIndex(ele.id);
|
|
282
364
|
this.draggingEle = ele;
|
|
283
365
|
|
|
284
|
-
// Use getBoundingClientRect for accurate offsets
|
|
366
|
+
// Use getBoundingClientRect for accurate offsets and store original dimensions
|
|
285
367
|
const rect = ele.getBoundingClientRect();
|
|
368
|
+
this.originalElementRect = rect; // Store the original rect before hiding
|
|
286
369
|
this.xOffset = event.clientX - rect.left;
|
|
287
370
|
this.yOffset = event.clientY - rect.top;
|
|
288
371
|
this.yDown = event.clientY;
|
|
@@ -304,38 +387,43 @@ export class SortableList extends RapidElement {
|
|
|
304
387
|
id: this.downEle.id
|
|
305
388
|
});
|
|
306
389
|
|
|
307
|
-
|
|
308
|
-
this.
|
|
390
|
+
// Capture the original index BEFORE hiding the element
|
|
391
|
+
this.originalDragIndex = this.getRowIndex(this.downEle.id);
|
|
309
392
|
|
|
310
|
-
//
|
|
311
|
-
this.
|
|
312
|
-
|
|
393
|
+
// Create a clone of the element to use as the ghost
|
|
394
|
+
this.ghostElement = this.cloneElementWithState(
|
|
395
|
+
this.downEle
|
|
396
|
+
) as HTMLDivElement;
|
|
313
397
|
|
|
314
|
-
|
|
315
|
-
this.
|
|
398
|
+
// Hide the original element during dragging using inline styles
|
|
399
|
+
this.originalDownDisplay = this.downEle.style.display;
|
|
400
|
+
this.downEle.style.display = 'none';
|
|
401
|
+
|
|
402
|
+
// Style the clone as a ghost
|
|
403
|
+
this.ghostElement.classList.add('ghost');
|
|
404
|
+
|
|
405
|
+
// Use the stored original dimensions for positioning
|
|
406
|
+
const rect = this.originalElementRect;
|
|
316
407
|
|
|
317
|
-
this.ghostElement.style.width = rect.width + 'px';
|
|
318
|
-
this.ghostElement.style.height = rect.height + 'px';
|
|
319
408
|
this.ghostElement.style.position = 'fixed';
|
|
320
409
|
this.ghostElement.style.left = event.clientX - this.xOffset + 'px';
|
|
321
410
|
this.ghostElement.style.top = event.clientY - this.yOffset + 'px';
|
|
322
|
-
this.ghostElement.style.
|
|
323
|
-
this.ghostElement.style.
|
|
324
|
-
'1px solid var(--color-primary, #1c7cd6)';
|
|
411
|
+
this.ghostElement.style.width = rect.width + 'px';
|
|
412
|
+
this.ghostElement.style.height = rect.height + 'px';
|
|
325
413
|
this.ghostElement.style.zIndex = '99999';
|
|
326
|
-
this.ghostElement.style.
|
|
327
|
-
this.ghostElement.style.
|
|
328
|
-
this.ghostElement.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.2)';
|
|
329
|
-
this.ghostElement.style.borderRadius = 'var(--curvature)';
|
|
414
|
+
this.ghostElement.style.opacity = '0.8';
|
|
415
|
+
this.ghostElement.style.transform = 'scale(1.03)';
|
|
330
416
|
|
|
331
417
|
// allow component to customize the ghost node
|
|
332
418
|
if (this.prepareGhost) {
|
|
333
419
|
this.prepareGhost(this.ghostElement);
|
|
334
420
|
}
|
|
335
421
|
|
|
422
|
+
// Add the clone to document.body for dragging
|
|
336
423
|
document.body.appendChild(this.ghostElement);
|
|
337
424
|
|
|
338
|
-
//
|
|
425
|
+
// Show initial placeholder in the original position to maintain layout
|
|
426
|
+
this.showInitialPlaceholder();
|
|
339
427
|
|
|
340
428
|
// Add global click blocker when drag starts
|
|
341
429
|
if (!this.clickBlocker) {
|
|
@@ -356,17 +444,23 @@ export class SortableList extends RapidElement {
|
|
|
356
444
|
if (targetInfo) {
|
|
357
445
|
const { element: targetElement, insertAfter } = targetInfo;
|
|
358
446
|
const targetIdx = this.getRowIndex(targetElement.id);
|
|
359
|
-
const originalDragIdx = this.getRowIndex(this.draggingEle.id);
|
|
360
|
-
|
|
361
|
-
// Calculate the intended drop index
|
|
362
|
-
let dropIdx = targetIdx;
|
|
363
|
-
if (insertAfter) {
|
|
364
|
-
dropIdx += 1;
|
|
365
|
-
}
|
|
366
447
|
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
448
|
+
// Use the original drag index we captured before moving the element
|
|
449
|
+
const originalDragIdx = this.originalDragIndex;
|
|
450
|
+
|
|
451
|
+
// Calculate where the dragged element will end up in the final array
|
|
452
|
+
// targetIdx is the position of target element in current DOM (missing dragged element)
|
|
453
|
+
|
|
454
|
+
let dropIdx;
|
|
455
|
+
if (targetIdx < originalDragIdx) {
|
|
456
|
+
// Target is before the original drag position - moving backward
|
|
457
|
+
dropIdx = insertAfter ? targetIdx + 1 : targetIdx;
|
|
458
|
+
} else {
|
|
459
|
+
// Target was originally after the drag position - moving forward
|
|
460
|
+
// When moving the dragged element forward (i.e., to a higher index), the targetIdx is based on the current DOM,
|
|
461
|
+
// which no longer includes the dragged element. This means all elements after the original position have shifted left by one,
|
|
462
|
+
// so we need to subtract 1 from targetIdx to get the correct insertion index. If inserting after the target, we use targetIdx as is.
|
|
463
|
+
dropIdx = insertAfter ? targetIdx : targetIdx - 1;
|
|
370
464
|
}
|
|
371
465
|
|
|
372
466
|
// Store pending drop info but don't fire event yet
|
|
@@ -374,10 +468,10 @@ export class SortableList extends RapidElement {
|
|
|
374
468
|
this.pendingDropIndex = dropIdx;
|
|
375
469
|
this.pendingTargetElement = targetElement;
|
|
376
470
|
|
|
377
|
-
// Show drop
|
|
378
|
-
this.
|
|
471
|
+
// Show drop placeholder
|
|
472
|
+
this.showDropPlaceholder(targetElement, insertAfter);
|
|
379
473
|
} else {
|
|
380
|
-
this.
|
|
474
|
+
this.hideDropPlaceholder();
|
|
381
475
|
this.dropTargetId = null;
|
|
382
476
|
this.pendingDropIndex = -1;
|
|
383
477
|
this.pendingTargetElement = null;
|
|
@@ -390,16 +484,23 @@ export class SortableList extends RapidElement {
|
|
|
390
484
|
evt.preventDefault();
|
|
391
485
|
evt.stopPropagation();
|
|
392
486
|
|
|
393
|
-
//
|
|
487
|
+
// Remove the ghost clone from document.body
|
|
488
|
+
if (this.ghostElement) {
|
|
489
|
+
this.ghostElement.remove();
|
|
490
|
+
}
|
|
394
491
|
|
|
492
|
+
// Restore visibility of the original element by clearing inline styles
|
|
395
493
|
if (this.downEle) {
|
|
396
|
-
this.downEle.style.
|
|
397
|
-
this.downEle.style.opacity = '1';
|
|
494
|
+
this.downEle.style.display = this.originalDownDisplay;
|
|
398
495
|
}
|
|
399
496
|
|
|
497
|
+
// Clear visual effects before firing events
|
|
498
|
+
this.hideDropPlaceholder();
|
|
499
|
+
|
|
400
500
|
// fire the order changed event only when dropped if we have a valid drop position
|
|
401
501
|
if (this.pendingDropIndex >= 0 && this.pendingTargetElement) {
|
|
402
|
-
|
|
502
|
+
// Use the original drag index we captured before hiding the element
|
|
503
|
+
const originalDragIdx = this.originalDragIndex;
|
|
403
504
|
|
|
404
505
|
// use swap-based logic - report which indexes need to be swapped
|
|
405
506
|
const fromIdx = originalDragIdx;
|
|
@@ -420,18 +521,15 @@ export class SortableList extends RapidElement {
|
|
|
420
521
|
this.draggingId = null;
|
|
421
522
|
this.dropTargetId = null;
|
|
422
523
|
this.downEle = null;
|
|
524
|
+
this.originalElementRect = null;
|
|
525
|
+
this.originalDragIndex = -1;
|
|
423
526
|
this.pendingDropIndex = -1;
|
|
424
527
|
this.pendingTargetElement = null;
|
|
425
528
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (this.ghostElement.parentNode) {
|
|
429
|
-
this.ghostElement.parentNode.removeChild(this.ghostElement);
|
|
430
|
-
}
|
|
431
|
-
this.ghostElement = null;
|
|
432
|
-
}
|
|
529
|
+
// Clear the ghost reference since we removed it
|
|
530
|
+
this.ghostElement = null;
|
|
433
531
|
|
|
434
|
-
this.
|
|
532
|
+
this.hideDropPlaceholder();
|
|
435
533
|
|
|
436
534
|
// Keep the click blocker active for a short time after drop
|
|
437
535
|
if (this.clickBlocker) {
|
|
@@ -451,7 +549,10 @@ export class SortableList extends RapidElement {
|
|
|
451
549
|
|
|
452
550
|
public render(): TemplateResult {
|
|
453
551
|
return html`
|
|
454
|
-
<div
|
|
552
|
+
<div
|
|
553
|
+
class="container ${this.horizontal ? 'horizontal' : ''}"
|
|
554
|
+
style="gap: ${this.gap}"
|
|
555
|
+
>
|
|
455
556
|
<slot @mousedown=${this.handleMouseDown}></slot>
|
|
456
557
|
</div>
|
|
457
558
|
`;
|
package/src/live/ContactChat.ts
CHANGED
|
@@ -789,16 +789,13 @@ export class ContactChat extends ContactStoreElement {
|
|
|
789
789
|
text: renderChannelEvent(event as ChannelEvent)
|
|
790
790
|
};
|
|
791
791
|
break;
|
|
792
|
+
default:
|
|
793
|
+
console.error('Unknown event type', event);
|
|
792
794
|
}
|
|
793
795
|
|
|
794
|
-
if (message
|
|
796
|
+
if (message) {
|
|
797
|
+
message.id = event.uuid;
|
|
795
798
|
message.date = new Date(event.created_on);
|
|
796
|
-
} else {
|
|
797
|
-
console.error('Unknown event type', event);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
if (!message.id) {
|
|
801
|
-
message.id = event.uuid || event.type + '@' + event.created_on;
|
|
802
799
|
}
|
|
803
800
|
|
|
804
801
|
return message;
|
|
@@ -855,9 +852,14 @@ export class ContactChat extends ContactStoreElement {
|
|
|
855
852
|
} else if (
|
|
856
853
|
event.type === 'msg_created' ||
|
|
857
854
|
event.type === 'msg_received' ||
|
|
855
|
+
event.type === 'ivr_created' ||
|
|
858
856
|
event.type === 'broadcast_created'
|
|
859
857
|
) {
|
|
860
858
|
const msgEvent = event as MsgEvent;
|
|
859
|
+
const status = msgEvent.status || msgEvent._status;
|
|
860
|
+
const failedReason =
|
|
861
|
+
msgEvent.failed_reason_display || msgEvent._failed_reason;
|
|
862
|
+
|
|
861
863
|
messages.push({
|
|
862
864
|
id: event.uuid,
|
|
863
865
|
type: msgEvent.type === 'msg_received' ? 'msg_in' : 'msg_out',
|
|
@@ -865,7 +867,7 @@ export class ContactChat extends ContactStoreElement {
|
|
|
865
867
|
date: new Date(msgEvent.created_on),
|
|
866
868
|
attachments: msgEvent.msg.attachments,
|
|
867
869
|
text: msgEvent.msg.text,
|
|
868
|
-
sendError:
|
|
870
|
+
sendError: status === 'E' || status === 'F',
|
|
869
871
|
popup: html`<div
|
|
870
872
|
style="display: flex; flex-direction: row; align-items:center; justify-content: space-between;font-size:0.9em;line-height:1em;min-width:10em"
|
|
871
873
|
>
|
|
@@ -880,25 +882,30 @@ export class ContactChat extends ContactStoreElement {
|
|
|
880
882
|
${msgEvent.optin.name}
|
|
881
883
|
</div>`
|
|
882
884
|
: null}
|
|
883
|
-
${
|
|
885
|
+
${failedReason
|
|
884
886
|
? html`
|
|
885
887
|
<div
|
|
886
888
|
style="margin-top:0.2em;margin-right: 0.5em;min-width:10em;max-width:15em;color:var(--color-error);font-size:0.9em"
|
|
887
889
|
>
|
|
888
|
-
${
|
|
890
|
+
${failedReason}
|
|
889
891
|
</div>
|
|
890
892
|
`
|
|
891
893
|
: null}
|
|
892
894
|
</div>
|
|
893
|
-
${msgEvent.logs_url
|
|
894
|
-
? html`<a
|
|
895
|
+
${msgEvent.logs_url || msgEvent._logs_url
|
|
896
|
+
? html`<a
|
|
897
|
+
style="margin-left:0.5em"
|
|
898
|
+
href="${msgEvent.logs_url || msgEvent._logs_url}"
|
|
895
899
|
><temba-icon name="log"></temba-icon
|
|
896
900
|
></a>`
|
|
897
901
|
: null}
|
|
898
902
|
</div> `
|
|
899
903
|
});
|
|
900
904
|
} else {
|
|
901
|
-
|
|
905
|
+
const msg = this.getEventMessage(event);
|
|
906
|
+
if (msg) {
|
|
907
|
+
messages.push(msg);
|
|
908
|
+
}
|
|
902
909
|
}
|
|
903
910
|
});
|
|
904
911
|
|