feat(client): on sources page, have new modal for sync that asks user what kind of sync they would like to run

asdf
This commit is contained in:
perf3ct 2025-07-15 20:43:52 +00:00
parent f2adcab1da
commit 4bd9bae8b2
No known key found for this signature in database
GPG Key ID: 569C4EEC436F5232
2 changed files with 178 additions and 8 deletions

View File

@ -60,6 +60,8 @@ import {
Sync as SyncIcon, Sync as SyncIcon,
MoreVert as MoreVertIcon, MoreVert as MoreVertIcon,
Menu as MenuIcon, Menu as MenuIcon,
Speed as QuickSyncIcon,
ManageSearch as DeepScanIcon,
Folder as FolderIcon, Folder as FolderIcon,
Assessment as AssessmentIcon, Assessment as AssessmentIcon,
Extension as ExtensionIcon, Extension as ExtensionIcon,
@ -74,7 +76,7 @@ import {
Error as CriticalIcon, Error as CriticalIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import api, { queueService } from '../services/api'; import api, { queueService, sourcesService } from '../services/api';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
@ -165,6 +167,11 @@ const SourcesPage: React.FC = () => {
const [stoppingSync, setStoppingSync] = useState<string | null>(null); const [stoppingSync, setStoppingSync] = useState<string | null>(null);
const [validating, setValidating] = useState<string | null>(null); const [validating, setValidating] = useState<string | null>(null);
const [autoRefreshing, setAutoRefreshing] = useState(false); const [autoRefreshing, setAutoRefreshing] = useState(false);
// Sync modal state
const [syncModalOpen, setSyncModalOpen] = useState(false);
const [sourceToSync, setSourceToSync] = useState<Source | null>(null);
const [deepScanning, setDeepScanning] = useState(false);
useEffect(() => { useEffect(() => {
loadSources(); loadSources();
@ -482,11 +489,26 @@ const SourcesPage: React.FC = () => {
} }
}; };
const handleTriggerSync = async (sourceId: string) => { // Open sync modal instead of directly triggering sync
setSyncingSource(sourceId); const handleOpenSyncModal = (source: Source) => {
setSourceToSync(source);
setSyncModalOpen(true);
};
const handleCloseSyncModal = () => {
setSyncModalOpen(false);
setSourceToSync(null);
};
const handleQuickSync = async () => {
if (!sourceToSync) return;
setSyncingSource(sourceToSync.id);
handleCloseSyncModal();
try { try {
await api.post(`/sources/${sourceId}/sync`); await sourcesService.triggerSync(sourceToSync.id);
showSnackbar('Sync started successfully', 'success'); showSnackbar('Quick sync started successfully', 'success');
setTimeout(loadSources, 1000); setTimeout(loadSources, 1000);
} catch (error: any) { } catch (error: any) {
console.error('Failed to trigger sync:', error); console.error('Failed to trigger sync:', error);
@ -500,10 +522,34 @@ const SourcesPage: React.FC = () => {
} }
}; };
const handleDeepScan = async () => {
if (!sourceToSync) return;
setDeepScanning(true);
handleCloseSyncModal();
try {
await sourcesService.triggerDeepScan(sourceToSync.id);
showSnackbar('Deep scan started successfully', 'success');
setTimeout(loadSources, 1000);
} catch (error: any) {
console.error('Failed to trigger deep scan:', error);
if (error.response?.status === 409) {
showSnackbar('Source is already syncing', 'warning');
} else if (error.response?.status === 400 && error.response?.data?.message?.includes('only supported for WebDAV')) {
showSnackbar('Deep scan is only supported for WebDAV sources', 'warning');
} else {
showSnackbar('Failed to start deep scan', 'error');
}
} finally {
setDeepScanning(false);
}
};
const handleStopSync = async (sourceId: string) => { const handleStopSync = async (sourceId: string) => {
setStoppingSync(sourceId); setStoppingSync(sourceId);
try { try {
await api.post(`/sources/${sourceId}/sync/stop`); await sourcesService.stopSync(sourceId);
showSnackbar('Sync stopped successfully', 'success'); showSnackbar('Sync stopped successfully', 'success');
setTimeout(loadSources, 1000); setTimeout(loadSources, 1000);
} catch (error: any) { } catch (error: any) {
@ -929,8 +975,8 @@ const SourcesPage: React.FC = () => {
<Tooltip title="Trigger Sync"> <Tooltip title="Trigger Sync">
<span> <span>
<IconButton <IconButton
onClick={() => handleTriggerSync(source.id)} onClick={() => handleOpenSyncModal(source)}
disabled={syncingSource === source.id || !source.enabled} disabled={syncingSource === source.id || deepScanning || !source.enabled}
sx={{ sx={{
bgcolor: alpha(theme.palette.primary.main, 0.1), bgcolor: alpha(theme.palette.primary.main, 0.1),
'&:hover': { bgcolor: alpha(theme.palette.primary.main, 0.2) }, '&:hover': { bgcolor: alpha(theme.palette.primary.main, 0.2) },
@ -2320,6 +2366,116 @@ const SourcesPage: React.FC = () => {
</DialogActions> </DialogActions>
</Dialog> </Dialog>
{/* Sync Type Selection Modal */}
<Dialog
open={syncModalOpen}
onClose={handleCloseSyncModal}
maxWidth="sm"
fullWidth
>
<DialogTitle sx={{ pb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<SyncIcon color="primary" />
Choose Sync Type
</Box>
</DialogTitle>
<DialogContent>
<DialogContentText sx={{ mb: 3 }}>
{sourceToSync && (
<>
Select the type of synchronization for <strong>{sourceToSync.name}</strong>:
</>
)}
</DialogContentText>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<Card
sx={{
cursor: 'pointer',
border: '2px solid transparent',
transition: 'all 0.2s',
'&:hover': {
borderColor: 'primary.main',
bgcolor: 'action.hover',
},
}}
onClick={handleQuickSync}
>
<CardContent sx={{ textAlign: 'center', py: 3 }}>
<QuickSyncIcon
sx={{
fontSize: 48,
color: 'primary.main',
mb: 2,
}}
/>
<Typography variant="h6" gutterBottom>
Quick Sync
</Typography>
<Typography variant="body2" color="text.secondary">
Fast incremental sync using ETags. Only processes new or changed files.
</Typography>
<Box sx={{ mt: 2 }}>
<Chip label="Recommended" color="primary" size="small" />
</Box>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={6}>
<Card
sx={{
cursor: sourceToSync?.source_type === 'webdav' ? 'pointer' : 'not-allowed',
border: '2px solid transparent',
transition: 'all 0.2s',
opacity: sourceToSync?.source_type === 'webdav' ? 1 : 0.6,
'&:hover': sourceToSync?.source_type === 'webdav' ? {
borderColor: 'warning.main',
bgcolor: 'action.hover',
} : {},
}}
onClick={sourceToSync?.source_type === 'webdav' ? handleDeepScan : undefined}
>
<CardContent sx={{ textAlign: 'center', py: 3 }}>
<DeepScanIcon
sx={{
fontSize: 48,
color: sourceToSync?.source_type === 'webdav' ? 'warning.main' : 'text.disabled',
mb: 2,
}}
/>
<Typography variant="h6" gutterBottom>
Deep Scan
</Typography>
<Typography variant="body2" color="text.secondary">
Complete rescan that resets ETag expectations. Use for troubleshooting sync issues.
</Typography>
<Box sx={{ mt: 2 }}>
{sourceToSync?.source_type === 'webdav' ? (
<Chip label="WebDAV Only" color="warning" size="small" />
) : (
<Chip label="Not Available" color="default" size="small" />
)}
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
{sourceToSync?.source_type !== 'webdav' && (
<Alert severity="info" sx={{ mt: 2 }}>
Deep scan is currently only available for WebDAV sources. Other source types will use quick sync.
</Alert>
)}
</DialogContent>
<DialogActions>
<Button onClick={handleCloseSyncModal}>
Cancel
</Button>
</DialogActions>
</Dialog>
{/* Snackbar */} {/* Snackbar */}
<Snackbar <Snackbar
open={snackbar.open} open={snackbar.open}

View File

@ -468,4 +468,18 @@ export const ocrService = {
} }
return api.post(`/documents/${documentId}/retry-ocr`, data) return api.post(`/documents/${documentId}/retry-ocr`, data)
}, },
}
export const sourcesService = {
triggerSync: (sourceId: string) => {
return api.post(`/sources/${sourceId}/sync`)
},
triggerDeepScan: (sourceId: string) => {
return api.post(`/sources/${sourceId}/deep-scan`)
},
stopSync: (sourceId: string) => {
return api.post(`/sources/${sourceId}/sync/stop`)
},
} }