From 660ba072647ab69270982c37051ff2ab261f8ac0 Mon Sep 17 00:00:00 2001
From: t8y2 <1156263951@qq.com>
Date: Fri, 29 May 2026 01:17:46 +0800
Subject: [PATCH] feat(sidebar): add compact object display mode
---
.../editor/EditorSettingsDialog.vue | 88 +++++++++++++++++++
apps/desktop/src/i18n/locales/en.ts | 7 ++
apps/desktop/src/i18n/locales/es.ts | 7 ++
apps/desktop/src/i18n/locales/zh-CN.ts | 5 ++
apps/desktop/src/lib/sqlServerTree.ts | 25 ++++--
apps/desktop/src/lib/tableTree.ts | 44 ++++++++++
apps/desktop/src/stores/connectionStore.ts | 19 ++--
apps/desktop/src/stores/settingsStore.ts | 8 ++
.../queryEditorSnippetRefresh.test.ts | 2 +-
.../app-tests/startupChunkBoundaries.test.ts | 2 +-
10 files changed, 192 insertions(+), 15 deletions(-)
diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
index 7030398297..ff561eb822 100644
--- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue
+++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
@@ -98,6 +98,8 @@ const editCompactColumnHeaderActions = ref(settingsStore.editorSettings.compactC
const editRedisScanPageSize = ref(settingsStore.editorSettings.redisScanPageSize);
const editShortcuts = ref(normalizeShortcutSettings(settingsStore.editorSettings.shortcuts));
const editSidebarActivation = ref(settingsStore.editorSettings.sidebarActivation);
+const editSidebarObjectDisplay = ref(settingsStore.editorSettings.sidebarObjectDisplay);
+const sidebarObjectDisplayHelp = ref<"grouped" | "simple" | null>(null);
const editAutoSelectActiveSidebarNode = ref(settingsStore.editorSettings.autoSelectActiveSidebarNode);
const editSidebarHiddenTablePrefixes = ref(settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n"));
const editSidebarHideTableComments = ref(settingsStore.editorSettings.sidebarHideTableComments);
@@ -231,6 +233,7 @@ watch(
editRedisScanPageSize.value = settingsStore.editorSettings.redisScanPageSize;
editShortcuts.value = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
editSidebarActivation.value = settingsStore.editorSettings.sidebarActivation;
+ editSidebarObjectDisplay.value = settingsStore.editorSettings.sidebarObjectDisplay;
editAutoSelectActiveSidebarNode.value = settingsStore.editorSettings.autoSelectActiveSidebarNode;
editSidebarHiddenTablePrefixes.value = settingsStore.editorSettings.sidebarHiddenTablePrefixes.join("\n");
editSidebarHideTableComments.value = settingsStore.editorSettings.sidebarHideTableComments;
@@ -268,6 +271,7 @@ function hasChanges(): boolean {
editRedisScanPageSize.value !== settingsStore.editorSettings.redisScanPageSize ||
JSON.stringify(editShortcuts.value) !== JSON.stringify(settingsStore.editorSettings.shortcuts) ||
editSidebarActivation.value !== settingsStore.editorSettings.sidebarActivation ||
+ editSidebarObjectDisplay.value !== settingsStore.editorSettings.sidebarObjectDisplay ||
editAutoSelectActiveSidebarNode.value !== settingsStore.editorSettings.autoSelectActiveSidebarNode ||
editSidebarHideTableComments.value !== settingsStore.editorSettings.sidebarHideTableComments ||
JSON.stringify(normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value)) !==
@@ -278,6 +282,8 @@ function hasChanges(): boolean {
async function persistSettings() {
if (hasBlockingShortcutConflicts.value) return;
+ const sidebarObjectDisplayChanged =
+ editSidebarObjectDisplay.value !== settingsStore.editorSettings.sidebarObjectDisplay;
settingsStore.updateEditorSettings({
fontFamily: editFontFamily.value,
fontSize: editFontSize.value,
@@ -291,6 +297,7 @@ async function persistSettings() {
redisScanPageSize: editRedisScanPageSize.value,
shortcuts: editShortcuts.value,
sidebarActivation: editSidebarActivation.value,
+ sidebarObjectDisplay: editSidebarObjectDisplay.value,
autoSelectActiveSidebarNode: editAutoSelectActiveSidebarNode.value,
sidebarHideTableComments: editSidebarHideTableComments.value,
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(editSidebarHiddenTablePrefixes.value),
@@ -299,6 +306,9 @@ async function persistSettings() {
await settingsStore.updateDesktopSettings({
show_tray_icon: editShowTrayIcon.value,
});
+ if (sidebarObjectDisplayChanged) {
+ await connectionStore.refreshAllTree();
+ }
}
async function applySettings() {
@@ -324,6 +334,7 @@ function resetDefaults() {
editRedisScanPageSize.value = DEFAULT_EDITOR_SETTINGS.redisScanPageSize;
editShortcuts.value = normalizeShortcutSettings(DEFAULT_EDITOR_SETTINGS.shortcuts);
editSidebarActivation.value = DEFAULT_EDITOR_SETTINGS.sidebarActivation;
+ editSidebarObjectDisplay.value = DEFAULT_EDITOR_SETTINGS.sidebarObjectDisplay;
editAutoSelectActiveSidebarNode.value = DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode;
editSidebarHideTableComments.value = DEFAULT_EDITOR_SETTINGS.sidebarHideTableComments;
editSidebarHiddenTablePrefixes.value = DEFAULT_EDITOR_SETTINGS.sidebarHiddenTablePrefixes.join("\n");
@@ -347,6 +358,10 @@ function onRedisScanPageSizeChange(v: any) {
if (redisScanPageSizeOptions.includes(value)) editRedisScanPageSize.value = value;
}
+function setSidebarObjectDisplay(value: "grouped" | "simple") {
+ editSidebarObjectDisplay.value = value;
+}
+
function onShortcutChange(actionId: ShortcutActionId, value: any) {
if (typeof value !== "string") return;
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === actionId);
@@ -1223,6 +1238,79 @@ watch(
+
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 7e73e930e2..235150e9ca 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -1422,6 +1422,13 @@ export default {
sidebarActivationSingleDescription: "Open actionable sidebar items with one click.",
sidebarActivationDouble: "Double click",
sidebarActivationDoubleDescription: "Single click selects rows; double click opens items.",
+ sidebarObjectDisplay: "Sidebar object layout",
+ sidebarObjectDisplayGrouped: "Advanced view",
+ sidebarObjectDisplayGroupedDescription:
+ "Group objects by tables, views, procedures, and functions for databases with more object types.",
+ sidebarObjectDisplaySimple: "Simple view",
+ sidebarObjectDisplaySimpleDescription:
+ "List tables and views directly under each database or schema to reduce intermediate levels.",
autoSelectActiveSidebarNode: "Always select opened item",
autoSelectActiveSidebarNodeDescription:
"When switching tabs, select the matching visible table, collection, or SQL file in the sidebar.",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index 68e7e7336c..fe41a9cb2b 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -1316,6 +1316,13 @@ export default {
sidebarActivationSingleDescription: "Abrir elementos accionables de la barra lateral con un clic.",
sidebarActivationDouble: "Doble clic",
sidebarActivationDoubleDescription: "Un clic selecciona la fila; doble clic abre elementos.",
+ sidebarObjectDisplay: "Diseño de objetos en la barra lateral",
+ sidebarObjectDisplayGrouped: "Vista avanzada",
+ sidebarObjectDisplayGroupedDescription:
+ "Agrupa objetos por tablas, vistas, procedimientos y funciones para bases con más tipos de objeto.",
+ sidebarObjectDisplaySimple: "Vista simple",
+ sidebarObjectDisplaySimpleDescription:
+ "Muestra tablas y vistas directamente bajo cada base de datos o esquema para reducir niveles intermedios.",
autoSelectActiveSidebarNode: "Seleccionar siempre el elemento abierto",
autoSelectActiveSidebarNodeDescription:
"Al cambiar de pestaña, selecciona la tabla, colección o archivo SQL visible correspondiente en la barra lateral.",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 077b48cc06..6b89897d6f 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -1394,6 +1394,11 @@ export default {
sidebarActivationSingleDescription: "单击即可打开侧边栏中的可操作项目。",
sidebarActivationDouble: "双击打开",
sidebarActivationDoubleDescription: "单击只选中高亮,双击打开项目。",
+ sidebarObjectDisplay: "侧边栏对象层级",
+ sidebarObjectDisplayGrouped: "高级视图",
+ sidebarObjectDisplayGroupedDescription: "按表、视图、过程、函数分组展示,适合对象类型较多的库。",
+ sidebarObjectDisplaySimple: "简单视图",
+ sidebarObjectDisplaySimpleDescription: "数据库或 schema 下直接平铺表和视图,减少中间层级。",
autoSelectActiveSidebarNode: "始终选中已打开项目",
autoSelectActiveSidebarNodeDescription: "切换标签页时,在侧边栏选中匹配的可见表、集合或 SQL 文件。",
sidebarHiddenTablePrefixes: "隐藏表名前缀",
diff --git a/apps/desktop/src/lib/sqlServerTree.ts b/apps/desktop/src/lib/sqlServerTree.ts
index 9024ecf435..eb1b98cdc2 100644
--- a/apps/desktop/src/lib/sqlServerTree.ts
+++ b/apps/desktop/src/lib/sqlServerTree.ts
@@ -1,5 +1,5 @@
import type { ObjectInfo, TreeNode } from "@/types/database";
-import { buildGroupedObjectTreeNodes } from "@/lib/tableTree";
+import { buildGroupedObjectTreeNodes, buildSimpleObjectTreeNodes } from "@/lib/tableTree";
export const SQLSERVER_DEFAULT_SCHEMA = "dbo";
@@ -12,17 +12,26 @@ export function buildSqlServerDatabaseTreeNodes(
database: string,
schemas: string[],
defaultSchemaObjects: ObjectInfo[],
+ options: { simpleObjectDisplay?: boolean } = {},
): TreeNode[] {
const databaseNodeId = `${connectionId}:${database}`;
const defaultSchema = schemas.find(isDefaultSchema) || SQLSERVER_DEFAULT_SCHEMA;
- const defaultObjectNodes = buildGroupedObjectTreeNodes({
- nodeId: databaseNodeId,
- connectionId,
- database,
- schema: defaultSchema,
- objects: defaultSchemaObjects,
- });
+ const defaultObjectNodes = options.simpleObjectDisplay
+ ? buildSimpleObjectTreeNodes({
+ nodeId: databaseNodeId,
+ connectionId,
+ database,
+ schema: defaultSchema,
+ objects: defaultSchemaObjects,
+ })
+ : buildGroupedObjectTreeNodes({
+ nodeId: databaseNodeId,
+ connectionId,
+ database,
+ schema: defaultSchema,
+ objects: defaultSchemaObjects,
+ });
const schemaNodes = schemas
.filter((schema) => !isDefaultSchema(schema))
diff --git a/apps/desktop/src/lib/tableTree.ts b/apps/desktop/src/lib/tableTree.ts
index 7fef0a4d8b..7eadc677fa 100644
--- a/apps/desktop/src/lib/tableTree.ts
+++ b/apps/desktop/src/lib/tableTree.ts
@@ -36,6 +36,50 @@ export function buildTableTreeNodes({
});
}
+export function buildSimpleObjectTreeNodes({
+ nodeId,
+ connectionId,
+ database,
+ schema,
+ objects,
+}: {
+ nodeId: string;
+ connectionId: string;
+ database: string;
+ schema?: string;
+ objects: ObjectInfo[];
+}): TreeNode[] {
+ const seen = new Set();
+ const nodes: TreeNode[] = [];
+
+ for (const obj of objects) {
+ const objectType = normalizeObjectType(obj.object_type);
+ if (objectType !== "TABLE" && objectType !== "VIEW") continue;
+
+ const name = normalizeDatabaseObjectName(obj.name);
+ if (!name) continue;
+
+ const childSchema = obj.schema ? normalizeDatabaseObjectName(obj.schema) : schema;
+ const dedupeKey = `${objectType}\0${(childSchema || "").toLowerCase()}\0${name.toLowerCase()}`;
+ if (seen.has(dedupeKey)) continue;
+ seen.add(dedupeKey);
+
+ nodes.push({
+ id: `${nodeId}:${childSchema ? `${childSchema}:` : ""}${name}`,
+ label: name,
+ type: objectType === "VIEW" ? ("view" as const) : ("table" as const),
+ comment: obj.comment,
+ connectionId,
+ database,
+ schema: childSchema,
+ isExpanded: false,
+ children: [],
+ });
+ }
+
+ return nodes;
+}
+
function normalizeObjectType(type: string): "TABLE" | "VIEW" | "PROCEDURE" | "FUNCTION" {
const v = type.toUpperCase();
if (v.includes("VIEW")) return "VIEW";
diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts
index f2659f4f26..62780469a8 100644
--- a/apps/desktop/src/stores/connectionStore.ts
+++ b/apps/desktop/src/stores/connectionStore.ts
@@ -44,6 +44,7 @@ import {
import { decodeSchemaTreeCache, encodeSchemaTreeCache } from "@/lib/schemaTreeCache";
import { prunePinnedTreeNodeIdsForConnection } from "@/lib/pinnedTreeNodeIds";
import { useSavedSqlStore } from "@/stores/savedSqlStore";
+import { useSettingsStore } from "@/stores/settingsStore";
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
type ImportSource = "dbx" | "navicat" | "dbeaver";
@@ -966,7 +967,9 @@ export const useConnectionStore = defineStore("connection", () => {
api.listSchemas(connectionId, database),
api.listObjects(connectionId, database, SQLSERVER_DEFAULT_SCHEMA),
]);
- const children = buildSqlServerDatabaseTreeNodes(connectionId, database, schemas, defaultSchemaObjects);
+ const children = buildSqlServerDatabaseTreeNodes(connectionId, database, schemas, defaultSchemaObjects, {
+ simpleObjectDisplay: useSettingsStore().editorSettings.sidebarObjectDisplay === "simple",
+ });
setChildren(node, children);
await savePersistedTreeChildren(cacheKey, children);
node.isExpanded = true;
@@ -998,13 +1001,19 @@ export const useConnectionStore = defineStore("connection", () => {
const querySchema = schema || database;
const config = getConfig(connectionId);
const effectiveSchema = schema || (config?.db_type && isSchemaAware(config.db_type) ? database : undefined);
+ const simpleObjectDisplay = useSettingsStore().editorSettings.sidebarObjectDisplay === "simple";
let children: TreeNode[];
- try {
- const objects = await api.listObjects(connectionId, database, querySchema);
- children = buildGroupedObjectTreeNodes({ nodeId, connectionId, database, schema: effectiveSchema, objects });
- } catch {
+ if (simpleObjectDisplay) {
const tables = await api.listTables(connectionId, database, querySchema);
children = buildTableTreeNodes({ nodeId, connectionId, database, schema: effectiveSchema, tables });
+ } else {
+ try {
+ const objects = await api.listObjects(connectionId, database, querySchema);
+ children = buildGroupedObjectTreeNodes({ nodeId, connectionId, database, schema: effectiveSchema, objects });
+ } catch {
+ const tables = await api.listTables(connectionId, database, querySchema);
+ children = buildTableTreeNodes({ nodeId, connectionId, database, schema: effectiveSchema, tables });
+ }
}
setChildren(node, children);
await savePersistedTreeChildren(cacheKey, children);
diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts
index 89ec2288f0..9aa105527b 100644
--- a/apps/desktop/src/stores/settingsStore.ts
+++ b/apps/desktop/src/stores/settingsStore.ts
@@ -185,6 +185,7 @@ export interface EditorSettings {
compactColumnHeaderActions: boolean;
shortcuts: ShortcutSettings;
sidebarActivation: SidebarActivation;
+ sidebarObjectDisplay: "grouped" | "simple";
autoSelectActiveSidebarNode: boolean;
sidebarHiddenTablePrefixes: string[];
sidebarHideTableComments: boolean;
@@ -234,6 +235,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
compactColumnHeaderActions: true,
shortcuts: normalizeShortcutSettings(),
sidebarActivation: "single",
+ sidebarObjectDisplay: "grouped",
autoSelectActiveSidebarNode: false,
sidebarHiddenTablePrefixes: [],
sidebarHideTableComments: false,
@@ -320,6 +322,10 @@ export function normalizeEditorSettings(settings: Partial, exist
settings.sidebarActivation === "single" || settings.sidebarActivation === "double"
? settings.sidebarActivation
: DEFAULT_EDITOR_SETTINGS.sidebarActivation,
+ sidebarObjectDisplay:
+ settings.sidebarObjectDisplay === "simple" || settings.sidebarObjectDisplay === "grouped"
+ ? settings.sidebarObjectDisplay
+ : DEFAULT_EDITOR_SETTINGS.sidebarObjectDisplay,
autoSelectActiveSidebarNode:
settings.autoSelectActiveSidebarNode ?? DEFAULT_EDITOR_SETTINGS.autoSelectActiveSidebarNode,
sidebarHiddenTablePrefixes: normalizeSidebarHiddenTablePrefixes(settings.sidebarHiddenTablePrefixes),
@@ -443,6 +449,8 @@ export const useSettingsStore = defineStore("settings", () => {
editorSettings.value.compactColumnHeaderActions = partial.compactColumnHeaderActions;
if (partial.shortcuts !== undefined) editorSettings.value.shortcuts = normalizeShortcutSettings(partial.shortcuts);
if (partial.sidebarActivation !== undefined) editorSettings.value.sidebarActivation = partial.sidebarActivation;
+ if (partial.sidebarObjectDisplay !== undefined)
+ editorSettings.value.sidebarObjectDisplay = partial.sidebarObjectDisplay;
if (partial.autoSelectActiveSidebarNode !== undefined)
editorSettings.value.autoSelectActiveSidebarNode = partial.autoSelectActiveSidebarNode;
if (partial.sidebarHiddenTablePrefixes !== undefined)
diff --git a/packages/app-tests/queryEditorSnippetRefresh.test.ts b/packages/app-tests/queryEditorSnippetRefresh.test.ts
index 9cdcca9909..e9b73f8c9c 100644
--- a/packages/app-tests/queryEditorSnippetRefresh.test.ts
+++ b/packages/app-tests/queryEditorSnippetRefresh.test.ts
@@ -18,5 +18,5 @@ test("query editor reconfigures autocompletion when snippets change", () => {
});
test("query editor disables CodeMirror label re-filtering for custom SQL completions", () => {
- assert.match(source, /from: position - completionContext\.prefix\.length,\s*filter: false,\s*options:/s);
+ assert.match(source, /from: position - prefixLength,\s*filter: false,\s*options:/s);
});
diff --git a/packages/app-tests/startupChunkBoundaries.test.ts b/packages/app-tests/startupChunkBoundaries.test.ts
index 91e19eee57..31e5c3987d 100644
--- a/packages/app-tests/startupChunkBoundaries.test.ts
+++ b/packages/app-tests/startupChunkBoundaries.test.ts
@@ -17,7 +17,7 @@ test("app defers cold-start side panels and modal pages behind async components"
});
test("AI assistant panel stays closed on first launch to preserve startup memory", () => {
- assert.match(appSource, /const showAiPanel = ref\(localStorage\.getItem\("dbx-ai-panel-open"\) === "true"\)/);
+ assert.match(appSource, /const showAiPanel = ref\(safeLocalStorageGet\("dbx-ai-panel-open"\) === "true"\)/);
});
test("dock side panels layer above the editor content", () => {