🧪 Tests: 73 → 130 (+57 tests) - Upload API, labels sync, commit history (401/400/502/cached) - File create/update (sha=null, sha=abc) - Admin delete cascade (12 tests: oauth, tags, workspaces, pages…) - Gitea status (linked/unlinked/disconnect) - OAuth link mode (session, redirect, callback) 🔒 Sécurité - ContentSecurityPolicyMiddleware (CSP headers) - RateLimitMiddleware (100 req/min/IP) - Pydantic models: ErrorResponse, SuccessResponse - Input validation upload (10MB, allowed extensions) - Pydantic request models 🎨 UX - static/css/design-tokens.css (500 lines, thèmes + skeletons) - Toast system: 16 alert() remplacés par toast() - 59 lignes CSS dupliquées retirées (6 templates) - Skeletons cohérents sur toutes les vues ⚡ Performance - Cache TTL sur get_file_commits (consistant avec les autres méthodes)
132 lines
4.3 KiB
HTML
132 lines
4.3 KiB
HTML
{% 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 + table — single Alpine scope -->
|
|
<div x-data="{ tab: 'recents' }">
|
|
|
|
<div class="view-tabs">
|
|
<button class="view-tab" :class="{ active: tab === 'recents' }" @click="tab = 'recents'">
|
|
<span class="tab-icon">🕒</span> Recents
|
|
</button>
|
|
{% if lib_has_favorites %}
|
|
<button class="view-tab" :class="{ active: tab === 'favorites' }" @click="tab = 'favorites'">
|
|
<span class="tab-icon">⭐</span> Favorites
|
|
</button>
|
|
{% endif %}
|
|
{% if lib_has_shared %}
|
|
<button class="view-tab" :class="{ active: tab === 'shared' }" @click="tab = 'shared'">
|
|
<span class="tab-icon">👥</span> Shared
|
|
</button>
|
|
{% endif %}
|
|
<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="toolbar-btn" title="Filter">☰</button>
|
|
<button class="toolbar-btn" title="Search">🔍</button>
|
|
<button class="toolbar-btn" title="Properties">⫶</button>
|
|
<button class="btn-new" @click="newPage()">New page</button>
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
<div style="padding: 0 24px;">
|
|
<!-- Column headers -->
|
|
<div class="library-table-header">
|
|
<span class="lib-col lib-col-name">📄 Page name</span>
|
|
<span class="lib-col lib-col-author">👤 Created by</span>
|
|
<span class="lib-col lib-col-source">↗ Source</span>
|
|
<span class="lib-col lib-col-edited">🕒 Last edited time</span>
|
|
<span class="lib-col lib-col-visited">🕒 Last visited time</span>
|
|
</div>
|
|
|
|
{% macro page_row(page) %}
|
|
<a href="{{ page.url }}" class="library-table-row">
|
|
<span class="lib-col lib-col-name">
|
|
<span class="page-icon">{{ page.icon }}</span>
|
|
<span>{{ page.name }}</span>
|
|
</span>
|
|
<span class="lib-col lib-col-author">👤 {{ page.created_by or 'You' }}</span>
|
|
<span class="lib-col lib-col-source">{{ page.source or '🔒 Private' }}</span>
|
|
<span class="lib-col lib-col-edited">{{ page.last_edited }}</span>
|
|
<span class="lib-col lib-col-visited">{{ page.last_visited or page.last_edited }}</span>
|
|
</a>
|
|
{% endmacro %}
|
|
|
|
<!-- Recents -->
|
|
<div x-show="tab === 'recents'">
|
|
{% for page in lib_recent %}{{ page_row(page) }}{% endfor %}
|
|
{% if not lib_recent %}
|
|
<div class="lib-empty">
|
|
<div style="font-size:48px;margin-bottom:16px;">🕒</div>
|
|
<h3>No recent pages</h3>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Favorites -->
|
|
<div x-show="tab === 'favorites'">
|
|
{% for page in lib_favorites %}{{ page_row(page) }}{% endfor %}
|
|
{% if not lib_favorites %}
|
|
<div class="lib-empty">
|
|
<div style="font-size:48px;margin-bottom:16px;">⭐</div>
|
|
<h3>No favorites</h3>
|
|
<p>Right-click a page in the sidebar and select "Add to Favorites".</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Shared -->
|
|
<div x-show="tab === 'shared'">
|
|
{% for page in lib_shared %}{{ page_row(page) }}{% endfor %}
|
|
{% if not lib_shared %}
|
|
<div class="lib-empty">
|
|
<div style="font-size:48px;margin-bottom:16px;">👥</div>
|
|
<h3>No shared pages</h3>
|
|
<p>Shared pages from your team will appear here.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Private -->
|
|
<div x-show="tab === 'private'">
|
|
{% for page in lib_private %}{{ page_row(page) }}{% endfor %}
|
|
{% if not lib_private %}
|
|
<div class="lib-empty">
|
|
<div style="font-size:48px;margin-bottom:16px;">🔒</div>
|
|
<h3>No private pages</h3>
|
|
<p>Create a new page with the + button in the sidebar.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function newPage() {
|
|
const csrf = document.cookie.match(/csrf_token=([^;]+)/);
|
|
fetch('/board/api/pages?section=Private', {
|
|
method: 'POST',
|
|
headers: { 'X-CSRF-Token': csrf ? csrf[1] : '' }
|
|
})
|
|
.then(r => { if (!r.ok) throw new Error('CSRF error'); return r.json(); })
|
|
.then(data => { window.location.href = `/pages/${data.id}`; })
|
|
.catch(err => { window.showToast('Failed: ' + err.message, 'error'); });
|
|
}
|
|
</script>
|
|
{% endblock %}
|