export type HasParent = { id: string; parent?: { id: string } | null } export type TreeNode = T & { children: Array> expanded: boolean } export type RootNode = { id?: string children: Array> } export function arrayToTree( nodes: T[], currentState?: RootNode ): RootNode { const topLevelNodes: Array> = [] const mappedArr: { [id: string]: TreeNode } = {} const currentStateMap = treeToMap(currentState) // First map the nodes of the array to an object -> create a hash table. for (const node of nodes) { mappedArr[node.id] = { ...(node as any), children: [] } } for (const id of nodes.map((n) => n.id)) { if (mappedArr.hasOwnProperty(id)) { const mappedElem = mappedArr[id] mappedElem.expanded = currentStateMap.get(id)?.expanded ?? false const parent = mappedElem.parent if (!parent) { continue } // If the element is not at the root level, add it to its parent array of children. const parentIsRoot = !mappedArr[parent.id] if (!parentIsRoot) { if (mappedArr[parent.id]) { mappedArr[parent.id].children.push(mappedElem) } else { mappedArr[parent.id] = { children: [mappedElem] } as any } } else { topLevelNodes.push(mappedElem) } } } // tslint:disable-next-line:no-non-null-assertion const rootId = topLevelNodes.length ? topLevelNodes[0].parent!.id : undefined return { id: rootId, children: topLevelNodes } } /** * Converts an existing tree (as generated by the arrayToTree function) into a flat * Map. This is used to persist certain states (e.g. `expanded`) when re-building the * tree. */ function treeToMap( tree?: RootNode ): Map> { const nodeMap = new Map>() function visit(node: TreeNode) { nodeMap.set(node.id, node) node.children.forEach(visit) } if (tree) { visit(tree as TreeNode) } return nodeMap }