From 1ab5b1271fe4fc93e1b5006abeb403b542e67ebe Mon Sep 17 00:00:00 2001 From: bruno Date: Fri, 10 Jul 2026 21:22:11 -0400 Subject: [PATCH] feat: recursive tree, drag & drop, sub-folders in sidebar workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _load_workspace_pages now recursively loads children via _load_children() - Sidebar: recursive Jinja2 macro render_workspace_tree() - Drag & drop: HTML5 DnD — drag files into folders or reorder - move API: PUT /api/local-workspace/items/{id}/move - Folder hover shows 📄 (new file) + 📁 (sub-folder) buttons - newPageInFolder/newFolderInFolder with parent_id for nesting - Visual: dragging opacity + drop target highlight - Items indented by depth (16px per level) --- app/routers/board.py | 33 ++++++++++++-- app/routers/dashboard.py | 15 +++++++ app/templates/base.html | 95 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 132 insertions(+), 11 deletions(-) diff --git a/app/routers/board.py b/app/routers/board.py index db894b2..f12f303 100644 --- a/app/routers/board.py +++ b/app/routers/board.py @@ -141,7 +141,7 @@ def _build_page_tree(conn, parent_id: int | None, ws_key: str, depth: int = 0, m def _load_workspace_pages(ws_cookie: str) -> list: - """Load top-level pages for the active workspace.""" + """Load top-level pages with children for the active workspace.""" if not ws_cookie: return [] try: @@ -151,12 +151,39 @@ def _load_workspace_pages(ws_cookie: str) -> list: "SELECT id, title, parent_section FROM pages WHERE workspace_id=? AND parent_id IS NULL ORDER BY created_at DESC", (ws_id,), ).fetchall() - return [{"db_id": r["id"], "name": r["title"] or "Untitled", "id": f"page/{r['id']}", - "icon": "📁" if r["parent_section"] == "Workspace" else "📄"} for r in rows] + items = [] + for r in rows: + items.append({ + "db_id": r["id"], "name": r["title"] or "Untitled", + "id": f"page/{r['id']}", + "icon": "📁" if r["parent_section"] == "Workspace" else "📄", + "is_folder": r["parent_section"] == "Workspace", + "children": _load_children(r["id"]), + }) + return items except (ValueError, Exception): return [] +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 FROM pages WHERE parent_id=? ORDER BY created_at", + (parent_id,), + ).fetchall() + children = [] + for r in rows: + children.append({ + "db_id": r["id"], "name": r["title"] or "Untitled", + "id": f"page/{r['id']}", + "icon": "📁" if r["parent_section"] == "Workspace" else "📄", + "is_folder": r["parent_section"] == "Workspace", + "children": _load_children(r["id"]), + }) + return children + + def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict: user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) ws_name = user.get("login", "Bruno") if user else "Bruno" diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 4805a50..585b77e 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -581,6 +581,21 @@ async def delete_local_workspace_item(request: Request, item_id: int): return {"status": "ok"} +@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).""" + import json + body = await request.json() + new_parent_id = body.get("parent_id") # None = move to root + with get_conn() as conn: + conn.execute( + "UPDATE pages SET parent_id=? WHERE id=?", + (new_parent_id, item_id), + ) + conn.commit() + return {"status": "ok"} + + # ═══════════ Workspaces CRUD ═══════════ WORKSPACE_COOKIE = "flowdeck_workspace" diff --git a/app/templates/base.html b/app/templates/base.html index 457ea6e..fd575a0 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -92,16 +92,38 @@ +{% macro render_workspace_tree(pages, depth=0) %} + {% for page in pages %} + + {% if page.children %} + {{ render_workspace_tree(page.children, depth + 1) }} + {% endif %} + {% endfor %} +{% endmacro %}