Compare commits

..

3 Commits

Author SHA1 Message Date
Connor Yoh
307f960a8a Check for orphans 2025-09-11 16:00:43 +01:00
Connor Yoh
2b60f56ddc bulk deleted 2025-09-11 15:55:44 +01:00
Connor Yoh
410a6b8e9d Fix deleting history 2025-09-11 15:43:43 +01:00
3 changed files with 147 additions and 94 deletions

View File

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

View File

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

View File

@ -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,