@flowuent-org/diagramming-core 1.3.9 → 1.3.10

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.
@@ -749,6 +749,52 @@ export const automationDefaultNodes = [
749
749
  measured: { width: 300, height: 200 },
750
750
  },
751
751
  // =====================================
752
+ // Email Node
753
+ // =====================================
754
+ {
755
+ id: 'email-node',
756
+ type: 'AutomationEmailNode',
757
+ position: { x: 2200, y: 450 },
758
+ data: {
759
+ label: 'Send Email',
760
+ description: 'Send an email notification',
761
+ operationType: 'send-email',
762
+ status: 'Ready',
763
+ parameters: {
764
+ to: ['recipient@example.com'],
765
+ cc: [],
766
+ bcc: [],
767
+ subject: 'Workflow Notification',
768
+ body: 'Your workflow has completed successfully!',
769
+ htmlBody: '',
770
+ attachments: [],
771
+ emailAccessToken: '',
772
+ emailTokenExpiresAt: undefined,
773
+ emailProvider: 'smtp',
774
+ emailAddress: '',
775
+ },
776
+ emailAuth: {
777
+ provider: 'smtp',
778
+ isAuthenticated: false,
779
+ isLoading: false,
780
+ email: '',
781
+ },
782
+ formData: {
783
+ nodeId: 'email-node',
784
+ title: 'Send Email',
785
+ type: 'email',
786
+ operationType: 'send-email',
787
+ to: ['recipient@example.com'],
788
+ subject: 'Workflow Notification',
789
+ body: 'Your workflow has completed successfully!',
790
+ },
791
+ lastRun: 'Never',
792
+ },
793
+ width: 300,
794
+ height: 200,
795
+ measured: { width: 300, height: 200 },
796
+ },
797
+ // =====================================
752
798
  // Interaction Node
753
799
  // =====================================
754
800
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowuent-org/diagramming-core",
3
- "version": "1.3.9",
3
+ "version": "1.3.10",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,773 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { Handle, Position, useNodeId } from '@xyflow/react';
4
+ import {
5
+ Box,
6
+ Typography,
7
+ Chip,
8
+ IconButton,
9
+ Card,
10
+ CardContent,
11
+ Button,
12
+ CircularProgress,
13
+ Tooltip,
14
+ LinearProgress,
15
+ } from '@mui/material';
16
+ import {
17
+ Email as EmailIcon,
18
+ Send as SendIcon,
19
+ AttachFile as AttachFileIcon,
20
+ AccessTime as AccessTimeIcon,
21
+ } from '@mui/icons-material';
22
+ import { RiCloseLine } from 'react-icons/ri';
23
+ import ReactJson from 'react-json-view';
24
+ import { useTranslation } from 'react-i18next';
25
+ import { useDiagram } from '../../contexts/DiagramProvider';
26
+ import { useSearch } from '../../contexts/SearchContext';
27
+ import { NodeActionButtons } from './NodeActionButtons';
28
+ import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
29
+ import { getStatusColor } from './statusColors';
30
+
31
+ // ========================
32
+ // Types
33
+ // ========================
34
+
35
+ export type EmailOperationType =
36
+ | 'send-email'
37
+ | 'send-with-attachment'
38
+ | 'send-template'
39
+ | 'send-bulk'
40
+ | 'reply-to-email'
41
+ | 'forward-email';
42
+
43
+ export interface EmailTokenData {
44
+ accessToken: string;
45
+ expiresAt?: number;
46
+ email?: string;
47
+ provider?: 'smtp' | 'gmail' | 'outlook' | 'sendgrid' | 'mailgun';
48
+ }
49
+
50
+ export interface AutomationEmailNodeData {
51
+ label: string;
52
+ description: string;
53
+ operationType?: EmailOperationType;
54
+ status: 'Ready' | 'Running' | 'Completed' | 'Failed' | 'Need to Config' | 'authenticated';
55
+ parameters?: {
56
+ // Email operations
57
+ to?: string[];
58
+ cc?: string[];
59
+ bcc?: string[];
60
+ subject?: string;
61
+ body?: string;
62
+ htmlBody?: string;
63
+ attachments?: Array<{
64
+ filename: string;
65
+ content: string;
66
+ contentType?: string;
67
+ }>;
68
+
69
+ // Template operations
70
+ templateId?: string;
71
+ templateVariables?: Record<string, any>;
72
+
73
+ // Bulk operations
74
+ recipients?: Array<{
75
+ email: string;
76
+ variables?: Record<string, any>;
77
+ }>;
78
+
79
+ // Reply/Forward operations
80
+ replyTo?: string;
81
+ inReplyTo?: string;
82
+ forwardTo?: string[];
83
+
84
+ // Token data
85
+ emailAccessToken?: string;
86
+ emailTokenExpiresAt?: number;
87
+ emailProvider?: string;
88
+ emailAddress?: string;
89
+
90
+ [key: string]: any;
91
+ };
92
+ emailAuth?: {
93
+ provider?: string;
94
+ isAuthenticated?: boolean;
95
+ isLoading?: boolean;
96
+ email?: string;
97
+ };
98
+ formData?: {
99
+ nodeId?: string;
100
+ title?: string;
101
+ type?: string;
102
+ operationType?: string;
103
+ [key: string]: any;
104
+ };
105
+ lastRun?: string;
106
+ duration?: string;
107
+ executionResult?: {
108
+ success: boolean;
109
+ data?: any;
110
+ error?: string;
111
+ };
112
+ onEmailLogin?: () => void;
113
+ onEmailDisconnect?: () => void;
114
+ }
115
+
116
+ export interface AutomationEmailNodeProps {
117
+ data: AutomationEmailNodeData;
118
+ selected?: boolean;
119
+ }
120
+
121
+ // ========================
122
+ // Operation Configuration
123
+ // ========================
124
+
125
+ const OPERATION_CONFIG: Record<
126
+ EmailOperationType,
127
+ {
128
+ icon: React.ElementType;
129
+ label: string;
130
+ color: string;
131
+ description: string;
132
+ }
133
+ > = {
134
+ 'send-email': {
135
+ icon: SendIcon,
136
+ label: 'Send Email',
137
+ color: '#4285F4',
138
+ description: 'Send a simple email',
139
+ },
140
+ 'send-with-attachment': {
141
+ icon: AttachFileIcon,
142
+ label: 'Send with Attachment',
143
+ color: '#34A853',
144
+ description: 'Send an email with file attachments',
145
+ },
146
+ 'send-template': {
147
+ icon: EmailIcon,
148
+ label: 'Send Template',
149
+ color: '#FBBC04',
150
+ description: 'Send an email using a template',
151
+ },
152
+ 'send-bulk': {
153
+ icon: EmailIcon,
154
+ label: 'Send Bulk',
155
+ color: '#EA4335',
156
+ description: 'Send bulk emails to multiple recipients',
157
+ },
158
+ 'reply-to-email': {
159
+ icon: EmailIcon,
160
+ label: 'Reply to Email',
161
+ color: '#4285F4',
162
+ description: 'Reply to an existing email',
163
+ },
164
+ 'forward-email': {
165
+ icon: EmailIcon,
166
+ label: 'Forward Email',
167
+ color: '#34A853',
168
+ description: 'Forward an email to recipients',
169
+ },
170
+ };
171
+
172
+ // ========================
173
+ // Helper Functions
174
+ // ========================
175
+
176
+ const isTokenExpired = (expiresAt?: number): boolean => {
177
+ if (!expiresAt) return false;
178
+ return Date.now() > expiresAt;
179
+ };
180
+
181
+ const getTimeUntilExpiry = (expiresAt?: number): string => {
182
+ if (!expiresAt) return 'No expiry';
183
+ const diff = expiresAt - Date.now();
184
+ if (diff <= 0) return 'Expired';
185
+ const minutes = Math.floor(diff / 60000);
186
+ if (minutes < 60) return `${minutes}m`;
187
+ const hours = Math.floor(minutes / 60);
188
+ return `${hours}h ${minutes % 60}m`;
189
+ };
190
+
191
+ // ========================
192
+ // Main Component
193
+ // ========================
194
+
195
+ export const AutomationEmailNode: React.FC<AutomationEmailNodeProps> = ({
196
+ data,
197
+ selected,
198
+ }) => {
199
+ const { t } = useTranslation();
200
+ const { highlightText } = useSearch();
201
+ const [isJsonOpen, setIsJsonOpen] = useState(false);
202
+ const rootRef = useRef<any>(null);
203
+ const portalRef = useRef<HTMLDivElement | null>(null);
204
+ const nodeRef = useRef<HTMLDivElement | null>(null);
205
+ const nodeId = useNodeId();
206
+ const setSelectedNode = useDiagram((state) => state.setSelectedNode);
207
+ const enableJson = useDiagram((state) => state.enableNodeJsonPopover ?? true);
208
+ const onNodesChange = useDiagram((state) => state.onNodesChange);
209
+ const nodes = useDiagram((state) => state.nodes);
210
+ const setNodes = useDiagram((state) => state.setNodes);
211
+
212
+ // Get operation configuration
213
+ const operationTypeRaw = data.operationType || data.formData?.operationType || 'send-email';
214
+ const operationType = (Object.keys(OPERATION_CONFIG).includes(operationTypeRaw) ? operationTypeRaw : 'send-email') as EmailOperationType;
215
+ const operationConfig = OPERATION_CONFIG[operationType];
216
+ const OperationIcon = operationConfig.icon;
217
+
218
+ // Auth state
219
+ const isAuthenticated = data.emailAuth?.isAuthenticated || !!data.parameters?.emailAccessToken;
220
+ const isAuthLoading = data.emailAuth?.isLoading || false;
221
+ const emailAddress = data.emailAuth?.email || data.parameters?.emailAddress;
222
+ const tokenExpired = isTokenExpired(data.parameters?.emailTokenExpiresAt);
223
+
224
+ // Execution state
225
+ const status = data.status || 'Ready';
226
+ const executionProgress = status === 'Running' ? 50 : 0;
227
+
228
+ // Status configuration - using centralized status colors
229
+ const statusConfig = getStatusColor(status, status === 'authenticated' ? 'authenticated' : 'ready');
230
+
231
+ // Handle JSON view
232
+ const handleJsonClick = () => {
233
+ if (nodeId) setSelectedNode(nodeId);
234
+ if (!enableJson) return;
235
+ setIsJsonOpen(!isJsonOpen);
236
+ };
237
+
238
+ const handleClose = () => {
239
+ setIsJsonOpen(false);
240
+ // Clean up portal
241
+ if (rootRef.current) {
242
+ rootRef.current.unmount();
243
+ rootRef.current = null;
244
+ }
245
+ if (portalRef.current) {
246
+ document.body.removeChild(portalRef.current);
247
+ portalRef.current = null;
248
+ }
249
+ };
250
+
251
+ // Render email auth button
252
+ const renderEmailAuthButton = () => {
253
+ if (isAuthenticated && !tokenExpired) {
254
+ return (
255
+ <Button
256
+ onClick={data.onEmailDisconnect}
257
+ disabled={isAuthLoading}
258
+ fullWidth
259
+ sx={{
260
+ backgroundColor: '#4285F4',
261
+ borderRadius: '8px',
262
+ padding: '8px 12px',
263
+ gap: '8px',
264
+ color: '#FFFFFF',
265
+ textTransform: 'none',
266
+ fontSize: '13px',
267
+ fontWeight: 500,
268
+ '&:hover': {
269
+ backgroundColor: '#357ae8',
270
+ },
271
+ }}
272
+ >
273
+ <EmailIcon sx={{ fontSize: 18 }} />
274
+ {emailAddress ? emailAddress : 'Connected'}
275
+ </Button>
276
+ );
277
+ }
278
+
279
+ return (
280
+ <Button
281
+ onClick={data.onEmailLogin}
282
+ disabled={isAuthLoading}
283
+ fullWidth
284
+ sx={{
285
+ backgroundColor: '#171C29',
286
+ border: '1px solid #374151',
287
+ borderRadius: '8px',
288
+ padding: '8px 12px',
289
+ gap: '8px',
290
+ color: '#B2BCD8',
291
+ textTransform: 'none',
292
+ fontSize: '13px',
293
+ fontWeight: 500,
294
+ '&:hover': {
295
+ backgroundColor: '#1F2937',
296
+ borderColor: '#4285F4',
297
+ },
298
+ '&:disabled': {
299
+ opacity: 0.6,
300
+ },
301
+ }}
302
+ >
303
+ {isAuthLoading ? (
304
+ <>
305
+ <CircularProgress size={16} sx={{ color: '#B2BCD8' }} />
306
+ Connecting...
307
+ </>
308
+ ) : (
309
+ <>
310
+ <EmailIcon sx={{ fontSize: 18 }} />
311
+ Connect Email
312
+ </>
313
+ )}
314
+ </Button>
315
+ );
316
+ };
317
+
318
+ // Render status badge
319
+ const renderStatusBadge = () => {
320
+ return (
321
+ <Chip
322
+ label={status}
323
+ size="small"
324
+ sx={{
325
+ backgroundColor: statusConfig.bgColor,
326
+ color: statusConfig.color,
327
+ fontWeight: 500,
328
+ fontSize: '11px',
329
+ height: '22px',
330
+ borderRadius: '11px',
331
+ }}
332
+ />
333
+ );
334
+ };
335
+
336
+ useEffect(() => {
337
+ const handleClickOutside = (event: MouseEvent) => {
338
+ if (isJsonOpen && !(event.target as Element).closest('#automation-json-popover')) {
339
+ handleClose();
340
+ }
341
+ };
342
+ document.addEventListener('mousedown', handleClickOutside);
343
+ return () => {
344
+ document.removeEventListener('mousedown', handleClickOutside);
345
+ };
346
+ }, [isJsonOpen]);
347
+
348
+ useEffect(() => {
349
+ if (isJsonOpen) {
350
+ const portalRoot = document.createElement('div');
351
+ document.body.appendChild(portalRoot);
352
+ portalRef.current = portalRoot;
353
+
354
+ const root = createRoot(portalRoot);
355
+ rootRef.current = root;
356
+
357
+ root.render(
358
+ <Card
359
+ id="automation-json-popover"
360
+ sx={{
361
+ position: 'fixed',
362
+ top: 0,
363
+ right: 0,
364
+ zIndex: 9999,
365
+ width: '400px',
366
+ height: '100vh',
367
+ overflow: 'auto',
368
+ bgcolor: '#242424',
369
+ color: '#fff',
370
+ border: '1px solid #333',
371
+ '&::-webkit-scrollbar': {
372
+ width: '6px',
373
+ },
374
+ '&::-webkit-scrollbar-track': {
375
+ background: 'transparent',
376
+ },
377
+ '&::-webkit-scrollbar-thumb': {
378
+ background: '#444',
379
+ borderRadius: '3px',
380
+ '&:hover': {
381
+ background: '#666',
382
+ },
383
+ },
384
+ }}
385
+ >
386
+ <CardContent sx={{ bgcolor: '#242424', color: '#fff' }}>
387
+ <IconButton
388
+ aria-label="close"
389
+ onClick={handleClose}
390
+ sx={{
391
+ color: '#999',
392
+ '&:hover': {
393
+ color: '#fff',
394
+ bgcolor: 'rgba(255, 255, 255, 0.1)',
395
+ },
396
+ }}
397
+ >
398
+ <RiCloseLine />
399
+ </IconButton>
400
+ {/* Show execution result prominently if available */}
401
+ {data.formData?.executionResult && (
402
+ <Box sx={{ mb: 2 }}>
403
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
404
+ {t('automation.common.executionResult')}
405
+ </Typography>
406
+ <Box sx={{
407
+ bgcolor: data.formData.executionResult.success ? '#1e3a8a' : '#dc2626',
408
+ p: 1,
409
+ borderRadius: 1,
410
+ mb: 1
411
+ }}>
412
+ <Typography variant="body2" sx={{ color: '#fff' }}>
413
+ {t('automation.common.status')}: {data.formData.executionResult.success ? t('automation.common.success') : t('automation.common.failed')}
414
+ </Typography>
415
+ <Typography variant="body2" sx={{ color: '#fff' }}>
416
+ {t('automation.common.timestamp')}: {new Date(data.formData.executionResult.timestamp).toLocaleString()}
417
+ </Typography>
418
+ {data.formData.executionResult.error && (
419
+ <Typography variant="body2" sx={{ color: '#fff' }}>
420
+ {t('automation.common.error')}: {data.formData.executionResult.error}
421
+ </Typography>
422
+ )}
423
+ </Box>
424
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
425
+ {t('automation.emailNode.executionData')}
426
+ </Typography>
427
+ <ReactJson
428
+ theme={'monokai'}
429
+ src={data.formData.executionResult.data}
430
+ collapsed={false}
431
+ />
432
+ </Box>
433
+ )}
434
+
435
+ {/* Show full node data */}
436
+ <Typography variant="h6" sx={{ color: '#fff', mb: 1 }}>
437
+ {t('automation.common.fullNodeData')}
438
+ </Typography>
439
+ <ReactJson theme={'monokai'} src={data.formData || data} collapsed={false} />
440
+ </CardContent>
441
+ </Card>
442
+ );
443
+ } else {
444
+ // Clean up when closing
445
+ if (rootRef.current) {
446
+ rootRef.current.unmount();
447
+ rootRef.current = null;
448
+ }
449
+ if (portalRef.current) {
450
+ document.body.removeChild(portalRef.current);
451
+ portalRef.current = null;
452
+ }
453
+ }
454
+ }, [isJsonOpen, data]);
455
+
456
+ // ========================
457
+ // Render
458
+ // ========================
459
+ return (
460
+ <Box
461
+ sx={{
462
+ position: 'relative',
463
+ width: '336px',
464
+ overflow: 'visible',
465
+ }}
466
+ >
467
+ <Box
468
+ ref={nodeRef}
469
+ sx={{
470
+ width: '336px',
471
+ minHeight: '150px',
472
+ backgroundColor: '#181C25',
473
+ border: selected ? '2px solid #3b82f6' : '1px solid #1e293b',
474
+ borderRadius: '12px',
475
+ color: '#ffffff',
476
+ position: 'relative',
477
+ boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
478
+ transition: 'all 0.2s ease',
479
+ cursor: 'pointer',
480
+ overflow: 'hidden',
481
+ ...(status === 'Running' && {
482
+ animation: 'pulse-glow 2s ease-in-out infinite',
483
+ '@keyframes pulse-glow': {
484
+ '0%, 100%': {
485
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
486
+ borderColor: 'rgba(59, 130, 246, 0.6)',
487
+ },
488
+ '50%': {
489
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
490
+ borderColor: 'rgba(59, 130, 246, 0.9)',
491
+ },
492
+ },
493
+ }),
494
+ }}
495
+ onClick={handleJsonClick}
496
+ >
497
+ {/* Top Header Section */}
498
+ <Box sx={{
499
+ backgroundColor: "rgba(67, 93, 132, 0.1)",
500
+ padding: '8px 16px',
501
+ borderRadius: '12px 12px 0 0'
502
+ }}>
503
+ <Typography variant="body2" sx={{
504
+ color: '#ffffff',
505
+ fontSize: '12px',
506
+ fontWeight: 500
507
+ }}>
508
+ {highlightText(data.description || operationConfig.description)}
509
+ </Typography>
510
+ </Box>
511
+
512
+ {/* Main Content */}
513
+ <Box sx={{ padding: '16px' }}>
514
+ {/* Title Section */}
515
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
516
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
517
+ <Box
518
+ sx={{
519
+ width: '32px',
520
+ height: '32px',
521
+ backgroundColor: '#0ea5e9',
522
+ borderRadius: '50%',
523
+ display: 'flex',
524
+ alignItems: 'center',
525
+ justifyContent: 'center',
526
+ }}
527
+ >
528
+ <EmailIcon sx={{ fontSize: 18, color: '#FFFFFF' }} />
529
+ </Box>
530
+ <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
531
+ {highlightText(data.label || operationConfig.label)}
532
+ </Typography>
533
+ </Box>
534
+ <Chip
535
+ label={status}
536
+ size="small"
537
+ sx={{
538
+ backgroundColor: statusConfig.bgColor,
539
+ color: statusConfig.color,
540
+ fontWeight: 500,
541
+ fontSize: '12px',
542
+ height: '24px',
543
+ borderRadius: '12px',
544
+ }}
545
+ />
546
+ </Box>
547
+
548
+ {/* Description Box */}
549
+ <Box sx={{
550
+ backgroundColor: '#1F2937',
551
+ borderRadius: '8px',
552
+ padding: '12px',
553
+ mb: 2,
554
+ border: '1px solid #374151'
555
+ }}>
556
+ {/* Inner text boundary box */}
557
+ <Box sx={{
558
+ backgroundColor: 'transparent',
559
+ borderRadius: '4px',
560
+ padding: '8px',
561
+ border: '1px solid #4B5563',
562
+ minHeight: '40px',
563
+ display: 'flex',
564
+ alignItems: 'center'
565
+ }}>
566
+ <Typography variant="body2" sx={{
567
+ color: '#9CA3AF',
568
+ fontSize: '12px',
569
+ lineHeight: 1.4,
570
+ margin: 0
571
+ }}>
572
+ {data.description || operationConfig.description}
573
+ </Typography>
574
+ </Box>
575
+ </Box>
576
+
577
+ {/* Email Auth Button */}
578
+ {renderEmailAuthButton()}
579
+
580
+ {/* Operation-specific Parameters Display */}
581
+ {(operationType === 'send-email' && (data.parameters?.to || data.parameters?.subject)) && (
582
+ <Box sx={{ mt: 2, mb: 1 }}>
583
+ {data.parameters?.to && (
584
+ <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: '11px', display: 'block' }}>
585
+ To: {data.parameters.to.join(', ')}
586
+ </Typography>
587
+ )}
588
+ {data.parameters?.subject && (
589
+ <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: '11px', display: 'block', mt: 0.5 }}>
590
+ Subject: {data.parameters.subject}
591
+ </Typography>
592
+ )}
593
+ </Box>
594
+ )}
595
+
596
+ {/* Last Run Info */}
597
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 1 }}>
598
+ <AccessTimeIcon sx={{ fontSize: '14px', color: '#9CA3AF' }} />
599
+ <Typography variant="body2" sx={{ color: '#9CA3AF', fontSize: '11px' }}>
600
+ {t('automation.common.lastRan')}: {data.lastRun || 'Never'}
601
+ </Typography>
602
+ </Box>
603
+ </Box>
604
+
605
+ {/* Connection Handles - Bidirectional (source + target at each position) */}
606
+ {/* Top - Source */}
607
+ <Handle
608
+ type="source"
609
+ position={Position.Top}
610
+ id="top-source"
611
+ className="connection-handle"
612
+ style={{
613
+ background: selected ? '#10B981' : '#1a1a2e',
614
+ width: '14px',
615
+ height: '14px',
616
+ border: '3px solid #10B981',
617
+ top: '-8px',
618
+ opacity: selected ? 1 : 0,
619
+ transition: 'all 0.2s ease-in-out',
620
+ cursor: 'crosshair',
621
+ zIndex: 10,
622
+ }}
623
+ />
624
+ {/* Top - Target (hidden but functional) */}
625
+ <Handle
626
+ type="target"
627
+ position={Position.Top}
628
+ id="top-target"
629
+ style={{
630
+ background: 'transparent',
631
+ width: '14px',
632
+ height: '14px',
633
+ border: 'none',
634
+ top: '-8px',
635
+ opacity: 0,
636
+ pointerEvents: selected ? 'all' : 'none',
637
+ }}
638
+ />
639
+ {/* Bottom - Source */}
640
+ <Handle
641
+ type="source"
642
+ position={Position.Bottom}
643
+ id="bottom-source"
644
+ className="connection-handle"
645
+ style={{
646
+ background: selected ? '#10B981' : '#1a1a2e',
647
+ width: '14px',
648
+ height: '14px',
649
+ border: '3px solid #10B981',
650
+ bottom: '-8px',
651
+ opacity: selected ? 1 : 0,
652
+ transition: 'all 0.2s ease-in-out',
653
+ cursor: 'crosshair',
654
+ zIndex: 10,
655
+ }}
656
+ />
657
+ {/* Bottom - Target (hidden but functional) */}
658
+ <Handle
659
+ type="target"
660
+ position={Position.Bottom}
661
+ id="bottom-target"
662
+ style={{
663
+ background: 'transparent',
664
+ width: '14px',
665
+ height: '14px',
666
+ border: 'none',
667
+ bottom: '-8px',
668
+ opacity: 0,
669
+ pointerEvents: selected ? 'all' : 'none',
670
+ }}
671
+ />
672
+ {/* Left - Source */}
673
+ <Handle
674
+ type="source"
675
+ position={Position.Left}
676
+ id="left-source"
677
+ className="connection-handle"
678
+ style={{
679
+ background: selected ? '#10B981' : '#1a1a2e',
680
+ width: '14px',
681
+ height: '14px',
682
+ border: '3px solid #10B981',
683
+ left: '-8px',
684
+ opacity: selected ? 1 : 0,
685
+ transition: 'all 0.2s ease-in-out',
686
+ cursor: 'crosshair',
687
+ zIndex: 10,
688
+ }}
689
+ />
690
+ {/* Left - Target (hidden but functional) */}
691
+ <Handle
692
+ type="target"
693
+ position={Position.Left}
694
+ id="left-target"
695
+ style={{
696
+ background: 'transparent',
697
+ width: '14px',
698
+ height: '14px',
699
+ border: 'none',
700
+ left: '-8px',
701
+ opacity: 0,
702
+ pointerEvents: selected ? 'all' : 'none',
703
+ }}
704
+ />
705
+ {/* Right - Source */}
706
+ <Handle
707
+ type="source"
708
+ position={Position.Right}
709
+ id="right-source"
710
+ className="connection-handle"
711
+ style={{
712
+ background: selected ? '#10B981' : '#1a1a2e',
713
+ width: '14px',
714
+ height: '14px',
715
+ border: '3px solid #10B981',
716
+ right: '-8px',
717
+ opacity: selected ? 1 : 0,
718
+ transition: 'all 0.2s ease-in-out',
719
+ cursor: 'crosshair',
720
+ zIndex: 10,
721
+ }}
722
+ />
723
+ {/* Right - Target (hidden but functional) */}
724
+ <Handle
725
+ type="target"
726
+ position={Position.Right}
727
+ id="right-target"
728
+ style={{
729
+ background: 'transparent',
730
+ width: '14px',
731
+ height: '14px',
732
+ border: 'none',
733
+ right: '-8px',
734
+ opacity: 0,
735
+ pointerEvents: selected ? 'all' : 'none',
736
+ }}
737
+ />
738
+ </Box>
739
+
740
+ {/* Node Action Buttons - Shows when selected */}
741
+ <NodeActionButtons
742
+ selected={selected}
743
+ onOpenAIAssistant={(buttonElement) => {
744
+ if (nodeId) {
745
+ showNodeAIAssistantPopup(nodeId, 'Email Node', buttonElement);
746
+ }
747
+ }}
748
+ onDelete={() => {
749
+ if (nodeId && onNodesChange) {
750
+ onNodesChange([{ id: nodeId, type: 'remove' }]);
751
+ }
752
+ }}
753
+ onDuplicate={() => {
754
+ if (nodeId) {
755
+ const currentNode = nodes.find(n => n.id === nodeId);
756
+ if (currentNode) {
757
+ const newNode = {
758
+ ...currentNode,
759
+ id: `${currentNode.id}-copy-${Date.now()}`,
760
+ position: {
761
+ x: currentNode.position.x + 50,
762
+ y: currentNode.position.y + 50,
763
+ },
764
+ selected: false,
765
+ };
766
+ setNodes([...nodes, newNode]);
767
+ }
768
+ }
769
+ }}
770
+ />
771
+ </Box>
772
+ );
773
+ };
@@ -463,7 +463,7 @@ export const AutomationGoogleServicesNode: React.FC<AutomationGoogleServicesNode
463
463
  borderRadius: '12px',
464
464
  color: '#ffffff',
465
465
  position: 'relative',
466
- boxShadow: selected ? `0 0 0 2px ${serviceConfig.color}50` : '0 4px 8px rgba(0, 0, 0, 0.3)',
466
+ boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
467
467
  transition: 'all 0.2s ease',
468
468
  cursor: 'pointer',
469
469
  overflow: 'hidden',
@@ -471,12 +471,12 @@ export const AutomationGoogleServicesNode: React.FC<AutomationGoogleServicesNode
471
471
  animation: 'pulse-glow 2s ease-in-out infinite',
472
472
  '@keyframes pulse-glow': {
473
473
  '0%, 100%': {
474
- boxShadow: `0 0 20px ${serviceConfig.color}40, 0 0 40px ${serviceConfig.color}20`,
475
- borderColor: `${serviceConfig.color}60`,
474
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
475
+ borderColor: 'rgba(59, 130, 246, 0.6)',
476
476
  },
477
477
  '50%': {
478
- boxShadow: `0 0 30px ${serviceConfig.color}60, 0 0 60px ${serviceConfig.color}30`,
479
- borderColor: `${serviceConfig.color}90`,
478
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
479
+ borderColor: 'rgba(59, 130, 246, 0.9)',
480
480
  },
481
481
  },
482
482
  }),
@@ -403,7 +403,7 @@ export const AutomationInteractionNode: React.FC<AutomationInteractionNodeProps>
403
403
  borderRadius: '12px',
404
404
  color: '#ffffff',
405
405
  position: 'relative',
406
- boxShadow: selected ? `0 0 0 2px ${interactionConfig.color}50` : '0 4px 8px rgba(0, 0, 0, 0.3)',
406
+ boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
407
407
  transition: 'all 0.2s ease',
408
408
  cursor: 'pointer',
409
409
  overflow: 'hidden',
@@ -411,12 +411,12 @@ export const AutomationInteractionNode: React.FC<AutomationInteractionNodeProps>
411
411
  animation: 'pulse-glow 2s ease-in-out infinite',
412
412
  '@keyframes pulse-glow': {
413
413
  '0%, 100%': {
414
- boxShadow: `0 0 20px ${interactionConfig.color}40, 0 0 40px ${interactionConfig.color}20`,
415
- borderColor: `${interactionConfig.color}60`,
414
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
415
+ borderColor: 'rgba(59, 130, 246, 0.6)',
416
416
  },
417
417
  '50%': {
418
- boxShadow: `0 0 30px ${interactionConfig.color}60, 0 0 60px ${interactionConfig.color}30`,
419
- borderColor: `${interactionConfig.color}90`,
418
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
419
+ borderColor: 'rgba(59, 130, 246, 0.9)',
420
420
  },
421
421
  },
422
422
  }),
@@ -190,24 +190,22 @@ export const AutomationMonitoringNode: React.FC<AutomationMonitoringNodeProps> =
190
190
  sx={{
191
191
  bgcolor: '#1E1E2E',
192
192
  borderRadius: 2,
193
- border: selected ? `2px solid ${monitoringColor}` : '1px solid #374151',
193
+ border: selected ? '2px solid rgba(59, 130, 246, 0.5)' : '1px solid #374151',
194
194
  minWidth: 280,
195
195
  maxWidth: 320,
196
196
  overflow: 'hidden',
197
- boxShadow: selected
198
- ? `0 0 20px ${monitoringColor}30`
199
- : '0 4px 12px rgba(0, 0, 0, 0.3)',
197
+ boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
200
198
  transition: 'all 0.2s ease',
201
199
  ...(status === 'Running' && {
202
200
  animation: 'pulse-glow 2s ease-in-out infinite',
203
201
  '@keyframes pulse-glow': {
204
202
  '0%, 100%': {
205
- boxShadow: `0 0 20px ${monitoringColor}40, 0 0 40px ${monitoringColor}20`,
206
- borderColor: `${monitoringColor}60`,
203
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
204
+ borderColor: 'rgba(59, 130, 246, 0.6)',
207
205
  },
208
206
  '50%': {
209
- boxShadow: `0 0 30px ${monitoringColor}60, 0 0 60px ${monitoringColor}30`,
210
- borderColor: `${monitoringColor}90`,
207
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
208
+ borderColor: 'rgba(59, 130, 246, 0.9)',
211
209
  },
212
210
  },
213
211
  }),
@@ -434,14 +434,25 @@ export const AutomationSlackNode: React.FC<AutomationSlackNodeProps> = ({
434
434
  sx={{
435
435
  bgcolor: '#1E1E2E',
436
436
  borderRadius: 2,
437
- border: selected ? `2px solid ${operationConfig.color}` : '1px solid #374151',
437
+ border: selected ? '2px solid rgba(59, 130, 246, 0.5)' : '1px solid #374151',
438
438
  minWidth: 280,
439
439
  maxWidth: 320,
440
440
  overflow: 'hidden',
441
- boxShadow: selected
442
- ? `0 0 20px ${operationConfig.color}30`
443
- : '0 4px 12px rgba(0, 0, 0, 0.3)',
441
+ boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
444
442
  transition: 'all 0.2s ease',
443
+ ...(status === 'Running' && {
444
+ animation: 'pulse-glow 2s ease-in-out infinite',
445
+ '@keyframes pulse-glow': {
446
+ '0%, 100%': {
447
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
448
+ borderColor: 'rgba(59, 130, 246, 0.6)',
449
+ },
450
+ '50%': {
451
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
452
+ borderColor: 'rgba(59, 130, 246, 0.9)',
453
+ },
454
+ },
455
+ }),
445
456
  }}
446
457
  onClick={() => setSelectedNode(nodeId || '')}
447
458
  >
@@ -427,14 +427,25 @@ export const AutomationTelegramNode: React.FC<AutomationTelegramNodeProps> = ({
427
427
  sx={{
428
428
  bgcolor: '#1E1E2E',
429
429
  borderRadius: 2,
430
- border: selected ? `2px solid ${operationConfig.color}` : '1px solid #374151',
430
+ border: selected ? '2px solid rgba(59, 130, 246, 0.5)' : '1px solid #374151',
431
431
  minWidth: 280,
432
432
  maxWidth: 320,
433
433
  overflow: 'hidden',
434
- boxShadow: selected
435
- ? `0 0 20px ${operationConfig.color}30`
436
- : '0 4px 12px rgba(0, 0, 0, 0.3)',
434
+ boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
437
435
  transition: 'all 0.2s ease',
436
+ ...(status === 'Running' && {
437
+ animation: 'pulse-glow 2s ease-in-out infinite',
438
+ '@keyframes pulse-glow': {
439
+ '0%, 100%': {
440
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
441
+ borderColor: 'rgba(59, 130, 246, 0.6)',
442
+ },
443
+ '50%': {
444
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
445
+ borderColor: 'rgba(59, 130, 246, 0.9)',
446
+ },
447
+ },
448
+ }),
438
449
  }}
439
450
  onClick={() => setSelectedNode(nodeId || '')}
440
451
  >
@@ -43,6 +43,14 @@ export type {
43
43
  AutomationInteractionNodeData,
44
44
  AutomationInteractionNodeProps,
45
45
  } from './AutomationInteractionNode';
46
+ // Email Node
47
+ export { AutomationEmailNode } from './AutomationEmailNode';
48
+ export type {
49
+ EmailOperationType,
50
+ EmailTokenData,
51
+ AutomationEmailNodeData,
52
+ AutomationEmailNodeProps,
53
+ } from './AutomationEmailNode';
46
54
  // Monitoring Node
47
55
  export { AutomationMonitoringNode } from './AutomationMonitoringNode';
48
56
  export type {
@@ -16,6 +16,7 @@ import {
16
16
  AutomationSlackNode,
17
17
  AutomationTelegramNode,
18
18
  AutomationInteractionNode,
19
+ AutomationEmailNode,
19
20
  AutomationMonitoringNode,
20
21
  } from '../components/automation';
21
22
 
@@ -46,6 +47,8 @@ export default {
46
47
  AutomationTelegramNode,
47
48
  // Interaction Node
48
49
  AutomationInteractionNode,
50
+ // Email Node
51
+ AutomationEmailNode,
49
52
  // Monitoring Node
50
53
  AutomationMonitoringNode,
51
54
  };