fix(settings): replace remaining native selects with shared component

This commit is contained in:
ezreal authored and GitHub committed 2026-09-23 15:22:34 +08:00
1 parent de5fde242f
commit 517ddbfa05
6 files changed
+100 -55

No files matched your search

@@ -7730,29 +7730,44 @@ onUnmounted(() => {
<Label for="tableOpenSortMode">{{ t("settings.tableOpenSortMode") }}</Label>
<p class="text-xs text-muted-foreground">{{ t("settings.tableOpenSortDescription") }}</p>
</div>
<select id="tableOpenSortMode" v-model="editTableOpenSortMode" class="h-8 rounded-md border bg-background px-2 text-xs">
<option value="none">{{ t("settings.tableSortUnchanged") }}</option>
<option value="database">{{ t("settings.tableSortDatabase") }}</option>
<option value="local">{{ t("settings.tableSortLocal") }}</option>
</select>
<Select v-model="editTableOpenSortMode">
<SelectTrigger id="tableOpenSortMode" class="h-8 w-44 shrink-0 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">{{ t("settings.tableSortUnchanged") }}</SelectItem>
<SelectItem value="database">{{ t("settings.tableSortDatabase") }}</SelectItem>
<SelectItem value="local">{{ t("settings.tableSortLocal") }}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="tableDatabaseSortDirection">{{ t("settings.tableDatabaseSortDirection") }}</Label>
</div>
<select id="tableDatabaseSortDirection" v-model="editTableDatabaseSortDirection" class="h-8 rounded-md border bg-background px-2 text-xs">
<option value="asc">{{ t("settings.tableSortAscending") }}</option>
<option value="desc">{{ t("settings.tableSortDescending") }}</option>
</select>
<Select v-model="editTableDatabaseSortDirection">
<SelectTrigger id="tableDatabaseSortDirection" class="h-8 w-36 shrink-0 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="asc">{{ t("settings.tableSortAscending") }}</SelectItem>
<SelectItem value="desc">{{ t("settings.tableSortDescending") }}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="tableLocalSortDirection">{{ t("settings.tableLocalSortDirection") }}</Label>
</div>
<select id="tableLocalSortDirection" v-model="editTableLocalSortDirection" class="h-8 rounded-md border bg-background px-2 text-xs">
<option value="asc">{{ t("settings.tableSortAscending") }}</option>
<option value="desc">{{ t("settings.tableSortDescending") }}</option>
</select>
<Select v-model="editTableLocalSortDirection">
<SelectTrigger id="tableLocalSortDirection" class="h-8 w-36 shrink-0 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="asc">{{ t("settings.tableSortAscending") }}</SelectItem>
<SelectItem value="desc">{{ t("settings.tableSortDescending") }}</SelectItem>
</SelectContent>
</Select>
</div>
<div data-settings-search-id="default-auto-keep-results" :class="['settings-item flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2', settingsSearchTargetClass('default-auto-keep-results')]">
<div class="min-w-0 space-y-1">
@@ -5,6 +5,7 @@ import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { listDatabases, mongoListDatabases, redisListDatabases } from "@/lib/backend/api";
import { useConnectionStore } from "@/stores/connectionStore";
import type { McpConnectionPolicy } from "@/stores/settingsStore";
@@ -220,12 +221,15 @@ watch(databasePageCount, () => setDatabasePage(databasePage.value));
<div class="border-b p-2 md:border-b-0 md:border-r">
<div class="flex items-center justify-between gap-2 px-2 pb-2">
<p class="text-xs font-medium text-muted-foreground">{{ t("settings.mcpDatabaseScopeAllowedConnections") }}</p>
<label class="flex h-7 items-center gap-1 rounded border bg-background px-1.5 text-[11px] text-muted-foreground">
{{ t("settings.mcpPerPage") }}
<select v-model.number="pageSize" class="bg-transparent text-[11px] text-foreground outline-none" :disabled="disabled">
<option v-for="size in PAGE_SIZE_OPTIONS" :key="size" :value="size">{{ size }}</option>
</select>
</label>
<div class="flex items-center gap-1 text-[11px] text-muted-foreground">
<span>{{ t("settings.mcpPerPage") }}</span>
<Select :model-value="String(pageSize)" :disabled="disabled" @update:model-value="(value) => (pageSize = Number(value))">
<SelectTrigger size="sm" class="h-7 w-14 px-1.5 text-[11px]"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem v-for="size in PAGE_SIZE_OPTIONS" :key="size" :value="String(size)">{{ size }}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div class="space-y-1">
<button
@@ -335,19 +339,15 @@ watch(databasePageCount, () => setDatabasePage(databasePage.value));
<input type="checkbox" :checked="selectedDatabases.includes(database)" :disabled="disabled || busy" @change="toggleDatabase(database, ($event.target as HTMLInputElement).checked)" />
<span class="min-w-0 flex-1 truncate font-mono">{{ database }}</span>
<Check v-if="selectedDatabases.includes(database)" class="h-3.5 w-3.5 text-green-600" />
<select
v-if="selectedDatabases.includes(database)"
:value="databasePolicyMode(database)"
class="h-7 shrink-0 rounded border bg-background px-1.5 text-[11px]"
:disabled="disabled || busy"
@click.stop
@change="$emit('set:database-policy', selectedConnectionId, database, ($event.target as HTMLSelectElement).value as ExecutionMode | 'inherit')"
>
<option value="inherit">{{ t("settings.mcpConnectionPolicyInherit") }}</option>
<option value="read_only">{{ t("settings.mcpConnectionPolicyReadOnly") }}</option>
<option value="safe_write">{{ t("settings.mcpConnectionPolicySafeWrite") }}</option>
<option value="high_risk_write">{{ t("settings.mcpConnectionPolicyHighRiskWrite") }}</option>
</select>
<Select v-if="selectedDatabases.includes(database)" :model-value="databasePolicyMode(database)" :disabled="disabled || busy" @update:model-value="(value) => $emit('set:database-policy', selectedConnectionId, database, value as ExecutionMode | 'inherit')">
<SelectTrigger size="sm" class="h-7 w-44 shrink-0 text-[11px]" @click.stop><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="inherit">{{ t("settings.mcpConnectionPolicyInherit") }}</SelectItem>
<SelectItem value="read_only">{{ t("settings.mcpConnectionPolicyReadOnly") }}</SelectItem>
<SelectItem value="safe_write">{{ t("settings.mcpConnectionPolicySafeWrite") }}</SelectItem>
<SelectItem value="high_risk_write">{{ t("settings.mcpConnectionPolicyHighRiskWrite") }}</SelectItem>
</SelectContent>
</Select>
</label>
<div v-if="databasePageCount > 1" class="flex items-center justify-center gap-2 border-t px-3 py-2 text-xs text-muted-foreground">
<Button type="button" size="icon-sm" variant="ghost" :disabled="databasePage === 1" :title="t('settings.mcpPreviousPage')" :aria-label="t('settings.mcpPreviousPage')" @click="setDatabasePage(databasePage - 1)"><ChevronLeft /></Button>
@@ -5,6 +5,7 @@ import { useI18n } from "vue-i18n";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import type { ConnectionConfig, SidebarLayout, SidebarOrderEntry } from "@/types/database";
type ScopeMode = "all" | "custom";
@@ -325,32 +326,29 @@ function connectionPolicyMode(connectionId: string): ExecutionMode | "inherit" {
<p class="truncate font-medium">{{ node.type === "group" ? node.name : node.connection.name }}</p>
<p v-if="node.type === 'connection'" class="truncate font-mono text-[10px] text-muted-foreground">{{ node.connection.db_type }} · {{ node.connection.host || node.connection.database || node.id }}</p>
</div>
<select
v-if="node.type === 'group' && selectedGroupIds.has(node.id)"
:value="groupPolicyMode(node.id)"
class="h-7 shrink-0 rounded border bg-background px-1.5 text-[11px]"
:disabled="disabled || busy"
@click.stop
@change="emit('set:group-policy', node.id, ($event.target as HTMLSelectElement).value as ExecutionMode | 'inherit')"
>
<option value="inherit">{{ t("settings.mcpConnectionPolicyInherit") }}</option>
<option value="read_only">{{ t("settings.mcpConnectionPolicyReadOnly") }}</option>
<option value="safe_write">{{ t("settings.mcpConnectionPolicySafeWrite") }}</option>
<option value="high_risk_write">{{ t("settings.mcpConnectionPolicyHighRiskWrite") }}</option>
</select>
<select
<Select v-if="node.type === 'group' && selectedGroupIds.has(node.id)" :model-value="groupPolicyMode(node.id)" :disabled="disabled || busy" @update:model-value="(value) => emit('set:group-policy', node.id, value as ExecutionMode | 'inherit')">
<SelectTrigger size="sm" class="h-7 w-44 shrink-0 text-[11px]" @click.stop><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="inherit">{{ t("settings.mcpConnectionPolicyInherit") }}</SelectItem>
<SelectItem value="read_only">{{ t("settings.mcpConnectionPolicyReadOnly") }}</SelectItem>
<SelectItem value="safe_write">{{ t("settings.mcpConnectionPolicySafeWrite") }}</SelectItem>
<SelectItem value="high_risk_write">{{ t("settings.mcpConnectionPolicyHighRiskWrite") }}</SelectItem>
</SelectContent>
</Select>
<Select
v-if="node.type === 'connection' && (explicitConnectionIds.has(node.id) || Boolean(selectedAncestor(node)))"
:value="connectionPolicyMode(node.id)"
class="h-7 shrink-0 rounded border bg-background px-1.5 text-[11px]"
:model-value="connectionPolicyMode(node.id)"
:disabled="disabled || busy"
@click.stop
@change="emit('set:connection-policy', node.id, ($event.target as HTMLSelectElement).value as ExecutionMode | 'inherit')"
@update:model-value="(value) => emit('set:connection-policy', node.id, value as ExecutionMode | 'inherit')"
>
<option value="inherit">{{ selectedAncestor(node) ? t("settings.mcpGroupPolicyInherit") : t("settings.mcpConnectionPolicyInherit") }}</option>
<option value="read_only">{{ t("settings.mcpConnectionPolicyReadOnly") }}</option>
<option value="safe_write">{{ t("settings.mcpConnectionPolicySafeWrite") }}</option>
<option value="high_risk_write">{{ t("settings.mcpConnectionPolicyHighRiskWrite") }}</option>
</select>
<SelectTrigger size="sm" class="h-7 w-44 shrink-0 text-[11px]" @click.stop><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="inherit">{{ selectedAncestor(node) ? t("settings.mcpGroupPolicyInherit") : t("settings.mcpConnectionPolicyInherit") }}</SelectItem>
<SelectItem value="read_only">{{ t("settings.mcpConnectionPolicyReadOnly") }}</SelectItem>
<SelectItem value="safe_write">{{ t("settings.mcpConnectionPolicySafeWrite") }}</SelectItem>
<SelectItem value="high_risk_write">{{ t("settings.mcpConnectionPolicyHighRiskWrite") }}</SelectItem>
</SelectContent>
</Select>
<Badge v-if="selectedAncestor(node)" variant="secondary" class="shrink-0 rounded font-normal">{{ t("settings.mcpResourceScopeInherited", { group: groupLabel(selectedAncestor(node)!) }) }}</Badge>
<Badge v-else-if="node.type === 'group' && selectedGroupIds.has(node.id)" variant="outline" class="shrink-0 rounded font-normal">{{ t("settings.mcpResourceScopeDynamic") }}</Badge>
<Badge v-else-if="node.type === 'group' && groupHasSelectedDescendant(node)" variant="secondary" class="shrink-0 rounded font-normal">{{ t("settings.mcpResourceScopePartial") }}</Badge>
@@ -42,6 +42,13 @@ async function passthroughComponent() {
}
vi.mock("@/components/ui/button", async () => ({ Button: await passthroughComponent() }));
vi.mock("@/components/ui/badge", async () => ({ Badge: await passthroughComponent() }));
vi.mock("@/components/ui/select", async () => ({
Select: await passthroughComponent(),
SelectContent: await passthroughComponent(),
SelectItem: await passthroughComponent(),
SelectTrigger: await passthroughComponent(),
SelectValue: await passthroughComponent(),
}));
vi.mock("@/components/ui/input", async () => {
const { defineComponent, h } = await import("vue");
const Input = defineComponent({
@@ -19,6 +19,16 @@ vi.mock("@lucide/vue", async () => {
vi.mock("@/components/ui/badge", async () => ({ Badge: (await import("@/components/grid/__tests__/vueHostHarness")).createPassthroughStub("Badge", "span") }));
vi.mock("@/components/ui/button", async () => ({ Button: (await import("@/components/grid/__tests__/vueHostHarness")).createPassthroughStub("Button", "button") }));
vi.mock("@/components/ui/input", async () => ({ Input: (await import("@/components/grid/__tests__/vueHostHarness")).createPassthroughStub("Input", "input") }));
vi.mock("@/components/ui/select", async () => {
const { createPassthroughStub } = await import("@/components/grid/__tests__/vueHostHarness");
return {
Select: createPassthroughStub("Select", "div"),
SelectContent: createPassthroughStub("SelectContent", "div"),
SelectItem: createPassthroughStub("SelectItem", "div"),
SelectTrigger: createPassthroughStub("SelectTrigger", "button"),
SelectValue: createPassthroughStub("SelectValue", "span"),
};
});
import McpResourceScopePicker from "@/components/settings/McpResourceScopePicker.vue";
@@ -0,0 +1,15 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
const settingsSources = ["../../editor/EditorSettingsDialog.vue", "../../editor/SqlFormatterSettingsPanel.vue", "../McpAuthorizationStepper.vue", "../McpDatabaseScopePicker.vue", "../McpResourceScopePicker.vue", "../SettingsTransferPanel.vue"].map((path) => ({
path,
source: readFileSync(new URL(path, import.meta.url), "utf8"),
}));
describe("settings select styling", () => {
it("uses the shared Select component instead of native select controls", () => {
for (const { path, source } of settingsSources) {
expect(source, path).not.toMatch(/<select(?:\s|>)/);
}
});
});