@nyaruka/temba-components 0.132.0 → 0.134.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 (181) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/demo/components/flow/example.html +1 -0
  3. package/demo/components/webchat/example.html +1 -1
  4. package/demo/static/css/tailwind.css +30019 -0
  5. package/dist/locales/es.js +5 -5
  6. package/dist/locales/es.js.map +1 -1
  7. package/dist/locales/fr.js +5 -5
  8. package/dist/locales/fr.js.map +1 -1
  9. package/dist/locales/locale-codes.js +2 -11
  10. package/dist/locales/locale-codes.js.map +1 -1
  11. package/dist/locales/pt.js +5 -5
  12. package/dist/locales/pt.js.map +1 -1
  13. package/dist/temba-components.js +555 -476
  14. package/dist/temba-components.js.map +1 -1
  15. package/out-tsc/src/display/Chat.js +248 -95
  16. package/out-tsc/src/display/Chat.js.map +1 -1
  17. package/out-tsc/src/display/FloatingTab.js +4 -4
  18. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  19. package/out-tsc/src/display/TembaUser.js +3 -3
  20. package/out-tsc/src/display/TembaUser.js.map +1 -1
  21. package/out-tsc/src/events.js.map +1 -1
  22. package/out-tsc/src/flow/CanvasNode.js +132 -58
  23. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  24. package/out-tsc/src/flow/Editor.js +183 -58
  25. package/out-tsc/src/flow/Editor.js.map +1 -1
  26. package/out-tsc/src/flow/utils.js +141 -0
  27. package/out-tsc/src/flow/utils.js.map +1 -1
  28. package/out-tsc/src/interfaces.js.map +1 -1
  29. package/out-tsc/src/layout/FloatingWindow.js +1 -2
  30. package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
  31. package/out-tsc/src/list/ContentMenu.js +1 -0
  32. package/out-tsc/src/list/ContentMenu.js.map +1 -1
  33. package/out-tsc/src/list/SortableList.js +3 -2
  34. package/out-tsc/src/list/SortableList.js.map +1 -1
  35. package/out-tsc/src/live/ContactChat.js +184 -205
  36. package/out-tsc/src/live/ContactChat.js.map +1 -1
  37. package/out-tsc/src/locales/es.js +5 -5
  38. package/out-tsc/src/locales/es.js.map +1 -1
  39. package/out-tsc/src/locales/fr.js +5 -5
  40. package/out-tsc/src/locales/fr.js.map +1 -1
  41. package/out-tsc/src/locales/locale-codes.js +2 -11
  42. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  43. package/out-tsc/src/locales/pt.js +5 -5
  44. package/out-tsc/src/locales/pt.js.map +1 -1
  45. package/out-tsc/src/store/AppState.js +34 -0
  46. package/out-tsc/src/store/AppState.js.map +1 -1
  47. package/out-tsc/src/store/Store.js +5 -5
  48. package/out-tsc/src/store/Store.js.map +1 -1
  49. package/out-tsc/src/utils.js +3 -3
  50. package/out-tsc/src/utils.js.map +1 -1
  51. package/out-tsc/src/webchat/WebChat.js +22 -9
  52. package/out-tsc/src/webchat/WebChat.js.map +1 -1
  53. package/out-tsc/test/ActionHelper.js +6 -5
  54. package/out-tsc/test/ActionHelper.js.map +1 -1
  55. package/out-tsc/test/actions/send_broadcast.test.js +9 -4
  56. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
  57. package/out-tsc/test/temba-contact-chat.test.js +1 -1
  58. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  59. package/out-tsc/test/temba-floating-window.test.js +0 -2
  60. package/out-tsc/test/temba-floating-window.test.js.map +1 -1
  61. package/out-tsc/test/temba-flow-collision.test.js +673 -0
  62. package/out-tsc/test/temba-flow-collision.test.js.map +1 -0
  63. package/out-tsc/test/temba-flow-editor-node.test.js +195 -0
  64. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  65. package/out-tsc/test/temba-utils-uuid.test.js +45 -1
  66. package/out-tsc/test/temba-utils-uuid.test.js.map +1 -1
  67. package/out-tsc/test/utils.test.js +2 -2
  68. package/out-tsc/test/utils.test.js.map +1 -1
  69. package/package.json +1 -1
  70. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  71. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  72. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  73. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  74. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  75. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  76. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  77. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  78. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  79. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  80. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  81. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  82. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  83. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  84. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  85. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  86. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  87. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  88. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  89. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  90. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  91. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  92. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  93. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  94. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  95. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  96. package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
  97. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  98. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  99. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  100. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  101. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  102. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  103. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  104. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  105. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  106. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  107. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  108. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  109. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  110. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  111. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  112. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  113. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  114. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  115. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  116. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  117. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  118. package/screenshots/truth/contacts/chat-failure.png +0 -0
  119. package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
  120. package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
  121. package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
  122. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  123. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  124. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  125. package/screenshots/truth/floating-tab/default.png +0 -0
  126. package/screenshots/truth/floating-tab/gray.png +0 -0
  127. package/screenshots/truth/floating-tab/green.png +0 -0
  128. package/screenshots/truth/floating-tab/hover.png +0 -0
  129. package/screenshots/truth/floating-tab/purple.png +0 -0
  130. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  131. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  132. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  133. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  134. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  135. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  136. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  137. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  138. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  139. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  140. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  141. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  142. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  143. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  144. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  145. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  146. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  147. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  148. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  149. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  150. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  151. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  152. package/src/display/Chat.ts +331 -135
  153. package/src/display/FloatingTab.ts +4 -4
  154. package/src/display/TembaUser.ts +3 -2
  155. package/src/events.ts +12 -12
  156. package/src/flow/CanvasNode.ts +140 -57
  157. package/src/flow/Editor.ts +240 -58
  158. package/src/flow/utils.ts +207 -1
  159. package/src/interfaces.ts +7 -0
  160. package/src/layout/FloatingWindow.ts +1 -3
  161. package/src/list/ContentMenu.ts +1 -0
  162. package/src/list/SortableList.ts +3 -2
  163. package/src/live/ContactChat.ts +195 -221
  164. package/src/locales/es.ts +13 -18
  165. package/src/locales/fr.ts +13 -18
  166. package/src/locales/locale-codes.ts +2 -11
  167. package/src/locales/pt.ts +13 -18
  168. package/src/store/AppState.ts +43 -0
  169. package/src/store/Store.ts +5 -5
  170. package/src/utils.ts +3 -3
  171. package/src/webchat/WebChat.ts +24 -10
  172. package/test/ActionHelper.ts +13 -5
  173. package/test/actions/send_broadcast.test.ts +4 -2
  174. package/test/temba-contact-chat.test.ts +1 -1
  175. package/test/temba-floating-window.test.ts +0 -2
  176. package/test/temba-flow-collision.test.ts +833 -0
  177. package/test/temba-flow-editor-node.test.ts +224 -0
  178. package/test/temba-utils-uuid.test.ts +61 -1
  179. package/test/utils.test.ts +7 -2
  180. package/test-assets/contacts/history.json +22 -9
  181. package/web-test-runner.config.mjs +3 -3
@@ -13,7 +13,7 @@ export class FloatingTab extends RapidElement {
13
13
  .tab {
14
14
  position: fixed;
15
15
  right: 0;
16
- z-index: 9998;
16
+ z-index: 4998;
17
17
  transition: transform var(--transition-duration, 300ms) ease-in-out;
18
18
  display: flex;
19
19
  align-items: center;
@@ -36,8 +36,8 @@ export class FloatingTab extends RapidElement {
36
36
  display: flex;
37
37
  align-items: center;
38
38
  justify-content: center;
39
- width: 32px;
40
- height: 32px;
39
+ width: 16px;
40
+ height: 16px;
41
41
  }
42
42
 
43
43
  temba-icon {
@@ -164,7 +164,7 @@ export class FloatingTab extends RapidElement {
164
164
  <div class="${classes}" style="${tabStyle}" @click=${this.handleClick}>
165
165
  <div class="icon-container">
166
166
  ${this.icon
167
- ? html`<temba-icon size="2" name="${this.icon}"></temba-icon>`
167
+ ? html`<temba-icon size="1.5" name="${this.icon}"></temba-icon>`
168
168
  : ''}
169
169
  </div>
170
170
  <div class="label">${this.label}</div>
@@ -62,6 +62,9 @@ export class TembaUser extends RapidElement {
62
62
  @property({ type: String })
63
63
  email: string;
64
64
 
65
+ @property({ type: String })
66
+ uuid: string;
67
+
65
68
  @property({ type: String })
66
69
  avatar: string;
67
70
 
@@ -87,8 +90,6 @@ export class TembaUser extends RapidElement {
87
90
  if (changed.has('avatar')) {
88
91
  if (this.avatar) {
89
92
  this.bgimage = `url('${this.avatar}') center / contain no-repeat`;
90
- } else if (!this.system) {
91
- this.bgimage = null;
92
93
  }
93
94
  }
94
95
  }
package/src/events.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Msg, ObjectReference, User } from './interfaces';
2
+ import { ContactEvent } from './display/Chat';
2
3
 
3
4
  export interface EventGroup {
4
5
  type: string;
@@ -6,13 +7,6 @@ export interface EventGroup {
6
7
  open: boolean;
7
8
  }
8
9
 
9
- export interface ContactEvent {
10
- uuid?: string;
11
- type: string;
12
- created_on: string;
13
- _user?: ObjectReference;
14
- }
15
-
16
10
  export interface ChannelEvent extends ContactEvent {
17
11
  channel_event_type: string;
18
12
  duration: number;
@@ -57,7 +51,16 @@ export interface ChatStartedEvent extends ContactEvent {
57
51
  export interface MsgEvent extends ContactEvent {
58
52
  msg: Msg;
59
53
  optin?: ObjectReference;
60
- _status?: { created_on: string; status: string; reason: string };
54
+ _status?: {
55
+ created_on: string;
56
+ status: 'wired' | 'sent' | 'delivered' | 'read' | 'errored' | 'failed';
57
+ reason: 'error_limit' | 'too_old' | 'channel_removed';
58
+ };
59
+ _deleted?: {
60
+ created_on: string;
61
+ by_contact: boolean;
62
+ user: { name: string; uuid: string };
63
+ };
61
64
  _logs_url?: string;
62
65
  }
63
66
 
@@ -107,8 +110,5 @@ export type CallStartedEvent = ContactEvent;
107
110
 
108
111
  export interface ContactHistoryPage {
109
112
  events: ContactEvent[];
110
- has_older: boolean;
111
- recent_only: boolean;
112
- next_before: number;
113
- next_after: number;
113
+ next: string | null;
114
114
  }
@@ -76,6 +76,10 @@ export class CanvasNode extends RapidElement {
76
76
  actionHeight: number;
77
77
  } | null = null;
78
78
 
79
+ // Track if we're showing a placeholder for our own last action being dragged out
80
+ private showLastActionPlaceholder = false;
81
+ private lastActionPlaceholderHeight = 60;
82
+
79
83
  static get styles() {
80
84
  return css`
81
85
 
@@ -119,6 +123,7 @@ export class CanvasNode extends RapidElement {
119
123
 
120
124
  .action .cn-title:hover .remove-button,
121
125
  .router:hover .remove-button {
126
+ visibility: visible;
122
127
  opacity: 0.7;
123
128
  }
124
129
 
@@ -135,7 +140,7 @@ export class CanvasNode extends RapidElement {
135
140
  .remove-button {
136
141
  background: transparent;
137
142
  color: white;
138
- opacity: 0;
143
+ visibility: hidden;
139
144
  cursor: pointer;
140
145
  font-size: 1em;
141
146
  font-weight: 600;
@@ -143,16 +148,22 @@ export class CanvasNode extends RapidElement {
143
148
  z-index: 10;
144
149
  transition: all 100ms ease-in-out;
145
150
  align-self: center;
146
- padding:0.25em;
151
+ margin-right:0.15em;
147
152
  border: 0px solid red;
148
153
  width: 1em;
149
154
  pointer-events: auto; /* Ensure remove button can receive events */
150
155
  }
151
156
 
152
157
  .remove-button:hover {
158
+ visibility: visible;
153
159
  opacity: 1;
154
160
  }
155
161
 
162
+ .translating-hidden {
163
+ visibility: hidden !important;
164
+ pointer-events: none !important;
165
+ }
166
+
156
167
  .action.sortable {
157
168
  display: flex;
158
169
  align-items: stretch;
@@ -164,6 +175,7 @@ export class CanvasNode extends RapidElement {
164
175
  flex-direction: column;
165
176
  min-width: 0; /* Allow flex item to shrink below its content size */
166
177
  overflow: hidden;
178
+ background: #fff;
167
179
  }
168
180
 
169
181
  .action .body {
@@ -194,7 +206,7 @@ export class CanvasNode extends RapidElement {
194
206
  }
195
207
 
196
208
  .action .drag-handle {
197
- opacity: 0;
209
+ visibility: hidden;
198
210
  transition: all 200ms ease-in-out;
199
211
  cursor: move;
200
212
  background: rgba(0, 0, 0, 0.02);
@@ -209,6 +221,7 @@ export class CanvasNode extends RapidElement {
209
221
  }
210
222
 
211
223
  .action:hover .drag-handle {
224
+ visibility: visible;
212
225
  opacity: 0.7;
213
226
 
214
227
 
@@ -219,6 +232,7 @@ export class CanvasNode extends RapidElement {
219
232
  }
220
233
 
221
234
  .action .drag-handle:hover {
235
+ visibility: visible;
222
236
  opacity: 1;
223
237
 
224
238
  }
@@ -422,6 +436,18 @@ export class CanvasNode extends RapidElement {
422
436
  opacity: 1 !important;
423
437
  transform: scale(1.1);
424
438
  }
439
+
440
+ .empty-node-placeholder {
441
+ height: 60px;
442
+ background: #f3f4f6;
443
+ border: 2px dashed #d1d5db;
444
+ border-radius: var(--curvature);
445
+ display: flex;
446
+ align-items: center;
447
+ justify-content: center;
448
+ color: #9ca3af;
449
+ font-size: 0.9em;
450
+ }
425
451
  }`;
426
452
  }
427
453
 
@@ -471,6 +497,16 @@ export class CanvasNode extends RapidElement {
471
497
  changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
472
498
  ): void {
473
499
  super.updated(changes);
500
+
501
+ if (!!changes.get('ui') && changes.has('ui')) {
502
+ // run revalidation every 50ms until 350ms to catch animation updates
503
+ for (let delay = 25; delay <= 350; delay += 25) {
504
+ setTimeout(() => {
505
+ this.plumber.revalidate([this.node.uuid]);
506
+ }, delay);
507
+ }
508
+ }
509
+
474
510
  if (changes.has('node')) {
475
511
  // Only proceed if plumber is available (for tests that don't set it up)
476
512
  if (this.plumber) {
@@ -734,6 +770,8 @@ export class CanvasNode extends RapidElement {
734
770
  }
735
771
 
736
772
  // Fire the node deleted event
773
+ // The Editor will handle cleanup (Plumber connections) and call store.removeNodes()
774
+ // The store's removeNodes method handles rerouting of connections
737
775
  this.fireCustomEvent(CustomEventType.NodeDeleted, {
738
776
  uuid: this.node.uuid
739
777
  });
@@ -742,6 +780,12 @@ export class CanvasNode extends RapidElement {
742
780
  private handleActionOrderChanged(event: CustomEvent) {
743
781
  const [fromIdx, toIdx] = event.detail.swap;
744
782
 
783
+ // If we have an external drag in progress, ignore internal order changes
784
+ // as they'll be handled by the external drop handler
785
+ if (this.externalDragInfo) {
786
+ return;
787
+ }
788
+
745
789
  // swap our actions
746
790
  const newActions = [...this.node.actions];
747
791
  const movedAction = newActions.splice(fromIdx, 1)[0];
@@ -769,6 +813,13 @@ export class CanvasNode extends RapidElement {
769
813
  // Fallback to a reasonable default
770
814
  this.draggedActionHeight = 60;
771
815
  }
816
+
817
+ // If this is the last action, show placeholder
818
+ if (this.node.actions.length === 1) {
819
+ this.showLastActionPlaceholder = true;
820
+ this.lastActionPlaceholderHeight = this.draggedActionHeight;
821
+ this.requestUpdate();
822
+ }
772
823
  }
773
824
 
774
825
  private handleActionDragExternal(event: CustomEvent) {
@@ -785,6 +836,9 @@ export class CanvasNode extends RapidElement {
785
836
  const actionIndex = parseInt(splitId[1], 10);
786
837
  const action = this.node.actions[actionIndex];
787
838
 
839
+ // Check if this is the last action
840
+ const isLastAction = this.node.actions.length === 1;
841
+
788
842
  // fire event to editor to show canvas drop preview, including the captured height
789
843
  this.fireCustomEvent(CustomEventType.DragExternal, {
790
844
  action,
@@ -792,7 +846,8 @@ export class CanvasNode extends RapidElement {
792
846
  actionIndex,
793
847
  mouseX: event.detail.mouseX,
794
848
  mouseY: event.detail.mouseY,
795
- actionHeight: this.draggedActionHeight
849
+ actionHeight: this.draggedActionHeight,
850
+ isLastAction
796
851
  });
797
852
  }
798
853
 
@@ -807,6 +862,9 @@ export class CanvasNode extends RapidElement {
807
862
  private handleActionDragStop(event: CustomEvent) {
808
863
  const isExternal = event.detail.isExternal;
809
864
 
865
+ // Clear last action placeholder when drag stops
866
+ this.showLastActionPlaceholder = false;
867
+
810
868
  if (isExternal) {
811
869
  // stop propagation of the original event from SortableList
812
870
  event.stopPropagation();
@@ -821,16 +879,23 @@ export class CanvasNode extends RapidElement {
821
879
  const actionIndex = parseInt(split[1], 10);
822
880
  const action = this.node.actions[actionIndex];
823
881
 
824
- // fire event to editor to create new node
882
+ // Check if this is the last action in the node
883
+ const isLastAction = this.node.actions.length === 1;
884
+
885
+ // Always fire the DragStop event so the Editor can handle drops on other nodes
886
+ // The Editor will decide whether to create a new node or drop on existing node
825
887
  this.fireCustomEvent(CustomEventType.DragStop, {
826
888
  action,
827
889
  nodeUuid: this.node.uuid,
828
890
  actionIndex,
829
891
  isExternal: true,
892
+ isLastAction,
830
893
  mouseX: event.detail.mouseX,
831
894
  mouseY: event.detail.mouseY
832
895
  });
833
896
  }
897
+
898
+ this.requestUpdate();
834
899
  }
835
900
 
836
901
  private handleActionMouseDown(event: MouseEvent, action: Action): void {
@@ -1141,7 +1206,6 @@ export class CanvasNode extends RapidElement {
1141
1206
  // Clear external drag state
1142
1207
  this.externalDragInfo = null;
1143
1208
 
1144
- // Remove the action from the source node
1145
1209
  const store = getStore();
1146
1210
  if (!store) return;
1147
1211
 
@@ -1152,33 +1216,36 @@ export class CanvasNode extends RapidElement {
1152
1216
  (n) => n.uuid === sourceNodeUuid
1153
1217
  );
1154
1218
 
1155
- if (sourceNode) {
1156
- const updatedSourceActions = sourceNode.actions.filter(
1157
- (_a, idx) => idx !== actionIndex
1158
- );
1159
-
1160
- // If source node has no actions left, remove it
1161
- if (updatedSourceActions.length === 0) {
1162
- this.fireCustomEvent(CustomEventType.NodeDeleted, {
1163
- uuid: sourceNodeUuid
1164
- });
1165
- } else {
1166
- // Update source node
1167
- const updatedSourceNode = {
1168
- ...sourceNode,
1169
- actions: updatedSourceActions
1170
- };
1171
- getStore()?.getState().updateNode(sourceNodeUuid, updatedSourceNode);
1172
- }
1173
- }
1219
+ if (!sourceNode) return;
1174
1220
 
1175
- // Add the action to this node at the calculated position
1221
+ // IMPORTANT: Add the action to this node FIRST, before removing from source
1222
+ // This ensures we don't lose the action if the source node gets deleted
1176
1223
  const newActions = [...this.node.actions];
1177
1224
  newActions.splice(dropIndex, 0, action);
1178
1225
 
1179
1226
  const updatedNode = { ...this.node, actions: newActions };
1180
1227
  getStore()?.getState().updateNode(this.node.uuid, updatedNode);
1181
1228
 
1229
+ // Now remove the action from the source node
1230
+ const updatedSourceActions = sourceNode.actions.filter(
1231
+ (_a, idx) => idx !== actionIndex
1232
+ );
1233
+
1234
+ // If source node has no actions left, remove it
1235
+ if (updatedSourceActions.length === 0) {
1236
+ // Fire event to Editor so it can clean up jsPlumb connections properly
1237
+ this.fireCustomEvent(CustomEventType.NodeDeleted, {
1238
+ uuid: sourceNodeUuid
1239
+ });
1240
+ } else {
1241
+ // Update source node
1242
+ const updatedSourceNode = {
1243
+ ...sourceNode,
1244
+ actions: updatedSourceActions
1245
+ };
1246
+ getStore()?.getState().updateNode(sourceNodeUuid, updatedSourceNode);
1247
+ }
1248
+
1182
1249
  // Request update
1183
1250
  this.requestUpdate();
1184
1251
  }
@@ -1193,21 +1260,31 @@ export class CanvasNode extends RapidElement {
1193
1260
  ? ACTION_GROUP_METADATA[config.group]?.color
1194
1261
  : '#aaaaaa';
1195
1262
  return html`<div class="cn-title" style="background:${color}">
1196
- ${!this.isTranslating && this.node?.actions?.length > 1
1197
- ? html`<temba-icon class="drag-handle" name="sort"></temba-icon>`
1263
+ ${this.ui?.type === 'execute_actions'
1264
+ ? html`<temba-icon
1265
+ class="drag-handle ${this.isTranslating
1266
+ ? 'translating-hidden'
1267
+ : ''}"
1268
+ name="sort"
1269
+ ></temba-icon>`
1270
+ : this.node?.actions?.length > 1
1271
+ ? html`<temba-icon
1272
+ class="drag-handle ${this.isTranslating
1273
+ ? 'translating-hidden'
1274
+ : ''}"
1275
+ name="sort"
1276
+ ></temba-icon>`
1198
1277
  : html`<div class="title-spacer"></div>`}
1199
1278
 
1200
1279
  <div class="name">${isRemoving ? 'Remove?' : config.name}</div>
1201
- ${!this.isTranslating
1202
- ? html`<div
1203
- class="remove-button"
1204
- @click=${(e: MouseEvent) =>
1205
- this.handleActionRemoveClick(e, action, index)}
1206
- title="Remove action"
1207
- >
1208
-
1209
- </div>`
1210
- : html`<div class="title-spacer"></div>`}
1280
+ <div
1281
+ class="remove-button ${this.isTranslating ? 'translating-hidden' : ''}"
1282
+ @click=${(e: MouseEvent) =>
1283
+ this.handleActionRemoveClick(e, action, index)}
1284
+ title="Remove action"
1285
+ >
1286
+
1287
+ </div>
1211
1288
  </div>`;
1212
1289
  }
1213
1290
 
@@ -1234,15 +1311,13 @@ export class CanvasNode extends RapidElement {
1234
1311
  ? config.renderTitle(node, ui)
1235
1312
  : html`${config.name}`}
1236
1313
  </div>
1237
- ${!this.isTranslating
1238
- ? html`<div
1239
- class="remove-button"
1240
- @click=${(e: MouseEvent) => this.handleNodeRemoveClick(e)}
1241
- title="Remove node"
1242
- >
1243
-
1244
- </div>`
1245
- : html`<div class="title-spacer"></div>`}
1314
+ <div
1315
+ class="remove-button ${this.isTranslating ? 'translating-hidden' : ''}"
1316
+ @click=${(e: MouseEvent) => this.handleNodeRemoveClick(e)}
1317
+ title="Remove node"
1318
+ >
1319
+
1320
+ </div>
1246
1321
  </div>`;
1247
1322
  }
1248
1323
 
@@ -1528,19 +1603,27 @@ export class CanvasNode extends RapidElement {
1528
1603
  : this.node.actions.length > 0
1529
1604
  ? this.ui.type === 'execute_actions'
1530
1605
  ? html`<temba-sortable-list
1531
- dragHandle="drag-handle"
1532
- externalDrag
1533
- @temba-order-changed="${this.handleActionOrderChanged}"
1534
- @temba-drag-start="${this.handleActionDragStart}"
1535
- @temba-drag-external="${this.handleActionDragExternal}"
1536
- @temba-drag-internal="${this.handleActionDragInternal}"
1537
- @temba-drag-stop="${this.handleActionDragStop}"
1538
- >
1539
- ${this.renderActionsWithPlaceholder()}
1540
- </temba-sortable-list>`
1606
+ dragHandle="drag-handle"
1607
+ externalDrag
1608
+ @temba-order-changed="${this.handleActionOrderChanged}"
1609
+ @temba-drag-start="${this.handleActionDragStart}"
1610
+ @temba-drag-external="${this.handleActionDragExternal}"
1611
+ @temba-drag-internal="${this.handleActionDragInternal}"
1612
+ @temba-drag-stop="${this.handleActionDragStop}"
1613
+ >
1614
+ ${this.renderActionsWithPlaceholder()}
1615
+ </temba-sortable-list>
1616
+ ${this.showLastActionPlaceholder
1617
+ ? html`<div
1618
+ class="empty-node-placeholder"
1619
+ style="height: ${this.lastActionPlaceholderHeight}px;"
1620
+ ></div>`
1621
+ : ''}`
1541
1622
  : html`${this.node.actions.map((action, index) =>
1542
1623
  this.renderAction(this.node, action, index)
1543
1624
  )}`
1625
+ : this.ui.type === 'execute_actions'
1626
+ ? html`<div class="empty-node-placeholder"></div>`
1544
1627
  : ''}
1545
1628
  ${this.node.router
1546
1629
  ? html`<div class="router-section">