fix: sidebar Favorites + Shared, Library page with tabs, CSRF fix
- Section Favorites ajoutée dans le sidebar (⭐) - Section Shared ajoutée (👥) - Section Library remplacée par bouton 📚 qui ouvre /library - Page /library avec onglets: Recents | Favorites | Shared | Private - CSRF token injecté dans les appels fetch (newPage, newSubPage) - Bouton + sur éléments sidebar corrigé (POST /board/api/pages avec X-CSRF-Token) - Bouton 📚 sur sections ouvre /library - sidebar_data inclut favorite_pages=[]
This commit is contained in:
+19
-2
@@ -141,8 +141,8 @@ def _sidebar_data(request: Request, owner: str = "", repo: str = "") -> dict:
|
||||
})
|
||||
return {"workspace_name": ws, "workspace_initial": ws[0].upper() if ws else "B",
|
||||
"current_page": repo or "Dashboard", "last_edited": "now",
|
||||
"recent_pages": recent, "private_pages": [r for r in recent if r.get("active")],
|
||||
"user": user}
|
||||
"recent_pages": recent, "private_pages": [r for r in recent if r.get("active") or r["id"].startswith("page/")],
|
||||
"favorite_pages": [], "user": user}
|
||||
|
||||
|
||||
def _extract_ai_keywords(owner: str, repo: str, labels: list[dict], body: str = ""):
|
||||
@@ -241,6 +241,23 @@ def _apply_sorts(cards: list[dict], sorts: str) -> list[dict]:
|
||||
return cards
|
||||
|
||||
|
||||
# ═══════════ Library page ═══════════
|
||||
|
||||
@router.get("/library", response_class=HTMLResponse)
|
||||
async def library_page(request: Request):
|
||||
"""Library page avec onglets Recents, Favorites, Shared, Private."""
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
env = Environment(loader=FileSystemLoader("app/templates"))
|
||||
sidebar = _sidebar_data(request)
|
||||
# Enrich with timestamps
|
||||
for p in sidebar["recent_pages"]:
|
||||
p["last_edited"] = "now"
|
||||
for p in sidebar["private_pages"]:
|
||||
p["last_edited"] = "now"
|
||||
template = env.get_template("library.html")
|
||||
return template.render(**sidebar)
|
||||
|
||||
|
||||
# ═══════════ Board page ═══════════
|
||||
|
||||
@router.get("/{owner}/{repo}", response_class=HTMLResponse)
|
||||
|
||||
@@ -48,10 +48,25 @@ def _sidebar_data(request: Request, repos: list[dict]) -> dict:
|
||||
"current_page": "Dashboard", "last_edited": "now",
|
||||
"recent_pages": recent_pages,
|
||||
"private_pages": [p for p in recent_pages if p["id"].startswith("page/")],
|
||||
"favorite_pages": [],
|
||||
"user": user,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/library", response_class=HTMLResponse)
|
||||
async def library_page(request: Request):
|
||||
"""Library page avec onglets Recents, Favorites, Shared, Private."""
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
env = Environment(loader=FileSystemLoader("app/templates"))
|
||||
sidebar = _sidebar_data(request, [])
|
||||
for p in sidebar["recent_pages"]:
|
||||
p["last_edited"] = "now"
|
||||
for p in sidebar["private_pages"]:
|
||||
p["last_edited"] = "now"
|
||||
template = env.get_template("library.html")
|
||||
return template.render(**sidebar)
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def dashboard(
|
||||
request: Request,
|
||||
|
||||
+63
-15
@@ -120,21 +120,59 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Library -->
|
||||
<!-- Favorites -->
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-header">
|
||||
<div class="sidebar-section-title">
|
||||
<span class="chevron open">▶</span> Library
|
||||
<span class="chevron open">▶</span> Favorites
|
||||
</div>
|
||||
<div class="sidebar-section-actions">
|
||||
<button class="section-action-btn" title="Add page" @click="newPage('Favorites')">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="sidebar-items">
|
||||
<li class="sidebar-item"><span class="page-icon">✅</span><span class="page-name">My Tasks</span></li>
|
||||
<li class="sidebar-item"><span class="page-icon">🛒</span><span class="page-name">Marketplace</span></li>
|
||||
<li class="sidebar-item"><span class="page-icon">❓</span><span class="page-name">Help</span></li>
|
||||
<li class="sidebar-item"><span class="page-icon">🗑️</span><span class="page-name">Trash</span></li>
|
||||
{% for page in favorite_pages %}
|
||||
<li class="sidebar-item {% if page.active %}active{% endif %}"
|
||||
@click="navigateTo('{{ page.url }}')"
|
||||
@contextmenu.prevent="showContextMenu($event, '{{ page.id }}', '{{ page.name }}')">
|
||||
<span class="page-icon">{{ page.icon }}</span>
|
||||
<span class="page-name">{{ page.name }}</span>
|
||||
<span class="item-actions">
|
||||
<button class="item-action-btn" @click.stop="newSubPage('{{ page.id }}')">+</button>
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if not favorite_pages %}
|
||||
<li class="sidebar-item">
|
||||
<span class="page-icon">⭐</span>
|
||||
<span class="page-name text-dim">No favorites yet</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Shared -->
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section-header">
|
||||
<div class="sidebar-section-title">
|
||||
<span class="chevron">▶</span> Shared
|
||||
</div>
|
||||
</div>
|
||||
<ul class="sidebar-items">
|
||||
<li class="sidebar-item">
|
||||
<span class="page-icon">👥</span>
|
||||
<span class="page-name text-dim">No shared pages</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Library button -->
|
||||
<div class="sidebar-nav" style="margin-top: 8px;">
|
||||
<a href="/library" class="sidebar-nav-item" @click.prevent="navigateTo('/library')">
|
||||
<span class="nav-icon">📚</span> Library
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="sidebar-footer">
|
||||
<div class="sidebar-invite" x-show="showInvite" x-transition>
|
||||
@@ -326,23 +364,33 @@
|
||||
},
|
||||
|
||||
openLibrary(id) {
|
||||
// Open the library view for this section/page
|
||||
console.log('Open library for:', id);
|
||||
window.location.href = '/pages/' + id.replace('page/', '');
|
||||
window.location.href = '/library';
|
||||
},
|
||||
openInLibrary(id) { this.openLibrary(id); },
|
||||
showMore(id) { console.log('Show more:', id); },
|
||||
addPage(section) { this.newPage(section); },
|
||||
addSubPage(id) { this.newSubPage(id); },
|
||||
getCsrfToken() {
|
||||
const m = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
return m ? m[1] : '';
|
||||
},
|
||||
newPage(section) {
|
||||
fetch(`/board/api/pages?title=New%20page§ion=${encodeURIComponent(section)}`, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; });
|
||||
fetch(`/board/api/pages?title=New%20page§ion=${encodeURIComponent(section)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': this.getCsrfToken() }
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('CSRF or API error'); return r.json(); })
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; })
|
||||
.catch(err => { alert('Failed to create page: ' + err.message); });
|
||||
},
|
||||
newSubPage(parentId) {
|
||||
fetch(`/board/api/pages?title=New%20sub-page§ion=Private`, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; });
|
||||
fetch(`/board/api/pages?title=New%20page§ion=Private`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': this.getCsrfToken() }
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('CSRF or API error'); return r.json(); })
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; })
|
||||
.catch(err => { alert('Failed to create sub-page: ' + err.message); });
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
{% extends "base.html" %}
|
||||
{% block page_icon %}📚{% endblock %}
|
||||
{% block page_title %}Library{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-title-area">
|
||||
<div class="page-title">
|
||||
<span class="page-icon-lg">📚</span>
|
||||
<h1>Library</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Library tabs: Recents | Favorites | Shared | Private -->
|
||||
<div class="view-tabs" x-data="{ tab: 'recents' }">
|
||||
<button class="view-tab" :class="{ active: tab === 'recents' }" @click="tab = 'recents'">
|
||||
<span class="tab-icon">🕒</span> Recents
|
||||
</button>
|
||||
<button class="view-tab" :class="{ active: tab === 'favorites' }" @click="tab = 'favorites'">
|
||||
<span class="tab-icon">⭐</span> Favorites
|
||||
</button>
|
||||
<button class="view-tab" :class="{ active: tab === 'shared' }" @click="tab = 'shared'">
|
||||
<span class="tab-icon">👥</span> Shared
|
||||
</button>
|
||||
<button class="view-tab" :class="{ active: tab === 'private' }" @click="tab = 'private'">
|
||||
<span class="tab-icon">🔒</span> Private
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="view-toolbar">
|
||||
<div class="view-toolbar-spacer"></div>
|
||||
<button class="btn-new" @click="newPage()">New page</button>
|
||||
</div>
|
||||
|
||||
<!-- Library content -->
|
||||
<div x-data="libraryData()" style="padding: 0 24px;">
|
||||
<!-- Table headers -->
|
||||
<div style="display:flex; padding: 8px 12px; border-bottom:1px solid var(--border); font-size:12px; font-weight:600; color:var(--text-dim);">
|
||||
<span style="flex:1;">Page name</span>
|
||||
<span style="width:160px;">Last edited time</span>
|
||||
</div>
|
||||
|
||||
<!-- Recents tab -->
|
||||
<div x-show="tab === 'recents'">
|
||||
{% for page in recent_pages %}
|
||||
<a href="{{ page.url }}" style="display:flex; align-items:center; padding:8px 12px; text-decoration:none; color:var(--text-primary); border-radius:6px; transition:background 0.15s;"
|
||||
onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background='transparent'">
|
||||
<span style="font-size:16px; margin-right:10px;">{{ page.icon }}</span>
|
||||
<span style="flex:1;">{{ page.name }}</span>
|
||||
<span style="width:160px; font-size:13px; color:var(--text-dim);">{{ page.last_edited }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% if not recent_pages %}
|
||||
<div style="text-align:center; padding:48px; color:var(--text-dim);">
|
||||
<div style="font-size:48px; margin-bottom:16px;">🕒</div>
|
||||
<h3>No recent pages</h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Favorites tab -->
|
||||
<div x-show="tab === 'favorites'">
|
||||
{% for page in favorite_pages %}
|
||||
<a href="{{ page.url }}" style="display:flex; align-items:center; padding:8px 12px; text-decoration:none; color:var(--text-primary); border-radius:6px;"
|
||||
onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background='transparent'">
|
||||
<span style="font-size:16px; margin-right:10px;">{{ page.icon }}</span>
|
||||
<span style="flex:1;">{{ page.name }}</span>
|
||||
<span style="width:160px; font-size:13px; color:var(--text-dim);">{{ page.last_edited }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% if not favorite_pages %}
|
||||
<div style="text-align:center; padding:48px; color:var(--text-dim);">
|
||||
<div style="font-size:48px; margin-bottom:16px;">⭐</div>
|
||||
<h3>No favorites</h3>
|
||||
<p style="margin-top:8px;">Right-click a page in the sidebar and select "Add to Favorites".</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Shared tab -->
|
||||
<div x-show="tab === 'shared'">
|
||||
<div style="text-align:center; padding:48px; color:var(--text-dim);">
|
||||
<div style="font-size:48px; margin-bottom:16px;">👥</div>
|
||||
<h3>No shared pages</h3>
|
||||
<p style="margin-top:8px;">Shared pages from your team will appear here.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Private tab -->
|
||||
<div x-show="tab === 'private'">
|
||||
{% for page in private_pages %}
|
||||
<a href="{{ page.url }}" style="display:flex; align-items:center; padding:8px 12px; text-decoration:none; color:var(--text-primary); border-radius:6px;"
|
||||
onmouseover="this.style.background='var(--bg-hover)'" onmouseout="this.style.background='transparent'">
|
||||
<span style="font-size:16px; margin-right:10px;">{{ page.icon }}</span>
|
||||
<span style="flex:1;">{{ page.name }}</span>
|
||||
<span style="width:160px; font-size:13px; color:var(--text-dim);">{{ page.last_edited }}</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% if not private_pages %}
|
||||
<div style="text-align:center; padding:48px; color:var(--text-dim);">
|
||||
<div style="font-size:48px; margin-bottom:16px;">🔒</div>
|
||||
<h3>No private pages</h3>
|
||||
<p style="margin-top:8px;">Create a new page with the + button in the sidebar.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function libraryData() {
|
||||
return {
|
||||
get tab() { return document.querySelector('[x-data]').__x.$data.tab; }
|
||||
};
|
||||
}
|
||||
|
||||
function newPage() {
|
||||
const csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
||||
const token = csrf ? csrf[1] : '';
|
||||
fetch('/board/api/pages?title=New%20page§ion=Private', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': token }
|
||||
})
|
||||
.then(r => { if (!r.ok) throw new Error('CSRF error'); return r.json(); })
|
||||
.then(data => { window.location.href = `/pages/${data.id}`; })
|
||||
.catch(err => { alert('Failed: ' + err.message); });
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user