From c5ecd88e66a329e11632dfed573aff53dc0ecb10 Mon Sep 17 00:00:00 2001 From: Ida Dittrich Date: Wed, 13 Aug 2025 11:48:57 +0200 Subject: [PATCH] sharepoint integration --- .../FormGenerator/FormGenerator.tsx | 2 - .../TestSharepointTable.module.css | 40 + .../TestSharepoint/TestSharepointTable.tsx | 72 ++ src/components/TestSharepoint/index.ts | 11 + .../testSharepointInterfaces.ts | 112 +++ .../TestSharepoint/testSharepointLogic.tsx | 395 ++++++++ src/hooks/useSharePointTest.ts | 19 + src/locales/de.ts | 24 + src/locales/en.ts | 24 + src/locales/fr.ts | 24 + .../TestSharepoint.module.css | 121 +++ src/pages/Home/HomeStyles/pages.module.css | 2 +- src/pages/Home/TestSharepoint.tsx | 885 +++++++----------- 13 files changed, 1168 insertions(+), 563 deletions(-) create mode 100644 src/components/TestSharepoint/TestSharepointTable.module.css create mode 100644 src/components/TestSharepoint/TestSharepointTable.tsx create mode 100644 src/components/TestSharepoint/index.ts create mode 100644 src/components/TestSharepoint/testSharepointInterfaces.ts create mode 100644 src/components/TestSharepoint/testSharepointLogic.tsx rename src/pages/Home/{ => HomeStyles}/TestSharepoint.module.css (74%) diff --git a/src/components/FormGenerator/FormGenerator.tsx b/src/components/FormGenerator/FormGenerator.tsx index 4c89f02..6c74231 100644 --- a/src/components/FormGenerator/FormGenerator.tsx +++ b/src/components/FormGenerator/FormGenerator.tsx @@ -344,8 +344,6 @@ export function FormGenerator>({ return (
- {title &&

{title}

} - {(searchable || filterable) && (
diff --git a/src/components/TestSharepoint/TestSharepointTable.module.css b/src/components/TestSharepoint/TestSharepointTable.module.css new file mode 100644 index 0000000..8b8b048 --- /dev/null +++ b/src/components/TestSharepoint/TestSharepointTable.module.css @@ -0,0 +1,40 @@ +.testSharepointTable { + width: 100%; + display: flex; + flex-direction: column; + gap: 20px; +} + +.errorState { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; + background: var(--color-bg); + border: 1px solid var(--color-error, #dc3545); + border-radius: 8px; + color: var(--color-error, #dc3545); + text-align: center; + gap: 15px; +} + +.retryButton { + padding: 8px 16px; + background: var(--color-error, #dc3545); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: var(--font-family); + font-size: 14px; + transition: background-color 0.2s ease; +} + +.retryButton:hover { + background: var(--color-error-hover, #c82333); +} + +.sharepointFormGenerator { + width: 100%; +} diff --git a/src/components/TestSharepoint/TestSharepointTable.tsx b/src/components/TestSharepoint/TestSharepointTable.tsx new file mode 100644 index 0000000..614e3ab --- /dev/null +++ b/src/components/TestSharepoint/TestSharepointTable.tsx @@ -0,0 +1,72 @@ +import { FormGenerator } from '../FormGenerator'; +import { useLanguage } from '../../contexts/LanguageContext'; +import styles from './TestSharepointTable.module.css'; +import { useTestSharepointLogic } from './testSharepointLogic'; +import type { TestSharepointTableProps } from './testSharepointInterfaces'; + +export function TestSharepointTable({ + className = '', + documents, + documentsLoading, + documentsError, + columns, + actions, + onRowClick +}: TestSharepointTableProps) { + const { t } = useLanguage(); + + // Get fallback data from hook if props not provided (for backwards compatibility) + const hookData = useTestSharepointLogic(); + + // Use props data if provided, otherwise fall back to hook data + const actualDocuments = documents ?? hookData.documents; + const actualLoading = documentsLoading ?? hookData.documentsLoading; + const actualError = documentsError ?? hookData.documentsError; + const actualColumns = columns ?? hookData.columns; + const actualActions = actions ?? hookData.actions; + + // Debug the data being passed to FormGenerator + console.log('TestSharepointTable - actualDocuments:', actualDocuments); + console.log('TestSharepointTable - actualLoading:', actualLoading); + console.log('TestSharepointTable - actualError:', actualError); + console.log('TestSharepointTable - actualColumns:', actualColumns); + + // Show error state + if (actualError) { + return ( +
+
+

{t('sharepoint.error.loading', 'Error loading SharePoint documents:')} {actualError}

+ +
+
+ ); + } + + return ( +
+ +
+ ); +} + +export default TestSharepointTable; diff --git a/src/components/TestSharepoint/index.ts b/src/components/TestSharepoint/index.ts new file mode 100644 index 0000000..ea7a8e3 --- /dev/null +++ b/src/components/TestSharepoint/index.ts @@ -0,0 +1,11 @@ +export { TestSharepointTable } from './TestSharepointTable'; +export { useTestSharepointLogic } from './testSharepointLogic'; +export type { + TestSharepointTableProps, + TableAction, + SharePointHandlers, + SharePointOperationsReturn, + SharePointConnectionsReturn, + SharePointTableConfig, + TestSharepointLogicReturn +} from './testSharepointInterfaces'; diff --git a/src/components/TestSharepoint/testSharepointInterfaces.ts b/src/components/TestSharepoint/testSharepointInterfaces.ts new file mode 100644 index 0000000..122ef6e --- /dev/null +++ b/src/components/TestSharepoint/testSharepointInterfaces.ts @@ -0,0 +1,112 @@ +import { ColumnConfig } from '../FormGenerator'; +import React from 'react'; + +// Re-export SharePoint-related interfaces from hooks +export type { + SharePointConnection, + SharePointDocument, + SharePointResponse, + SharePointListRequest, + SharePointFindRequest, + SharePointReadRequest, + SharePointUploadRequest +} from '../../hooks/useSharePointTest'; + +// Import for local use +import type { SharePointConnection, SharePointDocument } from '../../hooks/useSharePointTest'; + +// Component Props Interfaces +export interface TestSharepointTableProps { + className?: string; + documents?: SharePointDocument[]; + documentsLoading?: boolean; + documentsError?: string | null; + columns?: any[]; + actions?: any[]; + onRowClick?: (row: any) => void; +} + +// Table Action Interface +export interface TableAction { + label: string; + onClick: (document: SharePointDocument) => Promise | void; + icon: React.ReactNode | ((document: SharePointDocument) => React.ReactNode); +} + +// SharePoint Operation Handler Types +export interface SharePointHandlers { + handleConnectionTest: (connectionId: string) => Promise; + handleListDocuments: (connectionId: string, siteUrl: string, folderPaths: string[]) => Promise; + handleFindDocuments: (connectionId: string, siteUrl: string, query: string) => Promise; + handleReadDocument: (connectionId: string, siteUrl: string, documentPath: string) => Promise<{ success: boolean; data?: any; error?: string }>; +} + +// Hook Return Types for SharePoint Operations +export interface SharePointOperationsReturn extends SharePointHandlers { + testingConnections: Set; + loadingDocuments: boolean; + connectionError: string | null; + documentsError: string | null; + isLoading: boolean; +} + +// Hook Return Types for SharePoint Connections +export interface SharePointConnectionsReturn { + connections: SharePointConnection[]; + loading: boolean; + error: string | null; + refetch: () => Promise; + testConnection: (connectionId: string) => Promise; +} + +// SharePoint Table Configuration +export interface SharePointTableConfig { + columns: ColumnConfig[]; + actions: TableAction[]; + pageSize: number; + searchable: boolean; + filterable: boolean; + sortable: boolean; + resizable: boolean; + pagination: boolean; +} + +// Hook Return Type for TestSharepoint Logic +export interface TestSharepointLogicReturn { + // Connection data + connections: SharePointConnection[]; + selectedConnection: SharePointConnection | null; + connectionLoading: boolean; + connectionError: string | null; + + // Document data + documents: SharePointDocument[]; + documentsLoading: boolean; + documentsError: string | null; + + // Table configuration + columns: ColumnConfig[]; + actions: TableAction[]; + + // Connection testing + testingConnections: Set; + connectionTestResults: Record; + + // Site discovery + discoveredSites: any[]; + sitesDiscovered: boolean; + + // Token debug + tokenDebugInfo: any; + + // Handlers + handleSelectConnection: (connectionId: string) => void; + handleTestConnection: (connectionId: string) => Promise; + handleListDocuments: (siteUrl?: string, folderPaths?: string[]) => Promise; + handleDiscoverSites: () => Promise; + handleSelectSite: (siteUrl: string) => void; + handleDebugTokens: () => Promise; + handleCleanupTokens: () => Promise; + handleFolderNavigation: (document: SharePointDocument, currentPath: string) => string; + refetchConnections: () => Promise; +} diff --git a/src/components/TestSharepoint/testSharepointLogic.tsx b/src/components/TestSharepoint/testSharepointLogic.tsx new file mode 100644 index 0000000..addec42 --- /dev/null +++ b/src/components/TestSharepoint/testSharepointLogic.tsx @@ -0,0 +1,395 @@ +import { useMemo, useState, useEffect } from 'react'; +import { IoIosLink, IoIosCloudDownload } from 'react-icons/io'; + +import { ColumnConfig } from '../FormGenerator'; +import { useSharePointTest } from '../../hooks/useSharePointTest'; +import { useLanguage } from '../../contexts/LanguageContext'; +import type { + TableAction, + SharePointConnection, + SharePointDocument, + TestSharepointLogicReturn +} from './testSharepointInterfaces'; + +export function useTestSharepointLogic(): TestSharepointLogicReturn { + const { + getConnections, + testConnection, + listDocuments, + discoverSites, + debugTokenDetails, + cleanupTokens, + isLoading + } = useSharePointTest(); + const { t } = useLanguage(); + + // State management + const [connections, setConnections] = useState([]); + const [selectedConnection, setSelectedConnection] = useState(null); + const [connectionLoading, setConnectionLoading] = useState(false); + const [connectionError, setConnectionError] = useState(null); + + const [documents, setDocuments] = useState([]); + const [documentsLoading, setDocumentsLoading] = useState(false); + const [documentsError, setDocumentsError] = useState(null); + + const [testingConnections, setTestingConnections] = useState>(new Set()); + const [connectionTestResults, setConnectionTestResults] = useState>({}); + + const [discoveredSites, setDiscoveredSites] = useState([]); + const [sitesDiscovered, setSitesDiscovered] = useState(false); + const [tokenDebugInfo, setTokenDebugInfo] = useState(null); + + // Load connections on mount + useEffect(() => { + loadConnections(); + }, []); + + const loadConnections = async () => { + setConnectionLoading(true); + setConnectionError(null); + try { + const conns = await getConnections(); + setConnections(conns); + if (conns.length > 0 && !selectedConnection) { + setSelectedConnection(conns[0]); + } + } catch (error) { + console.error('Failed to load connections:', error); + setConnectionError(error instanceof Error ? error.message : 'Failed to load connections'); + } finally { + setConnectionLoading(false); + } + }; + + // Configure columns for the SharePoint documents table + const columns: ColumnConfig[] = useMemo(() => [ + { + key: 'documentName', + label: t('sharepoint.column.documentName', 'Document Name'), + type: 'string', + width: 300, + minWidth: 200, + maxWidth: 400, + sortable: true, + filterable: true, + searchable: true, + formatter: (value: string, row: any) => ( + + {row?.type === 'folder' ? '📁' : '📄'} {value} + + ) + }, + { + key: 'mimeType', + label: t('sharepoint.column.mimeType', 'MIME Type'), + type: 'string', + width: 200, + minWidth: 150, + maxWidth: 300, + sortable: true, + filterable: true, + searchable: true, + }, + { + key: 'size', + label: t('sharepoint.column.size', 'Size'), + type: 'number', + width: 140, + minWidth: 120, + maxWidth: 180, + sortable: true, + filterable: false, + formatter: (value: number | string | undefined) => { + if (!value || value === 0) return '-'; + + const sizeInBytes = typeof value === 'string' ? parseInt(value, 10) : value; + const units = ['Bytes', 'KB', 'MB', 'GB']; + let size = sizeInBytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return ( + + {`${size.toFixed(1)} ${units[unitIndex]}`} + + ); + } + }, + { + key: 'path', + label: t('sharepoint.column.path', 'Path'), + type: 'string', + width: 250, + minWidth: 200, + maxWidth: 400, + sortable: true, + filterable: true, + searchable: true, + }, + ], [t]); + + // Handle connection selection + const handleSelectConnection = (connectionId: string) => { + const connection = connections.find(conn => conn.id === connectionId); + if (connection) { + setSelectedConnection(connection); + // Clear documents when changing connection + setDocuments([]); + setDocumentsError(null); + } + }; + + // Handle connection testing + const handleTestConnection = async (connectionId: string) => { + setTestingConnections(prev => new Set(prev).add(connectionId)); + try { + const result = await testConnection(connectionId); + setConnectionTestResults(prev => ({ ...prev, [connectionId]: result })); + } catch (error) { + console.error('Connection test failed:', error); + setConnectionTestResults(prev => ({ + ...prev, + [connectionId]: { + success: false, + error: error instanceof Error ? error.message : 'Connection test failed' + } + })); + } finally { + setTestingConnections(prev => { + const newSet = new Set(prev); + newSet.delete(connectionId); + return newSet; + }); + } + }; + + // Handle listing documents + const handleListDocuments = async (siteUrl?: string, folderPaths?: string[]) => { + if (!selectedConnection) { + setDocumentsError('No connection selected'); + return; + } + + setDocumentsLoading(true); + setDocumentsError(null); + + try { + const connectionReference = `connection:${selectedConnection.authority}:${selectedConnection.externalUsername}:${selectedConnection.id}`; + + const response = await listDocuments({ + connectionReference, + siteUrl: siteUrl || 'https://your-tenant.sharepoint.com/sites/your-site', + folderPaths: folderPaths || ['/'], + includeSubfolders: false, + expectedDocumentFormats: [{ extension: '.json', mimeType: 'application/json' }] + }); + + console.log('SharePoint response:', response); + + if (response.success && response.data?.documents) { + // Extract the actual files from the nested structure + const documents = response.data.documents; + if (documents.length > 0 && documents[0].documentData?.listResults) { + // Flatten all files from all folder results + const allFiles: SharePointDocument[] = []; + documents[0].documentData.listResults.forEach((folderResult: any) => { + if (folderResult.items && Array.isArray(folderResult.items)) { + folderResult.items.forEach((item: any) => { + // Convert SharePoint item to our document format + allFiles.push({ + documentName: item.name || 'Unknown', + mimeType: item.file?.mimeType || (item.type === 'folder' ? 'folder' : 'unknown'), + size: item.size || 0, + path: folderResult.folderPath || '/', + type: item.type || (item.folder ? 'folder' : 'file'), + id: item.id, + documentData: item // Store the full item data + }); + }); + } + }); + console.log('Extracted files:', allFiles); + console.log('Sample file structure:', allFiles[0]); + setDocuments(allFiles); + } else { + console.log('No listResults found in response'); + setDocuments([]); + } + } else { + console.log('Response error or no documents:', response); + setDocumentsError(response.error || 'Failed to list documents'); + setDocuments([]); + } + } catch (error) { + console.error('Failed to list documents:', error); + setDocumentsError(error instanceof Error ? error.message : 'Failed to list documents'); + setDocuments([]); + } finally { + setDocumentsLoading(false); + } + }; + + // Handle site discovery + const handleDiscoverSites = async () => { + if (!selectedConnection) { + return; + } + + try { + const result = await discoverSites(); + if (result.success && result.data && result.data.sites) { + setDiscoveredSites(result.data.sites); + setSitesDiscovered(true); + setDocumentsError(null); // Clear any previous errors + } else { + console.error('Site discovery failed:', result); + setDiscoveredSites([]); + setSitesDiscovered(true); // Set to true so we show the "no sites" message + // Set error message to help user understand what went wrong + const errorMsg = result.error || result.message || 'Unknown error occurred'; + setDocumentsError(`Site discovery failed: ${errorMsg}`); + } + } catch (error) { + console.error('Site discovery failed:', error); + setDiscoveredSites([]); + setSitesDiscovered(true); + setDocumentsError(`Site discovery error: ${error instanceof Error ? error.message : 'Network or authentication error'}`); + } + }; + + // Handle site selection + const handleSelectSite = (siteUrl: string) => { + // This will be used by the parent component + console.log('Site selected:', siteUrl); + }; + + // Handle token debug + const handleDebugTokens = async () => { + try { + const result = await debugTokenDetails(); + setTokenDebugInfo(result); + console.log('Token debug info:', result); + } catch (error) { + console.error('Token debug failed:', error); + setTokenDebugInfo({ error: error instanceof Error ? error.message : 'Failed to get token info' }); + } + }; + + // Handle token cleanup + const handleCleanupTokens = async () => { + try { + const result = await cleanupTokens(); + console.log('Token cleanup result:', result); + // Clear the debug info to force refresh + setTokenDebugInfo(null); + // Show success message + setDocumentsError(null); + alert(`Success! Deleted ${result.data?.tokensDeleted || 0} tokens. Please reconnect your Microsoft account now.`); + } catch (error) { + console.error('Token cleanup failed:', error); + alert(`Token cleanup failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + }; + + // Handle folder navigation + const handleFolderNavigation = (document: SharePointDocument, currentPath: string) => { + if (document.type === 'folder') { + // Build the new path by combining current path with folder name + const newPath = currentPath === '/' ? `/${document.documentName}` : `${currentPath}/${document.documentName}`; + console.log('Navigating to folder:', newPath); + return newPath; + } + return currentPath; + }; + + // Handle document actions + const handleViewDocument = async (document: SharePointDocument) => { + console.log('View document:', document); + // TODO: Implement document viewing + }; + + const handleDownloadDocument = async (document: SharePointDocument) => { + console.log('Download document:', document); + // TODO: Implement document download + }; + + // Configure action buttons + const actions: TableAction[] = useMemo(() => [ + { + label: t('sharepoint.action.view', 'View'), + icon: , + onClick: (document: SharePointDocument) => { + handleViewDocument(document); + } + }, + { + label: t('sharepoint.action.download', 'Download'), + icon: , + onClick: (document: SharePointDocument) => { + handleDownloadDocument(document); + } + } + ], [t]); + + // Refetch connections + const refetchConnections = async () => { + await loadConnections(); + }; + + return { + // Connection data + connections, + selectedConnection, + connectionLoading, + connectionError, + + // Document data + documents, + documentsLoading: documentsLoading || isLoading, + documentsError, + + // Table configuration + columns, + actions, + + // Connection testing + testingConnections, + connectionTestResults, + + // Site discovery + discoveredSites, + sitesDiscovered, + + // Token debug + tokenDebugInfo, + + // Handlers + handleSelectConnection, + handleTestConnection, + handleListDocuments, + handleDiscoverSites, + handleSelectSite, + handleDebugTokens, + handleCleanupTokens, + handleFolderNavigation, + refetchConnections + }; +} diff --git a/src/hooks/useSharePointTest.ts b/src/hooks/useSharePointTest.ts index fe4b5cb..92aa685 100644 --- a/src/hooks/useSharePointTest.ts +++ b/src/hooks/useSharePointTest.ts @@ -18,6 +18,10 @@ export interface SharePointDocument { documentName: string; documentData: any; mimeType: string; + size?: number; + path?: string; + type?: 'file' | 'folder'; + id?: string; } export interface SharePointResponse { @@ -242,6 +246,20 @@ export function useSharePointTest() { } }; + // Cleanup all Microsoft tokens + const cleanupTokens = async (): Promise => { + try { + const response = await request({ + url: '/api/test-sharepoint/cleanup-tokens', + method: 'delete' + }); + return response; + } catch (error) { + console.error('Error cleaning up tokens:', error); + throw error; + } + }; + // Discover SharePoint sites const discoverSites = async (): Promise => { try { @@ -267,6 +285,7 @@ export function useSharePointTest() { getExamples, debugTokens, debugTokenDetails, + cleanupTokens, discoverSites, // State diff --git a/src/locales/de.ts b/src/locales/de.ts index 8c420b2..96e612e 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -343,4 +343,28 @@ export default { 'formgen.pagination.prev': 'Vorherige Seite', 'formgen.pagination.next': 'Nächste Seite', 'formgen.pagination.last': 'Letzte Seite', + + // SharePoint Test + 'sharepoint.title': 'SharePoint Test', + 'sharepoint.table.title': 'SharePoint Dokumente', + 'sharepoint.error.loading': 'Fehler beim Laden der SharePoint Dokumente:', + 'sharepoint.button.retry': 'Wiederholen', + 'sharepoint.button.testConnection': 'Verbindung testen', + 'sharepoint.button.listDocuments': 'Dokumente auflisten', + 'sharepoint.button.discoverSites': 'Sites entdecken', + 'sharepoint.column.documentName': 'Dokumentname', + 'sharepoint.column.mimeType': 'MIME-Typ', + 'sharepoint.column.size': 'Größe', + 'sharepoint.column.path': 'Pfad', + 'sharepoint.action.view': 'Anzeigen', + 'sharepoint.action.download': 'Herunterladen', + 'sharepoint.connections.title': 'Microsoft Verbindungen', + 'sharepoint.connections.noConnections': 'Keine Microsoft-Verbindungen gefunden. Bitte erstellen Sie zuerst eine Verbindung.', + 'sharepoint.connections.loading': 'Verbindungen werden geladen...', + 'sharepoint.sites.discovered': 'Entdeckte Sites', + 'sharepoint.sites.noSites': 'Keine SharePoint-Sites gefunden', + 'sharepoint.sites.authError': 'Authentifizierungstoken abgelaufen oder ungültig. Bitte verbinden Sie Ihr Microsoft-Konto erneut.', + 'sharepoint.sites.retryConnection': 'Versuchen Sie, Ihr Microsoft-Konto auf der Verbindungsseite erneut zu verbinden.', + 'sharepoint.form.siteUrl': 'SharePoint Site URL', + 'sharepoint.form.folderPaths': 'Ordnerpfade', }; \ No newline at end of file diff --git a/src/locales/en.ts b/src/locales/en.ts index 09c4a44..d823979 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -344,4 +344,28 @@ export default { 'formgen.pagination.prev': 'Previous page', 'formgen.pagination.next': 'Next page', 'formgen.pagination.last': 'Last page', + + // SharePoint Test + 'sharepoint.title': 'SharePoint Test', + 'sharepoint.table.title': 'SharePoint Documents', + 'sharepoint.error.loading': 'Error loading SharePoint documents:', + 'sharepoint.button.retry': 'Retry', + 'sharepoint.button.testConnection': 'Test Connection', + 'sharepoint.button.listDocuments': 'List Documents', + 'sharepoint.button.discoverSites': 'Discover Sites', + 'sharepoint.column.documentName': 'Document Name', + 'sharepoint.column.mimeType': 'MIME Type', + 'sharepoint.column.size': 'Size', + 'sharepoint.column.path': 'Path', + 'sharepoint.action.view': 'View', + 'sharepoint.action.download': 'Download', + 'sharepoint.connections.title': 'Microsoft Connections', + 'sharepoint.connections.noConnections': 'No Microsoft connections found. Please create a connection first.', + 'sharepoint.connections.loading': 'Loading connections...', + 'sharepoint.sites.discovered': 'Discovered Sites', + 'sharepoint.sites.noSites': 'No SharePoint sites found', + 'sharepoint.sites.authError': 'Authentication token expired or invalid. Please reconnect your Microsoft account.', + 'sharepoint.sites.retryConnection': 'Try reconnecting your Microsoft account in the Connections page.', + 'sharepoint.form.siteUrl': 'SharePoint Site URL', + 'sharepoint.form.folderPaths': 'Folder Paths', }; \ No newline at end of file diff --git a/src/locales/fr.ts b/src/locales/fr.ts index d4ef499..ad5f9a2 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -343,4 +343,28 @@ export default { 'formgen.pagination.prev': 'Page précédente', 'formgen.pagination.next': 'Page suivante', 'formgen.pagination.last': 'Dernière page', + + // SharePoint Test + 'sharepoint.title': 'Test SharePoint', + 'sharepoint.table.title': 'Documents SharePoint', + 'sharepoint.error.loading': 'Erreur lors du chargement des documents SharePoint:', + 'sharepoint.button.retry': 'Réessayer', + 'sharepoint.button.testConnection': 'Tester la connexion', + 'sharepoint.button.listDocuments': 'Lister les documents', + 'sharepoint.button.discoverSites': 'Découvrir les sites', + 'sharepoint.column.documentName': 'Nom du document', + 'sharepoint.column.mimeType': 'Type MIME', + 'sharepoint.column.size': 'Taille', + 'sharepoint.column.path': 'Chemin', + 'sharepoint.action.view': 'Voir', + 'sharepoint.action.download': 'Télécharger', + 'sharepoint.connections.title': 'Connexions Microsoft', + 'sharepoint.connections.noConnections': 'Aucune connexion Microsoft trouvée. Veuillez d\'abord créer une connexion.', + 'sharepoint.connections.loading': 'Chargement des connexions...', + 'sharepoint.sites.discovered': 'Sites découverts', + 'sharepoint.sites.noSites': 'Aucun site SharePoint trouvé', + 'sharepoint.sites.authError': 'Token d\'authentification expiré ou invalide. Veuillez reconnecter votre compte Microsoft.', + 'sharepoint.sites.retryConnection': 'Essayez de reconnecter votre compte Microsoft dans la page Connexions.', + 'sharepoint.form.siteUrl': 'URL du site SharePoint', + 'sharepoint.form.folderPaths': 'Chemins des dossiers', }; \ No newline at end of file diff --git a/src/pages/Home/TestSharepoint.module.css b/src/pages/Home/HomeStyles/TestSharepoint.module.css similarity index 74% rename from src/pages/Home/TestSharepoint.module.css rename to src/pages/Home/HomeStyles/TestSharepoint.module.css index fc60778..e78b843 100644 --- a/src/pages/Home/TestSharepoint.module.css +++ b/src/pages/Home/HomeStyles/TestSharepoint.module.css @@ -17,6 +17,7 @@ padding: 20px; margin-bottom: 20px; border: 1px solid var(--border-primary); + flex-shrink: 0; /* Prevent sections from shrinking */ } .sectionTitle { @@ -302,6 +303,81 @@ line-height: 1.4; } +.sharepointTableContainer { + width: 100%; +} + +.loading { + text-align: center; + padding: 20px; + color: var(--text-secondary); + font-style: italic; +} + +.errorMessage { + background: var(--color-error-bg, #ffe6e6); + color: var(--color-error, #d32f2f); + padding: 12px; + border-radius: 4px; + border: 1px solid var(--color-error, #d32f2f); + margin-bottom: 15px; +} + +.successMessage { + color: var(--color-success, #4caf50); + font-weight: 500; +} + +.button { + padding: 6px 12px; + border: 1px solid var(--border-primary); + background: var(--bg-primary); + color: var(--text-primary); + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.button:hover { + background: var(--bg-hover); + border-color: var(--border-accent); +} + +.button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.label { + display: block; + margin-bottom: 5px; + font-weight: 500; + color: var(--text-primary); +} + +.input { + width: 100%; + padding: 8px; + border: 1px solid var(--border-primary); + border-radius: 4px; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 14px; +} + +.textarea { + width: 100%; + padding: 8px; + border: 1px solid var(--border-primary); + border-radius: 4px; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 14px; + resize: vertical; + min-height: 80px; +} + .requestPreview { background: var(--bg-code); border: 1px solid var(--border-secondary); @@ -394,4 +470,49 @@ color: #856404; text-align: center; margin-top: 10px; +} + +/* Breadcrumb Navigation */ +.breadcrumb { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + background: var(--color-bg-secondary, #f8f9fa); + border: 1px solid var(--color-border, #dee2e6); + border-radius: 6px; + margin-bottom: 16px; + font-size: 14px; +} + +.breadcrumbLabel { + font-weight: 600; + color: var(--color-text-secondary, #6c757d); + margin-right: 8px; +} + +.breadcrumbItem { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border-radius: 4px; + color: var(--color-text, #212529); + transition: all 0.2s ease; +} + +.breadcrumbClickable { + cursor: pointer; + color: var(--color-primary, #007bff); + text-decoration: underline; +} + +.breadcrumbClickable:hover { + background: var(--color-primary-bg, #e7f1ff); + color: var(--color-primary-dark, #0056b3); +} + +.breadcrumbSeparator { + color: var(--color-text-secondary, #6c757d); + font-weight: bold; } \ No newline at end of file diff --git a/src/pages/Home/HomeStyles/pages.module.css b/src/pages/Home/HomeStyles/pages.module.css index 3286333..23b8726 100644 --- a/src/pages/Home/HomeStyles/pages.module.css +++ b/src/pages/Home/HomeStyles/pages.module.css @@ -24,7 +24,7 @@ background: var(--color-bg); box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.30); gap: 20px; - height: 100%; + min-height: calc(100vh - 50px); /* Ensure minimum height but allow expansion */ } /* Page headers with consistent spacing */ diff --git a/src/pages/Home/TestSharepoint.tsx b/src/pages/Home/TestSharepoint.tsx index 517201a..1a81958 100644 --- a/src/pages/Home/TestSharepoint.tsx +++ b/src/pages/Home/TestSharepoint.tsx @@ -1,269 +1,86 @@ -import { useState, useEffect } from 'react'; -import { - useSharePointTest, - SharePointConnection, - SharePointListRequest, - SharePointFindRequest, - SharePointReadRequest, - SharePointUploadRequest, - SharePointResponse -} from '../../hooks/useSharePointTest'; -import styles from './TestSharepoint.module.css'; - -type TestOperation = 'list' | 'find' | 'read' | 'upload'; - -interface FormData { - connectionReference: string; - siteUrl: string; - folderPaths: string[]; - query: string; - searchScope: string; - documentList: string; - documentPaths: string[]; - fileNames: string[]; - includeMetadata: boolean; - includeSubfolders: boolean; -} +import { useState } from 'react'; +import { IoIosRefresh, IoIosLink } from 'react-icons/io'; +import { useLanguage } from '../../contexts/LanguageContext'; +import sharedStyles from './HomeStyles/pages.module.css' +import styles from './HomeStyles/TestSharepoint.module.css' +import { TestSharepointTable, useTestSharepointLogic } from '../../components/TestSharepoint' function TestSharepoint() { + const { t } = useLanguage(); const { - getConnections, - testConnection, - listDocuments, - findDocuments, - readDocuments, - uploadDocuments, - getExamples, - debugTokens, - debugTokenDetails, - discoverSites, - isLoading, - error, - lastResponse - } = useSharePointTest(); + connections, + selectedConnection, + connectionLoading, + connectionError, + documents, + documentsLoading, + documentsError, + columns, + actions, + testingConnections, + connectionTestResults, + discoveredSites, + sitesDiscovered, + tokenDebugInfo, + handleSelectConnection, + handleTestConnection, + handleListDocuments, + handleDiscoverSites, + handleSelectSite, + handleDebugTokens, + handleCleanupTokens, + handleFolderNavigation, + refetchConnections + } = useTestSharepointLogic(); - // State - const [connections, setConnections] = useState([]); - const [selectedConnection, setSelectedConnection] = useState(''); - const [activeTab, setActiveTab] = useState('list'); - const [examples, setExamples] = useState({}); - const [testResults, setTestResults] = useState({}); - const [tokenDebugInfo, setTokenDebugInfo] = useState(null); - const [discoveredSites, setDiscoveredSites] = useState([]); - const [sitesDiscovered, setSitesDiscovered] = useState(false); + const [tableRefreshKey, setTableRefreshKey] = useState(0); + const [siteUrl, setSiteUrl] = useState('https://your-tenant.sharepoint.com/sites/your-site'); + const [folderPaths, setFolderPaths] = useState(['/']); - // Form data - const [formData, setFormData] = useState({ - connectionReference: '', - siteUrl: 'https://your-tenant.sharepoint.com/sites/your-site', - folderPaths: ['/'], // Start at root folder - query: 'quarterly report 2024', - searchScope: 'all', - documentList: 'document_list_reference_from_chat', - documentPaths: ['/Shared Documents/file1.docx', '/Documents/file2.pdf'], - fileNames: ['uploaded_file1.docx', 'uploaded_file2.pdf'], - includeMetadata: true, - includeSubfolders: false // Default to false for better navigation UX - }); + const onTestConnection = async (connectionId: string) => { + await handleTestConnection(connectionId); + }; - // Load connections and examples on mount - useEffect(() => { - loadConnections(); - loadExamples(); - }, []); + const onListDocuments = async () => { + console.log('onListDocuments called with:', { siteUrl, folderPaths }); + await handleListDocuments(siteUrl, folderPaths); + // Force table refresh to show new data + const newKey = tableRefreshKey + 1; + console.log('Setting tableRefreshKey to:', newKey); + setTableRefreshKey(newKey); + }; - // Update connection reference when selected connection changes - useEffect(() => { - setFormData(prev => ({ ...prev, connectionReference: selectedConnection })); - }, [selectedConnection]); + const onDiscoverSites = async () => { + await handleDiscoverSites(); + }; - const loadConnections = async () => { - try { - const conns = await getConnections(); - setConnections(conns); - if (conns.length > 0) { - setSelectedConnection(conns[0].id); - } - } catch (error) { - console.error('Failed to load connections:', error); - } - }; + const onSelectSite = (selectedSiteUrl: string) => { + setSiteUrl(selectedSiteUrl); + handleSelectSite(selectedSiteUrl); + }; - const loadExamples = async () => { - try { - const exampleData = await getExamples(); - setExamples(exampleData); - } catch (error) { - console.error('Failed to load examples:', error); - } - }; + const onRowClick = (row: any) => { + console.log('Row clicked:', row); + if (row.type === 'folder') { + const currentPath = folderPaths[0] || '/'; + const newPath = handleFolderNavigation(row, currentPath); + console.log('Navigating from', currentPath, 'to', newPath); + setFolderPaths([newPath]); + // Automatically refresh the document list with the new path + handleListDocuments(siteUrl, [newPath]); + setTableRefreshKey(prev => prev + 1); + } + }; - const handleTestConnection = async (connectionId: string) => { - try { - const result = await testConnection(connectionId); - setTestResults((prev: any) => ({ ...prev, [connectionId]: result })); - } catch (error) { - console.error('Connection test failed:', error); - } - }; - - const handleDebugTokens = async () => { - try { - const result = await debugTokens(); - setTokenDebugInfo(result); - } catch (error) { - console.error('Token debug failed:', error); - } - }; - - const handleDebugTokenDetails = async () => { - try { - const result = await debugTokenDetails(); - setTokenDebugInfo(result); - } catch (error) { - console.error('Token details debug failed:', error); - } - }; - - const handleDiscoverSites = async () => { - try { - const result = await discoverSites(); - if (result.success && result.data.sites) { - setDiscoveredSites(result.data.sites); - setSitesDiscovered(true); - } else { - console.error('Site discovery failed:', result.message); - setDiscoveredSites([]); - setSitesDiscovered(false); - } - } catch (error) { - console.error('Site discovery failed:', error); - setDiscoveredSites([]); - setSitesDiscovered(false); - } - }; - - const handleSelectSite = (siteUrl: string) => { - setFormData(prev => ({ ...prev, siteUrl: siteUrl })); - }; - - const handleFormChange = (field: keyof FormData, value: any) => { - setFormData(prev => ({ ...prev, [field]: value })); - }; - - const handleArrayInputChange = (field: 'folderPaths' | 'documentPaths' | 'fileNames', value: string) => { - const array = value.split('\n').filter(item => item.trim() !== ''); - setFormData(prev => ({ ...prev, [field]: array })); - }; - - const loadExample = (operation: TestOperation) => { - const exampleKey = operation === 'list' ? 'listDocuments' : - operation === 'find' ? 'findDocuments' : - operation === 'read' ? 'readDocuments' : 'uploadDocuments'; - - if (examples[exampleKey]) { - const example = examples[exampleKey]; - - setFormData(prev => ({ - ...prev, - connectionReference: selectedConnection || prev.connectionReference, - siteUrl: example.siteUrl || prev.siteUrl, - folderPaths: example.folderPaths || prev.folderPaths, - query: example.query || prev.query, - searchScope: example.searchScope || prev.searchScope, - documentList: example.documentList || prev.documentList, - documentPaths: example.documentPaths || prev.documentPaths, - fileNames: example.fileNames || prev.fileNames, - includeMetadata: example.includeMetadata !== undefined ? example.includeMetadata : prev.includeMetadata, - includeSubfolders: example.includeSubfolders !== undefined ? example.includeSubfolders : prev.includeSubfolders - })); - } - }; - - const executeTest = async () => { - if (!selectedConnection) { - alert('Please select a connection first'); - return; - } - - // Find the selected connection to build proper reference - const selectedConn = connections.find(conn => conn.id === selectedConnection); - if (!selectedConn) { - alert('Selected connection not found'); - return; - } - - // Build connection reference in expected format: connection:{authority}:{username}:{id} - const connectionReference = `connection:${selectedConn.authority}:${selectedConn.externalUsername}:${selectedConn.id}`; - - try { - let result: SharePointResponse; - - switch (activeTab) { - case 'list': - const listRequest: SharePointListRequest = { - connectionReference: connectionReference, - siteUrl: formData.siteUrl, - folderPaths: formData.folderPaths, - includeSubfolders: formData.includeSubfolders, - expectedDocumentFormats: [{ extension: '.json', mimeType: 'application/json' }] - }; - result = await listDocuments(listRequest); - break; - - case 'find': - const findRequest: SharePointFindRequest = { - connectionReference: connectionReference, - siteUrl: formData.siteUrl, - query: formData.query, - searchScope: formData.searchScope, - expectedDocumentFormats: [{ extension: '.json', mimeType: 'application/json' }] - }; - result = await findDocuments(findRequest); - break; - - case 'read': - const readRequest: SharePointReadRequest = { - documentList: formData.documentList, - connectionReference: connectionReference, - siteUrl: formData.siteUrl, - documentPaths: formData.documentPaths, - includeMetadata: formData.includeMetadata, - expectedDocumentFormats: [{ extension: '.json', mimeType: 'application/json' }] - }; - result = await readDocuments(readRequest); - break; - - case 'upload': - const uploadRequest: SharePointUploadRequest = { - connectionReference: connectionReference, - siteUrl: formData.siteUrl, - documentPaths: formData.folderPaths, - documentList: formData.documentList, - fileNames: formData.fileNames, - expectedDocumentFormats: [{ extension: '.json', mimeType: 'application/json' }] - }; - result = await uploadDocuments(uploadRequest); - break; - - default: - throw new Error('Invalid operation'); - } - - console.log('Test result:', result); - } catch (error) { - console.error('Test execution failed:', error); - } - }; - - const renderConnectionCard = (connection: SharePointConnection) => { - const testResult = testResults[connection.id]; + const renderConnectionCard = (connection: any) => { + const testResult = connectionTestResults[connection.id]; + const isTestingThis = testingConnections.has(connection.id); return (
setSelectedConnection(connection.id)} + className={`${styles.connectionCard} ${selectedConnection?.id === connection.id ? styles.active : ''}`} + onClick={() => handleSelectConnection(connection.id)} >
@@ -283,11 +100,12 @@ function TestSharepoint() { className={styles.button} onClick={(e) => { e.stopPropagation(); - handleTestConnection(connection.id); + onTestConnection(connection.id); }} - disabled={isLoading} + disabled={isTestingThis} + aria-label={t('sharepoint.button.testConnection')} > - Test + {isTestingThis ? '⏳' : t('sharepoint.button.testConnection')} {testResult && ( @@ -299,57 +117,192 @@ function TestSharepoint() { ); }; - const renderTestForm = () => { return ( -
-
Request Configuration
- -
- - +
+
+
+
+

{t('sharepoint.title')}

+
+ + +
+
+
+ + {/* Connections Section */} +
+

+ {t('sharepoint.connections.title')} ({connections.length}) +

+ + {connectionError && ( +
+ {connectionError} +
+ )} + + {connections.length === 0 ? ( +
+ {connectionLoading ? + t('sharepoint.connections.loading') : + t('sharepoint.connections.noConnections') + } +
+ ) : ( +
+ {connections.map(renderConnectionCard)} +
+ )} +
+ + {/* Token Debug Section */} + {tokenDebugInfo && ( + <> +
+
+

🔍 Token Debug Information

+ +
+
+ User ID: {tokenDebugInfo.data?.userId || 'Unknown'} +
+
+ All Tokens Count: {tokenDebugInfo.data?.allTokensCount || 0}
+ + {tokenDebugInfo.data?.allTokens && tokenDebugInfo.data.allTokens.length > 0 && ( +
+ Microsoft Tokens: + {tokenDebugInfo.data.allTokens.map((token: any, index: number) => ( +
+
Token ID: {token.id}
+
Authority: {token.authority}
+
Expires At: {new Date(token.expiresAt * 1000).toLocaleString()}
+
Is Expired: {token.isExpired ? '❌ YES' : '✅ NO'}
+
Has Access Token: {token.hasAccessToken ? '✅ YES' : '❌ NO'}
+
Has Refresh Token: {token.hasRefreshToken ? '✅ YES' : '❌ NO'}
+
+ ))} +
+ )} + + {tokenDebugInfo.data?.sharepointMethodToken && ( +
+ SharePoint Method Token Status: +
+ {tokenDebugInfo.data.sharepointMethodToken.tokenFound ? ( +
+
✅ Token Found
+
Token ID: {tokenDebugInfo.data.sharepointMethodToken.tokenId}
+
Expires At: {new Date(tokenDebugInfo.data.sharepointMethodToken.expiresAt * 1000).toLocaleString()}
+
Is Expired: {tokenDebugInfo.data.sharepointMethodToken.isExpired ? '❌ YES' : '✅ NO'}
+
+ ) : ( +
❌ No Token Found: {tokenDebugInfo.data.sharepointMethodToken.reason || tokenDebugInfo.data.sharepointMethodToken.error}
+ )} +
+
+ )} + + {/* Provide action recommendations */} +
+ 💡 Recommendation: +
+ {tokenDebugInfo.data?.allTokens?.some((token: any) => token.isExpired) ? ( +
+ 🔄 Your tokens are stale! The tokens weren't properly cleared. Use the button below to force cleanup: +
+ +
+
+ After cleanup: Go to Connections page → Reconnect Microsoft account +
+
+ ) : !tokenDebugInfo.data?.allTokens?.some((token: any) => token.hasAccessToken) ? ( +
+ ⚠️ No valid access tokens found. Please reconnect your Microsoft account in the Connections page. +
+ ) : ( +
+ ✅ Tokens look valid. The issue might be with SharePoint permissions or scopes. +
+ )} +
+
+
+
+ + )} + + {/* SharePoint Configuration Section */} + {selectedConnection && ( + <> +
+
+

SharePoint Configuration

- +
handleFormChange('siteUrl', e.target.value)} + value={siteUrl} + onChange={(e) => setSiteUrl(e.target.value)} placeholder="https://your-tenant.sharepoint.com/sites/your-site" style={{ flex: 1 }} />
{sitesDiscovered && discoveredSites.length > 0 && (
- +
{discoveredSites.map((site, index) => (
handleSelectSite(site.url)} + className={`${styles.siteItem} ${siteUrl === site.url ? styles.selectedSite : ''}`} + onClick={() => onSelectSite(site.url)} >
{site.name} @@ -367,296 +320,108 @@ function TestSharepoint() { {sitesDiscovered && discoveredSites.length === 0 && (
- No SharePoint sites found. You may need additional permissions or the sites may not be accessible. + {t('sharepoint.sites.noSites')} + {documentsError && ( +
+
+ Technical details: {documentsError} +
+ {documentsError.includes('401') || documentsError.includes('InvalidAuthenticationToken') ? ( +
+
⚠️ {t('sharepoint.sites.authError')}
+
{t('sharepoint.sites.retryConnection')}
+
+ ) : ( +
+ Please check your Microsoft connection and permissions. +
+ )} +
+ )}
)}
- {activeTab === 'list' && ( - <>
- +