@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.
- package/.devcontainer/Dockerfile +0 -9
- package/.devcontainer/devcontainer.json +8 -3
- package/.github/workflows/build.yml +6 -1
- package/.github/workflows/cla.yml +1 -1
- package/.github/workflows/publish.yml +6 -1
- package/CHANGELOG.md +39 -0
- package/dist/locales/es.js +5 -5
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +5 -5
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/locale-codes.js +11 -2
- package/dist/locales/locale-codes.js.map +1 -1
- package/dist/locales/pt.js +5 -5
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +131 -98
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +16 -8
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/flow/CanvasMenu.js +33 -15
- package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +4 -0
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +279 -55
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeTypeSelector.js +13 -11
- package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +1 -1
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_field.js +5 -1
- package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
- package/out-tsc/src/list/RunList.js +2 -1
- package/out-tsc/src/list/RunList.js.map +1 -1
- package/out-tsc/src/list/TicketList.js +2 -1
- package/out-tsc/src/list/TicketList.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +18 -1
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/locales/es.js +5 -5
- package/out-tsc/src/locales/es.js.map +1 -1
- package/out-tsc/src/locales/fr.js +5 -5
- package/out-tsc/src/locales/fr.js.map +1 -1
- package/out-tsc/src/locales/locale-codes.js +11 -2
- package/out-tsc/src/locales/locale-codes.js.map +1 -1
- package/out-tsc/src/locales/pt.js +5 -5
- package/out-tsc/src/locales/pt.js.map +1 -1
- package/out-tsc/src/store/AppState.js +5 -0
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/test/temba-contact-fields.test.js +3 -3
- package/out-tsc/test/temba-contact-fields.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor-node.test.js +2 -1
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/out-tsc/test/temba-select.test.js +1 -0
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/floating-tab/gray.png +0 -0
- package/screenshots/truth/floating-tab/green.png +0 -0
- package/screenshots/truth/floating-tab/purple.png +0 -0
- package/screenshots/truth/node-type-selector/action-mode.png +0 -0
- package/screenshots/truth/node-type-selector/split-mode.png +0 -0
- package/src/display/FloatingTab.ts +18 -8
- package/src/flow/CanvasMenu.ts +38 -16
- package/src/flow/CanvasNode.ts +8 -0
- package/src/flow/Editor.ts +343 -58
- package/src/flow/NodeTypeSelector.ts +13 -11
- package/src/flow/Plumber.ts +1 -1
- package/src/flow/actions/set_contact_field.ts +5 -1
- package/src/list/RunList.ts +2 -1
- package/src/list/TicketList.ts +2 -1
- package/src/live/ContactChat.ts +19 -1
- package/src/locales/es.ts +18 -13
- package/src/locales/fr.ts +18 -13
- package/src/locales/locale-codes.ts +11 -2
- package/src/locales/pt.ts +18 -13
- package/src/store/AppState.ts +5 -0
- package/test/temba-contact-fields.test.ts +8 -3
- package/test/temba-flow-editor-node.test.ts +2 -1
- package/test/temba-select.test.ts +1 -0
package/src/flow/Editor.ts
CHANGED
|
@@ -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', (
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
.
|
|
842
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
857
|
-
|
|
858
|
-
this.
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
866
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1314
|
-
uuids.
|
|
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
|
-
|
|
1976
|
-
|
|
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
|
-
|
|
1986
|
-
|
|
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(
|
|
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([
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
)}
|
package/src/flow/Plumber.ts
CHANGED
|
@@ -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 (!
|
|
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) => ({
|
|
32
|
+
createArbitraryOption: (input: string) => ({
|
|
33
|
+
key: input,
|
|
34
|
+
name: input,
|
|
35
|
+
type: 'text'
|
|
36
|
+
})
|
|
33
37
|
},
|
|
34
38
|
value: {
|
|
35
39
|
type: 'text',
|
package/src/list/RunList.ts
CHANGED
|
@@ -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
|
-
|
|
182
|
+
const separator = this.endpoint.includes('?') ? '&' : '?';
|
|
183
|
+
return this.endpoint + separator + 'after=' + modifiedOn;
|
|
183
184
|
}
|
|
184
185
|
return this.endpoint;
|
|
185
186
|
}
|
package/src/list/TicketList.ts
CHANGED
|
@@ -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 + '
|
|
16
|
+
this.endpoint + separator + 'after=' + new Date(lastActivity).getTime() * 1000
|
|
16
17
|
);
|
|
17
18
|
}
|
|
18
19
|
return this.endpoint;
|