diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 021c0fe43..1b41d2849 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -2836,6 +2836,7 @@ export interface LibraryFileListItem { } export interface LibraryFileUpdate { + filename?: string; folder_id?: number | null; project_id?: number | null; notes?: string | null; diff --git a/frontend/src/pages/FileManagerPage.tsx b/frontend/src/pages/FileManagerPage.tsx index 13bfc0613..9d67db9d5 100644 --- a/frontend/src/pages/FileManagerPage.tsx +++ b/frontend/src/pages/FileManagerPage.tsx @@ -34,6 +34,8 @@ import { Archive as ArchiveIcon, Briefcase, Printer, + Pencil, + Play, } from 'lucide-react'; import { api } from '../api/client'; import type { @@ -120,6 +122,59 @@ function NewFolderModal({ parentId, onClose, onSave, isLoading }: NewFolderModal ); } +// Rename Modal +interface RenameModalProps { + type: 'file' | 'folder'; + currentName: string; + onClose: () => void; + onSave: (newName: string) => void; + isLoading: boolean; +} + +function RenameModal({ type, currentName, onClose, onSave, isLoading }: RenameModalProps) { + const [name, setName] = useState(currentName); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (name.trim() && name.trim() !== currentName) { + onSave(name.trim()); + } + }; + + return ( +
+
+
+

Rename {type === 'file' ? 'File' : 'Folder'}

+
+
+
+ + setName(e.target.value)} + className="w-full bg-bambu-dark border border-bambu-dark-tertiary rounded px-3 py-2 text-white placeholder-bambu-gray focus:outline-none focus:border-bambu-green" + autoFocus + required + /> +
+
+ + +
+
+
+
+ ); +} + // Move Files Modal interface MoveFilesModalProps { folders: LibraryFolderTree[]; @@ -568,10 +623,11 @@ interface FolderTreeItemProps { onSelect: (id: number | null) => void; onDelete: (id: number) => void; onLink: (folder: LibraryFolderTree) => void; + onRename: (folder: LibraryFolderTree) => void; depth?: number; } -function FolderTreeItem({ folder, selectedFolderId, onSelect, onDelete, onLink, depth = 0 }: FolderTreeItemProps) { +function FolderTreeItem({ folder, selectedFolderId, onSelect, onDelete, onLink, onRename, depth = 0 }: FolderTreeItemProps) { const [expanded, setExpanded] = useState(true); const [showActions, setShowActions] = useState(false); const hasChildren = folder.children.length > 0; @@ -643,6 +699,13 @@ function FolderTreeItem({ folder, selectedFolderId, onSelect, onDelete, onLink, <>
setShowActions(false)} />
+
- {/* Actions */} -
e.stopPropagation()}> + {/* Actions - always visible on mobile, hover on desktop */} +
e.stopPropagation()}> + {onRename && ( + + )}
- {/* Selection checkbox */} + {/* Selection checkbox - always visible on mobile, hover on desktop */}
{isSelected &&
}
@@ -840,6 +914,8 @@ export function FileManagerPage() { const [linkFolder, setLinkFolder] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState<{ type: 'file' | 'folder' | 'bulk'; id: number; count?: number } | null>(null); const [printFile, setPrintFile] = useState(null); + const [printMultiFile, setPrintMultiFile] = useState(null); + const [renameItem, setRenameItem] = useState<{ type: 'file' | 'folder'; id: number; name: string } | null>(null); const [viewMode, setViewMode] = useState<'grid' | 'list'>(() => { return (localStorage.getItem('library-view-mode') as 'grid' | 'list') || 'grid'; }); @@ -1040,6 +1116,34 @@ export function FileManagerPage() { onError: (error: Error) => showToast(error.message, 'error'), }); + const renameFileMutation = useMutation({ + mutationFn: ({ id, filename }: { id: number; filename: string }) => + api.updateLibraryFile(id, { filename }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['library-files'] }); + setRenameItem(null); + showToast('File renamed', 'success'); + }, + onError: (error: Error) => { + setRenameItem(null); + showToast(error.message, 'error'); + }, + }); + + const renameFolderMutation = useMutation({ + mutationFn: ({ id, name }: { id: number; name: string }) => + api.updateLibraryFolder(id, { name }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['library-folders'] }); + setRenameItem(null); + showToast('Folder renamed', 'success'); + }, + onError: (error: Error) => { + setRenameItem(null); + showToast(error.message, 'error'); + }, + }); + // Helper to check if a file is sliced (printable) const isSlicedFile = useCallback((filename: string) => { const lower = filename.toLowerCase(); @@ -1228,6 +1332,7 @@ export function FileManagerPage() { onSelect={setSelectedFolderId} onDelete={(id) => setDeleteConfirm({ type: 'folder', id })} onLink={setLinkFolder} + onRename={(f) => setRenameItem({ type: 'folder', id: f.id, name: f.name })} /> ))}
@@ -1332,14 +1437,24 @@ export function FileManagerPage() { {selectedFiles.length} selected
- {selectedSlicedFiles.length > 0 && ( + {selectedSlicedFiles.length === 1 && ( + )} + {selectedSlicedFiles.length > 0 && ( + )} @@ -1429,6 +1544,7 @@ export function FileManagerPage() { onDownload={handleDownload} onAddToQueue={(id) => addToQueueMutation.mutate([id])} onPrint={setPrintFile} + onRename={(f) => setRenameItem({ type: 'file', id: f.id, name: f.filename })} /> ))}
@@ -1544,6 +1660,13 @@ export function FileManagerPage() { > +
); }