feat(sidebar): add connection list sorting

This commit is contained in:
zhangyc authored and GitHub committed 2026-07-21 16:57:35 +08:00
1 parent 54363b6edf
commit 4b2a6c971a
14 files changed
+313 -4

No files matched your search

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, computed, nextTick, watch, provide, onMounted, onUnmounted, type Component, type ComponentPublicInstance, type CSSProperties } from "vue";
import { useI18n } from "vue-i18n";
import { Search, X, ListFilter, Crosshair, Server, Database, FolderTree, Table2, Eye, RotateCcw } from "@lucide/vue";
import { Search, X, ListFilter, ListOrdered, ArrowDownAZ, ArrowUpZA, Crosshair, Server, Database, FolderTree, Table2, Eye, RotateCcw } from "@lucide/vue";
import { useConnectionStore } from "@/stores/connectionStore";
import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
@@ -37,6 +37,7 @@ import { createSidebarActionTarget, findSidebarActionTarget, type SidebarActionT
import type { SidebarDangerDialogRequest } from "@/lib/sidebar/sidebarDangerDialog";
import { resetSidebarTreeDialogState } from "./sidebarTreeDialogState";
import { SidebarDangerConfirmDialog, SidebarDdlViewDialog, SidebarObjectSourceDialog, SidebarProcedureExecutionDialog, SidebarVisibleDatabasesDialog, SidebarVisibleSchemasDialog } from "./sidebarAsyncDialogs";
import { sortConnectionListForDisplay } from "@/lib/sidebar/connectionListSort";
const { t } = useI18n();
const store = useConnectionStore();
@@ -247,6 +248,20 @@ const searchScopeMenuItems = computed(() => [
: []),
]);
const connectionListSortMenuItems = computed(() => [
{ value: "manual", label: t("sidebar.sortConnectionsManual"), icon: ListOrdered },
{ value: "asc", label: t("sidebar.sortConnectionsAscending"), icon: ArrowDownAZ },
{ value: "desc", label: t("sidebar.sortConnectionsDescending"), icon: ArrowUpZA },
]);
const isConnectionListAlphabeticallySorted = computed(() => settingsStore.editorSettings.sidebarConnectionSortMode !== "manual");
function updateConnectionListSortMode(mode: string) {
if (mode === "manual" || mode === "asc" || mode === "desc") {
settingsStore.updateEditorSettings({ sidebarConnectionSortMode: mode });
}
}
const hasSearchScopeFilter = computed(() => selectedSearchScopes.value.length > 0);
const searchableNodeTypes = computed<Set<TreeNodeType> | undefined>(() => {
if (!hasSearchScopeFilter.value) return undefined;
@@ -321,8 +336,10 @@ function focusTableSearchInput(parentNodeId: string) {
});
}
const displayedTreeNodes = computed(() => sortConnectionListForDisplay(store.treeNodes, settingsStore.editorSettings.sidebarConnectionSortMode));
const filteredNodes = computed(() => {
let nodes = store.treeNodes;
let nodes = displayedTreeNodes.value;
const q = deferredSearchQuery.value;
nodes = filterSidebarTree(nodes, q, searchCollapsedIds.value, searchableNodeTypes.value);
@@ -1333,6 +1350,23 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
<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" :title="t('sidebar.locateActiveTab')" @click="locateActiveTabInSidebar">
<Crosshair class="h-3.5 w-3.5" />
</button>
<LightDropdown
:model-value="settingsStore.editorSettings.sidebarConnectionSortMode"
:items="connectionListSortMenuItems"
:aria-label="t('sidebar.sortConnections')"
:label="t('sidebar.sortConnections')"
:trigger-title="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"
/>
<LightDropdown
v-if="searchScopeOptions.length > 0"
model-value=""
@@ -1376,7 +1410,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
<TreeItem
:node="item.node"
:depth="item.depth"
:drag-disabled="isFiltering"
:drag-disabled="isFiltering || isConnectionListAlphabeticallySorted"
:pending-rename="pendingRenameGroupId === item.node.id"
:highlighted="highlightedNodeId === item.node.id"
@context-menu="(event, node) => openSidebarContextMenu(event, node, contextMenuSlot.onContextMenu)"
@@ -1399,7 +1433,7 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
:key="item.id"
:node="item.node"
:depth="item.depth"
:drag-disabled="isFiltering"
:drag-disabled="isFiltering || isConnectionListAlphabeticallySorted"
:pending-rename="pendingRenameGroupId === item.node.id"
:highlighted="highlightedNodeId === item.id"
@context-menu="(event, node) => openSidebarContextMenu(event, node, contextMenuSlot.onContextMenu)"
+4
View File
@@ -94,6 +94,10 @@ export default {
expand: "Expand sidebar",
showMore: "Show {count} more...",
filterByType: "Filter by type",
sortConnections: "Sort connections",
sortConnectionsManual: "Manual order",
sortConnectionsAscending: "Name: A–Z",
sortConnectionsDescending: "Name: Z–A",
searchScopeConnection: "Connection",
searchScopeDatabase: "Database",
searchScopeSchema: "Schema",
+4
View File
@@ -96,6 +96,10 @@ export default withEnglishFallback({
expand: "Expandir barra lateral",
showMore: "Mostrar {count} más...",
filterByType: "Filtrar por tipo",
sortConnections: "Ordenar conexiones",
sortConnectionsManual: "Orden manual",
sortConnectionsAscending: "Nombre: A–Z",
sortConnectionsDescending: "Nombre: Z–A",
searchScopeConnection: "Conexión",
searchScopeDatabase: "Base de datos",
searchScopeSchema: "Schema",
+4
View File
@@ -95,6 +95,10 @@ export default withEnglishFallback({
expand: "Espandi barra laterale",
showMore: "Mostra altri {count}...",
filterByType: "Filtra per tipo",
sortConnections: "Ordina connessioni",
sortConnectionsManual: "Ordine manuale",
sortConnectionsAscending: "Nome: A–Z",
sortConnectionsDescending: "Nome: Z–A",
searchScopeConnection: "Connessione",
searchScopeDatabase: "Database",
searchScopeSchema: "Schema",
+4
View File
@@ -96,6 +96,10 @@ export default withEnglishFallback({
expand: "サイドバーを展開",
showMore: "さらに{count}件表示...",
filterByType: "タイプでフィルター",
sortConnections: "接続を並べ替え",
sortConnectionsManual: "手動順",
sortConnectionsAscending: "名前: A–Z",
sortConnectionsDescending: "名前: Z–A",
searchScopeConnection: "接続",
searchScopeDatabase: "データベース",
searchScopeSchema: "スキーマ",
+4
View File
@@ -96,6 +96,10 @@ export default withEnglishFallback({
expand: "Expandir barra lateral",
showMore: "Mostrar mais {count}...",
filterByType: "Filtrar por tipo",
sortConnections: "Ordenar conexões",
sortConnectionsManual: "Ordem manual",
sortConnectionsAscending: "Nome: A–Z",
sortConnectionsDescending: "Nome: Z–A",
searchScopeConnection: "Conexão",
searchScopeDatabase: "Banco de dados",
searchScopeSchema: "Schema",
+4
View File
@@ -96,6 +96,10 @@ export default withEnglishFallback({
expand: "展开侧边栏",
showMore: "加载更多 ({count})...",
filterByType: "按类型筛选",
sortConnections: "连接排序",
sortConnectionsManual: "手动排序",
sortConnectionsAscending: "名称:A–Z",
sortConnectionsDescending: "名称:Z–A",
searchScopeConnection: "连接",
searchScopeDatabase: "数据库",
searchScopeSchema: "Schema",
+4
View File
@@ -96,6 +96,10 @@ export default withEnglishFallback({
expand: "展開側邊欄",
showMore: "再顯示 {count} 個……",
filterByType: "依類型篩選",
sortConnections: "連線排序",
sortConnectionsManual: "手動排序",
sortConnectionsAscending: "名稱:A–Z",
sortConnectionsDescending: "名稱:Z–A",
searchScopeConnection: "連線",
searchScopeDatabase: "資料庫",
searchScopeSchema: "Schema",
@@ -0,0 +1,39 @@
import { describe, expect, it } from "vitest";
import { sortConnectionListForDisplay } from "@/lib/sidebar/connectionListSort";
import type { TreeNode } from "@/types/database";
function connection(id: string, label: string): TreeNode {
return { id, label, type: "connection", connectionId: id, children: [] };
}
function group(id: string, label: string, children: TreeNode[]): TreeNode {
return { id, label, type: "connection-group", isExpanded: true, children };
}
describe("connection list display sort", () => {
const manualTree = [connection("zebra", "zebra"), group("team", "Team databases", [connection("beta", "Beta"), connection("alpha", "alpha"), connection("alpha-two", "Alpha 2")]), connection("alpha-root", "Alpha"), connection("alpha-root-two", "alpha")];
it("sorts root and grouped connections in ascending name order while preserving group placement", () => {
const sorted = sortConnectionListForDisplay(manualTree, "asc");
expect(sorted.map((node) => node.id)).toEqual(["alpha-root", "team", "alpha-root-two", "zebra"]);
expect(sorted[1]?.children?.map((node) => node.id)).toEqual(["alpha", "alpha-two", "beta"]);
});
it("sorts root and grouped connections in descending name order", () => {
const sorted = sortConnectionListForDisplay(manualTree, "desc");
expect(sorted.map((node) => node.id)).toEqual(["zebra", "team", "alpha-root", "alpha-root-two"]);
expect(sorted[1]?.children?.map((node) => node.id)).toEqual(["beta", "alpha-two", "alpha"]);
});
it("keeps manual order and stored tree references untouched", () => {
const manual = sortConnectionListForDisplay(manualTree, "manual");
const sorted = sortConnectionListForDisplay(manualTree, "asc");
expect(manual).toBe(manualTree);
expect(manualTree.map((node) => node.id)).toEqual(["zebra", "team", "alpha-root", "alpha-root-two"]);
expect(manualTree[1]?.children?.map((node) => node.id)).toEqual(["beta", "alpha", "alpha-two"]);
expect(sorted[1]).not.toBe(manualTree[1]);
});
});
@@ -0,0 +1,40 @@
import type { TreeNode } from "@/types/database";
export type ConnectionListSortMode = "manual" | "asc" | "desc";
const connectionNameCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
function sortConnectionSiblingsForDisplay(nodes: readonly TreeNode[], mode: Exclude<ConnectionListSortMode, "manual">): TreeNode[] {
const sortedConnections = nodes
.map((node, index) => ({ node, index }))
.filter(({ node }) => node.type === "connection")
.sort((left, right) => {
const compared = connectionNameCollator.compare(left.node.label, right.node.label);
const directional = mode === "asc" ? compared : -compared;
return directional || left.index - right.index;
})
.map(({ node }) => node);
let nextConnectionIndex = 0;
const displayNodes = nodes.map((node) => {
if (node.type === "connection") {
return sortedConnections[nextConnectionIndex++]!;
}
if (node.type !== "connection-group" || !node.children) return node;
const children = sortConnectionSiblingsForDisplay(node.children, mode);
return children === node.children ? node : { ...node, children };
});
return displayNodes.every((node, index) => node === nodes[index]) ? (nodes as TreeNode[]) : displayNodes;
}
/**
* Produces a display-only ordering of saved connections. Group positions and
* the persisted manual layout remain untouched, so switching back to manual
* order restores drag-and-drop placement without a data migration.
*/
export function sortConnectionListForDisplay(nodes: readonly TreeNode[], mode: ConnectionListSortMode): TreeNode[] {
if (mode === "manual") return nodes as TreeNode[];
return sortConnectionSiblingsForDisplay(nodes, mode);
}
+26
View File
@@ -18,6 +18,13 @@ describe("normalizeEditorSettings", () => {
expect(normalizeEditorSettings({ autoAliasTables: false }).autoAliasTables).toBe(false);
});
it("defaults sidebar connection sorting to manual order and preserves valid alphabetical modes", () => {
expect(normalizeEditorSettings({}).sidebarConnectionSortMode).toBe("manual");
expect(normalizeEditorSettings({ sidebarConnectionSortMode: "asc" }).sidebarConnectionSortMode).toBe("asc");
expect(normalizeEditorSettings({ sidebarConnectionSortMode: "desc" }).sidebarConnectionSortMode).toBe("desc");
expect(normalizeEditorSettings({ sidebarConnectionSortMode: "invalid" as any }).sidebarConnectionSortMode).toBe("manual");
});
it("shows the current statement frame by default", () => {
expect(normalizeEditorSettings({}).showCurrentStatementFrame).toBe(true);
});
@@ -257,6 +264,25 @@ describe("settingsStore MCP policy persistence", () => {
});
});
describe("settingsStore sidebar connection sort persistence", () => {
beforeEach(() => {
vi.resetModules();
setActivePinia(createPinia());
});
it("persists the selected alphabetical sort mode", async () => {
const saveEditorSettings = vi.fn().mockResolvedValue(undefined);
vi.doMock("@/lib/backend/api", () => ({ saveEditorSettings }));
const { useSettingsStore } = await import("@/stores/settingsStore");
const store = useSettingsStore();
store.updateEditorSettings({ sidebarConnectionSortMode: "desc" });
expect(store.editorSettings.sidebarConnectionSortMode).toBe("desc");
expect(saveEditorSettings).toHaveBeenCalledWith(expect.objectContaining({ sidebarConnectionSortMode: "desc" }));
});
});
// --- activeModel lifecycle tests ---
describe("settingsStore activeModel lifecycle", () => {
+9
View File
@@ -6,6 +6,7 @@ import { normalizeColumnFormatter, normalizeCustomColumnFormatter, normalizeGlob
import { normalizeShortcutSettings, type ShortcutSettings } from "@/lib/editor/shortcutRegistry";
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
import { normalizeSidebarHiddenTablePrefixes } from "@/lib/sidebar/sidebarTableNameDisplay";
import type { ConnectionListSortMode } from "@/lib/sidebar/connectionListSort";
import { DEFAULT_SQL_FORMATTER_SETTINGS, normalizeSqlFormatterSettings, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig";
import { normalizeSqlVariableSyntaxOverrides, type SqlVariableSyntaxOverrides } from "@/lib/sql/sqlVariableSyntax";
import type { SidebarActivation } from "@/lib/sidebar/treeNodeClick";
@@ -426,6 +427,7 @@ export interface EditorSettings {
shortcuts: ShortcutSettings;
sqlFormatter: SqlFormatterSettings;
sidebarActivation: SidebarActivation;
sidebarConnectionSortMode: ConnectionListSortMode;
sidebarObjectDisplay: "grouped" | "simple";
sidebarTableSearchEnabled: boolean;
autoSelectActiveSidebarNode: boolean;
@@ -574,6 +576,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
shortcuts: normalizeShortcutSettings(),
sqlFormatter: normalizeSqlFormatterSettings(DEFAULT_SQL_FORMATTER_SETTINGS),
sidebarActivation: "single",
sidebarConnectionSortMode: "manual",
sidebarObjectDisplay: "grouped",
sidebarTableSearchEnabled: false,
autoSelectActiveSidebarNode: false,
@@ -691,6 +694,10 @@ function normalizeOpenTabsRestoreMode(value: unknown, legacyRestoreOpenTabsOnLau
return DEFAULT_EDITOR_SETTINGS.openTabsRestoreMode;
}
function normalizeConnectionListSortMode(value: unknown): ConnectionListSortMode {
return value === "asc" || value === "desc" ? value : "manual";
}
function normalizeColumnFormatters(value: unknown): Record<string, ColumnFormatterConfig> {
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
const formatters: Record<string, ColumnFormatterConfig> = {};
@@ -828,6 +835,7 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
shortcuts: normalizeShortcutSettings(settings.shortcuts),
sqlFormatter: normalizeSqlFormatterSettings(settings.sqlFormatter),
sidebarActivation: settings.sidebarActivation === "single" || settings.sidebarActivation === "double" ? settings.sidebarActivation : DEFAULT_EDITOR_SETTINGS.sidebarActivation,
sidebarConnectionSortMode: normalizeConnectionListSortMode(settings.sidebarConnectionSortMode),
sidebarObjectDisplay: settings.sidebarObjectDisplay === "simple" || settings.sidebarObjectDisplay === "grouped" ? settings.sidebarObjectDisplay : DEFAULT_EDITOR_SETTINGS.sidebarObjectDisplay,
sidebarTableSearchEnabled: typeof settings.sidebarTableSearchEnabled === "boolean" ? settings.sidebarTableSearchEnabled : DEFAULT_EDITOR_SETTINGS.sidebarTableSearchEnabled,
autoSelectActiveSidebarNode: settings.autoSelectActiveSidebarNode ?? DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode,
@@ -1186,6 +1194,7 @@ export const useSettingsStore = defineStore("settings", () => {
if (partial.shortcuts !== undefined) editorSettings.value.shortcuts = normalizeShortcutSettings(partial.shortcuts);
if (partial.sqlFormatter !== undefined) editorSettings.value.sqlFormatter = normalizeSqlFormatterSettings(partial.sqlFormatter);
if (partial.sidebarActivation !== undefined) editorSettings.value.sidebarActivation = partial.sidebarActivation;
if (partial.sidebarConnectionSortMode !== undefined) editorSettings.value.sidebarConnectionSortMode = normalizeConnectionListSortMode(partial.sidebarConnectionSortMode);
if (partial.sidebarObjectDisplay !== undefined) editorSettings.value.sidebarObjectDisplay = partial.sidebarObjectDisplay;
if (partial.sidebarTableSearchEnabled !== undefined) editorSettings.value.sidebarTableSearchEnabled = partial.sidebarTableSearchEnabled;
if (partial.autoSelectActiveSidebarNode !== undefined) editorSettings.value.autoSelectActiveSidebarNode = partial.autoSelectActiveSidebarNode;