feat(ui): streamline sidebar and settings layout

This commit is contained in:
zipg authored and GitHub committed 2026-08-26 21:31:26 +08:00
1 parent cb4cc43685
commit 589fbd9de8
21 files changed
+806 -429

No files matched your search

@@ -4,7 +4,38 @@ import type { Ref } from "vue";
import type { EditorView as EditorViewType } from "@codemirror/view";
import { useI18n } from "vue-i18n";
import { translateBackendError } from "@/i18n/backend-errors";
import { AlertTriangle, ArrowLeft, Check, CheckCircle2, CircleHelp, Cloud, Copy, Download, ExternalLink, GripVertical, Loader2, Moon, PackageSearch, Palette, Pencil, Plus, RefreshCw, RotateCcw, Search, Settings, Sun, SunMoon, Terminal, Trash2, Upload, X } from "@lucide/vue";
import {
AlertTriangle,
ArrowLeft,
Check,
CheckCircle2,
ChevronDown,
ChevronUp,
CircleHelp,
Cloud,
Copy,
Download,
ExternalLink,
Eye,
Filter,
GripVertical,
Loader2,
Moon,
PackageSearch,
Palette,
Pencil,
Plus,
RefreshCw,
RotateCcw,
Search,
Settings,
Sun,
SunMoon,
Terminal,
Trash2,
Upload,
X,
} from "@lucide/vue";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
@@ -323,7 +354,8 @@ const editShortcuts = ref(normalizeShortcutSettings(settingsStore.editorSettings
function translateWithExecuteShortcut(key: string): string {
return t(key, { shortcut: formatShortcutDisplay(editShortcuts.value.executeSql) });
}
const executeModeLabel = computed(() => translateWithExecuteShortcut("settings.executeMode"));
const executeModeLabel = computed(() => t("settings.executeMode"));
const executeModeDescription = computed(() => translateWithExecuteShortcut("settings.executeModeDescription"));
const editExecuteAllOnBlankLine = ref(settingsStore.editorSettings.executeAllOnBlankLine);
const editShowExecutionTargetPicker = ref(settingsStore.editorSettings.showExecutionTargetPicker);
const editShowStatementRunButtons = ref(settingsStore.editorSettings.showStatementRunButtons);
@@ -335,6 +367,14 @@ const editInsertSpaceAfterCompletion = ref(settingsStore.editorSettings.insertSp
const editSortCompletionColumnsAlphabetically = ref(settingsStore.editorSettings.sortCompletionColumnsAlphabetically);
const editSelectFirstCompletionOnOpen = ref(settingsStore.editorSettings.selectFirstCompletionOnOpen);
const editCompletionTriggerMode = ref<SqlCompletionTriggerMode>(settingsStore.editorSettings.completionTriggerMode);
const completionTriggerModeDescription = computed(() => {
const key = {
manual: "settings.completionTriggerModeManualDescription",
"require-prefix": "settings.completionTriggerModeRequirePrefixDescription",
positional: "settings.completionTriggerModePositionalDescription",
}[editCompletionTriggerMode.value];
return t(key, { shortcut: formatShortcutDisplay(editShortcuts.value.triggerCompletion) });
});
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
const editVimModeEnabled = ref(settingsStore.editorSettings.vimModeEnabled);
const editAutoCloseBrackets = ref(settingsStore.editorSettings.autoCloseBrackets);
@@ -370,6 +410,7 @@ const editShowIndexIndicatorsInHeader = ref(settingsStore.editorSettings.showInd
const editCompactColumnHeaderActions = ref(settingsStore.editorSettings.compactColumnHeaderActions);
const editDataGridQuickEntry = ref(settingsStore.editorSettings.dataGridQuickEntry);
const editDataGridFilterEditorView = ref<DataGridFilterEditorView>(settingsStore.editorSettings.dataGridFilterEditorView);
const dataGridFilterViewPreviewExpanded = ref(true);
const editDataGridTextFilterPanelHeight = ref(settingsStore.editorSettings.dataGridTextFilterPanelHeight);
const editDataGridAutoTransposeSingleRow = ref(settingsStore.editorSettings.dataGridAutoTransposeSingleRow);
const editDataGridCellDetailButtonVisible = ref(settingsStore.editorSettings.dataGridCellDetailButtonVisible);
@@ -619,14 +660,6 @@ const snippetDialogOpen = ref(false);
const snippetEditingId = ref<string | null>(null);
const snippetForm = ref({ label: "", prefix: "", body: "" });
const snippetFormPrefixError = ref("");
const iconThemeDescTruncated = {
default: ref<boolean>(false),
black: ref<boolean>(false),
};
const iconThemeDescRef = {
default: ref<HTMLElement | null>(null),
black: ref<HTMLElement | null>(null),
};
const iconThemeBlackDescriptionText = computed(() => (isMacOS() ? t("settings.iconThemeBlackDescriptionMac") : t("settings.iconThemeBlackDescription")));
const layoutDescTruncated = {
separated: ref<boolean>(false),
@@ -640,11 +673,6 @@ let layoutDescObservers: Record<InterfaceLayout, ResizeObserver | undefined> = {
separated: undefined,
classic: undefined,
};
let iconThemeDescObservers: Record<DesktopIconTheme, ResizeObserver | undefined> = {
default: undefined,
black: undefined,
};
function observeElementTruncation(el: Ref<HTMLElement | null>, truncated: Ref<boolean>) {
if (!el.value) return;
@@ -659,25 +687,17 @@ function observeElementTruncation(el: Ref<HTMLElement | null>, truncated: Ref<bo
function initTruncationObservers() {
layoutDescObservers.separated = observeElementTruncation(layoutDescRefs.separated, layoutDescTruncated.separated);
layoutDescObservers.classic = observeElementTruncation(layoutDescRefs.classic, layoutDescTruncated.classic);
iconThemeDescObservers.default = observeElementTruncation(iconThemeDescRef.default, iconThemeDescTruncated.default);
iconThemeDescObservers.black = observeElementTruncation(iconThemeDescRef.black, iconThemeDescTruncated.black);
}
function cleanupTruncationObservers() {
layoutDescObservers.separated?.disconnect();
layoutDescObservers.classic?.disconnect();
iconThemeDescObservers.default?.disconnect();
iconThemeDescObservers.black?.disconnect();
}
function setLayoutDescRef(layout: InterfaceLayout, el: unknown) {
layoutDescRefs[layout].value = el instanceof HTMLElement ? el : null;
}
function setIconThemeDescRef(theme: DesktopIconTheme, el: unknown) {
iconThemeDescRef[theme].value = el instanceof HTMLElement ? el : null;
}
function checkLayoutDescTruncation() {
checkTruncationForRefs([
{ el: layoutDescRefs.separated, truncated: layoutDescTruncated.separated },
@@ -685,13 +705,6 @@ function checkLayoutDescTruncation() {
]);
}
function checkIconThemeDescTruncation() {
checkTruncationForRefs([
{ el: iconThemeDescRef.default, truncated: iconThemeDescTruncated.default },
{ el: iconThemeDescRef.black, truncated: iconThemeDescTruncated.black },
]);
}
function checkTruncationForRefs(items: Array<{ el: Ref<HTMLElement | null>; truncated: Ref<boolean> }>) {
nextTick(() => {
for (const item of items) {
@@ -2591,7 +2604,6 @@ watch(activeSettingsTab, async (tab) => {
if (tab === "about" && !appSupportInfo.value) void refreshAppSupportInfo();
if (tab === "appearance") {
checkLayoutDescTruncation();
checkIconThemeDescTruncation();
}
const result = pendingSettingsSearchResult;
if (result) {
@@ -2620,7 +2632,6 @@ watch(
onMounted(() => {
void refreshWebDavPasswordStatus();
checkLayoutDescTruncation();
checkIconThemeDescTruncation();
initTruncationObservers();
});
@@ -3940,6 +3951,48 @@ onUnmounted(() => {
</div>
</div>
<div class="grid grid-cols-2 gap-2 lg:grid-cols-4" data-editor-preview-controls>
<div class="flex min-w-0 items-center justify-between gap-2 rounded-md border bg-muted/20 px-2 py-1.5">
<div class="flex min-w-0 items-center gap-1">
<Label for="editor-show-statement-run-buttons" class="truncate text-xs">{{ t("settings.showStatementRunButtons") }}</Label>
<HelpTooltip :label="t('settings.showStatementRunButtons')" trigger-class="[&_svg]:h-3 [&_svg]:w-3" content-class="max-w-64">
{{ t("settings.showStatementRunButtonsDescription") }}
</HelpTooltip>
</div>
<Switch id="editor-show-statement-run-buttons" v-model="editShowStatementRunButtons" size="sm" />
</div>
<div class="flex min-w-0 items-center justify-between gap-2 rounded-md border bg-muted/20 px-2 py-1.5">
<div class="flex min-w-0 items-center gap-1">
<Label for="editor-show-line-numbers" class="truncate text-xs">{{ t("settings.showLineNumbers") }}</Label>
<HelpTooltip :label="t('settings.showLineNumbers')" trigger-class="[&_svg]:h-3 [&_svg]:w-3" content-class="max-w-64">
{{ t("settings.showLineNumbersDescription") }}
</HelpTooltip>
</div>
<Switch id="editor-show-line-numbers" v-model="editShowLineNumbers" size="sm" />
</div>
<div class="flex min-w-0 items-center justify-between gap-2 rounded-md border bg-muted/20 px-2 py-1.5">
<div class="flex min-w-0 items-center gap-1">
<Label for="editor-show-current-statement-frame" class="truncate text-xs">{{ t("settings.showCurrentStatementFrame") }}</Label>
<HelpTooltip :label="t('settings.showCurrentStatementFrame')" trigger-class="[&_svg]:h-3 [&_svg]:w-3" content-class="max-w-64">
{{ t("settings.showCurrentStatementFrameDescription") }}
</HelpTooltip>
</div>
<Switch id="editor-show-current-statement-frame" v-model="editShowCurrentStatementFrame" size="sm" />
</div>
<div class="flex min-w-0 items-center justify-between gap-2 rounded-md border bg-muted/20 px-2 py-1.5">
<div class="flex min-w-0 items-center gap-1">
<Label for="editor-sql-semantic-diagnostics" class="truncate text-xs">{{ t("settings.sqlSemanticDiagnosticsEnabled") }}</Label>
<HelpTooltip :label="t('settings.sqlSemanticDiagnosticsEnabled')" trigger-class="[&_svg]:h-3 [&_svg]:w-3" content-class="max-w-64">
{{ t("settings.sqlSemanticDiagnosticsEnabledDescription") }}
</HelpTooltip>
</div>
<Switch id="editor-sql-semantic-diagnostics" :model-value="editSqlSemanticDiagnosticsEnabled" size="sm" @update:model-value="onSqlSemanticDiagnosticsEnabledChange" />
</div>
</div>
<!-- Live Preview -->
<div class="space-y-2">
<Label>{{ t("settings.preview") }}</Label>
@@ -3953,11 +4006,16 @@ onUnmounted(() => {
<Separator />
<div class="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<div class="space-y-2">
<Label>{{ executeModeLabel }}</Label>
<div class="grid gap-4 md:grid-cols-2" data-editor-execution-settings>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2 md:col-span-2" data-editor-execute-mode>
<div class="min-w-0 space-y-1">
<Label>{{ executeModeLabel }}</Label>
<p class="text-xs text-muted-foreground">
{{ executeModeDescription }}
</p>
</div>
<Select :model-value="editExecuteMode" @update:model-value="onExecuteModeChange">
<SelectTrigger>
<SelectTrigger class="h-8 w-48 shrink-0">
<SelectValue :placeholder="executeModeLabel" />
</SelectTrigger>
<SelectContent>
@@ -3989,34 +4047,101 @@ onUnmounted(() => {
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-show-statement-run-buttons">{{ t("settings.showStatementRunButtons") }}</Label>
<Label for="editor-confirm-dangerous-sql">{{ t("settings.confirmDangerousSqlExecution") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.showStatementRunButtonsDescription") }}
{{ t("settings.confirmDangerousSqlExecutionDescription") }}
</p>
</div>
<Switch id="editor-show-statement-run-buttons" v-model="editShowStatementRunButtons" class="mt-0.5" />
<Switch id="editor-confirm-dangerous-sql" v-model="editConfirmDangerousSqlExecution" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-show-line-numbers">{{ t("settings.showLineNumbers") }}</Label>
<Label for="editor-continue-on-error">{{ t("settings.continueOnErrorOnBatch") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.showLineNumbersDescription") }}
{{ t("settings.continueOnErrorOnBatchDescription") }}
</p>
</div>
<Switch id="editor-show-line-numbers" v-model="editShowLineNumbers" class="mt-0.5" />
<Switch id="editor-continue-on-error" v-model="editContinueOnErrorOnBatch" class="mt-0.5" />
</div>
</div>
<Separator />
<div class="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]" data-editor-sql-completion-settings>
<div class="flex items-start justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2" data-editor-completion-trigger-mode>
<div class="min-w-0 space-y-1">
<Label>{{ t("settings.completionTriggerMode") }}</Label>
<p class="text-xs leading-tight text-muted-foreground">
{{ completionTriggerModeDescription }}
</p>
</div>
<Select :model-value="editCompletionTriggerMode" @update:model-value="onCompletionTriggerModeChange">
<SelectTrigger class="h-8 w-44 shrink-0">
<SelectValue :placeholder="t('settings.completionTriggerMode')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="manual">{{ t("settings.completionTriggerModeManual") }}</SelectItem>
<SelectItem value="require-prefix">{{ t("settings.completionTriggerModeRequirePrefix") }}</SelectItem>
<SelectItem value="positional">{{ t("settings.completionTriggerModePositional") }}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-show-current-statement-frame">{{ t("settings.showCurrentStatementFrame") }}</Label>
<Label for="editor-select-first-completion-on-open">{{ t("settings.selectFirstCompletionOnOpen") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.showCurrentStatementFrameDescription") }}
{{ t("settings.selectFirstCompletionOnOpenDescription") }}
</p>
</div>
<Switch id="editor-show-current-statement-frame" v-model="editShowCurrentStatementFrame" class="mt-0.5" />
<Switch id="editor-select-first-completion-on-open" v-model="editSelectFirstCompletionOnOpen" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-auto-close-brackets">{{ t("settings.autoCloseBrackets") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.autoCloseBracketsDescription") }}
</p>
</div>
<Switch id="editor-auto-close-brackets" v-model="editAutoCloseBrackets" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-insert-space-after-completion">{{ t("settings.insertSpaceAfterCompletion") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.insertSpaceAfterCompletionDescription") }}
</p>
</div>
<Switch id="editor-insert-space-after-completion" v-model="editInsertSpaceAfterCompletion" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-sort-completion-columns-alphabetically">{{ t("settings.sortCompletionColumnsAlphabetically") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.sortCompletionColumnsAlphabeticallyDescription") }}
</p>
</div>
<Switch id="editor-sort-completion-columns-alphabetically" v-model="editSortCompletionColumnsAlphabetically" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-auto-alias-tables">{{ t("settings.autoAliasTables") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.autoAliasTablesDescription") }}
</p>
</div>
<Switch id="editor-auto-alias-tables" v-model="editAutoAliasTables" class="mt-0.5" />
</div>
</div>
<Separator />
<div class="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]" data-editor-other-settings>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-show-insert-value-hints">{{ t("settings.showInsertValueHints") }}</Label>
@@ -4066,83 +4191,6 @@ onUnmounted(() => {
<Separator />
<div class="space-y-3">
<div class="text-sm font-medium text-muted-foreground">
{{ t("settings.sqlCompletionSection") }}
</div>
<div class="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-auto-close-brackets">{{ t("settings.autoCloseBrackets") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.autoCloseBracketsDescription") }}
</p>
</div>
<Switch id="editor-auto-close-brackets" v-model="editAutoCloseBrackets" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-insert-space-after-completion">{{ t("settings.insertSpaceAfterCompletion") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.insertSpaceAfterCompletionDescription") }}
</p>
</div>
<Switch id="editor-insert-space-after-completion" v-model="editInsertSpaceAfterCompletion" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-sort-completion-columns-alphabetically">{{ t("settings.sortCompletionColumnsAlphabetically") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.sortCompletionColumnsAlphabeticallyDescription") }}
</p>
</div>
<Switch id="editor-sort-completion-columns-alphabetically" v-model="editSortCompletionColumnsAlphabetically" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-select-first-completion-on-open">{{ t("settings.selectFirstCompletionOnOpen") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.selectFirstCompletionOnOpenDescription") }}
</p>
</div>
<Switch id="editor-select-first-completion-on-open" v-model="editSelectFirstCompletionOnOpen" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-auto-alias-tables">{{ t("settings.autoAliasTables") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.autoAliasTablesDescription") }}
</p>
</div>
<Switch id="editor-auto-alias-tables" v-model="editAutoAliasTables" class="mt-0.5" />
</div>
<div class="space-y-2">
<Label>{{ t("settings.completionTriggerMode") }}</Label>
<Select :model-value="editCompletionTriggerMode" @update:model-value="onCompletionTriggerModeChange">
<SelectTrigger>
<SelectValue :placeholder="t('settings.completionTriggerMode')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="manual">{{ t("settings.completionTriggerModeManual") }}</SelectItem>
<SelectItem value="require-prefix">{{ t("settings.completionTriggerModeRequirePrefix") }}</SelectItem>
<SelectItem value="positional">{{ t("settings.completionTriggerModePositional") }}</SelectItem>
</SelectContent>
</Select>
<p class="text-xs text-muted-foreground">
{{ t("settings.completionTriggerModeDescription") }}
</p>
</div>
</div>
</div>
<Separator />
<div class="grid gap-3 md:grid-cols-2">
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2 md:col-span-2">
<div class="min-w-0 space-y-1">
@@ -4162,85 +4210,41 @@ onUnmounted(() => {
</Select>
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-sql-semantic-diagnostics">{{ t("settings.sqlSemanticDiagnosticsEnabled") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.sqlSemanticDiagnosticsEnabledDescription") }}
</p>
</div>
<Switch id="editor-sql-semantic-diagnostics" :model-value="editSqlSemanticDiagnosticsEnabled" class="mt-0.5" @update:model-value="onSqlSemanticDiagnosticsEnabledChange" />
</div>
<div class="rounded-md border bg-muted/20 p-3 md:col-span-2" data-editor-unsaved-sql-settings>
<div class="grid gap-4 md:grid-cols-2">
<div class="flex min-w-0 items-start justify-between gap-4">
<div class="min-w-0 space-y-1">
<Label for="editor-confirm-unsaved-sql-close">{{ t("settings.confirmUnsavedSqlClose") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.confirmUnsavedSqlCloseDescription") }}
</p>
</div>
<Switch id="editor-confirm-unsaved-sql-close" v-model="editConfirmUnsavedSqlClose" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-confirm-dangerous-sql">{{ t("settings.confirmDangerousSqlExecution") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.confirmDangerousSqlExecutionDescription") }}
</p>
<div v-if="editConfirmUnsavedSqlClose" class="flex min-w-0 items-start justify-between gap-4 border-t pt-4 md:border-l md:border-t-0 md:pl-4 md:pt-0">
<div class="min-w-0 space-y-1">
<div class="flex items-center gap-2">
<Label for="app-close-unsaved-tabs-mode">{{ t("settings.appCloseUnsavedTabsMode") }}</Label>
<HelpTooltip :label="t('settings.appCloseUnsavedTabsMode')">
{{ t("settings.appCloseUnsavedTabsModeDescription") }}
</HelpTooltip>
</div>
<p class="text-xs text-muted-foreground">
{{ t("settings.appCloseUnsavedTabsModeHint") }}
</p>
</div>
<Select v-model="editAppCloseUnsavedTabsMode">
<SelectTrigger id="app-close-unsaved-tabs-mode" class="h-8 w-44 shrink-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="prompt">{{ t("settings.appCloseUnsavedTabsModePrompt") }}</SelectItem>
<SelectItem value="keep-drafts">{{ t("settings.appCloseUnsavedTabsModeKeepDrafts") }}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Switch id="editor-confirm-dangerous-sql" v-model="editConfirmDangerousSqlExecution" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-continue-on-error">{{ t("settings.continueOnErrorOnBatch") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.continueOnErrorOnBatchDescription") }}
</p>
</div>
<Switch id="editor-continue-on-error" v-model="editContinueOnErrorOnBatch" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-confirm-unsaved-sql-close">{{ t("settings.confirmUnsavedSqlClose") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.confirmUnsavedSqlCloseDescription") }}
</p>
</div>
<Switch id="editor-confirm-unsaved-sql-close" v-model="editConfirmUnsavedSqlClose" class="mt-0.5" />
</div>
<div v-if="editConfirmUnsavedSqlClose" class="space-y-2 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center gap-2">
<Label for="app-close-unsaved-tabs-mode">{{ t("settings.appCloseUnsavedTabsMode") }}</Label>
<HelpTooltip :label="t('settings.appCloseUnsavedTabsMode')">
{{ t("settings.appCloseUnsavedTabsModeDescription") }}
</HelpTooltip>
</div>
<Select v-model="editAppCloseUnsavedTabsMode">
<SelectTrigger id="app-close-unsaved-tabs-mode" class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="prompt">{{ t("settings.appCloseUnsavedTabsModePrompt") }}</SelectItem>
<SelectItem value="keep-drafts">{{ t("settings.appCloseUnsavedTabsModeKeepDrafts") }}</SelectItem>
</SelectContent>
</Select>
<p class="text-xs text-muted-foreground">
{{ t("settings.appCloseUnsavedTabsModeHint") }}
</p>
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-click-table-navigation-ddl">{{ t("settings.clickTableNavigationTarget") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.clickTableNavigationTargetDescription") }}
</p>
</div>
<Switch id="editor-click-table-navigation-ddl" :model-value="editClickTableNavigationTarget === 'ddl'" @update:model-value="editClickTableNavigationTarget = $event ? 'ddl' : 'data'" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-prefill-new-query">{{ t("settings.prefillNewQueryWithSelect") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.prefillNewQueryWithSelectDescription") }}
</p>
</div>
<Switch id="editor-prefill-new-query" v-model="editPrefillNewQueryWithSelect" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
@@ -4614,6 +4618,38 @@ onUnmounted(() => {
</Button>
</div>
</div>
<div class="settings-appearance-group min-w-0">
<Label>{{ t("settings.iconTheme") }}</Label>
<div class="settings-appearance-button-row flex flex-wrap gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<Button type="button" variant="outline" size="sm" class="settings-choice-button h-8 gap-1.5 px-3" :class="editIconTheme === 'default' ? 'dbx-choice-selected' : 'text-foreground'" @click="setIconTheme('default')">
<img :src="webPath('/icon-preview-default.png')" alt="DBX" class="h-7 w-7 shrink-0" />
{{ t("settings.iconThemeDefault") }}
</Button>
</TooltipTrigger>
<TooltipContent class="max-w-[320px] text-xs leading-relaxed">
{{ t("settings.iconThemeDefaultDescription") }}
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<Button type="button" variant="outline" size="sm" class="settings-choice-button h-8 gap-1.5 px-3" :class="editIconTheme === 'black' ? 'dbx-choice-selected' : 'text-foreground'" @click="setIconTheme('black')">
<img :src="webPath('/icon-preview-black.png')" alt="DBX" class="h-7 w-7 shrink-0" />
{{ t("settings.iconThemeBlack") }}
</Button>
</TooltipTrigger>
<TooltipContent class="max-w-[320px] text-xs leading-relaxed">
{{ iconThemeBlackDescriptionText }}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</div>
<Separator />
@@ -4690,57 +4726,6 @@ onUnmounted(() => {
</div>
</div>
<!-- <div v-if="!isWeb" class="space-y-2"> -->
<div class="settings-appearance-group">
<Label>{{ t("settings.iconTheme") }}</Label>
<div class="settings-appearance-choice-grid">
<Button type="button" variant="outline" class="settings-choice-card h-auto justify-start border p-3" :class="editIconTheme === 'default' ? 'dbx-choice-selected' : ''" @click="setIconTheme('default')">
<div class="flex items-center gap-3 text-left w-full min-w-0">
<img :src="webPath('/icon-preview-default.png')" alt="DBX" class="h-12 w-12 shrink-0" />
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<div class="w-full min-w-0 text-left">
<div class="text-sm font-medium">
{{ t("settings.iconThemeDefault") }}
</div>
<div :ref="(el) => setIconThemeDescRef('default', el)" class="text-xs text-muted-foreground truncate">
{{ t("settings.iconThemeDefaultDescription") }}
</div>
</div>
</TooltipTrigger>
<TooltipContent v-if="iconThemeDescTruncated.default.value" class="max-w-[320px] text-xs leading-relaxed">
{{ t("settings.iconThemeDefaultDescription") }}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</Button>
<Button type="button" variant="outline" class="settings-choice-card h-auto justify-start border p-3" :class="editIconTheme === 'black' ? 'dbx-choice-selected' : ''" @click="setIconTheme('black')">
<div class="flex items-center gap-3 text-left w-full min-w-0">
<img :src="webPath('/icon-preview-black.png')" alt="DBX" class="h-12 w-12 shrink-0" />
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<div class="w-full min-w-0 text-left">
<div class="text-sm font-medium">
{{ t("settings.iconThemeBlack") }}
</div>
<div :ref="(el) => setIconThemeDescRef('black', el)" class="text-xs text-muted-foreground truncate">
{{ iconThemeBlackDescriptionText }}
</div>
</div>
</TooltipTrigger>
<TooltipContent v-if="iconThemeDescTruncated.black.value" class="max-w-[320px] text-xs leading-relaxed">
{{ iconThemeBlackDescriptionText }}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</Button>
</div>
</div>
<div v-if="!isWeb" class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="show-tray-icon">{{ t("settings.showTrayIcon") }}</Label>
@@ -4986,6 +4971,36 @@ onUnmounted(() => {
</Button>
</div>
</div>
<div class="grid gap-3 md:grid-cols-2">
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-click-table-navigation-ddl">{{ t("settings.clickTableNavigationTarget") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.clickTableNavigationTargetDescription") }}
</p>
</div>
<Switch id="editor-click-table-navigation-ddl" :model-value="editClickTableNavigationTarget === 'ddl'" @update:model-value="editClickTableNavigationTarget = $event ? 'ddl' : 'data'" class="mt-0.5" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="editor-prefill-new-query">{{ t("settings.prefillNewQueryWithSelect") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.prefillNewQueryWithSelectDescription") }}
</p>
</div>
<Switch id="editor-prefill-new-query" v-model="editPrefillNewQueryWithSelect" class="mt-0.5" />
</div>
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center gap-2">
<Label for="sidebar-open-database-on-single-click">{{ t("settings.sidebarOpenDatabaseOnSingleClick") }}</Label>
<HelpTooltip :label="t('settings.sidebarOpenDatabaseOnSingleClick')">
{{ t("settings.sidebarOpenDatabaseOnSingleClickDescription") }}
</HelpTooltip>
</div>
<Switch id="sidebar-open-database-on-single-click" v-model="editSidebarOpenDatabaseOnSingleClick" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center gap-2">
<Label for="sidebar-table-search-enabled">{{ t("settings.sidebarTableSearchEnabled") }}</Label>
@@ -5004,15 +5019,6 @@ onUnmounted(() => {
</div>
<Switch id="auto-select-active-sidebar-node" v-model="editAutoSelectActiveSidebarNode" />
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center gap-2">
<Label for="sidebar-open-database-on-single-click">{{ t("settings.sidebarOpenDatabaseOnSingleClick") }}</Label>
<HelpTooltip :label="t('settings.sidebarOpenDatabaseOnSingleClick')">
{{ t("settings.sidebarOpenDatabaseOnSingleClickDescription") }}
</HelpTooltip>
</div>
<Switch id="sidebar-open-database-on-single-click" v-model="editSidebarOpenDatabaseOnSingleClick" />
</div>
<div class="space-y-2 rounded-md border bg-muted/20 px-3 py-2">
<div class="flex items-center gap-2">
<Label for="open-tabs-restore-mode">{{ t("settings.openTabsRestoreMode") }}</Label>
@@ -5175,6 +5181,136 @@ onUnmounted(() => {
<!-- Data Tab -->
<section v-else-if="activeSettingsTab === 'data'" data-settings-search-id="data" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('data')]">
<div data-settings-search-id="data-grid-filter-view" :class="['overflow-hidden rounded-md border bg-muted/20', settingsSearchTargetClass('data-grid-filter-view')]">
<div class="space-y-3 p-3">
<div class="flex items-start justify-between gap-4">
<div class="min-w-0 space-y-1">
<Label>{{ t("settings.dataGridFilterView") }}</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.dataGridFilterViewDescription") }}
</p>
</div>
<Button
type="button"
variant="ghost"
size="sm"
class="h-7 shrink-0 gap-1.5 px-2 text-xs text-muted-foreground"
:aria-expanded="dataGridFilterViewPreviewExpanded"
:aria-label="t(dataGridFilterViewPreviewExpanded ? 'settings.dataGridFilterViewPreviewCollapse' : 'settings.dataGridFilterViewPreviewExpand')"
data-data-grid-filter-view-preview-toggle
@click="dataGridFilterViewPreviewExpanded = !dataGridFilterViewPreviewExpanded"
>
<Eye class="h-3.5 w-3.5" />
<span>{{ t("settings.dataGridFilterViewPreview") }}</span>
<ChevronUp v-if="dataGridFilterViewPreviewExpanded" class="h-3.5 w-3.5" />
<ChevronDown v-else class="h-3.5 w-3.5" />
</Button>
</div>
<div class="grid grid-cols-3 gap-2" data-data-grid-filter-view-options>
<Button
type="button"
variant="outline"
class="settings-choice-card h-10 min-w-0 justify-start overflow-hidden whitespace-normal border px-3"
:class="editDataGridFilterEditorView === 'quick' ? 'dbx-choice-selected' : ''"
:aria-pressed="editDataGridFilterEditorView === 'quick'"
@click="editDataGridFilterEditorView = 'quick'"
>
<span class="truncate">{{ t("grid.filterQuickView") }}</span>
</Button>
<Button
type="button"
variant="outline"
class="settings-choice-card h-10 min-w-0 justify-start overflow-hidden whitespace-normal border px-3"
:class="editDataGridFilterEditorView === 'conditions' ? 'dbx-choice-selected' : ''"
:aria-pressed="editDataGridFilterEditorView === 'conditions'"
@click="editDataGridFilterEditorView = 'conditions'"
>
<span class="truncate">{{ t("grid.filterConditionView") }}</span>
</Button>
<Button
type="button"
variant="outline"
class="settings-choice-card h-10 min-w-0 justify-start overflow-hidden whitespace-normal border px-3"
:class="editDataGridFilterEditorView === 'text' ? 'dbx-choice-selected' : ''"
:aria-pressed="editDataGridFilterEditorView === 'text'"
@click="editDataGridFilterEditorView = 'text'"
>
<span class="truncate">{{ t("grid.filterTextView") }}</span>
</Button>
</div>
</div>
<div v-if="dataGridFilterViewPreviewExpanded" class="pointer-events-none select-none overflow-hidden border-t bg-background" data-data-grid-filter-view-preview>
<div class="flex h-8 min-w-0 items-center border-b bg-muted/20 text-[11px]">
<div class="flex h-full shrink-0 items-center border-r px-2">
<span class="flex h-5 w-5 items-center justify-center rounded border" :class="editDataGridFilterEditorView === 'quick' ? 'border-primary/40 bg-primary/10 text-primary' : 'border-border/70 text-muted-foreground'">
<Filter class="h-3 w-3" />
</span>
</div>
<div class="flex min-w-0 flex-1 items-center gap-1.5 border-r px-2">
<span class="shrink-0 text-muted-foreground">WHERE</span>
<code class="truncate text-foreground">status = 'ACTIVE'</code>
</div>
<div class="flex min-w-0 flex-1 items-center gap-1.5 px-2">
<span class="shrink-0 text-muted-foreground">ORDER BY</span>
<code class="truncate text-foreground">id</code>
</div>
</div>
<div v-if="editDataGridFilterEditorView === 'quick'" class="h-[104px] bg-muted/5 p-2" data-data-grid-filter-preview-quick>
<div class="w-[360px] max-w-full space-y-2 rounded-md border bg-background p-2 shadow-sm">
<div class="flex items-center justify-between text-xs font-medium">
<span>{{ t("grid.filter") }}</span>
<Trash2 class="h-3.5 w-3.5 text-muted-foreground" />
</div>
<div class="grid grid-cols-[minmax(0,1fr)_72px_minmax(0,1fr)] gap-1.5 text-[11px]">
<span class="truncate rounded border px-2 py-1">status</span>
<span class="truncate rounded border px-2 py-1">=</span>
<span class="truncate rounded border px-2 py-1">ACTIVE</span>
</div>
</div>
</div>
<div v-else-if="editDataGridFilterEditorView === 'conditions'" class="flex h-[104px] flex-col bg-muted/5" data-data-grid-filter-preview-conditions>
<div class="flex-1 p-2">
<div class="grid grid-cols-[18px_minmax(0,1fr)_92px_minmax(0,1.2fr)_24px] items-center gap-1.5 text-[11px]">
<GripVertical class="h-3.5 w-3.5 text-muted-foreground" />
<span class="truncate rounded border bg-background px-2 py-1">status</span>
<span class="truncate rounded border bg-background px-2 py-1">=</span>
<span class="truncate rounded border bg-background px-2 py-1">ACTIVE</span>
<X class="h-3.5 w-3.5 justify-self-center text-muted-foreground" />
</div>
</div>
<div class="flex h-8 min-w-0 items-center gap-2 border-t bg-background/35 px-2 text-[11px]">
<span class="shrink-0 text-muted-foreground">{{ t("grid.filterSqlPreview") }}</span>
<code class="min-w-0 flex-1 truncate rounded bg-muted/35 px-1.5 py-0.5">WHERE status = 'ACTIVE'</code>
<span class="shrink-0 rounded bg-primary px-2 py-1 text-primary-foreground">{{ t("grid.applyFilter") }}</span>
</div>
</div>
<div v-else class="flex h-[104px] flex-col bg-muted/5" data-data-grid-filter-preview-text>
<div class="flex-1 px-2 py-1.5 text-[11px]">
<div class="grid min-h-7 grid-cols-[18px_18px_minmax(0,1fr)_72px_minmax(0,1.2fr)_36px] items-center gap-1 border-b border-border/45">
<GripVertical class="h-3.5 w-3.5 text-muted-foreground" />
<span class="flex h-3.5 w-3.5 items-center justify-center border border-primary bg-primary/10 text-primary"><Check class="h-3 w-3" /></span>
<span class="truncate px-1">status</span>
<span class="truncate px-1">=</span>
<span class="truncate px-1">ACTIVE</span>
<Plus class="h-3.5 w-3.5 justify-self-center text-muted-foreground" />
</div>
</div>
<div class="flex h-8 min-w-0 items-center gap-2 border-t bg-background/45 px-2 text-[11px]">
<span class="shrink-0 text-muted-foreground">{{ t("grid.filterSqlPreview") }}</span>
<code class="min-w-0 flex-1 truncate">WHERE status = 'ACTIVE'</code>
<span class="shrink-0 rounded bg-primary px-2 py-1 text-primary-foreground">{{ t("grid.applyFilter") }}</span>
</div>
</div>
</div>
</div>
<Separator />
<div class="space-y-3">
<div class="text-sm font-medium text-muted-foreground">
{{ t("settings.dataGridDisplay") }}
@@ -5194,26 +5330,6 @@ onUnmounted(() => {
</Button>
</div>
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="data-grid-filter-view">
{{ t("settings.dataGridFilterView") }}
</Label>
<p class="text-xs text-muted-foreground">
{{ t("settings.dataGridFilterViewDescription") }}
</p>
</div>
<Select v-model="editDataGridFilterEditorView">
<SelectTrigger id="data-grid-filter-view" class="h-8 w-44 shrink-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="quick">{{ t("grid.filterQuickView") }}</SelectItem>
<SelectItem value="conditions">{{ t("grid.filterConditionView") }}</SelectItem>
<SelectItem value="text">{{ t("grid.filterTextView") }}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
<div class="space-y-1">
<Label for="table-open-page-size">
@@ -7435,7 +7551,7 @@ onUnmounted(() => {
.settings-appearance-theme-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.75rem;
}
@@ -113,9 +113,17 @@ describe("EditorSettingsDialog choice card containment", () => {
});
it("preserves intentional single-line truncation for cards with tooltips", () => {
for (const key of ["appLayoutSeparatedDescription", "appLayoutClassicDescription", "iconThemeDefaultDescription"] as const) {
for (const key of ["appLayoutSeparatedDescription", "appLayoutClassicDescription"] as const) {
expectClassTokens(classNameFromTag(elementTagForKey(key)), ["truncate"]);
}
expect(dialogSource).toContain('class="text-xs text-muted-foreground truncate">\n {{ iconThemeBlackDescriptionText }}');
});
it("keeps icon theme choices compact beside the corner style controls", () => {
expect(dialogSource).toContain("grid-template-columns: repeat(3, minmax(0, 1fr));");
for (const key of ["iconThemeDefault", "iconThemeBlack"] as const) {
const block = buttonBlockForKey(key);
expectClassTokens(classNameFromTag(openingTag(block, "Button")), ["settings-choice-button", "h-8"]);
expect(block).toContain('class="h-7 w-7 shrink-0"');
}
});
});
@@ -5,12 +5,16 @@ const dialogSource = readFileSync(new URL("../EditorSettingsDialog.vue", import.
const editorSectionStart = dialogSource.indexOf(`activeSettingsTab === 'editor'`);
const formatterSectionStart = dialogSource.indexOf(`activeSettingsTab === 'formatter'`, editorSectionStart);
const editorSection = dialogSource.slice(editorSectionStart, formatterSectionStart);
const dataSectionStart = dialogSource.indexOf(`activeSettingsTab === 'data'`);
const shortcutsSectionStart = dialogSource.indexOf(`activeSettingsTab === 'shortcuts'`, dataSectionStart);
const dataSection = dialogSource.slice(dataSectionStart, shortcutsSectionStart);
describe("EditorSettingsDialog live preview placement", () => {
it("renders one preview after the appearance controls and before behavior settings", () => {
it("renders preview-linked controls after appearance settings and before the live preview", () => {
const fontFamily = editorSection.indexOf('t("settings.fontFamily")');
const theme = editorSection.indexOf('t("settings.theme")');
const fontSize = editorSection.indexOf('t("settings.fontSize")');
const previewControls = editorSection.indexOf("data-editor-preview-controls");
const preview = editorSection.indexOf('ref="previewRef"');
const executeMode = editorSection.indexOf("executeModeLabel");
@@ -19,9 +23,16 @@ describe("EditorSettingsDialog live preview placement", () => {
expect(fontFamily).toBeGreaterThanOrEqual(0);
expect(theme).toBeGreaterThan(fontFamily);
expect(fontSize).toBeGreaterThan(theme);
expect(preview).toBeGreaterThan(fontSize);
expect(previewControls).toBeGreaterThan(fontSize);
expect(preview).toBeGreaterThan(previewControls);
expect(preview).toBeLessThan(executeMode);
expect(editorSection.match(/ref="previewRef"/g)).toHaveLength(1);
for (const id of ["editor-show-statement-run-buttons", "editor-show-line-numbers", "editor-show-current-statement-frame", "editor-sql-semantic-diagnostics"]) {
expect(editorSection.indexOf(`id="${id}"`)).toBeGreaterThan(previewControls);
expect(editorSection.indexOf(`id="${id}"`)).toBeLessThan(preview);
expect(editorSection.match(new RegExp(`id="${id}"`, "g"))).toHaveLength(1);
}
});
it("keeps the preview lifecycle tied to the same template ref", () => {
@@ -29,6 +40,129 @@ describe("EditorSettingsDialog live preview placement", () => {
expect(dialogSource).toContain("cleanupPreviewEditor();");
});
it("places execution behavior before SQL completion with high-priority controls first", () => {
const preview = editorSection.indexOf('ref="previewRef"');
const completionSection = editorSection.indexOf("data-editor-sql-completion-settings");
const completionTriggerMode = editorSection.indexOf('t("settings.completionTriggerMode")');
const selectFirstCompletion = editorSection.indexOf('t("settings.selectFirstCompletionOnOpen")');
const executionSection = editorSection.indexOf("data-editor-execution-settings");
const executeMode = editorSection.indexOf("executeModeLabel", executionSection);
expect(executionSection).toBeGreaterThan(preview);
expect(executeMode).toBeGreaterThan(executionSection);
expect(completionSection).toBeGreaterThan(executeMode);
expect(completionTriggerMode).toBeGreaterThan(completionSection);
expect(selectFirstCompletion).toBeGreaterThan(completionTriggerMode);
expect(editorSection).not.toContain('t("settings.sqlCompletionSection")');
expect(editorSection).toContain('class="flex items-start justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2" data-editor-completion-trigger-mode');
expect(editorSection).toContain('<SelectTrigger class="h-8 w-44 shrink-0">');
expect(editorSection).toContain('class="grid gap-4 md:grid-cols-2" data-editor-execution-settings');
expect(editorSection).toContain('class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2 md:col-span-2" data-editor-execute-mode');
expect(editorSection.match(/:model-value="editCompletionTriggerMode"/g)).toHaveLength(1);
expect(editorSection.match(/id="editor-select-first-completion-on-open"/g)).toHaveLength(1);
});
it("groups related execution and unsaved SQL controls without removing saved-target behavior", () => {
const executionGroup = editorSection.indexOf("data-editor-execution-settings");
const executionGroupEnd = editorSection.indexOf("data-editor-sql-completion-settings", executionGroup);
const unsavedGroup = editorSection.indexOf("data-editor-unsaved-sql-settings");
const unsavedGroupEnd = editorSection.indexOf("generate-sql-include-database-name", unsavedGroup);
for (const id of ["editor-execute-all-on-blank-line", "editor-show-execution-target-picker", "editor-confirm-dangerous-sql", "editor-continue-on-error"]) {
expect(editorSection.indexOf(`id="${id}"`)).toBeGreaterThan(executionGroup);
expect(editorSection.indexOf(`id="${id}"`)).toBeLessThan(executionGroupEnd);
expect(editorSection.match(new RegExp(`id="${id}"`, "g"))).toHaveLength(1);
}
for (const id of ["editor-confirm-unsaved-sql-close", "app-close-unsaved-tabs-mode"]) {
expect(editorSection.indexOf(`id="${id}"`)).toBeGreaterThan(unsavedGroup);
expect(editorSection.indexOf(`id="${id}"`)).toBeLessThan(unsavedGroupEnd);
}
expect(editorSection).toContain('id="editor-saved-sql-open-target"');
expect(editorSection).toContain('class="flex min-w-0 items-start justify-between gap-4"');
expect(editorSection).toContain('id="app-close-unsaved-tabs-mode" class="h-8 w-44 shrink-0"');
expect(editorSection).not.toContain('id="editor-click-table-navigation-ddl"');
expect(editorSection).not.toContain('id="editor-prefill-new-query"');
});
it("describes the execute shortcut below the mode selector in every supported locale", () => {
expect(editorSection.indexOf("executeModeDescription")).toBeGreaterThan(editorSection.indexOf('ref="previewRef"'));
for (const locale of ["zh-CN", "zh-TW", "en", "es", "it", "ja", "ko", "pt-BR"]) {
const source = readFileSync(new URL(`../../../i18n/locales/${locale}.ts`, import.meta.url), "utf8");
expect(source, locale).toContain("executeModeDescription:");
}
});
it("shows a concise description for the selected completion trigger mode", () => {
expect(dialogSource).toContain("const completionTriggerModeDescription = computed(() => {");
expect(editorSection).toContain("{{ completionTriggerModeDescription }}");
expect(editorSection).not.toContain('t("settings.completionTriggerModeDescription")');
for (const locale of ["zh-CN", "zh-TW", "en", "es", "it", "ja", "ko", "pt-BR"]) {
const source = readFileSync(new URL(`../../../i18n/locales/${locale}.ts`, import.meta.url), "utf8");
for (const key of ["completionTriggerModeManualDescription", "completionTriggerModeRequirePrefixDescription", "completionTriggerModePositionalDescription"]) {
expect(source, `${locale}:${key}`).toContain(`${key}:`);
}
}
});
it("moves table navigation and query prefill controls to Navigation", () => {
const navigationSection = dialogSource.slice(dialogSource.indexOf(`activeSettingsTab === 'navigation'`));
for (const id of ["editor-click-table-navigation-ddl", "editor-prefill-new-query"]) {
expect(navigationSection).toContain(`id="${id}"`);
expect(dialogSource.match(new RegExp(`id="${id}"`, "g"))).toHaveLength(1);
}
});
it("groups navigation settings by data tabs and sidebar browsing flow", () => {
const navigationSection = dialogSource.slice(dialogSource.indexOf(`activeSettingsTab === 'navigation'`));
const adjacentDataTabs = navigationSection.indexOf('id="open-data-tabs-next-to-active"');
const objectDisplay = navigationSection.indexOf('t("settings.sidebarObjectDisplay")');
const routineOpenMode = navigationSection.indexOf('t("settings.routineSourceOpenMode")');
const tableNavigation = navigationSection.indexOf('id="editor-click-table-navigation-ddl"');
const openDatabaseOnClick = navigationSection.indexOf('id="sidebar-open-database-on-single-click"');
const tableSearch = navigationSection.indexOf('id="sidebar-table-search-enabled"');
const activeNodeSelection = navigationSection.indexOf('id="auto-select-active-sidebar-node"');
expect(adjacentDataTabs).toBeGreaterThanOrEqual(0);
expect(objectDisplay).toBeGreaterThan(adjacentDataTabs);
expect(routineOpenMode).toBeGreaterThan(objectDisplay);
expect(tableNavigation).toBeGreaterThan(routineOpenMode);
expect(openDatabaseOnClick).toBeGreaterThan(tableNavigation);
expect(tableSearch).toBeGreaterThan(openDatabaseOnClick);
expect(activeNodeSelection).toBeGreaterThan(tableSearch);
});
it("puts the data grid filter view first with flat choices and a live preview", () => {
const filterView = dataSection.indexOf('data-settings-search-id="data-grid-filter-view"');
const dataGridDisplay = dataSection.indexOf('t("settings.dataGridDisplay")');
expect(dataSectionStart).toBeGreaterThanOrEqual(0);
expect(shortcutsSectionStart).toBeGreaterThan(dataSectionStart);
expect(filterView).toBeGreaterThanOrEqual(0);
expect(filterView).toBeLessThan(dataGridDisplay);
expect(dataSection).toContain(`:class="['overflow-hidden rounded-md border bg-muted/20', settingsSearchTargetClass('data-grid-filter-view')]"`);
expect(dataSection).toContain("data-data-grid-filter-view-options");
expect(dataSection).toContain('v-if="dataGridFilterViewPreviewExpanded" class="pointer-events-none select-none overflow-hidden border-t bg-background" data-data-grid-filter-view-preview');
expect(dataSection).toContain("data-data-grid-filter-view-preview-toggle");
expect(dataSection).toContain(':aria-expanded="dataGridFilterViewPreviewExpanded"');
expect(dataSection).toContain('t("settings.dataGridFilterViewPreview")');
expect(dataSection).toContain("dataGridFilterViewPreviewExpanded = !dataGridFilterViewPreviewExpanded");
expect(dataSection).toContain("data-data-grid-filter-preview-quick");
expect(dataSection).toContain("data-data-grid-filter-preview-conditions");
expect(dataSection).toContain("data-data-grid-filter-preview-text");
expect(dataSection.match(/editDataGridFilterEditorView = '(quick|conditions|text)'/g)).toHaveLength(3);
expect(dataSection).not.toContain('<SelectTrigger id="data-grid-filter-view"');
for (const locale of ["zh-CN", "zh-TW", "en", "es", "it", "ja", "ko", "pt-BR"]) {
const source = readFileSync(new URL(`../../../i18n/locales/${locale}.ts`, import.meta.url), "utf8");
for (const key of ["dataGridFilterViewPreview", "dataGridFilterViewPreviewExpand", "dataGridFilterViewPreviewCollapse"]) {
expect(source, `${locale}:${key}`).toContain(`${key}:`);
}
}
});
it("updates preview line-number visibility from the unsaved editor draft", () => {
expect(dialogSource).toContain("showLineNumbers: editShowLineNumbers.value");
expect(dialogSource).toContain('import { buildQueryEditorLineNumbersExtension } from "@/lib/editor/queryEditorLineNumbers";');
@@ -2,7 +2,7 @@
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import { translateBackendError } from "@/i18n/backend-errors";
import { Upload, Download, FolderPlus, FolderOpen, RefreshCw, ChevronsLeft, ChevronsUp, Trash2, FolderInput, Check, Minus, Square, X } from "@lucide/vue";
import { Upload, Download, ArrowDownUp, FolderPlus, FolderOpen, RefreshCw, ChevronsLeft, ChevronsDownUp, Trash2, FolderInput, Check, Minus, Square, X } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
@@ -45,6 +45,20 @@ const importSourceItems = computed(() => [
{ value: "dbeaver", label: t("sidebar.importDbeaver") },
{ value: "datagrip", label: t("sidebar.importDatagrip") },
]);
const connectionTransferItems = computed(() => [
...importSourceItems.value.map((item) => ({
...item,
value: `import:${item.value}`,
icon: Download,
})),
{
value: "export",
label: t("sidebar.export"),
icon: Upload,
separatorBefore: true,
},
]);
const connectionTransferLabel = computed(() => t("sidebar.importExport"));
const connectionIdSet = computed(() => new Set(connectionStore.connections.map((connection) => connection.id)));
const allConnectionIds = computed(() => connectionStore.connections.map((connection) => connection.id));
const selectedConnectionIds = computed(() => (connectionStore.connectionMultiSelectActive ? connectionStore.selectedTreeNodeIds.filter((id) => connectionIdSet.value.has(id)) : []));
@@ -84,6 +98,14 @@ function selectImportSource(source: string) {
emit("import", source as ImportSource);
}
function selectConnectionTransferAction(action: string) {
if (action === "export") {
emit("export");
return;
}
if (action.startsWith("import:")) selectImportSource(action.slice("import:".length));
}
function collapseAllTreeNodes() {
connectionTreeRef.value?.collapseAllTreeNodes();
}
@@ -178,12 +200,31 @@ defineExpose({ focusSearch, locateTabInSidebar });
<div class="app-sidebar-panel h-full shrink-0 relative select-none" :class="classicLayout ? '' : 'rounded-md border border-border/80 bg-background'" :style="{ width: sidebarWidth + 'px' }" @keydown="onSidebarKeydown">
<div class="h-full flex flex-col overflow-hidden">
<div class="app-sidebar-toolbar flex items-center gap-px px-3 text-xs font-medium text-muted-foreground border-b bg-muted/20" :class="classicLayout ? 'h-9' : 'h-10'">
<span class="flex min-w-0 self-stretch items-center" data-tauri-drag-region>
<span v-if="showConnectionMultiSelectToolbar" class="flex min-w-0 self-stretch items-center" data-tauri-drag-region>
<span class="truncate" data-tauri-drag-region>{{ t("sidebar.connections") }}</span>
<span v-if="showConnectionMultiSelectToolbar" class="ml-1.5 shrink-0 text-[11px] font-normal text-muted-foreground/80" data-connection-selection-count>
<span class="ml-1.5 shrink-0 text-[11px] font-normal text-muted-foreground/80" data-connection-selection-count>
{{ t("connectionGroup.selectedConnections", { count: selectedConnectionCount }) }}
</span>
</span>
<LightDropdown
v-else
model-value=""
:items="connectionTransferItems"
:aria-label="connectionTransferLabel"
:trigger-title="connectionTransferLabel"
:trigger-icon="ArrowDownUp"
:trigger-label="connectionTransferLabel"
trigger-class="inline-flex h-7 min-w-0 items-center gap-1 rounded-md px-1 outline-none hover:bg-muted hover:text-foreground focus-visible:ring-0"
trigger-icon-class="h-3.5 w-3.5 shrink-0"
item-icon-class="h-3.5 w-3.5"
content-class="w-48"
:show-trigger-label="true"
:show-chevron="true"
:highlight-selected="false"
check-position="none"
align="start"
@update:model-value="selectConnectionTransferAction"
/>
<span class="flex-1 self-stretch" data-tauri-drag-region />
<template v-if="showConnectionMultiSelectToolbar">
<LightTooltip :text="t('connectionGroup.createGroup')" side="bottom" :delay="0" :close-delay="0" nowrap>
@@ -227,33 +268,9 @@ defineExpose({ focusSearch, locateTabInSidebar });
</LightTooltip>
</template>
<template v-else>
<LightTooltip :text="t('sidebar.import')" side="bottom" :delay="0" :close-delay="0" nowrap>
<span class="inline-flex">
<LightDropdown
model-value=""
:items="importSourceItems"
:aria-label="t('sidebar.import')"
:trigger-icon="Download"
trigger-class="inline-flex h-6 w-5 items-center justify-center rounded-md outline-none hover:bg-muted hover:text-foreground focus-visible:ring-0"
trigger-icon-class="h-4 w-4"
content-class="w-44"
:show-trigger-label="false"
:show-chevron="false"
:highlight-selected="false"
check-position="none"
align="end"
@update:model-value="selectImportSource"
/>
</span>
</LightTooltip>
<LightTooltip :text="t('sidebar.export')" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="emit('export')">
<Upload class="h-3 w-3" />
</Button>
</LightTooltip>
<LightTooltip :text="t('sidebar.collapseAll')" side="bottom" :delay="0" :close-delay="0" nowrap>
<Button variant="ghost" size="icon" class="h-5 w-5" @click="collapseAllTreeNodes">
<ChevronsUp class="h-3 w-3" />
<ChevronsDownUp class="h-3 w-3" />
</Button>
</LightTooltip>
<LightTooltip :text="t('connectionGroup.createGroup')" side="bottom" :delay="0" :close-delay="0" nowrap>
@@ -6,6 +6,7 @@ import { ArrowDownWideNarrow, Download, FilePlus, FileText, FolderCog, FolderClo
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import CustomContextMenu, { type ContextMenuItem as CtxMenuItem } from "@/components/ui/CustomContextMenu.vue";
import HelpTooltip from "@/components/ui/tooltip/HelpTooltip.vue";
import LightTooltip from "@/components/ui/LightTooltip.vue";
import { useToast } from "@/composables/useToast";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
@@ -1121,6 +1122,9 @@ function showDropInside(targetId: string) {
<div class="h-full flex flex-col overflow-hidden border-l bg-background select-none">
<div class="h-9 flex items-center gap-1 px-2 border-b shrink-0 bg-muted/20">
<span class="text-[13px] font-medium">{{ t("sqlLibrary.title") }}</span>
<HelpTooltip :label="t('sqlLibrary.storageHelp')" side="bottom" :side-offset="4" trigger-class="h-4 w-4" content-class="max-w-[320px] whitespace-pre-line">
{{ t("sqlLibrary.storageHelp") }}
</HelpTooltip>
<span v-if="hasSelection" class="text-[12px] text-muted-foreground ml-1">({{ selectedCount }})</span>
<span class="flex-1" />
<LightTooltip :text="sortMode === 'folder' ? t('sqlLibrary.sortByDate') : t('sqlLibrary.sortByFolder')" side="bottom" :delay="0" :close-delay="0" nowrap>
@@ -4,6 +4,12 @@ import { describe, expect, it } from "vitest";
const panelSource = readFileSync(new URL("../SqlLibraryPanel.vue", import.meta.url), "utf8");
describe("SqlLibraryPanel selection contrast", () => {
it("explains how SQL Library content differs from local SQL files", () => {
expect(panelSource).toContain('import HelpTooltip from "@/components/ui/tooltip/HelpTooltip.vue";');
expect(panelSource).toMatch(/<HelpTooltip :label="t\('sqlLibrary\.storageHelp'\)" side="bottom"[\s\S]*?\{\{ t\("sqlLibrary\.storageHelp"\) \}\}[\s\S]*?<\/HelpTooltip>/);
expect(panelSource).toContain('content-class="max-w-[320px] whitespace-pre-line"');
});
it("uses the accent foreground for selected rows and their metadata", () => {
expect(panelSource).toContain('return "bg-accent text-accent-foreground";');
expect(panelSource).toMatch(/function fileMetaClass[\s\S]*\? "text-accent-foreground" : "text-muted-foreground";/);
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, shallowRef, computed, nextTick, watch, provide, onMounted, onUnmounted, type Component, type ComponentPublicInstance, type CSSProperties } from "vue";
import { useI18n } from "vue-i18n";
import { Search, X, ListFilter, ListOrdered, ArrowDownAZ, ArrowUpZA, Crosshair, Server, Database, FolderTree, Table2, Eye, RotateCcw, Loader2, Unplug } from "@lucide/vue";
import { Search, X, SlidersHorizontal, ListOrdered, ArrowDownAZ, ArrowUpZA, LocateFixed, Server, Database, FolderTree, Table2, Eye, RotateCcw, Loader2, Unplug } from "@lucide/vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import { useSavedSqlStore } from "@/stores/savedSqlStore";
@@ -443,24 +443,6 @@ const searchScopeOptions = computed(() => {
{ scope: "view", label: t("sidebar.searchScopeView"), icon: Eye },
] as const satisfies ReadonlyArray<{ scope: SearchScope; label: string; icon: Component }>;
});
const searchScopeMenuItems = computed(() => [
...searchScopeOptions.value.map((item) => ({
value: item.scope,
label: item.label,
icon: item.icon,
})),
...(hasSearchScopeFilter.value
? [
{
value: "__clear",
label: t("sidebar.clearFilter"),
icon: RotateCcw,
separatorBefore: true,
},
]
: []),
]);
const connectionListSortMenuItems = computed(() => [
{ value: "manual", label: t("sidebar.sortConnectionsManual"), icon: ListOrdered },
{ value: "asc", label: t("sidebar.sortConnectionsAscending"), icon: ArrowDownAZ },
@@ -468,6 +450,33 @@ const connectionListSortMenuItems = computed(() => [
]);
const isConnectionListAlphabeticallySorted = computed(() => settingsStore.editorSettings.sidebarConnectionSortMode !== "manual");
const sidebarListOptionsLabel = computed(() => `${t("sidebar.sortConnections")} / ${t("sidebar.filterByType")}`);
const sidebarListOptionItems = computed(() => [
...connectionListSortMenuItems.value.map((item, index) => ({
...item,
value: `sort:${item.value}`,
groupLabel: index === 0 ? t("sidebar.sortConnections") : undefined,
})),
...searchScopeOptions.value.map((item, index) => ({
value: `scope:${item.scope}`,
label: item.label,
icon: item.icon,
separatorBefore: index === 0,
groupLabel: index === 0 ? t("sidebar.filterByType") : undefined,
})),
...(hasSearchScopeFilter.value
? [
{
value: "clear-scopes",
label: t("sidebar.clearFilter"),
icon: RotateCcw,
separatorBefore: true,
},
]
: []),
]);
const selectedSidebarListOptions = computed(() => [`sort:${settingsStore.editorSettings.sidebarConnectionSortMode}`, ...selectedSearchScopes.value.map((scope) => `scope:${scope}`)]);
const hasCustomSidebarListOptions = computed(() => isConnectionListAlphabeticallySorted.value || hasSearchScopeFilter.value);
function updateConnectionListSortMode(mode: string) {
if (mode === "manual" || mode === "asc" || mode === "desc") {
@@ -496,12 +505,16 @@ function toggleSearchScope(scope: SearchScope) {
}
}
function selectSearchScopeMenuItem(value: string) {
if (value === "__clear") {
clearSearchScopeFilter();
function selectSidebarListOption(value: string) {
if (value.startsWith("sort:")) {
updateConnectionListSortMode(value.slice("sort:".length));
return;
}
toggleSearchScope(value as SearchScope);
if (value.startsWith("scope:")) {
toggleSearchScope(value.slice("scope:".length) as SearchScope);
return;
}
if (value === "clear-scopes") clearSearchScopeFilter();
}
function clearSearchScopeFilter() {
@@ -2373,7 +2386,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes, locateTabInSid
/>
<div class="connection-tree-search sticky top-0 z-10 bg-background px-2 py-1">
<div class="relative flex items-center gap-1">
<div class="relative flex-1">
<div class="relative min-w-0 flex-1">
<Loader2 v-if="isSidebarSearchLoading" class="absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 animate-spin text-muted-foreground" />
<Search v-else class="absolute left-2 top-1/2 -translate-y-1/2 h-3 w-3 text-muted-foreground" />
<input
@@ -2382,66 +2395,47 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes, locateTabInSid
autocapitalize="off"
autocorrect="off"
spellcheck="false"
class="w-full h-6 pl-7 pr-6 text-xs rounded border bg-background focus:outline-none focus:ring-1 focus:ring-ring"
class="w-full h-6 pl-7 pr-[4.75rem] text-xs rounded border bg-background focus:outline-none focus:ring-1 focus:ring-ring"
:class="regexMode && compileSearchRegex(searchQuery).invalid ? 'border-destructive focus:ring-destructive' : 'border-border'"
:aria-invalid="regexMode && compileSearchRegex(searchQuery).invalid ? 'true' : 'false'"
:placeholder="t('grid.search')"
@keydown="onSearchKeydown"
/>
<button v-if="searchQuery" class="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" @click="searchQuery = ''">
<X class="h-3 w-3" />
</button>
<div class="absolute inset-y-0 right-0.5 flex items-center">
<button v-if="searchQuery" type="button" class="flex h-5 w-5 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground" :aria-label="t('common.clear')" @click="searchQuery = ''">
<X class="h-3 w-3" />
</button>
<LightTooltip :text="t('sidebar.regexSearchTooltip')" side="top" :delay="300">
<button
type="button"
class="flex h-5 min-w-5 items-center justify-center rounded-sm px-0.5 text-[10px] font-mono text-muted-foreground hover:bg-accent hover:text-foreground"
:class="{ 'text-primary bg-primary/10': regexMode, 'text-destructive': regexMode && compileSearchRegex(searchQuery).invalid }"
:aria-label="t('sidebar.regexSearch')"
:aria-pressed="regexMode"
@click="regexMode = !regexMode"
>
.*
</button>
</LightTooltip>
<LightTooltip :text="t('sidebar.globalLocalSearchTooltip')" side="top" :delay="300">
<Switch size="sm" :model-value="settingsStore.editorSettings.sidebarGlobalSearchLocal" :disabled="regexMode" :aria-label="t('sidebar.globalLocalSearch')" @update:model-value="settingsStore.updateEditorSettings({ sidebarGlobalSearchLocal: Boolean($event) })" />
</LightTooltip>
</div>
</div>
<LightTooltip :text="t('sidebar.globalLocalSearchTooltip')" side="top" :delay="300">
<Switch size="sm" :model-value="settingsStore.editorSettings.sidebarGlobalSearchLocal" :disabled="regexMode" :aria-label="t('sidebar.globalLocalSearch')" @update:model-value="settingsStore.updateEditorSettings({ sidebarGlobalSearchLocal: Boolean($event) })" />
</LightTooltip>
<LightTooltip :text="t('sidebar.regexSearchTooltip')" side="top" :delay="300">
<button
type="button"
class="shrink-0 h-6 min-w-6 px-1 flex items-center justify-center rounded border border-border text-[10px] font-mono hover:bg-accent"
:class="{ 'text-primary bg-primary/10 border-primary/30': regexMode, 'text-destructive border-destructive/60': regexMode && compileSearchRegex(searchQuery).invalid }"
:aria-label="t('sidebar.regexSearch')"
:aria-pressed="regexMode"
@click="regexMode = !regexMode"
>
.*
</button>
</LightTooltip>
<LightTooltip :text="t('sidebar.locateActiveTab')" side="top" :delay="300" nowrap>
<button type="button" class="shrink-0 h-6 w-6 flex items-center justify-center rounded border border-border text-muted-foreground hover:bg-accent hover:text-foreground" :aria-label="t('sidebar.locateActiveTab')" @click="locateActiveTabInSidebar">
<Crosshair class="h-3.5 w-3.5" />
<button type="button" class="flex h-6 w-6 shrink-0 items-center justify-center rounded border border-border text-muted-foreground hover:bg-accent hover:text-foreground" :aria-label="t('sidebar.locateActiveTab')" @click="locateActiveTabInSidebar">
<LocateFixed class="h-3.5 w-3.5" />
</button>
</LightTooltip>
<LightTooltip :text="t('sidebar.sortConnections')" side="top" :delay="300" nowrap>
<span class="inline-flex">
<LightDropdown
:model-value="settingsStore.editorSettings.sidebarConnectionSortMode"
:items="connectionListSortMenuItems"
:aria-label="t('sidebar.sortConnections')"
:label="t('sidebar.sortConnections')"
:trigger-class="['shrink-0 h-6 w-6 flex items-center justify-center rounded border border-border hover:bg-accent', isConnectionListAlphabeticallySorted ? 'text-primary bg-primary/10 border-primary/30' : 'text-muted-foreground'].join(' ')"
trigger-icon-class="h-3.5 w-3.5"
item-icon-class="h-3.5 w-3.5"
content-class="w-max min-w-0"
selected-item-class="bg-primary/10 text-primary"
selected-check-class="text-primary"
:show-trigger-label="false"
:show-chevron="false"
align="end"
@update:model-value="updateConnectionListSortMode"
/>
</span>
</LightTooltip>
<LightTooltip v-if="searchScopeOptions.length > 0" :text="t('sidebar.filterByType')" side="top" :delay="300" nowrap>
<LightTooltip :text="sidebarListOptionsLabel" side="top" :delay="300" nowrap>
<span class="inline-flex">
<LightDropdown
model-value=""
:items="searchScopeMenuItems"
:selected-values="selectedSearchScopes"
:aria-label="t('sidebar.filterByType')"
:label="t('sidebar.filterByType')"
:trigger-icon="ListFilter"
:trigger-class="['shrink-0 h-6 w-6 flex items-center justify-center rounded border border-border hover:bg-accent', hasSearchScopeFilter ? 'text-primary bg-primary/10 border-primary/30' : 'text-muted-foreground'].join(' ')"
:items="sidebarListOptionItems"
:selected-values="selectedSidebarListOptions"
:aria-label="sidebarListOptionsLabel"
:trigger-icon="SlidersHorizontal"
:trigger-class="['shrink-0 h-6 w-6 flex items-center justify-center rounded border border-border hover:bg-accent', hasCustomSidebarListOptions ? 'text-primary bg-primary/10 border-primary/30' : 'text-muted-foreground'].join(' ')"
trigger-icon-class="h-3.5 w-3.5"
item-icon-class="h-3.5 w-3.5"
content-class="w-max min-w-0"
@@ -2451,7 +2445,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes, locateTabInSid
:show-chevron="false"
:close-on-select="false"
align="end"
@update:model-value="selectSearchScopeMenuItem"
@update:model-value="selectSidebarListOption"
/>
</span>
</LightTooltip>
@@ -5,6 +5,7 @@ import { Check, ChevronDown } from "@lucide/vue";
export interface LightDropdownItem {
label: string;
value: string;
groupLabel?: string;
title?: string;
icon?: Component;
iconClass?: string;
@@ -182,6 +183,7 @@ onBeforeUnmount(close);
<div v-if="label" class="bg-border -mx-1 my-1 h-px" />
<template v-for="item in items" :key="item.value">
<div v-if="item.separatorBefore" class="bg-border -mx-1 my-1 h-px" />
<div v-if="item.groupLabel" class="px-1.5 py-1 text-[11px] font-medium text-muted-foreground">{{ item.groupLabel }}</div>
<button
type="button"
class="flex w-full items-center gap-1.5 rounded-md px-1.5 py-1 text-left text-sm outline-hidden hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50"
@@ -21,16 +21,18 @@ describe("execute-shortcut hints reflect the configured binding", () => {
expect(formatShortcutDisplay(shortcut, platform)).toBe(expected);
});
it("interpolates the formatted shortcut into toolbar and settings labels", async () => {
it("interpolates the formatted shortcut into the toolbar label and settings description", async () => {
const shortcut = formatShortcutDisplay("Shift+Mod+Enter", "Win32");
await setLocale("en");
expect(i18n.global.t("toolbar.executeShortcut", { shortcut })).toBe("Execute selection/query (Ctrl + Shift + ↵)");
expect(i18n.global.t("settings.executeMode", { shortcut })).toBe("Execute Mode (Ctrl + Shift + ↵)");
expect(i18n.global.t("settings.executeMode")).toBe("Execute mode");
expect(i18n.global.t("settings.executeModeDescription", { shortcut })).toBe("Run SQL shortcut: Ctrl + Shift + ↵. Controls whether it executes all SQL or the statement at the cursor by default.");
await setLocale("zh-CN");
expect(i18n.global.t("toolbar.executeShortcut", { shortcut })).toBe("执行选中/全部 (Ctrl + Shift + ↵)");
expect(i18n.global.t("settings.executeMode", { shortcut })).toBe("执行模式 (Ctrl + Shift + ↵)");
expect(i18n.global.t("settings.executeMode")).toBe("执行模式");
expect(i18n.global.t("settings.executeModeDescription", { shortcut })).toBe("执行 SQL 快捷键:Ctrl + Shift + ↵。用于控制默认执行全部 SQL 或光标所在语句。");
await setLocale("en");
});
@@ -41,7 +43,7 @@ describe("execute-shortcut hints reflect the configured binding", () => {
it("EditorSettingsDialog.vue uses the currently edited executeSql shortcut", () => {
expect(settingsDialogSource).toMatch(/function translateWithExecuteShortcut\([^)]*\)[^}]*formatShortcutDisplay\(editShortcuts\.value\.executeSql\)/);
expect(settingsDialogSource).toContain('translateWithExecuteShortcut("settings.executeMode")');
expect(settingsDialogSource).toContain('translateWithExecuteShortcut("settings.executeModeDescription")');
});
it("EditorSettingsDialog.vue uses the same edited shortcut in settings search", () => {
+12 -3
View File
@@ -310,6 +310,7 @@ export default {
sidebar: {
connections: "CONNECTIONS",
noConnections: "No connections yet",
importExport: "Import / Export",
import: "Import Connections",
importDbx: "Import DBX Config",
importNavicat: "Import Navicat NCX",
@@ -380,6 +381,7 @@ export default {
},
sqlLibrary: {
title: "SQL Library",
storageHelp: "SQL Library content is stored in DBX app data, not as standalone .sql files on disk.\nTo use files in the file system, export the SQL Library.",
unassociated: "Unassociated",
deletedConnection: "Connection deleted",
openInCurrentDatabase: "Open in Current Database",
@@ -6255,6 +6257,9 @@ export default {
dataGridDisplay: "Data grid display",
dataGridFilterView: "Table filter view",
dataGridFilterViewDescription: "Use a filter popover, a fixed conditions panel, or a compact text conditions panel.",
dataGridFilterViewPreview: "Effect preview",
dataGridFilterViewPreviewExpand: "Expand effect preview",
dataGridFilterViewPreviewCollapse: "Collapse effect preview",
tableOpenPageSize: "Default rows per page when opening tables",
tableOpenPageSizeDescription: "Rows loaded per page when opening a new table. Changing rows per page in a table updates this default for future table tabs.",
queryPageSize: "Default query rows per page",
@@ -6471,7 +6476,8 @@ export default {
resetAllDefaults: "Reset All Defaults",
save: "Save",
cancel: "Cancel",
executeMode: "Execute Mode ({shortcut})",
executeMode: "Execute mode",
executeModeDescription: "Run SQL shortcut: {shortcut}. Controls whether it executes all SQL or the statement at the cursor by default.",
executeModeAll: "Execute all SQL",
executeModeCurrent: "Execute statement at cursor",
executeAllOnBlankLine: "Execute all SQL from blank lines",
@@ -6500,11 +6506,14 @@ export default {
sortCompletionColumnsAlphabeticallyDescription: "Turn off to keep table fields in their original database order",
selectFirstCompletionOnOpen: "Automatically select the first completion",
selectFirstCompletionOnOpenDescription: "When enabled, the first candidate is selected when the list opens, and Enter or Tab accepts it. When disabled, Enter inserts a new line and Tab accepts the first candidate.",
completionTriggerMode: "Auto-completion trigger mode",
completionTriggerModeDescription: "Controls when the SQL completion popup opens automatically. Manual requires a shortcut key; Typed requires a typed identifier prefix; Smart preserves the current behavior.",
completionTriggerMode: "SQL completion trigger mode",
completionTriggerModeDescription: "Choose how SQL completion candidates are shown.",
completionTriggerModeManual: "Manual only",
completionTriggerModeManualDescription: "Show candidates only when using the completion shortcut {shortcut}.",
completionTriggerModeRequirePrefix: "Typed identifier prefix",
completionTriggerModeRequirePrefixDescription: "Show candidates after at least one identifier character is typed.",
completionTriggerModePositional: "Smart position",
completionTriggerModePositionalDescription: "Show candidates automatically based on the current SQL syntax position.",
sqlSemanticDiagnosticsEnabled: "SQL semantic diagnostics",
sqlSemanticDiagnosticsEnabledDescription: "When enabled, the editor reports semantic issues such as unknown tables and columns. Disable it to reduce parsing and metadata checks for large SQL.",
confirmDangerousSqlExecution: "Confirm before dangerous operations",
+12 -3
View File
@@ -312,6 +312,7 @@ export default withEnglishFallback({
sidebar: {
connections: "CONEXIONES",
noConnections: "Sin conexiones",
importExport: "Importar / Exportar",
import: "Importar conexiones",
importDbx: "Importar configuración DBX",
importNavicat: "Importar Navicat NCX",
@@ -382,6 +383,7 @@ export default withEnglishFallback({
},
sqlLibrary: {
title: "Biblioteca SQL",
storageHelp: "El contenido de la Biblioteca SQL se guarda en los datos de la aplicación DBX, no como archivos .sql independientes en el disco.\nPara usarlo en el sistema de archivos, exporta la Biblioteca SQL.",
unassociated: "Sin asociación",
deletedConnection: "Conexión eliminada",
openInCurrentDatabase: "Abrir en la base de datos actual",
@@ -5975,6 +5977,9 @@ export default withEnglishFallback({
dataGridDisplay: "Visualización de la tabla",
dataGridFilterView: "Vista de filtro de tabla",
dataGridFilterViewDescription: "Usa un filtro emergente, un panel de condiciones fijo o un panel de texto compacto.",
dataGridFilterViewPreview: "Vista previa del efecto",
dataGridFilterViewPreviewExpand: "Expandir vista previa del efecto",
dataGridFilterViewPreviewCollapse: "Contraer vista previa del efecto",
tableOpenPageSize: "Filas predeterminadas por página al abrir tablas",
tableOpenPageSizeDescription: "Filas cargadas por página al abrir una tabla nueva. Cambiar las filas por página actualiza este valor para futuras pestañas de tablas.",
queryPageSize: "Filas predeterminadas por página de consulta",
@@ -6157,7 +6162,8 @@ export default withEnglishFallback({
resetAllDefaults: "Restablecer todos los valores por defecto",
save: "Guardar",
cancel: "Cancelar",
executeMode: "Modo de ejecución ({shortcut})",
executeMode: "Modo de ejecución",
executeModeDescription: "Atajo para ejecutar SQL: {shortcut}. Controla si se ejecuta todo el SQL o la sentencia en el cursor de forma predeterminada.",
executeModeAll: "Ejecutar todo el SQL",
executeModeCurrent: "Ejecutar sentencia en el cursor",
executeAllOnBlankLine: "Ejecutar todo el SQL desde líneas en blanco",
@@ -6182,11 +6188,14 @@ export default withEnglishFallback({
sqlCompletionSection: "Autocompletado SQL",
selectFirstCompletionOnOpen: "Seleccionar automáticamente la primera sugerencia",
selectFirstCompletionOnOpenDescription: "Al activarlo, se selecciona la primera sugerencia al abrir la lista y se acepta con Enter o Tab. Al desactivarlo, Enter inserta una nueva línea y Tab acepta la primera sugerencia.",
completionTriggerMode: "Modo de activación de autocompletado",
completionTriggerModeDescription: "Controla cuándo se abre automáticamente la ventana de autocompletado SQL. Manual requiere una tecla de acceso rápido; Prefijo escrito requiere un identificador escrito; Inteligente conserva el comportamiento actual.",
completionTriggerMode: "Modo de activación del autocompletado SQL",
completionTriggerModeDescription: "Elige cómo se muestran las sugerencias de autocompletado SQL.",
completionTriggerModeManual: "Solo manual",
completionTriggerModeManualDescription: "Muestra sugerencias solo al usar el atajo de autocompletado {shortcut}.",
completionTriggerModeRequirePrefix: "Prefijo de identificador escrito",
completionTriggerModeRequirePrefixDescription: "Muestra sugerencias después de escribir al menos un carácter del identificador.",
completionTriggerModePositional: "Posición inteligente",
completionTriggerModePositionalDescription: "Muestra sugerencias automáticamente según la posición sintáctica SQL actual.",
sqlSemanticDiagnosticsEnabled: "Diagnóstico semántico de SQL",
sqlSemanticDiagnosticsEnabledDescription: "Al activarse, el editor informa de problemas semánticos como tablas y columnas desconocidas. Desactívalo para reducir el análisis y las comprobaciones de metadatos en SQL grandes.",
confirmDangerousSqlExecution: "Confirmar antes de operaciones peligrosas",
+12 -3
View File
@@ -311,6 +311,7 @@ export default withEnglishFallback({
sidebar: {
connections: "CONNESSIONI",
noConnections: "Nessuna connessione",
importExport: "Importa / Esporta",
import: "Importa Connessioni",
importDbx: "Importa Configurazione DBX",
importNavicat: "Importa Navicat NCX",
@@ -381,6 +382,7 @@ export default withEnglishFallback({
},
sqlLibrary: {
title: "Libreria SQL",
storageHelp: "Il contenuto della Libreria SQL viene salvato nei dati dell'app DBX, non come file .sql autonomi sul disco.\nPer usarlo nel file system, esporta la Libreria SQL.",
unassociated: "Non associato",
deletedConnection: "Connessione eliminata",
openInCurrentDatabase: "Apri nel database corrente",
@@ -5975,6 +5977,9 @@ export default withEnglishFallback({
dataGridDisplay: "Visualizzazione griglia dati",
dataGridFilterView: "Vista filtro tabella",
dataGridFilterViewDescription: "Usa un filtro popup, un pannello condizioni fisso o un pannello di testo compatto.",
dataGridFilterViewPreview: "Anteprima effetto",
dataGridFilterViewPreviewExpand: "Espandi anteprima effetto",
dataGridFilterViewPreviewCollapse: "Comprimi anteprima effetto",
tableOpenPageSize: "Righe predefinite per pagina all'apertura delle tabelle",
tableOpenPageSizeDescription: "Righe caricate per pagina quando si apre una nuova tabella. La modifica delle righe per pagina aggiorna questo valore per le future schede tabella.",
queryPageSize: "Righe predefinite per pagina di query",
@@ -6157,7 +6162,8 @@ export default withEnglishFallback({
resetAllDefaults: "Ripristina Tutti i Predefiniti",
save: "Salva",
cancel: "Annulla",
executeMode: "Modalità Esecuzione ({shortcut})",
executeMode: "Modalità di esecuzione",
executeModeDescription: "Scorciatoia Esegui SQL: {shortcut}. Stabilisce se eseguire tutto l'SQL o l'istruzione al cursore per impostazione predefinita.",
executeModeAll: "Esegui tutto l'SQL",
executeModeCurrent: "Esegui istruzione al cursore",
executeAllOnBlankLine: "Esegui tutto l'SQL dalle righe vuote",
@@ -6182,11 +6188,14 @@ export default withEnglishFallback({
sqlCompletionSection: "Completamento SQL",
selectFirstCompletionOnOpen: "Seleziona automaticamente il primo suggerimento",
selectFirstCompletionOnOpenDescription: "Se attivo, il primo suggerimento viene selezionato all'apertura dell'elenco e accettato con Invio o Tab. Se disattivo, Invio inserisce una nuova riga e Tab accetta il primo suggerimento.",
completionTriggerMode: "Modalità di attivazione autocompletamento",
completionTriggerModeDescription: "Controlla quando si apre automaticamente il popup di autocompletamento SQL. Manuale richiede un tasto di scelta rapida; Prefisso digitato richiede un identificatore digitato; Intelligente mantiene il comportamento attuale.",
completionTriggerMode: "Modalità di attivazione del completamento SQL",
completionTriggerModeDescription: "Scegli come mostrare i suggerimenti di completamento SQL.",
completionTriggerModeManual: "Solo manuale",
completionTriggerModeManualDescription: "Mostra i suggerimenti solo usando la scorciatoia di completamento {shortcut}.",
completionTriggerModeRequirePrefix: "Prefisso identificatore digitato",
completionTriggerModeRequirePrefixDescription: "Mostra i suggerimenti dopo aver digitato almeno un carattere dell'identificatore.",
completionTriggerModePositional: "Posizione intelligente",
completionTriggerModePositionalDescription: "Mostra automaticamente i suggerimenti in base alla posizione sintattica SQL corrente.",
sqlSemanticDiagnosticsEnabled: "Diagnostica semantica SQL",
sqlSemanticDiagnosticsEnabledDescription: "Se abilitata, l'editor segnala problemi semantici come tabelle e colonne sconosciute. Disabilitala per ridurre l'analisi e i controlli dei metadati per file SQL di grandi dimensioni.",
confirmDangerousSqlExecution: "Conferma prima delle operazioni pericolose",
+12 -3
View File
@@ -312,6 +312,7 @@ export default withEnglishFallback({
sidebar: {
connections: "接続",
noConnections: "まだ接続がありません",
importExport: "インポート / エクスポート",
import: "接続をインポート",
importDbx: "DBX設定をインポート",
importNavicat: "Navicat NCXをインポート",
@@ -382,6 +383,7 @@ export default withEnglishFallback({
},
sqlLibrary: {
title: "SQLライブラリ",
storageHelp: "SQLライブラリの内容はDBXのアプリデータに保存され、ディスク上の独立した.sqlファイルではありません。\nファイルシステムで使用するには、SQLライブラリをエクスポートしてください。",
unassociated: "未関連付け",
deletedConnection: "削除された接続",
openInCurrentDatabase: "現在のデータベースで開く",
@@ -6009,6 +6011,9 @@ export default withEnglishFallback({
dataGridDisplay: "データグリッド表示",
dataGridFilterView: "テーブルフィルタービュー",
dataGridFilterViewDescription: "フィルターポップオーバー、固定条件パネル、またはコンパクトなテキスト条件パネルを使用します。",
dataGridFilterViewPreview: "効果プレビュー",
dataGridFilterViewPreviewExpand: "効果プレビューを展開",
dataGridFilterViewPreviewCollapse: "効果プレビューを折りたたむ",
tableOpenPageSize: "テーブルを開くときのデフォルト行数",
tableOpenPageSizeDescription: "新しいテーブルを開くときに 1 ページあたり読み込む行数です。テーブルで行数を変更すると、今後のテーブルタブのデフォルトも更新されます。",
queryPageSize: "クエリ結果のデフォルト行数",
@@ -6183,7 +6188,8 @@ export default withEnglishFallback({
resetAllDefaults: "すべての設定をリセット",
save: "保存",
cancel: "キャンセル",
executeMode: "実行モード ({shortcut})",
executeMode: "実行モード",
executeModeDescription: "SQL実行ショートカット: {shortcut}。既定でSQL全体またはカーソル位置のステートメントのどちらを実行するかを設定します。",
executeModeAll: "すべてのSQLを実行",
executeModeCurrent: "カーソル位置の文を実行",
executeAllOnBlankLine: "空行ではすべてのSQLを実行",
@@ -6592,11 +6598,14 @@ export default withEnglishFallback({
insertSpaceAfterCompletionDescription: "キーワード、テーブル名、列名の補全確定時、次の文字が許容する場合に自動でスペースを補填します",
selectFirstCompletionOnOpen: "最初の補完候補を自動選択",
selectFirstCompletionOnOpenDescription: "有効にすると、候補一覧を開いた時に先頭項目を選択し、Enter または Tab で確定します。無効にすると、Enter は改行し、Tab で先頭候補を確定します。",
completionTriggerMode: "自動補完トリガーモード",
completionTriggerModeDescription: "SQL補完ポップアップが自動的に開くタイミングを制御します。手動はショートカットキーが必要; 識別子入力後は1文字以上の入力が必要; スマート位置は現在の動作を維持します。",
completionTriggerMode: "SQL補完トリガーモード",
completionTriggerModeDescription: "SQL補完候補の表示方法を選択します。",
completionTriggerModeManual: "手動のみ",
completionTriggerModeManualDescription: "補完ショートカット {shortcut} を使用したときだけ候補を表示します。",
completionTriggerModeRequirePrefix: "識別子入力後",
completionTriggerModeRequirePrefixDescription: "識別子を1文字以上入力すると候補を表示します。",
completionTriggerModePositional: "スマート位置",
completionTriggerModePositionalDescription: "現在のSQL構文位置に応じて候補を自動表示します。",
sidebarIndent: "サイドバーのインデント",
sidebarIndentDescription: "各ツリー階層のインデントのピクセルオフセット。",
sidebarFontSize: "サイドバーのフォントサイズ",
+12 -3
View File
@@ -310,6 +310,7 @@ export default withEnglishFallback({
sidebar: {
connections: "연결",
noConnections: "아직 연결이 없습니다",
importExport: "가져오기 / 내보내기",
import: "연결 가져오기",
importDbx: "DBX 설정 가져오기",
importNavicat: "Navicat NCX 가져오기",
@@ -375,6 +376,7 @@ export default withEnglishFallback({
},
sqlLibrary: {
title: "SQL 라이브러리",
storageHelp: "SQL 라이브러리 내용은 디스크의 독립된 .sql 파일이 아니라 DBX 앱 데이터에 저장됩니다.\n파일 시스템에서 사용하려면 SQL 라이브러리를 내보내세요.",
unassociated: "연결되지 않음",
deletedConnection: "삭제된 연결",
openInCurrentDatabase: "현재 데이터베이스에서 열기",
@@ -5652,6 +5654,9 @@ export default withEnglishFallback({
dataGridDisplay: "데이터 그리드 표시",
dataGridFilterView: "테이블 필터 보기",
dataGridFilterViewDescription: "필터 팝오버, 고정 조건 패널 또는 간결한 텍스트 조건 패널을 사용합니다.",
dataGridFilterViewPreview: "효과 미리보기",
dataGridFilterViewPreviewExpand: "효과 미리보기 펼치기",
dataGridFilterViewPreviewCollapse: "효과 미리보기 접기",
tableOpenPageSize: "테이블을 열 때 기본 페이지당 행 수",
tableOpenPageSizeDescription: "새 테이블을 열 때 페이지당 로드되는 행 수입니다. 테이블에서 페이지당 행을 변경하면 향후 테이블 탭의 기본값이 업데이트됩니다.",
queryPageSize: "쿼리 결과 기본 페이지당 행 수",
@@ -5852,7 +5857,8 @@ export default withEnglishFallback({
resetAllDefaults: "모든 기본값 재설정",
save: "저장",
cancel: "취소",
executeMode: "실행 모드 ({shortcut})",
executeMode: "실행 모드",
executeModeDescription: "SQL 실행 단축키: {shortcut}. 기본적으로 전체 SQL 또는 커서가 있는 구문 중 무엇을 실행할지 설정합니다.",
executeModeAll: "모든 SQL 실행",
executeModeCurrent: "커서 위치의 구문 실행",
executeAllOnBlankLine: "빈 줄에서 모든 SQL 실행",
@@ -5879,11 +5885,14 @@ export default withEnglishFallback({
insertSpaceAfterCompletionDescription: "다음 문자가 허용할 때 키워드, 테이블 또는 컬럼 완성을 수락한 후 공백을 추가합니다",
selectFirstCompletionOnOpen: "첫 번째 자동 완성 후보 자동 선택",
selectFirstCompletionOnOpenDescription: "활성화하면 후보 목록이 열릴 때 첫 항목을 선택하고 Enter 또는 Tab으로 적용합니다. 비활성화하면 Enter는 줄을 바꾸고 Tab은 첫 후보를 적용합니다.",
completionTriggerMode: "자동 완성 트리거 모드",
completionTriggerModeDescription: "SQL 완성 팝업이 자동으로 열리는 시기를 제어합니다. 수동은 단축키 필요; 입력 식별자는 하나 이상의 문자 입력 필요; 스마트 위치는 현재 동작 유지.",
completionTriggerMode: "SQL 자동 완성 트리거 모드",
completionTriggerModeDescription: "SQL 자동 완성 후보 표시 방식을 선택합니다.",
completionTriggerModeManual: "수동만",
completionTriggerModeManualDescription: "자동 완성 단축키 {shortcut}을 사용할 때만 후보를 표시합니다.",
completionTriggerModeRequirePrefix: "식별자 입력 후",
completionTriggerModeRequirePrefixDescription: "식별자 문자를 하나 이상 입력하면 후보를 표시합니다.",
completionTriggerModePositional: "스마트 위치",
completionTriggerModePositionalDescription: "현재 SQL 구문 위치에 따라 후보를 자동으로 표시합니다.",
sqlSemanticDiagnosticsEnabled: "SQL 의미 진단",
sqlSemanticDiagnosticsEnabledDescription: "활성화하면 편집기가 알 수 없는 테이블과 컬럼 같은 의미 문제를 보고합니다. 대규모 SQL의 파싱과 메타데이터 검사를 줄이려면 비활성화하세요.",
confirmDangerousSqlExecution: "위험한 작업 전에 확인",
+12 -3
View File
@@ -312,6 +312,7 @@ export default withEnglishFallback({
sidebar: {
connections: "CONEXÕES",
noConnections: "Nenhuma conexão ainda",
importExport: "Importar / Exportar",
import: "Importar Conexões",
importDbx: "Importar Configuração DBX",
importNavicat: "Importar Navicat NCX",
@@ -382,6 +383,7 @@ export default withEnglishFallback({
},
sqlLibrary: {
title: "Biblioteca SQL",
storageHelp: "O conteúdo da Biblioteca SQL é salvo nos dados do aplicativo DBX, não como arquivos .sql independentes no disco.\nPara usá-lo no sistema de arquivos, exporte a Biblioteca SQL.",
unassociated: "Sem associação",
deletedConnection: "Conexão excluída",
openInCurrentDatabase: "Abrir no banco de dados atual",
@@ -5977,6 +5979,9 @@ export default withEnglishFallback({
dataGridDisplay: "Exibição da grade de dados",
dataGridFilterView: "Visualização de filtro da tabela",
dataGridFilterViewDescription: "Use um filtro pop-up, um painel de condições fixo ou um painel de texto compacto.",
dataGridFilterViewPreview: "Prévia do efeito",
dataGridFilterViewPreviewExpand: "Expandir prévia do efeito",
dataGridFilterViewPreviewCollapse: "Recolher prévia do efeito",
tableOpenPageSize: "Linhas padrão por página ao abrir tabelas",
tableOpenPageSizeDescription: "Linhas carregadas por página ao abrir uma nova tabela. Alterar as linhas por página atualiza esse padrão para futuras abas de tabelas.",
queryPageSize: "Linhas padrão por página de consulta",
@@ -6159,7 +6164,8 @@ export default withEnglishFallback({
resetAllDefaults: "Redefinir todos os padrões",
save: "Salvar",
cancel: "Cancelar",
executeMode: "Modo de execução ({shortcut})",
executeMode: "Modo de execução",
executeModeDescription: "Atalho Executar SQL: {shortcut}. Controla se todo o SQL ou a instrução no cursor é executada por padrão.",
executeModeAll: "Executar todo o SQL",
executeModeCurrent: "Executar instrução no cursor",
executeAllOnBlankLine: "Executar todo o SQL em linhas em branco",
@@ -6184,11 +6190,14 @@ export default withEnglishFallback({
sqlCompletionSection: "Preenchimento de SQL",
selectFirstCompletionOnOpen: "Selecionar automaticamente a primeira sugestão",
selectFirstCompletionOnOpenDescription: "Quando ativado, a primeira sugestão é selecionada ao abrir a lista e aceita com Enter ou Tab. Quando desativado, Enter insere uma nova linha e Tab aceita a primeira sugestão.",
completionTriggerMode: "Modo de acionamento do autocompletar",
completionTriggerModeDescription: "Controla quando o popup de autocompletar SQL abre automaticamente. Manual requer uma tecla de atalho; Prefixo digitado requer um identificador digitado; Inteligente mantém o comportamento atual.",
completionTriggerMode: "Modo de acionamento do autocompletar SQL",
completionTriggerModeDescription: "Escolha como as sugestões de autocompletar SQL são exibidas.",
completionTriggerModeManual: "Apenas manual",
completionTriggerModeManualDescription: "Mostra sugestões apenas ao usar o atalho de autocompletar {shortcut}.",
completionTriggerModeRequirePrefix: "Prefixo de identificador digitado",
completionTriggerModeRequirePrefixDescription: "Mostra sugestões após digitar pelo menos um caractere do identificador.",
completionTriggerModePositional: "Posição inteligente",
completionTriggerModePositionalDescription: "Mostra sugestões automaticamente conforme a posição atual da sintaxe SQL.",
sqlSemanticDiagnosticsEnabled: "Diagnóstico semântico de SQL",
sqlSemanticDiagnosticsEnabledDescription: "Quando ativado, o editor relata problemas semânticos como tabelas e colunas desconhecidas. Desative para reduzir a análise e verificações de metadados em SQL grande.",
confirmDangerousSqlExecution: "Confirmar antes de operações perigosas",
+15 -6
View File
@@ -235,6 +235,7 @@ export default withEnglishFallback({
sidebar: {
connections: "连接",
noConnections: "暂无连接",
importExport: "导入/导出",
import: "导入连接",
importDbx: "导入 DBX 配置",
importNavicat: "导入 Navicat NCX",
@@ -264,7 +265,7 @@ export default withEnglishFallback({
localTableSearchTooltip: "开启后搜索本地表索引,不发送远程请求;关闭后使用远程搜索。请使用刷新按钮构建或更新索引。",
refreshLocalTableSearchIndex: "刷新本地表索引",
globalLocalSearch: "本地搜索",
globalLocalSearchTooltip: "开启后仅搜索当前已加载的侧边栏对象,不发送远程请求。",
globalLocalSearchTooltip: "开启后仅搜索已加载的侧边栏对象,不发送远程请求",
regexSearch: "正则搜索",
regexSearchTooltip: "使用 JavaScript 正则表达式匹配侧边栏名称、备注和别名。",
clearFilter: "清除筛选",
@@ -305,6 +306,7 @@ export default withEnglishFallback({
},
sqlLibrary: {
title: "SQL 库",
storageHelp: "SQL 库内容保存在 DBX 应用数据中,并非磁盘上的独立 .sql 文件。\n如需在文件系统中使用,请导出 SQL 库。",
unassociated: "未关联",
deletedConnection: "连接已删除",
openInCurrentDatabase: "在当前库中打开",
@@ -6239,6 +6241,9 @@ export default withEnglishFallback({
dataGridDisplay: "数据表格显示",
dataGridFilterView: "表格筛选视图",
dataGridFilterViewDescription: "选择弹出式快捷筛选、固定条件面板或紧凑文本条件面板。",
dataGridFilterViewPreview: "效果预览",
dataGridFilterViewPreviewExpand: "展开效果预览",
dataGridFilterViewPreviewCollapse: "收起效果预览",
tableOpenPageSize: "打开表默认每页行数",
tableOpenPageSizeDescription: "新打开数据表时每页加载的行数。在表数据页修改每页行数后,也会更新后续新建表标签页的默认值。",
queryPageSize: "查询结果默认每页行数",
@@ -6454,7 +6459,8 @@ export default withEnglishFallback({
resetAllDefaults: "恢复所有默认设置",
save: "保存",
cancel: "取消",
executeMode: "执行模式 ({shortcut})",
executeMode: "执行模式",
executeModeDescription: "执行 SQL 快捷键:{shortcut}。用于控制默认执行全部 SQL 或光标所在语句。",
executeModeAll: "执行全部 SQL",
executeModeCurrent: "执行光标所在语句",
executeAllOnBlankLine: "空白位置执行全部 SQL",
@@ -6483,11 +6489,14 @@ export default withEnglishFallback({
sortCompletionColumnsAlphabeticallyDescription: "关闭后,表字段将保持数据库中的原始顺序",
selectFirstCompletionOnOpen: "自动选中首个补全候选",
selectFirstCompletionOnOpenDescription: "开启后,候选列表弹出时会选中第一项,可用 Enter 或 Tab 接受;关闭后,Enter 换行,Tab 接受第一项。",
completionTriggerMode: "自动补全触发方式",
completionTriggerModeDescription: "控制 SQL 补全弹窗何时自动打开。仅手动提示需快捷键;输入标识符后提示需输入至少一个字符;智能位置提示保持当前行为。",
completionTriggerMode: "SQL 补全触发方式",
completionTriggerModeDescription: "选择 SQL 补全候选的显示方式。",
completionTriggerModeManual: "仅手动提示",
completionTriggerModeManualDescription: "仅使用补全快捷键 {shortcut} 时显示候选。",
completionTriggerModeRequirePrefix: "输入标识符后提示",
completionTriggerModeRequirePrefixDescription: "输入至少一个标识符字符后显示候选。",
completionTriggerModePositional: "智能位置提示",
completionTriggerModePositionalDescription: "根据当前 SQL 语法位置自动显示候选。",
sqlSemanticDiagnosticsEnabled: "SQL 语义诊断",
sqlSemanticDiagnosticsEnabledDescription: "开启后,编辑器会提示未知表、字段等语义问题;关闭可减少 SQL 解析和元数据检查的性能开销。",
confirmDangerousSqlExecution: "执行危险操作前弹出确认",
@@ -6500,7 +6509,7 @@ export default withEnglishFallback({
appCloseUnsavedTabsModeDescription: "选择退出 DBX 时如何处理有未保存内容的 SQL 标签页。",
appCloseUnsavedTabsModePrompt: "退出前询问是否保存",
appCloseUnsavedTabsModeKeepDrafts: "直接退出并保留草稿",
appCloseUnsavedTabsModeHint: "草稿会以未保存标签页的形式保留,下次启动 DBX 时恢复;需要“启动时恢复标签页”不是“不保留标签”。",
appCloseUnsavedTabsModeHint: "保留的草稿将在下次启动时恢复,需允许启动时保留标签。",
appCloseDraftPersistFailed: "DBX 无法保存打开的标签页草稿,因此已取消退出。请检查磁盘空间和文件权限后重试。详情:{message}",
savedSqlOpenTarget: "打开已保存 SQL 时的默认目标",
savedSqlOpenTargetSaved: "保存时的目标",
@@ -6531,7 +6540,7 @@ export default withEnglishFallback({
sqlVariableSyntax_atSet: "内联变量展开",
sqlVariableSyntax_atSetDescription: "执行前展开内联变量声明并就地代入其值。",
autoAliasTables: "自动添加表别名",
autoAliasTablesDescription: "在 FROM 或 JOIN 中选择表名补全时,自动插入类似 order_items AS oi 的表别名。",
autoAliasTablesDescription: "补全 FROM 或 JOIN 表名时自动添加别名。",
redisKeyTemplatesSection: "Redis",
redisKeyTemplates: "Key 搜索模板",
redisKeyTemplatesPlaceholder: "例如:\nuser:{'{'}$userId{'}'}\nv3:adui:{'{'}$positionID{'}'}",
+12 -3
View File
@@ -312,6 +312,7 @@ export default withEnglishFallback({
sidebar: {
connections: "連線",
noConnections: "暫無連線",
importExport: "匯入/匯出",
import: "匯入連線",
importDbx: "匯入 DBX 設定",
importNavicat: "匯入 Navicat NCX",
@@ -382,6 +383,7 @@ export default withEnglishFallback({
},
sqlLibrary: {
title: "SQL 庫",
storageHelp: "SQL 庫內容儲存在 DBX 應用程式資料中,並非磁碟上的獨立 .sql 檔案。\n如需在檔案系統中使用,請匯出 SQL 庫。",
unassociated: "未關聯",
deletedConnection: "連線已刪除",
openInCurrentDatabase: "在目前資料庫中開啟",
@@ -5294,6 +5296,9 @@ export default withEnglishFallback({
dataGridDisplay: "資料表格顯示",
dataGridFilterView: "表格篩選檢視",
dataGridFilterViewDescription: "選擇彈出式快速篩選、固定條件面板或精簡文字條件面板。",
dataGridFilterViewPreview: "效果預覽",
dataGridFilterViewPreviewExpand: "展開效果預覽",
dataGridFilterViewPreviewCollapse: "收合效果預覽",
tableOpenPageSize: "開啟資料表預設每頁列數",
tableOpenPageSizeDescription: "新開啟資料表時每頁載入的列數。在資料表頁面調整每頁列數後,也會更新後續新建資料表分頁的預設值。",
queryPageSize: "查詢結果預設每頁列數",
@@ -5475,7 +5480,8 @@ export default withEnglishFallback({
resetAllDefaults: "還原所有預設設定",
save: "儲存",
cancel: "取消",
executeMode: "執行模式 ({shortcut})",
executeMode: "執行模式",
executeModeDescription: "執行 SQL 快捷鍵:{shortcut}。用於控制預設執行全部 SQL 或游標所在語句。",
executeModeAll: "執行全部 SQL",
executeModeCurrent: "執行指標所在語句",
executeAllOnBlankLine: "空白位置執行全部 SQL",
@@ -5500,11 +5506,14 @@ export default withEnglishFallback({
sqlCompletionSection: "SQL 補全",
selectFirstCompletionOnOpen: "自動選取第一個補全候選",
selectFirstCompletionOnOpenDescription: "開啟後,候選清單彈出時會選取第一項,可用 Enter 或 Tab 接受;關閉後,Enter 換行,Tab 接受第一項。",
completionTriggerMode: "自動補全觸發方式",
completionTriggerModeDescription: "控制 SQL 補全彈窗何時自動開啟。僅手動提示需快捷鍵;輸入識別碼後提示需輸入至少一個字元;智慧位置提示保持目前行為。",
completionTriggerMode: "SQL 補全觸發方式",
completionTriggerModeDescription: "選擇 SQL 補全候選的顯示方式。",
completionTriggerModeManual: "僅手動提示",
completionTriggerModeManualDescription: "僅使用補全快捷鍵 {shortcut} 時顯示候選。",
completionTriggerModeRequirePrefix: "輸入識別碼後提示",
completionTriggerModeRequirePrefixDescription: "輸入至少一個識別碼字元後顯示候選。",
completionTriggerModePositional: "智慧位置提示",
completionTriggerModePositionalDescription: "根據目前 SQL 語法位置自動顯示候選。",
sqlSemanticDiagnosticsEnabled: "SQL 語意診斷",
sqlSemanticDiagnosticsEnabledDescription: "開啟後,編輯器會提示未知資料表、欄位等語意問題;關閉可減少 SQL 解析和中繼資料檢查的效能負擔。",
confirmDangerousSqlExecution: "執行危險操作前彈出確認",
@@ -216,7 +216,7 @@ describe("settings search", () => {
{ titleKey: "settings.confirmDangerousSqlExecution", category: "editor", targetId: "editor" },
{ titleKey: "settings.continueOnErrorOnBatch", category: "editor", targetId: "editor" },
{ titleKey: "settings.dataGridQuickEntry", category: "data", targetId: "data" },
{ titleKey: "settings.dataGridFilterView", category: "data", targetId: "data" },
{ titleKey: "settings.dataGridFilterView", category: "data", targetId: "data-grid-filter-view" },
{ titleKey: "settings.colorizeDataGridCellTypes", category: "data", targetId: "data" },
{ titleKey: "transfer.dataTransfer", category: "appearance", targetId: "appearance" },
{ titleKey: "toolbar.driverManager", category: "appearance", targetId: "appearance" },
@@ -225,7 +225,8 @@ describe("settings search", () => {
{ titleKey: "settings.insertSpaceAfterCompletion", category: "editor", targetId: "editor" },
{ titleKey: "settings.completionTriggerMode", category: "editor", targetId: "editor" },
{ titleKey: "settings.autoAliasTables", category: "editor", targetId: "editor" },
{ titleKey: "settings.clickTableNavigationTarget", category: "editor", targetId: "editor" },
{ titleKey: "settings.clickTableNavigationTarget", category: "navigation", targetId: "navigation" },
{ titleKey: "settings.prefillNewQueryWithSelect", category: "navigation", targetId: "navigation" },
{ titleKey: "settings.generateSqlIncludeDatabaseName", category: "editor", targetId: "editor" },
{ titleKey: "settings.formatSqlOnSqlFileSave", category: "editor", targetId: "editor" },
{ titleKey: "settings.sqlFormatterKeywordCase", category: "formatter", targetId: "formatter" },
@@ -120,7 +120,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "editor-auto-alias", category: "editor", titleKey: "settings.autoAliasTables", descriptionKey: "settings.autoAliasTablesDescription", targetId: "editor" },
{ id: "editor-unsaved-close", category: "editor", titleKey: "settings.confirmUnsavedSqlClose", descriptionKey: "settings.confirmUnsavedSqlCloseDescription", targetId: "editor" },
{ id: "editor-app-close-unsaved-tabs", category: "editor", titleKey: "settings.appCloseUnsavedTabsMode", descriptionKey: "settings.appCloseUnsavedTabsModeDescription", targetId: "editor" },
{ id: "editor-prefill-query", category: "editor", titleKey: "settings.prefillNewQueryWithSelect", descriptionKey: "settings.prefillNewQueryWithSelectDescription", targetId: "editor" },
{ id: "editor-prefill-query", category: "navigation", titleKey: "settings.prefillNewQueryWithSelect", descriptionKey: "settings.prefillNewQueryWithSelectDescription", targetId: "navigation" },
{ id: "editor-generate-sql-include-database", category: "editor", titleKey: "settings.generateSqlIncludeDatabaseName", descriptionKey: "settings.generateSqlIncludeDatabaseNameDescription", targetId: "editor" },
{ id: "editor-format-sql-on-sql-file-save", category: "editor", titleKey: "settings.formatSqlOnSqlFileSave", descriptionKey: "settings.formatSqlOnSqlFileSaveDescription", targetId: "editor" },
{ id: "editor-diagnostics", category: "editor", titleKey: "settings.sqlSemanticDiagnosticsEnabled", descriptionKey: "settings.sqlSemanticDiagnosticsEnabledDescription", targetId: "editor" },
@@ -128,7 +128,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "editor-saved-sql-target", category: "editor", titleKey: "settings.savedSqlOpenTarget", targetId: "editor" },
{ id: "editor-confirm-dangerous-sql", category: "editor", titleKey: "settings.confirmDangerousSqlExecution", descriptionKey: "settings.confirmDangerousSqlExecutionDescription", targetId: "editor" },
{ id: "editor-continue-batch-on-error", category: "editor", titleKey: "settings.continueOnErrorOnBatch", descriptionKey: "settings.continueOnErrorOnBatchDescription", targetId: "editor" },
{ id: "editor-table-click-navigation", category: "editor", titleKey: "settings.clickTableNavigationTarget", descriptionKey: "settings.clickTableNavigationTargetDescription", targetId: "editor" },
{ id: "editor-table-click-navigation", category: "navigation", titleKey: "settings.clickTableNavigationTarget", descriptionKey: "settings.clickTableNavigationTargetDescription", targetId: "navigation" },
{ id: "formatter", category: "formatter", titleKey: "settings.sqlFormatterTab", targetId: "formatter" },
{ id: "formatter-shortcuts", category: "formatter", titleKey: "settings.sqlFormatterEditorShortcuts", targetId: "formatter" },
{ id: "formatter-keyword-case", category: "formatter", titleKey: "settings.sqlFormatterKeywordCase", targetId: "formatter" },
@@ -191,7 +191,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "redis-key-templates", category: "data", titleKey: "settings.redisKeyTemplates", descriptionKey: "settings.redisKeyTemplatesDescription", targetId: "redis-key-templates" },
{ id: "data-grid-auto-transpose", category: "data", titleKey: "settings.dataGridAutoTransposeSingleRow", descriptionKey: "settings.dataGridAutoTransposeSingleRowDescription", targetId: "data" },
{ id: "data-grid-quick-entry", category: "data", titleKey: "settings.dataGridQuickEntry", descriptionKey: "settings.dataGridQuickEntryDescription", targetId: "data" },
{ id: "data-grid-filter-view", category: "data", titleKey: "settings.dataGridFilterView", descriptionKey: "settings.dataGridFilterViewDescription", targetId: "data" },
{ id: "data-grid-filter-view", category: "data", titleKey: "settings.dataGridFilterView", descriptionKey: "settings.dataGridFilterViewDescription", targetId: "data-grid-filter-view" },
{ id: "data-grid-flattening-multi-line", category: "data", titleKey: "settings.flatteningMultiLineText", descriptionKey: "settings.flatteningMultiLineTextDescription", targetId: "data" },
{ id: "appearance-toolbar", category: "appearance", titleKey: "settings.toolbarTitle", descriptionKey: "settings.toolbarHiddenHint", targetId: "appearance" },
{ id: "appearance-exclusive-sidebar-panels", category: "appearance", titleKey: "settings.exclusiveRightSidebarPanels", descriptionKey: "settings.exclusiveRightSidebarPanelsDescription", targetId: "appearance" },
@@ -100,8 +100,8 @@ describe("legacy WebView CSS fallbacks", () => {
expect(globalsCss).toContain(".hover\\:bg-primary\\/15:hover");
const activeConnectionSources = `${connectionTreeSource}\n${activeConnectionFilterSource}`;
expect(activeConnectionSources).toContain("showActiveConnectionsOnly");
// Three pre-existing usages plus the sidebar regex-search toggle.
expect(activeConnectionSources.match(/bg-primary\/10 border-primary\/30/g)?.length).toBe(4);
expect(connectionTreeSource).toContain("text-primary bg-primary/10 border-primary/30");
expect(activeConnectionFilterSource).toContain("text-primary bg-primary/10 border-primary/30");
});
it("keeps legacy tab triggers connected to the configured corner style", () => {