Merge branch 'master' into 13885-refactor-and-unify-icons-on-tree-component
[arvados-workbench2.git] / src / components / tree / tree.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import * as React from 'react';
6 import List from "@material-ui/core/List/List";
7 import ListItem from "@material-ui/core/ListItem/ListItem";
8 import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
9 import { ReactElement } from "react";
10 import Collapse from "@material-ui/core/Collapse/Collapse";
11 import CircularProgress from '@material-ui/core/CircularProgress';
12 import * as classnames from "classnames";
13 import { ListItemIcon } from '@material-ui/core/';
14
15 import { ArvadosTheme } from '../../common/custom-theme';
16 import { SidePanelRightArrowIcon } from '../icon/icon';
17
18 type CssRules = 'list' | 'active' | 'loader' | 'toggableIconContainer' | 'iconClose' | 'iconOpen' | 'toggableIcon';
19
20 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
21     list: {
22         paddingBottom: '3px',
23         paddingTop: '3px',
24     },
25     loader: {
26         position: 'absolute',
27         transform: 'translate(0px)',
28         top: '3px'
29     },
30     toggableIconContainer: {
31         color: theme.palette.grey["700"],
32         height: '14px',
33         position: 'absolute'
34     },
35     toggableIcon: {
36         fontSize: '14px'
37     },
38     active: {
39         color: theme.palette.primary.main,
40     },
41     iconClose: {
42         transition: 'all 0.1s ease',
43     },
44     iconOpen: {
45         transition: 'all 0.1s ease',
46         transform: 'rotate(90deg)',
47     }
48 });
49
50 export enum TreeItemStatus {
51     Initial,
52     Pending,
53     Loaded
54 }
55
56 export interface TreeItem<T> {
57     data: T;
58     id: string;
59     open: boolean;
60     active: boolean;
61     status: TreeItemStatus;
62     toggled?: boolean;
63     items?: Array<TreeItem<T>>;
64 }
65
66 interface TreeProps<T> {
67     items?: Array<TreeItem<T>>;
68     render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
69     toggleItemOpen: (id: string, status: TreeItemStatus) => void;
70     toggleItemActive: (id: string, status: TreeItemStatus) => void;
71     level?: number;
72     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
73 }
74
75 export const Tree = withStyles(styles)(
76     class Component<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
77         render(): ReactElement<any> {
78             const level = this.props.level ? this.props.level : 0;
79             const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu } = this.props;
80             const { list, loader, toggableIconContainer } = classes;
81             return <List component="div" className={list}>
82                 {items && items.map((it: TreeItem<T>, idx: number) =>
83                     <div key={`item/${level}/${idx}`}>
84                         <ListItem button className={list} style={{ paddingLeft: (level + 1) * 20 }}
85                             onClick={() => toggleItemActive(it.id, it.status)}
86                             onContextMenu={this.handleRowContextMenu(it)}>
87                             {it.status === TreeItemStatus.Pending ?
88                                 <CircularProgress size={10} className={loader} /> : null}
89                             <i onClick={() => this.props.toggleItemOpen(it.id, it.status)}
90                                 className={toggableIconContainer}>
91                                 <ListItemIcon className={this.getToggableIconClassNames(it.open, it.active)}>
92                                     {it.toggled && it.items && it.items.length === 0 ? <span /> : <SidePanelRightArrowIcon />}
93                                 </ListItemIcon>
94                             </i>
95                             {render(it, level)}
96                         </ListItem>
97                         {it.items && it.items.length > 0 &&
98                             <Collapse in={it.open} timeout="auto" unmountOnExit>
99                                 <Tree
100                                     items={it.items}
101                                     render={render}
102                                     toggleItemOpen={toggleItemOpen}
103                                     toggleItemActive={toggleItemActive}
104                                     level={level + 1}
105                                     onContextMenu={onContextMenu} />
106                             </Collapse>}
107                     </div>)}
108             </List>;
109         }
110
111         getToggableIconClassNames = (isOpen?: boolean, isActive?: boolean) => {
112             const { iconOpen, iconClose, active, toggableIcon } = this.props.classes;
113             return classnames(toggableIcon, {
114                 [iconOpen]: isOpen,
115                 [iconClose]: !isOpen,
116                 [active]: isActive
117             });
118         }
119
120         handleRowContextMenu = (item: TreeItem<T>) =>
121             (event: React.MouseEvent<HTMLElement>) =>
122                 this.props.onContextMenu(event, item)
123     }
124 );