feat: drag & drop de fichiers dans l'arborescence (portion 5)
- Backend: nouvel endpoint POST /api/move/{vault_name} pour déplacer
fichiers et dossiers vers un autre répertoire parent
- Frontend: module dragdrop.js avec délégation d'événements sur le
vault-tree (dragstart, dragover, dragleave, drop)
- CSS: styles pour drag-source, drag-valid-target, drag-over
- Protections: pas de drop sur soi-même, pas dans un enfant,
pas de cross-vault
- Mise à jour de l'index et broadcast SSE après move
- ROADMAP: portion 5 marquée comme complétée
This commit is contained in:
+127
@@ -319,9 +319,24 @@ class FileRenameResponse(BaseModel):
|
||||
new_path: str
|
||||
|
||||
|
||||
class FileMoveRequest(BaseModel):
|
||||
"""Request to move a file or directory to a different parent directory."""
|
||||
source_path: str = Field(description="Current relative path of the file/directory")
|
||||
destination_dir: str = Field(description="Target directory relative path (empty string for vault root)")
|
||||
|
||||
|
||||
class FileMoveResponse(BaseModel):
|
||||
"""Response after moving a file or directory."""
|
||||
success: bool = Field(description="Whether move succeeded")
|
||||
old_path: str = Field(description="Original path")
|
||||
new_path: str = Field(description="New path after move")
|
||||
item_type: str = Field(description="Type of item moved: 'file' or 'directory'")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SSE Manager — Server-Sent Events for real-time notifications
|
||||
# ---------------------------------------------------------------------------
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class SSEManager:
|
||||
"""Manages SSE client connections and broadcasts events."""
|
||||
@@ -1748,6 +1763,118 @@ async def api_file_rename(
|
||||
raise HTTPException(status_code=500, detail=f"Error renaming file: {str(e)}")
|
||||
|
||||
|
||||
@app.post("/api/move/{vault_name}", response_model=FileMoveResponse)
|
||||
async def api_file_move(
|
||||
vault_name: str,
|
||||
body: FileMoveRequest,
|
||||
current_user=Depends(require_auth),
|
||||
):
|
||||
"""Move a file or directory to a different parent directory within the same vault.
|
||||
|
||||
Supports both files and directories. The item keeps its original name;
|
||||
only the parent directory changes.
|
||||
|
||||
Args:
|
||||
vault_name: Name of the vault.
|
||||
body: Request body with source_path and destination_dir.
|
||||
|
||||
Returns:
|
||||
FileMoveResponse with old and new paths.
|
||||
"""
|
||||
if not check_vault_access(vault_name, current_user):
|
||||
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
|
||||
|
||||
vault_data = get_vault_data(vault_name)
|
||||
if not vault_data:
|
||||
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
|
||||
|
||||
vault_root = Path(vault_data["path"])
|
||||
|
||||
# Check if vault is writable
|
||||
if not _check_vault_writable(vault_root):
|
||||
raise HTTPException(status_code=403, detail="Vault is read-only")
|
||||
|
||||
# Resolve source path
|
||||
source = _resolve_safe_path(vault_root, body.source_path)
|
||||
|
||||
if not source.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Source not found: {body.source_path}")
|
||||
|
||||
is_directory = source.is_dir()
|
||||
|
||||
# Determine the item name (last segment)
|
||||
item_name = source.name
|
||||
|
||||
# Construct destination path: destination_dir / item_name
|
||||
dest_dir_clean = body.destination_dir.strip("/")
|
||||
if dest_dir_clean:
|
||||
dest_parent = _resolve_safe_path(vault_root, dest_dir_clean)
|
||||
if not dest_parent.exists() or not dest_parent.is_dir():
|
||||
raise HTTPException(status_code=404, detail=f"Destination directory not found: {body.destination_dir}")
|
||||
else:
|
||||
dest_parent = vault_root
|
||||
|
||||
destination = dest_parent / item_name
|
||||
|
||||
# Validate destination is within vault
|
||||
destination = _resolve_safe_path(vault_root, str(destination.relative_to(vault_root)))
|
||||
|
||||
# Don't move to the same location
|
||||
if source.resolve() == destination.resolve():
|
||||
return {
|
||||
"success": True,
|
||||
"old_path": body.source_path,
|
||||
"new_path": body.source_path,
|
||||
"item_type": "directory" if is_directory else "file",
|
||||
}
|
||||
|
||||
# Check if destination already exists
|
||||
if destination.exists():
|
||||
raise HTTPException(status_code=409, detail=f"A file or directory already exists at the destination: {destination.name}")
|
||||
|
||||
# For files, validate extension
|
||||
if not is_directory:
|
||||
ext = destination.suffix.lower()
|
||||
if ext not in SUPPORTED_EXTENSIONS and destination.name.lower() not in ("dockerfile", "makefile"):
|
||||
raise HTTPException(status_code=400, detail=f"Unsupported file extension: {ext}")
|
||||
|
||||
try:
|
||||
# Move the file/directory
|
||||
source.rename(destination)
|
||||
|
||||
old_path_str = str(source.relative_to(vault_root)).replace("\\", "/")
|
||||
# After rename, source is now at destination — reconstruct the new relative path
|
||||
new_path_str = body.destination_dir.strip("/")
|
||||
new_path_str = (new_path_str + "/" if new_path_str else "") + item_name
|
||||
|
||||
item_type = "directory" if is_directory else "file"
|
||||
|
||||
logger.info(f"Item moved: {vault_name}/{old_path_str} -> {new_path_str}")
|
||||
|
||||
# Update index
|
||||
if is_directory:
|
||||
from backend.indexer import reload_single_vault
|
||||
await reload_single_vault(vault_name)
|
||||
else:
|
||||
await handle_file_move(vault_name, old_path_str, new_path_str)
|
||||
|
||||
# Broadcast SSE event
|
||||
await sse_manager.broadcast("item_moved", {
|
||||
"vault": vault_name,
|
||||
"old_path": old_path_str,
|
||||
"new_path": new_path_str,
|
||||
"item_type": item_type,
|
||||
})
|
||||
await dispatch_webhooks("item_moved", {"vault": vault_name, "old_path": old_path_str, "new_path": new_path_str, "item_type": item_type})
|
||||
|
||||
return {"success": True, "old_path": old_path_str, "new_path": new_path_str, "item_type": item_type}
|
||||
except PermissionError:
|
||||
raise HTTPException(status_code=403, detail="Permission denied: cannot move item")
|
||||
except Exception as e:
|
||||
logger.error(f"Error moving item {vault_name}/{body.source_path}: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error moving item: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/api/file/{vault_name}/backlinks")
|
||||
async def api_file_backlinks(
|
||||
vault_name: str,
|
||||
|
||||
Reference in New Issue
Block a user