diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 830e987..9372f35 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -491,8 +491,11 @@ async def remove_member(request: Request, ws_id: int, user_id: int): # ═══════════ Local Workspace (file/folder CRUD) ═══════════ @router.get("/local-workspace", response_class=HTMLResponse) -async def local_workspace_page(request: Request): - """Local workspace page with file/folder tree.""" +async def local_workspace_page(request: Request, folder: int = None): + """Local workspace page with file/folder tree. + + If ?folder=ID is provided, shows that folder's contents with breadcrumb. + """ from jinja2 import Environment, FileSystemLoader env = Environment(loader=FileSystemLoader("app/templates")) sidebar = _sidebar_data(request, []) @@ -502,40 +505,108 @@ async def local_workspace_page(request: Request): ws = _get_active_workspace(request) ws_id = ws["id"] if ws else None - with get_conn() as conn: - rows = conn.execute( - "SELECT id, title FROM pages WHERE parent_id IS NULL AND workspace_id=? ORDER BY created_at DESC", - (ws_id,), - ).fetchall() if ws_id else [] - items = [{"id": r["id"], "name": r["title"] or "Untitled", "type": "page"} for r in rows] - ctx = {**sidebar, "user": user, "workspace_name": sidebar.get("active_ws_name", "My Workspace"), "items": items} + # Build breadcrumb if navigating into a folder + breadcrumb = [] + current_folder_id = folder + if folder and ws_id: + with get_conn() as conn: + breadcrumb = _build_breadcrumb(conn, folder) + + ctx = { + **sidebar, + "user": user, + "workspace_name": sidebar.get("active_ws_name", "My Workspace"), + "current_folder_id": current_folder_id or 0, + "breadcrumb": breadcrumb, + } template = env.get_template("local_workspace.html") return template.render(**ctx) @router.get("/api/local-workspace/tree") -async def local_workspace_tree(request: Request): - """Return the file/folder tree filtered by active workspace.""" +async def local_workspace_tree(request: Request, folder: int = None): + """Return the file/folder tree filtered by active workspace. + + If ?folder=ID is provided, returns only that folder's children. + Otherwise returns the full recursive tree from root. + """ ws = _get_active_workspace(request) ws_id = ws["id"] if ws else None + if not ws_id: + return {"tree": [], "breadcrumb": []} + with get_conn() as conn: - roots = conn.execute( - "SELECT id, title, parent_section FROM pages WHERE parent_id IS NULL AND workspace_id=? ORDER BY created_at DESC", + if folder: + # Show only this folder's children + build breadcrumb + children = _build_tree_children(conn, folder, ws_id) + breadcrumb = _build_breadcrumb(conn, folder) + return {"tree": children, "breadcrumb": breadcrumb, "current_folder": folder} + else: + # Full tree from root + roots = _build_tree_children(conn, None, ws_id) + return {"tree": roots, "breadcrumb": [], "current_folder": None} + + +@router.get("/api/local-workspace/breadcrumb") +async def local_workspace_breadcrumb(request: Request, folder: int): + """Return breadcrumb trail for a folder.""" + with get_conn() as conn: + breadcrumb = _build_breadcrumb(conn, folder) + return {"breadcrumb": breadcrumb} + + +def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list: + """Recursively build the tree of children for a node.""" + if parent_id is None: + rows = conn.execute( + "SELECT id, title, parent_section FROM pages " + "WHERE parent_id IS NULL AND workspace_id=? ORDER BY created_at DESC", (ws_id,), - ).fetchall() if ws_id else [] + ).fetchall() + else: + rows = conn.execute( + "SELECT id, title, parent_section FROM pages " + "WHERE parent_id=? AND workspace_id=? ORDER BY created_at DESC", + (parent_id, ws_id), + ).fetchall() + tree = [] - for r in roots: - with get_conn() as conn: - children = conn.execute( - "SELECT id, title, parent_section FROM pages WHERE parent_id=? ORDER BY created_at", (r["id"],) - ).fetchall() + for r in rows: is_folder = r["parent_section"] == "Workspace" - rtype = "folder" if is_folder else "page" - item = {"id": r["id"], "name": r["title"] or "Untitled", "type": rtype, - "children": [{"id": c["id"], "name": c["title"] or "Untitled", "type": "page"} for c in children]} - tree.append(item) - return {"tree": tree} + children = _build_tree_children(conn, r["id"], ws_id) + tree.append({ + "id": r["id"], + "name": r["title"] or "Untitled", + "type": "folder" if is_folder else "page", + "is_folder": is_folder, + "children": children, + "has_children": len(children) > 0, + }) + return tree + + +def _build_breadcrumb(conn, folder_id: int) -> list: + """Build breadcrumb trail from root to folder_id.""" + breadcrumb = [] + current = folder_id + seen = set() + while current and current not in seen: + seen.add(current) + row = conn.execute( + "SELECT id, title, parent_id, parent_section FROM pages WHERE id=?", + (current,), + ).fetchone() + if row: + breadcrumb.insert(0, { + "id": row["id"], + "name": row["title"] or "Untitled", + "is_folder": row["parent_section"] == "Workspace", + }) + current = row["parent_id"] + else: + break + return breadcrumb @router.post("/api/local-workspace/items") diff --git a/app/templates/base.html b/app/templates/base.html index 94d9012..9796ff4 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -110,15 +110,30 @@