diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 9e3469b..53ba168 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -992,16 +992,17 @@ async def local_workspace_tree(request: Request, folder: int = None): ws_id = ws["id"] if ws else None if not ws_id: return {"tree": [], "breadcrumb": []} + uid = _get_user_id(request) with get_conn() as conn: if folder: # Show only this folder's children + build breadcrumb - children = _build_tree_children(conn, folder, ws_id) + children = _build_tree_children(conn, folder, ws_id, uid) 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) + roots = _build_tree_children(conn, None, ws_id, uid) return {"tree": roots, "breadcrumb": [], "current_folder": None} @@ -1028,11 +1029,11 @@ async def local_workspace_breadcrumb(request: Request, folder: int): return {"breadcrumb": breadcrumb} -def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list: +def _build_tree_children(conn, parent_id: int | None, ws_id: int, uid: int | None = None) -> list: """Recursively build the tree of children for a node.""" if parent_id is None: rows = conn.execute( - "SELECT id, title, parent_section, content_format, content, " + "SELECT id, title, parent_section, content_format, content, page_icon, " "(is_shared OR share_mode != 'private' OR COALESCE(published,0)) as is_shared, " "created_at, updated_at FROM pages " "WHERE parent_id IS NULL AND workspace_id=? AND deleted_at IS NULL " @@ -1041,7 +1042,7 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list: ).fetchall() else: rows = conn.execute( - "SELECT id, title, parent_section, content_format, content, " + "SELECT id, title, parent_section, content_format, content, page_icon, " "(is_shared OR share_mode != 'private' OR COALESCE(published,0)) as is_shared, " "created_at, updated_at FROM pages " "WHERE parent_id=? AND workspace_id=? AND deleted_at IS NULL " @@ -1059,6 +1060,7 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list: # Collect all page IDs to fetch tags in one query all_ids = [r["id"] for r in rows] tags_map = {} + favorited_ids = set() if all_ids: placeholders = ",".join("?" for _ in all_ids) tag_rows = conn.execute( @@ -1070,11 +1072,17 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list: tags_map.setdefault(tr["page_id"], []).append({ "id": tr["id"], "name": tr["name"], "color": tr["color"], }) + if uid is not None: + fav_rows = conn.execute( + f"SELECT page_id FROM favorites WHERE user_id=? AND page_id IN ({placeholders})", + [uid, *all_ids], + ).fetchall() + favorited_ids = {fr["page_id"] for fr in fav_rows} tree = [] for r in rows: is_folder = r["parent_section"] == "Workspace" - children = _build_tree_children(conn, r["id"], ws_id) + children = _build_tree_children(conn, r["id"], ws_id, uid) # Compute size size = 0 @@ -1094,6 +1102,7 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list: "type": "folder" if is_folder else "page", "is_folder": is_folder, "content_format": r["content_format"] if not is_folder else None, + "page_icon": r["page_icon"] or "", "children": children, "has_children": len(children) > 0, "child_count": len(children), @@ -1104,6 +1113,7 @@ def _build_tree_children(conn, parent_id: int | None, ws_id: int) -> list: "author": author, "tags": tags_map.get(r["id"], []), "is_shared": bool(r["is_shared"]), + "favorited": r["id"] in favorited_ids, }) return tree diff --git a/app/templates/_ctx_menu.html b/app/templates/_ctx_menu.html index c8692c1..d5fe7a9 100644 --- a/app/templates/_ctx_menu.html +++ b/app/templates/_ctx_menu.html @@ -1,5 +1,5 @@ {############################################################################### - _ctx_menu.html — LE menu contextuel UNIQUE de FlowDeck (v5.4.x) + _ctx_menu.html — LE menu contextuel UNIQUE de FlowDeck ═══════════════════════════════════════════════════════════════════════════ Inclus par library.html ET local_workspace.html (les 2 seules pages du contexte fichier). Ce partial est LE SEUL menu : son markup décrit l'UNION @@ -7,21 +7,13 @@ chaque page au travers d'un store Alpine enregistré PLUS BAS (IIFE + défense d'exécution unique → include 2 fois = store 1 fois). - Union des items (identiques sur les 2 pages) : - 1. Open .................... library: openItem / ws: ctxMenuOpenPage - 2. Open in side peek ....... library: openPeek (ws: item caché) - 3. Open folder ............. ws: ctxMenuOpenFolder (dossier uniquement) - 4. Rename .................. library: startRename / ws: ctxMenuRename - 5. Add/Remove Favorites .... library: toggleFavoriteItem (ws: caché) - 6. Duplicate ............... library: duplicateItem / ws: ctxMenuDuplicate - 7. Copy link ............... library: copyLink (ws: caché) - 8. Move to ................. library: openMovePicker (ws: caché) - 9. Tags (section dynamique) ws: ctxTagExisting/… (s'affiche si node.tags) - 10. Delete .................. library: deleteItem / ws: ctxMenuDelete (danger) + Chaque item se masque quand le handler correspondant n'existe pas sur la + page (x-show="$store.fdCtx.has('key')") → menu structurellement identique, + avec la capacité respective de la page au clic. - Chaque item masque ses entrées quand `handlers` de l'autre page est vide - (x-show="fdCtx.has('key')") → le menu est STRUCTURELLEMENT identique des - deux côtés, avec la capacité respective au clic. + Handlers attendus (tous optionnels) : + open, openTab, peek, folder, rename, setIcon, duplicate, link, move, + fav, recent, delete, tagExisting, tagAdd, tagRemove ############################################################################} -