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", () => {