From 8c0b55635c04c61f197381ea0bd24fcbf55c6519 Mon Sep 17 00:00:00 2001 From: bruno Date: Thu, 16 Jul 2026 10:28:17 -0400 Subject: [PATCH] feat: soft-delete and undo/restore for local-workspace items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - DELETE /api/local-workspace/items/{id} now soft-deletes (sets deleted_at instead of hard DELETE) — items go to trash, not permanently gone - New POST /api/local-workspace/items/{id}/restore to undo a delete - _load_workspace_pages() and _load_children() now filter deleted_at IS NULL so soft-deleted items disappear from sidebar and tree queries Frontend: - undoDelete() now calls the restore API per item (single or bulk) - bulkDelete() now shows the same undo banner as context menu delete - All delete paths (ctxMenu, bulk, modal) use the same soft-delete + undo flow --- app/routers/board.py | 4 ++-- app/routers/dashboard.py | 32 +++++++++++++++++------------- app/templates/local_workspace.html | 29 +++++++++++++++++++++++---- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/app/routers/board.py b/app/routers/board.py index 0f97179..3e83135 100644 --- a/app/routers/board.py +++ b/app/routers/board.py @@ -172,7 +172,7 @@ def _load_workspace_pages(ws_cookie: str) -> list: ws_id = int(ws_cookie) with get_conn() as conn: rows = conn.execute( - "SELECT id, title, parent_section, content_format FROM pages WHERE workspace_id=? AND parent_id IS NULL ORDER BY created_at DESC", + "SELECT id, title, parent_section, content_format FROM pages WHERE workspace_id=? AND parent_id IS NULL AND deleted_at IS NULL ORDER BY created_at DESC", (ws_id,), ).fetchall() items = [] @@ -197,7 +197,7 @@ def _load_children(parent_id: int) -> list: """Recursively load children of a page.""" with get_conn() as conn: rows = conn.execute( - "SELECT id, title, parent_section, content_format FROM pages WHERE parent_id=? ORDER BY created_at", + "SELECT id, title, parent_section, content_format FROM pages WHERE parent_id=? AND deleted_at IS NULL ORDER BY created_at", (parent_id,), ).fetchall() children = [] diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index c0d0a6c..0214ee3 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -837,21 +837,25 @@ 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). Also removes uploaded file from disk.""" - import json - from pathlib import Path + """Soft-delete a file/folder (sets deleted_at).""" + from datetime import datetime 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.execute( + "UPDATE pages SET deleted_at=? WHERE id=? AND deleted_at IS NULL", + (datetime.utcnow().isoformat(), item_id), + ) + conn.commit() + return {"status": "ok"} + + +@router.post("/api/local-workspace/items/{item_id:int}/restore") +async def restore_local_workspace_item(request: Request, item_id: int): + """Restore a soft-deleted file/folder.""" + with get_conn() as conn: + conn.execute( + "UPDATE pages SET deleted_at=NULL WHERE id=?", + (item_id,), + ) conn.commit() return {"status": "ok"} diff --git a/app/templates/local_workspace.html b/app/templates/local_workspace.html index 36f04e5..f8bd1d8 100644 --- a/app/templates/local_workspace.html +++ b/app/templates/local_workspace.html @@ -567,11 +567,20 @@ window._wsInitData = (function() { if (ids.length === 0) return; if (!confirm('Delete ' + ids.length + ' selected items?')) return; var self = this; + // Soft-delete all selected items for (var i=0; i= total) { + self._reloadAfterAction(); + return; + } + fetch('/api/local-workspace/items/' + ids[i] + '/restore', { method: 'POST' }) + .then(function(r) { if (r.ok) restored++; }) + .finally(function() { restoreOne(i + 1); }); + } + restoreOne(0); }, _removeFromTree(nodes, id) { for (var i=0; i