@nyaruka/temba-components 0.136.1 → 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 (73) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/demo/components/webchat/example.html +2 -2
  3. package/dist/temba-components.js +692 -622
  4. package/dist/temba-components.js.map +1 -1
  5. package/out-tsc/src/display/Chat.js +123 -44
  6. package/out-tsc/src/display/Chat.js.map +1 -1
  7. package/out-tsc/src/display/FloatingTab.js +2 -2
  8. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  9. package/out-tsc/src/events/eventRenderers.js +442 -0
  10. package/out-tsc/src/events/eventRenderers.js.map +1 -0
  11. package/out-tsc/src/flow/CanvasNode.js +45 -24
  12. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  13. package/out-tsc/src/flow/Editor.js +308 -18
  14. package/out-tsc/src/flow/Editor.js.map +1 -1
  15. package/out-tsc/src/flow/NodeEditor.js +0 -1
  16. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  17. package/out-tsc/src/flow/Plumber.js +110 -64
  18. package/out-tsc/src/flow/Plumber.js.map +1 -1
  19. package/out-tsc/src/list/ShortcutList.js +1 -1
  20. package/out-tsc/src/list/ShortcutList.js.map +1 -1
  21. package/out-tsc/src/live/ContactChat.js +12 -321
  22. package/out-tsc/src/live/ContactChat.js.map +1 -1
  23. package/out-tsc/src/simulator/Simulator.js +439 -575
  24. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  25. package/out-tsc/src/store/AppState.js +12 -2
  26. package/out-tsc/src/store/AppState.js.map +1 -1
  27. package/out-tsc/test/temba-flow-editor-node.test.js +2 -1
  28. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  29. package/out-tsc/test/temba-flow-editor-revisions.test.js +106 -0
  30. package/out-tsc/test/temba-flow-editor-revisions.test.js.map +1 -0
  31. package/out-tsc/test/temba-flow-editor.test.js +14 -10
  32. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  33. package/out-tsc/test/temba-flow-plumber-connections.test.js +7 -1
  34. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  35. package/out-tsc/test/temba-flow-plumber.test.js +6 -0
  36. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  37. package/out-tsc/test/temba-simulator.test.js +51 -32
  38. package/out-tsc/test/temba-simulator.test.js.map +1 -1
  39. package/package.json +1 -1
  40. package/screenshots/truth/contacts/chat-failure.png +0 -0
  41. package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
  42. package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
  43. package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
  44. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  45. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  46. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  47. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  48. package/screenshots/truth/simulator/after-message-sent.png +0 -0
  49. package/screenshots/truth/simulator/after-reset.png +0 -0
  50. package/screenshots/truth/simulator/attachment-menu.png +0 -0
  51. package/screenshots/truth/simulator/context-expanded.png +0 -0
  52. package/screenshots/truth/simulator/context-explorer-open.png +0 -0
  53. package/screenshots/truth/simulator/event-info.png +0 -0
  54. package/screenshots/truth/simulator/image-attachment.png +0 -0
  55. package/screenshots/truth/simulator/open-initial.png +0 -0
  56. package/screenshots/truth/simulator/quick-replies.png +0 -0
  57. package/src/display/Chat.ts +123 -44
  58. package/src/display/FloatingTab.ts +2 -2
  59. package/src/events/eventRenderers.ts +527 -0
  60. package/src/flow/CanvasNode.ts +54 -29
  61. package/src/flow/Editor.ts +360 -19
  62. package/src/flow/NodeEditor.ts +0 -1
  63. package/src/flow/Plumber.ts +123 -69
  64. package/src/list/ShortcutList.ts +1 -1
  65. package/src/live/ContactChat.ts +17 -376
  66. package/src/simulator/Simulator.ts +498 -617
  67. package/src/store/AppState.ts +13 -2
  68. package/test/temba-flow-editor-node.test.ts +2 -1
  69. package/test/temba-flow-editor-revisions.test.ts +134 -0
  70. package/test/temba-flow-editor.test.ts +16 -10
  71. package/test/temba-flow-plumber-connections.test.ts +7 -1
  72. package/test/temba-flow-plumber.test.ts +6 -0
  73. package/test/temba-simulator.test.ts +64 -34
@@ -9,12 +9,31 @@ import {
9
9
  NodeUI
10
10
  } from '../store/flow-definition';
11
11
  import { getStore } from '../store/Store';
12
- import { AppState, fromStore, zustand } from '../store/AppState';
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
@@ -309,6 +346,7 @@ export class Editor extends RapidElement {
309
346
  background-position: 10px 10px;
310
347
  width: 100%;
311
348
  display: flex;
349
+ padding-top: 20px;
312
350
  }
313
351
 
314
352
  #canvas {
@@ -328,6 +366,29 @@ export class Editor extends RapidElement {
328
366
  transition: none !important;
329
367
  }
330
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
+
331
392
  body .jtk-endpoint {
332
393
  width: initial;
333
394
  height: initial;
@@ -382,12 +443,28 @@ export class Editor extends RapidElement {
382
443
  stroke-width: 3px;
383
444
  }
384
445
 
446
+ body #canvas.read-only-connections svg.jtk-connector.jtk-hover path {
447
+ stroke: var(--color-connectors) !important;
448
+ }
449
+
385
450
  body .plumb-connector.jtk-hover .plumb-arrow {
386
451
  fill: var(--color-success) !important;
387
452
  stroke-width: 0px;
388
453
  z-index: 10;
389
454
  }
390
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
+
391
468
  /* Activity overlays on connections */
392
469
  .jtk-overlay.activity-overlay {
393
470
  background: #f3f3f3;
@@ -409,13 +486,13 @@ export class Editor extends RapidElement {
409
486
  background: #3498db;
410
487
  border: 1px solid #2980b9;
411
488
  border-radius: 12px;
412
- padding: 3px 5px;
489
+ padding: 3px 6px;
413
490
  color: #fff;
414
491
  font-weight: 500;
415
492
  top: -10px;
416
493
  left: -10px;
417
494
  font-size: 13px;
418
- min-width: 22px;
495
+
419
496
  text-align: center;
420
497
  z-index: 600;
421
498
  line-height: 1;
@@ -676,6 +753,18 @@ export class Editor extends RapidElement {
676
753
  width: 100%;
677
754
  }
678
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
+
679
768
  .auto-translate-button {
680
769
  background: var(--color-primary-dark);
681
770
  border: none;
@@ -894,10 +983,11 @@ export class Editor extends RapidElement {
894
983
  }, SAVE_QUIET_TIME);
895
984
  }
896
985
 
897
- private saveChanges(): void {
986
+ private saveChanges(definitionOverride?: FlowDefinition): Promise<void> {
987
+ const definition = definitionOverride || this.definition;
898
988
  // post the flow definition to the server
899
- getStore()
900
- .postJSON(`/flow/revisions/${this.flow}/`, this.definition)
989
+ return getStore()
990
+ .postJSON(`/flow/revisions/${this.flow}/`, definition)
901
991
  .then((response) => {
902
992
  // Update flow info and revision with the response data
903
993
  if (response.json) {
@@ -910,6 +1000,11 @@ export class Editor extends RapidElement {
910
1000
  if (response.json.revision?.revision !== undefined) {
911
1001
  state.setRevision(response.json.revision.revision);
912
1002
  }
1003
+
1004
+ // if the revisions window is open, refresh the list
1005
+ if (!this.revisionsWindowHidden) {
1006
+ this.fetchRevisions();
1007
+ }
913
1008
  }
914
1009
  })
915
1010
  .catch((error) => {
@@ -960,7 +1055,7 @@ export class Editor extends RapidElement {
960
1055
  }
961
1056
 
962
1057
  this.activityTimer = window.setTimeout(() => {
963
- this.fetchActivityData();
1058
+ // this.fetchActivityData();
964
1059
  }, this.activityInterval);
965
1060
  });
966
1061
  }
@@ -1072,6 +1167,8 @@ export class Editor extends RapidElement {
1072
1167
  // ignore right clicks
1073
1168
  if (event.button !== 0) return;
1074
1169
 
1170
+ if (this.isReadOnly()) return;
1171
+
1075
1172
  const element = event.currentTarget as HTMLElement;
1076
1173
  // Only start dragging if clicking on the element itself, not on exits or other interactive elements
1077
1174
  const target = event.target as HTMLElement;
@@ -1141,6 +1238,8 @@ export class Editor extends RapidElement {
1141
1238
  }
1142
1239
 
1143
1240
  private handleCanvasMouseDown(event: MouseEvent): void {
1241
+ if (this.isReadOnly()) return;
1242
+
1144
1243
  const target = event.target as HTMLElement;
1145
1244
  if (target.id === 'canvas' || target.id === 'grid') {
1146
1245
  // Ignore clicks on exits
@@ -1214,6 +1313,7 @@ export class Editor extends RapidElement {
1214
1313
  // Clean up jsPlumb connections for nodes before removing them
1215
1314
  uuids.forEach((uuid) => {
1216
1315
  this.plumber.removeNodeConnections(uuid);
1316
+ this.plumber.removeAllEndpoints(uuid);
1217
1317
  });
1218
1318
 
1219
1319
  // Now remove them from the definition
@@ -1734,6 +1834,11 @@ export class Editor extends RapidElement {
1734
1834
  }
1735
1835
 
1736
1836
  private handleCanvasContextMenu(event: MouseEvent): void {
1837
+ if (this.isReadOnly()) {
1838
+ event.preventDefault();
1839
+ return;
1840
+ }
1841
+
1737
1842
  // Check if we right-clicked on empty canvas space
1738
1843
  const target = event.target as HTMLElement;
1739
1844
  if (target.id !== 'canvas') {
@@ -2653,6 +2758,7 @@ export class Editor extends RapidElement {
2653
2758
  }
2654
2759
 
2655
2760
  this.localizationWindowHidden = false;
2761
+ this.revisionsWindowHidden = true;
2656
2762
 
2657
2763
  const alreadySelected = languages.some(
2658
2764
  (lang) => lang.code === this.languageCode
@@ -2915,6 +3021,223 @@ export class Editor extends RapidElement {
2915
3021
  this.autoTranslating = false;
2916
3022
  }
2917
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
+
2918
3241
  private renderLocalizationWindow(): TemplateResult | string {
2919
3242
  const languages = this.getLocalizationLanguages();
2920
3243
  if (!languages.length) {
@@ -3166,6 +3489,10 @@ export class Editor extends RapidElement {
3166
3489
  });
3167
3490
  }
3168
3491
 
3492
+ private isReadOnly(): boolean {
3493
+ return this.viewingRevision !== null || this.isTranslating;
3494
+ }
3495
+
3169
3496
  public render(): TemplateResult {
3170
3497
  // we have to embed our own style since we are in light DOM
3171
3498
  const style = html`<style>
@@ -3175,20 +3502,30 @@ export class Editor extends RapidElement {
3175
3502
 
3176
3503
  const stickies = this.definition?._ui?.stickies || {};
3177
3504
 
3178
- return html`${style} ${this.renderLocalizationWindow()}
3179
- ${this.renderAutoTranslateDialog()}
3505
+ return html`${style} ${this.renderRevisionsWindow()}
3506
+ ${this.renderLocalizationWindow()} ${this.renderAutoTranslateDialog()}
3180
3507
  <div id="editor">
3181
3508
  <div
3182
3509
  id="grid"
3510
+ class="${this.viewingRevision ? 'viewing-revision' : ''}"
3183
3511
  style="min-width:100%;width:${this.canvasSize.width}px; height:${this
3184
3512
  .canvasSize.height}px"
3185
3513
  >
3186
- <div id="canvas">
3514
+ <div
3515
+ id="canvas"
3516
+ class="${getClasses({
3517
+ 'viewing-revision': !!this.viewingRevision,
3518
+ 'read-only-connections':
3519
+ !!this.viewingRevision || this.isTranslating
3520
+ })}"
3521
+ >
3187
3522
  ${this.definition
3188
3523
  ? repeat(
3189
- this.definition.nodes,
3524
+ [...this.definition.nodes].sort((a, b) =>
3525
+ a.uuid.localeCompare(b.uuid)
3526
+ ),
3190
3527
  (node) => node.uuid,
3191
- (node, index) => {
3528
+ (node) => {
3192
3529
  const position = this.definition._ui?.nodes[node.uuid]
3193
3530
  ?.position || {
3194
3531
  left: 0,
@@ -3202,7 +3539,9 @@ export class Editor extends RapidElement {
3202
3539
  const selected = this.selectedItems.has(node.uuid);
3203
3540
 
3204
3541
  // first node is the flow start (nodes are sorted by position)
3205
- const isFlowStart = index === 0;
3542
+ const isFlowStart =
3543
+ this.definition.nodes.length > 0 &&
3544
+ this.definition.nodes[0].uuid === node.uuid;
3206
3545
 
3207
3546
  return html`<temba-flow-node
3208
3547
  class="draggable ${dragging ? 'dragging' : ''} ${selected
@@ -3262,10 +3601,12 @@ export class Editor extends RapidElement {
3262
3601
  : ''}
3263
3602
 
3264
3603
  <temba-canvas-menu></temba-canvas-menu>
3265
- <temba-node-type-selector
3266
- .flowType=${this.flowType}
3267
- .features=${this.features}
3268
- ></temba-node-type-selector>
3269
- ${this.renderLocalizationTab()} `;
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()} `;
3270
3611
  }
3271
3612
  }
@@ -1331,7 +1331,6 @@ export class NodeEditor extends RapidElement {
1331
1331
  return html`
1332
1332
  <div class="optional-field-link">
1333
1333
  <a
1334
- href="#"
1335
1334
  @click="${(e: Event) => {
1336
1335
  e.preventDefault();
1337
1336
  this.revealOptionalField(fieldName);