feat(upload): improve folder browser UX and performance - add loading state, fix chevron visibility, implement backend caching with 5min TTL, add cache invalidation on file operations

This commit is contained in:
Geert Rademakes 2025-08-14 09:17:54 +02:00
parent 10e38fae4d
commit 7557eddeb4
3 changed files with 69 additions and 3 deletions

View File

@ -95,6 +95,9 @@ router.post('/upload', upload.single('file'), async (req, res) => {
await musicFile.save();
// Invalidate folder cache since we added a new file
invalidateFolderCache();
// Optionally add to a special playlist for scanning
if (markForScan) {
try {
@ -191,6 +194,9 @@ router.post('/batch-upload', upload.array('files', 10), async (req, res) => {
await musicFile.save();
results.push({ success: true, musicFile });
// Invalidate folder cache since we added a new file
invalidateFolderCache();
} catch (error) {
console.error(`Error uploading ${file.originalname}:`, error);
results.push({ success: false, fileName: file.originalname, error: (error as Error).message });
@ -223,17 +229,51 @@ router.get('/files', async (req, res) => {
/**
* List folders in the S3 bucket for folder selection
*/
// Cache for folder listing to improve performance
let folderCache: { folders: string[]; timestamp: number } | null = null;
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
// Function to invalidate folder cache
const invalidateFolderCache = () => {
folderCache = null;
};
router.get('/folders', async (req, res) => {
try {
// Check if we have a valid cached result
if (folderCache && (Date.now() - folderCache.timestamp) < CACHE_DURATION) {
return res.json({ folders: folderCache.folders });
}
const folders = await s3Service.listAllFolders('');
// Include root option as empty string
res.json({ folders: ['', ...folders] });
const result = ['', ...folders];
// Cache the result
folderCache = {
folders: result,
timestamp: Date.now()
};
res.json({ folders: result });
} catch (error) {
console.error('Error fetching S3 folders:', error);
res.status(500).json({ error: 'Failed to fetch S3 folders' });
}
});
/**
* Force refresh folder cache
*/
router.post('/folders/refresh', async (req, res) => {
try {
invalidateFolderCache();
res.json({ message: 'Folder cache refreshed successfully' });
} catch (error) {
console.error('Error refreshing folder cache:', error);
res.status(500).json({ error: 'Failed to refresh folder cache' });
}
});
/**
* Sync S3 files with database - now uses background job system
*/
@ -256,6 +296,9 @@ router.post('/sync-s3', async (req, res) => {
status: 'started'
});
// Invalidate folder cache since sync might change folder structure
invalidateFolderCache();
} catch (error) {
console.error('❌ Error starting S3 sync job:', error);
res.status(500).json({ error: 'Failed to start S3 sync job' });
@ -396,6 +439,9 @@ router.delete('/:id', async (req, res) => {
// Delete from database
await MusicFile.findByIdAndDelete(req.params.id);
// Invalidate folder cache since we removed a file
invalidateFolderCache();
res.json({ message: 'Music file deleted successfully' });
} catch (error) {
console.error('Delete error:', error);

View File

@ -135,6 +135,9 @@ const FolderBrowser: React.FC<FolderBrowserProps> = ({
toggleFolder(node.path);
}}
aria-label={expanded ? 'Collapse folder' : 'Expand folder'}
color="gray.300"
_hover={{ bg: 'gray.700', color: 'white' }}
_active={{ bg: 'gray.600' }}
/>
) : (
<Box w="32px" />

View File

@ -14,6 +14,7 @@ import {
Icon,
Input,
Checkbox,
Spinner,
useToast,
} from '@chakra-ui/react';
import { FiUpload, FiMusic, FiCheck, FiX } from 'react-icons/fi';
@ -35,6 +36,7 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
const [isUploading, setIsUploading] = useState(false);
const [targetFolder, setTargetFolder] = useState<string>('uploads');
const [folders, setFolders] = useState<string[]>([]);
const [isLoadingFolders, setIsLoadingFolders] = useState(true);
const [markForScan, setMarkForScan] = useState<boolean>(true);
const toast = useToast();
@ -126,6 +128,7 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
React.useEffect(() => {
(async () => {
try {
setIsLoadingFolders(true);
const res = await fetch('/api/music/folders');
if (!res.ok) throw new Error('Failed to load folders');
const data = await res.json();
@ -138,6 +141,8 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
}
} catch (e) {
// ignore, keep text input fallback
} finally {
setIsLoadingFolders(false);
}
})();
}, []);
@ -150,7 +155,19 @@ export const MusicUpload: React.FC<MusicUploadProps> = ({ onUploadComplete }) =>
<VStack spacing={4} align="stretch" w="full">
<Box>
<Text color="gray.300" fontSize="sm" mb={3}>Target S3 folder</Text>
{folders.length > 0 ? (
{isLoadingFolders ? (
<Box
p={6}
textAlign="center"
bg="gray.800"
border="1px solid"
borderColor="gray.700"
borderRadius="md"
>
<Spinner size="md" color="blue.400" mb={3} />
<Text color="gray.400" fontSize="sm">Loading folder structure...</Text>
</Box>
) : folders.length > 0 ? (
<FolderBrowser
folders={folders}
selectedFolder={targetFolder}