sharepoint integration
This commit is contained in:
parent
10c7073cce
commit
c5ecd88e66
13 changed files with 1168 additions and 563 deletions
|
|
@ -344,8 +344,6 @@ export function FormGenerator<T extends Record<string, any>>({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.formGenerator} ${className}`}>
|
<div className={`${styles.formGenerator} ${className}`}>
|
||||||
{title && <h2 className={styles.title}>{title}</h2>}
|
|
||||||
|
|
||||||
|
|
||||||
{(searchable || filterable) && (
|
{(searchable || filterable) && (
|
||||||
<div className={styles.controls}>
|
<div className={styles.controls}>
|
||||||
|
|
|
||||||
40
src/components/TestSharepoint/TestSharepointTable.module.css
Normal file
40
src/components/TestSharepoint/TestSharepointTable.module.css
Normal 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%;
|
||||||
|
}
|
||||||
72
src/components/TestSharepoint/TestSharepointTable.tsx
Normal file
72
src/components/TestSharepoint/TestSharepointTable.tsx
Normal 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;
|
||||||
11
src/components/TestSharepoint/index.ts
Normal file
11
src/components/TestSharepoint/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export { TestSharepointTable } from './TestSharepointTable';
|
||||||
|
export { useTestSharepointLogic } from './testSharepointLogic';
|
||||||
|
export type {
|
||||||
|
TestSharepointTableProps,
|
||||||
|
TableAction,
|
||||||
|
SharePointHandlers,
|
||||||
|
SharePointOperationsReturn,
|
||||||
|
SharePointConnectionsReturn,
|
||||||
|
SharePointTableConfig,
|
||||||
|
TestSharepointLogicReturn
|
||||||
|
} from './testSharepointInterfaces';
|
||||||
112
src/components/TestSharepoint/testSharepointInterfaces.ts
Normal file
112
src/components/TestSharepoint/testSharepointInterfaces.ts
Normal 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>;
|
||||||
|
}
|
||||||
395
src/components/TestSharepoint/testSharepointLogic.tsx
Normal file
395
src/components/TestSharepoint/testSharepointLogic.tsx
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,10 @@ export interface SharePointDocument {
|
||||||
documentName: string;
|
documentName: string;
|
||||||
documentData: any;
|
documentData: any;
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
|
size?: number;
|
||||||
|
path?: string;
|
||||||
|
type?: 'file' | 'folder';
|
||||||
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharePointResponse {
|
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
|
// Discover SharePoint sites
|
||||||
const discoverSites = async (): Promise<any> => {
|
const discoverSites = async (): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -267,6 +285,7 @@ export function useSharePointTest() {
|
||||||
getExamples,
|
getExamples,
|
||||||
debugTokens,
|
debugTokens,
|
||||||
debugTokenDetails,
|
debugTokenDetails,
|
||||||
|
cleanupTokens,
|
||||||
discoverSites,
|
discoverSites,
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
|
||||||
|
|
@ -343,4 +343,28 @@ export default {
|
||||||
'formgen.pagination.prev': 'Vorherige Seite',
|
'formgen.pagination.prev': 'Vorherige Seite',
|
||||||
'formgen.pagination.next': 'Nächste Seite',
|
'formgen.pagination.next': 'Nächste Seite',
|
||||||
'formgen.pagination.last': 'Letzte 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',
|
||||||
};
|
};
|
||||||
|
|
@ -344,4 +344,28 @@ export default {
|
||||||
'formgen.pagination.prev': 'Previous page',
|
'formgen.pagination.prev': 'Previous page',
|
||||||
'formgen.pagination.next': 'Next page',
|
'formgen.pagination.next': 'Next page',
|
||||||
'formgen.pagination.last': 'Last 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',
|
||||||
};
|
};
|
||||||
|
|
@ -343,4 +343,28 @@ export default {
|
||||||
'formgen.pagination.prev': 'Page précédente',
|
'formgen.pagination.prev': 'Page précédente',
|
||||||
'formgen.pagination.next': 'Page suivante',
|
'formgen.pagination.next': 'Page suivante',
|
||||||
'formgen.pagination.last': 'Dernière page',
|
'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',
|
||||||
};
|
};
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
border: 1px solid var(--border-primary);
|
border: 1px solid var(--border-primary);
|
||||||
|
flex-shrink: 0; /* Prevent sections from shrinking */
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionTitle {
|
.sectionTitle {
|
||||||
|
|
@ -302,6 +303,81 @@
|
||||||
line-height: 1.4;
|
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 {
|
.requestPreview {
|
||||||
background: var(--bg-code);
|
background: var(--bg-code);
|
||||||
border: 1px solid var(--border-secondary);
|
border: 1px solid var(--border-secondary);
|
||||||
|
|
@ -394,4 +470,49 @@
|
||||||
color: #856404;
|
color: #856404;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 10px;
|
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;
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.30);
|
box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.30);
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
height: 100%;
|
min-height: calc(100vh - 50px); /* Ensure minimum height but allow expansion */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Page headers with consistent spacing */
|
/* Page headers with consistent spacing */
|
||||||
|
|
|
||||||
|
|
@ -1,269 +1,86 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import { IoIosRefresh, IoIosLink } from 'react-icons/io';
|
||||||
useSharePointTest,
|
import { useLanguage } from '../../contexts/LanguageContext';
|
||||||
SharePointConnection,
|
import sharedStyles from './HomeStyles/pages.module.css'
|
||||||
SharePointListRequest,
|
import styles from './HomeStyles/TestSharepoint.module.css'
|
||||||
SharePointFindRequest,
|
import { TestSharepointTable, useTestSharepointLogic } from '../../components/TestSharepoint'
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TestSharepoint() {
|
function TestSharepoint() {
|
||||||
|
const { t } = useLanguage();
|
||||||
const {
|
const {
|
||||||
getConnections,
|
connections,
|
||||||
testConnection,
|
selectedConnection,
|
||||||
listDocuments,
|
connectionLoading,
|
||||||
findDocuments,
|
connectionError,
|
||||||
readDocuments,
|
documents,
|
||||||
uploadDocuments,
|
documentsLoading,
|
||||||
getExamples,
|
documentsError,
|
||||||
debugTokens,
|
columns,
|
||||||
debugTokenDetails,
|
actions,
|
||||||
discoverSites,
|
testingConnections,
|
||||||
isLoading,
|
connectionTestResults,
|
||||||
error,
|
discoveredSites,
|
||||||
lastResponse
|
sitesDiscovered,
|
||||||
} = useSharePointTest();
|
tokenDebugInfo,
|
||||||
|
handleSelectConnection,
|
||||||
|
handleTestConnection,
|
||||||
|
handleListDocuments,
|
||||||
|
handleDiscoverSites,
|
||||||
|
handleSelectSite,
|
||||||
|
handleDebugTokens,
|
||||||
|
handleCleanupTokens,
|
||||||
|
handleFolderNavigation,
|
||||||
|
refetchConnections
|
||||||
|
} = useTestSharepointLogic();
|
||||||
|
|
||||||
// State
|
const [tableRefreshKey, setTableRefreshKey] = useState(0);
|
||||||
const [connections, setConnections] = useState<SharePointConnection[]>([]);
|
const [siteUrl, setSiteUrl] = useState('https://your-tenant.sharepoint.com/sites/your-site');
|
||||||
const [selectedConnection, setSelectedConnection] = useState<string>('');
|
const [folderPaths, setFolderPaths] = useState(['/']);
|
||||||
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);
|
|
||||||
|
|
||||||
// Form data
|
const onTestConnection = async (connectionId: string) => {
|
||||||
const [formData, setFormData] = useState<FormData>({
|
await handleTestConnection(connectionId);
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load connections and examples on mount
|
const onListDocuments = async () => {
|
||||||
useEffect(() => {
|
console.log('onListDocuments called with:', { siteUrl, folderPaths });
|
||||||
loadConnections();
|
await handleListDocuments(siteUrl, folderPaths);
|
||||||
loadExamples();
|
// 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
|
const onDiscoverSites = async () => {
|
||||||
useEffect(() => {
|
await handleDiscoverSites();
|
||||||
setFormData(prev => ({ ...prev, connectionReference: selectedConnection }));
|
};
|
||||||
}, [selectedConnection]);
|
|
||||||
|
|
||||||
const loadConnections = async () => {
|
const onSelectSite = (selectedSiteUrl: string) => {
|
||||||
try {
|
setSiteUrl(selectedSiteUrl);
|
||||||
const conns = await getConnections();
|
handleSelectSite(selectedSiteUrl);
|
||||||
setConnections(conns);
|
};
|
||||||
if (conns.length > 0) {
|
|
||||||
setSelectedConnection(conns[0].id);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load connections:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadExamples = async () => {
|
const onRowClick = (row: any) => {
|
||||||
try {
|
console.log('Row clicked:', row);
|
||||||
const exampleData = await getExamples();
|
if (row.type === 'folder') {
|
||||||
setExamples(exampleData);
|
const currentPath = folderPaths[0] || '/';
|
||||||
} catch (error) {
|
const newPath = handleFolderNavigation(row, currentPath);
|
||||||
console.error('Failed to load examples:', error);
|
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) => {
|
const renderConnectionCard = (connection: any) => {
|
||||||
try {
|
const testResult = connectionTestResults[connection.id];
|
||||||
const result = await testConnection(connectionId);
|
const isTestingThis = testingConnections.has(connection.id);
|
||||||
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];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={connection.id}
|
key={connection.id}
|
||||||
className={`${styles.connectionCard} ${selectedConnection === connection.id ? styles.active : ''}`}
|
className={`${styles.connectionCard} ${selectedConnection?.id === connection.id ? styles.active : ''}`}
|
||||||
onClick={() => setSelectedConnection(connection.id)}
|
onClick={() => handleSelectConnection(connection.id)}
|
||||||
>
|
>
|
||||||
<div className={styles.connectionInfo}>
|
<div className={styles.connectionInfo}>
|
||||||
<div className={styles.connectionName}>
|
<div className={styles.connectionName}>
|
||||||
|
|
@ -283,11 +100,12 @@ function TestSharepoint() {
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
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>
|
</button>
|
||||||
{testResult && (
|
{testResult && (
|
||||||
<span className={testResult.success ? styles.successMessage : styles.errorMessage}>
|
<span className={testResult.success ? styles.successMessage : styles.errorMessage}>
|
||||||
|
|
@ -299,57 +117,192 @@ function TestSharepoint() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTestForm = () => {
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.requestPanel}>
|
<div className={sharedStyles.pageContainer}>
|
||||||
<div className={styles.panelTitle}>Request Configuration</div>
|
<div className={sharedStyles.contentWrapper}>
|
||||||
|
<div className={sharedStyles.pageCard}>
|
||||||
<div className={styles.formGroup}>
|
<div className={sharedStyles.pageHeader}>
|
||||||
<label className={styles.label}>Connection</label>
|
<h1 className={sharedStyles.pageTitle}>{t('sharepoint.title')}</h1>
|
||||||
<select
|
<div style={{ display: 'flex', gap: '15px', alignItems: 'center' }}>
|
||||||
className={styles.select}
|
<button
|
||||||
value={selectedConnection}
|
className={sharedStyles.primaryButton}
|
||||||
onChange={(e) => setSelectedConnection(e.target.value)}
|
onClick={refetchConnections}
|
||||||
>
|
disabled={connectionLoading}
|
||||||
<option value="">Select a connection</option>
|
aria-label="Refresh connections"
|
||||||
{connections.map(conn => (
|
>
|
||||||
<option key={conn.id} value={conn.id}>
|
<span className={sharedStyles.buttonIcon}><IoIosRefresh /></span>
|
||||||
{conn.externalUsername || conn.id} ({conn.status})
|
{connectionLoading ? 'Loading...' : 'Refresh Connections'}
|
||||||
</option>
|
</button>
|
||||||
))}
|
<button
|
||||||
</select>
|
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>
|
</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}>
|
<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' }}>
|
<div style={{ display: 'flex', gap: '10px', alignItems: 'center', marginBottom: '10px' }}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
value={formData.siteUrl}
|
value={siteUrl}
|
||||||
onChange={(e) => handleFormChange('siteUrl', e.target.value)}
|
onChange={(e) => setSiteUrl(e.target.value)}
|
||||||
placeholder="https://your-tenant.sharepoint.com/sites/your-site"
|
placeholder="https://your-tenant.sharepoint.com/sites/your-site"
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className={`${styles.button} ${styles.exampleButton}`}
|
className={sharedStyles.secondaryButton}
|
||||||
onClick={handleDiscoverSites}
|
onClick={onDiscoverSites}
|
||||||
disabled={isLoading}
|
disabled={connectionLoading}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Discover Sites
|
<span className={sharedStyles.buttonIcon}><IoIosLink /></span>
|
||||||
|
{t('sharepoint.button.discoverSites')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sitesDiscovered && discoveredSites.length > 0 && (
|
{sitesDiscovered && discoveredSites.length > 0 && (
|
||||||
<div className={styles.sitesDiscovery}>
|
<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}>
|
<div className={styles.sitesList}>
|
||||||
{discoveredSites.map((site, index) => (
|
{discoveredSites.map((site, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`${styles.siteItem} ${formData.siteUrl === site.url ? styles.selectedSite : ''}`}
|
className={`${styles.siteItem} ${siteUrl === site.url ? styles.selectedSite : ''}`}
|
||||||
onClick={() => handleSelectSite(site.url)}
|
onClick={() => onSelectSite(site.url)}
|
||||||
>
|
>
|
||||||
<div className={styles.siteName}>
|
<div className={styles.siteName}>
|
||||||
<strong>{site.name}</strong>
|
<strong>{site.name}</strong>
|
||||||
|
|
@ -367,296 +320,108 @@ function TestSharepoint() {
|
||||||
|
|
||||||
{sitesDiscovered && discoveredSites.length === 0 && (
|
{sitesDiscovered && discoveredSites.length === 0 && (
|
||||||
<div className={styles.noSitesFound}>
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeTab === 'list' && (
|
|
||||||
<>
|
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<label className={styles.label}>Folder Paths (one per line)</label>
|
<label className={styles.label}>{t('sharepoint.form.folderPaths')}</label>
|
||||||
<textarea
|
<textarea
|
||||||
className={styles.textarea}
|
className={styles.textarea}
|
||||||
value={formData.folderPaths.join('\n')}
|
value={folderPaths.join('\n')}
|
||||||
onChange={(e) => handleArrayInputChange('folderPaths', e.target.value)}
|
onChange={(e) => setFolderPaths(e.target.value.split('\n').filter(path => path.trim()))}
|
||||||
placeholder="/Shared Documents /Documents"
|
placeholder="/ /Documents /Sites/YourSite/Shared Documents"
|
||||||
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.formGroup}>
|
|
||||||
<label>
|
<div className={styles.buttonGroup}>
|
||||||
<input
|
<button
|
||||||
type="checkbox"
|
className={sharedStyles.primaryButton}
|
||||||
checked={formData.includeSubfolders}
|
onClick={onListDocuments}
|
||||||
onChange={(e) => handleFormChange('includeSubfolders', e.target.checked)}
|
disabled={connectionLoading || !selectedConnection}
|
||||||
/>
|
>
|
||||||
Include Subfolders
|
{t('sharepoint.button.listDocuments')}
|
||||||
</label>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'find' && (
|
{/* Documents Table Section */}
|
||||||
<>
|
<div className={sharedStyles.horizontalDivider}></div>
|
||||||
<div className={styles.formGroup}>
|
|
||||||
<label className={styles.label}>Search Query</label>
|
{/* Breadcrumb Navigation */}
|
||||||
<input
|
<div className={styles.section}>
|
||||||
type="text"
|
<div className={styles.breadcrumb}>
|
||||||
className={styles.input}
|
<span className={styles.breadcrumbLabel}>Current Path:</span>
|
||||||
value={formData.query}
|
{folderPaths[0] === '/' ? (
|
||||||
onChange={(e) => handleFormChange('query', e.target.value)}
|
<span className={styles.breadcrumbItem}>📁 Root</span>
|
||||||
placeholder="quarterly report 2024"
|
) : (
|
||||||
/>
|
<>
|
||||||
</div>
|
<span
|
||||||
<div className={styles.formGroup}>
|
className={styles.breadcrumbItem + ' ' + styles.breadcrumbClickable}
|
||||||
<label className={styles.label}>Search Scope</label>
|
onClick={() => {
|
||||||
<select
|
setFolderPaths(['/']);
|
||||||
className={styles.select}
|
handleListDocuments(siteUrl, ['/']);
|
||||||
value={formData.searchScope}
|
setTableRefreshKey(prev => prev + 1);
|
||||||
onChange={(e) => handleFormChange('searchScope', e.target.value)}
|
}}
|
||||||
>
|
>
|
||||||
<option value="all">All</option>
|
📁 Root
|
||||||
<option value="documents">Documents Only</option>
|
</span>
|
||||||
<option value="pages">Pages Only</option>
|
{folderPaths[0].split('/').filter(Boolean).map((part, index, array) => {
|
||||||
</select>
|
const pathToHere = '/' + array.slice(0, index + 1).join('/');
|
||||||
</div>
|
const isLast = index === array.length - 1;
|
||||||
</>
|
return (
|
||||||
)}
|
<span key={index}>
|
||||||
|
<span className={styles.breadcrumbSeparator}>/</span>
|
||||||
{activeTab === 'read' && (
|
<span
|
||||||
<>
|
className={styles.breadcrumbItem + (isLast ? '' : ' ' + styles.breadcrumbClickable)}
|
||||||
<div className={styles.formGroup}>
|
onClick={!isLast ? () => {
|
||||||
<label className={styles.label}>Document List Reference</label>
|
setFolderPaths([pathToHere]);
|
||||||
<input
|
handleListDocuments(siteUrl, [pathToHere]);
|
||||||
type="text"
|
setTableRefreshKey(prev => prev + 1);
|
||||||
className={styles.input}
|
} : undefined}
|
||||||
value={formData.documentList}
|
>
|
||||||
onChange={(e) => handleFormChange('documentList', e.target.value)}
|
📁 {part}
|
||||||
placeholder="document_list_reference_from_chat"
|
</span>
|
||||||
/>
|
</span>
|
||||||
<div className={styles.helpText}>
|
);
|
||||||
This should be a reference from a chat session or document management system
|
})}
|
||||||
</div>
|
</>
|
||||||
</div>
|
)}
|
||||||
<div className={styles.formGroup}>
|
</div>
|
||||||
<label className={styles.label}>Document Paths (one per line)</label>
|
</div>
|
||||||
<textarea
|
|
||||||
className={styles.textarea}
|
<div className={sharedStyles.contentArea}>
|
||||||
value={formData.documentPaths.join('\n')}
|
<TestSharepointTable
|
||||||
onChange={(e) => handleArrayInputChange('documentPaths', e.target.value)}
|
key={tableRefreshKey}
|
||||||
placeholder="/Shared Documents/file1.docx /Documents/file2.pdf"
|
className={styles.sharepointTableContainer}
|
||||||
/>
|
documents={documents}
|
||||||
</div>
|
documentsLoading={documentsLoading}
|
||||||
<div className={styles.formGroup}>
|
documentsError={documentsError}
|
||||||
<label>
|
columns={columns}
|
||||||
<input
|
actions={actions}
|
||||||
type="checkbox"
|
onRowClick={onRowClick}
|
||||||
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/ /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 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()}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue