// #region DictionaryDetailModel [C:4] [TYPE Model] [SEMANTICS translate,dictionary,entry,crud,import,model] // @BRIEF Screen model for dictionary detail page — manages entry list, CRUD operations, and CSV import. // @INVARIANT Expanding an entry clears previous expanded state. // @INVARIANT Import preview is cleared when import form is closed. // @STATE loading | editing | importing | import_preview | import_conflict | saving // @ACTION loadDictionary() / loadEntries() — Fetch data. // @ACTION saveEntry() / addEntry() / deleteEntry() — CRUD. // @ACTION importDictionary() / previewImport() — Import flow. // @RELATION DEPENDS_ON -> [api] // @RELATION CALLS -> [log] import { api } from '$lib/api.js'; import { log } from '$lib/cot-logger'; import { addToast } from '$lib/toasts.svelte.js'; import { t } from '$lib/i18n/index.svelte.js'; import { LANGUAGE_LABELS, ALL_LANGUAGES } from '$lib/i18n/languages.js'; type AppState = 'idle' | 'loading' | 'editing' | 'importing' | 'import_preview' | 'import_conflict' | 'saving'; interface DictionaryEntry { id: string; source_term: string; target_term: string; source_language?: string; target_language?: string; context_notes?: string; context_data?: unknown; usage_notes?: string; } interface EditForm { source_term: string; target_term: string; source_language: string; target_language: string; context_notes: string; context_data: unknown; usage_notes: string; } interface ImportResult { imported?: number; skipped?: number; errors?: string[]; } export class DictionaryDetailModel { // ── Dictionary data ─────────────────────────────────────────── dictionary: Record | null = $state(null); entries: DictionaryEntry[] = $state([]); totalEntries: number = $state(0); entryPage: number = $state(1); appState: AppState = $state('loading'); // ── Edit ────────────────────────────────────────────────────── editEntryId: string | null = $state(null); editForm: EditForm = $state({ source_term: '', target_term: '', source_language: '', target_language: '', context_notes: '', context_data: null, usage_notes: '' }); editContextDataRaw: string = $state(''); // ── Add new ─────────────────────────────────────────────────── showAddForm: boolean = $state(false); addForm: { source_term: string; target_term: string; source_language: string; target_language: string; context_notes: string } = $state({ source_term: '', target_term: '', source_language: 'und', target_language: 'und', context_notes: '' }); isAdding: boolean = $state(false); // ── Expand (view detail) ────────────────────────────────────── expandedEntryId: string | null = $state(null); expandedEditing: boolean = $state(false); expandedContextData: string = $state(''); expandedUsageNotes: string = $state(''); // ── Import ──────────────────────────────────────────────────── showImportForm: boolean = $state(false); importContent: string = $state(''); importDelimiter: string = $state(''); importOnConflict: string = $state('overwrite'); importPreview: unknown[] = $state([]); importErrors: string[] = $state([]); importResult: ImportResult | null = $state(null); isImporting: boolean = $state(false); // ── Filter ──────────────────────────────────────────────────── filterSourceLanguage: string = $state(''); // ── Context ─────────────────────────────────────────────────── dictionaryId: string = $state(''); allowedLanguages: string[] = $state([]); // ── Derived ─────────────────────────────────────────────────── isLoading = $derived(this.appState === 'loading'); isEditing = $derived(this.appState === 'editing'); isSaving = $derived(this.appState === 'saving'); /** Language options for selects, filtered by allowed_languages (fallback to all). */ languageOptions = $derived( (this.allowedLanguages.length > 0 ? ALL_LANGUAGES.filter(l => this.allowedLanguages.includes(l.code)) : ALL_LANGUAGES) ); // ── Actions ─────────────────────────────────────────────────── async loadAllowedLanguages(): Promise { try { this.allowedLanguages = await api.getAllowedLanguages() || []; } catch { // Non-critical: fall back to full list this.allowedLanguages = []; } } async loadDictionary(): Promise { if (!this.dictionaryId) return; this.appState = 'loading'; try { this.dictionary = await api.requestApi(`/translate/dictionaries/${this.dictionaryId}`) as Record; this.appState = 'idle'; this.loadAllowedLanguages(); this.loadEntries(); } catch (e: unknown) { addToast(e instanceof Error ? e.message : 'Failed to load dictionary', 'error'); this.appState = 'idle'; } } async loadEntries(): Promise { if (!this.dictionaryId) return; try { const params = new URLSearchParams({ page: String(this.entryPage), page_size: '50' }); if (this.filterSourceLanguage) params.append('source_language', this.filterSourceLanguage); const res = await api.requestApi(`/translate/dictionaries/${this.dictionaryId}/entries?${params}`) as { items?: DictionaryEntry[]; entries?: DictionaryEntry[]; total?: number }; this.entries = (res.items || res.entries || []) as DictionaryEntry[]; this.totalEntries = res.total || 0; } catch (e: unknown) { addToast(e instanceof Error ? e.message : 'Failed to load entries', 'error'); } } startEdit(entry: DictionaryEntry): void { this.editEntryId = entry.id; this.editForm = { source_term: entry.source_term, target_term: entry.target_term, source_language: entry.source_language || '', target_language: entry.target_language || '', context_notes: entry.context_notes || '', context_data: entry.context_data, usage_notes: entry.usage_notes || '' }; this.editContextDataRaw = entry.context_data ? JSON.stringify(entry.context_data, null, 2) : ''; this.appState = 'editing'; } cancelEdit(): void { this.editEntryId = null; this.appState = 'idle'; } async saveEntry(): Promise { if (!this.editEntryId) return; this.appState = 'saving'; try { const payload = { ...this.editForm }; if (this.editContextDataRaw) { try { payload.context_data = JSON.parse(this.editContextDataRaw); } catch { /* keep as string */ } } await api.requestApi(`/translate/dictionaries/${this.dictionaryId}/entries/${this.editEntryId}`, 'PUT', payload); addToast(t.translate?.dictionaries?.entry_saved || 'Entry saved', 'success'); this.cancelEdit(); this.loadEntries(); } catch (e: unknown) { addToast(e instanceof Error ? e.message : 'Failed to save entry', 'error'); this.appState = 'editing'; } } async addEntry(): Promise { this.isAdding = true; try { await api.requestApi(`/translate/dictionaries/${this.dictionaryId}/entries`, 'POST', this.addForm); addToast(t.translate?.dictionaries?.entry_added || 'Entry added', 'success'); this.showAddForm = false; this.addForm = { source_term: '', target_term: '', source_language: 'und', target_language: 'und', context_notes: '' }; this.loadEntries(); } catch (e: unknown) { addToast(e instanceof Error ? e.message : 'Failed to add entry', 'error'); } finally { this.isAdding = false; } } async deleteEntry(entryId: string): Promise { if (!confirm(t.translate?.dictionaries?.confirm_delete || 'Delete this entry?')) return; try { await api.requestApi(`/translate/dictionaries/${this.dictionaryId}/entries/${entryId}`, 'DELETE'); addToast(t.translate?.dictionaries?.entry_deleted || 'Entry deleted', 'success'); this.loadEntries(); } catch (e: unknown) { addToast(e instanceof Error ? e.message : 'Failed to delete entry', 'error'); } } toggleExpand(entryId: string): void { if (this.expandedEntryId === entryId) { this.expandedEntryId = null; return; } this.expandedEntryId = entryId; this.expandedEditing = false; } async previewImport(): Promise { if (!this.importContent) return; this.appState = 'importing'; try { const res = await api.requestApi(`/translate/dictionaries/${this.dictionaryId}/import`, 'POST', { content: this.importContent, delimiter: this.importDelimiter || undefined, on_conflict: this.importOnConflict, preview: true }) as { preview?: unknown[]; errors?: string[] }; this.importPreview = (res.preview || []) as unknown[]; this.importErrors = (res.errors || []) as string[]; this.appState = 'import_preview'; } catch (e: unknown) { addToast(e instanceof Error ? e.message : 'Preview failed', 'error'); this.appState = 'idle'; } } async executeImport(): Promise { this.isImporting = true; try { const res = await api.requestApi(`/translate/dictionaries/${this.dictionaryId}/import`, 'POST', { content: this.importContent, delimiter: this.importDelimiter || undefined, on_conflict: this.importOnConflict }) as ImportResult; this.importResult = res; this.showImportForm = false; this.loadEntries(); addToast(t.translate?.dictionaries?.import_success || `Imported ${res.imported || 0} entries`, 'success'); } catch (e: unknown) { addToast(e instanceof Error ? e.message : 'Import failed', 'error'); } finally { this.isImporting = false; } } } // #endregion DictionaryDetailModel