From 9999a9db9fede0e1971dc792389982b428a1bb19 Mon Sep 17 00:00:00 2001 From: Pawel Kowalczyk Date: Fri, 15 Jun 2018 15:34:59 +0200 Subject: [PATCH] Tree-component-adjustments-for-dynamic-contents Feature #13618 Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk --- .../project-tree/project-tree.test.tsx | 75 +++++++++++----- src/components/project-tree/project-tree.tsx | 7 +- src/components/tree/tree.test.tsx | 54 +++++++++++- src/components/tree/tree.tsx | 33 ++++--- .../project-service/project-service.ts | 2 +- src/store/project/project-action.ts | 2 +- src/store/project/project-reducer.test.ts | 6 +- src/store/project/project-reducer.ts | 27 +++--- src/views/workbench/workbench.tsx | 88 ++++++++++--------- 9 files changed, 192 insertions(+), 102 deletions(-) diff --git a/src/components/project-tree/project-tree.test.tsx b/src/components/project-tree/project-tree.test.tsx index d42df088..e88e55ad 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 = { data: { name: "sample name", @@ -28,14 +29,15 @@ describe("ProjectTree component", () => { }, id: "3", open: true, - active: true + active: true, + status: 1 }; const wrapper = mount( { }} />); 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> = [ { 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( { }} />); @@ -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> = [ + { + 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( { }} />); + + expect(wrapper.find(Collapse).length).toEqual(1); + }); + + it("should render CircularProgress", () => { const project: TreeItem = { 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( { }} />); - 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 5243b5e7..56b3da32 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 = (theme: Theme) => ({ marginLeft: '20px', }, treeContainer: { - position: 'absolute', overflowX: 'visible', - marginTop: '80px', + overflowY: 'auto', minWidth: '240px', whiteSpace: 'nowrap', } @@ -37,7 +36,7 @@ const styles: StyleRulesCallback = (theme: Theme) => ({ export interface ProjectTreeProps { projects: Array>; - toggleProjectTreeItem: (id: string) => void; + toggleProjectTreeItem: (id: string, status: TreeItemStatus) => void; } class ProjectTree extends React.Component> { diff --git a/src/components/tree/tree.test.tsx b/src/components/tree/tree.test.tsx index ffdc74f9..0fab2f37 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 = { + 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(
} toggleItem={() => { }} items={[project]}/>) + expect(wrapper.find(ListItem).length).toEqual(1); + }); + + it("should render arrow", () => { + const project: TreeItem = { + 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(
} 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 fd6299f7..936c5965 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 = (theme: Theme) => ({ list: { @@ -22,16 +22,19 @@ const styles: StyleRulesCallback = (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 = (theme: Theme) => ({ } }); +export enum TreeItemStatus { + Initial, + Pending, + Loaded +} + export interface TreeItem { data: T; id: string; open: boolean; active: boolean; - isLoaded: boolean; + status: TreeItemStatus; + toggled?: boolean; items?: Array>; } interface TreeProps { items?: Array>; render: (item: TreeItem, level?: number) => ReactElement<{}>; - toggleItem: (id: string) => any; + toggleItem: (id: string, status: TreeItemStatus) => any; level?: number; } class Tree extends React.Component & WithStyles, {}> { - renderArrow (arrowClass: string, open: boolean){ - return + renderArrow (status: TreeItemStatus, arrowClass: string, open: boolean){ + return } render(): ReactElement { 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 {items && items.map((it: TreeItem, idx: number) =>
- toggleItem(it.id)} className={list} style={{paddingLeft: (level + 1) * 20}}> - {it.isLoaded ? this.renderArrow(it.active ? activeArrow : arrow, it.open) : } + toggleItem(it.id, it.status)} className={list} style={{paddingLeft: (level + 1) * 20}}> + {it.status === TreeItemStatus.Pending ? : null} + {it.toggled && it.items && it.items.length === 0 ? null : this.renderArrow(it.status, it.active ? activeArrow : inactiveArrow, it.open)} {render(it, level)} {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 e1e490bb..e1f4af94 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 => { - 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 87ecbda9..2856de66 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(), REMOVE_PROJECT: ofType(), - PROJECTS_REQUEST: {}, + PROJECTS_REQUEST: ofType(), PROJECTS_SUCCESS: ofType<{ projects: Project[], parentItemId?: string }>(), TOGGLE_PROJECT_TREE_ITEM: ofType() }, { diff --git a/src/store/project/project-reducer.test.ts b/src/store/project/project-reducer.test.ts index 9c1ed3b4..adfce969 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 694235a6..8aa69ff2 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>; @@ -29,24 +29,19 @@ function resetTreeActivity(tree: Array>) { } } -function resetTreeLoader(tree: Array>) { - for (const t of tree) { - t.isLoaded = true; - resetTreeLoader(t.items ? t.items : []); - } -} - function updateProjectTree(tree: Array>, projects: Project[], parentItemId?: string): Array> { - 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)); @@ -59,12 +54,18 @@ function updateProjectTree(tree: Array>, 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 3767e7b0..59495e94 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 { }); }; - toggleProjectTreeItem = (itemId: string) => { - this.props.projects ? - this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId)) : ( - this.props.dispatch(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(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 (
- - Arvados
Workbench 2 + + Arvados
Workbench 2
{user ? - - + + {user.firstName} {user.lastName} - + aria-owns={this.state.anchorEl ? 'menu-appbar' : undefined} + aria-haspopup="true" + onClick={this.handleOpenMenu} + color="inherit"> + - Logout - My account + id="menu-appbar" + anchorEl={this.state.anchorEl} + anchorOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + open={!!this.state.anchorEl} + onClose={this.handleClose}> + Logout + My account : @@ -160,20 +162,20 @@ class Workbench extends React.Component {
{user && - -
- - } + +
+ + }
-
+
- +
-- 2.30.2