@nyaruka/temba-components 0.156.4 → 0.156.6
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 +16 -0
- package/dist/temba-components.js +620 -617
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/flow/CanvasNode.ts +83 -20
- package/src/flow/Editor.ts +271 -43
- package/src/form/select/Select.ts +1 -0
- package/src/list/SortableList.ts +6 -0
package/package.json
CHANGED
package/src/flow/CanvasNode.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { ACTION_GROUP_METADATA, SPLIT_GROUP_METADATA } from './types';
|
|
|
5
5
|
import { Action, Exit, Node, NodeUI, Router } from '../store/flow-definition';
|
|
6
6
|
import { property } from 'lit/decorators.js';
|
|
7
7
|
import { RapidElement } from '../RapidElement';
|
|
8
|
-
import { getClasses } from '../utils';
|
|
8
|
+
import { generateUUID, getClasses } from '../utils';
|
|
9
|
+
import { SortableList } from '../list/SortableList';
|
|
9
10
|
import { isRightClick, localizeAction, renderClamped } from './utils';
|
|
10
11
|
import { Plumber } from './Plumber';
|
|
11
12
|
import { getStore } from '../store/Store';
|
|
@@ -363,6 +364,7 @@ export class CanvasNode extends RapidElement {
|
|
|
363
364
|
/* Localizable category - yellow background */
|
|
364
365
|
.category.localizable {
|
|
365
366
|
background-color: #fff8dc;
|
|
367
|
+
border-color: #e8daa0;
|
|
366
368
|
}
|
|
367
369
|
|
|
368
370
|
.action-exits {
|
|
@@ -594,6 +596,8 @@ export class CanvasNode extends RapidElement {
|
|
|
594
596
|
this.handleExternalActionDragLeave.bind(this);
|
|
595
597
|
this.handleActionShowGhost = this.handleActionShowGhost.bind(this);
|
|
596
598
|
this.handleActionHideGhost = this.handleActionHideGhost.bind(this);
|
|
599
|
+
this.handleActionShowOriginal = this.handleActionShowOriginal.bind(this);
|
|
600
|
+
this.handleActionHideOriginal = this.handleActionHideOriginal.bind(this);
|
|
597
601
|
}
|
|
598
602
|
|
|
599
603
|
connectedCallback() {
|
|
@@ -620,6 +624,14 @@ export class CanvasNode extends RapidElement {
|
|
|
620
624
|
'action-hide-ghost',
|
|
621
625
|
this.handleActionHideGhost as EventListener
|
|
622
626
|
);
|
|
627
|
+
this.addEventListener(
|
|
628
|
+
'action-show-original',
|
|
629
|
+
this.handleActionShowOriginal as EventListener
|
|
630
|
+
);
|
|
631
|
+
this.addEventListener(
|
|
632
|
+
'action-hide-original',
|
|
633
|
+
this.handleActionHideOriginal as EventListener
|
|
634
|
+
);
|
|
623
635
|
|
|
624
636
|
// Observe size changes to revalidate plumbing connections
|
|
625
637
|
this.resizeObserver = new ResizeObserver(() => {
|
|
@@ -731,6 +743,14 @@ export class CanvasNode extends RapidElement {
|
|
|
731
743
|
'action-hide-ghost',
|
|
732
744
|
this.handleActionHideGhost as EventListener
|
|
733
745
|
);
|
|
746
|
+
this.removeEventListener(
|
|
747
|
+
'action-show-original',
|
|
748
|
+
this.handleActionShowOriginal as EventListener
|
|
749
|
+
);
|
|
750
|
+
this.removeEventListener(
|
|
751
|
+
'action-hide-original',
|
|
752
|
+
this.handleActionHideOriginal as EventListener
|
|
753
|
+
);
|
|
734
754
|
|
|
735
755
|
// Clear any pending exit removal timeouts
|
|
736
756
|
this.exitRemovalTimeouts.forEach((timeoutId) => {
|
|
@@ -1551,11 +1571,32 @@ export class CanvasNode extends RapidElement {
|
|
|
1551
1571
|
}
|
|
1552
1572
|
}
|
|
1553
1573
|
|
|
1574
|
+
private handleActionShowOriginal(_event: CustomEvent): void {
|
|
1575
|
+
const sortableList = this.querySelector(
|
|
1576
|
+
'temba-sortable-list'
|
|
1577
|
+
) as SortableList;
|
|
1578
|
+
sortableList?.setOriginalVisible(true);
|
|
1579
|
+
this.showLastActionPlaceholder = false;
|
|
1580
|
+
this.requestUpdate();
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
private handleActionHideOriginal(_event: CustomEvent): void {
|
|
1584
|
+
const sortableList = this.querySelector(
|
|
1585
|
+
'temba-sortable-list'
|
|
1586
|
+
) as SortableList;
|
|
1587
|
+
sortableList?.setOriginalVisible(false);
|
|
1588
|
+
// Restore the placeholder if this is the last action
|
|
1589
|
+
if (this.node.actions.length === 1) {
|
|
1590
|
+
this.showLastActionPlaceholder = true;
|
|
1591
|
+
}
|
|
1592
|
+
this.requestUpdate();
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1554
1595
|
private handleExternalActionDrop(event: CustomEvent): void {
|
|
1555
1596
|
// Only handle if this is an execute_actions node
|
|
1556
1597
|
if (this.ui.type !== 'execute_actions') return;
|
|
1557
1598
|
|
|
1558
|
-
const { action, sourceNodeUuid, actionIndex } = event.detail;
|
|
1599
|
+
const { action, sourceNodeUuid, actionIndex, isCopy } = event.detail;
|
|
1559
1600
|
|
|
1560
1601
|
// Don't accept drops from the same node
|
|
1561
1602
|
if (sourceNodeUuid === this.node.uuid) return;
|
|
@@ -1581,30 +1622,52 @@ export class CanvasNode extends RapidElement {
|
|
|
1581
1622
|
|
|
1582
1623
|
// IMPORTANT: Add the action to this node FIRST, before removing from source
|
|
1583
1624
|
// This ensures we don't lose the action if the source node gets deleted
|
|
1625
|
+
const droppedAction = isCopy
|
|
1626
|
+
? { ...action, uuid: generateUUID() }
|
|
1627
|
+
: action;
|
|
1584
1628
|
const newActions = [...this.node.actions];
|
|
1585
|
-
newActions.splice(dropIndex, 0,
|
|
1629
|
+
newActions.splice(dropIndex, 0, droppedAction);
|
|
1586
1630
|
|
|
1587
1631
|
const updatedNode = { ...this.node, actions: newActions };
|
|
1588
1632
|
getStore()?.getState().updateNode(this.node.uuid, updatedNode);
|
|
1589
1633
|
|
|
1590
|
-
//
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1634
|
+
// Copy localizations from the original action to the new one
|
|
1635
|
+
if (isCopy) {
|
|
1636
|
+
const localization = flowDefinition.localization;
|
|
1637
|
+
if (localization) {
|
|
1638
|
+
for (const langCode of Object.keys(localization)) {
|
|
1639
|
+
const entry = localization[langCode]?.[action.uuid];
|
|
1640
|
+
if (entry) {
|
|
1641
|
+
store.getState().updateLocalization(
|
|
1642
|
+
langCode,
|
|
1643
|
+
droppedAction.uuid,
|
|
1644
|
+
JSON.parse(JSON.stringify(entry))
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1594
1650
|
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1651
|
+
if (!isCopy) {
|
|
1652
|
+
// Remove the action from the source node
|
|
1653
|
+
const updatedSourceActions = sourceNode.actions.filter(
|
|
1654
|
+
(_a, idx) => idx !== actionIndex
|
|
1655
|
+
);
|
|
1656
|
+
|
|
1657
|
+
// If source node has no actions left, remove it
|
|
1658
|
+
if (updatedSourceActions.length === 0) {
|
|
1659
|
+
// Fire event to Editor so it can clean up jsPlumb connections properly
|
|
1660
|
+
this.fireCustomEvent(CustomEventType.NodeDeleted, {
|
|
1661
|
+
uuid: sourceNodeUuid
|
|
1662
|
+
});
|
|
1663
|
+
} else {
|
|
1664
|
+
// Update source node
|
|
1665
|
+
const updatedSourceNode = {
|
|
1666
|
+
...sourceNode,
|
|
1667
|
+
actions: updatedSourceActions
|
|
1668
|
+
};
|
|
1669
|
+
getStore()?.getState().updateNode(sourceNodeUuid, updatedSourceNode);
|
|
1670
|
+
}
|
|
1608
1671
|
}
|
|
1609
1672
|
|
|
1610
1673
|
// Request update and notify that this node's size changed
|
package/src/flow/Editor.ts
CHANGED
|
@@ -104,8 +104,9 @@ export interface SelectionBox {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
const DRAG_THRESHOLD = 5;
|
|
107
|
-
const AUTO_SCROLL_EDGE_ZONE =
|
|
107
|
+
const AUTO_SCROLL_EDGE_ZONE = 150;
|
|
108
108
|
const AUTO_SCROLL_MAX_SPEED = 15;
|
|
109
|
+
const AUTO_SCROLL_BEYOND_MULTIPLIER = 5;
|
|
109
110
|
|
|
110
111
|
type TranslationType = 'property' | 'category';
|
|
111
112
|
|
|
@@ -499,6 +500,19 @@ export class Editor extends RapidElement {
|
|
|
499
500
|
// Track previous target node to clear placeholder when moving between nodes
|
|
500
501
|
private previousActionDragTargetNodeUuid: string | null = null;
|
|
501
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
|
+
|
|
502
516
|
// Connection placeholder state for dropping connections on empty canvas
|
|
503
517
|
@state()
|
|
504
518
|
private connectionPlaceholder: {
|
|
@@ -633,6 +647,7 @@ export class Editor extends RapidElement {
|
|
|
633
647
|
|
|
634
648
|
#canvas.shift-held {
|
|
635
649
|
--shift-held-cursor: copy;
|
|
650
|
+
cursor: copy;
|
|
636
651
|
}
|
|
637
652
|
|
|
638
653
|
#canvas.viewing-revision {
|
|
@@ -1415,6 +1430,11 @@ export class Editor extends RapidElement {
|
|
|
1415
1430
|
}
|
|
1416
1431
|
|
|
1417
1432
|
private makeConnection(info) {
|
|
1433
|
+
this.stopAutoScroll();
|
|
1434
|
+
this.autoScrollDeltaX = 0;
|
|
1435
|
+
this.autoScrollDeltaY = 0;
|
|
1436
|
+
this.lastPointerPos = null;
|
|
1437
|
+
|
|
1418
1438
|
if (this.sourceId && this.targetId && this.isValidTarget) {
|
|
1419
1439
|
// going to the same target, just put it back
|
|
1420
1440
|
if (info.target.id === this.targetId) {
|
|
@@ -2311,7 +2331,7 @@ export class Editor extends RapidElement {
|
|
|
2311
2331
|
if (event.key === 'Shift') {
|
|
2312
2332
|
this.querySelector('#canvas')?.classList.add('shift-held');
|
|
2313
2333
|
|
|
2314
|
-
// Toggle to copy mode mid-drag
|
|
2334
|
+
// Toggle to copy mode mid-drag (nodes)
|
|
2315
2335
|
if (this.isDragging && !this.currentDragIsCopy) {
|
|
2316
2336
|
this.hideDragHint();
|
|
2317
2337
|
this.performShiftDragCopy();
|
|
@@ -2322,6 +2342,18 @@ export class Editor extends RapidElement {
|
|
|
2322
2342
|
this.updateDragPositions();
|
|
2323
2343
|
});
|
|
2324
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
|
+
}
|
|
2325
2357
|
}
|
|
2326
2358
|
|
|
2327
2359
|
// Cmd/Ctrl+F opens flow search (unless a dialog is already open)
|
|
@@ -2357,11 +2389,23 @@ export class Editor extends RapidElement {
|
|
|
2357
2389
|
if (event.key === 'Shift') {
|
|
2358
2390
|
this.querySelector('#canvas')?.classList.remove('shift-held');
|
|
2359
2391
|
|
|
2360
|
-
// Toggle back to move mode mid-drag
|
|
2392
|
+
// Toggle back to move mode mid-drag (nodes)
|
|
2361
2393
|
if (this.isDragging && this.currentDragIsCopy) {
|
|
2362
2394
|
this.revertShiftDragCopy();
|
|
2363
2395
|
requestAnimationFrame(() => this.updateDragPositions());
|
|
2364
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
|
+
}
|
|
2365
2409
|
}
|
|
2366
2410
|
}
|
|
2367
2411
|
|
|
@@ -2373,6 +2417,16 @@ export class Editor extends RapidElement {
|
|
|
2373
2417
|
this.revertShiftDragCopy();
|
|
2374
2418
|
requestAnimationFrame(() => this.updateDragPositions());
|
|
2375
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
|
+
}
|
|
2376
2430
|
}
|
|
2377
2431
|
|
|
2378
2432
|
// --- Flow settings cookie (LRU, max 50 flows) ---
|
|
@@ -3513,6 +3567,9 @@ export class Editor extends RapidElement {
|
|
|
3513
3567
|
}
|
|
3514
3568
|
|
|
3515
3569
|
if (this.plumber.connectionDragging) {
|
|
3570
|
+
this.lastPointerPos = { clientX: event.clientX, clientY: event.clientY };
|
|
3571
|
+
this.startAutoScroll();
|
|
3572
|
+
|
|
3516
3573
|
const targetNode = document.querySelector('temba-flow-node:hover');
|
|
3517
3574
|
|
|
3518
3575
|
// Clear previous target styles
|
|
@@ -3716,6 +3773,11 @@ export class Editor extends RapidElement {
|
|
|
3716
3773
|
|
|
3717
3774
|
this.currentDragIsCopy = false;
|
|
3718
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
|
+
|
|
3719
3781
|
// Restore drag context to originals
|
|
3720
3782
|
this.currentDragItem = { ...this.originalDragItem };
|
|
3721
3783
|
if (this.originalSelectedItems) {
|
|
@@ -3772,7 +3834,10 @@ export class Editor extends RapidElement {
|
|
|
3772
3834
|
if (!editor) return;
|
|
3773
3835
|
|
|
3774
3836
|
const tick = () => {
|
|
3775
|
-
if (
|
|
3837
|
+
if (
|
|
3838
|
+
(!this.isDragging && !this.plumber?.connectionDragging) ||
|
|
3839
|
+
!this.lastPointerPos
|
|
3840
|
+
) {
|
|
3776
3841
|
this.autoScrollAnimationId = null;
|
|
3777
3842
|
return;
|
|
3778
3843
|
}
|
|
@@ -3784,32 +3849,56 @@ export class Editor extends RapidElement {
|
|
|
3784
3849
|
let scrollDx = 0;
|
|
3785
3850
|
let scrollDy = 0;
|
|
3786
3851
|
|
|
3787
|
-
// Left edge
|
|
3852
|
+
// Left edge (including beyond)
|
|
3788
3853
|
const distFromLeft = mouseX - editorRect.left;
|
|
3789
|
-
if (distFromLeft
|
|
3790
|
-
const
|
|
3791
|
-
|
|
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);
|
|
3792
3863
|
}
|
|
3793
3864
|
|
|
3794
|
-
// Right edge
|
|
3865
|
+
// Right edge (including beyond)
|
|
3795
3866
|
const distFromRight = editorRect.right - mouseX;
|
|
3796
|
-
if (distFromRight
|
|
3797
|
-
const
|
|
3798
|
-
|
|
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;
|
|
3799
3876
|
}
|
|
3800
3877
|
|
|
3801
|
-
// Top edge
|
|
3878
|
+
// Top edge (including beyond)
|
|
3802
3879
|
const distFromTop = mouseY - editorRect.top;
|
|
3803
|
-
if (distFromTop
|
|
3804
|
-
const
|
|
3805
|
-
|
|
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);
|
|
3806
3889
|
}
|
|
3807
3890
|
|
|
3808
|
-
// Bottom edge
|
|
3891
|
+
// Bottom edge (including beyond)
|
|
3809
3892
|
const distFromBottom = editorRect.bottom - mouseY;
|
|
3810
|
-
if (distFromBottom
|
|
3811
|
-
const
|
|
3812
|
-
|
|
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;
|
|
3813
3902
|
}
|
|
3814
3903
|
|
|
3815
3904
|
if (scrollDx !== 0 || scrollDy !== 0) {
|
|
@@ -3823,7 +3912,7 @@ export class Editor extends RapidElement {
|
|
|
3823
3912
|
(editor.scrollLeft + editor.clientWidth + scrollDx) / this.zoom;
|
|
3824
3913
|
const neededHeight =
|
|
3825
3914
|
(editor.scrollTop + editor.clientHeight + scrollDy) / this.zoom;
|
|
3826
|
-
getStore()
|
|
3915
|
+
getStore()?.getState()?.expandCanvas(neededWidth, neededHeight);
|
|
3827
3916
|
}
|
|
3828
3917
|
|
|
3829
3918
|
editor.scrollLeft += scrollDx;
|
|
@@ -4941,6 +5030,32 @@ export class Editor extends RapidElement {
|
|
|
4941
5030
|
isLastAction = false
|
|
4942
5031
|
} = event.detail;
|
|
4943
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
|
+
|
|
4944
5059
|
// Check if mouse is over another execute_actions node
|
|
4945
5060
|
const targetNode = this.getNodeAtPosition(mouseX, mouseY);
|
|
4946
5061
|
|
|
@@ -5037,9 +5152,9 @@ export class Editor extends RapidElement {
|
|
|
5037
5152
|
`temba-flow-node[data-node-uuid="${nodeUuid}"]`
|
|
5038
5153
|
);
|
|
5039
5154
|
|
|
5040
|
-
// Show canvas drop preview
|
|
5041
|
-
//
|
|
5042
|
-
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) {
|
|
5043
5158
|
// Hide ghost when showing canvas preview (for canvas drops)
|
|
5044
5159
|
if (sourceElement) {
|
|
5045
5160
|
sourceElement.dispatchEvent(
|
|
@@ -5061,7 +5176,7 @@ export class Editor extends RapidElement {
|
|
|
5061
5176
|
actionHeight
|
|
5062
5177
|
};
|
|
5063
5178
|
} else {
|
|
5064
|
-
// 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)
|
|
5065
5180
|
if (sourceElement) {
|
|
5066
5181
|
sourceElement.dispatchEvent(
|
|
5067
5182
|
new CustomEvent('action-show-ghost', {
|
|
@@ -5098,6 +5213,75 @@ export class Editor extends RapidElement {
|
|
|
5098
5213
|
|
|
5099
5214
|
this.canvasDropPreview = null;
|
|
5100
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
|
+
}
|
|
5101
5285
|
}
|
|
5102
5286
|
|
|
5103
5287
|
private handleActionDropExternal(event: CustomEvent): void {
|
|
@@ -5110,6 +5294,15 @@ export class Editor extends RapidElement {
|
|
|
5110
5294
|
isLastAction = false
|
|
5111
5295
|
} = event.detail;
|
|
5112
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
|
+
|
|
5113
5306
|
// Check if we're dropping on an existing execute_actions node
|
|
5114
5307
|
const targetNodeUuid = this.actionDragTargetNodeUuid;
|
|
5115
5308
|
|
|
@@ -5126,7 +5319,8 @@ export class Editor extends RapidElement {
|
|
|
5126
5319
|
sourceNodeUuid: nodeUuid,
|
|
5127
5320
|
actionIndex,
|
|
5128
5321
|
mouseX,
|
|
5129
|
-
mouseY
|
|
5322
|
+
mouseY,
|
|
5323
|
+
isCopy
|
|
5130
5324
|
},
|
|
5131
5325
|
bubbles: false
|
|
5132
5326
|
})
|
|
@@ -5139,9 +5333,9 @@ export class Editor extends RapidElement {
|
|
|
5139
5333
|
return;
|
|
5140
5334
|
}
|
|
5141
5335
|
|
|
5142
|
-
// If this is the last action and
|
|
5336
|
+
// If this is the last action and not copying, do nothing
|
|
5143
5337
|
// Last actions can only be moved to other nodes, not dropped on canvas
|
|
5144
|
-
if (isLastAction) {
|
|
5338
|
+
if (isLastAction && !isCopy) {
|
|
5145
5339
|
this.canvasDropPreview = null;
|
|
5146
5340
|
this.actionDragTargetNodeUuid = null;
|
|
5147
5341
|
return;
|
|
@@ -5151,28 +5345,36 @@ export class Editor extends RapidElement {
|
|
|
5151
5345
|
// Snap to grid for the final drop position
|
|
5152
5346
|
const position = this.calculateCanvasDropPosition(mouseX, mouseY, true);
|
|
5153
5347
|
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
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;
|
|
5157
5354
|
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5355
|
+
const updatedActions = originalNode.actions.filter(
|
|
5356
|
+
(_a, idx) => idx !== actionIndex
|
|
5357
|
+
);
|
|
5161
5358
|
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
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
|
+
}
|
|
5170
5368
|
}
|
|
5171
5369
|
|
|
5172
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;
|
|
5173
5375
|
const newNode: Node = {
|
|
5174
5376
|
uuid: generateUUID(),
|
|
5175
|
-
actions: [
|
|
5377
|
+
actions: [droppedAction],
|
|
5176
5378
|
exits: [
|
|
5177
5379
|
{
|
|
5178
5380
|
uuid: generateUUID(),
|
|
@@ -5190,6 +5392,11 @@ export class Editor extends RapidElement {
|
|
|
5190
5392
|
// add the new node
|
|
5191
5393
|
getStore()?.getState().addNode(newNode, newNodeUI);
|
|
5192
5394
|
|
|
5395
|
+
// Copy localizations from the original action to the new one
|
|
5396
|
+
if (isCopy) {
|
|
5397
|
+
this.copyActionLocalizations(action.uuid, droppedAction.uuid);
|
|
5398
|
+
}
|
|
5399
|
+
|
|
5193
5400
|
// clear the preview
|
|
5194
5401
|
this.canvasDropPreview = null;
|
|
5195
5402
|
this.actionDragTargetNodeUuid = null;
|
|
@@ -5200,6 +5407,27 @@ export class Editor extends RapidElement {
|
|
|
5200
5407
|
});
|
|
5201
5408
|
}
|
|
5202
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
|
+
|
|
5203
5431
|
private getLocalizationLanguages(): Array<{ code: string; name: string }> {
|
|
5204
5432
|
if (!this.definition) {
|
|
5205
5433
|
return [];
|
package/src/list/SortableList.ts
CHANGED
|
@@ -731,6 +731,12 @@ export class SortableList extends RapidElement {
|
|
|
731
731
|
}
|
|
732
732
|
/* c8 ignore stop */
|
|
733
733
|
|
|
734
|
+
/** Show or hide the original element that is being dragged. */
|
|
735
|
+
public setOriginalVisible(visible: boolean): void {
|
|
736
|
+
if (!this.downEle) return;
|
|
737
|
+
this.downEle.style.display = visible ? this.originalDownDisplay : 'none';
|
|
738
|
+
}
|
|
739
|
+
|
|
734
740
|
public render(): TemplateResult {
|
|
735
741
|
return html`
|
|
736
742
|
<div
|