feat(ui): streamline sidebar and settings layout
This commit is contained in:
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;
|
||||
}
|
||||
|
||||
|
||||
+10
-2
@@ -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"');
|
||||
}
|
||||
});
|
||||
});
|
||||
+136
-2
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user