@nyaruka/temba-components 0.141.1 → 0.142.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 (193) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/static/svg/index.svg +1 -1
  3. package/dist/temba-components.js +849 -655
  4. package/dist/temba-components.js.map +1 -1
  5. package/out-tsc/src/Icons.js +3 -1
  6. package/out-tsc/src/Icons.js.map +1 -1
  7. package/out-tsc/src/display/Button.js +2 -2
  8. package/out-tsc/src/display/Button.js.map +1 -1
  9. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  10. package/out-tsc/src/flow/CanvasMenu.js +24 -1
  11. package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
  12. package/out-tsc/src/flow/CanvasNode.js +7 -2
  13. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  14. package/out-tsc/src/flow/Editor.js +654 -66
  15. package/out-tsc/src/flow/Editor.js.map +1 -1
  16. package/out-tsc/src/flow/NodeEditor.js +8 -5
  17. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  18. package/out-tsc/src/flow/Plumber.js +40 -28
  19. package/out-tsc/src/flow/Plumber.js.map +1 -1
  20. package/out-tsc/src/flow/actions/send_msg.js +2 -1
  21. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  22. package/out-tsc/src/flow/nodes/wait_for_response.js +1 -1
  23. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  24. package/out-tsc/src/flow/reflow.js +393 -0
  25. package/out-tsc/src/flow/reflow.js.map +1 -0
  26. package/out-tsc/src/flow/types.js.map +1 -1
  27. package/out-tsc/src/flow/utils.js +18 -3
  28. package/out-tsc/src/flow/utils.js.map +1 -1
  29. package/out-tsc/src/form/Compose.js +5 -0
  30. package/out-tsc/src/form/Compose.js.map +1 -1
  31. package/out-tsc/src/form/FieldRenderer.js +1 -3
  32. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  33. package/out-tsc/src/layout/Dialog.js +2 -0
  34. package/out-tsc/src/layout/Dialog.js.map +1 -1
  35. package/out-tsc/src/list/SortableList.js +39 -19
  36. package/out-tsc/src/list/SortableList.js.map +1 -1
  37. package/out-tsc/test/temba-canvas-menu.test.js +44 -0
  38. package/out-tsc/test/temba-canvas-menu.test.js.map +1 -1
  39. package/out-tsc/test/temba-flow-collision.test.js +25 -0
  40. package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
  41. package/out-tsc/test/temba-flow-editor-zoom.test.js +491 -0
  42. package/out-tsc/test/temba-flow-editor-zoom.test.js.map +1 -0
  43. package/out-tsc/test/temba-flow-editor.test.js +145 -1
  44. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  45. package/out-tsc/test/temba-flow-node-drag.test.js +123 -0
  46. package/out-tsc/test/temba-flow-node-drag.test.js.map +1 -1
  47. package/out-tsc/test/temba-flow-plumber.test.js +31 -0
  48. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  49. package/out-tsc/test/temba-flow-reflow.test.js +472 -0
  50. package/out-tsc/test/temba-flow-reflow.test.js.map +1 -0
  51. package/out-tsc/test/temba-sortable-list.test.js +93 -0
  52. package/out-tsc/test/temba-sortable-list.test.js.map +1 -1
  53. package/package.json +1 -1
  54. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  55. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  56. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  57. package/screenshots/truth/actions/add_contact_urn/editor/expression-facebook.png +0 -0
  58. package/screenshots/truth/actions/add_contact_urn/editor/expression-phone.png +0 -0
  59. package/screenshots/truth/actions/add_contact_urn/editor/facebook-id.png +0 -0
  60. package/screenshots/truth/actions/add_contact_urn/editor/instagram-handle.png +0 -0
  61. package/screenshots/truth/actions/add_contact_urn/editor/line-id.png +0 -0
  62. package/screenshots/truth/actions/add_contact_urn/editor/phone-number.png +0 -0
  63. package/screenshots/truth/actions/add_contact_urn/editor/telegram-id.png +0 -0
  64. package/screenshots/truth/actions/add_contact_urn/editor/viber-id.png +0 -0
  65. package/screenshots/truth/actions/add_contact_urn/editor/wechat-id.png +0 -0
  66. package/screenshots/truth/actions/add_contact_urn/editor/whatsapp.png +0 -0
  67. package/screenshots/truth/actions/enter_flow/editor/basic-flow.png +0 -0
  68. package/screenshots/truth/actions/enter_flow/editor/long-flow-name.png +0 -0
  69. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  70. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  71. package/screenshots/truth/actions/say_msg/editor/multiline-text.png +0 -0
  72. package/screenshots/truth/actions/say_msg/editor/simple-text.png +0 -0
  73. package/screenshots/truth/actions/say_msg/editor/text-with-audio-url.png +0 -0
  74. package/screenshots/truth/actions/send_broadcast/editor/contacts-only.png +0 -0
  75. package/screenshots/truth/actions/send_broadcast/editor/groups-and-contacts.png +0 -0
  76. package/screenshots/truth/actions/send_broadcast/editor/groups-only.png +0 -0
  77. package/screenshots/truth/actions/send_broadcast/editor/many-groups.png +0 -0
  78. package/screenshots/truth/actions/send_broadcast/editor/multiline-text.png +0 -0
  79. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  80. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  81. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  82. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  83. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  84. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  85. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  86. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  87. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  88. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  89. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  90. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  91. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  92. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  93. package/screenshots/truth/actions/set_contact_channel/editor/sms-channel.png +0 -0
  94. package/screenshots/truth/actions/set_contact_channel/editor/whatsapp-channel.png +0 -0
  95. package/screenshots/truth/actions/set_contact_field/editor/clear-value.png +0 -0
  96. package/screenshots/truth/actions/set_contact_field/editor/set-value.png +0 -0
  97. package/screenshots/truth/actions/set_contact_language/editor/english.png +0 -0
  98. package/screenshots/truth/actions/set_contact_language/editor/french.png +0 -0
  99. package/screenshots/truth/actions/set_contact_status/editor/active.png +0 -0
  100. package/screenshots/truth/actions/set_contact_status/editor/archived.png +0 -0
  101. package/screenshots/truth/actions/set_contact_status/editor/blocked.png +0 -0
  102. package/screenshots/truth/actions/set_run_result/editor/expression-value.png +0 -0
  103. package/screenshots/truth/actions/set_run_result/editor/with-category.png +0 -0
  104. package/screenshots/truth/actions/start_session/editor/contact-query.png +0 -0
  105. package/screenshots/truth/actions/start_session/editor/contacts-only.png +0 -0
  106. package/screenshots/truth/actions/start_session/editor/create-contact.png +0 -0
  107. package/screenshots/truth/actions/start_session/editor/groups-and-contacts.png +0 -0
  108. package/screenshots/truth/actions/start_session/editor/groups-only.png +0 -0
  109. package/screenshots/truth/actions/start_session/editor/many-recipients.png +0 -0
  110. package/screenshots/truth/list/fields-dragging.png +0 -0
  111. package/screenshots/truth/list/sortable-dragging.png +0 -0
  112. package/screenshots/truth/modax/simple.png +0 -0
  113. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  114. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  115. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  116. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  117. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  118. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  119. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  120. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  121. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  122. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  123. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  124. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  125. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  126. package/screenshots/truth/nodes/wait_for_dial/editor/dial-with-limits.png +0 -0
  127. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  128. package/screenshots/truth/nodes/wait_for_digits/editor/digits-with-rules.png +0 -0
  129. package/screenshots/truth/nodes/wait_for_menu/editor/menu-with-digits.png +0 -0
  130. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  131. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  132. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  133. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  134. package/src/Icons.ts +3 -1
  135. package/src/display/Button.ts +2 -2
  136. package/src/display/FloatingTab.ts +1 -1
  137. package/src/flow/CanvasMenu.ts +28 -3
  138. package/src/flow/CanvasNode.ts +7 -2
  139. package/src/flow/Editor.ts +755 -75
  140. package/src/flow/NodeEditor.ts +8 -4
  141. package/src/flow/Plumber.ts +65 -35
  142. package/src/flow/actions/send_msg.ts +2 -1
  143. package/src/flow/nodes/wait_for_response.ts +1 -1
  144. package/src/flow/reflow.ts +534 -0
  145. package/src/flow/types.ts +1 -0
  146. package/src/flow/utils.ts +19 -3
  147. package/src/form/Compose.ts +5 -0
  148. package/src/form/FieldRenderer.ts +1 -3
  149. package/src/layout/Dialog.ts +2 -0
  150. package/src/list/SortableList.ts +40 -19
  151. package/static/svg/index.svg +1 -1
  152. package/static/svg/work/traced/expand-06.svg +1 -0
  153. package/static/svg/work/used/expand-06.svg +3 -0
  154. package/test/temba-canvas-menu.test.ts +55 -0
  155. package/test/temba-flow-collision.test.ts +31 -0
  156. package/test/temba-flow-editor-zoom.test.ts +583 -0
  157. package/test/temba-flow-editor.test.ts +187 -1
  158. package/test/temba-flow-node-drag.test.ts +171 -0
  159. package/test/temba-flow-plumber.test.ts +38 -0
  160. package/test/temba-flow-reflow.test.ts +703 -0
  161. package/test/temba-sortable-list.test.ts +120 -0
  162. package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
  163. package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
  164. package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
  165. package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
  166. package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
  167. package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
  168. package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
  169. package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
  170. package/screenshots/truth/actions/send_broadcast/editor/with-attachments.png +0 -0
  171. package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
  172. package/screenshots/truth/compose/attachments-with-failures.png +0 -0
  173. package/screenshots/truth/compose/attachments-with-files-and-failures.png +0 -0
  174. package/screenshots/truth/contacts/tickets-assignment.png +0 -0
  175. package/screenshots/truth/contacts/tickets.png +0 -0
  176. package/screenshots/truth/flow/editor-basic.png +0 -0
  177. package/screenshots/truth/formfield/markdown-errors.png +0 -0
  178. package/screenshots/truth/formfield/no-errors.png +0 -0
  179. package/screenshots/truth/formfield/plain-text-errors.png +0 -0
  180. package/screenshots/truth/formfield/widget-only-markdown-errors.png +0 -0
  181. package/screenshots/truth/omnibox/selected.png +0 -0
  182. package/screenshots/truth/select/enabled-multi-selection.png +0 -0
  183. package/screenshots/truth/select/endpoint-initial-value-updated.png +0 -0
  184. package/screenshots/truth/select/endpoint-initial-value.png +0 -0
  185. package/screenshots/truth/select/initial-value.png +0 -0
  186. package/screenshots/truth/select/multi-reorder-final.png +0 -0
  187. package/screenshots/truth/select/multi-reorder-initial.png +0 -0
  188. package/screenshots/truth/select/selected-multi-test.png +0 -0
  189. package/screenshots/truth/select/value-initial.png +0 -0
  190. package/screenshots/truth/wait-for-response/rules-editor.png +0 -0
  191. package/screenshots/truth/wait-for-response/timeout-editor-unchecked.png +0 -0
  192. package/screenshots/truth/wait-for-response/timeout-editor.png +0 -0
  193. package/screenshots/truth/webchat/connecting-state.png +0 -0
@@ -221,7 +221,7 @@ export class NodeEditor extends RapidElement {
221
221
  }
222
222
 
223
223
  .form-group-content {
224
- padding: 6px;
224
+ padding: var(--group-content-padding, 6px);
225
225
  display: flex;
226
226
  flex-direction: column;
227
227
  gap: 15px;
@@ -233,8 +233,8 @@ export class NodeEditor extends RapidElement {
233
233
 
234
234
  .form-group-content.collapsed {
235
235
  max-height: 0;
236
- padding-top: 0;
237
- padding-bottom: 0;
236
+ padding-top: 0 !important;
237
+ padding-bottom: 0 !important;
238
238
  opacity: 0;
239
239
  }
240
240
 
@@ -335,7 +335,7 @@ export class NodeEditor extends RapidElement {
335
335
  }
336
336
 
337
337
  .gutter-fields temba-select {
338
- min-width: 120px;
338
+ min-width: 150px;
339
339
  }
340
340
 
341
341
  .optional-field-link {
@@ -1770,6 +1770,7 @@ export class NodeEditor extends RapidElement {
1770
1770
  collapsible = false,
1771
1771
  collapsed = false,
1772
1772
  helpText,
1773
+ contentPadding,
1773
1774
  getGroupValueCount
1774
1775
  } = groupConfig;
1775
1776
 
@@ -1887,6 +1888,9 @@ export class NodeEditor extends RapidElement {
1887
1888
  </div>
1888
1889
  <div
1889
1890
  class="form-group-content ${isCollapsed ? 'collapsed' : 'expanded'}"
1891
+ style="${contentPadding
1892
+ ? `--group-content-padding: ${contentPadding}`
1893
+ : ''}"
1890
1894
  >
1891
1895
  ${items.map((item) =>
1892
1896
  this.renderLayoutItem(item, config, renderedFields)
@@ -1,3 +1,5 @@
1
+ import { isRightClick } from './utils';
2
+
1
3
  export type TargetFace = 'top' | 'left' | 'right';
2
4
 
3
5
  // Shared arrow/drag constants used by both Plumber and Editor
@@ -196,6 +198,7 @@ export class Plumber {
196
198
  private showContactsTimeout: number | null = null;
197
199
 
198
200
  public connectionDragging = false;
201
+ public zoom = 1.0;
199
202
 
200
203
  constructor(canvas: HTMLElement, editor: any) {
201
204
  this.canvas = canvas;
@@ -246,7 +249,7 @@ export class Plumber {
246
249
  const DRAG_THRESHOLD = 5;
247
250
 
248
251
  const onMouseDown = (e: MouseEvent) => {
249
- if (e.button !== 0) return;
252
+ if (isRightClick(e)) return;
250
253
 
251
254
  // Don't start drag from exit if it already has a connection —
252
255
  // existing connections are picked up from the arrowhead instead
@@ -356,15 +359,17 @@ export class Plumber {
356
359
 
357
360
  // --- Anchor point distribution ---
358
361
 
362
+ /** Convert a viewport-to-canvas rect difference to canvas coordinates */
363
+ private toCanvas(viewportDiff: number): number {
364
+ return viewportDiff / this.zoom;
365
+ }
366
+
359
367
  private determineTargetFace(
360
368
  sourceX: number,
361
369
  sourceY: number,
362
- targetRect: DOMRect,
363
- canvasRect: DOMRect
370
+ targetCenterX: number,
371
+ targetTop: number
364
372
  ): TargetFace {
365
- const targetCenterX =
366
- targetRect.left + targetRect.width / 2 - canvasRect.left;
367
- const targetTop = targetRect.top - canvasRect.top;
368
373
  const verticalGap = targetTop - sourceY;
369
374
 
370
375
  // Top face requires enough vertical room for the exit stub, entry stub,
@@ -396,14 +401,23 @@ export class Plumber {
396
401
 
397
402
  if (fromRect.width === 0 || toRect.width === 0) return null;
398
403
 
399
- const sourceX = fromRect.left + fromRect.width / 2 - canvasRect.left;
400
- const sourceY = fromRect.bottom - canvasRect.top;
404
+ // All coordinates are converted to canvas space by dividing by zoom
405
+ const sourceX = this.toCanvas(
406
+ fromRect.left + fromRect.width / 2 - canvasRect.left
407
+ );
408
+ const sourceY = this.toCanvas(fromRect.bottom - canvasRect.top);
409
+
410
+ // Pre-compute target canvas-space values for determineTargetFace
411
+ const toCenterX = this.toCanvas(
412
+ toRect.left + toRect.width / 2 - canvasRect.left
413
+ );
414
+ const toTopCanvas = this.toCanvas(toRect.top - canvasRect.top);
401
415
 
402
416
  const targetFace = this.determineTargetFace(
403
417
  sourceX,
404
418
  sourceY,
405
- toRect,
406
- canvasRect
419
+ toCenterX,
420
+ toTopCanvas
407
421
  );
408
422
 
409
423
  // Find all connections targeting the same node, grouped by face
@@ -417,14 +431,17 @@ export class Plumber {
417
431
  const connFromEl = document.getElementById(conn.fromId);
418
432
  if (connFromEl) {
419
433
  const connFromRect = connFromEl.getBoundingClientRect();
420
- const connSourceX =
421
- connFromRect.left + connFromRect.width / 2 - canvasRect.left;
422
- const connSourceY = connFromRect.bottom - canvasRect.top;
434
+ const connSourceX = this.toCanvas(
435
+ connFromRect.left + connFromRect.width / 2 - canvasRect.left
436
+ );
437
+ const connSourceY = this.toCanvas(
438
+ connFromRect.bottom - canvasRect.top
439
+ );
423
440
  const face = this.determineTargetFace(
424
441
  connSourceX,
425
442
  connSourceY,
426
- toRect,
427
- canvasRect
443
+ toCenterX,
444
+ toTopCanvas
428
445
  );
429
446
  if (!faceConnections.has(face)) {
430
447
  faceConnections.set(face, []);
@@ -451,11 +468,11 @@ export class Plumber {
451
468
  const index = faceGroup.findIndex((e) => e.fromId === fromId);
452
469
  const count = faceGroup.length;
453
470
 
454
- // Calculate anchor point on the chosen face
455
- const targetLeft = toRect.left - canvasRect.left;
456
- const targetTop = toRect.top - canvasRect.top;
457
- const targetW = toRect.width;
458
- const targetH = toRect.height;
471
+ // Calculate anchor point on the chosen face (all in canvas space)
472
+ const targetLeft = this.toCanvas(toRect.left - canvasRect.left);
473
+ const targetTop = toTopCanvas;
474
+ const targetW = this.toCanvas(toRect.width);
475
+ const targetH = this.toCanvas(toRect.height);
459
476
 
460
477
  let targetX: number;
461
478
  let targetY: number;
@@ -504,18 +521,27 @@ export class Plumber {
504
521
  if (connFromEl && connToEl) {
505
522
  const connFromRect = connFromEl.getBoundingClientRect();
506
523
  const connToRect = connToEl.getBoundingClientRect();
507
- const connSourceX =
508
- connFromRect.left + connFromRect.width / 2 - canvasRect.left;
509
- const connSourceY = connFromRect.bottom - canvasRect.top;
524
+ const connSourceX = this.toCanvas(
525
+ connFromRect.left + connFromRect.width / 2 - canvasRect.left
526
+ );
527
+ const connSourceY = this.toCanvas(
528
+ connFromRect.bottom - canvasRect.top
529
+ );
530
+ const connToCenterX = this.toCanvas(
531
+ connToRect.left + connToRect.width / 2 - canvasRect.left
532
+ );
533
+ const connToTop = this.toCanvas(connToRect.top - canvasRect.top);
510
534
  const connFace = this.determineTargetFace(
511
535
  connSourceX,
512
536
  connSourceY,
513
- connToRect,
514
- canvasRect
537
+ connToCenterX,
538
+ connToTop
515
539
  );
516
540
  if (connFace === 'top') {
517
- const connTargetLeft = connToRect.left - canvasRect.left;
518
- const connTargetW = connToRect.width;
541
+ const connTargetLeft = this.toCanvas(
542
+ connToRect.left - canvasRect.left
543
+ );
544
+ const connTargetW = this.toCanvas(connToRect.width);
519
545
  siblings.push({
520
546
  fromId: conn.fromId,
521
547
  targetX: connTargetLeft + connTargetW / 2
@@ -689,7 +715,7 @@ export class Plumber {
689
715
  // Make arrowhead draggable for picking up existing connections
690
716
  const DRAG_THRESHOLD = 5;
691
717
  const onArrowMouseDown = (e: MouseEvent) => {
692
- if (e.button !== 0) return;
718
+ if (isRightClick(e)) return;
693
719
  e.stopPropagation();
694
720
 
695
721
  const startX = e.clientX;
@@ -1195,8 +1221,10 @@ export class Plumber {
1195
1221
 
1196
1222
  const canvasRect = this.canvas.getBoundingClientRect();
1197
1223
  const exitRect = exitEl.getBoundingClientRect();
1198
- const sourceX = exitRect.left + exitRect.width / 2 - canvasRect.left;
1199
- const sourceY = exitRect.bottom - canvasRect.top;
1224
+ const sourceX = this.toCanvas(
1225
+ exitRect.left + exitRect.width / 2 - canvasRect.left
1226
+ );
1227
+ const sourceY = this.toCanvas(exitRect.bottom - canvasRect.top);
1200
1228
 
1201
1229
  const aw = ARROW_HALF_WIDTH;
1202
1230
  const al = ARROW_LENGTH;
@@ -1252,16 +1280,18 @@ export class Plumber {
1252
1280
  }
1253
1281
  };
1254
1282
 
1255
- // Initial path to cursor
1256
- const cursorX = e.clientX - canvasRect.left;
1257
- const cursorY = e.clientY - canvasRect.top;
1283
+ // Initial path to cursor (convert viewport to canvas coordinates)
1284
+ const cursorX = this.toCanvas(e.clientX - canvasRect.left);
1285
+ const cursorY = this.toCanvas(e.clientY - canvasRect.top);
1258
1286
  updateDragPath(cursorX, cursorY);
1259
1287
 
1260
1288
  this.connectionDragging = true;
1261
1289
 
1262
1290
  const onMove = (me: MouseEvent) => {
1263
- const cx = me.clientX - canvasRect.left;
1264
- const cy = me.clientY - canvasRect.top;
1291
+ // Re-read canvasRect each move since scroll may have changed
1292
+ const rect = this.canvas.getBoundingClientRect();
1293
+ const cx = this.toCanvas(me.clientX - rect.left);
1294
+ const cy = this.toCanvas(me.clientY - rect.top);
1265
1295
  updateDragPath(cx, cy);
1266
1296
  };
1267
1297
 
@@ -63,7 +63,6 @@ export const send_msg: ActionConfig = {
63
63
  },
64
64
  runtime_attachments: {
65
65
  type: 'array',
66
- helpText: 'Add dynamic attachments using expressions',
67
66
  itemLabel: 'Attachment',
68
67
  sortable: true,
69
68
  maxItems: 10,
@@ -73,6 +72,7 @@ export const send_msg: ActionConfig = {
73
72
  itemConfig: {
74
73
  type: {
75
74
  type: 'select',
75
+ width: '140px',
76
76
  options: [
77
77
  { value: 'image', name: 'Image' },
78
78
  { value: 'audio', name: 'Audio' },
@@ -113,6 +113,7 @@ export const send_msg: ActionConfig = {
113
113
  collapsible: true,
114
114
  collapsed: true,
115
115
  helpText: 'Add dynamic attachments that are evaluated at runtime',
116
+ contentPadding: '12px',
116
117
  getGroupValueCount: (formData: FormData) => {
117
118
  return (
118
119
  formData.runtime_attachments?.filter(
@@ -101,7 +101,7 @@ export const wait_for_response: NodeConfig = {
101
101
  type: 'select',
102
102
  placeholder: '5 minutes',
103
103
  multi: false,
104
- maxWidth: '100px',
104
+ maxWidth: '130px',
105
105
  flavor: 'xsmall',
106
106
  options: TIMEOUT_OPTIONS,
107
107