@nyaruka/temba-components 0.131.3 → 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 +22 -0
- package/demo/components/flow/example.html +1 -0
- package/demo/static/css/tailwind.css +30019 -0
- package/dist/temba-components.js +449 -417
- 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 +89 -40
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeTypeSelector.js +8 -2
- package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
- package/out-tsc/src/flow/config.js +17 -4
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +2 -2
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_run_result.js +6 -0
- package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
- package/out-tsc/src/flow/types.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 +105 -69
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/store/AppState.js +39 -1
- 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/nodes/split_by_run_result.test.js +83 -0
- package/out-tsc/test/nodes/split_by_run_result.test.js.map +1 -1
- package/out-tsc/test/temba-backwards-compatibility.test.js +30 -0
- package/out-tsc/test/temba-backwards-compatibility.test.js.map +1 -0
- package/out-tsc/test/temba-contact-chat.test.js +28 -13
- 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-localization.test.js +24 -5
- package/out-tsc/test/temba-localization.test.js.map +1 -1
- package/out-tsc/test/temba-node-type-selector.test.js +70 -3
- package/out-tsc/test/temba-node-type-selector.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 +3 -3
- 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 +107 -40
- package/src/flow/NodeTypeSelector.ts +7 -1
- package/src/flow/config.ts +22 -4
- package/src/flow/nodes/split_by_llm_categorize.ts +2 -8
- package/src/flow/nodes/split_by_run_result.ts +7 -0
- package/src/flow/types.ts +2 -0
- 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 +112 -78
- package/src/store/AppState.ts +53 -1
- package/src/utils.ts +3 -3
- package/test/ActionHelper.ts +13 -5
- package/test/actions/send_broadcast.test.ts +2 -1
- package/test/nodes/split_by_run_result.test.ts +99 -0
- package/test/temba-backwards-compatibility.test.ts +37 -0
- package/test/temba-contact-chat.test.ts +28 -13
- package/test/temba-floating-window.test.ts +0 -2
- package/test/temba-flow-editor-node.test.ts +129 -0
- package/test/temba-localization.test.ts +29 -5
- package/test/temba-node-type-selector.test.ts +89 -3
- package/test/temba-utils-uuid.test.ts +61 -1
- package/test/utils.test.ts +8 -3
- 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
|
@@ -12,7 +12,7 @@ import { getStore } from '../store/Store';
|
|
|
12
12
|
import { AppState, fromStore, zustand } from '../store/AppState';
|
|
13
13
|
import { RapidElement } from '../RapidElement';
|
|
14
14
|
import { repeat } from 'lit-html/directives/repeat.js';
|
|
15
|
-
import { CustomEventType } from '../interfaces';
|
|
15
|
+
import { CustomEventType, Workspace } from '../interfaces';
|
|
16
16
|
import { generateUUID, postJSON } from '../utils';
|
|
17
17
|
import { ACTION_CONFIG, NODE_CONFIG } from './config';
|
|
18
18
|
import { ACTION_GROUP_METADATA } from './types';
|
|
@@ -134,6 +134,9 @@ export class Editor extends RapidElement {
|
|
|
134
134
|
@fromStore(zustand, (state: AppState) => state.isTranslating)
|
|
135
135
|
private isTranslating!: boolean;
|
|
136
136
|
|
|
137
|
+
@fromStore(zustand, (state: AppState) => state.workspace)
|
|
138
|
+
private workspace!: Workspace;
|
|
139
|
+
|
|
137
140
|
// Drag state
|
|
138
141
|
@state()
|
|
139
142
|
private isDragging = false;
|
|
@@ -233,15 +236,23 @@ export class Editor extends RapidElement {
|
|
|
233
236
|
|
|
234
237
|
private canvasMouseDown = false;
|
|
235
238
|
|
|
236
|
-
// Default languages if not specified in flow definition
|
|
237
|
-
private readonly DEFAULT_LANGUAGES = [
|
|
238
|
-
{ code: 'eng', name: 'English' },
|
|
239
|
-
{ code: 'fra', name: 'French' },
|
|
240
|
-
{ code: 'esp', name: 'Spanish' }
|
|
241
|
-
];
|
|
242
|
-
|
|
243
239
|
private getAvailableLanguages(): Array<{ code: string; name: string }> {
|
|
244
|
-
// Use languages from
|
|
240
|
+
// Use languages from workspace if available
|
|
241
|
+
if (this.workspace?.languages && this.workspace.languages.length > 0) {
|
|
242
|
+
const languageNames = new Intl.DisplayNames(['en'], { type: 'language' });
|
|
243
|
+
return this.workspace.languages
|
|
244
|
+
.map((code) => {
|
|
245
|
+
try {
|
|
246
|
+
const name = languageNames.of(code);
|
|
247
|
+
return name ? { code, name } : { code, name: code };
|
|
248
|
+
} catch {
|
|
249
|
+
return { code, name: code };
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
.filter((lang) => lang.code && lang.name);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Fall back to flow definition languages if available
|
|
245
256
|
if (
|
|
246
257
|
this.definition?._ui?.languages &&
|
|
247
258
|
this.definition._ui.languages.length > 0
|
|
@@ -251,7 +262,9 @@ export class Editor extends RapidElement {
|
|
|
251
262
|
name: typeof lang === 'string' ? lang : lang.name
|
|
252
263
|
}));
|
|
253
264
|
}
|
|
254
|
-
|
|
265
|
+
|
|
266
|
+
// No languages available
|
|
267
|
+
return [];
|
|
255
268
|
}
|
|
256
269
|
|
|
257
270
|
// Bound event handlers to maintain proper 'this' context
|
|
@@ -279,8 +292,6 @@ export class Editor extends RapidElement {
|
|
|
279
292
|
);
|
|
280
293
|
background-size: 20px 20px;
|
|
281
294
|
background-position: 10px 10px;
|
|
282
|
-
box-shadow: inset -5px 0 10px rgba(0, 0, 0, 0.05);
|
|
283
|
-
border-top: 1px solid #e0e0e0;
|
|
284
295
|
width: 100%;
|
|
285
296
|
display: flex;
|
|
286
297
|
}
|
|
@@ -728,11 +739,19 @@ export class Editor extends RapidElement {
|
|
|
728
739
|
private saveChanges(): void {
|
|
729
740
|
// post the flow definition to the server
|
|
730
741
|
getStore()
|
|
731
|
-
.postJSON(`/flow/revisions/${this.flow}
|
|
742
|
+
.postJSON(`/flow/revisions/${this.flow}/`, this.definition)
|
|
732
743
|
.then((response) => {
|
|
733
|
-
// Update flow info with the response data
|
|
734
|
-
if (response.json
|
|
735
|
-
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
|
+
}
|
|
736
755
|
}
|
|
737
756
|
})
|
|
738
757
|
.catch((error) => {
|
|
@@ -799,6 +818,12 @@ export class Editor extends RapidElement {
|
|
|
799
818
|
this.handleNodeEditRequested.bind(this)
|
|
800
819
|
);
|
|
801
820
|
|
|
821
|
+
// Listen for node deletion events
|
|
822
|
+
this.addEventListener(
|
|
823
|
+
CustomEventType.NodeDeleted,
|
|
824
|
+
this.handleNodeDeleted.bind(this)
|
|
825
|
+
);
|
|
826
|
+
|
|
802
827
|
// Listen for canvas menu selections
|
|
803
828
|
this.addEventListener(CustomEventType.Selection, (event: CustomEvent) => {
|
|
804
829
|
const target = event.target as HTMLElement;
|
|
@@ -1586,6 +1611,13 @@ export class Editor extends RapidElement {
|
|
|
1586
1611
|
this.editingNodeUI = event.detail.nodeUI;
|
|
1587
1612
|
}
|
|
1588
1613
|
|
|
1614
|
+
private handleNodeDeleted(event: CustomEvent): void {
|
|
1615
|
+
const nodeUuid = event.detail.uuid;
|
|
1616
|
+
if (nodeUuid) {
|
|
1617
|
+
this.deleteNodes([nodeUuid]);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1589
1621
|
private handleActionSaved(updatedAction: Action): void {
|
|
1590
1622
|
if (this.editingNode && this.editingAction) {
|
|
1591
1623
|
let updatedActions: Action[];
|
|
@@ -1788,7 +1820,8 @@ export class Editor extends RapidElement {
|
|
|
1788
1820
|
actionIndex,
|
|
1789
1821
|
mouseX,
|
|
1790
1822
|
mouseY,
|
|
1791
|
-
actionHeight = 60
|
|
1823
|
+
actionHeight = 60,
|
|
1824
|
+
isLastAction = false
|
|
1792
1825
|
} = event.detail;
|
|
1793
1826
|
|
|
1794
1827
|
// Check if mouse is over another execute_actions node
|
|
@@ -1883,29 +1916,47 @@ export class Editor extends RapidElement {
|
|
|
1883
1916
|
|
|
1884
1917
|
this.actionDragTargetNodeUuid = null;
|
|
1885
1918
|
|
|
1886
|
-
// Tell source node to hide ghost (we're not over a valid target)
|
|
1887
1919
|
const sourceElement = this.querySelector(
|
|
1888
1920
|
`temba-flow-node[data-node-uuid="${nodeUuid}"]`
|
|
1889
1921
|
);
|
|
1890
|
-
if (sourceElement) {
|
|
1891
|
-
sourceElement.dispatchEvent(
|
|
1892
|
-
new CustomEvent('action-hide-ghost', {
|
|
1893
|
-
detail: {},
|
|
1894
|
-
bubbles: false
|
|
1895
|
-
})
|
|
1896
|
-
);
|
|
1897
|
-
}
|
|
1898
1922
|
|
|
1899
|
-
//
|
|
1900
|
-
|
|
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
|
+
}
|
|
1901
1935
|
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
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
|
+
}
|
|
1909
1960
|
|
|
1910
1961
|
// Force re-render to update preview position
|
|
1911
1962
|
this.requestUpdate();
|
|
@@ -1933,7 +1984,14 @@ export class Editor extends RapidElement {
|
|
|
1933
1984
|
}
|
|
1934
1985
|
|
|
1935
1986
|
private handleActionDropExternal(event: CustomEvent): void {
|
|
1936
|
-
const {
|
|
1987
|
+
const {
|
|
1988
|
+
action,
|
|
1989
|
+
nodeUuid,
|
|
1990
|
+
actionIndex,
|
|
1991
|
+
mouseX,
|
|
1992
|
+
mouseY,
|
|
1993
|
+
isLastAction = false
|
|
1994
|
+
} = event.detail;
|
|
1937
1995
|
|
|
1938
1996
|
// Check if we're dropping on an existing execute_actions node
|
|
1939
1997
|
const targetNodeUuid = this.actionDragTargetNodeUuid;
|
|
@@ -1964,6 +2022,14 @@ export class Editor extends RapidElement {
|
|
|
1964
2022
|
return;
|
|
1965
2023
|
}
|
|
1966
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
|
+
|
|
1967
2033
|
// Not dropping on another node, create a new one on canvas
|
|
1968
2034
|
// Snap to grid for the final drop position
|
|
1969
2035
|
const position = this.calculateCanvasDropPosition(mouseX, mouseY, true);
|
|
@@ -1978,7 +2044,8 @@ export class Editor extends RapidElement {
|
|
|
1978
2044
|
|
|
1979
2045
|
// if no actions remain, delete the node
|
|
1980
2046
|
if (updatedActions.length === 0) {
|
|
1981
|
-
|
|
2047
|
+
// Use deleteNodes to properly clean up Plumber connections before removing
|
|
2048
|
+
this.deleteNodes([nodeUuid]);
|
|
1982
2049
|
} else {
|
|
1983
2050
|
// update the node
|
|
1984
2051
|
const updatedNode = { ...originalNode, actions: updatedActions };
|
|
@@ -2560,7 +2627,7 @@ export class Editor extends RapidElement {
|
|
|
2560
2627
|
header="Translations"
|
|
2561
2628
|
.width=${360}
|
|
2562
2629
|
.maxHeight=${600}
|
|
2563
|
-
.top=${
|
|
2630
|
+
.top=${170}
|
|
2564
2631
|
color="#6b7280"
|
|
2565
2632
|
.hidden=${this.localizationWindowHidden}
|
|
2566
2633
|
@temba-dialog-hidden=${this.handleLocalizationWindowClosed}
|
|
@@ -2725,6 +2792,7 @@ export class Editor extends RapidElement {
|
|
|
2725
2792
|
icon="language"
|
|
2726
2793
|
label="Translate Flow"
|
|
2727
2794
|
color="#6b7280"
|
|
2795
|
+
top="180"
|
|
2728
2796
|
.hidden=${!this.localizationWindowHidden}
|
|
2729
2797
|
@temba-button-clicked=${this.handleLocalizationTabClick}
|
|
2730
2798
|
></temba-floating-tab>
|
|
@@ -2797,8 +2865,7 @@ export class Editor extends RapidElement {
|
|
|
2797
2865
|
? 'selected'
|
|
2798
2866
|
: ''}"
|
|
2799
2867
|
@mousedown=${this.handleMouseDown.bind(this)}
|
|
2800
|
-
style="left:${position.left}px; top:${position.top}px;
|
|
2801
|
-
position.top}"
|
|
2868
|
+
style="left:${position.left}px; top:${position.top}px;"
|
|
2802
2869
|
uuid=${uuid}
|
|
2803
2870
|
.data=${sticky}
|
|
2804
2871
|
.dragging=${dragging}
|
|
@@ -318,8 +318,11 @@ export class NodeTypeSelector extends RapidElement {
|
|
|
318
318
|
|
|
319
319
|
// Collect regular actions (from ACTION_CONFIG, unless hideFromActions is true)
|
|
320
320
|
Object.entries(ACTION_CONFIG)
|
|
321
|
-
.filter(([
|
|
321
|
+
.filter(([type, config]) => {
|
|
322
|
+
// exclude aliases - if config has aliases, check if this type is an alias
|
|
323
|
+
const isAlias = config.aliases && config.aliases.includes(type);
|
|
322
324
|
return (
|
|
325
|
+
!isAlias &&
|
|
323
326
|
config.name &&
|
|
324
327
|
!config.hideFromActions &&
|
|
325
328
|
config.group &&
|
|
@@ -341,6 +344,7 @@ export class NodeTypeSelector extends RapidElement {
|
|
|
341
344
|
.filter(([type, config]) => {
|
|
342
345
|
return (
|
|
343
346
|
type !== 'execute_actions' &&
|
|
347
|
+
type === config.type && // exclude aliases (type won't match config.type for aliases)
|
|
344
348
|
config.name &&
|
|
345
349
|
config.showAsAction &&
|
|
346
350
|
config.group &&
|
|
@@ -440,8 +444,10 @@ export class NodeTypeSelector extends RapidElement {
|
|
|
440
444
|
.filter(([type, config]) => {
|
|
441
445
|
// exclude execute_actions (it's the default action-only node)
|
|
442
446
|
// exclude nodes that have showAsAction=true (they appear in action mode)
|
|
447
|
+
// exclude aliases (type won't match config.type for aliases)
|
|
443
448
|
return (
|
|
444
449
|
type !== 'execute_actions' &&
|
|
450
|
+
type === config.type &&
|
|
445
451
|
config.name &&
|
|
446
452
|
!config.showAsAction &&
|
|
447
453
|
this.isConfigAvailable(config)
|