diff --git a/routers/weblinks.py b/routers/weblinks.py index a3f7d3b..1e74c15 100644 --- a/routers/weblinks.py +++ b/routers/weblinks.py @@ -61,7 +61,8 @@ async def list_bookmarks( current_folder = f break - # Get links (filtered by folder or all unfiled) + # Get links (filtered by folder or all) + available_links = [] if folder_id: result = await db.execute(text(""" SELECT l.* FROM links l @@ -69,6 +70,16 @@ async def list_bookmarks( WHERE fl.folder_id = :fid AND l.is_deleted = false ORDER BY fl.sort_order, l.label """), {"fid": folder_id}) + items = [dict(r._mapping) for r in result] + + # Links NOT in this folder (for "add existing" dropdown) + result = await db.execute(text(""" + SELECT l.id, l.label FROM links l + WHERE l.is_deleted = false + AND l.id NOT IN (SELECT link_id FROM folder_links WHERE folder_id = :fid) + ORDER BY l.label + """), {"fid": folder_id}) + available_links = [dict(r._mapping) for r in result] else: # Show all links result = await db.execute(text(""" @@ -76,13 +87,14 @@ async def list_bookmarks( WHERE l.is_deleted = false ORDER BY l.sort_order, l.label """)) - items = [dict(r._mapping) for r in result] + items = [dict(r._mapping) for r in result] return templates.TemplateResponse("weblinks.html", { "request": request, "sidebar": sidebar, "items": items, "top_folders": top_folders, "child_folder_map": child_folder_map, "current_folder": current_folder, "current_folder_id": folder_id or "", + "available_links": available_links, "page_title": "Bookmarks", "active_nav": "bookmarks", }) @@ -220,6 +232,96 @@ async def delete_link(link_id: str, request: Request, db: AsyncSession = Depends return RedirectResponse(url=referer, status_code=303) +# ---- Reorder links within a folder ---- + +@router.post("/folders/{folder_id}/reorder") +async def reorder_link( + folder_id: str, + link_id: str = Form(...), + direction: str = Form(...), + db: AsyncSession = Depends(get_db), +): + # Get all folder_links for this folder, ordered by sort_order then created_at + result = await db.execute(text(""" + SELECT link_id, sort_order FROM folder_links + WHERE folder_id = :fid + ORDER BY sort_order, created_at + """), {"fid": folder_id}) + rows = [dict(r._mapping) for r in result] + + if not rows: + return RedirectResponse(url=f"/weblinks?folder_id={folder_id}", status_code=303) + + # Lazy-initialize sort_order if all zeros + all_zero = all(r["sort_order"] == 0 for r in rows) + if all_zero: + for i, r in enumerate(rows): + await db.execute(text(""" + UPDATE folder_links SET sort_order = :so + WHERE folder_id = :fid AND link_id = :lid + """), {"so": (i + 1) * 10, "fid": folder_id, "lid": r["link_id"]}) + r["sort_order"] = (i + 1) * 10 + + # Find target index + idx = None + for i, r in enumerate(rows): + if str(r["link_id"]) == link_id: + idx = i + break + + if idx is None: + return RedirectResponse(url=f"/weblinks?folder_id={folder_id}", status_code=303) + + # Determine swap partner + if direction == "up" and idx > 0: + swap_idx = idx - 1 + elif direction == "down" and idx < len(rows) - 1: + swap_idx = idx + 1 + else: + return RedirectResponse(url=f"/weblinks?folder_id={folder_id}", status_code=303) + + # Swap sort_order values + so_a, so_b = rows[idx]["sort_order"], rows[swap_idx]["sort_order"] + lid_a, lid_b = rows[idx]["link_id"], rows[swap_idx]["link_id"] + + await db.execute(text(""" + UPDATE folder_links SET sort_order = :so + WHERE folder_id = :fid AND link_id = :lid + """), {"so": so_b, "fid": folder_id, "lid": lid_a}) + await db.execute(text(""" + UPDATE folder_links SET sort_order = :so + WHERE folder_id = :fid AND link_id = :lid + """), {"so": so_a, "fid": folder_id, "lid": lid_b}) + + return RedirectResponse(url=f"/weblinks?folder_id={folder_id}", status_code=303) + + +# ---- Add existing link to folder ---- + +@router.post("/folders/{folder_id}/add-link") +async def add_link_to_folder( + folder_id: str, + link_id: str = Form(...), + db: AsyncSession = Depends(get_db), +): + # Remove link from any existing folder (single-folder membership) + await db.execute(text("DELETE FROM folder_links WHERE link_id = :lid"), {"lid": link_id}) + + # Get max sort_order in target folder + result = await db.execute(text(""" + SELECT COALESCE(MAX(sort_order), 0) FROM folder_links WHERE folder_id = :fid + """), {"fid": folder_id}) + max_so = result.scalar() + + # Insert into target folder at end + await db.execute(text(""" + INSERT INTO folder_links (folder_id, link_id, sort_order) + VALUES (:fid, :lid, :so) ON CONFLICT DO NOTHING + """), {"fid": folder_id, "lid": link_id, "so": max_so + 10}) + + return RedirectResponse(url=f"/weblinks?folder_id={folder_id}", status_code=303) + + # ---- Folders ---- @router.get("/folders/create") diff --git a/templates/weblinks.html b/templates/weblinks.html index 0ffa959..4cdc546 100644 --- a/templates/weblinks.html +++ b/templates/weblinks.html @@ -38,6 +38,23 @@ + {% if available_links %} +