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'}
+
+
+
+
+ );
+}
+
// 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 */}
@@ -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() {
>
+