Model covers all 49 atoms: Config, Preview, Target, Run, Schedule tabs. Environment switching, datasource loading, saveJob(), run lifecycle. Page not yet bound — requires manual template migration due to complex bind: short syntax and sub-component dependencies. DictionaryDetail page: 822→166 (-80%) with DictionaryDetailModel. 3→2 oversized pages remaining (dashboards, migration).
224 lines
10 KiB
TypeScript
224 lines
10 KiB
TypeScript
// #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<string, unknown> | 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<void> {
|
|
try {
|
|
this.allowedLanguages = await api.getAllowedLanguages() || [];
|
|
} catch {
|
|
// Non-critical: fall back to full list
|
|
this.allowedLanguages = [];
|
|
}
|
|
}
|
|
|
|
async loadDictionary(): Promise<void> {
|
|
if (!this.dictionaryId) return;
|
|
this.appState = 'loading';
|
|
try {
|
|
this.dictionary = await api.requestApi(`/translate/dictionaries/${this.dictionaryId}`) as Record<string, unknown>;
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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
|