feat(ui): streamline sidebar and settings layout

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

No files matched your search

@@ -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>