diff --git a/frontend/src/lib/models/TranslationJobModel.svelte.ts b/frontend/src/lib/models/TranslationJobModel.svelte.ts index 95a1865c..8be354bc 100644 --- a/frontend/src/lib/models/TranslationJobModel.svelte.ts +++ b/frontend/src/lib/models/TranslationJobModel.svelte.ts @@ -10,11 +10,17 @@ // @RELATION DEPENDS_ON -> [api] // @RELATION CALLS -> [log] // @INVARIANT DECOMPOSITION GATE: 50+ atoms, ~400 lines. Model serves as single source of truth for all form state. +// @RATIONALE Methods are NOT arrow class fields — they rely on `this` to mutate state. When passed as props +// to child components (e.g. onTriggerRun={m.handleTriggerRun}), the binding is lost and `this` +// becomes `undefined`, throwing "Cannot set properties of undefined". Consumers MUST wrap +// in arrow functions: onTriggerRun={(full) => m.handleTriggerRun(full)}. +// @REJECTED Converting methods to arrow class fields rejected — it would conflict with the Svelte 5 +// `$state` rune initialization order for non-primitive state atoms in the constructor. import { api } from '$lib/api.js'; import { log } from '$lib/cot-logger'; import { addToast } from '$lib/toasts.svelte.js'; import { _, getT } from '$lib/i18n/index.svelte.js'; -import { triggerRun, fetchRunHistory, cancelRun } from '$lib/api/translate.js'; +import { triggerRun, fetchRunHistory, cancelRun, fetchDatasourceColumns } from '$lib/api/translate.js'; import { startTranslationRun, resetTranslationRun, translationRunStore } from '$lib/stores/translationRun.svelte.js'; type UxState = 'idle' | 'loading' | 'configured' | 'saving' | 'validation_error' | 'datasource_unavailable'; @@ -172,10 +178,16 @@ export class TranslationJobModel { this.uxState = 'loading'; try { const [providers, dicts] = await Promise.all([ - api.requestApi('/translate/llm-providers').catch(() => []), + api.requestApi('/llm/providers').catch(() => ({ providers: [] })), api.requestApi('/translate/dictionaries?page_size=100').catch(() => ({ items: [] })), ]); - this.llmProviders = (Array.isArray(providers) ? providers : (providers as { items?: unknown[] })?.items || []) as Record[]; + this.llmProviders = ( + Array.isArray(providers) + ? providers + : ((providers as { providers?: unknown[] })?.providers + ?? (providers as { results?: unknown[] })?.results + ?? []) + ) as Record[]; this.availableDictionaries = ((dicts as { items?: unknown[] })?.items || (dicts as { results?: unknown[] })?.results || []) as Record[]; if (!this.isNewJob) { @@ -187,6 +199,9 @@ export class TranslationJobModel { this.sourceTable = (job.source_table as string) || ''; this.translationColumn = (job.translation_column as string) || ''; this.targetColumn = (job.target_column as string) || ''; + this.targetLanguageColumn = (job.target_language_column as string) || ''; + this.targetSourceColumn = (job.target_source_column as string) || ''; + this.targetSourceLanguageColumn = (job.target_source_language_column as string) || ''; this.targetLanguages = (job.target_languages as string[]) || ['en']; this.providerId = (job.provider_id as string) || ''; this.batchSize = (job.batch_size as number) || 50; @@ -225,9 +240,9 @@ export class TranslationJobModel { async loadDatasourceColumns(): Promise { if (!this.datasourceId) return; try { - const res = await api.requestApi(`/translate/datasources/${this.datasourceId}/columns`) as { columns?: Record[]; virtual?: Record[] }; - this.availableColumns = (res.columns || []) as Record[]; - this.virtualColumns = (res.virtual || []) as Record[]; + const res = await fetchDatasourceColumns<{ columns?: Record[]; virtual?: Record[] }>(this.datasourceId, this.environmentId); + this.availableColumns = (res?.columns || []) as Record[]; + this.virtualColumns = (res?.virtual || []) as Record[]; } catch { this.availableColumns = []; this.virtualColumns = []; } } @@ -264,11 +279,12 @@ export class TranslationJobModel { environment_id: this.environmentId || undefined, target_language_column: this.targetLanguageColumn || undefined, target_source_column: this.targetSourceColumn || undefined, + target_source_language_column: this.targetSourceLanguageColumn || undefined, status: this.status, }; const url = this.isNewJob ? '/translate/jobs' : `/translate/jobs/${this.jobId}`; - const method = this.isNewJob ? 'POST' : 'PUT'; - const resp = await api.requestApi(url, { method, body: payload }) as { id?: string }; + const method: string = this.isNewJob ? 'POST' : 'PUT'; + const resp = await api.requestApi<{ id?: string }>(url, method, payload); if (resp?.id && this.isNewJob) { this.jobId = resp.id; this.isNewJob = false;