feat(desktop): add zen mode toggle for data tabs
This commit is contained in:
16 files changed
+105
-9
No files matched your search
@@ -88,6 +88,7 @@ import {
|
||||
isSwitchToPreviousTabShortcut,
|
||||
isToggleResultsPaneShortcut,
|
||||
isToggleSidebarShortcut,
|
||||
isToggleZenModeShortcut,
|
||||
isZoomInShortcut,
|
||||
isZoomOutShortcut,
|
||||
switchToTabIndexFromShortcut,
|
||||
@@ -254,6 +255,7 @@ const agentDriverUpdateCount = ref(0);
|
||||
const showHistory = ref(false);
|
||||
const showAiPanel = ref(safeLocalStorageGet("dbx-ai-panel-open") === "true");
|
||||
const isAiPanelMaximized = ref(false);
|
||||
const isZenMode = ref(false);
|
||||
const showSqlLibraryPanel = ref(safeLocalStorageGet("dbx-sql-library-open") === "true");
|
||||
const showSqlFilePanel = ref(safeLocalStorageGet("dbx-sql-file-panel-open") === "true");
|
||||
const rightSidebarPanelRefs: Record<RightSidebarPanelId, typeof showAiPanel> = {
|
||||
@@ -323,6 +325,18 @@ const activeTab = computed(() => queryStore.tabs.find((t) => t.id === queryStore
|
||||
const tabNavigationHistory = ref(createTabNavigationHistory());
|
||||
let pendingTabHistoryNavigationId: string | null = null;
|
||||
|
||||
watch(
|
||||
() => activeTab.value?.mode,
|
||||
(mode) => {
|
||||
if (mode !== "data") isZenMode.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
function toggleZenMode() {
|
||||
if (activeTab.value?.mode !== "data") return;
|
||||
isZenMode.value = !isZenMode.value;
|
||||
}
|
||||
|
||||
const externalSqlFileChanges = useExternalSqlFileChanges({
|
||||
activeTab,
|
||||
recreateFile: async (tab) => {
|
||||
@@ -2610,6 +2624,12 @@ async function handleKeydown(e: KeyboardEvent) {
|
||||
setSidebarOpen(!sidebarOpen.value);
|
||||
return;
|
||||
}
|
||||
if (isToggleZenModeShortcut(e, shortcuts) && activeTab.value?.mode === "data") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleZenMode();
|
||||
return;
|
||||
}
|
||||
if (switchTabIndex != null) {
|
||||
if (activateTabByIndex(switchTabIndex)) {
|
||||
e.preventDefault();
|
||||
@@ -2977,7 +2997,7 @@ onUnmounted(() => {
|
||||
|
||||
<div :class="isClassicLayout ? 'app-layout-classic flex-1 flex min-h-0' : 'app-panel-gutter flex-1 flex min-h-0 gap-1 p-1'">
|
||||
<AppSidebar
|
||||
v-show="sidebarOpen"
|
||||
v-show="sidebarOpen && !isZenMode"
|
||||
ref="appSidebarRef"
|
||||
:sidebar-width="sidebarWidth"
|
||||
:classic-layout="isClassicLayout"
|
||||
@@ -2988,13 +3008,13 @@ onUnmounted(() => {
|
||||
@open-settings="(initialTab) => openSettings(initialTab ?? 'appearance')"
|
||||
@add-to-ai="addToAi"
|
||||
/>
|
||||
<div v-show="!sidebarOpen" class="flex h-full w-8 shrink-0 items-start justify-center border-r bg-background/80 pt-2" :class="isClassicLayout ? '' : 'rounded-md border border-border/80'">
|
||||
<div v-show="!sidebarOpen && !isZenMode" class="flex h-full w-8 shrink-0 items-start justify-center border-r bg-background/80 pt-2" :class="isClassicLayout ? '' : 'rounded-md border border-border/80'">
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" :title="t('sidebar.expand')" :aria-label="t('sidebar.expand')" @click="setSidebarOpen(true)">
|
||||
<ChevronsRight class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-show="!isAiPanelMaximized" :class="isClassicLayout ? 'flex-1 min-w-0 overflow-hidden' : 'flex-1 min-w-0 overflow-hidden rounded-md border border-border/80 bg-background'">
|
||||
<div v-show="!isAiPanelMaximized || isZenMode" :class="isClassicLayout ? 'flex-1 min-w-0 overflow-hidden' : 'flex-1 min-w-0 overflow-hidden rounded-md border border-border/80 bg-background'">
|
||||
<div class="h-full flex flex-col min-w-0">
|
||||
<AppTabBar
|
||||
ref="appTabBarRef"
|
||||
@@ -3003,6 +3023,7 @@ onUnmounted(() => {
|
||||
:settings-page-open="settingsPageTabOpen"
|
||||
:settings-page-active="settingsStore.settingsPageActive"
|
||||
:agent-driver-update-count="toolbarAgentDriverUpdateCount"
|
||||
@toggle-zen-mode="toggleZenMode"
|
||||
@activate-driver-store="openDriverStorePage"
|
||||
@activate-settings-page="activateSettingsPage"
|
||||
@locate-tab="locateTabInSidebar"
|
||||
@@ -3168,6 +3189,7 @@ onUnmounted(() => {
|
||||
|
||||
<div
|
||||
v-if="showAiPanel"
|
||||
v-show="!isZenMode"
|
||||
:class="[isClassicLayout ? 'h-full relative z-30 isolate bg-background' : 'h-full relative z-30 isolate rounded-md border border-border/80 bg-background', isAiPanelMaximized ? 'min-w-0 flex-1' : 'min-w-[180px] max-w-full']"
|
||||
:style="isAiPanelMaximized ? {} : { width: aiPanelWidth + 'px' }"
|
||||
>
|
||||
@@ -3192,21 +3214,21 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showHistory" v-show="!isAiPanelMaximized" :class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate rounded-md border border-border/80 bg-background'" :style="{ width: historyWidth + 'px' }">
|
||||
<div v-if="showHistory" v-show="!isAiPanelMaximized && !isZenMode" :class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate rounded-md border border-border/80 bg-background'" :style="{ width: historyWidth + 'px' }">
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @mousedown="startHistoryResize" />
|
||||
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]">
|
||||
<QueryHistory :current-connection-id="activeTab?.connectionId" :current-database="activeTab?.database" @restore="restoreHistorySql" @analyze-ai="analyzeHistoryWithAi" @close="closeRightSidebarPanel('history')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showSqlLibraryPanel" v-show="!isAiPanelMaximized" :class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate rounded-md border border-border/80 bg-background'" :style="{ width: sqlLibraryWidth + 'px' }">
|
||||
<div v-if="showSqlLibraryPanel" v-show="!isAiPanelMaximized && !isZenMode" :class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate rounded-md border border-border/80 bg-background'" :style="{ width: sqlLibraryWidth + 'px' }">
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @mousedown="startSqlLibraryResize" />
|
||||
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]">
|
||||
<SqlLibraryPanel @close="closeRightSidebarPanel('sqlLibrary')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showSqlFilePanel" v-show="!isAiPanelMaximized" :class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate rounded-md border border-border/80 bg-background'" :style="{ width: sqlFilePanelWidth + 'px' }">
|
||||
<div v-if="showSqlFilePanel" v-show="!isAiPanelMaximized && !isZenMode" :class="isClassicLayout ? 'h-full shrink-0 relative z-30 isolate bg-background' : 'h-full shrink-0 relative z-30 isolate rounded-md border border-border/80 bg-background'" :style="{ width: sqlFilePanelWidth + 'px' }">
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @mousedown="startSqlFilePanelResize" />
|
||||
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]">
|
||||
<SqlFilePanel @close="closeRightSidebarPanel('sqlFile')" />
|
||||
|
||||
@@ -34,6 +34,7 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
"activate-tab": [];
|
||||
"toggle-zen-mode": [];
|
||||
"locate-tab": [tab: QueryTab];
|
||||
"activate-driver-store": [];
|
||||
"close-driver-store": [];
|
||||
@@ -577,6 +578,17 @@ function handleTabClick(tab: QueryTab) {
|
||||
activateTab(tab.id);
|
||||
}
|
||||
|
||||
function handleTabDoubleClick(tab: QueryTab, event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
if (tabDrag.state.suppressClick || (event.target instanceof Element && event.target.closest("button, input, [role='button']"))) return;
|
||||
if (tab.mode === "data") {
|
||||
if (tab.id !== queryStore.activeTabId) activateTab(tab.id);
|
||||
emit("toggle-zen-mode");
|
||||
return;
|
||||
}
|
||||
startRenameTab(tab);
|
||||
}
|
||||
|
||||
function handleTabMouseDown(event: PointerEvent, tabId: string) {
|
||||
if (event.button === 0) {
|
||||
dispatchBeforeTabSwitch(tabId);
|
||||
@@ -697,7 +709,7 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
||||
:style="[tabColorStyle(tab), tabDropStyle(tab.id)]"
|
||||
:data-active-tab="tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive"
|
||||
@click="handleTabClick(tab)"
|
||||
@dblclick.stop="startRenameTab(tab)"
|
||||
@dblclick="handleTabDoubleClick(tab, $event)"
|
||||
@mousedown.middle.prevent="queryStore.closeTab(tab.id)"
|
||||
@pointerdown="handleTabMouseDown($event, tab.id)"
|
||||
@mouseenter="handleTabDragTarget($event, tab)"
|
||||
@@ -902,7 +914,7 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
||||
:style="[tabColorStyle(tab), tabDropStyle(tab.id)]"
|
||||
:data-active-tab="tab.id === queryStore.activeTabId && !driverStoreActive && !settingsPageActive"
|
||||
@click="handleTabClick(tab)"
|
||||
@dblclick.stop="startRenameTab(tab)"
|
||||
@dblclick="handleTabDoubleClick(tab, $event)"
|
||||
@mousedown.middle.prevent="queryStore.closeTab(tab.id)"
|
||||
@pointerdown="handleTabMouseDown($event, tab.id)"
|
||||
@mouseenter="handleTabDragTarget($event, tab)"
|
||||
|
||||
@@ -66,6 +66,19 @@ describe("AppTabBar locate-in-sidebar action", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("AppTabBar Zen mode interaction", () => {
|
||||
it("switches Zen mode for data tabs while preserving query-tab renaming", () => {
|
||||
const handler = sourceBetween("function handleTabDoubleClick", "function handleTabMouseDown");
|
||||
|
||||
expect(tabBarSource).toContain('"toggle-zen-mode": [];');
|
||||
expect(handler).toContain('if (tab.mode === "data") {');
|
||||
expect(handler).toContain('emit("toggle-zen-mode");');
|
||||
expect(handler).toContain("startRenameTab(tab);");
|
||||
expect(handler).toContain("event.target instanceof Element && event.target.closest(\"button, input, [role='button']\")");
|
||||
expect(tabBarSource.match(/@dblclick="handleTabDoubleClick\(tab, \$event\)"/g)).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AppTabBar right-side close action", () => {
|
||||
it("places the action after close-other and disables it when the target has no tabs to its right", () => {
|
||||
expect(tabBarSource).toContain('label: t("contextMenu.closeRightTabs")');
|
||||
|
||||
@@ -6674,6 +6674,7 @@ export default {
|
||||
shortcutToggleTranspose: "Toggle transpose view",
|
||||
shortcutCancelSearch: "Cancel search",
|
||||
shortcutToggleSidebar: "Toggle sidebar",
|
||||
shortcutToggleZenMode: "Toggle Zen mode",
|
||||
shortcutCopySidebarSelection: "Copy sidebar selection",
|
||||
shortcutPasteSidebarSelection: "Paste into sidebar",
|
||||
shortcutEditSidebarConnection: "Edit sidebar connection",
|
||||
|
||||
@@ -6304,6 +6304,7 @@ export default withEnglishFallback({
|
||||
shortcutOpenSettings: "Abrir configuración",
|
||||
shortcutCloseTab: "Cerrar pestaña",
|
||||
shortcutToggleSidebar: "Alternar barra lateral",
|
||||
shortcutToggleZenMode: "Alternar modo Zen",
|
||||
shortcutFocusSearch: "Enfocar búsqueda de la vista actual",
|
||||
shortcutQuickOpen: "Abrir rápido (buscar todos los objetos de base de datos)",
|
||||
shortcutNavigateTabHistoryBack: "Volver a la pestaña vista anteriormente",
|
||||
|
||||
@@ -6327,6 +6327,7 @@ export default withEnglishFallback({
|
||||
shortcutToggleTranspose: "Attiva/disattiva vista trasposta",
|
||||
shortcutCancelSearch: "Annulla ricerca",
|
||||
shortcutToggleSidebar: "Attiva/disattiva barra laterale",
|
||||
shortcutToggleZenMode: "Attiva/disattiva modalità Zen",
|
||||
shortcutCopySidebarSelection: "Copia selezione barra laterale",
|
||||
shortcutPasteSidebarSelection: "Incolla nella barra laterale",
|
||||
shortcutEditSidebarConnection: "Modifica connessione barra laterale",
|
||||
|
||||
@@ -6335,6 +6335,7 @@ export default withEnglishFallback({
|
||||
shortcutToggleTranspose: "転置ビューを切替",
|
||||
shortcutCancelSearch: "検索をキャンセル",
|
||||
shortcutToggleSidebar: "サイドバーを切替",
|
||||
shortcutToggleZenMode: "Zenモードを切替",
|
||||
shortcutCopySidebarSelection: "サイドバーの選択をコピー",
|
||||
shortcutPasteSidebarSelection: "サイドバーに貼り付け",
|
||||
shortcutEditSidebarConnection: "サイドバー接続を編集",
|
||||
|
||||
@@ -6050,6 +6050,7 @@ export default withEnglishFallback({
|
||||
shortcutToggleTranspose: "전치 보기 전환",
|
||||
shortcutCancelSearch: "검색 취소",
|
||||
shortcutToggleSidebar: "사이드바 전환",
|
||||
shortcutToggleZenMode: "Zen 모드 전환",
|
||||
shortcutCopySidebarSelection: "사이드바 선택 복사",
|
||||
shortcutPasteSidebarSelection: "사이드바에 붙여넣기",
|
||||
shortcutEditSidebarConnection: "사이드바 연결 편집",
|
||||
|
||||
@@ -6306,6 +6306,7 @@ export default withEnglishFallback({
|
||||
shortcutOpenSettings: "Abrir configurações",
|
||||
shortcutCloseTab: "Fechar aba",
|
||||
shortcutToggleSidebar: "Alternar barra lateral",
|
||||
shortcutToggleZenMode: "Alternar modo Zen",
|
||||
shortcutFocusSearch: "Focar na pesquisa da tela atual",
|
||||
shortcutQuickOpen: "Abrir rapidamente (pesquisar todos os objetos do banco de dados)",
|
||||
shortcutNavigateTabHistoryBack: "Voltar para a aba visualizada anteriormente",
|
||||
|
||||
@@ -6637,6 +6637,7 @@ export default withEnglishFallback({
|
||||
shortcutOpenSettings: "打开设置",
|
||||
shortcutCloseTab: "关闭标签页",
|
||||
shortcutToggleSidebar: "切换侧边栏",
|
||||
shortcutToggleZenMode: "切换禅模式",
|
||||
shortcutFocusSearch: "聚焦当前页面搜索",
|
||||
shortcutQuickOpen: "快速打开 (搜索所有数据库对象)",
|
||||
shortcutNavigateTabHistoryBack: "返回到上一个查看的标签页",
|
||||
|
||||
@@ -5611,6 +5611,7 @@ export default withEnglishFallback({
|
||||
shortcutOpenSettings: "開啟設定",
|
||||
shortcutCloseTab: "關閉分頁",
|
||||
shortcutToggleSidebar: "切換側邊欄",
|
||||
shortcutToggleZenMode: "切換禪模式",
|
||||
shortcutFocusSearch: "聚焦目前頁面搜尋",
|
||||
shortcutQuickOpen: "快速開啟 (搜尋所有資料庫物件)",
|
||||
shortcutNavigateTabHistoryBack: "返回到上一個檢視的分頁",
|
||||
|
||||
@@ -55,7 +55,20 @@ describe("right sidebar panel entry points", () => {
|
||||
expect(appSource).toContain(':maximized="isAiPanelMaximized"');
|
||||
expect(appSource).toContain('@toggle-maximize="toggleAiPanelMaximized"');
|
||||
expect(appSource).toContain("isAiPanelMaximized ? 'min-w-0 flex-1' : 'min-w-[180px] max-w-full'");
|
||||
expect(appSource).toContain('v-show="!isAiPanelMaximized"');
|
||||
expect(appSource).toContain('v-show="!isAiPanelMaximized && !isZenMode"');
|
||||
expect(functionSource("setRightSidebarPanelOpen", "toggleRightSidebarPanel")).toContain("isAiPanelMaximized.value = false;");
|
||||
});
|
||||
|
||||
it("keeps Zen mode as a temporary data-tab layout", () => {
|
||||
expect(appSource).toContain("const isZenMode = ref(false);");
|
||||
expect(appSource).toContain('if (mode !== "data") isZenMode.value = false;');
|
||||
expect(appSource).toContain("function toggleZenMode() {");
|
||||
expect(appSource).toContain('if (activeTab.value?.mode !== "data") return;');
|
||||
expect(appSource).toContain('isToggleZenModeShortcut(e, shortcuts) && activeTab.value?.mode === "data"');
|
||||
expect(appSource).toContain('@toggle-zen-mode="toggleZenMode"');
|
||||
expect(appSource).toContain('v-show="sidebarOpen && !isZenMode"');
|
||||
expect(appSource).toContain('v-show="!sidebarOpen && !isZenMode"');
|
||||
expect(appSource).toContain('v-show="!isAiPanelMaximized || isZenMode"');
|
||||
expect(appSource).toContain('v-show="!isZenMode"');
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
isGoToLastPageShortcut,
|
||||
isGoToNextPageShortcut,
|
||||
isGoToPreviousPageShortcut,
|
||||
isToggleZenModeShortcut,
|
||||
matchesModifierOnlyShortcut,
|
||||
matchesShortcut,
|
||||
tabSwitcherDirectionFromShortcut,
|
||||
@@ -98,6 +99,14 @@ describe("keyboard shortcut matching", () => {
|
||||
expect(isExecuteSqlInNewResultTabShortcut({ key: "\\", metaKey: true }, { executeSqlInNewResultTab: "Mod+\\" })).toBe(true);
|
||||
});
|
||||
|
||||
it("matches the configurable Zen mode shortcut", () => {
|
||||
const platformModEvent = isMacShortcutPlatform() ? { key: "F12", metaKey: true, shiftKey: true } : { key: "F12", ctrlKey: true, shiftKey: true };
|
||||
|
||||
expect(isToggleZenModeShortcut(platformModEvent, { toggleZenMode: "Shift+Mod+F12" })).toBe(true);
|
||||
expect(isToggleZenModeShortcut({ ...platformModEvent, shiftKey: false }, { toggleZenMode: "Shift+Mod+F12" })).toBe(false);
|
||||
expect(isToggleZenModeShortcut(platformModEvent, { toggleZenMode: "" })).toBe(false);
|
||||
});
|
||||
|
||||
it("matches legacy plus-key shortcuts saved with plus as a separator", () => {
|
||||
expect(matchesShortcut({ key: "+", ctrlKey: true }, "Mod++", "Win32")).toBe(true);
|
||||
expect(matchesShortcut({ key: "+", ctrlKey: true, shiftKey: true }, "Shift+Mod++", "Win32")).toBe(true);
|
||||
|
||||
@@ -146,6 +146,14 @@ describe("shortcutRegistry editor actions", () => {
|
||||
expect(findShortcutConflict("find", DEFAULT_SHORTCUT_SETTINGS.find, DEFAULT_SHORTCUT_SETTINGS)).toBeNull();
|
||||
});
|
||||
|
||||
it("registers a conflict-free global shortcut for Zen mode", () => {
|
||||
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === "toggleZenMode");
|
||||
|
||||
expect(definition).toMatchObject({ labelKey: "settings.shortcutToggleZenMode", scope: "global", defaultShortcut: "Shift+Mod+F12" });
|
||||
expect(DEFAULT_SHORTCUT_SETTINGS.toggleZenMode).toBe("Shift+Mod+F12");
|
||||
expect(findShortcutConflict("toggleZenMode", DEFAULT_SHORTCUT_SETTINGS.toggleZenMode, DEFAULT_SHORTCUT_SETTINGS)).toBeNull();
|
||||
});
|
||||
|
||||
it("uses Shift+Enter for inserting a complete line below", () => {
|
||||
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === "insertLineBelow");
|
||||
|
||||
|
||||
@@ -246,6 +246,10 @@ export function isToggleSidebarShortcut(event: ShortcutLikeEvent, shortcuts?: Pa
|
||||
return matchesShortcut(event, actionShortcut("toggleSidebar", shortcuts));
|
||||
}
|
||||
|
||||
export function isToggleZenModeShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
|
||||
return matchesShortcut(event, actionShortcut("toggleZenMode", shortcuts));
|
||||
}
|
||||
|
||||
export function isCopySidebarSelectionShortcut(event: ShortcutLikeEvent, shortcuts?: Partial<ShortcutSettings>): boolean {
|
||||
return matchesShortcut(event, actionShortcut("copySidebarSelection", shortcuts));
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ export type ShortcutActionId =
|
||||
| "toggleTranspose"
|
||||
| "cancelSearch"
|
||||
| "toggleSidebar"
|
||||
| "toggleZenMode"
|
||||
| "copySidebarSelection"
|
||||
| "pasteSidebarSelection"
|
||||
| "editSidebarConnection"
|
||||
@@ -503,6 +504,12 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
|
||||
scope: "global",
|
||||
defaultShortcut: "Mod+B",
|
||||
},
|
||||
{
|
||||
id: "toggleZenMode",
|
||||
labelKey: "settings.shortcutToggleZenMode",
|
||||
scope: "global",
|
||||
defaultShortcut: "Shift+Mod+F12",
|
||||
},
|
||||
{
|
||||
id: "copySidebarSelection",
|
||||
labelKey: "settings.shortcutCopySidebarSelection",
|
||||
|
||||
Reference in new issue
Block a user