"""FlowDeck — Collections router: Notion-style databases (v1.3.0).""" from __future__ import annotations import json import logging from fastapi import APIRouter, Request, HTTPException, Query from fastapi.responses import HTMLResponse from app.db import get_conn from app.auth.session import SessionManager logger = logging.getLogger(__name__) router = APIRouter(tags=["collections"], prefix="/db") # ── API: List & Create (no path params) ── @router.get("", response_class=HTMLResponse) async def list_collections(request: Request): """Page listing all collections in the workspace.""" with get_conn() as conn: rows = conn.execute( "SELECT * FROM collections ORDER BY name" ).fetchall() collections = [dict(r) for r in rows] return HTMLResponse( f"
{json.dumps(collections, indent=2, default=str)}"
f"{len(pages)} pages in collection
""") def _render_gallery(view_type: str, collection: dict, pages: list[dict], config: dict) -> str: card_size = config.get("card_size", "medium") size_css = {"small": "160px", "medium": "220px", "large": "300px"}.get(card_size, "220px") cards = "" for p in pages: props = json.loads(p.get("property_values_json", "{}")) cover_url = config.get("cover_property") cover_html = "" if cover_url: for k, v in props.items(): if isinstance(v, list) and len(v) > 0: url = v[0].get("url", "") if isinstance(v[0], dict) else str(v[0]) if url.startswith("http"): cover_html = f"" break prop_tags = "".join( f"{str(v)[:30]}" for v in list(props.values())[:3] if v ) cards += f"""{len(pages)} cards
""") def _render_list(view_type: str, collection: dict, pages: list[dict], config: dict) -> str: items = "" for p in pages: props = json.loads(p.get("property_values_json", "{}")) preview = " · ".join(str(v)[:60] for v in list(props.values())[:3] if v) items += f"""{len(pages)} items
""") def _render_timeline(view_type: str, collection: dict, pages: list[dict], config: dict) -> str: # Find date range dates = [] page_dates = [] for p in pages: props = json.loads(p.get("property_values_json", "{}")) start_val = end_val = None for k, v in props.items(): if isinstance(v, str) and v.startswith("20"): if "..." in v: parts = v.split("...") start_val, end_val = parts[0][:10], parts[1][:10] if len(parts) > 1 else parts[0][:10] else: start_val = end_val = v[:10] break if start_val: dates.append(start_val) if end_val: dates.append(end_val) page_dates.append((p, start_val, end_val or start_val)) if not dates: return _base_html(collection["name"], collection.get("icon", "📈"), view_type, "No date data to display timeline.
") from datetime import date as dt_date min_date = min(dt_date.fromisoformat(d) for d in dates) max_date = max(dt_date.fromisoformat(d) for d in dates) total = (max_date - min_date).days or 1 bars = "" for p, start, end in page_dates: sd = dt_date.fromisoformat(start) ed = dt_date.fromisoformat(end) left = (sd - min_date).days / total * 100 width = max((ed - sd).days / total * 100, 1) bars += f"""{len(pages)} items · {min_date} → {max_date}
""") def _render_table(view_type: str, collection: dict, pages: list[dict], config: dict) -> str: rows = "" for p in pages: props = json.loads(p.get("property_values_json", "{}")) prop_cells = "".join(f"| Title | Properties |
|---|
{len(pages)} rows
""") @router.get("/{collection_id}/api") async def get_collection_api(request: Request, collection_id: int): """API: get a single collection with its pages.""" with get_conn() as conn: collection = conn.execute( "SELECT * FROM collections WHERE id=?", (collection_id,) ).fetchone() if not collection: raise HTTPException(status_code=404, detail="Collection not found") pages = conn.execute( "SELECT * FROM collection_pages WHERE collection_id=? ORDER BY position", (collection_id,), ).fetchall() views = conn.execute( "SELECT * FROM collection_views WHERE collection_id=? ORDER BY position", (collection_id,), ).fetchall() return { "collection": dict(collection), "pages": [dict(p) for p in pages], "views": [dict(v) for v in views], } @router.post("/{collection_id}/pages/api") async def create_page_api(request: Request, collection_id: int): """API: create a page in a collection.""" try: body = await request.json() except Exception: body = {} title = body.get("title", "").strip() if not title: raise HTTPException(status_code=400, detail="title is required") icon = body.get("icon", "📄") property_values = json.dumps(body.get("properties", {})) gitea_issue_id = body.get("gitea_issue_id") gitea_issue_number = body.get("gitea_issue_number") with get_conn() as conn: coll = conn.execute("SELECT id FROM collections WHERE id=?", (collection_id,)).fetchone() if not coll: raise HTTPException(status_code=404, detail="Collection not found") max_pos = conn.execute( "SELECT COALESCE(MAX(position), -1) + 1 FROM collection_pages WHERE collection_id=?", (collection_id,), ).fetchone()[0] cur = conn.execute( """INSERT INTO collection_pages (collection_id, title, icon, position, gitea_issue_id, gitea_issue_number, property_values_json) VALUES (?, ?, ?, ?, ?, ?, ?)""", (collection_id, title, icon, max_pos, gitea_issue_id, gitea_issue_number, property_values), ) conn.commit() page_id = cur.lastrowid return {"id": page_id, "title": title, "status": "created"}