From 209b6638bd91b30a75d20a08f9ab4e8dbbb0dbca Mon Sep 17 00:00:00 2001 From: kunghim Date: Wed, 5 Aug 2026 14:04:24 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(tab):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E6=A0=87=E7=AD=BE=E5=8F=B3=E9=94=AE=E9=87=8D?= =?UTF-8?q?=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QueryEditor.external-sql-save.test.tsx | 38 +++++++++++++++++++ frontend/src/components/QueryEditor.tsx | 16 +++++++- .../src/components/TabManager.hover.test.tsx | 16 ++++++++ frontend/src/components/TabManager.tsx | 25 +++++++++++- frontend/src/utils/queryTabTitle.ts | 1 + 5 files changed, 94 insertions(+), 2 deletions(-) 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',