@nyaruka/temba-components 0.137.0 → 0.138.4
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 +42 -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 +445 -278
- 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 +49 -24
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +583 -70
- 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 +110 -64
- 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/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/simulator/Simulator.js +11 -4
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/src/store/AppState.js +17 -2
- 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 +3 -1
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor-revisions.test.js +106 -0
- package/out-tsc/test/temba-flow-editor-revisions.test.js.map +1 -0
- package/out-tsc/test/temba-flow-editor.test.js +14 -10
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js +7 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +6 -0
- package/out-tsc/test/temba-flow-plumber.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 +62 -29
- package/src/flow/Editor.ts +699 -74
- package/src/flow/NodeTypeSelector.ts +13 -11
- package/src/flow/Plumber.ts +123 -69
- 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/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/simulator/Simulator.ts +11 -5
- package/src/store/AppState.ts +18 -2
- package/test/temba-contact-fields.test.ts +8 -3
- package/test/temba-flow-editor-node.test.ts +3 -1
- package/test/temba-flow-editor-revisions.test.ts +134 -0
- package/test/temba-flow-editor.test.ts +16 -10
- package/test/temba-flow-plumber-connections.test.ts +7 -1
- package/test/temba-flow-plumber.test.ts +6 -0
- package/test/temba-select.test.ts +1 -0
package/src/flow/Editor.ts
CHANGED
|
@@ -9,12 +9,31 @@ import {
|
|
|
9
9
|
NodeUI
|
|
10
10
|
} from '../store/flow-definition';
|
|
11
11
|
import { getStore } from '../store/Store';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
AppState,
|
|
14
|
+
fromStore,
|
|
15
|
+
zustand,
|
|
16
|
+
FLOW_SPEC_VERSION
|
|
17
|
+
} from '../store/AppState';
|
|
13
18
|
import { RapidElement } from '../RapidElement';
|
|
14
19
|
import { repeat } from 'lit-html/directives/repeat.js';
|
|
15
20
|
import { CustomEventType, Workspace } from '../interfaces';
|
|
16
|
-
import { generateUUID, postJSON } from '../utils';
|
|
21
|
+
import { generateUUID, postJSON, fetchResults, getClasses } from '../utils';
|
|
17
22
|
import { ACTION_CONFIG, NODE_CONFIG } from './config';
|
|
23
|
+
|
|
24
|
+
interface Revision {
|
|
25
|
+
id: number;
|
|
26
|
+
user: {
|
|
27
|
+
id: number;
|
|
28
|
+
username: string;
|
|
29
|
+
first_name: string;
|
|
30
|
+
last_name: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
};
|
|
33
|
+
created_on: string;
|
|
34
|
+
comment?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
18
37
|
import { ACTION_GROUP_METADATA } from './types';
|
|
19
38
|
import { Checkbox } from '../form/Checkbox';
|
|
20
39
|
|
|
@@ -30,6 +49,7 @@ import {
|
|
|
30
49
|
NodeBounds,
|
|
31
50
|
nodesOverlap
|
|
32
51
|
} from './utils';
|
|
52
|
+
import { FloatingWindow } from '../layout/FloatingWindow';
|
|
33
53
|
|
|
34
54
|
export function snapToGrid(value: number): number {
|
|
35
55
|
const snapped = Math.round(value / 20) * 20;
|
|
@@ -186,6 +206,9 @@ export class Editor extends RapidElement {
|
|
|
186
206
|
@state()
|
|
187
207
|
private dragFromNodeId: string | null = null;
|
|
188
208
|
|
|
209
|
+
@state()
|
|
210
|
+
private originalConnectionTargetId: string | null = null;
|
|
211
|
+
|
|
189
212
|
@state()
|
|
190
213
|
private isValidTarget = true;
|
|
191
214
|
|
|
@@ -212,6 +235,23 @@ export class Editor extends RapidElement {
|
|
|
212
235
|
@state()
|
|
213
236
|
private autoTranslateError: string | null = null;
|
|
214
237
|
|
|
238
|
+
@state()
|
|
239
|
+
private revisionsWindowHidden = true;
|
|
240
|
+
|
|
241
|
+
@state()
|
|
242
|
+
private revisions: Revision[] = [];
|
|
243
|
+
|
|
244
|
+
@state()
|
|
245
|
+
private viewingRevision: Revision | null = null;
|
|
246
|
+
|
|
247
|
+
@state()
|
|
248
|
+
private isLoadingRevisions = false;
|
|
249
|
+
|
|
250
|
+
private preRevertState: {
|
|
251
|
+
definition: FlowDefinition;
|
|
252
|
+
dirtyDate: Date | null;
|
|
253
|
+
} | null = null;
|
|
254
|
+
|
|
215
255
|
private translationCache = new Map<string, string>();
|
|
216
256
|
|
|
217
257
|
// NodeEditor state - handles both node and action editing
|
|
@@ -249,6 +289,20 @@ export class Editor extends RapidElement {
|
|
|
249
289
|
// Track previous target node to clear placeholder when moving between nodes
|
|
250
290
|
private previousActionDragTargetNodeUuid: string | null = null;
|
|
251
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
|
+
|
|
252
306
|
private canvasMouseDown = false;
|
|
253
307
|
|
|
254
308
|
private getAvailableLanguages(): Array<{ code: string; name: string }> {
|
|
@@ -329,6 +383,29 @@ export class Editor extends RapidElement {
|
|
|
329
383
|
transition: none !important;
|
|
330
384
|
}
|
|
331
385
|
|
|
386
|
+
#canvas.viewing-revision {
|
|
387
|
+
pointer-events: none;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
#canvas.read-only svg {
|
|
391
|
+
pointer-events: none;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
#grid.viewing-revision {
|
|
395
|
+
background-color: #fff9fc;
|
|
396
|
+
background-image: radial-gradient(
|
|
397
|
+
circle,
|
|
398
|
+
rgba(166, 38, 164, 0.2) 1px,
|
|
399
|
+
transparent 1px
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#grid.viewing-revision temba-flow-node,
|
|
404
|
+
#grid.viewing-revision svg.jtk-connector,
|
|
405
|
+
#grid.viewing-revision .activity-overlay {
|
|
406
|
+
opacity: 0.5;
|
|
407
|
+
}
|
|
408
|
+
|
|
332
409
|
body .jtk-endpoint {
|
|
333
410
|
width: initial;
|
|
334
411
|
height: initial;
|
|
@@ -383,12 +460,28 @@ export class Editor extends RapidElement {
|
|
|
383
460
|
stroke-width: 3px;
|
|
384
461
|
}
|
|
385
462
|
|
|
463
|
+
body #canvas.read-only-connections svg.jtk-connector.jtk-hover path {
|
|
464
|
+
stroke: var(--color-connectors) !important;
|
|
465
|
+
}
|
|
466
|
+
|
|
386
467
|
body .plumb-connector.jtk-hover .plumb-arrow {
|
|
387
468
|
fill: var(--color-success) !important;
|
|
388
469
|
stroke-width: 0px;
|
|
389
470
|
z-index: 10;
|
|
390
471
|
}
|
|
391
472
|
|
|
473
|
+
body
|
|
474
|
+
#canvas.read-only-connections
|
|
475
|
+
.plumb-connector.jtk-hover
|
|
476
|
+
.plumb-arrow {
|
|
477
|
+
fill: var(--color-connectors) !important;
|
|
478
|
+
ponter-events: none;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
body #canvas.read-only-connections svg {
|
|
482
|
+
pointer-events: none;
|
|
483
|
+
}
|
|
484
|
+
|
|
392
485
|
/* Activity overlays on connections */
|
|
393
486
|
.jtk-overlay.activity-overlay {
|
|
394
487
|
background: #f3f3f3;
|
|
@@ -677,6 +770,18 @@ export class Editor extends RapidElement {
|
|
|
677
770
|
width: 100%;
|
|
678
771
|
}
|
|
679
772
|
|
|
773
|
+
.revert-button {
|
|
774
|
+
background: var(--color-primary-dark);
|
|
775
|
+
border: none;
|
|
776
|
+
color: #fff;
|
|
777
|
+
padding: 6px 10px;
|
|
778
|
+
border-radius: var(--curvature);
|
|
779
|
+
font-size: 11px;
|
|
780
|
+
font-weight: 600;
|
|
781
|
+
cursor: pointer;
|
|
782
|
+
transition: opacity 0.2s ease;
|
|
783
|
+
}
|
|
784
|
+
|
|
680
785
|
.auto-translate-button {
|
|
681
786
|
background: var(--color-primary-dark);
|
|
682
787
|
border: none;
|
|
@@ -747,36 +852,91 @@ export class Editor extends RapidElement {
|
|
|
747
852
|
getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
|
|
748
853
|
}
|
|
749
854
|
|
|
750
|
-
this.plumber.on('connection:drag', (
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
.
|
|
754
|
-
|
|
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;
|
|
755
862
|
});
|
|
756
863
|
|
|
757
|
-
this.plumber.on('connection:abort', () => {
|
|
758
|
-
|
|
864
|
+
this.plumber.on('connection:abort', (info) => {
|
|
865
|
+
// console.log('Connection aborted', info);
|
|
866
|
+
this.makeConnection(info);
|
|
759
867
|
});
|
|
760
868
|
|
|
761
|
-
this.plumber.on('connection:detach', () => {
|
|
762
|
-
|
|
869
|
+
this.plumber.on('connection:detach', (info) => {
|
|
870
|
+
// console.log('Connection detached', info);
|
|
871
|
+
this.makeConnection(info);
|
|
763
872
|
});
|
|
764
873
|
}
|
|
765
874
|
|
|
766
|
-
private makeConnection() {
|
|
875
|
+
private makeConnection(info) {
|
|
767
876
|
if (this.sourceId && this.targetId && this.isValidTarget) {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
this.
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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();
|
|
776
937
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
}, 100);
|
|
938
|
+
// Don't clear placeholder or connection info yet - keep them for menu interaction
|
|
939
|
+
return;
|
|
780
940
|
}
|
|
781
941
|
|
|
782
942
|
// Clean up visual feedback
|
|
@@ -787,9 +947,12 @@ export class Editor extends RapidElement {
|
|
|
787
947
|
);
|
|
788
948
|
});
|
|
789
949
|
|
|
790
|
-
|
|
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
|
+
}
|
|
791
955
|
this.targetId = null;
|
|
792
|
-
this.dragFromNodeId = null;
|
|
793
956
|
this.isValidTarget = true;
|
|
794
957
|
}
|
|
795
958
|
|
|
@@ -895,10 +1058,11 @@ export class Editor extends RapidElement {
|
|
|
895
1058
|
}, SAVE_QUIET_TIME);
|
|
896
1059
|
}
|
|
897
1060
|
|
|
898
|
-
private saveChanges(): void {
|
|
1061
|
+
private saveChanges(definitionOverride?: FlowDefinition): Promise<void> {
|
|
1062
|
+
const definition = definitionOverride || this.definition;
|
|
899
1063
|
// post the flow definition to the server
|
|
900
|
-
getStore()
|
|
901
|
-
.postJSON(`/flow/revisions/${this.flow}/`,
|
|
1064
|
+
return getStore()
|
|
1065
|
+
.postJSON(`/flow/revisions/${this.flow}/`, definition)
|
|
902
1066
|
.then((response) => {
|
|
903
1067
|
// Update flow info and revision with the response data
|
|
904
1068
|
if (response.json) {
|
|
@@ -911,6 +1075,11 @@ export class Editor extends RapidElement {
|
|
|
911
1075
|
if (response.json.revision?.revision !== undefined) {
|
|
912
1076
|
state.setRevision(response.json.revision.revision);
|
|
913
1077
|
}
|
|
1078
|
+
|
|
1079
|
+
// if the revisions window is open, refresh the list
|
|
1080
|
+
if (!this.revisionsWindowHidden) {
|
|
1081
|
+
this.fetchRevisions();
|
|
1082
|
+
}
|
|
914
1083
|
}
|
|
915
1084
|
})
|
|
916
1085
|
.catch((error) => {
|
|
@@ -961,20 +1130,13 @@ export class Editor extends RapidElement {
|
|
|
961
1130
|
}
|
|
962
1131
|
|
|
963
1132
|
this.activityTimer = window.setTimeout(() => {
|
|
964
|
-
this.fetchActivityData();
|
|
1133
|
+
// this.fetchActivityData();
|
|
965
1134
|
}, this.activityInterval);
|
|
966
1135
|
});
|
|
967
1136
|
}
|
|
968
1137
|
|
|
969
1138
|
private handleLanguageChange(languageCode: string): void {
|
|
970
1139
|
zustand.getState().setLanguageCode(languageCode);
|
|
971
|
-
|
|
972
|
-
// Repaint connections after language change since node sizes can change
|
|
973
|
-
if (this.plumber) {
|
|
974
|
-
requestAnimationFrame(() => {
|
|
975
|
-
this.plumber.repaintEverything();
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
1140
|
}
|
|
979
1141
|
|
|
980
1142
|
disconnectedCallback(): void {
|
|
@@ -1043,6 +1205,16 @@ export class Editor extends RapidElement {
|
|
|
1043
1205
|
}
|
|
1044
1206
|
});
|
|
1045
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
|
+
|
|
1046
1218
|
// Listen for action drag events from nodes
|
|
1047
1219
|
this.addEventListener(
|
|
1048
1220
|
CustomEventType.DragExternal,
|
|
@@ -1073,6 +1245,8 @@ export class Editor extends RapidElement {
|
|
|
1073
1245
|
// ignore right clicks
|
|
1074
1246
|
if (event.button !== 0) return;
|
|
1075
1247
|
|
|
1248
|
+
if (this.isReadOnly()) return;
|
|
1249
|
+
|
|
1076
1250
|
const element = event.currentTarget as HTMLElement;
|
|
1077
1251
|
// Only start dragging if clicking on the element itself, not on exits or other interactive elements
|
|
1078
1252
|
const target = event.target as HTMLElement;
|
|
@@ -1142,6 +1316,8 @@ export class Editor extends RapidElement {
|
|
|
1142
1316
|
}
|
|
1143
1317
|
|
|
1144
1318
|
private handleCanvasMouseDown(event: MouseEvent): void {
|
|
1319
|
+
if (this.isReadOnly()) return;
|
|
1320
|
+
|
|
1145
1321
|
const target = event.target as HTMLElement;
|
|
1146
1322
|
if (target.id === 'canvas' || target.id === 'grid') {
|
|
1147
1323
|
// Ignore clicks on exits
|
|
@@ -1212,13 +1388,8 @@ export class Editor extends RapidElement {
|
|
|
1212
1388
|
}
|
|
1213
1389
|
|
|
1214
1390
|
private deleteNodes(uuids: string[]): void {
|
|
1215
|
-
//
|
|
1216
|
-
uuids.
|
|
1217
|
-
this.plumber.removeNodeConnections(uuid);
|
|
1218
|
-
});
|
|
1219
|
-
|
|
1220
|
-
// Now remove them from the definition
|
|
1221
|
-
if (uuids.length > 0 && this.plumber) {
|
|
1391
|
+
// Remove nodes from the definition - CanvasNode will handle plumber cleanup
|
|
1392
|
+
if (uuids.length > 0) {
|
|
1222
1393
|
getStore().getState().removeNodes(uuids);
|
|
1223
1394
|
}
|
|
1224
1395
|
}
|
|
@@ -1395,6 +1566,111 @@ export class Editor extends RapidElement {
|
|
|
1395
1566
|
</div>`;
|
|
1396
1567
|
}
|
|
1397
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
|
+
|
|
1398
1674
|
/**
|
|
1399
1675
|
* Checks for node collisions and reflows nodes as needed.
|
|
1400
1676
|
* Nodes are only moved downward to resolve collisions.
|
|
@@ -1496,10 +1772,37 @@ export class Editor extends RapidElement {
|
|
|
1496
1772
|
} else {
|
|
1497
1773
|
targetNode.classList.add('connection-target-invalid');
|
|
1498
1774
|
}
|
|
1775
|
+
|
|
1776
|
+
// Hide connection placeholder when over a node
|
|
1777
|
+
this.connectionPlaceholder = null;
|
|
1499
1778
|
} else {
|
|
1500
1779
|
this.targetId = null;
|
|
1501
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
|
+
}
|
|
1502
1802
|
}
|
|
1803
|
+
|
|
1804
|
+
// Force update to show/hide placeholder
|
|
1805
|
+
this.requestUpdate();
|
|
1503
1806
|
}
|
|
1504
1807
|
|
|
1505
1808
|
// Handle item dragging
|
|
@@ -1735,6 +2038,11 @@ export class Editor extends RapidElement {
|
|
|
1735
2038
|
}
|
|
1736
2039
|
|
|
1737
2040
|
private handleCanvasContextMenu(event: MouseEvent): void {
|
|
2041
|
+
if (this.isReadOnly()) {
|
|
2042
|
+
event.preventDefault();
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
1738
2046
|
// Check if we right-clicked on empty canvas space
|
|
1739
2047
|
const target = event.target as HTMLElement;
|
|
1740
2048
|
if (target.id !== 'canvas') {
|
|
@@ -1779,6 +2087,11 @@ export class Editor extends RapidElement {
|
|
|
1779
2087
|
left: selection.position.x,
|
|
1780
2088
|
top: selection.position.y
|
|
1781
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;
|
|
1782
2095
|
} else {
|
|
1783
2096
|
// Show node type selector
|
|
1784
2097
|
const selector = this.querySelector(
|
|
@@ -1787,12 +2100,55 @@ export class Editor extends RapidElement {
|
|
|
1787
2100
|
if (selector) {
|
|
1788
2101
|
selector.show(selection.action, selection.position);
|
|
1789
2102
|
}
|
|
2103
|
+
// Note: we don't clear pendingCanvasConnection or placeholder here,
|
|
2104
|
+
// they will be used in handleNodeTypeSelection
|
|
1790
2105
|
}
|
|
1791
2106
|
}
|
|
1792
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;
|
|
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();
|
|
2140
|
+
}
|
|
2141
|
+
|
|
1793
2142
|
private handleNodeTypeSelection(event: CustomEvent): void {
|
|
1794
2143
|
const selection = event.detail as NodeTypeSelection;
|
|
1795
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
|
+
|
|
1796
2152
|
// Check if we're adding an action to an existing node
|
|
1797
2153
|
if (this.addActionToNodeUuid) {
|
|
1798
2154
|
// Find the existing node
|
|
@@ -1867,20 +2223,24 @@ export class Editor extends RapidElement {
|
|
|
1867
2223
|
}
|
|
1868
2224
|
|
|
1869
2225
|
const tempNodeUI: NodeUI = {
|
|
1870
|
-
position:
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
2226
|
+
position: this.pendingCanvasConnection
|
|
2227
|
+
? this.pendingCanvasConnection.position
|
|
2228
|
+
: {
|
|
2229
|
+
left: selection.position.x,
|
|
2230
|
+
top: selection.position.y
|
|
2231
|
+
},
|
|
1874
2232
|
type: nodeType as any,
|
|
1875
2233
|
config: {}
|
|
1876
2234
|
};
|
|
1877
2235
|
|
|
1878
2236
|
// Mark that we're creating a new node and store the position
|
|
1879
2237
|
this.isCreatingNewNode = true;
|
|
1880
|
-
this.pendingNodePosition =
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
2238
|
+
this.pendingNodePosition = this.pendingCanvasConnection
|
|
2239
|
+
? this.pendingCanvasConnection.position
|
|
2240
|
+
: {
|
|
2241
|
+
left: selection.position.x,
|
|
2242
|
+
top: selection.position.y
|
|
2243
|
+
};
|
|
1884
2244
|
|
|
1885
2245
|
// Open the node editor with the temporary node
|
|
1886
2246
|
this.editingNode = tempNode;
|
|
@@ -1988,6 +2348,23 @@ export class Editor extends RapidElement {
|
|
|
1988
2348
|
// Add the node to the store
|
|
1989
2349
|
store.getState().addNode(updatedNode, nodeUI);
|
|
1990
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
|
+
|
|
1991
2368
|
// Reset the creation flags
|
|
1992
2369
|
this.isCreatingNewNode = false;
|
|
1993
2370
|
this.pendingNodePosition = null;
|
|
@@ -1997,12 +2374,13 @@ export class Editor extends RapidElement {
|
|
|
1997
2374
|
this.checkCollisionsAndReflow([updatedNode.uuid]);
|
|
1998
2375
|
});
|
|
1999
2376
|
} else {
|
|
2377
|
+
const uuid = this.editingNode.uuid;
|
|
2000
2378
|
// Update existing node in the store
|
|
2001
|
-
getStore()?.getState().updateNode(
|
|
2379
|
+
getStore()?.getState().updateNode(uuid, updatedNode);
|
|
2002
2380
|
|
|
2003
2381
|
// Check for collisions and reflow in case node size changed
|
|
2004
2382
|
requestAnimationFrame(() => {
|
|
2005
|
-
this.checkCollisionsAndReflow([
|
|
2383
|
+
this.checkCollisionsAndReflow([uuid]);
|
|
2006
2384
|
});
|
|
2007
2385
|
}
|
|
2008
2386
|
}
|
|
@@ -2017,10 +2395,7 @@ export class Editor extends RapidElement {
|
|
|
2017
2395
|
|
|
2018
2396
|
private handleActionEditCanceled(): void {
|
|
2019
2397
|
// If we were creating a new node, just discard it
|
|
2020
|
-
|
|
2021
|
-
this.isCreatingNewNode = false;
|
|
2022
|
-
this.pendingNodePosition = null;
|
|
2023
|
-
}
|
|
2398
|
+
this.cleanUpConnection();
|
|
2024
2399
|
this.closeNodeEditor();
|
|
2025
2400
|
}
|
|
2026
2401
|
|
|
@@ -2042,6 +2417,23 @@ export class Editor extends RapidElement {
|
|
|
2042
2417
|
// Add the node to the store
|
|
2043
2418
|
store.getState().addNode(updatedNode, nodeUI);
|
|
2044
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
|
+
|
|
2045
2437
|
// Reset the creation flags
|
|
2046
2438
|
this.isCreatingNewNode = false;
|
|
2047
2439
|
this.pendingNodePosition = null;
|
|
@@ -2089,11 +2481,7 @@ export class Editor extends RapidElement {
|
|
|
2089
2481
|
}
|
|
2090
2482
|
|
|
2091
2483
|
private handleNodeEditCanceled(): void {
|
|
2092
|
-
|
|
2093
|
-
if (this.isCreatingNewNode) {
|
|
2094
|
-
this.isCreatingNewNode = false;
|
|
2095
|
-
this.pendingNodePosition = null;
|
|
2096
|
-
}
|
|
2484
|
+
this.cleanUpConnection();
|
|
2097
2485
|
this.closeNodeEditor();
|
|
2098
2486
|
}
|
|
2099
2487
|
|
|
@@ -2468,7 +2856,7 @@ export class Editor extends RapidElement {
|
|
|
2468
2856
|
const bundles: TranslationBundle[] = [];
|
|
2469
2857
|
|
|
2470
2858
|
this.definition.nodes.forEach((node) => {
|
|
2471
|
-
node.actions
|
|
2859
|
+
node.actions?.forEach((action) => {
|
|
2472
2860
|
const config = ACTION_CONFIG[action.type];
|
|
2473
2861
|
if (!config?.localizable || config.localizable.length === 0) {
|
|
2474
2862
|
return;
|
|
@@ -2654,6 +3042,7 @@ export class Editor extends RapidElement {
|
|
|
2654
3042
|
}
|
|
2655
3043
|
|
|
2656
3044
|
this.localizationWindowHidden = false;
|
|
3045
|
+
this.revisionsWindowHidden = true;
|
|
2657
3046
|
|
|
2658
3047
|
const alreadySelected = languages.some(
|
|
2659
3048
|
(lang) => lang.code === this.languageCode
|
|
@@ -2916,6 +3305,223 @@ export class Editor extends RapidElement {
|
|
|
2916
3305
|
this.autoTranslating = false;
|
|
2917
3306
|
}
|
|
2918
3307
|
|
|
3308
|
+
private handleRevisionsTabClick(): void {
|
|
3309
|
+
if (this.revisionsWindowHidden) {
|
|
3310
|
+
this.fetchRevisions();
|
|
3311
|
+
this.revisionsWindowHidden = false;
|
|
3312
|
+
this.localizationWindowHidden = true; // Close other window
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
private handleRevisionsWindowClosed(): void {
|
|
3317
|
+
this.resetRevisionsScroll();
|
|
3318
|
+
this.revisionsWindowHidden = true;
|
|
3319
|
+
if (this.viewingRevision) {
|
|
3320
|
+
this.handleCancelRevisionView();
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
private resetRevisionsScroll() {
|
|
3325
|
+
const list =
|
|
3326
|
+
this.querySelector('#revisions-window').shadowRoot?.querySelector(
|
|
3327
|
+
'.body'
|
|
3328
|
+
);
|
|
3329
|
+
if (list) {
|
|
3330
|
+
list.scrollTop = 0;
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
private async fetchRevisions() {
|
|
3335
|
+
this.isLoadingRevisions = true;
|
|
3336
|
+
try {
|
|
3337
|
+
const results = await fetchResults(
|
|
3338
|
+
`/flow/revisions/${this.flow}/?version=${FLOW_SPEC_VERSION}`
|
|
3339
|
+
);
|
|
3340
|
+
this.revisions = results.slice(1);
|
|
3341
|
+
} catch (e) {
|
|
3342
|
+
console.error('Error fetching revisions', e);
|
|
3343
|
+
} finally {
|
|
3344
|
+
this.isLoadingRevisions = false;
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
private async handleRevisionClick(revision: Revision) {
|
|
3349
|
+
if (this.viewingRevision?.id === revision.id) {
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
if (!this.viewingRevision) {
|
|
3354
|
+
// Save current state first
|
|
3355
|
+
this.preRevertState = {
|
|
3356
|
+
definition: this.definition,
|
|
3357
|
+
dirtyDate: this.dirtyDate
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
this.viewingRevision = revision;
|
|
3362
|
+
this.isLoadingRevisions = true;
|
|
3363
|
+
this.plumber?.reset();
|
|
3364
|
+
|
|
3365
|
+
try {
|
|
3366
|
+
await getStore()
|
|
3367
|
+
.getState()
|
|
3368
|
+
.fetchRevision(`/flow/revisions/${this.flow}`, revision.id.toString());
|
|
3369
|
+
} catch (e) {
|
|
3370
|
+
console.error('Error fetching revision details', e);
|
|
3371
|
+
this.handleCancelRevisionView();
|
|
3372
|
+
} finally {
|
|
3373
|
+
this.isLoadingRevisions = false;
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
private handleCancelRevisionView() {
|
|
3378
|
+
this.plumber?.reset();
|
|
3379
|
+
if (this.preRevertState) {
|
|
3380
|
+
const currentInfo = getStore().getState().flowInfo;
|
|
3381
|
+
getStore().getState().setFlowContents({
|
|
3382
|
+
definition: this.preRevertState.definition,
|
|
3383
|
+
info: currentInfo
|
|
3384
|
+
});
|
|
3385
|
+
if (this.preRevertState.dirtyDate) {
|
|
3386
|
+
getStore().getState().setDirtyDate(this.preRevertState.dirtyDate);
|
|
3387
|
+
}
|
|
3388
|
+
} else {
|
|
3389
|
+
// Fallback if no pre-revert definition
|
|
3390
|
+
getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
this.viewingRevision = null;
|
|
3394
|
+
this.preRevertState = null;
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
private async handleRevertClick() {
|
|
3398
|
+
if (!this.viewingRevision || !this.preRevertState) return;
|
|
3399
|
+
this.plumber?.reset();
|
|
3400
|
+
|
|
3401
|
+
// Use the content of the viewing revision (this.definition)
|
|
3402
|
+
// but the revision number of the current head (preRevertState)
|
|
3403
|
+
// so the server accepts it as a valid update
|
|
3404
|
+
const definitionToSave = {
|
|
3405
|
+
...this.definition,
|
|
3406
|
+
revision: this.preRevertState.definition.revision
|
|
3407
|
+
};
|
|
3408
|
+
|
|
3409
|
+
await this.saveChanges(definitionToSave);
|
|
3410
|
+
this.viewingRevision = null;
|
|
3411
|
+
this.preRevertState = null;
|
|
3412
|
+
this.revisionsWindowHidden = true;
|
|
3413
|
+
|
|
3414
|
+
const revisionsWindow = document.getElementById(
|
|
3415
|
+
'revisions-window'
|
|
3416
|
+
) as FloatingWindow;
|
|
3417
|
+
revisionsWindow.handleClose();
|
|
3418
|
+
|
|
3419
|
+
// Refresh revisions list to show the new one
|
|
3420
|
+
this.fetchRevisions();
|
|
3421
|
+
|
|
3422
|
+
// Fetch the latest version of the flow to ensure the store is up to date
|
|
3423
|
+
getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
private renderRevisionsTab(): TemplateResult | string {
|
|
3427
|
+
return html`
|
|
3428
|
+
<temba-floating-tab
|
|
3429
|
+
id="revisions-tab"
|
|
3430
|
+
icon="revisions"
|
|
3431
|
+
label="Revisions"
|
|
3432
|
+
color="rgb(142, 94, 167)"
|
|
3433
|
+
top="105"
|
|
3434
|
+
.hidden=${!this.revisionsWindowHidden && this.localizationWindowHidden}
|
|
3435
|
+
@temba-button-clicked=${this.handleRevisionsTabClick}
|
|
3436
|
+
></temba-floating-tab>
|
|
3437
|
+
`;
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
private renderRevisionsWindow(): TemplateResult | string {
|
|
3441
|
+
return html`
|
|
3442
|
+
<temba-floating-window
|
|
3443
|
+
id="revisions-window"
|
|
3444
|
+
header="Revisions"
|
|
3445
|
+
.width=${360}
|
|
3446
|
+
.maxHeight=${600}
|
|
3447
|
+
.top=${75}
|
|
3448
|
+
color="rgb(142, 94, 167)"
|
|
3449
|
+
.hidden=${this.revisionsWindowHidden}
|
|
3450
|
+
@temba-dialog-hidden=${this.handleRevisionsWindowClosed}
|
|
3451
|
+
>
|
|
3452
|
+
<div class="localization-window-content">
|
|
3453
|
+
<div
|
|
3454
|
+
class="revisions-list"
|
|
3455
|
+
style="display:flex; flex-direction:column; gap:8px; overflow-y:auto; padding-bottom:10px;"
|
|
3456
|
+
>
|
|
3457
|
+
${this.isLoadingRevisions && !this.revisions.length
|
|
3458
|
+
? html`<temba-loading></temba-loading>`
|
|
3459
|
+
: this.revisions.map((rev) => {
|
|
3460
|
+
const isSelected = this.viewingRevision?.id === rev.id;
|
|
3461
|
+
return html`
|
|
3462
|
+
<div
|
|
3463
|
+
class="revision-item ${isSelected ? 'selected' : ''}"
|
|
3464
|
+
style="padding:8px; border-radius:4px; cursor:pointer; background:${
|
|
3465
|
+
isSelected
|
|
3466
|
+
? '#f0f6ff' // Light blue bg for selected
|
|
3467
|
+
: '#f9fafb'
|
|
3468
|
+
}; border:1px solid ${
|
|
3469
|
+
isSelected ? '#a4cafe' : '#e5e7eb'
|
|
3470
|
+
}; transition: all 0.2s ease;"
|
|
3471
|
+
@click=${() => this.handleRevisionClick(rev)}
|
|
3472
|
+
>
|
|
3473
|
+
<div
|
|
3474
|
+
style="display:flex; justify-content:space-between; align-items:center;"
|
|
3475
|
+
>
|
|
3476
|
+
<div
|
|
3477
|
+
class="revision-header"
|
|
3478
|
+
style="margin-bottom: 2px;"
|
|
3479
|
+
>
|
|
3480
|
+
<div
|
|
3481
|
+
style="font-weight:600; font-size:13px; color:#111827;"
|
|
3482
|
+
>
|
|
3483
|
+
<temba-date value=${
|
|
3484
|
+
rev.created_on
|
|
3485
|
+
} display="duration"></temba-date>
|
|
3486
|
+
|
|
3487
|
+
</div>
|
|
3488
|
+
<div style="font-size:11px; color:#6b7280;">
|
|
3489
|
+
${rev.user.name || rev.user.username}
|
|
3490
|
+
</div>
|
|
3491
|
+
</div>
|
|
3492
|
+
${
|
|
3493
|
+
isSelected
|
|
3494
|
+
? html`<button
|
|
3495
|
+
class="revert-button"
|
|
3496
|
+
@click=${this.handleRevertClick}
|
|
3497
|
+
>
|
|
3498
|
+
Revert
|
|
3499
|
+
</button>`
|
|
3500
|
+
: html``
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
</button>
|
|
3504
|
+
</div>
|
|
3505
|
+
|
|
3506
|
+
${
|
|
3507
|
+
rev.comment
|
|
3508
|
+
? html`<div
|
|
3509
|
+
style="font-size:12px; color:#4b5563; margin-top:4px;"
|
|
3510
|
+
>
|
|
3511
|
+
${rev.comment}
|
|
3512
|
+
</div>`
|
|
3513
|
+
: ''
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
</div>
|
|
3517
|
+
`;
|
|
3518
|
+
})}
|
|
3519
|
+
</div>
|
|
3520
|
+
</div>
|
|
3521
|
+
</temba-floating-window>
|
|
3522
|
+
`;
|
|
3523
|
+
}
|
|
3524
|
+
|
|
2919
3525
|
private renderLocalizationWindow(): TemplateResult | string {
|
|
2920
3526
|
const languages = this.getLocalizationLanguages();
|
|
2921
3527
|
if (!languages.length) {
|
|
@@ -3167,6 +3773,10 @@ export class Editor extends RapidElement {
|
|
|
3167
3773
|
});
|
|
3168
3774
|
}
|
|
3169
3775
|
|
|
3776
|
+
private isReadOnly(): boolean {
|
|
3777
|
+
return this.viewingRevision !== null || this.isTranslating;
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3170
3780
|
public render(): TemplateResult {
|
|
3171
3781
|
// we have to embed our own style since we are in light DOM
|
|
3172
3782
|
const style = html`<style>
|
|
@@ -3176,20 +3786,30 @@ export class Editor extends RapidElement {
|
|
|
3176
3786
|
|
|
3177
3787
|
const stickies = this.definition?._ui?.stickies || {};
|
|
3178
3788
|
|
|
3179
|
-
return html`${style} ${this.
|
|
3180
|
-
${this.renderAutoTranslateDialog()}
|
|
3789
|
+
return html`${style} ${this.renderRevisionsWindow()}
|
|
3790
|
+
${this.renderLocalizationWindow()} ${this.renderAutoTranslateDialog()}
|
|
3181
3791
|
<div id="editor">
|
|
3182
3792
|
<div
|
|
3183
3793
|
id="grid"
|
|
3794
|
+
class="${this.viewingRevision ? 'viewing-revision' : ''}"
|
|
3184
3795
|
style="min-width:100%;width:${this.canvasSize.width}px; height:${this
|
|
3185
3796
|
.canvasSize.height}px"
|
|
3186
3797
|
>
|
|
3187
|
-
<div
|
|
3798
|
+
<div
|
|
3799
|
+
id="canvas"
|
|
3800
|
+
class="${getClasses({
|
|
3801
|
+
'viewing-revision': !!this.viewingRevision,
|
|
3802
|
+
'read-only-connections':
|
|
3803
|
+
!!this.viewingRevision || this.isTranslating
|
|
3804
|
+
})}"
|
|
3805
|
+
>
|
|
3188
3806
|
${this.definition
|
|
3189
3807
|
? repeat(
|
|
3190
|
-
this.definition.nodes,
|
|
3808
|
+
[...this.definition.nodes].sort((a, b) =>
|
|
3809
|
+
a.uuid.localeCompare(b.uuid)
|
|
3810
|
+
),
|
|
3191
3811
|
(node) => node.uuid,
|
|
3192
|
-
(node
|
|
3812
|
+
(node) => {
|
|
3193
3813
|
const position = this.definition._ui?.nodes[node.uuid]
|
|
3194
3814
|
?.position || {
|
|
3195
3815
|
left: 0,
|
|
@@ -3203,7 +3823,9 @@ export class Editor extends RapidElement {
|
|
|
3203
3823
|
const selected = this.selectedItems.has(node.uuid);
|
|
3204
3824
|
|
|
3205
3825
|
// first node is the flow start (nodes are sorted by position)
|
|
3206
|
-
const isFlowStart =
|
|
3826
|
+
const isFlowStart =
|
|
3827
|
+
this.definition.nodes.length > 0 &&
|
|
3828
|
+
this.definition.nodes[0].uuid === node.uuid;
|
|
3207
3829
|
|
|
3208
3830
|
return html`<temba-flow-node
|
|
3209
3831
|
class="draggable ${dragging ? 'dragging' : ''} ${selected
|
|
@@ -3245,6 +3867,7 @@ export class Editor extends RapidElement {
|
|
|
3245
3867
|
}
|
|
3246
3868
|
)}
|
|
3247
3869
|
${this.renderSelectionBox()} ${this.renderCanvasDropPreview()}
|
|
3870
|
+
${this.renderConnectionPlaceholder()}
|
|
3248
3871
|
</div>
|
|
3249
3872
|
</div>
|
|
3250
3873
|
</div>
|
|
@@ -3263,10 +3886,12 @@ export class Editor extends RapidElement {
|
|
|
3263
3886
|
: ''}
|
|
3264
3887
|
|
|
3265
3888
|
<temba-canvas-menu></temba-canvas-menu>
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3889
|
+
${!this.viewingRevision
|
|
3890
|
+
? html`<temba-node-type-selector
|
|
3891
|
+
.flowType=${this.flowType}
|
|
3892
|
+
.features=${this.features}
|
|
3893
|
+
></temba-node-type-selector>`
|
|
3894
|
+
: ''}
|
|
3895
|
+
${this.renderRevisionsTab()} ${this.renderLocalizationTab()} `;
|
|
3271
3896
|
}
|
|
3272
3897
|
}
|