mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-12 09:29:43 +08:00
⚡️ perf(sidebar): 优化 v2 命令搜索输入和结果展示
- 修复中文输入法组合输入时按 Enter 误关闭搜索弹窗 - 限制搜索弹窗关闭方式为 ESC 或有效结果确认 - 移除关键词搜索下已加载表结果的固定条数截断 - 同步筛选开启时使用 deferred 值和防抖持久化,降低输入卡顿 - 补充命令搜索 Enter 判定和表匹配完整性测试
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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<any>(null);
|
||||
const [isV2CommandSearchOpen, setIsV2CommandSearchOpen] = useState(false);
|
||||
const [v2CommandSearchValue, setV2CommandSearchValue] = useState('');
|
||||
const deferredV2CommandSearchValue = useDeferredValue(v2CommandSearchValue);
|
||||
const [v2CommandActiveIndex, setV2CommandActiveIndex] = useState(0);
|
||||
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
|
||||
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 (
|
||||
<div className="gn-v2-command-backdrop" data-v2-command-search="true" onMouseDown={closeV2CommandSearch}>
|
||||
<div className="gn-v2-command-backdrop" data-v2-command-search="true">
|
||||
<div className="gn-v2-command-palette" role="dialog" aria-modal="true" aria-label="搜索表、连接、动作" onMouseDown={(event) => event.stopPropagation()}>
|
||||
<div className="gn-v2-command-searchbar">
|
||||
<SearchOutlined />
|
||||
|
||||
Reference in New Issue
Block a user