From dd8af73887bd3d5b22b2567f9191c63a6ba97fb0 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Tue, 2 Jun 2026 17:10:31 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(sidebar):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20v2=20=E6=90=9C=E7=B4=A2=E5=85=B3=E9=97=AD=E4=BA=A4?= =?UTF-8?q?=E4=BA=92=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 关闭命令搜索前提交同步筛选值,避免输入框清空覆盖侧栏筛选 - 限制弹窗打开期间才同步命令输入到侧栏持久筛选 - 增加全局 ESC 关闭监听,修复焦点离开弹窗后无法关闭 - 补充回归测试覆盖筛选保留和全局 ESC 关闭规则 --- .../Sidebar.locate-toolbar.test.tsx | 49 +++++++++++++ frontend/src/components/Sidebar.tsx | 72 ++++++++++++++++++- 2 files changed, 118 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Sidebar.locate-toolbar.test.tsx b/frontend/src/components/Sidebar.locate-toolbar.test.tsx index 0a630e7..13dd899 100644 --- a/frontend/src/components/Sidebar.locate-toolbar.test.tsx +++ b/frontend/src/components/Sidebar.locate-toolbar.test.tsx @@ -14,6 +14,7 @@ import Sidebar, { hasSidebarLazyChildren, normalizeSidebarTreeRelativeDropPosition, parseV2CommandSearchQuery, + resolveV2CommandSearchPersistentFilter, type V2CommandSearchItem, resolveSidebarDropNodeFromDomEvent, resolveSidebarTagDropInsertBefore, @@ -27,6 +28,7 @@ import Sidebar, { shouldSkipSidebarLoadOnExpandWhileDragging, shouldSkipSidebarSelectWhileDragging, shouldLoadSidebarNodeOnExpand, + shouldCloseV2CommandSearchOnGlobalKey, shouldRunV2CommandSearchEnter, sortSidebarTableEntries, } from './Sidebar'; @@ -302,6 +304,51 @@ describe('Sidebar locate toolbar', () => { })).toBe(false); }); + it('keeps v2 command search persisted filter after closing the palette', () => { + expect(resolveV2CommandSearchPersistentFilter({ + commandSearchValue: ' org ', + persistedFilter: '', + enabled: true, + isOpen: true, + })).toBe('org'); + + expect(resolveV2CommandSearchPersistentFilter({ + commandSearchValue: '', + persistedFilter: 'org', + enabled: true, + isOpen: false, + })).toBe('org'); + + expect(resolveV2CommandSearchPersistentFilter({ + commandSearchValue: 'org', + persistedFilter: 'org', + enabled: false, + isOpen: true, + })).toBe(''); + }); + + it('closes v2 command search on global escape only while the palette is open', () => { + expect(shouldCloseV2CommandSearchOnGlobalKey({ + key: 'Escape', + isOpen: true, + })).toBe(true); + + expect(shouldCloseV2CommandSearchOnGlobalKey({ + key: 'Esc', + isOpen: true, + })).toBe(true); + + expect(shouldCloseV2CommandSearchOnGlobalKey({ + key: 'Escape', + isOpen: false, + })).toBe(false); + + expect(shouldCloseV2CommandSearchOnGlobalKey({ + key: 'Enter', + isOpen: true, + })).toBe(false); + }); + it('keeps all loaded v2 command table matches once a keyword is entered', () => { const items: V2CommandSearchItem[] = Array.from({ length: 40 }, (_, index) => ({ key: `node-table-${index}`, @@ -538,6 +585,8 @@ describe('Sidebar locate toolbar', () => { expect(source).toContain('handleV2CommandSearchValueChange(event.target.value)'); expect(source).toContain('toggleV2CommandSearchPersistentFilter'); expect(source).toContain('gn-v2-command-filter-switch'); + expect(source).toContain("window.addEventListener('keydown', handleV2CommandSearchGlobalKeyDown, true)"); + expect(source).toContain("window.removeEventListener('keydown', handleV2CommandSearchGlobalKeyDown, true)"); expect(source).toContain('onClick={() => setV2ExplorerFilter(item.key)}'); expect(source).toContain('treeData={isV2Ui ? v2VisibleTreeData : displayTreeData}'); expect(markup).toContain('gn-v2-sidebar-log-footer'); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 6d64db6..34146ba 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -648,6 +648,38 @@ export const shouldRunV2CommandSearchEnter = ({ return activeItemCount > 0; }; +export interface V2CommandSearchPersistentFilterState { + commandSearchValue: string; + persistedFilter: string; + enabled: boolean; + isOpen: boolean; +} + +export const resolveV2CommandSearchPersistentFilter = ({ + commandSearchValue, + persistedFilter, + enabled, + isOpen, +}: V2CommandSearchPersistentFilterState): string => { + if (!enabled) return ''; + if (!isOpen) return String(persistedFilter ?? '').trim(); + return String(commandSearchValue ?? '').trim(); +}; + +export interface V2CommandSearchGlobalKeyState { + key: string; + isOpen: boolean; +} + +export const shouldCloseV2CommandSearchOnGlobalKey = ({ + key, + isOpen, +}: V2CommandSearchGlobalKeyState): boolean => { + if (!isOpen) return false; + const normalizedKey = String(key || '').toLowerCase(); + return normalizedKey === 'escape' || normalizedKey === 'esc'; +}; + export const resolveSidebarConnectionIdFromKey = ( key: unknown, connectionIds: string[], @@ -1156,11 +1188,23 @@ const Sidebar: React.FC<{ setV2CommandActiveIndex(0); }, []); + const commitV2CommandSearchPersistentFilter = useCallback((value = v2CommandSearchValue) => { + if (!v2CommandSearchPersistentFilterEnabled) { + return; + } + const nextFilter = value.trim(); + setSearchValue(nextFilter); + if (nextFilter !== v2PersistedSidebarFilter) { + setAppearance({ v2SidebarPersistedFilter: nextFilter }); + } + }, [setAppearance, v2CommandSearchPersistentFilterEnabled, v2CommandSearchValue, v2PersistedSidebarFilter]); + const closeV2CommandSearch = useCallback(() => { + commitV2CommandSearchPersistentFilter(); setIsV2CommandSearchOpen(false); setV2CommandSearchValue(''); setV2CommandActiveIndex(0); - }, []); + }, [commitV2CommandSearchPersistentFilter]); useEffect(() => { setSearchValue(v2PersistedSidebarFilter); @@ -1184,13 +1228,21 @@ const Sidebar: React.FC<{ if (!v2CommandSearchPersistentFilterEnabled) { return; } - const nextFilter = deferredV2CommandSearchValue.trim(); + if (!isV2CommandSearchOpen) { + return; + } + const nextFilter = resolveV2CommandSearchPersistentFilter({ + commandSearchValue: deferredV2CommandSearchValue, + persistedFilter: v2PersistedSidebarFilter, + enabled: v2CommandSearchPersistentFilterEnabled, + isOpen: isV2CommandSearchOpen, + }); setSearchValue(nextFilter); const timer = window.setTimeout(() => { setAppearance({ v2SidebarPersistedFilter: nextFilter }); }, 160); return () => window.clearTimeout(timer); - }, [deferredV2CommandSearchValue, setAppearance, v2CommandSearchPersistentFilterEnabled]); + }, [deferredV2CommandSearchValue, isV2CommandSearchOpen, setAppearance, v2CommandSearchPersistentFilterEnabled, v2PersistedSidebarFilter]); const toggleV2CommandSearchPersistentFilter = useCallback((enabled: boolean) => { const nextFilter = enabled ? v2CommandSearchValue.trim() : ''; @@ -1264,6 +1316,20 @@ const Sidebar: React.FC<{ }, 0); return () => window.clearTimeout(timer); }, [isV2CommandSearchOpen]); + + useEffect(() => { + if (!isV2CommandSearchOpen) return; + const handleV2CommandSearchGlobalKeyDown = (event: KeyboardEvent) => { + if (!shouldCloseV2CommandSearchOnGlobalKey({ key: event.key, isOpen: isV2CommandSearchOpen })) { + return; + } + event.preventDefault(); + event.stopPropagation(); + closeV2CommandSearch(); + }; + window.addEventListener('keydown', handleV2CommandSearchGlobalKeyDown, true); + return () => window.removeEventListener('keydown', handleV2CommandSearchGlobalKeyDown, true); + }, [closeV2CommandSearch, isV2CommandSearchOpen]); // Connection Status State: key -> 'success' | 'error' const [connectionStates, setConnectionStates] = useState>({});