diff --git a/TODO.md b/TODO.md index 9392d2a..812ac30 100644 --- a/TODO.md +++ b/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
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 diff --git a/app/public/admin/actions/language.php b/app/public/admin/actions/language.php new file mode 100644 index 0000000..1ee26aa --- /dev/null +++ b/app/public/admin/actions/language.php @@ -0,0 +1,72 @@ +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(); diff --git a/app/public/admin/actions/tag.php b/app/public/admin/actions/tag.php index 83594a4..6848a1a 100644 --- a/app/public/admin/actions/tag.php +++ b/app/public/admin/actions/tag.php @@ -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(); diff --git a/app/public/admin/contenus-langues-fragment.php b/app/public/admin/contenus-langues-fragment.php new file mode 100644 index 0000000..21ff1e2 --- /dev/null +++ b/app/public/admin/contenus-langues-fragment.php @@ -0,0 +1,114 @@ +searchLanguages($searchQuery) : $db->getAllLanguagesWithCount(); +} catch (Exception $e) { + die('
Erreur : ' . htmlspecialchars($e->getMessage()) . '
'); +} +?> + + +
+ + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
NomTFE AssociéActions
Aucune langue trouvée.
+
+
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + +
+
+
diff --git a/app/public/admin/tags-fragment.php b/app/public/admin/tags-fragment.php new file mode 100644 index 0000000..93c77fa --- /dev/null +++ b/app/public/admin/tags-fragment.php @@ -0,0 +1,109 @@ +searchTags($searchQuery) : $db->getAllTagsWithCount(); +} catch (Exception $e) { + die('
Erreur : ' . htmlspecialchars($e->getMessage()) . '
'); +} +?> + + +
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
NomTFE associésActions
Aucun mot-clé trouvé.
+
+
+ + + + + +
+ +
+ + + + + +
+ +
+ + + + +
+
+
diff --git a/app/public/admin/tags.php b/app/public/admin/tags.php index dea685d..f10229c 100644 --- a/app/public/admin/tags.php +++ b/app/public/admin/tags.php @@ -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'; diff --git a/app/public/assets/css/admin.css b/app/public/assets/css/admin.css index e02beac..ff20373 100644 --- a/app/public/assets/css/admin.css +++ b/app/public/assets/css/admin.css @@ -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)); +} diff --git a/app/src/Database.php b/app/src/Database.php index 12983e9..5e22fa6 100644 --- a/app/src/Database.php +++ b/app/src/Database.php @@ -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 */ diff --git a/app/templates/admin/acces.php b/app/templates/admin/acces.php index c201867..4d18e7f 100644 --- a/app/templates/admin/acces.php +++ b/app/templates/admin/acces.php @@ -1,3 +1,5 @@ +
+

Accès

@@ -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'])) : ''; ?> @@ -389,6 +404,7 @@
+
diff --git a/app/templates/admin/contenus.php b/app/templates/admin/contenus.php index f832295..4c9e37e 100644 --- a/app/templates/admin/contenus.php +++ b/app/templates/admin/contenus.php @@ -1,3 +1,5 @@ +
+

Contenus

@@ -44,6 +46,35 @@ + +
+

Données Secondaires

+ + +

Gérer les mots-clés

+ + +
+ Langues + +
+ +
+ +
+ +
+
+
+ @@ -163,44 +194,22 @@ '', 'name' => '', 'enabled' => 0]; $title = $b['name'] ?: ($fieldsetName ?? $helpKey); ?> @@ -239,6 +247,131 @@
+
+ + + + +
+

Fusionner la langue

+ +
+
+

Fusionner dans «  » ? La langue source sera supprimée.

+
+ +
+ + +
+

Supprimer la langue

+ +
+
+

Supprimer «  » ? Cette action est irréversible.

+
+ +
+ + +
+

Fusionner des langues

+ +
+
+

Fusionner langue(s) sélectionnée(s) dans :

+ +
+ +
+ + diff --git a/app/templates/admin/tags.php b/app/templates/admin/tags.php index dda2f27..9eb83d4 100644 --- a/app/templates/admin/tags.php +++ b/app/templates/admin/tags.php @@ -1,18 +1,17 @@
-

Mots-clés ()

+

Mots-clés

+ +
+ +
-
- - - - - - - - - - - - - - - - - -
NomTFE associésActions
- -
- - - - - -
- - -
- - - - - -
- - -
- - - - -
-
+
+
-

Fusionner le tag

@@ -112,7 +109,6 @@ function _submitPendingTagForm() {
-

Supprimer le tag

@@ -127,3 +123,21 @@ function _submitPendingTagForm() {
+ + +
+

Fusionner des tags

+ +
+
+

Fusionner tag(s) sélectionné(s) dans :

+ +
+ +