import { useState, useEffect, useCallback } from 'react'; import { useApiRequest } from './useApi'; import { getApiBaseUrl } from '../../config/config'; import api from '../api'; import { usePermissions, type UserPermissions } from './usePermissions'; import { fetchConnections as fetchConnectionsApi, createConnection as createConnectionApi, connectService as connectServiceApi, disconnectService as disconnectServiceApi, deleteConnection as deleteConnectionApi, updateConnection as updateConnectionApi, refreshMicrosoftToken as refreshMicrosoftTokenApi, refreshGoogleToken as refreshGoogleTokenApi, type Connection, type AttributeDefinition, type PaginationParams, type CreateConnectionData, type ConnectResponse } from '../api/connectionApi'; // Re-export types for backward compatibility export type { Connection, AttributeDefinition, PaginationParams, CreateConnectionData, ConnectResponse }; // Hook for managing connections export function useConnections() { const [connections, setConnections] = useState([]); const [attributes, setAttributes] = useState([]); const [permissions, setPermissions] = useState(null); const [pagination, setPagination] = useState<{ currentPage: number; pageSize: number; totalItems: number; totalPages: number; } | null>(null); const [isConnecting, setIsConnecting] = useState(false); const [connectError, setConnectError] = useState(null); const { request, isLoading, error } = useApiRequest(); const { checkPermission } = usePermissions(); // Fetch attributes from backend const fetchAttributes = useCallback(async () => { try { const response = await api.get('/api/attributes/UserConnection'); // Extract attributes from response let attrs: AttributeDefinition[] = []; if (response.data?.attributes && Array.isArray(response.data.attributes)) { attrs = response.data.attributes; } else if (Array.isArray(response.data)) { attrs = response.data; } else if (response.data && typeof response.data === 'object') { const keys = Object.keys(response.data); for (const key of keys) { if (Array.isArray(response.data[key])) { attrs = response.data[key]; break; } } } setAttributes(attrs); return attrs; } catch (error: any) { console.error('Error fetching attributes:', error); setAttributes([]); return []; } }, []); // Fetch permissions from backend const fetchPermissions = useCallback(async () => { try { const perms = await checkPermission('DATA', 'UserConnection'); setPermissions(perms); return perms; } catch (error: any) { console.error('Error fetching permissions:', error); const defaultPerms: UserPermissions = { view: false, read: 'n', create: 'n', update: 'n', delete: 'n', }; setPermissions(defaultPerms); return defaultPerms; } }, [checkPermission]); // Fetch connections with pagination support const fetchConnections = useCallback(async (params?: PaginationParams): Promise => { try { const data = await fetchConnectionsApi(request, params); // Handle paginated response if (data && typeof data === 'object' && 'items' in data) { const items = Array.isArray(data.items) ? data.items : []; setConnections(items); if (data.pagination) { setPagination(data.pagination); } } else { // Handle non-paginated response (backward compatibility) const items = Array.isArray(data) ? data : []; setConnections(items); setPagination(null); } return Array.isArray(data) ? data : (data?.items || []); } catch (error) { console.error('Error fetching connections:', error); setConnections([]); setPagination(null); throw error; } }, [request]); // Create a new connection const createConnection = async (connectionData: CreateConnectionData): Promise => { try { const data = await createConnectionApi(request, connectionData); // Update local state setConnections(prev => { const existing = prev.find(conn => conn.id === data.id); if (existing) { return prev.map(conn => conn.id === data.id ? data : conn); } else { return [...prev, data]; } }); return data; } catch (error) { console.error('Error creating connection:', error); throw error; } }; // Connect to a service (initiate OAuth) const connectService = async (connectionId: string): Promise => { try { const data = await connectServiceApi(request, connectionId); return data; } catch (error) { console.error('Error connecting service:', error); throw error; } }; // Disconnect from a service const disconnectService = async (connectionId: string): Promise<{ message: string }> => { try { const data = await disconnectServiceApi(request, connectionId); // Update local state setConnections(prev => prev.map(conn => conn.id === connectionId ? { ...conn, status: 'inactive' as any, lastChecked: Math.floor(Date.now() / 1000) } : conn ) ); return data; } catch (error) { console.error('Error disconnecting service:', error); throw error; } }; // Delete a connection const deleteConnection = async (connectionId: string): Promise<{ message: string }> => { try { const data = await deleteConnectionApi(request, connectionId); // Update local state setConnections(prev => prev.filter(conn => conn.id !== connectionId)); return data; } catch (error) { console.error('Error deleting connection:', error); throw error; } }; // Update a connection const updateConnection = async (connectionId: string, updateData: Partial): Promise => { try { const data = await updateConnectionApi(request, connectionId, updateData); // Update local state setConnections(prev => prev.map(conn => conn.id === connectionId ? { ...conn, ...data } : conn) ); return data; } catch (error) { console.error('Error updating connection:', error); throw error; } }; // Refresh Microsoft token const refreshMicrosoftToken = async (connectionId: string): Promise => { try { const data = await refreshMicrosoftTokenApi(request, connectionId); // Update local state setConnections(prev => prev.map(conn => conn.id === connectionId ? { ...conn, ...data } : conn) ); return data; } catch (error) { console.error('Error refreshing Microsoft token:', error); throw error; } }; // Refresh Google token const refreshGoogleToken = async (connectionId: string): Promise => { try { const data = await refreshGoogleTokenApi(request, connectionId); // Update local state setConnections(prev => prev.map(conn => conn.id === connectionId ? { ...conn, ...data } : conn) ); return data; } catch (error) { console.error('Error refreshing Google token:', error); throw error; } }; // Connect with popup (OAuth flow) const connectWithPopup = async (connectionId: string): Promise => { setIsConnecting(true); setConnectError(null); try { // Get the OAuth URL from backend const response = await connectService(connectionId); if (!response.authUrl) { throw new Error('No OAuth URL received from backend'); } console.log('OAuth URL from backend:', response.authUrl); return new Promise((resolve, reject) => { // Convert relative URL to absolute URL if needed let authUrl = response.authUrl; if (authUrl.startsWith('/')) { authUrl = `${getApiBaseUrl()}${authUrl}`; } // Open popup const popup = window.open( authUrl, 'oauth-connect', 'width=500,height=600,scrollbars=yes,resizable=yes' ); if (!popup) { setConnectError('Popup was blocked. Please allow popups and try again.'); setIsConnecting(false); reject(new Error('Popup was blocked')); return; } // Handle popup closing without completing auth const checkClosed = setInterval(() => { if (popup.closed) { clearInterval(checkClosed); window.removeEventListener('message', messageListener); setIsConnecting(false); console.log('OAuth popup closed'); // Refresh connections anyway in case it succeeded fetchConnections(); resolve(); // Resolve instead of reject to avoid error on normal close } }, 1000); // Listen for messages from the popup const messageListener = (event: MessageEvent) => { // Verify origin for security const apiUrl = new URL(getApiBaseUrl()); if (event.origin !== apiUrl.origin) { return; } if ( event.data.type === 'msft_connection_success' || event.data.type === 'google_connection_success' || event.data.type === 'clickup_connection_success' ) { // Clean up clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); setIsConnecting(false); console.log('OAuth connection successful'); // Refresh connections fetchConnections(); resolve(); } else if ( event.data.type === 'msft_connection_error' || event.data.type === 'google_connection_error' || event.data.type === 'clickup_connection_error' ) { // Handle error clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); setIsConnecting(false); setConnectError(event.data.error || 'OAuth connection failed'); reject(new Error(event.data.error || 'OAuth connection failed')); } }; // Add message listener window.addEventListener('message', messageListener); }); } catch (error: any) { setConnectError(error.message || 'OAuth connection failed'); setIsConnecting(false); throw error; } }; // Create Google connection and open OAuth popup const createGoogleConnectionAndAuth = async (): Promise => { try { // Step 1: Create a Google connection const newConnection = await createConnection({ type: 'google', authority: 'google' }); // Step 2: Get the OAuth URL from backend for this specific connection const connectResponse = await connectServiceApi(request, newConnection.id); if (!connectResponse.authUrl) { throw new Error('No OAuth URL received from backend'); } // Step 3: Open popup to the OAuth URL const apiBaseUrl = getApiBaseUrl(); let authUrl = connectResponse.authUrl; if (authUrl.startsWith('/')) { authUrl = `${apiBaseUrl}${authUrl}`; } return new Promise((resolve, reject) => { const popup = window.open( authUrl, 'google-connection', 'width=500,height=600,scrollbars=yes,resizable=yes' ); if (!popup) { reject(new Error('Popup was blocked. Please allow popups and try again.')); return; } // Handle popup closing without completing auth const checkClosed = setInterval(() => { if (popup.closed) { clearInterval(checkClosed); window.removeEventListener('message', messageListener); console.log('Google OAuth popup closed'); // Refresh connections in case it succeeded fetchConnections(); resolve(); } }, 1000); // Listen for messages from the popup const messageListener = (event: MessageEvent) => { // Verify origin for security const apiUrl = new URL(apiBaseUrl); if (event.origin !== apiUrl.origin) { return; } if (event.data.type === 'google_connection_success' || event.data.type === 'google_auth_success') { clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); console.log('Google connection successful'); // Refresh connections fetchConnections(); resolve(); } else if (event.data.type === 'google_connection_error') { clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); reject(new Error(event.data.error || 'Google connection failed')); } }; window.addEventListener('message', messageListener); }); } catch (error) { console.error('Error creating Google connection:', error); throw error; } }; // Create ClickUp connection and open OAuth popup const createClickupConnectionAndAuth = async (): Promise => { try { const newConnection = await createConnection({ type: 'clickup', authority: 'clickup', }); const connectResponse = await connectServiceApi(request, newConnection.id); if (!connectResponse.authUrl) { throw new Error('No OAuth URL received from backend'); } const apiBaseUrl = getApiBaseUrl(); let authUrl = connectResponse.authUrl; if (authUrl.startsWith('/')) { authUrl = `${apiBaseUrl}${authUrl}`; } return new Promise((resolve, reject) => { const popup = window.open( authUrl, 'clickup-connection', 'width=500,height=600,scrollbars=yes,resizable=yes' ); if (!popup) { reject(new Error('Popup was blocked. Please allow popups and try again.')); return; } const checkClosed = setInterval(() => { if (popup.closed) { clearInterval(checkClosed); window.removeEventListener('message', messageListener); console.log('ClickUp OAuth popup closed'); fetchConnections(); resolve(); } }, 1000); const messageListener = (event: MessageEvent) => { const apiUrl = new URL(apiBaseUrl); if (event.origin !== apiUrl.origin) { return; } if (event.data.type === 'clickup_connection_success') { clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); console.log('ClickUp connection successful'); fetchConnections(); resolve(); } else if (event.data.type === 'clickup_connection_error') { clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); reject(new Error(event.data.error || 'ClickUp connection failed')); } }; window.addEventListener('message', messageListener); }); } catch (error) { console.error('Error creating ClickUp connection:', error); throw error; } }; // Create Microsoft connection and open OAuth popup const createMicrosoftConnectionAndAuth = async (): Promise => { try { // Step 1: Create a Microsoft connection const newConnection = await createConnection({ type: 'msft', authority: 'msft' }); // Step 2: Get the OAuth URL from backend for this specific connection const connectResponse = await connectServiceApi(request, newConnection.id); if (!connectResponse.authUrl) { throw new Error('No OAuth URL received from backend'); } // Step 3: Open popup to the OAuth URL const apiBaseUrl = getApiBaseUrl(); let authUrl = connectResponse.authUrl; if (authUrl.startsWith('/')) { authUrl = `${apiBaseUrl}${authUrl}`; } return new Promise((resolve, reject) => { const popup = window.open( authUrl, 'msft-connection', 'width=500,height=600,scrollbars=yes,resizable=yes' ); if (!popup) { reject(new Error('Popup was blocked. Please allow popups and try again.')); return; } // Handle popup closing without completing auth const checkClosed = setInterval(() => { if (popup.closed) { clearInterval(checkClosed); window.removeEventListener('message', messageListener); console.log('Microsoft OAuth popup closed'); // Refresh connections in case it succeeded fetchConnections(); resolve(); } }, 1000); // Listen for messages from the popup const messageListener = (event: MessageEvent) => { // Verify origin for security const apiUrl = new URL(apiBaseUrl); if (event.origin !== apiUrl.origin) { return; } if (event.data.type === 'msft_connection_success' || event.data.type === 'msft_auth_success') { clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); console.log('Microsoft connection successful'); // Refresh connections fetchConnections(); resolve(); } else if (event.data.type === 'msft_connection_error') { clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); reject(new Error(event.data.error || 'Microsoft connection failed')); } }; window.addEventListener('message', messageListener); }); } catch (error) { console.error('Error creating Microsoft connection:', error); throw error; } }; // Generate edit fields from attributes dynamically const generateEditFieldsFromAttributes = useCallback((): Array<{ key: string; label: string; type: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'readonly'; editable?: boolean; required?: boolean; validator?: (value: any) => string | null; minRows?: number; maxRows?: number; }> => { if (!attributes || attributes.length === 0) { return []; } const editableFields = attributes .filter(attr => { // Filter out non-editable fields const nonEditableFields = ['id', 'userId', 'connectedAt', 'lastChecked']; return !nonEditableFields.includes(attr.name); }) .map(attr => { // Map attribute type to form field type let fieldType: 'string' | 'boolean' | 'email' | 'textarea' | 'date' | 'enum' | 'readonly' = 'string'; if (attr.type === 'boolean') { fieldType = 'boolean'; } else if (attr.type === 'date') { fieldType = 'date'; } else if (attr.type === 'enum' && attr.filterOptions) { fieldType = 'enum'; } else if (attr.name.toLowerCase().includes('email')) { fieldType = 'email'; } return { key: attr.name, label: attr.label || attr.name, type: fieldType, editable: true, required: false }; }); return editableFields; }, [attributes]); // Ensure attributes are loaded const ensureAttributesLoaded = useCallback(async () => { if (attributes && attributes.length > 0) { return attributes; } const fetchedAttributes = await fetchAttributes(); return fetchedAttributes; }, [attributes, fetchAttributes]); // Fetch attributes and permissions on mount useEffect(() => { fetchAttributes(); fetchPermissions(); }, [fetchAttributes, fetchPermissions]); // Initial fetch useEffect(() => { fetchConnections(); }, [fetchConnections]); // Optimistically update a connection in local state const updateOptimistically = useCallback((connectionId: string, updateData: Partial) => { setConnections(prev => prev.map(conn => conn.id === connectionId ? { ...conn, ...updateData } : conn) ); }, []); // Generic inline update handler for FormGeneratorTable const handleInlineUpdate = useCallback(async (connectionId: string, changes: Partial, existingRow?: any) => { if (!existingRow) { throw new Error('Existing row data required for inline update'); } try { const result = await updateConnection(connectionId, changes); return { success: true, data: result }; } catch (error: any) { throw new Error(error.message || 'Failed to update'); } }, [updateConnection]); // Fetch connection by ID const fetchConnectionById = useCallback(async (connectionId: string): Promise => { try { // Since there's no individual connection endpoint, find from current list or fetch all const existing = connections.find(c => c.id === connectionId); if (existing) return existing; const data = await fetchConnectionsApi(request); const items = Array.isArray(data) ? data : (data?.items || []); return items.find((c: Connection) => c.id === connectionId) || null; } catch (error) { console.error('Error fetching connection by ID:', error); return null; } }, [connections, request]); return { connections, data: connections, // Alias for FormGenerator compatibility fetchConnections, refetch: fetchConnections, // Alias for FormGenerator compatibility createConnection, updateConnection, connectService, disconnectService, deleteConnection, refreshMicrosoftToken, refreshGoogleToken, connectWithPopup, createGoogleConnectionAndAuth, createMicrosoftConnectionAndAuth, createClickupConnectionAndAuth, isLoading, loading: isLoading, // Alias for FormGenerator compatibility isConnecting, error: error || connectError, // Attributes and permissions for dynamic column/button generation attributes, permissions, pagination, generateEditFieldsFromAttributes, ensureAttributesLoaded, fetchAttributes, fetchPermissions, // Additional methods for FormGenerator updateOptimistically, handleInlineUpdate, fetchConnectionById }; } // Hook for OAuth connection popup flow (similar to useMsalAuth) export function useOAuthConnect() { const { connectService, fetchConnections } = useConnections(); const [isConnecting, setIsConnecting] = useState(false); const [connectError, setConnectError] = useState(null); const connectWithPopup = async (connectionId: string): Promise => { setIsConnecting(true); setConnectError(null); try { // Get the OAuth URL from backend const response = await connectService(connectionId); if (!response.authUrl) { throw new Error('No OAuth URL received from backend'); } console.log('OAuth URL from backend:', response.authUrl); return new Promise((resolve, reject) => { // Convert relative URL to absolute URL if needed let authUrl = response.authUrl; if (authUrl.startsWith('/')) { authUrl = `${getApiBaseUrl()}${authUrl}`; } // Open popup using the same pattern as useAuthentication.ts const popup = window.open( authUrl, 'oauth-connect', 'width=500,height=600,scrollbars=yes,resizable=yes' ); if (!popup) { setConnectError('Popup was blocked. Please allow popups and try again.'); setIsConnecting(false); reject(new Error('Popup was blocked')); return; } // Handle popup closing without completing auth const checkClosed = setInterval(() => { if (popup.closed) { clearInterval(checkClosed); window.removeEventListener('message', messageListener); setIsConnecting(false); console.log('OAuth popup closed'); // Refresh connections anyway in case it succeeded fetchConnections(); setConnectError('Authentication was cancelled'); reject(new Error('Authentication was cancelled')); } }, 1000); // Listen for messages from the popup (similar to useMsalAuth) const messageListener = (event: MessageEvent) => { // Verify origin for security const apiUrl = new URL(getApiBaseUrl()); if (event.origin !== apiUrl.origin) { return; } if ( event.data.type === 'msft_connection_success' || event.data.type === 'google_connection_success' || event.data.type === 'clickup_connection_success' ) { // Clean up - IMPORTANT: clear the checkClosed interval first clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); setIsConnecting(false); console.log('OAuth connection successful'); // Refresh connections fetchConnections(); resolve(); } else if ( event.data.type === 'msft_connection_error' || event.data.type === 'google_connection_error' || event.data.type === 'clickup_connection_error' ) { // Handle error - also clear the checkClosed interval clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); setIsConnecting(false); setConnectError(event.data.error || 'OAuth connection failed'); reject(new Error(event.data.error || 'OAuth connection failed')); } }; // Add message listener window.addEventListener('message', messageListener); }); } catch (error: any) { setConnectError(error.message || 'OAuth connection failed'); setIsConnecting(false); throw error; } }; return { connectWithPopup, isConnecting, error: connectError }; } // Hook for disconnecting services export function useDisconnect() { const { disconnectService, fetchConnections } = useConnections(); const [isDisconnecting, setIsDisconnecting] = useState(false); const [disconnectError, setDisconnectError] = useState(null); const disconnect = async (connectionId: string): Promise => { setIsDisconnecting(true); setDisconnectError(null); try { await disconnectService(connectionId); console.log('Service disconnected successfully'); // Refresh connections to update the status await fetchConnections(); } catch (error: any) { setDisconnectError(error.message || 'Disconnect failed'); console.error('Error disconnecting service:', error); throw error; } finally { setIsDisconnecting(false); } }; return { disconnect, isDisconnecting, error: disconnectError }; } // Hook for individual connection operations export function useConnection(connectionId?: string) { const [connection, setConnection] = useState(null); const { request, isLoading, error } = useApiRequest(); const fetchConnection = async (id: string = connectionId!): Promise => { if (!id) return null; try { // Since there's no individual connection endpoint, fetch all and filter const data = await fetchConnectionsApi(request); const connections = Array.isArray(data) ? data : (data?.items || []); const foundConnection = connections.find((conn: Connection) => conn.id === id); setConnection(foundConnection || null); return foundConnection || null; } catch (error) { console.error('Error fetching connection:', error); setConnection(null); throw error; } }; return { connection, fetchConnection, isLoading, error }; }