feat(tab): 增加查询标签右键重命名 (#844)

## 变更说明

- 在查询 Tab 的右键菜单中增加“重命名查询”入口。
- 复用 QueryEditor 现有的重命名流程,保持保存记录、Tab 标题和未保存查询引导行为一致。
- 右键非当前查询 Tab 时先激活目标 Tab,再打开对应的重命名窗口。
- SQL 文件 Tab 保留现有不可重命名限制,表数据等非查询 Tab 不显示该菜单项。
- 通过带 tabId 的事件只通知目标 QueryEditor,避免误操作其他查询 Tab。

## 测试

- 定向测试:325 passed
- npm run build:TypeScript 和 Vite 构建通过
- Wails Windows 测试包:GoNavi-issue-835-query-tab-rename.exe
- 已完成人工测试验证

Fixes #835
This commit is contained in:
Syngnat
2026-08-05 14:35:49 +08:00
committed by GitHub
5 changed files with 94 additions and 2 deletions

View File

@@ -10,6 +10,7 @@ import type { SavedQuery, TabData } from '../types';
import { ORACLE_ROWID_LOCATOR_COLUMN } from '../utils/rowLocator';
import { setGlobalImeCompositionActive } from '../utils/shortcuts';
import { clearQueryEditorResultSession } from '../utils/queryEditorResultSessionCache';
import { QUERY_TAB_RENAME_REQUEST_EVENT } from '../utils/queryTabTitle';
import { clearQueryTabDraft, clearSQLFileTabDraft, getQueryTabDraft, getSQLFileTabDraft } from '../utils/sqlFileTabDrafts';
import { clearQueryEditorInlineRuntimeReadinessCache } from './queryEditor/QueryEditorAiAssist';
import QueryEditor, {
@@ -10208,6 +10209,43 @@ END;`;
expect(messageApi.success).toHaveBeenCalledWith('查询已重命名。');
});
it('opens the existing rename flow for the query tab context-menu request', async () => {
storeState.savedQueries = [
{
id: 'saved-1',
name: '常用查询',
sql: 'select 1;',
connectionId: 'conn-1',
dbName: 'main',
createdAt: 100,
},
];
let renderer!: ReactTestRenderer;
await act(async () => {
renderer = create(<QueryEditor tab={createTab({ savedQueryId: 'saved-1' })} />);
});
const renameRequestListenerCalls = (window.addEventListener as any).mock.calls
.filter(([eventName]: [string]) => eventName === QUERY_TAB_RENAME_REQUEST_EVENT);
const renameRequestListener = renameRequestListenerCalls[renameRequestListenerCalls.length - 1]?.[1];
expect(renameRequestListener).toBeTypeOf('function');
await act(async () => {
renameRequestListener(new CustomEvent(QUERY_TAB_RENAME_REQUEST_EVENT, {
detail: { tabId: 'another-tab' },
}));
});
expect(findExactButton(renderer!, '重命名')).toBeUndefined();
await act(async () => {
renameRequestListener(new CustomEvent(QUERY_TAB_RENAME_REQUEST_EVENT, {
detail: { tabId: 'tab-1' },
}));
});
expect(findExactButton(renderer!, '重命名')).toBeTruthy();
});
it('exports the current editor SQL without changing saved query state', async () => {
storeState.savedQueries = [
{

View File

@@ -57,7 +57,7 @@ import {
import { resolveUniqueKeyGroupsFromIndexes } from './dataGridCopyInsert';
import { t as translate } from '../i18n';
import { buildSqlAnalysisWorkbenchTab } from '../utils/sqlAnalysisTab';
import { isLocalizedUntitledQueryTitle } from '../utils/queryTabTitle';
import { isLocalizedUntitledQueryTitle, QUERY_TAB_RENAME_REQUEST_EVENT } from '../utils/queryTabTitle';
import { buildSqlServerObjectDefinitionQueries } from '../utils/sqlServerObjectDefinition';
import { formatDdlForDisplay } from '../utils/ddlFormat';
import {
@@ -9680,6 +9680,20 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
openSaveQueryModal('rename');
};
useEffect(() => {
const handleRenameQueryRequest = (event: Event) => {
if (!(event instanceof CustomEvent) || event.detail?.tabId !== tab.id) {
return;
}
handleRenameQuery();
};
window.addEventListener(QUERY_TAB_RENAME_REQUEST_EVENT, handleRenameQueryRequest as EventListener);
return () => {
window.removeEventListener(QUERY_TAB_RENAME_REQUEST_EVENT, handleRenameQueryRequest as EventListener);
};
}, [handleRenameQuery, tab.id]);
const handleExportSQLFile = async () => {
try {
const res = await ExportSQLFile(currentSavedQuery?.name || resolveDefaultQueryName(), getCurrentQuery());

View File

@@ -7,6 +7,7 @@ import {
handleTabDragPointerDown,
resolveTabHoverOpen,
resolveTabHoverTitle,
resolveQueryTabRenameMenuState,
shouldShowV2ConnectionLabel,
TabHoverInfo,
isMiddleMouseButton,
@@ -118,6 +119,21 @@ describe('TabManager hover info', () => {
expect(isMiddleMouseButton(2)).toBe(false);
});
it('shows query rename only for query tabs and disables SQL file tabs', () => {
expect(resolveQueryTabRenameMenuState({ type: 'query' })).toEqual({
visible: true,
disabled: false,
});
expect(resolveQueryTabRenameMenuState({ type: 'query', filePath: 'D:/queries/report.sql' })).toEqual({
visible: true,
disabled: true,
});
expect(resolveQueryTabRenameMenuState({ type: 'table' })).toEqual({
visible: false,
disabled: false,
});
});
it('keeps the tab workbench as a full-height flex child in legacy and v2 UI', () => {
expect(TAB_WORKBENCH_CLASS_NAME).toBe('tab-workbench');

View File

@@ -1,7 +1,7 @@
import Modal from './common/ResizableDraggableModal';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { Button, Dropdown, message, Tabs, Tooltip } from 'antd';
import { CloseOutlined, ConsoleSqlOutlined, DatabaseOutlined, FileTextOutlined, FolderOpenOutlined, HistoryOutlined, PlusOutlined, PushpinOutlined, RightOutlined, RobotOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons';
import { CloseOutlined, ConsoleSqlOutlined, DatabaseOutlined, EditOutlined, FileTextOutlined, FolderOpenOutlined, HistoryOutlined, PlusOutlined, PushpinOutlined, RightOutlined, RobotOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons';
import type { MenuProps, TabsProps } from 'antd';
import { DndContext, PointerSensor, closestCenter, useSensor, useSensors } from '@dnd-kit/core';
import type { DragEndEvent, DragMoveEvent, DragStartEvent } from '@dnd-kit/core';
@@ -47,6 +47,7 @@ import { openNativeWorkbenchTabWindow } from '../utils/nativeDetachedWindowHost'
import { useWorkbenchTabs } from '../hooks/useWorkbenchTabs';
import { resolveConnectionEnvironmentPresentation } from '../utils/connectionEnvironment';
import { createSidebarResizeAwareFrameScheduler } from '../utils/sidebarResizeLifecycle';
import { QUERY_TAB_RENAME_REQUEST_EVENT } from '../utils/queryTabTitle';
const getTabKindLabel = (tab: TabData): string => {
if (tab.type === 'query') return t('tab_manager.kind_badge.query');
@@ -78,6 +79,13 @@ export const isBackgroundTaskWorkbenchTab = (tab: Pick<TabData, 'type'>): boolea
tab.type === 'table-export' || tab.type === 'data-import' || tab.type === 'data-sync'
);
export const resolveQueryTabRenameMenuState = (
tab: Pick<TabData, 'type' | 'filePath'>,
): { visible: boolean; disabled: boolean } => ({
visible: tab.type === 'query',
disabled: Boolean(tab.filePath),
});
export const isRunningDataImportWorkbenchTab = (
tab: Pick<TabData, 'type' | 'dataImportRunning'>,
): boolean => tab.type === 'data-import' && tab.dataImportRunning === true;
@@ -1254,8 +1262,23 @@ const TabManager: React.FC<TabManagerProps> = React.memo<TabManagerProps>(({ onF
const displayTitle = displayModel.fullTitle;
const hostSummary = resolveConnectionHostSummary(connection?.config);
const tabIsActive = tab.id === dockedActiveTabId;
const renameQueryMenuState = resolveQueryTabRenameMenuState(tab);
const menuItems: MenuProps['items'] = [
...(renameQueryMenuState.visible ? [{
key: 'rename-query',
icon: <EditOutlined />,
label: t('query_editor.action.rename_query'),
disabled: renameQueryMenuState.disabled,
onClick: () => {
setActiveTab(tab.id);
window.setTimeout(() => {
window.dispatchEvent(new CustomEvent(QUERY_TAB_RENAME_REQUEST_EVENT, {
detail: { tabId: tab.id },
}));
}, 0);
},
}] : []),
{
key: 'tab-display-settings',
icon: <SettingOutlined />,

View File

@@ -3,6 +3,7 @@ import { SUPPORTED_LANGUAGES } from '../i18n/resolveLanguage';
import type { I18nParams } from '../i18n/types';
const UNTITLED_QUERY_DATABASE_PLACEHOLDER = '__GONAVI_QUERY_DATABASE__';
export const QUERY_TAB_RENAME_REQUEST_EVENT = 'gonavi:request-query-tab-rename';
const UNTITLED_QUERY_TITLE_KEYS = [
'query.new',
'sidebar.tab.new_query',