891 lines
No EOL
29 KiB
TypeScript
891 lines
No EOL
29 KiB
TypeScript
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<Connection[]>([]);
|
|
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
|
|
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
|
|
const [pagination, setPagination] = useState<{
|
|
currentPage: number;
|
|
pageSize: number;
|
|
totalItems: number;
|
|
totalPages: number;
|
|
} | null>(null);
|
|
const [isConnecting, setIsConnecting] = useState(false);
|
|
const [connectError, setConnectError] = useState<string | null>(null);
|
|
const { request, isLoading, error } = useApiRequest<any, any>();
|
|
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<Connection[]> => {
|
|
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<Connection> => {
|
|
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<ConnectResponse> => {
|
|
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<Connection>): Promise<Connection> => {
|
|
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<Connection> => {
|
|
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<Connection> => {
|
|
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<void> => {
|
|
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<void> => {
|
|
if (isConnecting) return;
|
|
setIsConnecting(true);
|
|
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 await new Promise<void>((resolve, reject) => {
|
|
const popup = window.open(
|
|
authUrl,
|
|
'google-connection',
|
|
'width=500,height=600,scrollbars=yes,resizable=yes'
|
|
);
|
|
|
|
if (!popup) {
|
|
setIsConnecting(false);
|
|
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);
|
|
setIsConnecting(false);
|
|
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();
|
|
setIsConnecting(false);
|
|
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();
|
|
setIsConnecting(false);
|
|
reject(new Error(event.data.error || 'Google connection failed'));
|
|
}
|
|
};
|
|
|
|
window.addEventListener('message', messageListener);
|
|
});
|
|
} catch (error) {
|
|
setIsConnecting(false);
|
|
console.error('Error creating Google connection:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Create ClickUp connection and open OAuth popup
|
|
const createClickupConnectionAndAuth = async (): Promise<void> => {
|
|
if (isConnecting) return;
|
|
setIsConnecting(true);
|
|
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 await new Promise<void>((resolve, reject) => {
|
|
const popup = window.open(
|
|
authUrl,
|
|
'clickup-connection',
|
|
'width=500,height=600,scrollbars=yes,resizable=yes'
|
|
);
|
|
|
|
if (!popup) {
|
|
setIsConnecting(false);
|
|
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);
|
|
setIsConnecting(false);
|
|
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();
|
|
setIsConnecting(false);
|
|
console.log('ClickUp connection successful');
|
|
fetchConnections();
|
|
resolve();
|
|
} else if (event.data.type === 'clickup_connection_error') {
|
|
clearInterval(checkClosed);
|
|
window.removeEventListener('message', messageListener);
|
|
popup.close();
|
|
setIsConnecting(false);
|
|
reject(new Error(event.data.error || 'ClickUp connection failed'));
|
|
}
|
|
};
|
|
|
|
window.addEventListener('message', messageListener);
|
|
});
|
|
} catch (error) {
|
|
setIsConnecting(false);
|
|
console.error('Error creating ClickUp connection:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// Create Microsoft connection and open OAuth popup
|
|
const createMicrosoftConnectionAndAuth = async (): Promise<void> => {
|
|
if (isConnecting) return;
|
|
setIsConnecting(true);
|
|
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 await new Promise<void>((resolve, reject) => {
|
|
const popup = window.open(
|
|
authUrl,
|
|
'msft-connection',
|
|
'width=500,height=600,scrollbars=yes,resizable=yes'
|
|
);
|
|
|
|
if (!popup) {
|
|
setIsConnecting(false);
|
|
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);
|
|
setIsConnecting(false);
|
|
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();
|
|
setIsConnecting(false);
|
|
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();
|
|
setIsConnecting(false);
|
|
reject(new Error(event.data.error || 'Microsoft connection failed'));
|
|
}
|
|
};
|
|
|
|
window.addEventListener('message', messageListener);
|
|
});
|
|
} catch (error) {
|
|
setIsConnecting(false);
|
|
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<Connection>) => {
|
|
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<Connection>, 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<Connection | null> => {
|
|
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<string | null>(null);
|
|
|
|
const connectWithPopup = async (connectionId: string): Promise<void> => {
|
|
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<string | null>(null);
|
|
|
|
const disconnect = async (connectionId: string): Promise<void> => {
|
|
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<Connection | null>(null);
|
|
const { request, isLoading, error } = useApiRequest<any, Connection[]>();
|
|
|
|
const fetchConnection = async (id: string = connectionId!): Promise<Connection | null> => {
|
|
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
|
|
};
|
|
}
|