[ARVADOS-WORKBENCH2] created: 1.1.4-32-g9999a9d

Git user git at public.curoverse.com
Fri Jun 15 09:50:25 EDT 2018


        at  9999a9db9fede0e1971dc792389982b428a1bb19 (commit)


commit 9999a9db9fede0e1971dc792389982b428a1bb19
Author: Pawel Kowalczyk <pawel.kowalczyk at contractors.roche.com>
Date:   Fri Jun 15 15:34:59 2018 +0200

    Tree-component-adjustments-for-dynamic-contents
    
    Feature #13618
    
    Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk at contractors.roche.com>

diff --git a/src/components/project-tree/project-tree.test.tsx b/src/components/project-tree/project-tree.test.tsx
index d42df08..e88e55a 100644
--- a/src/components/project-tree/project-tree.test.tsx
+++ b/src/components/project-tree/project-tree.test.tsx
@@ -8,6 +8,7 @@ import * as Enzyme from 'enzyme';
 import * as Adapter from 'enzyme-adapter-react-16';
 import ListItemIcon from '@material-ui/core/ListItemIcon';
 import { Collapse } from '@material-ui/core';
+import CircularProgress from '@material-ui/core/CircularProgress';
 
 import ProjectTree from './project-tree';
 import { TreeItem } from '../tree/tree';
@@ -16,7 +17,7 @@ Enzyme.configure({ adapter: new Adapter() });
 
 describe("ProjectTree component", () => {
 
-    it("checks is there ListItemIcon in the ProjectTree component", () => {
+    it("should render ListItemIcon", () => {
         const project: TreeItem<Project> = {
             data: {
                 name: "sample name",
@@ -28,14 +29,15 @@ describe("ProjectTree component", () => {
             },
             id: "3",
             open: true,
-            active: true
+            active: true,
+            status: 1
         };
         const wrapper = mount(<ProjectTree projects={[project]} toggleProjectTreeItem={() => { }} />);
 
         expect(wrapper.find(ListItemIcon).length).toEqual(1);
     });
 
-    it("checks are there two ListItemIcon's in the ProjectTree component", () => {
+    it("should render 2 ListItemIcons", () => {
         const project: Array<TreeItem<Project>> = [
             {
                 data: {
@@ -48,7 +50,8 @@ describe("ProjectTree component", () => {
                 },
                 id: "3",
                 open: false,
-                active: true
+                active: true,
+                status: 1
             },
             {
                 data: {
@@ -61,7 +64,8 @@ describe("ProjectTree component", () => {
                 },
                 id: "3",
                 open: false,
-                active: true
+                active: true,
+                status: 1
             }
         ];
         const wrapper = mount(<ProjectTree projects={project} toggleProjectTreeItem={() => { }} />);
@@ -69,7 +73,45 @@ describe("ProjectTree component", () => {
         expect(wrapper.find(ListItemIcon).length).toEqual(2);
     });
 
-    it("check ProjectTree, when open is changed", () => {
+    it("should render Collapse", () => {
+        const project: Array<TreeItem<Project>> = [
+            {
+                data: {
+                    name: "sample name",
+                    createdAt: "2018-06-12",
+                    modifiedAt: "2018-06-13",
+                    uuid: "uuid",
+                    ownerUuid: "ownerUuid",
+                    href: "href",
+                },
+                id: "3",
+                open: true,
+                active: true,
+                status: 2,
+                items: [
+                    {
+                        data: {
+                            name: "sample name",
+                            createdAt: "2018-06-12",
+                            modifiedAt: "2018-06-13",
+                            uuid: "uuid",
+                            ownerUuid: "ownerUuid",
+                            href: "href",
+                        },
+                        id: "3",
+                        open: true,
+                        active: true,
+                        status: 1
+                    }
+                ]
+            }
+        ];
+        const wrapper = mount(<ProjectTree projects={project} toggleProjectTreeItem={() => { }} />);
+
+        expect(wrapper.find(Collapse).length).toEqual(1);
+    });
+
+    it("should render CircularProgress", () => {
         const project: TreeItem<Project> = {
             data: {
                 name: "sample name",
@@ -80,27 +122,12 @@ describe("ProjectTree component", () => {
                 href: "href",
             },
             id: "3",
-            open: true,
+            open: false,
             active: true,
-            items: [
-                {
-                    data: {
-                        name: "sample name",
-                        createdAt: "2018-06-12",
-                        modifiedAt: "2018-06-13",
-                        uuid: "uuid",
-                        ownerUuid: "ownerUuid",
-                        href: "href",
-                    },
-                    id: "4",
-                    open: false,
-                    active: true
-                }
-            ]
+            status: 1
         };
         const wrapper = mount(<ProjectTree projects={[project]} toggleProjectTreeItem={() => { }} />);
-        wrapper.setState({open: true });
 
-        expect(wrapper.find(Collapse).length).toEqual(1);
+        expect(wrapper.find(CircularProgress).length).toEqual(1);
     });
 });
diff --git a/src/components/project-tree/project-tree.tsx b/src/components/project-tree/project-tree.tsx
index 5243b5e..56b3da3 100644
--- a/src/components/project-tree/project-tree.tsx
+++ b/src/components/project-tree/project-tree.tsx
@@ -9,7 +9,7 @@ import ListItemText from "@material-ui/core/ListItemText/ListItemText";
 import ListItemIcon from '@material-ui/core/ListItemIcon';
 import Typography from '@material-ui/core/Typography';
 
-import Tree, { TreeItem } from '../tree/tree';
+import Tree, { TreeItem, TreeItemStatus } from '../tree/tree';
 import { Project } from '../../models/project';
 
 type CssRules = 'active' | 'listItemText' | 'row' | 'treeContainer';
@@ -27,9 +27,8 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
         marginLeft: '20px',
     },
     treeContainer: {
-        position: 'absolute',
         overflowX: 'visible',
-        marginTop: '80px',
+        overflowY: 'auto',
         minWidth: '240px',
         whiteSpace: 'nowrap',
     }
@@ -37,7 +36,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
 
 export interface ProjectTreeProps {
     projects: Array<TreeItem<Project>>;
-    toggleProjectTreeItem: (id: string) => void;
+    toggleProjectTreeItem: (id: string, status: TreeItemStatus) => void;
 }
 
 class ProjectTree<T> extends React.Component<ProjectTreeProps & WithStyles<CssRules>> {
diff --git a/src/components/tree/tree.test.tsx b/src/components/tree/tree.test.tsx
index ffdc74f..0fab2f3 100644
--- a/src/components/tree/tree.test.tsx
+++ b/src/components/tree/tree.test.tsx
@@ -1,7 +1,55 @@
 // Copyright (C) The Arvados Authors. All rights reserved.
 //
 // SPDX-License-Identifier: AGPL-3.0
+import * as React from 'react';
+import { mount } from 'enzyme';
+import * as Enzyme from 'enzyme';
+import * as Adapter from 'enzyme-adapter-react-16';
+import { Collapse } from '@material-ui/core';
+import CircularProgress from '@material-ui/core/CircularProgress';
+import ListItem from "@material-ui/core/ListItem/ListItem";
 
-it("should render the tree", () => {
-	expect(true).toBe(true);
-});
\ No newline at end of file
+import Tree, {TreeItem} from './tree';
+import { Project } from '../../models/project';
+Enzyme.configure({ adapter: new Adapter() });
+
+describe("ProjectTree component", () => {
+
+	it("should render ListItem", () => {
+		const project: TreeItem<Project> = {
+            data: {
+                name: "sample name",
+                createdAt: "2018-06-12",
+                modifiedAt: "2018-06-13",
+                uuid: "uuid",
+                ownerUuid: "ownerUuid",
+                href: "href",
+            },
+            id: "3",
+            open: true,
+			active: true,
+			loading: true,
+        };
+		const wrapper = mount(<Tree render={project => <div/>} toggleItem={() => { }} items={[project]}/>)
+		expect(wrapper.find(ListItem).length).toEqual(1);
+	});
+    
+    it("should render arrow", () => {
+		const project: TreeItem<Project> = {
+            data: {
+                name: "sample name",
+                createdAt: "2018-06-12",
+                modifiedAt: "2018-06-13",
+                uuid: "uuid",
+                ownerUuid: "ownerUuid",
+                href: "href",
+            },
+            id: "3",
+            open: true,
+			active: true,
+			loading: true,
+        };
+		const wrapper = mount(<Tree render={project => <div/>} toggleItem={() => { }} items={[project]}/>)
+		expect(wrapper.find('i').length).toEqual(1);
+	});
+});
diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx
index fd6299f..936c596 100644
--- a/src/components/tree/tree.tsx
+++ b/src/components/tree/tree.tsx
@@ -11,7 +11,7 @@ import Collapse from "@material-ui/core/Collapse/Collapse";
 import CircularProgress from '@material-ui/core/CircularProgress';
 import { inherits } from 'util';
 
-type CssRules = 'list' | 'activeArrow' | 'arrow' | 'arrowRotate' | 'arrowTransition' | 'loader';
+type CssRules = 'list' | 'activeArrow' | 'inactiveArrow' | 'arrowRotate' | 'arrowTransition' | 'loader' | 'arrowVisibility';
 
 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
     list: {
@@ -22,16 +22,19 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
         color: '#4285F6',
         position: 'absolute',
     },
-    arrow: {
+    inactiveArrow: {
         position: 'absolute',
     },
     arrowTransition: { 
-        transition: 'all 0.3s ease',
+        transition: 'all 0.1s ease',
     },
     arrowRotate: {
-        transition: 'all 0.3s ease',
+        transition: 'all 0.1s ease',
         transform: 'rotate(-90deg)',
     },
+    arrowVisibility: {
+        opacity: 0,
+    },
     loader: {
         position: 'absolute',
         transform: 'translate(0px)',
@@ -39,35 +42,43 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
     }
 });
 
+export enum TreeItemStatus {
+    Initial,
+    Pending,
+    Loaded
+}
+
 export interface TreeItem<T> {
     data: T;
     id: string;
     open: boolean;
     active: boolean;
-    isLoaded: boolean;
+    status: TreeItemStatus;
+    toggled?: boolean;
     items?: Array<TreeItem<T>>;
 }
 
 interface TreeProps<T> {
     items?: Array<TreeItem<T>>;
     render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
-    toggleItem: (id: string) => any;
+    toggleItem: (id: string, status: TreeItemStatus) => any;
     level?: number;
 }
 
 class Tree<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
-    renderArrow (arrowClass: string, open: boolean){
-        return <i className={`${arrowClass} ${open ? `fas fa-caret-down ${this.props.classes.arrowTransition}` : `fas fa-caret-down ${this.props.classes.arrowRotate}`}`} />
+    renderArrow (status: TreeItemStatus, arrowClass: string, open: boolean){
+        return <i className={`${arrowClass} ${status === TreeItemStatus.Pending ? this.props.classes.arrowVisibility : ''} ${open ? `fas fa-caret-down ${this.props.classes.arrowTransition}` : `fas fa-caret-down ${this.props.classes.arrowRotate}`}`} />
     }
     render(): ReactElement<any> {
         const level = this.props.level ? this.props.level : 0;
         const {classes, render, toggleItem, items} = this.props;
-        const {list, arrow, activeArrow, loader} = classes;
+        const {list, inactiveArrow, activeArrow, loader} = classes;
         return <List component="div" className={list}>
             {items && items.map((it: TreeItem<T>, idx: number) =>
              <div key={`item/${level}/${idx}`}>
-                <ListItem button onClick={() => toggleItem(it.id)} className={list} style={{paddingLeft: (level + 1) * 20}}>
-                    {it.isLoaded ? this.renderArrow(it.active ? activeArrow : arrow, it.open) : <CircularProgress size={10} className={loader}/> }
+                <ListItem button onClick={() => toggleItem(it.id, it.status)} className={list} style={{paddingLeft: (level + 1) * 20}}>
+                    {it.status === TreeItemStatus.Pending ? <CircularProgress size={10} className={loader}/> : null}
+                    {it.toggled && it.items && it.items.length === 0 ? null : this.renderArrow(it.status, it.active ? activeArrow : inactiveArrow, it.open)}
                     {render(it, level)}
                 </ListItem>
                 {it.items && it.items.length > 0 &&
diff --git a/src/services/project-service/project-service.ts b/src/services/project-service/project-service.ts
index e1e490b..e1f4af9 100644
--- a/src/services/project-service/project-service.ts
+++ b/src/services/project-service/project-service.ts
@@ -34,7 +34,7 @@ interface GroupsResponse {
 
 export default class ProjectService {
     public getProjectList = (parentUuid?: string) => (dispatch: Dispatch): Promise<Project[]> => {
-        dispatch(actions.PROJECTS_REQUEST());
+        dispatch(actions.PROJECTS_REQUEST(parentUuid));
         if (parentUuid) {
             const fb = new FilterBuilder();
             fb.addLike(FilterField.OWNER_UUID, parentUuid);
diff --git a/src/store/project/project-action.ts b/src/store/project/project-action.ts
index 87ecbda..2856de6 100644
--- a/src/store/project/project-action.ts
+++ b/src/store/project/project-action.ts
@@ -8,7 +8,7 @@ import { default as unionize, ofType, UnionOf } from "unionize";
 const actions = unionize({
     CREATE_PROJECT: ofType<Project>(),
     REMOVE_PROJECT: ofType<string>(),
-    PROJECTS_REQUEST: {},
+    PROJECTS_REQUEST: ofType<any>(),
     PROJECTS_SUCCESS: ofType<{ projects: Project[], parentItemId?: string }>(),
     TOGGLE_PROJECT_TREE_ITEM: ofType<string>()
 }, {
diff --git a/src/store/project/project-reducer.test.ts b/src/store/project/project-reducer.test.ts
index 9c1ed3b..adfce96 100644
--- a/src/store/project/project-reducer.test.ts
+++ b/src/store/project/project-reducer.test.ts
@@ -39,13 +39,15 @@ describe('project-reducer', () => {
                 open: false,
                 id: "test123",
                 items: [],
-                data: project
+                data: project,
+                status: 0
             }, {
                 active: false,
                 open: false,
                 id: "test123",
                 items: [],
-                data: project
+                data: project,
+                status: 0
             }
         ]);
     });
diff --git a/src/store/project/project-reducer.ts b/src/store/project/project-reducer.ts
index 694235a..8aa69ff 100644
--- a/src/store/project/project-reducer.ts
+++ b/src/store/project/project-reducer.ts
@@ -4,7 +4,7 @@
 
 import { Project } from "../../models/project";
 import actions, { ProjectAction } from "./project-action";
-import { TreeItem } from "../../components/tree/tree";
+import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
 import * as _ from "lodash";
 
 export type ProjectState = Array<TreeItem<Project>>;
@@ -29,24 +29,19 @@ function resetTreeActivity<T>(tree: Array<TreeItem<T>>) {
     }
 }
 
-function resetTreeLoader<T>(tree: Array<TreeItem<T>>) {
-    for (const t of tree) {
-        t.isLoaded = true;
-        resetTreeLoader(t.items ? t.items : []);
-    }
-}
-
 function updateProjectTree(tree: Array<TreeItem<Project>>, projects: Project[], parentItemId?: string): Array<TreeItem<Project>> {
-    resetTreeLoader(tree)
     let treeItem;
     if (parentItemId) {
         treeItem = findTreeItem(tree, parentItemId);
+        if (treeItem) {
+            treeItem.status = TreeItemStatus.Loaded;
+        }
     }
     const items = projects.map((p, idx) => ({
         id: p.uuid,
         open: false,
         active: false,
-        isLoaded: true,
+        status: TreeItemStatus.Initial,
         data: p,
         items: []
     } as TreeItem<Project>));
@@ -59,12 +54,18 @@ function updateProjectTree(tree: Array<TreeItem<Project>>, projects: Project[],
     return items;
 }
 
-
 const projectsReducer = (state: ProjectState = [], action: ProjectAction) => {
     return actions.match(action, {
         CREATE_PROJECT: project => [...state, project],
         REMOVE_PROJECT: () => state,
-        PROJECTS_REQUEST: () => state,
+        PROJECTS_REQUEST: itemId => {
+            const tree = _.cloneDeep(state);
+            const item = findTreeItem(tree, itemId);
+            if (item) {
+                item.status = TreeItemStatus.Pending;
+            }
+            return tree;
+        },
         PROJECTS_SUCCESS: ({ projects, parentItemId }) => {
             return updateProjectTree(state, projects, parentItemId);
         },
@@ -75,7 +76,7 @@ const projectsReducer = (state: ProjectState = [], action: ProjectAction) => {
             if (item) {
                 item.open = !item.open;
                 item.active = true;
-                item.isLoaded = false;
+                item.toggled = true;
             }
             return tree;
         },
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 3767e7b..59495e9 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -25,7 +25,7 @@ import { RootState } from "../../store/store";
 import projectActions from "../../store/project/project-action"
 
 import ProjectTree from '../../components/project-tree/project-tree';
-import { TreeItem } from "../../components/tree/tree";
+import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
 import { Project } from "../../models/project";
 import { projectService } from '../../services/services';
 
@@ -104,54 +104,56 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
         });
     };
 
-    toggleProjectTreeItem = (itemId: string) => {
-        this.props.projects ? 
-            this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId)) : ( 
-                this.props.dispatch<any>(projectService.getProjectList(itemId)).then(() => {
-                    this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId));
-                }))
+    toggleProjectTreeItem = (itemId: string, status: TreeItemStatus) => {
+        if (status === TreeItemStatus.Loaded) {
+            this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId))
+        } else {
+            this.props.dispatch<any>(projectService.getProjectList(itemId)).then(() => {
+                this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId));
+            })
+        }
     };
 
     render() {
-        const {classes, user} = this.props;
+        const { classes, user } = this.props;
         return (
             <div className={classes.root}>
                 <AppBar position="absolute" className={classes.appBar}>
                     <Toolbar>
-                        <Typography variant="title" color="inherit" noWrap style={{flexGrow: 1}}>
-                            <span>Arvados</span><br/><span style={{fontSize: 12}}>Workbench 2</span>
+                        <Typography variant="title" color="inherit" noWrap style={{ flexGrow: 1 }}>
+                            <span>Arvados</span><br /><span style={{ fontSize: 12 }}>Workbench 2</span>
                         </Typography>
                         {user ?
-                            <Grid container style={{width: 'auto'}}>
-                                <Grid container style={{width: 'auto'}} alignItems='center'>
+                            <Grid container style={{ width: 'auto' }}>
+                                <Grid container style={{ width: 'auto' }} alignItems='center'>
                                     <Typography variant="title" color="inherit" noWrap>
                                         {user.firstName} {user.lastName}
                                     </Typography>
                                 </Grid>
                                 <Grid item>
                                     <IconButton
-                                          aria-owns={this.state.anchorEl ? 'menu-appbar' : undefined}
-                                          aria-haspopup="true"
-                                          onClick={this.handleOpenMenu}
-                                          color="inherit">
-                                      <AccountCircle/>
+                                        aria-owns={this.state.anchorEl ? 'menu-appbar' : undefined}
+                                        aria-haspopup="true"
+                                        onClick={this.handleOpenMenu}
+                                        color="inherit">
+                                        <AccountCircle />
                                     </IconButton>
                                 </Grid>
                                 <Menu
-                                  id="menu-appbar"
-                                  anchorEl={this.state.anchorEl}
-                                  anchorOrigin={{
-                                    vertical: 'top',
-                                    horizontal: 'right',
-                                  }}
-                                  transformOrigin={{
-                                    vertical: 'top',
-                                    horizontal: 'right',
-                                  }}
-                                  open={!!this.state.anchorEl}
-                                  onClose={this.handleClose}>
-                                  <MenuItem onClick={this.logout}>Logout</MenuItem>
-                                  <MenuItem onClick={this.handleClose}>My account</MenuItem>
+                                    id="menu-appbar"
+                                    anchorEl={this.state.anchorEl}
+                                    anchorOrigin={{
+                                        vertical: 'top',
+                                        horizontal: 'right',
+                                    }}
+                                    transformOrigin={{
+                                        vertical: 'top',
+                                        horizontal: 'right',
+                                    }}
+                                    open={!!this.state.anchorEl}
+                                    onClose={this.handleClose}>
+                                    <MenuItem onClick={this.logout}>Logout</MenuItem>
+                                    <MenuItem onClick={this.handleClose}>My account</MenuItem>
                                 </Menu>
                             </Grid>
                             :
@@ -160,20 +162,20 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
                     </Toolbar>
                 </AppBar>
                 {user &&
-                <Drawer
-                    variant="permanent"
-                    classes={{
-                        paper: classes.drawerPaper,
-                    }}>
-                    <div className={classes.toolbar}/>
-                    <ProjectTree
-                        projects={this.props.projects}
-                        toggleProjectTreeItem={this.toggleProjectTreeItem}/>
-                </Drawer>}
+                    <Drawer
+                        variant="permanent"
+                        classes={{
+                            paper: classes.drawerPaper,
+                        }}>
+                        <div className={classes.toolbar} />
+                        <ProjectTree
+                            projects={this.props.projects}
+                            toggleProjectTreeItem={this.toggleProjectTreeItem} />
+                    </Drawer>}
                 <main className={classes.content}>
-                    <div className={classes.toolbar}/>
+                    <div className={classes.toolbar} />
                     <Switch>
-                        <Route path="/project/:name" component={ProjectList}/>
+                        <Route path="/project/:name" component={ProjectList} />
                     </Switch>
                 </main>
             </div>

commit 7ba0401cff3906814ba976ea36adab9b3d5c3922
Merge: 7f95b6e d5fb635
Author: Pawel Kowalczyk <pawel.kowalczyk at contractors.roche.com>
Date:   Thu Jun 14 09:40:20 2018 +0200

    Merge branch 'master' of git.curoverse.com:arvados-workbench2 into 13618-Tree-component-adjustments-for-dynamic-contents
    
    Feature #13618
    
    Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk at contractors.roche.com>


commit 7f95b6ead934a15e849c7650a08475633a8bfb09
Author: Pawel Kowalczyk <pawel.kowalczyk at contractors.roche.com>
Date:   Thu Jun 14 09:39:55 2018 +0200

    Tree component adjsustments for dynamic contents
    
    Feature #13618
    
    Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk at contractors.roche.com>

diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx
index d8397d6..fd6299f 100644
--- a/src/components/tree/tree.tsx
+++ b/src/components/tree/tree.tsx
@@ -8,8 +8,10 @@ import ListItem from "@material-ui/core/ListItem/ListItem";
 import { StyleRulesCallback, Theme, withStyles, WithStyles } from '@material-ui/core/styles';
 import { ReactElement } from "react";
 import Collapse from "@material-ui/core/Collapse/Collapse";
+import CircularProgress from '@material-ui/core/CircularProgress';
+import { inherits } from 'util';
 
-type CssRules = 'list' | 'activeArrow' | 'arrow' | 'arrowRotate';
+type CssRules = 'list' | 'activeArrow' | 'arrow' | 'arrowRotate' | 'arrowTransition' | 'loader';
 
 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
     list: {
@@ -23,8 +25,17 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
     arrow: {
         position: 'absolute',
     },
+    arrowTransition: { 
+        transition: 'all 0.3s ease',
+    },
     arrowRotate: {
+        transition: 'all 0.3s ease',
         transform: 'rotate(-90deg)',
+    },
+    loader: {
+        position: 'absolute',
+        transform: 'translate(0px)',
+        top: '3px'  
     }
 });
 
@@ -33,6 +44,7 @@ export interface TreeItem<T> {
     id: string;
     open: boolean;
     active: boolean;
+    isLoaded: boolean;
     items?: Array<TreeItem<T>>;
 }
 
@@ -44,18 +56,18 @@ interface TreeProps<T> {
 }
 
 class Tree<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
-    renderArrow (items: boolean, arrowClass: string, open: boolean){
-        return <i className={`${arrowClass} ${open ? "fas fa-caret-down" : `fas fa-caret-down ${this.props.classes.arrowRotate}`}`} />
+    renderArrow (arrowClass: string, open: boolean){
+        return <i className={`${arrowClass} ${open ? `fas fa-caret-down ${this.props.classes.arrowTransition}` : `fas fa-caret-down ${this.props.classes.arrowRotate}`}`} />
     }
     render(): ReactElement<any> {
         const level = this.props.level ? this.props.level : 0;
         const {classes, render, toggleItem, items} = this.props;
-        const {list, arrow, activeArrow} = classes;
+        const {list, arrow, activeArrow, loader} = classes;
         return <List component="div" className={list}>
             {items && items.map((it: TreeItem<T>, idx: number) =>
              <div key={`item/${level}/${idx}`}>
                 <ListItem button onClick={() => toggleItem(it.id)} className={list} style={{paddingLeft: (level + 1) * 20}}>
-                    {this.renderArrow(true, it.active ? activeArrow : arrow, it.open)}
+                    {it.isLoaded ? this.renderArrow(it.active ? activeArrow : arrow, it.open) : <CircularProgress size={10} className={loader}/> }
                     {render(it, level)}
                 </ListItem>
                 {it.items && it.items.length > 0 &&
diff --git a/src/store/project/project-reducer.ts b/src/store/project/project-reducer.ts
index 887cf89..694235a 100644
--- a/src/store/project/project-reducer.ts
+++ b/src/store/project/project-reducer.ts
@@ -29,7 +29,15 @@ function resetTreeActivity<T>(tree: Array<TreeItem<T>>) {
     }
 }
 
+function resetTreeLoader<T>(tree: Array<TreeItem<T>>) {
+    for (const t of tree) {
+        t.isLoaded = true;
+        resetTreeLoader(t.items ? t.items : []);
+    }
+}
+
 function updateProjectTree(tree: Array<TreeItem<Project>>, projects: Project[], parentItemId?: string): Array<TreeItem<Project>> {
+    resetTreeLoader(tree)
     let treeItem;
     if (parentItemId) {
         treeItem = findTreeItem(tree, parentItemId);
@@ -38,6 +46,7 @@ function updateProjectTree(tree: Array<TreeItem<Project>>, projects: Project[],
         id: p.uuid,
         open: false,
         active: false,
+        isLoaded: true,
         data: p,
         items: []
     } as TreeItem<Project>));
@@ -66,6 +75,7 @@ const projectsReducer = (state: ProjectState = [], action: ProjectAction) => {
             if (item) {
                 item.open = !item.open;
                 item.active = true;
+                item.isLoaded = false;
             }
             return tree;
         },
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index d18d113..3767e7b 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -105,9 +105,11 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
     };
 
     toggleProjectTreeItem = (itemId: string) => {
-        this.props.dispatch<any>(projectService.getProjectList(itemId)).then(() => {
-            this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId));
-        });
+        this.props.projects ? 
+            this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId)) : ( 
+                this.props.dispatch<any>(projectService.getProjectList(itemId)).then(() => {
+                    this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId));
+                }))
     };
 
     render() {

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list