"""Links: URL references attached to domains/projects/tasks/meetings.""" from fastapi import APIRouter, Request, Form, Depends from fastapi.templating import Jinja2Templates from fastapi.responses import RedirectResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import text from typing import Optional from core.database import get_db from core.base_repository import BaseRepository from core.sidebar import get_sidebar_data from routers.weblinks import get_default_folder_id router = APIRouter(prefix="/links", tags=["links"]) templates = Jinja2Templates(directory="templates") @router.get("/") async def list_links(request: Request, domain_id: Optional[str] = None, db: AsyncSession = Depends(get_db)): sidebar = await get_sidebar_data(db) where_clauses = ["l.is_deleted = false"] params = {} if domain_id: where_clauses.append("l.domain_id = :domain_id") params["domain_id"] = domain_id where_sql = " AND ".join(where_clauses) result = await db.execute(text(f""" SELECT l.*, d.name as domain_name, d.color as domain_color, p.name as project_name FROM links l LEFT JOIN domains d ON l.domain_id = d.id LEFT JOIN projects p ON l.project_id = p.id WHERE {where_sql} ORDER BY l.sort_order, l.created_at """), params) items = [dict(r._mapping) for r in result] domains_repo = BaseRepository("domains", db) domains = await domains_repo.list() return templates.TemplateResponse("links.html", { "request": request, "sidebar": sidebar, "items": items, "domains": domains, "current_domain_id": domain_id or "", "page_title": "Links", "active_nav": "links", }) @router.get("/create") async def create_form( request: Request, domain_id: Optional[str] = None, project_id: Optional[str] = None, task_id: Optional[str] = None, meeting_id: Optional[str] = None, contact_id: Optional[str] = None, focus_id: Optional[str] = None, db: AsyncSession = Depends(get_db), ): sidebar = await get_sidebar_data(db) domains_repo = BaseRepository("domains", db) domains = await domains_repo.list() projects_repo = BaseRepository("projects", db) projects = await projects_repo.list() return templates.TemplateResponse("link_form.html", { "request": request, "sidebar": sidebar, "domains": domains, "projects": projects, "page_title": "New Link", "active_nav": "links", "item": None, "prefill_domain_id": domain_id or "", "prefill_project_id": project_id or "", "prefill_task_id": task_id or "", "prefill_meeting_id": meeting_id or "", "prefill_contact_id": contact_id or "", "prefill_focus_id": focus_id or "", }) @router.post("/create") async def create_link( request: Request, label: str = Form(...), url: str = Form(...), domain_id: Optional[str] = Form(None), project_id: Optional[str] = Form(None), task_id: Optional[str] = Form(None), meeting_id: Optional[str] = Form(None), contact_id: Optional[str] = Form(None), focus_id: Optional[str] = Form(None), description: Optional[str] = Form(None), tags: Optional[str] = Form(None), db: AsyncSession = Depends(get_db), ): repo = BaseRepository("links", db) data = {"label": label, "url": url, "description": description} if domain_id and domain_id.strip(): data["domain_id"] = domain_id if project_id and project_id.strip(): data["project_id"] = project_id if task_id and task_id.strip(): data["task_id"] = task_id if meeting_id and meeting_id.strip(): data["meeting_id"] = meeting_id if tags and tags.strip(): data["tags"] = [t.strip() for t in tags.split(",") if t.strip()] link = await repo.create(data) # Assign to Default folder default_fid = await get_default_folder_id(db) await db.execute(text(""" INSERT INTO folder_links (folder_id, link_id) VALUES (:fid, :lid) ON CONFLICT DO NOTHING """), {"fid": default_fid, "lid": link["id"]}) # Attach to contact if created from contact context if contact_id and contact_id.strip(): await db.execute(text(""" INSERT INTO contact_links (contact_id, link_id) VALUES (:cid, :lid) ON CONFLICT DO NOTHING """), {"cid": contact_id, "lid": link["id"]}) await db.commit() return RedirectResponse(url=f"/contacts/{contact_id}", status_code=303) # Attach to focus item if created from focus context if focus_id and focus_id.strip(): await db.execute(text(""" INSERT INTO focus_links (focus_id, link_id) VALUES (:fid, :lid) ON CONFLICT DO NOTHING """), {"fid": focus_id, "lid": link["id"]}) await db.commit() return RedirectResponse(url=f"/focus/{focus_id}", status_code=303) # Redirect back to context if created from task/meeting/project if task_id and task_id.strip(): return RedirectResponse(url=f"/tasks/{task_id}?tab=links", status_code=303) if meeting_id and meeting_id.strip(): return RedirectResponse(url=f"/meetings/{meeting_id}?tab=links", status_code=303) if project_id and project_id.strip(): return RedirectResponse(url=f"/projects/{project_id}?tab=links", status_code=303) return RedirectResponse(url="/links", status_code=303) @router.get("/{link_id}/edit") async def edit_form(link_id: str, request: Request, from_project: Optional[str] = None, db: AsyncSession = Depends(get_db)): repo = BaseRepository("links", db) sidebar = await get_sidebar_data(db) item = await repo.get(link_id) if not item: return RedirectResponse(url="/links", status_code=303) domains_repo = BaseRepository("domains", db) domains = await domains_repo.list() projects_repo = BaseRepository("projects", db) projects = await projects_repo.list() return templates.TemplateResponse("link_form.html", { "request": request, "sidebar": sidebar, "domains": domains, "projects": projects, "page_title": "Edit Link", "active_nav": "links", "item": item, "prefill_domain_id": "", "prefill_project_id": "", "prefill_task_id": "", "prefill_meeting_id": "", "from_project": from_project or "", }) @router.post("/{link_id}/edit") async def update_link( link_id: str, label: str = Form(...), url: str = Form(...), domain_id: Optional[str] = Form(None), project_id: Optional[str] = Form(None), description: Optional[str] = Form(None), tags: Optional[str] = Form(None), from_project: Optional[str] = Form(None), db: AsyncSession = Depends(get_db), ): repo = BaseRepository("links", db) data = { "label": label, "url": url, "domain_id": domain_id if domain_id and domain_id.strip() else None, "project_id": project_id if project_id and project_id.strip() else None, "description": description, } if tags and tags.strip(): data["tags"] = [t.strip() for t in tags.split(",") if t.strip()] else: data["tags"] = None await repo.update(link_id, data) new_project = data.get("project_id") if from_project and from_project.strip(): if new_project and new_project != from_project: return RedirectResponse(url=f"/projects/{new_project}?tab=links", status_code=303) return RedirectResponse(url=f"/projects/{from_project}?tab=links", status_code=303) return RedirectResponse(url="/links", status_code=303) @router.post("/{link_id}/delete") async def delete_link(link_id: str, request: Request, db: AsyncSession = Depends(get_db)): repo = BaseRepository("links", db) await repo.soft_delete(link_id) return RedirectResponse(url=request.headers.get("referer", "/links"), status_code=303) @router.post("/reorder") async def reorder_link( request: Request, item_id: str = Form(...), direction: str = Form(...), db: AsyncSession = Depends(get_db), ): repo = BaseRepository("links", db) await repo.move_in_order(item_id, direction) return RedirectResponse(url=request.headers.get("referer", "/links"), status_code=303)