🐛 fix(sidebar): 修复表名快捷复制

- 为表节点菜单绑定 macOS Command+C 与 Windows/Linux Ctrl+C
- 使用捕获阶段处理快捷键并复制当前菜单表名
- 增加平台映射、非法组合和最新节点回归测试

Refs #736
This commit is contained in:
Syngnat
2026-07-28 11:31:14 +08:00
parent c39fa3e8fe
commit 33ddee28ff
2 changed files with 136 additions and 5 deletions

View File

@@ -0,0 +1,91 @@
import { describe, expect, it, vi } from 'vitest';
import {
handleSidebarV2ContextMenuShortcut,
type SidebarContextMenuState,
} from './useSidebarV2ContextMenu';
const createKeyEvent = (overrides: Partial<KeyboardEvent> = {}): KeyboardEvent => ({
key: 'c',
code: 'KeyC',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
isComposing: false,
keyCode: 67,
which: 67,
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
...overrides,
} as unknown as KeyboardEvent);
const createTableMenu = (node: Record<string, unknown>): SidebarContextMenuState => ({
x: 10,
y: 20,
items: [],
kind: 'v2-table',
node,
});
describe('V2 sidebar context-menu shortcuts', () => {
it.each([
['mac', { metaKey: true }],
['windows', { ctrlKey: true }],
] as const)('routes the primary copy shortcut on %s to the current table', (platform, modifiers) => {
const node = { key: `table-${platform}`, title: 'ir_staff' };
const event = createKeyEvent(modifiers);
const onTableAction = vi.fn();
const onClose = vi.fn();
expect(handleSidebarV2ContextMenuShortcut({
event,
contextMenu: createTableMenu(node),
shortcutPlatform: platform,
onTableAction,
onClose,
})).toBe(true);
expect(event.preventDefault).toHaveBeenCalledTimes(1);
expect(event.stopPropagation).toHaveBeenCalledTimes(1);
expect(onClose).toHaveBeenCalledTimes(1);
expect(onTableAction).toHaveBeenCalledWith(node, 'copy-table-name');
});
it.each([
createKeyEvent(),
createKeyEvent({ altKey: true, metaKey: true }),
createKeyEvent({ ctrlKey: true, shiftKey: true }),
])('ignores copy-like keys that do not match the platform shortcut', (event) => {
const onTableAction = vi.fn();
const onClose = vi.fn();
expect(handleSidebarV2ContextMenuShortcut({
event,
contextMenu: createTableMenu({ key: 'table-a' }),
shortcutPlatform: 'mac',
onTableAction,
onClose,
})).toBe(false);
expect(onTableAction).not.toHaveBeenCalled();
expect(onClose).not.toHaveBeenCalled();
});
it('uses the latest node when a table menu is replaced without changing kind', () => {
const onTableAction = vi.fn();
const firstNode = { key: 'table-a' };
const latestNode = { key: 'table-b' };
handleSidebarV2ContextMenuShortcut({
event: createKeyEvent({ metaKey: true }),
contextMenu: createTableMenu(latestNode),
shortcutPlatform: 'mac',
onTableAction,
onClose: vi.fn(),
});
expect(onTableAction).toHaveBeenCalledWith(latestNode, 'copy-table-name');
expect(onTableAction).not.toHaveBeenCalledWith(firstNode, 'copy-table-name');
});
});

View File

@@ -29,6 +29,7 @@ import {
SIDEBAR_CONTEXT_MENU_FALLBACK_WIDTH,
resolveSidebarContextMenuPosition,
} from '../sidebarCoreUtils';
import { isShortcutMatch, type ShortcutPlatform } from '../../utils/shortcuts';
export type SidebarContextMenuState = {
x: number;
@@ -46,7 +47,7 @@ export type SidebarContextMenuState = {
type SidebarV2ContextMenuOptions = {
connections: SavedConnection[];
connectionTags: Array<{ id: string; name: string; connectionIds: string[] }>;
activeShortcutPlatform: any;
activeShortcutPlatform: ShortcutPlatform;
flattenConnectionNodes: (nodes: TreeNode[]) => TreeNode[];
v2TreeMetrics: {
databaseTableCounts: Map<React.Key, number>;
@@ -73,6 +74,39 @@ type SidebarV2ContextMenuOptions = {
handleV2ConnectionGroupContextMenuAction: (group: V2RailConnectionGroup, action: V2ConnectionGroupContextMenuActionKey) => void;
};
export const handleSidebarV2ContextMenuShortcut = ({
event,
contextMenu,
shortcutPlatform,
onTableAction,
onClose,
}: {
event: KeyboardEvent;
contextMenu: SidebarContextMenuState | null;
shortcutPlatform: ShortcutPlatform;
onTableAction: (node: any, action: V2TableContextMenuActionKey) => void;
onClose: () => void;
}): boolean => {
if (event.key === 'Escape') {
onClose();
return true;
}
if (event.defaultPrevented || contextMenu?.kind !== 'v2-table' || !contextMenu.node) {
return false;
}
const copyShortcut = shortcutPlatform === 'mac' ? 'Meta+C' : 'Ctrl+C';
if (!isShortcutMatch(event, copyShortcut)) {
return false;
}
event.preventDefault();
event.stopPropagation();
onClose();
onTableAction(contextMenu.node, 'copy-table-name');
return true;
};
export const useSidebarV2ContextMenu = ({
connections,
connectionTags,
@@ -408,15 +442,21 @@ export const useSidebarV2ContextMenu = ({
setContextMenu(null);
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') setContextMenu(null);
handleSidebarV2ContextMenuShortcut({
event,
contextMenu,
shortcutPlatform: activeShortcutPlatform,
onTableAction: handleV2TableContextMenuAction,
onClose: () => setContextMenu(null),
});
};
document.addEventListener('mousedown', onPointerDown);
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keydown', onKeyDown, true);
return () => {
document.removeEventListener('mousedown', onPointerDown);
document.removeEventListener('keydown', onKeyDown);
document.removeEventListener('keydown', onKeyDown, true);
};
}, [contextMenu?.kind]);
}, [activeShortcutPlatform, contextMenu, handleV2TableContextMenuAction]);
useEffect(() => {
if (!contextMenu?.kind) return;