feat(sidebar): add connection list sorting
This commit is contained in:
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)"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -96,6 +96,10 @@ export default withEnglishFallback({
|
||||
expand: "サイドバーを展開",
|
||||
showMore: "さらに{count}件表示...",
|
||||
filterByType: "タイプでフィルター",
|
||||
sortConnections: "接続を並べ替え",
|
||||
sortConnectionsManual: "手動順",
|
||||
sortConnectionsAscending: "名前: A–Z",
|
||||
sortConnectionsDescending: "名前: Z–A",
|
||||
searchScopeConnection: "接続",
|
||||
searchScopeDatabase: "データベース",
|
||||
searchScopeSchema: "スキーマ",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -96,6 +96,10 @@ export default withEnglishFallback({
|
||||
expand: "展开侧边栏",
|
||||
showMore: "加载更多 ({count})...",
|
||||
filterByType: "按类型筛选",
|
||||
sortConnections: "连接排序",
|
||||
sortConnectionsManual: "手动排序",
|
||||
sortConnectionsAscending: "名称:A–Z",
|
||||
sortConnectionsDescending: "名称:Z–A",
|
||||
searchScopeConnection: "连接",
|
||||
searchScopeDatabase: "数据库",
|
||||
searchScopeSchema: "Schema",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user