From e9a419b83faa43290cbc87b8083c92ebc30df223 Mon Sep 17 00:00:00 2001 From: bruno Date: Sat, 11 Jul 2026 08:24:57 -0400 Subject: [PATCH] feat: arborescence interactive avec expand/collapse, breadcrumb et navigation par dossier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API /api/local-workspace/tree: arbre récursif complet (support optionnel ?folder=ID) - API /api/local-workspace/breadcrumb: fil d'Ariane parent pour un dossier - Page /local-workspace: supporte ?folder=ID pour naviguer dans un dossier - Sidebar: expand/collapse (▶/▼) sur les dossiers, clic dossier → /local-workspace?folder=ID - CSS: styles chevron .tree-chevron, .tree-folder-link - Alpine.js: expandedFolders, toggleTreeFolder, navigateToFolder --- app/routers/dashboard.py | 119 +++++++++++++---- app/templates/base.html | 35 ++++- app/templates/local_workspace.html | 197 +++++++++++++++++++++++------ static/css/app.css | 45 ++++++- 4 files changed, 331 insertions(+), 65 deletions(-) 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 @@ {% if page.children %} - {{ render_workspace_tree(page.children, depth + 1) }} +
  • + +
  • {% endif %} {% endfor %} -{% endmacro %} +{% endmacro %}