X-Git-Url: https://git.arvados.org/arvados-workbench2.git/blobdiff_plain/42de1303354a30d004095003d6143deaa3f62992..e92207c912aed73a07340b5fb2a9e2cb23e1da5f:/src/components/tree/tree.tsx diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index f16348c7..b5ce5ec5 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -3,13 +3,13 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; -import { List, ListItem, ListItemIcon, Collapse, Checkbox } from "@material-ui/core"; +import { List, ListItem, ListItemIcon, Collapse, Checkbox, Radio } from "@material-ui/core"; import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles'; import { ReactElement } from "react"; import CircularProgress from '@material-ui/core/CircularProgress'; import * as classnames from "classnames"; -import { ArvadosTheme } from '../../common/custom-theme'; +import { ArvadosTheme } from '~/common/custom-theme'; import { SidePanelRightArrowIcon } from '../icon/icon'; type CssRules = 'list' @@ -60,14 +60,15 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ width: theme.spacing.unit * 3, height: theme.spacing.unit * 3, margin: `0 ${theme.spacing.unit}px`, - color: theme.palette.grey["500"] + padding: 0, + color: theme.palette.grey["500"], } }); export enum TreeItemStatus { - INITIAL, - PENDING, - LOADED + INITIAL = 'initial', + PENDING = 'pending', + LOADED = 'loaded' } export interface TreeItem { @@ -78,45 +79,74 @@ export interface TreeItem { selected?: boolean; status: TreeItemStatus; items?: Array>; + level?: number; } -interface TreeProps { +export interface TreeProps { + disableRipple?: boolean; + currentItemUuid?: string; items?: Array>; - render: (item: TreeItem, level?: number) => ReactElement<{}>; - toggleItemOpen: (id: string, status: TreeItemStatus) => void; - toggleItemActive: (id: string, status: TreeItemStatus) => void; level?: number; onContextMenu: (event: React.MouseEvent, item: TreeItem) => void; - showSelection?: boolean; - onSelectionChange?: (event: React.ChangeEvent, item: TreeItem) => void; + render: (item: TreeItem, level?: number) => ReactElement<{}>; + showSelection?: boolean | ((item: TreeItem) => boolean); + levelIndentation?: number; + itemRightPadding?: number; + toggleItemActive: (event: React.MouseEvent, item: TreeItem) => void; + toggleItemOpen: (event: React.MouseEvent, item: TreeItem) => void; + toggleItemSelection?: (event: React.MouseEvent, item: TreeItem) => void; + + /** + * When set to true use radio buttons instead of checkboxes for item selection. + * This does not guarantee radio group behavior (i.e item mutual exclusivity). + * Any item selection logic must be done in the toggleItemActive callback prop. + */ + useRadioButtons?: boolean; } export const Tree = withStyles(styles)( class Component extends React.Component & WithStyles, {}> { render(): ReactElement { const level = this.props.level ? this.props.level : 0; - const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu } = this.props; + const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu, disableRipple, currentItemUuid, useRadioButtons } = this.props; const { list, listItem, loader, toggableIconContainer, renderContainer } = classes; - return + const showSelection = typeof this.props.showSelection === 'function' + ? this.props.showSelection + : () => this.props.showSelection ? true : false; + + const { levelIndentation = 20, itemRightPadding = 20 } = this.props; + + return {items && items.map((it: TreeItem, idx: number) =>
- toggleItemActive(it.id, it.status)} + toggleItemActive(event, it)} + selected={showSelection(it) && it.id === currentItemUuid} onContextMenu={this.handleRowContextMenu(it)}> {it.status === TreeItemStatus.PENDING ? : null} - this.props.toggleItemOpen(it.id, it.status)} + - {it.status !== TreeItemStatus.INITIAL && it.items && it.items.length === 0 ? : } + {this.getProperArrowAnimation(it.status, it.items!)} - {this.props.showSelection && + {showSelection(it) && !useRadioButtons && } + onClick={this.handleCheckboxChange(it)} />} + {showSelection(it) && useRadioButtons && + }
{render(it, level)}
@@ -127,15 +157,27 @@ export const Tree = withStyles(styles)( showSelection={this.props.showSelection} items={it.items} render={render} + disableRipple={disableRipple} toggleItemOpen={toggleItemOpen} toggleItemActive={toggleItemActive} level={level + 1} - onContextMenu={onContextMenu} /> + onContextMenu={onContextMenu} + toggleItemSelection={this.props.toggleItemSelection} /> }
)}
; } + getProperArrowAnimation = (status: string, items: Array>) => { + return this.isSidePanelIconNotNeeded(status, items) ? : ; + } + + isSidePanelIconNotNeeded = (status: string, items: Array>) => { + return status === TreeItemStatus.PENDING || + (status === TreeItemStatus.LOADED && !items) || + (status === TreeItemStatus.LOADED && items && items.length === 0); + } + getToggableIconClassNames = (isOpen?: boolean, isActive?: boolean) => { const { iconOpen, iconClose, active, toggableIcon } = this.props.classes; return classnames(toggableIcon, { @@ -150,12 +192,18 @@ export const Tree = withStyles(styles)( this.props.onContextMenu(event, item) handleCheckboxChange = (item: TreeItem) => { - const { onSelectionChange } = this.props; - return onSelectionChange - ? (event: React.ChangeEvent, checked: boolean) => { - onSelectionChange(event, item); + const { toggleItemSelection } = this.props; + return toggleItemSelection + ? (event: React.MouseEvent) => { + event.stopPropagation(); + toggleItemSelection(event, item); } : undefined; } + + handleToggleItemOpen = (item: TreeItem) => (event: React.MouseEvent) => { + event.stopPropagation(); + this.props.toggleItemOpen(event, item); + } } );