From 6ba39cc01b5cea01aa09f68ec2c50d8793c87549 Mon Sep 17 00:00:00 2001 From: sepehr Date: Sun, 17 May 2026 01:11:18 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20glossary=20selector=20=E2=80=94=20clear?= =?UTF-8?q?=20selected=20state,=20error=20feedback,=20click=20existing=20t?= =?UTF-8?q?emplates=20to=20select?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Round check icon on selected glossary (unmissable) - Error banner when import fails (no more silent spinner) - Click template that already exists = select it (no re-import) - Single fetch for glossaries + templates (faster load) Co-Authored-By: Claude Opus 4.7 --- .../dashboard/translate/GlossarySelector.tsx | 202 ++++++++++-------- 1 file changed, 110 insertions(+), 92 deletions(-) diff --git a/frontend/src/app/dashboard/translate/GlossarySelector.tsx b/frontend/src/app/dashboard/translate/GlossarySelector.tsx index 68dbd60..17bd747 100644 --- a/frontend/src/app/dashboard/translate/GlossarySelector.tsx +++ b/frontend/src/app/dashboard/translate/GlossarySelector.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect, useCallback } from 'react'; -import { BookText, Plus, Loader2, Lock, Check } from 'lucide-react'; +import { BookText, Plus, Loader2, Lock, Check, AlertCircle } from 'lucide-react'; import { API_BASE } from '@/lib/config'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; @@ -36,69 +36,72 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on const { t } = useI18n(); const [glossaries, setGlossaries] = useState([]); const [templates, setTemplates] = useState([]); - const [isLoadingGlossaries, setIsLoadingGlossaries] = useState(true); - const [isLoadingTemplates, setIsLoadingTemplates] = useState(true); + const [isLoading, setIsLoading] = useState(true); const [importingId, setImportingId] = useState(null); + const [error, setError] = useState(null); - const fetchGlossaries = useCallback(async () => { + const fetchData = useCallback(async () => { try { const token = localStorage.getItem('token'); const headers: Record = {}; if (token) headers['Authorization'] = `Bearer ${token}`; - const res = await fetch(`${API_BASE}/api/v1/glossaries?per_page=100`, { headers }); - if (res.ok) { - const data = await res.json(); + + const [glossaryRes, templateRes] = await Promise.all([ + fetch(`${API_BASE}/api/v1/glossaries?per_page=100`, { headers }), + fetch(`${API_BASE}/api/v1/glossaries/templates/list`, { headers }), + ]); + + if (glossaryRes.ok) { + const data = await glossaryRes.json(); setGlossaries(data.data || []); } - } catch { - // ignore - } finally { - setIsLoadingGlossaries(false); - } - }, []); - - const fetchTemplates = useCallback(async () => { - try { - const token = localStorage.getItem('token'); - const headers: Record = {}; - if (token) headers['Authorization'] = `Bearer ${token}`; - const res = await fetch(`${API_BASE}/api/v1/glossaries/templates/list`, { headers }); - if (res.ok) { - const data = await res.json(); + if (templateRes.ok) { + const data = await templateRes.json(); setTemplates(data.data || []); } } catch { // ignore } finally { - setIsLoadingTemplates(false); + setIsLoading(false); } }, []); - useEffect(() => { fetchGlossaries(); }, [fetchGlossaries]); - useEffect(() => { fetchTemplates(); }, [fetchTemplates]); + useEffect(() => { fetchData(); }, [fetchData]); const handleImportTemplate = async (template: TemplateOption) => { + // If a glossary with this template's name already exists, just select it + const existing = glossaries.find( + g => g.name.toLowerCase().includes(template.name.toLowerCase().split('/')[0].trim()) + ); + if (existing) { + onChange(existing.id); + return; + } + const token = localStorage.getItem('token'); - const headers: Record = { - 'Content-Type': 'application/json', - }; + const headers: Record = { 'Content-Type': 'application/json' }; if (token) headers['Authorization'] = `Bearer ${token}`; setImportingId(template.id); + setError(null); try { const res = await fetch(`${API_BASE}/api/v1/glossaries/import`, { method: 'POST', headers, body: JSON.stringify({ template_id: template.id }), }); + if (res.ok) { const data = await res.json(); const newId = data.data?.id; - await fetchGlossaries(); + await fetchData(); // Refresh glossary list if (newId) onChange(newId); + } else { + const errData = await res.json().catch(() => null); + setError(errData?.message || `Import failed (${res.status})`); } - } catch { - // ignore + } catch (e) { + setError('Network error'); } finally { setImportingId(null); } @@ -107,7 +110,6 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on const sourceFlag = SUPPORTED_LANGUAGES.find(l => l.code === sourceLang)?.flag ?? ''; const targetFlag = SUPPORTED_LANGUAGES.find(l => l.code === targetLang)?.flag ?? ''; - // Filter glossaries by source language (show all if auto) const filteredGlossaries = sourceLang === 'auto' ? glossaries : glossaries.filter(g => g.source_language === sourceLang); @@ -133,46 +135,20 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on ) : (
- {/* Selected glossary indicator */} - {selected && ( -
- - - {selected.name} - - - ({selected.terms_count} {t('translate.glossary.terms')}) - - + {/* Error */} + {error && ( +
+ + {error}
)} {/* My glossaries */} - {!isLoadingGlossaries && (filteredGlossaries.length > 0 || selected) && ( + {!isLoading && filteredGlossaries.length > 0 && (
{t('translate.glossary.myGlossaries') || 'Mes glossaires'} - {selected && !filteredGlossaries.find(g => g.id === selected.id) && ( - - )} {filteredGlossaries.map(g => { const flag = SUPPORTED_LANGUAGES.find(l => l.code === g.source_language)?.flag ?? ''; const isSelected = g.id === glossaryId; @@ -182,27 +158,33 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on onClick={() => onChange(isSelected ? null : g.id)} disabled={disabled} className={cn( - "w-full px-4 py-3 text-left text-[10px] font-black uppercase tracking-widest rounded-xl transition-all flex items-center gap-2", + "w-full px-4 py-3.5 text-left rounded-xl transition-all flex items-center gap-3", isSelected - ? "bg-brand-accent/10 text-brand-accent border border-brand-accent/20 shadow-sm" - : "bg-brand-muted/50 dark:bg-white/5 text-brand-dark/60 dark:text-white/60 hover:bg-brand-accent/5 hover:text-brand-dark dark:hover:text-white border border-transparent", + ? "bg-brand-accent/10 border-2 border-brand-accent/30 shadow-sm" + : "bg-brand-muted/50 dark:bg-white/5 border-2 border-transparent hover:border-brand-accent/15 hover:bg-brand-accent/5", disabled && "opacity-50 cursor-not-allowed" )} > {isSelected ? ( - +
+ +
) : ( - {flag} + {flag} )} - {g.name} - - {g.terms_count} {t('translate.glossary.terms')} - +
+
+ {g.name} +
+
+ {g.terms_count} {t('translate.glossary.terms')} +
+
{isSelected && ( - + )} ); @@ -210,8 +192,33 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on
)} + {/* Also show selected glossary if filtered out */} + {selected && !filteredGlossaries.find(g => g.id === selected.id) && ( +
+ + {t('translate.glossary.myGlossaries') || 'Mes glossaires'} + + +
+ )} + {/* Templates */} - {!isLoadingTemplates && templates.length > 0 && ( + {!isLoading && templates.length > 0 && (
{t('translate.glossary.fromTemplate') || 'Créer depuis un template'} @@ -219,36 +226,47 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, glossaryId, on
{templates.map(tmpl => { const isImporting = importingId === tmpl.id; - const alreadyExists = glossaries.some( + const existingGlossary = glossaries.find( g => g.name.toLowerCase().includes(tmpl.name.toLowerCase().split('/')[0].trim()) ); + const isAlreadySelected = existingGlossary?.id === glossaryId; return (