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 = 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="/")
|
response.set_cookie("flowdeck_session", new_session, httponly=True, max_age=86400 * 7, samesite="lax", path="/")
|
||||||
return response
|
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':
|
case 'rename':
|
||||||
var newName = prompt('Rename:', name);
|
var newName = prompt('Rename:', name);
|
||||||
if (newName && newName.trim()) {
|
if (newName && newName.trim()) {
|
||||||
|
var self = this;
|
||||||
fetch('/api/local-workspace/items/' + id, {
|
fetch('/api/local-workspace/items/' + id, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
||||||
body: JSON.stringify({ name: newName.trim() })
|
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;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
@@ -970,7 +976,7 @@
|
|||||||
body: JSON.stringify({ name: name.trim(), type: 'folder' })
|
body: JSON.stringify({ name: name.trim(), type: 'folder' })
|
||||||
})
|
})
|
||||||
.then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); })
|
.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'); });
|
.catch(err => { this.toast('Failed: ' + err.message, 'error'); });
|
||||||
},
|
},
|
||||||
deleteWorkspacePage(pageId) {
|
deleteWorkspacePage(pageId) {
|
||||||
@@ -980,10 +986,25 @@
|
|||||||
headers: { 'X-CSRF-Token': this.getCsrfToken() }
|
headers: { 'X-CSRF-Token': this.getCsrfToken() }
|
||||||
})
|
})
|
||||||
.then(r => { if (!r.ok) throw new Error('Failed'); return r.json(); })
|
.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'); });
|
.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 ──
|
// ── Drag & Drop ──
|
||||||
dragOverId: null,
|
dragOverId: null,
|
||||||
dragStart(ev, id) {
|
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 ═══════════
|
// ═══════════ GLOBAL KEYBOARD SHORTCUTS ═══════════
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener('keydown', function(e) {
|
||||||
var tag = (document.activeElement && document.activeElement.tagName) || '';
|
var tag = (document.activeElement && document.activeElement.tagName) || '';
|
||||||
|
|||||||
@@ -1111,6 +1111,8 @@ window._wsInitData = (function() {
|
|||||||
_reloadAfterAction() {
|
_reloadAfterAction() {
|
||||||
this._saveExpandedState();
|
this._saveExpandedState();
|
||||||
this._loadFolder(this.currentFolder);
|
this._loadFolder(this.currentFolder);
|
||||||
|
// Notify sidebar to refresh its tree
|
||||||
|
document.dispatchEvent(new CustomEvent('flowdeck:sidebar-refresh'));
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Inline Rename ──
|
// ── Inline Rename ──
|
||||||
@@ -1555,6 +1557,8 @@ window._wsInitData = (function() {
|
|||||||
this.doFilter();
|
this.doFilter();
|
||||||
}
|
}
|
||||||
this._saveExpandedState();
|
this._saveExpandedState();
|
||||||
|
// Notify sidebar to refresh its tree
|
||||||
|
document.dispatchEvent(new CustomEvent('flowdeck:sidebar-refresh'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user