mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-24 04:26:14 +00:00
Compare commits
3 Commits
777c54dbe4
...
307f960a8a
Author | SHA1 | Date | |
---|---|---|---|
![]() |
307f960a8a | ||
![]() |
2b60f56ddc | ||
![]() |
410a6b8e9d |
@ -10,7 +10,7 @@ interface FileHistoryGroupProps {
|
|||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
onDownloadSingle: (file: StirlingFileStub) => void;
|
onDownloadSingle: (file: StirlingFileStub) => void;
|
||||||
onFileDoubleClick: (file: StirlingFileStub) => void;
|
onFileDoubleClick: (file: StirlingFileStub) => void;
|
||||||
onFileRemove: (index: number) => void;
|
onHistoryFileRemove: (file: StirlingFileStub) => void;
|
||||||
isFileSupported: (fileName: string) => boolean;
|
isFileSupported: (fileName: string) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ const FileHistoryGroup: React.FC<FileHistoryGroupProps> = ({
|
|||||||
isExpanded,
|
isExpanded,
|
||||||
onDownloadSingle,
|
onDownloadSingle,
|
||||||
onFileDoubleClick,
|
onFileDoubleClick,
|
||||||
onFileRemove,
|
onHistoryFileRemove,
|
||||||
isFileSupported,
|
isFileSupported,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -44,14 +44,14 @@ const FileHistoryGroup: React.FC<FileHistoryGroupProps> = ({
|
|||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Box ml="md">
|
<Box ml="md">
|
||||||
{sortedHistory.map((historyFile, index) => (
|
{sortedHistory.map((historyFile, _index) => (
|
||||||
<FileListItem
|
<FileListItem
|
||||||
key={`history-${historyFile.id}-${historyFile.versionNumber || 1}`}
|
key={`history-${historyFile.id}-${historyFile.versionNumber || 1}`}
|
||||||
file={historyFile}
|
file={historyFile}
|
||||||
isSelected={false} // History files are not selectable
|
isSelected={false} // History files are not selectable
|
||||||
isSupported={isFileSupported(historyFile.name)}
|
isSupported={isFileSupported(historyFile.name)}
|
||||||
onSelect={() => {}} // No selection for history files
|
onSelect={() => {}} // No selection for history files
|
||||||
onRemove={() => onFileRemove(index)} // Pass through remove handler
|
onRemove={() => onHistoryFileRemove(historyFile)} // Remove specific history file
|
||||||
onDownload={() => onDownloadSingle(historyFile)}
|
onDownload={() => onDownloadSingle(historyFile)}
|
||||||
onDoubleClick={() => onFileDoubleClick(historyFile)}
|
onDoubleClick={() => onFileDoubleClick(historyFile)}
|
||||||
isHistoryFile={true} // This enables "Add to Recents" in menu
|
isHistoryFile={true} // This enables "Add to Recents" in menu
|
||||||
|
@ -25,6 +25,7 @@ const FileListArea: React.FC<FileListAreaProps> = ({
|
|||||||
loadedHistoryFiles,
|
loadedHistoryFiles,
|
||||||
onFileSelect,
|
onFileSelect,
|
||||||
onFileRemove,
|
onFileRemove,
|
||||||
|
onHistoryFileRemove,
|
||||||
onFileDoubleClick,
|
onFileDoubleClick,
|
||||||
onDownloadSingle,
|
onDownloadSingle,
|
||||||
isFileSupported,
|
isFileSupported,
|
||||||
@ -78,7 +79,7 @@ const FileListArea: React.FC<FileListAreaProps> = ({
|
|||||||
isExpanded={isExpanded}
|
isExpanded={isExpanded}
|
||||||
onDownloadSingle={onDownloadSingle}
|
onDownloadSingle={onDownloadSingle}
|
||||||
onFileDoubleClick={onFileDoubleClick}
|
onFileDoubleClick={onFileDoubleClick}
|
||||||
onFileRemove={onFileRemove}
|
onHistoryFileRemove={onHistoryFileRemove}
|
||||||
isFileSupported={isFileSupported}
|
isFileSupported={isFileSupported}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -24,6 +24,7 @@ interface FileManagerContextValue {
|
|||||||
onLocalFileClick: () => void;
|
onLocalFileClick: () => void;
|
||||||
onFileSelect: (file: StirlingFileStub, index: number, shiftKey?: boolean) => void;
|
onFileSelect: (file: StirlingFileStub, index: number, shiftKey?: boolean) => void;
|
||||||
onFileRemove: (index: number) => void;
|
onFileRemove: (index: number) => void;
|
||||||
|
onHistoryFileRemove: (file: StirlingFileStub) => void;
|
||||||
onFileDoubleClick: (file: StirlingFileStub) => void;
|
onFileDoubleClick: (file: StirlingFileStub) => void;
|
||||||
onOpenFiles: () => void;
|
onOpenFiles: () => void;
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
@ -172,7 +173,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
|
|
||||||
// Helper function to safely determine which files can be deleted
|
// Helper function to safely determine which files can be deleted
|
||||||
const getSafeFilesToDelete = useCallback((
|
const getSafeFilesToDelete = useCallback((
|
||||||
leafFileIds: string[],
|
fileIds: string[],
|
||||||
allStoredStubs: StirlingFileStub[]
|
allStoredStubs: StirlingFileStub[]
|
||||||
): string[] => {
|
): string[] => {
|
||||||
const fileMap = new Map(allStoredStubs.map(f => [f.id as string, f]));
|
const fileMap = new Map(allStoredStubs.map(f => [f.id as string, f]));
|
||||||
@ -180,7 +181,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
const filesToPreserve = new Set<string>();
|
const filesToPreserve = new Set<string>();
|
||||||
|
|
||||||
// First, identify all files in the lineages of the leaf files being deleted
|
// First, identify all files in the lineages of the leaf files being deleted
|
||||||
for (const leafFileId of leafFileIds) {
|
for (const leafFileId of fileIds) {
|
||||||
const currentFile = fileMap.get(leafFileId);
|
const currentFile = fileMap.get(leafFileId);
|
||||||
if (!currentFile) continue;
|
if (!currentFile) continue;
|
||||||
|
|
||||||
@ -206,7 +207,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
const fileOriginalId = file.originalFileId || file.id;
|
const fileOriginalId = file.originalFileId || file.id;
|
||||||
|
|
||||||
// If this file is a leaf node (not being deleted) and its lineage overlaps with files we want to delete
|
// If this file is a leaf node (not being deleted) and its lineage overlaps with files we want to delete
|
||||||
if (file.isLeaf !== false && !leafFileIds.includes(file.id)) {
|
if (file.isLeaf !== false && !fileIds.includes(file.id)) {
|
||||||
// Find all files in this preserved lineage
|
// Find all files in this preserved lineage
|
||||||
const preservedChainFiles = allStoredStubs.filter((chainFile: StirlingFileStub) =>
|
const preservedChainFiles = allStoredStubs.filter((chainFile: StirlingFileStub) =>
|
||||||
(chainFile.originalFileId || chainFile.id) === fileOriginalId
|
(chainFile.originalFileId || chainFile.id) === fileOriginalId
|
||||||
@ -218,20 +219,50 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Final list: files to delete minus files that must be preserved
|
// Final list: files to delete minus files that must be preserved
|
||||||
const safeToDelete = Array.from(filesToDelete).filter(fileId => !filesToPreserve.has(fileId));
|
let safeToDelete = Array.from(filesToDelete).filter(fileId => !filesToPreserve.has(fileId));
|
||||||
|
|
||||||
|
// Check for orphaned non-leaf files after main deletion
|
||||||
|
const remainingFiles = allStoredStubs.filter(file => !safeToDelete.includes(file.id as string));
|
||||||
|
const orphanedNonLeafFiles: string[] = [];
|
||||||
|
|
||||||
|
for (const file of remainingFiles) {
|
||||||
|
// Only check non-leaf files (files that have been processed and have children)
|
||||||
|
if (file.isLeaf === false) {
|
||||||
|
const fileOriginalId = file.originalFileId || file.id;
|
||||||
|
|
||||||
|
// Check if this non-leaf file has any living descendants
|
||||||
|
const hasLivingDescendants = remainingFiles.some(otherFile => {
|
||||||
|
// Check if otherFile is a descendant of this file
|
||||||
|
const otherOriginalId = otherFile.originalFileId || otherFile.id;
|
||||||
|
return (
|
||||||
|
// Direct parent relationship
|
||||||
|
otherFile.parentFileId === file.id ||
|
||||||
|
// Same lineage but different from this file
|
||||||
|
(otherOriginalId === fileOriginalId && otherFile.id !== file.id)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasLivingDescendants) {
|
||||||
|
orphanedNonLeafFiles.push(file.id as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add orphaned non-leaf files to deletion list
|
||||||
|
safeToDelete = [...safeToDelete, ...orphanedNonLeafFiles];
|
||||||
|
|
||||||
console.log('Deletion analysis:', {
|
console.log('Deletion analysis:', {
|
||||||
candidatesForDeletion: Array.from(filesToDelete),
|
candidatesForDeletion: Array.from(filesToDelete),
|
||||||
mustPreserve: Array.from(filesToPreserve),
|
mustPreserve: Array.from(filesToPreserve),
|
||||||
|
orphanedNonLeafFiles,
|
||||||
safeToDelete
|
safeToDelete
|
||||||
});
|
});
|
||||||
|
|
||||||
return safeToDelete;
|
return safeToDelete;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleFileRemove = useCallback(async (index: number) => {
|
// Shared internal delete logic
|
||||||
const fileToRemove = filteredFiles[index];
|
const performFileDelete = useCallback(async (fileToRemove: StirlingFileStub, fileIndex: number) => {
|
||||||
if (fileToRemove) {
|
|
||||||
const deletedFileId = fileToRemove.id;
|
const deletedFileId = fileToRemove.id;
|
||||||
|
|
||||||
// Get all stored files to analyze lineages
|
// Get all stored files to analyze lineages
|
||||||
@ -280,11 +311,68 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Call the parent's deletion logic for the main file only
|
// Call the parent's deletion logic for the main file only
|
||||||
onFileRemove(index);
|
onFileRemove(fileIndex);
|
||||||
|
|
||||||
// Refresh to ensure consistent state
|
// Refresh to ensure consistent state
|
||||||
await refreshRecentFiles();
|
await refreshRecentFiles();
|
||||||
|
}, [getSafeFilesToDelete, setSelectedFileIds, setExpandedFileIds, setLoadedHistoryFiles, onFileRemove, refreshRecentFiles]);
|
||||||
|
|
||||||
|
const handleFileRemove = useCallback(async (index: number) => {
|
||||||
|
const fileToRemove = filteredFiles[index];
|
||||||
|
if (fileToRemove) {
|
||||||
|
await performFileDelete(fileToRemove, index);
|
||||||
}
|
}
|
||||||
|
}, [filteredFiles, performFileDelete]);
|
||||||
|
|
||||||
|
// Handle deletion by fileId (more robust than index-based)
|
||||||
|
const handleFileRemoveById = useCallback(async (fileId: FileId) => {
|
||||||
|
// Find the file and its index in filteredFiles
|
||||||
|
const fileIndex = filteredFiles.findIndex(file => file.id === fileId);
|
||||||
|
const fileToRemove = filteredFiles[fileIndex];
|
||||||
|
|
||||||
|
if (fileToRemove && fileIndex !== -1) {
|
||||||
|
await performFileDelete(fileToRemove, fileIndex);
|
||||||
|
}
|
||||||
|
}, [filteredFiles, performFileDelete]);
|
||||||
|
|
||||||
|
// Handle deletion of specific history files (not index-based)
|
||||||
|
const handleHistoryFileRemove = useCallback(async (fileToRemove: StirlingFileStub) => {
|
||||||
|
const deletedFileId = fileToRemove.id;
|
||||||
|
|
||||||
|
// Clear from expanded state to prevent ghost entries
|
||||||
|
setExpandedFileIds(prev => {
|
||||||
|
const newExpanded = new Set(prev);
|
||||||
|
newExpanded.delete(deletedFileId);
|
||||||
|
return newExpanded;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear from history cache - remove all files in the chain
|
||||||
|
setLoadedHistoryFiles(prev => {
|
||||||
|
const newCache = new Map(prev);
|
||||||
|
|
||||||
|
// Remove cache entries for all deleted files
|
||||||
|
newCache.delete(deletedFileId);
|
||||||
|
|
||||||
|
// Also remove deleted files from any other file's history cache
|
||||||
|
for (const [mainFileId, historyFiles] of newCache.entries()) {
|
||||||
|
const filteredHistory = historyFiles.filter(histFile => deletedFileId != histFile.id);
|
||||||
|
if (filteredHistory.length !== historyFiles.length) {
|
||||||
|
newCache.set(mainFileId, filteredHistory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newCache;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete safe files from IndexedDB
|
||||||
|
try {
|
||||||
|
await fileStorage.deleteStirlingFile(deletedFileId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete files from chain:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh to ensure consistent state
|
||||||
|
await refreshRecentFiles();
|
||||||
}, [filteredFiles, onFileRemove, refreshRecentFiles, getSafeFilesToDelete]);
|
}, [filteredFiles, onFileRemove, refreshRecentFiles, getSafeFilesToDelete]);
|
||||||
|
|
||||||
const handleFileDoubleClick = useCallback((file: StirlingFileStub) => {
|
const handleFileDoubleClick = useCallback((file: StirlingFileStub) => {
|
||||||
@ -337,53 +425,14 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
if (selectedFileIds.length === 0) return;
|
if (selectedFileIds.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all stored files to analyze lineages
|
// Delete each selected file using the proven single delete logic
|
||||||
const allStoredStubs = await fileStorage.getAllStirlingFileStubs();
|
for (const fileId of selectedFileIds) {
|
||||||
|
await handleFileRemoveById(fileId);
|
||||||
// Get safe files to delete (respecting shared lineages)
|
|
||||||
const filesToDelete = getSafeFilesToDelete(selectedFileIds, allStoredStubs);
|
|
||||||
|
|
||||||
console.log(`Bulk safely deleting files and their history chains:`, filesToDelete);
|
|
||||||
|
|
||||||
// Update history cache synchronously
|
|
||||||
setLoadedHistoryFiles(prev => {
|
|
||||||
const newCache = new Map(prev);
|
|
||||||
|
|
||||||
// Remove cache entries for all deleted files
|
|
||||||
filesToDelete.forEach(id => newCache.delete(id as FileId));
|
|
||||||
|
|
||||||
// Also remove deleted files from any other file's history cache
|
|
||||||
for (const [mainFileId, historyFiles] of newCache.entries()) {
|
|
||||||
const filteredHistory = historyFiles.filter(histFile => !filesToDelete.includes(histFile.id as string));
|
|
||||||
if (filteredHistory.length !== historyFiles.length) {
|
|
||||||
newCache.set(mainFileId, filteredHistory);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return newCache;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Also clear any expanded state for deleted files to prevent ghost entries
|
|
||||||
setExpandedFileIds(prev => {
|
|
||||||
const newExpanded = new Set(prev);
|
|
||||||
filesToDelete.forEach(id => newExpanded.delete(id));
|
|
||||||
return newExpanded;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear selection immediately to prevent ghost selections
|
|
||||||
setSelectedFileIds(prev => prev.filter(id => !filesToDelete.includes(id)));
|
|
||||||
|
|
||||||
// Delete safe files from IndexedDB
|
|
||||||
for (const fileId of filesToDelete) {
|
|
||||||
await fileStorage.deleteStirlingFile(fileId as FileId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh the file list to get updated data
|
|
||||||
await refreshRecentFiles();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete selected files:', error);
|
console.error('Failed to delete selected files:', error);
|
||||||
}
|
}
|
||||||
}, [selectedFileIds, filteredFiles, refreshRecentFiles, getSafeFilesToDelete]);
|
}, [selectedFileIds, handleFileRemoveById]);
|
||||||
|
|
||||||
|
|
||||||
const handleDownloadSelected = useCallback(async () => {
|
const handleDownloadSelected = useCallback(async () => {
|
||||||
@ -540,6 +589,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
onLocalFileClick: handleLocalFileClick,
|
onLocalFileClick: handleLocalFileClick,
|
||||||
onFileSelect: handleFileSelect,
|
onFileSelect: handleFileSelect,
|
||||||
onFileRemove: handleFileRemove,
|
onFileRemove: handleFileRemove,
|
||||||
|
onHistoryFileRemove: handleHistoryFileRemove,
|
||||||
onFileDoubleClick: handleFileDoubleClick,
|
onFileDoubleClick: handleFileDoubleClick,
|
||||||
onOpenFiles: handleOpenFiles,
|
onOpenFiles: handleOpenFiles,
|
||||||
onSearchChange: handleSearchChange,
|
onSearchChange: handleSearchChange,
|
||||||
@ -570,6 +620,8 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
|
|||||||
handleLocalFileClick,
|
handleLocalFileClick,
|
||||||
handleFileSelect,
|
handleFileSelect,
|
||||||
handleFileRemove,
|
handleFileRemove,
|
||||||
|
handleFileRemoveById,
|
||||||
|
performFileDelete,
|
||||||
handleFileDoubleClick,
|
handleFileDoubleClick,
|
||||||
handleOpenFiles,
|
handleOpenFiles,
|
||||||
handleSearchChange,
|
handleSearchChange,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user