From 6f40c8953a4d62fee5f502d4d60a9703d0ade16a Mon Sep 17 00:00:00 2001 From: bruno Date: Thu, 16 Jul 2026 08:53:44 -0400 Subject: [PATCH] feat: sidebar tree dynamically refreshes after CRUD in local-workspace - New endpoint GET /api/sidebar/workspace-tree returns sidebar tree HTML fragment - refreshSidebarTree() in appState() fetches and replaces #sidebar-workspace-items - Custom event 'flowdeck:sidebar-refresh' dispatched after doCreate/doRename/doDelete - newFolderInWorkspace, deleteWorkspacePage, wsCtxAction('rename') now use refreshSidebarTree() instead of window.location.reload() - Sidebar stays in sync without full page refresh --- app/routers/dashboard.py | 60 ++++++++++++++++++++++++++++++ app/templates/base.html | 34 +++++++++++++++-- app/templates/local_workspace.html | 4 ++ 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index b0d73f7..789683b 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -1507,3 +1507,63 @@ async def update_account(request: Request): response = JSONResponse({"status": "ok", "user": {k: user_data[k] for k in ("id","login","full_name","email","is_admin")}}) response.set_cookie("flowdeck_session", new_session, httponly=True, max_age=86400 * 7, samesite="lax", path="/") return response + + +# ═══════════ Sidebar Refresh API ═══════════ + +@router.get("/api/sidebar/workspace-tree") +async def sidebar_workspace_tree(request: Request): + """Return the sidebar workspace tree as HTML fragment. + + Called by appState().refreshSidebarTree() after CRUD operations + in the main content area to keep the sidebar in sync. + """ + from jinja2 import Environment, FileSystemLoader + from app.routers.board import _load_workspace_pages + + user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) + if not user: + return HTMLResponse("") + + ws_cookie = request.cookies.get("flowdeck_workspace", "") + if not ws_cookie: + return HTMLResponse('') + + # Gitea workspace — no server-side tree, loaded client-side + if ws_cookie.startswith("gitea:"): + return HTMLResponse('') + + try: + ws_id = int(ws_cookie) + with get_conn() as conn: + # Verify workspace belongs to user + row = conn.execute( + "SELECT id FROM workspaces WHERE id=? AND owner_id=?", + (ws_id, user["id"]) + ).fetchone() + if not row: + return HTMLResponse( + '' + ) + + pages = _load_workspace_pages(ws_cookie) + if not pages: + return HTMLResponse( + '' + ) + + # Render the tree using the same Jinja2 macro as base.html + env = Environment(loader=FileSystemLoader("app/templates")) + template = env.from_string( + "{% from 'base.html' import render_workspace_tree %}" + "{{ render_workspace_tree(pages) }}" + ) + html = template.render(pages=pages) + return HTMLResponse(html) + except (ValueError, Exception): + return HTMLResponse( + '' + ) diff --git a/app/templates/base.html b/app/templates/base.html index 46ff33a..5c1d9f1 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -636,11 +636,17 @@ case 'rename': var newName = prompt('Rename:', name); if (newName && newName.trim()) { + var self = this; fetch('/api/local-workspace/items/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() }, body: JSON.stringify({ name: newName.trim() }) - }).then(function(r) { if (r.ok) window.location.reload(); }); + }).then(function(r) { + if (r.ok) { + self.refreshSidebarTree(); + document.dispatchEvent(new CustomEvent('flowdeck:sidebar-refresh')); + } + }); } break; case 'delete': @@ -970,7 +976,7 @@ body: JSON.stringify({ name: name.trim(), type: 'folder' }) }) .then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); }) - .then(() => { window.location.reload(); }) + .then(() => { this.refreshSidebarTree(); }) .catch(err => { this.toast('Failed: ' + err.message, 'error'); }); }, deleteWorkspacePage(pageId) { @@ -980,10 +986,25 @@ headers: { 'X-CSRF-Token': this.getCsrfToken() } }) .then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); }) - .then(() => { window.location.reload(); }) + .then(() => { this.refreshSidebarTree(); document.dispatchEvent(new CustomEvent('flowdeck:sidebar-refresh')); }) .catch(err => { this.toast('Failed: ' + err.message, 'error'); }); }, + refreshSidebarTree() { + // Reload the sidebar workspace tree after CRUD in the main content area + fetch('/api/sidebar/workspace-tree') + .then(r => r.text()) + .then(html => { + const ul = document.getElementById('sidebar-workspace-items'); + if (!ul) return; + // Preserve the "Manage Workspaces" link (last
  • ) + const manageLink = ul.querySelector('li:last-child'); + ul.innerHTML = html; + if (manageLink) ul.appendChild(manageLink); + }) + .catch(e => console.error('Sidebar refresh failed:', e)); + }, + // ── Drag & Drop ── dragOverId: null, dragStart(ev, id) { @@ -1176,6 +1197,13 @@ } }; + // -- Global sidebar refresh handler (called by local_workspace after CRUD) -- + document.addEventListener('flowdeck:sidebar-refresh', function() { + var appEl = document.querySelector('[x-data]'); + var data = appEl && appEl.__x ? appEl.__x.$data : null; + if (data && data.refreshSidebarTree) data.refreshSidebarTree(); + }); + // ═══════════ GLOBAL KEYBOARD SHORTCUTS ═══════════ document.addEventListener('keydown', function(e) { var tag = (document.activeElement && document.activeElement.tagName) || ''; diff --git a/app/templates/local_workspace.html b/app/templates/local_workspace.html index d16f023..47f57cd 100644 --- a/app/templates/local_workspace.html +++ b/app/templates/local_workspace.html @@ -1111,6 +1111,8 @@ window._wsInitData = (function() { _reloadAfterAction() { this._saveExpandedState(); this._loadFolder(this.currentFolder); + // Notify sidebar to refresh its tree + document.dispatchEvent(new CustomEvent('flowdeck:sidebar-refresh')); }, // ── Inline Rename ── @@ -1555,6 +1557,8 @@ window._wsInitData = (function() { this.doFilter(); } this._saveExpandedState(); + // Notify sidebar to refresh its tree + document.dispatchEvent(new CustomEvent('flowdeck:sidebar-refresh')); } },