From c36082be6ad280bb938063696afa457729881cd1 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 11 Sep 2026 22:00:54 -0400 Subject: [PATCH] fix(ctxmenu): repair library rendering and complete unified row menu - library.html: remove leftover syntax error + orphan that broke libraryPage() (page showed 'undefined' and no files), call the correct fdCtx.openMenu and use self instead of a throwaway libraryPage() instance - _ctx_menu.html: full conventional menu with shortcuts, tags + colour picker, edit-icon picker; closes on outside click and Escape - local_workspace.html: migrate onContextMenu to the shared fdCtx store with complete handlers; drop legacy window._ctxMenuData DOM hacks - dashboard.py: expose page_icon + favorited in the workspace tree --- app/routers/dashboard.py | 22 ++- app/templates/_ctx_menu.html | 243 ++++++++++++++++++-------- app/templates/library.html | 79 ++++++--- app/templates/local_workspace.html | 268 +++++++++++++---------------- 4 files changed, 359 insertions(+), 253 deletions(-) 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 ############################################################################} -
+.fd-ctx-menu{position:fixed !important;top:0;left:0;min-width:230px;max-width:280px;max-height:calc(100vh - 16px);overflow-y:auto;z-index:2000;padding:4px;} +.fd-ctx-menu .menu-item{display:flex;align-items:center;gap:8px;padding:6px 10px;font-size:13px;color:var(--text-primary);cursor:pointer;border-radius:4px;transition:background .1s;white-space:nowrap;} +.fd-ctx-menu .menu-item:hover{background:var(--bg-hover);} +.fd-ctx-menu .menu-item svg{width:14px;height:14px;flex-shrink:0;} +.fd-ctx-menu .menu-danger{color:var(--danger);} +.fd-ctx-menu .menu-shortcut{margin-left:auto;font-size:11px;color:var(--text-dim);padding-left:16px;} +.fd-ctx-menu .more-sep{height:1px;background:var(--border);margin:4px 6px;} +.fd-ctx-menu .ctx-label{color:var(--text-dim);font-size:11px;cursor:default;text-transform:uppercase;letter-spacing:.04em;} +.fd-ctx-menu .ctx-label:hover{background:transparent;} +.fd-ctx-menu .ctx-empty{color:var(--text-dim);font-size:11px;cursor:default;} +.fd-ctx-menu .ctx-empty:hover{background:transparent;} +.fd-ctx-menu .ctx-caret{font-size:10px;color:var(--text-dim);} +.fd-ctx-menu .ctx-x{color:var(--text-dim);font-size:14px;line-height:1;padding:0 2px;} +.fd-ctx-menu .ctx-x:hover{color:var(--danger);} +.fd-ctx-menu .ctx-sub{padding:2px 0 4px;background:var(--bg-tertiary);border-radius:6px;margin:2px 6px;} +.fd-ctx-menu .ctx-tag-color{width:10px;height:10px;border-radius:3px;display:inline-block;flex-shrink:0;} +.fd-ctx-menu .ctx-colors{display:grid;grid-template-columns:repeat(7,1fr);gap:4px;padding:2px 4px 6px;} +.fd-ctx-menu .ctx-swatch{width:16px;height:16px;border-radius:4px;cursor:pointer;border:2px solid transparent;} +.fd-ctx-menu .ctx-swatch.selected{border-color:var(--text-primary);} +.fd-ctx-menu .ctx-input{flex:1;min-width:0;font-size:12px;padding:3px 6px;background:var(--bg-primary);border:1px solid var(--border);border-radius:4px;color:var(--text-primary);outline:none;} +.fd-ctx-menu .ctx-input:focus{border-color:var(--accent);} +.fd-ctx-menu .ctx-btn{padding:3px 10px;font-size:12px;background:var(--accent);color:#fff;border:none;border-radius:4px;cursor:pointer;} +.fd-ctx-menu .ctx-icons{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;padding:2px 4px 4px;} +.fd-ctx-menu .ctx-icon{width:26px;height:26px;display:flex;align-items:center;justify-content:center;font-size:15px;background:transparent;border:none;border-radius:4px;cursor:pointer;} +.fd-ctx-menu .ctx-icon:hover{background:var(--bg-hover);} + + +
- {# ── 1. Open ── #} -