mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-05 18:11:32 +08:00
✨ feat(sidebar): 增强 v2 侧栏搜索持久筛选
- 新增 v2 侧栏搜索模式配置,支持新版命令搜索和旧版侧栏筛选切换 - 命令搜索面板增加同步筛选开关和重置筛选按钮 - 侧栏顶部支持展示并清空已同步筛选词 - 补充 appearance 持久化字段清洗和回归测试
This commit is contained in:
@@ -439,7 +439,10 @@ describe('Sidebar locate toolbar', () => {
|
||||
expect(markup).toContain('gn-v2-object-explorer');
|
||||
expect(markup).toContain('gn-v2-active-connection-header');
|
||||
expect(markup).toContain('gn-v2-explorer-search');
|
||||
expect(markup).toContain('data-v2-sidebar-search-mode="command"');
|
||||
expect(markup).toContain('gn-v2-explorer-command-trigger');
|
||||
expect(markup).toContain('gn-v2-explorer-filter-action');
|
||||
expect(markup).toContain('重置侧栏筛选');
|
||||
expect(markup).toContain('搜索表、连接、动作... 或问 AI');
|
||||
expect(markup).toContain('gn-v2-search-shortcut');
|
||||
expect(markup).toContain('<kbd>⌘</kbd>');
|
||||
@@ -453,6 +456,11 @@ describe('Sidebar locate toolbar', () => {
|
||||
expect(markup).toContain('函数');
|
||||
expect(markup).toContain('aria-pressed="true"');
|
||||
expect(source).toContain("const [v2ExplorerFilter, setV2ExplorerFilter] = useState<V2ExplorerFilter>('all');");
|
||||
expect(source).toContain("const v2SidebarSearchMode = appearance.v2SidebarSearchMode ?? 'command';");
|
||||
expect(source).toContain('const v2CommandSearchPersistentFilterEnabled = appearance.v2CommandSearchPersistentFilterEnabled === true;');
|
||||
expect(source).toContain('handleV2CommandSearchValueChange(event.target.value)');
|
||||
expect(source).toContain('toggleV2CommandSearchPersistentFilter');
|
||||
expect(source).toContain('gn-v2-command-filter-switch');
|
||||
expect(source).toContain('onClick={() => setV2ExplorerFilter(item.key)}');
|
||||
expect(source).toContain('treeData={isV2Ui ? v2VisibleTreeData : displayTreeData}');
|
||||
expect(markup).toContain('gn-v2-sidebar-log-footer');
|
||||
@@ -496,6 +504,18 @@ describe('Sidebar locate toolbar', () => {
|
||||
expect(contextMenuFunction).not.toContain('setActiveContext');
|
||||
});
|
||||
|
||||
it('can render the v2 sidebar with legacy persistent filter input', () => {
|
||||
mocks.state.appearance.v2SidebarSearchMode = 'filter';
|
||||
mocks.state.appearance.v2SidebarPersistedFilter = 'fs_org';
|
||||
|
||||
const markup = renderToStaticMarkup(<Sidebar uiVersion="v2" />);
|
||||
|
||||
expect(markup).toContain('data-v2-sidebar-search-mode="filter"');
|
||||
expect(markup).toContain('筛选左侧表、连接、对象...');
|
||||
expect(markup).toContain('value="fs_org"');
|
||||
expect(markup).toContain('重置侧栏筛选');
|
||||
});
|
||||
|
||||
it('renders the v2 search shortcut from the user shortcut settings', () => {
|
||||
mocks.state.shortcutOptions = cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS);
|
||||
mocks.state.shortcutOptions.focusSidebarSearch.mac = { combo: 'Meta+F', enabled: true };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState, useMemo, useRef, useCallback, useDeferredValue } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Tree, message, Dropdown, MenuProps, Input, Button, Modal, Form, Badge, Checkbox, Space, Select, Popover, Tooltip, Progress } from 'antd';
|
||||
import { Tree, message, Dropdown, MenuProps, Input, Button, Modal, Form, Badge, Checkbox, Space, Select, Popover, Tooltip, Progress, Switch } from 'antd';
|
||||
import {
|
||||
DatabaseOutlined,
|
||||
TableOutlined,
|
||||
@@ -954,6 +954,7 @@ const Sidebar: React.FC<{
|
||||
const addSqlLog = useStore(state => state.addSqlLog);
|
||||
const sqlLogs = useStore(state => state.sqlLogs) || [];
|
||||
const shortcutOptions = useStore(state => state.shortcutOptions);
|
||||
const setAppearance = useStore(state => state.setAppearance);
|
||||
const setAIPanelVisible = useStore(state => state.setAIPanelVisible);
|
||||
const addAIContext = useStore(state => state.addAIContext);
|
||||
const darkMode = theme === 'dark';
|
||||
@@ -966,6 +967,7 @@ const Sidebar: React.FC<{
|
||||
const focusSidebarSearchShortcutTokens = focusSidebarSearchShortcut === '-'
|
||||
? []
|
||||
: focusSidebarSearchShortcut.match(/Ctrl|Alt|Shift|Esc|Space|[⌘⌃⌥⇧↵↑↓←→]|[^+]/g) ?? [];
|
||||
const isV2Ui = (uiVersion ?? appearance.uiVersion) === 'v2';
|
||||
const [treeData, setTreeData] = useState<TreeNode[]>([]);
|
||||
const activeTab = useMemo(() => tabs.find(tab => tab.id === activeTabId) || null, [tabs, activeTabId]);
|
||||
const activeTabLocateRequest = useMemo(() => normalizeSidebarLocateObjectRequestFromTab(activeTab), [activeTab]);
|
||||
@@ -1021,7 +1023,11 @@ const Sidebar: React.FC<{
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const v2SidebarSearchMode = appearance.v2SidebarSearchMode ?? 'command';
|
||||
const v2UseLegacySidebarFilter = isV2Ui && v2SidebarSearchMode === 'filter';
|
||||
const v2CommandSearchPersistentFilterEnabled = appearance.v2CommandSearchPersistentFilterEnabled === true;
|
||||
const v2PersistedSidebarFilter = appearance.v2SidebarPersistedFilter ?? '';
|
||||
const [searchValue, setSearchValue] = useState(v2PersistedSidebarFilter);
|
||||
const deferredSearchValue = useDeferredValue(searchValue);
|
||||
const [searchScopes, setSearchScopes] = useState<SearchScope[]>(['smart']);
|
||||
const [v2ExplorerFilter, setV2ExplorerFilter] = useState<V2ExplorerFilter>('all');
|
||||
@@ -1093,6 +1099,49 @@ const Sidebar: React.FC<{
|
||||
setV2CommandSearchValue('');
|
||||
setV2CommandActiveIndex(0);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchValue(v2PersistedSidebarFilter);
|
||||
}, [v2PersistedSidebarFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!v2UseLegacySidebarFilter) {
|
||||
return;
|
||||
}
|
||||
const nextFilter = searchValue.trim();
|
||||
if (nextFilter !== v2PersistedSidebarFilter) {
|
||||
setAppearance({ v2SidebarPersistedFilter: nextFilter });
|
||||
}
|
||||
}, [searchValue, setAppearance, v2PersistedSidebarFilter, v2UseLegacySidebarFilter]);
|
||||
|
||||
const handleV2CommandSearchValueChange = useCallback((value: string) => {
|
||||
setV2CommandSearchValue(value);
|
||||
if (!v2CommandSearchPersistentFilterEnabled) {
|
||||
return;
|
||||
}
|
||||
const nextFilter = value.trim();
|
||||
setSearchValue(nextFilter);
|
||||
setAppearance({ v2SidebarPersistedFilter: nextFilter });
|
||||
}, [setAppearance, v2CommandSearchPersistentFilterEnabled]);
|
||||
|
||||
const toggleV2CommandSearchPersistentFilter = useCallback((enabled: boolean) => {
|
||||
const nextFilter = enabled ? v2CommandSearchValue.trim() : '';
|
||||
setSearchValue(nextFilter);
|
||||
setAppearance({
|
||||
v2CommandSearchPersistentFilterEnabled: enabled,
|
||||
v2SidebarPersistedFilter: nextFilter,
|
||||
});
|
||||
message.success(enabled ? '已开启左侧筛选同步' : '已关闭左侧筛选同步');
|
||||
}, [setAppearance, v2CommandSearchValue]);
|
||||
|
||||
const resetV2SidebarFilter = useCallback(() => {
|
||||
setSearchValue('');
|
||||
setAppearance({
|
||||
v2CommandSearchPersistentFilterEnabled: false,
|
||||
v2SidebarPersistedFilter: '',
|
||||
});
|
||||
message.success('已重置侧栏筛选');
|
||||
}, [setAppearance]);
|
||||
|
||||
// Virtual Scroll State
|
||||
const [treeHeight, setTreeHeight] = useState(500);
|
||||
@@ -1121,7 +1170,7 @@ const Sidebar: React.FC<{
|
||||
|
||||
useEffect(() => {
|
||||
const handleFocusSidebarSearch = () => {
|
||||
if ((uiVersion ?? appearance.uiVersion) === 'v2') {
|
||||
if (isV2Ui && !v2UseLegacySidebarFilter) {
|
||||
openV2CommandSearch();
|
||||
return;
|
||||
}
|
||||
@@ -1136,7 +1185,7 @@ const Sidebar: React.FC<{
|
||||
return () => {
|
||||
window.removeEventListener('gonavi:focus-sidebar-search', handleFocusSidebarSearch as EventListener);
|
||||
};
|
||||
}, [appearance.uiVersion, openV2CommandSearch, uiVersion]);
|
||||
}, [isV2Ui, openV2CommandSearch, v2UseLegacySidebarFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isV2CommandSearchOpen) return;
|
||||
@@ -3110,8 +3159,6 @@ const Sidebar: React.FC<{
|
||||
});
|
||||
};
|
||||
|
||||
const isV2Ui = (uiVersion ?? appearance.uiVersion) === 'v2';
|
||||
|
||||
const onSelect = (keys: React.Key[], info: any) => {
|
||||
if (isV2Ui && info?.node?.type === 'v2-table-section') {
|
||||
return;
|
||||
@@ -6792,15 +6839,15 @@ const Sidebar: React.FC<{
|
||||
setV2CommandActiveIndex((prev) => Math.max(prev - 1, 0));
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
if (v2CommandSearchAiMode && !v2CommandSearchQuery.aiPrompt) {
|
||||
message.warning('请输入要问 AI 的问题');
|
||||
return;
|
||||
}
|
||||
runCommandSearchItem(commandSearchFlatItems[v2CommandActiveIndex]);
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
if (v2CommandSearchAiMode && !v2CommandSearchQuery.aiPrompt) {
|
||||
message.warning('请输入要问 AI 的问题');
|
||||
return;
|
||||
}
|
||||
runCommandSearchItem(commandSearchFlatItems[v2CommandActiveIndex]);
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
closeV2CommandSearch();
|
||||
@@ -6855,10 +6902,29 @@ const Sidebar: React.FC<{
|
||||
ref={commandSearchInputRef}
|
||||
variant="borderless"
|
||||
value={v2CommandSearchValue}
|
||||
onChange={(event) => setV2CommandSearchValue(event.target.value)}
|
||||
onChange={(event) => handleV2CommandSearchValueChange(event.target.value)}
|
||||
onKeyDown={handleV2CommandSearchKeyDown}
|
||||
placeholder="搜索表、连接、动作... 或问 AI"
|
||||
/>
|
||||
<Tooltip title="同步输入内容到左侧筛选">
|
||||
<span className="gn-v2-command-filter-switch" aria-label="同步到左侧筛选">
|
||||
<Switch
|
||||
size="small"
|
||||
checked={v2CommandSearchPersistentFilterEnabled}
|
||||
onChange={toggleV2CommandSearchPersistentFilter}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title={v2PersistedSidebarFilter ? '重置侧栏筛选' : '没有已同步的侧栏筛选'}>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<ReloadOutlined />}
|
||||
aria-label="重置侧栏筛选"
|
||||
disabled={!v2PersistedSidebarFilter}
|
||||
onClick={resetV2SidebarFilter}
|
||||
/>
|
||||
</Tooltip>
|
||||
<kbd>esc</kbd>
|
||||
</div>
|
||||
<div className="gn-v2-command-list">
|
||||
@@ -8489,26 +8555,62 @@ const Sidebar: React.FC<{
|
||||
</div>
|
||||
)}
|
||||
<div className={isV2Ui ? 'gn-v2-explorer-search' : undefined} style={{ padding: '8px 14px', borderBottom: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}` }}>
|
||||
{isV2Ui ? (
|
||||
<button
|
||||
type="button"
|
||||
className="gn-v2-explorer-command-trigger"
|
||||
onClick={() => {
|
||||
openV2CommandSearch();
|
||||
onFocusCommandSearch?.();
|
||||
}}
|
||||
aria-label="搜索表、连接、动作"
|
||||
>
|
||||
<SearchOutlined />
|
||||
<span>搜索表、连接、动作... 或问 AI</span>
|
||||
{focusSidebarSearchShortcutTokens.length > 0 ? (
|
||||
<span className="gn-v2-search-shortcut" aria-hidden="true">
|
||||
{focusSidebarSearchShortcutTokens.map((token, index) => (
|
||||
<kbd key={`${token}-${index}`}>{token}</kbd>
|
||||
))}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
{isV2Ui && !v2UseLegacySidebarFilter ? (
|
||||
<div className="gn-v2-explorer-command-row" data-v2-sidebar-search-mode="command">
|
||||
<button
|
||||
type="button"
|
||||
className="gn-v2-explorer-command-trigger"
|
||||
onClick={() => {
|
||||
openV2CommandSearch();
|
||||
onFocusCommandSearch?.();
|
||||
}}
|
||||
aria-label="搜索表、连接、动作"
|
||||
>
|
||||
<SearchOutlined />
|
||||
<span>{v2PersistedSidebarFilter || '搜索表、连接、动作... 或问 AI'}</span>
|
||||
{focusSidebarSearchShortcutTokens.length > 0 ? (
|
||||
<span className="gn-v2-search-shortcut" aria-hidden="true">
|
||||
{focusSidebarSearchShortcutTokens.map((token, index) => (
|
||||
<kbd key={`${token}-${index}`}>{token}</kbd>
|
||||
))}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
<Tooltip title={v2PersistedSidebarFilter ? '重置侧栏筛选' : '没有已同步的侧栏筛选'}>
|
||||
<button
|
||||
type="button"
|
||||
className="gn-v2-explorer-filter-action"
|
||||
aria-label="重置侧栏筛选"
|
||||
disabled={!v2PersistedSidebarFilter}
|
||||
onClick={resetV2SidebarFilter}
|
||||
>
|
||||
<ReloadOutlined />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : isV2Ui ? (
|
||||
<div className="gn-v2-explorer-legacy-filter-row" data-v2-sidebar-search-mode="filter">
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
ref={searchInputRef}
|
||||
value={searchValue}
|
||||
placeholder="筛选左侧表、连接、对象..."
|
||||
onChange={onSearch}
|
||||
size="small"
|
||||
prefix={<SearchOutlined />}
|
||||
/>
|
||||
<Tooltip title={searchValue ? '重置侧栏筛选' : '没有筛选内容'}>
|
||||
<button
|
||||
type="button"
|
||||
className="gn-v2-explorer-filter-action"
|
||||
aria-label="重置侧栏筛选"
|
||||
disabled={!searchValue}
|
||||
onClick={resetV2SidebarFilter}
|
||||
>
|
||||
<ReloadOutlined />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
{...noAutoCapInputProps}
|
||||
|
||||
Reference in New Issue
Block a user