From 8c88017703c4af820bd4d75b13bcba5b9b5fa0d1 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Tue, 2 Jun 2026 13:40:48 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20perf(sidebar):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20v2=20=E5=91=BD=E4=BB=A4=E6=90=9C=E7=B4=A2=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E5=92=8C=E7=BB=93=E6=9E=9C=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复中文输入法组合输入时按 Enter 误关闭搜索弹窗 - 限制搜索弹窗关闭方式为 ESC 或有效结果确认 - 移除关键词搜索下已加载表结果的固定条数截断 - 同步筛选开启时使用 deferred 值和防抖持久化,降低输入卡顿 - 补充命令搜索 Enter 判定和表匹配完整性测试 --- .../Sidebar.locate-toolbar.test.tsx | 77 +++++++++++ frontend/src/components/Sidebar.tsx | 129 +++++++++++------- 2 files changed, 160 insertions(+), 46 deletions(-) diff --git a/frontend/src/components/Sidebar.locate-toolbar.test.tsx b/frontend/src/components/Sidebar.locate-toolbar.test.tsx index 60459c2..58790f4 100644 --- a/frontend/src/components/Sidebar.locate-toolbar.test.tsx +++ b/frontend/src/components/Sidebar.locate-toolbar.test.tsx @@ -8,11 +8,13 @@ import Sidebar, { buildV2SidebarTableSectionedChildren, buildV2RailConnectionGroups, estimateV2TreeHorizontalScrollWidth, + filterV2CommandSearchTreeItems, filterV2ExplorerTreeByKind, getV2RailConnectionGroupBadgeText, hasSidebarLazyChildren, normalizeSidebarTreeRelativeDropPosition, parseV2CommandSearchQuery, + type V2CommandSearchItem, resolveSidebarDropNodeFromDomEvent, resolveSidebarTagDropInsertBefore, resolveSidebarDropTargetMetricsFromDomEvent, @@ -25,6 +27,7 @@ import Sidebar, { shouldSkipSidebarLoadOnExpandWhileDragging, shouldSkipSidebarSelectWhileDragging, shouldLoadSidebarNodeOnExpand, + shouldRunV2CommandSearchEnter, sortSidebarTableEntries, } from './Sidebar'; import { @@ -274,6 +277,80 @@ describe('Sidebar locate toolbar', () => { }); }); + it('only runs v2 command search enter for a real selected result outside IME composition', () => { + expect(shouldRunV2CommandSearchEnter({ + key: 'Enter', + activeItemCount: 1, + })).toBe(true); + expect(shouldRunV2CommandSearchEnter({ + key: 'Enter', + isComposing: true, + activeItemCount: 1, + })).toBe(false); + expect(shouldRunV2CommandSearchEnter({ + key: 'Enter', + keyCode: 229, + activeItemCount: 1, + })).toBe(false); + expect(shouldRunV2CommandSearchEnter({ + key: 'Enter', + activeItemCount: 0, + })).toBe(false); + expect(shouldRunV2CommandSearchEnter({ + key: 'Escape', + activeItemCount: 1, + })).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}`, + kind: 'node' as const, + title: `fs_order_${index}`, + meta: '开发240 · front_end_sys', + icon: null, + node: { + type: 'table', + key: `table-${index}`, + title: `fs_order_${index}`, + dataRef: { + tableName: `fs_order_${index}`, + dbName: 'front_end_sys', + }, + }, + })); + + expect(filterV2CommandSearchTreeItems( + items, + parseV2CommandSearchQuery('fs_order'), + )).toHaveLength(40); + expect(filterV2CommandSearchTreeItems( + items, + parseV2CommandSearchQuery(''), + )).toHaveLength(24); + expect(filterV2CommandSearchTreeItems( + [ + ...items, + { + key: 'node-db', + kind: 'node' as const, + title: 'front_end_sys', + meta: '开发240', + icon: null, + node: { + type: 'database', + key: 'db-front-end-sys', + title: 'front_end_sys', + dataRef: { + dbName: 'front_end_sys', + }, + }, + }, + ], + parseV2CommandSearchQuery('@fs_order'), + )).toHaveLength(40); + }); + it('keeps the v2 active host on the selected database connection', () => { const connectionIds = ['local', 'dev240', 'dev241']; const databaseNode = { diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 842688f..7a36736 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -512,7 +512,7 @@ interface BatchObjectItem { dataRef: any; } -type V2CommandSearchItem = +export type V2CommandSearchItem = | { key: string; kind: 'node'; @@ -587,6 +587,66 @@ export const parseV2CommandSearchQuery = (value: unknown): V2CommandSearchQuery }; }; +const isV2CommandSearchObjectNode = (node: TreeNode): boolean => { + return node.type === 'table' + || node.type === 'view' + || node.type === 'materialized-view'; +}; + +const V2_COMMAND_SEARCH_INITIAL_TREE_LIMIT = 24; + +export const filterV2CommandSearchTreeItems = ( + items: V2CommandSearchItem[], + query: V2CommandSearchQuery, +): V2CommandSearchItem[] => { + if (query.mode === 'ai') return []; + const normalizedKeyword = query.normalizedKeyword; + const objectMode = query.mode === 'object'; + const matchedItems = items.filter((item) => { + if (item.kind !== 'node') return false; + const node = item.node; + const dataRef = node.dataRef || {}; + if (objectMode && !isV2CommandSearchObjectNode(node)) { + return false; + } + if (!normalizedKeyword) return true; + const objectName = String(dataRef.tableName || dataRef.viewName || item.title || '').toLowerCase(); + if (objectMode) { + return objectName.includes(normalizedKeyword) + || String(item.title || '').toLowerCase().includes(normalizedKeyword); + } + const haystack = [ + item.title, + item.meta, + dataRef.tableName, + dataRef.viewName, + dataRef.dbName, + dataRef.name, + dataRef.config?.host, + ].filter(Boolean).join(' ').toLowerCase(); + return haystack.includes(normalizedKeyword); + }); + return normalizedKeyword ? matchedItems : matchedItems.slice(0, V2_COMMAND_SEARCH_INITIAL_TREE_LIMIT); +}; + +export interface V2CommandSearchEnterState { + key: string; + isComposing?: boolean; + keyCode?: number; + activeItemCount: number; +} + +export const shouldRunV2CommandSearchEnter = ({ + key, + isComposing, + keyCode, + activeItemCount, +}: V2CommandSearchEnterState): boolean => { + if (key !== 'Enter') return false; + if (isComposing || keyCode === 229) return false; + return activeItemCount > 0; +}; + export const resolveSidebarConnectionIdFromKey = ( key: unknown, connectionIds: string[], @@ -1036,6 +1096,7 @@ const Sidebar: React.FC<{ const commandSearchInputRef = useRef(null); const [isV2CommandSearchOpen, setIsV2CommandSearchOpen] = useState(false); const [v2CommandSearchValue, setV2CommandSearchValue] = useState(''); + const deferredV2CommandSearchValue = useDeferredValue(v2CommandSearchValue); const [v2CommandActiveIndex, setV2CommandActiveIndex] = useState(0); const [expandedKeys, setExpandedKeys] = useState([]); const [autoExpandParent, setAutoExpandParent] = useState(true); @@ -1116,13 +1177,19 @@ const Sidebar: React.FC<{ const handleV2CommandSearchValueChange = useCallback((value: string) => { setV2CommandSearchValue(value); + }, []); + + useEffect(() => { if (!v2CommandSearchPersistentFilterEnabled) { return; } - const nextFilter = value.trim(); + const nextFilter = deferredV2CommandSearchValue.trim(); setSearchValue(nextFilter); - setAppearance({ v2SidebarPersistedFilter: nextFilter }); - }, [setAppearance, v2CommandSearchPersistentFilterEnabled]); + const timer = window.setTimeout(() => { + setAppearance({ v2SidebarPersistedFilter: nextFilter }); + }, 160); + return () => window.clearTimeout(timer); + }, [deferredV2CommandSearchValue, setAppearance, v2CommandSearchPersistentFilterEnabled]); const toggleV2CommandSearchPersistentFilter = useCallback((enabled: boolean) => { const nextFilter = enabled ? v2CommandSearchValue.trim() : ''; @@ -5965,49 +6032,15 @@ const Sidebar: React.FC<{ ], [activeShortcutPlatform, onCreateConnection, onToggleAI, onToggleLogPanel, shortcutOptions]); const v2CommandSearchQuery = useMemo( - () => parseV2CommandSearchQuery(v2CommandSearchValue), - [v2CommandSearchValue], + () => parseV2CommandSearchQuery(deferredV2CommandSearchValue), + [deferredV2CommandSearchValue], ); const normalizedV2CommandSearchValue = v2CommandSearchQuery.normalizedKeyword; const v2CommandSearchObjectMode = v2CommandSearchQuery.mode === 'object'; const v2CommandSearchAiMode = v2CommandSearchQuery.mode === 'ai'; const filteredCommandSearchTreeItems = useMemo(() => { - if (v2CommandSearchAiMode) return []; - const matchLimit = v2CommandSearchObjectMode ? 16 : 8; - if (!normalizedV2CommandSearchValue) { - return commandSearchTreeItems - .filter((item) => !v2CommandSearchObjectMode || ( - item.kind === 'node' - && (item.node.type === 'table' || item.node.type === 'view' || item.node.type === 'materialized-view') - )) - .slice(0, matchLimit); - } - return commandSearchTreeItems - .filter((item) => { - if (item.kind !== 'node') return false; - const node = item.node; - const dataRef = node.dataRef || {}; - if (v2CommandSearchObjectMode && node.type !== 'table' && node.type !== 'view' && node.type !== 'materialized-view') { - return false; - } - const tableName = String(dataRef.tableName || dataRef.viewName || item.title || '').toLowerCase(); - if (v2CommandSearchObjectMode) { - return tableName.includes(normalizedV2CommandSearchValue) - || String(item.title || '').toLowerCase().includes(normalizedV2CommandSearchValue); - } - const haystack = [ - item.title, - item.meta, - dataRef.tableName, - dataRef.viewName, - dataRef.dbName, - dataRef.name, - dataRef.config?.host, - ].filter(Boolean).join(' ').toLowerCase(); - return haystack.includes(normalizedV2CommandSearchValue); - }) - .slice(0, matchLimit); - }, [commandSearchTreeItems, normalizedV2CommandSearchValue, v2CommandSearchAiMode, v2CommandSearchObjectMode]); + return filterV2CommandSearchTreeItems(commandSearchTreeItems, v2CommandSearchQuery); + }, [commandSearchTreeItems, v2CommandSearchQuery]); const filteredCommandSearchActionItems = useMemo(() => { if (v2CommandSearchObjectMode || v2CommandSearchAiMode) return []; @@ -6840,11 +6873,15 @@ const Sidebar: React.FC<{ return; } if (event.key === 'Enter') { - event.preventDefault(); - if (v2CommandSearchAiMode && !v2CommandSearchQuery.aiPrompt) { - message.warning('请输入要问 AI 的问题'); + if (!shouldRunV2CommandSearchEnter({ + key: event.key, + isComposing: event.nativeEvent.isComposing, + keyCode: event.nativeEvent.keyCode, + activeItemCount: commandSearchFlatItems.length, + })) { return; } + event.preventDefault(); runCommandSearchItem(commandSearchFlatItems[v2CommandActiveIndex]); return; } @@ -6893,7 +6930,7 @@ const Sidebar: React.FC<{ ? '未找到匹配的表、视图或物化视图。' : '未找到匹配项。可输入 @表名 只搜表对象,或输入 ?问题 让 AI 回答。'); return ( -
+
event.stopPropagation()}>