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

@@ -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";/);