feat: complete favorites system — sidebar, library, context menu, API
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

6 files changed:
- db.py: migrate favorites table FK from collection_pages(id) to pages(id)
- board.py: add favorites API (POST/DELETE /board/api/favorites/{id}, GET list)
- board.py: _sidebar_data() now loads favorite_pages from DB via JOIN
- dashboard.py: library_page loads lib_favorites from DB (not parent_section)
- csrf.py: exclude /board/api/favorites from CSRF checks
- base.html: context menu toggles Add/Remove Favorites based on state
- base.html: favoriteIds Alpine set initialized from server-rendered favorites
- test_app.py: test_favorites_crud rewritten for new page-based favorites API

Favorites now work end-to-end:
- Right-click → Add to Favorites (or Remove if already favorited)
- Sidebar Favorites section shows favorited pages
- Library Favorites tab shows the same pages
- API: POST/DELETE /board/api/favorites/{page_id}, GET /board/api/favorites
This commit is contained in:
2026-07-10 09:44:20 -04:00
parent fbf0335c3a
commit 0de4f411bd
6 changed files with 141 additions and 21 deletions
+70 -1
View File
@@ -169,10 +169,34 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
recent.append(p)
# Private pages: same as recent but filtered for page/ items (non-board views)
private_items = [r for r in recent if r.get("active") or r["id"].startswith("page/")]
# Load favorite pages from DB
uid = user["id"] if user and user.get("id") else 1
with get_conn() as conn:
fav_rows = conn.execute(
"SELECT p.id, p.title, p.workspace, p.updated_at FROM favorites f "
"JOIN pages p ON p.id = f.page_id "
"WHERE f.user_id=? ORDER BY f.position", (uid,)
).fetchall()
favorites = []
for r in fav_rows:
favorites.append({
"id": f"page/{r['id']}",
"db_id": r["id"],
"name": r["title"] or "New page",
"icon": "📄",
"url": f"/pages/{r['id']}",
"active": False,
"indent": 0,
"depth": 0,
"has_children": False,
"children": [],
})
return {"workspace_name": ws_name, "workspace_initial": ws_name[0].upper() if ws_name else "B",
"current_page": repo or "Dashboard", "last_edited": "now",
"recent_pages": recent, "private_pages": private_items,
"favorite_pages": [], "user": user,
"favorite_pages": favorites, "user": user,
"workspace_key": ws_key}
@@ -314,6 +338,51 @@ async def library_page(request: Request, owner: str = Query(default=""), repo: s
template = env.get_template("library.html")
return template.render(**sidebar)
# ═══════════ Favorites API ═══════════
@router.get("/api/favorites")
async def list_favorites(request: Request):
"""List favorited page IDs for the current user."""
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
uid = user["id"] if user and user.get("id") else 1
with get_conn() as conn:
rows = conn.execute(
"SELECT page_id FROM favorites WHERE user_id=? ORDER BY position", (uid,)
).fetchall()
return {"favorites": [r["page_id"] for r in rows]}
@router.post("/api/favorites/{page_id:int}")
async def add_favorite(request: Request, page_id: int):
"""Add a page to favorites."""
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
uid = user["id"] if user and user.get("id") else 1
with get_conn() as conn:
existing = conn.execute(
"SELECT id FROM favorites WHERE user_id=? AND page_id=?", (uid, page_id)
).fetchone()
if not existing:
pos = conn.execute(
"SELECT COALESCE(MAX(position), -1) + 1 FROM favorites WHERE user_id=?", (uid,)
).fetchone()[0]
conn.execute(
"INSERT INTO favorites (user_id, page_id, position) VALUES (?,?,?)",
(uid, page_id, pos),
)
conn.commit()
return {"status": "added", "page_id": page_id}
@router.delete("/api/favorites/{page_id:int}")
async def remove_favorite(request: Request, page_id: int):
"""Remove a page from favorites."""
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
uid = user["id"] if user and user.get("id") else 1
with get_conn() as conn:
conn.execute("DELETE FROM favorites WHERE user_id=? AND page_id=?", (uid, page_id))
conn.commit()
return {"status": "removed", "page_id": page_id}
# ═══════════ Trash (must be before /{owner}/{repo} catch-all) ═══════════
@router.get("/api/trash")
+28 -5
View File
@@ -109,13 +109,36 @@ async def library_page(request: Request, owner: str = Query(default=""), repo: s
all_pages = [to_page(r) for r in rows]
# Load favorites from DB for Library
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
uid = user["id"] if user and user.get("id") else 1
with get_conn() as conn:
fav_rows = conn.execute(
"SELECT p.id, p.title, p.workspace, p.updated_at FROM favorites f "
"JOIN pages p ON p.id = f.page_id "
"WHERE f.user_id=? ORDER BY f.position", (uid,)
).fetchall()
lib_favs = []
for r in fav_rows:
r = dict(r)
lib_favs.append({
"id": f"page/{r['id']}",
"name": r["title"] or "Untitled",
"icon": "📄",
"url": f"/pages/{r['id']}",
"created_by": "You",
"source": r.get("workspace") or "🔒 Private",
"last_edited": r.get("updated_at", "now"),
"last_visited": r.get("updated_at", "now"),
})
# Catégoriser par parent_section — sidebar data intacte
sidebar["lib_recent"] = all_pages
sidebar["lib_favorites"] = [p for p, r in zip(all_pages, rows) if (r["parent_section"] or "") == "Favorites"]
sidebar["lib_shared"] = [p for p, r in zip(all_pages, rows) if (r["parent_section"] or "") == "Shared"]
sidebar["lib_private"] = [p for p, r in zip(all_pages, rows) if (r["parent_section"] or "Private") == "Private"]
sidebar["lib_has_favorites"] = len(sidebar["lib_favorites"]) > 0
sidebar["lib_has_shared"] = len(sidebar["lib_shared"]) > 0
sidebar["lib_favorites"] = lib_favs
sidebar["lib_shared"] = []
sidebar["lib_private"] = [p for p in all_pages if p.get("source") == "🔒 Private"]
sidebar["lib_has_favorites"] = len(lib_favs) > 0
sidebar["lib_has_shared"] = False
template = env.get_template("library.html")
return template.render(**sidebar)