"""FlowDeck — Board : page_media. Découpe A28 de l'ancien app/routers/board.py (2 101 lignes, 53 routes) — un module par concern, contrat inchangé. """ from __future__ import annotations import logging from fastapi import APIRouter, Body, HTTPException, Request from app.auth.session import SessionManager from app.db import get_conn from app.services.automations import fire_event, run_event_sync from ._common import _store_uploaded_file, _ws_id_for logger = logging.getLogger(__name__) router = APIRouter(tags=["board"], prefix="/board") # ═══════════ v5.4.0: Page & collection duplication ═══════════ @router.post("/api/pages/{page_id}/duplicate") def duplicate_page(request: Request, page_id: int): """Duplicate a page (block/markdown content included) as a sibling.""" SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) with get_conn() as conn: row = conn.execute( "SELECT * FROM pages WHERE id=? AND deleted_at IS NULL", (page_id,) ).fetchone() if not row: raise HTTPException(404, "Page not found") page = dict(row) def copy_tree(src_id: int, parent_id) -> int: with get_conn() as conn: conn.execute("SELECT * FROM pages WHERE id=?", (src_id,)).fetchone() cur = conn.execute( "INSERT INTO pages (workspace, workspace_id, title, content, content_format, " "parent_section, parent_id, sort_order, share_mode, published, is_published, " "publish_slug, is_shared, cover_url, page_icon, created_at, updated_at) " "SELECT workspace, workspace_id, title || ' copy', content, content_format, " "parent_section, ?, sort_order, share_mode, 0, is_published, '', is_shared, " "cover_url, page_icon, created_at, updated_at FROM pages WHERE id=?", (parent_id, src_id), ) new_id = cur.lastrowid conn.commit() for child in conn.execute( "SELECT id FROM pages WHERE parent_id=? ", (src_id,) ).fetchall(): copy_tree(child["id"], new_id) return new_id new_id = copy_tree(page_id, page.get("parent_id")) title = (page.get("title") or "Untitled") + " copy" with get_conn() as conn: conn.execute("UPDATE pages SET title=? WHERE id=?", (title, new_id)) conn.commit() run_event_sync(fire_event("page.created", {"page_id": new_id, "title": title, "workspace": page.get("workspace")})) return {"status": "ok", "id": new_id, "title": title} # ═══════════ v5.5.0: Cover & icon ═══════════ @router.post("/api/pages/{page_id}/cover") async def set_page_cover(request: Request, page_id: int): """v5.5.0: upload an image cover for a page. JSON body {cover_url} accepts an external URL; multipart ``file`` uploads an image stored in the workspace's uploads directory. """ ctype = (request.headers.get("content-type") or "").lower() if ctype.startswith("application/json"): body = await request.json() cover_url = (body.get("cover_url") or "").strip() if not cover_url: raise HTTPException(400, "cover_url required") with get_conn() as conn: conn.execute("UPDATE pages SET cover_url=? WHERE id=?", (cover_url, page_id)) conn.commit() return {"status": "ok", "page_id": page_id, "cover_url": cover_url} ws_id = _ws_id_for(request, page_id) meta = await _store_uploaded_file(request, ws_id) with get_conn() as conn: conn.execute("UPDATE pages SET cover_url=? WHERE id=?", (meta["file_url"], page_id)) conn.commit() return {"status": "ok", "page_id": page_id, "cover_url": meta["file_url"]} @router.delete("/api/pages/{page_id}/cover") def remove_page_cover(request: Request, page_id: int): with get_conn() as conn: conn.execute("UPDATE pages SET cover_url='' WHERE id=?", (page_id,)) conn.commit() return {"status": "ok", "page_id": page_id} @router.post("/api/pages/{page_id}/icon") def set_page_icon(request: Request, page_id: int, body: dict = Body(default={})): """v5.5.0: set a page emoji/icon label (or a custom-emoji image URL).""" icon = (body.get("icon") or "").strip() if len(icon) > 512: raise HTTPException(400, "icon too long") with get_conn() as conn: conn.execute("UPDATE pages SET page_icon=? WHERE id=?", (icon, page_id)) conn.commit() return {"status": "ok", "page_id": page_id, "icon": icon} # ═══════════ v5.4.0: Import (Markdown / .md / Notion .zip) ═══════════