feat: soft-delete and undo/restore for local-workspace items
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

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
This commit is contained in:
2026-07-16 10:28:17 -04:00
parent 631c106b01
commit 8c0b55635c
3 changed files with 45 additions and 20 deletions
+2 -2
View File
@@ -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 = []
+18 -14
View File
@@ -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"}