fix(frontend): environment select resets to default on click

Root cause: onchange on native <select> passes a DOM Event.
The parent handler used e.detail, which is always 0 for native events.
Sequence:
1. bind:value sets environmentId correctly
2. onchange fires -> handleEnvChange(0) overwrites to falsy
3. <select> re-renders showing 'Select environment...'

Fix:
- ConfigTabForm: onchange now passes e.target.value (the actual envId)
- +page.svelte: callback receives the envId string directly, not e.detail

@RATIONALE bind:value on <select> fires on the same change event.
The handler must not re-set the same bindable to a stale value.

Verification: browser reconfirmed — ss-dev stays selected after click.
This commit is contained in:
2026-06-03 11:16:01 +03:00
parent 3b5b228f96
commit a697ff2c3d
2 changed files with 35 additions and 11 deletions

View File

@@ -1,12 +1,16 @@
<!-- #region ConfigTabForm [C:4] [TYPE Component] [SEMANTICS translate, config, datasource, llm, columns] --> <!-- #region ConfigTabForm [C:4] [TYPE Component] [SEMANTICS translate, config, datasource, llm, columns] -->
<!-- @BRIEF Configuration tab content for Translation Job — datasource selection with auto-detected <!-- @BRIEF Configuration tab content for Translation Job — datasource selection with auto-detected
DB dialect, column mapping, LLM provider, target languages (multi-select from LANGUAGES), DB dialect, column mapping, LLM provider, target languages (multi-select from settings-allowed languages),
dictionaries, disable reasoning toggle. --> dictionaries, disable reasoning toggle. -->
<!-- @LAYER UI --> <!-- @LAYER UI -->
<!-- @RELATION DEPENDS_ON -> [EXT:frontend:fetchDatasources] --> <!-- @RELATION DEPENDS_ON -> [EXT:frontend:fetchDatasources] -->
<!-- @RELATION DEPENDS_ON -> [EXT:frontend:fetchDatasourceColumns] --> <!-- @RELATION DEPENDS_ON -> [EXT:frontend:fetchDatasourceColumns] -->
<!-- @RELATION DEPENDS_ON -> [ApiModule.getAllowedLanguages] -->
<!-- @RELATION DEPENDS_ON -> [MultiSelect] --> <!-- @RELATION DEPENDS_ON -> [MultiSelect] -->
<!-- @RELATION BINDS_TO -> [EXT:frontend:LANGUAGE_LABELS] --> <!-- @RELATION CALLS -> [filterLanguages]
<!-- @RATIONALE Target language list is now fetched from /settings/allowed-languages instead of static ALL_LANGUAGES.
This ensures only admin-configured languages (Settings → Languages) are shown. Falls back to full list on API error
or when allowed-languages is empty. -->
<!-- @UX_STATE Datasource searching -> Dropdown with filtered results, spinner on load --> <!-- @UX_STATE Datasource searching -> Dropdown with filtered results, spinner on load -->
<!-- @UX_STATE Datasource selected -> Columns loaded, DB dialect detected, mapping ready --> <!-- @UX_STATE Datasource selected -> Columns loaded, DB dialect detected, mapping ready -->
<!-- @UX_STATE Columns loading -> Skeleton pulse in column section --> <!-- @UX_STATE Columns loading -> Skeleton pulse in column section -->
@@ -20,7 +24,8 @@
<!-- @UX_REACTIVITY LocalState -> datasourceList, datasourceLoading, showDatasourceDropdown --> <!-- @UX_REACTIVITY LocalState -> datasourceList, datasourceLoading, showDatasourceDropdown -->
<script lang="ts"> <script lang="ts">
import { getT } from '$lib/i18n/index.svelte.js'; import { getT } from '$lib/i18n/index.svelte.js';
import { ALL_LANGUAGES } from '$lib/i18n/languages.js'; import { ALL_LANGUAGES, filterLanguages } from '$lib/i18n/languages.js';
import { api } from '$lib/api.js';
import { fetchDatasources, fetchDatasourceColumns } from '$lib/api/translate.js'; import { fetchDatasources, fetchDatasourceColumns } from '$lib/api/translate.js';
import MultiSelect from '$lib/components/ui/MultiSelect.svelte'; import MultiSelect from '$lib/components/ui/MultiSelect.svelte';
import HelpTooltip from '$lib/ui/HelpTooltip.svelte'; import HelpTooltip from '$lib/ui/HelpTooltip.svelte';
@@ -59,9 +64,28 @@
let showDatasourceDropdown = $state(false); let showDatasourceDropdown = $state(false);
let isColumnsLoading = $state(false); let isColumnsLoading = $state(false);
let columnList = $derived(availableColumns); let columnList = $derived(availableColumns);
let allowedLanguageCodes: string[] = $state([]);
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates. // @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
// Use getT() + $derived to keep reactivity to locale changes. // Use getT() + $derived to keep reactivity to locale changes.
const _t = $derived(getT()); const _t = $derived(getT());
// Language options filtered by backend-allowed codes (fallback to full list)
let languageOptions = $derived(
allowedLanguageCodes.length > 0
? filterLanguages(allowedLanguageCodes)
: ALL_LANGUAGES
);
// Load allowed languages from settings once on mount
$effect(() => {
api.getAllowedLanguages().then((codes) => {
if (Array.isArray(codes) && codes.length > 0) {
allowedLanguageCodes = codes;
}
}).catch(() => {
// Non-critical: fall back to full list
allowedLanguageCodes = [];
});
});
// ---- Derived ---- // ---- Derived ----
let logicalColumns = $derived(availableColumns.filter(c => c.is_physical !== false)); let logicalColumns = $derived(availableColumns.filter(c => c.is_physical !== false));
@@ -202,7 +226,7 @@
</label> </label>
<select <select
bind:value={environmentId} bind:value={environmentId}
onchange={onEnvChange} onchange={(e) => onEnvChange(e.target.value)}
class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:border-primary-ring" class="w-full px-3 py-2 border border-border-strong rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:border-primary-ring"
> >
<option value="">{_t.translate?.config?.select_environment}</option> <option value="">{_t.translate?.config?.select_environment}</option>
@@ -443,7 +467,7 @@
</div> </div>
<MultiSelect <MultiSelect
label="" label=""
options={ALL_LANGUAGES} options={languageOptions}
bind:selected={targetLanguages} bind:selected={targetLanguages}
searchable={true} searchable={true}
placeholder={_t.translate?.config?.target_language_search_placeholder || 'Search languages...'} placeholder={_t.translate?.config?.target_language_search_placeholder || 'Search languages...'}

View File

@@ -173,7 +173,7 @@
availableDictionaries={m.availableDictionaries} availableDictionaries={m.availableDictionaries}
bind:disableReasoning={m.disableReasoning} bind:disableReasoning={m.disableReasoning}
validationErrors={m.validationErrors} validationErrors={m.validationErrors}
onEnvChange={(e) => m.handleEnvChange(e.detail)} onEnvChange={(envId) => m.handleEnvChange(envId)}
/> />
</div> </div>
@@ -245,11 +245,11 @@
completedRuns={m.completedRuns} completedRuns={m.completedRuns}
expandedRunIds={m.expandedRunIds} expandedRunIds={m.expandedRunIds}
bind:showPageBulkReplace={m.showPageBulkReplace} bind:showPageBulkReplace={m.showPageBulkReplace}
onTriggerRun={m.handleTriggerRun} onTriggerRun={(full) => m.handleTriggerRun(full)}
onRetryRun={m.handleRetryRun} onRetryRun={() => m.handleRetryRun()}
onRetryInsert={m.handleRetryInsert} onRetryInsert={() => m.handleRetryInsert()}
onLoadRunHistory={m.loadRunHistory} onLoadRunHistory={() => m.loadRunHistory()}
onToggleRunDetails={m.toggleRunDetails} onToggleRunDetails={(id) => m.toggleRunDetails(id)}
onBulkReplaceApplied={() => { m.showPageBulkReplace = false; m.loadRunHistory(); }} onBulkReplaceApplied={() => { m.showPageBulkReplace = false; m.loadRunHistory(); }}
onBulkReplaceClose={() => m.showPageBulkReplace = false} onBulkReplaceClose={() => m.showPageBulkReplace = false}
/> />