/** * Transform flattened path data into a tree structure * @param data Raw array returned by the backend */ export const transformToSymbolTree = ( data: API.SymbolDataItem[], ): API.SymbolTreeNode[] => { const tree: API.SymbolTreeNode[] = []; const map = new Map(); data.forEach((item) => { const parts = item.path.split('\\'); let currentPath = ''; parts.forEach((part, index) => { const isLast = index === parts.length - 1; const parentPath = currentPath; currentPath = parentPath ? `${parentPath}\\${part}` : part; if (!map.has(currentPath)) { const newNode: API.SymbolTreeNode = { title: part, // Leaf nodes use symbol, directory nodes use path to prevent name conflicts key: isLast ? item.symbol : currentPath, }; if (isLast) { newNode.value = item.symbol; newNode.isLeaf = true; newNode.meta = item; } else { newNode.children = []; newNode.selectable = false; // Directory nodes are not selectable } map.set(currentPath, newNode); // If there is no parent path, it is a root node if (!parentPath) { tree.push(newNode); } else { // Otherwise, add it to the parent's children map.get(parentPath)?.children?.push(newNode); } } }); }); return tree; }; type SymbolTreeNode = { title: string; key: string; value?: string; path?: string; children?: SymbolTreeNode[]; isLeaf?: boolean; selectable?: boolean; }; /** * Generate a Pattern string based on the checkedKeys of Ant Design Tree * @param treeData The tree structure of symbols (must contain complete symbol and key information) * @param checkedKeys The checked keys of Ant Design Tree * @param options Configuration items */ export function generatePattern( treeData: SymbolTreeNode[], checkedKeys: string[], options: { useShortMode?: boolean; separator?: string } = {}, ) { const { useShortMode = true, separator = '\\' } = options; const excludes: string[] = []; const includes: string[] = []; // --- Utility function: Get all leaf keys under a node --- const getAllLeafKeys = (node: SymbolTreeNode): string[] => { if (!node.children || node.children.length === 0) return [node.key]; return node.children.flatMap(getAllLeafKeys); }; // --- Formatting function: process paths and wildcards --- const format = (node: SymbolTreeNode, isExclude: boolean) => { const isLeaf = !node.children || node.children.length === 0; // Short mode (Logic from screenshot two): !*SYMBOL or path\* if (useShortMode && isLeaf && node.value) { return isExclude ? `!*${node.value}` : `*${node.value}`; } // Short mode still uses wildcards to match categories, but we want to keep the path structure for clarity const path = node.key.split('/').join(separator); // Adapt to backslashes const suffix = isLeaf ? '' : `${separator}*`; const prefix = isExclude ? '!' : ''; // Special handling for exclusion logic in short mode: // If it is short mode and we want to exclude a specific product, it should be !*SYMBOL according to screenshot two if (useShortMode && isExclude && isLeaf && node.value) { return `!*${node.value}`; } return `${prefix}${path}${suffix}`; }; // --- Core recursive logic --- const process = (node: SymbolTreeNode) => { const allLeaves = getAllLeafKeys(node); const selectedLeaves = allLeaves.filter((k) => checkedKeys.includes(k)); if (selectedLeaves.length === 0) return; // This branch is not selected at all // 1. If the current level is fully selected if (selectedLeaves.length === allLeaves.length) { includes.push(format(node, false)); return; } // 2. If it is a leaf node (and is selected) if (!node.children || node.children.length === 0) { includes.push(format(node, false)); return; } if (selectedLeaves.length > allLeaves.length / 2) { includes.push(format(node, false)); node.children.forEach((child) => { const childLeaves = getAllLeafKeys(child); const childSelected = childLeaves.filter((k) => checkedKeys.includes(k), ); if (childSelected.length === 0) { excludes.push(format(child, true)); } else if (childSelected.length < childLeaves.length) { findExcludesOnly(child); } }); } else { node.children.forEach(process); } }; const findExcludesOnly = (node: SymbolTreeNode) => { const allLeaves = getAllLeafKeys(node); const selectedLeaves = allLeaves.filter((k) => checkedKeys.includes(k)); if (selectedLeaves.length === 0) { excludes.push(format(node, true)); return; } node.children?.forEach((child) => { const childLeaves = getAllLeafKeys(child); const childSelected = childLeaves.filter((k) => checkedKeys.includes(k)); if (childSelected.length === 0) { excludes.push(format(child, true)); } else if (childSelected.length < childLeaves.length) { findExcludesOnly(child); } }); }; treeData.forEach(process); // 3. Final concatenation: Exclude items must be at the front return [...excludes, ...includes].join(','); } /** * Restore the background Pattern string to Ant Design Tree's checkedKeys * @param patternString The string returned by the background, such as "!forex\exotic\*,forex\*" * @param treeData Original tree structure (must contain complete symbol and key information) * @param options Configuration items */ export function parsePatternToKeys( patternString: string, treeData: SymbolTreeNode[], options: { separator?: string } = {}, ): string[] { if (!patternString) return []; const { separator = '\\' } = options; // 1. Split the string into rule array const rules = patternString.split(',').map((s) => s.trim()); // 2. Get all leaf nodes (final products), because the selection state finally appears on the leaf nodes const allLeafNodes: SymbolTreeNode[] = []; const collectLeaves = (nodes: SymbolTreeNode[], parentPath: string = '') => { nodes.forEach((node) => { if (!node.children || node.children.length === 0) { node.path = `${parentPath}${separator}${node.key}`; allLeafNodes.push(node); } else { collectLeaves(node.children, node.key); } }); }; collectLeaves(treeData); // 3. Define the matching logic (simulate the background matching process) const isMatched = ( node: SymbolTreeNode, rule: string, ): { matched: boolean; isExclude: boolean } => { let tempRule = rule; const isExclude = tempRule.startsWith('!'); if (isExclude) tempRule = tempRule.substring(1); // Remove ! let matched = false; // Case A: Abbreviated pattern wildcard *BITCOIN if (tempRule.startsWith('*')) { const symbol = tempRule.substring(1); matched = node.value === symbol; } // Case B: Path wildcard forex/general/* else if (tempRule.endsWith('*')) { const prefix = tempRule.substring(0, tempRule.length - 1); // Check if the node's key starts with the prefix path // Note: We need to handle separator consistency const nodePath = (node?.path || node.key).split('/').join(separator); matched = nodePath.startsWith(prefix); } // Case C: Exact path match forex/general/XAUUSD else { const nodePath = (node?.path || node.key).split('/').join(separator); matched = nodePath === tempRule; } return { matched, isExclude }; }; // 4. Execute rule matching for each leaf node const selectedKeys: string[] = []; for (const node of allLeafNodes) { // Simulate left-to-right matching, stop when hit for (const rule of rules) { const { matched, isExclude } = isMatched(node, rule); if (matched) { if (!isExclude && node.value) { selectedKeys.push(node.value); } // Once a rule is hit (whether it's inclusion or exclusion), immediately break out of the current node's rule loop break; } } } return selectedKeys; } export interface AntdTreeNode { title: string; value: string; key?: string; selectable?: boolean; children?: AntdTreeNode[]; } const CALC_MODE_MAP: Record = { 0: 'Forex', 1: 'Futures', 2: 'CFD', 3: 'CFD Index', 4: 'CFD Leverage', 5: 'Forex No Leverage', 32: 'Exchange Stocks', 33: 'Exchange Futures', 34: 'Exchange FORTS Futures', 35: 'Exchange Options', 36: 'Exchange Options Margin', 37: 'Exchange Bonds', 38: 'Exchange MOEX Stocks', 39: 'Exchange MOEX Bonds', 64: 'Collateral', }; export function transformToAssetsTree( sourceData: API.SymbolDataItem[], ): AntdTreeNode[] { const groupedData: Record> = {}; sourceData.forEach((item) => { const { calcMode, currencyBase, currencyProfit, symbol } = item; // Determine the category name, with 0 and 5 both classified as 'Forex' let categoryName = CALC_MODE_MAP[calcMode] || 'Other'; if (calcMode === 0 || calcMode === 5) { categoryName = 'Forex'; } if (!groupedData[categoryName]) { groupedData[categoryName] = new Set(); } if (calcMode === 0 || calcMode === 5) { if (currencyBase) groupedData[categoryName].add(currencyBase); if (currencyProfit) groupedData[categoryName].add(currencyProfit); } else { // Other cases extract the symbol directly to remove duplicates if (symbol) groupedData[categoryName].add(symbol); } }); const treeData: AntdTreeNode[] = Object.keys(groupedData).map((category) => { const uniqueValues = Array.from(groupedData[category]); return { title: category, value: `GROUP_${category}`, key: `GROUP_${category}`, selectable: false, children: uniqueValues.map((val) => ({ title: val, value: val, // key: `${category}_${val}`, })), }; }); return treeData; }