@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/temba-components.js +392 -258
  3. package/dist/temba-components.js.map +1 -1
  4. package/out-tsc/src/display/FloatingTab.js +2 -2
  5. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  6. package/out-tsc/src/flow/CanvasNode.js +45 -24
  7. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  8. package/out-tsc/src/flow/Editor.js +305 -16
  9. package/out-tsc/src/flow/Editor.js.map +1 -1
  10. package/out-tsc/src/flow/Plumber.js +110 -64
  11. package/out-tsc/src/flow/Plumber.js.map +1 -1
  12. package/out-tsc/src/simulator/Simulator.js +11 -4
  13. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  14. package/out-tsc/src/store/AppState.js +12 -2
  15. package/out-tsc/src/store/AppState.js.map +1 -1
  16. package/out-tsc/test/temba-flow-editor-node.test.js +2 -1
  17. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  18. package/out-tsc/test/temba-flow-editor-revisions.test.js +106 -0
  19. package/out-tsc/test/temba-flow-editor-revisions.test.js.map +1 -0
  20. package/out-tsc/test/temba-flow-editor.test.js +14 -10
  21. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  22. package/out-tsc/test/temba-flow-plumber-connections.test.js +7 -1
  23. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  24. package/out-tsc/test/temba-flow-plumber.test.js +6 -0
  25. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/display/FloatingTab.ts +2 -2
  28. package/src/flow/CanvasNode.ts +54 -29
  29. package/src/flow/Editor.ts +357 -17
  30. package/src/flow/Plumber.ts +123 -69
  31. package/src/simulator/Simulator.ts +11 -5
  32. package/src/store/AppState.ts +13 -2
  33. package/test/temba-flow-editor-node.test.ts +2 -1
  34. package/test/temba-flow-editor-revisions.test.ts +134 -0
  35. package/test/temba-flow-editor.test.ts +16 -10
  36. package/test/temba-flow-plumber-connections.test.ts +7 -1
  37. package/test/temba-flow-plumber.test.ts +6 -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;
@@ -544,6 +595,11 @@ export class Editor extends RapidElement {
544
595
  this.autoTranslating = false;
545
596
  this.autoTranslateModel = null;
546
597
  this.autoTranslateError = null;
598
+ this.revisionsWindowHidden = true;
599
+ this.revisions = [];
600
+ this.viewingRevision = null;
601
+ this.isLoadingRevisions = false;
602
+ this.preRevertState = null;
547
603
  this.translationCache = new Map();
548
604
  // NodeEditor state - handles both node and action editing
549
605
  this.editingNode = null;
@@ -693,10 +749,11 @@ export class Editor extends RapidElement {
693
749
  }
694
750
  }, SAVE_QUIET_TIME);
695
751
  }
696
- saveChanges() {
752
+ saveChanges(definitionOverride) {
753
+ const definition = definitionOverride || this.definition;
697
754
  // post the flow definition to the server
698
- getStore()
699
- .postJSON(`/flow/revisions/${this.flow}/`, this.definition)
755
+ return getStore()
756
+ .postJSON(`/flow/revisions/${this.flow}/`, definition)
700
757
  .then((response) => {
701
758
  var _b;
702
759
  // Update flow info and revision with the response data
@@ -708,6 +765,10 @@ export class Editor extends RapidElement {
708
765
  if (((_b = response.json.revision) === null || _b === void 0 ? void 0 : _b.revision) !== undefined) {
709
766
  state.setRevision(response.json.revision.revision);
710
767
  }
768
+ // if the revisions window is open, refresh the list
769
+ if (!this.revisionsWindowHidden) {
770
+ this.fetchRevisions();
771
+ }
711
772
  }
712
773
  })
713
774
  .catch((error) => {
@@ -751,7 +812,7 @@ export class Editor extends RapidElement {
751
812
  clearTimeout(this.activityTimer);
752
813
  }
753
814
  this.activityTimer = window.setTimeout(() => {
754
- this.fetchActivityData();
815
+ // this.fetchActivityData();
755
816
  }, this.activityInterval);
756
817
  });
757
818
  }
@@ -832,6 +893,8 @@ export class Editor extends RapidElement {
832
893
  // ignore right clicks
833
894
  if (event.button !== 0)
834
895
  return;
896
+ if (this.isReadOnly())
897
+ return;
835
898
  const element = event.currentTarget;
836
899
  // Only start dragging if clicking on the element itself, not on exits or other interactive elements
837
900
  const target = event.target;
@@ -893,6 +956,8 @@ export class Editor extends RapidElement {
893
956
  }
894
957
  handleCanvasMouseDown(event) {
895
958
  var _b;
959
+ if (this.isReadOnly())
960
+ return;
896
961
  const target = event.target;
897
962
  if (target.id === 'canvas' || target.id === 'grid') {
898
963
  // Ignore clicks on exits
@@ -954,6 +1019,7 @@ export class Editor extends RapidElement {
954
1019
  // Clean up jsPlumb connections for nodes before removing them
955
1020
  uuids.forEach((uuid) => {
956
1021
  this.plumber.removeNodeConnections(uuid);
1022
+ this.plumber.removeAllEndpoints(uuid);
957
1023
  });
958
1024
  // Now remove them from the definition
959
1025
  if (uuids.length > 0 && this.plumber) {
@@ -1372,6 +1438,10 @@ export class Editor extends RapidElement {
1372
1438
  store.getState().expandCanvas(maxWidth, maxHeight);
1373
1439
  }
1374
1440
  handleCanvasContextMenu(event) {
1441
+ if (this.isReadOnly()) {
1442
+ event.preventDefault();
1443
+ return;
1444
+ }
1375
1445
  // Check if we right-clicked on empty canvas space
1376
1446
  const target = event.target;
1377
1447
  if (target.id !== 'canvas') {
@@ -2048,6 +2118,7 @@ export class Editor extends RapidElement {
2048
2118
  return;
2049
2119
  }
2050
2120
  this.localizationWindowHidden = false;
2121
+ this.revisionsWindowHidden = true;
2051
2122
  const alreadySelected = languages.some((lang) => lang.code === this.languageCode);
2052
2123
  if (!alreadySelected) {
2053
2124
  this.handleLanguageChange(languages[0].code);
@@ -2255,6 +2326,199 @@ export class Editor extends RapidElement {
2255
2326
  }
2256
2327
  this.autoTranslating = false;
2257
2328
  }
2329
+ handleRevisionsTabClick() {
2330
+ if (this.revisionsWindowHidden) {
2331
+ this.fetchRevisions();
2332
+ this.revisionsWindowHidden = false;
2333
+ this.localizationWindowHidden = true; // Close other window
2334
+ }
2335
+ }
2336
+ handleRevisionsWindowClosed() {
2337
+ this.resetRevisionsScroll();
2338
+ this.revisionsWindowHidden = true;
2339
+ if (this.viewingRevision) {
2340
+ this.handleCancelRevisionView();
2341
+ }
2342
+ }
2343
+ resetRevisionsScroll() {
2344
+ var _b;
2345
+ const list = (_b = this.querySelector('#revisions-window').shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('.body');
2346
+ if (list) {
2347
+ list.scrollTop = 0;
2348
+ }
2349
+ }
2350
+ async fetchRevisions() {
2351
+ this.isLoadingRevisions = true;
2352
+ try {
2353
+ const results = await fetchResults(`/flow/revisions/${this.flow}/?version=${FLOW_SPEC_VERSION}`);
2354
+ this.revisions = results.slice(1);
2355
+ }
2356
+ catch (e) {
2357
+ console.error('Error fetching revisions', e);
2358
+ }
2359
+ finally {
2360
+ this.isLoadingRevisions = false;
2361
+ }
2362
+ }
2363
+ async handleRevisionClick(revision) {
2364
+ var _b, _c;
2365
+ if (((_b = this.viewingRevision) === null || _b === void 0 ? void 0 : _b.id) === revision.id) {
2366
+ return;
2367
+ }
2368
+ if (!this.viewingRevision) {
2369
+ // Save current state first
2370
+ this.preRevertState = {
2371
+ definition: this.definition,
2372
+ dirtyDate: this.dirtyDate
2373
+ };
2374
+ }
2375
+ this.viewingRevision = revision;
2376
+ this.isLoadingRevisions = true;
2377
+ (_c = this.plumber) === null || _c === void 0 ? void 0 : _c.reset();
2378
+ try {
2379
+ await getStore()
2380
+ .getState()
2381
+ .fetchRevision(`/flow/revisions/${this.flow}`, revision.id.toString());
2382
+ }
2383
+ catch (e) {
2384
+ console.error('Error fetching revision details', e);
2385
+ this.handleCancelRevisionView();
2386
+ }
2387
+ finally {
2388
+ this.isLoadingRevisions = false;
2389
+ }
2390
+ }
2391
+ handleCancelRevisionView() {
2392
+ var _b;
2393
+ (_b = this.plumber) === null || _b === void 0 ? void 0 : _b.reset();
2394
+ if (this.preRevertState) {
2395
+ const currentInfo = getStore().getState().flowInfo;
2396
+ getStore().getState().setFlowContents({
2397
+ definition: this.preRevertState.definition,
2398
+ info: currentInfo
2399
+ });
2400
+ if (this.preRevertState.dirtyDate) {
2401
+ getStore().getState().setDirtyDate(this.preRevertState.dirtyDate);
2402
+ }
2403
+ }
2404
+ else {
2405
+ // Fallback if no pre-revert definition
2406
+ getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
2407
+ }
2408
+ this.viewingRevision = null;
2409
+ this.preRevertState = null;
2410
+ }
2411
+ async handleRevertClick() {
2412
+ var _b;
2413
+ if (!this.viewingRevision || !this.preRevertState)
2414
+ return;
2415
+ (_b = this.plumber) === null || _b === void 0 ? void 0 : _b.reset();
2416
+ // Use the content of the viewing revision (this.definition)
2417
+ // but the revision number of the current head (preRevertState)
2418
+ // so the server accepts it as a valid update
2419
+ const definitionToSave = {
2420
+ ...this.definition,
2421
+ revision: this.preRevertState.definition.revision
2422
+ };
2423
+ await this.saveChanges(definitionToSave);
2424
+ this.viewingRevision = null;
2425
+ this.preRevertState = null;
2426
+ this.revisionsWindowHidden = true;
2427
+ const revisionsWindow = document.getElementById('revisions-window');
2428
+ revisionsWindow.handleClose();
2429
+ // Refresh revisions list to show the new one
2430
+ this.fetchRevisions();
2431
+ // Fetch the latest version of the flow to ensure the store is up to date
2432
+ getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
2433
+ }
2434
+ renderRevisionsTab() {
2435
+ return html `
2436
+ <temba-floating-tab
2437
+ id="revisions-tab"
2438
+ icon="revisions"
2439
+ label="Revisions"
2440
+ color="rgb(142, 94, 167)"
2441
+ top="105"
2442
+ .hidden=${!this.revisionsWindowHidden && this.localizationWindowHidden}
2443
+ @temba-button-clicked=${this.handleRevisionsTabClick}
2444
+ ></temba-floating-tab>
2445
+ `;
2446
+ }
2447
+ renderRevisionsWindow() {
2448
+ return html `
2449
+ <temba-floating-window
2450
+ id="revisions-window"
2451
+ header="Revisions"
2452
+ .width=${360}
2453
+ .maxHeight=${600}
2454
+ .top=${75}
2455
+ color="rgb(142, 94, 167)"
2456
+ .hidden=${this.revisionsWindowHidden}
2457
+ @temba-dialog-hidden=${this.handleRevisionsWindowClosed}
2458
+ >
2459
+ <div class="localization-window-content">
2460
+ <div
2461
+ class="revisions-list"
2462
+ style="display:flex; flex-direction:column; gap:8px; overflow-y:auto; padding-bottom:10px;"
2463
+ >
2464
+ ${this.isLoadingRevisions && !this.revisions.length
2465
+ ? html `<temba-loading></temba-loading>`
2466
+ : this.revisions.map((rev) => {
2467
+ var _b;
2468
+ const isSelected = ((_b = this.viewingRevision) === null || _b === void 0 ? void 0 : _b.id) === rev.id;
2469
+ return html `
2470
+ <div
2471
+ class="revision-item ${isSelected ? 'selected' : ''}"
2472
+ style="padding:8px; border-radius:4px; cursor:pointer; background:${isSelected
2473
+ ? '#f0f6ff' // Light blue bg for selected
2474
+ : '#f9fafb'}; border:1px solid ${isSelected ? '#a4cafe' : '#e5e7eb'}; transition: all 0.2s ease;"
2475
+ @click=${() => this.handleRevisionClick(rev)}
2476
+ >
2477
+ <div
2478
+ style="display:flex; justify-content:space-between; align-items:center;"
2479
+ >
2480
+ <div
2481
+ class="revision-header"
2482
+ style="margin-bottom: 2px;"
2483
+ >
2484
+ <div
2485
+ style="font-weight:600; font-size:13px; color:#111827;"
2486
+ >
2487
+ <temba-date value=${rev.created_on} display="duration"></temba-date>
2488
+
2489
+ </div>
2490
+ <div style="font-size:11px; color:#6b7280;">
2491
+ ${rev.user.name || rev.user.username}
2492
+ </div>
2493
+ </div>
2494
+ ${isSelected
2495
+ ? html `<button
2496
+ class="revert-button"
2497
+ @click=${this.handleRevertClick}
2498
+ >
2499
+ Revert
2500
+ </button>`
2501
+ : html ``}
2502
+
2503
+ </button>
2504
+ </div>
2505
+
2506
+ ${rev.comment
2507
+ ? html `<div
2508
+ style="font-size:12px; color:#4b5563; margin-top:4px;"
2509
+ >
2510
+ ${rev.comment}
2511
+ </div>`
2512
+ : ''}
2513
+
2514
+ </div>
2515
+ `;
2516
+ })}
2517
+ </div>
2518
+ </div>
2519
+ </temba-floating-window>
2520
+ `;
2521
+ }
2258
2522
  renderLocalizationWindow() {
2259
2523
  var _b, _c, _d;
2260
2524
  const languages = this.getLocalizationLanguages();
@@ -2481,6 +2745,9 @@ export class Editor extends RapidElement {
2481
2745
  behavior: 'smooth'
2482
2746
  });
2483
2747
  }
2748
+ isReadOnly() {
2749
+ return this.viewingRevision !== null || this.isTranslating;
2750
+ }
2484
2751
  render() {
2485
2752
  var _b, _c;
2486
2753
  // we have to embed our own style since we are in light DOM
@@ -2489,17 +2756,24 @@ export class Editor extends RapidElement {
2489
2756
  ${unsafeCSS(CanvasNode.styles.cssText)}
2490
2757
  </style>`;
2491
2758
  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.renderLocalizationWindow()}
2493
- ${this.renderAutoTranslateDialog()}
2759
+ return html `${style} ${this.renderRevisionsWindow()}
2760
+ ${this.renderLocalizationWindow()} ${this.renderAutoTranslateDialog()}
2494
2761
  <div id="editor">
2495
2762
  <div
2496
2763
  id="grid"
2764
+ class="${this.viewingRevision ? 'viewing-revision' : ''}"
2497
2765
  style="min-width:100%;width:${this.canvasSize.width}px; height:${this
2498
2766
  .canvasSize.height}px"
2499
2767
  >
2500
- <div id="canvas">
2768
+ <div
2769
+ id="canvas"
2770
+ class="${getClasses({
2771
+ 'viewing-revision': !!this.viewingRevision,
2772
+ 'read-only-connections': !!this.viewingRevision || this.isTranslating
2773
+ })}"
2774
+ >
2501
2775
  ${this.definition
2502
- ? repeat(this.definition.nodes, (node) => node.uuid, (node, index) => {
2776
+ ? repeat([...this.definition.nodes].sort((a, b) => a.uuid.localeCompare(b.uuid)), (node) => node.uuid, (node) => {
2503
2777
  var _b, _c, _d;
2504
2778
  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
2779
  left: 0,
@@ -2509,7 +2783,8 @@ export class Editor extends RapidElement {
2509
2783
  ((_d = this.currentDragItem) === null || _d === void 0 ? void 0 : _d.uuid) === node.uuid;
2510
2784
  const selected = this.selectedItems.has(node.uuid);
2511
2785
  // first node is the flow start (nodes are sorted by position)
2512
- const isFlowStart = index === 0;
2786
+ const isFlowStart = this.definition.nodes.length > 0 &&
2787
+ this.definition.nodes[0].uuid === node.uuid;
2513
2788
  return html `<temba-flow-node
2514
2789
  class="draggable ${dragging ? 'dragging' : ''} ${selected
2515
2790
  ? 'selected'
@@ -2561,11 +2836,13 @@ export class Editor extends RapidElement {
2561
2836
  : ''}
2562
2837
 
2563
2838
  <temba-canvas-menu></temba-canvas-menu>
2564
- <temba-node-type-selector
2565
- .flowType=${this.flowType}
2566
- .features=${this.features}
2567
- ></temba-node-type-selector>
2568
- ${this.renderLocalizationTab()} `;
2839
+ ${!this.viewingRevision
2840
+ ? html `<temba-node-type-selector
2841
+ .flowType=${this.flowType}
2842
+ .features=${this.features}
2843
+ ></temba-node-type-selector>`
2844
+ : ''}
2845
+ ${this.renderRevisionsTab()} ${this.renderLocalizationTab()} `;
2569
2846
  }
2570
2847
  }
2571
2848
  __decorate([
@@ -2652,6 +2929,18 @@ __decorate([
2652
2929
  __decorate([
2653
2930
  state()
2654
2931
  ], Editor.prototype, "autoTranslateError", void 0);
2932
+ __decorate([
2933
+ state()
2934
+ ], Editor.prototype, "revisionsWindowHidden", void 0);
2935
+ __decorate([
2936
+ state()
2937
+ ], Editor.prototype, "revisions", void 0);
2938
+ __decorate([
2939
+ state()
2940
+ ], Editor.prototype, "viewingRevision", void 0);
2941
+ __decorate([
2942
+ state()
2943
+ ], Editor.prototype, "isLoadingRevisions", void 0);
2655
2944
  __decorate([
2656
2945
  state()
2657
2946
  ], Editor.prototype, "editingNode", void 0);