mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 08:09:18 +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:
91
TODO.md
91
TODO.md
@@ -1,88 +1,7 @@
|
||||
# TODO
|
||||
|
||||
- [x] Remove delete-all TFE from parametres (template, dialog, controller, DB method, logger)
|
||||
- [x] Move Formulaire + Types de travaux from parametres to contenus under Paramètres du Formulaire h2
|
||||
- [x] Restructure contenus Formulaire: sub-headings for Restrictions, Degré d'ouverture, Types de travaux, Structure
|
||||
- [x] Copy mots-clé htmx system (dropdown, pills, create) to Autre Langue input
|
||||
- [x] Languages: store lowercase, display with ucfirst (getOrCreateLanguage, CSV import, getAllLanguages, v_theses_full, schema seed data, migration 025)
|
||||
- [x] CSV importer: add AP aliases for D&P du multiple, PACS variants, Narraion typo
|
||||
- [x] Move default semantic form element styles (checkbox, radio, select) from admin.css/form.css into common.css
|
||||
- [x] Keep specific layouts/classes in form.css (admin-form grid, checkbox-group layout, etc.)
|
||||
- [x] Ensure selects, checkboxes, and radios are properly styled globally
|
||||
- [x] Converge towards the styled form appearance rather than unstyled
|
||||
- [x] Fix: replace mb_strlen/mb_substr/mb_strtolower with strlen/substr/strtolower (mbstring extension missing on server, caused fatal error on partage submit at ThesisCreateController line 511)
|
||||
- [x] Fix: annexes checkbox in partage form clears other file inputs — scoped HTMX swap to #annexes-input-block instead of replacing entire #format-fichiers-block
|
||||
- [x] Fix: website/video/audio inputs should be inline in Fichiers fieldset (not sub-fieldsets) — removed <fieldset class="fichiers-format-extra"> wrappers
|
||||
- [x] Fix: video/audio show direct upload input when PeerTube disabled — parallel inputs: PeerTube upload when enabled, direct `files[]` upload when disabled
|
||||
- [x] Fix: format checkboxes HTMX include missing has_annexes — added it so annexes state preserved across format changes
|
||||
- [x] Fix: format checkbox toggle clears file inputs — split into two blocks: #format-fichiers-block (stable: TFE/annexes/couverture/note) and #format-extras-block (swappable: website/video/audio extras)
|
||||
- [x] Fix: remove website label/legend input — website section now shows only URL field
|
||||
- [x] Fix: format-extras not appearing — moved #format-extras-block inside Fichiers fieldset (after annexes), uses hx-select to extract from response
|
||||
- [x] Remove duration_pages, duration_minutes, file_size_info entirely (form, schema, DB, views, controllers, tests, CSV export, email)
|
||||
- [x] Rename cc4r → cc2r everywhere (DB column, schema, PHP code) to fix pre-existing naming inconsistency
|
||||
- [x] Merge Publication fieldset's is_published checkbox into Backoffice fieldset
|
||||
- [x] Fix: PHP parse error in admin/index.php — `''` escape in single-quoted string not valid in PHP 8.5
|
||||
- [x] Add explanation hint to is_published checkbox
|
||||
- [x] Admin index: use AP code instead of full name in list and filter dropdown
|
||||
- [x] Admin index: remove pagination, show all theses in table
|
||||
- [x] Admin index: HTMX column sorting (click header → reload table via HTMX)
|
||||
- [x] Admin index: prevent action buttons from stacking vertically
|
||||
- [x] Admin index: compact icon-only buttons (SVG) with tooltips replacing text labels
|
||||
- [x] Admin index: reduce status badge font size
|
||||
- [x] Admin index: change Voir icon to spectacles/circles SVG
|
||||
- [x] Admin index: split Statut column into Publié and Accès
|
||||
- [x] Admin index: tighten table cell padding to --space-3xs
|
||||
- [x] Admin index: remove main padding, add padding to .admin-list-toolbar and #admin-table-wrap
|
||||
- [x] Admin index: remove subtitles from Titre column
|
||||
- [x] Admin index: add alternating row background colors
|
||||
- [x] Admin index: remove #admin-table-container wrapper element, use #admin-table-wrap
|
||||
- [x] Admin index: rearrange toolbar — stats beside title, buttons in single row, search inline with selects on right
|
||||
- [x] Admin index: fix toolbar search inputs vertical stacking (add flex-direction: row)
|
||||
- [x] Admin index: stats as fieldsets with legend labels (Total/Publiés/Attente), centered content
|
||||
- [x] Admin index: remove horizontal padding from toolbar and table-wrap (keep bottom padding only)
|
||||
- [x] Admin index: make Filtrer/Réinitialiser buttons same size as inputs (add btn--sm)
|
||||
- [x] Admin index: rename Importer un CSV → Importer, merge Export CSV + Export fichiers → Exporter modal with checkboxes
|
||||
- [x] Create unified /admin/actions/export.php endpoint with ?csv=1&files=1&db=1 support
|
||||
- [x] Admin index: move export DB from parametres into exporter modal
|
||||
- [x] Admin index: color stats — green for Publiés, yellow for Attente
|
||||
- [x] Remove export-db fieldset and dialog from parametres.php
|
||||
- [x] Replace large JS script in admin index with minimal version (8 lines vs ~70)
|
||||
- [x] Bulk actions: form wraps all checkboxes, no dynamic DOM building in JS
|
||||
- [x] Replace emoji/text buttons in acces.php/acces-etudiante.php with Phosphor SVG icon buttons
|
||||
- [x] Replace text button in contenus.php with pencil SVG icon button
|
||||
- [x] Add Phosphor Icons credit to about page
|
||||
- [x] Add back-to-list arrow button to add/edit/recapitulatif/contenus-edit/tags page titles, make bigger (32px)
|
||||
- [x] Remove Voir button from admin index — row click navigates to recapitulatif
|
||||
- [x] Add hover highlight on clickable table rows
|
||||
- [x] AP column no-wrap, N/A values greyed
|
||||
- [x] Tags page: back button, admin-main--list, no padding, icon buttons, #admin-table-wrap
|
||||
- [x] Move #bulk-actions into fixed-height #bulk-meta-bar at top, prevent layout shift
|
||||
- [x] Credits: move Iconographie below Typographies
|
||||
- [x] Rename Accès étudiant·e → Liens étudiant·e
|
||||
- [x] Add 'name' column to share_links (schema + migration + model)
|
||||
- [x] Add edit dialog for share links (edit name, password, expiration)
|
||||
- [x] Row click opens link in new tab, remove Visiter button
|
||||
- [x] Add update action to acces-etudiante.php controller
|
||||
- [x] Add ShareLink::update() method
|
||||
- [x] Remove admin-bulk-meta__default (count bar), clean up layout
|
||||
- [x] Fix nested form issue: per-row publish/unpublish buttons now submit correctly
|
||||
- [x] Fix delete button: stopPropagation prevents row nav on confirm
|
||||
- [x] CSV import: set is_published=0 by default instead of 1
|
||||
- [x] Fix AP column wrapping: CSS selector main > table didn't match (table nested in div)
|
||||
- [x] Admin mobile block screen: fix inline style beating media query, use CSS default display:none instead
|
||||
- [x] Fix deploy: add missing 023b migration to rename cc4r→cc2r, make run.php skip 'no such column' errors
|
||||
- [x] Fix FK constraint violation on edit: pass null instead of 0 for absent orientation/ap/finality
|
||||
- [x] Mots-clés: interactive tag search with HTMX suggestions, pill display, round bin-icon remove buttons
|
||||
- [x] Mots-clés: lowercase enforcement, deduplication, absolute dropdown, keyboard arrows/enter/escape, blur hide, spacing + counter above input, CSV import lowercased, space-collapse normalization, minimum 3 keywords required
|
||||
- [x] ErrorHandler: shared static helper for structured error_log + user-friendly messages with precise FK field extraction from SQLite errors. Applied to 12 action files + 6 public controllers + 2 form controllers + partage. Covers FK, UNIQUE, NOT NULL constraint types.
|
||||
- [x] Fix: findOrCreateAuthor cannot clear email (empty string skips update, leaves old email)
|
||||
- [x] Fix: "NON" stored as literal email string in authors table — cleaned existing DB rows, added OUI/NON→null guard in findOrCreateAuthor and CSV import
|
||||
- [x] Fix: contact_interne field in edit form never saved — removed dead field from form and dead validation from create controller
|
||||
- [x] Fix: formulaire.php unconditionally suppresses display_errors even in dev mode
|
||||
- [x] Fix: access_type_id radio has no "none" option — added "— Non défini" radio for admin mode
|
||||
- [x] Fix: radio button checked detection broken (int vs string strict comparison in fieldset-licence-explanation.php)
|
||||
- [x] Rename author_email → contact_interne in v_theses_full view, controllers, forms, and templates
|
||||
- [x] Rename author_show_contact → contact_public in v_theses_full view
|
||||
- [x] Restore contact_interne backoffice field with proper variable name, wire to save (takes precedence over mail)
|
||||
- [x] Fix: htmlspecialchars(null) crash in old() on admin/add.php and admin/edit.php (null values in form data)
|
||||
- [x] Fix: jury-fieldset.php old() return type confusion (array vs string) for jury_lecteur:_interne:_externe keys
|
||||
- [x] Rename "Éditer Données Secondaires" → "Données Secondaires", remove wrapping fieldset on Mots-clés
|
||||
- [x] Create admin-toc.php sidebar TOC partial with IntersectionObserver
|
||||
- [x] Include TOC in contenus.php, acces.php, parametres.php
|
||||
- [x] Add .admin-with-toc flex layout and .admin-toc CSS
|
||||
- [x] Fonts: verified Ductus + BBB DM Sans are loaded via variables.css → common.css
|
||||
|
||||
72
app/public/admin/actions/language.php
Normal file
72
app/public/admin/actions/language.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../../bootstrap.php';
|
||||
require_once __DIR__ . '/../../../src/AdminAuth.php';
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST'
|
||||
|| !isset($_POST['csrf_token'], $_SESSION['csrf_token'])
|
||||
|| !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
||||
http_response_code(403);
|
||||
die("Accès refusé.");
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../../../src/Database.php';
|
||||
require_once __DIR__ . '/../../../src/AdminLogger.php';
|
||||
require_once __DIR__ . '/../../../src/ErrorHandler.php';
|
||||
|
||||
try {
|
||||
$db = new Database();
|
||||
$logger = AdminLogger::make();
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
switch ($action) {
|
||||
case 'rename':
|
||||
$id = filter_var($_POST['language_id'] ?? '', FILTER_VALIDATE_INT);
|
||||
$newName = trim($_POST['new_name'] ?? '');
|
||||
if (!$id || $newName === '') throw new Exception("Paramètres invalides.");
|
||||
$db->renameLanguage($id, $newName);
|
||||
break;
|
||||
|
||||
case 'merge':
|
||||
$sourceId = filter_var($_POST['source_id'] ?? '', FILTER_VALIDATE_INT);
|
||||
$targetId = filter_var($_POST['target_id'] ?? '', FILTER_VALIDATE_INT);
|
||||
if (!$sourceId || !$targetId) throw new Exception("Paramètres invalides.");
|
||||
$db->mergeLanguage($sourceId, $targetId);
|
||||
break;
|
||||
|
||||
case 'merge_bulk':
|
||||
$targetId = filter_var($_POST['target_id'] ?? '', FILTER_VALIDATE_INT);
|
||||
$sourceIds = isset($_POST['selected_langs']) && is_array($_POST['selected_langs'])
|
||||
? array_map('intval', $_POST['selected_langs'])
|
||||
: [];
|
||||
if (!$targetId || empty($sourceIds)) throw new Exception("Paramètres invalides.");
|
||||
$sourceIds = array_values(array_diff($sourceIds, [$targetId]));
|
||||
if (empty($sourceIds)) throw new Exception("Aucune source à fusionner.");
|
||||
foreach ($sourceIds as $sid) {
|
||||
$db->mergeLanguage($sid, $targetId);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
$id = filter_var($_POST['language_id'] ?? '', FILTER_VALIDATE_INT);
|
||||
if (!$id) throw new Exception("ID invalide.");
|
||||
$db->deleteLanguage($id);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception("Action inconnue.");
|
||||
}
|
||||
|
||||
App::flash('success', "Opération effectuée.");
|
||||
} catch (Exception $e) {
|
||||
ErrorHandler::log('language', $e);
|
||||
App::flash('error', ErrorHandler::userMessage($e));
|
||||
}
|
||||
|
||||
$redirect = '/admin/contenus.php';
|
||||
// Allow the caller to override the redirect
|
||||
if (!empty($_POST['return']) && str_starts_with($_POST['return'], '/')) {
|
||||
$redirect = $_POST['return'];
|
||||
}
|
||||
header('Location: ' . $redirect);
|
||||
exit();
|
||||
@@ -36,6 +36,20 @@ try {
|
||||
$logger->logTagAction('merge', ['source_id' => $sourceId, 'target_id' => $targetId]);
|
||||
break;
|
||||
|
||||
case 'merge_bulk':
|
||||
$targetId = filter_var($_POST['target_id'] ?? '', FILTER_VALIDATE_INT);
|
||||
$sourceIds = isset($_POST['selected_tags']) && is_array($_POST['selected_tags'])
|
||||
? array_map('intval', $_POST['selected_tags'])
|
||||
: [];
|
||||
if (!$targetId || empty($sourceIds)) throw new Exception("Paramètres invalides.");
|
||||
$sourceIds = array_values(array_diff($sourceIds, [$targetId]));
|
||||
if (empty($sourceIds)) throw new Exception("Aucune source à fusionner.");
|
||||
foreach ($sourceIds as $sid) {
|
||||
$db->mergeTag($sid, $targetId);
|
||||
$logger->logTagAction('merge', ['source_id' => $sid, 'target_id' => $targetId]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
$id = filter_var($_POST['tag_id'] ?? '', FILTER_VALIDATE_INT);
|
||||
if (!$id) throw new Exception("ID invalide.");
|
||||
@@ -53,5 +67,10 @@ try {
|
||||
App::flash('error', ErrorHandler::userMessage($e));
|
||||
}
|
||||
|
||||
header('Location: /admin/tags.php');
|
||||
$redirect = '/admin/tags.php';
|
||||
// Allow the caller to override the redirect
|
||||
if (!empty($_POST['return']) && str_starts_with($_POST['return'], '/')) {
|
||||
$redirect = $_POST['return'];
|
||||
}
|
||||
header('Location: ' . $redirect);
|
||||
exit();
|
||||
|
||||
114
app/public/admin/contenus-langues-fragment.php
Normal file
114
app/public/admin/contenus-langues-fragment.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
/**
|
||||
* contenus-langues-fragment.php
|
||||
*
|
||||
* HTMX fragment: returns the langues table for the contenus page,
|
||||
* optionally filtered by a search query.
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../../src/Database.php';
|
||||
|
||||
$searchQuery = trim($_GET['q'] ?? '');
|
||||
|
||||
try {
|
||||
$db = new Database();
|
||||
$languages = ($searchQuery !== '') ? $db->searchLanguages($searchQuery) : $db->getAllLanguagesWithCount();
|
||||
} catch (Exception $e) {
|
||||
die('<div class="flash-error">Erreur : ' . htmlspecialchars($e->getMessage()) . '</div>');
|
||||
}
|
||||
?>
|
||||
<div id="langues-bulk-actions" class="admin-bulk-actions" style="display:none">
|
||||
<strong><span id="langues-selected-count">0</span> langue(s) sélectionnée(s)</strong>
|
||||
<div class="admin-bulk-btns">
|
||||
<button type="button" class="btn btn--sm btn--warning admin-btn-merge"
|
||||
onclick="languesConfirmBulkMerge()"
|
||||
title="Fusionner les langues sélectionnées">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M224,152V96a8,8,0,0,0-8-8H168V40a8,8,0,0,0-8-8H40a8,8,0,0,0-8,8v64h0v56a8,8,0,0,0,8,8H88v48a8,8,0,0,0,8,8H216a8,8,0,0,0,8-8V152Zm-68.69,56L48,100.69V59.31L196.69,208Zm-96-160h41.38L208,155.31v41.38ZM208,132.69,179.31,104H208Zm-56-56L123.31,48H152ZM48,123.31,76.69,152H48Zm56,56L132.69,208H104Z"></path></svg>
|
||||
Fusionner
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="langues-bulk-form" method="post" action="actions/language.php">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="merge_bulk">
|
||||
<input type="hidden" name="return" value="/admin/contenus.php">
|
||||
<input type="hidden" name="target_id" id="langues-bulk-target" value="">
|
||||
<div id="langues-bulk-checkboxes"></div>
|
||||
</form>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><input type="checkbox" onchange="languesToggleAll(this)"></th>
|
||||
<th scope="col">Nom</th>
|
||||
<th scope="col">TFE Associé</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($languages)): ?>
|
||||
<tr><td colspan="4" class="admin-empty">Aucune langue trouvée.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($languages as $lang): ?>
|
||||
<tr>
|
||||
<td><input type="checkbox" name="selected_langs[]" value="<?= (int)$lang['id'] ?>" onchange="languesUpdateBulk()"></td>
|
||||
<td><?= htmlspecialchars($lang['name']) ?></td>
|
||||
<td class="admin-tags-count"><?= (int)$lang['thesis_count'] ?></td>
|
||||
<td class="admin-actions-col">
|
||||
<div class="admin-actions">
|
||||
<form method="post" action="actions/language.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="return" value="/admin/contenus.php">
|
||||
<input type="hidden" name="language_id" value="<?= (int)$lang['id'] ?>">
|
||||
<input class="admin-input--inline" type="text" name="new_name"
|
||||
value="<?= htmlspecialchars($lang['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>
|
||||
|
||||
<form method="post" action="actions/language.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="return" value="/admin/contenus.php">
|
||||
<input type="hidden" name="source_id" value="<?= (int)$lang['id'] ?>">
|
||||
<select name="target_id" class="admin-select--inline" required>
|
||||
<option value="">— Fusionner dans… —</option>
|
||||
<?php foreach ($languages as $other): ?>
|
||||
<?php if ($other['id'] !== $lang['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 languesConfirmMerge(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>
|
||||
|
||||
<form method="post" action="actions/language.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="return" value="/admin/contenus.php">
|
||||
<input type="hidden" name="language_id" value="<?= (int)$lang['id'] ?>">
|
||||
<button type="button" class="admin-icon-btn admin-icon-btn--delete" title="Supprimer"
|
||||
onclick="languesConfirmDelete(this, <?= htmlspecialchars(json_encode($lang['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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
109
app/public/admin/tags-fragment.php
Normal file
109
app/public/admin/tags-fragment.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* tags-fragment.php
|
||||
*
|
||||
* HTMX fragment: returns the tags table, optionally filtered by search query.
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../../src/Database.php';
|
||||
|
||||
$searchQuery = trim($_GET['q'] ?? '');
|
||||
|
||||
try {
|
||||
$db = new Database();
|
||||
$tags = ($searchQuery !== '') ? $db->searchTags($searchQuery) : $db->getAllTagsWithCount();
|
||||
} catch (Exception $e) {
|
||||
die('<div class="flash-error">Erreur : ' . htmlspecialchars($e->getMessage()) . '</div>');
|
||||
}
|
||||
?>
|
||||
<div id="tags-bulk-actions" class="admin-bulk-actions" style="display:none">
|
||||
<strong><span id="tags-selected-count">0</span> mot(s)-clé(s) sélectionné(s)</strong>
|
||||
<div class="admin-bulk-btns">
|
||||
<button type="button" class="btn btn--sm btn--warning admin-btn-merge"
|
||||
onclick="tagsConfirmBulkMerge()"
|
||||
title="Fusionner les mots-clés sélectionnés">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M224,152V96a8,8,0,0,0-8-8H168V40a8,8,0,0,0-8-8H40a8,8,0,0,0-8,8v64h0v56a8,8,0,0,0,8,8H88v48a8,8,0,0,0,8,8H216a8,8,0,0,0,8-8V152Zm-68.69,56L48,100.69V59.31L196.69,208Zm-96-160h41.38L208,155.31v41.38ZM208,132.69,179.31,104H208Zm-56-56L123.31,48H152ZM48,123.31,76.69,152H48Zm56,56L132.69,208H104Z"></path></svg>
|
||||
Fusionner
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="tags-bulk-form" method="post" action="actions/tag.php">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
||||
<input type="hidden" name="action" value="merge_bulk">
|
||||
<input type="hidden" name="target_id" id="tags-bulk-target" value="">
|
||||
<div id="tags-bulk-checkboxes"></div>
|
||||
</form>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><input type="checkbox" onchange="tagsToggleAll(this)"></th>
|
||||
<th scope="col">Nom</th>
|
||||
<th scope="col">TFE associés</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($tags)): ?>
|
||||
<tr><td colspan="4" class="admin-empty">Aucun mot-clé trouvé.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($tags as $tag): ?>
|
||||
<tr>
|
||||
<td><input type="checkbox" name="selected_tags[]" value="<?= (int)$tag['id'] ?>" onchange="tagsUpdateBulk()"></td>
|
||||
<td><?= htmlspecialchars($tag['name']) ?></td>
|
||||
<td class="admin-tags-count"><?= (int)$tag['thesis_count'] ?></td>
|
||||
<td class="admin-actions-col">
|
||||
<div class="admin-actions">
|
||||
<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>
|
||||
|
||||
<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 tagsConfirmMerge(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>
|
||||
|
||||
<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="tagsConfirmDelete(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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -7,17 +7,8 @@ if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../../src/Database.php';
|
||||
|
||||
$pageTitle = "Gestion des mots-clés";
|
||||
|
||||
try {
|
||||
$db = new Database();
|
||||
$tags = $db->getAllTagsWithCount();
|
||||
} catch (Exception $e) {
|
||||
die("Erreur : " . htmlspecialchars($e->getMessage()));
|
||||
}
|
||||
|
||||
$isAdmin = true; $bodyClass = 'admin-body';
|
||||
require_once APP_ROOT . '/templates/head.php';
|
||||
include APP_ROOT . '/templates/header.php';
|
||||
|
||||
@@ -615,16 +615,14 @@ th.admin-ap-col {
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
/* Fieldsets inside flat sections: no card border */
|
||||
.admin-body main > section[aria-labelledby^="settings-"] fieldset,
|
||||
.admin-body main > section[aria-labelledby^="form-settings-"] fieldset {
|
||||
/* Fieldsets inside flat settings sections: no card border */
|
||||
.admin-body main > section[aria-labelledby^="settings-"] fieldset {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: var(--space-m) 0;
|
||||
}
|
||||
|
||||
.admin-body main > section[aria-labelledby^="settings-"] fieldset legend,
|
||||
.admin-body main > section[aria-labelledby^="form-settings-"] fieldset legend {
|
||||
.admin-body main > section[aria-labelledby^="settings-"] fieldset legend {
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
@@ -633,8 +631,7 @@ th.admin-ap-col {
|
||||
}
|
||||
|
||||
.admin-body main > section[aria-labelledby^="settings-"] > h2,
|
||||
.admin-body main > section[aria-labelledby^="static-pages-"] > h2,
|
||||
.admin-body main > section[aria-labelledby^="form-settings-"] > h2 {
|
||||
.admin-body main > section[aria-labelledby^="static-pages-"] > h2 {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
@@ -1992,3 +1989,56 @@ th.admin-ap-col {
|
||||
50.01% { transform: scaleX(1); transform-origin: right; }
|
||||
100% { transform: scaleX(0); transform-origin: right; }
|
||||
}
|
||||
|
||||
/* ── Sidebar TOC ───────────────────────────────────────────────────────────── */
|
||||
|
||||
.admin-with-toc {
|
||||
display: flex;
|
||||
gap: var(--space-m);
|
||||
align-items: flex-start;
|
||||
max-width: var(--content-max-width, 1200px);
|
||||
margin: 0 auto;
|
||||
padding: 0 var(--space-s);
|
||||
}
|
||||
|
||||
.admin-with-toc > main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-toc {
|
||||
position: sticky;
|
||||
top: var(--space-m);
|
||||
width: 160px;
|
||||
flex-shrink: 0;
|
||||
padding-top: var(--space-s);
|
||||
}
|
||||
|
||||
.admin-toc-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.admin-toc-list a {
|
||||
display: block;
|
||||
padding: var(--space-3xs) var(--space-2xs);
|
||||
font-size: var(--step--2);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
border-left: 2px solid transparent;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.admin-toc-list a:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.admin-toc-list a.admin-toc-active {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
border-left-color: var(--accent, var(--color-primary));
|
||||
}
|
||||
|
||||
@@ -1252,6 +1252,107 @@ class Database
|
||||
$this->pdo->prepare('DELETE FROM tags WHERE id = ?')->execute([$id]);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// LANGUAGE MANAGEMENT (admin)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Search languages by name prefix. Returns up to 10 matching languages.
|
||||
* If $query is empty, returns the most-used languages (up to 10).
|
||||
*/
|
||||
public function searchLanguages(string $query = ''): array
|
||||
{
|
||||
$query = trim($query);
|
||||
if ($query === '') {
|
||||
$stmt = $this->pdo->query('
|
||||
SELECT l.id, UPPER(SUBSTR(l.name,1,1)) || SUBSTR(l.name,2) as name,
|
||||
COUNT(DISTINCT tl.thesis_id) as thesis_count
|
||||
FROM languages l
|
||||
LEFT JOIN thesis_languages tl ON l.id = tl.language_id
|
||||
GROUP BY l.id
|
||||
ORDER BY thesis_count DESC, l.name COLLATE NOCASE
|
||||
LIMIT 10
|
||||
');
|
||||
} else {
|
||||
$stmt = $this->pdo->prepare('
|
||||
SELECT l.id, UPPER(SUBSTR(l.name,1,1)) || SUBSTR(l.name,2) as name,
|
||||
COUNT(DISTINCT tl.thesis_id) as thesis_count
|
||||
FROM languages l
|
||||
LEFT JOIN thesis_languages tl ON l.id = tl.language_id
|
||||
WHERE LOWER(l.name) LIKE LOWER(?)
|
||||
GROUP BY l.id
|
||||
ORDER BY LOWER(l.name) = LOWER(?) DESC, thesis_count DESC, l.name COLLATE NOCASE
|
||||
LIMIT 10
|
||||
');
|
||||
$stmt->execute([$query . '%', $query]);
|
||||
}
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all languages with a count of associated theses.
|
||||
*/
|
||||
public function getAllLanguagesWithCount(): array
|
||||
{
|
||||
$stmt = $this->pdo->query('
|
||||
SELECT l.id, UPPER(SUBSTR(l.name,1,1)) || SUBSTR(l.name,2) as name,
|
||||
COUNT(DISTINCT tl.thesis_id) as thesis_count
|
||||
FROM languages l
|
||||
LEFT JOIN thesis_languages tl ON l.id = tl.language_id
|
||||
GROUP BY l.id
|
||||
ORDER BY l.name COLLATE NOCASE
|
||||
');
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a language. Throws if the new name already exists.
|
||||
*/
|
||||
public function renameLanguage(int $id, string $newName): void
|
||||
{
|
||||
$newName = trim($newName);
|
||||
if ($newName === '') {
|
||||
throw new Exception('Le nom de la langue ne peut pas être vide.');
|
||||
}
|
||||
$stmt = $this->pdo->prepare('SELECT id FROM languages WHERE LOWER(name) = LOWER(?) AND id != ?');
|
||||
$stmt->execute([$newName, $id]);
|
||||
if ($stmt->fetch()) {
|
||||
throw new Exception('Une langue avec ce nom existe déjà.');
|
||||
}
|
||||
$this->pdo->prepare('UPDATE languages SET name = ? WHERE id = ?')->execute([$newName, $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge sourceId into targetId: reassign all thesis_languages rows, then delete source.
|
||||
*/
|
||||
public function mergeLanguage(int $sourceId, int $targetId): void
|
||||
{
|
||||
if ($sourceId === $targetId) {
|
||||
throw new Exception('Source et destination identiques.');
|
||||
}
|
||||
$this->pdo->beginTransaction();
|
||||
try {
|
||||
$this->pdo->prepare('
|
||||
INSERT OR IGNORE INTO thesis_languages (language_id, thesis_id)
|
||||
SELECT ?, thesis_id FROM thesis_languages WHERE language_id = ?
|
||||
')->execute([$targetId, $sourceId]);
|
||||
$this->pdo->prepare('DELETE FROM thesis_languages WHERE language_id = ?')->execute([$sourceId]);
|
||||
$this->pdo->prepare('DELETE FROM languages WHERE id = ?')->execute([$sourceId]);
|
||||
$this->pdo->commit();
|
||||
} catch (\Throwable $e) {
|
||||
$this->pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a language and all its thesis_languages rows.
|
||||
*/
|
||||
public function deleteLanguage(int $id): void
|
||||
{
|
||||
$this->pdo->prepare('DELETE FROM languages WHERE id = ?')->execute([$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get orientation ID by name
|
||||
*/
|
||||
|
||||
@@ -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