@nyaruka/temba-components 0.142.1 → 0.142.3

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 (140) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/temba-components.js +953 -708
  3. package/dist/temba-components.js.map +1 -1
  4. package/out-tsc/src/Icons.js +1 -0
  5. package/out-tsc/src/Icons.js.map +1 -1
  6. package/out-tsc/src/flow/CanvasMenu.js +38 -38
  7. package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
  8. package/out-tsc/src/flow/CanvasNode.js +171 -17
  9. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  10. package/out-tsc/src/flow/Editor.js +491 -22
  11. package/out-tsc/src/flow/Editor.js.map +1 -1
  12. package/out-tsc/src/flow/NodeEditor.js +346 -10
  13. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  14. package/out-tsc/src/flow/NodeTypeSelector.js +2 -0
  15. package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
  16. package/out-tsc/src/flow/Plumber.js +92 -28
  17. package/out-tsc/src/flow/Plumber.js.map +1 -1
  18. package/out-tsc/src/flow/StickyNote.js +63 -3
  19. package/out-tsc/src/flow/StickyNote.js.map +1 -1
  20. package/out-tsc/src/flow/actions/add_contact_urn.js +2 -6
  21. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  22. package/out-tsc/src/flow/actions/enter_flow.js +2 -2
  23. package/out-tsc/src/flow/actions/enter_flow.js.map +1 -1
  24. package/out-tsc/src/flow/actions/say_msg.js +2 -1
  25. package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
  26. package/out-tsc/src/flow/actions/send_broadcast.js +2 -6
  27. package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
  28. package/out-tsc/src/flow/actions/send_email.js +2 -6
  29. package/out-tsc/src/flow/actions/send_email.js.map +1 -1
  30. package/out-tsc/src/flow/actions/send_msg.js +55 -35
  31. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  32. package/out-tsc/src/flow/actions/set_contact_channel.js +2 -1
  33. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  34. package/out-tsc/src/flow/actions/set_contact_field.js +4 -5
  35. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  36. package/out-tsc/src/flow/actions/set_contact_language.js +3 -3
  37. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  38. package/out-tsc/src/flow/actions/set_contact_name.js +2 -1
  39. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  40. package/out-tsc/src/flow/actions/set_contact_status.js +2 -1
  41. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  42. package/out-tsc/src/flow/actions/set_run_result.js +3 -3
  43. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  44. package/out-tsc/src/flow/actions/start_session.js +2 -2
  45. package/out-tsc/src/flow/actions/start_session.js.map +1 -1
  46. package/out-tsc/src/flow/nodes/split_by_llm.js +4 -5
  47. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
  48. package/out-tsc/src/flow/nodes/split_by_resthook.js +3 -8
  49. package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -1
  50. package/out-tsc/src/flow/nodes/split_by_subflow.js +4 -2
  51. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  52. package/out-tsc/src/flow/nodes/split_by_webhook.js +25 -33
  53. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
  54. package/out-tsc/src/flow/nodes/wait_for_response.js +1 -0
  55. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  56. package/out-tsc/src/flow/types.js.map +1 -1
  57. package/out-tsc/src/flow/utils.js +66 -0
  58. package/out-tsc/src/flow/utils.js.map +1 -1
  59. package/out-tsc/src/form/FieldRenderer.js +17 -2
  60. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  61. package/out-tsc/src/interfaces.js +1 -0
  62. package/out-tsc/src/interfaces.js.map +1 -1
  63. package/out-tsc/src/list/SortableList.js +104 -43
  64. package/out-tsc/src/list/SortableList.js.map +1 -1
  65. package/out-tsc/src/simulator/Simulator.js +6 -2
  66. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  67. package/out-tsc/test/temba-canvas-menu.test.js +13 -9
  68. package/out-tsc/test/temba-canvas-menu.test.js.map +1 -1
  69. package/out-tsc/test/temba-flow-reflow.test.js.map +1 -1
  70. package/out-tsc/test/temba-node-editor.test.js +9 -10
  71. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  72. package/out-tsc/test/temba-node-type-selector.test.js +3 -3
  73. package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
  74. package/out-tsc/test/temba-simulator.test.js +2 -2
  75. package/out-tsc/test/temba-simulator.test.js.map +1 -1
  76. package/package.json +1 -1
  77. package/screenshots/truth/actions/enter_flow/render/basic-flow.png +0 -0
  78. package/screenshots/truth/actions/enter_flow/render/long-flow-name.png +0 -0
  79. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  80. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  81. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  82. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  83. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  84. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  85. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  86. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  87. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  88. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  89. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  90. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  91. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  92. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  93. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  94. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  95. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  96. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  97. package/screenshots/truth/canvas-menu/open.png +0 -0
  98. package/screenshots/truth/node-type-selector/action-mode.png +0 -0
  99. package/screenshots/truth/node-type-selector/split-mode.png +0 -0
  100. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  101. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  102. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  103. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  104. package/src/Icons.ts +1 -0
  105. package/src/flow/CanvasMenu.ts +50 -43
  106. package/src/flow/CanvasNode.ts +201 -17
  107. package/src/flow/Editor.ts +585 -25
  108. package/src/flow/NodeEditor.ts +373 -10
  109. package/src/flow/NodeTypeSelector.ts +2 -0
  110. package/src/flow/Plumber.ts +104 -37
  111. package/src/flow/StickyNote.ts +76 -4
  112. package/src/flow/actions/add_contact_urn.ts +5 -6
  113. package/src/flow/actions/enter_flow.ts +2 -2
  114. package/src/flow/actions/say_msg.ts +2 -1
  115. package/src/flow/actions/send_broadcast.ts +2 -6
  116. package/src/flow/actions/send_email.ts +2 -6
  117. package/src/flow/actions/send_msg.ts +59 -38
  118. package/src/flow/actions/set_contact_channel.ts +5 -1
  119. package/src/flow/actions/set_contact_field.ts +10 -5
  120. package/src/flow/actions/set_contact_language.ts +6 -3
  121. package/src/flow/actions/set_contact_name.ts +5 -1
  122. package/src/flow/actions/set_contact_status.ts +5 -1
  123. package/src/flow/actions/set_run_result.ts +6 -3
  124. package/src/flow/actions/start_session.ts +2 -2
  125. package/src/flow/nodes/split_by_llm.ts +5 -5
  126. package/src/flow/nodes/split_by_resthook.ts +3 -8
  127. package/src/flow/nodes/split_by_subflow.ts +4 -2
  128. package/src/flow/nodes/split_by_webhook.ts +26 -34
  129. package/src/flow/nodes/wait_for_response.ts +1 -0
  130. package/src/flow/types.ts +25 -2
  131. package/src/flow/utils.ts +79 -1
  132. package/src/form/FieldRenderer.ts +32 -3
  133. package/src/interfaces.ts +1 -0
  134. package/src/list/SortableList.ts +117 -47
  135. package/src/simulator/Simulator.ts +6 -2
  136. package/test/temba-canvas-menu.test.ts +13 -9
  137. package/test/temba-flow-reflow.test.ts +4 -2
  138. package/test/temba-node-editor.test.ts +9 -10
  139. package/test/temba-node-type-selector.test.ts +3 -3
  140. package/test/temba-simulator.test.ts +2 -2
@@ -7,7 +7,13 @@ import { CustomEventType } from '../interfaces';
7
7
  * Event detail for canvas menu selection
8
8
  */
9
9
  export interface CanvasMenuSelection {
10
- action: 'sticky' | 'action' | 'split' | 'reflow';
10
+ action:
11
+ | 'sticky'
12
+ | 'action'
13
+ | 'split'
14
+ | 'send_msg'
15
+ | 'wait_for_response'
16
+ | 'reflow';
11
17
  position: { x: number; y: number };
12
18
  }
13
19
 
@@ -50,13 +56,6 @@ export class CanvasMenu extends RapidElement {
50
56
 
51
57
  .menu-item temba-icon {
52
58
  --icon-color: var(--color-text);
53
- margin-top: 0.15em;
54
- }
55
-
56
- .menu-item-content {
57
- display: flex;
58
- flex-direction: column;
59
- gap: 0.15em;
60
59
  }
61
60
 
62
61
  .menu-item-title {
@@ -65,11 +64,6 @@ export class CanvasMenu extends RapidElement {
65
64
  color: var(--color-text-dark);
66
65
  }
67
66
 
68
- .menu-item-description {
69
- font-size: 0.85rem;
70
- color: var(--color-text);
71
- }
72
-
73
67
  .divider {
74
68
  height: 1px;
75
69
  background: rgba(0, 0, 0, 0.1);
@@ -90,6 +84,9 @@ export class CanvasMenu extends RapidElement {
90
84
  @property({ type: Boolean })
91
85
  public showStickyNote = true;
92
86
 
87
+ @property({ type: Boolean })
88
+ public showWaitForResponse = true;
89
+
93
90
  @property({ type: Boolean })
94
91
  public showReflow = false;
95
92
 
@@ -101,16 +98,20 @@ export class CanvasMenu extends RapidElement {
101
98
  ): void {
102
99
  super.firstUpdated(_changedProperties);
103
100
 
104
- // Close menu when clicking outside — use mousedown instead of click
105
- // to avoid being triggered by the click synthesized from a drag-and-drop
106
- // (mousedown on exit + mouseup on canvas = click on common ancestor)
107
- const handleClickOutside = (e: MouseEvent) => {
101
+ // Close menu when clicking/tapping outside — use mousedown instead of
102
+ // click to avoid being triggered by the click synthesized from a
103
+ // drag-and-drop (mousedown on exit + mouseup on canvas = click on
104
+ // common ancestor). Also listen for touchstart for touch devices.
105
+ const handleClickOutside = (e: MouseEvent | TouchEvent) => {
108
106
  if (this.open && !this.contains(e.target as Node)) {
109
107
  this.close();
110
108
  }
111
109
  };
112
110
 
113
111
  document.addEventListener('mousedown', handleClickOutside);
112
+ document.addEventListener('touchstart', handleClickOutside, {
113
+ passive: true
114
+ });
114
115
 
115
116
  // Store cleanup function
116
117
  (this as any)._clickOutsideHandler = handleClickOutside;
@@ -123,6 +124,10 @@ export class CanvasMenu extends RapidElement {
123
124
  'mousedown',
124
125
  (this as any)._clickOutsideHandler
125
126
  );
127
+ document.removeEventListener(
128
+ 'touchstart',
129
+ (this as any)._clickOutsideHandler
130
+ );
126
131
  }
127
132
  }
128
133
 
@@ -131,13 +136,15 @@ export class CanvasMenu extends RapidElement {
131
136
  y: number,
132
137
  clickPosition: { x: number; y: number },
133
138
  showStickyNote: boolean = true,
134
- showReflow: boolean = false
139
+ showReflow: boolean = false,
140
+ showWaitForResponse: boolean = true
135
141
  ) {
136
142
  this.x = x;
137
143
  this.y = y;
138
144
  this.clickPosition = clickPosition;
139
145
  this.showStickyNote = showStickyNote;
140
146
  this.showReflow = showReflow;
147
+ this.showWaitForResponse = showWaitForResponse;
141
148
  this.open = true;
142
149
 
143
150
  // Adjust position after menu renders to ensure it fits on screen
@@ -185,9 +192,7 @@ export class CanvasMenu extends RapidElement {
185
192
  }
186
193
  }
187
194
 
188
- private handleMenuItemClick(
189
- action: 'sticky' | 'action' | 'split' | 'reflow'
190
- ) {
195
+ private handleMenuItemClick(action: CanvasMenuSelection['action']) {
191
196
  this.fireCustomEvent(CustomEventType.Selection, {
192
197
  action,
193
198
  position: this.clickPosition
@@ -203,17 +208,32 @@ export class CanvasMenu extends RapidElement {
203
208
 
204
209
  return html`
205
210
  <div class="menu" style="left: ${this.x}px; top: ${this.y}px;">
211
+ <div
212
+ class="menu-item"
213
+ @click=${() => this.handleMenuItemClick('send_msg')}
214
+ >
215
+ <temba-icon name="send" size="1.25"></temba-icon>
216
+ <div class="menu-item-title">Send Message</div>
217
+ </div>
218
+
219
+ ${this.showWaitForResponse
220
+ ? html`
221
+ <div
222
+ class="menu-item"
223
+ @click=${() => this.handleMenuItemClick('wait_for_response')}
224
+ >
225
+ <temba-icon name="message" size="1.25"></temba-icon>
226
+ <div class="menu-item-title">Wait for Response</div>
227
+ </div>
228
+ `
229
+ : ''}
230
+
206
231
  <div
207
232
  class="menu-item"
208
233
  @click=${() => this.handleMenuItemClick('action')}
209
234
  >
210
235
  <temba-icon name="action" size="1.25"></temba-icon>
211
- <div class="menu-item-content">
212
- <div class="menu-item-title">Add Action</div>
213
- <div class="menu-item-description">
214
- Send messages, update contacts
215
- </div>
216
- </div>
236
+ <div class="menu-item-title">Add Action</div>
217
237
  </div>
218
238
 
219
239
  <div
@@ -221,10 +241,7 @@ export class CanvasMenu extends RapidElement {
221
241
  @click=${() => this.handleMenuItemClick('split')}
222
242
  >
223
243
  <temba-icon name="split" size="1.25"></temba-icon>
224
- <div class="menu-item-content">
225
- <div class="menu-item-title">Add Split</div>
226
- <div class="menu-item-description">Branch based on conditions</div>
227
- </div>
244
+ <div class="menu-item-title">Add Split</div>
228
245
  </div>
229
246
 
230
247
  ${this.showStickyNote
@@ -236,12 +253,7 @@ export class CanvasMenu extends RapidElement {
236
253
  @click=${() => this.handleMenuItemClick('sticky')}
237
254
  >
238
255
  <temba-icon name="note" size="1.25"></temba-icon>
239
- <div class="menu-item-content">
240
- <div class="menu-item-title">Add Sticky Note</div>
241
- <div class="menu-item-description">
242
- Add a note to the canvas
243
- </div>
244
- </div>
256
+ <div class="menu-item-title">Add Sticky Note</div>
245
257
  </div>
246
258
  `
247
259
  : ''}
@@ -254,12 +266,7 @@ export class CanvasMenu extends RapidElement {
254
266
  @click=${() => this.handleMenuItemClick('reflow')}
255
267
  >
256
268
  <temba-icon name="flow" size="1.25"></temba-icon>
257
- <div class="menu-item-content">
258
- <div class="menu-item-title">Reflow</div>
259
- <div class="menu-item-description">
260
- Auto-arrange nodes in this flow
261
- </div>
262
- </div>
269
+ <div class="menu-item-title">Reflow</div>
263
270
  </div>
264
271
  `
265
272
  : ''}
@@ -6,7 +6,7 @@ import { Action, Exit, Node, NodeUI, Router } from '../store/flow-definition';
6
6
  import { property } from 'lit/decorators.js';
7
7
  import { RapidElement } from '../RapidElement';
8
8
  import { getClasses } from '../utils';
9
- import { isRightClick } from './utils';
9
+ import { isRightClick, renderClamped } from './utils';
10
10
  import { Plumber } from './Plumber';
11
11
  import { getStore } from '../store/Store';
12
12
  import { CustomEventType } from '../interfaces';
@@ -251,24 +251,20 @@ export class CanvasNode extends RapidElement {
251
251
  }
252
252
  .title-spacer {
253
253
  width: 1.8em;
254
-
255
254
  }
256
255
 
257
256
  .action:hover .drag-handle {
258
257
  visibility: visible;
259
258
  opacity: 0.7;
260
-
261
-
262
- }
263
-
264
- strong {
265
- font-weight: 500;
266
259
  }
267
260
 
268
261
  .action .drag-handle:hover {
269
262
  visibility: visible;
270
263
  opacity: 1;
271
-
264
+ }
265
+
266
+ strong {
267
+ font-weight: 500;
272
268
  }
273
269
 
274
270
  .action .cn-title,
@@ -295,6 +291,8 @@ export class CanvasNode extends RapidElement {
295
291
 
296
292
  .quick-replies {
297
293
  margin-top: 0.5em;
294
+ display: flex;
295
+ flex-wrap: wrap;
298
296
  }
299
297
 
300
298
  .quick-reply {
@@ -302,9 +300,14 @@ export class CanvasNode extends RapidElement {
302
300
  border: 1px solid #e0e0e0;
303
301
  border-radius: calc(var(--curvature) * 1.5);
304
302
  padding: 0.2em 1em;
305
- display: inline-block;
306
303
  font-size: 0.8em;
307
304
  margin: 0.2em;
305
+ flex: 0 1 auto;
306
+ min-width: 0;
307
+ max-width: 100%;
308
+ overflow: hidden;
309
+ text-overflow: ellipsis;
310
+ white-space: nowrap;
308
311
  }
309
312
 
310
313
  .router-section {
@@ -349,6 +352,7 @@ export class CanvasNode extends RapidElement {
349
352
 
350
353
  .router .body {
351
354
  padding: 0.75em;
355
+ max-width: 180px;
352
356
  }
353
357
 
354
358
  .result-name {
@@ -488,6 +492,22 @@ export class CanvasNode extends RapidElement {
488
492
  color: #9ca3af;
489
493
  font-size: 0.9em;
490
494
  }
495
+
496
+ /* On touch devices, always show interactive controls.
497
+ The .touch-device class is added to the editor on first touch. */
498
+ .touch-device .remove-button {
499
+ visibility: visible !important;
500
+ opacity: 0.7;
501
+ }
502
+
503
+ .touch-device .action .drag-handle {
504
+ visibility: visible !important;
505
+ opacity: 0.7;
506
+ }
507
+
508
+ .touch-device .add-action-button {
509
+ opacity: 0.8 !important;
510
+ }
491
511
  }`;
492
512
  }
493
513
 
@@ -973,11 +993,12 @@ export class CanvasNode extends RapidElement {
973
993
  private handleActionMouseDown(event: MouseEvent, action: Action): void {
974
994
  if (isRightClick(event)) return;
975
995
 
976
- // Don't handle clicks on the remove button, drag handle, or when action is in removing state
996
+ // Don't handle clicks on the remove button, drag handle, linked elements, or when action is in removing state
977
997
  const target = event.target as HTMLElement;
978
998
  if (
979
999
  target.closest('.remove-button') ||
980
1000
  target.closest('.drag-handle') ||
1001
+ target.closest('.linked-name') ||
981
1002
  this.actionRemovingState.has(action.uuid)
982
1003
  ) {
983
1004
  return;
@@ -1000,11 +1021,12 @@ export class CanvasNode extends RapidElement {
1000
1021
  return;
1001
1022
  }
1002
1023
 
1003
- // Don't handle clicks on the remove button, drag handle, or when action is in removing state
1024
+ // Don't handle clicks on the remove button, drag handle, linked elements, or when action is in removing state
1004
1025
  const target = event.target as HTMLElement;
1005
1026
  if (
1006
1027
  target.closest('.remove-button') ||
1007
1028
  target.closest('.drag-handle') ||
1029
+ target.closest('.linked-name') ||
1008
1030
  this.actionRemovingState.has(action.uuid)
1009
1031
  ) {
1010
1032
  this.actionClickStartPos = null;
@@ -1046,6 +1068,75 @@ export class CanvasNode extends RapidElement {
1046
1068
  this.pendingActionClick = null;
1047
1069
  }
1048
1070
 
1071
+ /* c8 ignore start -- touch-only handlers untestable in headless Chromium */
1072
+ private handleActionTouchStart(event: TouchEvent, action: Action): void {
1073
+ const target = event.target as HTMLElement;
1074
+ if (
1075
+ target.closest('.remove-button') ||
1076
+ target.closest('.drag-handle') ||
1077
+ target.closest('.linked-name') ||
1078
+ this.actionRemovingState.has(action.uuid)
1079
+ ) {
1080
+ return;
1081
+ }
1082
+
1083
+ const touch = event.touches[0];
1084
+ if (!touch) return;
1085
+ this.actionClickStartPos = { x: touch.clientX, y: touch.clientY };
1086
+ this.pendingActionClick = { action, event: event as any };
1087
+ }
1088
+
1089
+ private handleActionTouchEnd(event: TouchEvent, action: Action): void {
1090
+ if (
1091
+ !this.pendingActionClick ||
1092
+ this.pendingActionClick.action.uuid !== action.uuid
1093
+ ) {
1094
+ this.actionClickStartPos = null;
1095
+ this.pendingActionClick = null;
1096
+ return;
1097
+ }
1098
+
1099
+ const target = event.target as HTMLElement;
1100
+ if (
1101
+ target.closest('.remove-button') ||
1102
+ target.closest('.drag-handle') ||
1103
+ target.closest('.linked-name') ||
1104
+ this.actionRemovingState.has(action.uuid)
1105
+ ) {
1106
+ this.actionClickStartPos = null;
1107
+ this.pendingActionClick = null;
1108
+ return;
1109
+ }
1110
+
1111
+ const touch = event.changedTouches[0];
1112
+ if (this.actionClickStartPos && touch) {
1113
+ const deltaX = touch.clientX - this.actionClickStartPos.x;
1114
+ const deltaY = touch.clientY - this.actionClickStartPos.y;
1115
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
1116
+
1117
+ const editor = this.closest('temba-flow-editor') as any;
1118
+ const editorWasDragging = editor?.dragging;
1119
+
1120
+ if (distance <= DRAG_THRESHOLD && (!editor || !editorWasDragging)) {
1121
+ const actionEl = event.currentTarget as Element;
1122
+ const origin = actionEl
1123
+ ? this.getTopCenter(actionEl)
1124
+ : { x: touch.clientX, y: touch.clientY };
1125
+
1126
+ this.fireCustomEvent(CustomEventType.ActionEditRequested, {
1127
+ action,
1128
+ nodeUuid: this.node.uuid,
1129
+ originX: origin.x,
1130
+ originY: origin.y
1131
+ });
1132
+ }
1133
+ }
1134
+
1135
+ this.actionClickStartPos = null;
1136
+ this.pendingActionClick = null;
1137
+ }
1138
+ /* c8 ignore stop */
1139
+
1049
1140
  private handleActionClick(event: MouseEvent, action: Action): void {
1050
1141
  // This method is kept for backward compatibility but should not be used
1051
1142
  // The new mousedown/mouseup approach handles click vs drag properly
@@ -1102,13 +1193,14 @@ export class CanvasNode extends RapidElement {
1102
1193
  private handleNodeMouseDown(event: MouseEvent): void {
1103
1194
  if (isRightClick(event)) return;
1104
1195
 
1105
- // Don't handle clicks on the remove button, exits, drag handle, or when node is in removing state
1196
+ // Don't handle clicks on the remove button, exits, drag handle, linked elements, or when node is in removing state
1106
1197
  const target = event.target as HTMLElement;
1107
1198
  if (
1108
1199
  target.closest('.remove-button') ||
1109
1200
  target.closest('.exit') ||
1110
1201
  target.closest('.exit-wrapper') ||
1111
1202
  target.closest('.drag-handle') ||
1203
+ target.closest('.linked-name') ||
1112
1204
  this.actionRemovingState.has(this.node.uuid)
1113
1205
  ) {
1114
1206
  return;
@@ -1128,13 +1220,14 @@ export class CanvasNode extends RapidElement {
1128
1220
  return;
1129
1221
  }
1130
1222
 
1131
- // Don't handle clicks on the remove button, exits, drag handle, or when node is in removing state
1223
+ // Don't handle clicks on the remove button, exits, drag handle, linked elements, or when node is in removing state
1132
1224
  const target = event.target as HTMLElement;
1133
1225
  if (
1134
1226
  target.closest('.remove-button') ||
1135
1227
  target.closest('.exit') ||
1136
1228
  target.closest('.exit-wrapper') ||
1137
1229
  target.closest('.drag-handle') ||
1230
+ target.closest('.linked-name') ||
1138
1231
  this.actionRemovingState.has(this.node.uuid)
1139
1232
  ) {
1140
1233
  this.nodeClickStartPos = null;
@@ -1187,6 +1280,84 @@ export class CanvasNode extends RapidElement {
1187
1280
  this.pendingNodeClick = null;
1188
1281
  }
1189
1282
 
1283
+ /* c8 ignore start -- touch-only handlers */
1284
+ private handleNodeTouchStart(event: TouchEvent): void {
1285
+ const target = event.target as HTMLElement;
1286
+ if (
1287
+ target.closest('.remove-button') ||
1288
+ target.closest('.exit') ||
1289
+ target.closest('.exit-wrapper') ||
1290
+ target.closest('.drag-handle') ||
1291
+ target.closest('.linked-name') ||
1292
+ this.actionRemovingState.has(this.node.uuid)
1293
+ ) {
1294
+ return;
1295
+ }
1296
+
1297
+ const touch = event.touches[0];
1298
+ if (!touch) return;
1299
+ this.nodeClickStartPos = { x: touch.clientX, y: touch.clientY };
1300
+ this.pendingNodeClick = { event: event as any };
1301
+ }
1302
+
1303
+ private handleNodeTouchEnd(event: TouchEvent): void {
1304
+ if (!this.pendingNodeClick) {
1305
+ this.nodeClickStartPos = null;
1306
+ this.pendingNodeClick = null;
1307
+ return;
1308
+ }
1309
+
1310
+ const target = event.target as HTMLElement;
1311
+ if (
1312
+ target.closest('.remove-button') ||
1313
+ target.closest('.exit') ||
1314
+ target.closest('.exit-wrapper') ||
1315
+ target.closest('.drag-handle') ||
1316
+ target.closest('.linked-name') ||
1317
+ this.actionRemovingState.has(this.node.uuid)
1318
+ ) {
1319
+ this.nodeClickStartPos = null;
1320
+ this.pendingNodeClick = null;
1321
+ return;
1322
+ }
1323
+
1324
+ const touch = event.changedTouches[0];
1325
+ if (this.nodeClickStartPos && touch) {
1326
+ const deltaX = touch.clientX - this.nodeClickStartPos.x;
1327
+ const deltaY = touch.clientY - this.nodeClickStartPos.y;
1328
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
1329
+
1330
+ const editor = this.closest('temba-flow-editor') as any;
1331
+ const editorWasDragging = editor?.dragging;
1332
+
1333
+ if (distance <= 5 && (!editor || !editorWasDragging)) {
1334
+ if (this.node.router) {
1335
+ const origin = this.getTopCenter(this);
1336
+
1337
+ if (this.node.actions && this.node.actions.length === 1) {
1338
+ this.fireCustomEvent(CustomEventType.ActionEditRequested, {
1339
+ action: this.node.actions[0],
1340
+ nodeUuid: this.node.uuid,
1341
+ originX: origin.x,
1342
+ originY: origin.y
1343
+ });
1344
+ } else {
1345
+ this.fireCustomEvent(CustomEventType.NodeEditRequested, {
1346
+ node: this.node,
1347
+ nodeUI: this.ui,
1348
+ originX: origin.x,
1349
+ originY: origin.y
1350
+ });
1351
+ }
1352
+ }
1353
+ }
1354
+ }
1355
+
1356
+ this.nodeClickStartPos = null;
1357
+ this.pendingNodeClick = null;
1358
+ }
1359
+ /* c8 ignore stop */
1360
+
1190
1361
  private handleAddActionClick(event: MouseEvent): void {
1191
1362
  event.preventDefault();
1192
1363
  event.stopPropagation();
@@ -1498,6 +1669,10 @@ export class CanvasNode extends RapidElement {
1498
1669
  !isDisabled && this.handleActionMouseDown(e, action)}
1499
1670
  @mouseup=${(e: MouseEvent) =>
1500
1671
  !isDisabled && this.handleActionMouseUp(e, action)}
1672
+ @touchstart=${(e: TouchEvent) =>
1673
+ !isDisabled && this.handleActionTouchStart(e, action)}
1674
+ @touchend=${(e: TouchEvent) =>
1675
+ !isDisabled && this.handleActionTouchEnd(e, action)}
1501
1676
  style="cursor: ${isDisabled ? 'not-allowed' : 'pointer'}"
1502
1677
  >
1503
1678
  ${this.renderTitle(config, action, index, isRemoving)}
@@ -1559,10 +1734,15 @@ export class CanvasNode extends RapidElement {
1559
1734
  class="body"
1560
1735
  @mousedown=${(e: MouseEvent) => this.handleNodeMouseDown(e)}
1561
1736
  @mouseup=${(e: MouseEvent) => this.handleNodeMouseUp(e)}
1737
+ @touchstart=${(e: TouchEvent) => this.handleNodeTouchStart(e)}
1738
+ @touchend=${(e: TouchEvent) => this.handleNodeTouchEnd(e)}
1562
1739
  style="cursor: pointer;"
1563
1740
  >
1564
- Save as
1565
- <div class="result-name">${router.result_name}</div>
1741
+ ${renderClamped(
1742
+ html`Save as
1743
+ <span class="result-name">${router.result_name}</span>`,
1744
+ `Save as ${router.result_name}`
1745
+ )}
1566
1746
  </div>`
1567
1747
  : null}
1568
1748
  </div>`;
@@ -1622,9 +1802,11 @@ export class CanvasNode extends RapidElement {
1622
1802
  })}
1623
1803
  @mousedown=${(e: MouseEvent) => this.handleNodeMouseDown(e)}
1624
1804
  @mouseup=${(e: MouseEvent) => this.handleNodeMouseUp(e)}
1805
+ @touchstart=${(e: TouchEvent) => this.handleNodeTouchStart(e)}
1806
+ @touchend=${(e: TouchEvent) => this.handleNodeTouchEnd(e)}
1625
1807
  style="cursor: pointer;"
1626
1808
  >
1627
- <div class="cn-title">${displayName}</div>
1809
+ <div class="cn-title" title="${displayName}">${displayName}</div>
1628
1810
  ${this.renderExit(exit)}
1629
1811
  </div>`;
1630
1812
  }
@@ -1697,6 +1879,8 @@ export class CanvasNode extends RapidElement {
1697
1879
  <div
1698
1880
  @mousedown=${(e: MouseEvent) => this.handleNodeMouseDown(e)}
1699
1881
  @mouseup=${(e: MouseEvent) => this.handleNodeMouseUp(e)}
1882
+ @touchstart=${(e: TouchEvent) => this.handleNodeTouchStart(e)}
1883
+ @touchend=${(e: TouchEvent) => this.handleNodeTouchEnd(e)}
1700
1884
  style="cursor: pointer;"
1701
1885
  >
1702
1886
  ${this.renderNodeTitle(