mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Add Mots-clés and Langues management to contenus page
- Add searchLanguages, getAllLanguagesWithCount, renameLanguage, mergeLanguage, deleteLanguage to Database - Create actions/language.php handler with rename/merge/merge_bulk/delete actions - Add merge_bulk action to actions/tag.php - Add Mots-clés section to contenus template with HTMX search, select checkboxes, rename/delete/merge buttons, and multi-select merge toolbar - Add Langues section to contenus template with same pattern - Create contenus-tags-fragment.php and contenus-languages-fragment.php HTMX fragments - Remove form-settings- from flat-fieldset CSS selector so fieldsets in contenus retain border/padding - contenus.php: add 'Gérer les mots-clés' link to /admin/tags.php - contenus.php: add Langues fieldset with HTMX search + table (rename/merge/delete/bulk) - tags.php: add HTMX search bar, checkbox column, bulk merge toolbar - Create tags-fragment.php and contenus-langues-fragment.php for HTMX - Remove tab component and associated CSS - Simplify JS: separate tags/langues-prefixed functions - Fix redirects: tag.php defaults to /admin/tags.php, supports return override - Keep tags.php standalone page and Mots-clés button unchanged
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
<div class="admin-with-toc">
|
||||
<?php include APP_ROOT . '/templates/admin/partials/admin-toc.php'; ?>
|
||||
<main id="main-content">
|
||||
<h1>Accès</h1>
|
||||
|
||||
@@ -126,6 +128,19 @@
|
||||
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
+\\\\\\\ to: sntroxlt 6a5b93f3 "Move Formulaire settings to contenus, remove delete-all TFE" (rebased revision)
|
||||
++ $linkName = $link['name'] ?? '';
|
||||
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: sntroxlt 6a5b93f3 "Move Formulaire settings to contenus, remove delete-all TFE" (rebased revision)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
- $linkName = $link['name'] ?? '';
|
||||
- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination)
|
||||
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: tyotlpxt ceaca548 "Add Mots-clés and Langues management to contenus page" (rebased revision)
|
||||
$linkName = $link['name'] ?? '';
|
||||
$linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
$linkLockedYear = $link['locked_year'] ?? null;
|
||||
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||
+\\\\\\\ to: tyotlpxt f7b0f560 "Add Mots-clés and Langues management to contenus page" (rebased revision)
|
||||
++ $linkName = $link['name'] ?? '';
|
||||
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||
?>
|
||||
<tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer">
|
||||
@@ -389,6 +404,7 @@
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ CREATE DIALOG ═══════════════════════ -->
|
||||
<dialog id="create-dialog" class="admin-dialog" aria-labelledby="create-dialog-title">
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<div class="admin-with-toc">
|
||||
<?php include APP_ROOT . '/templates/admin/partials/admin-toc.php'; ?>
|
||||
<main id="main-content">
|
||||
<h1>Contenus</h1>
|
||||
|
||||
@@ -44,6 +46,35 @@
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════════
|
||||
DONNÉES SECONDAIRES
|
||||
═══════════════════════════════════════════════════════════════════ -->
|
||||
<section aria-labelledby="donnees-secondaires-title">
|
||||
<h2 id="donnees-secondaires-title">Données Secondaires</h2>
|
||||
|
||||
<!-- ── Mots-clés ── -->
|
||||
<p><a href="/admin/tags.php" class="btn btn--sm btn--primary">Gérer les mots-clés</a></p>
|
||||
|
||||
<!-- ── Langues ── -->
|
||||
<fieldset>
|
||||
<legend>Langues</legend>
|
||||
|
||||
<form id="langues-search-form"
|
||||
hx-get="/admin/contenus-langues-fragment.php"
|
||||
hx-target="#langues-table-wrap"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="input changed delay:200ms from:input[name=q], keyup[key=='Enter'] from:input[name=q]"
|
||||
hx-push-url="false"
|
||||
style="margin-bottom:var(--space-xs)">
|
||||
<input type="text" name="q" placeholder="Rechercher une langue…" style="max-width:300px">
|
||||
</form>
|
||||
|
||||
<div id="langues-table-wrap" hx-get="/admin/contenus-langues-fragment.php" hx-trigger="load" hx-swap="innerHTML">
|
||||
<!-- populated by HTMX -->
|
||||
</div>
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════════════
|
||||
PARAMÈTRES DU FORMULAIRE
|
||||
═══════════════════════════════════════════════════════════════════ -->
|
||||
@@ -163,44 +194,22 @@
|
||||
<?php
|
||||
$blocks = $formHelpBlocks;
|
||||
|
||||
// ── Student form structure — each help block above its fieldset ───────────
|
||||
// Pairs: [help_key, fieldset_name, fieldset_inputs]
|
||||
$pairs = [
|
||||
// Top of form
|
||||
['partage_intro', null, null],
|
||||
|
||||
// Informations du TFE
|
||||
['fieldset_tfe_info', 'Informations du TFE',
|
||||
['Titre', 'Sous-titre', 'Auteur·ice(s)', 'Contact visible', 'Synopsis']],
|
||||
|
||||
// Langue(s)
|
||||
['fieldset_languages', 'Langue(s)',
|
||||
['Langues du TFE (cases à cocher)', 'Autre(s) langue(s)']],
|
||||
|
||||
// Mots-clés
|
||||
['fieldset_keywords', 'Mots-clés',
|
||||
['Mots-clés (max 10), séparés par des virgules']],
|
||||
|
||||
// Cadre académique
|
||||
['fieldset_academic', 'Cadre académique',
|
||||
['Année', 'Orientation', 'AP', 'Finalité']],
|
||||
|
||||
// Composition du jury
|
||||
['fieldset_jury', 'Composition du jury',
|
||||
['Président·e', 'Promoteur·ice(s)', 'Lecteur·ices']],
|
||||
|
||||
// Format(s) + Fichiers
|
||||
['fieldset_files', 'Format(s) + Fichiers',
|
||||
['Formats (PDF, vidéo, audio, site web…)', 'Couverture', 'Note d\'intention', 'Fichier principal', 'Annexes']],
|
||||
|
||||
// Métadonnées complémentaires (supprimé)
|
||||
// Ces champs sont redondants avec les fichiers attachés
|
||||
|
||||
// Degrés d'ouverture et licences
|
||||
['fieldset_access', 'Degrés d\'ouverture et licences',
|
||||
['Généralités', 'Degré (libre/interne/interdit)', 'Licence', 'CC2r']],
|
||||
|
||||
// E-mail de confirmation
|
||||
['fieldset_email', 'E-mail de confirmation',
|
||||
['Adresse e-mail']],
|
||||
];
|
||||
@@ -208,7 +217,6 @@
|
||||
|
||||
<div class="fhb-structure">
|
||||
<?php foreach ($pairs as [$helpKey, $fieldsetName, $inputs]):
|
||||
// Help block
|
||||
$b = $blocks[$helpKey] ?? ['content' => '', 'name' => '', 'enabled' => 0];
|
||||
$title = $b['name'] ?: ($fieldsetName ?? $helpKey);
|
||||
?>
|
||||
@@ -239,6 +247,131 @@
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
CONFIRM DIALOGS FOR LANGUES
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
|
||||
<dialog id="langues-merge-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="langues-merge-title">
|
||||
<div class="admin-dialog__header">
|
||||
<h2 id="langues-merge-title">Fusionner la langue</h2>
|
||||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||||
onclick="this.closest('dialog').close()">✕</button>
|
||||
</div>
|
||||
<div class="admin-dialog__alert">
|
||||
<p>Fusionner dans « <strong id="langues-merge-target-name"></strong> » ? La langue source sera supprimée.</p>
|
||||
</div>
|
||||
<div class="admin-dialog__footer">
|
||||
<button type="button" class="btn btn--warning" onclick="this.closest('dialog').close(); languesSubmitPending()">Fusionner</button>
|
||||
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="langues-delete-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="langues-delete-title">
|
||||
<div class="admin-dialog__header">
|
||||
<h2 id="langues-delete-title">Supprimer la langue</h2>
|
||||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||||
onclick="this.closest('dialog').close()">✕</button>
|
||||
</div>
|
||||
<div class="admin-dialog__alert">
|
||||
<p>Supprimer « <strong id="langues-delete-name"></strong> » ? Cette action est irréversible.</p>
|
||||
</div>
|
||||
<div class="admin-dialog__footer">
|
||||
<button type="button" class="btn btn--danger" onclick="this.closest('dialog').close(); languesSubmitPending()">Supprimer</button>
|
||||
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="langues-bulk-merge-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="langues-bulk-merge-title">
|
||||
<div class="admin-dialog__header">
|
||||
<h2 id="langues-bulk-merge-title">Fusionner des langues</h2>
|
||||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||||
onclick="this.closest('dialog').close()">✕</button>
|
||||
</div>
|
||||
<div class="admin-dialog__alert">
|
||||
<p>Fusionner <strong><span id="langues-bulk-merge-count"></span> langue(s)</strong> sélectionnée(s) dans :</p>
|
||||
<select id="langues-bulk-merge-target-select" class="admin-select--inline" style="margin-top:var(--space-xs); width:100%" required>
|
||||
<option value="">— Choisir la destination —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="admin-dialog__footer">
|
||||
<button type="button" class="btn btn--warning" onclick="languesExecBulkMerge()">Fusionner</button>
|
||||
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
let _languesPendingForm = null;
|
||||
|
||||
function languesConfirmMerge(btn) {
|
||||
const form = btn.closest('form');
|
||||
const select = form.querySelector('select[name="target_id"]');
|
||||
if (!select.value) return true;
|
||||
_languesPendingForm = form;
|
||||
document.getElementById('langues-merge-target-name').textContent = select.options[select.selectedIndex]?.text ?? '';
|
||||
document.getElementById('langues-merge-dialog').showModal();
|
||||
return false;
|
||||
}
|
||||
|
||||
function languesConfirmDelete(btn, name) {
|
||||
_languesPendingForm = btn.closest('form');
|
||||
document.getElementById('langues-delete-name').textContent = name;
|
||||
document.getElementById('langues-delete-dialog').showModal();
|
||||
}
|
||||
|
||||
function languesSubmitPending() {
|
||||
if (_languesPendingForm) _languesPendingForm.submit();
|
||||
}
|
||||
|
||||
function languesToggleAll(src) {
|
||||
document.querySelectorAll('input[name="selected_langs[]"]').forEach(cb => cb.checked = src.checked);
|
||||
languesUpdateBulk();
|
||||
}
|
||||
|
||||
function languesUpdateBulk() {
|
||||
const n = document.querySelectorAll('input[name="selected_langs[]"]:checked').length;
|
||||
document.getElementById('langues-selected-count').textContent = n;
|
||||
document.getElementById('langues-bulk-actions').style.display = n > 1 ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
function languesConfirmBulkMerge() {
|
||||
const checked = document.querySelectorAll('input[name="selected_langs[]"]:checked');
|
||||
if (checked.length < 2) return;
|
||||
document.getElementById('langues-bulk-merge-count').textContent = checked.length;
|
||||
const sel = document.getElementById('langues-bulk-merge-target-select');
|
||||
sel.innerHTML = '<option value="">— Choisir la destination —</option>';
|
||||
checked.forEach(cb => {
|
||||
const tr = cb.closest('tr');
|
||||
sel.innerHTML += '<option value="' + cb.value + '">' + tr.querySelector('td:nth-child(2)').textContent.trim() + '</option>';
|
||||
});
|
||||
document.getElementById('langues-bulk-merge-dialog').showModal();
|
||||
}
|
||||
|
||||
function languesExecBulkMerge() {
|
||||
const targetId = document.getElementById('langues-bulk-merge-target-select').value;
|
||||
if (!targetId) return;
|
||||
document.getElementById('langues-bulk-target').value = targetId;
|
||||
const container = document.getElementById('langues-bulk-checkboxes');
|
||||
container.innerHTML = '';
|
||||
document.querySelectorAll('input[name="selected_langs[]"]:checked').forEach(cb => {
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'hidden';
|
||||
inp.name = 'selected_langs[]';
|
||||
inp.value = cb.value;
|
||||
container.appendChild(inp);
|
||||
});
|
||||
document.getElementById('langues-bulk-merge-dialog').close();
|
||||
document.getElementById('langues-bulk-form').submit();
|
||||
}
|
||||
|
||||
document.addEventListener('htmx:afterSwap', function(evt) {
|
||||
if (evt.target.id === 'langues-table-wrap') {
|
||||
document.querySelectorAll('input[name="selected_langs[]"]').forEach(cb => cb.addEventListener('change', languesUpdateBulk));
|
||||
languesUpdateBulk();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<div class="admin-with-toc">
|
||||
<?php include APP_ROOT . '/templates/admin/partials/admin-toc.php'; ?>
|
||||
<main id="main-content">
|
||||
<h1>Paramètres</h1>
|
||||
|
||||
@@ -458,6 +460,7 @@
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyLogContent(btn) {
|
||||
|
||||
78
app/templates/admin/partials/admin-toc.php
Normal file
78
app/templates/admin/partials/admin-toc.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/**
|
||||
* admin-toc.php — sidebar table-of-contents for long admin pages.
|
||||
*
|
||||
* Scans <section aria-labelledby="..."> elements in #main-content and builds a
|
||||
* slim vertical nav. Uses IntersectionObserver to highlight the active section.
|
||||
*
|
||||
* Usage: include APP_ROOT . '/templates/admin/partials/admin-toc.php';
|
||||
*/
|
||||
?>
|
||||
<nav id="admin-toc" class="admin-toc" aria-label="Sur cette page">
|
||||
<ul class="admin-toc-list" id="admin-toc-list">
|
||||
<!-- populated by JS -->
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var main = document.getElementById('main-content');
|
||||
if (!main) return;
|
||||
|
||||
var tocList = document.getElementById('admin-toc-list');
|
||||
if (!tocList) return;
|
||||
|
||||
// Find all labelled sections
|
||||
var sections = main.querySelectorAll('section[aria-labelledby]');
|
||||
if (sections.length < 2) {
|
||||
document.getElementById('admin-toc').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
var items = [];
|
||||
|
||||
sections.forEach(function(sec) {
|
||||
var headingId = sec.getAttribute('aria-labelledby');
|
||||
var heading = document.getElementById(headingId);
|
||||
if (!heading) return;
|
||||
|
||||
var li = document.createElement('li');
|
||||
var a = document.createElement('a');
|
||||
a.href = '#' + sec.id;
|
||||
a.textContent = heading.textContent;
|
||||
a.setAttribute('data-toc-target', sec.id);
|
||||
li.appendChild(a);
|
||||
tocList.appendChild(li);
|
||||
|
||||
// Ensure section has an id for anchoring
|
||||
if (!sec.id) {
|
||||
sec.id = headingId;
|
||||
}
|
||||
|
||||
items.push({ section: sec, link: a });
|
||||
});
|
||||
|
||||
// IntersectionObserver: highlight the link whose section is most visible
|
||||
var observer = new IntersectionObserver(function(entries) {
|
||||
var best = null;
|
||||
var bestRatio = 0;
|
||||
entries.forEach(function(e) {
|
||||
if (e.intersectionRatio > bestRatio) {
|
||||
bestRatio = e.intersectionRatio;
|
||||
best = e.target;
|
||||
}
|
||||
});
|
||||
if (best) {
|
||||
items.forEach(function(item) {
|
||||
var isActive = item.section === best;
|
||||
item.link.classList.toggle('admin-toc-active', isActive);
|
||||
});
|
||||
}
|
||||
}, {
|
||||
rootMargin: '-10% 0px -70% 0px',
|
||||
threshold: [0, 0.25, 0.5, 0.75, 1]
|
||||
});
|
||||
|
||||
items.forEach(function(item) { observer.observe(item.section); });
|
||||
})();
|
||||
</script>
|
||||
@@ -1,18 +1,17 @@
|
||||
<script>
|
||||
let _pendingTagForm = null;
|
||||
|
||||
function confirmMergeTag(btn) {
|
||||
function tagsConfirmMerge(btn) {
|
||||
const form = btn.closest('form');
|
||||
const select = form.querySelector('select[name="target_id"]');
|
||||
const targetName = select.options[select.selectedIndex]?.text ?? '';
|
||||
if (!select.value) { return true; } // let HTML validation handle empty
|
||||
if (!select.value) return true;
|
||||
_pendingTagForm = form;
|
||||
document.getElementById('merge-target-name').textContent = targetName;
|
||||
document.getElementById('merge-target-name').textContent = select.options[select.selectedIndex]?.text ?? '';
|
||||
document.getElementById('merge-tag-dialog').showModal();
|
||||
return false;
|
||||
}
|
||||
|
||||
function confirmDeleteTag(btn, name) {
|
||||
function tagsConfirmDelete(btn, name) {
|
||||
_pendingTagForm = btn.closest('form');
|
||||
document.getElementById('delete-tag-name').textContent = name;
|
||||
document.getElementById('delete-tag-dialog').showModal();
|
||||
@@ -21,82 +20,80 @@ function confirmDeleteTag(btn, name) {
|
||||
function _submitPendingTagForm() {
|
||||
if (_pendingTagForm) _pendingTagForm.submit();
|
||||
}
|
||||
|
||||
function tagsToggleAll(src) {
|
||||
document.querySelectorAll('input[name="selected_tags[]"]').forEach(cb => cb.checked = src.checked);
|
||||
tagsUpdateBulk();
|
||||
}
|
||||
|
||||
function tagsUpdateBulk() {
|
||||
const n = document.querySelectorAll('input[name="selected_tags[]"]:checked').length;
|
||||
document.getElementById('tags-selected-count').textContent = n;
|
||||
document.getElementById('tags-bulk-actions').style.display = n > 1 ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
function tagsConfirmBulkMerge() {
|
||||
const checked = document.querySelectorAll('input[name="selected_tags[]"]:checked');
|
||||
if (checked.length < 2) return;
|
||||
document.getElementById('bulk-merge-count').textContent = checked.length;
|
||||
const sel = document.getElementById('bulk-merge-target-select');
|
||||
sel.innerHTML = '<option value="">— Choisir la destination —</option>';
|
||||
checked.forEach(cb => {
|
||||
const tr = cb.closest('tr');
|
||||
sel.innerHTML += '<option value="' + cb.value + '">' + tr.querySelector('td:nth-child(2)').textContent.trim() + '</option>';
|
||||
});
|
||||
document.getElementById('bulk-merge-dialog').showModal();
|
||||
}
|
||||
|
||||
function tagsExecBulkMerge() {
|
||||
const targetId = document.getElementById('bulk-merge-target-select').value;
|
||||
if (!targetId) return;
|
||||
document.getElementById('tags-bulk-target').value = targetId;
|
||||
const container = document.getElementById('tags-bulk-checkboxes');
|
||||
container.innerHTML = '';
|
||||
document.querySelectorAll('input[name="selected_tags[]"]:checked').forEach(cb => {
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'hidden';
|
||||
inp.name = 'selected_tags[]';
|
||||
inp.value = cb.value;
|
||||
container.appendChild(inp);
|
||||
});
|
||||
document.getElementById('bulk-merge-dialog').close();
|
||||
document.getElementById('tags-bulk-form').submit();
|
||||
}
|
||||
|
||||
document.addEventListener('htmx:afterSwap', function(evt) {
|
||||
if (evt.target.id === 'tags-table-wrap') {
|
||||
document.querySelectorAll('input[name="selected_tags[]"]').forEach(cb => cb.addEventListener('change', tagsUpdateBulk));
|
||||
tagsUpdateBulk();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<main id="main-content" class="admin-main--list">
|
||||
<div class="admin-list-toolbar admin-list-toolbar--list" style="margin-bottom:var(--space-s)">
|
||||
<div class="admin-toolbar-top">
|
||||
<div class="admin-toolbar-title-row">
|
||||
<h1><a href="/admin/" class="admin-back-btn" title="Retour à la liste"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm48-88a8,8,0,0,1-8,8H107.31l18.35,18.34a8,8,0,0,1-11.32,11.32l-32-32a8,8,0,0,1,0-11.32l32-32a8,8,0,0,1,11.32,11.32L107.31,120H168A8,8,0,0,1,176,128Z"></path></svg></a> Mots-clés (<?= count($tags) ?>)</h1>
|
||||
<h1><a href="/admin/" class="admin-back-btn" title="Retour à la liste"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm48-88a8,8,0,0,1-8,8H107.31l18.35,18.34a8,8,0,0,1-11.32,11.32l-32-32a8,8,0,0,1,0-11.32l32-32a8,8,0,0,1,11.32,11.32L107.31,120H168A8,8,0,0,1,176,128Z"></path></svg></a> Mots-clés</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="tags-search-form"
|
||||
hx-get="/admin/tags-fragment.php"
|
||||
hx-target="#tags-table-wrap"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="input changed delay:200ms from:input[name=q], keyup[key=='Enter'] from:input[name=q]"
|
||||
hx-push-url="false"
|
||||
style="margin-bottom:var(--space-xs)">
|
||||
<input type="text" name="q" placeholder="Rechercher un mot-clé…" style="max-width:300px">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="admin-table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Nom</th>
|
||||
<th scope="col">TFE associés</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($tags as $tag): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($tag['name']) ?></td>
|
||||
<td class="admin-tags-count"><?= (int)$tag['thesis_count'] ?></td>
|
||||
<td>
|
||||
<!-- Rename -->
|
||||
<form method="post" action="actions/tag.php" class="admin-inline-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="rename">
|
||||
<input type="hidden" name="tag_id" value="<?= (int)$tag['id'] ?>">
|
||||
<input class="admin-input--inline" type="text" name="new_name"
|
||||
value="<?= htmlspecialchars($tag['name']) ?>" required>
|
||||
<button type="submit" class="admin-icon-btn admin-icon-btn--edit" title="Renommer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M248,92.68a15.86,15.86,0,0,0-4.69-11.31L174.63,12.68a16,16,0,0,0-22.63,0L123.57,41.11l-58,21.77A16.06,16.06,0,0,0,55.35,75.23L32.11,214.68A8,8,0,0,0,40,224a8.4,8.4,0,0,0,1.32-.11l139.44-23.24a16,16,0,0,0,12.35-10.17l21.77-58L243.31,104A15.87,15.87,0,0,0,248,92.68Zm-69.87,92.19L63.32,204l47.37-47.37a28,28,0,1,0-11.32-11.32L52,192.7,71.13,77.86,126,57.29,198.7,130ZM112,132a12,12,0,1,1,12,12A12,12,0,0,1,112,132Zm96-15.32L139.31,48l24-24L232,92.68Z"></path></svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Merge into another tag -->
|
||||
<form method="post" action="actions/tag.php" class="admin-inline-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="merge">
|
||||
<input type="hidden" name="source_id" value="<?= (int)$tag['id'] ?>">
|
||||
<select name="target_id" class="admin-select--inline" required>
|
||||
<option value="">— Fusionner dans… —</option>
|
||||
<?php foreach ($tags as $other): ?>
|
||||
<?php if ($other['id'] !== $tag['id']): ?>
|
||||
<option value="<?= (int)$other['id'] ?>"><?= htmlspecialchars($other['name']) ?></option>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="button" class="admin-icon-btn admin-icon-btn--merge" title="Fusionner"
|
||||
onclick="return confirmMergeTag(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M216,32H88a8,8,0,0,0-8,8V80H40a8,8,0,0,0-8,8V216a8,8,0,0,0,8,8H168a8,8,0,0,0,8-8V176h40a8,8,0,0,0,8-8V40A8,8,0,0,0,216,32ZM160,208H48V96H160Zm48-48H176V88a8,8,0,0,0-8-8H96V48H208Z"></path></svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Delete -->
|
||||
<form method="post" action="actions/tag.php" class="admin-inline-form">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="tag_id" value="<?= (int)$tag['id'] ?>">
|
||||
<button type="button" class="admin-icon-btn admin-icon-btn--delete" title="Supprimer"
|
||||
onclick="confirmDeleteTag(this, <?= htmlspecialchars(json_encode($tag['name']), ENT_QUOTES) ?>)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="tags-table-wrap" hx-get="/admin/tags-fragment.php" hx-trigger="load" hx-swap="innerHTML">
|
||||
<!-- populated by HTMX -->
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Merge tag confirm -->
|
||||
<dialog id="merge-tag-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="merge-tag-title">
|
||||
<div class="admin-dialog__header">
|
||||
<h2 id="merge-tag-title">Fusionner le tag</h2>
|
||||
@@ -112,7 +109,6 @@ function _submitPendingTagForm() {
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Delete tag confirm -->
|
||||
<dialog id="delete-tag-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="delete-tag-title">
|
||||
<div class="admin-dialog__header">
|
||||
<h2 id="delete-tag-title">Supprimer le tag</h2>
|
||||
@@ -127,3 +123,21 @@ function _submitPendingTagForm() {
|
||||
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="bulk-merge-dialog" class="admin-dialog admin-dialog--sm" aria-labelledby="bulk-merge-title">
|
||||
<div class="admin-dialog__header">
|
||||
<h2 id="bulk-merge-title">Fusionner des tags</h2>
|
||||
<button type="button" class="admin-dialog__close" aria-label="Fermer"
|
||||
onclick="this.closest('dialog').close()">✕</button>
|
||||
</div>
|
||||
<div class="admin-dialog__alert">
|
||||
<p>Fusionner <strong><span id="bulk-merge-count"></span> tag(s)</strong> sélectionné(s) dans :</p>
|
||||
<select id="bulk-merge-target-select" class="admin-select--inline" style="margin-top:var(--space-xs); width:100%" required>
|
||||
<option value="">— Choisir la destination —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="admin-dialog__footer">
|
||||
<button type="button" class="btn btn--warning" onclick="tagsExecBulkMerge()">Fusionner</button>
|
||||
<button type="button" class="btn btn--secondary" onclick="this.closest('dialog').close()">Annuler</button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
Reference in New Issue
Block a user