mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
feat: fix file deletion on save + trash policy + documents/ prefix + relink browser
1. note_intention: Delete old file only when a genuinely new upload arrives
(32-char hex file_id), not when the FilePond pool preserves an existing
file by sending its DB integer ID. Previously the DB integer ID
triggered $hasNewNote=true, which deleted the existing note_intention
from disk+DB, then handleFilePondSingleFile couldn't re-process it
because the regex requires a hex pattern. Same fix applied to cover.
2. All file deletions now use deleteThesisFileToTrash() which renames
files to tmp/_trash/ instead of unlinking. The trash preserves
original filenames prefixed with DB id for traceability. Skips
website URLs and PeerTube refs (no disk file).
3. Storage prefix changed from theses/ to documents/ to reflect that
the folder holds all document types (determined by file_type in DB).
MediaController visibility gate supports both prefixes for backward
compat with existing files.
4. File browser + relink feature for orphaned files:
- /admin/fragments/file-browser.php — HTMX tree browser for
storage/documents/ and storage/theses/
- /admin/actions/filepond/relink.php — POST endpoint that inserts
a thesis_files row pointing to existing on-disk file
- Per-pool "📂 Relier" buttons (edit mode only)
- JS: XamxamOpenFileBrowser / XamxamRelinkFile with FilePond integration
- CSS: .relink-modal dialog + .file-browser tree styles
This commit is contained in:
8
app/public/partage/fragments/pill-search.php
Normal file
8
app/public/partage/fragments/pill-search.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
/**
|
||||
* Partagé fragment: generic pill-search (HTMX partial).
|
||||
*/
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
App::boot();
|
||||
|
||||
require_once APP_ROOT . '/public/partage/pill-search-fragment.php';
|
||||
@@ -79,6 +79,12 @@ if ($slug === 'validate-file-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST')
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($slug === 'pill-search-fragment' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
App::boot();
|
||||
require_once __DIR__ . '/fragments/pill-search.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
// Special route: /partage/recapitulatif?id=N
|
||||
if ($slug === 'recapitulatif' || $slug === 'recapitulatif.php') {
|
||||
App::boot();
|
||||
@@ -434,6 +440,7 @@ function renderShareLinkForm(string $slug, array $link): void
|
||||
<script src="<?= App::assetV('/assets/js/app/file-upload-filepond.js') ?>" defer></script>
|
||||
<script src="<?= App::assetV('/assets/js/app/beforeunload-guard.js') ?>" defer></script>
|
||||
<script src="<?= App::assetV('/assets/js/app/pill-search.js') ?>" defer></script>
|
||||
<script src="<?= App::assetV('/assets/js/app/jury-autocomplete.js') ?>" defer></script>
|
||||
<script src="<?= App::assetV('/assets/js/vendor/htmx.min.js') ?>" defer></script>
|
||||
</head>
|
||||
<body class="student-body">
|
||||
@@ -612,7 +619,7 @@ function handleShareLinkSubmission(string $slug): void
|
||||
header('Location: /partage/' . urlencode($slug));
|
||||
exit();
|
||||
|
||||
} catch (Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
$logger->logError('partage', $e->getMessage(), [
|
||||
'share_slug' => $slug,
|
||||
'author' => $authorName,
|
||||
|
||||
@@ -1,72 +1,10 @@
|
||||
<?php
|
||||
/**
|
||||
* language-search-fragment.php
|
||||
*
|
||||
* Shared HTMX fragment: returns matching language suggestions for the
|
||||
* "Autre(s) langue(s)" interactive search input.
|
||||
* @deprecated Use /partage/fragments/pill-search.php with type=language instead.
|
||||
* Kept for backward compatibility with existing hx-post URLs.
|
||||
*/
|
||||
require_once __DIR__ . '/../../src/Database.php';
|
||||
|
||||
$query = trim(preg_replace('/\s+/', ' ', strtolower($_POST['language_search_q'] ?? '')));
|
||||
|
||||
$currentLanguages = isset($_POST['language_autre']) && is_array($_POST['language_autre'])
|
||||
? array_map(function($l) { return trim(preg_replace('/\s+/', ' ', strtolower($l))); }, $_POST['language_autre'])
|
||||
: [];
|
||||
|
||||
error_log("[lang-search] q=" . var_export($query, true) . " cur=" . json_encode($currentLanguages));
|
||||
|
||||
$db = Database::getInstance();
|
||||
$results = $db->searchLanguages($query);
|
||||
|
||||
error_log("[lang-search] raw results count=" . count($results) . " rows=" . json_encode(array_slice($results, 0, 5)));
|
||||
|
||||
// Deduplicate
|
||||
$seen = [];
|
||||
$results = array_values(array_filter($results, function($lang) use (&$seen) {
|
||||
$key = strtolower($lang['name']);
|
||||
if (isset($seen[$key])) return false;
|
||||
$seen[$key] = true;
|
||||
return true;
|
||||
}));
|
||||
|
||||
// Exclude the main languages (handled by the checkbox list above)
|
||||
$excludedLanguages = ['français', 'anglais', 'néerlandais'];
|
||||
|
||||
// Filter out already-selected and excluded main languages
|
||||
$results = array_values(array_filter($results, function($lang) use ($currentLanguages, $excludedLanguages) {
|
||||
$lower = strtolower($lang['name']);
|
||||
return !in_array($lower, $currentLanguages, true)
|
||||
&& !in_array($lower, $excludedLanguages, true);
|
||||
}));
|
||||
|
||||
// Exact match check
|
||||
$exactExists = false;
|
||||
foreach ($results as $lang) {
|
||||
if (strcasecmp($lang['name'], $query) === 0) {
|
||||
$exactExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$inCurrent = in_array($query, $currentLanguages, true);
|
||||
$isExcluded = in_array($query, $excludedLanguages, true);
|
||||
$canCreate = ($query !== '' && !$exactExists && !$inCurrent && !$isExcluded);
|
||||
|
||||
error_log("[lang-search] exactExists=" . var_export($exactExists, true) . " inCurrent=" . var_export($inCurrent, true) . " canCreate=" . var_export($canCreate, true));
|
||||
?>
|
||||
<?php if (empty($results) && !$canCreate): ?>
|
||||
<div class="tag-search-empty">Aucune langue trouvée.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($results as $lang): ?>
|
||||
<button type="button" class="tag-search-item" data-tag-id="<?= (int)$lang['id'] ?>" data-tag-name="<?= htmlspecialchars($lang['name']) ?>">
|
||||
<span class="tag-search-item-name"><?= htmlspecialchars($lang['name']) ?></span>
|
||||
<span class="tag-search-item-count">(<?= (int)$lang['thesis_count'] ?>)</span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if ($canCreate): ?>
|
||||
<button type="button" class="tag-search-item tag-search-item--create" data-tag-name="<?= htmlspecialchars($query) ?>">
|
||||
<span class="tag-search-item-name">Créer « <?= htmlspecialchars($query) ?> »</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
$_POST['type'] = 'language';
|
||||
$_POST['q'] = $_POST['language_search_q'] ?? '';
|
||||
$_POST['exclude'] = $_POST['language_autre'] ?? [];
|
||||
require_once __DIR__ . '/pill-search-fragment.php';
|
||||
|
||||
123
app/public/partage/pill-search-fragment.php
Normal file
123
app/public/partage/pill-search-fragment.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/**
|
||||
* pill-search-fragment.php
|
||||
*
|
||||
* Generic HTMX fragment: returns matching suggestions for search inputs.
|
||||
*
|
||||
* Supports three entity types:
|
||||
* - tag → used by mot-clé pill-search component
|
||||
* - language → used by "Autre(s) langue(s)" pill-search component
|
||||
* - supervisor → used by jury member text inputs (inline autocomplete, no pills)
|
||||
*
|
||||
* Included by:
|
||||
* - /admin/fragments/pill-search.php (AdminAuth gated)
|
||||
* - /partage/fragments/pill-search.php (public, session already booted)
|
||||
*
|
||||
* Expected POST:
|
||||
* type — 'tag' | 'language' | 'supervisor'
|
||||
* q — search query string (partial name)
|
||||
* exclude[] — already-selected names to exclude from suggestions
|
||||
*/
|
||||
require_once __DIR__ . '/../../src/Database.php';
|
||||
|
||||
$db = Database::getInstance();
|
||||
$type = trim($_POST['pill_type'] ?? $_POST['type'] ?? 'tag');
|
||||
|
||||
// Accept both generic 'q' and type-specific field names (tag_search_q, language_search_q)
|
||||
// Use the type-specific field first to avoid collision when both are present (same <form>).
|
||||
$rawQ = $_POST['q']
|
||||
?? ($type === 'language' ? ($_POST['language_search_q'] ?? '') : ($_POST['tag_search_q'] ?? ''))
|
||||
?? '';
|
||||
$q = trim(preg_replace('/\s+/', ' ', strtolower((string)$rawQ)));
|
||||
|
||||
// Accept both generic 'exclude[]' and type-specific field names (tag[], language_autre[])
|
||||
$exclude = [];
|
||||
$rawExclude = $_POST['exclude']
|
||||
?? ($type === 'language' ? ($_POST['language_autre'] ?? null) : ($_POST['tag'] ?? null))
|
||||
?? null;
|
||||
if (isset($rawExclude) && is_array($rawExclude)) {
|
||||
$exclude = array_map(function($v) {
|
||||
return trim(preg_replace('/\s+/', ' ', strtolower($v)));
|
||||
}, $rawExclude);
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$emptyMessage = 'Aucun résultat.';
|
||||
$createLabel = 'Créer';
|
||||
|
||||
switch ($type) {
|
||||
case 'language':
|
||||
$results = $db->searchLanguages($q);
|
||||
$emptyMessage = 'Aucune langue trouvée.';
|
||||
$createLabel = 'Créer';
|
||||
break;
|
||||
case 'supervisor':
|
||||
$role = trim($_POST['role'] ?? '');
|
||||
$results = $db->searchSupervisors($q, $role);
|
||||
$emptyMessage = 'Aucun·e membre trouvé·e.';
|
||||
break;
|
||||
default: // tag
|
||||
$results = $db->searchTags($q);
|
||||
$emptyMessage = 'Aucun mot-clé trouvé.';
|
||||
$createLabel = 'Créer';
|
||||
break;
|
||||
}
|
||||
|
||||
// Deduplicate results by lowercase name
|
||||
$seen = [];
|
||||
$results = array_values(array_filter($results, function($item) use (&$seen) {
|
||||
$key = strtolower($item['name']);
|
||||
if (isset($seen[$key])) return false;
|
||||
$seen[$key] = true;
|
||||
return true;
|
||||
}));
|
||||
|
||||
// Exclude already-selected items and, for languages, the main three
|
||||
if ($type === 'language') {
|
||||
$excludedMain = ['français', 'anglais', 'néerlandais'];
|
||||
$results = array_values(array_filter($results, function($lang) use ($excludedMain, $exclude) {
|
||||
$lower = strtolower($lang['name']);
|
||||
return !in_array($lower, $excludedMain, true)
|
||||
&& !in_array($lower, $exclude, true);
|
||||
}));
|
||||
} elseif (!empty($exclude)) {
|
||||
$results = array_values(array_filter($results, function($item) use ($exclude) {
|
||||
return !in_array(strtolower($item['name']), $exclude, true);
|
||||
}));
|
||||
}
|
||||
|
||||
// "Créer" button logic
|
||||
$exactExists = false;
|
||||
foreach ($results as $item) {
|
||||
if (strcasecmp($item['name'], $q) === 0) {
|
||||
$exactExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$inExcluded = in_array($q, $exclude, true);
|
||||
$canCreate = ($q !== '' && !$exactExists && !$inExcluded);
|
||||
|
||||
if ($type === 'language') {
|
||||
$excludedMain = ['français', 'anglais', 'néerlandais'];
|
||||
if (in_array($q, $excludedMain, true)) {
|
||||
$canCreate = false;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<?php if (empty($results) && !$canCreate): ?>
|
||||
<div class="tag-search-empty"><?= htmlspecialchars($emptyMessage) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($results as $item): ?>
|
||||
<button type="button" class="tag-search-item" data-tag-name="<?= htmlspecialchars($item['name']) ?>">
|
||||
<span class="tag-search-item-name"><?= htmlspecialchars($item['name']) ?></span>
|
||||
<span class="tag-search-item-count">(<?= (int)$item['thesis_count'] ?>)</span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if ($canCreate): ?>
|
||||
<button type="button" class="tag-search-item tag-search-item--create" data-tag-name="<?= htmlspecialchars($q) ?>">
|
||||
<span class="tag-search-item-name"><?= htmlspecialchars($createLabel) ?> « <?= htmlspecialchars($q) ?> »</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
@@ -1,6 +1,10 @@
|
||||
<?php
|
||||
/**
|
||||
* Partagé fragment: tag search (HTMX partial).
|
||||
* @deprecated Use /partage/fragments/tag-search.php instead.
|
||||
* tag-search-fragment.php
|
||||
* @deprecated Use /partage/fragments/pill-search.php with type=tag instead.
|
||||
* Kept for backward compatibility with existing hx-post URLs.
|
||||
*/
|
||||
require_once __DIR__ . '/fragments/tag-search.php';
|
||||
$_POST['type'] = 'tag';
|
||||
$_POST['q'] = $_POST['tag_search_q'] ?? '';
|
||||
$_POST['exclude'] = $_POST['tag'] ?? [];
|
||||
require_once __DIR__ . '/pill-search-fragment.php';
|
||||
|
||||
Reference in New Issue
Block a user