@flowuent-org/diagramming-core 1.2.2 → 1.2.3

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.
@@ -525,6 +525,163 @@ export const automationDefaultNodes = [
525
525
  height: 150,
526
526
  measured: { width: 336, height: 150 },
527
527
  },
528
+ // =====================================
529
+ // Google Services Nodes
530
+ // =====================================
531
+ {
532
+ id: 'google-docs-node',
533
+ type: 'AutomationGoogleDocsNode',
534
+ position: { x: 100, y: 450 },
535
+ data: {
536
+ label: 'Create Report',
537
+ description: 'Google Docs document එකක් create කරන්න',
538
+ serviceType: 'docs',
539
+ status: 'idle',
540
+ parameters: {
541
+ documentTitle: 'Automation Report',
542
+ operation: 'create',
543
+ content: 'Report content will be generated here...',
544
+ },
545
+ googleAuth: {
546
+ clientId: '', // Will be set from environment
547
+ scopes: [
548
+ 'https://www.googleapis.com/auth/documents',
549
+ 'https://www.googleapis.com/auth/drive',
550
+ ],
551
+ },
552
+ formData: {
553
+ nodeId: 'google-docs-node',
554
+ title: 'Create Report',
555
+ type: 'navigation',
556
+ serviceType: 'docs',
557
+ },
558
+ },
559
+ width: 300,
560
+ height: 200,
561
+ measured: { width: 300, height: 200 },
562
+ },
563
+ {
564
+ id: 'gmail-node',
565
+ type: 'AutomationGmailNode',
566
+ position: { x: 450, y: 450 },
567
+ data: {
568
+ label: 'Send Email',
569
+ description: 'Gmail හරහා email එකක් send කරන්න',
570
+ serviceType: 'gmail',
571
+ status: 'idle',
572
+ parameters: {
573
+ to: ['recipient@example.com'],
574
+ subject: 'Automation Report',
575
+ body: 'Please find the attached report...',
576
+ },
577
+ googleAuth: {
578
+ clientId: '',
579
+ scopes: [
580
+ 'https://www.googleapis.com/auth/gmail.send',
581
+ 'https://www.googleapis.com/auth/gmail.compose',
582
+ ],
583
+ },
584
+ formData: {
585
+ nodeId: 'gmail-node',
586
+ title: 'Send Email',
587
+ type: 'navigation',
588
+ serviceType: 'gmail',
589
+ },
590
+ },
591
+ width: 300,
592
+ height: 200,
593
+ measured: { width: 300, height: 200 },
594
+ },
595
+ {
596
+ id: 'google-meet-node',
597
+ type: 'AutomationGoogleMeetNode',
598
+ position: { x: 800, y: 450 },
599
+ data: {
600
+ label: 'Create Meeting',
601
+ description: 'Google Meet link එකක් create කරන්න',
602
+ serviceType: 'meet',
603
+ status: 'idle',
604
+ parameters: {
605
+ meetingTitle: 'Project Discussion',
606
+ attendees: ['team@example.com'],
607
+ },
608
+ googleAuth: {
609
+ clientId: '',
610
+ scopes: ['https://www.googleapis.com/auth/meetings.space.created'],
611
+ },
612
+ formData: {
613
+ nodeId: 'google-meet-node',
614
+ title: 'Create Meeting',
615
+ type: 'navigation',
616
+ serviceType: 'meet',
617
+ },
618
+ },
619
+ width: 300,
620
+ height: 200,
621
+ measured: { width: 300, height: 200 },
622
+ },
623
+ {
624
+ id: 'google-slides-node',
625
+ type: 'AutomationGoogleSlidesNode',
626
+ position: { x: 100, y: 700 },
627
+ data: {
628
+ label: 'Create Presentation',
629
+ description: 'Google Slides presentation එකක් create කරන්න',
630
+ serviceType: 'slides',
631
+ status: 'idle',
632
+ parameters: {
633
+ presentationTitle: 'Monthly Report',
634
+ slideContent: 'Slide content here...',
635
+ },
636
+ googleAuth: {
637
+ clientId: '',
638
+ scopes: [
639
+ 'https://www.googleapis.com/auth/presentations',
640
+ 'https://www.googleapis.com/auth/drive',
641
+ ],
642
+ },
643
+ formData: {
644
+ nodeId: 'google-slides-node',
645
+ title: 'Create Presentation',
646
+ type: 'navigation',
647
+ serviceType: 'slides',
648
+ },
649
+ },
650
+ width: 300,
651
+ height: 200,
652
+ measured: { width: 300, height: 200 },
653
+ },
654
+ {
655
+ id: 'google-calendar-node',
656
+ type: 'AutomationGoogleCalendarNode',
657
+ position: { x: 450, y: 700 },
658
+ data: {
659
+ label: 'Create Event',
660
+ description: 'Google Calendar event එකක් create කරන්න',
661
+ serviceType: 'calendar',
662
+ status: 'idle',
663
+ parameters: {
664
+ calendarId: 'primary',
665
+ eventTitle: 'Team Meeting',
666
+ eventDescription: 'Weekly sync up',
667
+ startTime: new Date().toISOString(),
668
+ endTime: new Date(Date.now() + 3600000).toISOString(),
669
+ },
670
+ googleAuth: {
671
+ clientId: '',
672
+ scopes: ['https://www.googleapis.com/auth/calendar'],
673
+ },
674
+ formData: {
675
+ nodeId: 'google-calendar-node',
676
+ title: 'Create Event',
677
+ type: 'navigation',
678
+ serviceType: 'calendar',
679
+ },
680
+ },
681
+ width: 300,
682
+ height: 200,
683
+ measured: { width: 300, height: 200 },
684
+ },
528
685
  ];
529
686
 
530
687
  // Default edges for the automation diagram
@@ -624,4 +781,64 @@ export const automationDefaultEdges = [
624
781
  color: '#ffffff',
625
782
  },
626
783
  },
784
+ // =====================================
785
+ // Google Services Node Edges
786
+ // =====================================
787
+ {
788
+ id: 'edge-docs-to-gmail',
789
+ source: 'google-docs-node',
790
+ target: 'gmail-node',
791
+ sourceHandle: 'right',
792
+ targetHandle: 'left',
793
+ data: {
794
+ label: 'Send Report',
795
+ type: 'flow',
796
+ },
797
+ style: {
798
+ stroke: '#4285F4',
799
+ strokeWidth: 2,
800
+ },
801
+ markerEnd: {
802
+ type: MarkerType.ArrowClosed,
803
+ color: '#4285F4',
804
+ },
805
+ },
806
+ {
807
+ id: 'edge-gmail-to-meet',
808
+ source: 'gmail-node',
809
+ target: 'google-meet-node',
810
+ sourceHandle: 'right',
811
+ targetHandle: 'left',
812
+ data: {
813
+ label: 'Schedule Meet',
814
+ type: 'flow',
815
+ },
816
+ style: {
817
+ stroke: '#EA4335',
818
+ strokeWidth: 2,
819
+ },
820
+ markerEnd: {
821
+ type: MarkerType.ArrowClosed,
822
+ color: '#EA4335',
823
+ },
824
+ },
825
+ {
826
+ id: 'edge-slides-to-calendar',
827
+ source: 'google-slides-node',
828
+ target: 'google-calendar-node',
829
+ sourceHandle: 'right',
830
+ targetHandle: 'left',
831
+ data: {
832
+ label: 'Add to Calendar',
833
+ type: 'flow',
834
+ },
835
+ style: {
836
+ stroke: '#FBBC04',
837
+ strokeWidth: 2,
838
+ },
839
+ markerEnd: {
840
+ type: MarkerType.ArrowClosed,
841
+ color: '#FBBC04',
842
+ },
843
+ },
627
844
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowuent-org/diagramming-core",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,1021 @@
1
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
2
+ import { Handle, Position, useNodeId } from '@xyflow/react';
3
+ import {
4
+ Box,
5
+ Typography,
6
+ Chip,
7
+ IconButton,
8
+ Button,
9
+ CircularProgress,
10
+ Tooltip,
11
+ LinearProgress,
12
+ } from '@mui/material';
13
+ import {
14
+ Description as DocsIcon,
15
+ Slideshow as SlidesIcon,
16
+ VideoCall as MeetIcon,
17
+ Email as GmailIcon,
18
+ PlayArrow as PlayIcon,
19
+ Stop as StopIcon,
20
+ CheckCircle as CheckCircleIcon,
21
+ Error as ErrorIcon,
22
+ Settings as SettingsIcon,
23
+ Link as LinkIcon,
24
+ LinkOff as LinkOffIcon,
25
+ Refresh as RefreshIcon,
26
+ TableChart as SheetsIcon,
27
+ CalendarMonth as CalendarIcon,
28
+ } from '@mui/icons-material';
29
+ import { FcGoogle } from 'react-icons/fc';
30
+ import { useTranslation } from 'react-i18next';
31
+ import { useDiagram } from '../../contexts/DiagramProvider';
32
+ import { useSearch } from '../../contexts/SearchContext';
33
+ import { NodeActionButtons } from './NodeActionButtons';
34
+
35
+ // ========================
36
+ // Types / Interfaces
37
+ // ========================
38
+
39
+ export type GoogleServiceType =
40
+ | 'docs'
41
+ | 'slides'
42
+ | 'meet'
43
+ | 'gmail'
44
+ | 'sheets'
45
+ | 'calendar';
46
+
47
+ export type NodeStatus =
48
+ | 'idle'
49
+ | 'authenticating'
50
+ | 'authenticated'
51
+ | 'running'
52
+ | 'completed'
53
+ | 'error';
54
+
55
+ export interface GoogleTokenData {
56
+ accessToken: string;
57
+ expiresAt: number;
58
+ email?: string;
59
+ scope?: string;
60
+ }
61
+
62
+ export interface AutomationGoogleServicesNodeData {
63
+ label: string;
64
+ description?: string;
65
+ serviceType: GoogleServiceType;
66
+ status?: NodeStatus;
67
+ iconName?: string;
68
+
69
+ // Google Auth config
70
+ googleAuth?: {
71
+ clientId?: string;
72
+ scopes?: string[];
73
+ tokens?: GoogleTokenData;
74
+ };
75
+
76
+ // Service-specific parameters
77
+ parameters?: {
78
+ // Google Docs
79
+ documentId?: string;
80
+ documentTitle?: string;
81
+ content?: string;
82
+ operation?: 'create' | 'read' | 'update' | 'append';
83
+
84
+ // Google Slides
85
+ presentationId?: string;
86
+ presentationTitle?: string;
87
+ slideContent?: string;
88
+
89
+ // Google Meet
90
+ meetingTitle?: string;
91
+ startTime?: string;
92
+ endTime?: string;
93
+ attendees?: string[];
94
+
95
+ // Gmail
96
+ to?: string[];
97
+ subject?: string;
98
+ body?: string;
99
+ attachments?: string[];
100
+
101
+ // Google Sheets
102
+ spreadsheetId?: string;
103
+ sheetName?: string;
104
+ range?: string;
105
+ data?: any[];
106
+
107
+ // Google Calendar
108
+ calendarId?: string;
109
+ eventTitle?: string;
110
+ eventDescription?: string;
111
+
112
+ // Stored tokens
113
+ useUserAccount?: boolean;
114
+ googleAccessToken?: string;
115
+ googleTokenExpiresAt?: number;
116
+ googleUserEmail?: string;
117
+ };
118
+
119
+ // Form data for configuration
120
+ formData?: {
121
+ aiSuggestionsCount?: number;
122
+ [key: string]: any;
123
+ };
124
+
125
+ // Execution result
126
+ executionResult?: {
127
+ success: boolean;
128
+ data?: any;
129
+ error?: string;
130
+ timestamp?: string;
131
+ executionTime?: number;
132
+ };
133
+
134
+ // Callbacks
135
+ onExecute?: (tokens: GoogleTokenData) => Promise<any>;
136
+ onAuthSuccess?: (tokens: GoogleTokenData) => void;
137
+ onAuthError?: (error: Error) => void;
138
+ }
139
+
140
+ export interface AutomationGoogleServicesNodeProps {
141
+ data: AutomationGoogleServicesNodeData;
142
+ selected?: boolean;
143
+ }
144
+
145
+ // ========================
146
+ // Constants
147
+ // ========================
148
+
149
+ const SERVICE_CONFIG: Record<
150
+ GoogleServiceType,
151
+ {
152
+ icon: React.ElementType;
153
+ label: string;
154
+ color: string;
155
+ scopes: string[];
156
+ description: string;
157
+ }
158
+ > = {
159
+ docs: {
160
+ icon: DocsIcon,
161
+ label: 'Google Docs',
162
+ color: '#4285F4',
163
+ scopes: [
164
+ 'https://www.googleapis.com/auth/documents',
165
+ 'https://www.googleapis.com/auth/drive',
166
+ ],
167
+ description: 'Document එකක් create/edit කරන්න',
168
+ },
169
+ slides: {
170
+ icon: SlidesIcon,
171
+ label: 'Google Slides',
172
+ color: '#FBBC04',
173
+ scopes: [
174
+ 'https://www.googleapis.com/auth/presentations',
175
+ 'https://www.googleapis.com/auth/drive',
176
+ ],
177
+ description: 'Presentation එකක් create/edit කරන්න',
178
+ },
179
+ meet: {
180
+ icon: MeetIcon,
181
+ label: 'Google Meet',
182
+ color: '#34A853',
183
+ scopes: ['https://www.googleapis.com/auth/meetings.space.created'],
184
+ description: 'Meeting එකක් create කරන්න',
185
+ },
186
+ gmail: {
187
+ icon: GmailIcon,
188
+ label: 'Gmail',
189
+ color: '#EA4335',
190
+ scopes: [
191
+ 'https://www.googleapis.com/auth/gmail.send',
192
+ 'https://www.googleapis.com/auth/gmail.compose',
193
+ ],
194
+ description: 'Email එකක් send කරන්න',
195
+ },
196
+ sheets: {
197
+ icon: SheetsIcon,
198
+ label: 'Google Sheets',
199
+ color: '#0F9D58',
200
+ scopes: ['https://www.googleapis.com/auth/spreadsheets'],
201
+ description: 'Spreadsheet එකක් read/write කරන්න',
202
+ },
203
+ calendar: {
204
+ icon: CalendarIcon,
205
+ label: 'Google Calendar',
206
+ color: '#4285F4',
207
+ scopes: ['https://www.googleapis.com/auth/calendar'],
208
+ description: 'Calendar event එකක් create කරන්න',
209
+ },
210
+ };
211
+
212
+ // ========================
213
+ // Helper Functions
214
+ // ========================
215
+
216
+ const isTokenExpired = (expiresAt?: number): boolean => {
217
+ if (!expiresAt) return true;
218
+ // Token expires 5 minutes before actual expiry for safety
219
+ return Date.now() > expiresAt - 5 * 60 * 1000;
220
+ };
221
+
222
+ const getTimeUntilExpiry = (expiresAt?: number): string => {
223
+ if (!expiresAt) return 'N/A';
224
+ const remaining = expiresAt - Date.now();
225
+ if (remaining <= 0) return 'කල් ඉකුත් වී ඇත';
226
+ const minutes = Math.floor(remaining / 60000);
227
+ if (minutes < 60) return `${minutes} min`;
228
+ const hours = Math.floor(minutes / 60);
229
+ return `${hours}h ${minutes % 60}m`;
230
+ };
231
+
232
+ // ========================
233
+ // Main Component
234
+ // ========================
235
+
236
+ export const AutomationGoogleServicesNode: React.FC<
237
+ AutomationGoogleServicesNodeProps
238
+ > = ({ data, selected }) => {
239
+ const { t } = useTranslation();
240
+ const { highlightText } = useSearch();
241
+ const nodeId = useNodeId();
242
+ const setSelectedNode = useDiagram((state) => state.setSelectedNode);
243
+ const nodes = useDiagram((state) => state.nodes);
244
+ const setNodes = useDiagram((state) => state.setNodes);
245
+
246
+ // ========================
247
+ // State
248
+ // ========================
249
+ const [status, setStatus] = useState<NodeStatus>(data.status || 'idle');
250
+ const [tokens, setTokens] = useState<GoogleTokenData | null>(null);
251
+ const [isExecuting, setIsExecuting] = useState(false);
252
+ const [executionProgress, setExecutionProgress] = useState(0);
253
+ const [error, setError] = useState<string | null>(null);
254
+ const [result, setResult] = useState<any>(null);
255
+
256
+ const googleClientRef = useRef<any>(null);
257
+ const serviceType = data.serviceType || 'docs';
258
+ const serviceConfig = SERVICE_CONFIG[serviceType];
259
+ const ServiceIcon = serviceConfig.icon;
260
+
261
+ // ========================
262
+ // Initialize tokens from data
263
+ // ========================
264
+ useEffect(() => {
265
+ if (data.parameters?.googleAccessToken) {
266
+ const savedTokens: GoogleTokenData = {
267
+ accessToken: data.parameters.googleAccessToken,
268
+ expiresAt: data.parameters.googleTokenExpiresAt || 0,
269
+ email: data.parameters.googleUserEmail,
270
+ };
271
+
272
+ if (!isTokenExpired(savedTokens.expiresAt)) {
273
+ setTokens(savedTokens);
274
+ setStatus('authenticated');
275
+ } else {
276
+ setStatus('idle');
277
+ }
278
+ }
279
+ }, [data.parameters?.googleAccessToken]);
280
+
281
+ // ========================
282
+ // Google OAuth Login
283
+ // ========================
284
+ const handleGoogleLogin = useCallback(async () => {
285
+ setStatus('authenticating');
286
+ setError(null);
287
+
288
+ try {
289
+ // Load Google Identity Services if not loaded
290
+ if (!(window as any).google?.accounts?.oauth2) {
291
+ await new Promise<void>((resolve, reject) => {
292
+ const script = document.createElement('script');
293
+ script.src = 'https://accounts.google.com/gsi/client';
294
+ script.async = true;
295
+ script.defer = true;
296
+ script.onload = () => resolve();
297
+ script.onerror = () => reject(new Error('Failed to load Google Identity Services'));
298
+ document.head.appendChild(script);
299
+ });
300
+ }
301
+
302
+ // Initialize OAuth client
303
+ const clientId = data.googleAuth?.clientId || process.env.REACT_APP_GOOGLE_CLIENT_ID;
304
+
305
+ if (!clientId) {
306
+ throw new Error('Google Client ID not configured');
307
+ }
308
+
309
+ const tokenClient = (window as any).google.accounts.oauth2.initTokenClient({
310
+ client_id: clientId,
311
+ scope: serviceConfig.scopes.join(' '),
312
+ callback: (response: any) => {
313
+ if (response.error) {
314
+ setError(response.error_description || 'Authentication failed');
315
+ setStatus('error');
316
+ data.onAuthError?.(new Error(response.error_description));
317
+ return;
318
+ }
319
+
320
+ const newTokens: GoogleTokenData = {
321
+ accessToken: response.access_token,
322
+ expiresAt: Date.now() + response.expires_in * 1000,
323
+ scope: response.scope,
324
+ };
325
+
326
+ // Get user email
327
+ fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
328
+ headers: { Authorization: `Bearer ${response.access_token}` },
329
+ })
330
+ .then((res) => res.json())
331
+ .then((userInfo) => {
332
+ newTokens.email = userInfo.email;
333
+ setTokens(newTokens);
334
+ setStatus('authenticated');
335
+ data.onAuthSuccess?.(newTokens);
336
+
337
+ // Save tokens to node data
338
+ updateNodeData({
339
+ parameters: {
340
+ ...data.parameters,
341
+ useUserAccount: true,
342
+ googleAccessToken: newTokens.accessToken,
343
+ googleTokenExpiresAt: newTokens.expiresAt,
344
+ googleUserEmail: newTokens.email,
345
+ },
346
+ });
347
+ })
348
+ .catch(() => {
349
+ setTokens(newTokens);
350
+ setStatus('authenticated');
351
+ data.onAuthSuccess?.(newTokens);
352
+ });
353
+ },
354
+ });
355
+
356
+ googleClientRef.current = tokenClient;
357
+ tokenClient.requestAccessToken();
358
+ } catch (err: any) {
359
+ setError(err.message || 'Authentication failed');
360
+ setStatus('error');
361
+ data.onAuthError?.(err);
362
+ }
363
+ }, [data, serviceConfig.scopes]);
364
+
365
+ // ========================
366
+ // Disconnect Google Account
367
+ // ========================
368
+ const handleDisconnect = useCallback(() => {
369
+ setTokens(null);
370
+ setStatus('idle');
371
+ setResult(null);
372
+ setError(null);
373
+
374
+ // Clear tokens from node data
375
+ updateNodeData({
376
+ parameters: {
377
+ ...data.parameters,
378
+ useUserAccount: false,
379
+ googleAccessToken: undefined,
380
+ googleTokenExpiresAt: undefined,
381
+ googleUserEmail: undefined,
382
+ },
383
+ });
384
+ }, [data.parameters]);
385
+
386
+ // ========================
387
+ // Execute Node
388
+ // ========================
389
+ const handleExecute = useCallback(async () => {
390
+ // Check if authenticated
391
+ if (!tokens || isTokenExpired(tokens.expiresAt)) {
392
+ // Need to authenticate first
393
+ handleGoogleLogin();
394
+ return;
395
+ }
396
+
397
+ setIsExecuting(true);
398
+ setStatus('running');
399
+ setExecutionProgress(0);
400
+ setError(null);
401
+
402
+ try {
403
+ // Simulate progress
404
+ const progressInterval = setInterval(() => {
405
+ setExecutionProgress((prev) => Math.min(prev + 10, 90));
406
+ }, 200);
407
+
408
+ // Execute the actual operation
409
+ let executionResult: any;
410
+
411
+ if (data.onExecute) {
412
+ executionResult = await data.onExecute(tokens);
413
+ } else {
414
+ // Default execution based on service type
415
+ executionResult = await executeGoogleService(serviceType, tokens, data.parameters);
416
+ }
417
+
418
+ clearInterval(progressInterval);
419
+ setExecutionProgress(100);
420
+ setResult(executionResult);
421
+ setStatus('completed');
422
+
423
+ // Update node data with result
424
+ updateNodeData({
425
+ executionResult: {
426
+ success: true,
427
+ data: executionResult,
428
+ timestamp: new Date().toISOString(),
429
+ },
430
+ });
431
+ } catch (err: any) {
432
+ setError(err.message || 'Execution failed');
433
+ setStatus('error');
434
+
435
+ // Update node data with error
436
+ updateNodeData({
437
+ executionResult: {
438
+ success: false,
439
+ error: err.message,
440
+ timestamp: new Date().toISOString(),
441
+ },
442
+ });
443
+ } finally {
444
+ setIsExecuting(false);
445
+ }
446
+ }, [tokens, data, serviceType, handleGoogleLogin]);
447
+
448
+ // ========================
449
+ // Update Node Data
450
+ // ========================
451
+ const updateNodeData = useCallback(
452
+ (updates: Partial<AutomationGoogleServicesNodeData>) => {
453
+ if (!nodeId) return;
454
+
455
+ setNodes(
456
+ nodes.map((node) => {
457
+ if (node.id === nodeId) {
458
+ return {
459
+ ...node,
460
+ data: {
461
+ ...node.data,
462
+ ...updates,
463
+ },
464
+ };
465
+ }
466
+ return node;
467
+ })
468
+ );
469
+ },
470
+ [nodeId, nodes, setNodes]
471
+ );
472
+
473
+ // ========================
474
+ // Execute Google Service
475
+ // ========================
476
+ const executeGoogleService = async (
477
+ service: GoogleServiceType,
478
+ tokenData: GoogleTokenData,
479
+ params?: any
480
+ ): Promise<any> => {
481
+ const headers = {
482
+ Authorization: `Bearer ${tokenData.accessToken}`,
483
+ 'Content-Type': 'application/json',
484
+ };
485
+
486
+ switch (service) {
487
+ case 'docs': {
488
+ // Create or update Google Doc
489
+ const docTitle = params?.documentTitle || 'New Document';
490
+ const response = await fetch(
491
+ 'https://docs.googleapis.com/v1/documents',
492
+ {
493
+ method: 'POST',
494
+ headers,
495
+ body: JSON.stringify({ title: docTitle }),
496
+ }
497
+ );
498
+ return response.json();
499
+ }
500
+
501
+ case 'slides': {
502
+ // Create Google Slides presentation
503
+ const slideTitle = params?.presentationTitle || 'New Presentation';
504
+ const response = await fetch(
505
+ 'https://slides.googleapis.com/v1/presentations',
506
+ {
507
+ method: 'POST',
508
+ headers,
509
+ body: JSON.stringify({ title: slideTitle }),
510
+ }
511
+ );
512
+ return response.json();
513
+ }
514
+
515
+ case 'meet': {
516
+ // Create Google Meet link
517
+ const response = await fetch(
518
+ 'https://meet.googleapis.com/v2/spaces',
519
+ {
520
+ method: 'POST',
521
+ headers,
522
+ body: JSON.stringify({
523
+ config: {
524
+ accessType: 'OPEN',
525
+ entryPointAccess: 'ALL',
526
+ },
527
+ }),
528
+ }
529
+ );
530
+ return response.json();
531
+ }
532
+
533
+ case 'gmail': {
534
+ // Send Gmail
535
+ const to = params?.to?.join(', ') || '';
536
+ const subject = params?.subject || 'No Subject';
537
+ const body = params?.body || '';
538
+
539
+ const email = [
540
+ `To: ${to}`,
541
+ `Subject: ${subject}`,
542
+ '',
543
+ body,
544
+ ].join('\n');
545
+
546
+ const encodedEmail = btoa(unescape(encodeURIComponent(email)))
547
+ .replace(/\+/g, '-')
548
+ .replace(/\//g, '_')
549
+ .replace(/=+$/, '');
550
+
551
+ const response = await fetch(
552
+ 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send',
553
+ {
554
+ method: 'POST',
555
+ headers,
556
+ body: JSON.stringify({ raw: encodedEmail }),
557
+ }
558
+ );
559
+ return response.json();
560
+ }
561
+
562
+ case 'sheets': {
563
+ // Read/Write Google Sheets
564
+ const spreadsheetId = params?.spreadsheetId;
565
+ const range = params?.range || 'Sheet1!A1:Z100';
566
+
567
+ if (!spreadsheetId) {
568
+ // Create new spreadsheet
569
+ const response = await fetch(
570
+ 'https://sheets.googleapis.com/v4/spreadsheets',
571
+ {
572
+ method: 'POST',
573
+ headers,
574
+ body: JSON.stringify({
575
+ properties: {
576
+ title: params?.sheetName || 'New Spreadsheet',
577
+ },
578
+ }),
579
+ }
580
+ );
581
+ return response.json();
582
+ }
583
+
584
+ // Read existing spreadsheet
585
+ const response = await fetch(
586
+ `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${range}`,
587
+ { headers }
588
+ );
589
+ return response.json();
590
+ }
591
+
592
+ case 'calendar': {
593
+ // Create calendar event
594
+ const calendarId = params?.calendarId || 'primary';
595
+ const response = await fetch(
596
+ `https://www.googleapis.com/calendar/v3/calendars/${calendarId}/events`,
597
+ {
598
+ method: 'POST',
599
+ headers,
600
+ body: JSON.stringify({
601
+ summary: params?.eventTitle || 'New Event',
602
+ description: params?.eventDescription,
603
+ start: {
604
+ dateTime: params?.startTime || new Date().toISOString(),
605
+ timeZone: 'UTC',
606
+ },
607
+ end: {
608
+ dateTime:
609
+ params?.endTime ||
610
+ new Date(Date.now() + 3600000).toISOString(),
611
+ timeZone: 'UTC',
612
+ },
613
+ }),
614
+ }
615
+ );
616
+ return response.json();
617
+ }
618
+
619
+ default:
620
+ throw new Error(`Unsupported service: ${service}`);
621
+ }
622
+ };
623
+
624
+ // ========================
625
+ // Render Status Badge
626
+ // ========================
627
+ const renderStatusBadge = () => {
628
+ const statusConfig: Record<
629
+ NodeStatus,
630
+ { color: string; bgColor: string; label: string; icon?: React.ReactNode }
631
+ > = {
632
+ idle: {
633
+ color: '#6B7280',
634
+ bgColor: 'rgba(107, 114, 128, 0.1)',
635
+ label: 'සූදානම්',
636
+ },
637
+ authenticating: {
638
+ color: '#F59E0B',
639
+ bgColor: 'rgba(245, 158, 11, 0.1)',
640
+ label: 'සත්‍යාපනය...',
641
+ icon: <CircularProgress size={12} color="inherit" />,
642
+ },
643
+ authenticated: {
644
+ color: '#10B981',
645
+ bgColor: 'rgba(16, 185, 129, 0.1)',
646
+ label: 'සම්බන්ධයි',
647
+ icon: <CheckCircleIcon sx={{ fontSize: 12 }} />,
648
+ },
649
+ running: {
650
+ color: '#3B82F6',
651
+ bgColor: 'rgba(59, 130, 246, 0.1)',
652
+ label: 'ක්‍රියාත්මක...',
653
+ icon: <CircularProgress size={12} color="inherit" />,
654
+ },
655
+ completed: {
656
+ color: '#10B981',
657
+ bgColor: 'rgba(16, 185, 129, 0.1)',
658
+ label: 'සාර්ථකයි',
659
+ icon: <CheckCircleIcon sx={{ fontSize: 12 }} />,
660
+ },
661
+ error: {
662
+ color: '#EF4444',
663
+ bgColor: 'rgba(239, 68, 68, 0.1)',
664
+ label: 'දෝෂය',
665
+ icon: <ErrorIcon sx={{ fontSize: 12 }} />,
666
+ },
667
+ };
668
+
669
+ const config = statusConfig[status];
670
+
671
+ return (
672
+ <Chip
673
+ size="small"
674
+ label={
675
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
676
+ {config.icon}
677
+ <span>{config.label}</span>
678
+ </Box>
679
+ }
680
+ sx={{
681
+ height: 20,
682
+ fontSize: '10px',
683
+ fontWeight: 600,
684
+ color: config.color,
685
+ bgcolor: config.bgColor,
686
+ border: `1px solid ${config.color}20`,
687
+ '& .MuiChip-label': { px: 1 },
688
+ }}
689
+ />
690
+ );
691
+ };
692
+
693
+ // ========================
694
+ // Render
695
+ // ========================
696
+ return (
697
+ <Box
698
+ sx={{
699
+ bgcolor: '#1E1E2E',
700
+ borderRadius: 2,
701
+ border: selected ? `2px solid ${serviceConfig.color}` : '1px solid #374151',
702
+ minWidth: 280,
703
+ maxWidth: 320,
704
+ overflow: 'hidden',
705
+ boxShadow: selected
706
+ ? `0 0 20px ${serviceConfig.color}30`
707
+ : '0 4px 12px rgba(0, 0, 0, 0.3)',
708
+ transition: 'all 0.2s ease',
709
+ }}
710
+ onClick={() => setSelectedNode(nodeId || '')}
711
+ >
712
+ {/* Input Handle */}
713
+ <Handle
714
+ type="target"
715
+ position={Position.Left}
716
+ style={{
717
+ background: serviceConfig.color,
718
+ width: 10,
719
+ height: 10,
720
+ border: '2px solid #1E1E2E',
721
+ }}
722
+ />
723
+
724
+ {/* Header */}
725
+ <Box
726
+ sx={{
727
+ bgcolor: `${serviceConfig.color}15`,
728
+ borderBottom: `1px solid ${serviceConfig.color}30`,
729
+ px: 2,
730
+ py: 1.5,
731
+ display: 'flex',
732
+ alignItems: 'center',
733
+ justifyContent: 'space-between',
734
+ }}
735
+ >
736
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
737
+ <Box
738
+ sx={{
739
+ bgcolor: serviceConfig.color,
740
+ borderRadius: 1,
741
+ p: 0.75,
742
+ display: 'flex',
743
+ alignItems: 'center',
744
+ justifyContent: 'center',
745
+ }}
746
+ >
747
+ <ServiceIcon sx={{ fontSize: 18, color: '#fff' }} />
748
+ </Box>
749
+ <Box>
750
+ <Typography
751
+ variant="subtitle2"
752
+ sx={{ color: '#fff', fontWeight: 600, fontSize: 13 }}
753
+ >
754
+ {highlightText(data.label || serviceConfig.label)}
755
+ </Typography>
756
+ <Typography
757
+ variant="caption"
758
+ sx={{ color: '#9CA3AF', fontSize: 10 }}
759
+ >
760
+ {serviceConfig.description}
761
+ </Typography>
762
+ </Box>
763
+ </Box>
764
+ {renderStatusBadge()}
765
+ </Box>
766
+
767
+ {/* Content */}
768
+ <Box sx={{ px: 2, py: 1.5 }}>
769
+ {/* Authentication Status */}
770
+ <Box
771
+ sx={{
772
+ bgcolor: tokens ? 'rgba(16, 185, 129, 0.1)' : 'rgba(107, 114, 128, 0.1)',
773
+ borderRadius: 1,
774
+ p: 1.5,
775
+ mb: 1.5,
776
+ }}
777
+ >
778
+ {tokens ? (
779
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
780
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
781
+ <FcGoogle size={20} />
782
+ <Box>
783
+ <Typography
784
+ variant="caption"
785
+ sx={{ color: '#10B981', display: 'block', fontWeight: 600 }}
786
+ >
787
+ සම්බන්ධයි
788
+ </Typography>
789
+ <Typography
790
+ variant="caption"
791
+ sx={{ color: '#9CA3AF', fontSize: 10 }}
792
+ >
793
+ {tokens.email || 'Google Account'}
794
+ </Typography>
795
+ </Box>
796
+ </Box>
797
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
798
+ <Tooltip title={`කල් ඉකුත් වන්නේ: ${getTimeUntilExpiry(tokens.expiresAt)}`}>
799
+ <Typography
800
+ variant="caption"
801
+ sx={{
802
+ color: isTokenExpired(tokens.expiresAt) ? '#EF4444' : '#9CA3AF',
803
+ fontSize: 10,
804
+ }}
805
+ >
806
+ {getTimeUntilExpiry(tokens.expiresAt)}
807
+ </Typography>
808
+ </Tooltip>
809
+ <IconButton
810
+ size="small"
811
+ onClick={handleDisconnect}
812
+ sx={{ color: '#EF4444', p: 0.5 }}
813
+ >
814
+ <LinkOffIcon sx={{ fontSize: 14 }} />
815
+ </IconButton>
816
+ </Box>
817
+ </Box>
818
+ ) : (
819
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
820
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
821
+ <FcGoogle size={20} style={{ opacity: 0.5 }} />
822
+ <Typography variant="caption" sx={{ color: '#6B7280' }}>
823
+ Google ගිණුම සම්බන්ධ කර නැත
824
+ </Typography>
825
+ </Box>
826
+ <Button
827
+ size="small"
828
+ variant="outlined"
829
+ onClick={handleGoogleLogin}
830
+ disabled={status === 'authenticating'}
831
+ startIcon={
832
+ status === 'authenticating' ? (
833
+ <CircularProgress size={12} color="inherit" />
834
+ ) : (
835
+ <LinkIcon sx={{ fontSize: 14 }} />
836
+ )
837
+ }
838
+ sx={{
839
+ fontSize: 10,
840
+ py: 0.25,
841
+ px: 1,
842
+ borderColor: '#374151',
843
+ color: '#9CA3AF',
844
+ '&:hover': {
845
+ borderColor: serviceConfig.color,
846
+ color: serviceConfig.color,
847
+ },
848
+ }}
849
+ >
850
+ සම්බන්ධ කරන්න
851
+ </Button>
852
+ </Box>
853
+ )}
854
+ </Box>
855
+
856
+ {/* Progress Bar (when running) */}
857
+ {status === 'running' && (
858
+ <Box sx={{ mb: 1.5 }}>
859
+ <LinearProgress
860
+ variant="determinate"
861
+ value={executionProgress}
862
+ sx={{
863
+ height: 4,
864
+ borderRadius: 2,
865
+ bgcolor: 'rgba(59, 130, 246, 0.1)',
866
+ '& .MuiLinearProgress-bar': {
867
+ bgcolor: serviceConfig.color,
868
+ },
869
+ }}
870
+ />
871
+ <Typography
872
+ variant="caption"
873
+ sx={{ color: '#9CA3AF', fontSize: 10, mt: 0.5, display: 'block' }}
874
+ >
875
+ ක්‍රියාත්මක වෙමින්... {executionProgress}%
876
+ </Typography>
877
+ </Box>
878
+ )}
879
+
880
+ {/* Error Display */}
881
+ {error && (
882
+ <Box
883
+ sx={{
884
+ bgcolor: 'rgba(239, 68, 68, 0.1)',
885
+ borderRadius: 1,
886
+ p: 1,
887
+ mb: 1.5,
888
+ border: '1px solid rgba(239, 68, 68, 0.3)',
889
+ }}
890
+ >
891
+ <Typography
892
+ variant="caption"
893
+ sx={{ color: '#EF4444', fontSize: 10, display: 'block' }}
894
+ >
895
+ {error}
896
+ </Typography>
897
+ </Box>
898
+ )}
899
+
900
+ {/* Result Display (when completed) */}
901
+ {status === 'completed' && result && (
902
+ <Box
903
+ sx={{
904
+ bgcolor: 'rgba(16, 185, 129, 0.1)',
905
+ borderRadius: 1,
906
+ p: 1,
907
+ mb: 1.5,
908
+ border: '1px solid rgba(16, 185, 129, 0.3)',
909
+ }}
910
+ >
911
+ <Typography
912
+ variant="caption"
913
+ sx={{ color: '#10B981', fontSize: 10, display: 'block', fontWeight: 600 }}
914
+ >
915
+ ✓ සාර්ථකව ක්‍රියාත්මක විය
916
+ </Typography>
917
+ {result.documentId && (
918
+ <Typography
919
+ variant="caption"
920
+ sx={{ color: '#9CA3AF', fontSize: 10 }}
921
+ >
922
+ Doc ID: {result.documentId}
923
+ </Typography>
924
+ )}
925
+ {result.presentationId && (
926
+ <Typography
927
+ variant="caption"
928
+ sx={{ color: '#9CA3AF', fontSize: 10 }}
929
+ >
930
+ Presentation ID: {result.presentationId}
931
+ </Typography>
932
+ )}
933
+ {result.meetingUri && (
934
+ <Typography
935
+ variant="caption"
936
+ sx={{ color: '#9CA3AF', fontSize: 10 }}
937
+ >
938
+ Meet Link: {result.meetingUri}
939
+ </Typography>
940
+ )}
941
+ </Box>
942
+ )}
943
+
944
+ {/* Execute Button */}
945
+ <Button
946
+ fullWidth
947
+ variant="contained"
948
+ onClick={handleExecute}
949
+ disabled={isExecuting}
950
+ startIcon={
951
+ isExecuting ? (
952
+ <CircularProgress size={16} color="inherit" />
953
+ ) : tokens ? (
954
+ <PlayIcon />
955
+ ) : (
956
+ <FcGoogle size={16} />
957
+ )
958
+ }
959
+ sx={{
960
+ bgcolor: tokens ? serviceConfig.color : '#374151',
961
+ color: '#fff',
962
+ fontWeight: 600,
963
+ fontSize: 12,
964
+ py: 1,
965
+ '&:hover': {
966
+ bgcolor: tokens ? `${serviceConfig.color}CC` : '#4B5563',
967
+ },
968
+ '&:disabled': {
969
+ bgcolor: '#374151',
970
+ color: '#6B7280',
971
+ },
972
+ }}
973
+ >
974
+ {isExecuting
975
+ ? 'ක්‍රියාත්මක වෙමින්...'
976
+ : tokens
977
+ ? 'ක්‍රියාත්මක කරන්න'
978
+ : 'Google සමඟ ක්‍රියාත්මක කරන්න'}
979
+ </Button>
980
+
981
+ {/* Hint Text */}
982
+ {!tokens && (
983
+ <Typography
984
+ variant="caption"
985
+ sx={{
986
+ color: '#6B7280',
987
+ fontSize: 9,
988
+ display: 'block',
989
+ textAlign: 'center',
990
+ mt: 1,
991
+ }}
992
+ >
993
+ බොත්තම ඔබන්න → Google සත්‍යාපනය → ක්‍රියාත්මක වීම
994
+ </Typography>
995
+ )}
996
+ </Box>
997
+
998
+ {/* Node Action Buttons */}
999
+ <NodeActionButtons
1000
+ nodeId={nodeId || ''}
1001
+ showSettings
1002
+ onSettings={() => setSelectedNode(nodeId || '')}
1003
+ />
1004
+
1005
+ {/* Output Handle */}
1006
+ <Handle
1007
+ type="source"
1008
+ position={Position.Right}
1009
+ style={{
1010
+ background: serviceConfig.color,
1011
+ width: 10,
1012
+ height: 10,
1013
+ border: '2px solid #1E1E2E',
1014
+ }}
1015
+ />
1016
+ </Box>
1017
+ );
1018
+ };
1019
+
1020
+ export default AutomationGoogleServicesNode;
1021
+
@@ -12,6 +12,14 @@ export { AISuggestionsPanel } from './AISuggestionsPanel';
12
12
  export { NodeActionButtons } from './NodeActionButtons';
13
13
  export { NodeAIAssistantPopup, showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
14
14
  export type { AISuggestion } from './AISuggestionsModal';
15
+ // Google Services Node (Docs, Slides, Meet, Gmail, Sheets, Calendar)
16
+ export { AutomationGoogleServicesNode } from './AutomationGoogleServicesNode';
17
+ export type {
18
+ GoogleServiceType,
19
+ GoogleTokenData,
20
+ AutomationGoogleServicesNodeData,
21
+ AutomationGoogleServicesNodeProps,
22
+ } from './AutomationGoogleServicesNode';
15
23
  export {
16
24
  setNodeAIAssistantEndpoint,
17
25
  setNodeAIAssistantHeaders,
@@ -264,13 +264,77 @@ export interface AutomationNavigationNodeForm extends AutomationNodeFormData {
264
264
  };
265
265
  }
266
266
 
267
+ // Google Services Node Form
268
+ export type GoogleServiceType =
269
+ | 'docs'
270
+ | 'slides'
271
+ | 'meet'
272
+ | 'gmail'
273
+ | 'sheets'
274
+ | 'calendar';
275
+
276
+ export interface AutomationGoogleServicesNodeForm extends AutomationNodeFormData {
277
+ type: 'start' | 'api' | 'formatting' | 'sheets' | 'end' | 'navigation';
278
+ serviceType: GoogleServiceType;
279
+
280
+ // Google Auth Configuration
281
+ googleAuth?: {
282
+ clientId?: string;
283
+ scopes?: string[];
284
+ accessToken?: string;
285
+ expiresAt?: number;
286
+ userEmail?: string;
287
+ };
288
+
289
+ // Service-specific Parameters
290
+ // Google Docs
291
+ documentId?: string;
292
+ documentTitle?: string;
293
+ documentContent?: string;
294
+ documentOperation?: 'create' | 'read' | 'update' | 'append';
295
+
296
+ // Google Slides
297
+ presentationId?: string;
298
+ presentationTitle?: string;
299
+ slideContent?: string;
300
+
301
+ // Google Meet
302
+ meetingTitle?: string;
303
+ meetingStartTime?: string;
304
+ meetingEndTime?: string;
305
+ meetingAttendees?: string[];
306
+
307
+ // Gmail
308
+ emailTo?: string[];
309
+ emailSubject?: string;
310
+ emailBody?: string;
311
+ emailAttachments?: string[];
312
+
313
+ // Google Sheets
314
+ spreadsheetId?: string;
315
+ sheetName?: string;
316
+ sheetRange?: string;
317
+ sheetData?: any[];
318
+
319
+ // Google Calendar
320
+ calendarId?: string;
321
+ eventTitle?: string;
322
+ eventDescription?: string;
323
+ eventStartTime?: string;
324
+ eventEndTime?: string;
325
+
326
+ // Output
327
+ outputVariable?: string;
328
+ }
329
+
267
330
  export type AutomationNodeForm =
268
331
  | AutomationStartNodeForm
269
332
  | AutomationApiNodeForm
270
333
  | AutomationFormattingNodeForm
271
334
  | AutomationSheetsNodeForm
272
335
  | AutomationEndNodeForm
273
- | AutomationNavigationNodeForm;
336
+ | AutomationNavigationNodeForm
337
+ | AutomationGoogleServicesNodeForm;
274
338
 
275
339
  // Automation Workflow Data Structure
276
340
  export interface AutomationWorkflowData {
@@ -12,6 +12,7 @@ import {
12
12
  AutomationNoteNode,
13
13
  AutomationNavigationNode,
14
14
  AutomationAISuggestionNode,
15
+ AutomationGoogleServicesNode,
15
16
  } from '../components/automation';
16
17
 
17
18
  export default {
@@ -28,4 +29,11 @@ export default {
28
29
  AutomationNoteNode,
29
30
  AutomationNavigationNode,
30
31
  AutomationAISuggestionNode,
32
+ // Google Services Nodes (Docs, Slides, Meet, Gmail, Sheets, Calendar)
33
+ AutomationGoogleServicesNode,
34
+ AutomationGoogleDocsNode: AutomationGoogleServicesNode,
35
+ AutomationGoogleSlidesNode: AutomationGoogleServicesNode,
36
+ AutomationGoogleMeetNode: AutomationGoogleServicesNode,
37
+ AutomationGmailNode: AutomationGoogleServicesNode,
38
+ AutomationGoogleCalendarNode: AutomationGoogleServicesNode,
31
39
  };