From 3e6323a85683a6ab5a6e8f94f9f87d19a65ed199 Mon Sep 17 00:00:00 2001 From: bruno Date: Fri, 10 Jul 2026 16:41:40 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20workspaces=20system=20=E2=80=94=20creat?= =?UTF-8?q?e,=20select,=20rename,=20delete=20workspaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DB: pages.workspace_id column linking pages to workspaces table - /workspaces: list, create, rename, delete workspaces page - API: GET/POST/PUT/DELETE /api/workspaces + POST select - Session: flowdeck_workspace cookie for active workspace tracking - Sidebar: Workspace section shows active workspace name + page tree - Local workspace APIs filter by active workspace_id - Home redirects non-Gitea users to /workspaces --- app/db.py | 5 ++ app/routers/dashboard.py | 115 ++++++++++++++++++++++++++ app/templates/base.html | 14 +++- app/templates/workspaces.html | 149 ++++++++++++++++++++++++++++++++++ 4 files changed, 279 insertions(+), 4 deletions(-) create mode 100644 app/templates/workspaces.html diff --git a/app/db.py b/app/db.py index 26c3fd2..0cebc7f 100644 --- a/app/db.py +++ b/app/db.py @@ -152,6 +152,7 @@ def init_db(): CREATE TABLE IF NOT EXISTS pages ( id INTEGER PRIMARY KEY AUTOINCREMENT, workspace TEXT NOT NULL, + workspace_id INTEGER REFERENCES workspaces(id), title TEXT NOT NULL DEFAULT 'New page', content TEXT NOT NULL DEFAULT '', parent_section TEXT DEFAULT 'Private', @@ -322,6 +323,10 @@ def init_db(): conn.execute("ALTER TABLE pages ADD COLUMN published INTEGER DEFAULT 0") except sqlite3.OperationalError: pass + try: + conn.execute("ALTER TABLE pages ADD COLUMN workspace_id INTEGER REFERENCES workspaces(id)") + except sqlite3.OperationalError: + pass # v2.2: Auth locale for col in ["password_hash", "is_active", "last_login", "login_attempts", "locked_until"]: try: diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index e7bb230..7e203a3 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -547,3 +547,118 @@ async def delete_local_workspace_item(request: Request, item_id: int): conn.execute("DELETE FROM pages WHERE id=? OR parent_id=?", (item_id, item_id)) conn.commit() return {"status": "ok"} + + +# ═══════════ Workspaces CRUD ═══════════ + +WORKSPACE_COOKIE = "flowdeck_workspace" + + +def _get_active_workspace(request: Request) -> dict | None: + """Get the active workspace ID from the cookie, or first workspace, or None.""" + ws_id = request.cookies.get(WORKSPACE_COOKIE) + if ws_id: + try: + with get_conn() as conn: + ws = conn.execute("SELECT * FROM workspaces WHERE id=?", (int(ws_id),)).fetchone() + if ws: + return dict(ws) + except (ValueError, Exception): + pass + # Fallback: first workspace + with get_conn() as conn: + ws = conn.execute("SELECT * FROM workspaces ORDER BY id LIMIT 1").fetchone() + if ws: + return dict(ws) + return None + + +@router.get("/workspaces", response_class=HTMLResponse) +async def workspaces_page(request: Request): + """Workspaces list page.""" + from jinja2 import Environment, FileSystemLoader + env = Environment(loader=FileSystemLoader("app/templates")) + sidebar = _sidebar_data(request, []) + user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) + if not user: + return RedirectResponse("/auth/login?provider=local", status_code=302) + ctx = {**sidebar, "user": user} + template = env.get_template("workspaces.html") + return template.render(**ctx) + + +@router.get("/api/workspaces") +async def list_workspaces(request: Request): + """List all workspaces for the current user.""" + user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) + uid = user["id"] if user and user.get("id") else 1 + with get_conn() as conn: + rows = conn.execute( + "SELECT w.*, (SELECT COUNT(*) FROM pages WHERE workspace_id=w.id) as page_count " + "FROM workspaces w WHERE w.owner_id=? ORDER BY w.created_at DESC", + (uid,), + ).fetchall() + workspaces = [] + for r in rows: + d = dict(r) + workspaces.append(d) + active = _get_active_workspace(request) + return {"workspaces": workspaces, "active_id": active["id"] if active else None} + + +@router.post("/api/workspaces") +async def create_workspace(request: Request): + """Create a new workspace.""" + import json + body = await request.json() + name = body.get("name", "New Workspace").strip() + user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) + uid = user["id"] if user and user.get("id") else 1 + with get_conn() as conn: + cursor = conn.execute( + "INSERT INTO workspaces (name, owner_id) VALUES (?, ?)", + (name, uid), + ) + conn.commit() + ws_id = cursor.lastrowid + # Add owner as member + conn.execute( + "INSERT INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, 'owner')", + (ws_id, uid), + ) + conn.commit() + return {"id": ws_id, "name": name} + + +@router.put("/api/workspaces/{ws_id:int}") +async def rename_workspace(request: Request, ws_id: int): + """Rename a workspace.""" + import json + body = await request.json() + name = body.get("name", "").strip() + if not name: + return {"error": "Name required"} + with get_conn() as conn: + conn.execute("UPDATE workspaces SET name=? WHERE id=?", (name, ws_id)) + conn.commit() + return {"status": "ok"} + + +@router.delete("/api/workspaces/{ws_id:int}") +async def delete_workspace(request: Request, ws_id: int): + """Delete a workspace and all its pages.""" + with get_conn() as conn: + conn.execute("DELETE FROM pages WHERE workspace_id=?", (ws_id,)) + conn.execute("DELETE FROM workspace_members WHERE workspace_id=?", (ws_id,)) + conn.execute("DELETE FROM workspaces WHERE id=?", (ws_id,)) + conn.commit() + return {"status": "ok"} + + +@router.post("/api/workspaces/{ws_id:int}/select") +async def select_workspace(request: Request, ws_id: int): + """Set the active workspace via cookie.""" + from fastapi.responses import JSONResponse + response = JSONResponse({"status": "ok", "workspace_id": ws_id}) + response.set_cookie(WORKSPACE_COOKIE, str(ws_id), max_age=86400 * 30, httponly=True, path="/") + return response diff --git a/app/templates/base.html b/app/templates/base.html index 9b58d7d..3e2d3dd 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -85,16 +85,22 @@ diff --git a/app/templates/workspaces.html b/app/templates/workspaces.html new file mode 100644 index 0000000..f653f60 --- /dev/null +++ b/app/templates/workspaces.html @@ -0,0 +1,149 @@ +{% extends "base.html" %} +{% block page_title %}Workspaces{% endblock %} +{% block page_icon %}🏠{% endblock %} + +{% block topbar %} +
+
+ Home + Workspaces +
+
+ +
+
+{% endblock %} + +{% block content %} + + +
+ +
+

🏠 Workspaces

+ +
+ +
+ +
+ + +
+
+ + +
+
+

+ +
+ + +
+
+
+ +
+ + +{% endblock %}