diff --git a/frontend/src/App.settings-center.test.ts b/frontend/src/App.settings-center.test.ts index 8199fc74..7b46f8f7 100644 --- a/frontend/src/App.settings-center.test.ts +++ b/frontend/src/App.settings-center.test.ts @@ -13,9 +13,10 @@ const aiSettingsModalSource = readFileSync( ); describe('settings center layout', () => { - it('uses the same split navigation shell as the tool center', () => { - expect(appSource).toContain("type SettingsCenterGroupKey = 'preferences' | 'services' | 'about';"); - expect(appSource).toContain("type SettingsCenterPaneKey = 'language' | 'theme' | 'sidebar-metadata' | 'sidebar-objects' | 'proxy' | 'web-auth' | 'ai' | 'about-go-navi';"); + it('hosts settings and tools in one split navigation shell', () => { + expect(appSource).toContain("type SettingsCenterGroupKey = 'preferences' | 'services' | ToolCenterGroupKey | 'about';"); + expect(appSource).toContain('type SettingsCenterPaneKey ='); + expect(appSource).toContain('| ToolCenterPaneKey'); expect(appSource).toContain("const [activeSettingsCenterGroupKey, setActiveSettingsCenterGroupKey] = useState('preferences');"); expect(appSource).toContain("const [activeSettingsCenterPane, setActiveSettingsCenterPane] = useState(null);"); expect(appSource).toContain('style={toolCenterModalWorkspaceStyle}'); @@ -24,11 +25,17 @@ describe('settings center layout', () => { expect(appSource).toContain('style={toolCenterNavScrollStyle}'); expect(appSource).toContain('style={toolCenterContentPanelStyle}'); expect(appSource).toContain('style={toolCenterDetailPanelStyle}'); - expect(appSource).toContain('style={toolCenterDetailBodyStyle}'); + expect(appSource).toContain('style={isActiveToolCenterPane ? toolCenterDetailBodyStyle : settingsCenterDetailBodyStyle}'); expect(appSource).toContain('style={toolCenterScrollableListStyle}'); expect(appSource).toContain("title: t('app.settings.group.preferences.title')"); expect(appSource).toContain("title: t('app.settings.group.services.title')"); + expect(appSource).toContain("title: t('app.tools.group.config.title')"); + expect(appSource).toContain("title: t('app.tools.group.workflow.title')"); + expect(appSource).toContain("title: t('app.tools.group.workspace.title')"); expect(appSource).toContain("title: t('app.settings.group.about.title')"); + expect(appSource).toContain('const combinedSettingsCenterGroups = ['); + expect(appSource).not.toContain('const [isToolsModalOpen'); + expect(appSource).not.toContain('{isToolsModalOpen &&'); }); it('moves sidebar table metadata configuration into the settings center', () => { @@ -87,6 +94,43 @@ describe('settings center layout', () => { expect(appSource).toContain("t('common.back_to_previous')"); }); + it('clears embedded tool transient state before switching settings groups or panes', () => { + const cleanupStart = appSource.indexOf('const clearSettingsCenterTransientPaneState = useCallback(() => {'); + const cleanupSource = appSource.slice( + cleanupStart, + appSource.indexOf('const handleOpenToolsModal', cleanupStart), + ); + + expect(cleanupStart).toBeGreaterThan(-1); + expect(cleanupSource).toContain('setCapturingShortcutAction(null);'); + expect(cleanupSource).toContain("activeSettingsCenterPaneRef.current?.key === 'connection-package'"); + expect(cleanupSource).toContain('closeConnectionPackageDialog();'); + expect(cleanupSource).toContain("activeSettingsCenterPaneRef.current?.key === 'ai'"); + expect(cleanupSource).toContain('setFocusedAIProviderId(undefined);'); + expect(cleanupSource).toContain('setSecurityUpdateRepairSource(null);'); + expect(appSource).toContain('const handleOpenSettingsModal = useCallback'); + expect(appSource).toContain('const handleOpenToolCenterPane = useCallback'); + expect(appSource.match(/clearSettingsCenterTransientPaneState\(\);/g)?.length).toBeGreaterThanOrEqual(4); + }); + + it('routes every security-update detail and repair return through settings center', () => { + const openDetailsStart = appSource.indexOf('const openSecurityUpdateSettings = useCallback('); + const openDetailsSource = appSource.slice( + openDetailsStart, + appSource.indexOf('const handleOpenSecurityUpdateSettings', openDetailsStart), + ); + + expect(openDetailsStart).toBeGreaterThan(-1); + expect(openDetailsSource).toContain("setActiveSettingsCenterGroupKey('config');"); + expect(openDetailsSource).toContain("setActiveSettingsCenterPane({ key: 'security-update', group: 'config' });"); + expect(openDetailsSource).toContain('setIsSettingsModalOpen(true);'); + expect(appSource).toContain("const detailsWereOpen = isSettingsModalOpen && activeSettingsCenterPane?.key === 'security-update';"); + expect(appSource.match(/ { expect(appSource).toContain("isV2Ui && activeSettingsCenterPane.key === 'theme' ? ("); expect(appSource).toContain("t('common.close')"); @@ -141,7 +185,7 @@ describe('settings center layout', () => { expect(appSource).toContain('const resolveSettingsCenterGroupInitialPane = (group: SettingsCenterGroupKey): SettingsCenterPaneState | null => ('); expect(appSource).toContain("group === 'about' ? { key: 'about-go-navi', group: 'about' } : null"); expect(appSource).toContain('setActiveSettingsCenterPane(resolveSettingsCenterGroupInitialPane(group));'); - expect(appSource).toContain('setActiveSettingsCenterPane(resolveSettingsCenterGroupInitialPane(group.key));'); + expect(appSource).toContain('handleOpenSettingsModal(group.key);'); }); it('routes silent update discovery to the settings center about pane via bridge', () => { @@ -157,8 +201,10 @@ describe('settings center layout', () => { expect(appSource).toContain('const renderSettingsCenterAboutPane = () => {'); expect(appSource).toContain('const renderSettingsCenterAboutProjectEntry = ({'); expect(appSource).toContain("padding: '18px 22px'"); - expect(appSource).toContain('width: 64'); - expect(appSource).toContain('height: 64'); + expect(appSource).toContain('width={108}'); + expect(appSource).toContain('height={108}'); + expect(appSource).toContain('width: 108'); + expect(appSource).toContain('height: 108'); expect(appSource).toContain('minWidth: 260'); expect(appSource).toContain('const releaseTimeText = formatAboutReleaseTime(lastUpdateInfo?.releasePublishedAt);'); expect(appSource).toContain("[t('app.about.version.release_time'), releaseTimeText]"); @@ -186,7 +232,7 @@ describe('settings center layout', () => { expect(appSource).toContain("activeSettingsCenterPane?.key === 'theme' || activeSettingsCenterPane?.key === 'ai'"); expect(appSource).toContain('const settingsCenterDetailBodyStyle: React.CSSProperties = isSettingsCenterContainedScrollPane'); expect(appSource).toContain("overflowY: 'hidden'"); - expect(appSource).toContain('style={settingsCenterDetailBodyStyle}'); + expect(appSource).toContain('style={isActiveToolCenterPane ? toolCenterDetailBodyStyle : settingsCenterDetailBodyStyle}'); expect(appSource).toContain("boxSizing: 'border-box'"); expect(appSource).toContain("overscrollBehavior: 'contain'"); expect(aiSettingsModalSource).toContain("boxSizing: 'border-box'"); diff --git a/frontend/src/App.tool-center.test.ts b/frontend/src/App.tool-center.test.ts index 3010d2ce..1286c642 100644 --- a/frontend/src/App.tool-center.test.ts +++ b/frontend/src/App.tool-center.test.ts @@ -45,15 +45,14 @@ const getGlobalShortcutCaseBlock = (action: string) => { return afterCase.slice(0, endIndex); }; -describe('tool center menu entries', () => { +describe('settings center tool entries', () => { it('exposes snippet management next to shortcut management', () => { expect(appSource).toContain("key: 'snippet-settings'"); expect(appSource).toContain("title: t('app.tools.entry.snippets.title')"); expect(appSource).toContain("description: t('app.tools.entry.snippets.description')"); expect(appSource).toContain("handleOpenToolCenterPane('workspace', 'snippet-settings')"); expect(appSource).toContain('gonavi:open-snippet-settings'); - expect(appSource).toContain("setIsSnippetModalOpen(false);"); - expect(appSource).not.toContain('setIsSnippetModalOpen(true)'); + expect(appSource).not.toContain('isSnippetModalOpen'); const snippetIndex = appSource.indexOf("key: 'snippet-settings'"); const shortcutIndex = appSource.indexOf("key: 'shortcut-settings'", snippetIndex); @@ -61,14 +60,15 @@ describe('tool center menu entries', () => { expect(shortcutIndex).toBeGreaterThan(snippetIndex); }); - it('uses scalable side navigation for the tool center instead of horizontal segmented switching', () => { + it('uses the settings center side navigation for every tool group', () => { expect(appSource).toContain("type ToolCenterGroupKey = 'config' | 'workflow' | 'workspace';"); - expect(appSource).toContain("const [activeToolCenterGroupKey, setActiveToolCenterGroupKey] = useState('config');"); + expect(appSource).toContain("type SettingsCenterGroupKey = 'preferences' | 'services' | ToolCenterGroupKey | 'about';"); expect(appSource).toContain("const [toolCenterBackGroupKey, setToolCenterBackGroupKey] = useState(null);"); expect(appSource).toContain("title: t('app.tools.group.config.title')"); expect(appSource).toContain("title: t('app.tools.group.workflow.title')"); expect(appSource).toContain("title: t('app.tools.group.workspace.title')"); - expect(appSource).toContain("filteredToolCenterGroups.find((group) => group.key === activeToolCenterGroupKey)"); + expect(appSource).toContain('const combinedSettingsCenterGroups = ['); + expect(appSource).toContain('(group) => group.key === activeSettingsCenterGroupKey'); expect(appUtilityStylesSource).toContain("const toolCenterModalSplitStyle = useMemo(() => ({"); expect(appUtilityStylesSource).toContain("gridTemplateColumns: '232px minmax(0, 1fr)'"); expect(appUtilityStylesSource).toContain("const toolCenterNavPanelStyle = useMemo(() => ({"); @@ -81,17 +81,18 @@ describe('tool center menu entries', () => { expect(appSource).toContain('aria-selected={active}'); expect(appSource).toContain('title={`${group.title} - ${group.description}`}'); expect(appUtilityStylesSource).toContain("borderRight: `1px solid ${overlayTheme.divider}`"); - expect(appSource).toContain('setActiveToolCenterPane(null);'); + expect(appSource).toContain('setActiveSettingsCenterPane(null);'); expect(appSource).toContain('group.items.length'); expect(appSource).toContain("const handleOpenToolCenterPane = useCallback((group: ToolCenterGroupKey, key: ToolCenterPaneKey) => {"); - expect(appSource).toContain("const [activeToolCenterPane, setActiveToolCenterPane] = useState(null);"); + expect(appSource).toContain("const [activeSettingsCenterPane, setActiveSettingsCenterPane] = useState(null);"); expect(appSource).toContain("const handleReturnToToolCenter = useCallback((closeChild?: () => void) => {"); expect(appSource).toContain("t('common.back_to_previous')"); expect(appSource).toContain("width={1080}"); expect(appSource).toContain('centered'); + expect(appSource).not.toContain('const [isToolsModalOpen'); }); - it('keeps the tool center modal height fixed across group switches and scrolls the list area internally', () => { + it('keeps the unified settings modal height fixed across group switches and scrolls the list area internally', () => { expect(appUtilityStylesSource).toContain('const toolCenterModalContentStyle = useMemo(() => ({'); expect(appUtilityStylesSource).toContain("height: 'min(820px, calc(100vh - 64px))'"); expect(appUtilityStylesSource).toContain("const toolCenterModalWorkspaceStyle = useMemo(() => ({"); @@ -104,7 +105,7 @@ describe('tool center menu entries', () => { expect(appSource).toContain('style={toolCenterNavScrollStyle}'); expect(appSource).toContain('style={toolCenterContentPanelStyle}'); expect(appSource).toContain('style={toolCenterDetailPanelStyle}'); - expect(appSource).toContain('style={toolCenterDetailBodyStyle}'); + expect(appSource).toContain('style={isActiveToolCenterPane ? toolCenterDetailBodyStyle : settingsCenterDetailBodyStyle}'); expect(appSource).toContain('style={toolCenterScrollableListStyle}'); expect(appUtilityStylesSource).toContain("overflowY: 'auto'"); expect(appSource).toContain("borderTop: index === 0 ? `1px solid ${overlayTheme.divider}` : 'none'"); @@ -112,12 +113,23 @@ describe('tool center menu entries', () => { }); it('keeps browser-compatible connection transfer and mounted data-root entries available in the web runtime', () => { + const toolGroupsStart = appSource.indexOf('const toolCenterGroups: SettingsCenterNavigationGroup[] = ['); + const configGroupStart = appSource.indexOf("key: 'config',", toolGroupsStart); + const configGroupSource = appSource.slice( + configGroupStart, + appSource.indexOf("key: 'workflow',", configGroupStart), + ); + + expect(toolGroupsStart).toBeGreaterThan(-1); + expect(configGroupStart).toBeGreaterThan(toolGroupsStart); expect(appSource).toContain("accept=\".gonavi-conn,.json,.xml,.ncx\""); expect(appSource).toContain('ExportConnectionsPayload'); expect(appSource).toContain('downloadBrowserTextFile'); expect(appSource).toContain("__GONAVI_WEB_RUNTIME__?.buildType === 'web'"); expect(appSource).toContain('if (isWebRuntime) {\n return ('); - expect(appSource).toContain('items: group.items,'); + expect(configGroupSource).toContain("key: 'data-root'"); + expect(configGroupSource).toContain("handleOpenToolCenterPane('config', 'data-root')"); + expect(appSource).toContain('...toolCenterGroups,'); }); it('lets the tool center detail header own embedded tool titles', () => { @@ -127,35 +139,35 @@ describe('tool center menu entries', () => { appSource.indexOf('};\n\n return (', renderPaneStart), ); const connectionPackageSource = renderPaneSource.slice( - renderPaneSource.indexOf("if (activeToolCenterPane.key === 'connection-package')"), - renderPaneSource.indexOf("if (activeToolCenterPane.key === 'data-root')"), + renderPaneSource.indexOf("if (activeSettingsCenterPane.key === 'connection-package')"), + renderPaneSource.indexOf("if (activeSettingsCenterPane.key === 'data-root')"), ); const dataRootSource = renderPaneSource.slice( - renderPaneSource.indexOf("if (activeToolCenterPane.key === 'data-root')"), - renderPaneSource.indexOf("if (activeToolCenterPane.key === 'security-update')"), + renderPaneSource.indexOf("if (activeSettingsCenterPane.key === 'data-root')"), + renderPaneSource.indexOf("if (activeSettingsCenterPane.key === 'security-update')"), ); const securityUpdateSource = renderPaneSource.slice( - renderPaneSource.indexOf("if (activeToolCenterPane.key === 'security-update')"), - renderPaneSource.indexOf("activeToolCenterPane.key === 'schema-compare'"), + renderPaneSource.indexOf("if (activeSettingsCenterPane.key === 'security-update')"), + renderPaneSource.indexOf("activeSettingsCenterPane.key === 'schema-compare'"), ); const dataSyncSource = renderPaneSource.slice( - renderPaneSource.indexOf("activeToolCenterPane.key === 'schema-compare'"), - renderPaneSource.indexOf("if (activeToolCenterPane.key === 'drivers')"), + renderPaneSource.indexOf("activeSettingsCenterPane.key === 'schema-compare'"), + renderPaneSource.indexOf("if (activeSettingsCenterPane.key === 'drivers')"), ); const driverSource = renderPaneSource.slice( - renderPaneSource.indexOf("if (activeToolCenterPane.key === 'drivers')"), - renderPaneSource.indexOf("if (activeToolCenterPane.key === 'snippet-settings')"), + renderPaneSource.indexOf("if (activeSettingsCenterPane.key === 'drivers')"), + renderPaneSource.indexOf("if (activeSettingsCenterPane.key === 'snippet-settings')"), ); const snippetSource = renderPaneSource.slice( - renderPaneSource.indexOf("if (activeToolCenterPane.key === 'snippet-settings')"), - renderPaneSource.indexOf("if (activeToolCenterPane.key === 'shortcut-settings')"), + renderPaneSource.indexOf("if (activeSettingsCenterPane.key === 'snippet-settings')"), + renderPaneSource.indexOf("if (activeSettingsCenterPane.key === 'shortcut-settings')"), ); const shortcutSource = renderPaneSource.slice( - renderPaneSource.indexOf("if (activeToolCenterPane.key === 'shortcut-settings')"), - renderPaneSource.indexOf('return null;', renderPaneSource.indexOf("if (activeToolCenterPane.key === 'shortcut-settings')")), + renderPaneSource.indexOf("if (activeSettingsCenterPane.key === 'shortcut-settings')"), + renderPaneSource.indexOf('return null;', renderPaneSource.indexOf("if (activeSettingsCenterPane.key === 'shortcut-settings')")), ); - expect(appSource).toContain('activeToolCenterPaneItem?.title ?? activeToolCenterGroup.title'); + expect(appSource).toContain('activeSettingsCenterPaneItem?.title ?? activeSettingsCenterGroup.title'); expect(connectionPackageSource).toContain(' { expect(shortcutSource).not.toContain('renderUtilityModalTitle'); }); - it('does not render an extra top back button in the tool center detail header', () => { + it('does not render an extra top back button in tool detail headers', () => { + const combinedGroupsIndex = appSource.indexOf('combinedSettingsCenterGroups.map'); const detailHeaderSource = appSource.slice( - appSource.indexOf("{activeToolCenterPane ? ("), - appSource.indexOf('
', appSource.indexOf("{activeToolCenterPane ? (")), + appSource.indexOf('{activeSettingsCenterPane ? (', combinedGroupsIndex), + appSource.indexOf('
'); expect(detailHeaderSource).not.toContain("{t('common.back_to_previous')}"); }); @@ -207,11 +220,10 @@ describe('tool center menu entries', () => { expect(appSource).toContain('const handleFocusSidebarSearch = useCallback('); expect(appSource).toContain('const antdTheme = useMemo(() => ({'); expect(appSource).toContain('theme={antdTheme}'); - expect(appSource).toContain('onOpenTools={handleOpenToolsModal}'); expect(appSource).toContain('onOpenSettings={handleOpenSettingsModal}'); expect(appSource).toContain('onToggleLogPanel={handleToggleLogPanel}'); expect(appSource).toContain('onFocusCommandSearch={handleFocusSidebarSearch}'); - expect(appSource).not.toContain('onOpenTools={() => setIsToolsModalOpen(true)}'); + expect(appSource).not.toContain('onOpenTools='); expect(appSource).not.toContain('onOpenSettings={() => setIsSettingsModalOpen(true)}'); expect(appSource).not.toContain('onToggleLogPanel={() => setIsLogPanelOpen((prev) => !prev)}'); expect(appSource).not.toContain('sqlLogCount={sqlLogCount}'); @@ -313,10 +325,10 @@ describe('tool center menu entries', () => { it('keeps connection modal warm-mounted while leaving the other heavyweight modals conditional', () => { expect(appSource).toContain('const [isConnectionModalMounted, setIsConnectionModalMounted] = useState(false);'); expect(appSource).toContain('{isConnectionModalMounted && ('); - expect(appSource).toContain('{isToolsModalOpen && ('); expect(appSource).toContain('{isSettingsModalOpen && ('); expect(appSource).toContain('{isThemeModalOpen && ('); - expect(appSource).toContain('{isShortcutModalOpen && ('); + expect(appSource).not.toContain('{isToolsModalOpen && ('); + expect(appSource).not.toContain('{isShortcutModalOpen && ('); expect(appSource).not.toContain('{isAISettingsOpen && ('); expect(appSource).toContain('{isDriverModalOpen && ('); expect(appSource).toContain('{isSyncModalOpen && ('); @@ -347,10 +359,10 @@ describe('tool center menu entries', () => { expect(appSource).not.toContain('setPrimaryPasswordVisible(String(config.password || "").trim() !== "")'); }); - it('keeps shortcut manager scrolling inside the modal body', () => { + it('keeps shortcut manager scrolling inside the embedded settings pane', () => { expect(appSource).toContain('centered'); - expect(appSource).toContain("height: 'min(760px, calc(100vh - 80px))'"); - expect(appSource).toContain("maxHeight: 'calc(100vh - 80px)'"); + expect(appSource).toContain("if (activeSettingsCenterPane.key === 'shortcut-settings')"); + expect(appSource).toContain('embedded'); expect(appSource).toContain("body: { paddingTop: 8, overflow: 'hidden', flex: 1, minHeight: 0 }"); expect(appSource).toContain('data-gonavi-shortcut-modal-scroll="true"'); expect(appSource).toContain("height: '100%'"); @@ -373,7 +385,7 @@ describe('tool center menu entries', () => { ['toggleAIPanel', 'toggleAIPanel();'], ['toggleLogPanel', 'handleToggleLogPanel();'], ['toggleTheme', 'selectPresetTheme('], - ['openShortcutManager', 'setIsShortcutModalOpen(true);'], + ['openShortcutManager', "handleOpenToolCenterPane('workspace', 'shortcut-settings');"], ['toggleMacFullscreen', 'handleTitleBarWindowToggle({ allowMacNativeFullscreen: true });'], ['resetWindowZoom', 'handleManualResetWindowZoom();'], ]); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d8bf03fd..971cc3e4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,7 @@ import Modal from './components/common/ResizableDraggableModal'; import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { Layout, Button, ConfigProvider, theme, message, Spin, Slider, Progress, Switch, Input, InputNumber, Select, Segmented, Tooltip, Alert } from 'antd'; -import { PlusOutlined, ConsoleSqlOutlined, UploadOutlined, DownloadOutlined, CloudDownloadOutlined, BugOutlined, ToolOutlined, GlobalOutlined, InfoCircleOutlined, GithubOutlined, SkinOutlined, CheckOutlined, MinusOutlined, BorderOutlined, CloseOutlined, SettingOutlined, LinkOutlined, BgColorsOutlined, AppstoreOutlined, RobotOutlined, FolderOpenOutlined, HddOutlined, SafetyCertificateOutlined, SwitcherOutlined, CodeOutlined, RightOutlined, TableOutlined, MenuOutlined, PoweroffOutlined, TagOutlined, UserOutlined, UpCircleOutlined, MessageOutlined, FileTextOutlined, SyncOutlined, SendOutlined, AuditOutlined } from '@ant-design/icons'; +import { PlusOutlined, ConsoleSqlOutlined, UploadOutlined, DownloadOutlined, CloudDownloadOutlined, BugOutlined, GlobalOutlined, InfoCircleOutlined, GithubOutlined, SkinOutlined, CheckOutlined, MinusOutlined, BorderOutlined, CloseOutlined, SettingOutlined, LinkOutlined, BgColorsOutlined, AppstoreOutlined, RobotOutlined, FolderOpenOutlined, HddOutlined, SafetyCertificateOutlined, SwitcherOutlined, CodeOutlined, RightOutlined, TableOutlined, MenuOutlined, PoweroffOutlined, TagOutlined, UserOutlined, UpCircleOutlined, MessageOutlined, FileTextOutlined, SyncOutlined, SendOutlined, AuditOutlined } from '@ant-design/icons'; import { DndContext, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent } from '@dnd-kit/core'; import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; @@ -471,18 +471,27 @@ type ToolCenterPaneKey = | 'snippet-settings' | 'shortcut-settings'; -type ToolCenterPaneState = { - key: ToolCenterPaneKey; - group: ToolCenterGroupKey; -}; - -type SettingsCenterGroupKey = 'preferences' | 'services' | 'about'; -type SettingsCenterPaneKey = 'language' | 'theme' | 'brand-icon' | 'sidebar-metadata' | 'sidebar-objects' | 'proxy' | 'web-auth' | 'ai' | 'about-go-navi'; +type SettingsCenterGroupKey = 'preferences' | 'services' | ToolCenterGroupKey | 'about'; +type SettingsCenterPaneKey = + | 'language' + | 'theme' + | 'brand-icon' + | 'sidebar-metadata' + | 'sidebar-objects' + | 'proxy' + | 'web-auth' + | 'ai' + | ToolCenterPaneKey + | 'about-go-navi'; type SettingsCenterPaneState = { key: SettingsCenterPaneKey; group: SettingsCenterGroupKey; }; +const isToolCenterGroupKey = (group: SettingsCenterGroupKey): group is ToolCenterGroupKey => ( + group === 'config' || group === 'workflow' || group === 'workspace' +); + const resolveSettingsCenterGroupInitialPane = (group: SettingsCenterGroupKey): SettingsCenterPaneState | null => ( group === 'about' ? { key: 'about-go-navi', group: 'about' } : null ); @@ -959,12 +968,16 @@ function App() { const [securityUpdateHasLegacySensitiveItems, setSecurityUpdateHasLegacySensitiveItems] = useState(false); const [isSecurityUpdateIntroOpen, setIsSecurityUpdateIntroOpen] = useState(false); const [isSecurityUpdateBannerDismissed, setIsSecurityUpdateBannerDismissed] = useState(false); - const [isSecurityUpdateSettingsOpen, setIsSecurityUpdateSettingsOpen] = useState(false); const [securityUpdateSettingsFocusTarget, setSecurityUpdateSettingsFocusTarget] = useState(null); const [securityUpdateSettingsFocusRequest, setSecurityUpdateSettingsFocusRequest] = useState(0); const [isSecurityUpdateProgressOpen, setIsSecurityUpdateProgressOpen] = useState(false); const [securityUpdateProgressStage, setSecurityUpdateProgressStage] = useState(() => t('app.security_update.stage.checking_saved_config')); const [securityUpdateRepairSource, setSecurityUpdateRepairSource] = useState(null); + const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); + const [activeSettingsCenterGroupKey, setActiveSettingsCenterGroupKey] = useState('preferences'); + const [activeSettingsCenterPane, setActiveSettingsCenterPane] = useState(null); + const activeSettingsCenterPaneRef = useRef(null); + activeSettingsCenterPaneRef.current = activeSettingsCenterPane; const [focusedTabDisplayElementKey, setFocusedTabDisplayElementKey] = useState(null); const [focusedAIProviderId, setFocusedAIProviderId] = useState(undefined); const [connectionPackageDialog, setConnectionPackageDialog] = useState(() => createClosedConnectionPackageDialogState()); @@ -1138,7 +1151,10 @@ function App() { setSecurityUpdateSettingsFocusTarget(resolveSecurityUpdateSettingsFocusTarget(nextStatus)); setSecurityUpdateSettingsFocusRequest((current) => current + 1); } - setIsSecurityUpdateSettingsOpen(true); + setToolCenterBackGroupKey('config'); + setActiveSettingsCenterGroupKey('config'); + setActiveSettingsCenterPane({ key: 'security-update', group: 'config' }); + setIsSettingsModalOpen(true); } return nextStatus; }, [normalizeSecurityUpdateStatus]); @@ -1915,11 +1931,16 @@ function App() { const saveQuery = useStore(state => state.saveQuery); const applicationQuitConfirmRef = useRef<{ destroy: () => void } | null>(null); const applicationQuitHandlingRef = useRef(false); - const openSecurityUpdateSettings = useCallback((focusTarget: SecurityUpdateSettingsFocusTarget | null = null) => { + const openSecurityUpdateSettings = useCallback((focusTarget?: SecurityUpdateSettingsFocusTarget | null) => { setIsSecurityUpdateIntroOpen(false); - setSecurityUpdateSettingsFocusTarget(focusTarget); - setSecurityUpdateSettingsFocusRequest((current) => current + 1); - setIsSecurityUpdateSettingsOpen(true); + if (focusTarget !== undefined) { + setSecurityUpdateSettingsFocusTarget(focusTarget); + setSecurityUpdateSettingsFocusRequest((current) => current + 1); + } + setToolCenterBackGroupKey('config'); + setActiveSettingsCenterGroupKey('config'); + setActiveSettingsCenterPane({ key: 'security-update', group: 'config' }); + setIsSettingsModalOpen(true); }, []); const handleOpenSecurityUpdateSettings = useCallback((focusTarget: SecurityUpdateSettingsFocusTarget | null = null) => { openSecurityUpdateSettings(focusTarget); @@ -1931,11 +1952,10 @@ function App() { : (mode === 'retry' ? t('app.security_update.stage.verifying_result') : t('app.security_update.stage.updating_secure_storage')); - const detailsWereOpen = isSecurityUpdateSettingsOpen; + const detailsWereOpen = isSettingsModalOpen && activeSettingsCenterPane?.key === 'security-update'; setSecurityUpdateProgressStage(stageText); setIsSecurityUpdateProgressOpen(true); setIsSecurityUpdateIntroOpen(false); - setIsSecurityUpdateSettingsOpen(false); let nextStatus: SecurityUpdateStatus | null = null; let shouldOpenSettings = false; @@ -1992,7 +2012,10 @@ function App() { console.warn('Failed to execute security update round', err); setIsSecurityUpdateProgressOpen(false); if (detailsWereOpen) { - setIsSecurityUpdateSettingsOpen(true); + setToolCenterBackGroupKey('config'); + setActiveSettingsCenterGroupKey('config'); + setActiveSettingsCenterPane({ key: 'security-update', group: 'config' }); + setIsSettingsModalOpen(true); } void message.error(err?.message || t('app.security_update.message.not_finished_retry_later')); return; @@ -2011,7 +2034,6 @@ function App() { if (nextStatus.overallStatus === 'completed') { setSecurityUpdateHasLegacySensitiveItems(false); setSecurityUpdateRawPayload(null); - setIsSecurityUpdateSettingsOpen(false); void message.success(t('app.security_update.message.completed')); } else if (nextStatus.overallStatus === 'needs_attention') { void message.warning(t('app.security_update.message.needs_attention')); @@ -2020,7 +2042,8 @@ function App() { } }, [ applySecurityUpdateStatus, - isSecurityUpdateSettingsOpen, + activeSettingsCenterPane?.key, + isSettingsModalOpen, normalizeSecurityUpdateStatus, replaceConnections, replaceGlobalProxy, @@ -2056,7 +2079,6 @@ function App() { if (nextStatus.overallStatus === 'completed') { setSecurityUpdateHasLegacySensitiveItems(false); setSecurityUpdateRawPayload(null); - setIsSecurityUpdateSettingsOpen(false); return; } @@ -2120,20 +2142,17 @@ function App() { return; } if (repairEntry.type === 'connection') { - setIsSecurityUpdateSettingsOpen(false); setSecurityUpdateRepairSource(repairEntry.repairSource); setEditingConnection(repairEntry.connection); setIsModalOpen(true); return; } if (repairEntry.type === 'proxy') { - setIsSecurityUpdateSettingsOpen(false); setSecurityUpdateRepairSource(repairEntry.repairSource); setIsProxyModalOpen(true); return; } if (repairEntry.type === 'ai') { - setIsSecurityUpdateSettingsOpen(false); setSecurityUpdateRepairSource(repairEntry.repairSource); setFocusedAIProviderId(repairEntry.providerId); setActiveSettingsCenterGroupKey('services'); @@ -2539,7 +2558,7 @@ function App() { setConnectionPackageDialog(createClosedConnectionPackageDialogState()); setPendingConnectionImportPayload(null); setToolCenterBackGroupKey(null); - setActiveToolCenterPane((current) => (current?.key === 'connection-package' ? null : current)); + setActiveSettingsCenterPane((current) => (current?.key === 'connection-package' ? null : current)); }, []); const refreshConnectionsAfterImport = useCallback(async (importedViews: SavedConnection[]) => { @@ -2622,7 +2641,8 @@ function App() { if (isConnectionPackagePasswordRequiredError(e)) { if (sourceGroup) { setToolCenterBackGroupKey(sourceGroup); - setActiveToolCenterPane({ key: 'connection-package', group: sourceGroup }); + setActiveSettingsCenterGroupKey(sourceGroup); + setActiveSettingsCenterPane({ key: 'connection-package', group: sourceGroup }); } setPendingConnectionImportPayload(raw); setConnectionPackageDialog({ @@ -2691,7 +2711,8 @@ function App() { setToolCenterBackGroupKey(sourceGroup ?? null); if (sourceGroup) { - setActiveToolCenterPane({ key: 'connection-package', group: sourceGroup }); + setActiveSettingsCenterGroupKey(sourceGroup); + setActiveSettingsCenterPane({ key: 'connection-package', group: sourceGroup }); } setConnectionPackageDialog({ open: true, @@ -2807,13 +2828,7 @@ function App() { } }; - const [isToolsModalOpen, setIsToolsModalOpen] = useState(false); - const [activeToolCenterGroupKey, setActiveToolCenterGroupKey] = useState('config'); const [toolCenterBackGroupKey, setToolCenterBackGroupKey] = useState(null); - const [activeToolCenterPane, setActiveToolCenterPane] = useState(null); - const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); - const [activeSettingsCenterGroupKey, setActiveSettingsCenterGroupKey] = useState('preferences'); - const [activeSettingsCenterPane, setActiveSettingsCenterPane] = useState(null); const [isThemeModalOpen, setIsThemeModalOpen] = useState(false); type ThemeSettingsSection = 'theme' | 'appearance' | 'workspace'; const THEME_SETTINGS_SECTION_STORAGE_KEY = 'gonavi.themeSettingsSection'; @@ -2840,8 +2855,6 @@ function App() { }, [themeModalSection]); const [isLinuxCJKFontBannerDismissed, setIsLinuxCJKFontBannerDismissed] = useState(false); const [isAppearanceModalOpen, setIsAppearanceModalOpen] = useState(false); - const [isShortcutModalOpen, setIsShortcutModalOpen] = useState(false); - const [isSnippetModalOpen, setIsSnippetModalOpen] = useState(false); const [capturingShortcutAction, setCapturingShortcutAction] = useState(null); const tabDisplaySettingsPanelRef = useRef(null); const [tabDisplaySettingsFocusRequest, setTabDisplaySettingsFocusRequest] = useState(0); @@ -3177,30 +3190,43 @@ function App() { effectiveUiScale, }) ), [aiPanelVisible, darkMode, effectiveUiScale]); + const clearSettingsCenterTransientPaneState = useCallback(() => { + setCapturingShortcutAction(null); + if (activeSettingsCenterPaneRef.current?.key === 'connection-package') { + closeConnectionPackageDialog(); + } + if (activeSettingsCenterPaneRef.current?.key === 'ai') { + setFocusedAIProviderId(undefined); + setSecurityUpdateRepairSource(null); + } + }, [closeConnectionPackageDialog]); const handleOpenToolsModal = useCallback((group: ToolCenterGroupKey = 'config') => { + clearSettingsCenterTransientPaneState(); setToolCenterBackGroupKey(null); - setActiveToolCenterPane(null); - setActiveToolCenterGroupKey(group); - setIsToolsModalOpen(true); - }, []); + setActiveSettingsCenterGroupKey(group); + setActiveSettingsCenterPane(null); + setIsSettingsModalOpen(true); + }, [clearSettingsCenterTransientPaneState]); const handleOpenSettingsModal = useCallback((group: SettingsCenterGroupKey = 'preferences') => { + clearSettingsCenterTransientPaneState(); setActiveSettingsCenterGroupKey(group); setActiveSettingsCenterPane(resolveSettingsCenterGroupInitialPane(group)); setIsSettingsModalOpen(true); - }, []); + }, [clearSettingsCenterTransientPaneState]); const handleOpenSettingsCenterPane = useCallback((group: SettingsCenterGroupKey, key: SettingsCenterPaneKey) => { + clearSettingsCenterTransientPaneState(); setActiveSettingsCenterGroupKey(group); setActiveSettingsCenterPane({ key, group }); setIsSettingsModalOpen(true); - }, []); + }, [clearSettingsCenterTransientPaneState]); const finalizeSecurityRepairReturnFromAISettings = useCallback(() => { const reopenSecurityUpdateDetails = shouldReopenSecurityUpdateDetails(securityUpdateRepairSource); setFocusedAIProviderId(undefined); setSecurityUpdateRepairSource(null); if (reopenSecurityUpdateDetails) { - setIsSecurityUpdateSettingsOpen(true); + openSecurityUpdateSettings(); } - }, [securityUpdateRepairSource]); + }, [openSecurityUpdateSettings, securityUpdateRepairSource]); const handleBackFromSettingsCenterPane = useCallback(() => { const leavingAI = activeSettingsCenterPane?.key === 'ai'; const returnGroup = activeSettingsCenterPane?.group ?? activeSettingsCenterGroupKey; @@ -3217,12 +3243,17 @@ function App() { ]); const handleCancelSettingsCenterPane = useCallback(() => { const leavingAI = activeSettingsCenterPane?.key === 'ai'; + if (activeSettingsCenterPane?.key === 'connection-package') { + closeConnectionPackageDialog(); + } + setCapturingShortcutAction(null); + setToolCenterBackGroupKey(null); setActiveSettingsCenterPane(null); setIsSettingsModalOpen(false); if (leavingAI) { finalizeSecurityRepairReturnFromAISettings(); } - }, [activeSettingsCenterPane?.key, finalizeSecurityRepairReturnFromAISettings]); + }, [activeSettingsCenterPane?.key, closeConnectionPackageDialog, finalizeSecurityRepairReturnFromAISettings]); const isSettingsAboutPaneOpen = isSettingsModalOpen && activeSettingsCenterPane?.key === 'about-go-navi'; const isSettingsAboutPaneOpenRef = useRef(false); useEffect(() => { @@ -3250,27 +3281,22 @@ function App() { prepareAboutSurface(); }, [isSettingsAboutPaneOpen, prepareAboutSurface]); const handleOpenToolCenterPane = useCallback((group: ToolCenterGroupKey, key: ToolCenterPaneKey) => { + clearSettingsCenterTransientPaneState(); setToolCenterBackGroupKey(group); - setActiveToolCenterGroupKey(group); - setActiveToolCenterPane({ key, group }); - setIsToolsModalOpen(true); - }, []); + setActiveSettingsCenterGroupKey(group); + setActiveSettingsCenterPane({ key, group }); + setIsSettingsModalOpen(true); + }, [clearSettingsCenterTransientPaneState]); const handleReturnToToolCenter = useCallback((closeChild?: () => void) => { const returnGroup = toolCenterBackGroupKey ?? 'config'; closeChild?.(); setToolCenterBackGroupKey(null); - setActiveToolCenterGroupKey(returnGroup); - setActiveToolCenterPane(null); - setIsToolsModalOpen(true); + setActiveSettingsCenterGroupKey(returnGroup); + setActiveSettingsCenterPane(null); + setIsSettingsModalOpen(true); }, [toolCenterBackGroupKey]); const sidebarUtilityItems = useMemo(() => { const itemMap = { - tools: { - key: 'tools', - title: t('app.sidebar.tools'), - icon: , - onClick: () => handleOpenToolsModal(), - }, settings: { key: 'settings', title: t('app.sidebar.settings'), @@ -3280,7 +3306,7 @@ function App() { } as const; return SIDEBAR_UTILITY_ITEM_KEYS.map((key) => itemMap[key]); - }, [handleOpenSettingsModal, handleOpenToolsModal, t]); + }, [handleOpenSettingsModal, t]); const handleFocusSidebarSearch = useCallback(() => { window.dispatchEvent(new CustomEvent('gonavi:focus-sidebar-search')); }, []); @@ -3317,11 +3343,11 @@ function App() { }, [t]); useEffect(() => { - if (!isDataRootModalOpen && activeToolCenterPane?.key !== 'data-root') { + if (!isDataRootModalOpen && activeSettingsCenterPane?.key !== 'data-root') { return; } void loadDataRootInfo(); - }, [activeToolCenterPane?.key, isDataRootModalOpen, loadDataRootInfo]); + }, [activeSettingsCenterPane?.key, isDataRootModalOpen, loadDataRootInfo]); const handleSelectDataRoot = useCallback(async () => { try { @@ -3532,7 +3558,7 @@ function App() { setEditingConnection(null); setSecurityUpdateRepairSource(null); if (reopenSecurityUpdateDetails) { - setIsSecurityUpdateSettingsOpen(true); + openSecurityUpdateSettings(); } }; @@ -3549,9 +3575,9 @@ function App() { setToolCenterBackGroupKey(null); setSecurityUpdateRepairSource(null); if (reopenSecurityUpdateDetails) { - setIsSecurityUpdateSettingsOpen(true); + openSecurityUpdateSettings(); } - }, [securityUpdateRepairSource]); + }, [openSecurityUpdateSettings, securityUpdateRepairSource]); const handleOpenGlobalProxySettings = useCallback(() => { setSecurityUpdateRepairSource(null); @@ -3563,9 +3589,9 @@ function App() { setIsProxyModalOpen(false); setSecurityUpdateRepairSource(null); if (reopenSecurityUpdateDetails) { - setIsSecurityUpdateSettingsOpen(true); + openSecurityUpdateSettings(); } - }, [securityUpdateRepairSource]); + }, [openSecurityUpdateSettings, securityUpdateRepairSource]); /** 从聊天面板等入口打开 AI 配置:走设置中心,不再弹独立 AISettingsModal */ const handleOpenAISettings = useCallback((providerId?: string) => { @@ -3757,17 +3783,16 @@ function App() { useEffect(() => { const handleOpenShortcutSettingsEvent = () => { - setIsShortcutModalOpen(true); + handleOpenToolCenterPane('workspace', 'shortcut-settings'); }; window.addEventListener('gonavi:open-shortcut-settings', handleOpenShortcutSettingsEvent as EventListener); return () => { window.removeEventListener('gonavi:open-shortcut-settings', handleOpenShortcutSettingsEvent as EventListener); }; - }, []); + }, [handleOpenToolCenterPane]); useEffect(() => { const handleOpenSnippetSettingsEvent = () => { - setIsSnippetModalOpen(false); handleOpenToolCenterPane('workspace', 'snippet-settings'); }; window.addEventListener('gonavi:open-snippet-settings', handleOpenSnippetSettingsEvent as EventListener); @@ -3877,7 +3902,7 @@ function App() { selectPresetTheme(themeMode === 'dark' ? 'light' : 'dark'); break; case 'openShortcutManager': - setIsShortcutModalOpen(true); + handleOpenToolCenterPane('workspace', 'shortcut-settings'); break; case 'toggleMacFullscreen': if (isMacRuntime && useNativeMacWindowControls) { @@ -3894,7 +3919,7 @@ function App() { return () => { window.removeEventListener('keydown', handleGlobalShortcut, true); }; - }, [activeShortcutPlatform, handleCreateConnection, handleManualResetWindowZoom, handleNewQuery, handleTitleBarWindowToggle, handleToggleLogPanel, isMacRuntime, selectPresetTheme, shortcutOptions, switchActiveTabByOffset, themeMode, toggleAIPanel, useNativeMacWindowControls]); + }, [activeShortcutPlatform, handleCreateConnection, handleManualResetWindowZoom, handleNewQuery, handleOpenToolCenterPane, handleTitleBarWindowToggle, handleToggleLogPanel, isMacRuntime, selectPresetTheme, shortcutOptions, switchActiveTabByOffset, themeMode, toggleAIPanel, useNativeMacWindowControls]); useEffect(() => { if (!capturingShortcutAction) { @@ -6531,7 +6556,21 @@ function App() { isV2Ui ? renderThemeSettingsContentV2() : renderThemeSettingsContentLegacy() ); - const settingsCenterGroups = [ + type SettingsCenterNavigationGroup = { + key: SettingsCenterGroupKey; + icon: React.ReactNode; + title: string; + description: string; + items: ReadonlyArray<{ + key: string; + icon: React.ReactNode; + title: string; + description: string; + onClick: () => void; + }>; + }; + + const settingsCenterGroups: SettingsCenterNavigationGroup[] = [ { key: 'preferences' as const, icon: , @@ -6627,12 +6666,6 @@ function App() { ], }, ]; - const activeSettingsCenterGroup = settingsCenterGroups.find((group) => group.key === activeSettingsCenterGroupKey) ?? settingsCenterGroups[0]; - const activeSettingsCenterPaneItem = activeSettingsCenterPane - ? settingsCenterGroups - .find((group) => group.key === activeSettingsCenterPane.group) - ?.items.find((item) => item.key === activeSettingsCenterPane.key) - : null; const isSettingsCenterContainedScrollPane = activeSettingsCenterPane?.key === 'theme' || activeSettingsCenterPane?.key === 'ai'; const isV2ThemeSettingsPane = isV2Ui && activeSettingsCenterPane?.key === 'theme'; @@ -6892,7 +6925,6 @@ function App() { )} - {isToolsModalOpen && (() => { - const toolCenterGroups = [ + {isSettingsModalOpen && (() => { + const toolCenterGroups: SettingsCenterNavigationGroup[] = [ { key: 'config', icon: , @@ -7304,42 +7336,46 @@ function App() { title: t('app.tools.entry.sql_audit.title'), description: t('app.tools.entry.sql_audit.description'), onClick: () => { - setIsToolsModalOpen(false); - setActiveToolCenterPane(null); - setToolCenterBackGroupKey(null); + handleCancelSettingsCenterPane(); addTab(buildSqlAuditWorkbenchTab()); }, }, ], }, - ] as const; - const filteredToolCenterGroups = toolCenterGroups.map((group) => ({ - ...group, - items: group.items, - })).filter((group) => group.items.length > 0); - const activeToolCenterGroup = filteredToolCenterGroups.find((group) => group.key === activeToolCenterGroupKey) ?? filteredToolCenterGroups[0]; - const activeToolCenterPaneItem = activeToolCenterPane - ? filteredToolCenterGroups - .find((group) => group.key === activeToolCenterPane.group) - ?.items.find((item) => item.key === activeToolCenterPane.key) + ]; + const combinedSettingsCenterGroups = [ + ...settingsCenterGroups.filter((group) => group.key !== 'about'), + ...toolCenterGroups, + ...settingsCenterGroups.filter((group) => group.key === 'about'), + ]; + const activeSettingsCenterGroup = combinedSettingsCenterGroups.find( + (group) => group.key === activeSettingsCenterGroupKey, + ) ?? combinedSettingsCenterGroups[0]; + const activeSettingsCenterPaneItem = activeSettingsCenterPane + ? combinedSettingsCenterGroups + .find((group) => group.key === activeSettingsCenterPane.group) + ?.items.find((item) => item.key === activeSettingsCenterPane.key) : null; - if (!activeToolCenterGroup) { + const isActiveToolCenterPane = activeSettingsCenterPane + ? isToolCenterGroupKey(activeSettingsCenterPane.group) + : false; + if (!activeSettingsCenterGroup) { return null; } const closeToolCenterPane = () => { - if (activeToolCenterPane?.key === 'connection-package') { + if (activeSettingsCenterPane?.key === 'connection-package') { closeConnectionPackageDialog(); return; } setToolCenterBackGroupKey(null); - setActiveToolCenterPane(null); + setActiveSettingsCenterPane(null); }; const renderToolCenterPane = () => { - if (!activeToolCenterPane) { + if (!activeSettingsCenterPane || !isToolCenterGroupKey(activeSettingsCenterPane.group)) { return null; } - if (activeToolCenterPane.key === 'connection-package') { + if (activeSettingsCenterPane.key === 'connection-package') { return ( @@ -7497,7 +7533,7 @@ function App() { ); } - if (activeToolCenterPane.key === 'security-update') { + if (activeSettingsCenterPane.key === 'security-update') { return ( , t('app.tools.title'), t('app.tools.description'))} - open={isToolsModalOpen} - onCancel={() => { - if (activeToolCenterPane?.key === 'connection-package') { - closeConnectionPackageDialog(); - } - setActiveToolCenterPane(null); - setToolCenterBackGroupKey(null); - setIsToolsModalOpen(false); - }} + title={renderUtilityModalTitle(, t('app.settings.title'), t('app.settings.description'))} + open={isSettingsModalOpen} + onCancel={handleCancelSettingsCenterPane} footer={null} centered width={1080} @@ -7702,8 +7731,8 @@ function App() {
- {toolCenterGroups.map((group) => { - const active = group.key === activeToolCenterGroup.key; + {combinedSettingsCenterGroups.map((group) => { + const active = group.key === activeSettingsCenterGroup.key; return (
- {activeToolCenterPane ? ( + {activeSettingsCenterPane ? (
- {activeToolCenterPaneItem?.title ?? activeToolCenterGroup.title} + {activeSettingsCenterPaneItem?.title ?? activeSettingsCenterGroup.title}
- {activeToolCenterPaneItem?.description ?? activeToolCenterGroup.description} + {activeSettingsCenterPaneItem?.description ?? activeSettingsCenterGroup.description}
-
- {renderToolCenterPane()} +
+ {isActiveToolCenterPane ? renderToolCenterPane() : renderSettingsCenterPane()}
+ {!isActiveToolCenterPane && ( +
+ {activeSettingsCenterPane.key === 'about-go-navi' ? ( + renderSettingsCenterAboutFooter() + ) : isV2Ui && activeSettingsCenterPane.key === 'theme' ? ( + <> + + + + ) : ( + <> + + + + )} +
+ )}
) : ( <>
-
{activeToolCenterGroup.title}
-
{activeToolCenterGroup.description}
+
{activeSettingsCenterGroup.title}
+
{activeSettingsCenterGroup.description}
- {activeToolCenterGroup.items.map((item, index) => ( + {activeSettingsCenterGroup.items.map((item, index) => ( - ); - })} -
-
-
- {activeSettingsCenterPane ? ( -
-
-
-
- {activeSettingsCenterPaneItem?.title ?? activeSettingsCenterGroup.title} -
-
- {activeSettingsCenterPaneItem?.description ?? activeSettingsCenterGroup.description} -
-
-
-
- {renderSettingsCenterPane()} -
-
- {activeSettingsCenterPane.key === 'about-go-navi' ? ( - renderSettingsCenterAboutFooter() - ) : isV2Ui && activeSettingsCenterPane.key === 'theme' ? ( - <> - - - - ) : ( - <> - - - - )} -
-
- ) : ( - <> -
-
{activeSettingsCenterGroup.title}
-
{activeSettingsCenterGroup.description}
-
-
- {activeSettingsCenterGroup.items.map((item, index) => ( - - ))} -
- - )} -
-
-
- - )} {isDataRootModalOpen && ( handleOpenSecurityUpdateSettings()} /> - { - setIsSecurityUpdateSettingsOpen(false); - setToolCenterBackGroupKey(null); - }} - onBack={toolCenterBackGroupKey === 'config' ? () => handleReturnToToolCenter(() => setIsSecurityUpdateSettingsOpen(false)) : undefined} - onStart={handleStartSecurityUpdate} - onRetry={handleRetrySecurityUpdate} - onRestart={handleRestartSecurityUpdate} - onIssueAction={handleSecurityUpdateIssueAction} - /> )} - {isShortcutModalOpen && ( - , - t('app.shortcuts.title'), - t('app.shortcuts.description'), - )} - open={isShortcutModalOpen} - onCancel={() => { - setIsShortcutModalOpen(false); - setCapturingShortcutAction(null); - setToolCenterBackGroupKey(null); - }} - width={760} - centered - style={{ top: 0, maxHeight: 'calc(100vh - 80px)' }} - styles={{ - content: { - ...utilityModalShellStyle, - height: 'min(760px, calc(100vh - 80px))', - display: 'flex', - flexDirection: 'column', - }, - header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, - body: { paddingTop: 8, overflow: 'hidden', flex: 1, minHeight: 0 }, - footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } - }} - footer={[ - , - , - toolCenterBackGroupKey === 'workspace' ? ( - - ) : null, - ]} - > -
-
-
- {t('app.shortcuts.capture_hint')} -
-
- {SHORTCUT_ACTION_ORDER.map((action) => { - const meta = SHORTCUT_ACTION_META[action]; - if (meta.platformOnly === 'mac' && !isMacRuntime) { - return null; - } - const binding = resolveShortcutBinding(shortcutOptions, action, activeShortcutPlatform); - const isCapturing = capturingShortcutAction === action; - const conflicts = shortcutConflictMap[action]; - const conflictInfo = conflicts?.length ? splitConflictsByContext(conflicts) : null; - return ( -
-
-
{meta.label}
-
{meta.description}
- {conflictInfo && ( -
- {conflictInfo.hasMonaco && ( - <>⚠ {t('app.shortcuts.message.reserved_conflict_info', { labels: conflictInfo.monacoLabels })} - )} - {conflictInfo.hasOther && ( - <>⚠ {t('app.shortcuts.message.reserved_conflict_warning', { contexts: conflictInfo.otherContexts, labels: conflictInfo.otherLabels })} - )} -
- )} -
-
- - - updateShortcut(action, { enabled: checked }, activeShortcutPlatform)} - /> -
-
- ); - })} -
-
- )} - {isSnippetModalOpen && ( - { - setIsSnippetModalOpen(false); - setToolCenterBackGroupKey(null); - }} - onBack={toolCenterBackGroupKey === 'workspace' ? () => handleReturnToToolCenter(() => setIsSnippetModalOpen(false)) : undefined} - darkMode={darkMode} - overlayTheme={overlayTheme} - /> - )} {isProxyModalOpen && ( , t('app.proxy.title'), t('app.proxy.description'))} diff --git a/frontend/src/components/Sidebar.locate-toolbar.test.tsx b/frontend/src/components/Sidebar.locate-toolbar.test.tsx index 00688b47..3d261be7 100644 --- a/frontend/src/components/Sidebar.locate-toolbar.test.tsx +++ b/frontend/src/components/Sidebar.locate-toolbar.test.tsx @@ -984,6 +984,17 @@ describe('Sidebar locate toolbar', () => { expect(source).not.toContain("justifyContent: 'space-between', borderTop: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}`, borderBottom: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}`, background: darkMode ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.015)' }}>"); }); + it('keeps only the unified settings entry in sidebar utility areas', () => { + const sidebarSource = readSourceFile('./Sidebar.tsx'); + const railSource = readSourceFile('./sidebar/SidebarConnectionRail.tsx'); + + expect(sidebarSource).not.toContain('onOpenTools'); + expect(sidebarSource).not.toContain('openTools:'); + expect(railSource).not.toContain('handlers.openTools'); + expect(railSource).not.toContain('data-gonavi-open-tools-action'); + expect(railSource).toContain('handlers.openSettings'); + }); + it('renders the v2 sidebar rail, command search hint, filter tabs and slow-query footer', () => { const markup = renderSidebarMarkup({ uiVersion: 'v2', onCreateConnection: mocks.noop }); const source = readSidebarSource(); @@ -1042,9 +1053,10 @@ describe('Sidebar locate toolbar', () => { expect(markup).toContain('data-gonavi-create-connection-action="true"'); expect(markup).toContain('aria-label="AI 助手"'); expect(markup).toContain('data-gonavi-ai-entry-action="true"'); - expect(markup).toContain('aria-label="工具"'); - expect(markup).toContain('data-gonavi-open-tools-action="true"'); + expect(markup).not.toContain('aria-label="工具"'); + expect(markup).not.toContain('data-gonavi-open-tools-action="true"'); expect(markup).toContain('aria-label="设置"'); + expect(source).not.toContain('handlers.openTools'); expect(source).toContain('export const buildV2RailConnectionGroups = ('); expect(source).toContain("if (menu.kind === 'v2-connection-group') return renderV2ConnectionGroupContextMenu(menu.node);"); expect(source).toContain('openV2ConnectionContextMenu(event, node);'); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index ed975c36..c126550b 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -103,7 +103,6 @@ import { Tree, message, Dropdown, MenuProps, Input, Button, Form, Popover, Radio WarningOutlined, AimOutlined, MoreOutlined, - ToolOutlined, SettingOutlined } from '@ant-design/icons'; import { @@ -497,7 +496,6 @@ export const buildAllSavedQueriesTreeNode = ( const Sidebar: React.FC<{ onCreateConnection?: () => void; onEditConnection?: (conn: SavedConnection) => void; - onOpenTools?: () => void; onOpenSettings?: () => void; onToggleAI?: () => void; onToggleLogPanel?: () => void; @@ -506,7 +504,6 @@ const Sidebar: React.FC<{ }> = React.memo(({ onCreateConnection, onEditConnection, - onOpenTools, onOpenSettings, onToggleAI, onToggleLogPanel, @@ -2920,7 +2917,6 @@ const Sidebar: React.FC<{ const v2LocateCurrentTableLabel = t('sidebar.action.locate_current_table'); const v2LocateCurrentTableUnavailableLabel = t('sidebar.message.locate_current_table_unavailable'); const v2AiAssistantLabel = t('app.sidebar.ai_assistant'); - const v2ToolsLabel = t('app.sidebar.tools'); const v2SettingsLabel = t('app.sidebar.settings'); const v2ActiveConnectionHeaderLabel = t('sidebar.active_connection.current_host_database'); const v2NoDatabaseSelectedLabel = t('sidebar.active_connection.no_database_selected'); @@ -2969,7 +2965,6 @@ const Sidebar: React.FC<{ locateCurrentTable: v2LocateCurrentTableLabel, locateCurrentTableUnavailable: v2LocateCurrentTableUnavailableLabel, aiAssistant: v2AiAssistantLabel, - tools: v2ToolsLabel, settings: v2SettingsLabel, }, handlers: { @@ -2979,7 +2974,6 @@ const Sidebar: React.FC<{ openExternalSqlFile: handleOpenSQLFileFromToolbar, locateActiveTab: handleLocateActiveTabInSidebar, toggleAI: onToggleAI ?? (() => {}), - openTools: onOpenTools ?? (() => {}), openSettings: onOpenSettings ?? (() => {}), }, canLocateActiveTab, diff --git a/frontend/src/components/audit/SqlAuditWorkbench.wiring.test.ts b/frontend/src/components/audit/SqlAuditWorkbench.wiring.test.ts index 261f86b3..f3212a36 100644 --- a/frontend/src/components/audit/SqlAuditWorkbench.wiring.test.ts +++ b/frontend/src/components/audit/SqlAuditWorkbench.wiring.test.ts @@ -15,7 +15,7 @@ describe('SQL audit workbench wiring', () => { expect(workbenchSource).toContain(''); }); - it('provides both the V2 footer shortcut and the cross-version tool-center entry', () => { + it('provides both the V2 footer shortcut and the cross-version settings-center entry', () => { const sidebarSource = read('../Sidebar.tsx'); const railSource = read('../sidebar/SqlAuditRailButton.tsx'); const appSource = read('../../App.tsx'); @@ -24,7 +24,7 @@ describe('SQL audit workbench wiring', () => { expect(railSource).toContain('buildSqlAuditWorkbenchTab()'); expect(appSource).toContain("key: 'sql-audit'"); expect(appSource).toContain('addTab(buildSqlAuditWorkbenchTab())'); - expect(appSource).toContain('setIsToolsModalOpen(false)'); + expect(appSource).toContain('handleCancelSettingsCenterPane();'); }); it('registers audit labels for docked and detached tab presentations', () => { diff --git a/frontend/src/components/sidebar/SidebarConnectionRail.tsx b/frontend/src/components/sidebar/SidebarConnectionRail.tsx index 12ed98d9..524b0c51 100644 --- a/frontend/src/components/sidebar/SidebarConnectionRail.tsx +++ b/frontend/src/components/sidebar/SidebarConnectionRail.tsx @@ -7,7 +7,6 @@ import { FileAddOutlined, AimOutlined, RobotOutlined, - ToolOutlined, SettingOutlined, } from '@ant-design/icons'; @@ -31,7 +30,6 @@ export interface SidebarConnectionRailProps { locateCurrentTable: string; locateCurrentTableUnavailable: string; aiAssistant: string; - tools: string; settings: string; }; handlers: { @@ -41,7 +39,6 @@ export interface SidebarConnectionRailProps { openExternalSqlFile: () => void; locateActiveTab: () => void; toggleAI: () => void; - openTools: () => void; openSettings: () => void; }; canLocateActiveTab: boolean; @@ -121,17 +118,6 @@ const SidebarConnectionRail: React.FC = ({ labels, h - - -