Просмотр исходного кода

Add File Manager rename, print button, and mobile accessibility
- Add rename functionality for files and folders:
- New RenameModal component with validation
- Backend endpoint supports filename updates (rejects path separators)
- Context menu "Rename" option in grid and list views
- Inline rename button in list view actions
- Add Print button to multi-selection toolbar:
- Appears when exactly one sliced file is selected
- Opens full PrintModal with plate selection and options
- Add to Queue button now uses Clock icon for clarity
- Improve mobile/PWA accessibility:
- Three-dot menu button always visible on mobile (md:opacity-0)
- Selection checkbox always visible on touch devices
- Better experience for PWA users on tablets and phones

Closes #94

maziggy 4 месяцев назад
Родитель
Сommit
19868b155b
2 измененных файлов с 163 добавлено и 8 удалено
  1. 1 0
      frontend/src/api/client.ts
  2. 162 8
      frontend/src/pages/FileManagerPage.tsx

+ 1 - 0
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;

+ 162 - 8
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 (
+    <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
+      <div className="bg-bambu-dark-secondary rounded-lg w-full max-w-sm border border-bambu-dark-tertiary">
+        <div className="p-4 border-b border-bambu-dark-tertiary">
+          <h2 className="text-lg font-semibold text-white">Rename {type === 'file' ? 'File' : 'Folder'}</h2>
+        </div>
+        <form onSubmit={handleSubmit} className="p-4 space-y-4">
+          <div>
+            <label className="block text-sm font-medium text-white mb-1">
+              Name
+            </label>
+            <input
+              type="text"
+              value={name}
+              onChange={(e) => 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
+            />
+          </div>
+          <div className="flex justify-end gap-2 pt-2">
+            <Button type="button" variant="secondary" onClick={onClose}>
+              Cancel
+            </Button>
+            <Button type="submit" disabled={!name.trim() || name.trim() === currentName || isLoading}>
+              {isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Rename'}
+            </Button>
+          </div>
+        </form>
+      </div>
+    </div>
+  );
+}
+
 // 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,
               <>
                 <div className="fixed inset-0 z-10" onClick={() => setShowActions(false)} />
                 <div className="absolute right-0 top-full mt-1 z-20 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg shadow-xl py-1 min-w-[120px]">
+                <button
+                  className="w-full px-3 py-1.5 text-left text-sm text-white hover:bg-bambu-dark flex items-center gap-2"
+                  onClick={() => { onRename(folder); setShowActions(false); }}
+                >
+                  <Pencil className="w-3.5 h-3.5" />
+                  Rename
+                </button>
                 <button
                   className="w-full px-3 py-1.5 text-left text-sm text-white hover:bg-bambu-dark flex items-center gap-2"
                   onClick={() => { onLink(folder); setShowActions(false); }}
@@ -673,6 +736,7 @@ function FolderTreeItem({ folder, selectedFolderId, onSelect, onDelete, onLink,
               onSelect={onSelect}
               onDelete={onDelete}
               onLink={onLink}
+              onRename={onRename}
               depth={depth + 1}
             />
           ))}
@@ -697,9 +761,10 @@ interface FileCardProps {
   onDownload: (id: number) => void;
   onAddToQueue?: (id: number) => void;
   onPrint?: (file: LibraryFileListItem) => void;
+  onRename?: (file: LibraryFileListItem) => void;
 }
 
-function FileCard({ file, isSelected, onSelect, onDelete, onDownload, onAddToQueue, onPrint }: FileCardProps) {
+function FileCard({ file, isSelected, onSelect, onDelete, onDownload, onAddToQueue, onPrint, onRename }: FileCardProps) {
   const [showActions, setShowActions] = useState(false);
 
   return (
@@ -761,8 +826,8 @@ function FileCard({ file, isSelected, onSelect, onDelete, onDownload, onAddToQue
         )}
       </div>
 
-      {/* Actions */}
-      <div className="absolute bottom-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity" onClick={(e) => e.stopPropagation()}>
+      {/* Actions - always visible on mobile, hover on desktop */}
+      <div className="absolute bottom-2 right-2 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity" onClick={(e) => e.stopPropagation()}>
         <button
           onClick={() => setShowActions(!showActions)}
           className="p-1.5 rounded bg-bambu-dark-secondary/90 hover:bg-bambu-dark-tertiary"
@@ -798,6 +863,15 @@ function FileCard({ file, isSelected, onSelect, onDelete, onDownload, onAddToQue
                 <Download className="w-3.5 h-3.5" />
                 Download
               </button>
+              {onRename && (
+                <button
+                  className="w-full px-3 py-1.5 text-left text-sm text-white hover:bg-bambu-dark flex items-center gap-2"
+                  onClick={() => { onRename(file); setShowActions(false); }}
+                >
+                  <Pencil className="w-3.5 h-3.5" />
+                  Rename
+                </button>
+              )}
               <button
                 className="w-full px-3 py-1.5 text-left text-sm text-red-400 hover:bg-bambu-dark flex items-center gap-2"
                 onClick={() => { onDelete(file.id); setShowActions(false); }}
@@ -810,11 +884,11 @@ function FileCard({ file, isSelected, onSelect, onDelete, onDownload, onAddToQue
         )}
       </div>
 
-      {/* Selection checkbox */}
+      {/* Selection checkbox - always visible on mobile, hover on desktop */}
       <div className={`absolute top-2 left-2 w-5 h-5 rounded border-2 flex items-center justify-center transition-all ${
         isSelected
           ? 'bg-bambu-green border-bambu-green'
-          : 'border-white/30 bg-black/30 opacity-0 group-hover:opacity-100'
+          : 'border-white/30 bg-black/30 opacity-100 md:opacity-0 md:group-hover:opacity-100'
       }`}>
         {isSelected && <div className="w-2 h-2 bg-white rounded-sm" />}
       </div>
@@ -840,6 +914,8 @@ export function FileManagerPage() {
   const [linkFolder, setLinkFolder] = useState<LibraryFolderTree | null>(null);
   const [deleteConfirm, setDeleteConfirm] = useState<{ type: 'file' | 'folder' | 'bulk'; id: number; count?: number } | null>(null);
   const [printFile, setPrintFile] = useState<LibraryFileListItem | null>(null);
+  const [printMultiFile, setPrintMultiFile] = useState<LibraryFileListItem | null>(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 })}
               />
             ))}
           </div>
@@ -1332,14 +1437,24 @@ export function FileManagerPage() {
                     {selectedFiles.length} selected
                   </span>
                   <div className="flex-1" />
-                  {selectedSlicedFiles.length > 0 && (
+                  {selectedSlicedFiles.length === 1 && (
                     <Button
                       variant="primary"
                       size="sm"
+                      onClick={() => setPrintMultiFile(selectedSlicedFiles[0])}
+                    >
+                      <Play className="w-4 h-4 mr-1" />
+                      Print
+                    </Button>
+                  )}
+                  {selectedSlicedFiles.length > 0 && (
+                    <Button
+                      variant={selectedSlicedFiles.length === 1 ? 'secondary' : 'primary'}
+                      size="sm"
                       onClick={() => addToQueueMutation.mutate(selectedSlicedFiles.map(f => f.id))}
                       disabled={addToQueueMutation.isPending}
                     >
-                      <Printer className="w-4 h-4 mr-1" />
+                      <Clock className="w-4 h-4 mr-1" />
                       {addToQueueMutation.isPending ? 'Adding...' : `Add to Queue${selectedSlicedFiles.length < selectedFiles.length ? ` (${selectedSlicedFiles.length})` : ''}`}
                     </Button>
                   )}
@@ -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 })}
                   />
                 ))}
               </div>
@@ -1544,6 +1660,13 @@ export function FileManagerPage() {
                       >
                         <Download className="w-4 h-4" />
                       </button>
+                      <button
+                        onClick={() => setRenameItem({ type: 'file', id: file.id, name: file.filename })}
+                        className="p-1.5 rounded hover:bg-bambu-dark text-bambu-gray hover:text-white transition-colors"
+                        title="Rename"
+                      >
+                        <Pencil className="w-4 h-4" />
+                      </button>
                       <button
                         onClick={() => setDeleteConfirm({ type: 'file', id: file.id })}
                         className="p-1.5 rounded hover:bg-bambu-dark text-bambu-gray hover:text-red-400 transition-colors"
@@ -1634,6 +1757,37 @@ export function FileManagerPage() {
           }}
         />
       )}
+
+      {printMultiFile && (
+        <PrintModal
+          mode="reprint"
+          libraryFileId={printMultiFile.id}
+          archiveName={printMultiFile.print_name || printMultiFile.filename}
+          onClose={() => setPrintMultiFile(null)}
+          onSuccess={() => {
+            setPrintMultiFile(null);
+            setSelectedFiles([]);
+            queryClient.invalidateQueries({ queryKey: ['library-files'] });
+            queryClient.invalidateQueries({ queryKey: ['archives'] });
+          }}
+        />
+      )}
+
+      {renameItem && (
+        <RenameModal
+          type={renameItem.type}
+          currentName={renameItem.name}
+          onClose={() => setRenameItem(null)}
+          onSave={(newName) => {
+            if (renameItem.type === 'file') {
+              renameFileMutation.mutate({ id: renameItem.id, filename: newName });
+            } else {
+              renameFolderMutation.mutate({ id: renameItem.id, name: newName });
+            }
+          }}
+          isLoading={renameFileMutation.isPending || renameFolderMutation.isPending}
+        />
+      )}
     </div>
   );
 }