@flowuent-org/diagramming-core 1.4.4 → 1.4.5

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.
@@ -459,6 +459,125 @@ export const automationDefaultNodes = [
459
459
  measured: { width: 300, height: 200 },
460
460
  },
461
461
  // =====================================
462
+ // Gmail Integration Node
463
+ // =====================================
464
+ {
465
+ id: 'gmail-node',
466
+ type: 'AutomationGmailNode',
467
+ position: { x: 1500, y: 700 },
468
+ data: {
469
+ label: 'Send Gmail',
470
+ description: 'Send an email via Gmail',
471
+ headerText: 'Gmail integration',
472
+ serviceType: 'gmail',
473
+ operationType: 'send-email',
474
+ status: 'Need to Config',
475
+ parameters: {
476
+ to: 'team@example.com',
477
+ subject: 'Workflow Update',
478
+ message: 'Your automation workflow completed successfully.',
479
+ },
480
+ formData: {
481
+ nodeId: 'gmail-node',
482
+ title: 'Send Gmail',
483
+ type: 'gmail',
484
+ operationType: 'send-email',
485
+ },
486
+ lastRun: 'Never',
487
+ },
488
+ width: 300,
489
+ height: 200,
490
+ measured: { width: 300, height: 200 },
491
+ },
492
+ // =====================================
493
+ // Google Docs Integration Node
494
+ // =====================================
495
+ {
496
+ id: 'google-docs-node',
497
+ type: 'AutomationGoogleDocsNode',
498
+ position: { x: 1850, y: 700 },
499
+ data: {
500
+ label: 'Create Google Doc',
501
+ description: 'Create or update a Google Document',
502
+ headerText: 'Google Docs integration',
503
+ serviceType: 'docs',
504
+ operationType: 'create-document',
505
+ status: 'Need to Config',
506
+ parameters: {
507
+ documentTitle: 'Workflow Report',
508
+ },
509
+ formData: {
510
+ nodeId: 'google-docs-node',
511
+ title: 'Create Google Doc',
512
+ type: 'google-docs',
513
+ operationType: 'create-document',
514
+ },
515
+ lastRun: 'Never',
516
+ },
517
+ width: 300,
518
+ height: 200,
519
+ measured: { width: 300, height: 200 },
520
+ },
521
+ // =====================================
522
+ // Google Slides Integration Node
523
+ // =====================================
524
+ {
525
+ id: 'google-slides-node',
526
+ type: 'AutomationGoogleSlidesNode',
527
+ position: { x: 2200, y: 700 },
528
+ data: {
529
+ label: 'Create Presentation',
530
+ description: 'Create or update a Google Slides deck',
531
+ headerText: 'Google Slides integration',
532
+ serviceType: 'slides',
533
+ operationType: 'create-presentation',
534
+ status: 'Need to Config',
535
+ parameters: {
536
+ presentationTitle: 'Workflow Summary Deck',
537
+ },
538
+ formData: {
539
+ nodeId: 'google-slides-node',
540
+ title: 'Create Presentation',
541
+ type: 'google-slides',
542
+ operationType: 'create-presentation',
543
+ },
544
+ lastRun: 'Never',
545
+ },
546
+ width: 300,
547
+ height: 200,
548
+ measured: { width: 300, height: 200 },
549
+ },
550
+ // =====================================
551
+ // Google Meet Integration Node
552
+ // =====================================
553
+ {
554
+ id: 'google-meet-node',
555
+ type: 'AutomationGoogleMeetNode',
556
+ position: { x: 2550, y: 700 },
557
+ data: {
558
+ label: 'Schedule Meet',
559
+ description: 'Create or schedule a Google Meet session',
560
+ headerText: 'Google Meet integration',
561
+ serviceType: 'meet',
562
+ operationType: 'schedule-meeting',
563
+ status: 'Need to Config',
564
+ parameters: {
565
+ meetingTitle: 'Workflow Review Meeting',
566
+ meetingLink: '',
567
+ },
568
+ formData: {
569
+ nodeId: 'google-meet-node',
570
+ title: 'Schedule Meet',
571
+ type: 'google-meet',
572
+ operationType: 'schedule-meeting',
573
+ },
574
+ lastRun: 'Never',
575
+ },
576
+ width: 300,
577
+ height: 200,
578
+ measured: { width: 300, height: 200 },
579
+ },
580
+ // =====================================
462
581
  // Interaction Node
463
582
  // =====================================
464
583
  {
@@ -848,6 +967,85 @@ export const automationDefaultEdges = [
848
967
  },
849
968
  },
850
969
  // =====================================
970
+ // Google Nodes Edges
971
+ // =====================================
972
+ {
973
+ id: 'edge-telegram-to-gmail',
974
+ source: 'telegram-node',
975
+ target: 'gmail-node',
976
+ sourceHandle: 'right',
977
+ targetHandle: 'left',
978
+ data: {
979
+ label: 'Email Report',
980
+ type: 'flow',
981
+ },
982
+ style: {
983
+ stroke: '#EA4335',
984
+ strokeWidth: 2,
985
+ },
986
+ markerEnd: {
987
+ type: MarkerType.ArrowClosed,
988
+ color: '#EA4335',
989
+ },
990
+ },
991
+ {
992
+ id: 'edge-gmail-to-docs',
993
+ source: 'gmail-node',
994
+ target: 'google-docs-node',
995
+ sourceHandle: 'right',
996
+ targetHandle: 'left',
997
+ data: {
998
+ label: 'Create Doc',
999
+ type: 'flow',
1000
+ },
1001
+ style: {
1002
+ stroke: '#4285F4',
1003
+ strokeWidth: 2,
1004
+ },
1005
+ markerEnd: {
1006
+ type: MarkerType.ArrowClosed,
1007
+ color: '#4285F4',
1008
+ },
1009
+ },
1010
+ {
1011
+ id: 'edge-docs-to-slides',
1012
+ source: 'google-docs-node',
1013
+ target: 'google-slides-node',
1014
+ sourceHandle: 'right',
1015
+ targetHandle: 'left',
1016
+ data: {
1017
+ label: 'Build Deck',
1018
+ type: 'flow',
1019
+ },
1020
+ style: {
1021
+ stroke: '#FBBC04',
1022
+ strokeWidth: 2,
1023
+ },
1024
+ markerEnd: {
1025
+ type: MarkerType.ArrowClosed,
1026
+ color: '#FBBC04',
1027
+ },
1028
+ },
1029
+ {
1030
+ id: 'edge-slides-to-meet',
1031
+ source: 'google-slides-node',
1032
+ target: 'google-meet-node',
1033
+ sourceHandle: 'right',
1034
+ targetHandle: 'left',
1035
+ data: {
1036
+ label: 'Schedule Meet',
1037
+ type: 'flow',
1038
+ },
1039
+ style: {
1040
+ stroke: '#00897B',
1041
+ strokeWidth: 2,
1042
+ },
1043
+ markerEnd: {
1044
+ type: MarkerType.ArrowClosed,
1045
+ color: '#00897B',
1046
+ },
1047
+ },
1048
+ // =====================================
851
1049
  // Monitoring Node Edge
852
1050
  // =====================================
853
1051
  {
@@ -453,9 +453,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
453
453
  onChange={onChange}
454
454
  availableFunctions={[]}
455
455
  id={8}
456
- >
457
- {/* {automationChildren} */}
458
- </DiagrammingPage>
456
+ />
459
457
  }
460
458
  />
461
459
  </Routes>
@@ -649,9 +649,13 @@
649
649
  "formattingNode": {
650
650
  "headerDescription": "AI-powered content analysis",
651
651
  "aiReasoning": "AI Reasoning",
652
- "aiReasoningDefault": "Analyzed the selected headline for key components and promises",
653
652
  "formattedData": "Formatted Data"
654
653
  },
654
+ "googleAuth": {
655
+ "connectGoogle": "Connect Google",
656
+ "connecting": "Connecting...",
657
+ "signOut": "Sign out"
658
+ },
655
659
  "endNode": {
656
660
  "headerDescription": "Workflow completion and results display",
657
661
  "finalOutput": "Final Output"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowuent-org/diagramming-core",
3
- "version": "1.4.4",
3
+ "version": "1.4.5",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -649,9 +649,13 @@
649
649
  "formattingNode": {
650
650
  "headerDescription": "AI-powered content analysis",
651
651
  "aiReasoning": "AI Reasoning",
652
- "aiReasoningDefault": "Analyzed the selected headline for key components and promises",
653
652
  "formattedData": "Formatted Data"
654
653
  },
654
+ "googleAuth": {
655
+ "connectGoogle": "Connect Google",
656
+ "connecting": "Connecting...",
657
+ "signOut": "Sign out"
658
+ },
655
659
  "endNode": {
656
660
  "headerDescription": "Workflow completion and results display",
657
661
  "finalOutput": "Final Output"
@@ -0,0 +1,680 @@
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
+ Button,
11
+ CircularProgress,
12
+ Tooltip,
13
+ LinearProgress,
14
+ } from '@mui/material';
15
+ import {
16
+ AccessTime as AccessTimeIcon,
17
+ CheckCircle as CheckCircleIcon,
18
+ Error as ErrorIcon,
19
+ } from '@mui/icons-material';
20
+ import { RiCloseLine } from 'react-icons/ri';
21
+ import ReactJson from 'react-json-view';
22
+ import { useTranslation } from 'react-i18next';
23
+ import { useDiagram } from '../../contexts/DiagramProvider';
24
+ import { useSearch } from '../../contexts/SearchContext';
25
+ import { NodeActionButtons } from './NodeActionButtons';
26
+ import { getStatusColor } from './statusColors';
27
+ import { AutomationNodeHeaderSection } from './AutomationNodeHeader';
28
+ import {
29
+ GOOGLE_SERVICE_CONFIGS,
30
+ GoogleServiceConfig,
31
+ GoogleServiceType,
32
+ } from './googleServiceNodeConfig';
33
+
34
+ const getGoogleUserEmail = (data: AutomationGoogleServiceNodeData): string | undefined => {
35
+ const formData = data.formData || {};
36
+ const parameters = data.parameters || {};
37
+
38
+ return (
39
+ (formData.googleUserEmail as string | undefined) ||
40
+ parameters.googleUserEmail ||
41
+ (formData.googleEmail as string | undefined) ||
42
+ parameters.googleEmail ||
43
+ data.googleAuth?.email
44
+ );
45
+ };
46
+
47
+ const isGoogleAuthenticated = (data: AutomationGoogleServiceNodeData): boolean => {
48
+ const formData = data.formData || {};
49
+ const parameters = data.parameters || {};
50
+
51
+ return !!(
52
+ formData.googleAccessToken ||
53
+ parameters.googleAccessToken ||
54
+ getGoogleUserEmail(data)
55
+ );
56
+ };
57
+
58
+ export interface AutomationGoogleServiceNodeData {
59
+ label: string;
60
+ description: string;
61
+ headerText?: string;
62
+ serviceType: GoogleServiceType;
63
+ operationType?: string;
64
+ status:
65
+ | 'Ready'
66
+ | 'Running'
67
+ | 'Completed'
68
+ | 'Failed'
69
+ | 'Need to Config'
70
+ | 'authenticated'
71
+ | 'idle';
72
+ parameters?: {
73
+ useUserAccount?: boolean;
74
+ googleAccessToken?: string;
75
+ googleRefreshToken?: string;
76
+ googleTokenExpiresAt?: number;
77
+ googleUserEmail?: string;
78
+ googleEmail?: string;
79
+ googleUserName?: string;
80
+ googleUserId?: string;
81
+ to?: string;
82
+ subject?: string;
83
+ message?: string;
84
+ documentId?: string;
85
+ documentTitle?: string;
86
+ presentationId?: string;
87
+ presentationTitle?: string;
88
+ meetingTitle?: string;
89
+ meetingLink?: string;
90
+ [key: string]: unknown;
91
+ };
92
+ googleAuth?: {
93
+ isLoading?: boolean;
94
+ email?: string;
95
+ userName?: string;
96
+ };
97
+ formData?: {
98
+ nodeId?: string;
99
+ title?: string;
100
+ type?: string;
101
+ operationType?: string;
102
+ [key: string]: unknown;
103
+ };
104
+ lastRun?: string;
105
+ duration?: string;
106
+ executionResult?: {
107
+ success: boolean;
108
+ data?: unknown;
109
+ error?: string;
110
+ };
111
+ onGoogleConnect?: () => void;
112
+ onGoogleDisconnect?: () => void;
113
+ }
114
+
115
+ export interface AutomationGoogleServiceNodeProps {
116
+ data: AutomationGoogleServiceNodeData;
117
+ selected?: boolean;
118
+ config: GoogleServiceConfig;
119
+ }
120
+
121
+ const isTokenExpired = (expiresAt?: number): boolean => {
122
+ if (!expiresAt) return false;
123
+ return Date.now() > expiresAt;
124
+ };
125
+
126
+ const getTimeUntilExpiry = (expiresAt?: number): string => {
127
+ if (!expiresAt) return 'No expiry';
128
+ const diff = expiresAt - Date.now();
129
+ if (diff <= 0) return 'Expired';
130
+ const minutes = Math.floor(diff / 60000);
131
+ if (minutes < 60) return `${minutes}m`;
132
+ const hours = Math.floor(minutes / 60);
133
+ return `${hours}h ${minutes % 60}m`;
134
+ };
135
+
136
+ export const AutomationGoogleServiceNode: React.FC<AutomationGoogleServiceNodeProps> = ({
137
+ data,
138
+ selected,
139
+ config,
140
+ }) => {
141
+ const { t } = useTranslation();
142
+ const { highlightText } = useSearch();
143
+ const [isJsonOpen, setIsJsonOpen] = useState(false);
144
+ const rootRef = useRef<ReturnType<typeof createRoot> | null>(null);
145
+ const portalRef = useRef<HTMLDivElement | null>(null);
146
+ const nodeId = useNodeId();
147
+ const setSelectedNode = useDiagram((state) => state.setSelectedNode);
148
+
149
+ const operationType =
150
+ data.operationType || data.formData?.operationType || config.defaultOperation;
151
+ const operationConfig =
152
+ config.operations[operationType] || config.operations[config.defaultOperation];
153
+ const OperationIcon = operationConfig.icon;
154
+ const BrandIcon = config.BrandIcon;
155
+
156
+ const isAuthenticated = isGoogleAuthenticated(data);
157
+ const isAuthLoading = data.googleAuth?.isLoading || false;
158
+ const userEmail = getGoogleUserEmail(data);
159
+ const userName = data.googleAuth?.userName || data.parameters?.googleUserName;
160
+ const tokenExpiresAt =
161
+ data.parameters?.googleTokenExpiresAt ||
162
+ (data.formData?.googleTokenExpiresAt as number | undefined);
163
+ const tokenExpired = isTokenExpired(tokenExpiresAt);
164
+
165
+ const status = data.status || 'Ready';
166
+ const executionProgress = status === 'Running' ? 50 : 0;
167
+ const statusConfig = getStatusColor(
168
+ status,
169
+ status === 'authenticated' ? 'authenticated' : 'ready',
170
+ );
171
+
172
+ const handleClose = () => {
173
+ setIsJsonOpen(false);
174
+ if (rootRef.current) {
175
+ rootRef.current.unmount();
176
+ rootRef.current = null;
177
+ }
178
+ if (portalRef.current) {
179
+ document.body.removeChild(portalRef.current);
180
+ portalRef.current = null;
181
+ }
182
+ };
183
+
184
+ useEffect(() => {
185
+ const handleClickOutside = (event: MouseEvent) => {
186
+ if (isJsonOpen && !(event.target as Element).closest('#automation-json-popover')) {
187
+ handleClose();
188
+ }
189
+ };
190
+ document.addEventListener('mousedown', handleClickOutside);
191
+ return () => {
192
+ document.removeEventListener('mousedown', handleClickOutside);
193
+ };
194
+ }, [isJsonOpen]);
195
+
196
+ useEffect(() => {
197
+ if (isJsonOpen) {
198
+ const portalRoot = document.createElement('div');
199
+ document.body.appendChild(portalRoot);
200
+ portalRef.current = portalRoot;
201
+
202
+ const root = createRoot(portalRoot);
203
+ rootRef.current = root;
204
+
205
+ root.render(
206
+ <Card
207
+ id="automation-json-popover"
208
+ sx={{
209
+ position: 'fixed',
210
+ top: 0,
211
+ right: 0,
212
+ zIndex: 9999,
213
+ width: '400px',
214
+ height: '100vh',
215
+ overflow: 'auto',
216
+ bgcolor: '#242424',
217
+ color: '#fff',
218
+ border: '1px solid #333',
219
+ }}
220
+ >
221
+ <Box
222
+ sx={{
223
+ p: 2,
224
+ borderBottom: '1px solid #333',
225
+ display: 'flex',
226
+ justifyContent: 'space-between',
227
+ alignItems: 'center',
228
+ }}
229
+ >
230
+ <Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
231
+ {config.serviceLabel} Configuration
232
+ </Typography>
233
+ <IconButton size="small" onClick={handleClose} sx={{ color: '#fff' }}>
234
+ <RiCloseLine />
235
+ </IconButton>
236
+ </Box>
237
+ <Box sx={{ p: 2 }}>
238
+ <ReactJson
239
+ src={data}
240
+ theme="monokai"
241
+ collapsed={1}
242
+ displayDataTypes={false}
243
+ enableClipboard={false}
244
+ style={{
245
+ backgroundColor: 'transparent',
246
+ fontSize: '12px',
247
+ }}
248
+ />
249
+ </Box>
250
+ </Card>,
251
+ );
252
+ }
253
+
254
+ return () => {
255
+ if (rootRef.current) {
256
+ rootRef.current.unmount();
257
+ rootRef.current = null;
258
+ }
259
+ if (portalRef.current && portalRef.current.parentNode) {
260
+ document.body.removeChild(portalRef.current);
261
+ portalRef.current = null;
262
+ }
263
+ };
264
+ }, [config.serviceLabel, data, isJsonOpen]);
265
+
266
+ const renderStatusBadge = () => (
267
+ <Chip
268
+ size="small"
269
+ label={
270
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
271
+ {status === 'Completed' && (
272
+ <CheckCircleIcon sx={{ fontSize: 12 }} />
273
+ )}
274
+ {status === 'Failed' && <ErrorIcon sx={{ fontSize: 12 }} />}
275
+ <span>{statusConfig.label}</span>
276
+ </Box>
277
+ }
278
+ sx={{
279
+ height: 20,
280
+ fontSize: '10px',
281
+ fontWeight: 600,
282
+ color: statusConfig.color,
283
+ bgcolor: statusConfig.bgColor,
284
+ border: `1px solid ${statusConfig.color}20`,
285
+ '& .MuiChip-label': { px: 1 },
286
+ }}
287
+ />
288
+ );
289
+
290
+ const renderGoogleAuthButton = () => {
291
+ if (isAuthenticated && !tokenExpired) {
292
+ return (
293
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
294
+ <Button
295
+ onClick={(event) => event.stopPropagation()}
296
+ disabled={isAuthLoading}
297
+ fullWidth
298
+ sx={{
299
+ backgroundColor: config.brandColor,
300
+ borderRadius: '8px',
301
+ padding: '8px 12px',
302
+ gap: '8px',
303
+ color: '#FFFFFF',
304
+ textTransform: 'none',
305
+ fontSize: '13px',
306
+ fontWeight: 500,
307
+ justifyContent: 'flex-start',
308
+ '&:hover': {
309
+ backgroundColor: config.iconBgColor,
310
+ },
311
+ }}
312
+ >
313
+ <BrandIcon size={18} />
314
+ {userEmail || userName || 'Connected'}
315
+ </Button>
316
+ <Button
317
+ onClick={(event) => {
318
+ event.stopPropagation();
319
+ data.onGoogleDisconnect?.();
320
+ }}
321
+ disabled={isAuthLoading || !data.onGoogleDisconnect}
322
+ fullWidth
323
+ sx={{
324
+ backgroundColor: 'transparent',
325
+ border: '1px solid #FFFFFF1A',
326
+ borderRadius: '8px',
327
+ padding: '6px 12px',
328
+ color: '#FCA5A5',
329
+ textTransform: 'none',
330
+ fontSize: '12px',
331
+ fontWeight: 500,
332
+ '&:hover': {
333
+ backgroundColor: 'rgba(239, 68, 68, 0.12)',
334
+ },
335
+ }}
336
+ >
337
+ {t('automation.googleAuth.signOut')}
338
+ </Button>
339
+ </Box>
340
+ );
341
+ }
342
+
343
+ return (
344
+ <Button
345
+ onClick={(event) => {
346
+ event.stopPropagation();
347
+ data.onGoogleConnect?.();
348
+ }}
349
+ disabled={isAuthLoading || !data.onGoogleConnect}
350
+ fullWidth
351
+ sx={{
352
+ backgroundColor: '#171C29',
353
+ border: '1px solid #FFFFFF1A',
354
+ borderRadius: '8px',
355
+ padding: '8px 12px',
356
+ gap: '8px',
357
+ color: '#B2BCD8',
358
+ textTransform: 'none',
359
+ fontSize: '13px',
360
+ fontWeight: 400,
361
+ '&:hover': {
362
+ backgroundColor: '#1e293b',
363
+ borderColor: config.brandColor,
364
+ },
365
+ '&:disabled': {
366
+ opacity: 0.6,
367
+ },
368
+ }}
369
+ >
370
+ {isAuthLoading ? (
371
+ <>
372
+ <CircularProgress size={16} sx={{ color: '#B2BCD8' }} />
373
+ {t('automation.googleAuth.connecting')}
374
+ </>
375
+ ) : (
376
+ <>
377
+ <BrandIcon size={18} />
378
+ {t('automation.googleAuth.connectGoogle')}
379
+ </>
380
+ )}
381
+ </Button>
382
+ );
383
+ };
384
+
385
+ const renderParameterSummary = () => {
386
+ if (config.serviceType === 'gmail') {
387
+ return (
388
+ <>
389
+ {data.parameters?.to && (
390
+ <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10, display: 'block' }}>
391
+ To: {data.parameters.to}
392
+ </Typography>
393
+ )}
394
+ {data.parameters?.subject && (
395
+ <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10, display: 'block' }}>
396
+ Subject: {data.parameters.subject}
397
+ </Typography>
398
+ )}
399
+ </>
400
+ );
401
+ }
402
+
403
+ if (config.serviceType === 'docs') {
404
+ return (
405
+ <>
406
+ {data.parameters?.documentTitle && (
407
+ <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10, display: 'block' }}>
408
+ Document: {data.parameters.documentTitle}
409
+ </Typography>
410
+ )}
411
+ {data.parameters?.documentId && (
412
+ <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10, display: 'block' }}>
413
+ ID: {data.parameters.documentId}
414
+ </Typography>
415
+ )}
416
+ </>
417
+ );
418
+ }
419
+
420
+ if (config.serviceType === 'slides') {
421
+ return (
422
+ <>
423
+ {data.parameters?.presentationTitle && (
424
+ <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10, display: 'block' }}>
425
+ Presentation: {data.parameters.presentationTitle}
426
+ </Typography>
427
+ )}
428
+ {data.parameters?.presentationId && (
429
+ <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10, display: 'block' }}>
430
+ ID: {data.parameters.presentationId}
431
+ </Typography>
432
+ )}
433
+ </>
434
+ );
435
+ }
436
+
437
+ return (
438
+ <>
439
+ {data.parameters?.meetingTitle && (
440
+ <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10, display: 'block' }}>
441
+ Meeting: {data.parameters.meetingTitle}
442
+ </Typography>
443
+ )}
444
+ {data.parameters?.meetingLink && (
445
+ <Typography
446
+ variant="caption"
447
+ sx={{
448
+ color: '#9CA3AF',
449
+ fontSize: 10,
450
+ display: 'block',
451
+ overflow: 'hidden',
452
+ textOverflow: 'ellipsis',
453
+ whiteSpace: 'nowrap',
454
+ }}
455
+ >
456
+ Link: {data.parameters.meetingLink}
457
+ </Typography>
458
+ )}
459
+ </>
460
+ );
461
+ };
462
+
463
+ return (
464
+ <Box
465
+ sx={{
466
+ bgcolor: '#1E1E2E',
467
+ borderRadius: 2,
468
+ border: selected ? '2px solid rgba(59, 130, 246, 0.5)' : '1px solid #374151',
469
+ minWidth: 280,
470
+ maxWidth: 320,
471
+ overflow: 'hidden',
472
+ boxShadow: selected ? '0 0 0 2px rgba(59, 130, 246, 0.5)' : '0 4px 8px rgba(0, 0, 0, 0.3)',
473
+ transition: 'all 0.2s ease',
474
+ ...(status === 'Running' && {
475
+ animation: 'pulse-glow 2s ease-in-out infinite',
476
+ '@keyframes pulse-glow': {
477
+ '0%, 100%': {
478
+ boxShadow: '0 0 20px rgba(59, 130, 246, 0.4), 0 0 40px rgba(59, 130, 246, 0.2)',
479
+ borderColor: 'rgba(59, 130, 246, 0.6)',
480
+ },
481
+ '50%': {
482
+ boxShadow: '0 0 30px rgba(59, 130, 246, 0.6), 0 0 60px rgba(59, 130, 246, 0.3)',
483
+ borderColor: 'rgba(59, 130, 246, 0.9)',
484
+ },
485
+ },
486
+ }),
487
+ }}
488
+ onClick={() => setSelectedNode(nodeId || '')}
489
+ >
490
+ <Handle
491
+ type="target"
492
+ position={Position.Left}
493
+ style={{
494
+ background: operationConfig.color,
495
+ width: 10,
496
+ height: 10,
497
+ border: '2px solid #1E1E2E',
498
+ }}
499
+ />
500
+
501
+ <AutomationNodeHeaderSection data={data} highlightText={highlightText} />
502
+
503
+ <Box
504
+ sx={{
505
+ bgcolor: 'rgba(67, 93, 132, 0.1)',
506
+ borderBottom: '1px solid rgba(67, 93, 132, 0.2)',
507
+ px: 2,
508
+ py: 1.5,
509
+ display: 'flex',
510
+ alignItems: 'center',
511
+ justifyContent: 'space-between',
512
+ }}
513
+ >
514
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
515
+ <Box
516
+ sx={{
517
+ width: '32px',
518
+ height: '32px',
519
+ minWidth: '32px',
520
+ backgroundColor: config.iconBgColor,
521
+ borderRadius: '50%',
522
+ display: 'flex',
523
+ alignItems: 'center',
524
+ justifyContent: 'center',
525
+ flexShrink: 0,
526
+ }}
527
+ >
528
+ <OperationIcon sx={{ fontSize: 18, color: '#fff' }} />
529
+ </Box>
530
+ <Box>
531
+ <Typography variant="subtitle2" sx={{ color: '#fff', fontWeight: 600, fontSize: 13 }}>
532
+ {highlightText(data.label || operationConfig.label)}
533
+ </Typography>
534
+ </Box>
535
+ </Box>
536
+ {renderStatusBadge()}
537
+ </Box>
538
+
539
+ <Box sx={{ px: 2, py: 1.5 }}>
540
+ <Box sx={{ mb: 1.5 }}>
541
+ {renderGoogleAuthButton()}
542
+ </Box>
543
+
544
+ {status === 'Running' && (
545
+ <Box sx={{ mb: 1.5 }}>
546
+ <LinearProgress
547
+ variant="determinate"
548
+ value={executionProgress}
549
+ sx={{
550
+ height: 4,
551
+ borderRadius: 2,
552
+ bgcolor: 'rgba(59, 130, 246, 0.1)',
553
+ '& .MuiLinearProgress-bar': {
554
+ bgcolor: operationConfig.color,
555
+ },
556
+ }}
557
+ />
558
+ </Box>
559
+ )}
560
+
561
+ {isAuthenticated && tokenExpiresAt && (
562
+ <Box sx={{ mb: 1.5 }}>
563
+ <Tooltip
564
+ title={`Token expires in ${getTimeUntilExpiry(tokenExpiresAt)}`}
565
+ >
566
+ <Chip
567
+ size="small"
568
+ label={getTimeUntilExpiry(tokenExpiresAt)}
569
+ sx={{
570
+ height: 18,
571
+ fontSize: '9px',
572
+ backgroundColor: tokenExpired ? '#EF4444' : config.brandColor,
573
+ color: '#FFFFFF',
574
+ fontWeight: 500,
575
+ }}
576
+ />
577
+ </Tooltip>
578
+ </Box>
579
+ )}
580
+
581
+ <Box sx={{ mb: 1.5 }}>{renderParameterSummary()}</Box>
582
+
583
+ {data.parameters?.message && (
584
+ <Typography
585
+ variant="caption"
586
+ sx={{
587
+ color: '#9CA3AF',
588
+ fontSize: 10,
589
+ display: 'block',
590
+ mb: 1.5,
591
+ overflow: 'hidden',
592
+ textOverflow: 'ellipsis',
593
+ whiteSpace: 'nowrap',
594
+ }}
595
+ >
596
+ Message: {data.parameters.message}
597
+ </Typography>
598
+ )}
599
+
600
+ {data.executionResult && (
601
+ <Box
602
+ sx={{
603
+ bgcolor: data.executionResult.success
604
+ ? 'rgba(16, 185, 129, 0.1)'
605
+ : 'rgba(239, 68, 68, 0.1)',
606
+ borderRadius: 1,
607
+ p: 1,
608
+ mb: 1.5,
609
+ border: `1px solid ${
610
+ data.executionResult.success
611
+ ? 'rgba(16, 185, 129, 0.3)'
612
+ : 'rgba(239, 68, 68, 0.3)'
613
+ }`,
614
+ }}
615
+ >
616
+ <Typography
617
+ variant="caption"
618
+ sx={{
619
+ color: data.executionResult.success ? '#10B981' : '#EF4444',
620
+ fontSize: 10,
621
+ }}
622
+ >
623
+ {data.executionResult.success
624
+ ? '✓ Operation completed'
625
+ : `✗ ${data.executionResult.error || 'Operation failed'}`}
626
+ </Typography>
627
+ </Box>
628
+ )}
629
+
630
+ {data.lastRun && (
631
+ <Box
632
+ sx={{
633
+ display: 'flex',
634
+ alignItems: 'center',
635
+ gap: 0.5,
636
+ justifyContent: 'center',
637
+ }}
638
+ >
639
+ <AccessTimeIcon sx={{ fontSize: 10, color: '#6B7280' }} />
640
+ <Typography variant="caption" sx={{ color: '#6B7280', fontSize: 9 }}>
641
+ Last run: {data.lastRun}
642
+ {data.duration && ` · ${data.duration}`}
643
+ </Typography>
644
+ </Box>
645
+ )}
646
+ </Box>
647
+
648
+ <NodeActionButtons selected={selected} />
649
+
650
+ <Handle
651
+ type="source"
652
+ position={Position.Right}
653
+ style={{
654
+ background: operationConfig.color,
655
+ width: 10,
656
+ height: 10,
657
+ border: '2px solid #1E1E2E',
658
+ }}
659
+ />
660
+ </Box>
661
+ );
662
+ };
663
+
664
+ const createGoogleServiceNode = (serviceType: GoogleServiceType) => {
665
+ const config = GOOGLE_SERVICE_CONFIGS[serviceType];
666
+
667
+ const GoogleServiceNode: React.FC<Omit<AutomationGoogleServiceNodeProps, 'config'>> = (
668
+ props,
669
+ ) => <AutomationGoogleServiceNode {...props} config={config} />;
670
+
671
+ GoogleServiceNode.displayName = `Automation${config.serviceLabel.replace(/\s+/g, '')}Node`;
672
+ return GoogleServiceNode;
673
+ };
674
+
675
+ export const AutomationGmailNode = createGoogleServiceNode('gmail');
676
+ export const AutomationGoogleDocsNode = createGoogleServiceNode('docs');
677
+ export const AutomationGoogleSlidesNode = createGoogleServiceNode('slides');
678
+ export const AutomationGoogleMeetNode = createGoogleServiceNode('meet');
679
+
680
+ export default AutomationGoogleServiceNode;
@@ -0,0 +1,158 @@
1
+ import React from 'react';
2
+ import {
3
+ Send as SendIcon,
4
+ Email as EmailIcon,
5
+ Search as SearchIcon,
6
+ Description as DescriptionIcon,
7
+ Edit as EditIcon,
8
+ Slideshow as SlideshowIcon,
9
+ VideoCall as VideoCallIcon,
10
+ Event as EventIcon,
11
+ Link as LinkIcon,
12
+ } from '@mui/icons-material';
13
+ import { SiGmail, SiGoogledocs, SiGoogleslides, SiGooglemeet } from 'react-icons/si';
14
+
15
+ export type GoogleServiceType = 'gmail' | 'docs' | 'slides' | 'meet';
16
+
17
+ export interface GoogleOperationConfig {
18
+ icon: React.ElementType;
19
+ label: string;
20
+ color: string;
21
+ description: string;
22
+ }
23
+
24
+ export interface GoogleServiceConfig {
25
+ serviceType: GoogleServiceType;
26
+ serviceLabel: string;
27
+ brandColor: string;
28
+ iconBgColor: string;
29
+ BrandIcon: React.ComponentType<{ size?: number }>;
30
+ scopes: string[];
31
+ defaultOperation: string;
32
+ operations: Record<string, GoogleOperationConfig>;
33
+ }
34
+
35
+ export const GOOGLE_SERVICE_CONFIGS: Record<GoogleServiceType, GoogleServiceConfig> = {
36
+ gmail: {
37
+ serviceType: 'gmail',
38
+ serviceLabel: 'Gmail',
39
+ brandColor: '#EA4335',
40
+ iconBgColor: '#B31412',
41
+ BrandIcon: SiGmail,
42
+ scopes: [
43
+ 'https://www.googleapis.com/auth/gmail.send',
44
+ 'https://www.googleapis.com/auth/gmail.readonly',
45
+ ],
46
+ defaultOperation: 'send-email',
47
+ operations: {
48
+ 'send-email': {
49
+ icon: SendIcon,
50
+ label: 'Send Email',
51
+ color: '#EA4335',
52
+ description: 'Send an email via Gmail',
53
+ },
54
+ 'read-email': {
55
+ icon: EmailIcon,
56
+ label: 'Read Email',
57
+ color: '#4285F4',
58
+ description: 'Read messages from inbox',
59
+ },
60
+ 'search-emails': {
61
+ icon: SearchIcon,
62
+ label: 'Search Emails',
63
+ color: '#34A853',
64
+ description: 'Search emails by query',
65
+ },
66
+ },
67
+ },
68
+ docs: {
69
+ serviceType: 'docs',
70
+ serviceLabel: 'Google Docs',
71
+ brandColor: '#4285F4',
72
+ iconBgColor: '#1967D2',
73
+ BrandIcon: SiGoogledocs,
74
+ scopes: ['https://www.googleapis.com/auth/documents'],
75
+ defaultOperation: 'create-document',
76
+ operations: {
77
+ 'create-document': {
78
+ icon: DescriptionIcon,
79
+ label: 'Create Document',
80
+ color: '#4285F4',
81
+ description: 'Create a new Google Doc',
82
+ },
83
+ 'read-document': {
84
+ icon: DescriptionIcon,
85
+ label: 'Read Document',
86
+ color: '#34A853',
87
+ description: 'Read content from a document',
88
+ },
89
+ 'update-document': {
90
+ icon: EditIcon,
91
+ label: 'Update Document',
92
+ color: '#FBBC04',
93
+ description: 'Update document content',
94
+ },
95
+ },
96
+ },
97
+ slides: {
98
+ serviceType: 'slides',
99
+ serviceLabel: 'Google Slides',
100
+ brandColor: '#FBBC04',
101
+ iconBgColor: '#F29900',
102
+ BrandIcon: SiGoogleslides,
103
+ scopes: ['https://www.googleapis.com/auth/presentations'],
104
+ defaultOperation: 'create-presentation',
105
+ operations: {
106
+ 'create-presentation': {
107
+ icon: SlideshowIcon,
108
+ label: 'Create Presentation',
109
+ color: '#FBBC04',
110
+ description: 'Create a new Google Slides deck',
111
+ },
112
+ 'read-presentation': {
113
+ icon: SlideshowIcon,
114
+ label: 'Read Presentation',
115
+ color: '#4285F4',
116
+ description: 'Read slides from a presentation',
117
+ },
118
+ 'update-slides': {
119
+ icon: EditIcon,
120
+ label: 'Update Slides',
121
+ color: '#EA4335',
122
+ description: 'Update slide content',
123
+ },
124
+ },
125
+ },
126
+ meet: {
127
+ serviceType: 'meet',
128
+ serviceLabel: 'Google Meet',
129
+ brandColor: '#00897B',
130
+ iconBgColor: '#00796B',
131
+ BrandIcon: SiGooglemeet,
132
+ scopes: [
133
+ 'https://www.googleapis.com/auth/calendar',
134
+ 'https://www.googleapis.com/auth/calendar.events',
135
+ ],
136
+ defaultOperation: 'create-meeting',
137
+ operations: {
138
+ 'create-meeting': {
139
+ icon: VideoCallIcon,
140
+ label: 'Create Meeting',
141
+ color: '#00897B',
142
+ description: 'Create an instant Google Meet link',
143
+ },
144
+ 'schedule-meeting': {
145
+ icon: EventIcon,
146
+ label: 'Schedule Meeting',
147
+ color: '#4285F4',
148
+ description: 'Schedule a calendar event with Meet',
149
+ },
150
+ 'get-meeting-link': {
151
+ icon: LinkIcon,
152
+ label: 'Get Meeting Link',
153
+ color: '#34A853',
154
+ description: 'Retrieve an existing Meet link',
155
+ },
156
+ },
157
+ },
158
+ };
@@ -25,6 +25,20 @@ export type {
25
25
  AutomationTelegramNodeData,
26
26
  AutomationTelegramNodeProps,
27
27
  } from './AutomationTelegramNode';
28
+ // Google Integration Nodes
29
+ export {
30
+ AutomationGmailNode,
31
+ AutomationGoogleDocsNode,
32
+ AutomationGoogleSlidesNode,
33
+ AutomationGoogleMeetNode,
34
+ AutomationGoogleServiceNode,
35
+ } from './AutomationGoogleServiceNode';
36
+ export type {
37
+ AutomationGoogleServiceNodeData,
38
+ AutomationGoogleServiceNodeProps,
39
+ } from './AutomationGoogleServiceNode';
40
+ export type { GoogleServiceType } from './googleServiceNodeConfig';
41
+ export { GOOGLE_SERVICE_CONFIGS } from './googleServiceNodeConfig';
28
42
  // Interaction Node
29
43
  export { AutomationInteractionNode } from './AutomationInteractionNode';
30
44
  export type {
@@ -0,0 +1,149 @@
1
+ // Google Identity Services OAuth helper for automation nodes
2
+
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ declare const google: any;
5
+
6
+ export interface GoogleAuthResult {
7
+ accessToken: string;
8
+ expiresAt?: number;
9
+ email?: string;
10
+ userName?: string;
11
+ userId?: string;
12
+ scopes: string[];
13
+ }
14
+
15
+ export class GoogleAuthService {
16
+ private gisLoaded = false;
17
+ private hasRequestedConsent = false;
18
+ private accessToken: string | null = null;
19
+ private expiresAt: number | undefined;
20
+ private requestedScopes = new Set<string>();
21
+
22
+ async connect(clientId: string, scopes: string[]): Promise<GoogleAuthResult> {
23
+ if (typeof window === 'undefined') {
24
+ throw new Error('Google OAuth is only available in browser environments');
25
+ }
26
+
27
+ await this.ensureGisLoaded();
28
+ const token = await this.requestAccessToken(clientId, scopes);
29
+ const userInfo = await this.fetchUserInfo(token);
30
+
31
+ return {
32
+ accessToken: token,
33
+ expiresAt: this.expiresAt,
34
+ email: userInfo.email,
35
+ userName: userInfo.name,
36
+ userId: userInfo.id,
37
+ scopes,
38
+ };
39
+ }
40
+
41
+ disconnect(): void {
42
+ if (this.accessToken && typeof google !== 'undefined') {
43
+ try {
44
+ google.accounts.oauth2.revoke(this.accessToken, () => undefined);
45
+ } catch {
46
+ // Ignore revoke failures in demo environments
47
+ }
48
+ }
49
+
50
+ this.accessToken = null;
51
+ this.expiresAt = undefined;
52
+ }
53
+
54
+ private async ensureGisLoaded(): Promise<void> {
55
+ if (this.gisLoaded) return;
56
+
57
+ await new Promise<void>((resolve, reject) => {
58
+ const existing = document.querySelector(
59
+ 'script[src="https://accounts.google.com/gsi/client"]',
60
+ );
61
+ if (existing) {
62
+ this.gisLoaded = true;
63
+ resolve();
64
+ return;
65
+ }
66
+
67
+ const script = document.createElement('script');
68
+ script.src = 'https://accounts.google.com/gsi/client';
69
+ script.async = true;
70
+ script.defer = true;
71
+ script.onload = () => {
72
+ this.gisLoaded = true;
73
+ resolve();
74
+ };
75
+ script.onerror = () => reject(new Error('Failed to load Google Identity Services'));
76
+ document.head.appendChild(script);
77
+ });
78
+ }
79
+
80
+ private async requestAccessToken(
81
+ clientId: string,
82
+ scopes: string[],
83
+ forceConsent = false,
84
+ ): Promise<string> {
85
+ await this.ensureGisLoaded();
86
+
87
+ const token = await new Promise<string>((resolve, reject) => {
88
+ try {
89
+ const tokenClient = google.accounts.oauth2.initTokenClient({
90
+ client_id: clientId,
91
+ scope: scopes.join(' '),
92
+ callback: (response: {
93
+ access_token?: string;
94
+ expires_in?: number;
95
+ error?: string;
96
+ error_description?: string;
97
+ }) => {
98
+ if (response?.access_token) {
99
+ this.hasRequestedConsent = true;
100
+ scopes.forEach((scope) => this.requestedScopes.add(scope));
101
+ if (response.expires_in) {
102
+ this.expiresAt = Date.now() + response.expires_in * 1000;
103
+ }
104
+ resolve(response.access_token);
105
+ return;
106
+ }
107
+
108
+ const err = response?.error || 'unknown_error';
109
+ const desc = response?.error_description || 'No description';
110
+ reject(new Error(`OAuth token error: ${err} - ${desc}`));
111
+ },
112
+ });
113
+
114
+ tokenClient.requestAccessToken({
115
+ prompt: forceConsent
116
+ ? 'consent'
117
+ : this.hasRequestedConsent
118
+ ? ''
119
+ : 'consent',
120
+ });
121
+ } catch (error) {
122
+ reject(error);
123
+ }
124
+ });
125
+
126
+ this.accessToken = token;
127
+ return token;
128
+ }
129
+
130
+ private async fetchUserInfo(accessToken: string): Promise<{
131
+ email?: string;
132
+ name?: string;
133
+ id?: string;
134
+ }> {
135
+ const response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
136
+ headers: {
137
+ Authorization: `Bearer ${accessToken}`,
138
+ },
139
+ });
140
+
141
+ if (!response.ok) {
142
+ return {};
143
+ }
144
+
145
+ return response.json();
146
+ }
147
+ }
148
+
149
+ export const googleAuthService = new GoogleAuthService();
@@ -566,9 +566,6 @@ export const DiagramContent: React.FC<DiagramContentProps> = ({
566
566
  ref={setNodeRef}
567
567
  sx={{ height: '100svh', width: '100svw', position: 'relative' }}
568
568
  >
569
- {/* Redo Toolbar */}
570
-
571
-
572
569
  <ReactFlow
573
570
  panOnDrag={isPannable}
574
571
  nodesDraggable={isPannable}
@@ -12,6 +12,10 @@ import {
12
12
  AutomationNavigationNode,
13
13
  AutomationSlackNode,
14
14
  AutomationTelegramNode,
15
+ AutomationGmailNode,
16
+ AutomationGoogleDocsNode,
17
+ AutomationGoogleSlidesNode,
18
+ AutomationGoogleMeetNode,
15
19
  AutomationInteractionNode,
16
20
  AutomationMonitoringNode,
17
21
  AutomationDecisionNode,
@@ -34,6 +38,10 @@ const nodeTypes = {
34
38
  AutomationNavigationNode,
35
39
  AutomationSlackNode,
36
40
  AutomationTelegramNode,
41
+ AutomationGmailNode,
42
+ AutomationGoogleDocsNode,
43
+ AutomationGoogleSlidesNode,
44
+ AutomationGoogleMeetNode,
37
45
  AutomationInteractionNode,
38
46
  AutomationMonitoringNode,
39
47
  AutomationDecisionNode,