@flowuent-org/diagramming-core 1.3.4 → 1.3.6

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