sharepoint integration

This commit is contained in:
Ida Dittrich 2025-08-13 11:48:57 +02:00
parent 10c7073cce
commit c5ecd88e66
13 changed files with 1168 additions and 563 deletions

View file

@ -344,8 +344,6 @@ export function FormGenerator<T extends Record<string, any>>({
return (
<div className={`${styles.formGenerator} ${className}`}>
{title && <h2 className={styles.title}>{title}</h2>}
{(searchable || filterable) && (
<div className={styles.controls}>

View file

@ -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%;
}

View file

@ -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 (
<div className={`${styles.testSharepointTable} ${className}`}>
<div className={styles.errorState}>
<p>{t('sharepoint.error.loading', 'Error loading SharePoint documents:')} {actualError}</p>
<button onClick={() => window.location.reload()} className={styles.retryButton}>
{t('sharepoint.button.retry', 'Retry')}
</button>
</div>
</div>
);
}
return (
<div className={`${styles.testSharepointTable} ${className}`}>
<FormGenerator
data={actualDocuments}
columns={actualColumns}
title={t('sharepoint.table.title', 'SharePoint Documents')}
loading={actualLoading}
searchable={true}
filterable={true}
sortable={true}
resizable={true}
pagination={true}
pageSize={10}
pageSizeOptions={[10, 25, 50, 100]}
showPageSizeSelector={true}
selectable={false}
onRowClick={onRowClick}
actions={actualActions}
className={styles.sharepointFormGenerator}
/>
</div>
);
}
export default TestSharepointTable;

View file

@ -0,0 +1,11 @@
export { TestSharepointTable } from './TestSharepointTable';
export { useTestSharepointLogic } from './testSharepointLogic';
export type {
TestSharepointTableProps,
TableAction,
SharePointHandlers,
SharePointOperationsReturn,
SharePointConnectionsReturn,
SharePointTableConfig,
TestSharepointLogicReturn
} from './testSharepointInterfaces';

View file

@ -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> | void;
icon: React.ReactNode | ((document: SharePointDocument) => React.ReactNode);
}
// SharePoint Operation Handler Types
export interface SharePointHandlers {
handleConnectionTest: (connectionId: string) => Promise<boolean>;
handleListDocuments: (connectionId: string, siteUrl: string, folderPaths: string[]) => Promise<SharePointDocument[]>;
handleFindDocuments: (connectionId: string, siteUrl: string, query: string) => Promise<SharePointDocument[]>;
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<string>;
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<void>;
testConnection: (connectionId: string) => Promise<any>;
}
// 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<string>;
connectionTestResults: Record<string, any>;
// Site discovery
discoveredSites: any[];
sitesDiscovered: boolean;
// Token debug
tokenDebugInfo: any;
// Handlers
handleSelectConnection: (connectionId: string) => void;
handleTestConnection: (connectionId: string) => Promise<void>;
handleListDocuments: (siteUrl?: string, folderPaths?: string[]) => Promise<void>;
handleDiscoverSites: () => Promise<void>;
handleSelectSite: (siteUrl: string) => void;
handleDebugTokens: () => Promise<void>;
handleCleanupTokens: () => Promise<void>;
handleFolderNavigation: (document: SharePointDocument, currentPath: string) => string;
refetchConnections: () => Promise<void>;
}

View file

@ -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<SharePointConnection[]>([]);
const [selectedConnection, setSelectedConnection] = useState<SharePointConnection | null>(null);
const [connectionLoading, setConnectionLoading] = useState(false);
const [connectionError, setConnectionError] = useState<string | null>(null);
const [documents, setDocuments] = useState<SharePointDocument[]>([]);
const [documentsLoading, setDocumentsLoading] = useState(false);
const [documentsError, setDocumentsError] = useState<string | null>(null);
const [testingConnections, setTestingConnections] = useState<Set<string>>(new Set());
const [connectionTestResults, setConnectionTestResults] = useState<Record<string, any>>({});
const [discoveredSites, setDiscoveredSites] = useState<any[]>([]);
const [sitesDiscovered, setSitesDiscovered] = useState(false);
const [tokenDebugInfo, setTokenDebugInfo] = useState<any>(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) => (
<span
style={{
color: 'var(--color-text)',
fontWeight: 500,
display: 'flex',
alignItems: 'center',
gap: '8px',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
cursor: row?.type === 'folder' ? 'pointer' : 'default'
}}
title={value}
>
{row?.type === 'folder' ? '📁' : '📄'} {value}
</span>
)
},
{
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 (
<span style={{ fontWeight: 500, color: 'var(--color-text)' }}>
{`${size.toFixed(1)} ${units[unitIndex]}`}
</span>
);
}
},
{
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: <IoIosLink />,
onClick: (document: SharePointDocument) => {
handleViewDocument(document);
}
},
{
label: t('sharepoint.action.download', 'Download'),
icon: <IoIosCloudDownload />,
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
};
}

View file

@ -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<any> => {
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<any> => {
try {
@ -267,6 +285,7 @@ export function useSharePointTest() {
getExamples,
debugTokens,
debugTokenDetails,
cleanupTokens,
discoverSites,
// State

View file

@ -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',
};

View file

@ -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',
};

View file

@ -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',
};

View file

@ -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;
}

View file

@ -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 */

View file

@ -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<SharePointConnection[]>([]);
const [selectedConnection, setSelectedConnection] = useState<string>('');
const [activeTab, setActiveTab] = useState<TestOperation>('list');
const [examples, setExamples] = useState<any>({});
const [testResults, setTestResults] = useState<any>({});
const [tokenDebugInfo, setTokenDebugInfo] = useState<any>(null);
const [discoveredSites, setDiscoveredSites] = useState<any[]>([]);
const [sitesDiscovered, setSitesDiscovered] = useState<boolean>(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<FormData>({
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 (
<div
key={connection.id}
className={`${styles.connectionCard} ${selectedConnection === connection.id ? styles.active : ''}`}
onClick={() => setSelectedConnection(connection.id)}
className={`${styles.connectionCard} ${selectedConnection?.id === connection.id ? styles.active : ''}`}
onClick={() => handleSelectConnection(connection.id)}
>
<div className={styles.connectionInfo}>
<div className={styles.connectionName}>
@ -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')}
</button>
{testResult && (
<span className={testResult.success ? styles.successMessage : styles.errorMessage}>
@ -299,57 +117,192 @@ function TestSharepoint() {
);
};
const renderTestForm = () => {
return (
<div className={styles.requestPanel}>
<div className={styles.panelTitle}>Request Configuration</div>
<div className={styles.formGroup}>
<label className={styles.label}>Connection</label>
<select
className={styles.select}
value={selectedConnection}
onChange={(e) => setSelectedConnection(e.target.value)}
>
<option value="">Select a connection</option>
{connections.map(conn => (
<option key={conn.id} value={conn.id}>
{conn.externalUsername || conn.id} ({conn.status})
</option>
))}
</select>
<div className={sharedStyles.pageContainer}>
<div className={sharedStyles.contentWrapper}>
<div className={sharedStyles.pageCard}>
<div className={sharedStyles.pageHeader}>
<h1 className={sharedStyles.pageTitle}>{t('sharepoint.title')}</h1>
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
<button
className={sharedStyles.primaryButton}
onClick={refetchConnections}
disabled={connectionLoading}
aria-label="Refresh connections"
>
<span className={sharedStyles.buttonIcon}><IoIosRefresh /></span>
{connectionLoading ? 'Loading...' : 'Refresh Connections'}
</button>
<button
className={sharedStyles.secondaryButton}
onClick={handleDebugTokens}
disabled={connectionLoading}
aria-label="Debug Token Info"
>
🔍 Debug Token
</button>
</div>
</div>
<div className={sharedStyles.horizontalDivider}></div>
{/* Connections Section */}
<div className={styles.section}>
<h2 className={styles.sectionTitle}>
{t('sharepoint.connections.title')} ({connections.length})
</h2>
{connectionError && (
<div className={styles.errorMessage}>
{connectionError}
</div>
)}
{connections.length === 0 ? (
<div className={styles.loading}>
{connectionLoading ?
t('sharepoint.connections.loading') :
t('sharepoint.connections.noConnections')
}
</div>
) : (
<div className={styles.connectionsGrid}>
{connections.map(renderConnectionCard)}
</div>
)}
</div>
{/* Token Debug Section */}
{tokenDebugInfo && (
<>
<div className={sharedStyles.horizontalDivider}></div>
<div className={styles.section}>
<h2 className={styles.sectionTitle}>🔍 Token Debug Information</h2>
<div style={{ background: 'var(--color-bg-secondary, #f8f9fa)', padding: '15px', borderRadius: '8px', border: '1px solid var(--color-border, #dee2e6)' }}>
<div style={{ marginBottom: '10px' }}>
<strong>User ID:</strong> {tokenDebugInfo.data?.userId || 'Unknown'}
</div>
<div style={{ marginBottom: '10px' }}>
<strong>All Tokens Count:</strong> {tokenDebugInfo.data?.allTokensCount || 0}
</div>
{tokenDebugInfo.data?.allTokens && tokenDebugInfo.data.allTokens.length > 0 && (
<div style={{ marginBottom: '15px' }}>
<strong>Microsoft Tokens:</strong>
{tokenDebugInfo.data.allTokens.map((token: any, index: number) => (
<div key={index} style={{ marginLeft: '15px', marginTop: '8px', padding: '8px', background: token.isExpired ? 'var(--color-error-bg, #ffe6e6)' : 'var(--color-success-bg, #e6ffe6)', borderRadius: '4px' }}>
<div><strong>Token ID:</strong> {token.id}</div>
<div><strong>Authority:</strong> {token.authority}</div>
<div><strong>Expires At:</strong> {new Date(token.expiresAt * 1000).toLocaleString()}</div>
<div><strong>Is Expired:</strong> {token.isExpired ? '❌ YES' : '✅ NO'}</div>
<div><strong>Has Access Token:</strong> {token.hasAccessToken ? '✅ YES' : '❌ NO'}</div>
<div><strong>Has Refresh Token:</strong> {token.hasRefreshToken ? '✅ YES' : '❌ NO'}</div>
</div>
))}
</div>
)}
{tokenDebugInfo.data?.sharepointMethodToken && (
<div style={{ marginTop: '15px', padding: '10px', background: 'var(--color-warning-bg, #fff3cd)', borderRadius: '4px' }}>
<strong>SharePoint Method Token Status:</strong>
<div style={{ marginLeft: '15px', marginTop: '5px' }}>
{tokenDebugInfo.data.sharepointMethodToken.tokenFound ? (
<div>
<div> Token Found</div>
<div><strong>Token ID:</strong> {tokenDebugInfo.data.sharepointMethodToken.tokenId}</div>
<div><strong>Expires At:</strong> {new Date(tokenDebugInfo.data.sharepointMethodToken.expiresAt * 1000).toLocaleString()}</div>
<div><strong>Is Expired:</strong> {tokenDebugInfo.data.sharepointMethodToken.isExpired ? '❌ YES' : '✅ NO'}</div>
</div>
) : (
<div> No Token Found: {tokenDebugInfo.data.sharepointMethodToken.reason || tokenDebugInfo.data.sharepointMethodToken.error}</div>
)}
</div>
</div>
)}
{/* Provide action recommendations */}
<div style={{ marginTop: '15px', padding: '12px', background: 'var(--color-info-bg, #e3f2fd)', borderRadius: '4px', border: '1px solid var(--color-info, #2196f3)' }}>
<strong>💡 Recommendation:</strong>
<div style={{ marginTop: '8px' }}>
{tokenDebugInfo.data?.allTokens?.some((token: any) => token.isExpired) ? (
<div>
🔄 <strong>Your tokens are stale!</strong> The tokens weren't properly cleared. Use the button below to force cleanup:
<div style={{ marginTop: '12px' }}>
<button
onClick={handleCleanupTokens}
style={{
padding: '8px 16px',
background: 'var(--color-error, #d32f2f)',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
🗑 Force Delete All Tokens
</button>
</div>
<div style={{ marginTop: '8px', fontSize: '12px' }}>
After cleanup: Go to Connections page Reconnect Microsoft account
</div>
</div>
) : !tokenDebugInfo.data?.allTokens?.some((token: any) => token.hasAccessToken) ? (
<div>
<strong>No valid access tokens found.</strong> Please reconnect your Microsoft account in the Connections page.
</div>
) : (
<div>
<strong>Tokens look valid.</strong> The issue might be with SharePoint permissions or scopes.
</div>
)}
</div>
</div>
</div>
</div>
</>
)}
{/* SharePoint Configuration Section */}
{selectedConnection && (
<>
<div className={sharedStyles.horizontalDivider}></div>
<div className={styles.section}>
<h2 className={styles.sectionTitle}>SharePoint Configuration</h2>
<div className={styles.formGroup}>
<label className={styles.label}>SharePoint Site URL</label>
<label className={styles.label}>{t('sharepoint.form.siteUrl')}</label>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center', marginBottom: '10px' }}>
<input
type="text"
className={styles.input}
value={formData.siteUrl}
onChange={(e) => 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 }}
/>
<button
className={`${styles.button} ${styles.exampleButton}`}
onClick={handleDiscoverSites}
disabled={isLoading}
className={sharedStyles.secondaryButton}
onClick={onDiscoverSites}
disabled={connectionLoading}
type="button"
>
Discover Sites
<span className={sharedStyles.buttonIcon}><IoIosLink /></span>
{t('sharepoint.button.discoverSites')}
</button>
</div>
{sitesDiscovered && discoveredSites.length > 0 && (
<div className={styles.sitesDiscovery}>
<label className={styles.label}>Discovered Sites ({discoveredSites.length}):</label>
<label className={styles.label}>
{t('sharepoint.sites.discovered')} ({discoveredSites.length}):
</label>
<div className={styles.sitesList}>
{discoveredSites.map((site, index) => (
<div
key={index}
className={`${styles.siteItem} ${formData.siteUrl === site.url ? styles.selectedSite : ''}`}
onClick={() => handleSelectSite(site.url)}
className={`${styles.siteItem} ${siteUrl === site.url ? styles.selectedSite : ''}`}
onClick={() => onSelectSite(site.url)}
>
<div className={styles.siteName}>
<strong>{site.name}</strong>
@ -367,296 +320,108 @@ function TestSharepoint() {
{sitesDiscovered && discoveredSites.length === 0 && (
<div className={styles.noSitesFound}>
No SharePoint sites found. You may need additional permissions or the sites may not be accessible.
<strong>{t('sharepoint.sites.noSites')}</strong>
{documentsError && (
<div style={{ marginTop: '10px' }}>
<div style={{ fontSize: '12px', color: 'var(--color-error, #d32f2f)', marginBottom: '8px' }}>
<strong>Technical details:</strong> {documentsError}
</div>
{documentsError.includes('401') || documentsError.includes('InvalidAuthenticationToken') ? (
<div style={{ fontSize: '13px', color: 'var(--color-text)', background: 'var(--color-warning-bg, #fff3cd)', padding: '8px', borderRadius: '4px', border: '1px solid var(--color-warning, #ffc107)' }}>
<div> {t('sharepoint.sites.authError')}</div>
<div style={{ marginTop: '4px' }}>{t('sharepoint.sites.retryConnection')}</div>
</div>
) : (
<div style={{ fontSize: '12px', color: 'var(--color-text-secondary)' }}>
Please check your Microsoft connection and permissions.
</div>
)}
</div>
)}
</div>
)}
</div>
{activeTab === 'list' && (
<>
<div className={styles.formGroup}>
<label className={styles.label}>Folder Paths (one per line)</label>
<label className={styles.label}>{t('sharepoint.form.folderPaths')}</label>
<textarea
className={styles.textarea}
value={formData.folderPaths.join('\n')}
onChange={(e) => handleArrayInputChange('folderPaths', e.target.value)}
placeholder="/Shared Documents&#10;/Documents"
value={folderPaths.join('\n')}
onChange={(e) => setFolderPaths(e.target.value.split('\n').filter(path => path.trim()))}
placeholder="/&#10;/Documents&#10;/Sites/YourSite/Shared Documents"
rows={3}
/>
</div>
<div className={styles.formGroup}>
<label>
<input
type="checkbox"
checked={formData.includeSubfolders}
onChange={(e) => handleFormChange('includeSubfolders', e.target.checked)}
/>
Include Subfolders
</label>
<div className={styles.buttonGroup}>
<button
className={sharedStyles.primaryButton}
onClick={onListDocuments}
disabled={connectionLoading || !selectedConnection}
>
{t('sharepoint.button.listDocuments')}
</button>
</div>
</div>
</>
)}
{activeTab === 'find' && (
<>
<div className={styles.formGroup}>
<label className={styles.label}>Search Query</label>
<input
type="text"
className={styles.input}
value={formData.query}
onChange={(e) => handleFormChange('query', e.target.value)}
placeholder="quarterly report 2024"
/>
</div>
<div className={styles.formGroup}>
<label className={styles.label}>Search Scope</label>
<select
className={styles.select}
value={formData.searchScope}
onChange={(e) => handleFormChange('searchScope', e.target.value)}
>
<option value="all">All</option>
<option value="documents">Documents Only</option>
<option value="pages">Pages Only</option>
</select>
</div>
</>
)}
{activeTab === 'read' && (
<>
<div className={styles.formGroup}>
<label className={styles.label}>Document List Reference</label>
<input
type="text"
className={styles.input}
value={formData.documentList}
onChange={(e) => handleFormChange('documentList', e.target.value)}
placeholder="document_list_reference_from_chat"
/>
<div className={styles.helpText}>
This should be a reference from a chat session or document management system
</div>
</div>
<div className={styles.formGroup}>
<label className={styles.label}>Document Paths (one per line)</label>
<textarea
className={styles.textarea}
value={formData.documentPaths.join('\n')}
onChange={(e) => handleArrayInputChange('documentPaths', e.target.value)}
placeholder="/Shared Documents/file1.docx&#10;/Documents/file2.pdf"
/>
</div>
<div className={styles.formGroup}>
<label>
<input
type="checkbox"
checked={formData.includeMetadata}
onChange={(e) => handleFormChange('includeMetadata', e.target.checked)}
/>
Include Metadata
</label>
</div>
</>
)}
{activeTab === 'upload' && (
<>
<div className={styles.formGroup}>
<label className={styles.label}>Document List Reference</label>
<input
type="text"
className={styles.input}
value={formData.documentList}
onChange={(e) => handleFormChange('documentList', e.target.value)}
placeholder="document_list_reference_from_chat"
/>
</div>
<div className={styles.formGroup}>
<label className={styles.label}>Upload Destination Paths (one per line)</label>
<textarea
className={styles.textarea}
value={formData.folderPaths.join('\n')}
onChange={(e) => handleArrayInputChange('folderPaths', e.target.value)}
placeholder="/Shared Documents/&#10;/Documents/"
/>
</div>
<div className={styles.formGroup}>
<label className={styles.label}>File Names (one per line)</label>
<textarea
className={styles.textarea}
value={formData.fileNames.join('\n')}
onChange={(e) => handleArrayInputChange('fileNames', e.target.value)}
placeholder="uploaded_file1.docx&#10;uploaded_file2.pdf"
/>
</div>
</>
)}
<div className={styles.buttonGroup}>
<button
className={`${styles.button} ${styles.primary}`}
onClick={executeTest}
disabled={isLoading || !selectedConnection}
>
{isLoading ? 'Testing...' : `Test ${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)}`}
</button>
<button
className={`${styles.button} ${styles.exampleButton}`}
onClick={() => loadExample(activeTab)}
>
Load Example
</button>
</div>
<div className={styles.requestPreview}>
<strong>Request Preview:</strong>
{JSON.stringify({
endpoint: `/api/test-sharepoint/${activeTab === 'list' ? 'list-documents' :
activeTab === 'find' ? 'find-documents' :
activeTab === 'read' ? 'read-documents' : 'upload-documents'}`,
method: 'POST',
body: activeTab === 'list' ? {
connectionReference: selectedConnection ?
`connection:${connections.find(c => c.id === selectedConnection)?.authority}:${connections.find(c => c.id === selectedConnection)?.externalUsername}:${selectedConnection}` :
'No connection selected',
siteUrl: formData.siteUrl,
folderPaths: formData.folderPaths,
includeSubfolders: formData.includeSubfolders
} : activeTab === 'find' ? {
connectionReference: selectedConnection ?
`connection:${connections.find(c => c.id === selectedConnection)?.authority}:${connections.find(c => c.id === selectedConnection)?.externalUsername}:${selectedConnection}` :
'No connection selected',
siteUrl: formData.siteUrl,
query: formData.query,
searchScope: formData.searchScope
} : activeTab === 'read' ? {
documentList: formData.documentList,
connectionReference: selectedConnection ?
`connection:${connections.find(c => c.id === selectedConnection)?.authority}:${connections.find(c => c.id === selectedConnection)?.externalUsername}:${selectedConnection}` :
'No connection selected',
siteUrl: formData.siteUrl,
documentPaths: formData.documentPaths,
includeMetadata: formData.includeMetadata
} : {
connectionReference: selectedConnection ?
`connection:${connections.find(c => c.id === selectedConnection)?.authority}:${connections.find(c => c.id === selectedConnection)?.externalUsername}:${selectedConnection}` :
'No connection selected',
siteUrl: formData.siteUrl,
documentPaths: formData.folderPaths,
documentList: formData.documentList,
fileNames: formData.fileNames
}
}, null, 2)}
</div>
</div>
);
};
const renderResponse = () => {
return (
<div className={styles.responsePanel}>
<div className={styles.panelTitle}>Response</div>
{error && (
<div className={styles.errorMessage}>
Error: {error}
</div>
)}
{lastResponse && (
<div className={`${styles.responseArea} ${lastResponse.success ? styles.success : styles.error}`}>
{JSON.stringify(lastResponse, null, 2)}
</div>
)}
{!lastResponse && !error && (
<div className={styles.responseArea}>
No response yet. Execute a test to see results here.
</div>
)}
</div>
);
};
return (
<div className={styles.container}>
<h1 className={styles.title}>SharePoint Method Testing</h1>
<div className={styles.section}>
<h2 className={styles.sectionTitle}>
Microsoft Connections ({connections.length})
</h2>
<div className={styles.buttonGroup} style={{ marginBottom: '15px' }}>
<button
className={`${styles.button} ${styles.exampleButton}`}
onClick={handleDebugTokens}
disabled={isLoading}
>
Debug Authentication Tokens
</button>
<button
className={`${styles.button} ${styles.exampleButton}`}
onClick={handleDebugTokenDetails}
disabled={isLoading}
>
Debug Token Details
</button>
</div>
{tokenDebugInfo && (
<div className={styles.responseArea} style={{ marginBottom: '15px' }}>
<strong>Token Debug Info:</strong>
<pre>{JSON.stringify(tokenDebugInfo, null, 2)}</pre>
</div>
)}
{connections.length === 0 ? (
<div className={styles.loading}>
{isLoading ? 'Loading connections...' : 'No Microsoft connections found. Please create a connection first.'}
</div>
) : (
<div className={styles.connectionsGrid}>
{connections.map(renderConnectionCard)}
</div>
)}
</div>
<div className={styles.section}>
<h2 className={styles.sectionTitle}>SharePoint Operations Testing</h2>
<div className={styles.tabs}>
<button
className={`${styles.tab} ${activeTab === 'list' ? styles.active : ''}`}
onClick={() => setActiveTab('list')}
>
List Documents
</button>
<button
className={`${styles.tab} ${activeTab === 'find' ? styles.active : ''}`}
onClick={() => setActiveTab('find')}
>
Find Documents
</button>
<button
className={`${styles.tab} ${activeTab === 'read' ? styles.active : ''}`}
onClick={() => setActiveTab('read')}
>
Read Documents
</button>
<button
className={`${styles.tab} ${activeTab === 'upload' ? styles.active : ''}`}
onClick={() => setActiveTab('upload')}
>
Upload Documents
</button>
</div>
<div className={styles.tabContent}>
<div className={styles.testArea}>
{renderTestForm()}
{renderResponse()}
{/* Documents Table Section */}
<div className={sharedStyles.horizontalDivider}></div>
{/* Breadcrumb Navigation */}
<div className={styles.section}>
<div className={styles.breadcrumb}>
<span className={styles.breadcrumbLabel}>Current Path:</span>
{folderPaths[0] === '/' ? (
<span className={styles.breadcrumbItem}>📁 Root</span>
) : (
<>
<span
className={styles.breadcrumbItem + ' ' + styles.breadcrumbClickable}
onClick={() => {
setFolderPaths(['/']);
handleListDocuments(siteUrl, ['/']);
setTableRefreshKey(prev => prev + 1);
}}
>
📁 Root
</span>
{folderPaths[0].split('/').filter(Boolean).map((part, index, array) => {
const pathToHere = '/' + array.slice(0, index + 1).join('/');
const isLast = index === array.length - 1;
return (
<span key={index}>
<span className={styles.breadcrumbSeparator}>/</span>
<span
className={styles.breadcrumbItem + (isLast ? '' : ' ' + styles.breadcrumbClickable)}
onClick={!isLast ? () => {
setFolderPaths([pathToHere]);
handleListDocuments(siteUrl, [pathToHere]);
setTableRefreshKey(prev => prev + 1);
} : undefined}
>
📁 {part}
</span>
</span>
);
})}
</>
)}
</div>
</div>
<div className={sharedStyles.contentArea}>
<TestSharepointTable
key={tableRefreshKey}
className={styles.sharepointTableContainer}
documents={documents}
documentsLoading={documentsLoading}
documentsError={documentsError}
columns={columns}
actions={actions}
onRowClick={onRowClick}
/>
</div>
</div>
</div>