@flowuent-org/diagramming-core 1.2.3 → 1.2.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.
@@ -534,20 +534,23 @@ export const automationDefaultNodes = [
534
534
  position: { x: 100, y: 450 },
535
535
  data: {
536
536
  label: 'Create Report',
537
- description: 'Google Docs document එකක් create කරන්න',
537
+ description: 'Create a Google Docs document',
538
538
  serviceType: 'docs',
539
539
  status: 'idle',
540
+ isFirstGoogleNode: true, // Only first Google node shows Connect button
540
541
  parameters: {
541
542
  documentTitle: 'Automation Report',
542
543
  operation: 'create',
543
544
  content: 'Report content will be generated here...',
544
545
  },
545
546
  googleAuth: {
546
- clientId: '', // Will be set from environment
547
+ clientId: '',
547
548
  scopes: [
548
549
  'https://www.googleapis.com/auth/documents',
549
550
  'https://www.googleapis.com/auth/drive',
550
551
  ],
552
+ isAuthenticated: false,
553
+ isLoading: false,
551
554
  },
552
555
  formData: {
553
556
  nodeId: 'google-docs-node',
@@ -566,9 +569,10 @@ export const automationDefaultNodes = [
566
569
  position: { x: 450, y: 450 },
567
570
  data: {
568
571
  label: 'Send Email',
569
- description: 'Gmail හරහා email එකක් send කරන්න',
572
+ description: 'Send email via Gmail',
570
573
  serviceType: 'gmail',
571
574
  status: 'idle',
575
+ isFirstGoogleNode: false, // Not first, no Connect button
572
576
  parameters: {
573
577
  to: ['recipient@example.com'],
574
578
  subject: 'Automation Report',
@@ -580,6 +584,8 @@ export const automationDefaultNodes = [
580
584
  'https://www.googleapis.com/auth/gmail.send',
581
585
  'https://www.googleapis.com/auth/gmail.compose',
582
586
  ],
587
+ isAuthenticated: false,
588
+ isLoading: false,
583
589
  },
584
590
  formData: {
585
591
  nodeId: 'gmail-node',
@@ -598,9 +604,10 @@ export const automationDefaultNodes = [
598
604
  position: { x: 800, y: 450 },
599
605
  data: {
600
606
  label: 'Create Meeting',
601
- description: 'Google Meet link එකක් create කරන්න',
607
+ description: 'Create a Google Meet link',
602
608
  serviceType: 'meet',
603
609
  status: 'idle',
610
+ isFirstGoogleNode: false, // Not first, no Connect button
604
611
  parameters: {
605
612
  meetingTitle: 'Project Discussion',
606
613
  attendees: ['team@example.com'],
@@ -608,6 +615,8 @@ export const automationDefaultNodes = [
608
615
  googleAuth: {
609
616
  clientId: '',
610
617
  scopes: ['https://www.googleapis.com/auth/meetings.space.created'],
618
+ isAuthenticated: false,
619
+ isLoading: false,
611
620
  },
612
621
  formData: {
613
622
  nodeId: 'google-meet-node',
@@ -620,68 +629,6 @@ export const automationDefaultNodes = [
620
629
  height: 200,
621
630
  measured: { width: 300, height: 200 },
622
631
  },
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
- },
685
632
  ];
686
633
 
687
634
  // Default edges for the automation diagram
@@ -810,7 +757,7 @@ export const automationDefaultEdges = [
810
757
  sourceHandle: 'right',
811
758
  targetHandle: 'left',
812
759
  data: {
813
- label: 'Schedule Meet',
760
+ label: 'Schedule Meeting',
814
761
  type: 'flow',
815
762
  },
816
763
  style: {
@@ -822,23 +769,4 @@ export const automationDefaultEdges = [
822
769
  color: '#EA4335',
823
770
  },
824
771
  },
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
- },
844
772
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowuent-org/diagramming-core",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -66,11 +66,17 @@ export interface AutomationGoogleServicesNodeData {
66
66
  status?: NodeStatus;
67
67
  iconName?: string;
68
68
 
69
+ // Flag to indicate if this is the first Google node (show auth button)
70
+ isFirstGoogleNode?: boolean;
71
+
69
72
  // Google Auth config
70
73
  googleAuth?: {
71
74
  clientId?: string;
72
75
  scopes?: string[];
73
76
  tokens?: GoogleTokenData;
77
+ isAuthenticated?: boolean;
78
+ isLoading?: boolean;
79
+ userEmail?: string;
74
80
  };
75
81
 
76
82
  // Service-specific parameters
@@ -131,10 +137,9 @@ export interface AutomationGoogleServicesNodeData {
131
137
  executionTime?: number;
132
138
  };
133
139
 
134
- // Callbacks
135
- onExecute?: (tokens: GoogleTokenData) => Promise<any>;
136
- onAuthSuccess?: (tokens: GoogleTokenData) => void;
137
- onAuthError?: (error: Error) => void;
140
+ // Callbacks - passed from parent workflow
141
+ onGoogleLogin?: () => void;
142
+ onGoogleDisconnect?: () => void;
138
143
  }
139
144
 
140
145
  export interface AutomationGoogleServicesNodeProps {
@@ -164,7 +169,7 @@ const SERVICE_CONFIG: Record<
164
169
  'https://www.googleapis.com/auth/documents',
165
170
  'https://www.googleapis.com/auth/drive',
166
171
  ],
167
- description: 'Document එකක් create/edit කරන්න',
172
+ description: 'Create or edit a document',
168
173
  },
169
174
  slides: {
170
175
  icon: SlidesIcon,
@@ -174,14 +179,14 @@ const SERVICE_CONFIG: Record<
174
179
  'https://www.googleapis.com/auth/presentations',
175
180
  'https://www.googleapis.com/auth/drive',
176
181
  ],
177
- description: 'Presentation එකක් create/edit කරන්න',
182
+ description: 'Create or edit a presentation',
178
183
  },
179
184
  meet: {
180
185
  icon: MeetIcon,
181
186
  label: 'Google Meet',
182
187
  color: '#34A853',
183
188
  scopes: ['https://www.googleapis.com/auth/meetings.space.created'],
184
- description: 'Meeting එකක් create කරන්න',
189
+ description: 'Create a meeting link',
185
190
  },
186
191
  gmail: {
187
192
  icon: GmailIcon,
@@ -191,21 +196,21 @@ const SERVICE_CONFIG: Record<
191
196
  'https://www.googleapis.com/auth/gmail.send',
192
197
  'https://www.googleapis.com/auth/gmail.compose',
193
198
  ],
194
- description: 'Email එකක් send කරන්න',
199
+ description: 'Send an email',
195
200
  },
196
201
  sheets: {
197
202
  icon: SheetsIcon,
198
203
  label: 'Google Sheets',
199
204
  color: '#0F9D58',
200
205
  scopes: ['https://www.googleapis.com/auth/spreadsheets'],
201
- description: 'Spreadsheet එකක් read/write කරන්න',
206
+ description: 'Read or write spreadsheet data',
202
207
  },
203
208
  calendar: {
204
209
  icon: CalendarIcon,
205
210
  label: 'Google Calendar',
206
211
  color: '#4285F4',
207
212
  scopes: ['https://www.googleapis.com/auth/calendar'],
208
- description: 'Calendar event එකක් create කරන්න',
213
+ description: 'Create a calendar event',
209
214
  },
210
215
  };
211
216
 
@@ -222,7 +227,7 @@ const isTokenExpired = (expiresAt?: number): boolean => {
222
227
  const getTimeUntilExpiry = (expiresAt?: number): string => {
223
228
  if (!expiresAt) return 'N/A';
224
229
  const remaining = expiresAt - Date.now();
225
- if (remaining <= 0) return 'කල් ඉකුත් වී ඇත';
230
+ if (remaining <= 0) return 'Expired';
226
231
  const minutes = Math.floor(remaining / 60000);
227
232
  if (minutes < 60) return `${minutes} min`;
228
233
  const hours = Math.floor(minutes / 60);
@@ -247,203 +252,23 @@ export const AutomationGoogleServicesNode: React.FC<
247
252
  // State
248
253
  // ========================
249
254
  const [status, setStatus] = useState<NodeStatus>(data.status || 'idle');
250
- const [tokens, setTokens] = useState<GoogleTokenData | null>(null);
251
255
  const [isExecuting, setIsExecuting] = useState(false);
252
256
  const [executionProgress, setExecutionProgress] = useState(0);
253
257
  const [error, setError] = useState<string | null>(null);
254
258
  const [result, setResult] = useState<any>(null);
255
259
 
256
- const googleClientRef = useRef<any>(null);
257
260
  const serviceType = data.serviceType || 'docs';
258
261
  const serviceConfig = SERVICE_CONFIG[serviceType];
259
262
  const ServiceIcon = serviceConfig.icon;
260
263
 
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]);
264
+ // Check if authenticated from data
265
+ const isAuthenticated = data.googleAuth?.isAuthenticated ||
266
+ (data.parameters?.googleAccessToken && !isTokenExpired(data.parameters?.googleTokenExpiresAt));
267
+ const isAuthLoading = data.googleAuth?.isLoading || false;
268
+ const userEmail = data.googleAuth?.userEmail || data.parameters?.googleUserEmail;
364
269
 
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]);
270
+ // Show Google Auth button only on first Google node
271
+ const showGoogleAuthButton = data.isFirstGoogleNode === true;
447
272
 
448
273
  // ========================
449
274
  // Update Node Data
@@ -470,157 +295,6 @@ export const AutomationGoogleServicesNode: React.FC<
470
295
  [nodeId, nodes, setNodes]
471
296
  );
472
297
 
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
298
  // ========================
625
299
  // Render Status Badge
626
300
  // ========================
@@ -632,41 +306,42 @@ export const AutomationGoogleServicesNode: React.FC<
632
306
  idle: {
633
307
  color: '#6B7280',
634
308
  bgColor: 'rgba(107, 114, 128, 0.1)',
635
- label: 'සූදානම්',
309
+ label: 'Ready',
636
310
  },
637
311
  authenticating: {
638
312
  color: '#F59E0B',
639
313
  bgColor: 'rgba(245, 158, 11, 0.1)',
640
- label: 'සත්‍යාපනය...',
314
+ label: 'Authenticating...',
641
315
  icon: <CircularProgress size={12} color="inherit" />,
642
316
  },
643
317
  authenticated: {
644
318
  color: '#10B981',
645
319
  bgColor: 'rgba(16, 185, 129, 0.1)',
646
- label: 'සම්බන්ධයි',
320
+ label: 'Connected',
647
321
  icon: <CheckCircleIcon sx={{ fontSize: 12 }} />,
648
322
  },
649
323
  running: {
650
324
  color: '#3B82F6',
651
325
  bgColor: 'rgba(59, 130, 246, 0.1)',
652
- label: 'ක්‍රියාත්මක...',
326
+ label: 'Running...',
653
327
  icon: <CircularProgress size={12} color="inherit" />,
654
328
  },
655
329
  completed: {
656
330
  color: '#10B981',
657
331
  bgColor: 'rgba(16, 185, 129, 0.1)',
658
- label: 'සාර්ථකයි',
332
+ label: 'Success',
659
333
  icon: <CheckCircleIcon sx={{ fontSize: 12 }} />,
660
334
  },
661
335
  error: {
662
336
  color: '#EF4444',
663
337
  bgColor: 'rgba(239, 68, 68, 0.1)',
664
- label: 'දෝෂය',
338
+ label: 'Error',
665
339
  icon: <ErrorIcon sx={{ fontSize: 12 }} />,
666
340
  },
667
341
  };
668
342
 
669
- const config = statusConfig[status];
343
+ const currentStatus = isAuthenticated ? 'authenticated' : status;
344
+ const config = statusConfig[currentStatus];
670
345
 
671
346
  return (
672
347
  <Chip
@@ -690,6 +365,81 @@ export const AutomationGoogleServicesNode: React.FC<
690
365
  );
691
366
  };
692
367
 
368
+ // ========================
369
+ // Render Google Auth Button
370
+ // ========================
371
+ const renderGoogleAuthButton = () => {
372
+ if (!showGoogleAuthButton) return null;
373
+
374
+ if (isAuthenticated) {
375
+ return (
376
+ <Button
377
+ onClick={data.onGoogleDisconnect}
378
+ disabled={isAuthLoading}
379
+ fullWidth
380
+ sx={{
381
+ backgroundColor: '#10B981',
382
+ border: '1px solid #10B981',
383
+ borderRadius: '8px',
384
+ padding: '8px 12px',
385
+ gap: '8px',
386
+ color: '#FFFFFF',
387
+ textTransform: 'none',
388
+ fontSize: '13px',
389
+ fontWeight: 500,
390
+ '&:hover': {
391
+ backgroundColor: '#059669',
392
+ },
393
+ '&:disabled': {
394
+ opacity: 0.6,
395
+ },
396
+ }}
397
+ >
398
+ <FcGoogle size={18} />
399
+ {userEmail ? userEmail.split('@')[0] : 'Connected'}
400
+ </Button>
401
+ );
402
+ }
403
+
404
+ return (
405
+ <Button
406
+ onClick={data.onGoogleLogin}
407
+ disabled={isAuthLoading}
408
+ fullWidth
409
+ sx={{
410
+ backgroundColor: '#171C29',
411
+ border: '1px solid #FFFFFF1A',
412
+ borderRadius: '8px',
413
+ padding: '8px 12px',
414
+ gap: '8px',
415
+ color: '#B2BCD8',
416
+ textTransform: 'none',
417
+ fontSize: '13px',
418
+ fontWeight: 400,
419
+ '&:hover': {
420
+ backgroundColor: '#1e293b',
421
+ borderColor: '#3B82F6',
422
+ },
423
+ '&:disabled': {
424
+ opacity: 0.6,
425
+ },
426
+ }}
427
+ >
428
+ {isAuthLoading ? (
429
+ <>
430
+ <CircularProgress size={16} sx={{ color: '#B2BCD8' }} />
431
+ Connecting...
432
+ </>
433
+ ) : (
434
+ <>
435
+ <FcGoogle size={18} />
436
+ Connect Google
437
+ </>
438
+ )}
439
+ </Button>
440
+ );
441
+ };
442
+
693
443
  // ========================
694
444
  // Render
695
445
  // ========================
@@ -757,7 +507,7 @@ export const AutomationGoogleServicesNode: React.FC<
757
507
  variant="caption"
758
508
  sx={{ color: '#9CA3AF', fontSize: 10 }}
759
509
  >
760
- {serviceConfig.description}
510
+ {data.description || serviceConfig.description}
761
511
  </Typography>
762
512
  </Box>
763
513
  </Box>
@@ -766,92 +516,63 @@ export const AutomationGoogleServicesNode: React.FC<
766
516
 
767
517
  {/* Content */}
768
518
  <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>
519
+ {/* Google Auth Button - Only on first Google node */}
520
+ {showGoogleAuthButton && (
521
+ <Box sx={{ mb: 1.5 }}>
522
+ {renderGoogleAuthButton()}
523
+ {!isAuthenticated && (
524
+ <Typography
525
+ variant="caption"
526
+ sx={{
527
+ color: '#6B7280',
528
+ fontSize: 10,
529
+ display: 'block',
530
+ textAlign: 'center',
531
+ mt: 0.5,
532
+ }}
533
+ >
534
+ Click to authenticate with Google
535
+ </Typography>
536
+ )}
537
+ </Box>
538
+ )}
539
+
540
+ {/* Authentication Status for non-first nodes */}
541
+ {!showGoogleAuthButton && (
542
+ <Box
543
+ sx={{
544
+ bgcolor: isAuthenticated ? 'rgba(16, 185, 129, 0.1)' : 'rgba(107, 114, 128, 0.1)',
545
+ borderRadius: 1,
546
+ p: 1.5,
547
+ mb: 1.5,
548
+ }}
549
+ >
550
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
551
+ <FcGoogle size={18} style={{ opacity: isAuthenticated ? 1 : 0.5 }} />
552
+ <Box>
553
+ <Typography
554
+ variant="caption"
555
+ sx={{
556
+ color: isAuthenticated ? '#10B981' : '#6B7280',
557
+ display: 'block',
558
+ fontWeight: 600,
559
+ fontSize: 11,
560
+ }}
561
+ >
562
+ {isAuthenticated ? 'Google Connected' : 'Google Not Connected'}
563
+ </Typography>
564
+ {isAuthenticated && userEmail && (
789
565
  <Typography
790
566
  variant="caption"
791
567
  sx={{ color: '#9CA3AF', fontSize: 10 }}
792
568
  >
793
- {tokens.email || 'Google Account'}
569
+ {userEmail}
794
570
  </Typography>
795
- </Box>
571
+ )}
796
572
  </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
573
  </Box>
853
- )}
854
- </Box>
574
+ </Box>
575
+ )}
855
576
 
856
577
  {/* Progress Bar (when running) */}
857
578
  {status === 'running' && (
@@ -872,7 +593,7 @@ export const AutomationGoogleServicesNode: React.FC<
872
593
  variant="caption"
873
594
  sx={{ color: '#9CA3AF', fontSize: 10, mt: 0.5, display: 'block' }}
874
595
  >
875
- ක්‍රියාත්මක වෙමින්... {executionProgress}%
596
+ Executing... {executionProgress}%
876
597
  </Typography>
877
598
  </Box>
878
599
  )}
@@ -912,7 +633,7 @@ export const AutomationGoogleServicesNode: React.FC<
912
633
  variant="caption"
913
634
  sx={{ color: '#10B981', fontSize: 10, display: 'block', fontWeight: 600 }}
914
635
  >
915
- සාර්ථකව ක්‍රියාත්මක විය
636
+ Executed successfully
916
637
  </Typography>
917
638
  {result.documentId && (
918
639
  <Typography
@@ -941,58 +662,55 @@ export const AutomationGoogleServicesNode: React.FC<
941
662
  </Box>
942
663
  )}
943
664
 
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
- }
665
+ {/* Service-specific details */}
666
+ <Box
959
667
  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
- },
668
+ bgcolor: 'rgba(255, 255, 255, 0.03)',
669
+ borderRadius: 1,
670
+ p: 1,
972
671
  }}
973
672
  >
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
- )}
673
+ {serviceType === 'docs' && data.parameters?.documentTitle && (
674
+ <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10 }}>
675
+ Document: {data.parameters.documentTitle}
676
+ </Typography>
677
+ )}
678
+ {serviceType === 'gmail' && data.parameters?.subject && (
679
+ <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10 }}>
680
+ Subject: {data.parameters.subject}
681
+ </Typography>
682
+ )}
683
+ {serviceType === 'meet' && data.parameters?.meetingTitle && (
684
+ <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10 }}>
685
+ Meeting: {data.parameters.meetingTitle}
686
+ </Typography>
687
+ )}
688
+ {serviceType === 'slides' && data.parameters?.presentationTitle && (
689
+ <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10 }}>
690
+ Presentation: {data.parameters.presentationTitle}
691
+ </Typography>
692
+ )}
693
+ {serviceType === 'calendar' && data.parameters?.eventTitle && (
694
+ <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10 }}>
695
+ Event: {data.parameters.eventTitle}
696
+ </Typography>
697
+ )}
698
+ {serviceType === 'sheets' && data.parameters?.sheetName && (
699
+ <Typography variant="caption" sx={{ color: '#9CA3AF', fontSize: 10 }}>
700
+ Sheet: {data.parameters.sheetName}
701
+ </Typography>
702
+ )}
703
+ {!data.parameters?.documentTitle &&
704
+ !data.parameters?.subject &&
705
+ !data.parameters?.meetingTitle &&
706
+ !data.parameters?.presentationTitle &&
707
+ !data.parameters?.eventTitle &&
708
+ !data.parameters?.sheetName && (
709
+ <Typography variant="caption" sx={{ color: '#6B7280', fontSize: 10, fontStyle: 'italic' }}>
710
+ Configure node parameters
711
+ </Typography>
712
+ )}
713
+ </Box>
996
714
  </Box>
997
715
 
998
716
  {/* Node Action Buttons */}
@@ -277,6 +277,9 @@ export interface AutomationGoogleServicesNodeForm extends AutomationNodeFormData
277
277
  type: 'start' | 'api' | 'formatting' | 'sheets' | 'end' | 'navigation';
278
278
  serviceType: GoogleServiceType;
279
279
 
280
+ // Flag to show Google auth button only on first node
281
+ isFirstGoogleNode?: boolean;
282
+
280
283
  // Google Auth Configuration
281
284
  googleAuth?: {
282
285
  clientId?: string;
@@ -284,6 +287,8 @@ export interface AutomationGoogleServicesNodeForm extends AutomationNodeFormData
284
287
  accessToken?: string;
285
288
  expiresAt?: number;
286
289
  userEmail?: string;
290
+ isAuthenticated?: boolean;
291
+ isLoading?: boolean;
287
292
  };
288
293
 
289
294
  // Service-specific Parameters
@@ -325,6 +330,10 @@ export interface AutomationGoogleServicesNodeForm extends AutomationNodeFormData
325
330
 
326
331
  // Output
327
332
  outputVariable?: string;
333
+
334
+ // Callbacks
335
+ onGoogleLogin?: () => void;
336
+ onGoogleDisconnect?: () => void;
328
337
  }
329
338
 
330
339
  export type AutomationNodeForm =
@@ -29,7 +29,7 @@ export default {
29
29
  AutomationNoteNode,
30
30
  AutomationNavigationNode,
31
31
  AutomationAISuggestionNode,
32
- // Google Services Nodes (Docs, Slides, Meet, Gmail, Sheets, Calendar)
32
+ // Google Services Nodes
33
33
  AutomationGoogleServicesNode,
34
34
  AutomationGoogleDocsNode: AutomationGoogleServicesNode,
35
35
  AutomationGoogleSlidesNode: AutomationGoogleServicesNode,