@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,257 @@
|
|
|
1
|
+
import { fixture, assert } from '@open-wc/testing';
|
|
2
|
+
import { html } from 'lit';
|
|
3
|
+
describe('temba-flow-node drag and drop functionality', () => {
|
|
4
|
+
it('should add drag styling and event listeners to elements', async () => {
|
|
5
|
+
// Create a simple div to test our drag functionality
|
|
6
|
+
const testElement = await fixture(html `
|
|
7
|
+
<div
|
|
8
|
+
class="node"
|
|
9
|
+
style="position: absolute; left: 100px; top: 100px; width: 200px; height: 100px; background: white; border: 1px solid #ccc; cursor: move;"
|
|
10
|
+
>
|
|
11
|
+
Test Node
|
|
12
|
+
<div class="exit" style="padding: 5px; background: red;">Exit</div>
|
|
13
|
+
</div>
|
|
14
|
+
`);
|
|
15
|
+
// Test that the node has the correct cursor style
|
|
16
|
+
const computedStyle = window.getComputedStyle(testElement);
|
|
17
|
+
assert.equal(computedStyle.cursor, 'move');
|
|
18
|
+
// Test drag event simulation
|
|
19
|
+
let dragStarted = false;
|
|
20
|
+
let dragEnded = false;
|
|
21
|
+
let newPosition = { left: 0, top: 0 };
|
|
22
|
+
// Simulate our drag implementation
|
|
23
|
+
let isDragging = false;
|
|
24
|
+
let dragStartPos = { x: 0, y: 0 };
|
|
25
|
+
let nodeStartPos = { left: 100, top: 100 };
|
|
26
|
+
const handleMouseDown = (event) => {
|
|
27
|
+
const target = event.target;
|
|
28
|
+
if (target.classList.contains('exit') || target.closest('.exit')) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
isDragging = true;
|
|
32
|
+
dragStarted = true;
|
|
33
|
+
dragStartPos = { x: event.clientX, y: event.clientY };
|
|
34
|
+
nodeStartPos = { left: 100, top: 100 };
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
event.stopPropagation();
|
|
37
|
+
};
|
|
38
|
+
const handleMouseMove = (event) => {
|
|
39
|
+
if (!isDragging)
|
|
40
|
+
return;
|
|
41
|
+
const deltaX = event.clientX - dragStartPos.x;
|
|
42
|
+
const deltaY = event.clientY - dragStartPos.y;
|
|
43
|
+
const newLeft = nodeStartPos.left + deltaX;
|
|
44
|
+
const newTop = nodeStartPos.top + deltaY;
|
|
45
|
+
// Update position
|
|
46
|
+
testElement.style.left = `${newLeft}px`;
|
|
47
|
+
testElement.style.top = `${newTop}px`;
|
|
48
|
+
};
|
|
49
|
+
const handleMouseUp = (event) => {
|
|
50
|
+
if (!isDragging)
|
|
51
|
+
return;
|
|
52
|
+
isDragging = false;
|
|
53
|
+
dragEnded = true;
|
|
54
|
+
const deltaX = event.clientX - dragStartPos.x;
|
|
55
|
+
const deltaY = event.clientY - dragStartPos.y;
|
|
56
|
+
newPosition = {
|
|
57
|
+
left: nodeStartPos.left + deltaX,
|
|
58
|
+
top: nodeStartPos.top + deltaY
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
// Add event listeners
|
|
62
|
+
testElement.addEventListener('mousedown', handleMouseDown);
|
|
63
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
64
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
65
|
+
// Test drag from node (should work)
|
|
66
|
+
const mouseDownEvent = new MouseEvent('mousedown', {
|
|
67
|
+
clientX: 150,
|
|
68
|
+
clientY: 150,
|
|
69
|
+
bubbles: true
|
|
70
|
+
});
|
|
71
|
+
testElement.dispatchEvent(mouseDownEvent);
|
|
72
|
+
assert.isTrue(dragStarted, 'Drag should start when clicking on node');
|
|
73
|
+
const mouseMoveEvent = new MouseEvent('mousemove', {
|
|
74
|
+
clientX: 200,
|
|
75
|
+
clientY: 200,
|
|
76
|
+
bubbles: true
|
|
77
|
+
});
|
|
78
|
+
document.dispatchEvent(mouseMoveEvent);
|
|
79
|
+
const mouseUpEvent = new MouseEvent('mouseup', {
|
|
80
|
+
clientX: 200,
|
|
81
|
+
clientY: 200,
|
|
82
|
+
bubbles: true
|
|
83
|
+
});
|
|
84
|
+
document.dispatchEvent(mouseUpEvent);
|
|
85
|
+
assert.isTrue(dragEnded, 'Drag should end on mouse up');
|
|
86
|
+
assert.equal(newPosition.left, 150, 'New left position should be calculated correctly');
|
|
87
|
+
assert.equal(newPosition.top, 150, 'New top position should be calculated correctly');
|
|
88
|
+
// Test that position was visually updated
|
|
89
|
+
assert.equal(testElement.style.left, '150px');
|
|
90
|
+
assert.equal(testElement.style.top, '150px');
|
|
91
|
+
// Reset for next test
|
|
92
|
+
dragStarted = false;
|
|
93
|
+
dragEnded = false;
|
|
94
|
+
// Test drag from exit element (should not work)
|
|
95
|
+
const exitElement = testElement.querySelector('.exit');
|
|
96
|
+
const exitMouseDownEvent = new MouseEvent('mousedown', {
|
|
97
|
+
clientX: 150,
|
|
98
|
+
clientY: 150,
|
|
99
|
+
bubbles: true
|
|
100
|
+
});
|
|
101
|
+
exitElement.dispatchEvent(exitMouseDownEvent);
|
|
102
|
+
assert.isFalse(dragStarted, 'Drag should not start when clicking on exit element');
|
|
103
|
+
// Clean up
|
|
104
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
105
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
106
|
+
});
|
|
107
|
+
it('should snap node positions to 20px grid during drag and drop', async () => {
|
|
108
|
+
// Create a test node element
|
|
109
|
+
const testElement = await fixture(html `
|
|
110
|
+
<div
|
|
111
|
+
class="node"
|
|
112
|
+
style="position: absolute; left: 100px; top: 100px; width: 200px; height: 100px; background: white; border: 1px solid #ccc; cursor: move;"
|
|
113
|
+
>
|
|
114
|
+
Test Node for Grid Snapping
|
|
115
|
+
</div>
|
|
116
|
+
`);
|
|
117
|
+
// Helper function to snap values to 20px grid (same as implementation)
|
|
118
|
+
const snapToGrid = (value) => {
|
|
119
|
+
return Math.round(value / 20) * 20;
|
|
120
|
+
};
|
|
121
|
+
// Simulate drag implementation with grid snapping
|
|
122
|
+
let isDragging = false;
|
|
123
|
+
let dragStartPos = { x: 0, y: 0 };
|
|
124
|
+
let nodeStartPos = { left: 100, top: 100 };
|
|
125
|
+
let finalPosition = { left: 0, top: 0 };
|
|
126
|
+
const handleMouseDown = (event) => {
|
|
127
|
+
isDragging = true;
|
|
128
|
+
dragStartPos = { x: event.clientX, y: event.clientY };
|
|
129
|
+
// Get current position from element style
|
|
130
|
+
const currentLeft = parseInt(testElement.style.left) || 0;
|
|
131
|
+
const currentTop = parseInt(testElement.style.top) || 0;
|
|
132
|
+
nodeStartPos = { left: currentLeft, top: currentTop };
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
event.stopPropagation();
|
|
135
|
+
};
|
|
136
|
+
const handleMouseMove = (event) => {
|
|
137
|
+
if (!isDragging)
|
|
138
|
+
return;
|
|
139
|
+
const deltaX = event.clientX - dragStartPos.x;
|
|
140
|
+
const deltaY = event.clientY - dragStartPos.y;
|
|
141
|
+
const newLeft = nodeStartPos.left + deltaX;
|
|
142
|
+
const newTop = nodeStartPos.top + deltaY;
|
|
143
|
+
// Snap to 20px grid
|
|
144
|
+
const snappedLeft = snapToGrid(newLeft);
|
|
145
|
+
const snappedTop = snapToGrid(newTop);
|
|
146
|
+
// Update position with snapped values
|
|
147
|
+
testElement.style.left = `${snappedLeft}px`;
|
|
148
|
+
testElement.style.top = `${snappedTop}px`;
|
|
149
|
+
};
|
|
150
|
+
const handleMouseUp = (event) => {
|
|
151
|
+
if (!isDragging)
|
|
152
|
+
return;
|
|
153
|
+
isDragging = false;
|
|
154
|
+
const deltaX = event.clientX - dragStartPos.x;
|
|
155
|
+
const deltaY = event.clientY - dragStartPos.y;
|
|
156
|
+
const newLeft = nodeStartPos.left + deltaX;
|
|
157
|
+
const newTop = nodeStartPos.top + deltaY;
|
|
158
|
+
// Snap final position to grid
|
|
159
|
+
finalPosition = {
|
|
160
|
+
left: snapToGrid(newLeft),
|
|
161
|
+
top: snapToGrid(newTop)
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
// Add event listeners
|
|
165
|
+
testElement.addEventListener('mousedown', handleMouseDown);
|
|
166
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
167
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
168
|
+
// Test Case 1: Drag to position that should snap to grid
|
|
169
|
+
// Starting at (100, 100), drag by (33, 27) pixels
|
|
170
|
+
// Should result in (133, 127) -> snapped to (140, 120)
|
|
171
|
+
const mouseDownEvent1 = new MouseEvent('mousedown', {
|
|
172
|
+
clientX: 150,
|
|
173
|
+
clientY: 150,
|
|
174
|
+
bubbles: true
|
|
175
|
+
});
|
|
176
|
+
testElement.dispatchEvent(mouseDownEvent1);
|
|
177
|
+
const mouseMoveEvent1 = new MouseEvent('mousemove', {
|
|
178
|
+
clientX: 183, // 150 + 33
|
|
179
|
+
clientY: 177, // 150 + 27
|
|
180
|
+
bubbles: true
|
|
181
|
+
});
|
|
182
|
+
document.dispatchEvent(mouseMoveEvent1);
|
|
183
|
+
// Check that position is snapped during drag
|
|
184
|
+
assert.equal(testElement.style.left, '140px');
|
|
185
|
+
assert.equal(testElement.style.top, '120px');
|
|
186
|
+
const mouseUpEvent1 = new MouseEvent('mouseup', {
|
|
187
|
+
clientX: 183,
|
|
188
|
+
clientY: 177,
|
|
189
|
+
bubbles: true
|
|
190
|
+
});
|
|
191
|
+
document.dispatchEvent(mouseUpEvent1);
|
|
192
|
+
// Check final snapped position
|
|
193
|
+
assert.equal(finalPosition.left, 140, 'Final left position should be snapped to 140px');
|
|
194
|
+
assert.equal(finalPosition.top, 120, 'Final top position should be snapped to 120px');
|
|
195
|
+
// Test Case 2: Test different snap scenarios
|
|
196
|
+
// Reset position
|
|
197
|
+
nodeStartPos = { left: 60, top: 80 };
|
|
198
|
+
testElement.style.left = '60px';
|
|
199
|
+
testElement.style.top = '80px';
|
|
200
|
+
const mouseDownEvent2 = new MouseEvent('mousedown', {
|
|
201
|
+
clientX: 100,
|
|
202
|
+
clientY: 100,
|
|
203
|
+
bubbles: true
|
|
204
|
+
});
|
|
205
|
+
testElement.dispatchEvent(mouseDownEvent2);
|
|
206
|
+
// Drag by (15, 25) -> should snap to nearest 20px grid
|
|
207
|
+
// (60 + 15, 80 + 25) = (75, 105) -> snapped to (80, 100)
|
|
208
|
+
const mouseMoveEvent2 = new MouseEvent('mousemove', {
|
|
209
|
+
clientX: 115, // 100 + 15
|
|
210
|
+
clientY: 125, // 100 + 25
|
|
211
|
+
bubbles: true
|
|
212
|
+
});
|
|
213
|
+
document.dispatchEvent(mouseMoveEvent2);
|
|
214
|
+
assert.equal(testElement.style.left, '80px');
|
|
215
|
+
assert.equal(testElement.style.top, '100px');
|
|
216
|
+
const mouseUpEvent2 = new MouseEvent('mouseup', {
|
|
217
|
+
clientX: 115,
|
|
218
|
+
clientY: 125,
|
|
219
|
+
bubbles: true
|
|
220
|
+
});
|
|
221
|
+
document.dispatchEvent(mouseUpEvent2);
|
|
222
|
+
assert.equal(finalPosition.left, 80, 'Final left position should be snapped to 80px');
|
|
223
|
+
assert.equal(finalPosition.top, 100, 'Final top position should be snapped to 100px');
|
|
224
|
+
// Test Case 3: Test exact grid positions remain unchanged
|
|
225
|
+
nodeStartPos = { left: 120, top: 160 }; // Already on grid
|
|
226
|
+
testElement.style.left = '120px';
|
|
227
|
+
testElement.style.top = '160px';
|
|
228
|
+
const mouseDownEvent3 = new MouseEvent('mousedown', {
|
|
229
|
+
clientX: 200,
|
|
230
|
+
clientY: 200,
|
|
231
|
+
bubbles: true
|
|
232
|
+
});
|
|
233
|
+
testElement.dispatchEvent(mouseDownEvent3);
|
|
234
|
+
// Small movement that should stay on same grid position
|
|
235
|
+
const mouseMoveEvent3 = new MouseEvent('mousemove', {
|
|
236
|
+
clientX: 202, // +2 pixels
|
|
237
|
+
clientY: 203, // +3 pixels
|
|
238
|
+
bubbles: true
|
|
239
|
+
});
|
|
240
|
+
document.dispatchEvent(mouseMoveEvent3);
|
|
241
|
+
// (120 + 2, 160 + 3) = (122, 163) -> snapped to (120, 160)
|
|
242
|
+
assert.equal(testElement.style.left, '120px');
|
|
243
|
+
assert.equal(testElement.style.top, '160px');
|
|
244
|
+
const mouseUpEvent3 = new MouseEvent('mouseup', {
|
|
245
|
+
clientX: 202,
|
|
246
|
+
clientY: 203,
|
|
247
|
+
bubbles: true
|
|
248
|
+
});
|
|
249
|
+
document.dispatchEvent(mouseUpEvent3);
|
|
250
|
+
assert.equal(finalPosition.left, 120, 'Position should remain on grid');
|
|
251
|
+
assert.equal(finalPosition.top, 160, 'Position should remain on grid');
|
|
252
|
+
// Clean up
|
|
253
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
254
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
//# sourceMappingURL=temba-flow-node-drag.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temba-flow-node-drag.test.js","sourceRoot":"","sources":["../../test/temba-flow-node-drag.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { fixture, assert } from '@open-wc/testing';\nimport { html } from 'lit';\n\ndescribe('temba-flow-node drag and drop functionality', () => {\n it('should add drag styling and event listeners to elements', async () => {\n // Create a simple div to test our drag functionality\n const testElement = await fixture(html`\n <div\n class=\"node\"\n style=\"position: absolute; left: 100px; top: 100px; width: 200px; height: 100px; background: white; border: 1px solid #ccc; cursor: move;\"\n >\n Test Node\n <div class=\"exit\" style=\"padding: 5px; background: red;\">Exit</div>\n </div>\n `);\n\n // Test that the node has the correct cursor style\n const computedStyle = window.getComputedStyle(testElement);\n assert.equal(computedStyle.cursor, 'move');\n\n // Test drag event simulation\n let dragStarted = false;\n let dragEnded = false;\n let newPosition = { left: 0, top: 0 };\n\n // Simulate our drag implementation\n let isDragging = false;\n let dragStartPos = { x: 0, y: 0 };\n let nodeStartPos = { left: 100, top: 100 };\n\n const handleMouseDown = (event: MouseEvent) => {\n const target = event.target as HTMLElement;\n if (target.classList.contains('exit') || target.closest('.exit')) {\n return;\n }\n\n isDragging = true;\n dragStarted = true;\n dragStartPos = { x: event.clientX, y: event.clientY };\n nodeStartPos = { left: 100, top: 100 };\n event.preventDefault();\n event.stopPropagation();\n };\n\n const handleMouseMove = (event: MouseEvent) => {\n if (!isDragging) return;\n\n const deltaX = event.clientX - dragStartPos.x;\n const deltaY = event.clientY - dragStartPos.y;\n\n const newLeft = nodeStartPos.left + deltaX;\n const newTop = nodeStartPos.top + deltaY;\n\n // Update position\n (testElement as HTMLElement).style.left = `${newLeft}px`;\n (testElement as HTMLElement).style.top = `${newTop}px`;\n };\n\n const handleMouseUp = (event: MouseEvent) => {\n if (!isDragging) return;\n\n isDragging = false;\n dragEnded = true;\n\n const deltaX = event.clientX - dragStartPos.x;\n const deltaY = event.clientY - dragStartPos.y;\n\n newPosition = {\n left: nodeStartPos.left + deltaX,\n top: nodeStartPos.top + deltaY\n };\n };\n\n // Add event listeners\n testElement.addEventListener('mousedown', handleMouseDown);\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n\n // Test drag from node (should work)\n const mouseDownEvent = new MouseEvent('mousedown', {\n clientX: 150,\n clientY: 150,\n bubbles: true\n });\n testElement.dispatchEvent(mouseDownEvent);\n\n assert.isTrue(dragStarted, 'Drag should start when clicking on node');\n\n const mouseMoveEvent = new MouseEvent('mousemove', {\n clientX: 200,\n clientY: 200,\n bubbles: true\n });\n document.dispatchEvent(mouseMoveEvent);\n\n const mouseUpEvent = new MouseEvent('mouseup', {\n clientX: 200,\n clientY: 200,\n bubbles: true\n });\n document.dispatchEvent(mouseUpEvent);\n\n assert.isTrue(dragEnded, 'Drag should end on mouse up');\n assert.equal(\n newPosition.left,\n 150,\n 'New left position should be calculated correctly'\n );\n assert.equal(\n newPosition.top,\n 150,\n 'New top position should be calculated correctly'\n );\n\n // Test that position was visually updated\n assert.equal((testElement as HTMLElement).style.left, '150px');\n assert.equal((testElement as HTMLElement).style.top, '150px');\n\n // Reset for next test\n dragStarted = false;\n dragEnded = false;\n\n // Test drag from exit element (should not work)\n const exitElement = testElement.querySelector('.exit') as HTMLElement;\n const exitMouseDownEvent = new MouseEvent('mousedown', {\n clientX: 150,\n clientY: 150,\n bubbles: true\n });\n exitElement.dispatchEvent(exitMouseDownEvent);\n\n assert.isFalse(\n dragStarted,\n 'Drag should not start when clicking on exit element'\n );\n\n // Clean up\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n });\n\n it('should snap node positions to 20px grid during drag and drop', async () => {\n // Create a test node element\n const testElement = await fixture(html`\n <div\n class=\"node\"\n style=\"position: absolute; left: 100px; top: 100px; width: 200px; height: 100px; background: white; border: 1px solid #ccc; cursor: move;\"\n >\n Test Node for Grid Snapping\n </div>\n `);\n\n // Helper function to snap values to 20px grid (same as implementation)\n const snapToGrid = (value: number): number => {\n return Math.round(value / 20) * 20;\n };\n\n // Simulate drag implementation with grid snapping\n let isDragging = false;\n let dragStartPos = { x: 0, y: 0 };\n let nodeStartPos = { left: 100, top: 100 };\n let finalPosition = { left: 0, top: 0 };\n\n const handleMouseDown = (event: MouseEvent) => {\n isDragging = true;\n dragStartPos = { x: event.clientX, y: event.clientY };\n // Get current position from element style\n const currentLeft =\n parseInt((testElement as HTMLElement).style.left) || 0;\n const currentTop = parseInt((testElement as HTMLElement).style.top) || 0;\n nodeStartPos = { left: currentLeft, top: currentTop };\n event.preventDefault();\n event.stopPropagation();\n };\n\n const handleMouseMove = (event: MouseEvent) => {\n if (!isDragging) return;\n\n const deltaX = event.clientX - dragStartPos.x;\n const deltaY = event.clientY - dragStartPos.y;\n\n const newLeft = nodeStartPos.left + deltaX;\n const newTop = nodeStartPos.top + deltaY;\n\n // Snap to 20px grid\n const snappedLeft = snapToGrid(newLeft);\n const snappedTop = snapToGrid(newTop);\n\n // Update position with snapped values\n (testElement as HTMLElement).style.left = `${snappedLeft}px`;\n (testElement as HTMLElement).style.top = `${snappedTop}px`;\n };\n\n const handleMouseUp = (event: MouseEvent) => {\n if (!isDragging) return;\n isDragging = false;\n\n const deltaX = event.clientX - dragStartPos.x;\n const deltaY = event.clientY - dragStartPos.y;\n\n const newLeft = nodeStartPos.left + deltaX;\n const newTop = nodeStartPos.top + deltaY;\n\n // Snap final position to grid\n finalPosition = {\n left: snapToGrid(newLeft),\n top: snapToGrid(newTop)\n };\n };\n\n // Add event listeners\n testElement.addEventListener('mousedown', handleMouseDown);\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n\n // Test Case 1: Drag to position that should snap to grid\n // Starting at (100, 100), drag by (33, 27) pixels\n // Should result in (133, 127) -> snapped to (140, 120)\n const mouseDownEvent1 = new MouseEvent('mousedown', {\n clientX: 150,\n clientY: 150,\n bubbles: true\n });\n testElement.dispatchEvent(mouseDownEvent1);\n\n const mouseMoveEvent1 = new MouseEvent('mousemove', {\n clientX: 183, // 150 + 33\n clientY: 177, // 150 + 27\n bubbles: true\n });\n document.dispatchEvent(mouseMoveEvent1);\n\n // Check that position is snapped during drag\n assert.equal((testElement as HTMLElement).style.left, '140px');\n assert.equal((testElement as HTMLElement).style.top, '120px');\n\n const mouseUpEvent1 = new MouseEvent('mouseup', {\n clientX: 183,\n clientY: 177,\n bubbles: true\n });\n document.dispatchEvent(mouseUpEvent1);\n\n // Check final snapped position\n assert.equal(\n finalPosition.left,\n 140,\n 'Final left position should be snapped to 140px'\n );\n assert.equal(\n finalPosition.top,\n 120,\n 'Final top position should be snapped to 120px'\n );\n\n // Test Case 2: Test different snap scenarios\n // Reset position\n nodeStartPos = { left: 60, top: 80 };\n (testElement as HTMLElement).style.left = '60px';\n (testElement as HTMLElement).style.top = '80px';\n\n const mouseDownEvent2 = new MouseEvent('mousedown', {\n clientX: 100,\n clientY: 100,\n bubbles: true\n });\n testElement.dispatchEvent(mouseDownEvent2);\n\n // Drag by (15, 25) -> should snap to nearest 20px grid\n // (60 + 15, 80 + 25) = (75, 105) -> snapped to (80, 100)\n const mouseMoveEvent2 = new MouseEvent('mousemove', {\n clientX: 115, // 100 + 15\n clientY: 125, // 100 + 25\n bubbles: true\n });\n document.dispatchEvent(mouseMoveEvent2);\n\n assert.equal((testElement as HTMLElement).style.left, '80px');\n assert.equal((testElement as HTMLElement).style.top, '100px');\n\n const mouseUpEvent2 = new MouseEvent('mouseup', {\n clientX: 115,\n clientY: 125,\n bubbles: true\n });\n document.dispatchEvent(mouseUpEvent2);\n\n assert.equal(\n finalPosition.left,\n 80,\n 'Final left position should be snapped to 80px'\n );\n assert.equal(\n finalPosition.top,\n 100,\n 'Final top position should be snapped to 100px'\n );\n\n // Test Case 3: Test exact grid positions remain unchanged\n nodeStartPos = { left: 120, top: 160 }; // Already on grid\n (testElement as HTMLElement).style.left = '120px';\n (testElement as HTMLElement).style.top = '160px';\n\n const mouseDownEvent3 = new MouseEvent('mousedown', {\n clientX: 200,\n clientY: 200,\n bubbles: true\n });\n testElement.dispatchEvent(mouseDownEvent3);\n\n // Small movement that should stay on same grid position\n const mouseMoveEvent3 = new MouseEvent('mousemove', {\n clientX: 202, // +2 pixels\n clientY: 203, // +3 pixels\n bubbles: true\n });\n document.dispatchEvent(mouseMoveEvent3);\n\n // (120 + 2, 160 + 3) = (122, 163) -> snapped to (120, 160)\n assert.equal((testElement as HTMLElement).style.left, '120px');\n assert.equal((testElement as HTMLElement).style.top, '160px');\n\n const mouseUpEvent3 = new MouseEvent('mouseup', {\n clientX: 202,\n clientY: 203,\n bubbles: true\n });\n document.dispatchEvent(mouseUpEvent3);\n\n assert.equal(finalPosition.left, 120, 'Position should remain on grid');\n assert.equal(finalPosition.top, 160, 'Position should remain on grid');\n\n // Clean up\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n });\n});\n"]}
|
package/package.json
CHANGED
package/src/chart/TembaChart.ts
CHANGED
|
@@ -295,6 +295,9 @@ export class TembaChart extends RapidElement {
|
|
|
295
295
|
@property({ type: String })
|
|
296
296
|
yType: 'count' | 'duration' = 'count';
|
|
297
297
|
|
|
298
|
+
@property({ type: String })
|
|
299
|
+
xFormat: 'MMM dd' | 'DD' | 'EEE' = 'MMM dd';
|
|
300
|
+
|
|
298
301
|
@property({ type: Boolean })
|
|
299
302
|
hideOther: boolean = false;
|
|
300
303
|
|
|
@@ -674,11 +677,13 @@ export class TembaChart extends RapidElement {
|
|
|
674
677
|
type: this.xType,
|
|
675
678
|
grid: { display: false },
|
|
676
679
|
stacked: true,
|
|
677
|
-
time
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
680
|
+
...(this.xType === 'time' && {
|
|
681
|
+
time: {
|
|
682
|
+
unit: 'day',
|
|
683
|
+
tooltipFormat: 'DDD',
|
|
684
|
+
displayFormats: { day: this.xFormat }
|
|
685
|
+
}
|
|
686
|
+
})
|
|
682
687
|
}
|
|
683
688
|
}
|
|
684
689
|
}
|
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 {
|