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";');
|
||||
|
||||
Reference in new issue
Block a user