@nyaruka/temba-components 0.156.3 → 0.156.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 +21 -0
- package/dist/temba-components.js +918 -807
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/flow/CanvasNode.ts +102 -20
- package/src/flow/Editor.ts +529 -543
- package/src/flow/EditorToolbar.ts +566 -0
- package/src/flow/StickyNote.ts +29 -5
- package/src/flow/actions/set_contact_language.ts +4 -13
- package/src/flow/utils.ts +11 -0
- package/src/form/select/Select.ts +1 -0
- package/src/list/SortableList.ts +6 -0
- package/temba-modules.ts +2 -0
package/src/flow/Editor.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import { TEMBA_COMPONENTS_VERSION } from '../version';
|
|
32
32
|
import {
|
|
33
33
|
formatIssueMessage,
|
|
34
|
+
getLanguageDisplayName,
|
|
34
35
|
getNodeBounds,
|
|
35
36
|
calculateReflowPositions,
|
|
36
37
|
isRightClick,
|
|
@@ -70,8 +71,8 @@ import { Dialog } from '../layout/Dialog';
|
|
|
70
71
|
import { CanvasMenu, CanvasMenuSelection } from './CanvasMenu';
|
|
71
72
|
import { NodeTypeSelector, NodeTypeSelection } from './NodeTypeSelector';
|
|
72
73
|
import { FloatingWindow } from '../layout/FloatingWindow';
|
|
73
|
-
import { Icon } from '../Icons';
|
|
74
74
|
import { FlowSearch, SearchResult } from './FlowSearch';
|
|
75
|
+
import { PRIMARY_LANGUAGE_OPTION_VALUE } from './EditorToolbar';
|
|
75
76
|
|
|
76
77
|
export function findNodeForExit(
|
|
77
78
|
definition: FlowDefinition,
|
|
@@ -103,8 +104,9 @@ export interface SelectionBox {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
const DRAG_THRESHOLD = 5;
|
|
106
|
-
const AUTO_SCROLL_EDGE_ZONE =
|
|
107
|
+
const AUTO_SCROLL_EDGE_ZONE = 150;
|
|
107
108
|
const AUTO_SCROLL_MAX_SPEED = 15;
|
|
109
|
+
const AUTO_SCROLL_BEYOND_MULTIPLIER = 5;
|
|
108
110
|
|
|
109
111
|
type TranslationType = 'property' | 'category';
|
|
110
112
|
|
|
@@ -134,7 +136,15 @@ interface LocalizationUpdate {
|
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
const AUTO_TRANSLATE_MODELS_ENDPOINT = '/api/internal/llms.json';
|
|
137
|
-
|
|
139
|
+
export type ToolbarAction =
|
|
140
|
+
| { action: 'view-change'; view: 'flow' | 'table' }
|
|
141
|
+
| { action: 'zoom-in' }
|
|
142
|
+
| { action: 'zoom-out' }
|
|
143
|
+
| { action: 'zoom-to-fit' }
|
|
144
|
+
| { action: 'zoom-to-full' }
|
|
145
|
+
| { action: 'revisions' }
|
|
146
|
+
| { action: 'search' }
|
|
147
|
+
| { action: 'language-change'; isPrimary?: boolean; languageCode?: string };
|
|
138
148
|
const EMPTY_FLOW_ISSUES: FlowIssue[] = [];
|
|
139
149
|
|
|
140
150
|
// How long the pending-changes auto-save countdown runs (in ms).
|
|
@@ -268,6 +278,13 @@ export class Editor extends RapidElement {
|
|
|
268
278
|
private currentDragIsCopy = false;
|
|
269
279
|
private dragStartPos = { x: 0, y: 0 };
|
|
270
280
|
|
|
281
|
+
// Mid-drag shift toggle: remember originals so we can switch between move/copy
|
|
282
|
+
private originalDragItem: DraggableItem | null = null;
|
|
283
|
+
private originalSelectedItems: Set<string> | null = null;
|
|
284
|
+
|
|
285
|
+
// Drag hint tooltip
|
|
286
|
+
private dragHintTimer: ReturnType<typeof setTimeout> | null = null;
|
|
287
|
+
|
|
271
288
|
// Public getter for drag state
|
|
272
289
|
public get dragging(): boolean {
|
|
273
290
|
return this.isDragging;
|
|
@@ -458,9 +475,6 @@ export class Editor extends RapidElement {
|
|
|
458
475
|
@property({ type: Boolean, reflect: true, attribute: 'message-view' })
|
|
459
476
|
showMessageTable = false;
|
|
460
477
|
|
|
461
|
-
@state()
|
|
462
|
-
private showLanguageOptions = false;
|
|
463
|
-
|
|
464
478
|
@state()
|
|
465
479
|
private isCreatingNewNode = false;
|
|
466
480
|
|
|
@@ -486,6 +500,19 @@ export class Editor extends RapidElement {
|
|
|
486
500
|
// Track previous target node to clear placeholder when moving between nodes
|
|
487
501
|
private previousActionDragTargetNodeUuid: string | null = null;
|
|
488
502
|
|
|
503
|
+
// Track action external drag state for shift-copy support
|
|
504
|
+
private isActionExternalDrag = false;
|
|
505
|
+
private actionDragIsCopy = false;
|
|
506
|
+
private actionDragLastDetail: {
|
|
507
|
+
action: Action;
|
|
508
|
+
nodeUuid: string;
|
|
509
|
+
actionIndex: number;
|
|
510
|
+
mouseX: number;
|
|
511
|
+
mouseY: number;
|
|
512
|
+
actionHeight: number;
|
|
513
|
+
isLastAction: boolean;
|
|
514
|
+
} | null = null;
|
|
515
|
+
|
|
489
516
|
// Connection placeholder state for dropping connections on empty canvas
|
|
490
517
|
@state()
|
|
491
518
|
private connectionPlaceholder: {
|
|
@@ -506,16 +533,8 @@ export class Editor extends RapidElement {
|
|
|
506
533
|
private getAvailableLanguages(): Array<{ code: string; name: string }> {
|
|
507
534
|
// Use languages from workspace if available
|
|
508
535
|
if (this.workspace?.languages && this.workspace.languages.length > 0) {
|
|
509
|
-
const languageNames = new Intl.DisplayNames(['en'], { type: 'language' });
|
|
510
536
|
return this.workspace.languages
|
|
511
|
-
.map((code) => {
|
|
512
|
-
try {
|
|
513
|
-
const name = languageNames.of(code);
|
|
514
|
-
return name ? { code, name } : { code, name: code };
|
|
515
|
-
} catch {
|
|
516
|
-
return { code, name: code };
|
|
517
|
-
}
|
|
518
|
-
})
|
|
537
|
+
.map((code) => ({ code, name: getLanguageDisplayName(code) }))
|
|
519
538
|
.filter((lang) => lang.code && lang.name);
|
|
520
539
|
}
|
|
521
540
|
|
|
@@ -539,6 +558,8 @@ export class Editor extends RapidElement {
|
|
|
539
558
|
private boundMouseUp = this.handleMouseUp.bind(this);
|
|
540
559
|
private boundGlobalMouseDown = this.handleGlobalMouseDown.bind(this);
|
|
541
560
|
private boundKeyDown = this.handleKeyDown.bind(this);
|
|
561
|
+
private boundKeyUp = this.handleKeyUp.bind(this);
|
|
562
|
+
private boundWindowBlur = this.handleWindowBlur.bind(this);
|
|
542
563
|
private boundCanvasContextMenu = this.handleCanvasContextMenu.bind(this);
|
|
543
564
|
private boundWheel = this.handleWheel.bind(this);
|
|
544
565
|
private boundTouchMove = this.handleTouchMove.bind(this);
|
|
@@ -557,198 +578,6 @@ export class Editor extends RapidElement {
|
|
|
557
578
|
min-height: 0;
|
|
558
579
|
}
|
|
559
580
|
|
|
560
|
-
.editor-toolbar {
|
|
561
|
-
--toolbar-control-height: 28px;
|
|
562
|
-
--toolbar-translation-control-height: 28px;
|
|
563
|
-
display: flex;
|
|
564
|
-
align-items: center;
|
|
565
|
-
padding: 6px 12px;
|
|
566
|
-
background: #fff;
|
|
567
|
-
border-bottom: 1px solid #e8e8e8;
|
|
568
|
-
flex-shrink: 0;
|
|
569
|
-
gap: 8px;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
.toolbar-left {
|
|
573
|
-
display: flex;
|
|
574
|
-
align-items: center;
|
|
575
|
-
gap: 2px;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
.toolbar-right {
|
|
579
|
-
display: flex;
|
|
580
|
-
align-items: center;
|
|
581
|
-
gap: 2px;
|
|
582
|
-
margin-left: auto;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
.toolbar-btn {
|
|
586
|
-
width: var(--toolbar-control-height);
|
|
587
|
-
height: var(--toolbar-control-height);
|
|
588
|
-
border: none;
|
|
589
|
-
background: transparent;
|
|
590
|
-
border-radius: var(--curvature);
|
|
591
|
-
cursor: pointer;
|
|
592
|
-
display: flex;
|
|
593
|
-
align-items: center;
|
|
594
|
-
justify-content: center;
|
|
595
|
-
padding: 0;
|
|
596
|
-
color: #888;
|
|
597
|
-
font-size: 16px;
|
|
598
|
-
line-height: 1;
|
|
599
|
-
outline: none;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
.toolbar-btn:focus {
|
|
603
|
-
outline: none;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
.toolbar-btn:focus-visible {
|
|
607
|
-
outline: 2px solid #0064c8;
|
|
608
|
-
outline-offset: 2px;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
.toolbar-btn:hover {
|
|
612
|
-
background: rgba(0, 0, 0, 0.06);
|
|
613
|
-
color: #555;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
.toolbar-btn:disabled {
|
|
617
|
-
opacity: 0.3;
|
|
618
|
-
cursor: default;
|
|
619
|
-
background: transparent;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
.toolbar-btn.active {
|
|
623
|
-
background: rgba(0, 100, 200, 0.1);
|
|
624
|
-
color: #0064c8;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
.toolbar-btn.active:hover {
|
|
628
|
-
background: rgba(0, 100, 200, 0.15);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
.toolbar-tip {
|
|
632
|
-
display: flex;
|
|
633
|
-
align-items: center;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
.toolbar-divider {
|
|
637
|
-
width: 1px;
|
|
638
|
-
height: 16px;
|
|
639
|
-
background: #e0e0e0;
|
|
640
|
-
margin: 0 4px;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
.toolbar-group {
|
|
644
|
-
display: flex;
|
|
645
|
-
align-items: center;
|
|
646
|
-
gap: 4px;
|
|
647
|
-
height: var(--toolbar-control-height);
|
|
648
|
-
box-sizing: border-box;
|
|
649
|
-
padding: 0 3px;
|
|
650
|
-
border: 1px solid #d7dce2;
|
|
651
|
-
border-radius: calc(var(--curvature) + 2px);
|
|
652
|
-
background: #f7f9fb;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
.toolbar-group-divider {
|
|
656
|
-
width: 1px;
|
|
657
|
-
height: 18px;
|
|
658
|
-
background: #d7dce2;
|
|
659
|
-
margin: 0 2px;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
.toolbar-language {
|
|
663
|
-
position: relative;
|
|
664
|
-
display: flex;
|
|
665
|
-
align-items: center;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
.toolbar-language-group {
|
|
669
|
-
display: flex;
|
|
670
|
-
align-items: center;
|
|
671
|
-
gap: 6px;
|
|
672
|
-
margin-left: 2px;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
.toolbar-zoom-group {
|
|
676
|
-
gap: 2px;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
.language-pill {
|
|
680
|
-
display: flex;
|
|
681
|
-
align-items: center;
|
|
682
|
-
gap: 6px;
|
|
683
|
-
background: #e9eef4;
|
|
684
|
-
color: #0064c8;
|
|
685
|
-
height: var(--toolbar-translation-control-height);
|
|
686
|
-
padding: 0 8px;
|
|
687
|
-
border-radius: var(--curvature);
|
|
688
|
-
box-sizing: border-box;
|
|
689
|
-
font-size: 13px;
|
|
690
|
-
font-weight: 400;
|
|
691
|
-
white-space: nowrap;
|
|
692
|
-
cursor: pointer;
|
|
693
|
-
--icon-color: #0064c8;
|
|
694
|
-
--icon-size: 16px;
|
|
695
|
-
border: none;
|
|
696
|
-
outline: none;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
.language-pill:hover {
|
|
700
|
-
filter: brightness(1.04);
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
.language-pill.primary {
|
|
704
|
-
background: #fff;
|
|
705
|
-
border: 1px solid #d7dce2;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
.language-pill.complete {
|
|
709
|
-
background: #d4f5e0;
|
|
710
|
-
color: #1a7f37;
|
|
711
|
-
--icon-color: #1a7f37;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
.language-pill-caret {
|
|
715
|
-
margin-left: 1px;
|
|
716
|
-
--icon-color: currentColor;
|
|
717
|
-
--icon-size: 12px;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
.language-percent {
|
|
721
|
-
display: inline-block;
|
|
722
|
-
font-size: 12px;
|
|
723
|
-
font-weight: 700;
|
|
724
|
-
line-height: 1;
|
|
725
|
-
color: #0064c8;
|
|
726
|
-
white-space: nowrap;
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
.language-pill.complete .language-percent {
|
|
730
|
-
color: #1a7f37;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
.toolbar-zoom-level {
|
|
734
|
-
font-size: 12px;
|
|
735
|
-
min-width: 40px;
|
|
736
|
-
text-align: center;
|
|
737
|
-
color: #555;
|
|
738
|
-
font-weight: 500;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
.toolbar-translation {
|
|
742
|
-
display: flex;
|
|
743
|
-
align-items: center;
|
|
744
|
-
gap: 4px;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
.toolbar-btn.language-tool {
|
|
748
|
-
width: var(--toolbar-translation-control-height);
|
|
749
|
-
height: var(--toolbar-translation-control-height);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
581
|
#editor {
|
|
753
582
|
overflow: scroll;
|
|
754
583
|
flex: 1;
|
|
@@ -816,6 +645,11 @@ export class Editor extends RapidElement {
|
|
|
816
645
|
transition: none !important;
|
|
817
646
|
}
|
|
818
647
|
|
|
648
|
+
#canvas.shift-held {
|
|
649
|
+
--shift-held-cursor: copy;
|
|
650
|
+
cursor: copy;
|
|
651
|
+
}
|
|
652
|
+
|
|
819
653
|
#canvas.viewing-revision {
|
|
820
654
|
pointer-events: none;
|
|
821
655
|
}
|
|
@@ -1050,6 +884,10 @@ export class Editor extends RapidElement {
|
|
|
1050
884
|
border-radius: var(--curvature);
|
|
1051
885
|
}
|
|
1052
886
|
|
|
887
|
+
.draggable.selected.drag-copy {
|
|
888
|
+
outline: none;
|
|
889
|
+
}
|
|
890
|
+
|
|
1053
891
|
/* Language banner replaced by toolbar language selector */
|
|
1054
892
|
|
|
1055
893
|
.localization-window-content {
|
|
@@ -1441,6 +1279,36 @@ export class Editor extends RapidElement {
|
|
|
1441
1279
|
opacity: 0.8;
|
|
1442
1280
|
}
|
|
1443
1281
|
|
|
1282
|
+
.drag-hint {
|
|
1283
|
+
position: absolute;
|
|
1284
|
+
bottom: 40px;
|
|
1285
|
+
left: 50%;
|
|
1286
|
+
transform: translateX(-50%);
|
|
1287
|
+
z-index: 100000;
|
|
1288
|
+
pointer-events: none;
|
|
1289
|
+
background: rgba(255, 255, 255, 0.5);
|
|
1290
|
+
backdrop-filter: blur(6px);
|
|
1291
|
+
color: #555;
|
|
1292
|
+
font-size: 18px;
|
|
1293
|
+
font-weight: 300;
|
|
1294
|
+
padding: 10px 32px;
|
|
1295
|
+
border-radius: 10px;
|
|
1296
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
1297
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
1298
|
+
white-space: nowrap;
|
|
1299
|
+
display: none;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
.drag-hint.visible {
|
|
1303
|
+
display: block;
|
|
1304
|
+
animation: drag-hint-in 0.15s ease forwards;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
@keyframes drag-hint-in {
|
|
1308
|
+
from { opacity: 0; }
|
|
1309
|
+
to { opacity: 1; }
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1444
1312
|
.reflow-card {
|
|
1445
1313
|
position: absolute;
|
|
1446
1314
|
top: 16px;
|
|
@@ -1562,6 +1430,11 @@ export class Editor extends RapidElement {
|
|
|
1562
1430
|
}
|
|
1563
1431
|
|
|
1564
1432
|
private makeConnection(info) {
|
|
1433
|
+
this.stopAutoScroll();
|
|
1434
|
+
this.autoScrollDeltaX = 0;
|
|
1435
|
+
this.autoScrollDeltaY = 0;
|
|
1436
|
+
this.lastPointerPos = null;
|
|
1437
|
+
|
|
1565
1438
|
if (this.sourceId && this.targetId && this.isValidTarget) {
|
|
1566
1439
|
// going to the same target, just put it back
|
|
1567
1440
|
if (info.target.id === this.targetId) {
|
|
@@ -2064,6 +1937,8 @@ export class Editor extends RapidElement {
|
|
|
2064
1937
|
document.removeEventListener('mouseup', this.boundMouseUp);
|
|
2065
1938
|
document.removeEventListener('mousedown', this.boundGlobalMouseDown);
|
|
2066
1939
|
document.removeEventListener('keydown', this.boundKeyDown);
|
|
1940
|
+
document.removeEventListener('keyup', this.boundKeyUp);
|
|
1941
|
+
window.removeEventListener('blur', this.boundWindowBlur);
|
|
2067
1942
|
document.removeEventListener('touchmove', this.boundTouchMove);
|
|
2068
1943
|
document.removeEventListener('touchend', this.boundTouchEnd);
|
|
2069
1944
|
document.removeEventListener('touchcancel', this.boundTouchCancel);
|
|
@@ -2096,6 +1971,8 @@ export class Editor extends RapidElement {
|
|
|
2096
1971
|
document.addEventListener('mouseup', this.boundMouseUp);
|
|
2097
1972
|
document.addEventListener('mousedown', this.boundGlobalMouseDown);
|
|
2098
1973
|
document.addEventListener('keydown', this.boundKeyDown);
|
|
1974
|
+
document.addEventListener('keyup', this.boundKeyUp);
|
|
1975
|
+
window.addEventListener('blur', this.boundWindowBlur);
|
|
2099
1976
|
document.addEventListener('touchmove', this.boundTouchMove, {
|
|
2100
1977
|
passive: false
|
|
2101
1978
|
});
|
|
@@ -2429,7 +2306,56 @@ export class Editor extends RapidElement {
|
|
|
2429
2306
|
}
|
|
2430
2307
|
}
|
|
2431
2308
|
|
|
2309
|
+
private showDragHint(): void {
|
|
2310
|
+
if (this.isReadOnly()) return;
|
|
2311
|
+
const hint = this.querySelector('#drag-hint') as HTMLElement;
|
|
2312
|
+
if (!hint) return;
|
|
2313
|
+
this.dragHintTimer = setTimeout(() => {
|
|
2314
|
+
hint.classList.add('visible');
|
|
2315
|
+
this.dragHintTimer = null;
|
|
2316
|
+
}, 600);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
private hideDragHint(): void {
|
|
2320
|
+
if (this.dragHintTimer) {
|
|
2321
|
+
clearTimeout(this.dragHintTimer);
|
|
2322
|
+
this.dragHintTimer = null;
|
|
2323
|
+
}
|
|
2324
|
+
const hint = this.querySelector('#drag-hint') as HTMLElement;
|
|
2325
|
+
if (hint) {
|
|
2326
|
+
hint.classList.remove('visible');
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2432
2330
|
private handleKeyDown(event: KeyboardEvent): void {
|
|
2331
|
+
if (event.key === 'Shift') {
|
|
2332
|
+
this.querySelector('#canvas')?.classList.add('shift-held');
|
|
2333
|
+
|
|
2334
|
+
// Toggle to copy mode mid-drag (nodes)
|
|
2335
|
+
if (this.isDragging && !this.currentDragIsCopy) {
|
|
2336
|
+
this.hideDragHint();
|
|
2337
|
+
this.performShiftDragCopy();
|
|
2338
|
+
// Clone elements aren't in the DOM until Lit re-renders;
|
|
2339
|
+
// schedule position update after the next frame.
|
|
2340
|
+
requestAnimationFrame(() => {
|
|
2341
|
+
this.markCopyElements();
|
|
2342
|
+
this.updateDragPositions();
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// Toggle to copy mode mid-drag (actions)
|
|
2347
|
+
if (this.isActionExternalDrag && !this.actionDragIsCopy) {
|
|
2348
|
+
this.actionDragIsCopy = true;
|
|
2349
|
+
this.hideDragHint();
|
|
2350
|
+
this.showActionOriginal(true);
|
|
2351
|
+
// If this is a last-action drag, now show the canvas preview
|
|
2352
|
+
if (this.actionDragLastDetail?.isLastAction) {
|
|
2353
|
+
this.reprocessActionDrag();
|
|
2354
|
+
}
|
|
2355
|
+
this.requestUpdate();
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2433
2359
|
// Cmd/Ctrl+F opens flow search (unless a dialog is already open)
|
|
2434
2360
|
if ((event.metaKey || event.ctrlKey) && event.key === 'f') {
|
|
2435
2361
|
event.preventDefault();
|
|
@@ -2459,6 +2385,50 @@ export class Editor extends RapidElement {
|
|
|
2459
2385
|
}
|
|
2460
2386
|
}
|
|
2461
2387
|
|
|
2388
|
+
private handleKeyUp(event: KeyboardEvent): void {
|
|
2389
|
+
if (event.key === 'Shift') {
|
|
2390
|
+
this.querySelector('#canvas')?.classList.remove('shift-held');
|
|
2391
|
+
|
|
2392
|
+
// Toggle back to move mode mid-drag (nodes)
|
|
2393
|
+
if (this.isDragging && this.currentDragIsCopy) {
|
|
2394
|
+
this.revertShiftDragCopy();
|
|
2395
|
+
requestAnimationFrame(() => this.updateDragPositions());
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
// Toggle back to move mode mid-drag (actions)
|
|
2399
|
+
if (this.isActionExternalDrag && this.actionDragIsCopy) {
|
|
2400
|
+
this.actionDragIsCopy = false;
|
|
2401
|
+
this.showDragHint();
|
|
2402
|
+
this.showActionOriginal(false);
|
|
2403
|
+
// If this is a last-action drag, hide the canvas preview again
|
|
2404
|
+
if (this.actionDragLastDetail?.isLastAction) {
|
|
2405
|
+
this.reprocessActionDrag();
|
|
2406
|
+
}
|
|
2407
|
+
this.requestUpdate();
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
private handleWindowBlur(): void {
|
|
2413
|
+
this.querySelector('#canvas')?.classList.remove('shift-held');
|
|
2414
|
+
|
|
2415
|
+
// Revert copy mode if blur happens mid-drag (keyup may never fire)
|
|
2416
|
+
if (this.isDragging && this.currentDragIsCopy) {
|
|
2417
|
+
this.revertShiftDragCopy();
|
|
2418
|
+
requestAnimationFrame(() => this.updateDragPositions());
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
// Revert action copy mode on blur
|
|
2422
|
+
if (this.isActionExternalDrag && this.actionDragIsCopy) {
|
|
2423
|
+
this.actionDragIsCopy = false;
|
|
2424
|
+
this.showActionOriginal(false);
|
|
2425
|
+
if (this.actionDragLastDetail?.isLastAction) {
|
|
2426
|
+
this.reprocessActionDrag();
|
|
2427
|
+
}
|
|
2428
|
+
this.requestUpdate();
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2462
2432
|
// --- Flow settings cookie (LRU, max 50 flows) ---
|
|
2463
2433
|
|
|
2464
2434
|
static MAX_FLOW_SETTINGS = 50;
|
|
@@ -3597,6 +3567,9 @@ export class Editor extends RapidElement {
|
|
|
3597
3567
|
}
|
|
3598
3568
|
|
|
3599
3569
|
if (this.plumber.connectionDragging) {
|
|
3570
|
+
this.lastPointerPos = { clientX: event.clientX, clientY: event.clientY };
|
|
3571
|
+
this.startAutoScroll();
|
|
3572
|
+
|
|
3600
3573
|
const targetNode = document.querySelector('temba-flow-node:hover');
|
|
3601
3574
|
|
|
3602
3575
|
// Clear previous target styles
|
|
@@ -3683,9 +3656,15 @@ export class Editor extends RapidElement {
|
|
|
3683
3656
|
this.isDragging = true;
|
|
3684
3657
|
this.startAutoScroll();
|
|
3685
3658
|
|
|
3686
|
-
|
|
3659
|
+
// Snapshot the original drag context before any copy occurs
|
|
3660
|
+
this.originalDragItem = { ...this.currentDragItem };
|
|
3661
|
+
this.originalSelectedItems = new Set(this.selectedItems);
|
|
3662
|
+
|
|
3663
|
+
if (this.shiftDragCopy || event.shiftKey) {
|
|
3687
3664
|
this.performShiftDragCopy();
|
|
3688
3665
|
this.shiftDragCopy = false;
|
|
3666
|
+
} else {
|
|
3667
|
+
this.showDragHint();
|
|
3689
3668
|
}
|
|
3690
3669
|
}
|
|
3691
3670
|
|
|
@@ -3696,14 +3675,14 @@ export class Editor extends RapidElement {
|
|
|
3696
3675
|
}
|
|
3697
3676
|
|
|
3698
3677
|
private performShiftDragCopy(): void {
|
|
3699
|
-
if (!this.
|
|
3678
|
+
if (!this.originalDragItem) return;
|
|
3700
3679
|
|
|
3701
|
-
//
|
|
3680
|
+
// Always use the original items as the source for copying
|
|
3702
3681
|
const itemsToCopy =
|
|
3703
|
-
this.
|
|
3704
|
-
this.
|
|
3705
|
-
? Array.from(this.
|
|
3706
|
-
: [this.
|
|
3682
|
+
this.originalSelectedItems?.has(this.originalDragItem.uuid) &&
|
|
3683
|
+
(this.originalSelectedItems?.size ?? 0) > 1
|
|
3684
|
+
? Array.from(this.originalSelectedItems!)
|
|
3685
|
+
: [this.originalDragItem.uuid];
|
|
3707
3686
|
|
|
3708
3687
|
if (itemsToCopy.length === 0) return;
|
|
3709
3688
|
|
|
@@ -3719,19 +3698,44 @@ export class Editor extends RapidElement {
|
|
|
3719
3698
|
}
|
|
3720
3699
|
this.currentDragIsCopy = true;
|
|
3721
3700
|
|
|
3701
|
+
// Snap original items back to their start positions.
|
|
3702
|
+
// Set position while 'dragging' class is still applied so
|
|
3703
|
+
// transitions are disabled and the move is instant.
|
|
3704
|
+
for (const uuid of itemsToCopy) {
|
|
3705
|
+
const element = this.querySelector(`[uuid="${uuid}"]`) as HTMLElement;
|
|
3706
|
+
const type =
|
|
3707
|
+
element?.tagName === 'TEMBA-FLOW-NODE' ? 'node' : 'sticky';
|
|
3708
|
+
const position = this.getPosition(uuid, type);
|
|
3709
|
+
if (element && position) {
|
|
3710
|
+
element.style.left = `${position.left}px`;
|
|
3711
|
+
element.style.top = `${position.top}px`;
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
this.plumber.revalidate(itemsToCopy);
|
|
3715
|
+
// Force layout so the position is committed with transitions
|
|
3716
|
+
// disabled, then remove the dragging class.
|
|
3717
|
+
for (const uuid of itemsToCopy) {
|
|
3718
|
+
const element = this.querySelector(`[uuid="${uuid}"]`) as HTMLElement;
|
|
3719
|
+
if (element) {
|
|
3720
|
+
// Reading offsetHeight forces a synchronous layout
|
|
3721
|
+
void element.offsetHeight;
|
|
3722
|
+
element.classList.remove('dragging');
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3722
3726
|
// Update drag item to reference the copy
|
|
3723
|
-
const newDragUuid = uuidMapping[this.
|
|
3727
|
+
const newDragUuid = uuidMapping[this.originalDragItem.uuid];
|
|
3724
3728
|
if (newDragUuid) {
|
|
3725
3729
|
this.currentDragItem = {
|
|
3726
|
-
...this.
|
|
3730
|
+
...this.originalDragItem,
|
|
3727
3731
|
uuid: newDragUuid
|
|
3728
3732
|
};
|
|
3729
3733
|
}
|
|
3730
3734
|
|
|
3731
3735
|
// Update selected items to reference copies
|
|
3732
|
-
if (this.
|
|
3736
|
+
if ((this.originalSelectedItems?.size ?? 0) > 1) {
|
|
3733
3737
|
const newSelectedItems = new Set<string>();
|
|
3734
|
-
for (const uuid of this.
|
|
3738
|
+
for (const uuid of this.originalSelectedItems!) {
|
|
3735
3739
|
const newUuid = uuidMapping[uuid];
|
|
3736
3740
|
newSelectedItems.add(newUuid || uuid);
|
|
3737
3741
|
}
|
|
@@ -3739,6 +3743,48 @@ export class Editor extends RapidElement {
|
|
|
3739
3743
|
}
|
|
3740
3744
|
}
|
|
3741
3745
|
|
|
3746
|
+
private markCopyElements(): void {
|
|
3747
|
+
for (const uuid of this.copiedItemUuids) {
|
|
3748
|
+
const el = this.querySelector(`[uuid="${uuid}"]`) as HTMLElement;
|
|
3749
|
+
el?.classList.add('drag-copy');
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
private revertShiftDragCopy(): void {
|
|
3754
|
+
if (!this.originalDragItem) return;
|
|
3755
|
+
|
|
3756
|
+
// Remove the cloned items
|
|
3757
|
+
if (this.copiedItemUuids.length > 0) {
|
|
3758
|
+
const nodeUuids = this.copiedItemUuids.filter((uuid) =>
|
|
3759
|
+
this.definition.nodes.some((n) => n.uuid === uuid)
|
|
3760
|
+
);
|
|
3761
|
+
const stickyUuids = this.copiedItemUuids.filter(
|
|
3762
|
+
(uuid) => this.definition._ui?.stickies?.[uuid]
|
|
3763
|
+
);
|
|
3764
|
+
|
|
3765
|
+
if (nodeUuids.length > 0) {
|
|
3766
|
+
getStore().getState().removeNodes(nodeUuids);
|
|
3767
|
+
}
|
|
3768
|
+
if (stickyUuids.length > 0) {
|
|
3769
|
+
getStore().getState().removeStickyNotes(stickyUuids);
|
|
3770
|
+
}
|
|
3771
|
+
this.copiedItemUuids = [];
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
this.currentDragIsCopy = false;
|
|
3775
|
+
|
|
3776
|
+
// The remove calls above set dirtyDate, but we're just reverting a
|
|
3777
|
+
// mid-drag copy — there's nothing to save yet. Clear it so no
|
|
3778
|
+
// revision is created while the drag is still in progress.
|
|
3779
|
+
getStore().getState().setDirtyDate(null);
|
|
3780
|
+
|
|
3781
|
+
// Restore drag context to originals
|
|
3782
|
+
this.currentDragItem = { ...this.originalDragItem };
|
|
3783
|
+
if (this.originalSelectedItems) {
|
|
3784
|
+
this.selectedItems = new Set(this.originalSelectedItems);
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3742
3788
|
private updateDragPositions(): void {
|
|
3743
3789
|
if (!this.currentDragItem || !this.lastPointerPos) return;
|
|
3744
3790
|
|
|
@@ -3770,11 +3816,15 @@ export class Editor extends RapidElement {
|
|
|
3770
3816
|
element.style.left = `${position.left + deltaX}px`;
|
|
3771
3817
|
element.style.top = `${position.top + deltaY}px`;
|
|
3772
3818
|
element.classList.add('dragging');
|
|
3819
|
+
if (this.currentDragIsCopy) {
|
|
3820
|
+
element.classList.add('drag-copy');
|
|
3821
|
+
}
|
|
3773
3822
|
}
|
|
3774
3823
|
}
|
|
3775
3824
|
});
|
|
3776
3825
|
|
|
3777
3826
|
this.plumber.revalidate(itemsToMove);
|
|
3827
|
+
|
|
3778
3828
|
}
|
|
3779
3829
|
|
|
3780
3830
|
private startAutoScroll(): void {
|
|
@@ -3784,7 +3834,10 @@ export class Editor extends RapidElement {
|
|
|
3784
3834
|
if (!editor) return;
|
|
3785
3835
|
|
|
3786
3836
|
const tick = () => {
|
|
3787
|
-
if (
|
|
3837
|
+
if (
|
|
3838
|
+
(!this.isDragging && !this.plumber?.connectionDragging) ||
|
|
3839
|
+
!this.lastPointerPos
|
|
3840
|
+
) {
|
|
3788
3841
|
this.autoScrollAnimationId = null;
|
|
3789
3842
|
return;
|
|
3790
3843
|
}
|
|
@@ -3796,32 +3849,56 @@ export class Editor extends RapidElement {
|
|
|
3796
3849
|
let scrollDx = 0;
|
|
3797
3850
|
let scrollDy = 0;
|
|
3798
3851
|
|
|
3799
|
-
// Left edge
|
|
3852
|
+
// Left edge (including beyond)
|
|
3800
3853
|
const distFromLeft = mouseX - editorRect.left;
|
|
3801
|
-
if (distFromLeft
|
|
3802
|
-
const
|
|
3803
|
-
|
|
3854
|
+
if (distFromLeft < AUTO_SCROLL_EDGE_ZONE) {
|
|
3855
|
+
const beyond = distFromLeft < 0;
|
|
3856
|
+
const ratio = Math.min(
|
|
3857
|
+
1,
|
|
3858
|
+
1 - distFromLeft / AUTO_SCROLL_EDGE_ZONE
|
|
3859
|
+
);
|
|
3860
|
+
const speed =
|
|
3861
|
+
AUTO_SCROLL_MAX_SPEED * (beyond ? AUTO_SCROLL_BEYOND_MULTIPLIER : 1);
|
|
3862
|
+
scrollDx = -(ratio * speed);
|
|
3804
3863
|
}
|
|
3805
3864
|
|
|
3806
|
-
// Right edge
|
|
3865
|
+
// Right edge (including beyond)
|
|
3807
3866
|
const distFromRight = editorRect.right - mouseX;
|
|
3808
|
-
if (distFromRight
|
|
3809
|
-
const
|
|
3810
|
-
|
|
3867
|
+
if (distFromRight < AUTO_SCROLL_EDGE_ZONE) {
|
|
3868
|
+
const beyond = distFromRight < 0;
|
|
3869
|
+
const ratio = Math.min(
|
|
3870
|
+
1,
|
|
3871
|
+
1 - distFromRight / AUTO_SCROLL_EDGE_ZONE
|
|
3872
|
+
);
|
|
3873
|
+
const speed =
|
|
3874
|
+
AUTO_SCROLL_MAX_SPEED * (beyond ? AUTO_SCROLL_BEYOND_MULTIPLIER : 1);
|
|
3875
|
+
scrollDx = ratio * speed;
|
|
3811
3876
|
}
|
|
3812
3877
|
|
|
3813
|
-
// Top edge
|
|
3878
|
+
// Top edge (including beyond)
|
|
3814
3879
|
const distFromTop = mouseY - editorRect.top;
|
|
3815
|
-
if (distFromTop
|
|
3816
|
-
const
|
|
3817
|
-
|
|
3880
|
+
if (distFromTop < AUTO_SCROLL_EDGE_ZONE) {
|
|
3881
|
+
const beyond = distFromTop < 0;
|
|
3882
|
+
const ratio = Math.min(
|
|
3883
|
+
1,
|
|
3884
|
+
1 - distFromTop / AUTO_SCROLL_EDGE_ZONE
|
|
3885
|
+
);
|
|
3886
|
+
const speed =
|
|
3887
|
+
AUTO_SCROLL_MAX_SPEED * (beyond ? AUTO_SCROLL_BEYOND_MULTIPLIER : 1);
|
|
3888
|
+
scrollDy = -(ratio * speed);
|
|
3818
3889
|
}
|
|
3819
3890
|
|
|
3820
|
-
// Bottom edge
|
|
3891
|
+
// Bottom edge (including beyond)
|
|
3821
3892
|
const distFromBottom = editorRect.bottom - mouseY;
|
|
3822
|
-
if (distFromBottom
|
|
3823
|
-
const
|
|
3824
|
-
|
|
3893
|
+
if (distFromBottom < AUTO_SCROLL_EDGE_ZONE) {
|
|
3894
|
+
const beyond = distFromBottom < 0;
|
|
3895
|
+
const ratio = Math.min(
|
|
3896
|
+
1,
|
|
3897
|
+
1 - distFromBottom / AUTO_SCROLL_EDGE_ZONE
|
|
3898
|
+
);
|
|
3899
|
+
const speed =
|
|
3900
|
+
AUTO_SCROLL_MAX_SPEED * (beyond ? AUTO_SCROLL_BEYOND_MULTIPLIER : 1);
|
|
3901
|
+
scrollDy = ratio * speed;
|
|
3825
3902
|
}
|
|
3826
3903
|
|
|
3827
3904
|
if (scrollDx !== 0 || scrollDy !== 0) {
|
|
@@ -3835,7 +3912,7 @@ export class Editor extends RapidElement {
|
|
|
3835
3912
|
(editor.scrollLeft + editor.clientWidth + scrollDx) / this.zoom;
|
|
3836
3913
|
const neededHeight =
|
|
3837
3914
|
(editor.scrollTop + editor.clientHeight + scrollDy) / this.zoom;
|
|
3838
|
-
getStore()
|
|
3915
|
+
getStore()?.getState()?.expandCanvas(neededWidth, neededHeight);
|
|
3839
3916
|
}
|
|
3840
3917
|
|
|
3841
3918
|
editor.scrollLeft += scrollDx;
|
|
@@ -3923,10 +4000,10 @@ export class Editor extends RapidElement {
|
|
|
3923
4000
|
const newPosition = { left: snappedLeft, top: snappedTop };
|
|
3924
4001
|
newPositions[uuid] = newPosition;
|
|
3925
4002
|
|
|
3926
|
-
// Remove dragging
|
|
4003
|
+
// Remove dragging/copy classes
|
|
3927
4004
|
const element = this.querySelector(`[uuid="${uuid}"]`) as HTMLElement;
|
|
3928
4005
|
if (element) {
|
|
3929
|
-
element.classList.remove('dragging');
|
|
4006
|
+
element.classList.remove('dragging', 'drag-copy');
|
|
3930
4007
|
element.style.left = `${snappedLeft}px`;
|
|
3931
4008
|
element.style.top = `${snappedTop}px`;
|
|
3932
4009
|
}
|
|
@@ -3960,11 +4037,14 @@ export class Editor extends RapidElement {
|
|
|
3960
4037
|
}
|
|
3961
4038
|
|
|
3962
4039
|
// Reset all drag state
|
|
4040
|
+
this.hideDragHint();
|
|
3963
4041
|
this.isDragging = false;
|
|
3964
4042
|
this.isMouseDown = false;
|
|
3965
4043
|
this.shiftDragCopy = false;
|
|
3966
4044
|
this.currentDragIsCopy = false;
|
|
3967
4045
|
this.currentDragItem = null;
|
|
4046
|
+
this.originalDragItem = null;
|
|
4047
|
+
this.originalSelectedItems = null;
|
|
3968
4048
|
this.canvasMouseDown = false;
|
|
3969
4049
|
this.autoScrollDeltaX = 0;
|
|
3970
4050
|
this.autoScrollDeltaY = 0;
|
|
@@ -4950,6 +5030,32 @@ export class Editor extends RapidElement {
|
|
|
4950
5030
|
isLastAction = false
|
|
4951
5031
|
} = event.detail;
|
|
4952
5032
|
|
|
5033
|
+
// Track action external drag state for shift-copy support
|
|
5034
|
+
const isFirstExternalEvent = !this.isActionExternalDrag;
|
|
5035
|
+
this.isActionExternalDrag = true;
|
|
5036
|
+
this.actionDragLastDetail = {
|
|
5037
|
+
action,
|
|
5038
|
+
nodeUuid,
|
|
5039
|
+
actionIndex,
|
|
5040
|
+
mouseX,
|
|
5041
|
+
mouseY,
|
|
5042
|
+
actionHeight,
|
|
5043
|
+
isLastAction
|
|
5044
|
+
};
|
|
5045
|
+
|
|
5046
|
+
// Initialize copy mode from current shift state (handles shift held before drag)
|
|
5047
|
+
if (isFirstExternalEvent) {
|
|
5048
|
+
const shiftHeld =
|
|
5049
|
+
this.querySelector('#canvas')?.classList.contains('shift-held') ??
|
|
5050
|
+
false;
|
|
5051
|
+
if (shiftHeld) {
|
|
5052
|
+
this.actionDragIsCopy = true;
|
|
5053
|
+
this.showActionOriginal(true);
|
|
5054
|
+
} else {
|
|
5055
|
+
this.showDragHint();
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
|
|
4953
5059
|
// Check if mouse is over another execute_actions node
|
|
4954
5060
|
const targetNode = this.getNodeAtPosition(mouseX, mouseY);
|
|
4955
5061
|
|
|
@@ -5046,9 +5152,9 @@ export class Editor extends RapidElement {
|
|
|
5046
5152
|
`temba-flow-node[data-node-uuid="${nodeUuid}"]`
|
|
5047
5153
|
);
|
|
5048
5154
|
|
|
5049
|
-
// Show canvas drop preview
|
|
5050
|
-
//
|
|
5051
|
-
if (!isLastAction) {
|
|
5155
|
+
// Show canvas drop preview if this is not the last action,
|
|
5156
|
+
// or if shift-copy is active (copying allows canvas drop for last action too)
|
|
5157
|
+
if (!isLastAction || this.actionDragIsCopy) {
|
|
5052
5158
|
// Hide ghost when showing canvas preview (for canvas drops)
|
|
5053
5159
|
if (sourceElement) {
|
|
5054
5160
|
sourceElement.dispatchEvent(
|
|
@@ -5070,7 +5176,7 @@ export class Editor extends RapidElement {
|
|
|
5070
5176
|
actionHeight
|
|
5071
5177
|
};
|
|
5072
5178
|
} else {
|
|
5073
|
-
// For last action, keep ghost visible (can't drop on canvas)
|
|
5179
|
+
// For last action without copy, keep ghost visible (can't drop on canvas)
|
|
5074
5180
|
if (sourceElement) {
|
|
5075
5181
|
sourceElement.dispatchEvent(
|
|
5076
5182
|
new CustomEvent('action-show-ghost', {
|
|
@@ -5107,6 +5213,75 @@ export class Editor extends RapidElement {
|
|
|
5107
5213
|
|
|
5108
5214
|
this.canvasDropPreview = null;
|
|
5109
5215
|
this.actionDragTargetNodeUuid = null;
|
|
5216
|
+
this.isActionExternalDrag = false;
|
|
5217
|
+
this.actionDragIsCopy = false;
|
|
5218
|
+
this.hideDragHint();
|
|
5219
|
+
this.actionDragLastDetail = null;
|
|
5220
|
+
}
|
|
5221
|
+
|
|
5222
|
+
/** Show or hide the original action element in the source node. */
|
|
5223
|
+
private showActionOriginal(visible: boolean): void {
|
|
5224
|
+
if (!this.actionDragLastDetail) return;
|
|
5225
|
+
const sourceElement = this.querySelector(
|
|
5226
|
+
`temba-flow-node[data-node-uuid="${this.actionDragLastDetail.nodeUuid}"]`
|
|
5227
|
+
);
|
|
5228
|
+
if (sourceElement) {
|
|
5229
|
+
sourceElement.dispatchEvent(
|
|
5230
|
+
new CustomEvent(
|
|
5231
|
+
visible ? 'action-show-original' : 'action-hide-original',
|
|
5232
|
+
{ detail: {}, bubbles: false }
|
|
5233
|
+
)
|
|
5234
|
+
);
|
|
5235
|
+
}
|
|
5236
|
+
}
|
|
5237
|
+
|
|
5238
|
+
/**
|
|
5239
|
+
* Reprocess the last action drag event with the current shift-copy state.
|
|
5240
|
+
* Used when shift is toggled mid-drag to show/hide the canvas preview
|
|
5241
|
+
* for last-action drags.
|
|
5242
|
+
*/
|
|
5243
|
+
private reprocessActionDrag(): void {
|
|
5244
|
+
if (!this.actionDragLastDetail) return;
|
|
5245
|
+
const { action, nodeUuid, actionIndex, mouseX, mouseY, actionHeight } =
|
|
5246
|
+
this.actionDragLastDetail;
|
|
5247
|
+
|
|
5248
|
+
const sourceElement = this.querySelector(
|
|
5249
|
+
`temba-flow-node[data-node-uuid="${nodeUuid}"]`
|
|
5250
|
+
);
|
|
5251
|
+
|
|
5252
|
+
// Only relevant when not hovering a target node
|
|
5253
|
+
if (this.actionDragTargetNodeUuid) return;
|
|
5254
|
+
|
|
5255
|
+
if (this.actionDragIsCopy) {
|
|
5256
|
+
// Shift pressed: show canvas preview
|
|
5257
|
+
if (sourceElement) {
|
|
5258
|
+
sourceElement.dispatchEvent(
|
|
5259
|
+
new CustomEvent('action-hide-ghost', {
|
|
5260
|
+
detail: {},
|
|
5261
|
+
bubbles: false
|
|
5262
|
+
})
|
|
5263
|
+
);
|
|
5264
|
+
}
|
|
5265
|
+
const position = this.calculateCanvasDropPosition(mouseX, mouseY, false);
|
|
5266
|
+
this.canvasDropPreview = {
|
|
5267
|
+
action,
|
|
5268
|
+
nodeUuid,
|
|
5269
|
+
actionIndex,
|
|
5270
|
+
position,
|
|
5271
|
+
actionHeight
|
|
5272
|
+
};
|
|
5273
|
+
} else {
|
|
5274
|
+
// Shift released: hide canvas preview, show ghost
|
|
5275
|
+
if (sourceElement) {
|
|
5276
|
+
sourceElement.dispatchEvent(
|
|
5277
|
+
new CustomEvent('action-show-ghost', {
|
|
5278
|
+
detail: {},
|
|
5279
|
+
bubbles: false
|
|
5280
|
+
})
|
|
5281
|
+
);
|
|
5282
|
+
}
|
|
5283
|
+
this.canvasDropPreview = null;
|
|
5284
|
+
}
|
|
5110
5285
|
}
|
|
5111
5286
|
|
|
5112
5287
|
private handleActionDropExternal(event: CustomEvent): void {
|
|
@@ -5119,6 +5294,15 @@ export class Editor extends RapidElement {
|
|
|
5119
5294
|
isLastAction = false
|
|
5120
5295
|
} = event.detail;
|
|
5121
5296
|
|
|
5297
|
+
const isCopy = this.actionDragIsCopy;
|
|
5298
|
+
|
|
5299
|
+
// Reset action drag state
|
|
5300
|
+
this.isActionExternalDrag = false;
|
|
5301
|
+
this.actionDragIsCopy = false;
|
|
5302
|
+
this.actionDragLastDetail = null;
|
|
5303
|
+
this.previousActionDragTargetNodeUuid = null;
|
|
5304
|
+
this.hideDragHint();
|
|
5305
|
+
|
|
5122
5306
|
// Check if we're dropping on an existing execute_actions node
|
|
5123
5307
|
const targetNodeUuid = this.actionDragTargetNodeUuid;
|
|
5124
5308
|
|
|
@@ -5135,7 +5319,8 @@ export class Editor extends RapidElement {
|
|
|
5135
5319
|
sourceNodeUuid: nodeUuid,
|
|
5136
5320
|
actionIndex,
|
|
5137
5321
|
mouseX,
|
|
5138
|
-
mouseY
|
|
5322
|
+
mouseY,
|
|
5323
|
+
isCopy
|
|
5139
5324
|
},
|
|
5140
5325
|
bubbles: false
|
|
5141
5326
|
})
|
|
@@ -5148,9 +5333,9 @@ export class Editor extends RapidElement {
|
|
|
5148
5333
|
return;
|
|
5149
5334
|
}
|
|
5150
5335
|
|
|
5151
|
-
// If this is the last action and
|
|
5336
|
+
// If this is the last action and not copying, do nothing
|
|
5152
5337
|
// Last actions can only be moved to other nodes, not dropped on canvas
|
|
5153
|
-
if (isLastAction) {
|
|
5338
|
+
if (isLastAction && !isCopy) {
|
|
5154
5339
|
this.canvasDropPreview = null;
|
|
5155
5340
|
this.actionDragTargetNodeUuid = null;
|
|
5156
5341
|
return;
|
|
@@ -5160,28 +5345,36 @@ export class Editor extends RapidElement {
|
|
|
5160
5345
|
// Snap to grid for the final drop position
|
|
5161
5346
|
const position = this.calculateCanvasDropPosition(mouseX, mouseY, true);
|
|
5162
5347
|
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5348
|
+
if (!isCopy) {
|
|
5349
|
+
// remove the action from the original node
|
|
5350
|
+
const originalNode = this.definition.nodes.find(
|
|
5351
|
+
(n) => n.uuid === nodeUuid
|
|
5352
|
+
);
|
|
5353
|
+
if (!originalNode) return;
|
|
5166
5354
|
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5355
|
+
const updatedActions = originalNode.actions.filter(
|
|
5356
|
+
(_a, idx) => idx !== actionIndex
|
|
5357
|
+
);
|
|
5170
5358
|
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5359
|
+
// if no actions remain, delete the node
|
|
5360
|
+
if (updatedActions.length === 0) {
|
|
5361
|
+
// Use deleteNodes to properly clean up Plumber connections before removing
|
|
5362
|
+
this.deleteNodes([nodeUuid]);
|
|
5363
|
+
} else {
|
|
5364
|
+
// update the node
|
|
5365
|
+
const updatedNode = { ...originalNode, actions: updatedActions };
|
|
5366
|
+
getStore()?.getState().updateNode(nodeUuid, updatedNode);
|
|
5367
|
+
}
|
|
5179
5368
|
}
|
|
5180
5369
|
|
|
5181
5370
|
// create a new execute_actions node with the dropped action
|
|
5371
|
+
// When copying, generate a fresh UUID so the clone doesn't share the original's
|
|
5372
|
+
const droppedAction = isCopy
|
|
5373
|
+
? { ...action, uuid: generateUUID() }
|
|
5374
|
+
: action;
|
|
5182
5375
|
const newNode: Node = {
|
|
5183
5376
|
uuid: generateUUID(),
|
|
5184
|
-
actions: [
|
|
5377
|
+
actions: [droppedAction],
|
|
5185
5378
|
exits: [
|
|
5186
5379
|
{
|
|
5187
5380
|
uuid: generateUUID(),
|
|
@@ -5199,6 +5392,11 @@ export class Editor extends RapidElement {
|
|
|
5199
5392
|
// add the new node
|
|
5200
5393
|
getStore()?.getState().addNode(newNode, newNodeUI);
|
|
5201
5394
|
|
|
5395
|
+
// Copy localizations from the original action to the new one
|
|
5396
|
+
if (isCopy) {
|
|
5397
|
+
this.copyActionLocalizations(action.uuid, droppedAction.uuid);
|
|
5398
|
+
}
|
|
5399
|
+
|
|
5202
5400
|
// clear the preview
|
|
5203
5401
|
this.canvasDropPreview = null;
|
|
5204
5402
|
this.actionDragTargetNodeUuid = null;
|
|
@@ -5209,6 +5407,27 @@ export class Editor extends RapidElement {
|
|
|
5209
5407
|
});
|
|
5210
5408
|
}
|
|
5211
5409
|
|
|
5410
|
+
/** Copy all localization entries from one action UUID to another. */
|
|
5411
|
+
private copyActionLocalizations(
|
|
5412
|
+
sourceUuid: string,
|
|
5413
|
+
targetUuid: string
|
|
5414
|
+
): void {
|
|
5415
|
+
const localization = this.definition?.localization;
|
|
5416
|
+
if (!localization) return;
|
|
5417
|
+
const store = getStore()?.getState();
|
|
5418
|
+
if (!store) return;
|
|
5419
|
+
for (const langCode of Object.keys(localization)) {
|
|
5420
|
+
const entry = localization[langCode]?.[sourceUuid];
|
|
5421
|
+
if (entry) {
|
|
5422
|
+
store.updateLocalization(
|
|
5423
|
+
langCode,
|
|
5424
|
+
targetUuid,
|
|
5425
|
+
JSON.parse(JSON.stringify(entry))
|
|
5426
|
+
);
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
5429
|
+
}
|
|
5430
|
+
|
|
5212
5431
|
private getLocalizationLanguages(): Array<{ code: string; name: string }> {
|
|
5213
5432
|
if (!this.definition) {
|
|
5214
5433
|
return [];
|
|
@@ -6233,116 +6452,10 @@ export class Editor extends RapidElement {
|
|
|
6233
6452
|
`;
|
|
6234
6453
|
}
|
|
6235
6454
|
|
|
6236
|
-
private
|
|
6237
|
-
if (this.showLanguageOptions) {
|
|
6238
|
-
this.showLanguageOptions = false;
|
|
6239
|
-
return;
|
|
6240
|
-
}
|
|
6241
|
-
this.showLanguageOptions = true;
|
|
6242
|
-
// Close on next click anywhere outside
|
|
6243
|
-
requestAnimationFrame(() => {
|
|
6244
|
-
const close = () => {
|
|
6245
|
-
this.showLanguageOptions = false;
|
|
6246
|
-
document.removeEventListener('click', close);
|
|
6247
|
-
};
|
|
6248
|
-
document.addEventListener('click', close, { once: true });
|
|
6249
|
-
});
|
|
6250
|
-
}
|
|
6251
|
-
|
|
6252
|
-
private handleLanguageOptionSelected(event: CustomEvent): void {
|
|
6253
|
-
if (!this.showLanguageOptions) return;
|
|
6254
|
-
const selected = event.detail?.selected;
|
|
6255
|
-
if (selected?.value === PRIMARY_LANGUAGE_OPTION_VALUE) {
|
|
6256
|
-
this.handleLanguageChange(this.definition?.language || '');
|
|
6257
|
-
} else if (selected?.value) {
|
|
6258
|
-
this.handleLanguageChange(selected.value);
|
|
6259
|
-
}
|
|
6260
|
-
this.showLanguageOptions = false;
|
|
6261
|
-
}
|
|
6262
|
-
|
|
6263
|
-
private renderToolbarTip(
|
|
6264
|
-
text: string | TemplateResult,
|
|
6265
|
-
content: TemplateResult
|
|
6266
|
-
): TemplateResult {
|
|
6267
|
-
const tipContent = text;
|
|
6268
|
-
return html`
|
|
6269
|
-
<temba-tip
|
|
6270
|
-
class="toolbar-tip"
|
|
6271
|
-
.text=${typeof tipContent === 'string' ? tipContent : ''}
|
|
6272
|
-
.content=${typeof tipContent === 'string' ? null : tipContent}
|
|
6273
|
-
position="top"
|
|
6274
|
-
>
|
|
6275
|
-
${content}
|
|
6276
|
-
</temba-tip>
|
|
6277
|
-
`;
|
|
6278
|
-
}
|
|
6279
|
-
|
|
6280
|
-
private isMacPlatform(): boolean {
|
|
6281
|
-
return (
|
|
6282
|
-
typeof navigator !== 'undefined' &&
|
|
6283
|
-
/Mac|iPod|iPhone|iPad/.test(navigator.platform)
|
|
6284
|
-
);
|
|
6285
|
-
}
|
|
6286
|
-
|
|
6287
|
-
private getSearchShortcutLabel(): string {
|
|
6288
|
-
return this.isMacPlatform() ? '⌘F' : 'Ctrl+F';
|
|
6289
|
-
}
|
|
6290
|
-
|
|
6291
|
-
private renderToolbarShortcutLabel(
|
|
6292
|
-
label: string,
|
|
6293
|
-
shortcut: string
|
|
6294
|
-
): TemplateResult {
|
|
6295
|
-
return html`<span style="display:inline-flex; align-items:center; gap:8px;">
|
|
6296
|
-
<span>${label}</span>
|
|
6297
|
-
<kbd>${shortcut}</kbd>
|
|
6298
|
-
</span>`;
|
|
6299
|
-
}
|
|
6300
|
-
|
|
6301
|
-
private renderToolbarLanguageOption(
|
|
6302
|
-
option: { name: string; value: string; percent?: number },
|
|
6303
|
-
selected: boolean
|
|
6304
|
-
): TemplateResult {
|
|
6305
|
-
if (option.value === PRIMARY_LANGUAGE_OPTION_VALUE) {
|
|
6306
|
-
const primaryBackground = selected ? '#e1e8ef' : '#edf1f5';
|
|
6307
|
-
return html`
|
|
6308
|
-
<div
|
|
6309
|
-
style="display:flex; align-items:center; justify-content:space-between; gap:8px; background:${primaryBackground}; color:#2f3f52; border-radius:4px; padding:6px 10px;"
|
|
6310
|
-
>
|
|
6311
|
-
<span>${option.name}</span>
|
|
6312
|
-
<span
|
|
6313
|
-
style="display:inline-flex; align-items:center; border-radius:999px; background:rgba(47, 63, 82, 0.12); color:#2f3f52; font-size:10px; font-weight:700; line-height:1; padding:3px 7px;"
|
|
6314
|
-
>Original</span
|
|
6315
|
-
>
|
|
6316
|
-
</div>
|
|
6317
|
-
`;
|
|
6318
|
-
}
|
|
6319
|
-
|
|
6320
|
-
const isComplete = option.percent === 100;
|
|
6321
|
-
const optionBg = isComplete ? '#d4f5e0' : '';
|
|
6322
|
-
const optionHoverBg = isComplete ? '#c0edce' : '';
|
|
6323
|
-
const optionRadius = isComplete ? 'border-radius:4px;' : '';
|
|
6324
|
-
const percentColor = isComplete ? 'color:#1a7f37;' : 'color:#5f6b7a;';
|
|
6325
|
-
|
|
6326
|
-
return html`
|
|
6327
|
-
<div
|
|
6328
|
-
style="display:flex; align-items:center; justify-content:space-between; gap:8px; padding:6px 10px; ${optionBg ? `background:${optionBg};` : ''} ${optionRadius}"
|
|
6329
|
-
@mouseenter=${isComplete ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.background = optionHoverBg; } : null}
|
|
6330
|
-
@mouseleave=${isComplete ? (e: MouseEvent) => { (e.currentTarget as HTMLElement).style.background = optionBg; } : null}
|
|
6331
|
-
>
|
|
6332
|
-
<span style="${isComplete ? 'color:#1a7f37;' : ''}">${option.name}</span>
|
|
6333
|
-
<span style="font-size:11px; font-weight:600; ${percentColor}"
|
|
6334
|
-
>${option.percent ?? 0}%</span
|
|
6335
|
-
>
|
|
6336
|
-
</div>
|
|
6337
|
-
`;
|
|
6338
|
-
}
|
|
6339
|
-
|
|
6340
|
-
private renderToolbar(): TemplateResult {
|
|
6455
|
+
private renderToolbarElement(): TemplateResult {
|
|
6341
6456
|
const languages = this.getLocalizationLanguages();
|
|
6342
6457
|
const availableLanguages = this.getAvailableLanguages();
|
|
6343
6458
|
const baseLanguage = this.definition?.language;
|
|
6344
|
-
const languageOptionCount = (baseLanguage ? 1 : 0) + languages.length;
|
|
6345
|
-
const showLanguageControls = languageOptionCount > 1;
|
|
6346
6459
|
const baseLanguageName =
|
|
6347
6460
|
availableLanguages.find((lang) => lang.code === baseLanguage)?.name ||
|
|
6348
6461
|
baseLanguage ||
|
|
@@ -6364,11 +6477,6 @@ export class Editor extends RapidElement {
|
|
|
6364
6477
|
const percent = Math.round(
|
|
6365
6478
|
(progress.localized / Math.max(progress.total, 1)) * 100
|
|
6366
6479
|
);
|
|
6367
|
-
const hasTranslations = progress.total > 0;
|
|
6368
|
-
const showLocalizationTools = Boolean(activeLanguage);
|
|
6369
|
-
const searchTargetLabel = this.showMessageTable
|
|
6370
|
-
? 'Search table'
|
|
6371
|
-
: 'Search flow';
|
|
6372
6480
|
const languageOptions = [
|
|
6373
6481
|
{
|
|
6374
6482
|
name: baseLanguageName,
|
|
@@ -6390,179 +6498,56 @@ export class Editor extends RapidElement {
|
|
|
6390
6498
|
];
|
|
6391
6499
|
|
|
6392
6500
|
return html`
|
|
6393
|
-
<
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
'Table View',
|
|
6409
|
-
html`
|
|
6410
|
-
<button
|
|
6411
|
-
class="toolbar-btn ${this.showMessageTable ? 'active' : ''}"
|
|
6412
|
-
@click=${() => { this.showMessageTable = true; }}
|
|
6413
|
-
aria-label="Table View"
|
|
6414
|
-
>
|
|
6415
|
-
<temba-icon name=${Icon.quick_replies} size="1"></temba-icon>
|
|
6416
|
-
</button>
|
|
6417
|
-
`
|
|
6418
|
-
)}
|
|
6419
|
-
${showLanguageControls
|
|
6420
|
-
? html`
|
|
6421
|
-
<div class="toolbar-divider"></div>
|
|
6422
|
-
<div class="toolbar-language-group">
|
|
6423
|
-
<div class="toolbar-language">
|
|
6424
|
-
${this.renderToolbarTip(
|
|
6425
|
-
'Change language',
|
|
6426
|
-
html`
|
|
6427
|
-
<button
|
|
6428
|
-
class="language-pill ${isBaseSelected ? 'primary' : percent === 100 ? 'complete' : ''}"
|
|
6429
|
-
id="language-btn"
|
|
6430
|
-
@click=${this.handleLanguageIconClick}
|
|
6431
|
-
aria-label="Change language"
|
|
6432
|
-
>
|
|
6433
|
-
<temba-icon name=${Icon.language}></temba-icon>
|
|
6434
|
-
<span>${currentLanguage.name}</span>
|
|
6435
|
-
${!isBaseSelected
|
|
6436
|
-
? html`<span class="language-percent">${percent}%</span>`
|
|
6437
|
-
: ''}
|
|
6438
|
-
<temba-icon
|
|
6439
|
-
class="language-pill-caret"
|
|
6440
|
-
name=${this.showLanguageOptions
|
|
6441
|
-
? Icon.arrow_up
|
|
6442
|
-
: Icon.arrow_down}
|
|
6443
|
-
></temba-icon>
|
|
6444
|
-
</button>
|
|
6445
|
-
`
|
|
6446
|
-
)}
|
|
6447
|
-
<temba-options
|
|
6448
|
-
.anchorTo=${this.querySelector('#language-btn') as HTMLElement}
|
|
6449
|
-
.options=${languageOptions}
|
|
6450
|
-
.renderOption=${this.renderToolbarLanguageOption}
|
|
6451
|
-
?visible=${this.showLanguageOptions}
|
|
6452
|
-
@temba-selection=${this.handleLanguageOptionSelected}
|
|
6453
|
-
style="--temba-options-option-margin:4px; --temba-options-option-padding:0; --temba-options-option-radius:4px;"
|
|
6454
|
-
min-width="230"
|
|
6455
|
-
></temba-options>
|
|
6456
|
-
</div>
|
|
6457
|
-
${showLocalizationTools
|
|
6458
|
-
? this.renderToolbarTranslationTools(hasTranslations)
|
|
6459
|
-
: ''}
|
|
6460
|
-
</div>
|
|
6461
|
-
`
|
|
6462
|
-
: ''}
|
|
6463
|
-
</div>
|
|
6464
|
-
<div class="toolbar-right">
|
|
6465
|
-
${!this.showMessageTable ? html`
|
|
6466
|
-
${this.renderToolbarTip(
|
|
6467
|
-
'Zoom to fit',
|
|
6468
|
-
html`
|
|
6469
|
-
<button
|
|
6470
|
-
class="toolbar-btn"
|
|
6471
|
-
@click=${this.zoomToFit}
|
|
6472
|
-
?disabled=${!this.zoomInitialized || this.zoomFitted}
|
|
6473
|
-
aria-label="Zoom to fit"
|
|
6474
|
-
>
|
|
6475
|
-
<temba-icon name=${Icon.zoom_fit} size="1"></temba-icon>
|
|
6476
|
-
</button>
|
|
6477
|
-
`
|
|
6478
|
-
)}
|
|
6479
|
-
<div class="toolbar-divider"></div>
|
|
6480
|
-
${this.renderToolbarTip(
|
|
6481
|
-
'Zoom out',
|
|
6482
|
-
html`
|
|
6483
|
-
<button
|
|
6484
|
-
class="toolbar-btn"
|
|
6485
|
-
@click=${this.zoomOut}
|
|
6486
|
-
?disabled=${!this.zoomInitialized || this.zoom <= 0.3}
|
|
6487
|
-
aria-label="Zoom out"
|
|
6488
|
-
>
|
|
6489
|
-
−
|
|
6490
|
-
</button>
|
|
6491
|
-
`
|
|
6492
|
-
)}
|
|
6493
|
-
<span class="toolbar-zoom-level">${this.zoomInitialized ? `${Math.round(this.zoom * 100)}%` : ''}</span>
|
|
6494
|
-
${this.renderToolbarTip(
|
|
6495
|
-
'Zoom in',
|
|
6496
|
-
html`
|
|
6497
|
-
<button
|
|
6498
|
-
class="toolbar-btn"
|
|
6499
|
-
@click=${this.zoomIn}
|
|
6500
|
-
?disabled=${!this.zoomInitialized || this.zoom >= 1.0}
|
|
6501
|
-
aria-label="Zoom in"
|
|
6502
|
-
>
|
|
6503
|
-
+
|
|
6504
|
-
</button>
|
|
6505
|
-
`
|
|
6506
|
-
)}
|
|
6507
|
-
<div class="toolbar-divider"></div>
|
|
6508
|
-
${this.renderToolbarTip(
|
|
6509
|
-
'Zoom to 100%',
|
|
6510
|
-
html`
|
|
6511
|
-
<button
|
|
6512
|
-
class="toolbar-btn"
|
|
6513
|
-
@click=${this.zoomToFull}
|
|
6514
|
-
?disabled=${!this.zoomInitialized || this.zoom >= 1.0}
|
|
6515
|
-
aria-label="Zoom to 100%"
|
|
6516
|
-
>
|
|
6517
|
-
<temba-icon name=${Icon.zoom_in} size="1"></temba-icon>
|
|
6518
|
-
</button>
|
|
6519
|
-
`
|
|
6520
|
-
)}
|
|
6521
|
-
<div class="toolbar-divider"></div>
|
|
6522
|
-
` : ''}
|
|
6523
|
-
${this.renderToolbarTip(
|
|
6524
|
-
'Revisions',
|
|
6525
|
-
html`
|
|
6526
|
-
<button
|
|
6527
|
-
class="toolbar-btn ${!this.revisionsWindowHidden
|
|
6528
|
-
? 'active'
|
|
6529
|
-
: ''}"
|
|
6530
|
-
@click=${this.handleRevisionsTabClick}
|
|
6531
|
-
aria-label="Revisions"
|
|
6532
|
-
>
|
|
6533
|
-
<temba-icon
|
|
6534
|
-
name=${this.isSaving ? 'progress_spinner' : 'revisions'}
|
|
6535
|
-
size="1"
|
|
6536
|
-
?spin=${this.isSaving}
|
|
6537
|
-
></temba-icon>
|
|
6538
|
-
</button>
|
|
6539
|
-
`
|
|
6540
|
-
)}
|
|
6541
|
-
<div class="toolbar-divider"></div>
|
|
6542
|
-
${this.renderToolbarTip(
|
|
6543
|
-
this.renderToolbarShortcutLabel(
|
|
6544
|
-
searchTargetLabel,
|
|
6545
|
-
this.getSearchShortcutLabel()
|
|
6546
|
-
),
|
|
6547
|
-
html`
|
|
6548
|
-
<button
|
|
6549
|
-
class="toolbar-btn"
|
|
6550
|
-
@click=${this.openFlowSearch}
|
|
6551
|
-
?disabled=${!!this.viewingRevision}
|
|
6552
|
-
aria-label=${searchTargetLabel}
|
|
6553
|
-
>
|
|
6554
|
-
<temba-icon name=${Icon.search} size="1"></temba-icon>
|
|
6555
|
-
</button>
|
|
6556
|
-
`
|
|
6557
|
-
)}
|
|
6558
|
-
</div>
|
|
6559
|
-
</div>
|
|
6501
|
+
<temba-editor-toolbar
|
|
6502
|
+
?message-view=${this.showMessageTable}
|
|
6503
|
+
.zoom=${this.zoom}
|
|
6504
|
+
?zoom-initialized=${this.zoomInitialized}
|
|
6505
|
+
?zoom-fitted=${this.zoomFitted}
|
|
6506
|
+
?revisions-active=${!this.revisionsWindowHidden}
|
|
6507
|
+
?is-saving=${this.isSaving}
|
|
6508
|
+
?search-disabled=${!!this.viewingRevision}
|
|
6509
|
+
.languageOptions=${languageOptions}
|
|
6510
|
+
current-language-name=${currentLanguage.name}
|
|
6511
|
+
?is-base-language=${isBaseSelected}
|
|
6512
|
+
.languagePercent=${percent}
|
|
6513
|
+
?show-localization-tools=${Boolean(activeLanguage)}
|
|
6514
|
+
@temba-button-clicked=${this.handleToolbarAction}
|
|
6515
|
+
></temba-editor-toolbar>
|
|
6560
6516
|
`;
|
|
6561
6517
|
}
|
|
6562
6518
|
|
|
6563
|
-
private
|
|
6564
|
-
|
|
6565
|
-
|
|
6519
|
+
private handleToolbarAction(e: CustomEvent): void {
|
|
6520
|
+
const detail = e.detail as ToolbarAction;
|
|
6521
|
+
switch (detail.action) {
|
|
6522
|
+
case 'view-change':
|
|
6523
|
+
this.showMessageTable = detail.view === 'table';
|
|
6524
|
+
break;
|
|
6525
|
+
case 'zoom-in':
|
|
6526
|
+
this.zoomIn();
|
|
6527
|
+
break;
|
|
6528
|
+
case 'zoom-out':
|
|
6529
|
+
this.zoomOut();
|
|
6530
|
+
break;
|
|
6531
|
+
case 'zoom-to-fit':
|
|
6532
|
+
this.zoomToFit();
|
|
6533
|
+
break;
|
|
6534
|
+
case 'zoom-to-full':
|
|
6535
|
+
this.zoomToFull();
|
|
6536
|
+
break;
|
|
6537
|
+
case 'revisions':
|
|
6538
|
+
this.handleRevisionsTabClick();
|
|
6539
|
+
break;
|
|
6540
|
+
case 'search':
|
|
6541
|
+
this.openFlowSearch();
|
|
6542
|
+
break;
|
|
6543
|
+
case 'language-change':
|
|
6544
|
+
if (detail.isPrimary) {
|
|
6545
|
+
this.handleLanguageChange(this.definition?.language || '');
|
|
6546
|
+
} else if (detail.languageCode) {
|
|
6547
|
+
this.handleLanguageChange(detail.languageCode);
|
|
6548
|
+
}
|
|
6549
|
+
break;
|
|
6550
|
+
}
|
|
6566
6551
|
}
|
|
6567
6552
|
|
|
6568
6553
|
/**
|
|
@@ -6721,7 +6706,7 @@ export class Editor extends RapidElement {
|
|
|
6721
6706
|
return html`${style} ${this.renderIssuesWindow()}
|
|
6722
6707
|
${this.renderRevisionsWindow()} ${this.renderLocalizationWindow()}
|
|
6723
6708
|
<div id="editor-container">
|
|
6724
|
-
${this.
|
|
6709
|
+
${this.renderToolbarElement()}
|
|
6725
6710
|
<div id="editor">
|
|
6726
6711
|
${this.showMessageTable
|
|
6727
6712
|
? html`<temba-message-table></temba-message-table>`
|
|
@@ -6852,6 +6837,7 @@ export class Editor extends RapidElement {
|
|
|
6852
6837
|
`}
|
|
6853
6838
|
</div>
|
|
6854
6839
|
${this.renderPendingCard()}
|
|
6840
|
+
<div class="drag-hint" id="drag-hint">Hold ⇧ to duplicate</div>
|
|
6855
6841
|
</div>
|
|
6856
6842
|
<div class="loupe" id="loupe">
|
|
6857
6843
|
<div class="loupe-content" id="loupe-content"></div>
|