fix: login → /workspaces, workspace tree filtered, sidebar CRUD
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

- Login redirect (all modes) now goes to /workspaces
- Workspace tree API filters by active workspace_id (no leak)
- Workspace name in sidebar from cookie (active_ws_name)
- Sidebar workspace section: 📄 new page + 📁 new folder buttons
- deleteWorkspacePage() in sidebar with confirmation
- _load_workspace_pages() helper with workspace_id filter
- CSS for sidebar-item-delete button (× on hover)
This commit is contained in:
2026-07-10 19:26:03 -04:00
parent ae0b964859
commit 1ae4aa4ab4
4 changed files with 59 additions and 9 deletions
+2 -2
View File
@@ -99,7 +99,7 @@ async def login(request: Request, provider: str = Query("gitea")):
user = conn.execute("SELECT * FROM users WHERE login='admin'").fetchone()
user_data = dict(user)
session = SessionManager.create_session(user_data)
response = RedirectResponse(url="/", status_code=302)
response = RedirectResponse(url="/workspaces", status_code=302)
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax")
return response
@@ -265,7 +265,7 @@ async def callback(
# Create session
session = SessionManager.create_session(user_data)
response = RedirectResponse(url="/", status_code=302)
response = RedirectResponse(url="/workspaces", status_code=302)
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax")
return response
+17
View File
@@ -140,6 +140,22 @@ def _build_page_tree(conn, parent_id: int | None, ws_key: str, depth: int = 0, m
return items
def _load_workspace_pages(ws_cookie: str) -> list:
"""Load top-level pages for the active workspace."""
if not ws_cookie:
return []
try:
ws_id = int(ws_cookie)
with get_conn() as conn:
rows = conn.execute(
"SELECT id, title FROM pages WHERE workspace_id=? AND parent_id IS NULL ORDER BY created_at DESC",
(ws_id,),
).fetchall()
return [{"db_id": r["id"], "name": r["title"] or "Untitled", "id": f"page/{r['id']}"} for r in rows]
except (ValueError, Exception):
return []
def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
ws_name = user.get("login", "Bruno") if user else "Bruno"
@@ -229,6 +245,7 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
return {"workspace_name": ws_name, "workspace_initial": ws_name[0].upper() if ws_name else "B",
"active_ws_name": active_ws_name,
"workspace_pages": _load_workspace_pages(ws_cookie),
"current_page": repo or "Dashboard", "last_edited": "now",
"recent_pages": recent, "private_pages": private_items,
"favorite_pages": favorites, "shared_pages": shared_pages,
+11 -5
View File
@@ -482,10 +482,13 @@ async def local_workspace_page(request: Request):
if not user:
return RedirectResponse("/auth/login?provider=local", status_code=302)
ws = _get_active_workspace(request)
ws_id = ws["id"] if ws else None
with get_conn() as conn:
rows = conn.execute(
"SELECT id, title FROM pages WHERE parent_id IS NULL ORDER BY created_at DESC"
).fetchall()
"SELECT id, title FROM pages WHERE parent_id IS NULL AND workspace_id=? ORDER BY created_at DESC",
(ws_id,),
).fetchall() if ws_id else []
items = [{"id": r["id"], "name": r["title"] or "Untitled", "type": "page"} for r in rows]
ctx = {**sidebar, "user": user, "workspace_name": sidebar.get("active_ws_name", "My Workspace"), "items": items}
@@ -495,11 +498,14 @@ async def local_workspace_page(request: Request):
@router.get("/api/local-workspace/tree")
async def local_workspace_tree(request: Request):
"""Return the file/folder tree."""
"""Return the file/folder tree filtered by active workspace."""
ws = _get_active_workspace(request)
ws_id = ws["id"] if ws else None
with get_conn() as conn:
roots = conn.execute(
"SELECT id, title FROM pages WHERE parent_id IS NULL ORDER BY created_at DESC"
).fetchall()
"SELECT id, title FROM pages WHERE parent_id IS NULL AND workspace_id=? ORDER BY created_at DESC",
(ws_id,),
).fetchall() if ws_id else []
tree = []
for r in roots:
with get_conn() as conn:
+29 -2
View File
@@ -88,13 +88,18 @@
<span>📁 {{ active_ws_name or 'Workspace' }}</span>
</div>
<div class="sidebar-section-actions">
<button class="section-action-btn" title="New Page" @click.stop="newPageInWorkspace()">+</button>
<button class="section-action-btn" title="New Page" @click.stop="newPageInWorkspace()">📄</button>
<button class="section-action-btn" title="New Folder" @click.stop="newFolderInWorkspace()">📁</button>
</div>
</div>
<div class="sidebar-section-items" x-show="sectionsOpen.workspace" x-transition>
<ul class="sidebar-items" data-section="workspace">
{% for page in workspace_pages %}
{{ render_tree_item(page) }}
<li class="sidebar-item" data-id="{{ page.db_id }}">
<span class="page-icon">📄</span>
<a href="/pages/{{ page.db_id }}" class="page-name">{{ page.name }}</a>
<button class="sidebar-item-delete" onclick="event.preventDefault();app.deleteWorkspacePage({{ page.db_id }})" title="Delete">×</button>
</li>
{% endfor %}
{% if not workspace_pages %}
<li class="sidebar-item empty-hint"><span class="page-icon">📄</span><span class="page-name text-dim">No pages yet</span></li>
@@ -637,6 +642,28 @@
.then(data => { window.location.href = `/pages/${data.id}`; })
.catch(err => { alert('Failed: ' + err.message); });
},
newFolderInWorkspace() {
const name = prompt('Folder name:');
if (!name || !name.trim()) return;
fetch('/api/local-workspace/items', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
body: JSON.stringify({ name: name.trim(), type: 'folder' })
})
.then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); })
.then(() => { window.location.reload(); })
.catch(err => { alert('Failed: ' + err.message); });
},
deleteWorkspacePage(pageId) {
if (!confirm('Delete this page?')) return;
fetch(`/api/local-workspace/items/${pageId}`, {
method: 'DELETE',
headers: { 'X-CSRF-Token': this.getCsrfToken() }
})
.then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); })
.then(() => { window.location.reload(); })
.catch(err => { alert('Failed: ' + err.message); });
},
createEmptyPage() {
this.showNewPageMenu = false;