fix(settings): replace remaining native selects with shared component
This commit is contained in:
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({
|
||||
|
||||
+10
@@ -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";
|
||||
|
||||
|
||||
+15
@@ -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|>)/);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user