@nyaruka/temba-components 0.138.6 → 0.139.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 +9 -0
- package/dist/locales/es.js +5 -5
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +5 -5
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/locale-codes.js +2 -11
- package/dist/locales/locale-codes.js.map +1 -1
- package/dist/locales/pt.js +5 -5
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +815 -851
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +23 -30
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/flow/CanvasMenu.js +5 -3
- package/out-tsc/src/flow/CanvasMenu.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +6 -7
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +152 -235
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +757 -403
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/flow/utils.js +138 -66
- package/out-tsc/src/flow/utils.js.map +1 -1
- package/out-tsc/src/interfaces.js +1 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/list/TicketList.js +4 -1
- package/out-tsc/src/list/TicketList.js.map +1 -1
- package/out-tsc/src/locales/es.js +5 -5
- package/out-tsc/src/locales/es.js.map +1 -1
- package/out-tsc/src/locales/fr.js +5 -5
- package/out-tsc/src/locales/fr.js.map +1 -1
- package/out-tsc/src/locales/locale-codes.js +2 -11
- package/out-tsc/src/locales/locale-codes.js.map +1 -1
- package/out-tsc/src/locales/pt.js +5 -5
- package/out-tsc/src/locales/pt.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +1 -0
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/test/temba-floating-tab.test.js +4 -6
- package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
- package/out-tsc/test/temba-flow-collision.test.js +221 -223
- package/out-tsc/test/temba-flow-collision.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor.test.js +0 -2
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js +83 -84
- package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +102 -93
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
- package/package.json +1 -1
- package/src/display/FloatingTab.ts +22 -31
- package/src/flow/CanvasMenu.ts +8 -3
- package/src/flow/CanvasNode.ts +6 -7
- package/src/flow/Editor.ts +184 -279
- package/src/flow/Plumber.ts +1011 -457
- package/src/flow/utils.ts +162 -84
- package/src/interfaces.ts +2 -1
- package/src/list/TicketList.ts +4 -1
- package/src/locales/es.ts +13 -18
- package/src/locales/fr.ts +13 -18
- package/src/locales/locale-codes.ts +2 -11
- package/src/locales/pt.ts +13 -18
- package/src/simulator/Simulator.ts +1 -0
- package/test/temba-floating-tab.test.ts +4 -6
- package/test/temba-flow-collision.test.ts +225 -303
- package/test/temba-flow-editor.test.ts +0 -2
- package/test/temba-flow-plumber-connections.test.ts +97 -97
- package/test/temba-flow-plumber.test.ts +116 -103
|
@@ -10,13 +10,9 @@ import { CustomEventType } from '../interfaces';
|
|
|
10
10
|
import { generateUUID, postJSON, fetchResults, getClasses } from '../utils';
|
|
11
11
|
import { ACTION_CONFIG, NODE_CONFIG } from './config';
|
|
12
12
|
import { ACTION_GROUP_METADATA } from './types';
|
|
13
|
-
import { Plumber } from './Plumber';
|
|
13
|
+
import { Plumber, calculateFlowchartPath, ARROW_LENGTH, ARROW_HALF_WIDTH, CURSOR_GAP } from './Plumber';
|
|
14
14
|
import { CanvasNode } from './CanvasNode';
|
|
15
|
-
import { getNodeBounds, calculateReflowPositions,
|
|
16
|
-
export function snapToGrid(value) {
|
|
17
|
-
const snapped = Math.round(value / 20) * 20;
|
|
18
|
-
return Math.max(snapped, 0);
|
|
19
|
-
}
|
|
15
|
+
import { getNodeBounds, calculateReflowPositions, snapToGrid } from './utils';
|
|
20
16
|
export function findNodeForExit(definition, exitUuid) {
|
|
21
17
|
for (const node of definition.nodes) {
|
|
22
18
|
const exit = node.exits.find((e) => e.uuid === exitUuid);
|
|
@@ -34,7 +30,7 @@ const AUTO_TRANSLATE_MODELS_ENDPOINT = '/api/internal/llms.json';
|
|
|
34
30
|
const DROP_PREVIEW_OFFSET_X = 20;
|
|
35
31
|
const DROP_PREVIEW_OFFSET_Y = 20;
|
|
36
32
|
export class Editor extends RapidElement {
|
|
37
|
-
//
|
|
33
|
+
// connection SVGs are appended directly to the canvas, so we need light DOM
|
|
38
34
|
createRenderRoot() {
|
|
39
35
|
return this;
|
|
40
36
|
}
|
|
@@ -128,100 +124,56 @@ export class Editor extends RapidElement {
|
|
|
128
124
|
}
|
|
129
125
|
|
|
130
126
|
#grid.viewing-revision temba-flow-node,
|
|
131
|
-
#grid.viewing-revision svg.
|
|
132
|
-
#grid.viewing-revision .activity-overlay {
|
|
127
|
+
#grid.viewing-revision svg.plumb-connector {
|
|
133
128
|
opacity: 0.5;
|
|
134
129
|
}
|
|
135
130
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
height: initial;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.jtk-endpoint {
|
|
142
|
-
z-index: 600;
|
|
143
|
-
opacity: 0;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
.plumb-source {
|
|
147
|
-
z-index: 600;
|
|
148
|
-
cursor: pointer;
|
|
149
|
-
opacity: 0;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
.plumb-source.connected {
|
|
153
|
-
border-radius: 50%;
|
|
154
|
-
pointer-events: none;
|
|
131
|
+
svg.plumb-connector {
|
|
132
|
+
z-index: 10;
|
|
155
133
|
}
|
|
156
134
|
|
|
157
|
-
.plumb-
|
|
158
|
-
|
|
135
|
+
svg.plumb-connector path {
|
|
136
|
+
stroke: var(--color-connectors);
|
|
137
|
+
stroke-width: 3px;
|
|
159
138
|
}
|
|
160
139
|
|
|
161
|
-
.plumb-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
cursor: pointer;
|
|
165
|
-
fill: transparent;
|
|
140
|
+
svg.plumb-connector .plumb-arrow {
|
|
141
|
+
fill: var(--color-connectors);
|
|
142
|
+
stroke: none;
|
|
166
143
|
}
|
|
167
144
|
|
|
168
|
-
|
|
169
|
-
stroke: var(--color-
|
|
170
|
-
stroke-width: 3px;
|
|
145
|
+
svg.plumb-connector.hover path {
|
|
146
|
+
stroke: var(--color-success);
|
|
171
147
|
}
|
|
172
148
|
|
|
173
|
-
|
|
174
|
-
|
|
149
|
+
svg.plumb-connector.hover .plumb-arrow {
|
|
150
|
+
fill: var(--color-success);
|
|
175
151
|
}
|
|
176
152
|
|
|
177
|
-
|
|
178
|
-
fill: var(--color-connectors);
|
|
153
|
+
#canvas.read-only-connections svg.plumb-connector.hover path {
|
|
179
154
|
stroke: var(--color-connectors);
|
|
180
|
-
stroke-width: 0px !important;
|
|
181
|
-
margin-top: 6px;
|
|
182
|
-
z-index: 10;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
body svg.jtk-connector.jtk-hover path {
|
|
186
|
-
stroke: var(--color-success) !important;
|
|
187
|
-
stroke-width: 3px;
|
|
188
155
|
}
|
|
189
156
|
|
|
190
|
-
|
|
191
|
-
|
|
157
|
+
#canvas.read-only-connections svg.plumb-connector.hover .plumb-arrow {
|
|
158
|
+
fill: var(--color-connectors);
|
|
192
159
|
}
|
|
193
160
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
161
|
+
#canvas.read-only-connections svg.plumb-connector,
|
|
162
|
+
#canvas.read-only-connections svg.plumb-connector * {
|
|
163
|
+
pointer-events: none !important;
|
|
164
|
+
cursor: default !important;
|
|
198
165
|
}
|
|
199
166
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
.plumb-connector.jtk-hover
|
|
203
|
-
.plumb-arrow {
|
|
204
|
-
fill: var(--color-connectors) !important;
|
|
205
|
-
ponter-events: none;
|
|
167
|
+
svg.plumb-connector.removing path {
|
|
168
|
+
stroke: var(--color-error);
|
|
206
169
|
}
|
|
207
170
|
|
|
208
|
-
|
|
209
|
-
|
|
171
|
+
svg.plumb-connector.removing .plumb-arrow {
|
|
172
|
+
fill: var(--color-error);
|
|
210
173
|
}
|
|
211
174
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
background: #f3f3f3;
|
|
215
|
-
border: 1px solid #d9d9d9;
|
|
216
|
-
color: #333;
|
|
217
|
-
border-radius: 4px;
|
|
218
|
-
padding: 2px 4px;
|
|
219
|
-
font-size: 10px;
|
|
220
|
-
font-weight: 600;
|
|
221
|
-
line-height: 0.9;
|
|
222
|
-
cursor: pointer;
|
|
223
|
-
z-index: 500;
|
|
224
|
-
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
175
|
+
svg.plumb-connector.dragging {
|
|
176
|
+
z-index: 99999;
|
|
225
177
|
}
|
|
226
178
|
|
|
227
179
|
/* Active contact count on nodes */
|
|
@@ -243,6 +195,30 @@ export class Editor extends RapidElement {
|
|
|
243
195
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
244
196
|
}
|
|
245
197
|
|
|
198
|
+
/* Activity overlay badges on connection exit stubs */
|
|
199
|
+
.activity-overlay {
|
|
200
|
+
position: absolute;
|
|
201
|
+
background: #f3f3f3;
|
|
202
|
+
border: 1px solid #d9d9d9;
|
|
203
|
+
color: #333;
|
|
204
|
+
border-radius: 4px;
|
|
205
|
+
padding: 2px 4px;
|
|
206
|
+
font-size: 10px;
|
|
207
|
+
font-weight: 600;
|
|
208
|
+
line-height: 0.9;
|
|
209
|
+
cursor: pointer;
|
|
210
|
+
z-index: 500;
|
|
211
|
+
pointer-events: auto;
|
|
212
|
+
white-space: nowrap;
|
|
213
|
+
user-select: none;
|
|
214
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#grid.viewing-revision .activity-overlay {
|
|
218
|
+
opacity: 0.5;
|
|
219
|
+
pointer-events: none;
|
|
220
|
+
}
|
|
221
|
+
|
|
246
222
|
/* Recent contacts popup */
|
|
247
223
|
@keyframes popupBounceIn {
|
|
248
224
|
0% {
|
|
@@ -312,7 +288,6 @@ export class Editor extends RapidElement {
|
|
|
312
288
|
|
|
313
289
|
.recent-contacts-popup .contact-name:hover {
|
|
314
290
|
text-decoration: underline;
|
|
315
|
-
color: var(--color-link-primary, #1d4ed8);
|
|
316
291
|
}
|
|
317
292
|
|
|
318
293
|
.recent-contacts-popup .contact-operand {
|
|
@@ -328,17 +303,6 @@ export class Editor extends RapidElement {
|
|
|
328
303
|
color: #999;
|
|
329
304
|
}
|
|
330
305
|
|
|
331
|
-
/* Connection dragging feedback */
|
|
332
|
-
body svg.jtk-connector.jtk-dragging {
|
|
333
|
-
z-index: 99999 !important;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
.katavorio-drag-no-select svg.jtk-connector path,
|
|
337
|
-
.katavorio-drag-no-select svg.jtk-endpoint path {
|
|
338
|
-
pointer-events: none !important;
|
|
339
|
-
border: 1px solid purple;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
306
|
/* Connection target feedback */
|
|
343
307
|
temba-flow-node.connection-target-valid {
|
|
344
308
|
outline: 3px solid var(--color-success, #22c55e) !important;
|
|
@@ -368,10 +332,6 @@ export class Editor extends RapidElement {
|
|
|
368
332
|
border-radius: var(--curvature);
|
|
369
333
|
}
|
|
370
334
|
|
|
371
|
-
.jtk-floating-endpoint {
|
|
372
|
-
pointer-events: none;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
335
|
.localization-window-content {
|
|
376
336
|
display: flex;
|
|
377
337
|
flex-direction: column;
|
|
@@ -587,6 +547,9 @@ export class Editor extends RapidElement {
|
|
|
587
547
|
this.dragFromNodeId = null;
|
|
588
548
|
this.originalConnectionTargetId = null;
|
|
589
549
|
this.isValidTarget = true;
|
|
550
|
+
// Canvas-relative source exit position (set at drag start)
|
|
551
|
+
this.connectionSourceX = null;
|
|
552
|
+
this.connectionSourceY = null;
|
|
590
553
|
this.localizationWindowHidden = true;
|
|
591
554
|
this.translationFilters = {
|
|
592
555
|
categories: false
|
|
@@ -635,15 +598,13 @@ export class Editor extends RapidElement {
|
|
|
635
598
|
getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
|
|
636
599
|
}
|
|
637
600
|
this.plumber.on('connection:drag', (connection) => {
|
|
638
|
-
|
|
639
|
-
this.dragFromNodeId =
|
|
640
|
-
connection.data.nodeId ||
|
|
641
|
-
document.getElementById(connection.sourceId).closest('.node').id;
|
|
601
|
+
this.dragFromNodeId = connection.data.nodeId;
|
|
642
602
|
this.sourceId = connection.sourceId;
|
|
603
|
+
this.connectionSourceX = connection.sourceX;
|
|
604
|
+
this.connectionSourceY = connection.sourceY;
|
|
643
605
|
this.originalConnectionTargetId = connection.target.id;
|
|
644
606
|
});
|
|
645
607
|
this.plumber.on('connection:abort', (info) => {
|
|
646
|
-
// console.log('Connection aborted', info);
|
|
647
608
|
this.makeConnection(info);
|
|
648
609
|
});
|
|
649
610
|
this.plumber.on('connection:detach', (info) => {
|
|
@@ -672,6 +633,7 @@ export class Editor extends RapidElement {
|
|
|
672
633
|
left: snapToGrid(this.connectionPlaceholder.position.left),
|
|
673
634
|
top: snapToGrid(this.connectionPlaceholder.position.top)
|
|
674
635
|
};
|
|
636
|
+
const isDragUp = !!this.connectionPlaceholder.dragUp;
|
|
675
637
|
// Update the placeholder to the snapped position
|
|
676
638
|
this.connectionPlaceholder.position = snappedPosition;
|
|
677
639
|
// Store the pending connection info
|
|
@@ -680,12 +642,14 @@ export class Editor extends RapidElement {
|
|
|
680
642
|
exitId: this.sourceId,
|
|
681
643
|
position: snappedPosition
|
|
682
644
|
};
|
|
683
|
-
// Show the context menu
|
|
645
|
+
// Show the context menu near the placeholder
|
|
684
646
|
const canvas = this.querySelector('#canvas');
|
|
685
647
|
if (canvas) {
|
|
686
648
|
const canvasRect = canvas.getBoundingClientRect();
|
|
687
|
-
const menuX = canvasRect.left + snappedPosition.left - 40;
|
|
688
|
-
const menuY =
|
|
649
|
+
const menuX = canvasRect.left + snappedPosition.left - 40;
|
|
650
|
+
const menuY = isDragUp
|
|
651
|
+
? canvasRect.top + snappedPosition.top + 74 // just below placeholder bottom
|
|
652
|
+
: canvasRect.top + snappedPosition.top + 80; // just below placeholder
|
|
689
653
|
const canvasMenu = this.querySelector('temba-canvas-menu');
|
|
690
654
|
if (canvasMenu) {
|
|
691
655
|
canvasMenu.show(menuX, menuY, {
|
|
@@ -706,6 +670,8 @@ export class Editor extends RapidElement {
|
|
|
706
670
|
// Clear connection state (but keep sourceId/dragFromNodeId if we have a pending connection)
|
|
707
671
|
if (!this.pendingCanvasConnection) {
|
|
708
672
|
this.sourceId = null;
|
|
673
|
+
this.connectionSourceX = null;
|
|
674
|
+
this.connectionSourceY = null;
|
|
709
675
|
this.dragFromNodeId = null;
|
|
710
676
|
}
|
|
711
677
|
this.targetId = null;
|
|
@@ -862,7 +828,7 @@ export class Editor extends RapidElement {
|
|
|
862
828
|
clearTimeout(this.activityTimer);
|
|
863
829
|
}
|
|
864
830
|
this.activityTimer = window.setTimeout(() => {
|
|
865
|
-
|
|
831
|
+
this.fetchActivityData();
|
|
866
832
|
}, this.activityInterval);
|
|
867
833
|
});
|
|
868
834
|
}
|
|
@@ -1219,72 +1185,52 @@ export class Editor extends RapidElement {
|
|
|
1219
1185
|
renderConnectionPlaceholder() {
|
|
1220
1186
|
if (!this.connectionPlaceholder || !this.connectionPlaceholder.visible)
|
|
1221
1187
|
return '';
|
|
1222
|
-
const { position } = this.connectionPlaceholder;
|
|
1188
|
+
const { position, dragUp } = this.connectionPlaceholder;
|
|
1223
1189
|
// Render connection line when we have a pending connection (after drop)
|
|
1224
1190
|
let svgPath = null;
|
|
1225
|
-
if (this.sourceId &&
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
//
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
pathData += ` L ${sourceX} ${midY - cornerRadius}`;
|
|
1252
|
-
pathData += ` Q ${sourceX} ${midY}, ${sourceX + (targetX > sourceX ? cornerRadius : -cornerRadius)} ${midY}`;
|
|
1253
|
-
pathData += ` L ${targetX - (targetX > sourceX ? cornerRadius : -cornerRadius)} ${midY}`;
|
|
1254
|
-
pathData += ` Q ${targetX} ${midY}, ${targetX} ${midY + cornerRadius}`;
|
|
1255
|
-
pathData += ` L ${targetX} ${verticalEnd}`;
|
|
1256
|
-
}
|
|
1257
|
-
else {
|
|
1258
|
-
// Direct horizontal transition
|
|
1259
|
-
pathData += ` L ${targetX} ${verticalStart}`;
|
|
1260
|
-
pathData += ` L ${targetX} ${verticalEnd}`;
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
else {
|
|
1264
|
-
// Straight vertical line
|
|
1265
|
-
pathData += ` L ${targetX} ${verticalEnd}`;
|
|
1266
|
-
}
|
|
1267
|
-
pathData += ` L ${targetX} ${targetY}`;
|
|
1268
|
-
svgPath = html `
|
|
1269
|
-
<svg
|
|
1270
|
-
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;"
|
|
1271
|
-
>
|
|
1272
|
-
<path
|
|
1273
|
-
d="${pathData}"
|
|
1274
|
-
fill="none"
|
|
1275
|
-
stroke="var(--color-connectors, #ccc)"
|
|
1276
|
-
stroke-width="3"
|
|
1277
|
-
class="plumb-connector"
|
|
1278
|
-
/>
|
|
1279
|
-
<polygon
|
|
1280
|
-
points="${targetX},${targetY} ${targetX - 6.5},${targetY -
|
|
1281
|
-
13} ${targetX + 6.5},${targetY - 13}"
|
|
1282
|
-
fill="var(--color-connectors, #ccc)"
|
|
1283
|
-
class="plumb-arrow"
|
|
1284
|
-
/>
|
|
1285
|
-
</svg>
|
|
1286
|
-
`;
|
|
1191
|
+
if (this.sourceId &&
|
|
1192
|
+
this.dragFromNodeId &&
|
|
1193
|
+
this.pendingCanvasConnection &&
|
|
1194
|
+
this.connectionSourceX != null &&
|
|
1195
|
+
this.connectionSourceY != null) {
|
|
1196
|
+
const sourceX = this.connectionSourceX;
|
|
1197
|
+
const sourceY = this.connectionSourceY;
|
|
1198
|
+
const targetX = position.left + 100;
|
|
1199
|
+
// When dragging up, connect to the placeholder bottom; otherwise to the top
|
|
1200
|
+
const targetY = dragUp ? position.top + 64 : position.top;
|
|
1201
|
+
const routeFace = dragUp
|
|
1202
|
+
? targetX < sourceX
|
|
1203
|
+
? 'left'
|
|
1204
|
+
: 'right'
|
|
1205
|
+
: 'top';
|
|
1206
|
+
const pathData = calculateFlowchartPath(sourceX, sourceY, targetX, targetY, 20, dragUp ? 0 : 10, 5, routeFace);
|
|
1207
|
+
const aw = ARROW_HALF_WIDTH;
|
|
1208
|
+
const al = ARROW_LENGTH;
|
|
1209
|
+
let arrowPoints;
|
|
1210
|
+
if (dragUp) {
|
|
1211
|
+
// Arrow tip pointing up, base at placeholder bottom
|
|
1212
|
+
arrowPoints = `${targetX},${targetY - al} ${targetX - aw},${targetY} ${targetX + aw},${targetY}`;
|
|
1213
|
+
}
|
|
1214
|
+
else {
|
|
1215
|
+
// Arrow pointing down into top of placeholder
|
|
1216
|
+
arrowPoints = `${targetX},${targetY} ${targetX - aw},${targetY - al} ${targetX + aw},${targetY - al}`;
|
|
1287
1217
|
}
|
|
1218
|
+
svgPath = html `
|
|
1219
|
+
<svg
|
|
1220
|
+
style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; z-index: 9999;"
|
|
1221
|
+
>
|
|
1222
|
+
<path
|
|
1223
|
+
d="${pathData}"
|
|
1224
|
+
fill="none"
|
|
1225
|
+
stroke="var(--color-connectors, #ccc)"
|
|
1226
|
+
stroke-width="3"
|
|
1227
|
+
/>
|
|
1228
|
+
<polygon
|
|
1229
|
+
points="${arrowPoints}"
|
|
1230
|
+
fill="var(--color-connectors, #ccc)"
|
|
1231
|
+
/>
|
|
1232
|
+
</svg>
|
|
1233
|
+
`;
|
|
1288
1234
|
}
|
|
1289
1235
|
return html `${svgPath}
|
|
1290
1236
|
<div
|
|
@@ -1306,17 +1252,13 @@ export class Editor extends RapidElement {
|
|
|
1306
1252
|
}
|
|
1307
1253
|
/**
|
|
1308
1254
|
* Checks for node collisions and reflows nodes as needed.
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1311
|
-
* @param movedNodeUuids - UUIDs of nodes that were just moved/dropped
|
|
1312
|
-
* @param droppedNodeUuid - UUID of the specific node that was dropped (if applicable)
|
|
1313
|
-
* @param dropTargetBounds - Bounds of the node that was dropped onto (if applicable)
|
|
1255
|
+
* Sacred nodes (just moved/dropped) keep their positions while
|
|
1256
|
+
* other nodes are moved in the least-disruptive direction.
|
|
1314
1257
|
*/
|
|
1315
|
-
checkCollisionsAndReflow(
|
|
1258
|
+
checkCollisionsAndReflow(sacredNodeUuids) {
|
|
1316
1259
|
var _b;
|
|
1317
1260
|
if (!this.definition)
|
|
1318
1261
|
return;
|
|
1319
|
-
// Get all node bounds (only for actual nodes, not stickies)
|
|
1320
1262
|
const allBounds = [];
|
|
1321
1263
|
for (const node of this.definition.nodes) {
|
|
1322
1264
|
const nodeUI = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.nodes[node.uuid];
|
|
@@ -1327,35 +1269,13 @@ export class Editor extends RapidElement {
|
|
|
1327
1269
|
allBounds.push(bounds);
|
|
1328
1270
|
}
|
|
1329
1271
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
const droppedBounds = allBounds.find((b) => b.uuid === droppedNodeUuid);
|
|
1334
|
-
if (droppedBounds) {
|
|
1335
|
-
// Check if the bottom of the dropped node is below the midpoint of the target
|
|
1336
|
-
// If bottom is above midpoint, dropped node gets preference (targetHasPriority = false)
|
|
1337
|
-
// If bottom is below midpoint, target gets preference (targetHasPriority = true)
|
|
1338
|
-
const droppedBottom = droppedBounds.bottom;
|
|
1339
|
-
const targetMidpoint = dropTargetBounds.top + dropTargetBounds.height / 2;
|
|
1340
|
-
targetHasPriority = droppedBottom > targetMidpoint;
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
// Calculate reflow positions for each moved node
|
|
1344
|
-
const allReflowPositions = {};
|
|
1345
|
-
for (const movedUuid of movedNodeUuids) {
|
|
1346
|
-
const movedBounds = allBounds.find((b) => b.uuid === movedUuid);
|
|
1347
|
-
if (!movedBounds)
|
|
1348
|
-
continue;
|
|
1349
|
-
// Calculate reflow for this moved node
|
|
1350
|
-
const reflowPositions = calculateReflowPositions(movedUuid, movedBounds, allBounds, droppedNodeUuid === movedUuid ? targetHasPriority : false);
|
|
1351
|
-
// Merge into all reflow positions
|
|
1272
|
+
const reflowPositions = calculateReflowPositions(sacredNodeUuids, allBounds);
|
|
1273
|
+
if (reflowPositions.size > 0) {
|
|
1274
|
+
const positions = {};
|
|
1352
1275
|
for (const [uuid, position] of reflowPositions.entries()) {
|
|
1353
|
-
|
|
1276
|
+
positions[uuid] = position;
|
|
1354
1277
|
}
|
|
1355
|
-
|
|
1356
|
-
// If there are positions to update, apply them
|
|
1357
|
-
if (Object.keys(allReflowPositions).length > 0) {
|
|
1358
|
-
getStore().getState().updateCanvasPositions(allReflowPositions);
|
|
1278
|
+
getStore().getState().updateCanvasPositions(positions);
|
|
1359
1279
|
}
|
|
1360
1280
|
}
|
|
1361
1281
|
handleMouseMove(event) {
|
|
@@ -1390,21 +1310,37 @@ export class Editor extends RapidElement {
|
|
|
1390
1310
|
this.targetId = null;
|
|
1391
1311
|
this.isValidTarget = true;
|
|
1392
1312
|
// Show connection placeholder when over empty canvas
|
|
1393
|
-
// Calculate position: horizontally centered at mouse, vertically just below mouse
|
|
1394
1313
|
const canvas = this.querySelector('#canvas');
|
|
1395
1314
|
if (canvas) {
|
|
1396
1315
|
const canvasRect = canvas.getBoundingClientRect();
|
|
1397
1316
|
const relativeX = event.clientX - canvasRect.left;
|
|
1398
1317
|
const relativeY = event.clientY - canvasRect.top;
|
|
1399
|
-
|
|
1400
|
-
const
|
|
1401
|
-
const
|
|
1318
|
+
const placeholderWidth = 200;
|
|
1319
|
+
const placeholderHeight = 64;
|
|
1320
|
+
const arrowLength = ARROW_LENGTH;
|
|
1321
|
+
const cursorGap = CURSOR_GAP;
|
|
1322
|
+
// Determine if cursor is above the source exit using stored sourceY
|
|
1323
|
+
const dragUp = this.connectionSourceY != null
|
|
1324
|
+
? relativeY < this.connectionSourceY
|
|
1325
|
+
: false;
|
|
1326
|
+
let top;
|
|
1327
|
+
if (dragUp) {
|
|
1328
|
+
// Arrow points up: tip at cy + cursorGap.
|
|
1329
|
+
// Placeholder bottom should sit just above the arrow tip.
|
|
1330
|
+
top = relativeY + cursorGap - placeholderHeight;
|
|
1331
|
+
}
|
|
1332
|
+
else {
|
|
1333
|
+
// Arrow points down: tip at cy - cursorGap + arrowLength.
|
|
1334
|
+
// Placeholder top sits just below the arrow tip.
|
|
1335
|
+
top = relativeY - cursorGap + arrowLength;
|
|
1336
|
+
}
|
|
1402
1337
|
this.connectionPlaceholder = {
|
|
1403
1338
|
position: {
|
|
1404
1339
|
left: relativeX - placeholderWidth / 2,
|
|
1405
|
-
top
|
|
1340
|
+
top
|
|
1406
1341
|
},
|
|
1407
|
-
visible: true
|
|
1342
|
+
visible: true,
|
|
1343
|
+
dragUp
|
|
1408
1344
|
};
|
|
1409
1345
|
}
|
|
1410
1346
|
}
|
|
@@ -1505,34 +1441,7 @@ export class Editor extends RapidElement {
|
|
|
1505
1441
|
if (nodeUuids.length > 0) {
|
|
1506
1442
|
// Allow DOM to update before checking collisions
|
|
1507
1443
|
setTimeout(() => {
|
|
1508
|
-
|
|
1509
|
-
// If only one node was moved, detect which node it might have been dropped onto
|
|
1510
|
-
let droppedNodeUuid = null;
|
|
1511
|
-
let dropTargetBounds = null;
|
|
1512
|
-
if (nodeUuids.length === 1) {
|
|
1513
|
-
droppedNodeUuid = nodeUuids[0];
|
|
1514
|
-
const droppedNodeUI = (_b = this.definition._ui) === null || _b === void 0 ? void 0 : _b.nodes[droppedNodeUuid];
|
|
1515
|
-
if (droppedNodeUI === null || droppedNodeUI === void 0 ? void 0 : droppedNodeUI.position) {
|
|
1516
|
-
const droppedBounds = getNodeBounds(droppedNodeUuid, droppedNodeUI.position);
|
|
1517
|
-
if (droppedBounds) {
|
|
1518
|
-
// Find which node (if any) the dropped node overlaps with
|
|
1519
|
-
for (const node of this.definition.nodes) {
|
|
1520
|
-
if (node.uuid === droppedNodeUuid)
|
|
1521
|
-
continue;
|
|
1522
|
-
const nodeUI = (_c = this.definition._ui) === null || _c === void 0 ? void 0 : _c.nodes[node.uuid];
|
|
1523
|
-
if (!(nodeUI === null || nodeUI === void 0 ? void 0 : nodeUI.position))
|
|
1524
|
-
continue;
|
|
1525
|
-
const targetBounds = getNodeBounds(node.uuid, nodeUI.position);
|
|
1526
|
-
if (targetBounds &&
|
|
1527
|
-
nodesOverlap(droppedBounds, targetBounds)) {
|
|
1528
|
-
dropTargetBounds = targetBounds;
|
|
1529
|
-
break; // Use the first overlapping node
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
this.checkCollisionsAndReflow(nodeUuids, droppedNodeUuid, dropTargetBounds);
|
|
1444
|
+
this.checkCollisionsAndReflow(nodeUuids);
|
|
1536
1445
|
}, 0);
|
|
1537
1446
|
}
|
|
1538
1447
|
else {
|
|
@@ -1642,6 +1551,8 @@ export class Editor extends RapidElement {
|
|
|
1642
1551
|
this.pendingCanvasConnection = null;
|
|
1643
1552
|
this.connectionPlaceholder = null;
|
|
1644
1553
|
this.sourceId = null;
|
|
1554
|
+
this.connectionSourceX = null;
|
|
1555
|
+
this.connectionSourceY = null;
|
|
1645
1556
|
this.dragFromNodeId = null;
|
|
1646
1557
|
}
|
|
1647
1558
|
else {
|
|
@@ -1669,6 +1580,8 @@ export class Editor extends RapidElement {
|
|
|
1669
1580
|
this.pendingCanvasConnection = null;
|
|
1670
1581
|
this.connectionPlaceholder = null;
|
|
1671
1582
|
this.sourceId = null;
|
|
1583
|
+
this.connectionSourceX = null;
|
|
1584
|
+
this.connectionSourceY = null;
|
|
1672
1585
|
this.dragFromNodeId = null;
|
|
1673
1586
|
this.originalConnectionTargetId = null;
|
|
1674
1587
|
}
|
|
@@ -1856,6 +1769,8 @@ export class Editor extends RapidElement {
|
|
|
1856
1769
|
this.pendingCanvasConnection = null;
|
|
1857
1770
|
this.connectionPlaceholder = null;
|
|
1858
1771
|
this.sourceId = null;
|
|
1772
|
+
this.connectionSourceX = null;
|
|
1773
|
+
this.connectionSourceY = null;
|
|
1859
1774
|
this.dragFromNodeId = null;
|
|
1860
1775
|
}
|
|
1861
1776
|
// Reset the creation flags
|
|
@@ -1910,6 +1825,8 @@ export class Editor extends RapidElement {
|
|
|
1910
1825
|
this.pendingCanvasConnection = null;
|
|
1911
1826
|
this.connectionPlaceholder = null;
|
|
1912
1827
|
this.sourceId = null;
|
|
1828
|
+
this.connectionSourceX = null;
|
|
1829
|
+
this.connectionSourceY = null;
|
|
1913
1830
|
this.dragFromNodeId = null;
|
|
1914
1831
|
}
|
|
1915
1832
|
// Reset the creation flags
|
|
@@ -2655,7 +2572,7 @@ export class Editor extends RapidElement {
|
|
|
2655
2572
|
icon="revisions"
|
|
2656
2573
|
label="Revisions"
|
|
2657
2574
|
color="rgb(142, 94, 167)"
|
|
2658
|
-
|
|
2575
|
+
order="1"
|
|
2659
2576
|
.hidden=${!this.revisionsWindowHidden && this.localizationWindowHidden}
|
|
2660
2577
|
@temba-button-clicked=${this.handleRevisionsTabClick}
|
|
2661
2578
|
></temba-floating-tab>
|
|
@@ -2926,7 +2843,7 @@ export class Editor extends RapidElement {
|
|
|
2926
2843
|
icon="language"
|
|
2927
2844
|
label="Translate Flow"
|
|
2928
2845
|
color="#6b7280"
|
|
2929
|
-
|
|
2846
|
+
order="2"
|
|
2930
2847
|
.hidden=${!this.localizationWindowHidden}
|
|
2931
2848
|
@temba-button-clicked=${this.handleLocalizationTabClick}
|
|
2932
2849
|
></temba-floating-tab>
|