feat(ui): streamline sidebar and settings layout
This commit is contained in:
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>
|
||||
|
||||
Reference in new issue
Block a user