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
This commit is contained in:
@@ -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
@@ -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) || '';
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user