X-Git-Url: https://git.arvados.org/arvados-workbench2.git/blobdiff_plain/86d0ec68c0a400ab6d4de1897a68a4593f450b60..1d6407bd7c7d0669c99b615c8ecc2be3a10b6ba9:/src/components/tree/tree.tsx diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index 9680e3c0..11a95402 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -2,16 +2,19 @@ // // SPDX-License-Identifier: AGPL-3.0 -import * as React from 'react'; +import React, { useCallback, useState } from 'react'; import { List, ListItem, ListItemIcon, Checkbox, Radio, Collapse } from "@material-ui/core"; import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles'; -import { ProjectIcon } from '~/components/icon/icon'; +import { CollectionIcon, DefaultIcon, DirectoryIcon, FileIcon, ProjectIcon, ProcessIcon, FilterGroupIcon, FreezeIcon } from 'components/icon/icon'; import { ReactElement } from "react"; import CircularProgress from '@material-ui/core/CircularProgress'; import classnames from "classnames"; -import { ArvadosTheme } from '~/common/custom-theme'; +import { ArvadosTheme } from 'common/custom-theme'; import { SidePanelRightArrowIcon } from '../icon/icon'; +import { ResourceKind } from 'models/resource'; +import { GroupClass } from 'models/group'; +import { SidePanelTreeCategory } from 'store/side-panel-tree/side-panel-tree-actions'; type CssRules = 'list' | 'listItem' @@ -24,7 +27,9 @@ type CssRules = 'list' | 'toggableIcon' | 'checkbox' | 'childItem' - | 'childItemIcon'; + | 'childItemIcon' + | 'frozenIcon' + | 'indentSpacer'; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ list: { @@ -42,9 +47,10 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ color: theme.palette.grey["700"], height: '14px', width: '14px', + marginBottom: '0.4rem', }, toggableIcon: { - fontSize: '14px' + fontSize: '14px', }, renderContainer: { flex: 1 @@ -81,6 +87,14 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ active: { color: theme.palette.primary.main, }, + frozenIcon: { + fontSize: 20, + color: theme.palette.grey["600"], + marginLeft: '10px', + }, + indentSpacer: { + width: '0.25rem' + } }); export enum TreeItemStatus { @@ -91,13 +105,17 @@ export enum TreeItemStatus { export interface TreeItem { data: T; + depth?: number; id: string; open: boolean; active: boolean; selected?: boolean; + initialState?: boolean; + indeterminate?: boolean; flatTree?: boolean; status: TreeItemStatus; items?: Array>; + isFrozen?: boolean; } export interface TreeProps { @@ -105,6 +123,7 @@ export interface TreeProps { currentItemUuid?: string; items?: Array>; level?: number; + itemsMap?: Map>; onContextMenu: (event: React.MouseEvent, item: TreeItem) => void; render: (item: TreeItem, level?: number) => ReactElement<{}>; showSelection?: boolean | ((item: TreeItem) => boolean); @@ -113,6 +132,7 @@ export interface TreeProps { toggleItemActive: (event: React.MouseEvent, item: TreeItem) => void; toggleItemOpen: (event: React.MouseEvent, item: TreeItem) => void; toggleItemSelection?: (event: React.MouseEvent, item: TreeItem) => void; + selectedRef?: (node: HTMLDivElement | null) => void; /** * When set to true use radio buttons instead of checkboxes for item selection. @@ -142,6 +162,10 @@ const getActionAndId = (event: any, initAction: string | undefined = undefined) return [action, id]; }; +const isInFavoritesTree = (item: TreeItem): boolean => { + return item.id === SidePanelTreeCategory.FAVORITES || item.id === SidePanelTreeCategory.PUBLIC_FAVORITES; +} + interface FlatTreeProps { it: TreeItem; levelIndentation: number; @@ -150,32 +174,75 @@ interface FlatTreeProps { toggleItemActive: Function; getToggableIconClassNames: Function; getProperArrowAnimation: Function; + itemsMap?: Map>; classes: any; + showSelection: any; + useRadioButtons?: boolean; + handleCheckboxChange: Function; + selectedRef?: (node: HTMLDivElement | null) => void; } - const FLAT_TREE_ACTIONS = { toggleOpen: 'TOGGLE_OPEN', contextMenu: 'CONTEXT_MENU', toggleActive: 'TOGGLE_ACTIVE', }; +const ItemIcon = React.memo(({ type, kind, headKind, active, groupClass, classes }: any) => { + let Icon = ProjectIcon; + + if (groupClass === GroupClass.FILTER) { + Icon = FilterGroupIcon; + } + + if (type) { + switch (type) { + case 'directory': + Icon = DirectoryIcon; + break; + case 'file': + Icon = FileIcon; + break; + default: + Icon = DefaultIcon; + } + } + + if (kind) { + if(kind === ResourceKind.LINK && headKind) kind = headKind; + switch (kind) { + case ResourceKind.COLLECTION: + Icon = CollectionIcon; + break; + case ResourceKind.CONTAINER_REQUEST: + Icon = ProcessIcon; + break; + default: + break; + } + } + + return ; +}); + const FlatTree = (props: FlatTreeProps) =>
{ - const [action, id] = getActionAndId(event, FLAT_TREE_ACTIONS.contextMenu); + const id = getActionAndId(event, FLAT_TREE_ACTIONS.contextMenu)[1]; props.onContextMenu(event, { id } as any); }} onClick={(event) => { const [action, id] = getActionAndId(event); if (action && id) { + const item = props.itemsMap ? props.itemsMap[id] : { id }; + switch (action) { case FLAT_TREE_ACTIONS.toggleOpen: - props.handleToggleItemOpen({ id } as any, event); + props.handleToggleItemOpen(item as any, event); break; case FLAT_TREE_ACTIONS.toggleActive: - props.toggleItemActive(event, { id } as any); + props.toggleItemActive(event, item as any); break; default: break; @@ -188,17 +255,34 @@ const FlatTree = (props: FlatTreeProps) => .map((item: any) =>
- - - {props.getProperArrowAnimation(item.status, item.items!)} - - -
+ {isInFavoritesTree(props.it) ? +
+ : + + + {props.getProperArrowAnimation(item.status, item.items!)} + + } + {props.showSelection(item) && !props.useRadioButtons && + } + {props.showSelection(item) && props.useRadioButtons && + } +
- + {item.data.name} + { + !!item.data.frozenByUuid ? : null + }
) @@ -206,94 +290,26 @@ const FlatTree = (props: FlatTreeProps) =>
; 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, items, toggleItemActive, toggleItemOpen, disableRipple, currentItemUuid, useRadioButtons } = this.props; - const { list, listItem, loader, toggableIconContainer, renderContainer } = classes; - 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(event, it)} - selected={showSelection(it) && it.id === currentItemUuid} - onContextMenu={(event) => this.props.onContextMenu(event, it)}> - {it.status === TreeItemStatus.PENDING ? - : null} - this.handleToggleItemOpen(it, e)} - className={toggableIconContainer}> - - {this.getProperArrowAnimation(it.status, it.items!)} - - - {showSelection(it) && !useRadioButtons && - } - {showSelection(it) && useRadioButtons && - } -
- {render(it, level)} -
-
- { - it.open && it.items && it.items.length > 0 && - it.flatTree ? - : - - - - } -
)} -
; - } + function(props: TreeProps & WithStyles) { + const level = props.level ? props.level : 0; + const { classes, render, items, toggleItemActive, toggleItemOpen, disableRipple, currentItemUuid, useRadioButtons, itemsMap } = props; + const { list, listItem, loader, toggableIconContainer, renderContainer } = classes; + const showSelection = typeof props.showSelection === 'function' + ? props.showSelection + : () => props.showSelection ? true : false; - getProperArrowAnimation = (status: string, items: Array>) => { - return this.isSidePanelIconNotNeeded(status, items) ? : ; + const getProperArrowAnimation = (status: string, items: Array>) => { + return isSidePanelIconNotNeeded(status, items) ? : ; } - isSidePanelIconNotNeeded = (status: string, items: Array>) => { + const 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; + const getToggableIconClassNames = (isOpen?: boolean, isActive?: boolean) => { + const { iconOpen, iconClose, active, toggableIcon } = props.classes; return classnames(toggableIcon, { [iconOpen]: isOpen, [iconClose]: !isOpen, @@ -301,8 +317,8 @@ export const Tree = withStyles(styles)( }); } - handleCheckboxChange = (item: TreeItem) => { - const { toggleItemSelection } = this.props; + const handleCheckboxChange = (item: TreeItem) => { + const { toggleItemSelection } = props; return toggleItemSelection ? (event: React.MouseEvent) => { event.stopPropagation(); @@ -311,9 +327,95 @@ export const Tree = withStyles(styles)( : undefined; } - handleToggleItemOpen = (item: TreeItem, event: React.MouseEvent) => { + const handleToggleItemOpen = (item: TreeItem, event: React.MouseEvent) => { event.stopPropagation(); - this.props.toggleItemOpen(event, item); + props.toggleItemOpen(event, item); } + + // Scroll to selected item whenever it changes, accepts selectedRef from props for recursive trees + const [cachedSelectedRef, setCachedRef] = useState(null) + const selectedRef = props.selectedRef || useCallback((node: HTMLDivElement | null) => { + if (node && node.scrollIntoView && node !== cachedSelectedRef) { + node.scrollIntoView({ behavior: "smooth", block: "center" }); + } + setCachedRef(node); + }, [cachedSelectedRef]); + + const { levelIndentation = 20, itemRightPadding = 20 } = props; + return + {items && items.map((it: TreeItem, idx: number) => { + if (isInFavoritesTree(it) && it.open === true && it.items && it.items.length) { + it = { ...it, items: it.items.filter(item => item.depth && item.depth < 3) } + } + return
+ toggleItemActive(event, it)} + selected={showSelection(it) && it.id === currentItemUuid} + onContextMenu={(event) => props.onContextMenu(event, it)}> + {it.status === TreeItemStatus.PENDING ? + : null} + handleToggleItemOpen(it, e)} + className={toggableIconContainer}> + + {getProperArrowAnimation(it.status, it.items!)} + + + {showSelection(it) && !useRadioButtons && + } + {showSelection(it) && useRadioButtons && + } +
+ {render(it, level)} +
+
+ { + it.open && it.items && it.items.length > 0 && + it.flatTree ? + : + + + + } +
; + })} +
; } );