@nyaruka/temba-components 0.132.0 → 0.133.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 (143) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/demo/components/flow/example.html +1 -0
  3. package/demo/static/css/tailwind.css +30019 -0
  4. package/dist/temba-components.js +434 -402
  5. package/dist/temba-components.js.map +1 -1
  6. package/out-tsc/src/display/Chat.js +26 -6
  7. package/out-tsc/src/display/Chat.js.map +1 -1
  8. package/out-tsc/src/display/FloatingTab.js +4 -4
  9. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  10. package/out-tsc/src/events.js.map +1 -1
  11. package/out-tsc/src/flow/CanvasNode.js +124 -58
  12. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  13. package/out-tsc/src/flow/Editor.js +66 -30
  14. package/out-tsc/src/flow/Editor.js.map +1 -1
  15. package/out-tsc/src/layout/FloatingWindow.js +1 -2
  16. package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
  17. package/out-tsc/src/list/ContentMenu.js +1 -0
  18. package/out-tsc/src/list/ContentMenu.js.map +1 -1
  19. package/out-tsc/src/list/SortableList.js +3 -2
  20. package/out-tsc/src/list/SortableList.js.map +1 -1
  21. package/out-tsc/src/live/ContactChat.js +63 -35
  22. package/out-tsc/src/live/ContactChat.js.map +1 -1
  23. package/out-tsc/src/store/AppState.js +31 -0
  24. package/out-tsc/src/store/AppState.js.map +1 -1
  25. package/out-tsc/src/utils.js +3 -3
  26. package/out-tsc/src/utils.js.map +1 -1
  27. package/out-tsc/test/ActionHelper.js +6 -5
  28. package/out-tsc/test/ActionHelper.js.map +1 -1
  29. package/out-tsc/test/actions/send_broadcast.test.js +1 -1
  30. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
  31. package/out-tsc/test/temba-contact-chat.test.js +1 -1
  32. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  33. package/out-tsc/test/temba-floating-window.test.js +0 -2
  34. package/out-tsc/test/temba-floating-window.test.js.map +1 -1
  35. package/out-tsc/test/temba-flow-editor-node.test.js +109 -0
  36. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  37. package/out-tsc/test/temba-utils-uuid.test.js +45 -1
  38. package/out-tsc/test/temba-utils-uuid.test.js.map +1 -1
  39. package/out-tsc/test/utils.test.js +2 -2
  40. package/out-tsc/test/utils.test.js.map +1 -1
  41. package/package.json +1 -1
  42. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  43. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  44. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  45. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  46. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  47. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  48. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  49. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  50. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  51. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  52. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  53. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  54. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  55. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  56. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  57. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  58. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  59. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  60. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  61. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  62. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  63. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  64. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  65. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  66. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  67. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  68. package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
  69. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  70. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  71. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  72. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  73. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  74. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  75. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  76. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  77. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  78. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  79. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  80. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  81. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  82. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  83. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  84. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  85. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  86. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  87. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  88. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  89. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  90. package/screenshots/truth/contacts/chat-failure.png +0 -0
  91. package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
  92. package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
  93. package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
  94. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  95. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  96. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  97. package/screenshots/truth/floating-tab/default.png +0 -0
  98. package/screenshots/truth/floating-tab/gray.png +0 -0
  99. package/screenshots/truth/floating-tab/green.png +0 -0
  100. package/screenshots/truth/floating-tab/hover.png +0 -0
  101. package/screenshots/truth/floating-tab/purple.png +0 -0
  102. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  103. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  104. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  105. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  106. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  107. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  108. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  109. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  110. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  111. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  112. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  113. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  114. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  115. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  116. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  117. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  118. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  119. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  120. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  121. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  122. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  123. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  124. package/src/display/Chat.ts +29 -7
  125. package/src/display/FloatingTab.ts +4 -4
  126. package/src/events.ts +1 -4
  127. package/src/flow/CanvasNode.ts +130 -57
  128. package/src/flow/Editor.ts +84 -30
  129. package/src/layout/FloatingWindow.ts +1 -3
  130. package/src/list/ContentMenu.ts +1 -0
  131. package/src/list/SortableList.ts +3 -2
  132. package/src/live/ContactChat.ts +68 -42
  133. package/src/store/AppState.ts +41 -0
  134. package/src/utils.ts +3 -3
  135. package/test/ActionHelper.ts +13 -5
  136. package/test/actions/send_broadcast.test.ts +2 -1
  137. package/test/temba-contact-chat.test.ts +1 -1
  138. package/test/temba-floating-window.test.ts +0 -2
  139. package/test/temba-flow-editor-node.test.ts +129 -0
  140. package/test/temba-utils-uuid.test.ts +61 -1
  141. package/test/utils.test.ts +7 -2
  142. package/test-assets/contacts/history.json +22 -9
  143. package/web-test-runner.config.mjs +3 -3
@@ -57,6 +57,7 @@ export class CanvasNode extends RapidElement {
57
57
 
58
58
  .action .cn-title:hover .remove-button,
59
59
  .router:hover .remove-button {
60
+ visibility: visible;
60
61
  opacity: 0.7;
61
62
  }
62
63
 
@@ -73,7 +74,7 @@ export class CanvasNode extends RapidElement {
73
74
  .remove-button {
74
75
  background: transparent;
75
76
  color: white;
76
- opacity: 0;
77
+ visibility: hidden;
77
78
  cursor: pointer;
78
79
  font-size: 1em;
79
80
  font-weight: 600;
@@ -81,16 +82,22 @@ export class CanvasNode extends RapidElement {
81
82
  z-index: 10;
82
83
  transition: all 100ms ease-in-out;
83
84
  align-self: center;
84
- padding:0.25em;
85
+ margin-right:0.15em;
85
86
  border: 0px solid red;
86
87
  width: 1em;
87
88
  pointer-events: auto; /* Ensure remove button can receive events */
88
89
  }
89
90
 
90
91
  .remove-button:hover {
92
+ visibility: visible;
91
93
  opacity: 1;
92
94
  }
93
95
 
96
+ .translating-hidden {
97
+ visibility: hidden !important;
98
+ pointer-events: none !important;
99
+ }
100
+
94
101
  .action.sortable {
95
102
  display: flex;
96
103
  align-items: stretch;
@@ -102,6 +109,7 @@ export class CanvasNode extends RapidElement {
102
109
  flex-direction: column;
103
110
  min-width: 0; /* Allow flex item to shrink below its content size */
104
111
  overflow: hidden;
112
+ background: #fff;
105
113
  }
106
114
 
107
115
  .action .body {
@@ -132,7 +140,7 @@ export class CanvasNode extends RapidElement {
132
140
  }
133
141
 
134
142
  .action .drag-handle {
135
- opacity: 0;
143
+ visibility: hidden;
136
144
  transition: all 200ms ease-in-out;
137
145
  cursor: move;
138
146
  background: rgba(0, 0, 0, 0.02);
@@ -147,6 +155,7 @@ export class CanvasNode extends RapidElement {
147
155
  }
148
156
 
149
157
  .action:hover .drag-handle {
158
+ visibility: visible;
150
159
  opacity: 0.7;
151
160
 
152
161
 
@@ -157,6 +166,7 @@ export class CanvasNode extends RapidElement {
157
166
  }
158
167
 
159
168
  .action .drag-handle:hover {
169
+ visibility: visible;
160
170
  opacity: 1;
161
171
 
162
172
  }
@@ -360,6 +370,18 @@ export class CanvasNode extends RapidElement {
360
370
  opacity: 1 !important;
361
371
  transform: scale(1.1);
362
372
  }
373
+
374
+ .empty-node-placeholder {
375
+ height: 60px;
376
+ background: #f3f4f6;
377
+ border: 2px dashed #d1d5db;
378
+ border-radius: var(--curvature);
379
+ display: flex;
380
+ align-items: center;
381
+ justify-content: center;
382
+ color: #9ca3af;
383
+ font-size: 0.9em;
384
+ }
363
385
  }`;
364
386
  }
365
387
  constructor() {
@@ -382,6 +404,9 @@ export class CanvasNode extends RapidElement {
382
404
  this.draggedActionHeight = 0;
383
405
  // Track external action drag (action being dragged from another node)
384
406
  this.externalDragInfo = null;
407
+ // Track if we're showing a placeholder for our own last action being dragged out
408
+ this.showLastActionPlaceholder = false;
409
+ this.lastActionPlaceholderHeight = 60;
385
410
  this.handleActionOrderChanged = this.handleActionOrderChanged.bind(this);
386
411
  this.handleActionDragStart = this.handleActionDragStart.bind(this);
387
412
  this.handleActionDragExternal = this.handleActionDragExternal.bind(this);
@@ -594,6 +619,8 @@ export class CanvasNode extends RapidElement {
594
619
  this.actionRemovalTimeouts.delete(nodeId);
595
620
  }
596
621
  // Fire the node deleted event
622
+ // The Editor will handle cleanup (Plumber connections) and call store.removeNodes()
623
+ // The store's removeNodes method handles rerouting of connections
597
624
  this.fireCustomEvent(CustomEventType.NodeDeleted, {
598
625
  uuid: this.node.uuid
599
626
  });
@@ -601,6 +628,11 @@ export class CanvasNode extends RapidElement {
601
628
  handleActionOrderChanged(event) {
602
629
  var _b;
603
630
  const [fromIdx, toIdx] = event.detail.swap;
631
+ // If we have an external drag in progress, ignore internal order changes
632
+ // as they'll be handled by the external drop handler
633
+ if (this.externalDragInfo) {
634
+ return;
635
+ }
604
636
  // swap our actions
605
637
  const newActions = [...this.node.actions];
606
638
  const movedAction = newActions.splice(fromIdx, 1)[0];
@@ -623,6 +655,12 @@ export class CanvasNode extends RapidElement {
623
655
  // Fallback to a reasonable default
624
656
  this.draggedActionHeight = 60;
625
657
  }
658
+ // If this is the last action, show placeholder
659
+ if (this.node.actions.length === 1) {
660
+ this.showLastActionPlaceholder = true;
661
+ this.lastActionPlaceholderHeight = this.draggedActionHeight;
662
+ this.requestUpdate();
663
+ }
626
664
  }
627
665
  handleActionDragExternal(event) {
628
666
  // stop propagation of the original event from SortableList
@@ -636,6 +674,8 @@ export class CanvasNode extends RapidElement {
636
674
  }
637
675
  const actionIndex = parseInt(splitId[1], 10);
638
676
  const action = this.node.actions[actionIndex];
677
+ // Check if this is the last action
678
+ const isLastAction = this.node.actions.length === 1;
639
679
  // fire event to editor to show canvas drop preview, including the captured height
640
680
  this.fireCustomEvent(CustomEventType.DragExternal, {
641
681
  action,
@@ -643,7 +683,8 @@ export class CanvasNode extends RapidElement {
643
683
  actionIndex,
644
684
  mouseX: event.detail.mouseX,
645
685
  mouseY: event.detail.mouseY,
646
- actionHeight: this.draggedActionHeight
686
+ actionHeight: this.draggedActionHeight,
687
+ isLastAction
647
688
  });
648
689
  }
649
690
  handleActionDragInternal(_event) {
@@ -654,6 +695,8 @@ export class CanvasNode extends RapidElement {
654
695
  }
655
696
  handleActionDragStop(event) {
656
697
  const isExternal = event.detail.isExternal;
698
+ // Clear last action placeholder when drag stops
699
+ this.showLastActionPlaceholder = false;
657
700
  if (isExternal) {
658
701
  // stop propagation of the original event from SortableList
659
702
  event.stopPropagation();
@@ -666,16 +709,21 @@ export class CanvasNode extends RapidElement {
666
709
  }
667
710
  const actionIndex = parseInt(split[1], 10);
668
711
  const action = this.node.actions[actionIndex];
669
- // fire event to editor to create new node
712
+ // Check if this is the last action in the node
713
+ const isLastAction = this.node.actions.length === 1;
714
+ // Always fire the DragStop event so the Editor can handle drops on other nodes
715
+ // The Editor will decide whether to create a new node or drop on existing node
670
716
  this.fireCustomEvent(CustomEventType.DragStop, {
671
717
  action,
672
718
  nodeUuid: this.node.uuid,
673
719
  actionIndex,
674
720
  isExternal: true,
721
+ isLastAction,
675
722
  mouseX: event.detail.mouseX,
676
723
  mouseY: event.detail.mouseY
677
724
  });
678
725
  }
726
+ this.requestUpdate();
679
727
  }
680
728
  handleActionMouseDown(event, action) {
681
729
  // Don't handle clicks on the remove button, drag handle, or when action is in removing state
@@ -931,7 +979,6 @@ export class CanvasNode extends RapidElement {
931
979
  const dropIndex = (_e = (_c = (_b = this.externalDragInfo) === null || _b === void 0 ? void 0 : _b.dropIndex) !== null && _c !== void 0 ? _c : (_d = this.node.actions) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0;
932
980
  // Clear external drag state
933
981
  this.externalDragInfo = null;
934
- // Remove the action from the source node
935
982
  const store = getStore();
936
983
  if (!store)
937
984
  return;
@@ -939,51 +986,64 @@ export class CanvasNode extends RapidElement {
939
986
  if (!flowDefinition)
940
987
  return;
941
988
  const sourceNode = flowDefinition.nodes.find((n) => n.uuid === sourceNodeUuid);
942
- if (sourceNode) {
943
- const updatedSourceActions = sourceNode.actions.filter((_a, idx) => idx !== actionIndex);
944
- // If source node has no actions left, remove it
945
- if (updatedSourceActions.length === 0) {
946
- this.fireCustomEvent(CustomEventType.NodeDeleted, {
947
- uuid: sourceNodeUuid
948
- });
949
- }
950
- else {
951
- // Update source node
952
- const updatedSourceNode = {
953
- ...sourceNode,
954
- actions: updatedSourceActions
955
- };
956
- (_f = getStore()) === null || _f === void 0 ? void 0 : _f.getState().updateNode(sourceNodeUuid, updatedSourceNode);
957
- }
958
- }
959
- // Add the action to this node at the calculated position
989
+ if (!sourceNode)
990
+ return;
991
+ // IMPORTANT: Add the action to this node FIRST, before removing from source
992
+ // This ensures we don't lose the action if the source node gets deleted
960
993
  const newActions = [...this.node.actions];
961
994
  newActions.splice(dropIndex, 0, action);
962
995
  const updatedNode = { ...this.node, actions: newActions };
963
- (_g = getStore()) === null || _g === void 0 ? void 0 : _g.getState().updateNode(this.node.uuid, updatedNode);
996
+ (_f = getStore()) === null || _f === void 0 ? void 0 : _f.getState().updateNode(this.node.uuid, updatedNode);
997
+ // Now remove the action from the source node
998
+ const updatedSourceActions = sourceNode.actions.filter((_a, idx) => idx !== actionIndex);
999
+ // If source node has no actions left, remove it
1000
+ if (updatedSourceActions.length === 0) {
1001
+ // Fire event to Editor so it can clean up jsPlumb connections properly
1002
+ this.fireCustomEvent(CustomEventType.NodeDeleted, {
1003
+ uuid: sourceNodeUuid
1004
+ });
1005
+ }
1006
+ else {
1007
+ // Update source node
1008
+ const updatedSourceNode = {
1009
+ ...sourceNode,
1010
+ actions: updatedSourceActions
1011
+ };
1012
+ (_g = getStore()) === null || _g === void 0 ? void 0 : _g.getState().updateNode(sourceNodeUuid, updatedSourceNode);
1013
+ }
964
1014
  // Request update
965
1015
  this.requestUpdate();
966
1016
  }
967
1017
  renderTitle(config, action, index, isRemoving = false) {
968
- var _b, _c, _d;
1018
+ var _b, _c, _d, _e;
969
1019
  const color = config.group
970
1020
  ? (_b = ACTION_GROUP_METADATA[config.group]) === null || _b === void 0 ? void 0 : _b.color
971
1021
  : '#aaaaaa';
972
1022
  return html `<div class="cn-title" style="background:${color}">
973
- ${!this.isTranslating && ((_d = (_c = this.node) === null || _c === void 0 ? void 0 : _c.actions) === null || _d === void 0 ? void 0 : _d.length) > 1
974
- ? html `<temba-icon class="drag-handle" name="sort"></temba-icon>`
975
- : html `<div class="title-spacer"></div>`}
1023
+ ${((_c = this.ui) === null || _c === void 0 ? void 0 : _c.type) === 'execute_actions'
1024
+ ? html `<temba-icon
1025
+ class="drag-handle ${this.isTranslating
1026
+ ? 'translating-hidden'
1027
+ : ''}"
1028
+ name="sort"
1029
+ ></temba-icon>`
1030
+ : ((_e = (_d = this.node) === null || _d === void 0 ? void 0 : _d.actions) === null || _e === void 0 ? void 0 : _e.length) > 1
1031
+ ? html `<temba-icon
1032
+ class="drag-handle ${this.isTranslating
1033
+ ? 'translating-hidden'
1034
+ : ''}"
1035
+ name="sort"
1036
+ ></temba-icon>`
1037
+ : html `<div class="title-spacer"></div>`}
976
1038
 
977
1039
  <div class="name">${isRemoving ? 'Remove?' : config.name}</div>
978
- ${!this.isTranslating
979
- ? html `<div
980
- class="remove-button"
981
- @click=${(e) => this.handleActionRemoveClick(e, action, index)}
982
- title="Remove action"
983
- >
984
-
985
- </div>`
986
- : html `<div class="title-spacer"></div>`}
1040
+ <div
1041
+ class="remove-button ${this.isTranslating ? 'translating-hidden' : ''}"
1042
+ @click=${(e) => this.handleActionRemoveClick(e, action, index)}
1043
+ title="Remove action"
1044
+ >
1045
+
1046
+ </div>
987
1047
  </div>`;
988
1048
  }
989
1049
  renderNodeTitle(config, node, ui, isRemoving = false) {
@@ -1005,15 +1065,13 @@ export class CanvasNode extends RapidElement {
1005
1065
  ? config.renderTitle(node, ui)
1006
1066
  : html `${config.name}`}
1007
1067
  </div>
1008
- ${!this.isTranslating
1009
- ? html `<div
1010
- class="remove-button"
1011
- @click=${(e) => this.handleNodeRemoveClick(e)}
1012
- title="Remove node"
1013
- >
1014
-
1015
- </div>`
1016
- : html `<div class="title-spacer"></div>`}
1068
+ <div
1069
+ class="remove-button ${this.isTranslating ? 'translating-hidden' : ''}"
1070
+ @click=${(e) => this.handleNodeRemoveClick(e)}
1071
+ title="Remove node"
1072
+ >
1073
+
1074
+ </div>
1017
1075
  </div>`;
1018
1076
  }
1019
1077
  renderDropPlaceholder() {
@@ -1251,18 +1309,26 @@ export class CanvasNode extends RapidElement {
1251
1309
  : this.node.actions.length > 0
1252
1310
  ? this.ui.type === 'execute_actions'
1253
1311
  ? html `<temba-sortable-list
1254
- dragHandle="drag-handle"
1255
- externalDrag
1256
- @temba-order-changed="${this.handleActionOrderChanged}"
1257
- @temba-drag-start="${this.handleActionDragStart}"
1258
- @temba-drag-external="${this.handleActionDragExternal}"
1259
- @temba-drag-internal="${this.handleActionDragInternal}"
1260
- @temba-drag-stop="${this.handleActionDragStop}"
1261
- >
1262
- ${this.renderActionsWithPlaceholder()}
1263
- </temba-sortable-list>`
1312
+ dragHandle="drag-handle"
1313
+ externalDrag
1314
+ @temba-order-changed="${this.handleActionOrderChanged}"
1315
+ @temba-drag-start="${this.handleActionDragStart}"
1316
+ @temba-drag-external="${this.handleActionDragExternal}"
1317
+ @temba-drag-internal="${this.handleActionDragInternal}"
1318
+ @temba-drag-stop="${this.handleActionDragStop}"
1319
+ >
1320
+ ${this.renderActionsWithPlaceholder()}
1321
+ </temba-sortable-list>
1322
+ ${this.showLastActionPlaceholder
1323
+ ? html `<div
1324
+ class="empty-node-placeholder"
1325
+ style="height: ${this.lastActionPlaceholderHeight}px;"
1326
+ ></div>`
1327
+ : ''}`
1264
1328
  : html `${this.node.actions.map((action, index) => this.renderAction(this.node, action, index))}`
1265
- : ''}
1329
+ : this.ui.type === 'execute_actions'
1330
+ ? html `<div class="empty-node-placeholder"></div>`
1331
+ : ''}
1266
1332
  ${this.node.router
1267
1333
  ? html `<div class="router-section">
1268
1334
  ${this.renderRouter(this.node.router, this.ui)}