@nyaruka/temba-components 0.125.0 → 0.127.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/demo/chart/example.html +18 -1
- package/demo/data/flows/sample-flow.json +82 -228
- package/demo/data/server/opened-tickets-long.json +53 -0
- package/dist/temba-components.js +53 -15
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/chart/TembaChart.js +33 -5
- package/out-tsc/src/chart/TembaChart.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +70 -5
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/EditorNode.js +136 -4
- package/out-tsc/src/flow/EditorNode.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +57 -0
- package/out-tsc/src/flow/Plumber.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/store/AppState.js +22 -5
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/test/temba-chart.test.js +26 -0
- package/out-tsc/test/temba-chart.test.js.map +1 -1
- package/out-tsc/test/temba-flow-node-drag.test.js +257 -0
- package/out-tsc/test/temba-flow-node-drag.test.js.map +1 -0
- package/out-tsc/test/temba-select.test.js +3 -2
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/package.json +1 -1
- package/src/chart/TembaChart.ts +35 -5
- package/src/flow/Editor.ts +78 -6
- package/src/flow/EditorNode.ts +159 -4
- package/src/flow/Plumber.ts +65 -0
- package/src/interfaces.ts +2 -1
- package/src/store/AppState.ts +28 -7
- package/test/temba-chart.test.ts +36 -0
- package/test/temba-flow-node-drag.test.ts +337 -0
- package/test/temba-select.test.ts +3 -2
- package/web-dev-server.config.mjs +39 -14
package/src/chart/TembaChart.ts
CHANGED
|
@@ -292,9 +292,15 @@ export class TembaChart extends RapidElement {
|
|
|
292
292
|
@property({ type: String })
|
|
293
293
|
xType: 'category' | 'time' = 'category';
|
|
294
294
|
|
|
295
|
+
@property({ type: Number })
|
|
296
|
+
xMaxTicks: number = 10;
|
|
297
|
+
|
|
295
298
|
@property({ type: String })
|
|
296
299
|
yType: 'count' | 'duration' = 'count';
|
|
297
300
|
|
|
301
|
+
@property({ type: String })
|
|
302
|
+
xFormat: 'MMM yy' | 'MMM yyyy' | 'MMM dd' | 'DD' | 'EEE' | 'auto' = 'auto';
|
|
303
|
+
|
|
298
304
|
@property({ type: Boolean })
|
|
299
305
|
hideOther: boolean = false;
|
|
300
306
|
|
|
@@ -581,6 +587,9 @@ export class TembaChart extends RapidElement {
|
|
|
581
587
|
|
|
582
588
|
const percentFormatter = (value: number): string => {
|
|
583
589
|
const pct = grandTotal ? (value / grandTotal) * 100 : 0;
|
|
590
|
+
if (pct === 0) {
|
|
591
|
+
return '';
|
|
592
|
+
}
|
|
584
593
|
return `${Math.round(pct)}%`;
|
|
585
594
|
};
|
|
586
595
|
|
|
@@ -604,6 +613,21 @@ export class TembaChart extends RapidElement {
|
|
|
604
613
|
|
|
605
614
|
this.chart.update();
|
|
606
615
|
} else {
|
|
616
|
+
let format = this.xFormat;
|
|
617
|
+
if (this.xType === 'time' && this.xFormat === 'auto') {
|
|
618
|
+
const firstDate = this.data.labels[0];
|
|
619
|
+
const lastDate = this.data.labels[this.data.labels.length - 1];
|
|
620
|
+
|
|
621
|
+
const first = Date.parse(firstDate);
|
|
622
|
+
const last = Date.parse(lastDate);
|
|
623
|
+
|
|
624
|
+
const dayDiff = Math.ceil((last - first) / (1000 * 60 * 60 * 24));
|
|
625
|
+
format = 'MMM dd';
|
|
626
|
+
if (dayDiff > 365) {
|
|
627
|
+
format = 'MMM yyyy';
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
607
631
|
const chartData = {
|
|
608
632
|
type: this.chartType,
|
|
609
633
|
data: {
|
|
@@ -664,6 +688,7 @@ export class TembaChart extends RapidElement {
|
|
|
664
688
|
},
|
|
665
689
|
ticks: {
|
|
666
690
|
display: !this.showPercent,
|
|
691
|
+
|
|
667
692
|
...(this.yType === 'duration' &&
|
|
668
693
|
!this.showPercent && {
|
|
669
694
|
callback: (value: any) => formatDurationFromSeconds(value)
|
|
@@ -674,11 +699,16 @@ export class TembaChart extends RapidElement {
|
|
|
674
699
|
type: this.xType,
|
|
675
700
|
grid: { display: false },
|
|
676
701
|
stacked: true,
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
702
|
+
ticks: {
|
|
703
|
+
maxTicksLimit: this.xMaxTicks
|
|
704
|
+
},
|
|
705
|
+
...(this.xType === 'time' && {
|
|
706
|
+
time: {
|
|
707
|
+
unit: 'day',
|
|
708
|
+
tooltipFormat: 'DDD',
|
|
709
|
+
displayFormats: { day: format }
|
|
710
|
+
}
|
|
711
|
+
})
|
|
682
712
|
}
|
|
683
713
|
}
|
|
684
714
|
}
|
package/src/flow/Editor.ts
CHANGED
|
@@ -9,15 +9,20 @@ import { RapidElement } from '../RapidElement';
|
|
|
9
9
|
import { Plumber } from './Plumber';
|
|
10
10
|
import { EditorNode } from './EditorNode';
|
|
11
11
|
|
|
12
|
+
const SAVE_QUIET_TIME = 500;
|
|
13
|
+
|
|
12
14
|
export class Editor extends RapidElement {
|
|
13
|
-
//
|
|
15
|
+
// unfortunately, jsplumb requires that we be in light DOM
|
|
14
16
|
createRenderRoot() {
|
|
15
17
|
return this;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
//
|
|
20
|
+
// this is the master plumber
|
|
19
21
|
private plumber: Plumber;
|
|
20
22
|
|
|
23
|
+
// timer for debounced saving
|
|
24
|
+
private saveTimer: number | null = null;
|
|
25
|
+
|
|
21
26
|
@property({ type: String })
|
|
22
27
|
public flow: string;
|
|
23
28
|
|
|
@@ -30,6 +35,9 @@ export class Editor extends RapidElement {
|
|
|
30
35
|
@fromStore(zustand, (state: AppState) => state.canvasSize)
|
|
31
36
|
private canvasSize!: { width: number; height: number };
|
|
32
37
|
|
|
38
|
+
@fromStore(zustand, (state: AppState) => state.dirtyDate)
|
|
39
|
+
private dirtyDate!: Date;
|
|
40
|
+
|
|
33
41
|
static get styles() {
|
|
34
42
|
return css`
|
|
35
43
|
#editor {
|
|
@@ -40,7 +48,7 @@ export class Editor extends RapidElement {
|
|
|
40
48
|
#grid {
|
|
41
49
|
position: relative;
|
|
42
50
|
background-color: #f9f9f9;
|
|
43
|
-
background-position:
|
|
51
|
+
background-position: 10px 10px;
|
|
44
52
|
background-image: linear-gradient(
|
|
45
53
|
0deg,
|
|
46
54
|
transparent 24%,
|
|
@@ -84,11 +92,11 @@ export class Editor extends RapidElement {
|
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
.jtk-endpoint {
|
|
87
|
-
z-index:
|
|
95
|
+
z-index: 600;
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
.plumb-source {
|
|
91
|
-
z-index:
|
|
99
|
+
z-index: 600;
|
|
92
100
|
border: 0px solid var(--color-connectors);
|
|
93
101
|
}
|
|
94
102
|
|
|
@@ -112,7 +120,7 @@ export class Editor extends RapidElement {
|
|
|
112
120
|
|
|
113
121
|
.plumb-target {
|
|
114
122
|
margin-top: -6px;
|
|
115
|
-
z-index:
|
|
123
|
+
z-index: 600;
|
|
116
124
|
opacity: 0;
|
|
117
125
|
cursor: pointer;
|
|
118
126
|
}
|
|
@@ -120,6 +128,29 @@ export class Editor extends RapidElement {
|
|
|
120
128
|
body .plumb-connector path {
|
|
121
129
|
stroke: var(--color-connectors) !important;
|
|
122
130
|
stroke-width: 3px;
|
|
131
|
+
z-index: 10;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
body .plumb-connector {
|
|
135
|
+
z-index: 10;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
body .plumb-connector.elevated {
|
|
139
|
+
z-index: 550;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
body .plumb-connector.elevated path {
|
|
143
|
+
stroke: var(--color-connectors) !important;
|
|
144
|
+
stroke-width: 3px;
|
|
145
|
+
z-index: 550;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
body .plumb-connector.elevated .plumb-arrow {
|
|
149
|
+
fill: var(--color-connectors);
|
|
150
|
+
stroke: var(--color-connectors);
|
|
151
|
+
stroke-width: 0px;
|
|
152
|
+
margin-top: 6px;
|
|
153
|
+
z-index: 550;
|
|
123
154
|
}
|
|
124
155
|
|
|
125
156
|
body .plumb-connector .plumb-arrow {
|
|
@@ -127,6 +158,7 @@ export class Editor extends RapidElement {
|
|
|
127
158
|
stroke: var(--color-connectors);
|
|
128
159
|
stroke-width: 0px;
|
|
129
160
|
margin-top: 6px;
|
|
161
|
+
z-index: 10;
|
|
130
162
|
}
|
|
131
163
|
|
|
132
164
|
body svg.jtk-connector.jtk-hover path {
|
|
@@ -137,6 +169,7 @@ export class Editor extends RapidElement {
|
|
|
137
169
|
body .plumb-connector.jtk-hover .plumb-arrow {
|
|
138
170
|
fill: var(--color-success) !important;
|
|
139
171
|
stroke-width: 0px;
|
|
172
|
+
z-index: 10;
|
|
140
173
|
}
|
|
141
174
|
`;
|
|
142
175
|
}
|
|
@@ -162,6 +195,45 @@ export class Editor extends RapidElement {
|
|
|
162
195
|
if (changes.has('canvasSize')) {
|
|
163
196
|
// console.log('Setting canvas size', this.canvasSize);
|
|
164
197
|
}
|
|
198
|
+
|
|
199
|
+
if (changes.has('dirtyDate')) {
|
|
200
|
+
if (this.dirtyDate) {
|
|
201
|
+
this.debouncedSave();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private debouncedSave(): void {
|
|
207
|
+
// Clear any existing timer
|
|
208
|
+
if (this.saveTimer !== null) {
|
|
209
|
+
clearTimeout(this.saveTimer);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.saveTimer = window.setTimeout(() => {
|
|
213
|
+
const now = new Date();
|
|
214
|
+
const timeSinceLastChange = now.getTime() - this.dirtyDate.getTime();
|
|
215
|
+
|
|
216
|
+
if (timeSinceLastChange >= SAVE_QUIET_TIME) {
|
|
217
|
+
this.saveChanges();
|
|
218
|
+
this.saveTimer = null;
|
|
219
|
+
} else {
|
|
220
|
+
this.debouncedSave();
|
|
221
|
+
}
|
|
222
|
+
}, SAVE_QUIET_TIME);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private saveChanges(): void {
|
|
226
|
+
// post the flow definition to the server
|
|
227
|
+
getStore().postJSON(`/flow/revisions/${this.flow}`, this.definition);
|
|
228
|
+
getStore().getState().setDirtyDate(null);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
disconnectedCallback(): void {
|
|
232
|
+
super.disconnectedCallback();
|
|
233
|
+
if (this.saveTimer !== null) {
|
|
234
|
+
clearTimeout(this.saveTimer);
|
|
235
|
+
this.saveTimer = null;
|
|
236
|
+
}
|
|
165
237
|
}
|
|
166
238
|
|
|
167
239
|
public render(): TemplateResult {
|
package/src/flow/EditorNode.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { css, html, PropertyValueMap, TemplateResult } from 'lit';
|
|
2
2
|
import { EDITOR_CONFIG, UIConfig } from './config';
|
|
3
3
|
import { Action, Exit, Node, NodeUI, Router } from '../store/flow-definition';
|
|
4
|
-
import {
|
|
4
|
+
import { property } from 'lit/decorators.js';
|
|
5
5
|
import { RapidElement } from '../RapidElement';
|
|
6
6
|
import { getClasses } from '../utils';
|
|
7
7
|
import { Plumber } from './Plumber';
|
|
@@ -12,15 +12,31 @@ export class EditorNode extends RapidElement {
|
|
|
12
12
|
return this;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
@
|
|
15
|
+
@property({ type: Object })
|
|
16
16
|
private plumber: Plumber;
|
|
17
17
|
|
|
18
|
-
@
|
|
18
|
+
@property({ type: Object })
|
|
19
19
|
private node: Node;
|
|
20
20
|
|
|
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
|
+
|
|
24
40
|
static get styles() {
|
|
25
41
|
return css`
|
|
26
42
|
.node {
|
|
@@ -31,6 +47,19 @@ export class EditorNode extends RapidElement {
|
|
|
31
47
|
border-radius: calc(var(--curvature) * 1.5);
|
|
32
48
|
overflow: hidden;
|
|
33
49
|
color: #333;
|
|
50
|
+
cursor: move;
|
|
51
|
+
user-select: none;
|
|
52
|
+
z-index: 500;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.node:hover {
|
|
56
|
+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.node.dragging {
|
|
60
|
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.4);
|
|
61
|
+
transform: scale(1.02);
|
|
62
|
+
z-index: 1000;
|
|
34
63
|
}
|
|
35
64
|
|
|
36
65
|
.action {
|
|
@@ -129,9 +158,131 @@ export class EditorNode extends RapidElement {
|
|
|
129
158
|
this.ui.position.left + rect.width,
|
|
130
159
|
this.ui.position.top + rect.height
|
|
131
160
|
);
|
|
161
|
+
|
|
162
|
+
// Add drag event listeners to the node
|
|
163
|
+
this.addDragEventListeners();
|
|
132
164
|
}
|
|
133
165
|
}
|
|
134
166
|
|
|
167
|
+
private addDragEventListeners(): void {
|
|
168
|
+
const nodeElement = this.querySelector('.node') as HTMLElement;
|
|
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
|
+
}
|
|
175
|
+
|
|
176
|
+
private handleMouseDown(event: MouseEvent): void {
|
|
177
|
+
// Only start dragging if clicking on the node itself, not on exits or other interactive elements
|
|
178
|
+
const target = event.target as HTMLElement;
|
|
179
|
+
if (target.classList.contains('exit') || target.closest('.exit')) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.isDragging = true;
|
|
184
|
+
this.dragStartPos = { x: event.clientX, y: event.clientY };
|
|
185
|
+
this.nodeStartPos = {
|
|
186
|
+
left: this.ui.position.left,
|
|
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
|
+
}
|
|
195
|
+
|
|
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
|
+
getStore()
|
|
256
|
+
.getState()
|
|
257
|
+
.updateCanvasPositions({
|
|
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);
|
|
284
|
+
}
|
|
285
|
+
|
|
135
286
|
private renderTitle(config: UIConfig) {
|
|
136
287
|
return html`<div class="title" style="background:${config.color}">
|
|
137
288
|
${config.name}
|
|
@@ -198,6 +349,10 @@ export class EditorNode extends RapidElement {
|
|
|
198
349
|
}
|
|
199
350
|
|
|
200
351
|
public render() {
|
|
352
|
+
if (!this.node || !this.ui) {
|
|
353
|
+
return html`<div class="node">Loading...</div>`;
|
|
354
|
+
}
|
|
355
|
+
|
|
201
356
|
return html`
|
|
202
357
|
<div
|
|
203
358
|
id="${this.node.uuid}"
|
package/src/flow/Plumber.ts
CHANGED
|
@@ -132,4 +132,69 @@ export class Plumber {
|
|
|
132
132
|
this.pendingConnections.push({ fromId, toId });
|
|
133
133
|
this.processPendingConnections();
|
|
134
134
|
}
|
|
135
|
+
|
|
136
|
+
public repaintEverything() {
|
|
137
|
+
if (this.jsPlumb) {
|
|
138
|
+
this.jsPlumb.repaintEverything();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
public elevateNodeConnections(nodeId: string) {
|
|
142
|
+
if (!this.jsPlumb) return;
|
|
143
|
+
|
|
144
|
+
// Get all connections
|
|
145
|
+
const connections = this.jsPlumb.getConnections();
|
|
146
|
+
|
|
147
|
+
// Get the node element to find its exit elements
|
|
148
|
+
const nodeElement = document.getElementById(nodeId);
|
|
149
|
+
const exitElements = nodeElement
|
|
150
|
+
? nodeElement.querySelectorAll('.exit')
|
|
151
|
+
: [];
|
|
152
|
+
const exitIds = Array.from(exitElements).map((exit) => exit.id);
|
|
153
|
+
|
|
154
|
+
connections.forEach((connection) => {
|
|
155
|
+
const sourceId = connection.source.id;
|
|
156
|
+
const targetId = connection.target.id;
|
|
157
|
+
|
|
158
|
+
// Check if this connection involves the dragged node:
|
|
159
|
+
// - Incoming: target is the node itself
|
|
160
|
+
// - Outgoing: source is one of the node's exits
|
|
161
|
+
if (targetId === nodeId || exitIds.includes(sourceId)) {
|
|
162
|
+
// Add elevated class to the connector element
|
|
163
|
+
const connectorElement = connection.connector.canvas;
|
|
164
|
+
if (connectorElement) {
|
|
165
|
+
connectorElement.classList.add('elevated');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public restoreNodeConnections(nodeId: string) {
|
|
172
|
+
if (!this.jsPlumb) return;
|
|
173
|
+
|
|
174
|
+
// Get all connections
|
|
175
|
+
const connections = this.jsPlumb.getConnections();
|
|
176
|
+
|
|
177
|
+
// Get the node element to find its exit elements
|
|
178
|
+
const nodeElement = document.getElementById(nodeId);
|
|
179
|
+
const exitElements = nodeElement
|
|
180
|
+
? nodeElement.querySelectorAll('.exit')
|
|
181
|
+
: [];
|
|
182
|
+
const exitIds = Array.from(exitElements).map((exit) => exit.id);
|
|
183
|
+
|
|
184
|
+
connections.forEach((connection) => {
|
|
185
|
+
const sourceId = connection.source.id;
|
|
186
|
+
const targetId = connection.target.id;
|
|
187
|
+
|
|
188
|
+
// Check if this connection involves the node:
|
|
189
|
+
// - Incoming: target is the node itself
|
|
190
|
+
// - Outgoing: source is one of the node's exits
|
|
191
|
+
if (targetId === nodeId || exitIds.includes(sourceId)) {
|
|
192
|
+
// Remove elevated class from the connector element
|
|
193
|
+
const connectorElement = connection.connector.canvas;
|
|
194
|
+
if (connectorElement) {
|
|
195
|
+
connectorElement.classList.remove('elevated');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
135
200
|
}
|
package/src/interfaces.ts
CHANGED
package/src/store/AppState.ts
CHANGED
|
@@ -65,6 +65,8 @@ export interface AppState {
|
|
|
65
65
|
workspace: Workspace;
|
|
66
66
|
isTranslating: boolean;
|
|
67
67
|
|
|
68
|
+
dirtyDate: Date | null;
|
|
69
|
+
|
|
68
70
|
canvasSize: { width: number; height: number };
|
|
69
71
|
|
|
70
72
|
fetchRevision: (endpoint: string, id?: string) => void;
|
|
@@ -77,10 +79,11 @@ export interface AppState {
|
|
|
77
79
|
setFlowContents: (flow: FlowContents) => void;
|
|
78
80
|
setFlowInfo: (info: FlowInfo) => void;
|
|
79
81
|
setLanguageCode: (languageCode: string) => void;
|
|
80
|
-
|
|
82
|
+
setDirtyDate: (date: Date) => void;
|
|
81
83
|
expandCanvas: (width: number, height: number) => void;
|
|
82
84
|
|
|
83
85
|
updateCanvasPositions: (positions: CanvasPositions) => void;
|
|
86
|
+
updateNodePosition(uuid: string, newPosition: FlowPosition): void;
|
|
84
87
|
removeNodes: (uuids: string[]) => void;
|
|
85
88
|
}
|
|
86
89
|
|
|
@@ -94,6 +97,13 @@ export const zustand = createStore<AppState>()(
|
|
|
94
97
|
flowDefinition: null,
|
|
95
98
|
flowInfo: null,
|
|
96
99
|
isTranslating: false,
|
|
100
|
+
dirtyDate: null,
|
|
101
|
+
|
|
102
|
+
setDirtyDate: (date: Date) => {
|
|
103
|
+
set((state: AppState) => {
|
|
104
|
+
state.dirtyDate = date;
|
|
105
|
+
});
|
|
106
|
+
},
|
|
97
107
|
|
|
98
108
|
fetchRevision: async (endpoint: string, id: string = null) => {
|
|
99
109
|
if (!id) {
|
|
@@ -178,12 +188,6 @@ export const zustand = createStore<AppState>()(
|
|
|
178
188
|
});
|
|
179
189
|
},
|
|
180
190
|
|
|
181
|
-
setTestUpdate: () => {
|
|
182
|
-
set((state: AppState) => {
|
|
183
|
-
state.flowDefinition.name = 'Bloop!';
|
|
184
|
-
});
|
|
185
|
-
},
|
|
186
|
-
|
|
187
191
|
expandCanvas: (width: number, height: number) => {
|
|
188
192
|
set((state: AppState) => {
|
|
189
193
|
const minWidth = Math.max(
|
|
@@ -211,6 +215,23 @@ export const zustand = createStore<AppState>()(
|
|
|
211
215
|
});
|
|
212
216
|
},
|
|
213
217
|
|
|
218
|
+
updateNodePosition: (uuid: string, newPosition: FlowPosition) => {
|
|
219
|
+
set((state: AppState) => {
|
|
220
|
+
if (state.flowDefinition._ui.nodes[uuid]) {
|
|
221
|
+
state.flowDefinition._ui.nodes[uuid].position = newPosition;
|
|
222
|
+
} else {
|
|
223
|
+
// If the node doesn't exist in _ui, we can add it
|
|
224
|
+
state.flowDefinition._ui.nodes[uuid] = {
|
|
225
|
+
position: newPosition,
|
|
226
|
+
type: null,
|
|
227
|
+
config: {}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
state.dirtyDate = new Date();
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
|
|
214
235
|
removeNodes: (uuids: string[]) => {
|
|
215
236
|
set((state: AppState) => {
|
|
216
237
|
for (const uuid of uuids) {
|
package/test/temba-chart.test.ts
CHANGED
|
@@ -146,6 +146,42 @@ describe('temba-chart', () => {
|
|
|
146
146
|
expect(tickCallback.call({}, 604800, 9, [])).to.equal('7d'); // 1 week in seconds
|
|
147
147
|
expect(tickCallback.call({}, 1209600, 10, [])).to.equal('14d'); // 2 weeks in seconds
|
|
148
148
|
});
|
|
149
|
+
|
|
150
|
+
it('applies xFormat when xType is time', async () => {
|
|
151
|
+
const chart: TembaChart = await getChart({
|
|
152
|
+
xType: 'time',
|
|
153
|
+
xFormat: 'DD'
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
chart.data = sampleData;
|
|
157
|
+
await chart.updateComplete;
|
|
158
|
+
|
|
159
|
+
// Wait for the chart to be created after data is set
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
161
|
+
|
|
162
|
+
expect(chart.chart).to.exist;
|
|
163
|
+
expect(chart.chart.options.scales.x.type).to.equal('time');
|
|
164
|
+
expect(
|
|
165
|
+
(chart.chart.options.scales.x as any).time.displayFormats.day
|
|
166
|
+
).to.equal('DD');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('does not include time config when xType is category', async () => {
|
|
170
|
+
const chart: TembaChart = await getChart({
|
|
171
|
+
xType: 'category',
|
|
172
|
+
xFormat: 'DD'
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
chart.data = sampleData;
|
|
176
|
+
await chart.updateComplete;
|
|
177
|
+
|
|
178
|
+
// Wait for the chart to be created after data is set
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
180
|
+
|
|
181
|
+
expect(chart.chart).to.exist;
|
|
182
|
+
expect(chart.chart.options.scales.x.type).to.equal('category');
|
|
183
|
+
expect((chart.chart.options.scales.x as any).time).to.be.undefined;
|
|
184
|
+
});
|
|
149
185
|
});
|
|
150
186
|
|
|
151
187
|
describe('formatDurationFromSeconds', () => {
|