feat: add search result actions to symbol select
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { ProFormTreeSelect } from '@ant-design/pro-components';
|
import { ProFormTreeSelect } from '@ant-design/pro-components';
|
||||||
import React, { useEffect, useState } from 'react';
|
import { Button, Space, Typography } from 'antd';
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import useUserInfo from '@/hooks/useUserInfo';
|
import useUserInfo from '@/hooks/useUserInfo';
|
||||||
import Loading from '@/loading';
|
import Loading from '@/loading';
|
||||||
import { getSymbolTreeList } from '@/utils/dataCenter';
|
import { getSymbolTreeList } from '@/utils/dataCenter';
|
||||||
@@ -13,6 +14,37 @@ interface SymbolSelectProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeSearchText = (value: string) =>
|
||||||
|
value?.trim().toLowerCase() || '';
|
||||||
|
|
||||||
|
const isNodeDefaultSearchMatched = (
|
||||||
|
node: API.SymbolTreeNode,
|
||||||
|
searchText: string,
|
||||||
|
) =>
|
||||||
|
String(node.value || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchText);
|
||||||
|
|
||||||
|
const collectSearchResultLeafKeys = (
|
||||||
|
nodes: API.SymbolTreeNode[],
|
||||||
|
searchText: string,
|
||||||
|
parentMatched = false,
|
||||||
|
): string[] => {
|
||||||
|
if (!searchText) return [];
|
||||||
|
|
||||||
|
return nodes.flatMap((node) => {
|
||||||
|
const matched =
|
||||||
|
parentMatched || isNodeDefaultSearchMatched(node, searchText);
|
||||||
|
const children = node.children || [];
|
||||||
|
|
||||||
|
if (children.length === 0) {
|
||||||
|
return matched ? [node.key] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return collectSearchResultLeafKeys(children, searchText, matched);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const SymbolSelect: React.FC<SymbolSelectProps> = ({
|
const SymbolSelect: React.FC<SymbolSelectProps> = ({
|
||||||
dataSourceId,
|
dataSourceId,
|
||||||
value,
|
value,
|
||||||
@@ -20,17 +52,29 @@ const SymbolSelect: React.FC<SymbolSelectProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { userInfo } = useUserInfo();
|
const { userInfo } = useUserInfo();
|
||||||
const companyInfo = userInfo?.company;
|
const companyInfo = userInfo?.company;
|
||||||
const [treeData, setTreeData] = useState<any[]>([]);
|
const [treeData, setTreeData] = useState<API.SymbolTreeNode[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
// Internally maintains a keys array for TreeSelect display
|
// Internally maintains a keys array for TreeSelect display
|
||||||
const [checkedKeys, setCheckedKeys] = useState<string[]>([]);
|
const [checkedKeys, setCheckedKeys] = useState<string[]>([]);
|
||||||
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
|
||||||
|
const normalizedSearchValue = useMemo(
|
||||||
|
() => normalizeSearchText(searchValue),
|
||||||
|
[searchValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchResultKeys = useMemo(
|
||||||
|
() => collectSearchResultLeafKeys(treeData, normalizedSearchValue),
|
||||||
|
[treeData, normalizedSearchValue],
|
||||||
|
);
|
||||||
|
|
||||||
const loadData = async (force: boolean = false) => {
|
const loadData = async (force: boolean = false) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await getSymbolTreeList(companyInfo, dataSourceId, force);
|
const data = await getSymbolTreeList(companyInfo, dataSourceId, force);
|
||||||
setCheckedKeys([]);
|
setCheckedKeys([]);
|
||||||
|
setSearchValue('');
|
||||||
setTreeData(data);
|
setTreeData(data);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -56,20 +100,36 @@ const SymbolSelect: React.FC<SymbolSelectProps> = ({
|
|||||||
}, [value, treeData]);
|
}, [value, treeData]);
|
||||||
|
|
||||||
// 3. When users manually check/uncheck items, convert internal keys back to string and trigger onChange
|
// 3. When users manually check/uncheck items, convert internal keys back to string and trigger onChange
|
||||||
const handleTreeChange = (keys: any) => {
|
const updateCheckedKeys = (keys: string[]) => {
|
||||||
// Handle the object format that antd tree may return
|
|
||||||
const actualKeys = Array.isArray(keys) ? keys : keys.checked;
|
|
||||||
|
|
||||||
// Update internal UI state
|
// Update internal UI state
|
||||||
setCheckedKeys(actualKeys);
|
setCheckedKeys(keys);
|
||||||
|
|
||||||
// Generate string to pass to external Form
|
// Generate string to pass to external Form
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
const patternString = generatePattern(treeData, actualKeys);
|
const patternString = generatePattern(treeData, keys);
|
||||||
onChange(patternString);
|
onChange(patternString);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTreeChange = (keys: any) => {
|
||||||
|
// Handle the object format that antd tree may return
|
||||||
|
const actualKeys = Array.isArray(keys) ? keys : keys?.checked || [];
|
||||||
|
updateCheckedKeys(actualKeys);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectSearchResults = () => {
|
||||||
|
updateCheckedKeys(
|
||||||
|
Array.from(new Set([...checkedKeys, ...searchResultKeys])),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeselectSearchResults = () => {
|
||||||
|
const searchResultKeySet = new Set(searchResultKeys);
|
||||||
|
updateCheckedKeys(
|
||||||
|
checkedKeys.filter((checkedKey) => !searchResultKeySet.has(checkedKey)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
if (!dataSourceId) return null;
|
if (!dataSourceId) return null;
|
||||||
if (loading) return <Loading padding="0" />;
|
if (loading) return <Loading padding="0" />;
|
||||||
|
|
||||||
@@ -83,10 +143,44 @@ const SymbolSelect: React.FC<SymbolSelectProps> = ({
|
|||||||
listHeight: 410,
|
listHeight: 410,
|
||||||
treeData: treeData,
|
treeData: treeData,
|
||||||
treeCheckable: true,
|
treeCheckable: true,
|
||||||
|
onSearch: setSearchValue,
|
||||||
value: checkedKeys, // Use the internally parsed array
|
value: checkedKeys, // Use the internally parsed array
|
||||||
onChange: handleTreeChange,
|
onChange: handleTreeChange,
|
||||||
maxTagCount: 'responsive',
|
maxTagCount: 'responsive',
|
||||||
virtual: true,
|
virtual: true,
|
||||||
|
popupRender: (originNode) =>
|
||||||
|
normalizedSearchValue && searchResultKeys.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
borderBottom: '1px solid rgba(5, 5, 5, 0.06)',
|
||||||
|
padding: '8px 12px',
|
||||||
|
}}
|
||||||
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
<Space wrap size={8} className="justify-between w-100">
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{$t('pages.rules.searchResults')}:{' '}
|
||||||
|
{searchResultKeys.length}
|
||||||
|
</Typography.Text>
|
||||||
|
<div>
|
||||||
|
<Button size="small" onClick={handleSelectSearchResults}>
|
||||||
|
{$t('pages.rules.selectSearchResults')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={handleDeselectSearchResults}
|
||||||
|
>
|
||||||
|
{$t('pages.rules.deselectSearchResults')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
{originNode}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
originNode
|
||||||
|
),
|
||||||
fieldNames: {
|
fieldNames: {
|
||||||
label: 'title',
|
label: 'title',
|
||||||
value: 'key',
|
value: 'key',
|
||||||
|
|||||||
Reference in New Issue
Block a user