feat(titlebar): 为新建操作提示平台快捷键

- 新建查询与新建连接悬浮提示展示当前生效快捷键
- 按 macOS 和 Windows 格式化组合键并跟随用户自定义绑定
- 快捷键禁用时回退操作名称并补齐平台与隔离回归测试
This commit is contained in:
Syngnat
2026-08-07 10:06:22 +08:00
parent 2f24c9667c
commit a3f032b1d9
3 changed files with 110 additions and 4 deletions

View File

@@ -7,7 +7,9 @@ import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy }
import { CSS } from '@dnd-kit/utilities';
import { BrowserOpenURL, Environment, EventsOn, WindowFullscreen, WindowGetPosition, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowIsMinimised, WindowIsNormal, WindowMaximise, WindowMinimise, WindowSetDarkTheme, WindowSetLightTheme, WindowSetPosition, WindowSetSize, WindowSetSystemDefaultTheme, WindowUnfullscreen, WindowUnmaximise } from '../wailsjs/runtime';
import Sidebar from './components/Sidebar';
import TitleBarPrimaryActions from './components/TitleBarPrimaryActions';
import TitleBarPrimaryActions, {
resolveTitleBarPrimaryActionShortcut,
} from './components/TitleBarPrimaryActions';
import TabManager from './components/TabManager';
import FloatingWorkbenchWindows from './components/FloatingWorkbenchWindows';
import FloatingAIChatWindow from './components/FloatingAIChatWindow';
@@ -2446,6 +2448,16 @@ function App() {
|| (runtimePlatform === '' && /mac/i.test(detectNavigatorPlatform()));
const useNativeMacWindowControls = isMacRuntime;
const activeShortcutPlatform = getShortcutPlatform(isMacRuntime);
const titleBarNewQueryShortcut = resolveTitleBarPrimaryActionShortcut(
shortcutOptions,
'newQueryTab',
activeShortcutPlatform,
);
const titleBarNewConnectionShortcut = resolveTitleBarPrimaryActionShortcut(
shortcutOptions,
'newConnection',
activeShortcutPlatform,
);
const macWindowDiagnosticsEnabled = shouldEnableMacWindowDiagnostics(
isMacRuntime,
import.meta.env.DEV,
@@ -7856,6 +7868,8 @@ function App() {
<TitleBarPrimaryActions
newQueryLabel={t('query.new')}
newConnectionLabel={t('connection.new')}
newQueryShortcut={titleBarNewQueryShortcut}
newConnectionShortcut={titleBarNewConnectionShortcut}
onNewQuery={handleNewQuery}
onNewConnection={handleCreateConnection}
/>

View File

@@ -3,7 +3,13 @@ import { readFileSync } from 'node:fs';
import { create } from 'react-test-renderer';
import { describe, expect, it, vi } from 'vitest';
import TitleBarPrimaryActions from './TitleBarPrimaryActions';
import TitleBarPrimaryActions, {
resolveTitleBarPrimaryActionShortcut,
} from './TitleBarPrimaryActions';
import {
cloneShortcutOptions,
DEFAULT_SHORTCUT_OPTIONS,
} from '../utils/shortcuts';
const appCss = readFileSync(new URL('../App.css', import.meta.url), 'utf8');
@@ -45,10 +51,13 @@ describe('TitleBarPrimaryActions', () => {
it('shows both labels in query-first order and invokes their actions', () => {
const onNewQuery = vi.fn();
const onNewConnection = vi.fn();
const shortcutOptions = cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS);
const renderer = create(
<TitleBarPrimaryActions
newQueryLabel="新建查询"
newConnectionLabel="新建连接"
newQueryShortcut={resolveTitleBarPrimaryActionShortcut(shortcutOptions, 'newQueryTab', 'mac')}
newConnectionShortcut={resolveTitleBarPrimaryActionShortcut(shortcutOptions, 'newConnection', 'mac')}
onNewQuery={onNewQuery}
onNewConnection={onNewConnection}
/>,
@@ -58,6 +67,10 @@ describe('TitleBarPrimaryActions', () => {
const buttons = actions.findAllByType('button');
expect(actions.props['data-no-titlebar-toggle']).toBe('true');
expect(buttons.map((button) => button.props['aria-label'])).toEqual(['新建查询', '新建连接']);
expect(buttons.map((button) => button.props.title)).toEqual([
'新建查询 · ⌘N',
'新建连接 · ⌘⇧N',
]);
expect(buttons.map((button) => button.children[button.children.length - 1])).toEqual(['新建查询', '新建连接']);
buttons[0].props.onClick();
@@ -65,4 +78,56 @@ describe('TitleBarPrimaryActions', () => {
expect(onNewQuery).toHaveBeenCalledTimes(1);
expect(onNewConnection).toHaveBeenCalledTimes(1);
});
it('shows both Windows shortcut labels', () => {
const shortcutOptions = cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS);
const renderer = create(
<TitleBarPrimaryActions
newQueryLabel="New Query"
newConnectionLabel="New Connection"
newQueryShortcut={resolveTitleBarPrimaryActionShortcut(shortcutOptions, 'newQueryTab', 'windows')}
newConnectionShortcut={resolveTitleBarPrimaryActionShortcut(shortcutOptions, 'newConnection', 'windows')}
onNewQuery={vi.fn()}
onNewConnection={vi.fn()}
/>,
);
const buttons = renderer.root.findAllByType('button');
expect(buttons.map((button) => button.props.title)).toEqual([
'New Query · Ctrl+N',
'New Connection · Ctrl+Shift+N',
]);
});
it('uses current platform custom bindings and hides disabled shortcuts', () => {
const shortcutOptions = cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS);
shortcutOptions.newQueryTab.mac = { combo: 'Meta+Alt+Q', enabled: true };
shortcutOptions.newQueryTab.windows = { combo: 'Ctrl+Alt+W', enabled: true };
shortcutOptions.newConnection.mac = { combo: 'Meta+Shift+C', enabled: false };
shortcutOptions.newConnection.windows = { combo: 'Ctrl+Alt+C', enabled: true };
expect(resolveTitleBarPrimaryActionShortcut(shortcutOptions, 'newQueryTab', 'mac')).toBe('⌘⌥Q');
expect(resolveTitleBarPrimaryActionShortcut(shortcutOptions, 'newQueryTab', 'windows')).toBe('Ctrl+Alt+W');
expect(resolveTitleBarPrimaryActionShortcut(shortcutOptions, 'newConnection', 'mac')).toBeUndefined();
expect(resolveTitleBarPrimaryActionShortcut(shortcutOptions, 'newConnection', 'windows')).toBe('Ctrl+Alt+C');
const renderer = create(
<TitleBarPrimaryActions
newQueryLabel="新建查询"
newConnectionLabel="新建连接"
newQueryShortcut={resolveTitleBarPrimaryActionShortcut(shortcutOptions, 'newQueryTab', 'mac')}
newConnectionShortcut={resolveTitleBarPrimaryActionShortcut(shortcutOptions, 'newConnection', 'mac')}
onNewQuery={vi.fn()}
onNewConnection={vi.fn()}
/>,
);
const buttons = renderer.root.findAllByType('button');
expect(buttons.map((button) => button.props.title)).toEqual([
'新建查询 · ⌘⌥Q',
'新建连接',
]);
expect(buttons.map((button) => button.props['aria-label'])).toEqual(['新建查询', '新建连接']);
expect(buttons.every((button) => button.props.disabled !== true)).toBe(true);
});
});

View File

@@ -1,16 +1,43 @@
import React from 'react';
import { ConsoleSqlOutlined, PlusOutlined } from '@ant-design/icons';
import {
getShortcutDisplayLabel,
resolveShortcutBinding,
type ShortcutOptions,
type ShortcutPlatform,
} from '../utils/shortcuts';
type TitleBarPrimaryShortcutAction = 'newQueryTab' | 'newConnection';
export const resolveTitleBarPrimaryActionShortcut = (
shortcutOptions: Partial<ShortcutOptions> | null | undefined,
action: TitleBarPrimaryShortcutAction,
platform: ShortcutPlatform,
): string | undefined => {
const binding = resolveShortcutBinding(shortcutOptions, action, platform);
return binding.enabled && binding.combo
? getShortcutDisplayLabel(binding.combo, platform)
: undefined;
};
interface TitleBarPrimaryActionsProps {
newQueryLabel: string;
newConnectionLabel: string;
newQueryShortcut?: string;
newConnectionShortcut?: string;
onNewQuery: () => void;
onNewConnection: () => void;
}
const getActionTitle = (label: string, shortcut?: string): string => (
shortcut ? `${label} \u00b7 ${shortcut}` : label
);
const TitleBarPrimaryActions: React.FC<TitleBarPrimaryActionsProps> = ({
newQueryLabel,
newConnectionLabel,
newQueryShortcut,
newConnectionShortcut,
onNewQuery,
onNewConnection,
}) => (
@@ -24,7 +51,7 @@ const TitleBarPrimaryActions: React.FC<TitleBarPrimaryActionsProps> = ({
type="button"
className="gonavi-titlebar-primary-action"
aria-label={newQueryLabel}
title={newQueryLabel}
title={getActionTitle(newQueryLabel, newQueryShortcut)}
data-gonavi-new-query-action="true"
onClick={onNewQuery}
>
@@ -35,7 +62,7 @@ const TitleBarPrimaryActions: React.FC<TitleBarPrimaryActionsProps> = ({
type="button"
className="gonavi-titlebar-primary-action"
aria-label={newConnectionLabel}
title={newConnectionLabel}
title={getActionTitle(newConnectionLabel, newConnectionShortcut)}
data-gonavi-create-connection-action="true"
onClick={onNewConnection}
>