feat: add selectable database charset options

This commit is contained in:
luoianun authored and GitHub committed 2026-06-23 21:17:55 +08:00
1 parent aed2192d8c
commit a36d2f12f7
10 files changed
+317 -3

No files matched your search

@@ -122,8 +122,10 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { SearchableSelect } from "@/components/ui/searchable-select";
import LightTooltip from "@/components/ui/LightTooltip.vue";
import { flattenTree } from "@/composables/useFlatTree";
import { createDatabaseCollationOptionsForCharset, fallbackCreateDatabaseCharsetMetadata, nextCreateDatabaseCollation, normalizeCreateDatabaseCharset, parseCreateDatabaseCharsetMetadata } from "@/lib/createDatabaseCharsetOptions";
const { t } = useI18n();
const labelRef = ref<HTMLElement>();
@@ -1357,6 +1359,10 @@ const showCreateDatabaseDialog = ref(false);
const createDatabaseName = ref("");
const createDatabaseCharset = ref("utf8mb4");
const createDatabaseCollation = ref("utf8mb4_unicode_ci");
const fallbackCreateDatabaseCharset = fallbackCreateDatabaseCharsetMetadata();
const createDatabaseCharsetOptions = ref<string[]>(fallbackCreateDatabaseCharset.charsets);
const createDatabaseCollationsByCharset = ref<Record<string, string[]>>(fallbackCreateDatabaseCharset.collationsByCharset);
const createDatabaseCharsetLoading = ref(false);
const showDropDatabaseConfirm = ref(false);
const dropDatabaseLoading = ref(false);
const showDropMongoCollectionConfirm = ref(false);
@@ -1983,7 +1989,40 @@ function openCreateDatabaseDialog() {
createDatabaseName.value = "";
createDatabaseCharset.value = "utf8mb4";
createDatabaseCollation.value = "utf8mb4_unicode_ci";
createDatabaseCharsetOptions.value = fallbackCreateDatabaseCharset.charsets;
createDatabaseCollationsByCharset.value = fallbackCreateDatabaseCharset.collationsByCharset;
showCreateDatabaseDialog.value = true;
void loadCreateDatabaseCharsetMetadata();
}
function updateCreateDatabaseCharset(value: string) {
const previousCharset = createDatabaseCharset.value;
createDatabaseCharset.value = value;
createDatabaseCollation.value = nextCreateDatabaseCollation(value, previousCharset, createDatabaseCollation.value, createDatabaseCollationsByCharset.value);
}
async function loadCreateDatabaseCharsetMetadata() {
const node = props.node;
if (!node.connectionId || createDatabaseCharsetLoading.value) return;
createDatabaseCharsetLoading.value = true;
try {
await connectionStore.ensureConnected(node.connectionId);
const [charsetResult, collationResult] = await Promise.all([api.executeQuery(node.connectionId, "", "SHOW CHARACTER SET", undefined, undefined, { maxRows: 1000 }), api.executeQuery(node.connectionId, "", "SHOW COLLATION", undefined, undefined, { maxRows: 10000 })]);
if (!showCreateDatabaseDialog.value) return;
const metadata = parseCreateDatabaseCharsetMetadata(charsetResult, collationResult);
createDatabaseCharsetOptions.value = metadata.charsets;
createDatabaseCollationsByCharset.value = metadata.collationsByCharset;
if (!createDatabaseCharsetOptions.value.includes(createDatabaseCharset.value) && createDatabaseCharsetOptions.value.length) {
updateCreateDatabaseCharset(createDatabaseCharsetOptions.value[0]);
} else {
createDatabaseCollation.value = nextCreateDatabaseCollation(createDatabaseCharset.value, createDatabaseCharset.value, createDatabaseCollation.value, createDatabaseCollationsByCharset.value);
}
} catch {
createDatabaseCharsetOptions.value = fallbackCreateDatabaseCharset.charsets;
createDatabaseCollationsByCharset.value = fallbackCreateDatabaseCharset.collationsByCharset;
} finally {
createDatabaseCharsetLoading.value = false;
}
}
function ensureDuckDbFileExtension(path: string): string {
@@ -3904,11 +3943,46 @@ function treeItemMenuItems(): ContextMenuItem[] {
<div v-if="canSetCreateDatabaseCharset" class="grid gap-2">
<div class="grid gap-1.5">
<label class="text-xs font-medium text-muted-foreground">{{ t("contextMenu.createDatabaseCharset") }}</label>
<Input v-model="createDatabaseCharset" :placeholder="t('contextMenu.createDatabaseCharsetPlaceholder')" @keydown.enter.prevent="confirmCreateDatabase" />
<SearchableSelect
:model-value="createDatabaseCharset"
:options="createDatabaseCharsetOptions"
:placeholder="t('contextMenu.createDatabaseCharsetPlaceholder')"
:search-placeholder="t('contextMenu.createDatabaseCharsetSearchPlaceholder')"
:empty-text="t('contextMenu.createDatabaseCharsetEmpty')"
:loading-text="t('contextMenu.createDatabaseCharsetLoading')"
:loading="createDatabaseCharsetLoading"
:normalize-custom="normalizeCreateDatabaseCharset"
allow-custom
trigger-variant="outline"
trigger-class="h-9 w-full max-w-none justify-between border bg-background px-3 text-sm shadow-xs hover:bg-accent"
content-class="w-[var(--reka-popover-trigger-width)]"
@update:model-value="updateCreateDatabaseCharset"
>
<template #custom-option-label="{ value }">
<span class="truncate">{{ t("contextMenu.createDatabaseCharsetCustomOption", { value }) }}</span>
</template>
</SearchableSelect>
</div>
<div class="grid gap-1.5">
<label class="text-xs font-medium text-muted-foreground">{{ t("contextMenu.createDatabaseCollation") }}</label>
<Input v-model="createDatabaseCollation" :placeholder="t('contextMenu.createDatabaseCollationPlaceholder')" @keydown.enter.prevent="confirmCreateDatabase" />
<SearchableSelect
v-model="createDatabaseCollation"
:options="createDatabaseCollationOptionsForCharset(createDatabaseCharset, createDatabaseCollationsByCharset)"
:placeholder="t('contextMenu.createDatabaseCollationPlaceholder')"
:search-placeholder="t('contextMenu.createDatabaseCollationSearchPlaceholder')"
:empty-text="t('contextMenu.createDatabaseCollationEmpty')"
:loading-text="t('contextMenu.createDatabaseCollationLoading')"
:loading="createDatabaseCharsetLoading"
:normalize-custom="normalizeCreateDatabaseCharset"
allow-custom
trigger-variant="outline"
trigger-class="h-9 w-full max-w-none justify-between border bg-background px-3 text-sm shadow-xs hover:bg-accent"
content-class="w-[var(--reka-popover-trigger-width)]"
>
<template #custom-option-label="{ value }">
<span class="truncate">{{ t("contextMenu.createDatabaseCollationCustomOption", { value }) }}</span>
</template>
</SearchableSelect>
</div>
</div>
<DialogFooter>
@@ -3,6 +3,7 @@ import { computed, nextTick, ref, watch } from "vue";
import type { HTMLAttributes } from "vue";
import { Check, ChevronDown, Search } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import type { ButtonVariants } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { filterDatabaseOptions } from "@/lib/databaseOptionSearch";
@@ -18,6 +19,7 @@ const props = withDefaults(
loadingText: string;
loading?: boolean;
allowCustom?: boolean;
triggerVariant?: ButtonVariants["variant"];
triggerClass?: HTMLAttributes["class"];
contentClass?: HTMLAttributes["class"];
itemClass?: HTMLAttributes["class"];
@@ -27,6 +29,7 @@ const props = withDefaults(
{
loading: false,
allowCustom: false,
triggerVariant: "ghost",
displayName: (option: string) => option,
normalizeCustom: (value: string) => value,
},
@@ -146,7 +149,7 @@ function handleKeydown(event: KeyboardEvent) {
<template>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button type="button" variant="ghost" :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" :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>