diff --git a/src/TreeViewList/TreeViewList.stories.tsx b/src/TreeViewList/TreeViewList.stories.tsx index ed0c7442a..8e440ae76 100644 --- a/src/TreeViewList/TreeViewList.stories.tsx +++ b/src/TreeViewList/TreeViewList.stories.tsx @@ -15,7 +15,7 @@ export default meta; // Story Template const Template = (args: TreeViewListProps>) => { - return ; + return ; }; // Default @@ -23,104 +23,71 @@ export const Default = { args: { items: [ { - __v: 0, - _id: "64f1b6fd511d08b5f6bc2a20", - c1_data_mgmt_score: "", - c1_data_mgmt_score_comment: "", - c1_expected_gate: "", - c2_data_mgmt_score: "", - c2_data_mgmt_score_comment: "", - c2_expected_gate: "", - c3_data_mgmt_score: "", - c3_data_mgmt_score_comment: "", - c3_expected_gate: "", - characteristic: "ConsiderationPointPosition", - contributor1: "Aerodynamics", - contributor2: "IPG Automotive", - contributor3: "", - data_type: "Vector", - description: - "When the vehicle is gripped by side wind, the wind starts to take effect only from a certain point on, e.g. if only the bumper is attacked by side wind the driver will usually not recognize the effects. Ahead of this point, the vehicle body does not offer enough contact surface to the wind to take effect.", - dimension: 0, - display_name: "AER.ConsiderationPointPosition", - label1: "Position X", - label2: "Position Y", - label3: "Position Z", - length: 3, - level1: "AER", - level2: "", - level3: "", - level4: "", - level5: "", - lower_limit1: null, - lower_limit_inequality1: "", - name: "AER.ConsiderationPointPosition", - unit1: "m", - unit2: "m", - unit3: "m", - upper_limit1: null, - upper_limit_inequality1: "" + children: [ + { + children: [ + { + children: [ + { + name: "PumpMaxDelivery", + options: [], + tooltip: + "Maximum delivery of the two hydraulic pumps (each responsible for one circuit) at 0 bar pressure difference. Parameter needed for CarMaker hydraulic ESC (Name: 'Pump.qMax')." + } + ], + name: "HydESPModel" + } + ], + name: "MCbooster" + }, + { + children: [ + { + name: "Ratio", + options: [], + tooltip: + "The pedal ratio amplifies the force of the brake pedal. Parameter needed for CarMaker hydraulic ESC (Name: 'Pedal.ratio')." + }, + { + name: "ResponseTime", + options: [], + tooltip: + "Period of time from pressing the pedal till the brake pressure begins to build up." + } + ], + name: "Pedal" + } + ], + id: "64f1b6fd511d08b5f6bc2a1e", + name: "BRK" }, { - __v: 0, - _id: "64f1b6fd511d08b5f6bc2a1f", - characteristic: "DragCoefficient1D", - contributor1: "Aerodynamics", - contributor2: "IPG Automotive", - contributor3: "", - data_type: "1D Lookup Table", - description: - "Defines the drag coefficient of the entire vehicle as a function of wind angle of attack (tau)", - dimension: 1, - display_name: "AER.DragCoefficient1D", - label1: "tau", - label2: "cD", - label3: "", - length: 1, - level1: "AER", - level2: "", - level3: "", - level4: "", - level5: "", - lower_limit1: null, - lower_limit_inequality1: "", - name: "AER.DragCoefficient1D", - unit1: "rad", - unit2: "-", - unit3: "", - upper_limit1: null, - upper_limit_inequality1: "" + children: [ + { + children: [ + { + children: [ + { + name: "Capacity", + options: [], + tooltip: + "This is the total capacity of the board high voltage battery" + } + ], + name: "HV" + } + ], + name: "Battery" + } + ], + name: "ELE" }, { - __v: 0, - _id: "64f1b6fd511d08b5f6bc2a21", - c3_expected_gate: "", - characteristic: "PumpMaxDelivery", - contributor1: "Brakes", - contributor2: "IPG Automotive", - contributor3: "", - data_type: "Scalar", - description: - "Maximum delivery of the two hydraulic pumps (each responsible for one circuit) at 0 bar pressure difference. Parameter needed for CarMaker hydraulic ESC (Name: 'Pump.qMax').", - dimension: 0, - display_name: "BRK.MCBooster.HydESPModel.PumpMaxDelivery", - label1: "Conductance", - label2: "", - label3: "", - length: 1, - level1: "BRK", - level2: "MCBooster", - level3: "HydESPModel", - level4: "", - level5: "", - lower_limit1: null, - lower_limit_inequality1: "", - name: "BRK.MCBooster.HydESPModel.PumpMaxDelivery", - unit1: "m3/s*Pa", - unit2: "", - unit3: "", - upper_limit1: null, - upper_limit_inequality1: "" + children: [ + { name: "Efficiency1D", options: [] }, + { name: "GearSpred", options: [] } + ], + name: "TRM" } ] }, diff --git a/src/TreeViewList/TreeViewList.tsx b/src/TreeViewList/TreeViewList.tsx index 397598621..5e8918225 100644 --- a/src/TreeViewList/TreeViewList.tsx +++ b/src/TreeViewList/TreeViewList.tsx @@ -1,7 +1,6 @@ import { - FindOrCreateNodeProps, + ChildData, Item, - ParseChildProps, TooltipTreeItemProps, TreeNode, TreeViewListProps @@ -17,12 +16,13 @@ import { alpha } from "@mui/material/styles"; /** * A component that renders a tree view list. * + * @template T The type of the options array elements. * @param props - The properties for the tree view list. * @property props.items - The items to display in the tree view list. * @property props.selected - The ID of the currently selected item. * @property props.searchTerm - The term to search for in the items. * @property [props.defaultExpanded=[]] - The IDs of the items that should be expanded by default. - * @property (selection: string) props.onSelectionChange - The function to call when the selection changes. + * @property props.onSelectionChange - The function to call when the selection changes. * @returns The tree view list component. */ const TreeViewList = ({ @@ -30,6 +30,7 @@ const TreeViewList = ({ selected, searchTerm, defaultExpanded = [], + width, onSelectionChange }: TreeViewListProps) => { const [expanded, setExpanded] = useState(defaultExpanded); @@ -79,26 +80,60 @@ const TreeViewList = ({ ); }; - // search function - const applySearch = (search: string, parameters: Item[]) => { + // Recursive search function + const applySearch = (search: string, parameters: Item[]): Item[] => { const terms = search .toUpperCase() .trim() .split(/(?:\.| )+/); - return parameters.filter(opt => - terms.every(term => - typeof opt.name === "string" - ? opt.name.toUpperCase().includes(term) + + const searchInItem = (item: Item): Item | null => { + const match = terms.some(term => + typeof item.name === "string" + ? item.name.toUpperCase().includes(term) : false - ) - ); + ); + + if (match) { + // If the item matches any of the search terms, return the whole item + return item; + } + + const children = Array.isArray(item.children) + ? item.children.map(searchInItem).filter(Boolean) + : []; + const options = Array.isArray(item.options) + ? item.options + .map((option: Item) => { + if (typeof option === "object" && option !== null) { + return searchInItem(option as Item); + } + return null; + }) + .filter(Boolean) + : []; + + if (children.length > 0 || options.length > 0) { + return { + ...item, + children: children as T, + options: options as T + }; + } + + return null; + }; + + return parameters.map(searchInItem).filter(Boolean) as Item[]; }; + // build tree nodes - const parameters = buildParameterTree( + const parameters = buildTree( searchTerm !== undefined && searchTerm !== "" ? applySearch(searchTerm, items) : items ); + const renderTree = (nodes: TreeNode[]) => nodes.map(node => ( ({ expanded={expanded} selected={selected} onNodeToggle={handleToggle} + sx={{ width }} > {renderTree(parameters)} @@ -136,12 +172,12 @@ export default TreeViewList; * @param name - The name of the new node. * @param tooltip - The tooltip of the new node. * @param [disable=true] - Optional parameter that indicates whether the node is disabled. Defaults to true. - * @returns TreeNode The newly created tree node. + * @returns The newly created tree node. */ function createNode( id: string, name: string, - tooltip: string, + tooltip?: string, disable = true ): TreeNode { return { @@ -156,52 +192,31 @@ function createNode( /** * Builds a tree structure from a flat list of items. * - * @param Item[] parameterMapping - The flat list of items to transform into a tree. - * @returns TreeNode[] The tree structure built from the input items. - * @example buildParameterTree(items) // returns TreeNode[] + * @template T The type of the options array elements. + * @param data - The flat list of items to transform into a tree. + * @returns The tree structure built from the input items. + * @example buildTree(items) // returns TreeNode[] */ -function buildParameterTree(parameterMapping: Item[]): TreeNode[] { +function buildTree(data: Item[]): TreeNode[] { const itemsTree: TreeNode[] = []; - // Find or create a node in the tree - const findOrCreateNode = ({ - nodes, - id, - name, - tooltip = "" - }: FindOrCreateNodeProps) => { - let node = nodes.find(n => n.id === id); - if (!node) { - node = createNode(id, name, tooltip); - nodes.push(node); - } - return node; - }; - // Iterate through each item - parameterMapping.forEach(item => { - // Start with the root level - let currentLevelNodes = itemsTree; - - // Iterate through each level - for (const level in item) { - if (level.startsWith("level") && item[level]) { - const node = findOrCreateNode({ - id: item[level] as string, - name: item.name as string, - nodes: currentLevelNodes - }); - currentLevelNodes = node.children; - } + data.forEach(item => { + // create node for this root model + const name = item.name ? item.name.toString() : "default"; + const tooltip = item.tooltip ? item.tooltip.toString() : undefined; + const parentNode = createNode(name, name, tooltip); + itemsTree.push(parentNode); + + if (Array.isArray(item.options)) { + item.options.forEach(childData => { + parseChild(parentNode, childData); + }); } - // Add characteristic to the last node in the hierarchy - if (currentLevelNodes !== itemsTree && item.characteristic) { - parseChild({ - characteristic: item.characteristic as string, - name: item.name as string, - nodes: currentLevelNodes, - tooltip: item.description as string + if (Array.isArray(item.children)) { + item.children.forEach(childData => { + parseChild(parentNode, childData); }); } }); @@ -210,18 +225,51 @@ function buildParameterTree(parameterMapping: Item[]): TreeNode[] { } /** - * Parses a child node and adds it to the provided nodes array. + * Type guard to check if a variable is of type ChildData. * - * @param props - The properties for parsing a child node. - * @property props.nodes - The array of nodes to add the new child node to. - * @property props.characteristic - The characteristic value of the new child node. - * @property props.name - The name of the new child node. - * @property props.tooltip - The tooltip of the new child node. + * @template T The type of the options array elements. + * @param data - The variable to check. + * @returns A boolean indicating whether the variable is of type ChildData. */ -function parseChild({ nodes, characteristic, name, tooltip }: ParseChildProps) { - if (!characteristic) return; +function isChildData(data: any): data is ChildData { + return data.children !== undefined || data.options !== undefined; +} + +/** + * Parses a child node and adds it to the parent node. + * + * @template T The type of the options array elements. + * @param parentNode - The parent node to which the child node will be added. + * @param childData - The data of the child node. + */ +function parseChild(parentNode: TreeNode, childData: ChildData | T) { + // If childData is of type T and not ChildData, return + if (!isChildData(childData)) return; + + // ignore if child data is a leaf node, i.e. has no children or options + if (!childData.children && !childData.options) return; + + const tooltip = childData.tooltip ? childData.tooltip.toString() : undefined; + + // create node for this child + const thisNode = createNode( + childData.name, + parentNode.name + "." + childData.name, + tooltip + ); + parentNode.children.push(thisNode); + + // parse children of this child recursively + if (childData.children) { + childData.children.forEach((child: ChildData) => { + parseChild(thisNode, child); + }); + } - const newId = characteristic; - const thisNode = createNode(newId, name, tooltip); - nodes.push(thisNode); + // parse options of this child recursively + if (childData.options) { + childData.options.forEach((child: T) => { + parseChild(thisNode, child); + }); + } } diff --git a/src/TreeViewList/TreeViewList.types.ts b/src/TreeViewList/TreeViewList.types.ts index acb279d92..726b78912 100644 --- a/src/TreeViewList/TreeViewList.types.ts +++ b/src/TreeViewList/TreeViewList.types.ts @@ -23,12 +23,12 @@ export type TreeViewListProps = { selected: string; /** - * The term to search for in the items. + * The term to search for in the items. This is optional. */ - searchTerm: string; + searchTerm?: string; /** - * The IDs of the items that should be expanded by default. + * The IDs of the items that should be expanded by default. This is optional. */ defaultExpanded?: string[]; @@ -36,6 +36,11 @@ export type TreeViewListProps = { * The function to call when the selection changes. */ onSelectionChange: (value: string) => void; + + /** + * The display width of the tree view list. This is optional. + */ + width?: string; }; /** @@ -99,51 +104,28 @@ export type TooltipTreeItemProps = { }; /** - * Properties for the parseChild function. + * Type representing a child in a tree structure. + * + * @template T The type of the options array elements. */ -export type ParseChildProps = { - /** - * The nodes to add the new child node to. - */ - nodes: TreeNode[]; - - /** - * The characteristic value of the new child node. This would be the last child displayed. - */ - characteristic: string; - - /** - * The name of the new child node. - */ +export type ChildData = { + /** The name of the node. */ name: string; /** - * The tooltip of the new child node. + * Optional array of child nodes. + * Each child is also a `ChildData` object, allowing for a nested tree structure. */ - tooltip: string; -}; - -/** - * Properties for the findOrCreateNode function. - */ -export type FindOrCreateNodeProps = { - /** - * The nodes to find or create a node in. - */ - nodes: TreeNode[]; + children?: ChildData[]; /** - * The ID of the node to find or create. + * Optional array of options. The options are displayed as the last child. + * The type of the elements in this array is defined by the generic parameter `T`. */ - id: string; - - /** - * The name of the node to find or create. - */ - name: string; + options?: T[]; /** - * The tooltip of the node to find or create. + * The tooltip of the child. This is optional. */ tooltip?: string; };