feat(ui): add searchable selects to compare and transfer dialogs

This commit is contained in:
大峰 authored and GitHub committed 2026-06-30 22:53:27 +08:00
1 parent 2e8d399dab
commit f1f99f6f10
11 files changed
+361 -209

No files matched your search

@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import SearchableSelect from "@/components/ui/searchable-select/SearchableSelect.vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { useToast } from "@/composables/useToast";
import { databaseOptionsForConnection } from "@/composables/useDatabaseOptions";
@@ -889,31 +890,46 @@ watch(
<div class="grid grid-cols-[1fr_auto_1fr] gap-4 items-start">
<div class="space-y-2">
<Label class="text-xs font-medium">{{ t("diff.source") }}</Label>
<Select :model-value="sourceConnectionId" @update:model-value="(v: any) => (sourceConnectionId = String(v))">
<SelectTrigger class="h-8 text-xs">
<SearchableSelect
v-model="sourceConnectionId"
:options="sqlConnections.map((c) => c.id)"
:placeholder="t('diff.selectConnection')"
:search-placeholder="t('diff.searchConnection')"
:empty-text="t('common.noResults')"
:display-name="(id) => sqlConnections.find((c) => c.id === id)?.name ?? id"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex items-center gap-2">
<DatabaseIcon v-if="sourceConnectionId" :db-type="connectionIconType(sourceConnectionId)" class="w-3.5 h-3.5" />
<SelectValue :placeholder="t('diff.selectConnection')" />
<DatabaseIcon :db-type="connectionIconType(option)" class="w-3.5 h-3.5" />
{{ label }}
</div>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="connection in sqlConnections" :key="connection.id" :value="connection.id">
{{ connection.name }}
</SelectItem>
</SelectContent>
</Select>
<Select :model-value="sourceDatabase" @update:model-value="(v: any) => (sourceDatabase = String(v))">
<SelectTrigger class="h-8 text-xs"><SelectValue :placeholder="t('diff.selectDatabase')" /></SelectTrigger>
<SelectContent>
<SelectItem v-for="database in sourceDatabases" :key="database" :value="database">{{ database }}</SelectItem>
</SelectContent>
</Select>
<Select v-if="sourceSchemas.length" :model-value="sourceSchema" @update:model-value="(v: any) => (sourceSchema = String(v))">
<SelectTrigger class="h-8 text-xs"><SelectValue :placeholder="t('diff.selectSchema')" /></SelectTrigger>
<SelectContent>
<SelectItem v-for="schema in sourceSchemas" :key="schema" :value="schema">{{ schema }}</SelectItem>
</SelectContent>
</Select>
</template>
</SearchableSelect>
<SearchableSelect
v-model="sourceDatabase"
:options="sourceDatabases"
:placeholder="t('diff.selectDatabase')"
:search-placeholder="t('diff.searchDatabase')"
:empty-text="t('common.noResults')"
:disabled="!sourceDatabases.length"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
/>
<SearchableSelect
v-if="sourceSchemas.length"
v-model="sourceSchema"
:options="sourceSchemas"
:placeholder="t('diff.selectSchema')"
:search-placeholder="t('diff.searchSchema')"
:empty-text="t('common.noResults')"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
/>
<div class="space-y-2 rounded-lg border p-2">
<div class="flex items-center justify-between gap-2">
@@ -960,42 +976,59 @@ watch(
<div class="space-y-2">
<Label class="text-xs font-medium">{{ t("diff.target") }}</Label>
<Select :model-value="targetConnectionId" @update:model-value="(v: any) => (targetConnectionId = String(v))">
<SelectTrigger class="h-8 text-xs">
<SearchableSelect
v-model="targetConnectionId"
:options="sqlConnections.map((c) => c.id)"
:placeholder="t('diff.selectConnection')"
:search-placeholder="t('diff.searchConnection')"
:empty-text="t('common.noResults')"
:display-name="(id) => sqlConnections.find((c) => c.id === id)?.name ?? id"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex items-center gap-2">
<DatabaseIcon v-if="targetConnectionId" :db-type="connectionIconType(targetConnectionId)" class="w-3.5 h-3.5" />
<SelectValue :placeholder="t('diff.selectConnection')" />
<DatabaseIcon :db-type="connectionIconType(option)" class="w-3.5 h-3.5" />
{{ label }}
</div>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="connection in sqlConnections" :key="connection.id" :value="connection.id">
{{ connection.name }}
</SelectItem>
</SelectContent>
</Select>
<Select :model-value="targetDatabase" @update:model-value="(v: any) => (targetDatabase = String(v))">
<SelectTrigger class="h-8 text-xs"><SelectValue :placeholder="t('diff.selectDatabase')" /></SelectTrigger>
<SelectContent>
<SelectItem v-for="database in targetDatabases" :key="database" :value="database">{{ database }}</SelectItem>
</SelectContent>
</Select>
<Select v-if="targetSchemas.length" :model-value="targetSchema" @update:model-value="(v: any) => (targetSchema = String(v))">
<SelectTrigger class="h-8 text-xs"><SelectValue :placeholder="t('diff.selectSchema')" /></SelectTrigger>
<SelectContent>
<SelectItem v-for="schema in targetSchemas" :key="schema" :value="schema">{{ schema }}</SelectItem>
</SelectContent>
</Select>
</template>
</SearchableSelect>
<SearchableSelect
v-model="targetDatabase"
:options="targetDatabases"
:placeholder="t('diff.selectDatabase')"
:search-placeholder="t('diff.searchDatabase')"
:empty-text="t('common.noResults')"
:disabled="!targetDatabases.length"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
/>
<SearchableSelect
v-if="targetSchemas.length"
v-model="targetSchema"
:options="targetSchemas"
:placeholder="t('diff.selectSchema')"
:search-placeholder="t('diff.searchSchema')"
:empty-text="t('common.noResults')"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
/>
<div v-if="!isBatchCompare" class="space-y-1">
<Label class="text-xs font-medium">{{ t("dataCompare.targetTable") }}</Label>
<Select :model-value="targetTable" @update:model-value="(v: any) => (targetTable = String(v))">
<SelectTrigger class="h-8 text-xs">
<SelectValue :placeholder="t('dataCompare.selectTable')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="table in targetTables" :key="table" :value="table">{{ table }}</SelectItem>
</SelectContent>
</Select>
<SearchableSelect
v-model="targetTable"
:options="targetTables"
:placeholder="t('dataCompare.selectTable')"
:search-placeholder="t('dataCompare.searchTable')"
:empty-text="t('common.noResults')"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
/>
</div>
<div v-else class="space-y-2 rounded-lg border p-3 text-xs">
<div class="font-medium">{{ t("dataCompare.autoMatchHint") }}</div>
@@ -4,6 +4,7 @@ import { useI18n } from "vue-i18n";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import SearchableSelect from "@/components/ui/searchable-select/SearchableSelect.vue";
import { useConnectionStore } from "@/stores/connectionStore";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import * as api from "@/lib/api";
@@ -253,46 +254,57 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st
<div class="space-y-1.5">
<Label class="text-xs">{{ t("diff.connection") }}</Label>
<Select :model-value="sourceConnectionId" @update:model-value="(v: any) => $emit('update:sourceConnectionId', String(v))">
<SelectTrigger class="h-8 text-xs">
<SearchableSelect
:model-value="sourceConnectionId"
@update:model-value="(v: string) => $emit('update:sourceConnectionId', v)"
:options="sqlConnections.map((c) => c.id)"
:placeholder="t('diff.selectConnection')"
:search-placeholder="t('diff.searchConnection')"
:empty-text="t('common.noResults')"
:display-name="(id) => sqlConnections.find((c) => c.id === id)?.name ?? id"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex items-center gap-2">
<DatabaseIcon v-if="sourceConnectionId" :db-type="connectionIconType(sourceConnectionId)" class="w-3.5 h-3.5" />
<SelectValue :placeholder="t('diff.selectConnection')" />
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.driver_profile || sqlConnections.find((c) => c.id === option)?.db_type || 'mysql'" class="w-3.5 h-3.5" />
{{ label }}
</div>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="c in sqlConnections" :key="c.id" :value="c.id">
<div class="flex items-center gap-2">
<DatabaseIcon :db-type="c.driver_profile || c.db_type" class="w-3.5 h-3.5" />
{{ c.name }}
</div>
</SelectItem>
</SelectContent>
</Select>
</template>
</SearchableSelect>
</div>
<div class="space-y-1.5">
<Label class="text-xs">{{ t("diff.database") }}</Label>
<Select :model-value="sourceDatabase" @update:model-value="(v: any) => $emit('update:sourceDatabase', String(v))">
<SelectTrigger class="h-8 text-xs" :disabled="!sourceDatabases.length">
<SelectValue :placeholder="t('diff.selectDatabase')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="db in sourceDatabases" :key="db" :value="db">{{ db }}</SelectItem>
</SelectContent>
</Select>
<SearchableSelect
:model-value="sourceDatabase"
@update:model-value="(v: string) => $emit('update:sourceDatabase', v)"
:options="sourceDatabases"
:placeholder="t('diff.selectDatabase')"
:search-placeholder="t('diff.searchDatabase')"
:empty-text="t('common.noResults')"
:disabled="!sourceDatabases.length"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
/>
</div>
<div v-if="isSchemaAware(sourceConfig?.db_type)" class="space-y-1.5">
<Label class="text-xs">{{ t("diff.schema") }}</Label>
<Select :model-value="sourceSchema" @update:model-value="(v: any) => $emit('update:sourceSchema', String(v))">
<SelectTrigger class="h-8 text-xs" :disabled="!sourceSchemas.length">
<SelectValue :placeholder="t('diff.selectSchema')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="schema in sourceSchemas" :key="schema" :value="schema">{{ schema }}</SelectItem>
</SelectContent>
</Select>
<SearchableSelect
:model-value="sourceSchema"
@update:model-value="(v: string) => $emit('update:sourceSchema', v)"
:options="sourceSchemas"
:placeholder="t('diff.selectSchema')"
:search-placeholder="t('diff.searchSchema')"
:empty-text="t('common.noResults')"
:disabled="!sourceSchemas.length"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
/>
</div>
<!-- Source Info -->
@@ -326,46 +338,57 @@ async function fetchDbVersion(connectionId: string, database: string, schema: st
<div class="space-y-1.5">
<Label class="text-xs">{{ t("diff.connection") }}</Label>
<Select :model-value="targetConnectionId" @update:model-value="(v: any) => $emit('update:targetConnectionId', String(v))">
<SelectTrigger class="h-8 text-xs">
<SearchableSelect
:model-value="targetConnectionId"
@update:model-value="(v: string) => $emit('update:targetConnectionId', v)"
:options="sqlConnections.map((c) => c.id)"
:placeholder="t('diff.selectConnection')"
:search-placeholder="t('diff.searchConnection')"
:empty-text="t('common.noResults')"
:display-name="(id) => sqlConnections.find((c) => c.id === id)?.name ?? id"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex items-center gap-2">
<DatabaseIcon v-if="targetConnectionId" :db-type="connectionIconType(targetConnectionId)" class="w-3.5 h-3.5" />
<SelectValue :placeholder="t('diff.selectConnection')" />
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.driver_profile || sqlConnections.find((c) => c.id === option)?.db_type || 'mysql'" class="w-3.5 h-3.5" />
{{ label }}
</div>
</SelectTrigger>
<SelectContent>
<SelectItem v-for="c in sqlConnections" :key="c.id" :value="c.id">
<div class="flex items-center gap-2">
<DatabaseIcon :db-type="c.driver_profile || c.db_type" class="w-3.5 h-3.5" />
{{ c.name }}
</div>
</SelectItem>
</SelectContent>
</Select>
</template>
</SearchableSelect>
</div>
<div class="space-y-1.5">
<Label class="text-xs">{{ t("diff.database") }}</Label>
<Select :model-value="targetDatabase" @update:model-value="(v: any) => $emit('update:targetDatabase', String(v))">
<SelectTrigger class="h-8 text-xs" :disabled="!targetDatabases.length">
<SelectValue :placeholder="t('diff.selectDatabase')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="db in targetDatabases" :key="db" :value="db">{{ db }}</SelectItem>
</SelectContent>
</Select>
<SearchableSelect
:model-value="targetDatabase"
@update:model-value="(v: string) => $emit('update:targetDatabase', v)"
:options="targetDatabases"
:placeholder="t('diff.selectDatabase')"
:search-placeholder="t('diff.searchDatabase')"
:empty-text="t('common.noResults')"
:disabled="!targetDatabases.length"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
/>
</div>
<div v-if="isSchemaAware(targetConfig?.db_type)" class="space-y-1.5">
<Label class="text-xs">{{ t("diff.schema") }}</Label>
<Select :model-value="targetSchema" @update:model-value="(v: any) => $emit('update:targetSchema', String(v))">
<SelectTrigger class="h-8 text-xs" :disabled="!targetSchemas.length">
<SelectValue :placeholder="t('diff.selectSchema')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="schema in targetSchemas" :key="schema" :value="schema">{{ schema }}</SelectItem>
</SelectContent>
</Select>
<SearchableSelect
:model-value="targetSchema"
@update:model-value="(v: string) => $emit('update:targetSchema', v)"
:options="targetSchemas"
:placeholder="t('diff.selectSchema')"
:search-placeholder="t('diff.searchSchema')"
:empty-text="t('common.noResults')"
:disabled="!targetSchemas.length"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
/>
</div>
<!-- Target Info -->
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import SearchableSelect from "@/components/ui/searchable-select/SearchableSelect.vue";
import { useConnectionStore } from "@/stores/connectionStore";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import * as api from "@/lib/api";
@@ -15,7 +16,7 @@ import type { DatabaseType } from "@/types/database";
import { isSchemaAware, supportsTransfer } from "@/lib/databaseCapabilities";
import { databaseOptionsForConnection } from "@/composables/useDatabaseOptions";
import { useExportTracker } from "@/composables/useExportTracker";
import { ArrowRightLeft, Loader2, Square, CheckSquare } from "@lucide/vue";
import { ArrowRightLeft, ArrowLeftRight, Loader2, Square, CheckSquare } from "@lucide/vue";
const { t } = useI18n();
const { startDataTransferTask } = useExportTracker();
@@ -289,15 +290,11 @@ async function startTransfer() {
function getConnectionName(id: string) {
return store.connections.find((c) => c.id === id)?.name ?? id;
}
function getConnectionType(id: string): DatabaseType {
return store.connections.find((c) => c.id === id)?.db_type ?? "mysql";
}
</script>
<template>
<Dialog v-model:open="open">
<DialogContent class="sm:max-w-[560px] max-h-[80vh] flex flex-col overflow-hidden" @interact-outside.prevent>
<DialogContent class="sm:max-w-[780px] max-h-[80vh] flex flex-col overflow-hidden" @interact-outside.prevent>
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<ArrowRightLeft class="w-4 h-4" />
@@ -307,111 +304,127 @@ function getConnectionType(id: string): DatabaseType {
<div class="flex-1 min-h-0 overflow-auto">
<div class="grid gap-4 py-3">
<!-- Source Section -->
<div class="space-y-3">
<div class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
{{ t("transfer.source") }}
</div>
<!-- Source / Target Side by Side -->
<div class="grid grid-cols-[1fr_auto_1fr] gap-4 items-start">
<!-- Source Section -->
<div class="space-y-3">
<div class="text-sm font-medium text-blue-500">
{{ t("transfer.source") }}
</div>
<div class="grid grid-cols-2 gap-3">
<div class="space-y-1.5">
<Label class="text-xs">{{ t("transfer.sourceConnection") }}</Label>
<Select v-model="sourceConnectionId">
<SelectTrigger class="h-8 text-xs">
<div v-if="sourceConnectionId" class="flex items-center gap-1.5">
<DatabaseIcon :db-type="getConnectionType(sourceConnectionId)" class="w-3.5 h-3.5" />
<span class="truncate">{{ getConnectionName(sourceConnectionId) }}</span>
<SearchableSelect
v-model="sourceConnectionId"
:options="sqlConnections.map((c) => c.id)"
:placeholder="t('transfer.selectConnection')"
:search-placeholder="t('transfer.searchConnection')"
:empty-text="t('common.noResults')"
:display-name="getConnectionName"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex items-center gap-1.5">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.db_type ?? 'mysql'" class="w-3.5 h-3.5" />
{{ label }}
</div>
<SelectValue v-else />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="c in sqlConnections" :key="c.id" :value="c.id">
<div class="flex items-center gap-1.5">
<DatabaseIcon :db-type="c.db_type" class="w-3.5 h-3.5" />
{{ c.name }}
</div>
</SelectItem>
</SelectContent>
</Select>
</template>
</SearchableSelect>
</div>
<div class="space-y-1.5">
<Label class="text-xs">{{ t("transfer.sourceDatabase") }}</Label>
<Select v-model="sourceDatabase" :disabled="!sourceDatabases.length">
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="db in sourceDatabases" :key="db" :value="db">{{ db }}</SelectItem>
</SelectContent>
</Select>
<SearchableSelect
v-model="sourceDatabase"
:options="sourceDatabases"
:placeholder="t('transfer.selectDatabase')"
:search-placeholder="t('transfer.searchDatabase')"
:empty-text="t('common.noResults')"
:disabled="!sourceDatabases.length"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
/>
</div>
<div v-if="sourceSchemas.length" class="space-y-1.5">
<Label class="text-xs">{{ t("transfer.sourceSchema") }}</Label>
<SearchableSelect
v-model="sourceSchema"
:options="sourceSchemas"
:placeholder="t('transfer.selectSchema')"
:search-placeholder="t('transfer.searchSchema')"
:empty-text="t('common.noResults')"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
/>
</div>
</div>
<div v-if="sourceSchemas.length" class="space-y-1.5">
<Label class="text-xs">{{ t("transfer.sourceSchema") }}</Label>
<Select v-model="sourceSchema">
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="schema in sourceSchemas" :key="schema" :value="schema">{{ schema }}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<!-- Target Section -->
<div class="space-y-3">
<div class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
{{ t("transfer.target") }}
<!-- Arrow -->
<div class="flex items-center pt-8">
<ArrowLeftRight class="w-5 h-5 text-muted-foreground" />
</div>
<div class="grid grid-cols-2 gap-3">
<!-- Target Section -->
<div class="space-y-3">
<div class="text-sm font-medium text-green-500">
{{ t("transfer.target") }}
</div>
<div class="space-y-1.5">
<Label class="text-xs">{{ t("transfer.targetConnection") }}</Label>
<Select v-model="targetConnectionId">
<SelectTrigger class="h-8 text-xs">
<div v-if="targetConnectionId" class="flex items-center gap-1.5">
<DatabaseIcon :db-type="getConnectionType(targetConnectionId)" class="w-3.5 h-3.5" />
<span class="truncate">{{ getConnectionName(targetConnectionId) }}</span>
<SearchableSelect
v-model="targetConnectionId"
:options="sqlConnections.map((c) => c.id)"
:placeholder="t('transfer.selectConnection')"
:search-placeholder="t('transfer.searchConnection')"
:empty-text="t('common.noResults')"
:display-name="getConnectionName"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #option-label="{ option, label }">
<div class="flex items-center gap-1.5">
<DatabaseIcon :db-type="sqlConnections.find((c) => c.id === option)?.db_type ?? 'mysql'" class="w-3.5 h-3.5" />
{{ label }}
</div>
<SelectValue v-else />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="c in sqlConnections" :key="c.id" :value="c.id">
<div class="flex items-center gap-1.5">
<DatabaseIcon :db-type="c.db_type" class="w-3.5 h-3.5" />
{{ c.name }}
</div>
</SelectItem>
</SelectContent>
</Select>
</template>
</SearchableSelect>
</div>
<div class="space-y-1.5">
<Label class="text-xs">{{ t("transfer.targetDatabase") }}</Label>
<Select v-model="targetDatabase" :disabled="!targetDatabases.length">
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="db in targetDatabases" :key="db" :value="db">{{ db }}</SelectItem>
</SelectContent>
</Select>
<SearchableSelect
v-model="targetDatabase"
:options="targetDatabases"
:placeholder="t('transfer.selectDatabase')"
:search-placeholder="t('transfer.searchDatabase')"
:empty-text="t('common.noResults')"
:disabled="!targetDatabases.length"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
/>
</div>
</div>
<div v-if="targetSchemas.length" class="space-y-1.5">
<Label class="text-xs">{{ t("transfer.targetSchema") }}</Label>
<Select v-model="targetSchema">
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="schema in targetSchemas" :key="schema" :value="schema">{{ schema }}</SelectItem>
</SelectContent>
</Select>
<div v-if="targetSchemas.length" class="space-y-1.5">
<Label class="text-xs">{{ t("transfer.targetSchema") }}</Label>
<SearchableSelect
v-model="targetSchema"
:options="targetSchemas"
:placeholder="t('transfer.selectSchema')"
:search-placeholder="t('transfer.searchSchema')"
:empty-text="t('common.noResults')"
trigger-variant="outline"
trigger-class="h-8 w-full justify-between text-xs"
content-class="w-[var(--reka-popover-trigger-width)]"
/>
</div>
</div>
</div>
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from "vue";
import type { HTMLAttributes } from "vue";
import { Check, ChevronDown, Search } from "@lucide/vue";
import { Check, ChevronDown, Search, X } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import type { ButtonVariants } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -16,8 +16,9 @@ const props = withDefaults(
placeholder: string;
searchPlaceholder: string;
emptyText: string;
loadingText: string;
loadingText?: string;
loading?: boolean;
disabled?: boolean;
allowCustom?: boolean;
triggerVariant?: ButtonVariants["variant"];
triggerClass?: HTMLAttributes["class"];
@@ -25,10 +26,14 @@ const props = withDefaults(
itemClass?: HTMLAttributes["class"];
displayName?: (option: string) => string;
normalizeCustom?: (value: string) => string;
clearable?: boolean;
}>(),
{
loading: false,
disabled: false,
allowCustom: false,
clearable: false,
loadingText: "Loading...",
triggerVariant: "ghost",
displayName: (option: string) => option,
normalizeCustom: (value: string) => value,
@@ -149,11 +154,12 @@ function handleKeydown(event: KeyboardEvent) {
<template>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button type="button" :variant="triggerVariant" :title="selectedLabel" :class="cn('h-6 w-auto max-w-56 min-w-0 justify-between gap-1 border-0 bg-transparent px-1 text-xs font-normal shadow-none hover:bg-muted/50 focus-visible:ring-0', triggerClass)">
<Button type="button" :variant="triggerVariant" :disabled="disabled" :title="selectedLabel" :class="cn('h-6 w-auto max-w-56 min-w-0 justify-between gap-1 border-0 bg-transparent px-1 text-xs font-normal shadow-none hover:bg-muted/50 focus-visible:ring-0', triggerClass)">
<slot name="trigger-label" :value="modelValue" :label="selectedLabel" :loading="loading">
<span class="truncate">{{ loading ? loadingText : selectedLabel }}</span>
</slot>
<ChevronDown class="h-3 w-3 shrink-0 opacity-60" />
<X v-if="clearable && !disabled && modelValue" class="h-3 w-3 shrink-0 opacity-60 hover:opacity-100" @pointerdown.stop.prevent="emit('update:modelValue', '')" />
<ChevronDown v-else class="h-3 w-3 shrink-0 opacity-60" />
</Button>
</PopoverTrigger>
<PopoverContent align="end" :class="cn('w-52 gap-1 p-1.5', contentClass)">
@@ -182,8 +188,8 @@ function handleKeydown(event: KeyboardEvent) {
@click="selectOption(option)"
>
<Check :class="cn('h-3.5 w-3.5 shrink-0', option === modelValue ? 'opacity-100' : 'opacity-0')" />
<slot name="option-label" :option="option" :label="displayName(option)">
<span class="truncate">{{ displayName(option) }}</span>
<slot name="option-label" :option="option" :label="displayName?.(option)">
<span class="truncate">{{ displayName?.(option) }}</span>
</slot>
</button>
<button