feat: sortable file table and dropdown folder picker

Replace flat folder button bar with compact dropdown select.
Add sortable columns (path, name, date) to file list table.
Restore soft-deleted files on sync when file still on disk.
Serve text file previews with white background.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 00:49:16 +00:00
parent ff9be1249a
commit 41b974c804
2 changed files with 92 additions and 41 deletions

View File

@@ -91,9 +91,11 @@ async def sync_files(db: AsyncSession):
))
db_records = [dict(r._mapping) for r in result]
# Build lookup sets
# Build lookup maps
all_db_paths = {r["storage_path"] for r in db_records}
active_db_paths = {r["storage_path"] for r in db_records if not r["is_deleted"]}
deleted_on_disk = {r["storage_path"]: r for r in db_records
if r["is_deleted"] and r["storage_path"] in disk_files}
# New on disk, not in DB at all → create record
new_files = disk_files - all_db_paths
@@ -116,6 +118,12 @@ async def sync_files(db: AsyncSession):
})
added += 1
# Soft-deleted in DB but still on disk → restore
for rel_path, record in deleted_on_disk.items():
repo = BaseRepository("files", db)
await repo.restore(record["id"])
added += 1
# Active in DB but missing from disk → soft-delete
missing_files = active_db_paths - disk_files
for record in db_records:
@@ -127,10 +135,21 @@ async def sync_files(db: AsyncSession):
return {"added": added, "removed": removed}
SORT_OPTIONS = {
"path": "storage_path ASC",
"path_desc": "storage_path DESC",
"name": "original_filename ASC",
"name_desc": "original_filename DESC",
"date": "created_at DESC",
"date_asc": "created_at ASC",
}
@router.get("/")
async def list_files(
request: Request,
folder: Optional[str] = None,
sort: Optional[str] = None,
context_type: Optional[str] = None,
context_id: Optional[str] = None,
db: AsyncSession = Depends(get_db),
@@ -142,36 +161,38 @@ async def list_files(
folders = get_folders()
order_by = SORT_OPTIONS.get(sort, "storage_path ASC")
if context_type and context_id:
# Files attached to a specific entity
result = await db.execute(text("""
result = await db.execute(text(f"""
SELECT f.*, fm.context_type, fm.context_id
FROM files f
JOIN file_mappings fm ON fm.file_id = f.id
WHERE f.is_deleted = false AND fm.context_type = :ct AND fm.context_id = :cid
ORDER BY f.created_at DESC
ORDER BY {order_by}
"""), {"ct": context_type, "cid": context_id})
elif folder is not None:
if folder == "":
# Root folder: files with no directory separator in storage_path
result = await db.execute(text("""
result = await db.execute(text(f"""
SELECT * FROM files
WHERE is_deleted = false AND storage_path NOT LIKE '%/%'
ORDER BY created_at DESC
ORDER BY {order_by}
"""))
else:
# Specific folder: storage_path starts with folder/
result = await db.execute(text("""
result = await db.execute(text(f"""
SELECT * FROM files
WHERE is_deleted = false AND storage_path LIKE :prefix
ORDER BY created_at DESC
ORDER BY {order_by}
"""), {"prefix": folder + "/%"})
else:
# All files
result = await db.execute(text("""
result = await db.execute(text(f"""
SELECT * FROM files
WHERE is_deleted = false
ORDER BY created_at DESC
ORDER BY {order_by}
"""))
items = [dict(r._mapping) for r in result]
@@ -184,6 +205,7 @@ async def list_files(
return templates.TemplateResponse("files.html", {
"request": request, "sidebar": sidebar, "items": items,
"folders": folders, "current_folder": folder,
"current_sort": sort or "path",
"sync_result": sync_result,
"context_type": context_type or "",
"context_id": context_id or "",