From 17297396242363863d3dcec97234c6760b6ea6ea Mon Sep 17 00:00:00 2001 From: busya Date: Wed, 3 Jun 2026 11:38:38 +0300 Subject: [PATCH] fix(frontend): target column mapping fields not loaded from job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fields were never loaded from backend job data: - targetLanguageColumn (job.target_language_column) - targetSourceColumn (job.target_source_column) - targetSourceLanguageColumn (job.target_source_language_column) Caused empty inputs in Target Config tab after page load. Also added missing target_source_language_column to save payload. @RATIONALE The model's loadJob() method was incomplete — it only loaded targetColumn but omitted the other three target mapping fields. Save payload also omitted target_source_language_column. Verification: browser reconfirmed — all 4 target columns pre-filled with saved values after reload. --- .../lib/models/TranslationJobModel.svelte.ts | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) 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;