mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-08-10 07:11:18 +02:00
cleanup: sélection multiple + page dédiée (/admin/cleanup.php)
- cleanup-tmp.php: accepte les arrays filepond_dirs[] et trash_files[] pour suppression en lot - cleanup-stats-fragment.php: ajout checkboxes + barre d'actions groupées + formulaire bulk HTMX - admin-cleanup-bulk.js: module JS pour toggleAll, updateBulk, bulkDelete - Nouvelle page /admin/cleanup.php remplace la modale (tmp-cleanup.php supprimé) - Bouton Nettoyer dans index.php devient un lien vers la page dédiée
This commit is contained in:
@@ -15,3 +15,5 @@
|
|||||||
- [x] Responsive paginator: hide first/last buttons on mobile, smaller touch targets
|
- [x] Responsive paginator: hide first/last buttons on mobile, smaller touch targets
|
||||||
- [x] Compact range counter on mobile: "1–15 / 112" instead of full "1–15 sur 112 résultats"
|
- [x] Compact range counter on mobile: "1–15 / 112" instead of full "1–15 sur 112 résultats"
|
||||||
- [x] Remove orientation display from search result card meta
|
- [x] Remove orientation display from search result card meta
|
||||||
|
- [x] Add bulk select/delete to tmp cleanup dialog (like admin/index.php pattern)
|
||||||
|
- [x] Move cleanup UI from modal to dedicated page (like add.php/edit.php)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ elseif ($staleSize > 0) { $staleHuman = $staleSize . ' B'; }
|
|||||||
$summaryMeta = $staleMeta;
|
$summaryMeta = $staleMeta;
|
||||||
if ($staleHuman) $summaryMeta .= ' · ' . $staleHuman;
|
if ($staleHuman) $summaryMeta .= ' · ' . $staleHuman;
|
||||||
|
|
||||||
if ($totalStale === 0 && $totalFiles === 0): ?>
|
<?php if ($totalStale === 0 && $totalFiles === 0): ?>
|
||||||
<div id="tmp-cleanup-stats-wrapper">
|
<div id="tmp-cleanup-stats-wrapper">
|
||||||
<details class="n-section" open>
|
<details class="n-section" open>
|
||||||
<summary>
|
<summary>
|
||||||
@@ -54,6 +54,27 @@ if ($totalStale === 0 && $totalFiles === 0): ?>
|
|||||||
<?php return; endif; ?>
|
<?php return; endif; ?>
|
||||||
|
|
||||||
<div id="tmp-cleanup-stats-wrapper">
|
<div id="tmp-cleanup-stats-wrapper">
|
||||||
|
<!-- Bulk actions bar -->
|
||||||
|
<div id="cleanup-bulk-actions" class="admin-bulk-actions" style="display:none">
|
||||||
|
<strong><span id="cleanup-selected-count">0</span> fichier(s) sélectionné(s)</strong>
|
||||||
|
<div class="admin-bulk-btns">
|
||||||
|
<button type="button" class="btn btn--sm btn--red" onclick="cleanupBulkDelete()">
|
||||||
|
<?= icon('trash') ?> Supprimer la sélection
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden bulk form (HTMX-powered) -->
|
||||||
|
<form id="cleanup-bulk-form" method="post" action="/admin/actions/cleanup-tmp.php"
|
||||||
|
hx-post="/admin/actions/cleanup-tmp.php"
|
||||||
|
hx-target="#tmp-cleanup-stats-wrapper"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-indicator="#tmp-cleanup-stats-wrapper"
|
||||||
|
style="display:none">
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||||
|
<div id="cleanup-bulk-checkboxes"></div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<details class="n-section" open>
|
<details class="n-section" open>
|
||||||
<summary>
|
<summary>
|
||||||
<?= icon('paint-brush-household') ?>
|
<?= icon('paint-brush-household') ?>
|
||||||
@@ -62,10 +83,11 @@ if ($totalStale === 0 && $totalFiles === 0): ?>
|
|||||||
|
|
||||||
<?php if ($fpStale > 0): ?>
|
<?php if ($fpStale > 0): ?>
|
||||||
<table class="n-table">
|
<table class="n-table">
|
||||||
<thead><tr><th>Nom</th><th>Taille</th><th>Âge</th><th width="1%"></th></tr></thead>
|
<thead><tr><th width="1%"><input type="checkbox" onchange="cleanupToggleAll(this, 'filepond')" title="Tout sélectionner"></th><th>Nom</th><th>Taille</th><th>Âge</th><th width="1%"></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($d['filepond_stale_files'] as $f): ?>
|
<?php foreach ($d['filepond_stale_files'] as $f): ?>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td><input type="checkbox" name="filepond_dirs[]" value="<?= htmlspecialchars($f['name']) ?>" data-cleanup-group="filepond" onchange="cleanupUpdateBulk()"></td>
|
||||||
<td><strong><?= htmlspecialchars($f['name']) ?></strong></td>
|
<td><strong><?= htmlspecialchars($f['name']) ?></strong></td>
|
||||||
<td style="white-space:nowrap"><?= htmlspecialchars($f['human']) ?></td>
|
<td style="white-space:nowrap"><?= htmlspecialchars($f['human']) ?></td>
|
||||||
<td style="white-space:nowrap">~<?= (int)$f['age_minutes'] ?> min</td>
|
<td style="white-space:nowrap">~<?= (int)$f['age_minutes'] ?> min</td>
|
||||||
@@ -93,10 +115,11 @@ if ($totalStale === 0 && $totalFiles === 0): ?>
|
|||||||
Corbeille
|
Corbeille
|
||||||
</p>
|
</p>
|
||||||
<table class="n-table">
|
<table class="n-table">
|
||||||
<thead><tr><th>Nom</th><th>Taille</th><th>Âge</th><th width="1%"></th></tr></thead>
|
<thead><tr><th width="1%"><input type="checkbox" onchange="cleanupToggleAll(this, 'trash')" title="Tout sélectionner"></th><th>Nom</th><th>Taille</th><th>Âge</th><th width="1%"></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($d['trash_stale_files'] as $f): ?>
|
<?php foreach ($d['trash_stale_files'] as $f): ?>
|
||||||
<tr>
|
<tr>
|
||||||
|
<td><input type="checkbox" name="trash_files[]" value="<?= htmlspecialchars($f['name']) ?>" data-cleanup-group="trash" onchange="cleanupUpdateBulk()"></td>
|
||||||
<td><strong><?= htmlspecialchars($f['name']) ?></strong></td>
|
<td><strong><?= htmlspecialchars($f['name']) ?></strong></td>
|
||||||
<td style="white-space:nowrap"><?= htmlspecialchars($f['human']) ?></td>
|
<td style="white-space:nowrap"><?= htmlspecialchars($f['human']) ?></td>
|
||||||
<td style="white-space:nowrap">~<?= (int)$f['age_days'] ?> j</td>
|
<td style="white-space:nowrap">~<?= (int)$f['age_days'] ?> j</td>
|
||||||
|
|||||||
@@ -43,32 +43,42 @@ $details = [];
|
|||||||
$db = new Database();
|
$db = new Database();
|
||||||
$pdo = $db->getPDO();
|
$pdo = $db->getPDO();
|
||||||
|
|
||||||
// ── Individual deletion mode ────────────────────────────────────────────
|
// ── Individual / bulk deletion mode ────────────────────────────────────
|
||||||
$individualFilepond = trim($_POST['filepond_dir'] ?? '');
|
$individualFilepond = trim($_POST['filepond_dir'] ?? '');
|
||||||
$individualTrash = trim($_POST['trash_file'] ?? '');
|
$individualTrash = trim($_POST['trash_file'] ?? '');
|
||||||
|
$bulkFilepond = $_POST['filepond_dirs'] ?? [];
|
||||||
|
$bulkTrash = $_POST['trash_files'] ?? [];
|
||||||
|
|
||||||
if ($individualFilepond !== '' || $individualTrash !== '') {
|
// Normalize: single values also become arrays for unified handling
|
||||||
if ($individualFilepond !== '') {
|
$filepondItems = $individualFilepond !== '' ? [$individualFilepond] : (is_array($bulkFilepond) ? $bulkFilepond : []);
|
||||||
$dirPath = $filepondDir . '/' . basename($individualFilepond);
|
$trashItems = $individualTrash !== '' ? [$individualTrash] : (is_array($bulkTrash) ? $bulkTrash : []);
|
||||||
|
|
||||||
|
if (!empty($filepondItems) || !empty($trashItems)) {
|
||||||
|
foreach ($filepondItems as $item) {
|
||||||
|
$item = trim($item);
|
||||||
|
if ($item === '') continue;
|
||||||
|
$dirPath = $filepondDir . '/' . basename($item);
|
||||||
if (is_dir($dirPath) && str_starts_with(realpath($dirPath), realpath($filepondDir))) {
|
if (is_dir($dirPath) && str_starts_with(realpath($dirPath), realpath($filepondDir))) {
|
||||||
rmdirRecursive($dirPath);
|
rmdirRecursive($dirPath);
|
||||||
$filepondRemoved = 1;
|
$filepondRemoved++;
|
||||||
$details[] = "filepond/$individualFilepond: suppression manuelle";
|
$details[] = "filepond/$item: suppression manuelle";
|
||||||
} else {
|
} else {
|
||||||
$errors[] = 'Dossier introuvable : ' . htmlspecialchars($individualFilepond);
|
$errors[] = 'Dossier introuvable : ' . htmlspecialchars($item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($individualTrash !== '') {
|
foreach ($trashItems as $item) {
|
||||||
$filePath = $trashDir . '/' . basename($individualTrash);
|
$item = trim($item);
|
||||||
|
if ($item === '') continue;
|
||||||
|
$filePath = $trashDir . '/' . basename($item);
|
||||||
if (is_file($filePath) && str_starts_with(realpath($filePath), realpath($trashDir))) {
|
if (is_file($filePath) && str_starts_with(realpath($filePath), realpath($trashDir))) {
|
||||||
if (@unlink($filePath)) {
|
if (@unlink($filePath)) {
|
||||||
$trashRemoved = 1;
|
$trashRemoved++;
|
||||||
$details[] = "_trash/$individualTrash: suppression manuelle";
|
$details[] = "_trash/$item: suppression manuelle";
|
||||||
} else {
|
} else {
|
||||||
$errors[] = 'Impossible de supprimer : ' . htmlspecialchars($individualTrash);
|
$errors[] = 'Impossible de supprimer : ' . htmlspecialchars($item);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$errors[] = 'Fichier introuvable : ' . htmlspecialchars($individualTrash);
|
$errors[] = 'Fichier introuvable : ' . htmlspecialchars($item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../../bootstrap.php';
|
||||||
|
require_once __DIR__ . '/../../src/AdminAuth.php';
|
||||||
|
AdminAuth::requireLogin();
|
||||||
|
|
||||||
|
if (empty($_SESSION['csrf_token'])) {
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
|
||||||
|
$pageTitle = 'Nettoyer les fichiers temporaires';
|
||||||
|
|
||||||
|
$isAdmin = true;
|
||||||
|
$bodyClass = 'admin-body';
|
||||||
|
$extraCssAdmin = [];
|
||||||
|
$extraJs = ['/assets/dist/admin.min.js'];
|
||||||
|
|
||||||
|
require_once APP_ROOT . '/templates/head.php';
|
||||||
|
include APP_ROOT . '/templates/header.php';
|
||||||
|
include APP_ROOT . '/templates/admin/cleanup.php';
|
||||||
|
require_once APP_ROOT . '/templates/admin/footer.php';
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* admin-cleanup-bulk.js — Bulk selection and deletion for the temp files cleanup dialog.
|
||||||
|
*
|
||||||
|
* Provides: cleanupToggleAll, cleanupUpdateBulk, cleanupBulkDelete.
|
||||||
|
* All functions are attached to `window` for onclick handlers in PHP templates.
|
||||||
|
*/
|
||||||
|
(() => {
|
||||||
|
function cleanupToggleAll(src, group) {
|
||||||
|
const selector = group === 'filepond'
|
||||||
|
? 'input[name="filepond_dirs[]"]'
|
||||||
|
: 'input[name="trash_files[]"]';
|
||||||
|
document.querySelectorAll(selector).forEach((cb) => {
|
||||||
|
cb.checked = src.checked;
|
||||||
|
});
|
||||||
|
cleanupUpdateBulk();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupUpdateBulk() {
|
||||||
|
const bar = document.getElementById('cleanup-bulk-actions');
|
||||||
|
const countEl = document.getElementById('cleanup-selected-count');
|
||||||
|
if (!bar || !countEl) return;
|
||||||
|
|
||||||
|
const n = document.querySelectorAll(
|
||||||
|
'input[name="filepond_dirs[]"]:checked, input[name="trash_files[]"]:checked'
|
||||||
|
).length;
|
||||||
|
countEl.textContent = n;
|
||||||
|
bar.style.display = n > 0 ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupBulkDelete() {
|
||||||
|
const form = document.getElementById('cleanup-bulk-form');
|
||||||
|
const container = document.getElementById('cleanup-bulk-checkboxes');
|
||||||
|
if (!form || !container) return;
|
||||||
|
|
||||||
|
// Populate hidden inputs from checked checkboxes
|
||||||
|
container.innerHTML = '';
|
||||||
|
document.querySelectorAll(
|
||||||
|
'input[name="filepond_dirs[]"]:checked, input[name="trash_files[]"]:checked'
|
||||||
|
).forEach((cb) => {
|
||||||
|
const inp = document.createElement('input');
|
||||||
|
inp.type = 'hidden';
|
||||||
|
inp.name = cb.name; // "filepond_dirs[]" or "trash_files[]"
|
||||||
|
inp.value = cb.value;
|
||||||
|
container.appendChild(inp);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Submit via HTMX
|
||||||
|
htmx.trigger(form, 'submit');
|
||||||
|
}
|
||||||
|
|
||||||
|
function reattachListeners() {
|
||||||
|
document.querySelectorAll(
|
||||||
|
'input[name="filepond_dirs[]"], input[name="trash_files[]"]'
|
||||||
|
).forEach((cb) => {
|
||||||
|
cb.addEventListener('change', cleanupUpdateBulk);
|
||||||
|
});
|
||||||
|
cleanupUpdateBulk();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('htmx:afterSwap', (evt) => {
|
||||||
|
// Only reattach if the swap target is our cleanup wrapper
|
||||||
|
if (evt.detail.target.id === 'tmp-cleanup-stats-wrapper') {
|
||||||
|
reattachListeners();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export to global scope for onclick handlers in HTML
|
||||||
|
window.cleanupToggleAll = cleanupToggleAll;
|
||||||
|
window.cleanupUpdateBulk = cleanupUpdateBulk;
|
||||||
|
window.cleanupBulkDelete = cleanupBulkDelete;
|
||||||
|
})();
|
||||||
@@ -16,6 +16,7 @@ import "./htmx-global-setup.js";
|
|||||||
|
|
||||||
// Admin features
|
// Admin features
|
||||||
import "./admin-index-bulk.js";
|
import "./admin-index-bulk.js";
|
||||||
|
import "./admin-cleanup-bulk.js";
|
||||||
import "./admin-tags.js";
|
import "./admin-tags.js";
|
||||||
import "./admin-contenus-langues.js";
|
import "./admin-contenus-langues.js";
|
||||||
import "./admin-contenus-motscles.js";
|
import "./admin-contenus-motscles.js";
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<main id="main-content" class="admin-main--list">
|
||||||
|
<div class="admin-list-toolbar admin-list-toolbar--list" style="margin-bottom:var(--space-m)">
|
||||||
|
<div class="admin-toolbar-top">
|
||||||
|
<div class="admin-toolbar-title-row">
|
||||||
|
<h1>
|
||||||
|
<a href="/admin/" class="admin-back-btn" title="Retour à la liste"><?= icon('arrow-left-circle') ?></a>
|
||||||
|
Nettoyer les fichiers temporaires
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="n-grid" id="cleanup-grid-parent">
|
||||||
|
<!-- ═══════ FilePond / Trash ═══════ -->
|
||||||
|
<div id="tmp-cleanup-stats-wrapper"
|
||||||
|
hx-get="/admin/actions/cleanup-stats-fragment.php"
|
||||||
|
hx-trigger="load"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-indicator="#tmp-cleanup-stats-wrapper">
|
||||||
|
<details class="n-section" open>
|
||||||
|
<summary>
|
||||||
|
<?= icon('paint-brush-household') ?>
|
||||||
|
Fichiers temporaires
|
||||||
|
</summary>
|
||||||
|
<p style="margin:0;color:var(--text-secondary)">Chargement…</p>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════ PeerTube ═══════ -->
|
||||||
|
<div id="peertube-orphans-fragment" style="display:contents"
|
||||||
|
hx-get="/admin/actions/peertube-orphans-fragment.php"
|
||||||
|
hx-trigger="load"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-indicator="#peertube-orphans-fragment">
|
||||||
|
<p style="margin:0;color:var(--text-secondary)">Chargement…</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
@@ -29,10 +29,9 @@
|
|||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($tmpTotalCount > 0): ?>
|
<?php if ($tmpTotalCount > 0): ?>
|
||||||
<button type="button" class="btn btn--sm btn--secondary" id="tmp-cleanup-btn"
|
<a href="/admin/cleanup.php" class="btn btn--sm btn--secondary">
|
||||||
onclick="document.getElementById('tmp-cleanup-dialog').showModal(); htmx.trigger('#tmp-cleanup-stats-wrapper','loadStats'); htmx.trigger('#peertube-orphans-fragment','loadPeertube')">
|
|
||||||
<?= icon('trash') ?> Nettoyer (<?= $tmpTotalCount ?>)
|
<?= icon('trash') ?> Nettoyer (<?= $tmpTotalCount ?>)
|
||||||
</button>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<button type="button" class="btn btn--primary btn--sm" id="import-dialog-btn"
|
<button type="button" class="btn btn--primary btn--sm" id="import-dialog-btn"
|
||||||
onclick="document.getElementById('import-dialog').showModal(); window.XamxamInitFilePonds()">
|
onclick="document.getElementById('import-dialog').showModal(); window.XamxamInitFilePonds()">
|
||||||
@@ -93,8 +92,6 @@
|
|||||||
|
|
||||||
<?php include APP_ROOT . '/templates/admin/partials/dialogs/import.php'; ?>
|
<?php include APP_ROOT . '/templates/admin/partials/dialogs/import.php'; ?>
|
||||||
|
|
||||||
<?php include APP_ROOT . '/templates/admin/partials/dialogs/tmp-cleanup.php'; ?>
|
|
||||||
|
|
||||||
<?php if ($importMessage || !empty($importErrors)): ?>
|
<?php if ($importMessage || !empty($importErrors)): ?>
|
||||||
<script>document.getElementById('import-dialog').showModal();</script>
|
<script>document.getElementById('import-dialog').showModal();</script>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
<dialog id="tmp-cleanup-dialog" class="admin-dialog admin-dialog--sheet" aria-labelledby="tmp-cleanup-title">
|
|
||||||
<div class="admin-dialog__header">
|
|
||||||
<h3 id="tmp-cleanup-title">Nettoyer les fichiers temporaires</h3>
|
|
||||||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
|
||||||
onclick="document.getElementById('tmp-cleanup-dialog').close()">✕</button>
|
|
||||||
</div>
|
|
||||||
<div class="admin-dialog__body">
|
|
||||||
<div id="tmp-cleanup-result" style="display:none;margin-bottom:var(--space-sm)"></div>
|
|
||||||
<div class="n-grid" id="cleanup-grid-parent">
|
|
||||||
<!-- ═══════ FilePond / Trash ═══════ -->
|
|
||||||
<div id="tmp-cleanup-stats-wrapper"
|
|
||||||
hx-get="/admin/actions/cleanup-stats-fragment.php"
|
|
||||||
hx-trigger="loadStats"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
hx-indicator="#tmp-cleanup-stats-wrapper">
|
|
||||||
<details class="n-section" open>
|
|
||||||
<summary>
|
|
||||||
<?= icon('paint-brush-household') ?>
|
|
||||||
Fichiers temporaires
|
|
||||||
</summary>
|
|
||||||
<p style="margin:0;color:var(--text-secondary)">Chargement…</p>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
<!-- ═══════ PeerTube ═══════ -->
|
|
||||||
<div id="peertube-orphans-fragment" style="display:contents"
|
|
||||||
hx-get="/admin/actions/peertube-orphans-fragment.php"
|
|
||||||
hx-trigger="loadPeertube"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
hx-indicator="#peertube-orphans-fragment">
|
|
||||||
<p style="margin:0;color:var(--text-secondary)">Chargement…</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
Reference in New Issue
Block a user