@nyaruka/temba-components 0.127.0 → 0.128.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 +13 -0
- package/demo/data/flows/sample-flow.json +127 -100
- package/demo/sticky-note-demo.html +152 -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 +11 -2
- 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 +346 -86
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/chart/TembaChart.js +20 -3
- package/out-tsc/src/chart/TembaChart.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +210 -1
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/EditorNode.js +98 -139
- package/out-tsc/src/flow/EditorNode.js.map +1 -1
- package/out-tsc/src/flow/StickyNote.js +272 -0
- package/out-tsc/src/flow/StickyNote.js.map +1 -0
- package/out-tsc/src/list/RunList.js +2 -1
- package/out-tsc/src/list/RunList.js.map +1 -1
- package/out-tsc/src/list/SortableList.js +9 -0
- package/out-tsc/src/list/SortableList.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 +11 -2
- 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/store/AppState.js +33 -0
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/src/vectoricon/index.js +2 -1
- package/out-tsc/src/vectoricon/index.js.map +1 -1
- package/out-tsc/temba-modules.js +2 -0
- package/out-tsc/temba-modules.js.map +1 -1
- package/out-tsc/test/temba-flow-editor-node.test.js +249 -5
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/out-tsc/test/temba-select.test.js +11 -17
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/out-tsc/test/utils.test.js +62 -0
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/sticky-note/blue.png +0 -0
- package/screenshots/truth/sticky-note/gray.png +0 -0
- package/screenshots/truth/sticky-note/green.png +0 -0
- package/screenshots/truth/sticky-note/pink.png +0 -0
- package/screenshots/truth/sticky-note/yellow.png +0 -0
- package/src/chart/TembaChart.ts +20 -3
- package/src/flow/Editor.ts +252 -2
- package/src/flow/EditorNode.ts +98 -156
- package/src/flow/StickyNote.ts +284 -0
- package/src/list/RunList.ts +2 -1
- package/src/list/SortableList.ts +11 -0
- package/src/locales/es.ts +18 -13
- package/src/locales/fr.ts +18 -13
- package/src/locales/locale-codes.ts +11 -2
- package/src/locales/pt.ts +18 -13
- package/src/store/AppState.ts +51 -1
- package/src/store/flow-definition.d.ts +8 -0
- package/src/vectoricon/index.ts +2 -1
- package/static/svg/index.pdf +137 -0
- package/temba-modules.ts +2 -0
- package/test/temba-flow-editor-node.test.ts +322 -6
- package/test/temba-select.test.ts +12 -20
- package/test/utils.test.ts +98 -0
- package/web-dev-server.config.mjs +30 -22
package/src/flow/Editor.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { html, TemplateResult } from 'lit-html';
|
|
2
2
|
import { css, PropertyValueMap, unsafeCSS } from 'lit';
|
|
3
|
-
import { property } from 'lit/decorators.js';
|
|
4
|
-
import { FlowDefinition } from '../store/flow-definition';
|
|
3
|
+
import { property, state } from 'lit/decorators.js';
|
|
4
|
+
import { FlowDefinition, FlowPosition } from '../store/flow-definition';
|
|
5
5
|
import { getStore } from '../store/Store';
|
|
6
6
|
import { AppState, fromStore, zustand } from '../store/AppState';
|
|
7
7
|
import { RapidElement } from '../RapidElement';
|
|
@@ -9,8 +9,21 @@ import { RapidElement } from '../RapidElement';
|
|
|
9
9
|
import { Plumber } from './Plumber';
|
|
10
10
|
import { EditorNode } from './EditorNode';
|
|
11
11
|
|
|
12
|
+
export function snapToGrid(value: number): number {
|
|
13
|
+
return Math.round(value / 20) * 20;
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
const SAVE_QUIET_TIME = 500;
|
|
13
17
|
|
|
18
|
+
export interface DraggableItem {
|
|
19
|
+
uuid: string;
|
|
20
|
+
position: FlowPosition;
|
|
21
|
+
element: HTMLElement;
|
|
22
|
+
type: 'node' | 'sticky';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DRAG_THRESHOLD = 10;
|
|
26
|
+
|
|
14
27
|
export class Editor extends RapidElement {
|
|
15
28
|
// unfortunately, jsplumb requires that we be in light DOM
|
|
16
29
|
createRenderRoot() {
|
|
@@ -38,6 +51,20 @@ export class Editor extends RapidElement {
|
|
|
38
51
|
@fromStore(zustand, (state: AppState) => state.dirtyDate)
|
|
39
52
|
private dirtyDate!: Date;
|
|
40
53
|
|
|
54
|
+
// Drag state
|
|
55
|
+
@state()
|
|
56
|
+
private isDragging = false;
|
|
57
|
+
private isMouseDown = false;
|
|
58
|
+
private dragStartPos = { x: 0, y: 0 };
|
|
59
|
+
|
|
60
|
+
@state()
|
|
61
|
+
private currentDragItem: DraggableItem | null = null;
|
|
62
|
+
private startPos = { left: 0, top: 0 };
|
|
63
|
+
|
|
64
|
+
// Bound event handlers to maintain proper 'this' context
|
|
65
|
+
private boundMouseMove = this.handleMouseMove.bind(this);
|
|
66
|
+
private boundMouseUp = this.handleMouseUp.bind(this);
|
|
67
|
+
|
|
41
68
|
static get styles() {
|
|
42
69
|
return css`
|
|
43
70
|
#editor {
|
|
@@ -86,6 +113,15 @@ export class Editor extends RapidElement {
|
|
|
86
113
|
margin: 20px;
|
|
87
114
|
}
|
|
88
115
|
|
|
116
|
+
#canvas > .draggable {
|
|
117
|
+
position: absolute;
|
|
118
|
+
z-index: 100;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#canvas > .dragging {
|
|
122
|
+
z-index: 10000 !important;
|
|
123
|
+
}
|
|
124
|
+
|
|
89
125
|
body .jtk-endpoint {
|
|
90
126
|
width: initial;
|
|
91
127
|
height: initial;
|
|
@@ -183,6 +219,7 @@ export class Editor extends RapidElement {
|
|
|
183
219
|
): void {
|
|
184
220
|
super.firstUpdated(changes);
|
|
185
221
|
this.plumber = new Plumber(this.querySelector('#canvas'));
|
|
222
|
+
this.setupGlobalEventListeners();
|
|
186
223
|
if (changes.has('flow')) {
|
|
187
224
|
getStore().getState().fetchRevision(`/flow/revisions/${this.flow}`);
|
|
188
225
|
}
|
|
@@ -196,6 +233,10 @@ export class Editor extends RapidElement {
|
|
|
196
233
|
// console.log('Setting canvas size', this.canvasSize);
|
|
197
234
|
}
|
|
198
235
|
|
|
236
|
+
if (changes.has('definition')) {
|
|
237
|
+
this.updateCanvasSize();
|
|
238
|
+
}
|
|
239
|
+
|
|
199
240
|
if (changes.has('dirtyDate')) {
|
|
200
241
|
if (this.dirtyDate) {
|
|
201
242
|
this.debouncedSave();
|
|
@@ -234,6 +275,189 @@ export class Editor extends RapidElement {
|
|
|
234
275
|
clearTimeout(this.saveTimer);
|
|
235
276
|
this.saveTimer = null;
|
|
236
277
|
}
|
|
278
|
+
document.removeEventListener('mousemove', this.boundMouseMove);
|
|
279
|
+
document.removeEventListener('mouseup', this.boundMouseUp);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private setupGlobalEventListeners(): void {
|
|
283
|
+
document.addEventListener('mousemove', this.boundMouseMove);
|
|
284
|
+
document.addEventListener('mouseup', this.boundMouseUp);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private getPosition(uuid: string, type: 'node' | 'sticky'): FlowPosition {
|
|
288
|
+
if (type === 'node') {
|
|
289
|
+
return this.definition._ui.nodes[uuid]?.position;
|
|
290
|
+
} else {
|
|
291
|
+
return this.definition._ui.stickies?.[uuid]?.position;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private updatePosition(
|
|
296
|
+
uuid: string,
|
|
297
|
+
type: 'node' | 'sticky',
|
|
298
|
+
position: FlowPosition
|
|
299
|
+
): void {
|
|
300
|
+
if (type === 'node') {
|
|
301
|
+
getStore().getState().updateNodePosition(uuid, position);
|
|
302
|
+
} else {
|
|
303
|
+
const currentSticky = this.definition._ui.stickies?.[uuid];
|
|
304
|
+
if (currentSticky) {
|
|
305
|
+
getStore()
|
|
306
|
+
.getState()
|
|
307
|
+
.updateStickyNote(uuid, {
|
|
308
|
+
...currentSticky,
|
|
309
|
+
position
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private handleMouseDown(event: MouseEvent): void {
|
|
316
|
+
const element = event.currentTarget as HTMLElement;
|
|
317
|
+
// Only start dragging if clicking on the element itself, not on exits or other interactive elements
|
|
318
|
+
const target = event.target as HTMLElement;
|
|
319
|
+
if (target.classList.contains('exit') || target.closest('.exit')) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const uuid = element.getAttribute('uuid');
|
|
324
|
+
const type = element.tagName === 'TEMBA-FLOW-NODE' ? 'node' : 'sticky';
|
|
325
|
+
|
|
326
|
+
const position = this.getPosition(uuid, type);
|
|
327
|
+
if (!position) return;
|
|
328
|
+
|
|
329
|
+
// Set up potential drag state, but don't start dragging yet
|
|
330
|
+
this.isMouseDown = true;
|
|
331
|
+
this.dragStartPos = { x: event.clientX, y: event.clientY };
|
|
332
|
+
this.startPos = { left: position.left, top: position.top };
|
|
333
|
+
this.currentDragItem = {
|
|
334
|
+
uuid,
|
|
335
|
+
position,
|
|
336
|
+
element,
|
|
337
|
+
type
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
event.preventDefault();
|
|
341
|
+
event.stopPropagation();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private handleMouseMove(event: MouseEvent): void {
|
|
345
|
+
if (!this.isMouseDown || !this.currentDragItem) return;
|
|
346
|
+
|
|
347
|
+
const deltaX = event.clientX - this.dragStartPos.x;
|
|
348
|
+
const deltaY = event.clientY - this.dragStartPos.y;
|
|
349
|
+
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
350
|
+
|
|
351
|
+
// Only start dragging if we've moved beyond the threshold
|
|
352
|
+
if (!this.isDragging && distance > DRAG_THRESHOLD) {
|
|
353
|
+
this.isDragging = true;
|
|
354
|
+
|
|
355
|
+
// If this is a node, elevate connections
|
|
356
|
+
if (this.currentDragItem.type === 'node' && this.plumber) {
|
|
357
|
+
this.plumber.elevateNodeConnections(this.currentDragItem.uuid);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// If we're actually dragging, update positions
|
|
362
|
+
if (this.isDragging) {
|
|
363
|
+
const newLeft = this.startPos.left + deltaX;
|
|
364
|
+
const newTop = this.startPos.top + deltaY;
|
|
365
|
+
|
|
366
|
+
// Update the visual position during drag
|
|
367
|
+
this.currentDragItem.element.style.left = `${newLeft}px`;
|
|
368
|
+
this.currentDragItem.element.style.top = `${newTop}px`;
|
|
369
|
+
|
|
370
|
+
// Repaint connections if this is a node
|
|
371
|
+
if (this.currentDragItem.type === 'node' && this.plumber) {
|
|
372
|
+
this.plumber.repaintEverything();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private handleMouseUp(event: MouseEvent): void {
|
|
378
|
+
if (!this.isMouseDown || !this.currentDragItem) return;
|
|
379
|
+
|
|
380
|
+
// If we were actually dragging, handle the drag end
|
|
381
|
+
if (this.isDragging) {
|
|
382
|
+
// Restore normal z-index for node connections
|
|
383
|
+
if (this.currentDragItem.type === 'node' && this.plumber) {
|
|
384
|
+
this.plumber.restoreNodeConnections(this.currentDragItem.uuid);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const deltaX = event.clientX - this.dragStartPos.x;
|
|
388
|
+
const deltaY = event.clientY - this.dragStartPos.y;
|
|
389
|
+
|
|
390
|
+
const newLeft = this.startPos.left + deltaX;
|
|
391
|
+
const newTop = this.startPos.top + deltaY;
|
|
392
|
+
|
|
393
|
+
// Snap to 20px grid for final position
|
|
394
|
+
const snappedLeft = snapToGrid(newLeft);
|
|
395
|
+
const snappedTop = snapToGrid(newTop);
|
|
396
|
+
|
|
397
|
+
const newPosition = { left: snappedLeft, top: snappedTop };
|
|
398
|
+
|
|
399
|
+
// Update the store with the new snapped position
|
|
400
|
+
this.updatePosition(
|
|
401
|
+
this.currentDragItem.uuid,
|
|
402
|
+
this.currentDragItem.type,
|
|
403
|
+
newPosition
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Update canvas positions for nodes
|
|
407
|
+
if (this.currentDragItem.type === 'node') {
|
|
408
|
+
getStore()
|
|
409
|
+
.getState()
|
|
410
|
+
.updateCanvasPositions({
|
|
411
|
+
[this.currentDragItem.uuid]: newPosition
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Repaint connections if this is a node
|
|
416
|
+
if (this.currentDragItem.type === 'node' && this.plumber) {
|
|
417
|
+
this.plumber.repaintEverything();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Reset all drag state
|
|
422
|
+
this.isDragging = false;
|
|
423
|
+
this.isMouseDown = false;
|
|
424
|
+
this.currentDragItem = null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private updateCanvasSize(): void {
|
|
428
|
+
if (!this.definition) return;
|
|
429
|
+
|
|
430
|
+
const store = getStore();
|
|
431
|
+
if (!store) return;
|
|
432
|
+
|
|
433
|
+
// Calculate required canvas size based on all elements
|
|
434
|
+
let maxWidth = 0;
|
|
435
|
+
let maxHeight = 0;
|
|
436
|
+
|
|
437
|
+
// Check node positions
|
|
438
|
+
this.definition.nodes.forEach((node) => {
|
|
439
|
+
const ui = this.definition._ui.nodes[node.uuid];
|
|
440
|
+
if (ui && ui.position) {
|
|
441
|
+
const nodeElement = this.querySelector(`[id="${node.uuid}"]`);
|
|
442
|
+
if (nodeElement) {
|
|
443
|
+
const rect = nodeElement.getBoundingClientRect();
|
|
444
|
+
maxWidth = Math.max(maxWidth, ui.position.left + rect.width);
|
|
445
|
+
maxHeight = Math.max(maxHeight, ui.position.top + rect.height);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Check sticky note positions
|
|
451
|
+
const stickies = this.definition._ui?.stickies || {};
|
|
452
|
+
Object.values(stickies).forEach((sticky) => {
|
|
453
|
+
if (sticky.position) {
|
|
454
|
+
maxWidth = Math.max(maxWidth, sticky.position.left + 200); // Sticky note width
|
|
455
|
+
maxHeight = Math.max(maxHeight, sticky.position.top + 100); // Sticky note height
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Update canvas size in store
|
|
460
|
+
store.getState().expandCanvas(maxWidth, maxHeight);
|
|
237
461
|
}
|
|
238
462
|
|
|
239
463
|
public render(): TemplateResult {
|
|
@@ -243,6 +467,8 @@ export class Editor extends RapidElement {
|
|
|
243
467
|
${unsafeCSS(EditorNode.styles.cssText)}
|
|
244
468
|
</style>`;
|
|
245
469
|
|
|
470
|
+
const stickies = this.definition?._ui?.stickies || {};
|
|
471
|
+
|
|
246
472
|
return html`${style}
|
|
247
473
|
<div id="editor">
|
|
248
474
|
<div
|
|
@@ -253,13 +479,37 @@ export class Editor extends RapidElement {
|
|
|
253
479
|
<div id="canvas">
|
|
254
480
|
${this.definition
|
|
255
481
|
? this.definition.nodes.map((node) => {
|
|
482
|
+
const position =
|
|
483
|
+
this.definition._ui.nodes[node.uuid].position;
|
|
484
|
+
|
|
485
|
+
const dragging =
|
|
486
|
+
this.isDragging && this.currentDragItem?.uuid === node.uuid;
|
|
487
|
+
|
|
256
488
|
return html`<temba-flow-node
|
|
489
|
+
class="draggable ${dragging ? 'dragging' : ''}"
|
|
490
|
+
@mousedown=${this.handleMouseDown.bind(this)}
|
|
491
|
+
uuid=${node.uuid}
|
|
492
|
+
style="left:${position.left}px; top:${position.top}px"
|
|
257
493
|
.plumber=${this.plumber}
|
|
258
494
|
.node=${node}
|
|
259
495
|
.ui=${this.definition._ui.nodes[node.uuid]}
|
|
260
496
|
></temba-flow-node>`;
|
|
261
497
|
})
|
|
262
498
|
: html`<temba-loading></temba-loading>`}
|
|
499
|
+
${Object.entries(stickies).map(([uuid, sticky]) => {
|
|
500
|
+
const position = sticky.position || { left: 0, top: 0 };
|
|
501
|
+
const dragging =
|
|
502
|
+
this.isDragging && this.currentDragItem?.uuid === uuid;
|
|
503
|
+
return html`<temba-sticky-note
|
|
504
|
+
class="draggable ${dragging ? 'dragging' : ''}"
|
|
505
|
+
@mousedown=${this.handleMouseDown.bind(this)}
|
|
506
|
+
style="left:${position.left}px; top:${position.top}px; z-index: ${1000 +
|
|
507
|
+
position.top}"
|
|
508
|
+
uuid=${uuid}
|
|
509
|
+
.data=${sticky}
|
|
510
|
+
.dragging=${dragging}
|
|
511
|
+
></temba-sticky-note>`;
|
|
512
|
+
})}
|
|
263
513
|
</div>
|
|
264
514
|
</div>
|
|
265
515
|
</div>`;
|
package/src/flow/EditorNode.ts
CHANGED
|
@@ -21,26 +21,10 @@ export class EditorNode extends RapidElement {
|
|
|
21
21
|
@property({ type: Object })
|
|
22
22
|
private ui: NodeUI;
|
|
23
23
|
|
|
24
|
-
// Drag state properties
|
|
25
|
-
private isDragging = false;
|
|
26
|
-
private dragStartPos = { x: 0, y: 0 };
|
|
27
|
-
private nodeStartPos = { left: 0, top: 0 };
|
|
28
|
-
|
|
29
|
-
// Bound event handlers to maintain proper 'this' context
|
|
30
|
-
private boundMouseMove = this.handleMouseMove.bind(this);
|
|
31
|
-
private boundMouseUp = this.handleMouseUp.bind(this);
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Snaps a coordinate value to the nearest 20px grid position
|
|
35
|
-
*/
|
|
36
|
-
private snapToGrid(value: number): number {
|
|
37
|
-
return Math.round(value / 20) * 20;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
24
|
static get styles() {
|
|
41
25
|
return css`
|
|
26
|
+
|
|
42
27
|
.node {
|
|
43
|
-
position: absolute;
|
|
44
28
|
background-color: #fff;
|
|
45
29
|
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
|
|
46
30
|
min-width: 200px;
|
|
@@ -49,7 +33,7 @@ export class EditorNode extends RapidElement {
|
|
|
49
33
|
color: #333;
|
|
50
34
|
cursor: move;
|
|
51
35
|
user-select: none;
|
|
52
|
-
|
|
36
|
+
|
|
53
37
|
}
|
|
54
38
|
|
|
55
39
|
.node:hover {
|
|
@@ -64,21 +48,59 @@ export class EditorNode extends RapidElement {
|
|
|
64
48
|
|
|
65
49
|
.action {
|
|
66
50
|
max-width: 200px;
|
|
51
|
+
position: relative;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.action.sortable {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: stretch;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.action .action-content {
|
|
60
|
+
flex-grow: 1;
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
67
63
|
}
|
|
68
64
|
|
|
69
65
|
.action .body {
|
|
70
66
|
padding: 1em;
|
|
71
67
|
}
|
|
72
68
|
|
|
69
|
+
.action .drag-handle {
|
|
70
|
+
opacity: 0;
|
|
71
|
+
transition: all 200ms ease-in-out;
|
|
72
|
+
cursor: move;
|
|
73
|
+
background: rgba(0, 0, 0, 0.02);
|
|
74
|
+
max-width:0px;
|
|
75
|
+
position: absolute;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.action:hover .drag-handle {
|
|
79
|
+
opacity: 0.5;
|
|
80
|
+
padding: 0.25em;
|
|
81
|
+
max-width: 20px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.action .drag-handle:hover {
|
|
85
|
+
opacity: 1;
|
|
86
|
+
|
|
87
|
+
}
|
|
88
|
+
|
|
73
89
|
.action .title,
|
|
74
90
|
.router .title {
|
|
91
|
+
display: flex;
|
|
75
92
|
color: #fff;
|
|
76
93
|
padding: 5px 1px;
|
|
77
94
|
text-align: center;
|
|
78
95
|
font-size: 1em;
|
|
79
96
|
font-weight: normal;
|
|
97
|
+
|
|
80
98
|
}
|
|
81
99
|
|
|
100
|
+
.title .name {
|
|
101
|
+
flex-grow: 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
82
104
|
.quick-replies {
|
|
83
105
|
margin-top: 0.5em;
|
|
84
106
|
}
|
|
@@ -133,176 +155,89 @@ export class EditorNode extends RapidElement {
|
|
|
133
155
|
}`;
|
|
134
156
|
}
|
|
135
157
|
|
|
158
|
+
constructor() {
|
|
159
|
+
super();
|
|
160
|
+
this.handleActionOrderChanged = this.handleActionOrderChanged.bind(this);
|
|
161
|
+
}
|
|
162
|
+
|
|
136
163
|
protected updated(
|
|
137
164
|
changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
|
138
165
|
): void {
|
|
139
166
|
super.updated(changes);
|
|
140
167
|
if (changes.has('node')) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
168
|
+
// make our initial connections
|
|
169
|
+
if (changes.get('node') === undefined) {
|
|
170
|
+
// this.plumber.makeTarget(this.node.uuid);
|
|
171
|
+
for (const exit of this.node.exits) {
|
|
172
|
+
if (!exit.destination_uuid) {
|
|
173
|
+
this.plumber.makeSource(exit.uuid);
|
|
174
|
+
} else {
|
|
175
|
+
this.plumber.connectIds(exit.uuid, exit.destination_uuid);
|
|
176
|
+
}
|
|
149
177
|
}
|
|
150
178
|
}
|
|
151
179
|
|
|
152
|
-
const ele = this.
|
|
180
|
+
const ele = this.parentElement;
|
|
153
181
|
const rect = ele.getBoundingClientRect();
|
|
154
182
|
|
|
155
183
|
getStore()
|
|
156
|
-
|
|
184
|
+
?.getState()
|
|
157
185
|
.expandCanvas(
|
|
158
186
|
this.ui.position.left + rect.width,
|
|
159
187
|
this.ui.position.top + rect.height
|
|
160
188
|
);
|
|
161
|
-
|
|
162
|
-
// Add drag event listeners to the node
|
|
163
|
-
this.addDragEventListeners();
|
|
164
189
|
}
|
|
165
190
|
}
|
|
166
191
|
|
|
167
|
-
private
|
|
168
|
-
const
|
|
169
|
-
if (!nodeElement) return;
|
|
170
|
-
|
|
171
|
-
nodeElement.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
|
172
|
-
document.addEventListener('mousemove', this.boundMouseMove);
|
|
173
|
-
document.addEventListener('mouseup', this.boundMouseUp);
|
|
174
|
-
}
|
|
192
|
+
private handleActionOrderChanged(event: CustomEvent) {
|
|
193
|
+
const [fromIdx, toIdx] = event.detail.swap;
|
|
175
194
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
195
|
+
// swap our actions
|
|
196
|
+
const newActions = [...this.node.actions];
|
|
197
|
+
const movedAction = newActions.splice(fromIdx, 1)[0];
|
|
198
|
+
newActions.splice(toIdx, 0, movedAction);
|
|
182
199
|
|
|
183
|
-
this
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
top: this.ui.position.top
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
// Add dragging class for visual feedback
|
|
191
|
-
const nodeElement = this.querySelector('.node') as HTMLElement;
|
|
192
|
-
if (nodeElement) {
|
|
193
|
-
nodeElement.classList.add('dragging');
|
|
194
|
-
}
|
|
200
|
+
// udate our internal reprensentation, this isn't strictly necessary
|
|
201
|
+
// since the editor will update us from it's definition subscription
|
|
202
|
+
// but it makes testing a lot easier
|
|
203
|
+
this.node = { ...this.node, actions: newActions };
|
|
195
204
|
|
|
196
|
-
// Elevate connections for this node during dragging
|
|
197
|
-
if (this.plumber) {
|
|
198
|
-
this.plumber.elevateNodeConnections(this.node.uuid);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
event.preventDefault();
|
|
202
|
-
event.stopPropagation();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
private handleMouseMove(event: MouseEvent): void {
|
|
206
|
-
if (!this.isDragging) return;
|
|
207
|
-
|
|
208
|
-
const deltaX = event.clientX - this.dragStartPos.x;
|
|
209
|
-
const deltaY = event.clientY - this.dragStartPos.y;
|
|
210
|
-
|
|
211
|
-
const newLeft = this.nodeStartPos.left + deltaX;
|
|
212
|
-
const newTop = this.nodeStartPos.top + deltaY;
|
|
213
|
-
|
|
214
|
-
// Update the UI position temporarily (for visual feedback)
|
|
215
|
-
const nodeElement = this.querySelector('.node') as HTMLElement;
|
|
216
|
-
if (nodeElement) {
|
|
217
|
-
nodeElement.style.left = `${newLeft}px`;
|
|
218
|
-
nodeElement.style.top = `${newTop}px`;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Repaint connections during dragging for smooth updates
|
|
222
|
-
if (this.plumber) {
|
|
223
|
-
this.plumber.repaintEverything();
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
private handleMouseUp(event: MouseEvent): void {
|
|
228
|
-
if (!this.isDragging) return;
|
|
229
|
-
|
|
230
|
-
this.isDragging = false;
|
|
231
|
-
|
|
232
|
-
// Remove dragging class
|
|
233
|
-
const nodeElement = this.querySelector('.node') as HTMLElement;
|
|
234
|
-
if (nodeElement) {
|
|
235
|
-
nodeElement.classList.remove('dragging');
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Restore normal z-index for connections
|
|
239
|
-
if (this.plumber) {
|
|
240
|
-
this.plumber.restoreNodeConnections(this.node.uuid);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const deltaX = event.clientX - this.dragStartPos.x;
|
|
244
|
-
const deltaY = event.clientY - this.dragStartPos.y;
|
|
245
|
-
|
|
246
|
-
const newLeft = this.nodeStartPos.left + deltaX;
|
|
247
|
-
const newTop = this.nodeStartPos.top + deltaY;
|
|
248
|
-
|
|
249
|
-
// Snap to 20px grid for final position
|
|
250
|
-
const snappedLeft = this.snapToGrid(newLeft);
|
|
251
|
-
const snappedTop = this.snapToGrid(newTop);
|
|
252
|
-
|
|
253
|
-
// Update the store with the new snapped position
|
|
254
|
-
const newPosition = { left: snappedLeft, top: snappedTop };
|
|
255
205
|
getStore()
|
|
256
|
-
|
|
257
|
-
.
|
|
258
|
-
[this.node.uuid]: newPosition
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// Repaint connections if plumber is available
|
|
262
|
-
if (this.plumber) {
|
|
263
|
-
this.plumber.repaintEverything();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
getStore().getState().updateNodePosition(this.node.uuid, newPosition);
|
|
267
|
-
|
|
268
|
-
// Fire a custom event with the new coordinates
|
|
269
|
-
/*this.fireCustomEvent(CustomEventType.Moved, {
|
|
270
|
-
nodeId: this.node.uuid,
|
|
271
|
-
position: newPosition,
|
|
272
|
-
oldPosition: {
|
|
273
|
-
left: this.nodeStartPos.left,
|
|
274
|
-
top: this.nodeStartPos.top
|
|
275
|
-
}
|
|
276
|
-
});*/
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
disconnectedCallback(): void {
|
|
280
|
-
super.disconnectedCallback();
|
|
281
|
-
// Clean up event listeners
|
|
282
|
-
document.removeEventListener('mousemove', this.boundMouseMove);
|
|
283
|
-
document.removeEventListener('mouseup', this.boundMouseUp);
|
|
206
|
+
?.getState()
|
|
207
|
+
.updateNode(this.node.uuid, { ...this.node, actions: newActions });
|
|
284
208
|
}
|
|
285
209
|
|
|
286
210
|
private renderTitle(config: UIConfig) {
|
|
287
211
|
return html`<div class="title" style="background:${config.color}">
|
|
288
|
-
${
|
|
212
|
+
${this.node?.actions?.length > 1
|
|
213
|
+
? html`<temba-icon class="drag-handle" name="sort"></temba-icon>`
|
|
214
|
+
: null}
|
|
215
|
+
|
|
216
|
+
<div class="name">${config.name}</div>
|
|
289
217
|
</div>`;
|
|
290
218
|
}
|
|
291
219
|
|
|
292
|
-
private renderAction(node: Node, action: Action) {
|
|
220
|
+
private renderAction(node: Node, action: Action, index: number) {
|
|
293
221
|
const config = EDITOR_CONFIG[action.type];
|
|
294
222
|
|
|
295
223
|
if (config) {
|
|
296
|
-
return html`<div
|
|
297
|
-
${
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
224
|
+
return html`<div
|
|
225
|
+
class="action sortable ${action.type}"
|
|
226
|
+
id="action-${index}"
|
|
227
|
+
>
|
|
228
|
+
<div class="action-content">
|
|
229
|
+
${this.renderTitle(config)}
|
|
230
|
+
<div class="body">
|
|
231
|
+
${config.render
|
|
232
|
+
? config.render(node, action)
|
|
233
|
+
: html`<pre>${action.type}</pre>`}
|
|
234
|
+
</div>
|
|
302
235
|
</div>
|
|
303
236
|
</div>`;
|
|
304
237
|
}
|
|
305
|
-
return html`<div
|
|
238
|
+
return html`<div class="action sortable" id="action-${index}">
|
|
239
|
+
${action.type}
|
|
240
|
+
</div>`;
|
|
306
241
|
}
|
|
307
242
|
|
|
308
243
|
private renderRouter(router: Router, ui: NodeUI) {
|
|
@@ -359,9 +294,16 @@ export class EditorNode extends RapidElement {
|
|
|
359
294
|
class="node"
|
|
360
295
|
style="left:${this.ui.position.left}px;top:${this.ui.position.top}px"
|
|
361
296
|
>
|
|
362
|
-
${this.node.actions.
|
|
363
|
-
|
|
364
|
-
|
|
297
|
+
${this.node.actions.length > 0
|
|
298
|
+
? html`<temba-sortable-list
|
|
299
|
+
dragHandle="drag-handle"
|
|
300
|
+
@temba-order-changed="${this.handleActionOrderChanged}"
|
|
301
|
+
>
|
|
302
|
+
${this.node.actions.map((actionSpec, index) => {
|
|
303
|
+
return this.renderAction(this.node, actionSpec, index);
|
|
304
|
+
})}
|
|
305
|
+
</temba-sortable-list>`
|
|
306
|
+
: ''}
|
|
365
307
|
${this.node.router
|
|
366
308
|
? html` ${this.renderRouter(this.node.router, this.ui)}
|
|
367
309
|
${this.renderCategories(this.node)}`
|