feat: workspaces system — create, select, rename, delete workspaces
- 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
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user