mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
Fix language-search fragment
- mots-clé and language where sharing the same q variable for the input value; they now have unique variables. The admin language-search-fragment was missing App::boot() which the tag-search fragment had. This caused the language suggestion dropdown to not return results in Firefox. Both fragments now follow the same bootstrap pattern. Rewrote language-search-fragment.php to use the same clean pattern as tag-search-fragment.php: ->searchLanguages(), simple exact match check, no predefined exclusion list. Both fragments now share identical structure. fix: exclude main languages (français, anglais, néerlandais) from language-search suggestions
This commit is contained in:
9
TODO.md
9
TODO.md
@@ -1,7 +1,6 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
- [x] Remove margin/padding from .admin-main--toc
|
- [x] Fix language-search-fragment: use searchLanguages() like tag fragment, remove broken predefined exclusion logic
|
||||||
- [x] .admin-main--toc gap: var(--space-xs), sticky top: var(--space-xs)
|
- [x] Both fragments now follow identical patterns
|
||||||
- [x] Reduce .admin-body main padding to --space-s / --space-m / --space-xl
|
- [x] Fix "Créer" button not appearing on language search: both language and tag inputs used name="q" in the same form, causing HTMX to submit the wrong (empty) value — renamed to unique names (language_search_q / tag_search_q)
|
||||||
- [x] Add padding-top: var(--space-m) to article
|
- [x] Exclude Français, Anglais, Néerlandais from language-search suggestions (handled by the checkbox list)
|
||||||
- [x] Language creation: verified getOrCreateLanguage still works; dedup runs before display
|
|
||||||
|
|||||||
@@ -4,58 +4,23 @@
|
|||||||
*
|
*
|
||||||
* Shared HTMX fragment: returns matching language suggestions for the
|
* Shared HTMX fragment: returns matching language suggestions for the
|
||||||
* "Autre(s) langue(s)" interactive search input.
|
* "Autre(s) langue(s)" interactive search input.
|
||||||
*
|
|
||||||
* Included by:
|
|
||||||
* - /admin/language-search-fragment.php (AdminAuth gated)
|
|
||||||
* - partage/index.php special route (public, session already booted)
|
|
||||||
*
|
|
||||||
* Expected POST:
|
|
||||||
* q — search query string (partial language name)
|
|
||||||
* language_autre[] — already selected language names (for exclusion)
|
|
||||||
*/
|
*/
|
||||||
require_once __DIR__ . '/../../src/Database.php';
|
require_once __DIR__ . '/../../src/Database.php';
|
||||||
|
|
||||||
$query = trim(preg_replace('/\s+/', ' ', strtolower($_POST['q'] ?? '')));
|
$query = trim(preg_replace('/\s+/', ' ', strtolower($_POST['language_search_q'] ?? '')));
|
||||||
|
|
||||||
$currentLanguages = isset($_POST['language_autre']) && is_array($_POST['language_autre'])
|
$currentLanguages = isset($_POST['language_autre']) && is_array($_POST['language_autre'])
|
||||||
? array_map(function($l) { return trim(preg_replace('/\s+/', ' ', strtolower($l))); }, $_POST['language_autre'])
|
? array_map(function($l) { return trim(preg_replace('/\s+/', ' ', strtolower($l))); }, $_POST['language_autre'])
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
error_log("[lang-search] q=" . var_export($query, true) . " cur=" . json_encode($currentLanguages));
|
||||||
|
|
||||||
$db = Database::getInstance();
|
$db = Database::getInstance();
|
||||||
|
$results = $db->searchLanguages($query);
|
||||||
|
|
||||||
// Search existing languages by name, excluding predefined ones (already shown as checkboxes)
|
error_log("[lang-search] raw results count=" . count($results) . " rows=" . json_encode(array_slice($results, 0, 5)));
|
||||||
$predefined = ["français", "anglais", "néerlandais", "francais", "neerlandais"];
|
|
||||||
if ($query !== '') {
|
|
||||||
$placeholders = implode(',', array_fill(0, count($predefined), '?'));
|
|
||||||
$stmt = $db->getConnection()->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 ?
|
|
||||||
AND LOWER(l.name) NOT IN ($placeholders)
|
|
||||||
GROUP BY l.id
|
|
||||||
ORDER BY LOWER(l.name) = ? DESC, thesis_count DESC, LOWER(l.name)
|
|
||||||
LIMIT 10"
|
|
||||||
);
|
|
||||||
$stmt->execute(array_merge([$query . '%'], $predefined, [$query]));
|
|
||||||
} else {
|
|
||||||
$placeholders = implode(',', array_fill(0, count($predefined), '?'));
|
|
||||||
$stmt = $db->getConnection()->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) NOT IN ($placeholders)
|
|
||||||
GROUP BY l.id
|
|
||||||
ORDER BY thesis_count DESC, LOWER(l.name)
|
|
||||||
LIMIT 10"
|
|
||||||
);
|
|
||||||
$stmt->execute($predefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
$results = $stmt->fetchAll();
|
// Deduplicate
|
||||||
|
|
||||||
// Deduplicate results by lowercase name
|
|
||||||
$seen = [];
|
$seen = [];
|
||||||
$results = array_values(array_filter($results, function($lang) use (&$seen) {
|
$results = array_values(array_filter($results, function($lang) use (&$seen) {
|
||||||
$key = strtolower($lang['name']);
|
$key = strtolower($lang['name']);
|
||||||
@@ -64,13 +29,17 @@ $results = array_values(array_filter($results, function($lang) use (&$seen) {
|
|||||||
return true;
|
return true;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Filter out already-selected languages (case-insensitive)
|
// Exclude the main languages (handled by the checkbox list above)
|
||||||
$results = array_values(array_filter($results, function($lang) use ($currentLanguages) {
|
$excludedLanguages = ['français', 'anglais', 'néerlandais'];
|
||||||
return !in_array(strtolower($lang['name']), $currentLanguages, true);
|
|
||||||
|
// Filter out already-selected and excluded main languages
|
||||||
|
$results = array_values(array_filter($results, function($lang) use ($currentLanguages, $excludedLanguages) {
|
||||||
|
$lower = strtolower($lang['name']);
|
||||||
|
return !in_array($lower, $currentLanguages, true)
|
||||||
|
&& !in_array($lower, $excludedLanguages, true);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Check if query exactly matches an existing language (case-insensitive)
|
// Exact match check
|
||||||
// Also check against predefined languages to avoid suggesting creation of a checkbox language
|
|
||||||
$exactExists = false;
|
$exactExists = false;
|
||||||
foreach ($results as $lang) {
|
foreach ($results as $lang) {
|
||||||
if (strcasecmp($lang['name'], $query) === 0) {
|
if (strcasecmp($lang['name'], $query) === 0) {
|
||||||
@@ -78,16 +47,12 @@ foreach ($results as $lang) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!$exactExists && $query !== '') {
|
|
||||||
$normalisedQuery = strtolower($query);
|
|
||||||
$normalisedPredefined = array_map('strtolower', $predefined);
|
|
||||||
if (in_array($normalisedQuery, $normalisedPredefined, true)) {
|
|
||||||
$exactExists = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no exact match and query non-empty, suggest creation
|
$inCurrent = in_array($query, $currentLanguages, true);
|
||||||
$canCreate = ($query !== '' && !$exactExists && !in_array($query, $currentLanguages, true));
|
$isExcluded = in_array($query, $excludedLanguages, true);
|
||||||
|
$canCreate = ($query !== '' && !$exactExists && !$inCurrent && !$isExcluded);
|
||||||
|
|
||||||
|
error_log("[lang-search] exactExists=" . var_export($exactExists, true) . " inCurrent=" . var_export($inCurrent, true) . " canCreate=" . var_export($canCreate, true));
|
||||||
?>
|
?>
|
||||||
<?php if (empty($results) && !$canCreate): ?>
|
<?php if (empty($results) && !$canCreate): ?>
|
||||||
<div class="tag-search-empty">Aucune langue trouvée.</div>
|
<div class="tag-search-empty">Aucune langue trouvée.</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
require_once __DIR__ . '/../../src/Database.php';
|
require_once __DIR__ . '/../../src/Database.php';
|
||||||
|
|
||||||
$query = trim(preg_replace('/\s+/', ' ', strtolower($_POST['q'] ?? '')));
|
$query = trim(preg_replace('/\s+/', ' ', strtolower($_POST['tag_search_q'] ?? '')));
|
||||||
$currentTags = isset($_POST['tag']) && is_array($_POST['tag'])
|
$currentTags = isset($_POST['tag']) && is_array($_POST['tag'])
|
||||||
? array_map(function($t) { return trim(preg_replace('/\s+/', ' ', strtolower($t))); }, $_POST['tag'])
|
? array_map(function($t) { return trim(preg_replace('/\s+/', ' ', strtolower($t))); }, $_POST['tag'])
|
||||||
: [];
|
: [];
|
||||||
|
|||||||
@@ -155,6 +155,19 @@
|
|||||||
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
+%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision)
|
||||||
+\\\\\\\ to: xvqonoyt a3f280bc "Add sidebar TOC, simplify Données Secondaires section" (rebased revision)
|
+\\\\\\\ to: xvqonoyt a3f280bc "Add sidebar TOC, simplify Données Secondaires section" (rebased revision)
|
||||||
++ $linkName = $link['name'] ?? '';
|
++ $linkName = $link['name'] ?? '';
|
||||||
|
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: xvqonoyt a3f280bc "Add sidebar TOC, simplify Données Secondaires section" (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: moumuszm 68c0e60c "Fix language-search fragment" (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: moumuszm 1e2ae09f "Fix language-search fragment" (rebased revision)
|
||||||
|
++ $linkName = $link['name'] ?? '';
|
||||||
++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : '';
|
++ $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">
|
<tr class="admin-table-row" onclick="event.stopPropagation(); window.open('/partage/<?= urlencode($link['slug']) ?>', '_blank')" style="cursor:pointer">
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ $langCount = count($selectedLanguages);
|
|||||||
<!-- Search input (hidden when max languages reached) -->
|
<!-- Search input (hidden when max languages reached) -->
|
||||||
<div class="tag-search-input-wrap"<?= $langCount >= $maxLanguages ? ' style="display:none"' : '' ?>>
|
<div class="tag-search-input-wrap"<?= $langCount >= $maxLanguages ? ' style="display:none"' : '' ?>>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="q"
|
name="language_search_q"
|
||||||
id="<?= htmlspecialchars($id) ?>-search"
|
id="<?= htmlspecialchars($id) ?>-search"
|
||||||
class="tag-search-input"
|
class="tag-search-input"
|
||||||
placeholder="<?= htmlspecialchars($placeholder) ?>"
|
placeholder="<?= htmlspecialchars($placeholder) ?>"
|
||||||
@@ -188,8 +188,10 @@ $langCount = count($selectedLanguages);
|
|||||||
|
|
||||||
// Click on suggestion
|
// Click on suggestion
|
||||||
dropdown.addEventListener('click', function(e) {
|
dropdown.addEventListener('click', function(e) {
|
||||||
|
console.log('[lang-search] dropdown click, target:', e.target.tagName, e.target.className);
|
||||||
const btn = e.target.closest('.tag-search-item');
|
const btn = e.target.closest('.tag-search-item');
|
||||||
if (!btn) return;
|
if (!btn) { console.log('[lang-search] no .tag-search-item found in click path'); return; }
|
||||||
|
console.log('[lang-search] found btn:', btn.getAttribute('data-tag-name'), btn.className);
|
||||||
selectLang(btn);
|
selectLang(btn);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -224,12 +226,21 @@ $langCount = count($selectedLanguages);
|
|||||||
search.addEventListener('blur', function() {
|
search.addEventListener('blur', function() {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
if (!dropdown.contains(document.activeElement)) {
|
if (!dropdown.contains(document.activeElement)) {
|
||||||
|
console.log('[lang-search] blur: hiding dropdown');
|
||||||
dropdown.innerHTML = '';
|
dropdown.innerHTML = '';
|
||||||
selectedIdx = -1;
|
selectedIdx = -1;
|
||||||
}
|
}
|
||||||
}, 150);
|
}, 150);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Log HTMX responses
|
||||||
|
document.body.addEventListener('htmx:afterSwap', function(e) {
|
||||||
|
if (e.detail.target && e.detail.target.id === '<?= htmlspecialchars($id) ?>-suggestions') {
|
||||||
|
console.log('[lang-search] htmx:afterSwap, target:', e.detail.target.id, 'html length:', e.detail.target.innerHTML.length);
|
||||||
|
console.log('[lang-search] innerHTML:', e.detail.target.innerHTML);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function htmlEscape(str) {
|
function htmlEscape(str) {
|
||||||
const el = document.createElement('span');
|
const el = document.createElement('span');
|
||||||
el.textContent = str;
|
el.textContent = str;
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ $tagCount = count($selectedTags);
|
|||||||
<!-- Search input (hidden when max tags reached) -->
|
<!-- Search input (hidden when max tags reached) -->
|
||||||
<div class="tag-search-input-wrap"<?= $tagCount >= $maxTags ? ' style="display:none"' : '' ?>>
|
<div class="tag-search-input-wrap"<?= $tagCount >= $maxTags ? ' style="display:none"' : '' ?>>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
name="q"
|
name="tag_search_q"
|
||||||
id="<?= htmlspecialchars($id) ?>-search"
|
id="<?= htmlspecialchars($id) ?>-search"
|
||||||
class="tag-search-input"
|
class="tag-search-input"
|
||||||
placeholder="<?= htmlspecialchars($placeholder) ?>"
|
placeholder="<?= htmlspecialchars($placeholder) ?>"
|
||||||
|
|||||||
Reference in New Issue
Block a user