326 lines
9.9 KiB
TypeScript
326 lines
9.9 KiB
TypeScript
/**
|
|
* 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<string, API.SymbolTreeNode>();
|
|
|
|
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<number, string> = {
|
|
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<string, Set<string>> = {};
|
|
|
|
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<string>();
|
|
}
|
|
|
|
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;
|
|
}
|