@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.
- package/CHANGELOG.md +20 -1
- package/demo/components/flow/example.html +1 -0
- package/demo/static/css/tailwind.css +30019 -0
- package/dist/temba-components.js +434 -402
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +26 -6
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +4 -4
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/events.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +124 -58
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +66 -30
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/layout/FloatingWindow.js +1 -2
- package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
- package/out-tsc/src/list/ContentMenu.js +1 -0
- package/out-tsc/src/list/ContentMenu.js.map +1 -1
- package/out-tsc/src/list/SortableList.js +3 -2
- package/out-tsc/src/list/SortableList.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +63 -35
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/store/AppState.js +31 -0
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/src/utils.js +3 -3
- package/out-tsc/src/utils.js.map +1 -1
- package/out-tsc/test/ActionHelper.js +6 -5
- package/out-tsc/test/ActionHelper.js.map +1 -1
- package/out-tsc/test/actions/send_broadcast.test.js +1 -1
- package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
- package/out-tsc/test/temba-contact-chat.test.js +1 -1
- package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
- package/out-tsc/test/temba-floating-window.test.js +0 -2
- package/out-tsc/test/temba-floating-window.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor-node.test.js +109 -0
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/out-tsc/test/temba-utils-uuid.test.js +45 -1
- package/out-tsc/test/temba-utils-uuid.test.js.map +1 -1
- package/out-tsc/test/utils.test.js +2 -2
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
- package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
- package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
- package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
- package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
- package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
- package/screenshots/truth/contacts/chat-failure.png +0 -0
- package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
- package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
- package/screenshots/truth/floating-tab/default.png +0 -0
- package/screenshots/truth/floating-tab/gray.png +0 -0
- package/screenshots/truth/floating-tab/green.png +0 -0
- package/screenshots/truth/floating-tab/hover.png +0 -0
- package/screenshots/truth/floating-tab/purple.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
- package/src/display/Chat.ts +29 -7
- package/src/display/FloatingTab.ts +4 -4
- package/src/events.ts +1 -4
- package/src/flow/CanvasNode.ts +130 -57
- package/src/flow/Editor.ts +84 -30
- package/src/layout/FloatingWindow.ts +1 -3
- package/src/list/ContentMenu.ts +1 -0
- package/src/list/SortableList.ts +3 -2
- package/src/live/ContactChat.ts +68 -42
- package/src/store/AppState.ts +41 -0
- package/src/utils.ts +3 -3
- package/test/ActionHelper.ts +13 -5
- package/test/actions/send_broadcast.test.ts +2 -1
- package/test/temba-contact-chat.test.ts +1 -1
- package/test/temba-floating-window.test.ts +0 -2
- package/test/temba-flow-editor-node.test.ts +129 -0
- package/test/temba-utils-uuid.test.ts +61 -1
- package/test/utils.test.ts +7 -2
- package/test-assets/contacts/history.json +22 -9
- package/web-test-runner.config.mjs +3 -3
package/src/flow/CanvasNode.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -734,6 +760,8 @@ export class CanvasNode extends RapidElement {
|
|
|
734
760
|
}
|
|
735
761
|
|
|
736
762
|
// Fire the node deleted event
|
|
763
|
+
// The Editor will handle cleanup (Plumber connections) and call store.removeNodes()
|
|
764
|
+
// The store's removeNodes method handles rerouting of connections
|
|
737
765
|
this.fireCustomEvent(CustomEventType.NodeDeleted, {
|
|
738
766
|
uuid: this.node.uuid
|
|
739
767
|
});
|
|
@@ -742,6 +770,12 @@ export class CanvasNode extends RapidElement {
|
|
|
742
770
|
private handleActionOrderChanged(event: CustomEvent) {
|
|
743
771
|
const [fromIdx, toIdx] = event.detail.swap;
|
|
744
772
|
|
|
773
|
+
// If we have an external drag in progress, ignore internal order changes
|
|
774
|
+
// as they'll be handled by the external drop handler
|
|
775
|
+
if (this.externalDragInfo) {
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
745
779
|
// swap our actions
|
|
746
780
|
const newActions = [...this.node.actions];
|
|
747
781
|
const movedAction = newActions.splice(fromIdx, 1)[0];
|
|
@@ -769,6 +803,13 @@ export class CanvasNode extends RapidElement {
|
|
|
769
803
|
// Fallback to a reasonable default
|
|
770
804
|
this.draggedActionHeight = 60;
|
|
771
805
|
}
|
|
806
|
+
|
|
807
|
+
// If this is the last action, show placeholder
|
|
808
|
+
if (this.node.actions.length === 1) {
|
|
809
|
+
this.showLastActionPlaceholder = true;
|
|
810
|
+
this.lastActionPlaceholderHeight = this.draggedActionHeight;
|
|
811
|
+
this.requestUpdate();
|
|
812
|
+
}
|
|
772
813
|
}
|
|
773
814
|
|
|
774
815
|
private handleActionDragExternal(event: CustomEvent) {
|
|
@@ -785,6 +826,9 @@ export class CanvasNode extends RapidElement {
|
|
|
785
826
|
const actionIndex = parseInt(splitId[1], 10);
|
|
786
827
|
const action = this.node.actions[actionIndex];
|
|
787
828
|
|
|
829
|
+
// Check if this is the last action
|
|
830
|
+
const isLastAction = this.node.actions.length === 1;
|
|
831
|
+
|
|
788
832
|
// fire event to editor to show canvas drop preview, including the captured height
|
|
789
833
|
this.fireCustomEvent(CustomEventType.DragExternal, {
|
|
790
834
|
action,
|
|
@@ -792,7 +836,8 @@ export class CanvasNode extends RapidElement {
|
|
|
792
836
|
actionIndex,
|
|
793
837
|
mouseX: event.detail.mouseX,
|
|
794
838
|
mouseY: event.detail.mouseY,
|
|
795
|
-
actionHeight: this.draggedActionHeight
|
|
839
|
+
actionHeight: this.draggedActionHeight,
|
|
840
|
+
isLastAction
|
|
796
841
|
});
|
|
797
842
|
}
|
|
798
843
|
|
|
@@ -807,6 +852,9 @@ export class CanvasNode extends RapidElement {
|
|
|
807
852
|
private handleActionDragStop(event: CustomEvent) {
|
|
808
853
|
const isExternal = event.detail.isExternal;
|
|
809
854
|
|
|
855
|
+
// Clear last action placeholder when drag stops
|
|
856
|
+
this.showLastActionPlaceholder = false;
|
|
857
|
+
|
|
810
858
|
if (isExternal) {
|
|
811
859
|
// stop propagation of the original event from SortableList
|
|
812
860
|
event.stopPropagation();
|
|
@@ -821,16 +869,23 @@ export class CanvasNode extends RapidElement {
|
|
|
821
869
|
const actionIndex = parseInt(split[1], 10);
|
|
822
870
|
const action = this.node.actions[actionIndex];
|
|
823
871
|
|
|
824
|
-
//
|
|
872
|
+
// Check if this is the last action in the node
|
|
873
|
+
const isLastAction = this.node.actions.length === 1;
|
|
874
|
+
|
|
875
|
+
// Always fire the DragStop event so the Editor can handle drops on other nodes
|
|
876
|
+
// The Editor will decide whether to create a new node or drop on existing node
|
|
825
877
|
this.fireCustomEvent(CustomEventType.DragStop, {
|
|
826
878
|
action,
|
|
827
879
|
nodeUuid: this.node.uuid,
|
|
828
880
|
actionIndex,
|
|
829
881
|
isExternal: true,
|
|
882
|
+
isLastAction,
|
|
830
883
|
mouseX: event.detail.mouseX,
|
|
831
884
|
mouseY: event.detail.mouseY
|
|
832
885
|
});
|
|
833
886
|
}
|
|
887
|
+
|
|
888
|
+
this.requestUpdate();
|
|
834
889
|
}
|
|
835
890
|
|
|
836
891
|
private handleActionMouseDown(event: MouseEvent, action: Action): void {
|
|
@@ -1141,7 +1196,6 @@ export class CanvasNode extends RapidElement {
|
|
|
1141
1196
|
// Clear external drag state
|
|
1142
1197
|
this.externalDragInfo = null;
|
|
1143
1198
|
|
|
1144
|
-
// Remove the action from the source node
|
|
1145
1199
|
const store = getStore();
|
|
1146
1200
|
if (!store) return;
|
|
1147
1201
|
|
|
@@ -1152,33 +1206,36 @@ export class CanvasNode extends RapidElement {
|
|
|
1152
1206
|
(n) => n.uuid === sourceNodeUuid
|
|
1153
1207
|
);
|
|
1154
1208
|
|
|
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
|
-
}
|
|
1209
|
+
if (!sourceNode) return;
|
|
1174
1210
|
|
|
1175
|
-
// Add the action to this node
|
|
1211
|
+
// IMPORTANT: Add the action to this node FIRST, before removing from source
|
|
1212
|
+
// This ensures we don't lose the action if the source node gets deleted
|
|
1176
1213
|
const newActions = [...this.node.actions];
|
|
1177
1214
|
newActions.splice(dropIndex, 0, action);
|
|
1178
1215
|
|
|
1179
1216
|
const updatedNode = { ...this.node, actions: newActions };
|
|
1180
1217
|
getStore()?.getState().updateNode(this.node.uuid, updatedNode);
|
|
1181
1218
|
|
|
1219
|
+
// Now remove the action from the source node
|
|
1220
|
+
const updatedSourceActions = sourceNode.actions.filter(
|
|
1221
|
+
(_a, idx) => idx !== actionIndex
|
|
1222
|
+
);
|
|
1223
|
+
|
|
1224
|
+
// If source node has no actions left, remove it
|
|
1225
|
+
if (updatedSourceActions.length === 0) {
|
|
1226
|
+
// Fire event to Editor so it can clean up jsPlumb connections properly
|
|
1227
|
+
this.fireCustomEvent(CustomEventType.NodeDeleted, {
|
|
1228
|
+
uuid: sourceNodeUuid
|
|
1229
|
+
});
|
|
1230
|
+
} else {
|
|
1231
|
+
// Update source node
|
|
1232
|
+
const updatedSourceNode = {
|
|
1233
|
+
...sourceNode,
|
|
1234
|
+
actions: updatedSourceActions
|
|
1235
|
+
};
|
|
1236
|
+
getStore()?.getState().updateNode(sourceNodeUuid, updatedSourceNode);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1182
1239
|
// Request update
|
|
1183
1240
|
this.requestUpdate();
|
|
1184
1241
|
}
|
|
@@ -1193,21 +1250,31 @@ export class CanvasNode extends RapidElement {
|
|
|
1193
1250
|
? ACTION_GROUP_METADATA[config.group]?.color
|
|
1194
1251
|
: '#aaaaaa';
|
|
1195
1252
|
return html`<div class="cn-title" style="background:${color}">
|
|
1196
|
-
${
|
|
1197
|
-
? html`<temba-icon
|
|
1253
|
+
${this.ui?.type === 'execute_actions'
|
|
1254
|
+
? html`<temba-icon
|
|
1255
|
+
class="drag-handle ${this.isTranslating
|
|
1256
|
+
? 'translating-hidden'
|
|
1257
|
+
: ''}"
|
|
1258
|
+
name="sort"
|
|
1259
|
+
></temba-icon>`
|
|
1260
|
+
: this.node?.actions?.length > 1
|
|
1261
|
+
? html`<temba-icon
|
|
1262
|
+
class="drag-handle ${this.isTranslating
|
|
1263
|
+
? 'translating-hidden'
|
|
1264
|
+
: ''}"
|
|
1265
|
+
name="sort"
|
|
1266
|
+
></temba-icon>`
|
|
1198
1267
|
: html`<div class="title-spacer"></div>`}
|
|
1199
1268
|
|
|
1200
1269
|
<div class="name">${isRemoving ? 'Remove?' : config.name}</div>
|
|
1201
|
-
|
|
1202
|
-
?
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
</div>`
|
|
1210
|
-
: html`<div class="title-spacer"></div>`}
|
|
1270
|
+
<div
|
|
1271
|
+
class="remove-button ${this.isTranslating ? 'translating-hidden' : ''}"
|
|
1272
|
+
@click=${(e: MouseEvent) =>
|
|
1273
|
+
this.handleActionRemoveClick(e, action, index)}
|
|
1274
|
+
title="Remove action"
|
|
1275
|
+
>
|
|
1276
|
+
✕
|
|
1277
|
+
</div>
|
|
1211
1278
|
</div>`;
|
|
1212
1279
|
}
|
|
1213
1280
|
|
|
@@ -1234,15 +1301,13 @@ export class CanvasNode extends RapidElement {
|
|
|
1234
1301
|
? config.renderTitle(node, ui)
|
|
1235
1302
|
: html`${config.name}`}
|
|
1236
1303
|
</div>
|
|
1237
|
-
|
|
1238
|
-
?
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
</div>`
|
|
1245
|
-
: html`<div class="title-spacer"></div>`}
|
|
1304
|
+
<div
|
|
1305
|
+
class="remove-button ${this.isTranslating ? 'translating-hidden' : ''}"
|
|
1306
|
+
@click=${(e: MouseEvent) => this.handleNodeRemoveClick(e)}
|
|
1307
|
+
title="Remove node"
|
|
1308
|
+
>
|
|
1309
|
+
✕
|
|
1310
|
+
</div>
|
|
1246
1311
|
</div>`;
|
|
1247
1312
|
}
|
|
1248
1313
|
|
|
@@ -1528,19 +1593,27 @@ export class CanvasNode extends RapidElement {
|
|
|
1528
1593
|
: this.node.actions.length > 0
|
|
1529
1594
|
? this.ui.type === 'execute_actions'
|
|
1530
1595
|
? html`<temba-sortable-list
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1596
|
+
dragHandle="drag-handle"
|
|
1597
|
+
externalDrag
|
|
1598
|
+
@temba-order-changed="${this.handleActionOrderChanged}"
|
|
1599
|
+
@temba-drag-start="${this.handleActionDragStart}"
|
|
1600
|
+
@temba-drag-external="${this.handleActionDragExternal}"
|
|
1601
|
+
@temba-drag-internal="${this.handleActionDragInternal}"
|
|
1602
|
+
@temba-drag-stop="${this.handleActionDragStop}"
|
|
1603
|
+
>
|
|
1604
|
+
${this.renderActionsWithPlaceholder()}
|
|
1605
|
+
</temba-sortable-list>
|
|
1606
|
+
${this.showLastActionPlaceholder
|
|
1607
|
+
? html`<div
|
|
1608
|
+
class="empty-node-placeholder"
|
|
1609
|
+
style="height: ${this.lastActionPlaceholderHeight}px;"
|
|
1610
|
+
></div>`
|
|
1611
|
+
: ''}`
|
|
1541
1612
|
: html`${this.node.actions.map((action, index) =>
|
|
1542
1613
|
this.renderAction(this.node, action, index)
|
|
1543
1614
|
)}`
|
|
1615
|
+
: this.ui.type === 'execute_actions'
|
|
1616
|
+
? html`<div class="empty-node-placeholder"></div>`
|
|
1544
1617
|
: ''}
|
|
1545
1618
|
${this.node.router
|
|
1546
1619
|
? html`<div class="router-section">
|
package/src/flow/Editor.ts
CHANGED
|
@@ -292,8 +292,6 @@ export class Editor extends RapidElement {
|
|
|
292
292
|
);
|
|
293
293
|
background-size: 20px 20px;
|
|
294
294
|
background-position: 10px 10px;
|
|
295
|
-
box-shadow: inset -5px 0 10px rgba(0, 0, 0, 0.05);
|
|
296
|
-
border-top: 1px solid #e0e0e0;
|
|
297
295
|
width: 100%;
|
|
298
296
|
display: flex;
|
|
299
297
|
}
|
|
@@ -741,11 +739,19 @@ export class Editor extends RapidElement {
|
|
|
741
739
|
private saveChanges(): void {
|
|
742
740
|
// post the flow definition to the server
|
|
743
741
|
getStore()
|
|
744
|
-
.postJSON(`/flow/revisions/${this.flow}
|
|
742
|
+
.postJSON(`/flow/revisions/${this.flow}/`, this.definition)
|
|
745
743
|
.then((response) => {
|
|
746
|
-
// Update flow info with the response data
|
|
747
|
-
if (response.json
|
|
748
|
-
getStore().getState()
|
|
744
|
+
// Update flow info and revision with the response data
|
|
745
|
+
if (response.json) {
|
|
746
|
+
const state = getStore().getState();
|
|
747
|
+
|
|
748
|
+
if (response.json.info) {
|
|
749
|
+
state.setFlowInfo(response.json.info);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (response.json.revision?.revision !== undefined) {
|
|
753
|
+
state.setRevision(response.json.revision.revision);
|
|
754
|
+
}
|
|
749
755
|
}
|
|
750
756
|
})
|
|
751
757
|
.catch((error) => {
|
|
@@ -812,6 +818,12 @@ export class Editor extends RapidElement {
|
|
|
812
818
|
this.handleNodeEditRequested.bind(this)
|
|
813
819
|
);
|
|
814
820
|
|
|
821
|
+
// Listen for node deletion events
|
|
822
|
+
this.addEventListener(
|
|
823
|
+
CustomEventType.NodeDeleted,
|
|
824
|
+
this.handleNodeDeleted.bind(this)
|
|
825
|
+
);
|
|
826
|
+
|
|
815
827
|
// Listen for canvas menu selections
|
|
816
828
|
this.addEventListener(CustomEventType.Selection, (event: CustomEvent) => {
|
|
817
829
|
const target = event.target as HTMLElement;
|
|
@@ -1599,6 +1611,13 @@ export class Editor extends RapidElement {
|
|
|
1599
1611
|
this.editingNodeUI = event.detail.nodeUI;
|
|
1600
1612
|
}
|
|
1601
1613
|
|
|
1614
|
+
private handleNodeDeleted(event: CustomEvent): void {
|
|
1615
|
+
const nodeUuid = event.detail.uuid;
|
|
1616
|
+
if (nodeUuid) {
|
|
1617
|
+
this.deleteNodes([nodeUuid]);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1602
1621
|
private handleActionSaved(updatedAction: Action): void {
|
|
1603
1622
|
if (this.editingNode && this.editingAction) {
|
|
1604
1623
|
let updatedActions: Action[];
|
|
@@ -1801,7 +1820,8 @@ export class Editor extends RapidElement {
|
|
|
1801
1820
|
actionIndex,
|
|
1802
1821
|
mouseX,
|
|
1803
1822
|
mouseY,
|
|
1804
|
-
actionHeight = 60
|
|
1823
|
+
actionHeight = 60,
|
|
1824
|
+
isLastAction = false
|
|
1805
1825
|
} = event.detail;
|
|
1806
1826
|
|
|
1807
1827
|
// Check if mouse is over another execute_actions node
|
|
@@ -1896,29 +1916,47 @@ export class Editor extends RapidElement {
|
|
|
1896
1916
|
|
|
1897
1917
|
this.actionDragTargetNodeUuid = null;
|
|
1898
1918
|
|
|
1899
|
-
// Tell source node to hide ghost (we're not over a valid target)
|
|
1900
1919
|
const sourceElement = this.querySelector(
|
|
1901
1920
|
`temba-flow-node[data-node-uuid="${nodeUuid}"]`
|
|
1902
1921
|
);
|
|
1903
|
-
if (sourceElement) {
|
|
1904
|
-
sourceElement.dispatchEvent(
|
|
1905
|
-
new CustomEvent('action-hide-ghost', {
|
|
1906
|
-
detail: {},
|
|
1907
|
-
bubbles: false
|
|
1908
|
-
})
|
|
1909
|
-
);
|
|
1910
|
-
}
|
|
1911
1922
|
|
|
1912
|
-
//
|
|
1913
|
-
|
|
1923
|
+
// Show canvas drop preview only if this is NOT the last action
|
|
1924
|
+
// Last actions can only be dropped on other nodes, not on canvas
|
|
1925
|
+
if (!isLastAction) {
|
|
1926
|
+
// Hide ghost when showing canvas preview (for canvas drops)
|
|
1927
|
+
if (sourceElement) {
|
|
1928
|
+
sourceElement.dispatchEvent(
|
|
1929
|
+
new CustomEvent('action-hide-ghost', {
|
|
1930
|
+
detail: {},
|
|
1931
|
+
bubbles: false
|
|
1932
|
+
})
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1914
1935
|
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1936
|
+
// Don't snap to grid for preview - let it follow cursor smoothly
|
|
1937
|
+
const position = this.calculateCanvasDropPosition(mouseX, mouseY, false);
|
|
1938
|
+
|
|
1939
|
+
this.canvasDropPreview = {
|
|
1940
|
+
action,
|
|
1941
|
+
nodeUuid,
|
|
1942
|
+
actionIndex,
|
|
1943
|
+
position,
|
|
1944
|
+
actionHeight
|
|
1945
|
+
};
|
|
1946
|
+
} else {
|
|
1947
|
+
// For last action, keep ghost visible (can't drop on canvas)
|
|
1948
|
+
if (sourceElement) {
|
|
1949
|
+
sourceElement.dispatchEvent(
|
|
1950
|
+
new CustomEvent('action-show-ghost', {
|
|
1951
|
+
detail: {},
|
|
1952
|
+
bubbles: false
|
|
1953
|
+
})
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
// Clear any existing preview for last action
|
|
1958
|
+
this.canvasDropPreview = null;
|
|
1959
|
+
}
|
|
1922
1960
|
|
|
1923
1961
|
// Force re-render to update preview position
|
|
1924
1962
|
this.requestUpdate();
|
|
@@ -1946,7 +1984,14 @@ export class Editor extends RapidElement {
|
|
|
1946
1984
|
}
|
|
1947
1985
|
|
|
1948
1986
|
private handleActionDropExternal(event: CustomEvent): void {
|
|
1949
|
-
const {
|
|
1987
|
+
const {
|
|
1988
|
+
action,
|
|
1989
|
+
nodeUuid,
|
|
1990
|
+
actionIndex,
|
|
1991
|
+
mouseX,
|
|
1992
|
+
mouseY,
|
|
1993
|
+
isLastAction = false
|
|
1994
|
+
} = event.detail;
|
|
1950
1995
|
|
|
1951
1996
|
// Check if we're dropping on an existing execute_actions node
|
|
1952
1997
|
const targetNodeUuid = this.actionDragTargetNodeUuid;
|
|
@@ -1977,6 +2022,14 @@ export class Editor extends RapidElement {
|
|
|
1977
2022
|
return;
|
|
1978
2023
|
}
|
|
1979
2024
|
|
|
2025
|
+
// If this is the last action and we're not dropping on another node, do nothing
|
|
2026
|
+
// Last actions can only be moved to other nodes, not dropped on canvas
|
|
2027
|
+
if (isLastAction) {
|
|
2028
|
+
this.canvasDropPreview = null;
|
|
2029
|
+
this.actionDragTargetNodeUuid = null;
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
|
|
1980
2033
|
// Not dropping on another node, create a new one on canvas
|
|
1981
2034
|
// Snap to grid for the final drop position
|
|
1982
2035
|
const position = this.calculateCanvasDropPosition(mouseX, mouseY, true);
|
|
@@ -1991,7 +2044,8 @@ export class Editor extends RapidElement {
|
|
|
1991
2044
|
|
|
1992
2045
|
// if no actions remain, delete the node
|
|
1993
2046
|
if (updatedActions.length === 0) {
|
|
1994
|
-
|
|
2047
|
+
// Use deleteNodes to properly clean up Plumber connections before removing
|
|
2048
|
+
this.deleteNodes([nodeUuid]);
|
|
1995
2049
|
} else {
|
|
1996
2050
|
// update the node
|
|
1997
2051
|
const updatedNode = { ...originalNode, actions: updatedActions };
|
|
@@ -2573,7 +2627,7 @@ export class Editor extends RapidElement {
|
|
|
2573
2627
|
header="Translations"
|
|
2574
2628
|
.width=${360}
|
|
2575
2629
|
.maxHeight=${600}
|
|
2576
|
-
.top=${
|
|
2630
|
+
.top=${170}
|
|
2577
2631
|
color="#6b7280"
|
|
2578
2632
|
.hidden=${this.localizationWindowHidden}
|
|
2579
2633
|
@temba-dialog-hidden=${this.handleLocalizationWindowClosed}
|
|
@@ -2738,6 +2792,7 @@ export class Editor extends RapidElement {
|
|
|
2738
2792
|
icon="language"
|
|
2739
2793
|
label="Translate Flow"
|
|
2740
2794
|
color="#6b7280"
|
|
2795
|
+
top="180"
|
|
2741
2796
|
.hidden=${!this.localizationWindowHidden}
|
|
2742
2797
|
@temba-button-clicked=${this.handleLocalizationTabClick}
|
|
2743
2798
|
></temba-floating-tab>
|
|
@@ -2810,8 +2865,7 @@ export class Editor extends RapidElement {
|
|
|
2810
2865
|
? 'selected'
|
|
2811
2866
|
: ''}"
|
|
2812
2867
|
@mousedown=${this.handleMouseDown.bind(this)}
|
|
2813
|
-
style="left:${position.left}px; top:${position.top}px;
|
|
2814
|
-
position.top}"
|
|
2868
|
+
style="left:${position.left}px; top:${position.top}px;"
|
|
2815
2869
|
uuid=${uuid}
|
|
2816
2870
|
.data=${sticky}
|
|
2817
2871
|
.dragging=${dragging}
|
|
@@ -18,7 +18,7 @@ export class FloatingWindow extends RapidElement {
|
|
|
18
18
|
transition: transform var(--transition-duration, 300ms) ease-in-out,
|
|
19
19
|
opacity var(--transition-duration, 300ms) ease-in-out;
|
|
20
20
|
position: fixed;
|
|
21
|
-
z-index:
|
|
21
|
+
z-index: 5000;
|
|
22
22
|
top: 100px;
|
|
23
23
|
background: white;
|
|
24
24
|
border-radius: 8px;
|
|
@@ -193,8 +193,6 @@ export class FloatingWindow extends RapidElement {
|
|
|
193
193
|
): void {
|
|
194
194
|
super.updated(changes);
|
|
195
195
|
if (changes.has('hidden')) {
|
|
196
|
-
this.classList.toggle('hidden', this.hidden);
|
|
197
|
-
|
|
198
196
|
// when hiding, reset positioning behavior to original
|
|
199
197
|
if (this.hidden && !changes.get('hidden')) {
|
|
200
198
|
if (this.defaultLeft === -1) {
|
package/src/list/ContentMenu.ts
CHANGED
package/src/list/SortableList.ts
CHANGED
|
@@ -577,8 +577,9 @@ export class SortableList extends RapidElement {
|
|
|
577
577
|
const fromIdx = originalDragIdx;
|
|
578
578
|
const toIdx = this.pendingDropIndex;
|
|
579
579
|
|
|
580
|
-
// only fire if the position actually changed
|
|
581
|
-
|
|
580
|
+
// only fire if the position actually changed AND this is not an external drag
|
|
581
|
+
// External drags are handled by external drop handlers
|
|
582
|
+
if (fromIdx !== toIdx && !this.isExternalDrag) {
|
|
582
583
|
this.fireCustomEvent(CustomEventType.OrderChanged, {
|
|
583
584
|
swap: [fromIdx, toIdx]
|
|
584
585
|
});
|