feat: sidebar tree dynamically refreshes after CRUD in local-workspace
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

- 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
This commit is contained in:
2026-07-16 08:53:44 -04:00
parent 72636a77ea
commit 6f40c8953a
3 changed files with 95 additions and 3 deletions
+60
View File
@@ -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('<li class="sidebar-item empty-hint"><span class="page-icon">📄</span><span class="page-name text-dim">No pages yet</span></li>')
# Gitea workspace — no server-side tree, loaded client-side
if ws_cookie.startswith("gitea:"):
return HTMLResponse('<li class="sidebar-item empty-hint"><span class="page-icon">🔗</span><span class="page-name text-dim">Remote workspace</span></li>')
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(
'<li class="sidebar-item empty-hint"><span class="page-icon">📄</span>'
'<span class="page-name text-dim">No pages yet</span></li>'
)
pages = _load_workspace_pages(ws_cookie)
if not pages:
return HTMLResponse(
'<li class="sidebar-item empty-hint"><span class="page-icon">📄</span>'
'<span class="page-name text-dim">No pages yet</span></li>'
)
# 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(
'<li class="sidebar-item empty-hint"><span class="page-icon">📄</span>'
'<span class="page-name text-dim">No pages yet</span></li>'
)
+31 -3
View File
@@ -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 <li>)
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) || '';
+4
View File
@@ -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'));
}
},