@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyaruka/temba-components",
3
- "version": "0.156.3",
3
+ "version": "0.156.5",
4
4
  "description": "Web components to support rapidpro and related projects",
5
5
  "author": "Nyaruka <code@nyaruka.coim>",
6
6
  "main": "dist/index.js",
@@ -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';
@@ -107,6 +108,26 @@ export class CanvasNode extends RapidElement {
107
108
  user-select: none;
108
109
  }
109
110
 
111
+ .shift-held > temba-flow-node,
112
+ .shift-held > temba-flow-node * {
113
+ cursor: copy !important;
114
+ }
115
+
116
+ .shift-held > temba-flow-node .exit,
117
+ .shift-held > temba-flow-node .exit *,
118
+ .shift-held > temba-flow-node .linked-name,
119
+ .shift-held > temba-flow-node .linked-name *,
120
+ .shift-held > temba-flow-node .remove-button,
121
+ .shift-held > temba-flow-node .remove-button * {
122
+ cursor: pointer !important;
123
+ }
124
+
125
+ temba-flow-node.drag-copy .node {
126
+ outline: 3px dashed var(--color-primary, #3b82f6);
127
+ outline-offset: 2px;
128
+ opacity: 0.7;
129
+ }
130
+
110
131
  /* Flow start indicator */
111
132
  temba-flow-node.flow-start .node::before {
112
133
  content: 'FLOW START';
@@ -574,6 +595,8 @@ export class CanvasNode extends RapidElement {
574
595
  this.handleExternalActionDragLeave.bind(this);
575
596
  this.handleActionShowGhost = this.handleActionShowGhost.bind(this);
576
597
  this.handleActionHideGhost = this.handleActionHideGhost.bind(this);
598
+ this.handleActionShowOriginal = this.handleActionShowOriginal.bind(this);
599
+ this.handleActionHideOriginal = this.handleActionHideOriginal.bind(this);
577
600
  }
578
601
 
579
602
  connectedCallback() {
@@ -600,6 +623,14 @@ export class CanvasNode extends RapidElement {
600
623
  'action-hide-ghost',
601
624
  this.handleActionHideGhost as EventListener
602
625
  );
626
+ this.addEventListener(
627
+ 'action-show-original',
628
+ this.handleActionShowOriginal as EventListener
629
+ );
630
+ this.addEventListener(
631
+ 'action-hide-original',
632
+ this.handleActionHideOriginal as EventListener
633
+ );
603
634
 
604
635
  // Observe size changes to revalidate plumbing connections
605
636
  this.resizeObserver = new ResizeObserver(() => {
@@ -711,6 +742,14 @@ export class CanvasNode extends RapidElement {
711
742
  'action-hide-ghost',
712
743
  this.handleActionHideGhost as EventListener
713
744
  );
745
+ this.removeEventListener(
746
+ 'action-show-original',
747
+ this.handleActionShowOriginal as EventListener
748
+ );
749
+ this.removeEventListener(
750
+ 'action-hide-original',
751
+ this.handleActionHideOriginal as EventListener
752
+ );
714
753
 
715
754
  // Clear any pending exit removal timeouts
716
755
  this.exitRemovalTimeouts.forEach((timeoutId) => {
@@ -1531,11 +1570,32 @@ export class CanvasNode extends RapidElement {
1531
1570
  }
1532
1571
  }
1533
1572
 
1573
+ private handleActionShowOriginal(_event: CustomEvent): void {
1574
+ const sortableList = this.querySelector(
1575
+ 'temba-sortable-list'
1576
+ ) as SortableList;
1577
+ sortableList?.setOriginalVisible(true);
1578
+ this.showLastActionPlaceholder = false;
1579
+ this.requestUpdate();
1580
+ }
1581
+
1582
+ private handleActionHideOriginal(_event: CustomEvent): void {
1583
+ const sortableList = this.querySelector(
1584
+ 'temba-sortable-list'
1585
+ ) as SortableList;
1586
+ sortableList?.setOriginalVisible(false);
1587
+ // Restore the placeholder if this is the last action
1588
+ if (this.node.actions.length === 1) {
1589
+ this.showLastActionPlaceholder = true;
1590
+ }
1591
+ this.requestUpdate();
1592
+ }
1593
+
1534
1594
  private handleExternalActionDrop(event: CustomEvent): void {
1535
1595
  // Only handle if this is an execute_actions node
1536
1596
  if (this.ui.type !== 'execute_actions') return;
1537
1597
 
1538
- const { action, sourceNodeUuid, actionIndex } = event.detail;
1598
+ const { action, sourceNodeUuid, actionIndex, isCopy } = event.detail;
1539
1599
 
1540
1600
  // Don't accept drops from the same node
1541
1601
  if (sourceNodeUuid === this.node.uuid) return;
@@ -1561,30 +1621,52 @@ export class CanvasNode extends RapidElement {
1561
1621
 
1562
1622
  // IMPORTANT: Add the action to this node FIRST, before removing from source
1563
1623
  // This ensures we don't lose the action if the source node gets deleted
1624
+ const droppedAction = isCopy
1625
+ ? { ...action, uuid: generateUUID() }
1626
+ : action;
1564
1627
  const newActions = [...this.node.actions];
1565
- newActions.splice(dropIndex, 0, action);
1628
+ newActions.splice(dropIndex, 0, droppedAction);
1566
1629
 
1567
1630
  const updatedNode = { ...this.node, actions: newActions };
1568
1631
  getStore()?.getState().updateNode(this.node.uuid, updatedNode);
1569
1632
 
1570
- // Now remove the action from the source node
1571
- const updatedSourceActions = sourceNode.actions.filter(
1572
- (_a, idx) => idx !== actionIndex
1573
- );
1633
+ // Copy localizations from the original action to the new one
1634
+ if (isCopy) {
1635
+ const localization = flowDefinition.localization;
1636
+ if (localization) {
1637
+ for (const langCode of Object.keys(localization)) {
1638
+ const entry = localization[langCode]?.[action.uuid];
1639
+ if (entry) {
1640
+ store.getState().updateLocalization(
1641
+ langCode,
1642
+ droppedAction.uuid,
1643
+ JSON.parse(JSON.stringify(entry))
1644
+ );
1645
+ }
1646
+ }
1647
+ }
1648
+ }
1574
1649
 
1575
- // If source node has no actions left, remove it
1576
- if (updatedSourceActions.length === 0) {
1577
- // Fire event to Editor so it can clean up jsPlumb connections properly
1578
- this.fireCustomEvent(CustomEventType.NodeDeleted, {
1579
- uuid: sourceNodeUuid
1580
- });
1581
- } else {
1582
- // Update source node
1583
- const updatedSourceNode = {
1584
- ...sourceNode,
1585
- actions: updatedSourceActions
1586
- };
1587
- getStore()?.getState().updateNode(sourceNodeUuid, updatedSourceNode);
1650
+ if (!isCopy) {
1651
+ // Remove the action from the source node
1652
+ const updatedSourceActions = sourceNode.actions.filter(
1653
+ (_a, idx) => idx !== actionIndex
1654
+ );
1655
+
1656
+ // If source node has no actions left, remove it
1657
+ if (updatedSourceActions.length === 0) {
1658
+ // Fire event to Editor so it can clean up jsPlumb connections properly
1659
+ this.fireCustomEvent(CustomEventType.NodeDeleted, {
1660
+ uuid: sourceNodeUuid
1661
+ });
1662
+ } else {
1663
+ // Update source node
1664
+ const updatedSourceNode = {
1665
+ ...sourceNode,
1666
+ actions: updatedSourceActions
1667
+ };
1668
+ getStore()?.getState().updateNode(sourceNodeUuid, updatedSourceNode);
1669
+ }
1588
1670
  }
1589
1671
 
1590
1672
  // Request update and notify that this node's size changed