@nyaruka/temba-components 0.137.0 → 0.138.0
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/CHANGELOG.md +9 -0
- package/dist/temba-components.js +392 -258
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +2 -2
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +45 -24
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +305 -16
- package/out-tsc/src/flow/Editor.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/simulator/Simulator.js +11 -4
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/src/store/AppState.js +12 -2
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/test/temba-flow-editor-node.test.js +2 -1
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/out-tsc/test/temba-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/package.json +1 -1
- package/src/display/FloatingTab.ts +2 -2
- package/src/flow/CanvasNode.ts +54 -29
- package/src/flow/Editor.ts +357 -17
- package/src/flow/Plumber.ts +123 -69
- package/src/simulator/Simulator.ts +11 -5
- package/src/store/AppState.ts +13 -2
- package/test/temba-flow-editor-node.test.ts +2 -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/src/flow/CanvasNode.ts
CHANGED
|
@@ -33,6 +33,9 @@ export class CanvasNode extends RapidElement {
|
|
|
33
33
|
@fromStore(zustand, (state: AppState) => state.languageCode)
|
|
34
34
|
private languageCode!: string;
|
|
35
35
|
|
|
36
|
+
@fromStore(zustand, (state: AppState) => state.viewingRevision)
|
|
37
|
+
private viewingRevision!: boolean;
|
|
38
|
+
|
|
36
39
|
@fromStore(zustand, (state: AppState) => state.flowDefinition)
|
|
37
40
|
private flowDefinition!: any;
|
|
38
41
|
|
|
@@ -49,6 +52,8 @@ export class CanvasNode extends RapidElement {
|
|
|
49
52
|
// Track exits that are in "removing" state
|
|
50
53
|
private exitRemovalTimeouts: Map<string, number> = new Map();
|
|
51
54
|
|
|
55
|
+
private connectionTimeout: number | null = null;
|
|
56
|
+
|
|
52
57
|
// Set of exit UUIDs that are in the removing state
|
|
53
58
|
private exitRemovingState: Set<string> = new Set();
|
|
54
59
|
|
|
@@ -91,11 +96,8 @@ export class CanvasNode extends RapidElement {
|
|
|
91
96
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
|
92
97
|
min-width: 200px;
|
|
93
98
|
border-radius: var(--curvature);
|
|
94
|
-
|
|
95
99
|
color: #333;
|
|
96
|
-
cursor: move;
|
|
97
100
|
user-select: none;
|
|
98
|
-
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
/* Flow start indicator */
|
|
@@ -178,7 +180,7 @@ export class CanvasNode extends RapidElement {
|
|
|
178
180
|
opacity: 1;
|
|
179
181
|
}
|
|
180
182
|
|
|
181
|
-
.
|
|
183
|
+
.read-only-hidden {
|
|
182
184
|
visibility: hidden !important;
|
|
183
185
|
pointer-events: none !important;
|
|
184
186
|
}
|
|
@@ -373,6 +375,12 @@ export class CanvasNode extends RapidElement {
|
|
|
373
375
|
.exit.connected:hover {
|
|
374
376
|
background-color: var(--color-connectors, #e6e6e6);
|
|
375
377
|
}
|
|
378
|
+
|
|
379
|
+
.exit.connected.read-only, .exit.connected.read-only:hover {
|
|
380
|
+
background-color: #fff;
|
|
381
|
+
pointer-events: none !important;
|
|
382
|
+
cursor: default;
|
|
383
|
+
}
|
|
376
384
|
|
|
377
385
|
.exit.removing, .exit.removing:hover {
|
|
378
386
|
background-color: var(--color-error);
|
|
@@ -530,23 +538,31 @@ export class CanvasNode extends RapidElement {
|
|
|
530
538
|
if (changes.has('node')) {
|
|
531
539
|
// Only proceed if plumber is available (for tests that don't set it up)
|
|
532
540
|
if (this.plumber) {
|
|
533
|
-
|
|
541
|
+
if (this.connectionTimeout) {
|
|
542
|
+
clearTimeout(this.connectionTimeout);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Pass exit IDs explicitly to avoid DOM querying dependency
|
|
546
|
+
const exitIds = this.node.exits.map((e) => e.uuid);
|
|
547
|
+
this.plumber.removeNodeConnections(this.node.uuid, exitIds);
|
|
548
|
+
|
|
534
549
|
// make our initial connections
|
|
535
|
-
for
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
// so make our source endpoint
|
|
550
|
+
// We use setTimeout to allow for DOM updates to complete before querying for exits
|
|
551
|
+
this.connectionTimeout = window.setTimeout(() => {
|
|
552
|
+
for (const exit of this.node.exits) {
|
|
539
553
|
this.plumber.makeSource(exit.uuid);
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
554
|
+
if (exit.destination_uuid) {
|
|
555
|
+
this.plumber.connectIds(
|
|
556
|
+
this.node.uuid,
|
|
557
|
+
exit.uuid,
|
|
558
|
+
exit.destination_uuid
|
|
559
|
+
);
|
|
560
|
+
}
|
|
546
561
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
562
|
+
// Note: revalidation is handled by plumber's processPendingConnections which calls repaintEverything
|
|
563
|
+
this.connectionTimeout = null;
|
|
564
|
+
this.plumber.revalidate([this.node.uuid]);
|
|
565
|
+
}, 0);
|
|
550
566
|
}
|
|
551
567
|
|
|
552
568
|
const ele = this.parentElement;
|
|
@@ -564,6 +580,15 @@ export class CanvasNode extends RapidElement {
|
|
|
564
580
|
}
|
|
565
581
|
|
|
566
582
|
disconnectedCallback() {
|
|
583
|
+
// Force cleanup of connections for this node
|
|
584
|
+
if (this.plumber && this.node) {
|
|
585
|
+
if (this.connectionTimeout) {
|
|
586
|
+
clearTimeout(this.connectionTimeout);
|
|
587
|
+
this.connectionTimeout = null;
|
|
588
|
+
}
|
|
589
|
+
this.plumber.forgetNode(this.node.uuid);
|
|
590
|
+
}
|
|
591
|
+
|
|
567
592
|
// Remove the event listener when the component is removed
|
|
568
593
|
super.disconnectedCallback();
|
|
569
594
|
|
|
@@ -609,7 +634,6 @@ export class CanvasNode extends RapidElement {
|
|
|
609
634
|
private handleExitClick(event: MouseEvent, exit: Exit) {
|
|
610
635
|
event.preventDefault();
|
|
611
636
|
event.stopPropagation();
|
|
612
|
-
|
|
613
637
|
const exitId = exit.uuid;
|
|
614
638
|
|
|
615
639
|
// If exit is not connected, do nothing
|
|
@@ -1282,23 +1306,19 @@ export class CanvasNode extends RapidElement {
|
|
|
1282
1306
|
return html`<div class="cn-title" style="background:${color}">
|
|
1283
1307
|
${this.ui?.type === 'execute_actions'
|
|
1284
1308
|
? html`<temba-icon
|
|
1285
|
-
class="drag-handle ${this.
|
|
1286
|
-
? 'translating-hidden'
|
|
1287
|
-
: ''}"
|
|
1309
|
+
class="drag-handle ${this.isReadOnly() ? 'read-only-hidden' : ''}"
|
|
1288
1310
|
name="sort"
|
|
1289
1311
|
></temba-icon>`
|
|
1290
1312
|
: this.node?.actions?.length > 1
|
|
1291
1313
|
? html`<temba-icon
|
|
1292
|
-
class="drag-handle ${this.
|
|
1293
|
-
? 'translating-hidden'
|
|
1294
|
-
: ''}"
|
|
1314
|
+
class="drag-handle ${this.isReadOnly() ? 'read-only-hidden' : ''}"
|
|
1295
1315
|
name="sort"
|
|
1296
1316
|
></temba-icon>`
|
|
1297
1317
|
: html`<div class="title-spacer"></div>`}
|
|
1298
1318
|
|
|
1299
1319
|
<div class="name">${isRemoving ? 'Remove?' : config.name}</div>
|
|
1300
1320
|
<div
|
|
1301
|
-
class="remove-button ${this.
|
|
1321
|
+
class="remove-button ${this.isReadOnly() ? 'read-only-hidden' : ''}"
|
|
1302
1322
|
@click=${(e: MouseEvent) =>
|
|
1303
1323
|
this.handleActionRemoveClick(e, action, index)}
|
|
1304
1324
|
title="Remove action"
|
|
@@ -1332,7 +1352,7 @@ export class CanvasNode extends RapidElement {
|
|
|
1332
1352
|
: html`${config.name}`}
|
|
1333
1353
|
</div>
|
|
1334
1354
|
<div
|
|
1335
|
-
class="remove-button ${this.
|
|
1355
|
+
class="remove-button ${this.isReadOnly() ? 'read-only-hidden' : ''}"
|
|
1336
1356
|
@click=${(e: MouseEvent) => this.handleNodeRemoveClick(e)}
|
|
1337
1357
|
title="Remove node"
|
|
1338
1358
|
>
|
|
@@ -1571,13 +1591,18 @@ export class CanvasNode extends RapidElement {
|
|
|
1571
1591
|
class=${getClasses({
|
|
1572
1592
|
exit: true,
|
|
1573
1593
|
connected: !!exit.destination_uuid,
|
|
1574
|
-
removing: this.exitRemovingState.has(exit.uuid)
|
|
1594
|
+
removing: this.exitRemovingState.has(exit.uuid),
|
|
1595
|
+
'read-only': this.isReadOnly()
|
|
1575
1596
|
})}
|
|
1576
1597
|
@click=${(e: MouseEvent) => this.handleExitClick(e, exit)}
|
|
1577
1598
|
></div>
|
|
1578
1599
|
</div>`;
|
|
1579
1600
|
}
|
|
1580
1601
|
|
|
1602
|
+
private isReadOnly(): boolean {
|
|
1603
|
+
return this.viewingRevision || this.isTranslating;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1581
1606
|
public render() {
|
|
1582
1607
|
if (!this.node || !this.ui) {
|
|
1583
1608
|
return html`<div class="node">Loading...</div>`;
|
|
@@ -1666,7 +1691,7 @@ export class CanvasNode extends RapidElement {
|
|
|
1666
1691
|
(exit) => this.renderExit(exit)
|
|
1667
1692
|
)}
|
|
1668
1693
|
</div>`}
|
|
1669
|
-
${this.ui.type === 'execute_actions' && !this.
|
|
1694
|
+
${this.ui.type === 'execute_actions' && !this.isReadOnly()
|
|
1670
1695
|
? html`<div
|
|
1671
1696
|
class="add-action-button"
|
|
1672
1697
|
@click=${(e: MouseEvent) => this.handleAddActionClick(e)}
|
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;
|
|
@@ -212,6 +232,23 @@ export class Editor extends RapidElement {
|
|
|
212
232
|
@state()
|
|
213
233
|
private autoTranslateError: string | null = null;
|
|
214
234
|
|
|
235
|
+
@state()
|
|
236
|
+
private revisionsWindowHidden = true;
|
|
237
|
+
|
|
238
|
+
@state()
|
|
239
|
+
private revisions: Revision[] = [];
|
|
240
|
+
|
|
241
|
+
@state()
|
|
242
|
+
private viewingRevision: Revision | null = null;
|
|
243
|
+
|
|
244
|
+
@state()
|
|
245
|
+
private isLoadingRevisions = false;
|
|
246
|
+
|
|
247
|
+
private preRevertState: {
|
|
248
|
+
definition: FlowDefinition;
|
|
249
|
+
dirtyDate: Date | null;
|
|
250
|
+
} | null = null;
|
|
251
|
+
|
|
215
252
|
private translationCache = new Map<string, string>();
|
|
216
253
|
|
|
217
254
|
// NodeEditor state - handles both node and action editing
|
|
@@ -329,6 +366,29 @@ export class Editor extends RapidElement {
|
|
|
329
366
|
transition: none !important;
|
|
330
367
|
}
|
|
331
368
|
|
|
369
|
+
#canvas.viewing-revision {
|
|
370
|
+
pointer-events: none;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
#canvas.read-only svg {
|
|
374
|
+
pointer-events: none;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
#grid.viewing-revision {
|
|
378
|
+
background-color: #fff9fc;
|
|
379
|
+
background-image: radial-gradient(
|
|
380
|
+
circle,
|
|
381
|
+
rgba(166, 38, 164, 0.2) 1px,
|
|
382
|
+
transparent 1px
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
#grid.viewing-revision temba-flow-node,
|
|
387
|
+
#grid.viewing-revision svg.jtk-connector,
|
|
388
|
+
#grid.viewing-revision .activity-overlay {
|
|
389
|
+
opacity: 0.5;
|
|
390
|
+
}
|
|
391
|
+
|
|
332
392
|
body .jtk-endpoint {
|
|
333
393
|
width: initial;
|
|
334
394
|
height: initial;
|
|
@@ -383,12 +443,28 @@ export class Editor extends RapidElement {
|
|
|
383
443
|
stroke-width: 3px;
|
|
384
444
|
}
|
|
385
445
|
|
|
446
|
+
body #canvas.read-only-connections svg.jtk-connector.jtk-hover path {
|
|
447
|
+
stroke: var(--color-connectors) !important;
|
|
448
|
+
}
|
|
449
|
+
|
|
386
450
|
body .plumb-connector.jtk-hover .plumb-arrow {
|
|
387
451
|
fill: var(--color-success) !important;
|
|
388
452
|
stroke-width: 0px;
|
|
389
453
|
z-index: 10;
|
|
390
454
|
}
|
|
391
455
|
|
|
456
|
+
body
|
|
457
|
+
#canvas.read-only-connections
|
|
458
|
+
.plumb-connector.jtk-hover
|
|
459
|
+
.plumb-arrow {
|
|
460
|
+
fill: var(--color-connectors) !important;
|
|
461
|
+
ponter-events: none;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
body #canvas.read-only-connections svg {
|
|
465
|
+
pointer-events: none;
|
|
466
|
+
}
|
|
467
|
+
|
|
392
468
|
/* Activity overlays on connections */
|
|
393
469
|
.jtk-overlay.activity-overlay {
|
|
394
470
|
background: #f3f3f3;
|
|
@@ -677,6 +753,18 @@ export class Editor extends RapidElement {
|
|
|
677
753
|
width: 100%;
|
|
678
754
|
}
|
|
679
755
|
|
|
756
|
+
.revert-button {
|
|
757
|
+
background: var(--color-primary-dark);
|
|
758
|
+
border: none;
|
|
759
|
+
color: #fff;
|
|
760
|
+
padding: 6px 10px;
|
|
761
|
+
border-radius: var(--curvature);
|
|
762
|
+
font-size: 11px;
|
|
763
|
+
font-weight: 600;
|
|
764
|
+
cursor: pointer;
|
|
765
|
+
transition: opacity 0.2s ease;
|
|
766
|
+
}
|
|
767
|
+
|
|
680
768
|
.auto-translate-button {
|
|
681
769
|
background: var(--color-primary-dark);
|
|
682
770
|
border: none;
|
|
@@ -895,10 +983,11 @@ export class Editor extends RapidElement {
|
|
|
895
983
|
}, SAVE_QUIET_TIME);
|
|
896
984
|
}
|
|
897
985
|
|
|
898
|
-
private saveChanges(): void {
|
|
986
|
+
private saveChanges(definitionOverride?: FlowDefinition): Promise<void> {
|
|
987
|
+
const definition = definitionOverride || this.definition;
|
|
899
988
|
// post the flow definition to the server
|
|
900
|
-
getStore()
|
|
901
|
-
.postJSON(`/flow/revisions/${this.flow}/`,
|
|
989
|
+
return getStore()
|
|
990
|
+
.postJSON(`/flow/revisions/${this.flow}/`, definition)
|
|
902
991
|
.then((response) => {
|
|
903
992
|
// Update flow info and revision with the response data
|
|
904
993
|
if (response.json) {
|
|
@@ -911,6 +1000,11 @@ export class Editor extends RapidElement {
|
|
|
911
1000
|
if (response.json.revision?.revision !== undefined) {
|
|
912
1001
|
state.setRevision(response.json.revision.revision);
|
|
913
1002
|
}
|
|
1003
|
+
|
|
1004
|
+
// if the revisions window is open, refresh the list
|
|
1005
|
+
if (!this.revisionsWindowHidden) {
|
|
1006
|
+
this.fetchRevisions();
|
|
1007
|
+
}
|
|
914
1008
|
}
|
|
915
1009
|
})
|
|
916
1010
|
.catch((error) => {
|
|
@@ -961,7 +1055,7 @@ export class Editor extends RapidElement {
|
|
|
961
1055
|
}
|
|
962
1056
|
|
|
963
1057
|
this.activityTimer = window.setTimeout(() => {
|
|
964
|
-
this.fetchActivityData();
|
|
1058
|
+
// this.fetchActivityData();
|
|
965
1059
|
}, this.activityInterval);
|
|
966
1060
|
});
|
|
967
1061
|
}
|
|
@@ -1073,6 +1167,8 @@ export class Editor extends RapidElement {
|
|
|
1073
1167
|
// ignore right clicks
|
|
1074
1168
|
if (event.button !== 0) return;
|
|
1075
1169
|
|
|
1170
|
+
if (this.isReadOnly()) return;
|
|
1171
|
+
|
|
1076
1172
|
const element = event.currentTarget as HTMLElement;
|
|
1077
1173
|
// Only start dragging if clicking on the element itself, not on exits or other interactive elements
|
|
1078
1174
|
const target = event.target as HTMLElement;
|
|
@@ -1142,6 +1238,8 @@ export class Editor extends RapidElement {
|
|
|
1142
1238
|
}
|
|
1143
1239
|
|
|
1144
1240
|
private handleCanvasMouseDown(event: MouseEvent): void {
|
|
1241
|
+
if (this.isReadOnly()) return;
|
|
1242
|
+
|
|
1145
1243
|
const target = event.target as HTMLElement;
|
|
1146
1244
|
if (target.id === 'canvas' || target.id === 'grid') {
|
|
1147
1245
|
// Ignore clicks on exits
|
|
@@ -1215,6 +1313,7 @@ export class Editor extends RapidElement {
|
|
|
1215
1313
|
// Clean up jsPlumb connections for nodes before removing them
|
|
1216
1314
|
uuids.forEach((uuid) => {
|
|
1217
1315
|
this.plumber.removeNodeConnections(uuid);
|
|
1316
|
+
this.plumber.removeAllEndpoints(uuid);
|
|
1218
1317
|
});
|
|
1219
1318
|
|
|
1220
1319
|
// Now remove them from the definition
|
|
@@ -1735,6 +1834,11 @@ export class Editor extends RapidElement {
|
|
|
1735
1834
|
}
|
|
1736
1835
|
|
|
1737
1836
|
private handleCanvasContextMenu(event: MouseEvent): void {
|
|
1837
|
+
if (this.isReadOnly()) {
|
|
1838
|
+
event.preventDefault();
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1738
1842
|
// Check if we right-clicked on empty canvas space
|
|
1739
1843
|
const target = event.target as HTMLElement;
|
|
1740
1844
|
if (target.id !== 'canvas') {
|
|
@@ -2654,6 +2758,7 @@ export class Editor extends RapidElement {
|
|
|
2654
2758
|
}
|
|
2655
2759
|
|
|
2656
2760
|
this.localizationWindowHidden = false;
|
|
2761
|
+
this.revisionsWindowHidden = true;
|
|
2657
2762
|
|
|
2658
2763
|
const alreadySelected = languages.some(
|
|
2659
2764
|
(lang) => lang.code === this.languageCode
|
|
@@ -2916,6 +3021,223 @@ export class Editor extends RapidElement {
|
|
|
2916
3021
|
this.autoTranslating = false;
|
|
2917
3022
|
}
|
|
2918
3023
|
|
|
3024
|
+
private handleRevisionsTabClick(): void {
|
|
3025
|
+
if (this.revisionsWindowHidden) {
|
|
3026
|
+
this.fetchRevisions();
|
|
3027
|
+
this.revisionsWindowHidden = false;
|
|
3028
|
+
this.localizationWindowHidden = true; // Close other window
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
private handleRevisionsWindowClosed(): void {
|
|
3033
|
+
this.resetRevisionsScroll();
|
|
3034
|
+
this.revisionsWindowHidden = true;
|
|
3035
|
+
if (this.viewingRevision) {
|
|
3036
|
+
this.handleCancelRevisionView();
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
private resetRevisionsScroll() {
|
|
3041
|
+
const list =
|
|
3042
|
+
this.querySelector('#revisions-window').shadowRoot?.querySelector(
|
|
3043
|
+
'.body'
|
|
3044
|
+
);
|
|
3045
|
+
if (list) {
|
|
3046
|
+
list.scrollTop = 0;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
private async fetchRevisions() {
|
|
3051
|
+
this.isLoadingRevisions = true;
|
|
3052
|
+
try {
|
|
3053
|
+
const results = await fetchResults(
|
|
3054
|
+
`/flow/revisions/${this.flow}/?version=${FLOW_SPEC_VERSION}`
|
|
3055
|
+
);
|
|
3056
|
+
this.revisions = results.slice(1);
|
|
3057
|
+
} catch (e) {
|
|
3058
|
+
console.error('Error fetching revisions', e);
|
|
3059
|
+
} finally {
|
|
3060
|
+
this.isLoadingRevisions = false;
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
private async handleRevisionClick(revision: Revision) {
|
|
3065
|
+
if (this.viewingRevision?.id === revision.id) {
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
if (!this.viewingRevision) {
|
|
3070
|
+
// Save current state first
|
|
3071
|
+
this.preRevertState = {
|
|
3072
|
+
definition: this.definition,
|
|
3073
|
+
dirtyDate: this.dirtyDate
|
|
3074
|
+
};
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
this.viewingRevision = revision;
|
|
3078
|
+
this.isLoadingRevisions = true;
|
|
3079
|
+
this.plumber?.reset();
|
|
3080
|
+
|
|
3081
|
+
try {
|
|
3082
|
+
await getStore()
|
|
3083
|
+
.getState()
|
|
3084
|
+
.fetchRevision(`/flow/revisions/${this.flow}`, revision.id.toString());
|
|
3085
|
+
} catch (e) {
|
|
3086
|
+
console.error('Error fetching revision details', e);
|
|
3087
|
+
this.handleCancelRevisionView();
|
|
3088
|
+
} finally {
|
|
3089
|
+
this.isLoadingRevisions = false;
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
private handleCancelRevisionView() {
|
|
3094
|
+
this.plumber?.reset();
|
|
3095
|
+
if (this.preRevertState) {
|
|
3096
|
+
const currentInfo = getStore().getState().flowInfo;
|
|
3097
|
+
getStore().getState().setFlowContents({
|
|
3098
|
+
definition: this.preRevertState.definition,
|
|
3099
|
+
info: currentInfo
|
|
3100
|
+
});
|
|
3101
|
+
if (this.preRevertState.dirtyDate) {
|
|
3102
|
+
getStore().getState().setDirtyDate(this.preRevertState.dirtyDate);
|
|
3103
|
+
}
|
|
3104
|
+
} else {
|
|
3105
|
+
// Fallback if no pre-revert definition
|
|
3106
|
+
getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
this.viewingRevision = null;
|
|
3110
|
+
this.preRevertState = null;
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
private async handleRevertClick() {
|
|
3114
|
+
if (!this.viewingRevision || !this.preRevertState) return;
|
|
3115
|
+
this.plumber?.reset();
|
|
3116
|
+
|
|
3117
|
+
// Use the content of the viewing revision (this.definition)
|
|
3118
|
+
// but the revision number of the current head (preRevertState)
|
|
3119
|
+
// so the server accepts it as a valid update
|
|
3120
|
+
const definitionToSave = {
|
|
3121
|
+
...this.definition,
|
|
3122
|
+
revision: this.preRevertState.definition.revision
|
|
3123
|
+
};
|
|
3124
|
+
|
|
3125
|
+
await this.saveChanges(definitionToSave);
|
|
3126
|
+
this.viewingRevision = null;
|
|
3127
|
+
this.preRevertState = null;
|
|
3128
|
+
this.revisionsWindowHidden = true;
|
|
3129
|
+
|
|
3130
|
+
const revisionsWindow = document.getElementById(
|
|
3131
|
+
'revisions-window'
|
|
3132
|
+
) as FloatingWindow;
|
|
3133
|
+
revisionsWindow.handleClose();
|
|
3134
|
+
|
|
3135
|
+
// Refresh revisions list to show the new one
|
|
3136
|
+
this.fetchRevisions();
|
|
3137
|
+
|
|
3138
|
+
// Fetch the latest version of the flow to ensure the store is up to date
|
|
3139
|
+
getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
private renderRevisionsTab(): TemplateResult | string {
|
|
3143
|
+
return html`
|
|
3144
|
+
<temba-floating-tab
|
|
3145
|
+
id="revisions-tab"
|
|
3146
|
+
icon="revisions"
|
|
3147
|
+
label="Revisions"
|
|
3148
|
+
color="rgb(142, 94, 167)"
|
|
3149
|
+
top="105"
|
|
3150
|
+
.hidden=${!this.revisionsWindowHidden && this.localizationWindowHidden}
|
|
3151
|
+
@temba-button-clicked=${this.handleRevisionsTabClick}
|
|
3152
|
+
></temba-floating-tab>
|
|
3153
|
+
`;
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
private renderRevisionsWindow(): TemplateResult | string {
|
|
3157
|
+
return html`
|
|
3158
|
+
<temba-floating-window
|
|
3159
|
+
id="revisions-window"
|
|
3160
|
+
header="Revisions"
|
|
3161
|
+
.width=${360}
|
|
3162
|
+
.maxHeight=${600}
|
|
3163
|
+
.top=${75}
|
|
3164
|
+
color="rgb(142, 94, 167)"
|
|
3165
|
+
.hidden=${this.revisionsWindowHidden}
|
|
3166
|
+
@temba-dialog-hidden=${this.handleRevisionsWindowClosed}
|
|
3167
|
+
>
|
|
3168
|
+
<div class="localization-window-content">
|
|
3169
|
+
<div
|
|
3170
|
+
class="revisions-list"
|
|
3171
|
+
style="display:flex; flex-direction:column; gap:8px; overflow-y:auto; padding-bottom:10px;"
|
|
3172
|
+
>
|
|
3173
|
+
${this.isLoadingRevisions && !this.revisions.length
|
|
3174
|
+
? html`<temba-loading></temba-loading>`
|
|
3175
|
+
: this.revisions.map((rev) => {
|
|
3176
|
+
const isSelected = this.viewingRevision?.id === rev.id;
|
|
3177
|
+
return html`
|
|
3178
|
+
<div
|
|
3179
|
+
class="revision-item ${isSelected ? 'selected' : ''}"
|
|
3180
|
+
style="padding:8px; border-radius:4px; cursor:pointer; background:${
|
|
3181
|
+
isSelected
|
|
3182
|
+
? '#f0f6ff' // Light blue bg for selected
|
|
3183
|
+
: '#f9fafb'
|
|
3184
|
+
}; border:1px solid ${
|
|
3185
|
+
isSelected ? '#a4cafe' : '#e5e7eb'
|
|
3186
|
+
}; transition: all 0.2s ease;"
|
|
3187
|
+
@click=${() => this.handleRevisionClick(rev)}
|
|
3188
|
+
>
|
|
3189
|
+
<div
|
|
3190
|
+
style="display:flex; justify-content:space-between; align-items:center;"
|
|
3191
|
+
>
|
|
3192
|
+
<div
|
|
3193
|
+
class="revision-header"
|
|
3194
|
+
style="margin-bottom: 2px;"
|
|
3195
|
+
>
|
|
3196
|
+
<div
|
|
3197
|
+
style="font-weight:600; font-size:13px; color:#111827;"
|
|
3198
|
+
>
|
|
3199
|
+
<temba-date value=${
|
|
3200
|
+
rev.created_on
|
|
3201
|
+
} display="duration"></temba-date>
|
|
3202
|
+
|
|
3203
|
+
</div>
|
|
3204
|
+
<div style="font-size:11px; color:#6b7280;">
|
|
3205
|
+
${rev.user.name || rev.user.username}
|
|
3206
|
+
</div>
|
|
3207
|
+
</div>
|
|
3208
|
+
${
|
|
3209
|
+
isSelected
|
|
3210
|
+
? html`<button
|
|
3211
|
+
class="revert-button"
|
|
3212
|
+
@click=${this.handleRevertClick}
|
|
3213
|
+
>
|
|
3214
|
+
Revert
|
|
3215
|
+
</button>`
|
|
3216
|
+
: html``
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
</button>
|
|
3220
|
+
</div>
|
|
3221
|
+
|
|
3222
|
+
${
|
|
3223
|
+
rev.comment
|
|
3224
|
+
? html`<div
|
|
3225
|
+
style="font-size:12px; color:#4b5563; margin-top:4px;"
|
|
3226
|
+
>
|
|
3227
|
+
${rev.comment}
|
|
3228
|
+
</div>`
|
|
3229
|
+
: ''
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
</div>
|
|
3233
|
+
`;
|
|
3234
|
+
})}
|
|
3235
|
+
</div>
|
|
3236
|
+
</div>
|
|
3237
|
+
</temba-floating-window>
|
|
3238
|
+
`;
|
|
3239
|
+
}
|
|
3240
|
+
|
|
2919
3241
|
private renderLocalizationWindow(): TemplateResult | string {
|
|
2920
3242
|
const languages = this.getLocalizationLanguages();
|
|
2921
3243
|
if (!languages.length) {
|
|
@@ -3167,6 +3489,10 @@ export class Editor extends RapidElement {
|
|
|
3167
3489
|
});
|
|
3168
3490
|
}
|
|
3169
3491
|
|
|
3492
|
+
private isReadOnly(): boolean {
|
|
3493
|
+
return this.viewingRevision !== null || this.isTranslating;
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3170
3496
|
public render(): TemplateResult {
|
|
3171
3497
|
// we have to embed our own style since we are in light DOM
|
|
3172
3498
|
const style = html`<style>
|
|
@@ -3176,20 +3502,30 @@ export class Editor extends RapidElement {
|
|
|
3176
3502
|
|
|
3177
3503
|
const stickies = this.definition?._ui?.stickies || {};
|
|
3178
3504
|
|
|
3179
|
-
return html`${style} ${this.
|
|
3180
|
-
${this.renderAutoTranslateDialog()}
|
|
3505
|
+
return html`${style} ${this.renderRevisionsWindow()}
|
|
3506
|
+
${this.renderLocalizationWindow()} ${this.renderAutoTranslateDialog()}
|
|
3181
3507
|
<div id="editor">
|
|
3182
3508
|
<div
|
|
3183
3509
|
id="grid"
|
|
3510
|
+
class="${this.viewingRevision ? 'viewing-revision' : ''}"
|
|
3184
3511
|
style="min-width:100%;width:${this.canvasSize.width}px; height:${this
|
|
3185
3512
|
.canvasSize.height}px"
|
|
3186
3513
|
>
|
|
3187
|
-
<div
|
|
3514
|
+
<div
|
|
3515
|
+
id="canvas"
|
|
3516
|
+
class="${getClasses({
|
|
3517
|
+
'viewing-revision': !!this.viewingRevision,
|
|
3518
|
+
'read-only-connections':
|
|
3519
|
+
!!this.viewingRevision || this.isTranslating
|
|
3520
|
+
})}"
|
|
3521
|
+
>
|
|
3188
3522
|
${this.definition
|
|
3189
3523
|
? repeat(
|
|
3190
|
-
this.definition.nodes,
|
|
3524
|
+
[...this.definition.nodes].sort((a, b) =>
|
|
3525
|
+
a.uuid.localeCompare(b.uuid)
|
|
3526
|
+
),
|
|
3191
3527
|
(node) => node.uuid,
|
|
3192
|
-
(node
|
|
3528
|
+
(node) => {
|
|
3193
3529
|
const position = this.definition._ui?.nodes[node.uuid]
|
|
3194
3530
|
?.position || {
|
|
3195
3531
|
left: 0,
|
|
@@ -3203,7 +3539,9 @@ export class Editor extends RapidElement {
|
|
|
3203
3539
|
const selected = this.selectedItems.has(node.uuid);
|
|
3204
3540
|
|
|
3205
3541
|
// first node is the flow start (nodes are sorted by position)
|
|
3206
|
-
const isFlowStart =
|
|
3542
|
+
const isFlowStart =
|
|
3543
|
+
this.definition.nodes.length > 0 &&
|
|
3544
|
+
this.definition.nodes[0].uuid === node.uuid;
|
|
3207
3545
|
|
|
3208
3546
|
return html`<temba-flow-node
|
|
3209
3547
|
class="draggable ${dragging ? 'dragging' : ''} ${selected
|
|
@@ -3263,10 +3601,12 @@ export class Editor extends RapidElement {
|
|
|
3263
3601
|
: ''}
|
|
3264
3602
|
|
|
3265
3603
|
<temba-canvas-menu></temba-canvas-menu>
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3604
|
+
${!this.viewingRevision
|
|
3605
|
+
? html`<temba-node-type-selector
|
|
3606
|
+
.flowType=${this.flowType}
|
|
3607
|
+
.features=${this.features}
|
|
3608
|
+
></temba-node-type-selector>`
|
|
3609
|
+
: ''}
|
|
3610
|
+
${this.renderRevisionsTab()} ${this.renderLocalizationTab()} `;
|
|
3271
3611
|
}
|
|
3272
3612
|
}
|