From d124efbb20f4f6c4c1128bede925630b492968bd Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Fri, 15 May 2026 18:09:24 +0800 Subject: [PATCH] feat(settings): add sidebar activation preference --- .../editor/EditorSettingsDialog.vue | 73 +++++++++++++++++-- src/components/sidebar/TreeItem.vue | 20 ++++- src/i18n/locales/en.ts | 6 ++ src/i18n/locales/es.ts | 6 ++ src/i18n/locales/zh-CN.ts | 6 ++ src/lib/treeNodeClick.ts | 26 ++++++- src/stores/settingsStore.ts | 7 ++ tests/settingsDialogLayout.test.ts | 6 ++ tests/settingsStore.test.ts | 10 +++ tests/treeNodeClick.test.ts | 22 ++++++ 10 files changed, 171 insertions(+), 11 deletions(-) diff --git a/src/components/editor/EditorSettingsDialog.vue b/src/components/editor/EditorSettingsDialog.vue index 925180a9ce..20349bc9c2 100644 --- a/src/components/editor/EditorSettingsDialog.vue +++ b/src/components/editor/EditorSettingsDialog.vue @@ -53,6 +53,7 @@ const editWordWrap = ref(settingsStore.editorSettings.wordWrap); const editAppLayout = ref(settingsStore.editorSettings.appLayout); const editRedisScanPageSize = ref(settingsStore.editorSettings.redisScanPageSize); const editShortcuts = ref(normalizeShortcutSettings(settingsStore.editorSettings.shortcuts)); +const editSidebarActivation = ref(settingsStore.editorSettings.sidebarActivation); const redisScanPageSizeOptions = [200, 1000, 5000, 10000]; // Sync from store when dialog opens @@ -68,6 +69,7 @@ watch( editAppLayout.value = settingsStore.editorSettings.appLayout; editRedisScanPageSize.value = settingsStore.editorSettings.redisScanPageSize; editShortcuts.value = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts); + editSidebarActivation.value = settingsStore.editorSettings.sidebarActivation; } }, ); @@ -89,7 +91,8 @@ function hasChanges(): boolean { editWordWrap.value !== settingsStore.editorSettings.wordWrap || editAppLayout.value !== settingsStore.editorSettings.appLayout || editRedisScanPageSize.value !== settingsStore.editorSettings.redisScanPageSize || - JSON.stringify(editShortcuts.value) !== JSON.stringify(settingsStore.editorSettings.shortcuts) + JSON.stringify(editShortcuts.value) !== JSON.stringify(settingsStore.editorSettings.shortcuts) || + editSidebarActivation.value !== settingsStore.editorSettings.sidebarActivation ); } @@ -104,6 +107,7 @@ function applySettings() { appLayout: editAppLayout.value, redisScanPageSize: editRedisScanPageSize.value, shortcuts: editShortcuts.value, + sidebarActivation: editSidebarActivation.value, }); emit("update:open", false); } @@ -117,6 +121,7 @@ function resetDefaults() { editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout; editRedisScanPageSize.value = DEFAULT_EDITOR_SETTINGS.redisScanPageSize; editShortcuts.value = normalizeShortcutSettings(DEFAULT_EDITOR_SETTINGS.shortcuts); + editSidebarActivation.value = DEFAULT_EDITOR_SETTINGS.sidebarActivation; } function onExecuteModeChange(v: any) { @@ -161,13 +166,18 @@ function setAppLayout(value: "separated" | "classic") { editAppLayout.value = value; } +function setSidebarActivation(value: "single" | "double") { + editSidebarActivation.value = value; +} + const activeSettingsTab = ref("editor"); const isWeb = !isTauriRuntime(); const displayedAppVersion = computed(() => (props.appVersion ? `v${props.appVersion}` : "")); -type SettingsCategory = "editor" | "appearance" | "redis" | "shortcuts" | "ai" | "security" | "about"; +type SettingsCategory = "editor" | "appearance" | "navigation" | "redis" | "shortcuts" | "ai" | "security" | "about"; const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[]>(() => [ { value: "editor", label: t("settings.editorTab") }, { value: "appearance", label: t("settings.appearanceTab") }, + { value: "navigation", label: t("settings.navigationTab") }, { value: "redis", label: t("settings.redisTab") }, { value: "shortcuts", label: t("settings.shortcutsTab") }, { value: "ai", label: t("settings.aiTab") }, @@ -458,7 +468,7 @@ watch( -
+
@@ -592,8 +602,8 @@ watch(
+
+
+ +
+ + +
+
+ + + +
+ + + +
+
diff --git a/src/components/sidebar/TreeItem.vue b/src/components/sidebar/TreeItem.vue index c76a68e4c5..16a83df4ee 100644 --- a/src/components/sidebar/TreeItem.vue +++ b/src/components/sidebar/TreeItem.vue @@ -57,6 +57,7 @@ import { import { useConnectionStore } from "@/stores/connectionStore"; import { useQueryStore } from "@/stores/queryStore"; import { useSavedSqlStore } from "@/stores/savedSqlStore"; +import { useSettingsStore } from "@/stores/settingsStore"; import { useToast } from "@/composables/useToast"; import { useDatabaseOptions } from "@/composables/useDatabaseOptions"; import type { DatabaseType, TreeNode, TreeNodeType } from "@/types/database"; @@ -108,6 +109,7 @@ const isTruncated = computed(() => { const connectionStore = useConnectionStore(); const queryStore = useQueryStore(); const savedSqlStore = useSavedSqlStore(); +const settingsStore = useSettingsStore(); const { toast } = useToast(); const { getDatabaseOptions } = useDatabaseOptions(); const showVisibleDatabasesDialog = ref(false); @@ -322,7 +324,7 @@ function runRowClickAction() { void openObjectBrowser(); return; } - const action = treeNodeRowAction(node.type, canExpand.value); + const action = treeNodeRowAction(node.type, canExpand.value, settingsStore.editorSettings.sidebarActivation); if (action === "open-data") { openData(); } else if (node.type === "procedure" || node.type === "function") { @@ -336,14 +338,28 @@ function runRowClickAction() { function onClick(event: MouseEvent) { connectionStore.selectedTreeNodeId = props.node.id; + if (settingsStore.editorSettings.sidebarActivation === "double") return; if (event.detail > 1) return; runRowClickAction(); } function onDoubleClick() { - const action = treeNodeRowDoubleClickAction(props.node.type, canOpenObjectBrowser.value); + const action = treeNodeRowDoubleClickAction( + props.node.type, + canOpenObjectBrowser.value, + settingsStore.editorSettings.sidebarActivation, + canExpand.value, + ); if (action === "open-object-browser") { void openObjectBrowser(); + } else if (action === "open-data") { + openData(); + } else if (action === "open-source") { + void viewObjectSource(); + } else if (action === "open-saved-sql") { + openSavedSqlFile(); + } else if (action === "toggle") { + toggle(); } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 58c3ad4b59..c4a4f40406 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -1005,6 +1005,7 @@ export default { title: "Settings", editorTab: "Editor", appearanceTab: "Appearance", + navigationTab: "Navigation", redisTab: "Redis", shortcutsTab: "Shortcuts", aiTab: "AI", @@ -1021,6 +1022,11 @@ export default { appLayoutSeparatedDescription: "Clear panel separation with block-style tabs.", appLayoutClassic: "Classic compact", appLayoutClassicDescription: "Continuous tab bar with compact panel headers.", + sidebarActivation: "Sidebar activation", + sidebarActivationSingle: "Single click", + sidebarActivationSingleDescription: "Open actionable sidebar items with one click.", + sidebarActivationDouble: "Double click", + sidebarActivationDoubleDescription: "Single click selects rows; double click opens items.", apply: "Apply", reset: "Reset", resetDefaults: "Reset Defaults", diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index ba995821b9..bf99c04625 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -983,6 +983,7 @@ export default { title: "Configuración", editorTab: "Editor", appearanceTab: "Apariencia", + navigationTab: "Navegación", redisTab: "Redis", shortcutsTab: "Atajos", aiTab: "IA", @@ -999,6 +1000,11 @@ export default { appLayoutSeparatedDescription: "Separación clara de paneles con pestañas en bloque.", appLayoutClassic: "Clásico compacto", appLayoutClassicDescription: "Barra de pestañas continua con encabezados de panel compactos.", + sidebarActivation: "Activación de la barra lateral", + sidebarActivationSingle: "Un clic", + sidebarActivationSingleDescription: "Abrir elementos accionables de la barra lateral con un clic.", + sidebarActivationDouble: "Doble clic", + sidebarActivationDoubleDescription: "Un clic selecciona la fila; doble clic abre elementos.", apply: "Aplicar", reset: "Restablecer", resetDefaults: "Restablecer valores por defecto", diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 1e595ebc85..c32ad2eb34 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -987,6 +987,7 @@ export default { title: "设置", editorTab: "编辑器", appearanceTab: "外观", + navigationTab: "导航", redisTab: "Redis", shortcutsTab: "快捷键", aiTab: "AI", @@ -1003,6 +1004,11 @@ export default { appLayoutSeparatedDescription: "面板分隔更清晰,标签页为块状样式。", appLayoutClassic: "经典紧凑", appLayoutClassicDescription: "使用连续标签栏和更紧凑的面板标题。", + sidebarActivation: "侧边栏打开方式", + sidebarActivationSingle: "单击打开", + sidebarActivationSingleDescription: "单击即可打开侧边栏中的可操作项目。", + sidebarActivationDouble: "双击打开", + sidebarActivationDoubleDescription: "单击只选中高亮,双击打开项目。", apply: "应用", reset: "重置", resetDefaults: "恢复默认", diff --git a/src/lib/treeNodeClick.ts b/src/lib/treeNodeClick.ts index 55566e76e9..8efe968110 100644 --- a/src/lib/treeNodeClick.ts +++ b/src/lib/treeNodeClick.ts @@ -1,13 +1,26 @@ import type { TreeNodeType } from "@/types/database"; export type TreeNodeRowAction = "open-data" | "toggle" | "none"; -export type TreeNodeRowDoubleClickAction = "open-object-browser" | "none"; +export type TreeNodeRowDoubleClickAction = + | "open-data" + | "open-object-browser" + | "open-source" + | "open-saved-sql" + | "toggle" + | "none"; +export type SidebarActivation = "single" | "double"; const dataNodeTypes = new Set(["table", "view"]); const toggleLeafNodeTypes = new Set(["redis-db", "mongo-collection"]); const objectBrowserNodeTypes = new Set(["database", "schema", "object-browser"]); +const sourceNodeTypes = new Set(["procedure", "function"]); -export function treeNodeRowAction(type: TreeNodeType, canExpand: boolean): TreeNodeRowAction { +export function treeNodeRowAction( + type: TreeNodeType, + canExpand: boolean, + activation: SidebarActivation = "single", +): TreeNodeRowAction { + if (activation === "double") return "none"; if (dataNodeTypes.has(type)) return "open-data"; if (toggleLeafNodeTypes.has(type)) return "toggle"; if (canExpand) return "toggle"; @@ -17,7 +30,16 @@ export function treeNodeRowAction(type: TreeNodeType, canExpand: boolean): TreeN export function treeNodeRowDoubleClickAction( type: TreeNodeType, canOpenObjectBrowser: boolean, + activation: SidebarActivation = "single", + canExpand = false, ): TreeNodeRowDoubleClickAction { + if (activation === "double") { + if (dataNodeTypes.has(type)) return "open-data"; + if (sourceNodeTypes.has(type)) return "open-source"; + if (type === "saved-sql-file") return "open-saved-sql"; + if (toggleLeafNodeTypes.has(type)) return "toggle"; + if (canExpand) return "toggle"; + } if (canOpenObjectBrowser && objectBrowserNodeTypes.has(type)) return "open-object-browser"; return "none"; } diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts index 1513c744e4..1b066e23bf 100644 --- a/src/stores/settingsStore.ts +++ b/src/stores/settingsStore.ts @@ -2,6 +2,7 @@ import { defineStore } from "pinia"; import { ref } from "vue"; import * as api from "@/lib/api"; import { normalizeShortcutSettings, type ShortcutSettings } from "@/lib/shortcutRegistry"; +import type { SidebarActivation } from "@/lib/treeNodeClick"; export type AiProvider = "claude" | "openai" | "custom"; export type AiApiStyle = "completions" | "responses"; @@ -54,6 +55,7 @@ export interface EditorSettings { pageSize: number; redisScanPageSize: number; shortcuts: ShortcutSettings; + sidebarActivation: SidebarActivation; } export const EDITOR_THEMES: { value: EditorTheme; label: string; dark: boolean }[] = [ @@ -88,6 +90,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = { pageSize: 100, redisScanPageSize: 1000, shortcuts: normalizeShortcutSettings(), + sidebarActivation: "single", }; export const STORAGE_KEY = "dbx-editor-settings"; @@ -104,6 +107,10 @@ export function normalizeEditorSettings(settings: Partial): Edit pageSize: settings.pageSize ?? DEFAULT_EDITOR_SETTINGS.pageSize, redisScanPageSize: settings.redisScanPageSize ?? DEFAULT_EDITOR_SETTINGS.redisScanPageSize, shortcuts: normalizeShortcutSettings(settings.shortcuts), + sidebarActivation: + settings.sidebarActivation === "single" || settings.sidebarActivation === "double" + ? settings.sidebarActivation + : DEFAULT_EDITOR_SETTINGS.sidebarActivation, }; } diff --git a/tests/settingsDialogLayout.test.ts b/tests/settingsDialogLayout.test.ts index ee169e8b39..a3c62a9c82 100644 --- a/tests/settingsDialogLayout.test.ts +++ b/tests/settingsDialogLayout.test.ts @@ -37,3 +37,9 @@ test("shortcut settings capture custom keydown input instead of fixed select opt assert.match(source, /@keydown="\(event: KeyboardEvent\) => onShortcutKeydown/); assert.doesNotMatch(source, /definition\.options/); }); + +test("settings dialog exposes sidebar activation in navigation settings", () => { + assert.match(source, /value: "navigation"/); + assert.match(source, /activeSettingsTab === ['"]navigation['"]/); + assert.match(source, /settings\.sidebarActivation/); +}); diff --git a/tests/settingsStore.test.ts b/tests/settingsStore.test.ts index 4ad81595b5..a44866e18e 100644 --- a/tests/settingsStore.test.ts +++ b/tests/settingsStore.test.ts @@ -25,3 +25,13 @@ test("keeps saved shortcut overrides", () => { assert.equal(settings.shortcuts.executeSql, "Shift+Mod+Enter"); assert.equal(settings.shortcuts.saveSql, "Mod+S"); }); + +test("defaults sidebar activation to single click", () => { + assert.equal(DEFAULT_EDITOR_SETTINGS.sidebarActivation, "single"); + assert.equal(normalizeEditorSettings({}).sidebarActivation, "single"); +}); + +test("keeps saved sidebar activation", () => { + assert.equal(normalizeEditorSettings({ sidebarActivation: "double" } as any).sidebarActivation, "double"); + assert.equal(normalizeEditorSettings({ sidebarActivation: "invalid" } as any).sidebarActivation, "single"); +}); diff --git a/tests/treeNodeClick.test.ts b/tests/treeNodeClick.test.ts index 8b7d43c7b9..ca9624e997 100644 --- a/tests/treeNodeClick.test.ts +++ b/tests/treeNodeClick.test.ts @@ -7,6 +7,26 @@ test("table and view rows open data without toggling structure groups", () => { assert.equal(treeNodeRowAction("view", true), "open-data"); }); +test("double click navigation mode selects rows on single click", () => { + assert.equal(treeNodeRowAction("table", true, "double"), "none"); + assert.equal(treeNodeRowAction("view", true, "double"), "none"); + assert.equal(treeNodeRowAction("procedure", false, "double"), "none"); + assert.equal(treeNodeRowAction("saved-sql-file", false, "double"), "none"); +}); + +test("double click navigation mode opens actionable rows on double click", () => { + assert.equal(treeNodeRowDoubleClickAction("table", true, "double"), "open-data"); + assert.equal(treeNodeRowDoubleClickAction("view", true, "double"), "open-data"); + assert.equal(treeNodeRowDoubleClickAction("procedure", false, "double"), "open-source"); + assert.equal(treeNodeRowDoubleClickAction("saved-sql-file", false, "double"), "open-saved-sql"); +}); + +test("double click navigation mode toggles expandable rows on double click", () => { + assert.equal(treeNodeRowDoubleClickAction("connection", false, "double", true), "toggle"); + assert.equal(treeNodeRowDoubleClickAction("group-columns", false, "double", true), "toggle"); + assert.equal(treeNodeRowDoubleClickAction("redis-db", false, "double", false), "toggle"); +}); + test("expandable non-table rows still toggle from row clicks", () => { assert.equal(treeNodeRowAction("connection", true), "toggle"); assert.equal(treeNodeRowAction("database", true), "toggle"); @@ -29,6 +49,8 @@ test("database and schema rows open object browser only on double click", () => assert.equal(treeNodeRowAction("schema", true), "toggle"); assert.equal(treeNodeRowDoubleClickAction("database", true), "open-object-browser"); assert.equal(treeNodeRowDoubleClickAction("schema", true), "open-object-browser"); + assert.equal(treeNodeRowAction("database", true, "double"), "none"); + assert.equal(treeNodeRowAction("schema", true, "double"), "none"); }); test("double click does not open object browser for non-browsable rows", () => {