feat: confirmations destructives de /trash en fenêtre thématisée (v7.45.6)
Les 3 confirmations destructives de la poubelle (suppression définitive unitaire, suppression groupée, Empty Trash) remplacent le confirm() du navigateur par une fenêtre FlowDeck : - réutilise les classes flowdeck-modal-overlay / .flowdeck-modal déjà présentes dans base.html (variables --bg-secondary / --border / --accent, rayon 12 px, boutons Cancel + confirmation coloré) → aucun nouveau CSS ; - overlay cliquable pour annuler, Échap pour annuler, × pour annuler ; - bouton de confirmation porteur du libellé exact (Delete / Delete all / Empty Trash) et message reprenant le nom de la page ou le nombre de pages ; - role="alertdialog" aria-modal + aria-labelledby ; - composant dans trashData() : askConfirm / cancelConfirm / acceptConfirm, mutations de propriétés individuelles (pitfall réactivité Alpine) ; - id #fd-trash-confirm (la classe flowdeck-modal-overlay sert déjà à #fd-global-folder-modal → locator unique côté test) ; - aucun confirm() natif ne subsiste dans trash.html. Gates : e2e/trash_ui.spec.js étendu — ouverture + titre + message de la fenêtre, ANNULATION vérifiée (page toujours présente), confirmation (suppression réelle : plus dans la poubelle + contenu effacé), fenêtre Empty Trash ouverte/annulée quel que soit le contenu, purge réelle seulement si la poubelle ne contient que des pages E2E (instance partagée) ; le handler dialog du test échoue si un dialog natif revient (NATIVE_DIALOG). Suite e2e 11 passed · pytest 1099 passed · ruff OK · OpenAPI 510 chemins / 7.45.6.
This commit is contained in:
+1
-1
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
|
||||
|
||||
app = FastAPI(
|
||||
title="FlowDeck",
|
||||
version="7.45.5",
|
||||
version="7.45.6",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
lifespan=lifespan,
|
||||
|
||||
+71
-12
@@ -106,6 +106,25 @@
|
||||
<span>Once a page has been in Trash for 30 days, it will be automatically deleted</span>
|
||||
<span style="font-size:16px;">ⓘ</span>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation thématique (remplace confirm() du navigateur) — classes
|
||||
flowdeck-modal définies dans base.html, thème variables du site -->
|
||||
<div id="fd-trash-confirm" class="flowdeck-modal-overlay" x-show="confirm.show" x-transition.opacity
|
||||
@click.self="cancelConfirm()" @keydown.escape.window="cancelConfirm()" style="display:none;">
|
||||
<div class="flowdeck-modal" x-transition role="alertdialog" aria-modal="true" aria-labelledby="fd-trash-confirm-title">
|
||||
<div class="flowdeck-modal-header">
|
||||
<h3 id="fd-trash-confirm-title" x-text="confirm.title"></h3>
|
||||
<button class="flowdeck-modal-close" @click="cancelConfirm()" aria-label="Close">×</button>
|
||||
</div>
|
||||
<div class="flowdeck-modal-body">
|
||||
<p x-text="confirm.message"></p>
|
||||
</div>
|
||||
<div class="flowdeck-modal-footer">
|
||||
<button class="flowdeck-modal-btn cancel" @click="cancelConfirm()">Cancel</button>
|
||||
<button class="flowdeck-modal-btn confirm" @click="acceptConfirm()" x-text="confirm.okText"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -127,6 +146,8 @@
|
||||
locOpen: false,
|
||||
selected: [],
|
||||
items: [],
|
||||
// Confirmation thématique (remplace confirm() du navigateur)
|
||||
confirm: { show: false, title: '', message: '', okText: 'Delete', action: null },
|
||||
|
||||
async init() {
|
||||
try {
|
||||
@@ -184,6 +205,27 @@
|
||||
setSort(v) { this.sort = v; this.sortOpen = false; },
|
||||
setLoc(v) { this.locFilter = v; this.locOpen = false; },
|
||||
|
||||
// ── Confirmation (modal thème FlowDeck, classes flowdeck-modal de base) ──
|
||||
// Mutations de propriétés individuelles (remplacer l'objet entier tue la
|
||||
// réactivité Alpine — pitfall connu).
|
||||
askConfirm(title, message, okText, action) {
|
||||
this.confirm.title = title;
|
||||
this.confirm.message = message;
|
||||
this.confirm.okText = okText || 'Delete';
|
||||
this.confirm.action = action;
|
||||
this.confirm.show = true;
|
||||
},
|
||||
cancelConfirm() {
|
||||
this.confirm.show = false;
|
||||
this.confirm.action = null;
|
||||
},
|
||||
acceptConfirm() {
|
||||
const action = this.confirm.action;
|
||||
this.confirm.show = false;
|
||||
this.confirm.action = null;
|
||||
if (action) action();
|
||||
},
|
||||
|
||||
// ── API ──────────────────────────────────────────────────────────
|
||||
// getCsrf() renvoie la CHAÎNE du jeton (base.html) : l'ancien code faisait
|
||||
// csrf?.[1] → 2e caractère → 403 CSRF sur restore/delete, silencieux.
|
||||
@@ -210,13 +252,6 @@
|
||||
else window.showToast('Restore failed', 'error');
|
||||
} catch (e) { window.showToast('Restore failed: ' + e.message, 'error'); }
|
||||
},
|
||||
async deleteForever(id) {
|
||||
if (!confirm('Permanently delete this page? This cannot be undone.')) return;
|
||||
try {
|
||||
if (await this._delete(id)) window.showToast('Page permanently deleted', 'success');
|
||||
else window.showToast('Delete failed', 'error');
|
||||
} catch (e) { window.showToast('Delete failed: ' + e.message, 'error'); }
|
||||
},
|
||||
async restoreSelected() {
|
||||
const ids = this.selected.slice();
|
||||
let ok = 0;
|
||||
@@ -225,18 +260,42 @@
|
||||
}
|
||||
window.showToast(ok + ' of ' + ids.length + ' page(s) restored', ok === ids.length ? 'success' : 'error');
|
||||
},
|
||||
async deleteSelected() {
|
||||
const ids = this.selected.slice();
|
||||
if (!confirm('Permanently delete ' + ids.length + ' page(s)? This cannot be undone.')) return;
|
||||
async _deleteAndToast(ids) {
|
||||
let ok = 0;
|
||||
for (const id of ids) {
|
||||
try { if (await this._delete(id)) ok++; } catch (e) { /* compté en échec */ }
|
||||
}
|
||||
window.showToast(ok + ' of ' + ids.length + ' page(s) deleted', ok === ids.length ? 'success' : 'error');
|
||||
const msg = ids.length === 1
|
||||
? (ok ? 'Page permanently deleted' : 'Delete failed')
|
||||
: ok + ' of ' + ids.length + ' page(s) deleted';
|
||||
window.showToast(msg, ok === ids.length ? 'success' : 'error');
|
||||
},
|
||||
deleteForever(id) {
|
||||
const self = this;
|
||||
const item = this.items.find(function (i) { return i.id === id; });
|
||||
const name = item ? item.name : 'this page';
|
||||
this.askConfirm('Delete permanently',
|
||||
'“' + name + '” will be deleted for good. This cannot be undone.',
|
||||
'Delete',
|
||||
function () { self._deleteAndToast([id]); });
|
||||
},
|
||||
deleteSelected() {
|
||||
const self = this;
|
||||
const ids = this.selected.slice();
|
||||
this.askConfirm('Delete ' + ids.length + ' page(s) for good?',
|
||||
'These pages will be permanently deleted. This cannot be undone.',
|
||||
'Delete all',
|
||||
function () { self._deleteAndToast(ids); });
|
||||
},
|
||||
async emptyTrash() {
|
||||
const self = this;
|
||||
const n = this.items.length;
|
||||
if (!confirm('Permanently delete ALL ' + n + ' page(s) in the Trash? This cannot be undone.')) return;
|
||||
this.askConfirm('Empty Trash?',
|
||||
'All ' + n + ' page(s) in the Trash will be permanently deleted. This cannot be undone.',
|
||||
'Empty Trash',
|
||||
function () { self._emptyAndToast(n); });
|
||||
},
|
||||
async _emptyAndToast(n) {
|
||||
try {
|
||||
const r = await fetch('/board/api/trash/empty', { method: 'POST', headers: this._headers() });
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
|
||||
Reference in New Issue
Block a user