@nyaruka/temba-components 0.125.0 → 0.126.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 +10 -0
- package/demo/data/flows/sample-flow.json +82 -228
- package/dist/temba-components.js +53 -15
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/chart/TembaChart.js +11 -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 +139 -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/package.json +1 -1
- package/src/chart/TembaChart.ts +10 -5
- package/src/flow/Editor.ts +78 -6
- package/src/flow/EditorNode.ts +163 -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/web-dev-server.config.mjs +39 -14
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { fixture, assert } from '@open-wc/testing';
|
|
2
|
+
import { html } from 'lit';
|
|
3
|
+
|
|
4
|
+
describe('temba-flow-node drag and drop functionality', () => {
|
|
5
|
+
it('should add drag styling and event listeners to elements', async () => {
|
|
6
|
+
// Create a simple div to test our drag functionality
|
|
7
|
+
const testElement = await fixture(html`
|
|
8
|
+
<div
|
|
9
|
+
class="node"
|
|
10
|
+
style="position: absolute; left: 100px; top: 100px; width: 200px; height: 100px; background: white; border: 1px solid #ccc; cursor: move;"
|
|
11
|
+
>
|
|
12
|
+
Test Node
|
|
13
|
+
<div class="exit" style="padding: 5px; background: red;">Exit</div>
|
|
14
|
+
</div>
|
|
15
|
+
`);
|
|
16
|
+
|
|
17
|
+
// Test that the node has the correct cursor style
|
|
18
|
+
const computedStyle = window.getComputedStyle(testElement);
|
|
19
|
+
assert.equal(computedStyle.cursor, 'move');
|
|
20
|
+
|
|
21
|
+
// Test drag event simulation
|
|
22
|
+
let dragStarted = false;
|
|
23
|
+
let dragEnded = false;
|
|
24
|
+
let newPosition = { left: 0, top: 0 };
|
|
25
|
+
|
|
26
|
+
// Simulate our drag implementation
|
|
27
|
+
let isDragging = false;
|
|
28
|
+
let dragStartPos = { x: 0, y: 0 };
|
|
29
|
+
let nodeStartPos = { left: 100, top: 100 };
|
|
30
|
+
|
|
31
|
+
const handleMouseDown = (event: MouseEvent) => {
|
|
32
|
+
const target = event.target as HTMLElement;
|
|
33
|
+
if (target.classList.contains('exit') || target.closest('.exit')) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
isDragging = true;
|
|
38
|
+
dragStarted = true;
|
|
39
|
+
dragStartPos = { x: event.clientX, y: event.clientY };
|
|
40
|
+
nodeStartPos = { left: 100, top: 100 };
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
event.stopPropagation();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleMouseMove = (event: MouseEvent) => {
|
|
46
|
+
if (!isDragging) return;
|
|
47
|
+
|
|
48
|
+
const deltaX = event.clientX - dragStartPos.x;
|
|
49
|
+
const deltaY = event.clientY - dragStartPos.y;
|
|
50
|
+
|
|
51
|
+
const newLeft = nodeStartPos.left + deltaX;
|
|
52
|
+
const newTop = nodeStartPos.top + deltaY;
|
|
53
|
+
|
|
54
|
+
// Update position
|
|
55
|
+
(testElement as HTMLElement).style.left = `${newLeft}px`;
|
|
56
|
+
(testElement as HTMLElement).style.top = `${newTop}px`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleMouseUp = (event: MouseEvent) => {
|
|
60
|
+
if (!isDragging) return;
|
|
61
|
+
|
|
62
|
+
isDragging = false;
|
|
63
|
+
dragEnded = true;
|
|
64
|
+
|
|
65
|
+
const deltaX = event.clientX - dragStartPos.x;
|
|
66
|
+
const deltaY = event.clientY - dragStartPos.y;
|
|
67
|
+
|
|
68
|
+
newPosition = {
|
|
69
|
+
left: nodeStartPos.left + deltaX,
|
|
70
|
+
top: nodeStartPos.top + deltaY
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Add event listeners
|
|
75
|
+
testElement.addEventListener('mousedown', handleMouseDown);
|
|
76
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
77
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
78
|
+
|
|
79
|
+
// Test drag from node (should work)
|
|
80
|
+
const mouseDownEvent = new MouseEvent('mousedown', {
|
|
81
|
+
clientX: 150,
|
|
82
|
+
clientY: 150,
|
|
83
|
+
bubbles: true
|
|
84
|
+
});
|
|
85
|
+
testElement.dispatchEvent(mouseDownEvent);
|
|
86
|
+
|
|
87
|
+
assert.isTrue(dragStarted, 'Drag should start when clicking on node');
|
|
88
|
+
|
|
89
|
+
const mouseMoveEvent = new MouseEvent('mousemove', {
|
|
90
|
+
clientX: 200,
|
|
91
|
+
clientY: 200,
|
|
92
|
+
bubbles: true
|
|
93
|
+
});
|
|
94
|
+
document.dispatchEvent(mouseMoveEvent);
|
|
95
|
+
|
|
96
|
+
const mouseUpEvent = new MouseEvent('mouseup', {
|
|
97
|
+
clientX: 200,
|
|
98
|
+
clientY: 200,
|
|
99
|
+
bubbles: true
|
|
100
|
+
});
|
|
101
|
+
document.dispatchEvent(mouseUpEvent);
|
|
102
|
+
|
|
103
|
+
assert.isTrue(dragEnded, 'Drag should end on mouse up');
|
|
104
|
+
assert.equal(
|
|
105
|
+
newPosition.left,
|
|
106
|
+
150,
|
|
107
|
+
'New left position should be calculated correctly'
|
|
108
|
+
);
|
|
109
|
+
assert.equal(
|
|
110
|
+
newPosition.top,
|
|
111
|
+
150,
|
|
112
|
+
'New top position should be calculated correctly'
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// Test that position was visually updated
|
|
116
|
+
assert.equal((testElement as HTMLElement).style.left, '150px');
|
|
117
|
+
assert.equal((testElement as HTMLElement).style.top, '150px');
|
|
118
|
+
|
|
119
|
+
// Reset for next test
|
|
120
|
+
dragStarted = false;
|
|
121
|
+
dragEnded = false;
|
|
122
|
+
|
|
123
|
+
// Test drag from exit element (should not work)
|
|
124
|
+
const exitElement = testElement.querySelector('.exit') as HTMLElement;
|
|
125
|
+
const exitMouseDownEvent = new MouseEvent('mousedown', {
|
|
126
|
+
clientX: 150,
|
|
127
|
+
clientY: 150,
|
|
128
|
+
bubbles: true
|
|
129
|
+
});
|
|
130
|
+
exitElement.dispatchEvent(exitMouseDownEvent);
|
|
131
|
+
|
|
132
|
+
assert.isFalse(
|
|
133
|
+
dragStarted,
|
|
134
|
+
'Drag should not start when clicking on exit element'
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Clean up
|
|
138
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
139
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should snap node positions to 20px grid during drag and drop', async () => {
|
|
143
|
+
// Create a test node element
|
|
144
|
+
const testElement = await fixture(html`
|
|
145
|
+
<div
|
|
146
|
+
class="node"
|
|
147
|
+
style="position: absolute; left: 100px; top: 100px; width: 200px; height: 100px; background: white; border: 1px solid #ccc; cursor: move;"
|
|
148
|
+
>
|
|
149
|
+
Test Node for Grid Snapping
|
|
150
|
+
</div>
|
|
151
|
+
`);
|
|
152
|
+
|
|
153
|
+
// Helper function to snap values to 20px grid (same as implementation)
|
|
154
|
+
const snapToGrid = (value: number): number => {
|
|
155
|
+
return Math.round(value / 20) * 20;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Simulate drag implementation with grid snapping
|
|
159
|
+
let isDragging = false;
|
|
160
|
+
let dragStartPos = { x: 0, y: 0 };
|
|
161
|
+
let nodeStartPos = { left: 100, top: 100 };
|
|
162
|
+
let finalPosition = { left: 0, top: 0 };
|
|
163
|
+
|
|
164
|
+
const handleMouseDown = (event: MouseEvent) => {
|
|
165
|
+
isDragging = true;
|
|
166
|
+
dragStartPos = { x: event.clientX, y: event.clientY };
|
|
167
|
+
// Get current position from element style
|
|
168
|
+
const currentLeft =
|
|
169
|
+
parseInt((testElement as HTMLElement).style.left) || 0;
|
|
170
|
+
const currentTop = parseInt((testElement as HTMLElement).style.top) || 0;
|
|
171
|
+
nodeStartPos = { left: currentLeft, top: currentTop };
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
event.stopPropagation();
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const handleMouseMove = (event: MouseEvent) => {
|
|
177
|
+
if (!isDragging) return;
|
|
178
|
+
|
|
179
|
+
const deltaX = event.clientX - dragStartPos.x;
|
|
180
|
+
const deltaY = event.clientY - dragStartPos.y;
|
|
181
|
+
|
|
182
|
+
const newLeft = nodeStartPos.left + deltaX;
|
|
183
|
+
const newTop = nodeStartPos.top + deltaY;
|
|
184
|
+
|
|
185
|
+
// Snap to 20px grid
|
|
186
|
+
const snappedLeft = snapToGrid(newLeft);
|
|
187
|
+
const snappedTop = snapToGrid(newTop);
|
|
188
|
+
|
|
189
|
+
// Update position with snapped values
|
|
190
|
+
(testElement as HTMLElement).style.left = `${snappedLeft}px`;
|
|
191
|
+
(testElement as HTMLElement).style.top = `${snappedTop}px`;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const handleMouseUp = (event: MouseEvent) => {
|
|
195
|
+
if (!isDragging) return;
|
|
196
|
+
isDragging = false;
|
|
197
|
+
|
|
198
|
+
const deltaX = event.clientX - dragStartPos.x;
|
|
199
|
+
const deltaY = event.clientY - dragStartPos.y;
|
|
200
|
+
|
|
201
|
+
const newLeft = nodeStartPos.left + deltaX;
|
|
202
|
+
const newTop = nodeStartPos.top + deltaY;
|
|
203
|
+
|
|
204
|
+
// Snap final position to grid
|
|
205
|
+
finalPosition = {
|
|
206
|
+
left: snapToGrid(newLeft),
|
|
207
|
+
top: snapToGrid(newTop)
|
|
208
|
+
};
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Add event listeners
|
|
212
|
+
testElement.addEventListener('mousedown', handleMouseDown);
|
|
213
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
214
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
215
|
+
|
|
216
|
+
// Test Case 1: Drag to position that should snap to grid
|
|
217
|
+
// Starting at (100, 100), drag by (33, 27) pixels
|
|
218
|
+
// Should result in (133, 127) -> snapped to (140, 120)
|
|
219
|
+
const mouseDownEvent1 = new MouseEvent('mousedown', {
|
|
220
|
+
clientX: 150,
|
|
221
|
+
clientY: 150,
|
|
222
|
+
bubbles: true
|
|
223
|
+
});
|
|
224
|
+
testElement.dispatchEvent(mouseDownEvent1);
|
|
225
|
+
|
|
226
|
+
const mouseMoveEvent1 = new MouseEvent('mousemove', {
|
|
227
|
+
clientX: 183, // 150 + 33
|
|
228
|
+
clientY: 177, // 150 + 27
|
|
229
|
+
bubbles: true
|
|
230
|
+
});
|
|
231
|
+
document.dispatchEvent(mouseMoveEvent1);
|
|
232
|
+
|
|
233
|
+
// Check that position is snapped during drag
|
|
234
|
+
assert.equal((testElement as HTMLElement).style.left, '140px');
|
|
235
|
+
assert.equal((testElement as HTMLElement).style.top, '120px');
|
|
236
|
+
|
|
237
|
+
const mouseUpEvent1 = new MouseEvent('mouseup', {
|
|
238
|
+
clientX: 183,
|
|
239
|
+
clientY: 177,
|
|
240
|
+
bubbles: true
|
|
241
|
+
});
|
|
242
|
+
document.dispatchEvent(mouseUpEvent1);
|
|
243
|
+
|
|
244
|
+
// Check final snapped position
|
|
245
|
+
assert.equal(
|
|
246
|
+
finalPosition.left,
|
|
247
|
+
140,
|
|
248
|
+
'Final left position should be snapped to 140px'
|
|
249
|
+
);
|
|
250
|
+
assert.equal(
|
|
251
|
+
finalPosition.top,
|
|
252
|
+
120,
|
|
253
|
+
'Final top position should be snapped to 120px'
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Test Case 2: Test different snap scenarios
|
|
257
|
+
// Reset position
|
|
258
|
+
nodeStartPos = { left: 60, top: 80 };
|
|
259
|
+
(testElement as HTMLElement).style.left = '60px';
|
|
260
|
+
(testElement as HTMLElement).style.top = '80px';
|
|
261
|
+
|
|
262
|
+
const mouseDownEvent2 = new MouseEvent('mousedown', {
|
|
263
|
+
clientX: 100,
|
|
264
|
+
clientY: 100,
|
|
265
|
+
bubbles: true
|
|
266
|
+
});
|
|
267
|
+
testElement.dispatchEvent(mouseDownEvent2);
|
|
268
|
+
|
|
269
|
+
// Drag by (15, 25) -> should snap to nearest 20px grid
|
|
270
|
+
// (60 + 15, 80 + 25) = (75, 105) -> snapped to (80, 100)
|
|
271
|
+
const mouseMoveEvent2 = new MouseEvent('mousemove', {
|
|
272
|
+
clientX: 115, // 100 + 15
|
|
273
|
+
clientY: 125, // 100 + 25
|
|
274
|
+
bubbles: true
|
|
275
|
+
});
|
|
276
|
+
document.dispatchEvent(mouseMoveEvent2);
|
|
277
|
+
|
|
278
|
+
assert.equal((testElement as HTMLElement).style.left, '80px');
|
|
279
|
+
assert.equal((testElement as HTMLElement).style.top, '100px');
|
|
280
|
+
|
|
281
|
+
const mouseUpEvent2 = new MouseEvent('mouseup', {
|
|
282
|
+
clientX: 115,
|
|
283
|
+
clientY: 125,
|
|
284
|
+
bubbles: true
|
|
285
|
+
});
|
|
286
|
+
document.dispatchEvent(mouseUpEvent2);
|
|
287
|
+
|
|
288
|
+
assert.equal(
|
|
289
|
+
finalPosition.left,
|
|
290
|
+
80,
|
|
291
|
+
'Final left position should be snapped to 80px'
|
|
292
|
+
);
|
|
293
|
+
assert.equal(
|
|
294
|
+
finalPosition.top,
|
|
295
|
+
100,
|
|
296
|
+
'Final top position should be snapped to 100px'
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// Test Case 3: Test exact grid positions remain unchanged
|
|
300
|
+
nodeStartPos = { left: 120, top: 160 }; // Already on grid
|
|
301
|
+
(testElement as HTMLElement).style.left = '120px';
|
|
302
|
+
(testElement as HTMLElement).style.top = '160px';
|
|
303
|
+
|
|
304
|
+
const mouseDownEvent3 = new MouseEvent('mousedown', {
|
|
305
|
+
clientX: 200,
|
|
306
|
+
clientY: 200,
|
|
307
|
+
bubbles: true
|
|
308
|
+
});
|
|
309
|
+
testElement.dispatchEvent(mouseDownEvent3);
|
|
310
|
+
|
|
311
|
+
// Small movement that should stay on same grid position
|
|
312
|
+
const mouseMoveEvent3 = new MouseEvent('mousemove', {
|
|
313
|
+
clientX: 202, // +2 pixels
|
|
314
|
+
clientY: 203, // +3 pixels
|
|
315
|
+
bubbles: true
|
|
316
|
+
});
|
|
317
|
+
document.dispatchEvent(mouseMoveEvent3);
|
|
318
|
+
|
|
319
|
+
// (120 + 2, 160 + 3) = (122, 163) -> snapped to (120, 160)
|
|
320
|
+
assert.equal((testElement as HTMLElement).style.left, '120px');
|
|
321
|
+
assert.equal((testElement as HTMLElement).style.top, '160px');
|
|
322
|
+
|
|
323
|
+
const mouseUpEvent3 = new MouseEvent('mouseup', {
|
|
324
|
+
clientX: 202,
|
|
325
|
+
clientY: 203,
|
|
326
|
+
bubbles: true
|
|
327
|
+
});
|
|
328
|
+
document.dispatchEvent(mouseUpEvent3);
|
|
329
|
+
|
|
330
|
+
assert.equal(finalPosition.left, 120, 'Position should remain on grid');
|
|
331
|
+
assert.equal(finalPosition.top, 160, 'Position should remain on grid');
|
|
332
|
+
|
|
333
|
+
// Clean up
|
|
334
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
335
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
@@ -12,21 +12,46 @@ export default {
|
|
|
12
12
|
preventAssignment: true,
|
|
13
13
|
'process.env.NODE_ENV': JSON.stringify('development'),
|
|
14
14
|
}),
|
|
15
|
-
|
|
15
|
+
{
|
|
16
|
+
name: 'flow-files',
|
|
17
|
+
serve(context) {
|
|
18
|
+
if (context.request.method === 'POST') {
|
|
19
|
+
let body = '';
|
|
20
|
+
context.req.on('data', chunk => {
|
|
21
|
+
body += chunk.toString();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
context.req.on('end', () => {
|
|
25
|
+
const parts = context.path.split('/');
|
|
26
|
+
const uuid = parts[3];
|
|
27
|
+
|
|
28
|
+
// read in the body
|
|
29
|
+
context.contentType = 'application/json';
|
|
30
|
+
if (body) {
|
|
31
|
+
fs.writeFileSync(
|
|
32
|
+
path.resolve(`./demo/data/flows/${uuid}.json`), JSON.stringify({ definition: JSON.parse(body) }, null, 2)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
context.body = {
|
|
36
|
+
status: 'success',
|
|
37
|
+
message: `Flow ${uuid} saved successfully.`,
|
|
38
|
+
};
|
|
39
|
+
} else {
|
|
40
|
+
console.log(`No body received for flow ${uuid}.`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
16
44
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
);
|
|
27
|
-
} else {
|
|
28
|
-
await next();
|
|
45
|
+
if (context.request.method === 'GET' && context.path.startsWith('/flow/revisions/')) {
|
|
46
|
+
const parts = context.path.split('/');
|
|
47
|
+
const uuid = parts[3];
|
|
48
|
+
context.contentType = 'application/json';
|
|
49
|
+
context.body = fs.readFileSync(
|
|
50
|
+
path.resolve(`./demo/data/flows/${uuid}.json`),
|
|
51
|
+
'utf-8',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
29
54
|
}
|
|
30
|
-
}
|
|
55
|
+
}
|
|
31
56
|
],
|
|
32
57
|
};
|