15610: Adds behaviors like the original Tree, and max height on VirtualTree.
authorLucas Di Pentima <lucas@di-pentima.com.ar>
Fri, 26 Jun 2020 20:25:10 +0000 (17:25 -0300)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Fri, 26 Jun 2020 20:25:10 +0000 (17:25 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@di-pentima.com.ar>

src/components/file-tree/file-tree.tsx
src/components/tree/tree.tsx
src/components/tree/virtual-tree.tsx
src/models/tree.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
src/views-components/collection-panel-files/collection-panel-files.ts

index b5d98c0833b35c9addc9c6f031183e956a2280c4..d4b47be9ce5d5bd41368eb054f84180f6cdb3a33 100644 (file)
@@ -4,7 +4,7 @@
 
 import * as React from "react";
 import { TreeItem, TreeItemStatus } from "../tree/tree";
-import { VirtualTree as Tree } from "../tree/virtual-tree";
+import { VirtualTree } from "../tree/virtual-tree";
 import { FileTreeData } from "./file-tree-data";
 import { FileTreeItem } from "./file-tree-item";
 
@@ -17,6 +17,7 @@ export interface FileTreeProps {
     currentItemUuid?: string;
 }
 
+const Tree = VirtualTree(20);
 export class FileTree extends React.Component<FileTreeProps> {
     render() {
         return <Tree
index 28833b85b8891b6ea85a59d5e4ce8b06fac46a58..76fbf011eb3face1a3bffe266db9b55e7fe1781e 100644 (file)
@@ -79,8 +79,6 @@ export interface TreeItem<T> {
     selected?: boolean;
     status: TreeItemStatus;
     items?: Array<TreeItem<T>>;
-    itemCount?: number;
-    level?: number;
 }
 
 export interface TreeProps<T> {
index 467969fa1417b357973c4dcb8b1fbaf0449c5814..9330fbe50ca44af0dba4fe0a95c5d7de6d5cb99a 100644 (file)
@@ -13,6 +13,7 @@ import { ArvadosTheme } from '~/common/custom-theme';
 import { TreeItem, TreeProps, TreeItemStatus } from './tree';
 import { ListItem, Radio, Checkbox, CircularProgress, ListItemIcon } from '@material-ui/core';
 import { SidePanelRightArrowIcon } from '../icon/icon';
+import { min } from 'lodash';
 
 type CssRules = 'list'
     | 'listItem'
@@ -71,35 +72,35 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     }
 });
 
-// export const RowA = <T, _>(items: TreeItem<T>[], render:any) => (index: number) => {
-//     return <div>
-//         {render(items[index])}
-//     </div>;
-// };
+export interface VirtualTreeItem<T> extends TreeItem<T> {
+    itemCount?: number;
+    level?: number;
+}
 
 // For some reason, on TSX files it isn't accepted just one generic param, so
 // I'm using <T, _> as a workaround.
-export const Row =  <T, _>(itemList: TreeItem<T>[], render: any) => withStyles(styles)(
-    (props: React.PropsWithChildren<ListChildComponentProps> & TreeProps<T> & WithStyles<CssRules>) => {
-        const { index, style } = props;
+export const Row =  <T, _>(itemList: VirtualTreeItem<T>[], render: any, treeProps: TreeProps<T>) => withStyles(styles)(
+    (props: React.PropsWithChildren<ListChildComponentProps> & WithStyles<CssRules>) => {
+        const { index, style, classes } = props;
         const it = itemList[index];
         const level = it.level || 0;
-        const { classes, toggleItemActive, disableRipple, currentItemUuid, useRadioButtons } = props;
+        const { toggleItemActive, disableRipple, currentItemUuid, useRadioButtons } = treeProps;
         const { listItem, loader, toggableIconContainer, renderContainer } = classes;
-        const { levelIndentation = 20, itemRightPadding = 20 } = props;
+        const { levelIndentation = 20, itemRightPadding = 20 } = treeProps;
 
-        const showSelection = typeof props.showSelection === 'function'
-            ? props.showSelection
-            : () => props.showSelection ? true : false;
+        const showSelection = typeof treeProps.showSelection === 'function'
+            ? treeProps.showSelection
+            : () => treeProps.showSelection ? true : false;
 
-        const handleRowContextMenu = (item: TreeItem<T>) =>
-            (event: React.MouseEvent<HTMLElement>) =>
-                props.onContextMenu(event, item);
+        const handleRowContextMenu = (item: VirtualTreeItem<T>) =>
+            (event: React.MouseEvent<HTMLElement>) => {
+                treeProps.onContextMenu(event, item);
+            };
 
-        const handleToggleItemOpen = (item: TreeItem<T>) =>
+        const handleToggleItemOpen = (item: VirtualTreeItem<T>) =>
             (event: React.MouseEvent<HTMLElement>) => {
                 event.stopPropagation();
-                props.toggleItemOpen(event, item);
+                treeProps.toggleItemOpen(event, item);
             };
 
         const getToggableIconClassNames = (isOpen?: boolean, isActive?: boolean) => {
@@ -120,8 +121,8 @@ export const Row =  <T, _>(itemList: TreeItem<T>[], render: any) => withStyles(s
             return isSidePanelIconNotNeeded(status, itemCount) ? <span /> : <SidePanelRightArrowIcon style={{ fontSize: '14px' }} />;
         };
 
-        const handleCheckboxChange = (item: TreeItem<T>) => {
-            const { toggleItemSelection } = props;
+        const handleCheckboxChange = (item: VirtualTreeItem<T>) => {
+            const { toggleItemSelection } = treeProps;
             return toggleItemSelection
                 ? (event: React.MouseEvent<HTMLElement>) => {
                     event.stopPropagation();
@@ -166,24 +167,30 @@ export const Row =  <T, _>(itemList: TreeItem<T>[], render: any) => withStyles(s
         </div>;
     });
 
-export const VirtualList = <T, _>(height: number, width: number, items: TreeItem<T>[], render: any) =>
+const itemSize = 30;
+
+export const VirtualList = <T, _>(height: number, width: number, items: VirtualTreeItem<T>[], render: any, treeProps: TreeProps<T>) =>
     <FixedSizeList
         height={height}
         itemCount={items.length}
-        itemSize={30}
+        itemSize={itemSize}
         width={width}
     >
-        {Row(items, render)}
+        {Row(items, render, treeProps)}
     </FixedSizeList>;
 
-export const VirtualTree = withStyles(styles)(
+export const VirtualTree = (maxElements: number) => withStyles(styles)(
     class Component<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
         render(): ReactElement<any> {
             const { items, render } = this.props;
 
-            return <div className={this.props.classes.virtualizedList}><AutoSizer>
+            // Virtual list viewport's maximum height
+            const itemsQty = items && items.length || 0;
+            const viewportHeight = min([itemsQty, maxElements])! * itemSize;
+
+            return <div style={{height: viewportHeight}}><AutoSizer>
                 {({ height, width }) => {
-                    return VirtualList(height, width, items || [], render);
+                    return VirtualList(height, width, items || [], render, this.props);
                 }}
             </AutoSizer></div>;
         }
index 69224059b88dc8277d0d31345d088f846bbcfc25..c7713cbcf08fc996429ff0905e601f14fd6a4ec8 100644 (file)
@@ -16,7 +16,6 @@ export interface TreeNode<T = any> {
     selected: boolean;
     expanded: boolean;
     status: TreeNodeStatus;
-    level?: number;
 }
 
 export enum TreeNodeStatus {
@@ -194,7 +193,6 @@ export const initTreeNode = <T>(data: Pick<TreeNode<T>, 'id' | 'value'> & { pare
     expanded: false,
     status: TreeNodeStatus.INITIAL,
     parent: '',
-    level: 0,
     ...data,
 });
 
index 175a8cef5e705f41a6caf978bd1a95f4e4ea3844..204d4c0e1dbe8f4da27ba74313f560a9a33909fb 100644 (file)
@@ -31,25 +31,15 @@ export const COLLECTION_PANEL_LOAD_FILES_THRESHOLD = 40000;
 
 export const loadCollectionFiles = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        let step = Date.now();
         dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PANEL_LOAD_FILES));
         const files = await services.collectionService.files(uuid);
-        console.log('Get files: ', (Date.now()-step)/1000);
 
         // Given the array of directories and files, create the appropriate tree nodes,
         // sort them, and add the complete url to each.
-        step = Date.now();
         const tree = createCollectionFilesTree(files);
-        console.log('Create tree: ', (Date.now()-step)/1000);
-        step = Date.now();
         const sorted = sortFilesTree(tree);
-        console.log('Sort tree: ', (Date.now()-step)/1000);
-        step = Date.now();
         const mapped = mapTreeValues(services.collectionService.extendFileURL)(sorted);
-        console.log('Add URL: ', (Date.now()-step)/1000);
-        step = Date.now();
         dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(mapped));
-        console.log('Dispatch: ', (Date.now()-step)/1000);
         dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PANEL_LOAD_FILES));
     };
 
index 3cc3569d4330f663ee337873a8c97d48696498ae..7997000350c2639330c91eb82dcda78eda2831e3 100644 (file)
@@ -8,7 +8,8 @@ import {
     CollectionPanelFilesProps
 } from "~/components/collection-panel-files/collection-panel-files";
 import { RootState } from "~/store/store";
-import { TreeItem, TreeItemStatus } from "~/components/tree/tree";
+import { TreeItemStatus } from "~/components/tree/tree";
+import { VirtualTreeItem as TreeItem } from "~/components/tree/virtual-tree";
 import {
     CollectionPanelDirectory,
     CollectionPanelFile,
@@ -75,10 +76,9 @@ const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps,
     },
 });
 
-
 export const CollectionPanelFiles = connect(memoizedMapStateToProps(), mapDispatchToProps)(Component);
 
-export const collectionItemToList = (level: number) => (tree: Tree<CollectionPanelDirectory | CollectionPanelFile>) =>
+const collectionItemToList = (level: number) => (tree: Tree<CollectionPanelDirectory | CollectionPanelFile>) =>
     (id: string): TreeItem<FileTreeData>[] => {
         const node: TreeNode<CollectionPanelDirectory | CollectionPanelFile> = getNode(id)(tree) || initTreeNode({
             id: '',
@@ -107,38 +107,12 @@ export const collectionItemToList = (level: number) => (tree: Tree<CollectionPan
             level,
         };
 
-        const treeItemChilds = [].concat.apply([], node.children.map(collectionItemToList(level+1)(tree)));
+        const treeItemChilds = treeItem.open
+            ? [].concat.apply([], node.children.map(collectionItemToList(level+1)(tree)))
+            : [];
 
         return [
             treeItem,
             ...treeItemChilds,
         ];
     };
-
-const collectionItemToTreeItem = (tree: Tree<CollectionPanelDirectory | CollectionPanelFile>) =>
-    (id: string): TreeItem<FileTreeData> => {
-        const node: TreeNode<CollectionPanelDirectory | CollectionPanelFile> = getNode(id)(tree) || initTreeNode({
-            id: '',
-            parent: '',
-            value: {
-                ...createCollectionDirectory({ name: 'Invalid file' }),
-                selected: false,
-                collapsed: true
-            }
-        });
-        return {
-            active: false,
-            data: {
-                name: node.value.name,
-                size: node.value.type === CollectionFileType.FILE ? node.value.size : undefined,
-                type: node.value.type,
-                url: node.value.url,
-            },
-            id: node.id,
-            items: getNodeChildrenIds(node.id)(tree)
-                .map(collectionItemToTreeItem(tree)),
-            open: node.value.type === CollectionFileType.DIRECTORY ? !node.value.collapsed : false,
-            selected: node.value.selected,
-            status: TreeItemStatus.LOADED
-        };
-    };