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:
@@ -1,12 +1,16 @@
|
||||
<!-- #region ConfigTabForm [C:4] [TYPE Component] [SEMANTICS translate, config, datasource, llm, columns] -->
|
||||
<!-- @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. -->
|
||||
<!-- @LAYER UI -->
|
||||
<!-- @RELATION DEPENDS_ON -> [EXT:frontend:fetchDatasources] -->
|
||||
<!-- @RELATION DEPENDS_ON -> [EXT:frontend:fetchDatasourceColumns] -->
|
||||
<!-- @RELATION DEPENDS_ON -> [ApiModule.getAllowedLanguages] -->
|
||||
<!-- @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 selected -> Columns loaded, DB dialect detected, mapping ready -->
|
||||
<!-- @UX_STATE Columns loading -> Skeleton pulse in column section -->
|
||||
@@ -20,7 +24,8 @@
|
||||
<!-- @UX_REACTIVITY LocalState -> datasourceList, datasourceLoading, showDatasourceDropdown -->
|
||||
<script lang="ts">
|
||||
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 MultiSelect from '$lib/components/ui/MultiSelect.svelte';
|
||||
import HelpTooltip from '$lib/ui/HelpTooltip.svelte';
|
||||
@@ -59,9 +64,28 @@
|
||||
let showDatasourceDropdown = $state(false);
|
||||
let isColumnsLoading = $state(false);
|
||||
let columnList = $derived(availableColumns);
|
||||
let allowedLanguageCodes: string[] = $state([]);
|
||||
// @RATIONALE Svelte 5 store detection fails for `$t` in deeply nested templates.
|
||||
// Use getT() + $derived to keep reactivity to locale changes.
|
||||
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 ----
|
||||
let logicalColumns = $derived(availableColumns.filter(c => c.is_physical !== false));
|
||||
@@ -202,7 +226,7 @@
|
||||
</label>
|
||||
<select
|
||||
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"
|
||||
>
|
||||
<option value="">{_t.translate?.config?.select_environment}</option>
|
||||
@@ -443,7 +467,7 @@
|
||||
</div>
|
||||
<MultiSelect
|
||||
label=""
|
||||
options={ALL_LANGUAGES}
|
||||
options={languageOptions}
|
||||
bind:selected={targetLanguages}
|
||||
searchable={true}
|
||||
placeholder={_t.translate?.config?.target_language_search_placeholder || 'Search languages...'}
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
availableDictionaries={m.availableDictionaries}
|
||||
bind:disableReasoning={m.disableReasoning}
|
||||
validationErrors={m.validationErrors}
|
||||
onEnvChange={(e) => m.handleEnvChange(e.detail)}
|
||||
onEnvChange={(envId) => m.handleEnvChange(envId)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -245,11 +245,11 @@
|
||||
completedRuns={m.completedRuns}
|
||||
expandedRunIds={m.expandedRunIds}
|
||||
bind:showPageBulkReplace={m.showPageBulkReplace}
|
||||
onTriggerRun={m.handleTriggerRun}
|
||||
onRetryRun={m.handleRetryRun}
|
||||
onRetryInsert={m.handleRetryInsert}
|
||||
onLoadRunHistory={m.loadRunHistory}
|
||||
onToggleRunDetails={m.toggleRunDetails}
|
||||
onTriggerRun={(full) => m.handleTriggerRun(full)}
|
||||
onRetryRun={() => m.handleRetryRun()}
|
||||
onRetryInsert={() => m.handleRetryInsert()}
|
||||
onLoadRunHistory={() => m.loadRunHistory()}
|
||||
onToggleRunDetails={(id) => m.toggleRunDetails(id)}
|
||||
onBulkReplaceApplied={() => { m.showPageBulkReplace = false; m.loadRunHistory(); }}
|
||||
onBulkReplaceClose={() => m.showPageBulkReplace = false}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user