Découpe par concern de l'ancien app/routers/dashboard.py (2 735 lignes, 63 routes) en package `app/routers/dashboard/` : - 8 modules de routes : local_workspace 559 L (15 r.), pages_html 485 (6), account_settings 439 (16), workspace 321 (9), pages_api 240 (6), workspaces 131 (6), public 78 (1), account_api 77 (4) - _common.py (774 L) : les 15 helpers top-level INTERCALÉS dans l'ancien fichier + état (logger, _VERSION, WORKSPACE_COOKIE) - __init__.py : ordre d'enregistrement identique à l'origine, re-export complet (7 importateurs inchangés : main, board ×3, my_tasks, web_clipper, wiki, sites `_dash._render_blocks_public`, tests) + __all__ Preuve contractuelle : docs/openapi-v2.json régénéré = IDENTIQUE byte-à-byte (509 chemins, ordre préservé). Pièges rattrapés : - segment décorateur sans sa fonction → assert `def in seg` + récupération git (corps perdus en silence à la 1ʳᵉ exécution) - collision `settings` (section vs from app.config import settings → hasattr du fromlist) → renommée account_settings - WORKSPACE_COOKIE utilisé sans import dans workspaces.py (F821) - script __all__ mangeant la fin du fichier → __init__ réécrit Reste A28 : collections.py 2 622 L, board.py 2 101 L (lot 3). suite **1091/1091** · ruff OK · OpenAPI 509 identique · docs à jour
241 lines
8.0 KiB
Python
241 lines
8.0 KiB
Python
"""FlowDeck — Dashboard : pages_api.
|
|
|
|
Découpe A28 de l'ancien app/routers/dashboard.py (2 735 lignes, 63 routes) — un module par concern, contrat inchangé.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Body, Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from app.auth.session import SessionManager
|
|
from app.db import get_conn
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(tags=["dashboard"])
|
|
|
|
|
|
|
|
|
|
# ═══════════ Library page actions API ═══════════
|
|
|
|
@router.get("/api/pages/{page_id:int}/content")
|
|
def api_page_content(page_id: int):
|
|
"""Get page content for side peek preview."""
|
|
with get_conn() as conn:
|
|
row = conn.execute(
|
|
"SELECT title, content, content_format FROM pages WHERE id=? AND deleted_at IS NULL",
|
|
(page_id,),
|
|
).fetchone()
|
|
if not row:
|
|
return JSONResponse({"error": "Not found"}, status_code=404)
|
|
# v6.5.0: resolve synced blocks server-side (fresh content on read).
|
|
from app.services.synced_blocks import resolve_content_json
|
|
return {
|
|
"title": row["title"],
|
|
"content": resolve_content_json(row["content"], row["content_format"]),
|
|
"format": row["content_format"] or "blocks",
|
|
}
|
|
|
|
|
|
|
|
|
|
@router.put("/api/pages/{page_id:int}/rename")
|
|
def api_rename_page(page_id: int, request: Request, body: dict = Body(default={})):
|
|
"""Inline rename a page title."""
|
|
title = (body.get("title") or "").strip()
|
|
if not title:
|
|
return JSONResponse({"error": "Title required"}, status_code=400)
|
|
with get_conn() as conn:
|
|
conn.execute(
|
|
"UPDATE pages SET title=?, updated_at=CURRENT_TIMESTAMP WHERE id=? AND deleted_at IS NULL",
|
|
(title, page_id),
|
|
)
|
|
# v6.5.0: renaming a database row's content page updates the row.
|
|
from app.services.row_pages import sync_page_title_to_row
|
|
sync_page_title_to_row(conn, page_id)
|
|
conn.commit()
|
|
return {"status": "ok", "title": title}
|
|
|
|
|
|
|
|
|
|
@router.post("/api/pages/{page_id:int}/trash")
|
|
def api_trash_page(page_id: int):
|
|
"""Soft-delete a page (move to trash)."""
|
|
with get_conn() as conn:
|
|
conn.execute(
|
|
"UPDATE pages SET parent_section='Trash', deleted_at=CURRENT_TIMESTAMP WHERE id=?",
|
|
(page_id,),
|
|
)
|
|
conn.commit()
|
|
return {"status": "ok"}
|
|
|
|
|
|
|
|
|
|
@router.post("/api/pages/{page_id:int}/convert-to-database")
|
|
def api_convert_to_database(page_id: int, request: Request, body: dict = Body(default={})):
|
|
"""Convert a page into a full-page database (Notion-style).
|
|
|
|
Creates a collection linked to this page, adds the default 'Name' property,
|
|
and sets the page's content_format to 'collection'.
|
|
"""
|
|
import json as _json
|
|
db_name = (body.get("name") or "").strip()
|
|
|
|
with get_conn() as conn:
|
|
page = conn.execute(
|
|
"SELECT id, title, workspace_id FROM pages WHERE id=? AND deleted_at IS NULL",
|
|
(page_id,),
|
|
).fetchone()
|
|
if not page:
|
|
return JSONResponse({"error": "Page not found"}, status_code=404)
|
|
|
|
if not db_name:
|
|
db_name = page["title"] or "New Database"
|
|
|
|
# Create the collection
|
|
cur = conn.execute(
|
|
"""INSERT INTO collections
|
|
(name, description, icon, schema_json, is_inline, parent_page_id, workspace_id)
|
|
VALUES (?, '', '📋', '[]', 0, ?, ?)""",
|
|
(db_name, page_id, page["workspace_id"]),
|
|
)
|
|
collection_id = cur.lastrowid
|
|
|
|
# Create default "Name" property (text, position 0)
|
|
conn.execute(
|
|
"""INSERT INTO collection_properties
|
|
(collection_id, name, prop_type, position, required, visible_in_views)
|
|
VALUES (?, 'Name', 'title', 0, 1, 1)""",
|
|
(collection_id,),
|
|
)
|
|
|
|
# Create default "Table" view
|
|
conn.execute(
|
|
"""INSERT INTO collection_views
|
|
(collection_id, name, view_type, config_json, position)
|
|
VALUES (?, 'Table', 'table', ?, 0)""",
|
|
(collection_id, _json.dumps({"visible_properties": ["Name"]})),
|
|
)
|
|
|
|
# Update the page to be a database page
|
|
conn.execute(
|
|
"UPDATE pages SET content_format='collection', collection_id=?, updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
|
(collection_id, page_id),
|
|
)
|
|
|
|
conn.commit()
|
|
|
|
return {
|
|
"status": "converted",
|
|
"collection_id": collection_id,
|
|
"name": db_name,
|
|
"view_url": f"/pages/{page_id}",
|
|
}
|
|
|
|
|
|
|
|
|
|
@router.get("/api/collections/{collection_id:int}/table-data")
|
|
def api_collection_table_data(collection_id: int):
|
|
"""Get collection properties + pages for rendering the table view."""
|
|
with get_conn() as conn:
|
|
coll = conn.execute(
|
|
"SELECT * FROM collections WHERE id=?", (collection_id,)
|
|
).fetchone()
|
|
if not coll:
|
|
return JSONResponse({"error": "Collection not found"}, status_code=404)
|
|
|
|
properties = [
|
|
dict(r) for r in conn.execute(
|
|
"SELECT * FROM collection_properties WHERE collection_id=? ORDER BY position",
|
|
(collection_id,),
|
|
).fetchall()
|
|
]
|
|
|
|
pages = [
|
|
dict(r) for r in conn.execute(
|
|
"SELECT * FROM collection_pages WHERE collection_id=? ORDER BY position",
|
|
(collection_id,),
|
|
).fetchall()
|
|
]
|
|
|
|
views = [
|
|
dict(r) for r in conn.execute(
|
|
"SELECT * FROM collection_views WHERE collection_id=? ORDER BY position",
|
|
(collection_id,),
|
|
).fetchall()
|
|
]
|
|
|
|
return {
|
|
"collection": dict(coll),
|
|
"properties": properties,
|
|
"pages": pages,
|
|
"views": views,
|
|
}
|
|
|
|
|
|
|
|
|
|
@router.post("/api/collections/{collection_id:int}/pages")
|
|
def api_create_collection_page(collection_id: int, request: Request, body: dict = Body(default={})):
|
|
"""Create a new page (row) in a collection."""
|
|
import json as _json
|
|
title = body.get("title", "New page").strip() or "New page"
|
|
icon = body.get("icon", "file")
|
|
|
|
with get_conn() as conn:
|
|
coll = conn.execute(
|
|
"SELECT id FROM collections WHERE id=?", (collection_id,)
|
|
).fetchone()
|
|
if not coll:
|
|
return JSONResponse({"error": "Collection not found"}, status_code=404)
|
|
|
|
# Get next position
|
|
max_pos = conn.execute(
|
|
"SELECT COALESCE(MAX(position), -1) + 1 FROM collection_pages WHERE collection_id=?",
|
|
(collection_id,),
|
|
).fetchone()[0]
|
|
|
|
# Load default property values from collection properties
|
|
props = [
|
|
dict(r) for r in conn.execute(
|
|
"SELECT id, name, prop_type FROM collection_properties WHERE collection_id=? ORDER BY position",
|
|
(collection_id,),
|
|
).fetchall()
|
|
]
|
|
default_values = {}
|
|
for p in props:
|
|
if p["prop_type"] == "title":
|
|
default_values[str(p["id"])] = title
|
|
# Caller-provided values (e.g. board "add card in column X") win.
|
|
incoming = body.get("properties") or {}
|
|
if isinstance(incoming, dict):
|
|
default_values.update(incoming)
|
|
|
|
from app.services.property_types import apply_auto_properties
|
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) or {"login": "admin", "id": 1}
|
|
apply_auto_properties(props, default_values, user, is_create=True)
|
|
|
|
cur = conn.execute(
|
|
"""INSERT INTO collection_pages
|
|
(collection_id, title, icon, cover_url, position, property_values_json)
|
|
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
(collection_id, title, icon, body.get("cover_url", ""), max_pos,
|
|
_json.dumps(default_values)),
|
|
)
|
|
page_id = cur.lastrowid
|
|
conn.commit()
|
|
|
|
return {
|
|
"id": page_id,
|
|
"title": title,
|
|
"icon": icon,
|
|
"position": max_pos,
|
|
"property_values_json": default_values,
|
|
"status": "created",
|
|
}
|