feat(v1.3.0): Step 2 — DB tables collections, collection_pages, collection_views
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

- Ajout des 3 nouvelles tables pour le Database Concept
- collections: base de données abstraite, indépendante de Gitea
- collection_pages: pages génériques avec parent_id et property_values_json
- collection_views: vues configurables par collection
- Mise à jour test_db_tables_exist pour inclure les nouvelles tables
- 28/28 tests passent
This commit is contained in:
2026-07-09 22:26:45 -04:00
parent 66ae1289a9
commit 3967c7df85
8 changed files with 285 additions and 69 deletions
+1 -1
View File
@@ -202,7 +202,7 @@
<ul class="sidebar-items">
<li class="sidebar-item" @click="navigateTo('/library')"><span class="page-icon">📚</span><span class="page-name">Library</span></li>
<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">Trash</span></li>
<li class="sidebar-item" @click="navigateTo('/trash')"><span class="page-icon">🗑️</span><span class="page-name">Trash</span></li>
<li class="sidebar-item"><span class="page-icon">❓</span><span class="page-name">Help</span></li>
</ul>
</div>
+46 -55
View File
@@ -10,7 +10,7 @@
</div>
</div>
<!-- Library tabs: Recents | Favorites | Shared | Private -->
<!-- Library tabs -->
<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
@@ -29,78 +29,76 @@
<!-- 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>
<!-- 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>
<!-- Table -->
<div x-data="{ tab: 'recents' }" 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>
<!-- Recents tab -->
{% 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 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 %}
{% for page in recent_pages %}{{ page_row(page) }}{% 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>
<div class="lib-empty">
<div style="font-size:48px;margin-bottom:16px;">🕒</div>
<h3>No recent pages</h3>
</div>
{% endif %}
</div>
<!-- Favorites tab -->
<!-- Favorites -->
<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 %}
{% for page in favorite_pages %}{{ page_row(page) }}{% 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>
<div class="lib-empty">
<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>
<p>Right-click a page in the sidebar and select "Add to Favorites".</p>
</div>
{% endif %}
</div>
<!-- Shared tab -->
<!-- Shared -->
<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>
<div class="lib-empty">
<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>
<p>Shared pages from your team will appear here.</p>
</div>
</div>
<!-- Private tab -->
<!-- Private -->
<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 %}
{% for page in private_pages %}{{ page_row(page) }}{% 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>
<div class="lib-empty">
<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>
<p>Create a new page with the + button in the sidebar.</p>
</div>
{% endif %}
</div>
@@ -109,18 +107,11 @@
{% 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&section=Private', {
fetch('/board/api/pages?section=Private', {
method: 'POST',
headers: { 'X-CSRF-Token': token }
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}`; })
+95
View File
@@ -0,0 +1,95 @@
{% extends "base.html" %}
{% block page_icon %}🗑️{% endblock %}
{% block page_title %}Trash{% endblock %}
{% block content %}
<div class="page-title-area">
<div class="page-title">
<span class="page-icon-lg">🗑️</span>
<h1>Trash</h1>
</div>
</div>
<div x-data="trashData()" style="padding: 0 24px; max-width: 800px;">
<!-- Search -->
<div style="position:relative; margin-bottom:12px;">
<span style="position:absolute; left:12px; top:50%; transform:translateY(-50%); color:var(--text-dim);">🔍</span>
<input type="text" placeholder="Search pages in Trash" x-model="search"
style="width:100%; padding:8px 12px 8px 36px; background:var(--bg-tertiary); border:1px solid var(--border); border-radius:6px; color:var(--text-primary); font-size:14px; outline:none; box-sizing:border-box;">
</div>
<!-- Filters -->
<div style="display:flex; gap:8px; margin-bottom:16px;">
<button class="trash-filter active">
<span style="color:#5b9bd5;">👤</span> Last edited by ▾
</button>
<button class="trash-filter">
<span>📁</span> In ▾
</button>
</div>
<!-- Items -->
<div style="min-height:200px;">
<template x-for="item in filteredItems" :key="item.id">
<div class="trash-item">
<span class="trash-item-icon" x-text="item.icon"></span>
<div class="trash-item-info">
<span class="trash-item-name" x-text="item.name"></span>
<span class="trash-item-path" x-text="item.path"></span>
</div>
<button class="trash-item-btn" title="Restore" @click="restore(item.id)">
↩️
</button>
<button class="trash-item-btn" title="Delete permanently" @click="deleteForever(item.id)">
🗑️
</button>
</div>
</template>
<div x-show="filteredItems.length === 0" class="lib-empty">
<div style="font-size:48px;margin-bottom:16px;">🗑️</div>
<h3>Trash is empty</h3>
<p>Deleted pages will appear here for 30 days.</p>
</div>
</div>
<!-- Info -->
<div style="padding:12px 16px; margin-top:16px; background:var(--bg-tertiary); border-radius:8px; font-size:13px; color:var(--text-dim); display:flex; align-items:center; gap:8px;">
<span>Once a page has been in Trash for 30 days, it will be automatically deleted</span>
<span style="font-size:16px;">ⓘ</span>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function trashData() {
return {
search: '',
items: [],
async init() {
const csrf = document.cookie.match(/csrf_token=([^;]+)/);
const token = csrf ? csrf[1] : '';
try {
const r = await fetch('/board/api/trash', { headers: { 'X-CSRF-Token': token } });
this.items = await r.json();
} catch(e) { this.items = []; }
},
get filteredItems() {
const q = this.search.toLowerCase();
return this.items.filter(i => !q || i.name.toLowerCase().includes(q) || (i.path||'').toLowerCase().includes(q));
},
async restore(id) {
const csrf = document.cookie.match(/csrf_token=([^;]+)/);
const r = await fetch(`/board/api/trash/${id}/restore`, { method: 'POST', headers: { 'X-CSRF-Token': csrf?.[1] || '' } });
if (r.ok) { this.items = this.items.filter(i => i.id !== id); }
},
async deleteForever(id) {
if (!confirm('Permanently delete this page? This cannot be undone.')) return;
const csrf = document.cookie.match(/csrf_token=([^;]+)/);
const r = await fetch(`/board/api/trash/${id}`, { method: 'DELETE', headers: { 'X-CSRF-Token': csrf?.[1] || '' } });
if (r.ok) { this.items = this.items.filter(i => i.id !== id); }
}
};
}
</script>
{% endblock %}