@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
|
@@ -3,11 +3,11 @@ import { html } from 'lit-html';
|
|
|
3
3
|
import { css, unsafeCSS } from 'lit';
|
|
4
4
|
import { property, state } from 'lit/decorators.js';
|
|
5
5
|
import { getStore } from '../store/Store';
|
|
6
|
-
import { fromStore, zustand } from '../store/AppState';
|
|
6
|
+
import { fromStore, zustand, FLOW_SPEC_VERSION } from '../store/AppState';
|
|
7
7
|
import { RapidElement } from '../RapidElement';
|
|
8
8
|
import { repeat } from 'lit-html/directives/repeat.js';
|
|
9
9
|
import { CustomEventType } from '../interfaces';
|
|
10
|
-
import { generateUUID, postJSON } from '../utils';
|
|
10
|
+
import { generateUUID, postJSON, fetchResults, getClasses } from '../utils';
|
|
11
11
|
import { ACTION_CONFIG, NODE_CONFIG } from './config';
|
|
12
12
|
import { ACTION_GROUP_METADATA } from './types';
|
|
13
13
|
import { Plumber } from './Plumber';
|
|
@@ -110,6 +110,29 @@ export class Editor extends RapidElement {
|
|
|
110
110
|
transition: none !important;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
#canvas.viewing-revision {
|
|
114
|
+
pointer-events: none;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#canvas.read-only svg {
|
|
118
|
+
pointer-events: none;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#grid.viewing-revision {
|
|
122
|
+
background-color: #fff9fc;
|
|
123
|
+
background-image: radial-gradient(
|
|
124
|
+
circle,
|
|
125
|
+
rgba(166, 38, 164, 0.2) 1px,
|
|
126
|
+
transparent 1px
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#grid.viewing-revision temba-flow-node,
|
|
131
|
+
#grid.viewing-revision svg.jtk-connector,
|
|
132
|
+
#grid.viewing-revision .activity-overlay {
|
|
133
|
+
opacity: 0.5;
|
|
134
|
+
}
|
|
135
|
+
|
|
113
136
|
body .jtk-endpoint {
|
|
114
137
|
width: initial;
|
|
115
138
|
height: initial;
|
|
@@ -164,12 +187,28 @@ export class Editor extends RapidElement {
|
|
|
164
187
|
stroke-width: 3px;
|
|
165
188
|
}
|
|
166
189
|
|
|
190
|
+
body #canvas.read-only-connections svg.jtk-connector.jtk-hover path {
|
|
191
|
+
stroke: var(--color-connectors) !important;
|
|
192
|
+
}
|
|
193
|
+
|
|
167
194
|
body .plumb-connector.jtk-hover .plumb-arrow {
|
|
168
195
|
fill: var(--color-success) !important;
|
|
169
196
|
stroke-width: 0px;
|
|
170
197
|
z-index: 10;
|
|
171
198
|
}
|
|
172
199
|
|
|
200
|
+
body
|
|
201
|
+
#canvas.read-only-connections
|
|
202
|
+
.plumb-connector.jtk-hover
|
|
203
|
+
.plumb-arrow {
|
|
204
|
+
fill: var(--color-connectors) !important;
|
|
205
|
+
ponter-events: none;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
body #canvas.read-only-connections svg {
|
|
209
|
+
pointer-events: none;
|
|
210
|
+
}
|
|
211
|
+
|
|
173
212
|
/* Activity overlays on connections */
|
|
174
213
|
.jtk-overlay.activity-overlay {
|
|
175
214
|
background: #f3f3f3;
|
|
@@ -458,6 +497,18 @@ export class Editor extends RapidElement {
|
|
|
458
497
|
width: 100%;
|
|
459
498
|
}
|
|
460
499
|
|
|
500
|
+
.revert-button {
|
|
501
|
+
background: var(--color-primary-dark);
|
|
502
|
+
border: none;
|
|
503
|
+
color: #fff;
|
|
504
|
+
padding: 6px 10px;
|
|
505
|
+
border-radius: var(--curvature);
|
|
506
|
+
font-size: 11px;
|
|
507
|
+
font-weight: 600;
|
|
508
|
+
cursor: pointer;
|
|
509
|
+
transition: opacity 0.2s ease;
|
|
510
|
+
}
|
|
511
|
+
|
|
461
512
|
.auto-translate-button {
|
|
462
513
|
background: var(--color-primary-dark);
|
|
463
514
|
border: none;
|
|
@@ -534,6 +585,7 @@ export class Editor extends RapidElement {
|
|
|
534
585
|
this.targetId = null;
|
|
535
586
|
this.sourceId = null;
|
|
536
587
|
this.dragFromNodeId = null;
|
|
588
|
+
this.originalConnectionTargetId = null;
|
|
537
589
|
this.isValidTarget = true;
|
|
538
590
|
this.localizationWindowHidden = true;
|
|
539
591
|
this.translationFilters = {
|
|
@@ -544,6 +596,11 @@ export class Editor extends RapidElement {
|
|
|
544
596
|
this.autoTranslating = false;
|
|
545
597
|
this.autoTranslateModel = null;
|
|
546
598
|
this.autoTranslateError = null;
|
|
599
|
+
this.revisionsWindowHidden = true;
|
|
600
|
+
this.revisions = [];
|
|
601
|
+
this.viewingRevision = null;
|
|
602
|
+
this.isLoadingRevisions = false;
|
|
603
|
+
this.preRevertState = null;
|
|
547
604
|
this.translationCache = new Map();
|
|
548
605
|
// NodeEditor state - handles both node and action editing
|
|
549
606
|
this.editingNode = null;
|
|
@@ -558,6 +615,10 @@ export class Editor extends RapidElement {
|
|
|
558
615
|
this.actionDragTargetNodeUuid = null;
|
|
559
616
|
// Track previous target node to clear placeholder when moving between nodes
|
|
560
617
|
this.previousActionDragTargetNodeUuid = null;
|
|
618
|
+
// Connection placeholder state for dropping connections on empty canvas
|
|
619
|
+
this.connectionPlaceholder = null;
|
|
620
|
+
// Track pending connection when dropping on canvas
|
|
621
|
+
this.pendingCanvasConnection = null;
|
|
561
622
|
this.canvasMouseDown = false;
|
|
562
623
|
// Bound event handlers to maintain proper 'this' context
|
|
563
624
|
this.boundMouseMove = this.handleMouseMove.bind(this);
|
|
@@ -573,36 +634,81 @@ export class Editor extends RapidElement {
|
|
|
573
634
|
if (changes.has('flow')) {
|
|
574
635
|
getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
|
|
575
636
|
}
|
|
576
|
-
this.plumber.on('connection:drag', (
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
.
|
|
580
|
-
|
|
637
|
+
this.plumber.on('connection:drag', (connection) => {
|
|
638
|
+
// console.log('connection:drag', connection);
|
|
639
|
+
this.dragFromNodeId =
|
|
640
|
+
connection.data.nodeId ||
|
|
641
|
+
document.getElementById(connection.sourceId).closest('.node').id;
|
|
642
|
+
this.sourceId = connection.sourceId;
|
|
643
|
+
this.originalConnectionTargetId = connection.target.id;
|
|
581
644
|
});
|
|
582
|
-
this.plumber.on('connection:abort', () => {
|
|
583
|
-
|
|
645
|
+
this.plumber.on('connection:abort', (info) => {
|
|
646
|
+
// console.log('Connection aborted', info);
|
|
647
|
+
this.makeConnection(info);
|
|
584
648
|
});
|
|
585
|
-
this.plumber.on('connection:detach', () => {
|
|
586
|
-
|
|
649
|
+
this.plumber.on('connection:detach', (info) => {
|
|
650
|
+
// console.log('Connection detached', info);
|
|
651
|
+
this.makeConnection(info);
|
|
587
652
|
});
|
|
588
653
|
}
|
|
589
|
-
makeConnection() {
|
|
654
|
+
makeConnection(info) {
|
|
590
655
|
if (this.sourceId && this.targetId && this.isValidTarget) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
.
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
656
|
+
// going to the same target, just put it back
|
|
657
|
+
if (info.target.id === this.targetId) {
|
|
658
|
+
this.plumber.connectIds(this.dragFromNodeId, this.sourceId, this.targetId);
|
|
659
|
+
}
|
|
660
|
+
// otherwise update the connection
|
|
661
|
+
else {
|
|
662
|
+
getStore()
|
|
663
|
+
.getState()
|
|
664
|
+
.updateConnection(this.dragFromNodeId, this.sourceId, this.targetId);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else if (this.connectionPlaceholder &&
|
|
668
|
+
this.connectionPlaceholder.visible &&
|
|
669
|
+
this.sourceId) {
|
|
670
|
+
// Snap the placeholder position to grid
|
|
671
|
+
const snappedPosition = {
|
|
672
|
+
left: snapToGrid(this.connectionPlaceholder.position.left),
|
|
673
|
+
top: snapToGrid(this.connectionPlaceholder.position.top)
|
|
674
|
+
};
|
|
675
|
+
// Update the placeholder to the snapped position
|
|
676
|
+
this.connectionPlaceholder.position = snappedPosition;
|
|
677
|
+
// Store the pending connection info
|
|
678
|
+
this.pendingCanvasConnection = {
|
|
679
|
+
fromNodeId: this.dragFromNodeId,
|
|
680
|
+
exitId: this.sourceId,
|
|
681
|
+
position: snappedPosition
|
|
682
|
+
};
|
|
683
|
+
// Show the context menu just below the placeholder
|
|
684
|
+
const canvas = this.querySelector('#canvas');
|
|
685
|
+
if (canvas) {
|
|
686
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
687
|
+
const menuX = canvasRect.left + snappedPosition.left - 40; // center horizontally
|
|
688
|
+
const menuY = canvasRect.top + snappedPosition.top + 80; // just below placeholder
|
|
689
|
+
const canvasMenu = this.querySelector('temba-canvas-menu');
|
|
690
|
+
if (canvasMenu) {
|
|
691
|
+
canvasMenu.show(menuX, menuY, {
|
|
692
|
+
x: snappedPosition.left,
|
|
693
|
+
y: snappedPosition.top
|
|
694
|
+
}, false); // Don't show sticky note option for connection drops
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// Request update to render the connection line
|
|
698
|
+
this.requestUpdate();
|
|
699
|
+
// Don't clear placeholder or connection info yet - keep them for menu interaction
|
|
700
|
+
return;
|
|
598
701
|
}
|
|
599
702
|
// Clean up visual feedback
|
|
600
703
|
document.querySelectorAll('temba-flow-node').forEach((node) => {
|
|
601
704
|
node.classList.remove('connection-target-valid', 'connection-target-invalid');
|
|
602
705
|
});
|
|
603
|
-
|
|
706
|
+
// Clear connection state (but keep sourceId/dragFromNodeId if we have a pending connection)
|
|
707
|
+
if (!this.pendingCanvasConnection) {
|
|
708
|
+
this.sourceId = null;
|
|
709
|
+
this.dragFromNodeId = null;
|
|
710
|
+
}
|
|
604
711
|
this.targetId = null;
|
|
605
|
-
this.dragFromNodeId = null;
|
|
606
712
|
this.isValidTarget = true;
|
|
607
713
|
}
|
|
608
714
|
updated(changes) {
|
|
@@ -693,10 +799,11 @@ export class Editor extends RapidElement {
|
|
|
693
799
|
}
|
|
694
800
|
}, SAVE_QUIET_TIME);
|
|
695
801
|
}
|
|
696
|
-
saveChanges() {
|
|
802
|
+
saveChanges(definitionOverride) {
|
|
803
|
+
const definition = definitionOverride || this.definition;
|
|
697
804
|
// post the flow definition to the server
|
|
698
|
-
getStore()
|
|
699
|
-
.postJSON(`/flow/revisions/${this.flow}/`,
|
|
805
|
+
return getStore()
|
|
806
|
+
.postJSON(`/flow/revisions/${this.flow}/`, definition)
|
|
700
807
|
.then((response) => {
|
|
701
808
|
var _b;
|
|
702
809
|
// Update flow info and revision with the response data
|
|
@@ -708,6 +815,10 @@ export class Editor extends RapidElement {
|
|
|
708
815
|
if (((_b = response.json.revision) === null || _b === void 0 ? void 0 : _b.revision) !== undefined) {
|
|
709
816
|
state.setRevision(response.json.revision.revision);
|
|
710
817
|
}
|
|
818
|
+
// if the revisions window is open, refresh the list
|
|
819
|
+
if (!this.revisionsWindowHidden) {
|
|
820
|
+
this.fetchRevisions();
|
|
821
|
+
}
|
|
711
822
|
}
|
|
712
823
|
})
|
|
713
824
|
.catch((error) => {
|
|
@@ -751,18 +862,12 @@ export class Editor extends RapidElement {
|
|
|
751
862
|
clearTimeout(this.activityTimer);
|
|
752
863
|
}
|
|
753
864
|
this.activityTimer = window.setTimeout(() => {
|
|
754
|
-
this.fetchActivityData();
|
|
865
|
+
// this.fetchActivityData();
|
|
755
866
|
}, this.activityInterval);
|
|
756
867
|
});
|
|
757
868
|
}
|
|
758
869
|
handleLanguageChange(languageCode) {
|
|
759
870
|
zustand.getState().setLanguageCode(languageCode);
|
|
760
|
-
// Repaint connections after language change since node sizes can change
|
|
761
|
-
if (this.plumber) {
|
|
762
|
-
requestAnimationFrame(() => {
|
|
763
|
-
this.plumber.repaintEverything();
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
871
|
}
|
|
767
872
|
disconnectedCallback() {
|
|
768
873
|
super.disconnectedCallback();
|
|
@@ -810,6 +915,16 @@ export class Editor extends RapidElement {
|
|
|
810
915
|
this.handleNodeTypeSelection(event);
|
|
811
916
|
}
|
|
812
917
|
});
|
|
918
|
+
// Listen for canvas menu cancel (close without selection)
|
|
919
|
+
this.addEventListener(CustomEventType.Canceled, (event) => {
|
|
920
|
+
const target = event.target;
|
|
921
|
+
if (target.tagName === 'TEMBA-CANVAS-MENU') {
|
|
922
|
+
this.handleCanvasMenuClosed();
|
|
923
|
+
}
|
|
924
|
+
else if (target.tagName === 'TEMBA-NODE-TYPE-SELECTOR') {
|
|
925
|
+
this.handleNodeTypeSelectorClosed();
|
|
926
|
+
}
|
|
927
|
+
});
|
|
813
928
|
// Listen for action drag events from nodes
|
|
814
929
|
this.addEventListener(CustomEventType.DragExternal, this.handleActionDragExternal.bind(this));
|
|
815
930
|
this.addEventListener(CustomEventType.DragInternal, this.handleActionDragInternal.bind(this));
|
|
@@ -832,6 +947,8 @@ export class Editor extends RapidElement {
|
|
|
832
947
|
// ignore right clicks
|
|
833
948
|
if (event.button !== 0)
|
|
834
949
|
return;
|
|
950
|
+
if (this.isReadOnly())
|
|
951
|
+
return;
|
|
835
952
|
const element = event.currentTarget;
|
|
836
953
|
// Only start dragging if clicking on the element itself, not on exits or other interactive elements
|
|
837
954
|
const target = event.target;
|
|
@@ -893,6 +1010,8 @@ export class Editor extends RapidElement {
|
|
|
893
1010
|
}
|
|
894
1011
|
handleCanvasMouseDown(event) {
|
|
895
1012
|
var _b;
|
|
1013
|
+
if (this.isReadOnly())
|
|
1014
|
+
return;
|
|
896
1015
|
const target = event.target;
|
|
897
1016
|
if (target.id === 'canvas' || target.id === 'grid') {
|
|
898
1017
|
// Ignore clicks on exits
|
|
@@ -951,12 +1070,8 @@ export class Editor extends RapidElement {
|
|
|
951
1070
|
});
|
|
952
1071
|
}
|
|
953
1072
|
deleteNodes(uuids) {
|
|
954
|
-
//
|
|
955
|
-
uuids.
|
|
956
|
-
this.plumber.removeNodeConnections(uuid);
|
|
957
|
-
});
|
|
958
|
-
// Now remove them from the definition
|
|
959
|
-
if (uuids.length > 0 && this.plumber) {
|
|
1073
|
+
// Remove nodes from the definition - CanvasNode will handle plumber cleanup
|
|
1074
|
+
if (uuids.length > 0) {
|
|
960
1075
|
getStore().getState().removeNodes(uuids);
|
|
961
1076
|
}
|
|
962
1077
|
}
|
|
@@ -1101,6 +1216,94 @@ export class Editor extends RapidElement {
|
|
|
1101
1216
|
</div>
|
|
1102
1217
|
</div>`;
|
|
1103
1218
|
}
|
|
1219
|
+
renderConnectionPlaceholder() {
|
|
1220
|
+
if (!this.connectionPlaceholder || !this.connectionPlaceholder.visible)
|
|
1221
|
+
return '';
|
|
1222
|
+
const { position } = this.connectionPlaceholder;
|
|
1223
|
+
// Render connection line when we have a pending connection (after drop)
|
|
1224
|
+
let svgPath = null;
|
|
1225
|
+
if (this.sourceId && this.dragFromNodeId && this.pendingCanvasConnection) {
|
|
1226
|
+
const sourceElement = document.getElementById(this.sourceId);
|
|
1227
|
+
if (sourceElement) {
|
|
1228
|
+
const sourceRect = sourceElement.getBoundingClientRect();
|
|
1229
|
+
const canvas = this.querySelector('#canvas');
|
|
1230
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
1231
|
+
// Source point (bottom center of exit)
|
|
1232
|
+
const sourceX = sourceRect.left + sourceRect.width / 2 - canvasRect.left;
|
|
1233
|
+
const sourceY = sourceRect.bottom - canvasRect.top;
|
|
1234
|
+
// Target point (top center of placeholder)
|
|
1235
|
+
const targetX = position.left + 100; // 100 is half the placeholder width (200px)
|
|
1236
|
+
const targetY = position.top;
|
|
1237
|
+
// Use jsPlumb FlowchartConnector parameters: stub [20, 10], cornerRadius 5
|
|
1238
|
+
const stubStart = 20;
|
|
1239
|
+
const stubEnd = 10;
|
|
1240
|
+
const cornerRadius = 5;
|
|
1241
|
+
// Calculate flowchart path with corners
|
|
1242
|
+
const verticalStart = sourceY + stubStart;
|
|
1243
|
+
const verticalEnd = targetY - stubEnd;
|
|
1244
|
+
const midY = (verticalStart + verticalEnd) / 2;
|
|
1245
|
+
// Build path with rounded corners (flowchart style)
|
|
1246
|
+
let pathData = `M ${sourceX} ${sourceY} L ${sourceX} ${verticalStart}`;
|
|
1247
|
+
if (sourceX !== targetX) {
|
|
1248
|
+
// Horizontal segment needed
|
|
1249
|
+
if (Math.abs(verticalEnd - verticalStart) > cornerRadius * 2) {
|
|
1250
|
+
// Enough space for corners
|
|
1251
|
+
pathData += ` L ${sourceX} ${midY - cornerRadius}`;
|
|
1252
|
+
pathData += ` Q ${sourceX} ${midY}, ${sourceX + (targetX > sourceX ? cornerRadius : -cornerRadius)} ${midY}`;
|
|
1253
|
+
pathData += ` L ${targetX - (targetX > sourceX ? cornerRadius : -cornerRadius)} ${midY}`;
|
|
1254
|
+
pathData += ` Q ${targetX} ${midY}, ${targetX} ${midY + cornerRadius}`;
|
|
1255
|
+
pathData += ` L ${targetX} ${verticalEnd}`;
|
|
1256
|
+
}
|
|
1257
|
+
else {
|
|
1258
|
+
// Direct horizontal transition
|
|
1259
|
+
pathData += ` L ${targetX} ${verticalStart}`;
|
|
1260
|
+
pathData += ` L ${targetX} ${verticalEnd}`;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
else {
|
|
1264
|
+
// Straight vertical line
|
|
1265
|
+
pathData += ` L ${targetX} ${verticalEnd}`;
|
|
1266
|
+
}
|
|
1267
|
+
pathData += ` L ${targetX} ${targetY}`;
|
|
1268
|
+
svgPath = html `
|
|
1269
|
+
<svg
|
|
1270
|
+
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;"
|
|
1271
|
+
>
|
|
1272
|
+
<path
|
|
1273
|
+
d="${pathData}"
|
|
1274
|
+
fill="none"
|
|
1275
|
+
stroke="var(--color-connectors, #ccc)"
|
|
1276
|
+
stroke-width="3"
|
|
1277
|
+
class="plumb-connector"
|
|
1278
|
+
/>
|
|
1279
|
+
<polygon
|
|
1280
|
+
points="${targetX},${targetY} ${targetX - 6.5},${targetY -
|
|
1281
|
+
13} ${targetX + 6.5},${targetY - 13}"
|
|
1282
|
+
fill="var(--color-connectors, #ccc)"
|
|
1283
|
+
class="plumb-arrow"
|
|
1284
|
+
/>
|
|
1285
|
+
</svg>
|
|
1286
|
+
`;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return html `${svgPath}
|
|
1290
|
+
<div
|
|
1291
|
+
class="connection-placeholder"
|
|
1292
|
+
style="position: absolute; left: ${position.left}px; top: ${position.top}px; opacity: 0.6; pointer-events: none; z-index: 10000;"
|
|
1293
|
+
>
|
|
1294
|
+
<div
|
|
1295
|
+
class="node execute-actions"
|
|
1296
|
+
style="outline: 3px dashed var(--color-primary, #3b82f6); outline-offset: 2px; border-radius: var(--curvature); min-width: 200px;"
|
|
1297
|
+
>
|
|
1298
|
+
<div class="empty-node-placeholder" style="height: 60px;"></div>
|
|
1299
|
+
<div class="action-exits">
|
|
1300
|
+
<div class="exit-wrapper">
|
|
1301
|
+
<div class="exit"></div>
|
|
1302
|
+
</div>
|
|
1303
|
+
</div>
|
|
1304
|
+
</div>
|
|
1305
|
+
</div>`;
|
|
1306
|
+
}
|
|
1104
1307
|
/**
|
|
1105
1308
|
* Checks for node collisions and reflows nodes as needed.
|
|
1106
1309
|
* Nodes are only moved downward to resolve collisions.
|
|
@@ -1180,11 +1383,33 @@ export class Editor extends RapidElement {
|
|
|
1180
1383
|
else {
|
|
1181
1384
|
targetNode.classList.add('connection-target-invalid');
|
|
1182
1385
|
}
|
|
1386
|
+
// Hide connection placeholder when over a node
|
|
1387
|
+
this.connectionPlaceholder = null;
|
|
1183
1388
|
}
|
|
1184
1389
|
else {
|
|
1185
1390
|
this.targetId = null;
|
|
1186
1391
|
this.isValidTarget = true;
|
|
1392
|
+
// Show connection placeholder when over empty canvas
|
|
1393
|
+
// Calculate position: horizontally centered at mouse, vertically just below mouse
|
|
1394
|
+
const canvas = this.querySelector('#canvas');
|
|
1395
|
+
if (canvas) {
|
|
1396
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
1397
|
+
const relativeX = event.clientX - canvasRect.left;
|
|
1398
|
+
const relativeY = event.clientY - canvasRect.top;
|
|
1399
|
+
// offset the placeholder so it's centered horizontally and just below the mouse
|
|
1400
|
+
const placeholderWidth = 200; // approximate node width
|
|
1401
|
+
const placeholderOffset = 20; // distance below mouse cursor
|
|
1402
|
+
this.connectionPlaceholder = {
|
|
1403
|
+
position: {
|
|
1404
|
+
left: relativeX - placeholderWidth / 2,
|
|
1405
|
+
top: relativeY + placeholderOffset
|
|
1406
|
+
},
|
|
1407
|
+
visible: true
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1187
1410
|
}
|
|
1411
|
+
// Force update to show/hide placeholder
|
|
1412
|
+
this.requestUpdate();
|
|
1188
1413
|
}
|
|
1189
1414
|
// Handle item dragging
|
|
1190
1415
|
if (!this.isMouseDown || !this.currentDragItem)
|
|
@@ -1372,6 +1597,10 @@ export class Editor extends RapidElement {
|
|
|
1372
1597
|
store.getState().expandCanvas(maxWidth, maxHeight);
|
|
1373
1598
|
}
|
|
1374
1599
|
handleCanvasContextMenu(event) {
|
|
1600
|
+
if (this.isReadOnly()) {
|
|
1601
|
+
event.preventDefault();
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1375
1604
|
// Check if we right-clicked on empty canvas space
|
|
1376
1605
|
const target = event.target;
|
|
1377
1606
|
if (target.id !== 'canvas') {
|
|
@@ -1409,6 +1638,11 @@ export class Editor extends RapidElement {
|
|
|
1409
1638
|
left: selection.position.x,
|
|
1410
1639
|
top: selection.position.y
|
|
1411
1640
|
});
|
|
1641
|
+
// Clear all pending connection state and placeholder
|
|
1642
|
+
this.pendingCanvasConnection = null;
|
|
1643
|
+
this.connectionPlaceholder = null;
|
|
1644
|
+
this.sourceId = null;
|
|
1645
|
+
this.dragFromNodeId = null;
|
|
1412
1646
|
}
|
|
1413
1647
|
else {
|
|
1414
1648
|
// Show node type selector
|
|
@@ -1416,10 +1650,43 @@ export class Editor extends RapidElement {
|
|
|
1416
1650
|
if (selector) {
|
|
1417
1651
|
selector.show(selection.action, selection.position);
|
|
1418
1652
|
}
|
|
1653
|
+
// Note: we don't clear pendingCanvasConnection or placeholder here,
|
|
1654
|
+
// they will be used in handleNodeTypeSelection
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
cleanUpConnection() {
|
|
1658
|
+
if (this.isCreatingNewNode) {
|
|
1659
|
+
this.isCreatingNewNode = false;
|
|
1660
|
+
this.pendingNodePosition = null;
|
|
1419
1661
|
}
|
|
1662
|
+
// see if we need to put our connection back
|
|
1663
|
+
if (this.originalConnectionTargetId) {
|
|
1664
|
+
this.plumber.connectIds(this.dragFromNodeId, this.sourceId, this.originalConnectionTargetId);
|
|
1665
|
+
this.originalConnectionTargetId = null;
|
|
1666
|
+
}
|
|
1667
|
+
// Menu closed without selection - clear placeholder and pending connection
|
|
1668
|
+
if (this.pendingCanvasConnection) {
|
|
1669
|
+
this.pendingCanvasConnection = null;
|
|
1670
|
+
this.connectionPlaceholder = null;
|
|
1671
|
+
this.sourceId = null;
|
|
1672
|
+
this.dragFromNodeId = null;
|
|
1673
|
+
this.originalConnectionTargetId = null;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
handleCanvasMenuClosed() {
|
|
1677
|
+
this.cleanUpConnection();
|
|
1678
|
+
}
|
|
1679
|
+
handleNodeTypeSelectorClosed() {
|
|
1680
|
+
this.cleanUpConnection();
|
|
1420
1681
|
}
|
|
1421
1682
|
handleNodeTypeSelection(event) {
|
|
1422
1683
|
const selection = event.detail;
|
|
1684
|
+
// Check if we have a pending canvas connection (from dropping on empty canvas)
|
|
1685
|
+
if (this.pendingCanvasConnection) {
|
|
1686
|
+
// Don't clear the placeholder yet - keep it visible while editing
|
|
1687
|
+
// The position is already stored in pendingCanvasConnection
|
|
1688
|
+
// Fall through to normal node creation flow below
|
|
1689
|
+
}
|
|
1423
1690
|
// Check if we're adding an action to an existing node
|
|
1424
1691
|
if (this.addActionToNodeUuid) {
|
|
1425
1692
|
// Find the existing node
|
|
@@ -1480,19 +1747,23 @@ export class Editor extends RapidElement {
|
|
|
1480
1747
|
};
|
|
1481
1748
|
}
|
|
1482
1749
|
const tempNodeUI = {
|
|
1483
|
-
position:
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1750
|
+
position: this.pendingCanvasConnection
|
|
1751
|
+
? this.pendingCanvasConnection.position
|
|
1752
|
+
: {
|
|
1753
|
+
left: selection.position.x,
|
|
1754
|
+
top: selection.position.y
|
|
1755
|
+
},
|
|
1487
1756
|
type: nodeType,
|
|
1488
1757
|
config: {}
|
|
1489
1758
|
};
|
|
1490
1759
|
// Mark that we're creating a new node and store the position
|
|
1491
1760
|
this.isCreatingNewNode = true;
|
|
1492
|
-
this.pendingNodePosition =
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1761
|
+
this.pendingNodePosition = this.pendingCanvasConnection
|
|
1762
|
+
? this.pendingCanvasConnection.position
|
|
1763
|
+
: {
|
|
1764
|
+
left: selection.position.x,
|
|
1765
|
+
top: selection.position.y
|
|
1766
|
+
};
|
|
1496
1767
|
// Open the node editor with the temporary node
|
|
1497
1768
|
this.editingNode = tempNode;
|
|
1498
1769
|
this.editingNodeUI = tempNodeUI;
|
|
@@ -1576,6 +1847,17 @@ export class Editor extends RapidElement {
|
|
|
1576
1847
|
};
|
|
1577
1848
|
// Add the node to the store
|
|
1578
1849
|
store.getState().addNode(updatedNode, nodeUI);
|
|
1850
|
+
// If we have a pending canvas connection, connect it to this new node
|
|
1851
|
+
if (this.pendingCanvasConnection) {
|
|
1852
|
+
store
|
|
1853
|
+
.getState()
|
|
1854
|
+
.updateConnection(this.pendingCanvasConnection.fromNodeId, this.pendingCanvasConnection.exitId, updatedNode.uuid);
|
|
1855
|
+
// Clear the pending connection and placeholder
|
|
1856
|
+
this.pendingCanvasConnection = null;
|
|
1857
|
+
this.connectionPlaceholder = null;
|
|
1858
|
+
this.sourceId = null;
|
|
1859
|
+
this.dragFromNodeId = null;
|
|
1860
|
+
}
|
|
1579
1861
|
// Reset the creation flags
|
|
1580
1862
|
this.isCreatingNewNode = false;
|
|
1581
1863
|
this.pendingNodePosition = null;
|
|
@@ -1585,11 +1867,12 @@ export class Editor extends RapidElement {
|
|
|
1585
1867
|
});
|
|
1586
1868
|
}
|
|
1587
1869
|
else {
|
|
1870
|
+
const uuid = this.editingNode.uuid;
|
|
1588
1871
|
// Update existing node in the store
|
|
1589
|
-
(_c = getStore()) === null || _c === void 0 ? void 0 : _c.getState().updateNode(
|
|
1872
|
+
(_c = getStore()) === null || _c === void 0 ? void 0 : _c.getState().updateNode(uuid, updatedNode);
|
|
1590
1873
|
// Check for collisions and reflow in case node size changed
|
|
1591
1874
|
requestAnimationFrame(() => {
|
|
1592
|
-
this.checkCollisionsAndReflow([
|
|
1875
|
+
this.checkCollisionsAndReflow([uuid]);
|
|
1593
1876
|
});
|
|
1594
1877
|
}
|
|
1595
1878
|
}
|
|
@@ -1602,10 +1885,7 @@ export class Editor extends RapidElement {
|
|
|
1602
1885
|
}
|
|
1603
1886
|
handleActionEditCanceled() {
|
|
1604
1887
|
// If we were creating a new node, just discard it
|
|
1605
|
-
|
|
1606
|
-
this.isCreatingNewNode = false;
|
|
1607
|
-
this.pendingNodePosition = null;
|
|
1608
|
-
}
|
|
1888
|
+
this.cleanUpConnection();
|
|
1609
1889
|
this.closeNodeEditor();
|
|
1610
1890
|
}
|
|
1611
1891
|
handleNodeSaved(updatedNode, uiConfig) {
|
|
@@ -1621,6 +1901,17 @@ export class Editor extends RapidElement {
|
|
|
1621
1901
|
};
|
|
1622
1902
|
// Add the node to the store
|
|
1623
1903
|
store.getState().addNode(updatedNode, nodeUI);
|
|
1904
|
+
// If we have a pending canvas connection, connect it to this new node
|
|
1905
|
+
if (this.pendingCanvasConnection) {
|
|
1906
|
+
store
|
|
1907
|
+
.getState()
|
|
1908
|
+
.updateConnection(this.pendingCanvasConnection.fromNodeId, this.pendingCanvasConnection.exitId, updatedNode.uuid);
|
|
1909
|
+
// Clear the pending connection and placeholder
|
|
1910
|
+
this.pendingCanvasConnection = null;
|
|
1911
|
+
this.connectionPlaceholder = null;
|
|
1912
|
+
this.sourceId = null;
|
|
1913
|
+
this.dragFromNodeId = null;
|
|
1914
|
+
}
|
|
1624
1915
|
// Reset the creation flags
|
|
1625
1916
|
this.isCreatingNewNode = false;
|
|
1626
1917
|
this.pendingNodePosition = null;
|
|
@@ -1658,11 +1949,7 @@ export class Editor extends RapidElement {
|
|
|
1658
1949
|
this.closeNodeEditor();
|
|
1659
1950
|
}
|
|
1660
1951
|
handleNodeEditCanceled() {
|
|
1661
|
-
|
|
1662
|
-
if (this.isCreatingNewNode) {
|
|
1663
|
-
this.isCreatingNewNode = false;
|
|
1664
|
-
this.pendingNodePosition = null;
|
|
1665
|
-
}
|
|
1952
|
+
this.cleanUpConnection();
|
|
1666
1953
|
this.closeNodeEditor();
|
|
1667
1954
|
}
|
|
1668
1955
|
getNodeAtPosition(mouseX, mouseY) {
|
|
@@ -1921,8 +2208,8 @@ export class Editor extends RapidElement {
|
|
|
1921
2208
|
const languageLocalization = this.getLanguageLocalization(languageCode);
|
|
1922
2209
|
const bundles = [];
|
|
1923
2210
|
this.definition.nodes.forEach((node) => {
|
|
1924
|
-
var _b, _c, _d, _e;
|
|
1925
|
-
node.actions.forEach((action) => {
|
|
2211
|
+
var _b, _c, _d, _e, _f;
|
|
2212
|
+
(_b = node.actions) === null || _b === void 0 ? void 0 : _b.forEach((action) => {
|
|
1926
2213
|
const config = ACTION_CONFIG[action.type];
|
|
1927
2214
|
if (!(config === null || config === void 0 ? void 0 : config.localizable) || config.localizable.length === 0) {
|
|
1928
2215
|
return;
|
|
@@ -1944,14 +2231,14 @@ export class Editor extends RapidElement {
|
|
|
1944
2231
|
if (!includeCategories) {
|
|
1945
2232
|
return;
|
|
1946
2233
|
}
|
|
1947
|
-
const nodeUI = (
|
|
2234
|
+
const nodeUI = (_d = (_c = this.definition._ui) === null || _c === void 0 ? void 0 : _c.nodes) === null || _d === void 0 ? void 0 : _d[node.uuid];
|
|
1948
2235
|
const nodeType = nodeUI === null || nodeUI === void 0 ? void 0 : nodeUI.type;
|
|
1949
2236
|
if (!nodeType) {
|
|
1950
2237
|
return;
|
|
1951
2238
|
}
|
|
1952
2239
|
const nodeConfig = NODE_CONFIG[nodeType];
|
|
1953
2240
|
if ((nodeConfig === null || nodeConfig === void 0 ? void 0 : nodeConfig.localizable) === 'categories' &&
|
|
1954
|
-
((
|
|
2241
|
+
((_f = (_e = node.router) === null || _e === void 0 ? void 0 : _e.categories) === null || _f === void 0 ? void 0 : _f.length)) {
|
|
1955
2242
|
const categoryTranslations = node.router.categories.flatMap((category) => this.findTranslations('category', category.uuid, ['name'], category, languageLocalization));
|
|
1956
2243
|
if (categoryTranslations.length > 0) {
|
|
1957
2244
|
bundles.push({
|
|
@@ -2048,6 +2335,7 @@ export class Editor extends RapidElement {
|
|
|
2048
2335
|
return;
|
|
2049
2336
|
}
|
|
2050
2337
|
this.localizationWindowHidden = false;
|
|
2338
|
+
this.revisionsWindowHidden = true;
|
|
2051
2339
|
const alreadySelected = languages.some((lang) => lang.code === this.languageCode);
|
|
2052
2340
|
if (!alreadySelected) {
|
|
2053
2341
|
this.handleLanguageChange(languages[0].code);
|
|
@@ -2255,6 +2543,199 @@ export class Editor extends RapidElement {
|
|
|
2255
2543
|
}
|
|
2256
2544
|
this.autoTranslating = false;
|
|
2257
2545
|
}
|
|
2546
|
+
handleRevisionsTabClick() {
|
|
2547
|
+
if (this.revisionsWindowHidden) {
|
|
2548
|
+
this.fetchRevisions();
|
|
2549
|
+
this.revisionsWindowHidden = false;
|
|
2550
|
+
this.localizationWindowHidden = true; // Close other window
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
handleRevisionsWindowClosed() {
|
|
2554
|
+
this.resetRevisionsScroll();
|
|
2555
|
+
this.revisionsWindowHidden = true;
|
|
2556
|
+
if (this.viewingRevision) {
|
|
2557
|
+
this.handleCancelRevisionView();
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
resetRevisionsScroll() {
|
|
2561
|
+
var _b;
|
|
2562
|
+
const list = (_b = this.querySelector('#revisions-window').shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('.body');
|
|
2563
|
+
if (list) {
|
|
2564
|
+
list.scrollTop = 0;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
async fetchRevisions() {
|
|
2568
|
+
this.isLoadingRevisions = true;
|
|
2569
|
+
try {
|
|
2570
|
+
const results = await fetchResults(`/flow/revisions/${this.flow}/?version=${FLOW_SPEC_VERSION}`);
|
|
2571
|
+
this.revisions = results.slice(1);
|
|
2572
|
+
}
|
|
2573
|
+
catch (e) {
|
|
2574
|
+
console.error('Error fetching revisions', e);
|
|
2575
|
+
}
|
|
2576
|
+
finally {
|
|
2577
|
+
this.isLoadingRevisions = false;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
async handleRevisionClick(revision) {
|
|
2581
|
+
var _b, _c;
|
|
2582
|
+
if (((_b = this.viewingRevision) === null || _b === void 0 ? void 0 : _b.id) === revision.id) {
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
if (!this.viewingRevision) {
|
|
2586
|
+
// Save current state first
|
|
2587
|
+
this.preRevertState = {
|
|
2588
|
+
definition: this.definition,
|
|
2589
|
+
dirtyDate: this.dirtyDate
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
this.viewingRevision = revision;
|
|
2593
|
+
this.isLoadingRevisions = true;
|
|
2594
|
+
(_c = this.plumber) === null || _c === void 0 ? void 0 : _c.reset();
|
|
2595
|
+
try {
|
|
2596
|
+
await getStore()
|
|
2597
|
+
.getState()
|
|
2598
|
+
.fetchRevision(`/flow/revisions/${this.flow}`, revision.id.toString());
|
|
2599
|
+
}
|
|
2600
|
+
catch (e) {
|
|
2601
|
+
console.error('Error fetching revision details', e);
|
|
2602
|
+
this.handleCancelRevisionView();
|
|
2603
|
+
}
|
|
2604
|
+
finally {
|
|
2605
|
+
this.isLoadingRevisions = false;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
handleCancelRevisionView() {
|
|
2609
|
+
var _b;
|
|
2610
|
+
(_b = this.plumber) === null || _b === void 0 ? void 0 : _b.reset();
|
|
2611
|
+
if (this.preRevertState) {
|
|
2612
|
+
const currentInfo = getStore().getState().flowInfo;
|
|
2613
|
+
getStore().getState().setFlowContents({
|
|
2614
|
+
definition: this.preRevertState.definition,
|
|
2615
|
+
info: currentInfo
|
|
2616
|
+
});
|
|
2617
|
+
if (this.preRevertState.dirtyDate) {
|
|
2618
|
+
getStore().getState().setDirtyDate(this.preRevertState.dirtyDate);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
else {
|
|
2622
|
+
// Fallback if no pre-revert definition
|
|
2623
|
+
getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
|
|
2624
|
+
}
|
|
2625
|
+
this.viewingRevision = null;
|
|
2626
|
+
this.preRevertState = null;
|
|
2627
|
+
}
|
|
2628
|
+
async handleRevertClick() {
|
|
2629
|
+
var _b;
|
|
2630
|
+
if (!this.viewingRevision || !this.preRevertState)
|
|
2631
|
+
return;
|
|
2632
|
+
(_b = this.plumber) === null || _b === void 0 ? void 0 : _b.reset();
|
|
2633
|
+
// Use the content of the viewing revision (this.definition)
|
|
2634
|
+
// but the revision number of the current head (preRevertState)
|
|
2635
|
+
// so the server accepts it as a valid update
|
|
2636
|
+
const definitionToSave = {
|
|
2637
|
+
...this.definition,
|
|
2638
|
+
revision: this.preRevertState.definition.revision
|
|
2639
|
+
};
|
|
2640
|
+
await this.saveChanges(definitionToSave);
|
|
2641
|
+
this.viewingRevision = null;
|
|
2642
|
+
this.preRevertState = null;
|
|
2643
|
+
this.revisionsWindowHidden = true;
|
|
2644
|
+
const revisionsWindow = document.getElementById('revisions-window');
|
|
2645
|
+
revisionsWindow.handleClose();
|
|
2646
|
+
// Refresh revisions list to show the new one
|
|
2647
|
+
this.fetchRevisions();
|
|
2648
|
+
// Fetch the latest version of the flow to ensure the store is up to date
|
|
2649
|
+
getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
|
|
2650
|
+
}
|
|
2651
|
+
renderRevisionsTab() {
|
|
2652
|
+
return html `
|
|
2653
|
+
<temba-floating-tab
|
|
2654
|
+
id="revisions-tab"
|
|
2655
|
+
icon="revisions"
|
|
2656
|
+
label="Revisions"
|
|
2657
|
+
color="rgb(142, 94, 167)"
|
|
2658
|
+
top="105"
|
|
2659
|
+
.hidden=${!this.revisionsWindowHidden && this.localizationWindowHidden}
|
|
2660
|
+
@temba-button-clicked=${this.handleRevisionsTabClick}
|
|
2661
|
+
></temba-floating-tab>
|
|
2662
|
+
`;
|
|
2663
|
+
}
|
|
2664
|
+
renderRevisionsWindow() {
|
|
2665
|
+
return html `
|
|
2666
|
+
<temba-floating-window
|
|
2667
|
+
id="revisions-window"
|
|
2668
|
+
header="Revisions"
|
|
2669
|
+
.width=${360}
|
|
2670
|
+
.maxHeight=${600}
|
|
2671
|
+
.top=${75}
|
|
2672
|
+
color="rgb(142, 94, 167)"
|
|
2673
|
+
.hidden=${this.revisionsWindowHidden}
|
|
2674
|
+
@temba-dialog-hidden=${this.handleRevisionsWindowClosed}
|
|
2675
|
+
>
|
|
2676
|
+
<div class="localization-window-content">
|
|
2677
|
+
<div
|
|
2678
|
+
class="revisions-list"
|
|
2679
|
+
style="display:flex; flex-direction:column; gap:8px; overflow-y:auto; padding-bottom:10px;"
|
|
2680
|
+
>
|
|
2681
|
+
${this.isLoadingRevisions && !this.revisions.length
|
|
2682
|
+
? html `<temba-loading></temba-loading>`
|
|
2683
|
+
: this.revisions.map((rev) => {
|
|
2684
|
+
var _b;
|
|
2685
|
+
const isSelected = ((_b = this.viewingRevision) === null || _b === void 0 ? void 0 : _b.id) === rev.id;
|
|
2686
|
+
return html `
|
|
2687
|
+
<div
|
|
2688
|
+
class="revision-item ${isSelected ? 'selected' : ''}"
|
|
2689
|
+
style="padding:8px; border-radius:4px; cursor:pointer; background:${isSelected
|
|
2690
|
+
? '#f0f6ff' // Light blue bg for selected
|
|
2691
|
+
: '#f9fafb'}; border:1px solid ${isSelected ? '#a4cafe' : '#e5e7eb'}; transition: all 0.2s ease;"
|
|
2692
|
+
@click=${() => this.handleRevisionClick(rev)}
|
|
2693
|
+
>
|
|
2694
|
+
<div
|
|
2695
|
+
style="display:flex; justify-content:space-between; align-items:center;"
|
|
2696
|
+
>
|
|
2697
|
+
<div
|
|
2698
|
+
class="revision-header"
|
|
2699
|
+
style="margin-bottom: 2px;"
|
|
2700
|
+
>
|
|
2701
|
+
<div
|
|
2702
|
+
style="font-weight:600; font-size:13px; color:#111827;"
|
|
2703
|
+
>
|
|
2704
|
+
<temba-date value=${rev.created_on} display="duration"></temba-date>
|
|
2705
|
+
|
|
2706
|
+
</div>
|
|
2707
|
+
<div style="font-size:11px; color:#6b7280;">
|
|
2708
|
+
${rev.user.name || rev.user.username}
|
|
2709
|
+
</div>
|
|
2710
|
+
</div>
|
|
2711
|
+
${isSelected
|
|
2712
|
+
? html `<button
|
|
2713
|
+
class="revert-button"
|
|
2714
|
+
@click=${this.handleRevertClick}
|
|
2715
|
+
>
|
|
2716
|
+
Revert
|
|
2717
|
+
</button>`
|
|
2718
|
+
: html ``}
|
|
2719
|
+
|
|
2720
|
+
</button>
|
|
2721
|
+
</div>
|
|
2722
|
+
|
|
2723
|
+
${rev.comment
|
|
2724
|
+
? html `<div
|
|
2725
|
+
style="font-size:12px; color:#4b5563; margin-top:4px;"
|
|
2726
|
+
>
|
|
2727
|
+
${rev.comment}
|
|
2728
|
+
</div>`
|
|
2729
|
+
: ''}
|
|
2730
|
+
|
|
2731
|
+
</div>
|
|
2732
|
+
`;
|
|
2733
|
+
})}
|
|
2734
|
+
</div>
|
|
2735
|
+
</div>
|
|
2736
|
+
</temba-floating-window>
|
|
2737
|
+
`;
|
|
2738
|
+
}
|
|
2258
2739
|
renderLocalizationWindow() {
|
|
2259
2740
|
var _b, _c, _d;
|
|
2260
2741
|
const languages = this.getLocalizationLanguages();
|
|
@@ -2481,6 +2962,9 @@ export class Editor extends RapidElement {
|
|
|
2481
2962
|
behavior: 'smooth'
|
|
2482
2963
|
});
|
|
2483
2964
|
}
|
|
2965
|
+
isReadOnly() {
|
|
2966
|
+
return this.viewingRevision !== null || this.isTranslating;
|
|
2967
|
+
}
|
|
2484
2968
|
render() {
|
|
2485
2969
|
var _b, _c;
|
|
2486
2970
|
// we have to embed our own style since we are in light DOM
|
|
@@ -2489,17 +2973,24 @@ export class Editor extends RapidElement {
|
|
|
2489
2973
|
${unsafeCSS(CanvasNode.styles.cssText)}
|
|
2490
2974
|
</style>`;
|
|
2491
2975
|
const stickies = ((_c = (_b = this.definition) === null || _b === void 0 ? void 0 : _b._ui) === null || _c === void 0 ? void 0 : _c.stickies) || {};
|
|
2492
|
-
return html `${style} ${this.
|
|
2493
|
-
${this.renderAutoTranslateDialog()}
|
|
2976
|
+
return html `${style} ${this.renderRevisionsWindow()}
|
|
2977
|
+
${this.renderLocalizationWindow()} ${this.renderAutoTranslateDialog()}
|
|
2494
2978
|
<div id="editor">
|
|
2495
2979
|
<div
|
|
2496
2980
|
id="grid"
|
|
2981
|
+
class="${this.viewingRevision ? 'viewing-revision' : ''}"
|
|
2497
2982
|
style="min-width:100%;width:${this.canvasSize.width}px; height:${this
|
|
2498
2983
|
.canvasSize.height}px"
|
|
2499
2984
|
>
|
|
2500
|
-
<div
|
|
2985
|
+
<div
|
|
2986
|
+
id="canvas"
|
|
2987
|
+
class="${getClasses({
|
|
2988
|
+
'viewing-revision': !!this.viewingRevision,
|
|
2989
|
+
'read-only-connections': !!this.viewingRevision || this.isTranslating
|
|
2990
|
+
})}"
|
|
2991
|
+
>
|
|
2501
2992
|
${this.definition
|
|
2502
|
-
? repeat(this.definition.nodes, (node) => node.uuid, (node
|
|
2993
|
+
? repeat([...this.definition.nodes].sort((a, b) => a.uuid.localeCompare(b.uuid)), (node) => node.uuid, (node) => {
|
|
2503
2994
|
var _b, _c, _d;
|
|
2504
2995
|
const position = ((_c = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.nodes[node.uuid]) === null || _c === void 0 ? void 0 : _c.position) || {
|
|
2505
2996
|
left: 0,
|
|
@@ -2509,7 +3000,8 @@ export class Editor extends RapidElement {
|
|
|
2509
3000
|
((_d = this.currentDragItem) === null || _d === void 0 ? void 0 : _d.uuid) === node.uuid;
|
|
2510
3001
|
const selected = this.selectedItems.has(node.uuid);
|
|
2511
3002
|
// first node is the flow start (nodes are sorted by position)
|
|
2512
|
-
const isFlowStart =
|
|
3003
|
+
const isFlowStart = this.definition.nodes.length > 0 &&
|
|
3004
|
+
this.definition.nodes[0].uuid === node.uuid;
|
|
2513
3005
|
return html `<temba-flow-node
|
|
2514
3006
|
class="draggable ${dragging ? 'dragging' : ''} ${selected
|
|
2515
3007
|
? 'selected'
|
|
@@ -2545,6 +3037,7 @@ export class Editor extends RapidElement {
|
|
|
2545
3037
|
></temba-sticky-note>`;
|
|
2546
3038
|
})}
|
|
2547
3039
|
${this.renderSelectionBox()} ${this.renderCanvasDropPreview()}
|
|
3040
|
+
${this.renderConnectionPlaceholder()}
|
|
2548
3041
|
</div>
|
|
2549
3042
|
</div>
|
|
2550
3043
|
</div>
|
|
@@ -2561,11 +3054,13 @@ export class Editor extends RapidElement {
|
|
|
2561
3054
|
: ''}
|
|
2562
3055
|
|
|
2563
3056
|
<temba-canvas-menu></temba-canvas-menu>
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
3057
|
+
${!this.viewingRevision
|
|
3058
|
+
? html `<temba-node-type-selector
|
|
3059
|
+
.flowType=${this.flowType}
|
|
3060
|
+
.features=${this.features}
|
|
3061
|
+
></temba-node-type-selector>`
|
|
3062
|
+
: ''}
|
|
3063
|
+
${this.renderRevisionsTab()} ${this.renderLocalizationTab()} `;
|
|
2569
3064
|
}
|
|
2570
3065
|
}
|
|
2571
3066
|
__decorate([
|
|
@@ -2628,6 +3123,9 @@ __decorate([
|
|
|
2628
3123
|
__decorate([
|
|
2629
3124
|
state()
|
|
2630
3125
|
], Editor.prototype, "dragFromNodeId", void 0);
|
|
3126
|
+
__decorate([
|
|
3127
|
+
state()
|
|
3128
|
+
], Editor.prototype, "originalConnectionTargetId", void 0);
|
|
2631
3129
|
__decorate([
|
|
2632
3130
|
state()
|
|
2633
3131
|
], Editor.prototype, "isValidTarget", void 0);
|
|
@@ -2652,6 +3150,18 @@ __decorate([
|
|
|
2652
3150
|
__decorate([
|
|
2653
3151
|
state()
|
|
2654
3152
|
], Editor.prototype, "autoTranslateError", void 0);
|
|
3153
|
+
__decorate([
|
|
3154
|
+
state()
|
|
3155
|
+
], Editor.prototype, "revisionsWindowHidden", void 0);
|
|
3156
|
+
__decorate([
|
|
3157
|
+
state()
|
|
3158
|
+
], Editor.prototype, "revisions", void 0);
|
|
3159
|
+
__decorate([
|
|
3160
|
+
state()
|
|
3161
|
+
], Editor.prototype, "viewingRevision", void 0);
|
|
3162
|
+
__decorate([
|
|
3163
|
+
state()
|
|
3164
|
+
], Editor.prototype, "isLoadingRevisions", void 0);
|
|
2655
3165
|
__decorate([
|
|
2656
3166
|
state()
|
|
2657
3167
|
], Editor.prototype, "editingNode", void 0);
|
|
@@ -2676,4 +3186,7 @@ __decorate([
|
|
|
2676
3186
|
__decorate([
|
|
2677
3187
|
state()
|
|
2678
3188
|
], Editor.prototype, "actionDragTargetNodeUuid", void 0);
|
|
3189
|
+
__decorate([
|
|
3190
|
+
state()
|
|
3191
|
+
], Editor.prototype, "connectionPlaceholder", void 0);
|
|
2679
3192
|
//# sourceMappingURL=Editor.js.map
|