432 lines
No EOL
23 KiB
TypeScript
432 lines
No EOL
23 KiB
TypeScript
import { useState } from 'react';
|
|
import { IoIosRefresh, IoIosLink } from 'react-icons/io';
|
|
import { useLanguage } from '../../contexts/LanguageContext';
|
|
import sharedStyles from '../../components/PageManager/pages.module.css';
|
|
import styles from './HomeStyles/TestSharepoint.module.css'
|
|
import { TestSharepointTable, useTestSharepointLogic } from '../../components/TestSharepoint'
|
|
|
|
function TestSharepoint() {
|
|
const { t } = useLanguage();
|
|
const {
|
|
connections,
|
|
selectedConnection,
|
|
connectionLoading,
|
|
connectionError,
|
|
documents,
|
|
documentsLoading,
|
|
documentsError,
|
|
columns,
|
|
actions,
|
|
testingConnections,
|
|
connectionTestResults,
|
|
discoveredSites,
|
|
sitesDiscovered,
|
|
tokenDebugInfo,
|
|
handleSelectConnection,
|
|
handleTestConnection,
|
|
handleListDocuments,
|
|
handleDiscoverSites,
|
|
handleSelectSite,
|
|
handleDebugTokens,
|
|
handleCleanupTokens,
|
|
handleFolderNavigation,
|
|
refetchConnections
|
|
} = useTestSharepointLogic();
|
|
|
|
const [tableRefreshKey, setTableRefreshKey] = useState(0);
|
|
const [siteUrl, setSiteUrl] = useState('https://your-tenant.sharepoint.com/sites/your-site');
|
|
const [folderPaths, setFolderPaths] = useState(['/']);
|
|
|
|
const onTestConnection = async (connectionId: string) => {
|
|
await handleTestConnection(connectionId);
|
|
};
|
|
|
|
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);
|
|
};
|
|
|
|
const onDiscoverSites = async () => {
|
|
await handleDiscoverSites();
|
|
};
|
|
|
|
const onSelectSite = (selectedSiteUrl: string) => {
|
|
setSiteUrl(selectedSiteUrl);
|
|
handleSelectSite(selectedSiteUrl);
|
|
};
|
|
|
|
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 renderConnectionCard = (connection: any) => {
|
|
const testResult = connectionTestResults[connection.id];
|
|
const isTestingThis = testingConnections.has(connection.id);
|
|
|
|
return (
|
|
<div
|
|
key={connection.id}
|
|
className={`${styles.connectionCard} ${selectedConnection?.id === connection.id ? styles.active : ''}`}
|
|
onClick={() => handleSelectConnection(connection.id)}
|
|
>
|
|
<div className={styles.connectionInfo}>
|
|
<div className={styles.connectionName}>
|
|
{connection.externalUsername || connection.id}
|
|
</div>
|
|
<div className={styles.connectionStatus}>
|
|
<span className={`${styles.statusBadge} ${styles[connection.status]}`}>
|
|
{connection.status}
|
|
</span>
|
|
{connection.externalEmail && (
|
|
<span style={{ marginLeft: '8px' }}>{connection.externalEmail}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className={styles.connectionActions}>
|
|
<button
|
|
className={styles.button}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onTestConnection(connection.id);
|
|
}}
|
|
disabled={isTestingThis}
|
|
aria-label={t('sharepoint.button.testConnection')}
|
|
>
|
|
{isTestingThis ? '⏳' : t('sharepoint.button.testConnection')}
|
|
</button>
|
|
{testResult && (
|
|
<span className={testResult.success ? styles.successMessage : styles.errorMessage}>
|
|
{testResult.success ? '✓' : '✗'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<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}>{t('sharepoint.form.siteUrl')}</label>
|
|
<div style={{ display: 'flex', gap: '10px', alignItems: 'center', marginBottom: '10px' }}>
|
|
<input
|
|
type="text"
|
|
className={styles.input}
|
|
value={siteUrl}
|
|
onChange={(e) => setSiteUrl(e.target.value)}
|
|
placeholder="https://your-tenant.sharepoint.com/sites/your-site"
|
|
style={{ flex: 1 }}
|
|
/>
|
|
<button
|
|
className={sharedStyles.secondaryButton}
|
|
onClick={onDiscoverSites}
|
|
disabled={connectionLoading}
|
|
type="button"
|
|
>
|
|
<span className={sharedStyles.buttonIcon}><IoIosLink /></span>
|
|
{t('sharepoint.button.discoverSites')}
|
|
</button>
|
|
</div>
|
|
|
|
{sitesDiscovered && discoveredSites.length > 0 && (
|
|
<div className={styles.sitesDiscovery}>
|
|
<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} ${siteUrl === site.url ? styles.selectedSite : ''}`}
|
|
onClick={() => onSelectSite(site.url)}
|
|
>
|
|
<div className={styles.siteName}>
|
|
<strong>{site.name}</strong>
|
|
<span className={styles.siteType}>({site.type})</span>
|
|
</div>
|
|
<div className={styles.siteUrl}>{site.url}</div>
|
|
{site.description && (
|
|
<div className={styles.siteDescription}>{site.description}</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{sitesDiscovered && discoveredSites.length === 0 && (
|
|
<div className={styles.noSitesFound}>
|
|
<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 className={styles.formGroup}>
|
|
<label className={styles.label}>{t('sharepoint.form.folderPaths')}</label>
|
|
<textarea
|
|
className={styles.textarea}
|
|
value={folderPaths.join('\n')}
|
|
onChange={(e) => setFolderPaths(e.target.value.split('\n').filter(path => path.trim()))}
|
|
placeholder="/ /Documents /Sites/YourSite/Shared Documents"
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
<div className={styles.buttonGroup}>
|
|
<button
|
|
className={sharedStyles.primaryButton}
|
|
onClick={onListDocuments}
|
|
disabled={connectionLoading || !selectedConnection}
|
|
>
|
|
{t('sharepoint.button.listDocuments')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 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>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default TestSharepoint; |