diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx
index 90a468f3..b6f026ef 100644
--- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx
+++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx
@@ -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();
+ });
+
+ 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 = [
{
diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx
index 856997b3..b79c851b 100644
--- a/frontend/src/components/QueryEditor.tsx
+++ b/frontend/src/components/QueryEditor.tsx
@@ -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());
diff --git a/frontend/src/components/TabManager.hover.test.tsx b/frontend/src/components/TabManager.hover.test.tsx
index 5c1153bb..4f80f09c 100644
--- a/frontend/src/components/TabManager.hover.test.tsx
+++ b/frontend/src/components/TabManager.hover.test.tsx
@@ -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');
diff --git a/frontend/src/components/TabManager.tsx b/frontend/src/components/TabManager.tsx
index 97fdfbfa..e5f5008e 100644
--- a/frontend/src/components/TabManager.tsx
+++ b/frontend/src/components/TabManager.tsx
@@ -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): boolea
tab.type === 'table-export' || tab.type === 'data-import' || tab.type === 'data-sync'
);
+export const resolveQueryTabRenameMenuState = (
+ tab: Pick,
+): { visible: boolean; disabled: boolean } => ({
+ visible: tab.type === 'query',
+ disabled: Boolean(tab.filePath),
+});
+
export const isRunningDataImportWorkbenchTab = (
tab: Pick,
): boolean => tab.type === 'data-import' && tab.dataImportRunning === true;
@@ -1254,8 +1262,23 @@ const TabManager: React.FC = React.memo(({ 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: ,
+ 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: ,
diff --git a/frontend/src/utils/queryTabTitle.ts b/frontend/src/utils/queryTabTitle.ts
index 43509d2a..1f1030a2 100644
--- a/frontend/src/utils/queryTabTitle.ts
+++ b/frontend/src/utils/queryTabTitle.ts
@@ -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',