@flowuent-org/diagramming-core 1.2.1 → 1.2.2

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.
Files changed (20) hide show
  1. package/apps/diagramming/src/AutomationDiagramData.ts +65 -1
  2. package/package.json +1 -1
  3. package/packages/diagrams/src/lib/components/CanvasSearchBar.tsx +281 -0
  4. package/packages/diagrams/src/lib/components/automation/AutomationApiNode.tsx +796 -788
  5. package/packages/diagrams/src/lib/components/automation/AutomationEndNode.tsx +608 -600
  6. package/packages/diagrams/src/lib/components/automation/AutomationFormattingNode.tsx +833 -825
  7. package/packages/diagrams/src/lib/components/automation/AutomationNavigationNode.tsx +584 -0
  8. package/packages/diagrams/src/lib/components/automation/AutomationNoteNode.tsx +422 -414
  9. package/packages/diagrams/src/lib/components/automation/AutomationSheetsNode.tsx +1120 -1112
  10. package/packages/diagrams/src/lib/components/automation/AutomationStartNode.tsx +511 -503
  11. package/packages/diagrams/src/lib/components/automation/NodeAIAssistantPopup.tsx +504 -0
  12. package/packages/diagrams/src/lib/components/automation/NodeActionButtons.tsx +146 -145
  13. package/packages/diagrams/src/lib/components/automation/index.ts +21 -12
  14. package/packages/diagrams/src/lib/contexts/SearchContext.tsx +78 -0
  15. package/packages/diagrams/src/lib/templates/DiagramContainer.tsx +79 -76
  16. package/packages/diagrams/src/lib/templates/DiagramContent.tsx +276 -61
  17. package/packages/diagrams/src/lib/types/automation-node-data-types.ts +22 -2
  18. package/packages/diagrams/src/lib/types/node-types.ts +2 -0
  19. package/packages/diagrams/src/lib/utils/highlightText.tsx +93 -0
  20. package/packages/diagrams/src/lib/utils/nodeAIAssistantConfig.ts +54 -0
@@ -0,0 +1,584 @@
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { Handle, Position, useNodeId } from '@xyflow/react';
4
+ import { Box, Typography, Chip, IconButton, Card, CardContent } from '@mui/material';
5
+ import {
6
+ AccessTime as AccessTimeIcon,
7
+ Navigation as NavigationIcon,
8
+ OpenInBrowser as OpenInBrowserIcon,
9
+ TouchApp as TouchAppIcon,
10
+ Search as SearchIcon,
11
+ } from '@mui/icons-material';
12
+ import { RiCloseLine } from 'react-icons/ri';
13
+ import ReactJson from 'react-json-view';
14
+ import { getIconByName } from '../../utils/iconMapper';
15
+ import { useTranslation } from 'react-i18next';
16
+ import { useDiagram } from '../../contexts/DiagramProvider';
17
+ import { NodeActionButtons } from './NodeActionButtons';
18
+ import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
19
+ import { useSearch } from '../../contexts/SearchContext';
20
+
21
+ interface AutomationNavigationNodeProps {
22
+ data: {
23
+ label: string;
24
+ description: string;
25
+ status: 'Ready' | 'Running' | 'Completed' | 'Error';
26
+ navigationType: 'navigate' | 'click' | 'scroll' | 'wait' | 'extract';
27
+ url?: string;
28
+ selector?: string;
29
+ action?: string;
30
+ waitTime?: number;
31
+ scrollDirection?: 'up' | 'down' | 'to-element';
32
+ extractSelector?: string;
33
+ lastRun: string;
34
+ backgroundColor: string;
35
+ textColor: string;
36
+ borderColor: string;
37
+ iconName?: string;
38
+ formData?: {
39
+ navigationType?: 'navigate' | 'click' | 'scroll' | 'wait' | 'extract';
40
+ url?: string;
41
+ selector?: string;
42
+ action?: string;
43
+ waitTime?: number;
44
+ scrollDirection?: 'up' | 'down' | 'to-element';
45
+ extractSelector?: string;
46
+ outputVariable?: string;
47
+ timeout?: number;
48
+ retryCount?: number;
49
+ [key: string]: any;
50
+ };
51
+ };
52
+ selected?: boolean;
53
+ }
54
+
55
+ export const AutomationNavigationNode: React.FC<AutomationNavigationNodeProps> = ({ data, selected }) => {
56
+ const { t } = useTranslation();
57
+ const { highlightText } = useSearch();
58
+ const [isJsonOpen, setIsJsonOpen] = useState(false);
59
+ const rootRef = useRef<any>(null);
60
+ const portalRef = useRef<HTMLDivElement | null>(null);
61
+ const nodeRef = useRef<HTMLDivElement | null>(null);
62
+ const nodeId = useNodeId();
63
+ const setSelectedNode = useDiagram((state) => state.setSelectedNode);
64
+ const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
65
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
66
+ const nodes = useDiagram((state) => state.nodes);
67
+ const setNodes = useDiagram((state) => state.setNodes);
68
+
69
+ // Get the icon component based on the iconName
70
+ const IconComponent = getIconByName(data.iconName) || NavigationIcon;
71
+
72
+ const handleJsonClick = () => {
73
+ if (nodeId) setSelectedNode(nodeId);
74
+ if (!enableJson) return;
75
+ setIsJsonOpen(!isJsonOpen);
76
+ };
77
+
78
+ const handleClose = () => {
79
+ setIsJsonOpen(false);
80
+ if (rootRef.current) {
81
+ rootRef.current.unmount();
82
+ rootRef.current = null;
83
+ }
84
+ if (portalRef.current) {
85
+ document.body.removeChild(portalRef.current);
86
+ portalRef.current = null;
87
+ }
88
+ };
89
+
90
+ useEffect(() => {
91
+ const handleClickOutside = (event: MouseEvent) => {
92
+ if (isJsonOpen && !(event.target as Element).closest('#automation-json-popover')) {
93
+ handleClose();
94
+ }
95
+ };
96
+ document.addEventListener('mousedown', handleClickOutside);
97
+ return () => {
98
+ document.removeEventListener('mousedown', handleClickOutside);
99
+ };
100
+ }, [isJsonOpen]);
101
+
102
+ useEffect(() => {
103
+ if (nodeRef.current) {
104
+ const rect = nodeRef.current.getBoundingClientRect();
105
+ const computedStyle = window.getComputedStyle(nodeRef.current);
106
+ }
107
+ }, [data.label]);
108
+
109
+ useEffect(() => {
110
+ if (isJsonOpen) {
111
+ const portalRoot = document.createElement('div');
112
+ document.body.appendChild(portalRoot);
113
+ portalRef.current = portalRoot;
114
+
115
+ const root = createRoot(portalRoot);
116
+ rootRef.current = root;
117
+
118
+ root.render(
119
+ <Card
120
+ id="automation-json-popover"
121
+ sx={{
122
+ position: 'fixed',
123
+ top: 0,
124
+ right: 0,
125
+ zIndex: 9999,
126
+ width: '400px',
127
+ height: '100vh',
128
+ overflow: 'auto',
129
+ bgcolor: '#242424',
130
+ color: '#fff',
131
+ border: '1px solid #333',
132
+ '&::-webkit-scrollbar': {
133
+ width: '6px',
134
+ },
135
+ '&::-webkit-scrollbar-track': {
136
+ background: 'transparent',
137
+ },
138
+ '&::-webkit-scrollbar-thumb': {
139
+ background: '#444',
140
+ borderRadius: '3px',
141
+ '&:hover': {
142
+ background: '#666',
143
+ },
144
+ },
145
+ }}
146
+ >
147
+ <CardContent sx={{ bgcolor: '#242424', color: '#fff' }}>
148
+ <IconButton
149
+ aria-label="close"
150
+ onClick={handleClose}
151
+ sx={{
152
+ color: '#999',
153
+ '&:hover': {
154
+ color: '#fff',
155
+ bgcolor: 'rgba(255, 255, 255, 0.1)',
156
+ },
157
+ }}
158
+ >
159
+ <RiCloseLine />
160
+ </IconButton>
161
+ {data.formData?.executionResult && (
162
+ <Box sx={{ mb: 2 }}>
163
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
164
+ {t('automation.common.executionResult')}
165
+ </Typography>
166
+ <Box sx={{
167
+ bgcolor: data.formData.executionResult.success ? '#1e3a8a' : '#dc2626',
168
+ p: 1,
169
+ borderRadius: 1,
170
+ mb: 1
171
+ }}>
172
+ <Typography variant="body2" sx={{ color: '#fff' }}>
173
+ {t('automation.common.status')}: {data.formData.executionResult.success ? t('automation.common.success') : t('automation.common.failed')}
174
+ </Typography>
175
+ <Typography variant="body2" sx={{ color: '#fff' }}>
176
+ {t('automation.common.timestamp')}: {new Date(data.formData.executionResult.timestamp).toLocaleString()}
177
+ </Typography>
178
+ {data.formData.executionResult.error && (
179
+ <Typography variant="body2" sx={{ color: '#fff' }}>
180
+ {t('automation.common.error')}: {data.formData.executionResult.error}
181
+ </Typography>
182
+ )}
183
+ </Box>
184
+ </Box>
185
+ )}
186
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
187
+ {t('automation.common.fullNodeData')}
188
+ </Typography>
189
+ <ReactJson theme={'monokai'} src={data.formData || data} collapsed={false} />
190
+ </CardContent>
191
+ </Card>
192
+ );
193
+ } else {
194
+ if (rootRef.current) {
195
+ rootRef.current.unmount();
196
+ rootRef.current = null;
197
+ }
198
+ if (portalRef.current) {
199
+ document.body.removeChild(portalRef.current);
200
+ portalRef.current = null;
201
+ }
202
+ }
203
+ }, [isJsonOpen, data]);
204
+
205
+ const getNavigationIcon = () => {
206
+ const navType = data.formData?.navigationType || data.navigationType || 'navigate';
207
+ switch (navType) {
208
+ case 'navigate':
209
+ return <OpenInBrowserIcon sx={{ fontSize: '18px' }} />;
210
+ case 'click':
211
+ return <TouchAppIcon sx={{ fontSize: '18px' }} />;
212
+ case 'scroll':
213
+ return <NavigationIcon sx={{ fontSize: '18px' }} />;
214
+ case 'wait':
215
+ return <AccessTimeIcon sx={{ fontSize: '18px' }} />;
216
+ case 'extract':
217
+ return <SearchIcon sx={{ fontSize: '18px' }} />;
218
+ default:
219
+ return <NavigationIcon sx={{ fontSize: '18px' }} />;
220
+ }
221
+ };
222
+
223
+ const getNavigationTypeLabel = () => {
224
+ const navType = data.formData?.navigationType || data.navigationType || 'navigate';
225
+ switch (navType) {
226
+ case 'navigate':
227
+ return 'Navigate to URL';
228
+ case 'click':
229
+ return 'Click Element';
230
+ case 'scroll':
231
+ return 'Scroll Page';
232
+ case 'wait':
233
+ return 'Wait';
234
+ case 'extract':
235
+ return 'Extract Data';
236
+ default:
237
+ return 'Navigation';
238
+ }
239
+ };
240
+
241
+ const getNavigationDetails = () => {
242
+ const navType = data.formData?.navigationType || data.navigationType || 'navigate';
243
+ const formData = data.formData || {};
244
+
245
+ switch (navType) {
246
+ case 'navigate':
247
+ return formData.url || data.url || 'No URL specified';
248
+ case 'click':
249
+ return formData.selector || data.selector || 'No selector specified';
250
+ case 'scroll':
251
+ return formData.scrollDirection || data.scrollDirection || 'down';
252
+ case 'wait':
253
+ return `${formData.waitTime || data.waitTime || 1000}ms`;
254
+ case 'extract':
255
+ return formData.extractSelector || data.extractSelector || 'No selector specified';
256
+ default:
257
+ return 'No details';
258
+ }
259
+ };
260
+
261
+ return (
262
+ <Box
263
+ sx={{
264
+ position: 'relative',
265
+ width: '336px',
266
+ overflow: 'visible',
267
+ }}
268
+ >
269
+ <Box
270
+ ref={nodeRef}
271
+ sx={{
272
+ width: '336px',
273
+ minHeight: '150px',
274
+ backgroundColor: '#181C25',
275
+ border: selected ? '2px solid #3b82f6' : '1px solid #1e293b',
276
+ borderRadius: '12px',
277
+ color: '#ffffff',
278
+ position: 'relative',
279
+ boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
280
+ transition: 'all 0.2s ease',
281
+ cursor: 'pointer',
282
+ overflow: 'hidden',
283
+ ...(data.status === 'Running' && {
284
+ animation: 'pulse-glow 2s ease-in-out infinite',
285
+ '@keyframes pulse-glow': {
286
+ '0%, 100%': {
287
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
288
+ borderColor: 'rgba(59, 130, 246, 0.6)',
289
+ },
290
+ '50%': {
291
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
292
+ borderColor: 'rgba(59, 130, 246, 0.9)',
293
+ },
294
+ },
295
+ }),
296
+ }}
297
+ onClick={handleJsonClick}
298
+ >
299
+ {/* Top Header Section */}
300
+ <Box sx={{
301
+ backgroundColor: "rgba(67, 93, 132, 0.1)",
302
+ padding: '8px 16px',
303
+ borderRadius: '12px 12px 0 0'
304
+ }}>
305
+ <Typography variant="body2" sx={{
306
+ color: '#ffffff',
307
+ fontSize: '12px',
308
+ fontWeight: 500
309
+ }}>
310
+ {data.formData?.description || data.description || 'Web navigation action'}
311
+ </Typography>
312
+ </Box>
313
+
314
+ {/* Main Content */}
315
+ <Box sx={{ padding: '16px' }}>
316
+ {/* Title Section */}
317
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
318
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
319
+ <Box
320
+ sx={{
321
+ width: '32px',
322
+ height: '32px',
323
+ backgroundColor: '#8b5cf6',
324
+ borderRadius: '50%',
325
+ display: 'flex',
326
+ alignItems: 'center',
327
+ justifyContent: 'center',
328
+ }}
329
+ >
330
+ {getNavigationIcon()}
331
+ </Box>
332
+ <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
333
+ {highlightText(data.label)}
334
+ </Typography>
335
+ </Box>
336
+ <Chip
337
+ label={data.status || 'Ready'}
338
+ size="small"
339
+ sx={{
340
+ backgroundColor: data.status === 'Completed'
341
+ ? 'rgba(37, 99, 235, 0.1)'
342
+ : data.status === 'Running'
343
+ ? 'rgba(251, 191, 36, 0.1)'
344
+ : data.status === 'Error'
345
+ ? 'rgba(239, 68, 68, 0.1)'
346
+ : 'rgba(16, 185, 129, 0.1)',
347
+ color: data.status === 'Completed'
348
+ ? '#93C5FD'
349
+ : data.status === 'Running'
350
+ ? '#FCD34D'
351
+ : data.status === 'Error'
352
+ ? '#FCA5A5'
353
+ : '#86EFAC',
354
+ fontWeight: 500,
355
+ fontSize: '12px',
356
+ height: '24px',
357
+ borderRadius: '12px',
358
+ }}
359
+ />
360
+ </Box>
361
+
362
+ {/* Navigation Type */}
363
+ <Box sx={{
364
+ backgroundColor: '#1F2937',
365
+ borderRadius: '8px',
366
+ padding: '12px',
367
+ mb: 2,
368
+ border: '1px solid #374151'
369
+ }}>
370
+ <Typography variant="body2" sx={{
371
+ color: '#9CA3AF',
372
+ fontSize: '11px',
373
+ mb: 0.5
374
+ }}>
375
+ {getNavigationTypeLabel()}
376
+ </Typography>
377
+ <Typography variant="body2" sx={{
378
+ color: '#ffffff',
379
+ fontSize: '12px',
380
+ fontWeight: 500
381
+ }}>
382
+ {getNavigationDetails()}
383
+ </Typography>
384
+ </Box>
385
+
386
+ {/* Description Box */}
387
+ <Box sx={{
388
+ backgroundColor: '#1F2937',
389
+ borderRadius: '8px',
390
+ padding: '12px',
391
+ mb: 2,
392
+ border: '1px solid #374151'
393
+ }}>
394
+ <Box sx={{
395
+ backgroundColor: 'transparent',
396
+ borderRadius: '4px',
397
+ padding: '8px',
398
+ border: '1px solid #4B5563',
399
+ minHeight: '40px',
400
+ display: 'flex',
401
+ alignItems: 'center'
402
+ }}>
403
+ <Typography variant="body2" sx={{
404
+ color: '#9CA3AF',
405
+ fontSize: '12px',
406
+ lineHeight: 1.4,
407
+ margin: 0
408
+ }}>
409
+ {highlightText(data.description)}
410
+ </Typography>
411
+ </Box>
412
+ </Box>
413
+
414
+ {/* Last Run Info */}
415
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 1 }}>
416
+ <AccessTimeIcon sx={{ fontSize: '14px', color: '#9CA3AF' }} />
417
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
418
+ {t('automation.common.lastRan')}: {data.lastRun}
419
+ </Typography>
420
+ </Box>
421
+ </Box>
422
+
423
+ {/* Connection Handles */}
424
+ <Handle
425
+ type="source"
426
+ position={Position.Top}
427
+ id="top-source"
428
+ className="connection-handle"
429
+ style={{
430
+ background: selected ? '#10B981' : '#1a1a2e',
431
+ width: '14px',
432
+ height: '14px',
433
+ border: '3px solid #10B981',
434
+ top: '-8px',
435
+ opacity: selected ? 1 : 0,
436
+ transition: 'all 0.2s ease-in-out',
437
+ cursor: 'crosshair',
438
+ zIndex: 10,
439
+ }}
440
+ />
441
+ <Handle
442
+ type="target"
443
+ position={Position.Top}
444
+ id="top-target"
445
+ style={{
446
+ background: 'transparent',
447
+ width: '14px',
448
+ height: '14px',
449
+ border: 'none',
450
+ top: '-8px',
451
+ opacity: 0,
452
+ pointerEvents: selected ? 'all' : 'none',
453
+ }}
454
+ />
455
+ <Handle
456
+ type="source"
457
+ position={Position.Bottom}
458
+ id="bottom-source"
459
+ className="connection-handle"
460
+ style={{
461
+ background: selected ? '#10B981' : '#1a1a2e',
462
+ width: '14px',
463
+ height: '14px',
464
+ border: '3px solid #10B981',
465
+ bottom: '-8px',
466
+ opacity: selected ? 1 : 0,
467
+ transition: 'all 0.2s ease-in-out',
468
+ cursor: 'crosshair',
469
+ zIndex: 10,
470
+ }}
471
+ />
472
+ <Handle
473
+ type="target"
474
+ position={Position.Bottom}
475
+ id="bottom-target"
476
+ style={{
477
+ background: 'transparent',
478
+ width: '14px',
479
+ height: '14px',
480
+ border: 'none',
481
+ bottom: '-8px',
482
+ opacity: 0,
483
+ pointerEvents: selected ? 'all' : 'none',
484
+ }}
485
+ />
486
+ <Handle
487
+ type="source"
488
+ position={Position.Left}
489
+ id="left-source"
490
+ className="connection-handle"
491
+ style={{
492
+ background: selected ? '#10B981' : '#1a1a2e',
493
+ width: '14px',
494
+ height: '14px',
495
+ border: '3px solid #10B981',
496
+ left: '-8px',
497
+ opacity: selected ? 1 : 0,
498
+ transition: 'all 0.2s ease-in-out',
499
+ cursor: 'crosshair',
500
+ zIndex: 10,
501
+ }}
502
+ />
503
+ <Handle
504
+ type="target"
505
+ position={Position.Left}
506
+ id="left-target"
507
+ style={{
508
+ background: 'transparent',
509
+ width: '14px',
510
+ height: '14px',
511
+ border: 'none',
512
+ left: '-8px',
513
+ opacity: 0,
514
+ pointerEvents: selected ? 'all' : 'none',
515
+ }}
516
+ />
517
+ <Handle
518
+ type="source"
519
+ position={Position.Right}
520
+ id="right-source"
521
+ className="connection-handle"
522
+ style={{
523
+ background: selected ? '#10B981' : '#1a1a2e',
524
+ width: '14px',
525
+ height: '14px',
526
+ border: '3px solid #10B981',
527
+ right: '-8px',
528
+ opacity: selected ? 1 : 0,
529
+ transition: 'all 0.2s ease-in-out',
530
+ cursor: 'crosshair',
531
+ zIndex: 10,
532
+ }}
533
+ />
534
+ <Handle
535
+ type="target"
536
+ position={Position.Right}
537
+ id="right-target"
538
+ style={{
539
+ background: 'transparent',
540
+ width: '14px',
541
+ height: '14px',
542
+ border: 'none',
543
+ right: '-8px',
544
+ opacity: 0,
545
+ pointerEvents: selected ? 'all' : 'none',
546
+ }}
547
+ />
548
+ </Box>
549
+
550
+ {/* Node Action Buttons */}
551
+ <NodeActionButtons
552
+ selected={selected}
553
+ onOpenAIAssistant={(buttonElement) => {
554
+ if (nodeId) {
555
+ showNodeAIAssistantPopup(nodeId, 'Navigation Node', buttonElement);
556
+ }
557
+ }}
558
+ onDelete={() => {
559
+ if (nodeId && onNodesChange) {
560
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
561
+ }
562
+ }}
563
+ onDuplicate={() => {
564
+ if (nodeId) {
565
+ const currentNode = nodes.find(n => n.id === nodeId);
566
+ if (currentNode) {
567
+ const newNode = {
568
+ ...currentNode,
569
+ id: `${currentNode.id}-copy-${Date.now()}`,
570
+ position: {
571
+ x: currentNode.position.x + 50,
572
+ y: currentNode.position.y + 50,
573
+ },
574
+ selected: false,
575
+ };
576
+ setNodes([...nodes, newNode]);
577
+ }
578
+ }
579
+ }}
580
+ />
581
+ </Box>
582
+ );
583
+ };
584
+