@nyaruka/temba-components 0.138.0 → 0.138.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.
Files changed (76) hide show
  1. package/.devcontainer/Dockerfile +0 -9
  2. package/.devcontainer/devcontainer.json +8 -3
  3. package/.github/workflows/build.yml +6 -1
  4. package/.github/workflows/cla.yml +1 -1
  5. package/.github/workflows/publish.yml +6 -1
  6. package/CHANGELOG.md +39 -0
  7. package/dist/locales/es.js +5 -5
  8. package/dist/locales/es.js.map +1 -1
  9. package/dist/locales/fr.js +5 -5
  10. package/dist/locales/fr.js.map +1 -1
  11. package/dist/locales/locale-codes.js +11 -2
  12. package/dist/locales/locale-codes.js.map +1 -1
  13. package/dist/locales/pt.js +5 -5
  14. package/dist/locales/pt.js.map +1 -1
  15. package/dist/temba-components.js +131 -98
  16. package/dist/temba-components.js.map +1 -1
  17. package/out-tsc/src/display/FloatingTab.js +16 -8
  18. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  19. package/out-tsc/src/flow/CanvasMenu.js +33 -15
  20. package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
  21. package/out-tsc/src/flow/CanvasNode.js +4 -0
  22. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  23. package/out-tsc/src/flow/Editor.js +279 -55
  24. package/out-tsc/src/flow/Editor.js.map +1 -1
  25. package/out-tsc/src/flow/NodeTypeSelector.js +13 -11
  26. package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
  27. package/out-tsc/src/flow/Plumber.js +1 -1
  28. package/out-tsc/src/flow/Plumber.js.map +1 -1
  29. package/out-tsc/src/flow/actions/set_contact_field.js +5 -1
  30. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  31. package/out-tsc/src/list/RunList.js +2 -1
  32. package/out-tsc/src/list/RunList.js.map +1 -1
  33. package/out-tsc/src/list/TicketList.js +2 -1
  34. package/out-tsc/src/list/TicketList.js.map +1 -1
  35. package/out-tsc/src/live/ContactChat.js +18 -1
  36. package/out-tsc/src/live/ContactChat.js.map +1 -1
  37. package/out-tsc/src/locales/es.js +5 -5
  38. package/out-tsc/src/locales/es.js.map +1 -1
  39. package/out-tsc/src/locales/fr.js +5 -5
  40. package/out-tsc/src/locales/fr.js.map +1 -1
  41. package/out-tsc/src/locales/locale-codes.js +11 -2
  42. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  43. package/out-tsc/src/locales/pt.js +5 -5
  44. package/out-tsc/src/locales/pt.js.map +1 -1
  45. package/out-tsc/src/store/AppState.js +5 -0
  46. package/out-tsc/src/store/AppState.js.map +1 -1
  47. package/out-tsc/test/temba-contact-fields.test.js +3 -3
  48. package/out-tsc/test/temba-contact-fields.test.js.map +1 -1
  49. package/out-tsc/test/temba-flow-editor-node.test.js +2 -1
  50. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  51. package/out-tsc/test/temba-select.test.js +1 -0
  52. package/out-tsc/test/temba-select.test.js.map +1 -1
  53. package/package.json +1 -1
  54. package/screenshots/truth/floating-tab/gray.png +0 -0
  55. package/screenshots/truth/floating-tab/green.png +0 -0
  56. package/screenshots/truth/floating-tab/purple.png +0 -0
  57. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  58. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  59. package/src/display/FloatingTab.ts +18 -8
  60. package/src/flow/CanvasMenu.ts +38 -16
  61. package/src/flow/CanvasNode.ts +8 -0
  62. package/src/flow/Editor.ts +343 -58
  63. package/src/flow/NodeTypeSelector.ts +13 -11
  64. package/src/flow/Plumber.ts +1 -1
  65. package/src/flow/actions/set_contact_field.ts +5 -1
  66. package/src/list/RunList.ts +2 -1
  67. package/src/list/TicketList.ts +2 -1
  68. package/src/live/ContactChat.ts +19 -1
  69. package/src/locales/es.ts +18 -13
  70. package/src/locales/fr.ts +18 -13
  71. package/src/locales/locale-codes.ts +11 -2
  72. package/src/locales/pt.ts +18 -13
  73. package/src/store/AppState.ts +5 -0
  74. package/test/temba-contact-fields.test.ts +8 -3
  75. package/test/temba-flow-editor-node.test.ts +2 -1
  76. package/test/temba-select.test.ts +1 -0
@@ -206,6 +206,9 @@ export class Editor extends RapidElement {
206
206
  @state()
207
207
  private dragFromNodeId: string | null = null;
208
208
 
209
+ @state()
210
+ private originalConnectionTargetId: string | null = null;
211
+
209
212
  @state()
210
213
  private isValidTarget = true;
211
214
 
@@ -286,6 +289,20 @@ export class Editor extends RapidElement {
286
289
  // Track previous target node to clear placeholder when moving between nodes
287
290
  private previousActionDragTargetNodeUuid: string | null = null;
288
291
 
292
+ // Connection placeholder state for dropping connections on empty canvas
293
+ @state()
294
+ private connectionPlaceholder: {
295
+ position: FlowPosition;
296
+ visible: boolean;
297
+ } | null = null;
298
+
299
+ // Track pending connection when dropping on canvas
300
+ private pendingCanvasConnection: {
301
+ fromNodeId: string;
302
+ exitId: string;
303
+ position: FlowPosition;
304
+ } | null = null;
305
+
289
306
  private canvasMouseDown = false;
290
307
 
291
308
  private getAvailableLanguages(): Array<{ code: string; name: string }> {
@@ -835,36 +852,91 @@ export class Editor extends RapidElement {
835
852
  getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
836
853
  }
837
854
 
838
- this.plumber.on('connection:drag', (info: Connection) => {
839
- this.dragFromNodeId = document
840
- .getElementById(info.sourceId)
841
- .closest('.node').id;
842
- this.sourceId = info.sourceId;
855
+ this.plumber.on('connection:drag', (connection: Connection) => {
856
+ // console.log('connection:drag', connection);
857
+ this.dragFromNodeId =
858
+ connection.data.nodeId ||
859
+ document.getElementById(connection.sourceId).closest('.node').id;
860
+ this.sourceId = connection.sourceId;
861
+ this.originalConnectionTargetId = connection.target.id;
843
862
  });
844
863
 
845
- this.plumber.on('connection:abort', () => {
846
- this.makeConnection();
864
+ this.plumber.on('connection:abort', (info) => {
865
+ // console.log('Connection aborted', info);
866
+ this.makeConnection(info);
847
867
  });
848
868
 
849
- this.plumber.on('connection:detach', () => {
850
- this.makeConnection();
869
+ this.plumber.on('connection:detach', (info) => {
870
+ // console.log('Connection detached', info);
871
+ this.makeConnection(info);
851
872
  });
852
873
  }
853
874
 
854
- private makeConnection() {
875
+ private makeConnection(info) {
855
876
  if (this.sourceId && this.targetId && this.isValidTarget) {
856
- this.plumber.connectIds(
857
- this.dragFromNodeId,
858
- this.sourceId,
859
- this.targetId
860
- );
861
- getStore()
862
- .getState()
863
- .updateConnection(this.dragFromNodeId, this.sourceId, this.targetId);
877
+ // going to the same target, just put it back
878
+ if (info.target.id === this.targetId) {
879
+ this.plumber.connectIds(
880
+ this.dragFromNodeId,
881
+ this.sourceId,
882
+ this.targetId
883
+ );
884
+ }
885
+ // otherwise update the connection
886
+ else {
887
+ getStore()
888
+ .getState()
889
+ .updateConnection(this.dragFromNodeId, this.sourceId, this.targetId);
890
+ }
891
+ } else if (
892
+ this.connectionPlaceholder &&
893
+ this.connectionPlaceholder.visible &&
894
+ this.sourceId
895
+ ) {
896
+ // Snap the placeholder position to grid
897
+ const snappedPosition = {
898
+ left: snapToGrid(this.connectionPlaceholder.position.left),
899
+ top: snapToGrid(this.connectionPlaceholder.position.top)
900
+ };
901
+
902
+ // Update the placeholder to the snapped position
903
+ this.connectionPlaceholder.position = snappedPosition;
904
+
905
+ // Store the pending connection info
906
+ this.pendingCanvasConnection = {
907
+ fromNodeId: this.dragFromNodeId,
908
+ exitId: this.sourceId,
909
+ position: snappedPosition
910
+ };
911
+
912
+ // Show the context menu just below the placeholder
913
+ const canvas = this.querySelector('#canvas');
914
+ if (canvas) {
915
+ const canvasRect = canvas.getBoundingClientRect();
916
+ const menuX = canvasRect.left + snappedPosition.left - 40; // center horizontally
917
+ const menuY = canvasRect.top + snappedPosition.top + 80; // just below placeholder
918
+
919
+ const canvasMenu = this.querySelector(
920
+ 'temba-canvas-menu'
921
+ ) as CanvasMenu;
922
+ if (canvasMenu) {
923
+ canvasMenu.show(
924
+ menuX,
925
+ menuY,
926
+ {
927
+ x: snappedPosition.left,
928
+ y: snappedPosition.top
929
+ },
930
+ false
931
+ ); // Don't show sticky note option for connection drops
932
+ }
933
+ }
934
+
935
+ // Request update to render the connection line
936
+ this.requestUpdate();
864
937
 
865
- setTimeout(() => {
866
- this.plumber.repaintEverything();
867
- }, 100);
938
+ // Don't clear placeholder or connection info yet - keep them for menu interaction
939
+ return;
868
940
  }
869
941
 
870
942
  // Clean up visual feedback
@@ -875,9 +947,12 @@ export class Editor extends RapidElement {
875
947
  );
876
948
  });
877
949
 
878
- this.sourceId = null;
950
+ // Clear connection state (but keep sourceId/dragFromNodeId if we have a pending connection)
951
+ if (!this.pendingCanvasConnection) {
952
+ this.sourceId = null;
953
+ this.dragFromNodeId = null;
954
+ }
879
955
  this.targetId = null;
880
- this.dragFromNodeId = null;
881
956
  this.isValidTarget = true;
882
957
  }
883
958
 
@@ -1062,13 +1137,6 @@ export class Editor extends RapidElement {
1062
1137
 
1063
1138
  private handleLanguageChange(languageCode: string): void {
1064
1139
  zustand.getState().setLanguageCode(languageCode);
1065
-
1066
- // Repaint connections after language change since node sizes can change
1067
- if (this.plumber) {
1068
- requestAnimationFrame(() => {
1069
- this.plumber.repaintEverything();
1070
- });
1071
- }
1072
1140
  }
1073
1141
 
1074
1142
  disconnectedCallback(): void {
@@ -1137,6 +1205,16 @@ export class Editor extends RapidElement {
1137
1205
  }
1138
1206
  });
1139
1207
 
1208
+ // Listen for canvas menu cancel (close without selection)
1209
+ this.addEventListener(CustomEventType.Canceled, (event: CustomEvent) => {
1210
+ const target = event.target as HTMLElement;
1211
+ if (target.tagName === 'TEMBA-CANVAS-MENU') {
1212
+ this.handleCanvasMenuClosed();
1213
+ } else if (target.tagName === 'TEMBA-NODE-TYPE-SELECTOR') {
1214
+ this.handleNodeTypeSelectorClosed();
1215
+ }
1216
+ });
1217
+
1140
1218
  // Listen for action drag events from nodes
1141
1219
  this.addEventListener(
1142
1220
  CustomEventType.DragExternal,
@@ -1310,14 +1388,8 @@ export class Editor extends RapidElement {
1310
1388
  }
1311
1389
 
1312
1390
  private deleteNodes(uuids: string[]): void {
1313
- // Clean up jsPlumb connections for nodes before removing them
1314
- uuids.forEach((uuid) => {
1315
- this.plumber.removeNodeConnections(uuid);
1316
- this.plumber.removeAllEndpoints(uuid);
1317
- });
1318
-
1319
- // Now remove them from the definition
1320
- if (uuids.length > 0 && this.plumber) {
1391
+ // Remove nodes from the definition - CanvasNode will handle plumber cleanup
1392
+ if (uuids.length > 0) {
1321
1393
  getStore().getState().removeNodes(uuids);
1322
1394
  }
1323
1395
  }
@@ -1494,6 +1566,111 @@ export class Editor extends RapidElement {
1494
1566
  </div>`;
1495
1567
  }
1496
1568
 
1569
+ private renderConnectionPlaceholder(): TemplateResult | string {
1570
+ if (!this.connectionPlaceholder || !this.connectionPlaceholder.visible)
1571
+ return '';
1572
+
1573
+ const { position } = this.connectionPlaceholder;
1574
+
1575
+ // Render connection line when we have a pending connection (after drop)
1576
+ let svgPath = null;
1577
+ if (this.sourceId && this.dragFromNodeId && this.pendingCanvasConnection) {
1578
+ const sourceElement = document.getElementById(this.sourceId);
1579
+ if (sourceElement) {
1580
+ const sourceRect = sourceElement.getBoundingClientRect();
1581
+ const canvas = this.querySelector('#canvas');
1582
+ const canvasRect = canvas.getBoundingClientRect();
1583
+
1584
+ // Source point (bottom center of exit)
1585
+ const sourceX =
1586
+ sourceRect.left + sourceRect.width / 2 - canvasRect.left;
1587
+ const sourceY = sourceRect.bottom - canvasRect.top;
1588
+
1589
+ // Target point (top center of placeholder)
1590
+ const targetX = position.left + 100; // 100 is half the placeholder width (200px)
1591
+ const targetY = position.top;
1592
+
1593
+ // Use jsPlumb FlowchartConnector parameters: stub [20, 10], cornerRadius 5
1594
+ const stubStart = 20;
1595
+ const stubEnd = 10;
1596
+ const cornerRadius = 5;
1597
+
1598
+ // Calculate flowchart path with corners
1599
+ const verticalStart = sourceY + stubStart;
1600
+ const verticalEnd = targetY - stubEnd;
1601
+ const midY = (verticalStart + verticalEnd) / 2;
1602
+
1603
+ // Build path with rounded corners (flowchart style)
1604
+ let pathData = `M ${sourceX} ${sourceY} L ${sourceX} ${verticalStart}`;
1605
+
1606
+ if (sourceX !== targetX) {
1607
+ // Horizontal segment needed
1608
+ if (Math.abs(verticalEnd - verticalStart) > cornerRadius * 2) {
1609
+ // Enough space for corners
1610
+ pathData += ` L ${sourceX} ${midY - cornerRadius}`;
1611
+ pathData += ` Q ${sourceX} ${midY}, ${
1612
+ sourceX + (targetX > sourceX ? cornerRadius : -cornerRadius)
1613
+ } ${midY}`;
1614
+ pathData += ` L ${
1615
+ targetX - (targetX > sourceX ? cornerRadius : -cornerRadius)
1616
+ } ${midY}`;
1617
+ pathData += ` Q ${targetX} ${midY}, ${targetX} ${
1618
+ midY + cornerRadius
1619
+ }`;
1620
+ pathData += ` L ${targetX} ${verticalEnd}`;
1621
+ } else {
1622
+ // Direct horizontal transition
1623
+ pathData += ` L ${targetX} ${verticalStart}`;
1624
+ pathData += ` L ${targetX} ${verticalEnd}`;
1625
+ }
1626
+ } else {
1627
+ // Straight vertical line
1628
+ pathData += ` L ${targetX} ${verticalEnd}`;
1629
+ }
1630
+
1631
+ pathData += ` L ${targetX} ${targetY}`;
1632
+
1633
+ svgPath = html`
1634
+ <svg
1635
+ style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;"
1636
+ >
1637
+ <path
1638
+ d="${pathData}"
1639
+ fill="none"
1640
+ stroke="var(--color-connectors, #ccc)"
1641
+ stroke-width="3"
1642
+ class="plumb-connector"
1643
+ />
1644
+ <polygon
1645
+ points="${targetX},${targetY} ${targetX - 6.5},${targetY -
1646
+ 13} ${targetX + 6.5},${targetY - 13}"
1647
+ fill="var(--color-connectors, #ccc)"
1648
+ class="plumb-arrow"
1649
+ />
1650
+ </svg>
1651
+ `;
1652
+ }
1653
+ }
1654
+
1655
+ return html`${svgPath}
1656
+ <div
1657
+ class="connection-placeholder"
1658
+ style="position: absolute; left: ${position.left}px; top: ${position.top}px; opacity: 0.6; pointer-events: none; z-index: 10000;"
1659
+ >
1660
+ <div
1661
+ class="node execute-actions"
1662
+ style="outline: 3px dashed var(--color-primary, #3b82f6); outline-offset: 2px; border-radius: var(--curvature); min-width: 200px;"
1663
+ >
1664
+ <div class="empty-node-placeholder" style="height: 60px;"></div>
1665
+ <div class="action-exits">
1666
+ <div class="exit-wrapper">
1667
+ <div class="exit"></div>
1668
+ </div>
1669
+ </div>
1670
+ </div>
1671
+ </div>`;
1672
+ }
1673
+
1497
1674
  /**
1498
1675
  * Checks for node collisions and reflows nodes as needed.
1499
1676
  * Nodes are only moved downward to resolve collisions.
@@ -1595,10 +1772,37 @@ export class Editor extends RapidElement {
1595
1772
  } else {
1596
1773
  targetNode.classList.add('connection-target-invalid');
1597
1774
  }
1775
+
1776
+ // Hide connection placeholder when over a node
1777
+ this.connectionPlaceholder = null;
1598
1778
  } else {
1599
1779
  this.targetId = null;
1600
1780
  this.isValidTarget = true;
1781
+
1782
+ // Show connection placeholder when over empty canvas
1783
+ // Calculate position: horizontally centered at mouse, vertically just below mouse
1784
+ const canvas = this.querySelector('#canvas');
1785
+ if (canvas) {
1786
+ const canvasRect = canvas.getBoundingClientRect();
1787
+ const relativeX = event.clientX - canvasRect.left;
1788
+ const relativeY = event.clientY - canvasRect.top;
1789
+
1790
+ // offset the placeholder so it's centered horizontally and just below the mouse
1791
+ const placeholderWidth = 200; // approximate node width
1792
+ const placeholderOffset = 20; // distance below mouse cursor
1793
+
1794
+ this.connectionPlaceholder = {
1795
+ position: {
1796
+ left: relativeX - placeholderWidth / 2,
1797
+ top: relativeY + placeholderOffset
1798
+ },
1799
+ visible: true
1800
+ };
1801
+ }
1601
1802
  }
1803
+
1804
+ // Force update to show/hide placeholder
1805
+ this.requestUpdate();
1602
1806
  }
1603
1807
 
1604
1808
  // Handle item dragging
@@ -1883,6 +2087,11 @@ export class Editor extends RapidElement {
1883
2087
  left: selection.position.x,
1884
2088
  top: selection.position.y
1885
2089
  });
2090
+ // Clear all pending connection state and placeholder
2091
+ this.pendingCanvasConnection = null;
2092
+ this.connectionPlaceholder = null;
2093
+ this.sourceId = null;
2094
+ this.dragFromNodeId = null;
1886
2095
  } else {
1887
2096
  // Show node type selector
1888
2097
  const selector = this.querySelector(
@@ -1891,12 +2100,55 @@ export class Editor extends RapidElement {
1891
2100
  if (selector) {
1892
2101
  selector.show(selection.action, selection.position);
1893
2102
  }
2103
+ // Note: we don't clear pendingCanvasConnection or placeholder here,
2104
+ // they will be used in handleNodeTypeSelection
2105
+ }
2106
+ }
2107
+
2108
+ private cleanUpConnection(): void {
2109
+ if (this.isCreatingNewNode) {
2110
+ this.isCreatingNewNode = false;
2111
+ this.pendingNodePosition = null;
2112
+ }
2113
+
2114
+ // see if we need to put our connection back
2115
+ if (this.originalConnectionTargetId) {
2116
+ this.plumber.connectIds(
2117
+ this.dragFromNodeId,
2118
+ this.sourceId,
2119
+ this.originalConnectionTargetId
2120
+ );
2121
+ this.originalConnectionTargetId = null;
1894
2122
  }
2123
+
2124
+ // Menu closed without selection - clear placeholder and pending connection
2125
+ if (this.pendingCanvasConnection) {
2126
+ this.pendingCanvasConnection = null;
2127
+ this.connectionPlaceholder = null;
2128
+ this.sourceId = null;
2129
+ this.dragFromNodeId = null;
2130
+ this.originalConnectionTargetId = null;
2131
+ }
2132
+ }
2133
+
2134
+ private handleCanvasMenuClosed(): void {
2135
+ this.cleanUpConnection();
2136
+ }
2137
+
2138
+ private handleNodeTypeSelectorClosed(): void {
2139
+ this.cleanUpConnection();
1895
2140
  }
1896
2141
 
1897
2142
  private handleNodeTypeSelection(event: CustomEvent): void {
1898
2143
  const selection = event.detail as NodeTypeSelection;
1899
2144
 
2145
+ // Check if we have a pending canvas connection (from dropping on empty canvas)
2146
+ if (this.pendingCanvasConnection) {
2147
+ // Don't clear the placeholder yet - keep it visible while editing
2148
+ // The position is already stored in pendingCanvasConnection
2149
+ // Fall through to normal node creation flow below
2150
+ }
2151
+
1900
2152
  // Check if we're adding an action to an existing node
1901
2153
  if (this.addActionToNodeUuid) {
1902
2154
  // Find the existing node
@@ -1971,20 +2223,24 @@ export class Editor extends RapidElement {
1971
2223
  }
1972
2224
 
1973
2225
  const tempNodeUI: NodeUI = {
1974
- position: {
1975
- left: selection.position.x,
1976
- top: selection.position.y
1977
- },
2226
+ position: this.pendingCanvasConnection
2227
+ ? this.pendingCanvasConnection.position
2228
+ : {
2229
+ left: selection.position.x,
2230
+ top: selection.position.y
2231
+ },
1978
2232
  type: nodeType as any,
1979
2233
  config: {}
1980
2234
  };
1981
2235
 
1982
2236
  // Mark that we're creating a new node and store the position
1983
2237
  this.isCreatingNewNode = true;
1984
- this.pendingNodePosition = {
1985
- left: selection.position.x,
1986
- top: selection.position.y
1987
- };
2238
+ this.pendingNodePosition = this.pendingCanvasConnection
2239
+ ? this.pendingCanvasConnection.position
2240
+ : {
2241
+ left: selection.position.x,
2242
+ top: selection.position.y
2243
+ };
1988
2244
 
1989
2245
  // Open the node editor with the temporary node
1990
2246
  this.editingNode = tempNode;
@@ -2092,6 +2348,23 @@ export class Editor extends RapidElement {
2092
2348
  // Add the node to the store
2093
2349
  store.getState().addNode(updatedNode, nodeUI);
2094
2350
 
2351
+ // If we have a pending canvas connection, connect it to this new node
2352
+ if (this.pendingCanvasConnection) {
2353
+ store
2354
+ .getState()
2355
+ .updateConnection(
2356
+ this.pendingCanvasConnection.fromNodeId,
2357
+ this.pendingCanvasConnection.exitId,
2358
+ updatedNode.uuid
2359
+ );
2360
+
2361
+ // Clear the pending connection and placeholder
2362
+ this.pendingCanvasConnection = null;
2363
+ this.connectionPlaceholder = null;
2364
+ this.sourceId = null;
2365
+ this.dragFromNodeId = null;
2366
+ }
2367
+
2095
2368
  // Reset the creation flags
2096
2369
  this.isCreatingNewNode = false;
2097
2370
  this.pendingNodePosition = null;
@@ -2101,12 +2374,13 @@ export class Editor extends RapidElement {
2101
2374
  this.checkCollisionsAndReflow([updatedNode.uuid]);
2102
2375
  });
2103
2376
  } else {
2377
+ const uuid = this.editingNode.uuid;
2104
2378
  // Update existing node in the store
2105
- getStore()?.getState().updateNode(this.editingNode.uuid, updatedNode);
2379
+ getStore()?.getState().updateNode(uuid, updatedNode);
2106
2380
 
2107
2381
  // Check for collisions and reflow in case node size changed
2108
2382
  requestAnimationFrame(() => {
2109
- this.checkCollisionsAndReflow([this.editingNode.uuid]);
2383
+ this.checkCollisionsAndReflow([uuid]);
2110
2384
  });
2111
2385
  }
2112
2386
  }
@@ -2121,10 +2395,7 @@ export class Editor extends RapidElement {
2121
2395
 
2122
2396
  private handleActionEditCanceled(): void {
2123
2397
  // If we were creating a new node, just discard it
2124
- if (this.isCreatingNewNode) {
2125
- this.isCreatingNewNode = false;
2126
- this.pendingNodePosition = null;
2127
- }
2398
+ this.cleanUpConnection();
2128
2399
  this.closeNodeEditor();
2129
2400
  }
2130
2401
 
@@ -2146,6 +2417,23 @@ export class Editor extends RapidElement {
2146
2417
  // Add the node to the store
2147
2418
  store.getState().addNode(updatedNode, nodeUI);
2148
2419
 
2420
+ // If we have a pending canvas connection, connect it to this new node
2421
+ if (this.pendingCanvasConnection) {
2422
+ store
2423
+ .getState()
2424
+ .updateConnection(
2425
+ this.pendingCanvasConnection.fromNodeId,
2426
+ this.pendingCanvasConnection.exitId,
2427
+ updatedNode.uuid
2428
+ );
2429
+
2430
+ // Clear the pending connection and placeholder
2431
+ this.pendingCanvasConnection = null;
2432
+ this.connectionPlaceholder = null;
2433
+ this.sourceId = null;
2434
+ this.dragFromNodeId = null;
2435
+ }
2436
+
2149
2437
  // Reset the creation flags
2150
2438
  this.isCreatingNewNode = false;
2151
2439
  this.pendingNodePosition = null;
@@ -2193,11 +2481,7 @@ export class Editor extends RapidElement {
2193
2481
  }
2194
2482
 
2195
2483
  private handleNodeEditCanceled(): void {
2196
- // If we were creating a new node, just discard it
2197
- if (this.isCreatingNewNode) {
2198
- this.isCreatingNewNode = false;
2199
- this.pendingNodePosition = null;
2200
- }
2484
+ this.cleanUpConnection();
2201
2485
  this.closeNodeEditor();
2202
2486
  }
2203
2487
 
@@ -2572,7 +2856,7 @@ export class Editor extends RapidElement {
2572
2856
  const bundles: TranslationBundle[] = [];
2573
2857
 
2574
2858
  this.definition.nodes.forEach((node) => {
2575
- node.actions.forEach((action) => {
2859
+ node.actions?.forEach((action) => {
2576
2860
  const config = ACTION_CONFIG[action.type];
2577
2861
  if (!config?.localizable || config.localizable.length === 0) {
2578
2862
  return;
@@ -3583,6 +3867,7 @@ export class Editor extends RapidElement {
3583
3867
  }
3584
3868
  )}
3585
3869
  ${this.renderSelectionBox()} ${this.renderCanvasDropPreview()}
3870
+ ${this.renderConnectionPlaceholder()}
3586
3871
  </div>
3587
3872
  </div>
3588
3873
  </div>
@@ -172,12 +172,13 @@ export class NodeTypeSelector extends RapidElement {
172
172
 
173
173
  .items-grid {
174
174
  display: grid;
175
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
175
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
176
176
  gap: 0.75em;
177
177
  }
178
178
 
179
179
  .node-item {
180
- padding: 1em;
180
+ padding: 0.5em;
181
+ padding-left: 1em;
181
182
  border: 1px solid rgba(0, 0, 0, 0.1);
182
183
  border-radius: calc(var(--curvature) * 0.75);
183
184
  cursor: pointer;
@@ -211,7 +212,6 @@ export class NodeTypeSelector extends RapidElement {
211
212
  font-weight: 500;
212
213
  font-size: 1rem;
213
214
  color: var(--color-text-dark);
214
- margin-bottom: 0.25em;
215
215
  }
216
216
 
217
217
  .node-item-type {
@@ -259,8 +259,14 @@ export class NodeTypeSelector extends RapidElement {
259
259
  this.open = true;
260
260
  }
261
261
 
262
- public close() {
263
- this.open = false;
262
+ public close(fireCanceledEvent: boolean = true) {
263
+ if (this.open) {
264
+ this.open = false;
265
+ // Fire canceled event so parent can clean up, but only if not from a selection
266
+ if (fireCanceledEvent) {
267
+ this.fireCustomEvent(CustomEventType.Canceled, {});
268
+ }
269
+ }
264
270
  }
265
271
 
266
272
  /**
@@ -297,7 +303,8 @@ export class NodeTypeSelector extends RapidElement {
297
303
  nodeType,
298
304
  position: this.clickPosition
299
305
  } as NodeTypeSelection);
300
- this.close();
306
+ // Close without firing canceled event since we made a selection
307
+ this.close(false);
301
308
  }
302
309
 
303
310
  private handleOverlayClick() {
@@ -549,7 +556,6 @@ export class NodeTypeSelector extends RapidElement {
549
556
  <div class="node-item-title">
550
557
  ${item.config.name}
551
558
  </div>
552
- <div class="node-item-type">${item.type}</div>
553
559
  </div>
554
560
  `
555
561
  )}
@@ -587,9 +593,6 @@ export class NodeTypeSelector extends RapidElement {
587
593
  <div class="node-item-title">
588
594
  ${item.config.name}
589
595
  </div>
590
- <div class="node-item-type">
591
- ${item.type}
592
- </div>
593
596
  </div>
594
597
  `
595
598
  )}
@@ -622,7 +625,6 @@ export class NodeTypeSelector extends RapidElement {
622
625
  <div class="node-item-title">
623
626
  ${item.config.name}
624
627
  </div>
625
- <div class="node-item-type">${item.type}</div>
626
628
  </div>
627
629
  `
628
630
  )}
@@ -208,7 +208,7 @@ export class Plumber {
208
208
  // each connection needs its own target endpoint
209
209
  const targetEndpoint = this.makeTarget(toId);
210
210
 
211
- if (!sourceEndpoint || !targetEndpoint) {
211
+ if (!source || !targetEndpoint) {
212
212
  console.warn(
213
213
  `Plumber: Cannot connect ${fromId} to ${toId}. Element(s) missing.`
214
214
  );
@@ -29,7 +29,11 @@ export const set_contact_field: ActionConfig = {
29
29
  endpoint: '/api/v2/fields.json',
30
30
  helpText: 'Select the contact field to update',
31
31
  allowCreate: true,
32
- createArbitraryOption: (input: string) => ({ key: input, name: input })
32
+ createArbitraryOption: (input: string) => ({
33
+ key: input,
34
+ name: input,
35
+ type: 'text'
36
+ })
33
37
  },
34
38
  value: {
35
39
  type: 'text',
@@ -179,7 +179,8 @@ export class RunList extends TembaList {
179
179
  public getRefreshEndpoint() {
180
180
  if (this.items.length > 0) {
181
181
  const modifiedOn = this.items[0].modified_on;
182
- return this.endpoint + '&after=' + modifiedOn;
182
+ const separator = this.endpoint.includes('?') ? '&' : '?';
183
+ return this.endpoint + separator + 'after=' + modifiedOn;
183
184
  }
184
185
  return this.endpoint;
185
186
  }
@@ -11,8 +11,9 @@ export class TicketList extends TembaList {
11
11
  public getRefreshEndpoint() {
12
12
  if (this.items.length > 0) {
13
13
  const lastActivity = this.items[0].ticket.last_activity_on;
14
+ const separator = this.endpoint.includes('?') ? '&' : '?';
14
15
  return (
15
- this.endpoint + '?after=' + new Date(lastActivity).getTime() * 1000
16
+ this.endpoint + separator + 'after=' + new Date(lastActivity).getTime() * 1000
16
17
  );
17
18
  }
18
19
  return this.endpoint;