// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 import { pipe, map, reduce } from 'lodash/fp'; export type Tree = Record>; export const TREE_ROOT_ID = ''; export interface TreeNode { children: string[]; value: T; id: string; parent: string; active: boolean; selected: boolean; expanded: boolean; status: TreeNodeStatus; } export enum TreeNodeStatus { INITIAL = 'INITIAL', PENDING = 'PENDING', LOADED = 'LOADED', } export enum TreePickerId { PROJECTS = 'Projects', SHARED_WITH_ME = 'Shared with me', FAVORITES = 'Favorites', PUBLIC_FAVORITES = 'Public Favorites' } export const createTree = (): Tree => ({}); export const getNode = (id: string) => (tree: Tree): TreeNode | undefined => tree[id]; export const appendSubtree = (id: string, subtree: Tree) => (tree: Tree) => pipe( getNodeDescendants(''), map(node => node.parent === '' ? { ...node, parent: id } : node), reduce((newTree, node) => setNode(node)(newTree), tree) )(subtree) as Tree; export const setNode = (node: TreeNode) => (tree: Tree): Tree => { if (tree[node.id] && tree[node.id] === node) { return tree; } tree[node.id] = node; if (tree[node.parent]) { tree[node.parent].children = Array.from(new Set([...tree[node.parent].children, node.id])); } return tree; }; export const getNodeValue = (id: string) => (tree: Tree) => { const node = getNode(id)(tree); return node ? node.value : undefined; }; export const setNodeValue = (id: string) => (value: T) => (tree: Tree) => { const node = getNode(id)(tree); return node ? setNode(mapNodeValue(() => value)(node))(tree) : tree; }; export const setNodeValueWith = (mapFn: (value: T) => T) => (id: string) => (tree: Tree) => { const node = getNode(id)(tree); return node ? setNode(mapNodeValue(mapFn)(node))(tree) : tree; }; export const mapTreeValues = (mapFn: (value: T) => R) => (tree: Tree): Tree => getNodeDescendantsIds('')(tree) .map(id => getNode(id)(tree)) .map(mapNodeValue(mapFn)) .reduce((newTree, node) => setNode(node)(newTree), createTree()); export const mapTree = (mapFn: (node: TreeNode) => TreeNode) => (tree: Tree): Tree => getNodeDescendantsIds('')(tree) .map(id => getNode(id)(tree)) .map(mapFn) .reduce((newTree, node) => setNode(node)(newTree), createTree()); export const getNodeAncestors = (id: string) => (tree: Tree) => mapIdsToNodes(getNodeAncestorsIds(id)(tree))(tree); export const getNodeAncestorsIds = (id: string) => (tree: Tree): string[] => { const node = getNode(id)(tree); return node && node.parent ? [...getNodeAncestorsIds(node.parent)(tree), node.parent] : []; }; export const getNodeDescendants = (id: string, limit = Infinity) => (tree: Tree) => mapIdsToNodes(getNodeDescendantsIds(id, limit)(tree))(tree); export const countNodes = (tree: Tree) => getNodeDescendantsIds('')(tree).length; export const countChildren = (id: string) => (tree: Tree) => getNodeChildren('')(tree).length; export const getNodeDescendantsIds = (id: string, limit = Infinity) => (tree: Tree): string[] => { const node = getNode(id)(tree); const children = node ? node.children : id === TREE_ROOT_ID ? getRootNodeChildrenIds(tree) : []; return children .concat(limit < 1 ? [] : children .map(id => getNodeDescendantsIds(id, limit - 1)(tree)) .reduce((nodes, nodeChildren) => [...nodes, ...nodeChildren], [])); }; export const getNodeChildren = (id: string) => (tree: Tree) => mapIdsToNodes(getNodeChildrenIds(id)(tree))(tree); export const getNodeChildrenIds = (id: string) => (tree: Tree): string[] => getNodeDescendantsIds(id, 0)(tree); export const mapIdsToNodes = (ids: string[]) => (tree: Tree) => ids.map(id => getNode(id)(tree)).filter((node): node is TreeNode => node !== undefined); export const activateNode = (id: string) => (tree: Tree) => mapTree((node: TreeNode) => node.id === id ? { ...node, active: true } : { ...node, active: false })(tree); export const deactivateNode = (tree: Tree) => mapTree((node: TreeNode) => node.active ? { ...node, active: false } : node)(tree); export const expandNode = (...ids: string[]) => (tree: Tree) => mapTree((node: TreeNode) => ids.some(id => id === node.id) ? { ...node, expanded: true } : node)(tree); export const collapseNode = (...ids: string[]) => (tree: Tree) => mapTree((node: TreeNode) => ids.some(id => id === node.id) ? { ...node, expanded: false } : node)(tree); export const toggleNodeCollapse = (...ids: string[]) => (tree: Tree) => mapTree((node: TreeNode) => ids.some(id => id === node.id) ? { ...node, expanded: !node.expanded } : node)(tree); export const setNodeStatus = (id: string) => (status: TreeNodeStatus) => (tree: Tree) => { const node = getNode(id)(tree); return node ? setNode({ ...node, status })(tree) : tree; }; export const toggleNodeSelection = (id: string) => (tree: Tree) => { const node = getNode(id)(tree); return node ? pipe( setNode({ ...node, selected: !node.selected }), toggleAncestorsSelection(id), toggleDescendantsSelection(id))(tree) : tree; }; export const selectNode = (id: string) => (tree: Tree) => { const node = getNode(id)(tree); return node && node.selected ? tree : toggleNodeSelection(id)(tree); }; export const selectNodes = (id: string | string[]) => (tree: Tree) => { const ids = typeof id === 'string' ? [id] : id; return ids.reduce((tree, id) => selectNode(id)(tree), tree); }; export const deselectNode = (id: string) => (tree: Tree) => { const node = getNode(id)(tree); return node && node.selected ? toggleNodeSelection(id)(tree) : tree; }; export const deselectNodes = (id: string | string[]) => (tree: Tree) => { const ids = typeof id === 'string' ? [id] : id; return ids.reduce((tree, id) => deselectNode(id)(tree), tree); }; export const getSelectedNodes = (tree: Tree) => getNodeDescendants('')(tree) .filter(node => node.selected); export const initTreeNode = (data: Pick, 'id' | 'value'> & { parent?: string }): TreeNode => ({ children: [], active: false, selected: false, expanded: false, status: TreeNodeStatus.INITIAL, parent: '', ...data, }); const toggleDescendantsSelection = (id: string) => (tree: Tree) => { const node = getNode(id)(tree); if (node) { return getNodeDescendants(id)(tree) .reduce((newTree, subNode) => setNode({ ...subNode, selected: node.selected })(newTree), tree); } return tree; }; const toggleAncestorsSelection = (id: string) => (tree: Tree) => { const ancestors = getNodeAncestorsIds(id)(tree).reverse(); return ancestors.reduce((newTree, parent) => parent ? toggleParentNodeSelection(parent)(newTree) : newTree, tree); }; const toggleParentNodeSelection = (id: string) => (tree: Tree) => { const node = getNode(id)(tree); if (node) { const parentNode = getNode(node.id)(tree); if (parentNode) { const selected = parentNode.children .map(id => getNode(id)(tree)) .every(node => node !== undefined && node.selected); return setNode({ ...parentNode, selected })(tree); } return setNode(node)(tree); } return tree; }; const mapNodeValue = (mapFn: (value: T) => R) => (node: TreeNode): TreeNode => ({ ...node, value: mapFn(node.value) }); const getRootNodeChildrenIds = (tree: Tree) => Object .keys(tree) .filter(id => getNode(id)(tree)!.parent === TREE_ROOT_ID);