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"