@nyaruka/temba-components 0.135.9 → 0.136.1
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 +25 -0
- package/demo/components/webchat/example.html +4 -2
- package/dist/static/svg/index.svg +1 -1
- package/dist/temba-components.js +1351 -322
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/Icons.js +2 -1
- package/out-tsc/src/Icons.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +2 -6
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +29 -1
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +229 -5
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +320 -1
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/interfaces.js +1 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/layout/FloatingWindow.js +30 -8
- package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +1861 -0
- package/out-tsc/src/simulator/Simulator.js.map +1 -0
- package/out-tsc/src/store/AppState.js +66 -0
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/src/utils.js +48 -0
- package/out-tsc/src/utils.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-appstate-node-sorting.test.js +430 -0
- package/out-tsc/test/temba-appstate-node-sorting.test.js.map +1 -0
- package/out-tsc/test/temba-floating-tab.test.js +0 -9
- package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor.test.js +262 -1
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js +3 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +3 -1
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
- package/out-tsc/test/temba-simulator.test.js +642 -0
- package/out-tsc/test/temba-simulator.test.js.map +1 -0
- package/out-tsc/test/utils.test.js +1 -1
- package/out-tsc/test/utils.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/add_contact_urn/render/expression-facebook.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/whatsapp.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_broadcast/render/contacts-only.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/multiline-text.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/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/actions/start_session/render/contact-query.png +0 -0
- package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
- package/screenshots/truth/floating-tab/gray.png +0 -0
- package/screenshots/truth/floating-tab/green.png +0 -0
- package/screenshots/truth/floating-tab/purple.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/translation-task.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/render/basic-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
- package/screenshots/truth/simulator/after-message-sent.png +0 -0
- package/screenshots/truth/simulator/after-reset.png +0 -0
- package/screenshots/truth/simulator/attachment-menu.png +0 -0
- package/screenshots/truth/simulator/context-expanded.png +0 -0
- package/screenshots/truth/simulator/context-explorer-open.png +0 -0
- package/screenshots/truth/simulator/event-info.png +0 -0
- package/screenshots/truth/simulator/image-attachment.png +0 -0
- package/screenshots/truth/simulator/open-initial.png +0 -0
- package/screenshots/truth/simulator/quick-replies.png +0 -0
- package/src/Icons.ts +2 -1
- package/src/display/FloatingTab.ts +2 -7
- package/src/flow/CanvasNode.ts +30 -1
- package/src/flow/Editor.ts +246 -4
- package/src/flow/Plumber.ts +371 -2
- package/src/interfaces.ts +2 -1
- package/src/layout/FloatingWindow.ts +37 -12
- package/src/simulator/Simulator.ts +2061 -0
- package/src/store/AppState.ts +109 -0
- package/src/utils.ts +53 -0
- package/static/svg/index.svg +1 -1
- package/static/svg/work/traced/route.svg +1 -0
- package/static/svg/work/used/route.svg +3 -0
- package/temba-modules.ts +2 -0
- package/test/temba-appstate-node-sorting.test.ts +506 -0
- package/test/temba-floating-tab.test.ts +0 -11
- package/test/temba-flow-editor.test.ts +298 -1
- package/test/temba-flow-plumber-connections.test.ts +4 -1
- package/test/temba-flow-plumber.test.ts +4 -1
- package/test/temba-simulator.test.ts +866 -0
- package/test/utils.test.ts +1 -1
package/src/flow/Plumber.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
INTERCEPT_BEFORE_DETACH,
|
|
13
13
|
EVENT_CONNECTION_DETACHED
|
|
14
14
|
} from '@jsplumb/browser-ui';
|
|
15
|
+
import { getStore } from '../store/Store';
|
|
15
16
|
|
|
16
17
|
const CONNECTOR_DEFAULTS = {
|
|
17
18
|
type: FlowchartConnector.type,
|
|
@@ -79,8 +80,17 @@ export class Plumber {
|
|
|
79
80
|
private connectionListeners = new Map();
|
|
80
81
|
public connectionDragging = false;
|
|
81
82
|
private connectionWait = null;
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
private activityData: { segments: { [key: string]: number } } | null = null;
|
|
84
|
+
private hoveredActivityKey: string | null = null;
|
|
85
|
+
private recentContactsPopup: HTMLElement | null = null;
|
|
86
|
+
private recentContactsCache: { [key: string]: any[] } = {};
|
|
87
|
+
private pendingFetches: { [key: string]: AbortController } = {};
|
|
88
|
+
private hideContactsTimeout: number | null = null;
|
|
89
|
+
private showContactsTimeout: number | null = null;
|
|
90
|
+
private editor: any;
|
|
91
|
+
|
|
92
|
+
constructor(canvas: HTMLElement, editor: any) {
|
|
93
|
+
this.editor = editor;
|
|
84
94
|
ready(() => {
|
|
85
95
|
this.jsPlumb = newInstance({
|
|
86
96
|
container: canvas,
|
|
@@ -210,6 +220,365 @@ export class Plumber {
|
|
|
210
220
|
this.processPendingConnections();
|
|
211
221
|
}
|
|
212
222
|
|
|
223
|
+
public setActivityData(
|
|
224
|
+
activityData: { segments: { [key: string]: number } } | null
|
|
225
|
+
) {
|
|
226
|
+
this.activityData = activityData;
|
|
227
|
+
// Clear recent contacts cache when activity data changes
|
|
228
|
+
this.clearRecentContactsCache();
|
|
229
|
+
this.updateActivityOverlays();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private updateActivityOverlays() {
|
|
233
|
+
if (!this.jsPlumb || !this.activityData) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Get all connections
|
|
238
|
+
const connections = this.jsPlumb.getConnections();
|
|
239
|
+
|
|
240
|
+
connections.forEach((connection: any) => {
|
|
241
|
+
// Get the source exit element
|
|
242
|
+
const sourceElement = connection.source;
|
|
243
|
+
if (!sourceElement) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Get destination node
|
|
248
|
+
const targetElement = connection.target;
|
|
249
|
+
if (!targetElement) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Create activity key: exitUuid:destinationUuid
|
|
254
|
+
const exitUuid = sourceElement.id;
|
|
255
|
+
const destinationUuid = targetElement.id;
|
|
256
|
+
const activityKey = `${exitUuid}:${destinationUuid}`;
|
|
257
|
+
|
|
258
|
+
// Get activity count for this segment
|
|
259
|
+
const count = this.activityData.segments[activityKey];
|
|
260
|
+
|
|
261
|
+
// Remove existing activity overlays
|
|
262
|
+
connection.removeOverlay('activity-label');
|
|
263
|
+
|
|
264
|
+
// Add new overlay if there's activity
|
|
265
|
+
if (count && count > 0) {
|
|
266
|
+
const overlay = connection.addOverlay({
|
|
267
|
+
type: 'Label',
|
|
268
|
+
options: {
|
|
269
|
+
label: count.toLocaleString(),
|
|
270
|
+
id: 'activity-label',
|
|
271
|
+
cssClass: 'activity-overlay',
|
|
272
|
+
location: 20 // Fixed pixel distance from the start (exit point)
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Add hover events for recent contacts popup
|
|
277
|
+
// Use setTimeout to ensure the overlay is fully rendered
|
|
278
|
+
setTimeout(() => {
|
|
279
|
+
// Try multiple ways to get the overlay element
|
|
280
|
+
let overlayElement =
|
|
281
|
+
overlay.canvas || overlay.element || overlay.getElement?.();
|
|
282
|
+
|
|
283
|
+
// If still not found, query the DOM directly
|
|
284
|
+
if (!overlayElement) {
|
|
285
|
+
const overlays = connection.getOverlays();
|
|
286
|
+
if (Array.isArray(overlays)) {
|
|
287
|
+
for (const ovl of overlays) {
|
|
288
|
+
if (ovl.id === 'activity-label') {
|
|
289
|
+
overlayElement =
|
|
290
|
+
ovl.canvas || ovl.element || ovl.getElement?.();
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Also try querying by CSS class
|
|
298
|
+
if (!overlayElement && connection.canvas) {
|
|
299
|
+
overlayElement =
|
|
300
|
+
connection.canvas.querySelector('.activity-overlay');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (overlayElement) {
|
|
304
|
+
overlayElement.style.cursor = 'pointer';
|
|
305
|
+
overlayElement.setAttribute('data-activity-key', activityKey);
|
|
306
|
+
overlayElement.addEventListener('mouseenter', () => {
|
|
307
|
+
// Don't show recent contacts when simulator is active
|
|
308
|
+
const store = getStore();
|
|
309
|
+
if (store?.getState().simulatorActive) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Get flow UUID from the editor element
|
|
314
|
+
const editor = document.querySelector('temba-flow-editor') as any;
|
|
315
|
+
const flowUuid = editor?.definition?.uuid;
|
|
316
|
+
if (flowUuid) {
|
|
317
|
+
// Start fetching immediately
|
|
318
|
+
this.fetchRecentContacts(activityKey, flowUuid);
|
|
319
|
+
|
|
320
|
+
// But delay showing the popup by half a second
|
|
321
|
+
this.showContactsTimeout = window.setTimeout(() => {
|
|
322
|
+
this.showRecentContacts(activityKey, flowUuid);
|
|
323
|
+
}, 500);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
overlayElement.addEventListener('mouseleave', () => {
|
|
327
|
+
// Cancel the show timeout if still pending
|
|
328
|
+
if (this.showContactsTimeout) {
|
|
329
|
+
clearTimeout(this.showContactsTimeout);
|
|
330
|
+
this.showContactsTimeout = null;
|
|
331
|
+
}
|
|
332
|
+
this.hoveredActivityKey = null;
|
|
333
|
+
this.hideRecentContacts();
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}, 50);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Force repaint to ensure overlays are positioned correctly
|
|
341
|
+
this.repaintEverything();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private findOverlayElement(activityKey: string): HTMLElement | null {
|
|
345
|
+
// Find overlay by data attribute
|
|
346
|
+
const overlays = document.querySelectorAll('.activity-overlay');
|
|
347
|
+
for (const overlay of overlays) {
|
|
348
|
+
if (overlay.getAttribute('data-activity-key') === activityKey) {
|
|
349
|
+
return overlay as HTMLElement;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private async fetchRecentContacts(activityKey: string, flowUuid: string) {
|
|
356
|
+
// Skip if already cached or currently fetching
|
|
357
|
+
if (
|
|
358
|
+
this.recentContactsCache[activityKey] ||
|
|
359
|
+
this.pendingFetches[activityKey]
|
|
360
|
+
) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Cancel any pending fetch for this key
|
|
365
|
+
if (this.pendingFetches[activityKey]) {
|
|
366
|
+
this.pendingFetches[activityKey].abort();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Fetch recent contacts from endpoint
|
|
370
|
+
const controller = new AbortController();
|
|
371
|
+
this.pendingFetches[activityKey] = controller;
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
// Parse exit UUID and destination UUID from activity key
|
|
375
|
+
const [exitUuid, destinationUuid] = activityKey.split(':');
|
|
376
|
+
|
|
377
|
+
const endpoint = `/flow/recent_contacts/${flowUuid}/${exitUuid}/${destinationUuid}/`;
|
|
378
|
+
|
|
379
|
+
const response = await fetch(endpoint, {
|
|
380
|
+
signal: controller.signal
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
if (!response.ok) {
|
|
384
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const data = await response.json();
|
|
388
|
+
// API returns array directly, not wrapped in results
|
|
389
|
+
const recentContacts = Array.isArray(data) ? data : data.results || [];
|
|
390
|
+
|
|
391
|
+
// Cache the results
|
|
392
|
+
this.recentContactsCache[activityKey] = recentContacts;
|
|
393
|
+
} catch (error) {
|
|
394
|
+
if ((error as Error).name !== 'AbortError') {
|
|
395
|
+
console.error('Failed to fetch recent contacts:', error);
|
|
396
|
+
}
|
|
397
|
+
} finally {
|
|
398
|
+
delete this.pendingFetches[activityKey];
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private async showRecentContacts(activityKey: string, flowUuid: string) {
|
|
403
|
+
// Don't show recent contacts when simulator is active
|
|
404
|
+
const store = getStore();
|
|
405
|
+
if (store?.getState().simulatorActive) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Find the overlay element fresh to avoid stale references
|
|
410
|
+
const overlayElement = this.findOverlayElement(activityKey);
|
|
411
|
+
if (!overlayElement) {
|
|
412
|
+
console.warn('Could not find overlay element for activity:', activityKey);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
// Clear any pending hide timeout
|
|
416
|
+
if (this.hideContactsTimeout) {
|
|
417
|
+
clearTimeout(this.hideContactsTimeout);
|
|
418
|
+
this.hideContactsTimeout = null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
this.hoveredActivityKey = activityKey;
|
|
422
|
+
|
|
423
|
+
// Create popup if it doesn't exist
|
|
424
|
+
if (!this.recentContactsPopup) {
|
|
425
|
+
this.recentContactsPopup = document.createElement('div');
|
|
426
|
+
this.recentContactsPopup.className = 'recent-contacts-popup';
|
|
427
|
+
// Add inline styles to ensure visibility
|
|
428
|
+
this.recentContactsPopup.style.position = 'absolute';
|
|
429
|
+
this.recentContactsPopup.style.width = '200px';
|
|
430
|
+
this.recentContactsPopup.style.background = '#f3f3f3';
|
|
431
|
+
this.recentContactsPopup.style.borderRadius = '10px';
|
|
432
|
+
this.recentContactsPopup.style.boxShadow =
|
|
433
|
+
'0 1px 3px 1px rgba(130, 130, 130, 0.2)';
|
|
434
|
+
this.recentContactsPopup.style.zIndex = '1015';
|
|
435
|
+
this.recentContactsPopup.style.display = 'none';
|
|
436
|
+
document.body.appendChild(this.recentContactsPopup);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Add hover events to keep popup open (only needs to be done once)
|
|
440
|
+
if (!this.recentContactsPopup.onmouseenter) {
|
|
441
|
+
this.recentContactsPopup.onmouseenter = () => {
|
|
442
|
+
if (this.hideContactsTimeout) {
|
|
443
|
+
clearTimeout(this.hideContactsTimeout);
|
|
444
|
+
this.hideContactsTimeout = null;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
this.recentContactsPopup.onmouseleave = () => {
|
|
448
|
+
this.hoveredActivityKey = null;
|
|
449
|
+
this.hideRecentContacts();
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// Add click event listener for contact names
|
|
453
|
+
this.recentContactsPopup.onclick = (e: MouseEvent) => {
|
|
454
|
+
const target = e.target as HTMLElement;
|
|
455
|
+
|
|
456
|
+
if (target.classList.contains('contact-name')) {
|
|
457
|
+
this.hideRecentContacts(false);
|
|
458
|
+
const contactUuid = target.getAttribute('data-uuid');
|
|
459
|
+
if (contactUuid) {
|
|
460
|
+
// Fire custom event through editor
|
|
461
|
+
this.editor.fireCustomEvent('temba-contact-clicked', {
|
|
462
|
+
uuid: contactUuid
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Check cache first
|
|
470
|
+
if (this.recentContactsCache[activityKey]) {
|
|
471
|
+
this.renderRecentContactsPopup(this.recentContactsCache[activityKey]);
|
|
472
|
+
this.positionPopup(overlayElement);
|
|
473
|
+
} else {
|
|
474
|
+
// Show loading state if data isn't ready yet
|
|
475
|
+
this.recentContactsPopup.innerHTML =
|
|
476
|
+
'<div class="no-contacts-message">Loading...</div>';
|
|
477
|
+
this.positionPopup(overlayElement);
|
|
478
|
+
|
|
479
|
+
// Wait for the fetch to complete
|
|
480
|
+
await this.fetchRecentContacts(activityKey, flowUuid);
|
|
481
|
+
|
|
482
|
+
// Render if still hovering over this activity
|
|
483
|
+
if (this.hoveredActivityKey === activityKey) {
|
|
484
|
+
const contacts = this.recentContactsCache[activityKey] || [];
|
|
485
|
+
this.renderRecentContactsPopup(contacts);
|
|
486
|
+
this.positionPopup(overlayElement);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private positionPopup(overlayElement: HTMLElement) {
|
|
492
|
+
if (!this.recentContactsPopup) return;
|
|
493
|
+
|
|
494
|
+
// Position popup near the overlay
|
|
495
|
+
const rect = overlayElement.getBoundingClientRect();
|
|
496
|
+
this.recentContactsPopup.style.left = `${rect.left + window.scrollX}px`;
|
|
497
|
+
this.recentContactsPopup.style.top = `${
|
|
498
|
+
rect.bottom + window.scrollY + 5
|
|
499
|
+
}px`;
|
|
500
|
+
|
|
501
|
+
// Remove inline display style so CSS class can work
|
|
502
|
+
this.recentContactsPopup.style.display = '';
|
|
503
|
+
|
|
504
|
+
// Trigger animation by adding class
|
|
505
|
+
this.recentContactsPopup.classList.remove('show');
|
|
506
|
+
// Force reflow to restart animation
|
|
507
|
+
void this.recentContactsPopup.offsetWidth;
|
|
508
|
+
this.recentContactsPopup.classList.add('show');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private renderRecentContactsPopup(recentContacts: any[]) {
|
|
512
|
+
if (!this.recentContactsPopup) return;
|
|
513
|
+
|
|
514
|
+
const hasContacts = recentContacts.length > 0;
|
|
515
|
+
|
|
516
|
+
if (!hasContacts) {
|
|
517
|
+
// Simple message when no contacts
|
|
518
|
+
this.recentContactsPopup.innerHTML =
|
|
519
|
+
'<div class="no-contacts-message">No Recent Contacts</div>';
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
let html = `<div class="popup-title">Recent Contacts</div>`;
|
|
524
|
+
|
|
525
|
+
recentContacts.forEach((contact: any) => {
|
|
526
|
+
html += `<div class="contact-row">`;
|
|
527
|
+
html += `<div class="contact-name" data-uuid="${contact.contact.uuid}">${contact.contact.name}</div>`;
|
|
528
|
+
if (contact.operand) {
|
|
529
|
+
html += `<div class="contact-operand">${contact.operand}</div>`;
|
|
530
|
+
}
|
|
531
|
+
if (contact.time) {
|
|
532
|
+
const time = new Date(contact.time);
|
|
533
|
+
const now = new Date();
|
|
534
|
+
const diffMs = now.getTime() - time.getTime();
|
|
535
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
536
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
537
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
538
|
+
|
|
539
|
+
let timeStr = '';
|
|
540
|
+
if (diffMins < 1) timeStr = 'just now';
|
|
541
|
+
else if (diffMins < 60) timeStr = `${diffMins}m ago`;
|
|
542
|
+
else if (diffHours < 24) timeStr = `${diffHours}h ago`;
|
|
543
|
+
else timeStr = `${diffDays}d ago`;
|
|
544
|
+
|
|
545
|
+
html += `<div class="contact-time">${timeStr}</div>`;
|
|
546
|
+
}
|
|
547
|
+
html += `</div>`;
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
this.recentContactsPopup.innerHTML = html;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private hideRecentContacts(wait = true) {
|
|
554
|
+
if (!wait) {
|
|
555
|
+
if (this.recentContactsPopup) {
|
|
556
|
+
this.recentContactsPopup.classList.remove('show');
|
|
557
|
+
this.recentContactsPopup.style.display = 'none';
|
|
558
|
+
this.hoveredActivityKey = null;
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
this.hideContactsTimeout = window.setTimeout(() => {
|
|
564
|
+
// Check if we're still hovering over an activity
|
|
565
|
+
if (!this.hoveredActivityKey && this.recentContactsPopup) {
|
|
566
|
+
this.recentContactsPopup.classList.remove('show');
|
|
567
|
+
this.recentContactsPopup.style.display = 'none';
|
|
568
|
+
this.hoveredActivityKey = null;
|
|
569
|
+
}
|
|
570
|
+
}, 200); // Small delay to allow moving between overlay and popup
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
public clearRecentContactsCache() {
|
|
574
|
+
this.recentContactsCache = {};
|
|
575
|
+
// Cancel any pending fetches
|
|
576
|
+
Object.values(this.pendingFetches).forEach((controller) =>
|
|
577
|
+
controller.abort()
|
|
578
|
+
);
|
|
579
|
+
this.pendingFetches = {};
|
|
580
|
+
}
|
|
581
|
+
|
|
213
582
|
public repaintEverything() {
|
|
214
583
|
if (this.jsPlumb) {
|
|
215
584
|
this.jsPlumb.repaintEverything();
|
package/src/interfaces.ts
CHANGED
|
@@ -302,5 +302,6 @@ export enum CustomEventType {
|
|
|
302
302
|
ActionEditCanceled = 'temba-action-edit-canceled',
|
|
303
303
|
NodeEditRequested = 'temba-node-edit-requested',
|
|
304
304
|
NodeSaved = 'temba-node-saved',
|
|
305
|
-
NodeEditCancelled = 'temba-node-edit-cancelled'
|
|
305
|
+
NodeEditCancelled = 'temba-node-edit-cancelled',
|
|
306
|
+
FollowSimulation = 'temba-follow-simulation'
|
|
306
307
|
}
|
|
@@ -32,6 +32,11 @@ export class FloatingWindow extends RapidElement {
|
|
|
32
32
|
background: transparent;
|
|
33
33
|
border-radius: 0;
|
|
34
34
|
box-shadow: none;
|
|
35
|
+
pointer-events: none;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.window.chromeless .body {
|
|
39
|
+
pointer-events: none;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
.window.dragging {
|
|
@@ -126,6 +131,18 @@ export class FloatingWindow extends RapidElement {
|
|
|
126
131
|
@property({ type: String })
|
|
127
132
|
color = '#6B7280';
|
|
128
133
|
|
|
134
|
+
@property({ type: Number })
|
|
135
|
+
leftBoundaryMargin = 0;
|
|
136
|
+
|
|
137
|
+
@property({ type: Number })
|
|
138
|
+
rightBoundaryMargin = 0;
|
|
139
|
+
|
|
140
|
+
@property({ type: Number })
|
|
141
|
+
topBoundaryMargin = 0;
|
|
142
|
+
|
|
143
|
+
@property({ type: Number })
|
|
144
|
+
bottomBoundaryMargin = 0;
|
|
145
|
+
|
|
129
146
|
private dragStartX = 0;
|
|
130
147
|
private dragStartY = 0;
|
|
131
148
|
private dragOffsetX = 0;
|
|
@@ -221,7 +238,7 @@ export class FloatingWindow extends RapidElement {
|
|
|
221
238
|
}
|
|
222
239
|
}
|
|
223
240
|
|
|
224
|
-
|
|
241
|
+
public handleClose() {
|
|
225
242
|
this.hidden = true;
|
|
226
243
|
// show all tabs when window is closed
|
|
227
244
|
FloatingTab.showAllTabs();
|
|
@@ -258,8 +275,11 @@ export class FloatingWindow extends RapidElement {
|
|
|
258
275
|
// keep window within viewport bounds with 20px padding
|
|
259
276
|
const padding = 20;
|
|
260
277
|
this.left = Math.max(
|
|
261
|
-
padding,
|
|
262
|
-
Math.min(
|
|
278
|
+
padding - this.leftBoundaryMargin,
|
|
279
|
+
Math.min(
|
|
280
|
+
this.left,
|
|
281
|
+
window.innerWidth - this.width - padding + this.rightBoundaryMargin
|
|
282
|
+
)
|
|
263
283
|
);
|
|
264
284
|
|
|
265
285
|
// get the actual rendered height of the window element
|
|
@@ -269,10 +289,13 @@ export class FloatingWindow extends RapidElement {
|
|
|
269
289
|
const currentHeight =
|
|
270
290
|
windowElement?.offsetHeight || this.maxHeight || window.innerHeight;
|
|
271
291
|
const maxTop = Math.max(
|
|
272
|
-
padding,
|
|
273
|
-
window.innerHeight - currentHeight - padding
|
|
292
|
+
padding - this.topBoundaryMargin,
|
|
293
|
+
window.innerHeight - currentHeight - padding + this.bottomBoundaryMargin
|
|
294
|
+
);
|
|
295
|
+
this.top = Math.max(
|
|
296
|
+
padding - this.topBoundaryMargin,
|
|
297
|
+
Math.min(this.top, maxTop)
|
|
274
298
|
);
|
|
275
|
-
this.top = Math.max(padding, Math.min(this.top, maxTop));
|
|
276
299
|
};
|
|
277
300
|
|
|
278
301
|
private handleMouseUp = () => {
|
|
@@ -297,11 +320,13 @@ export class FloatingWindow extends RapidElement {
|
|
|
297
320
|
|
|
298
321
|
// if positioned from right, always recalculate from right edge
|
|
299
322
|
if (this.positionFromRight) {
|
|
300
|
-
this.left =
|
|
323
|
+
this.left =
|
|
324
|
+
window.innerWidth - this.width - padding + this.rightBoundaryMargin;
|
|
301
325
|
} else {
|
|
302
326
|
// only adjust left if out of bounds
|
|
303
|
-
const minLeft = padding;
|
|
304
|
-
const maxLeft =
|
|
327
|
+
const minLeft = padding - this.leftBoundaryMargin;
|
|
328
|
+
const maxLeft =
|
|
329
|
+
window.innerWidth - this.width - padding + this.rightBoundaryMargin;
|
|
305
330
|
|
|
306
331
|
if (this.left < minLeft) {
|
|
307
332
|
this.left = minLeft;
|
|
@@ -311,10 +336,10 @@ export class FloatingWindow extends RapidElement {
|
|
|
311
336
|
}
|
|
312
337
|
|
|
313
338
|
// only adjust top if out of bounds
|
|
314
|
-
const minTop = padding;
|
|
339
|
+
const minTop = padding - this.topBoundaryMargin;
|
|
315
340
|
const maxTop = Math.max(
|
|
316
|
-
padding,
|
|
317
|
-
window.innerHeight - currentHeight - padding
|
|
341
|
+
padding - this.topBoundaryMargin,
|
|
342
|
+
window.innerHeight - currentHeight - padding + this.bottomBoundaryMargin
|
|
318
343
|
);
|
|
319
344
|
|
|
320
345
|
if (this.top < minTop) {
|