feat: workspaces system — create, select, rename, delete workspaces
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

- 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:
2026-07-10 16:41:40 -04:00
parent eb417d4ed5
commit 3e6323a856
4 changed files with 279 additions and 4 deletions
+115
View File
@@ -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