- 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
This commit is contained in:
maziggy
2026-01-20 17:39:07 +01:00
parent 19868b155b
commit d32059ab78
8 changed files with 124 additions and 72 deletions
+12
View File
@@ -58,6 +58,18 @@ All notable changes to Bambuddy will be documented in this file.
- "Embedded": Shows camera as a floating overlay on the main screen
- Embedded viewer is draggable and resizable with persistent position/size
- Configure in Settings → General → Camera section
- **File Manager Rename** - Rename files and folders directly in File Manager:
- Right-click context menu "Rename" option for files and folders
- Inline rename button in list view
- Validates filenames (no path separators allowed)
- **File Manager Print Button** - Print directly from multi-selection toolbar:
- "Print" button appears when exactly one sliced file is selected
- Opens full PrintModal with plate selection and print options
- "Add to Queue" button now uses Clock icon for clarity
- **File Manager Mobile Accessibility** - Improved touch device support:
- Three-dot menu button always visible on mobile (hover-only on desktop)
- Selection checkbox always visible on mobile devices
- Better PWA experience for file management
### Changed
- **Edit Queue Item modal** - Single printer selection only (reassigns item, doesn't duplicate)
+2
View File
@@ -82,10 +82,12 @@
### 📁 File Manager (Library)
- Upload and organize sliced files (3MF, gcode)
- Folder structure with drag-and-drop
- Rename files and folders via context menu
- Print directly to any printer with full options
- Add to queue without creating archive upfront
- Plate selection for multi-plate 3MF files
- Duplicate detection via file hash
- Mobile-friendly with always-visible action buttons
### 📁 Projects
- Group related prints (e.g., "Voron Build")
+6
View File
@@ -1406,6 +1406,12 @@ async def update_file(file_id: int, data: FileUpdate, db: AsyncSession = Depends
if not file:
raise HTTPException(status_code=404, detail="File not found")
if data.filename is not None:
# Validate filename doesn't contain path separators
if "/" in data.filename or "\\" in data.filename:
raise HTTPException(status_code=400, detail="Filename cannot contain path separators")
file.filename = data.filename
if data.folder_id is not None:
if data.folder_id == 0:
file.folder_id = None
+1
View File
@@ -80,6 +80,7 @@ class FileCreate(BaseModel):
class FileUpdate(BaseModel):
"""Schema for updating a file."""
filename: str | None = Field(None, min_length=1, max_length=255)
folder_id: int | None = None
project_id: int | None = None
notes: str | None = None
@@ -204,6 +204,37 @@ class TestLibraryFilesAPI:
result = response.json()
assert result.get("message") or result.get("success", True)
@pytest.mark.asyncio
@pytest.mark.integration
async def test_rename_file(self, async_client: AsyncClient, file_factory, db_session):
"""Verify file can be renamed."""
lib_file = await file_factory(filename="old_name.3mf")
data = {"filename": "new_name.3mf"}
response = await async_client.put(f"/api/v1/library/files/{lib_file.id}", json=data)
assert response.status_code == 200
result = response.json()
assert result["filename"] == "new_name.3mf"
@pytest.mark.asyncio
@pytest.mark.integration
async def test_rename_file_invalid_path_separator(self, async_client: AsyncClient, file_factory, db_session):
"""Verify file rename fails with path separators."""
lib_file = await file_factory(filename="test.3mf")
data = {"filename": "path/to/file.3mf"}
response = await async_client.put(f"/api/v1/library/files/{lib_file.id}", json=data)
assert response.status_code == 400
assert "path separator" in response.json()["detail"].lower()
@pytest.mark.asyncio
@pytest.mark.integration
async def test_rename_file_invalid_backslash(self, async_client: AsyncClient, file_factory, db_session):
"""Verify file rename fails with backslash."""
lib_file = await file_factory(filename="test.3mf")
data = {"filename": "path\\to\\file.3mf"}
response = await async_client.put(f"/api/v1/library/files/{lib_file.id}", json=data)
assert response.status_code == 400
assert "path separator" in response.json()["detail"].lower()
@pytest.mark.asyncio
@pytest.mark.integration
async def test_library_stats(self, async_client: AsyncClient, folder_factory, file_factory, db_session):
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -23,8 +23,8 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-Cw4dIiLT.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C__bvgqz.css">
<script type="module" crossorigin src="/assets/index-BL-2WI3B.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BmODu1qm.css">
</head>
<body>
<div id="root"></div>