@flowuent-org/diagramming-core 1.2.0 → 1.2.1

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,509 +1,503 @@
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 { AccessTime as AccessTimeIcon } from '@mui/icons-material';
6
- import { RiCloseLine, RiUser2Line } from 'react-icons/ri';
7
- import ReactJson from 'react-json-view';
8
- import { getIconByName } from '../../utils/iconMapper';
9
- import { useTranslation } from 'react-i18next';
10
- import { useDiagram } from '../../contexts/DiagramProvider';
11
- import { NodeActionButtons } from './NodeActionButtons';
12
- import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
13
-
14
- interface AutomationStartNodeProps {
15
- data: {
16
- label: string;
17
- description: string;
18
- status: 'Ready' | 'Running' | 'Completed' | 'Error';
19
- triggerType: 'manual' | 'scheduled';
20
- scheduleConfig?: {
21
- frequency: 'hourly' | 'daily' | 'weekly';
22
- time?: string;
23
- cron?: string;
24
- };
25
- lastRun: string;
26
- backgroundColor: string;
27
- textColor: string;
28
- borderColor: string;
29
- iconName?: string; // Add iconName to the interface
30
- formData?: any; // Include formData for configuration
31
- };
32
- selected?: boolean;
33
- }
34
-
35
- export const AutomationStartNode: React.FC<AutomationStartNodeProps> = ({ data, selected }) => {
36
- const { t } = useTranslation();
37
- const [isJsonOpen, setIsJsonOpen] = useState(false);
38
- const rootRef = useRef<any>(null);
39
- const portalRef = useRef<HTMLDivElement | null>(null);
40
- const nodeRef = useRef<HTMLDivElement | null>(null);
41
- const nodeId = useNodeId();
42
- const setSelectedNode = useDiagram((state) => state.setSelectedNode);
43
- const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
44
- const onNodesChange = useDiagram((state) => state.onNodesChange);
45
- const nodes = useDiagram((state) => state.nodes);
46
- const setNodes = useDiagram((state) => state.setNodes);
47
-
48
- // Get the icon component based on the iconName
49
- const IconComponent = getIconByName(data.iconName);
50
-
51
-
52
-
53
- const handleJsonClick = () => {
54
- if (nodeId) setSelectedNode(nodeId);
55
- if (!enableJson) return;
56
- setIsJsonOpen(!isJsonOpen);
57
- };
58
-
59
- const handleClose = () => {
60
- setIsJsonOpen(false);
61
- // Clean up portal
62
- if (rootRef.current) {
63
- rootRef.current.unmount();
64
- rootRef.current = null;
65
- }
66
- if (portalRef.current) {
67
- document.body.removeChild(portalRef.current);
68
- portalRef.current = null;
69
- }
70
- };
71
-
72
- useEffect(() => {
73
- const handleClickOutside = (event: MouseEvent) => {
74
- if (isJsonOpen && !(event.target as Element).closest('#automation-json-popover')) {
75
- handleClose();
76
- }
77
- };
78
- document.addEventListener('mousedown', handleClickOutside);
79
- return () => {
80
- document.removeEventListener('mousedown', handleClickOutside);
81
- };
82
- }, [isJsonOpen]);
83
-
84
- // Debug logging for node dimensions
85
- useEffect(() => {
86
- if (nodeRef.current) {
87
- const rect = nodeRef.current.getBoundingClientRect();
88
- const computedStyle = window.getComputedStyle(nodeRef.current);
89
- // Debug information available but not logged
90
- }
91
- }, [data.label]);
92
-
93
- useEffect(() => {
94
- if (isJsonOpen) {
95
- const portalRoot = document.createElement('div');
96
- document.body.appendChild(portalRoot);
97
- portalRef.current = portalRoot;
98
-
99
- const root = createRoot(portalRoot);
100
- rootRef.current = root;
101
-
102
- root.render(
103
- <Card
104
- id="automation-json-popover"
105
- sx={{
106
- position: 'fixed',
107
- top: 0,
108
- right: 0,
109
- zIndex: 9999,
110
- width: '400px',
111
- height: '100vh',
112
- overflow: 'auto',
113
- bgcolor: '#242424',
114
- color: '#fff',
115
- border: '1px solid #333',
116
- '&::-webkit-scrollbar': {
117
- width: '6px',
118
- },
119
- '&::-webkit-scrollbar-track': {
120
- background: 'transparent',
121
- },
122
- '&::-webkit-scrollbar-thumb': {
123
- background: '#444',
124
- borderRadius: '3px',
125
- '&:hover': {
126
- background: '#666',
127
- },
128
- },
129
- }}
130
- >
131
- <CardContent sx={{ bgcolor: '#242424', color: '#fff' }}>
132
- <IconButton
133
- aria-label="close"
134
- onClick={handleClose}
135
- sx={{
136
- color: '#999',
137
- '&:hover': {
138
- color: '#fff',
139
- bgcolor: 'rgba(255, 255, 255, 0.1)',
140
- },
141
- }}
142
- >
143
- <RiCloseLine />
144
- </IconButton>
145
- {/* Show execution result prominently if available */}
146
- {data.formData?.executionResult && (
147
- <Box sx={{ mb: 2 }}>
148
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
149
- {t('automation.common.executionResult')}
150
- </Typography>
151
- <Box sx={{
152
- bgcolor: data.formData.executionResult.success ? '#1e3a8a' : '#dc2626',
153
- p: 1,
154
- borderRadius: 1,
155
- mb: 1
156
- }}>
157
- <Typography variant="body2" sx={{ color: '#fff' }}>
158
- {t('automation.common.status')}: {data.formData.executionResult.success ? t('automation.common.success') : t('automation.common.failed')}
159
- </Typography>
160
- <Typography variant="body2" sx={{ color: '#fff' }}>
161
- {t('automation.common.timestamp')}: {new Date(data.formData.executionResult.timestamp).toLocaleString()}
162
- </Typography>
163
- {data.formData.executionResult.error && (
164
- <Typography variant="body2" sx={{ color: '#fff' }}>
165
- {t('automation.common.error')}: {data.formData.executionResult.error}
166
- </Typography>
167
- )}
168
- </Box>
169
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
170
- {t('automation.startNode.initialContext')}
171
- </Typography>
172
- <ReactJson
173
- theme={'monokai'}
174
- src={data.formData.executionResult.data}
175
- collapsed={false}
176
- />
177
- </Box>
178
- )}
179
-
180
- {/* Show full node data */}
181
- <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
182
- {t('automation.common.fullNodeData')}
183
- </Typography>
184
- <ReactJson theme={'monokai'} src={data.formData || data} collapsed={false} />
185
- </CardContent>
186
- </Card>
187
- );
188
- } else {
189
- // Clean up when closing
190
- if (rootRef.current) {
191
- rootRef.current.unmount();
192
- rootRef.current = null;
193
- }
194
- if (portalRef.current) {
195
- document.body.removeChild(portalRef.current);
196
- portalRef.current = null;
197
- }
198
- }
199
- }, [isJsonOpen, data]);
200
-
201
- return (
202
- <Box
203
- sx={{
204
- position: 'relative',
205
- width: '336px',
206
- overflow: 'visible',
207
- }}
208
- >
209
- <Box
210
- ref={nodeRef}
211
- sx={{
212
- width: '336px',
213
- minHeight: '150px',
214
- backgroundColor: '#181C25', // New background color from image
215
- border: selected ? '2px solid #3b82f6' : '1px solid #1e293b',
216
- borderRadius: '12px',
217
- color: '#ffffff',
218
- position: 'relative',
219
- boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
220
- transition: 'all 0.2s ease',
221
- cursor: 'pointer',
222
- overflow: 'hidden',
223
- ...(data.status === 'Running' && {
224
- animation: 'pulse-glow 2s ease-in-out infinite',
225
- '@keyframes pulse-glow': {
226
- '0%, 100%': {
227
- boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
228
- borderColor: 'rgba(59, 130, 246, 0.6)',
229
- },
230
- '50%': {
231
- boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
232
- borderColor: 'rgba(59, 130, 246, 0.9)',
233
- },
234
- },
235
- }),
236
- }}
237
- onClick={handleJsonClick}
238
- >
239
- {/* Top Header Section */}
240
- <Box sx={{
241
- backgroundColor: "rgba(67, 93, 132, 0.1)",
242
- padding: '8px 16px',
243
- borderRadius: '12px 12px 0 0'
244
- }}>
245
- <Typography variant="body2" sx={{
246
- color: '#ffffff',
247
- fontSize: '12px',
248
- fontWeight: 500
249
- }}>
250
- {data.formData?.description || t('automation.startNode.headerDescription')}
251
- </Typography>
252
- </Box>
253
-
254
- {/* Main Content */}
255
- <Box sx={{ padding: '16px' }}>
256
- {/* Title Section */}
257
- <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
258
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
259
- <Box
260
- sx={{
261
- width: '32px',
262
- height: '32px',
263
- backgroundColor: '#0ea5e9',
264
- borderRadius: '50%',
265
- display: 'flex',
266
- alignItems: 'center',
267
- justifyContent: 'center',
268
- }}
269
- >
270
- <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
271
- </Box>
272
- <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
273
- {data.label}
274
- </Typography>
275
- </Box>
276
- <Chip
277
- label={data.status || 'Ready'}
278
- size="small"
279
- sx={{
280
- backgroundColor: data.status === 'Completed'
281
- ? 'rgba(37, 99, 235, 0.1)'
282
- : data.status === 'Running'
283
- ? 'rgba(251, 191, 36, 0.1)'
284
- : data.status === 'Error'
285
- ? 'rgba(239, 68, 68, 0.1)'
286
- : 'rgba(16, 185, 129, 0.1)',
287
- color: data.status === 'Completed'
288
- ? '#93C5FD'
289
- : data.status === 'Running'
290
- ? '#FCD34D'
291
- : data.status === 'Error'
292
- ? '#FCA5A5'
293
- : '#86EFAC',
294
- fontWeight: 500,
295
- fontSize: '12px',
296
- height: '24px',
297
- borderRadius: '12px',
298
- }}
299
- />
300
- </Box>
301
-
302
- {/* Description Box */}
303
- <Box sx={{
304
- backgroundColor: '#1F2937',
305
- borderRadius: '8px',
306
- padding: '12px',
307
- mb: 2,
308
- border: '1px solid #374151'
309
- }}>
310
- {/* Inner text boundary box */}
311
- <Box sx={{
312
- backgroundColor: 'transparent',
313
- borderRadius: '4px',
314
- padding: '8px',
315
- border: '1px solid #4B5563', // Light grey border for inner box
316
- minHeight: '40px',
317
- display: 'flex',
318
- alignItems: 'center'
319
- }}>
320
- <Typography variant="body2" sx={{
321
- color: '#9CA3AF',
322
- fontSize: '12px',
323
- lineHeight: 1.4,
324
- margin: 0
325
- }}>
326
- {data.description}
327
- </Typography>
328
- </Box>
329
- </Box>
330
-
331
- {/* Last Run Info */}
332
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 1 }}>
333
- <AccessTimeIcon sx={{ fontSize: '14px', color: '#9CA3AF' }} />
334
- <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
335
- {t('automation.common.lastRan')}: {data.lastRun}
336
- </Typography>
337
- </Box>
338
- </Box>
339
-
340
- {/* Connection Handles - Bidirectional (source + target at each position) */}
341
- {/* Top - Source */}
342
- <Handle
343
- type="source"
344
- position={Position.Top}
345
- id="top-source"
346
- className="connection-handle"
347
- style={{
348
- background: selected ? '#10B981' : '#1a1a2e',
349
- width: '14px',
350
- height: '14px',
351
- border: '3px solid #10B981',
352
- top: '-8px',
353
- opacity: selected ? 1 : 0,
354
- transition: 'all 0.2s ease-in-out',
355
- cursor: 'crosshair',
356
- zIndex: 10,
357
- }}
358
- />
359
- {/* Top - Target (hidden but functional) */}
360
- <Handle
361
- type="target"
362
- position={Position.Top}
363
- id="top-target"
364
- style={{
365
- background: 'transparent',
366
- width: '14px',
367
- height: '14px',
368
- border: 'none',
369
- top: '-8px',
370
- opacity: 0,
371
- pointerEvents: selected ? 'all' : 'none',
372
- }}
373
- />
374
- {/* Bottom - Source */}
375
- <Handle
376
- type="source"
377
- position={Position.Bottom}
378
- id="bottom-source"
379
- className="connection-handle"
380
- style={{
381
- background: selected ? '#10B981' : '#1a1a2e',
382
- width: '14px',
383
- height: '14px',
384
- border: '3px solid #10B981',
385
- bottom: '-8px',
386
- opacity: selected ? 1 : 0,
387
- transition: 'all 0.2s ease-in-out',
388
- cursor: 'crosshair',
389
- zIndex: 10,
390
- }}
391
- />
392
- {/* Bottom - Target (hidden but functional) */}
393
- <Handle
394
- type="target"
395
- position={Position.Bottom}
396
- id="bottom-target"
397
- style={{
398
- background: 'transparent',
399
- width: '14px',
400
- height: '14px',
401
- border: 'none',
402
- bottom: '-8px',
403
- opacity: 0,
404
- pointerEvents: selected ? 'all' : 'none',
405
- }}
406
- />
407
- {/* Left - Source */}
408
- <Handle
409
- type="source"
410
- position={Position.Left}
411
- id="left-source"
412
- className="connection-handle"
413
- style={{
414
- background: selected ? '#10B981' : '#1a1a2e',
415
- width: '14px',
416
- height: '14px',
417
- border: '3px solid #10B981',
418
- left: '-8px',
419
- opacity: selected ? 1 : 0,
420
- transition: 'all 0.2s ease-in-out',
421
- cursor: 'crosshair',
422
- zIndex: 10,
423
- }}
424
- />
425
- {/* Left - Target (hidden but functional) */}
426
- <Handle
427
- type="target"
428
- position={Position.Left}
429
- id="left-target"
430
- style={{
431
- background: 'transparent',
432
- width: '14px',
433
- height: '14px',
434
- border: 'none',
435
- left: '-8px',
436
- opacity: 0,
437
- pointerEvents: selected ? 'all' : 'none',
438
- }}
439
- />
440
- {/* Right - Source */}
441
- <Handle
442
- type="source"
443
- position={Position.Right}
444
- id="right-source"
445
- className="connection-handle"
446
- style={{
447
- background: selected ? '#10B981' : '#1a1a2e',
448
- width: '14px',
449
- height: '14px',
450
- border: '3px solid #10B981',
451
- right: '-8px',
452
- opacity: selected ? 1 : 0,
453
- transition: 'all 0.2s ease-in-out',
454
- cursor: 'crosshair',
455
- zIndex: 10,
456
- }}
457
- />
458
- {/* Right - Target (hidden but functional) */}
459
- <Handle
460
- type="target"
461
- position={Position.Right}
462
- id="right-target"
463
- style={{
464
- background: 'transparent',
465
- width: '14px',
466
- height: '14px',
467
- border: 'none',
468
- right: '-8px',
469
- opacity: 0,
470
- pointerEvents: selected ? 'all' : 'none',
471
- }}
472
- />
473
-
474
- </Box>
475
-
476
- {/* Node Action Buttons - Shows when selected */}
477
- <NodeActionButtons
478
- selected={selected}
479
- onOpenAIAssistant={(buttonElement) => {
480
- if (nodeId) {
481
- showNodeAIAssistantPopup(nodeId, 'Start Node', buttonElement);
482
- }
483
- }}
484
- onDelete={() => {
485
- if (nodeId && onNodesChange) {
486
- onNodesChange([{ id: nodeId, type: 'remove' }]);
487
- }
488
- }}
489
- onDuplicate={() => {
490
- if (nodeId) {
491
- const currentNode = nodes.find(n => n.id === nodeId);
492
- if (currentNode) {
493
- const newNode = {
494
- ...currentNode,
495
- id: `${currentNode.id}-copy-${Date.now()}`,
496
- position: {
497
- x: currentNode.position.x + 50,
498
- y: currentNode.position.y + 50,
499
- },
500
- selected: false,
501
- };
502
- setNodes([...nodes, newNode]);
503
- }
504
- }
505
- }}
506
- />
507
- </Box>
508
- );
509
- };
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 { AccessTime as AccessTimeIcon } from '@mui/icons-material';
6
+ import { RiCloseLine, RiUser2Line } from 'react-icons/ri';
7
+ import ReactJson from 'react-json-view';
8
+ import { getIconByName } from '../../utils/iconMapper';
9
+ import { useTranslation } from 'react-i18next';
10
+ import { useDiagram } from '../../contexts/DiagramProvider';
11
+ import { NodeActionButtons } from './NodeActionButtons';
12
+
13
+ interface AutomationStartNodeProps {
14
+ data: {
15
+ label: string;
16
+ description: string;
17
+ status: 'Ready' | 'Running' | 'Completed' | 'Error';
18
+ triggerType: 'manual' | 'scheduled';
19
+ scheduleConfig?: {
20
+ frequency: 'hourly' | 'daily' | 'weekly';
21
+ time?: string;
22
+ cron?: string;
23
+ };
24
+ lastRun: string;
25
+ backgroundColor: string;
26
+ textColor: string;
27
+ borderColor: string;
28
+ iconName?: string; // Add iconName to the interface
29
+ formData?: any; // Include formData for configuration
30
+ };
31
+ selected?: boolean;
32
+ }
33
+
34
+ export const AutomationStartNode: React.FC<AutomationStartNodeProps> = ({ data, selected }) => {
35
+ const { t } = useTranslation();
36
+ const [isJsonOpen, setIsJsonOpen] = useState(false);
37
+ const rootRef = useRef<any>(null);
38
+ const portalRef = useRef<HTMLDivElement | null>(null);
39
+ const nodeRef = useRef<HTMLDivElement | null>(null);
40
+ const nodeId = useNodeId();
41
+ const setSelectedNode = useDiagram((state) => state.setSelectedNode);
42
+ const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
43
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
44
+ const nodes = useDiagram((state) => state.nodes);
45
+ const setNodes = useDiagram((state) => state.setNodes);
46
+
47
+ // Get the icon component based on the iconName
48
+ const IconComponent = getIconByName(data.iconName);
49
+
50
+
51
+
52
+ const handleJsonClick = () => {
53
+ if (nodeId) setSelectedNode(nodeId);
54
+ if (!enableJson) return;
55
+ setIsJsonOpen(!isJsonOpen);
56
+ };
57
+
58
+ const handleClose = () => {
59
+ setIsJsonOpen(false);
60
+ // Clean up portal
61
+ if (rootRef.current) {
62
+ rootRef.current.unmount();
63
+ rootRef.current = null;
64
+ }
65
+ if (portalRef.current) {
66
+ document.body.removeChild(portalRef.current);
67
+ portalRef.current = null;
68
+ }
69
+ };
70
+
71
+ useEffect(() => {
72
+ const handleClickOutside = (event: MouseEvent) => {
73
+ if (isJsonOpen && !(event.target as Element).closest('#automation-json-popover')) {
74
+ handleClose();
75
+ }
76
+ };
77
+ document.addEventListener('mousedown', handleClickOutside);
78
+ return () => {
79
+ document.removeEventListener('mousedown', handleClickOutside);
80
+ };
81
+ }, [isJsonOpen]);
82
+
83
+ // Debug logging for node dimensions
84
+ useEffect(() => {
85
+ if (nodeRef.current) {
86
+ const rect = nodeRef.current.getBoundingClientRect();
87
+ const computedStyle = window.getComputedStyle(nodeRef.current);
88
+ // Debug information available but not logged
89
+ }
90
+ }, [data.label]);
91
+
92
+ useEffect(() => {
93
+ if (isJsonOpen) {
94
+ const portalRoot = document.createElement('div');
95
+ document.body.appendChild(portalRoot);
96
+ portalRef.current = portalRoot;
97
+
98
+ const root = createRoot(portalRoot);
99
+ rootRef.current = root;
100
+
101
+ root.render(
102
+ <Card
103
+ id="automation-json-popover"
104
+ sx={{
105
+ position: 'fixed',
106
+ top: 0,
107
+ right: 0,
108
+ zIndex: 9999,
109
+ width: '400px',
110
+ height: '100vh',
111
+ overflow: 'auto',
112
+ bgcolor: '#242424',
113
+ color: '#fff',
114
+ border: '1px solid #333',
115
+ '&::-webkit-scrollbar': {
116
+ width: '6px',
117
+ },
118
+ '&::-webkit-scrollbar-track': {
119
+ background: 'transparent',
120
+ },
121
+ '&::-webkit-scrollbar-thumb': {
122
+ background: '#444',
123
+ borderRadius: '3px',
124
+ '&:hover': {
125
+ background: '#666',
126
+ },
127
+ },
128
+ }}
129
+ >
130
+ <CardContent sx={{ bgcolor: '#242424', color: '#fff' }}>
131
+ <IconButton
132
+ aria-label="close"
133
+ onClick={handleClose}
134
+ sx={{
135
+ color: '#999',
136
+ '&:hover': {
137
+ color: '#fff',
138
+ bgcolor: 'rgba(255, 255, 255, 0.1)',
139
+ },
140
+ }}
141
+ >
142
+ <RiCloseLine />
143
+ </IconButton>
144
+ {/* Show execution result prominently if available */}
145
+ {data.formData?.executionResult && (
146
+ <Box sx={{ mb: 2 }}>
147
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
148
+ {t('automation.common.executionResult')}
149
+ </Typography>
150
+ <Box sx={{
151
+ bgcolor: data.formData.executionResult.success ? '#1e3a8a' : '#dc2626',
152
+ p: 1,
153
+ borderRadius: 1,
154
+ mb: 1
155
+ }}>
156
+ <Typography variant="body2" sx={{ color: '#fff' }}>
157
+ {t('automation.common.status')}: {data.formData.executionResult.success ? t('automation.common.success') : t('automation.common.failed')}
158
+ </Typography>
159
+ <Typography variant="body2" sx={{ color: '#fff' }}>
160
+ {t('automation.common.timestamp')}: {new Date(data.formData.executionResult.timestamp).toLocaleString()}
161
+ </Typography>
162
+ {data.formData.executionResult.error && (
163
+ <Typography variant="body2" sx={{ color: '#fff' }}>
164
+ {t('automation.common.error')}: {data.formData.executionResult.error}
165
+ </Typography>
166
+ )}
167
+ </Box>
168
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
169
+ {t('automation.startNode.initialContext')}
170
+ </Typography>
171
+ <ReactJson
172
+ theme={'monokai'}
173
+ src={data.formData.executionResult.data}
174
+ collapsed={false}
175
+ />
176
+ </Box>
177
+ )}
178
+
179
+ {/* Show full node data */}
180
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
181
+ {t('automation.common.fullNodeData')}
182
+ </Typography>
183
+ <ReactJson theme={'monokai'} src={data.formData || data} collapsed={false} />
184
+ </CardContent>
185
+ </Card>
186
+ );
187
+ } else {
188
+ // Clean up when closing
189
+ if (rootRef.current) {
190
+ rootRef.current.unmount();
191
+ rootRef.current = null;
192
+ }
193
+ if (portalRef.current) {
194
+ document.body.removeChild(portalRef.current);
195
+ portalRef.current = null;
196
+ }
197
+ }
198
+ }, [isJsonOpen, data]);
199
+
200
+ return (
201
+ <Box
202
+ sx={{
203
+ position: 'relative',
204
+ width: '336px',
205
+ overflow: 'visible',
206
+ }}
207
+ >
208
+ <Box
209
+ ref={nodeRef}
210
+ sx={{
211
+ width: '336px',
212
+ minHeight: '150px',
213
+ backgroundColor: '#181C25', // New background color from image
214
+ border: selected ? '2px solid #3b82f6' : '1px solid #1e293b',
215
+ borderRadius: '12px',
216
+ color: '#ffffff',
217
+ position: 'relative',
218
+ boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
219
+ transition: 'all 0.2s ease',
220
+ cursor: 'pointer',
221
+ overflow: 'hidden',
222
+ ...(data.status === 'Running' && {
223
+ animation: 'pulse-glow 2s ease-in-out infinite',
224
+ '@keyframes pulse-glow': {
225
+ '0%, 100%': {
226
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
227
+ borderColor: 'rgba(59, 130, 246, 0.6)',
228
+ },
229
+ '50%': {
230
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
231
+ borderColor: 'rgba(59, 130, 246, 0.9)',
232
+ },
233
+ },
234
+ }),
235
+ }}
236
+ onClick={handleJsonClick}
237
+ >
238
+ {/* Top Header Section */}
239
+ <Box sx={{
240
+ backgroundColor: "rgba(67, 93, 132, 0.1)",
241
+ padding: '8px 16px',
242
+ borderRadius: '12px 12px 0 0'
243
+ }}>
244
+ <Typography variant="body2" sx={{
245
+ color: '#ffffff',
246
+ fontSize: '12px',
247
+ fontWeight: 500
248
+ }}>
249
+ {data.formData?.description || t('automation.startNode.headerDescription')}
250
+ </Typography>
251
+ </Box>
252
+
253
+ {/* Main Content */}
254
+ <Box sx={{ padding: '16px' }}>
255
+ {/* Title Section */}
256
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
257
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
258
+ <Box
259
+ sx={{
260
+ width: '32px',
261
+ height: '32px',
262
+ backgroundColor: '#0ea5e9',
263
+ borderRadius: '50%',
264
+ display: 'flex',
265
+ alignItems: 'center',
266
+ justifyContent: 'center',
267
+ }}
268
+ >
269
+ <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
270
+ </Box>
271
+ <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
272
+ {data.label}
273
+ </Typography>
274
+ </Box>
275
+ <Chip
276
+ label={data.status || 'Ready'}
277
+ size="small"
278
+ sx={{
279
+ backgroundColor: data.status === 'Completed'
280
+ ? 'rgba(37, 99, 235, 0.1)'
281
+ : data.status === 'Running'
282
+ ? 'rgba(251, 191, 36, 0.1)'
283
+ : data.status === 'Error'
284
+ ? 'rgba(239, 68, 68, 0.1)'
285
+ : 'rgba(16, 185, 129, 0.1)',
286
+ color: data.status === 'Completed'
287
+ ? '#93C5FD'
288
+ : data.status === 'Running'
289
+ ? '#FCD34D'
290
+ : data.status === 'Error'
291
+ ? '#FCA5A5'
292
+ : '#86EFAC',
293
+ fontWeight: 500,
294
+ fontSize: '12px',
295
+ height: '24px',
296
+ borderRadius: '12px',
297
+ }}
298
+ />
299
+ </Box>
300
+
301
+ {/* Description Box */}
302
+ <Box sx={{
303
+ backgroundColor: '#1F2937',
304
+ borderRadius: '8px',
305
+ padding: '12px',
306
+ mb: 2,
307
+ border: '1px solid #374151'
308
+ }}>
309
+ {/* Inner text boundary box */}
310
+ <Box sx={{
311
+ backgroundColor: 'transparent',
312
+ borderRadius: '4px',
313
+ padding: '8px',
314
+ border: '1px solid #4B5563', // Light grey border for inner box
315
+ minHeight: '40px',
316
+ display: 'flex',
317
+ alignItems: 'center'
318
+ }}>
319
+ <Typography variant="body2" sx={{
320
+ color: '#9CA3AF',
321
+ fontSize: '12px',
322
+ lineHeight: 1.4,
323
+ margin: 0
324
+ }}>
325
+ {data.description}
326
+ </Typography>
327
+ </Box>
328
+ </Box>
329
+
330
+ {/* Last Run Info */}
331
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 1 }}>
332
+ <AccessTimeIcon sx={{ fontSize: '14px', color: '#9CA3AF' }} />
333
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
334
+ {t('automation.common.lastRan')}: {data.lastRun}
335
+ </Typography>
336
+ </Box>
337
+ </Box>
338
+
339
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
340
+ {/* Top - Source */}
341
+ <Handle
342
+ type="source"
343
+ position={Position.Top}
344
+ id="top-source"
345
+ className="connection-handle"
346
+ style={{
347
+ background: selected ? '#10B981' : '#1a1a2e',
348
+ width: '14px',
349
+ height: '14px',
350
+ border: '3px solid #10B981',
351
+ top: '-8px',
352
+ opacity: selected ? 1 : 0,
353
+ transition: 'all 0.2s ease-in-out',
354
+ cursor: 'crosshair',
355
+ zIndex: 10,
356
+ }}
357
+ />
358
+ {/* Top - Target (hidden but functional) */}
359
+ <Handle
360
+ type="target"
361
+ position={Position.Top}
362
+ id="top-target"
363
+ style={{
364
+ background: 'transparent',
365
+ width: '14px',
366
+ height: '14px',
367
+ border: 'none',
368
+ top: '-8px',
369
+ opacity: 0,
370
+ pointerEvents: selected ? 'all' : 'none',
371
+ }}
372
+ />
373
+ {/* Bottom - Source */}
374
+ <Handle
375
+ type="source"
376
+ position={Position.Bottom}
377
+ id="bottom-source"
378
+ className="connection-handle"
379
+ style={{
380
+ background: selected ? '#10B981' : '#1a1a2e',
381
+ width: '14px',
382
+ height: '14px',
383
+ border: '3px solid #10B981',
384
+ bottom: '-8px',
385
+ opacity: selected ? 1 : 0,
386
+ transition: 'all 0.2s ease-in-out',
387
+ cursor: 'crosshair',
388
+ zIndex: 10,
389
+ }}
390
+ />
391
+ {/* Bottom - Target (hidden but functional) */}
392
+ <Handle
393
+ type="target"
394
+ position={Position.Bottom}
395
+ id="bottom-target"
396
+ style={{
397
+ background: 'transparent',
398
+ width: '14px',
399
+ height: '14px',
400
+ border: 'none',
401
+ bottom: '-8px',
402
+ opacity: 0,
403
+ pointerEvents: selected ? 'all' : 'none',
404
+ }}
405
+ />
406
+ {/* Left - Source */}
407
+ <Handle
408
+ type="source"
409
+ position={Position.Left}
410
+ id="left-source"
411
+ className="connection-handle"
412
+ style={{
413
+ background: selected ? '#10B981' : '#1a1a2e',
414
+ width: '14px',
415
+ height: '14px',
416
+ border: '3px solid #10B981',
417
+ left: '-8px',
418
+ opacity: selected ? 1 : 0,
419
+ transition: 'all 0.2s ease-in-out',
420
+ cursor: 'crosshair',
421
+ zIndex: 10,
422
+ }}
423
+ />
424
+ {/* Left - Target (hidden but functional) */}
425
+ <Handle
426
+ type="target"
427
+ position={Position.Left}
428
+ id="left-target"
429
+ style={{
430
+ background: 'transparent',
431
+ width: '14px',
432
+ height: '14px',
433
+ border: 'none',
434
+ left: '-8px',
435
+ opacity: 0,
436
+ pointerEvents: selected ? 'all' : 'none',
437
+ }}
438
+ />
439
+ {/* Right - Source */}
440
+ <Handle
441
+ type="source"
442
+ position={Position.Right}
443
+ id="right-source"
444
+ className="connection-handle"
445
+ style={{
446
+ background: selected ? '#10B981' : '#1a1a2e',
447
+ width: '14px',
448
+ height: '14px',
449
+ border: '3px solid #10B981',
450
+ right: '-8px',
451
+ opacity: selected ? 1 : 0,
452
+ transition: 'all 0.2s ease-in-out',
453
+ cursor: 'crosshair',
454
+ zIndex: 10,
455
+ }}
456
+ />
457
+ {/* Right - Target (hidden but functional) */}
458
+ <Handle
459
+ type="target"
460
+ position={Position.Right}
461
+ id="right-target"
462
+ style={{
463
+ background: 'transparent',
464
+ width: '14px',
465
+ height: '14px',
466
+ border: 'none',
467
+ right: '-8px',
468
+ opacity: 0,
469
+ pointerEvents: selected ? 'all' : 'none',
470
+ }}
471
+ />
472
+
473
+ </Box>
474
+
475
+ {/* Node Action Buttons - Shows when selected */}
476
+ <NodeActionButtons
477
+ selected={selected}
478
+ onDelete={() => {
479
+ if (nodeId && onNodesChange) {
480
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
481
+ }
482
+ }}
483
+ onDuplicate={() => {
484
+ if (nodeId) {
485
+ const currentNode = nodes.find(n => n.id === nodeId);
486
+ if (currentNode) {
487
+ const newNode = {
488
+ ...currentNode,
489
+ id: `${currentNode.id}-copy-${Date.now()}`,
490
+ position: {
491
+ x: currentNode.position.x + 50,
492
+ y: currentNode.position.y + 50,
493
+ },
494
+ selected: false,
495
+ };
496
+ setNodes([...nodes, newNode]);
497
+ }
498
+ }
499
+ }}
500
+ />
501
+ </Box>
502
+ );
503
+ };