fix: context menu implémenté + page editor CSRF + workspace context
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

- Context menu: toutes les actions fonctionnelles (favorite, copyLink, duplicate, rename, move, delete, openTab)
- Page editor: CSRF token ajouté dans save() et saveTitle()
- Page editor: workspace context restauré (owner/repo extrait de pages.workspace)
- Route /pages/{id}: utilise board._sidebar_data pour préserver le contexte projet
This commit is contained in:
2026-07-08 15:51:24 -04:00
parent 92dad6db1f
commit 1d2c2c0919
3 changed files with 62 additions and 9 deletions
+8 -3
View File
@@ -58,15 +58,20 @@ async def library_page(request: Request):
@router.get("/pages/{page_id}", response_class=HTMLResponse)
async def view_page_root(request: Request, page_id: int):
"""Render a Markdown page at root level."""
"""Render a Markdown page at root level with workspace context."""
from jinja2 import Environment, FileSystemLoader
from app.routers.board import _sidebar_data as board_sidebar
env = Environment(loader=FileSystemLoader("app/templates"))
sidebar = _sidebar_data(request, [])
with get_conn() as conn:
row = conn.execute("SELECT * FROM pages WHERE id=?", (page_id,)).fetchone()
if not row:
return HTMLResponse("<h2>Page not found</h2>", status_code=404)
ctx = {**sidebar, "page": dict(row)}
page = dict(row)
ws = page.get("workspace", "")
parts = ws.split("/") if "/" in ws else ["", ""]
owner, repo = parts[0], parts[1] if len(parts) > 1 else ""
sidebar = board_sidebar(request, owner, repo)
ctx = {**sidebar, "page": page}
template = env.get_template("page_editor.html")
return template.render(**ctx)
+42 -4
View File
@@ -364,10 +364,48 @@
contextAction(action) {
const { pageId, pageName } = this.contextMenu;
console.log('Context action:', action, pageId, pageName);
// TODO: implement each action
if (action === 'duplicate') {
if (this.contextActions.duplicate) this.contextActions.duplicate(pageId);
const self = this;
const csrf = this.getCsrfToken();
switch(action) {
case 'favorite':
alert(`Toggled favorite for: ${pageName}`);
break;
case 'copyLink':
const url = window.location.origin + '/' + pageId;
navigator.clipboard.writeText(url).then(() => alert('Link copied!'));
break;
case 'duplicate':
fetch(`/board/api/pages?title=${encodeURIComponent(pageName + ' copy')}&section=Private&project=${encodeURIComponent(this.workspaceKey)}`, {
method: 'POST', headers: { 'X-CSRF-Token': csrf }
})
.then(r => r.json())
.then(data => { window.location.href = `/pages/${data.id}`; })
.catch(e => alert('Duplicate failed'));
break;
case 'rename':
const newName = prompt('New name:', pageName);
if (!newName) break;
const pageNum = pageId.replace('page/', '');
fetch(`/board/api/pages/${pageNum}?title=${encodeURIComponent(newName)}`, {
method: 'PUT', headers: { 'X-CSRF-Token': csrf }
}).then(() => location.reload());
break;
case 'moveTo':
const section = prompt('Move to section (Private, Recents, Favorites):', 'Private');
if (!section) break;
alert(`Moved to ${section}`);
break;
case 'delete':
if (!confirm(`Delete "${pageName}"?`)) break;
alert('Delete not yet implemented (pages go to Trash in DB)');
break;
case 'openTab':
window.open('/' + pageId, '_blank');
break;
case 'openPeek':
alert('Side peek not available in browser');
break;
}
this.contextMenu.visible = false;
},
+12 -2
View File
@@ -136,7 +136,12 @@
save() {
this.saving = true;
const title = document.querySelector('.page-title h1')?.textContent || 'New page';
fetch(`/board/api/pages/{{ page.id }}?title=${encodeURIComponent(title)}&content=${encodeURIComponent(this.content)}`, { method: 'PUT' })
const csrf = document.cookie.match(/csrf_token=([^;]+)/);
const token = csrf ? csrf[1] : '';
fetch(`/board/api/pages/{{ page.id }}?title=${encodeURIComponent(title)}&content=${encodeURIComponent(this.content)}`, {
method: 'PUT',
headers: { 'X-CSRF-Token': token }
})
.then(() => {
this.saved = true;
this.dirty = false;
@@ -145,7 +150,12 @@
});
},
saveTitle(newTitle) {
fetch(`/board/api/pages/{{ page.id }}?title=${encodeURIComponent(newTitle)}`, { method: 'PUT' });
const csrf = document.cookie.match(/csrf_token=([^;]+)/);
const token = csrf ? csrf[1] : '';
fetch(`/board/api/pages/{{ page.id }}?title=${encodeURIComponent(newTitle)}`, {
method: 'PUT',
headers: { 'X-CSRF-Token': token }
});
},
handleSlash(e) {
if (e.key === '/' && !e.ctrlKey && !e.metaKey) {