feat: drag-and-drop interne, file viewer multi-type, sidebar persistante
1. Drag & drop interne: déplacer fichiers/dossiers existants vers d'autres
dossiers ou racine via l'API /api/local-workspace/items/{id}/move.
Items rendus draggable dans le template x-for et renderChildren.
2. File viewer pour fichiers uploadés: détection content_format='file',
visualiseur intégré avec rendu adapté au type MIME:
- Images: affichage direct
- PDF: iframe
- Texte/Code (.py .js .md .html .ps1 etc.): <pre> avec fetch du contenu
- Autres: lien de téléchargement
Route GET /api/files/{ws_id}/{filename:path} pour servir les fichiers.
3. Sidebar persistante: expandedFolders sauvegardé dans localStorage,
restauré au chargement, préservé avant navigation vers un dossier.
4. Suppression: nettoie aussi le fichier disque pour content_format='file'
This commit is contained in:
@@ -182,15 +182,20 @@ async def library_page(request: Request, owner: str = Query(default=""), repo: s
|
||||
|
||||
@router.get("/pages/{page_id}", response_class=HTMLResponse)
|
||||
async def view_page_root(request: Request, page_id: int):
|
||||
"""Render a Markdown page at root level with workspace context."""
|
||||
"""Render a Markdown page at root level with workspace context — or file viewer."""
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from app.routers.board import _sidebar_data as board_sidebar
|
||||
from app.routers.board import _render_file_viewer
|
||||
env = Environment(loader=FileSystemLoader("app/templates"))
|
||||
with get_conn() as conn:
|
||||
row = conn.execute("SELECT * FROM pages WHERE id=?", (page_id,)).fetchone()
|
||||
if not row:
|
||||
return HTMLResponse("<h2>Page not found</h2>", status_code=404)
|
||||
page = dict(row)
|
||||
|
||||
# ── File viewer for uploaded files ──
|
||||
if page.get("content_format") == "file":
|
||||
return _render_file_viewer(page, request)
|
||||
ws = page.get("workspace", "")
|
||||
parts = ws.split("/") if "/" in ws else ["", ""]
|
||||
owner, repo = parts[0], parts[1] if len(parts) > 1 else ""
|
||||
@@ -645,13 +650,41 @@ async def rename_local_workspace_item(request: Request, item_id: int):
|
||||
|
||||
@router.delete("/api/local-workspace/items/{item_id:int}")
|
||||
async def delete_local_workspace_item(request: Request, item_id: int):
|
||||
"""Delete a file (and its children)."""
|
||||
"""Delete a file (and its children). Also removes uploaded file from disk."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
with get_conn() as conn:
|
||||
# Check if it's a file-type page with a disk file
|
||||
row = conn.execute("SELECT content, content_format FROM pages WHERE id=?", (item_id,)).fetchone()
|
||||
if row and row["content_format"] == "file":
|
||||
try:
|
||||
meta = json.loads(row["content"])
|
||||
fp = Path(f"/data/{meta['file_path']}")
|
||||
if fp.exists():
|
||||
fp.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
conn.execute("DELETE FROM pages WHERE id=? OR parent_id=?", (item_id, item_id))
|
||||
conn.commit()
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/api/files/{ws_id:int}/{filename:path}")
|
||||
async def serve_uploaded_file(ws_id: int, filename: str):
|
||||
"""Serve an uploaded file from disk."""
|
||||
from pathlib import Path
|
||||
import mimetypes
|
||||
fp = Path(f"/data/uploads/workspace_{ws_id}/{filename}").resolve()
|
||||
if not str(fp).startswith(f"/data/uploads/workspace_{ws_id}/"):
|
||||
return JSONResponse({"error": "Path traversal denied"}, status_code=403)
|
||||
if not fp.exists():
|
||||
return JSONResponse({"error": "File not found"}, status_code=404)
|
||||
mime, _ = mimetypes.guess_type(str(fp))
|
||||
content = fp.read_bytes()
|
||||
from fastapi.responses import Response
|
||||
return Response(content=content, media_type=mime or "application/octet-stream")
|
||||
|
||||
|
||||
@router.put("/api/local-workspace/items/{item_id:int}/move")
|
||||
async def move_local_workspace_item(request: Request, item_id: int):
|
||||
"""Move an item to a new parent (drag & drop)."""
|
||||
|
||||
Reference in New Issue
Block a user