@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.
Files changed (88) hide show
  1. package/.devcontainer/Dockerfile +0 -9
  2. package/.devcontainer/devcontainer.json +8 -3
  3. package/.github/workflows/build.yml +6 -1
  4. package/.github/workflows/cla.yml +1 -1
  5. package/.github/workflows/publish.yml +6 -1
  6. package/CHANGELOG.md +42 -0
  7. package/dist/locales/es.js +5 -5
  8. package/dist/locales/es.js.map +1 -1
  9. package/dist/locales/fr.js +5 -5
  10. package/dist/locales/fr.js.map +1 -1
  11. package/dist/locales/locale-codes.js +11 -2
  12. package/dist/locales/locale-codes.js.map +1 -1
  13. package/dist/locales/pt.js +5 -5
  14. package/dist/locales/pt.js.map +1 -1
  15. package/dist/temba-components.js +445 -278
  16. package/dist/temba-components.js.map +1 -1
  17. package/out-tsc/src/display/FloatingTab.js +16 -8
  18. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  19. package/out-tsc/src/flow/CanvasMenu.js +33 -15
  20. package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
  21. package/out-tsc/src/flow/CanvasNode.js +49 -24
  22. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  23. package/out-tsc/src/flow/Editor.js +583 -70
  24. package/out-tsc/src/flow/Editor.js.map +1 -1
  25. package/out-tsc/src/flow/NodeTypeSelector.js +13 -11
  26. package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
  27. package/out-tsc/src/flow/Plumber.js +110 -64
  28. package/out-tsc/src/flow/Plumber.js.map +1 -1
  29. package/out-tsc/src/flow/actions/set_contact_field.js +5 -1
  30. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  31. package/out-tsc/src/list/RunList.js +2 -1
  32. package/out-tsc/src/list/RunList.js.map +1 -1
  33. package/out-tsc/src/list/TicketList.js +2 -1
  34. package/out-tsc/src/list/TicketList.js.map +1 -1
  35. package/out-tsc/src/locales/es.js +5 -5
  36. package/out-tsc/src/locales/es.js.map +1 -1
  37. package/out-tsc/src/locales/fr.js +5 -5
  38. package/out-tsc/src/locales/fr.js.map +1 -1
  39. package/out-tsc/src/locales/locale-codes.js +11 -2
  40. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  41. package/out-tsc/src/locales/pt.js +5 -5
  42. package/out-tsc/src/locales/pt.js.map +1 -1
  43. package/out-tsc/src/simulator/Simulator.js +11 -4
  44. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  45. package/out-tsc/src/store/AppState.js +17 -2
  46. package/out-tsc/src/store/AppState.js.map +1 -1
  47. package/out-tsc/test/temba-contact-fields.test.js +3 -3
  48. package/out-tsc/test/temba-contact-fields.test.js.map +1 -1
  49. package/out-tsc/test/temba-flow-editor-node.test.js +3 -1
  50. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  51. package/out-tsc/test/temba-flow-editor-revisions.test.js +106 -0
  52. package/out-tsc/test/temba-flow-editor-revisions.test.js.map +1 -0
  53. package/out-tsc/test/temba-flow-editor.test.js +14 -10
  54. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  55. package/out-tsc/test/temba-flow-plumber-connections.test.js +7 -1
  56. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  57. package/out-tsc/test/temba-flow-plumber.test.js +6 -0
  58. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  59. package/out-tsc/test/temba-select.test.js +1 -0
  60. package/out-tsc/test/temba-select.test.js.map +1 -1
  61. package/package.json +1 -1
  62. package/screenshots/truth/floating-tab/gray.png +0 -0
  63. package/screenshots/truth/floating-tab/green.png +0 -0
  64. package/screenshots/truth/floating-tab/purple.png +0 -0
  65. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  66. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  67. package/src/display/FloatingTab.ts +18 -8
  68. package/src/flow/CanvasMenu.ts +38 -16
  69. package/src/flow/CanvasNode.ts +62 -29
  70. package/src/flow/Editor.ts +699 -74
  71. package/src/flow/NodeTypeSelector.ts +13 -11
  72. package/src/flow/Plumber.ts +123 -69
  73. package/src/flow/actions/set_contact_field.ts +5 -1
  74. package/src/list/RunList.ts +2 -1
  75. package/src/list/TicketList.ts +2 -1
  76. package/src/locales/es.ts +18 -13
  77. package/src/locales/fr.ts +18 -13
  78. package/src/locales/locale-codes.ts +11 -2
  79. package/src/locales/pt.ts +18 -13
  80. package/src/simulator/Simulator.ts +11 -5
  81. package/src/store/AppState.ts +18 -2
  82. package/test/temba-contact-fields.test.ts +8 -3
  83. package/test/temba-flow-editor-node.test.ts +3 -1
  84. package/test/temba-flow-editor-revisions.test.ts +134 -0
  85. package/test/temba-flow-editor.test.ts +16 -10
  86. package/test/temba-flow-plumber-connections.test.ts +7 -1
  87. package/test/temba-flow-plumber.test.ts +6 -0
  88. package/test/temba-select.test.ts +1 -0
@@ -10,6 +10,7 @@ export class FloatingTab extends RapidElement {
10
10
  this.color = '#6B7280';
11
11
  this.top = -1; // -1 means auto-calculate position
12
12
  this.hidden = false;
13
+ this.autoPositioned = false;
13
14
  }
14
15
  static get styles() {
15
16
  return css `
@@ -72,7 +73,13 @@ export class FloatingTab extends RapidElement {
72
73
  connectedCallback() {
73
74
  super.connectedCallback();
74
75
  FloatingTab.allTabs.push(this);
75
- this.updatePosition();
76
+ }
77
+ firstUpdated() {
78
+ // only auto-calculate position if no top was provided (still at default -1)
79
+ if (this.top === -1) {
80
+ this.autoPositioned = true;
81
+ this.updatePosition();
82
+ }
76
83
  }
77
84
  disconnectedCallback() {
78
85
  super.disconnectedCallback();
@@ -80,8 +87,12 @@ export class FloatingTab extends RapidElement {
80
87
  if (index > -1) {
81
88
  FloatingTab.allTabs.splice(index, 1);
82
89
  }
83
- // update positions of remaining tabs
84
- FloatingTab.allTabs.forEach((tab) => tab.updatePosition());
90
+ // update positions of remaining tabs that use auto-positioning
91
+ FloatingTab.allTabs.forEach((tab) => {
92
+ if (tab.autoPositioned) {
93
+ tab.updatePosition();
94
+ }
95
+ });
85
96
  }
86
97
  updatePosition() {
87
98
  // auto-calculate position based on index
@@ -90,8 +101,8 @@ export class FloatingTab extends RapidElement {
90
101
  this.top = 100; // default fallback
91
102
  }
92
103
  else {
93
- // start at 100px and stack with 10px gap between tabs
94
- this.top = 100 + index * (FloatingTab.TAB_HEIGHT + 0);
104
+ // start at 150px and stack with TAB_HEIGHT gap between tabs
105
+ this.top = 150 + index * (FloatingTab.TAB_HEIGHT + 0);
95
106
  }
96
107
  }
97
108
  updated(changes) {
@@ -99,9 +110,6 @@ export class FloatingTab extends RapidElement {
99
110
  if (changes.has('hidden')) {
100
111
  this.classList.toggle('hidden', this.hidden);
101
112
  }
102
- if (changes.has('top')) {
103
- this.updatePosition();
104
- }
105
113
  }
106
114
  handleClick() {
107
115
  // hide all tabs when one is clicked
@@ -1 +1 @@
1
- {"version":3,"file":"FloatingTab.js","sourceRoot":"","sources":["../../../src/display/FloatingTab.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, PropertyValueMap, TemplateResult } from 'lit';\nimport { property } from 'lit/decorators.js';\nimport { RapidElement } from '../RapidElement';\nimport { CustomEventType } from '../interfaces';\nimport { getClasses } from '../utils';\n\nexport class FloatingTab extends RapidElement {\n static get styles() {\n return css`\n .tab.hidden {\n transform: translateX(100%);\n }\n .tab {\n position: fixed;\n right: 0;\n z-index: 4998;\n transition: transform var(--transition-duration, 300ms) ease-in-out;\n display: flex;\n align-items: center;\n padding: 12px;\n border-top-left-radius: 8px;\n border-bottom-left-radius: 8px;\n cursor: pointer;\n box-shadow: -2px 2px 8px rgba(0, 0, 0, 0.2);\n transition: all calc(var(--transition-duration, 300ms) * 0.7)\n ease-in-out;\n user-select: none;\n }\n\n .tab:hover {\n padding-right: 16px;\n box-shadow: -4px 4px 12px rgba(0, 0, 0, 0.3);\n }\n\n .icon-container {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n }\n\n temba-icon {\n --icon-color: white;\n }\n\n .label {\n color: white;\n font-weight: 500;\n font-size: 16px;\n max-width: 0;\n overflow: hidden;\n white-space: nowrap;\n margin-left: 0;\n opacity: 0;\n transition: all var(--transition-duration, 300ms) ease-in-out;\n }\n\n .tab:hover .label {\n max-width: 200px;\n margin-left: 12px;\n opacity: 1;\n }\n `;\n }\n\n static TAB_HEIGHT = 50; // height of tab for auto-stacking\n static allTabs: FloatingTab[] = [];\n\n @property({ type: String })\n icon: string;\n\n @property({ type: String })\n label: string;\n\n @property({ type: String })\n color = '#6B7280';\n\n @property({ type: Number })\n top = -1; // -1 means auto-calculate position\n\n @property({ type: Boolean })\n hidden = false;\n\n connectedCallback() {\n super.connectedCallback();\n FloatingTab.allTabs.push(this);\n this.updatePosition();\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n const index = FloatingTab.allTabs.indexOf(this);\n if (index > -1) {\n FloatingTab.allTabs.splice(index, 1);\n }\n // update positions of remaining tabs\n FloatingTab.allTabs.forEach((tab) => tab.updatePosition());\n }\n\n private updatePosition() {\n // auto-calculate position based on index\n const index = FloatingTab.allTabs.indexOf(this);\n if (index === -1) {\n this.top = 100; // default fallback\n } else {\n // start at 100px and stack with 10px gap between tabs\n this.top = 100 + index * (FloatingTab.TAB_HEIGHT + 0);\n }\n }\n\n public updated(\n changes: PropertyValueMap<any> | Map<PropertyKey, unknown>\n ): void {\n super.updated(changes);\n if (changes.has('hidden')) {\n this.classList.toggle('hidden', this.hidden);\n }\n if (changes.has('top')) {\n this.updatePosition();\n }\n }\n\n private handleClick() {\n // hide all tabs when one is clicked\n FloatingTab.allTabs.forEach((tab) => {\n tab.hidden = true;\n });\n\n this.fireCustomEvent(CustomEventType.ButtonClicked, {\n action: 'toggle'\n });\n }\n\n public static showAllTabs() {\n FloatingTab.allTabs.forEach((tab) => {\n tab.hidden = false;\n });\n }\n\n public static hideAllTabs() {\n FloatingTab.allTabs.forEach((tab) => {\n tab.hidden = true;\n });\n }\n\n public render(): TemplateResult {\n const tabStyle = `\n background-color: ${this.color};\n top: ${this.top}px;\n `;\n\n const classes = getClasses({\n tab: true,\n hidden: this.hidden\n });\n\n return html`\n <div class=\"${classes}\" style=\"${tabStyle}\" @click=${this.handleClick}>\n <div class=\"icon-container\">\n ${this.icon\n ? html`<temba-icon size=\"1.5\" name=\"${this.icon}\"></temba-icon>`\n : ''}\n </div>\n <div class=\"label\">${this.label}</div>\n </div>\n `;\n }\n}\n"]}
1
+ {"version":3,"file":"FloatingTab.js","sourceRoot":"","sources":["../../../src/display/FloatingTab.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, PropertyValueMap, TemplateResult } from 'lit';\nimport { property } from 'lit/decorators.js';\nimport { RapidElement } from '../RapidElement';\nimport { CustomEventType } from '../interfaces';\nimport { getClasses } from '../utils';\n\nexport class FloatingTab extends RapidElement {\n static get styles() {\n return css`\n .tab.hidden {\n transform: translateX(100%);\n }\n .tab {\n position: fixed;\n right: 0;\n z-index: 4998;\n transition: transform var(--transition-duration, 300ms) ease-in-out;\n display: flex;\n align-items: center;\n padding: 12px;\n border-top-left-radius: 8px;\n border-bottom-left-radius: 8px;\n cursor: pointer;\n box-shadow: -2px 2px 8px rgba(0, 0, 0, 0.2);\n transition: all calc(var(--transition-duration, 300ms) * 0.7)\n ease-in-out;\n user-select: none;\n }\n\n .tab:hover {\n padding-right: 16px;\n box-shadow: -4px 4px 12px rgba(0, 0, 0, 0.3);\n }\n\n .icon-container {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n }\n\n temba-icon {\n --icon-color: white;\n }\n\n .label {\n color: white;\n font-weight: 500;\n font-size: 16px;\n max-width: 0;\n overflow: hidden;\n white-space: nowrap;\n margin-left: 0;\n opacity: 0;\n transition: all var(--transition-duration, 300ms) ease-in-out;\n }\n\n .tab:hover .label {\n max-width: 200px;\n margin-left: 12px;\n opacity: 1;\n }\n `;\n }\n\n static TAB_HEIGHT = 50; // height of tab for auto-stacking\n static allTabs: FloatingTab[] = [];\n\n @property({ type: String })\n icon: string;\n\n @property({ type: String })\n label: string;\n\n @property({ type: String })\n color = '#6B7280';\n\n @property({ type: Number })\n top = -1; // -1 means auto-calculate position\n\n @property({ type: Boolean })\n hidden = false;\n\n private autoPositioned = false;\n\n connectedCallback() {\n super.connectedCallback();\n FloatingTab.allTabs.push(this);\n }\n\n protected firstUpdated(): void {\n // only auto-calculate position if no top was provided (still at default -1)\n if (this.top === -1) {\n this.autoPositioned = true;\n this.updatePosition();\n }\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n const index = FloatingTab.allTabs.indexOf(this);\n if (index > -1) {\n FloatingTab.allTabs.splice(index, 1);\n }\n // update positions of remaining tabs that use auto-positioning\n FloatingTab.allTabs.forEach((tab) => {\n if (tab.autoPositioned) {\n tab.updatePosition();\n }\n });\n }\n\n private updatePosition() {\n // auto-calculate position based on index\n const index = FloatingTab.allTabs.indexOf(this);\n if (index === -1) {\n this.top = 100; // default fallback\n } else {\n // start at 150px and stack with TAB_HEIGHT gap between tabs\n this.top = 150 + index * (FloatingTab.TAB_HEIGHT + 0);\n }\n }\n\n public updated(\n changes: PropertyValueMap<any> | Map<PropertyKey, unknown>\n ): void {\n super.updated(changes);\n if (changes.has('hidden')) {\n this.classList.toggle('hidden', this.hidden);\n }\n }\n\n private handleClick() {\n // hide all tabs when one is clicked\n FloatingTab.allTabs.forEach((tab) => {\n tab.hidden = true;\n });\n\n this.fireCustomEvent(CustomEventType.ButtonClicked, {\n action: 'toggle'\n });\n }\n\n public static showAllTabs() {\n FloatingTab.allTabs.forEach((tab) => {\n tab.hidden = false;\n });\n }\n\n public static hideAllTabs() {\n FloatingTab.allTabs.forEach((tab) => {\n tab.hidden = true;\n });\n }\n\n public render(): TemplateResult {\n const tabStyle = `\n background-color: ${this.color};\n top: ${this.top}px;\n `;\n\n const classes = getClasses({\n tab: true,\n hidden: this.hidden\n });\n\n return html`\n <div class=\"${classes}\" style=\"${tabStyle}\" @click=${this.handleClick}>\n <div class=\"icon-container\">\n ${this.icon\n ? html`<temba-icon size=\"1.5\" name=\"${this.icon}\"></temba-icon>`\n : ''}\n </div>\n <div class=\"label\">${this.label}</div>\n </div>\n `;\n }\n}\n"]}
@@ -13,6 +13,7 @@ export class CanvasMenu extends RapidElement {
13
13
  this.x = 0;
14
14
  this.y = 0;
15
15
  this.open = false;
16
+ this.showStickyNote = true;
16
17
  this.clickPosition = { x: 0, y: 0 };
17
18
  }
18
19
  static get styles() {
@@ -94,10 +95,11 @@ export class CanvasMenu extends RapidElement {
94
95
  document.removeEventListener('click', this._clickOutsideHandler);
95
96
  }
96
97
  }
97
- show(x, y, clickPosition) {
98
+ show(x, y, clickPosition, showStickyNote = true) {
98
99
  this.x = x;
99
100
  this.y = y;
100
101
  this.clickPosition = clickPosition;
102
+ this.showStickyNote = showStickyNote;
101
103
  this.open = true;
102
104
  // Adjust position after menu renders to ensure it fits on screen
103
105
  requestAnimationFrame(() => {
@@ -129,15 +131,22 @@ export class CanvasMenu extends RapidElement {
129
131
  this.y = adjustedY;
130
132
  }
131
133
  }
132
- close() {
133
- this.open = false;
134
+ close(fireCanceledEvent = true) {
135
+ if (this.open) {
136
+ this.open = false;
137
+ // Fire close event so parent can clean up, but only if not from a selection
138
+ if (fireCanceledEvent) {
139
+ this.fireCustomEvent(CustomEventType.Canceled, {});
140
+ }
141
+ }
134
142
  }
135
143
  handleMenuItemClick(action) {
136
144
  this.fireCustomEvent(CustomEventType.Selection, {
137
145
  action,
138
146
  position: this.clickPosition
139
147
  });
140
- this.close();
148
+ // Close without firing canceled event since we made a selection
149
+ this.close(false);
141
150
  }
142
151
  render() {
143
152
  if (!this.open) {
@@ -169,18 +178,24 @@ export class CanvasMenu extends RapidElement {
169
178
  </div>
170
179
  </div>
171
180
 
172
- <div class="divider"></div>
181
+ ${this.showStickyNote
182
+ ? html `
183
+ <div class="divider"></div>
173
184
 
174
- <div
175
- class="menu-item"
176
- @click=${() => this.handleMenuItemClick('sticky')}
177
- >
178
- <temba-icon name="note" size="1.25"></temba-icon>
179
- <div class="menu-item-content">
180
- <div class="menu-item-title">Add Sticky Note</div>
181
- <div class="menu-item-description">Add a note to the canvas</div>
182
- </div>
183
- </div>
185
+ <div
186
+ class="menu-item"
187
+ @click=${() => this.handleMenuItemClick('sticky')}
188
+ >
189
+ <temba-icon name="note" size="1.25"></temba-icon>
190
+ <div class="menu-item-content">
191
+ <div class="menu-item-title">Add Sticky Note</div>
192
+ <div class="menu-item-description">
193
+ Add a note to the canvas
194
+ </div>
195
+ </div>
196
+ </div>
197
+ `
198
+ : ''}
184
199
  </div>
185
200
  `;
186
201
  }
@@ -194,6 +209,9 @@ __decorate([
194
209
  __decorate([
195
210
  property({ type: Boolean })
196
211
  ], CanvasMenu.prototype, "open", void 0);
212
+ __decorate([
213
+ property({ type: Boolean })
214
+ ], CanvasMenu.prototype, "showStickyNote", void 0);
197
215
  __decorate([
198
216
  state()
199
217
  ], CanvasMenu.prototype, "clickPosition", void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"CanvasMenu.js","sourceRoot":"","sources":["../../../src/flow/CanvasMenu.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, PropertyValueMap, TemplateResult } from 'lit';\nimport { property, state } from 'lit/decorators.js';\nimport { RapidElement } from '../RapidElement';\nimport { CustomEventType } from '../interfaces';\n\n/**\n * Event detail for canvas menu selection\n */\nexport interface CanvasMenuSelection {\n action: 'sticky' | 'action' | 'split';\n position: { x: number; y: number };\n}\n\n/**\n * CanvasMenu - A popup menu for adding items to the flow canvas\n * Displayed when double-clicking on empty canvas space\n */\nexport class CanvasMenu extends RapidElement {\n static get styles() {\n return css`\n :host {\n position: fixed;\n z-index: 10000;\n display: block;\n pointer-events: none;\n }\n\n .menu {\n position: fixed;\n background: white;\n border-radius: var(--curvature);\n box-shadow: var(--dropdown-shadow);\n padding: 0.5em 0;\n width: 265px;\n pointer-events: auto;\n }\n\n .menu-item {\n padding: 0.75em 1.5em;\n cursor: pointer;\n display: flex;\n align-items: flex-start;\n gap: 0.75em;\n transition: background-color 0.15s ease;\n }\n\n .menu-item:hover {\n background-color: rgba(0, 0, 0, 0.05);\n }\n\n .menu-item temba-icon {\n --icon-color: var(--color-text);\n margin-top: 0.15em;\n }\n\n .menu-item-content {\n display: flex;\n flex-direction: column;\n gap: 0.15em;\n }\n\n .menu-item-title {\n font-weight: 500;\n font-size: 1rem;\n color: var(--color-text-dark);\n }\n\n .menu-item-description {\n font-size: 0.85rem;\n color: var(--color-text);\n }\n\n .divider {\n height: 1px;\n background: rgba(0, 0, 0, 0.1);\n margin: 0.5em 0;\n }\n `;\n }\n\n @property({ type: Number })\n public x = 0;\n\n @property({ type: Number })\n public y = 0;\n\n @property({ type: Boolean })\n public open = false;\n\n @state()\n private clickPosition = { x: 0, y: 0 };\n\n protected firstUpdated(\n _changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>\n ): void {\n super.firstUpdated(_changedProperties);\n\n // Close menu when clicking outside\n const handleClickOutside = (e: MouseEvent) => {\n if (this.open && !this.contains(e.target as Node)) {\n this.close();\n }\n };\n\n document.addEventListener('click', handleClickOutside);\n\n // Store cleanup function\n (this as any)._clickOutsideHandler = handleClickOutside;\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n if ((this as any)._clickOutsideHandler) {\n document.removeEventListener('click', (this as any)._clickOutsideHandler);\n }\n }\n\n public show(x: number, y: number, clickPosition: { x: number; y: number }) {\n this.x = x;\n this.y = y;\n this.clickPosition = clickPosition;\n this.open = true;\n\n // Adjust position after menu renders to ensure it fits on screen\n requestAnimationFrame(() => {\n this.adjustPosition();\n });\n }\n\n private adjustPosition(): void {\n const menuElement = this.shadowRoot?.querySelector('.menu') as HTMLElement;\n if (!menuElement) return;\n\n const menuRect = menuElement.getBoundingClientRect();\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n const margin = 10; // margin from viewport edges\n\n let adjustedX = this.x;\n let adjustedY = this.y;\n\n // Check if menu goes off the right edge\n if (this.x + menuRect.width + margin > viewportWidth) {\n adjustedX = viewportWidth - menuRect.width - margin;\n }\n\n // Check if menu goes off the bottom edge\n if (this.y + menuRect.height + margin > viewportHeight) {\n adjustedY = viewportHeight - menuRect.height - margin;\n }\n\n // Update position if needed\n if (adjustedX !== this.x || adjustedY !== this.y) {\n this.x = adjustedX;\n this.y = adjustedY;\n }\n }\n\n public close() {\n this.open = false;\n }\n\n private handleMenuItemClick(action: 'sticky' | 'action' | 'split') {\n this.fireCustomEvent(CustomEventType.Selection, {\n action,\n position: this.clickPosition\n } as CanvasMenuSelection);\n this.close();\n }\n\n public render(): TemplateResult {\n if (!this.open) {\n return html``;\n }\n\n return html`\n <div class=\"menu\" style=\"left: ${this.x}px; top: ${this.y}px;\">\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('action')}\n >\n <temba-icon name=\"action\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-content\">\n <div class=\"menu-item-title\">Add Action</div>\n <div class=\"menu-item-description\">\n Send messages, update contacts\n </div>\n </div>\n </div>\n\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('split')}\n >\n <temba-icon name=\"split\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-content\">\n <div class=\"menu-item-title\">Add Split</div>\n <div class=\"menu-item-description\">Branch based on conditions</div>\n </div>\n </div>\n\n <div class=\"divider\"></div>\n\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('sticky')}\n >\n <temba-icon name=\"note\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-content\">\n <div class=\"menu-item-title\">Add Sticky Note</div>\n <div class=\"menu-item-description\">Add a note to the canvas</div>\n </div>\n </div>\n </div>\n `;\n }\n}\n"]}
1
+ {"version":3,"file":"CanvasMenu.js","sourceRoot":"","sources":["../../../src/flow/CanvasMenu.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, PropertyValueMap, TemplateResult } from 'lit';\nimport { property, state } from 'lit/decorators.js';\nimport { RapidElement } from '../RapidElement';\nimport { CustomEventType } from '../interfaces';\n\n/**\n * Event detail for canvas menu selection\n */\nexport interface CanvasMenuSelection {\n action: 'sticky' | 'action' | 'split';\n position: { x: number; y: number };\n}\n\n/**\n * CanvasMenu - A popup menu for adding items to the flow canvas\n * Displayed when double-clicking on empty canvas space\n */\nexport class CanvasMenu extends RapidElement {\n static get styles() {\n return css`\n :host {\n position: fixed;\n z-index: 10000;\n display: block;\n pointer-events: none;\n }\n\n .menu {\n position: fixed;\n background: white;\n border-radius: var(--curvature);\n box-shadow: var(--dropdown-shadow);\n padding: 0.5em 0;\n width: 265px;\n pointer-events: auto;\n }\n\n .menu-item {\n padding: 0.75em 1.5em;\n cursor: pointer;\n display: flex;\n align-items: flex-start;\n gap: 0.75em;\n transition: background-color 0.15s ease;\n }\n\n .menu-item:hover {\n background-color: rgba(0, 0, 0, 0.05);\n }\n\n .menu-item temba-icon {\n --icon-color: var(--color-text);\n margin-top: 0.15em;\n }\n\n .menu-item-content {\n display: flex;\n flex-direction: column;\n gap: 0.15em;\n }\n\n .menu-item-title {\n font-weight: 500;\n font-size: 1rem;\n color: var(--color-text-dark);\n }\n\n .menu-item-description {\n font-size: 0.85rem;\n color: var(--color-text);\n }\n\n .divider {\n height: 1px;\n background: rgba(0, 0, 0, 0.1);\n margin: 0.5em 0;\n }\n `;\n }\n\n @property({ type: Number })\n public x = 0;\n\n @property({ type: Number })\n public y = 0;\n\n @property({ type: Boolean })\n public open = false;\n\n @property({ type: Boolean })\n public showStickyNote = true;\n\n @state()\n private clickPosition = { x: 0, y: 0 };\n\n protected firstUpdated(\n _changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>\n ): void {\n super.firstUpdated(_changedProperties);\n\n // Close menu when clicking outside\n const handleClickOutside = (e: MouseEvent) => {\n if (this.open && !this.contains(e.target as Node)) {\n this.close();\n }\n };\n\n document.addEventListener('click', handleClickOutside);\n\n // Store cleanup function\n (this as any)._clickOutsideHandler = handleClickOutside;\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n if ((this as any)._clickOutsideHandler) {\n document.removeEventListener('click', (this as any)._clickOutsideHandler);\n }\n }\n\n public show(\n x: number,\n y: number,\n clickPosition: { x: number; y: number },\n showStickyNote: boolean = true\n ) {\n this.x = x;\n this.y = y;\n this.clickPosition = clickPosition;\n this.showStickyNote = showStickyNote;\n this.open = true;\n\n // Adjust position after menu renders to ensure it fits on screen\n requestAnimationFrame(() => {\n this.adjustPosition();\n });\n }\n\n private adjustPosition(): void {\n const menuElement = this.shadowRoot?.querySelector('.menu') as HTMLElement;\n if (!menuElement) return;\n\n const menuRect = menuElement.getBoundingClientRect();\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n const margin = 10; // margin from viewport edges\n\n let adjustedX = this.x;\n let adjustedY = this.y;\n\n // Check if menu goes off the right edge\n if (this.x + menuRect.width + margin > viewportWidth) {\n adjustedX = viewportWidth - menuRect.width - margin;\n }\n\n // Check if menu goes off the bottom edge\n if (this.y + menuRect.height + margin > viewportHeight) {\n adjustedY = viewportHeight - menuRect.height - margin;\n }\n\n // Update position if needed\n if (adjustedX !== this.x || adjustedY !== this.y) {\n this.x = adjustedX;\n this.y = adjustedY;\n }\n }\n\n public close(fireCanceledEvent: boolean = true) {\n if (this.open) {\n this.open = false;\n // Fire close event so parent can clean up, but only if not from a selection\n if (fireCanceledEvent) {\n this.fireCustomEvent(CustomEventType.Canceled, {});\n }\n }\n }\n\n private handleMenuItemClick(action: 'sticky' | 'action' | 'split') {\n this.fireCustomEvent(CustomEventType.Selection, {\n action,\n position: this.clickPosition\n } as CanvasMenuSelection);\n // Close without firing canceled event since we made a selection\n this.close(false);\n }\n\n public render(): TemplateResult {\n if (!this.open) {\n return html``;\n }\n\n return html`\n <div class=\"menu\" style=\"left: ${this.x}px; top: ${this.y}px;\">\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('action')}\n >\n <temba-icon name=\"action\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-content\">\n <div class=\"menu-item-title\">Add Action</div>\n <div class=\"menu-item-description\">\n Send messages, update contacts\n </div>\n </div>\n </div>\n\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('split')}\n >\n <temba-icon name=\"split\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-content\">\n <div class=\"menu-item-title\">Add Split</div>\n <div class=\"menu-item-description\">Branch based on conditions</div>\n </div>\n </div>\n\n ${this.showStickyNote\n ? html`\n <div class=\"divider\"></div>\n\n <div\n class=\"menu-item\"\n @click=${() => this.handleMenuItemClick('sticky')}\n >\n <temba-icon name=\"note\" size=\"1.25\"></temba-icon>\n <div class=\"menu-item-content\">\n <div class=\"menu-item-title\">Add Sticky Note</div>\n <div class=\"menu-item-description\">\n Add a note to the canvas\n </div>\n </div>\n </div>\n `\n : ''}\n </div>\n `;\n }\n}\n"]}
@@ -22,11 +22,8 @@ export class CanvasNode extends RapidElement {
22
22
  box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
23
23
  min-width: 200px;
24
24
  border-radius: var(--curvature);
25
-
26
25
  color: #333;
27
- cursor: move;
28
26
  user-select: none;
29
-
30
27
  }
31
28
 
32
29
  /* Flow start indicator */
@@ -109,7 +106,7 @@ export class CanvasNode extends RapidElement {
109
106
  opacity: 1;
110
107
  }
111
108
 
112
- .translating-hidden {
109
+ .read-only-hidden {
113
110
  visibility: hidden !important;
114
111
  pointer-events: none !important;
115
112
  }
@@ -304,6 +301,12 @@ export class CanvasNode extends RapidElement {
304
301
  .exit.connected:hover {
305
302
  background-color: var(--color-connectors, #e6e6e6);
306
303
  }
304
+
305
+ .exit.connected.read-only, .exit.connected.read-only:hover {
306
+ background-color: #fff;
307
+ pointer-events: none !important;
308
+ cursor: default;
309
+ }
307
310
 
308
311
  .exit.removing, .exit.removing:hover {
309
312
  background-color: var(--color-error);
@@ -405,6 +408,7 @@ export class CanvasNode extends RapidElement {
405
408
  super();
406
409
  // Track exits that are in "removing" state
407
410
  this.exitRemovalTimeouts = new Map();
411
+ this.connectionTimeout = null;
408
412
  // Set of exit UUIDs that are in the removing state
409
413
  this.exitRemovingState = new Set();
410
414
  // Track actions that are in "removing" state
@@ -460,19 +464,25 @@ export class CanvasNode extends RapidElement {
460
464
  if (changes.has('node')) {
461
465
  // Only proceed if plumber is available (for tests that don't set it up)
462
466
  if (this.plumber) {
463
- this.plumber.removeNodeConnections(this.node.uuid);
467
+ if (this.connectionTimeout) {
468
+ clearTimeout(this.connectionTimeout);
469
+ }
470
+ // Pass exit IDs explicitly to avoid DOM querying dependency
471
+ const exitIds = this.node.exits.map((e) => e.uuid);
472
+ this.plumber.removeNodeConnections(this.node.uuid, exitIds);
464
473
  // make our initial connections
465
- for (const exit of this.node.exits) {
466
- if (!exit.destination_uuid) {
467
- // if we have no destination, then we are a source
468
- // so make our source endpoint
474
+ // We use setTimeout to allow for DOM updates to complete before querying for exits
475
+ this.connectionTimeout = window.setTimeout(() => {
476
+ for (const exit of this.node.exits) {
469
477
  this.plumber.makeSource(exit.uuid);
478
+ if (exit.destination_uuid) {
479
+ this.plumber.connectIds(this.node.uuid, exit.uuid, exit.destination_uuid);
480
+ }
470
481
  }
471
- else {
472
- this.plumber.connectIds(this.node.uuid, exit.uuid, exit.destination_uuid);
473
- }
474
- }
475
- this.plumber.revalidate([this.node.uuid]);
482
+ // Note: revalidation is handled by plumber's processPendingConnections which calls repaintEverything
483
+ this.connectionTimeout = null;
484
+ this.plumber.revalidate([this.node.uuid]);
485
+ }, 0);
476
486
  }
477
487
  const ele = this.parentElement;
478
488
  if (ele) {
@@ -482,6 +492,14 @@ export class CanvasNode extends RapidElement {
482
492
  }
483
493
  }
484
494
  disconnectedCallback() {
495
+ // Force cleanup of connections for this node
496
+ if (this.plumber && this.node) {
497
+ if (this.connectionTimeout) {
498
+ clearTimeout(this.connectionTimeout);
499
+ this.connectionTimeout = null;
500
+ }
501
+ this.plumber.forgetNode(this.node.uuid);
502
+ }
485
503
  // Remove the event listener when the component is removed
486
504
  super.disconnectedCallback();
487
505
  // Remove external drag event listeners
@@ -503,6 +521,10 @@ export class CanvasNode extends RapidElement {
503
521
  // Clear the removing state
504
522
  this.exitRemovingState.clear();
505
523
  this.actionRemovingState.clear();
524
+ // only proceed if plumber is available (for tests that don't set it up)
525
+ if (this.plumber) {
526
+ this.plumber.removeNodeConnections(this.node.uuid, this.node.exits.map((e) => e.uuid));
527
+ }
506
528
  }
507
529
  handleExitClick(event, exit) {
508
530
  event.preventDefault();
@@ -1047,23 +1069,19 @@ export class CanvasNode extends RapidElement {
1047
1069
  return html `<div class="cn-title" style="background:${color}">
1048
1070
  ${((_c = this.ui) === null || _c === void 0 ? void 0 : _c.type) === 'execute_actions'
1049
1071
  ? html `<temba-icon
1050
- class="drag-handle ${this.isTranslating
1051
- ? 'translating-hidden'
1052
- : ''}"
1072
+ class="drag-handle ${this.isReadOnly() ? 'read-only-hidden' : ''}"
1053
1073
  name="sort"
1054
1074
  ></temba-icon>`
1055
1075
  : ((_e = (_d = this.node) === null || _d === void 0 ? void 0 : _d.actions) === null || _e === void 0 ? void 0 : _e.length) > 1
1056
1076
  ? html `<temba-icon
1057
- class="drag-handle ${this.isTranslating
1058
- ? 'translating-hidden'
1059
- : ''}"
1077
+ class="drag-handle ${this.isReadOnly() ? 'read-only-hidden' : ''}"
1060
1078
  name="sort"
1061
1079
  ></temba-icon>`
1062
1080
  : html `<div class="title-spacer"></div>`}
1063
1081
 
1064
1082
  <div class="name">${isRemoving ? 'Remove?' : config.name}</div>
1065
1083
  <div
1066
- class="remove-button ${this.isTranslating ? 'translating-hidden' : ''}"
1084
+ class="remove-button ${this.isReadOnly() ? 'read-only-hidden' : ''}"
1067
1085
  @click=${(e) => this.handleActionRemoveClick(e, action, index)}
1068
1086
  title="Remove action"
1069
1087
  >
@@ -1091,7 +1109,7 @@ export class CanvasNode extends RapidElement {
1091
1109
  : html `${config.name}`}
1092
1110
  </div>
1093
1111
  <div
1094
- class="remove-button ${this.isTranslating ? 'translating-hidden' : ''}"
1112
+ class="remove-button ${this.isReadOnly() ? 'read-only-hidden' : ''}"
1095
1113
  @click=${(e) => this.handleNodeRemoveClick(e)}
1096
1114
  title="Remove node"
1097
1115
  >
@@ -1292,12 +1310,16 @@ export class CanvasNode extends RapidElement {
1292
1310
  class=${getClasses({
1293
1311
  exit: true,
1294
1312
  connected: !!exit.destination_uuid,
1295
- removing: this.exitRemovingState.has(exit.uuid)
1313
+ removing: this.exitRemovingState.has(exit.uuid),
1314
+ 'read-only': this.isReadOnly()
1296
1315
  })}
1297
1316
  @click=${(e) => this.handleExitClick(e, exit)}
1298
1317
  ></div>
1299
1318
  </div>`;
1300
1319
  }
1320
+ isReadOnly() {
1321
+ return this.viewingRevision || this.isTranslating;
1322
+ }
1301
1323
  render() {
1302
1324
  var _b;
1303
1325
  if (!this.node || !this.ui) {
@@ -1370,7 +1392,7 @@ export class CanvasNode extends RapidElement {
1370
1392
  : html `<div class="action-exits">
1371
1393
  ${repeat(this.node.exits, (exit) => exit.uuid, (exit) => this.renderExit(exit))}
1372
1394
  </div>`}
1373
- ${this.ui.type === 'execute_actions' && !this.isTranslating
1395
+ ${this.ui.type === 'execute_actions' && !this.isReadOnly()
1374
1396
  ? html `<div
1375
1397
  class="add-action-button"
1376
1398
  @click=${(e) => this.handleAddActionClick(e)}
@@ -1398,6 +1420,9 @@ __decorate([
1398
1420
  __decorate([
1399
1421
  fromStore(zustand, (state) => state.languageCode)
1400
1422
  ], CanvasNode.prototype, "languageCode", void 0);
1423
+ __decorate([
1424
+ fromStore(zustand, (state) => state.viewingRevision)
1425
+ ], CanvasNode.prototype, "viewingRevision", void 0);
1401
1426
  __decorate([
1402
1427
  fromStore(zustand, (state) => state.flowDefinition)
1403
1428
  ], CanvasNode.prototype, "flowDefinition", void 0);