@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.
- package/apps/diagramming/src/AutomationDiagramData.ts +198 -0
- package/apps/diagramming/src/main.tsx +1 -3
- package/apps/workflow/locales/en/translation.json +5 -1
- package/package.json +1 -1
- package/packages/diagrams/locales/en/translation.json +5 -1
- package/packages/diagrams/src/lib/components/automation/AutomationGoogleServiceNode.tsx +680 -0
- package/packages/diagrams/src/lib/components/automation/googleServiceNodeConfig.ts +158 -0
- package/packages/diagrams/src/lib/components/automation/index.ts +14 -0
- package/packages/diagrams/src/lib/services/GoogleAuthService.ts +149 -0
- package/packages/diagrams/src/lib/templates/DiagramContent.tsx +0 -3
- package/packages/diagrams/src/lib/types/node-types.ts +8 -0
|
@@ -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
|
{
|
|
@@ -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
|
@@ -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,
|