Ajout : - e2e/csp_preview.spec.js — apercu CSP strict SANS deployer : le build officiel @alpinejs/csp (e2e/fixtures/alpine.csp.js, 0 eval) est servi a la place de alpine.min.js par interception Playwright ; toute expression que le parseur maison ne digere pas = pageerror (filet). Premiere surface VERTE : library (composant lie, icones SVG via Alpine.effect, recherche ouverte + focalisee, 0 erreur). Changed : - 16 composants x-data="fn()" enregistres via Alpine.data (registre = seule resolution du build CSP, probe « Undefined variable » ; scripts classiques executes pendant le parsing => alpine:init toujours joint) : appState, libraryPage, workspacesPage, editorState, board x4, settings/import/table_view/team_load/trash/workspace/welcome/accounts/ card_detail. - base.html (shell) migre : x-effect document.* -> syncSidebarClass(), $nextTick(arrow) -> initSidebarSort(), window.FlowDeck.* -> fdCreatePage/fdCreateFolder/fdGwRefresh, Object.keys/Math.min/ window.innerWidth dans x-for et :style -> sidebarSections()/ sectionMenuPos() — tout = simple appel de methode. - x-html restants du shell -> x-init + Alpine.effect : icone agent, carte projet, library x3 ; recherche library -> toggleSearch() (Alpine.nextTick) ; openMoveSelected() pour Object.keys en expression. - eslint : 70 warnings -> 0/0 (globals getCsrf depuis A38 ph1, /* exported openCardDetail */ + /* global owner, repo */, 3 ;; residuels). suite **1093/1093** · ruff OK · eslint 0/0 · E2E 3/3 (csp_preview + smoke x2) · docs a jour (ROADMAP ph3 LOT 1, CHANGELOG, WORKLOAD, VERSION)
102 lines
4.0 KiB
HTML
102 lines
4.0 KiB
HTML
{% extends "base.html" %}
|
|
{% block page_icon %}{{ fd_icon("trash",18) }}{% endblock %}
|
|
{% block page_title %}Trash{% endblock %}
|
|
{% block title_prefix %}Trash{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-title-area">
|
|
<div class="page-title">
|
|
<span class="page-icon-lg">{{ fd_icon("trash",24) }}</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);">{{ fd_icon("search",14) }}</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:var(--accent);">{{ fd_icon("user",14) }}</span> Last edited by ▾
|
|
</button>
|
|
<button class="trash-filter">
|
|
<span>{{ fd_icon("folder",14) }}</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)">
|
|
{{ fd_icon("trash",14) }}
|
|
</button>
|
|
</div>
|
|
</template>
|
|
<div x-show="filteredItems.length === 0" class="lib-empty">
|
|
<div style="font-size:48px;margin-bottom:16px;">{{ fd_icon("trash",14) }}</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 data-cfasync="false" nonce="{{ csp_nonce() }}">
|
|
// A20 phase 3 : registre Alpine.data (le build CSP ne résout pas les
|
|
|
|
// globales window — probe « Undefined variable: trashData »).
|
|
|
|
document.addEventListener('alpine:init', function () { Alpine.data('trashData', trashData); });
|
|
function trashData() {
|
|
return {
|
|
search: '',
|
|
items: [],
|
|
async init() {
|
|
const csrf = getCsrf();;
|
|
const token = csrf;
|
|
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 = getCsrf();;
|
|
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 = getCsrf();;
|
|
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 %}
|