feat: arborescence interactive avec expand/collapse, breadcrumb et navigation par dossier
- 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
This commit is contained in:
+95
-24
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user