From 3015426750f11fdc97d55a29f2a662e2f272f5d4 Mon Sep 17 00:00:00 2001 From: Pawel Kowalczyk Date: Thu, 21 Jun 2018 00:05:57 +0200 Subject: [PATCH] Left-side-panel-without-tests Feature ##13598 Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk --- src/components/project-tree/project-tree.tsx | 64 ++++++----- src/components/side-panel/side-panel.tsx | 109 +++++++++++++++++++ src/components/tree/tree.tsx | 83 +++++++------- src/index.tsx | 5 +- src/store/project/project-action.ts | 5 +- src/store/project/project-reducer.ts | 16 ++- src/store/side-panel/side-panel-action.ts | 16 +++ src/store/side-panel/side-panel-reducer.ts | 86 +++++++++++++++ src/store/store.ts | 8 +- src/views/workbench/workbench.tsx | 54 +++++---- 10 files changed, 342 insertions(+), 104 deletions(-) create mode 100644 src/components/side-panel/side-panel.tsx create mode 100644 src/store/side-panel/side-panel-action.ts create mode 100644 src/store/side-panel/side-panel-reducer.ts diff --git a/src/components/project-tree/project-tree.tsx b/src/components/project-tree/project-tree.tsx index 275805ff..65e94e49 100644 --- a/src/components/project-tree/project-tree.tsx +++ b/src/components/project-tree/project-tree.tsx @@ -12,57 +12,55 @@ import Typography from '@material-ui/core/Typography'; import Tree, { TreeItem, TreeItemStatus } from '../tree/tree'; import { Project } from '../../models/project'; -type CssRules = 'active' | 'listItemText' | 'row' | 'treeContainer'; - -const styles: StyleRulesCallback = (theme: Theme) => ({ - active: { - color: '#4285F6', - }, - listItemText: { - padding: '0px', - }, - row: { - display: 'flex', - alignItems: 'center', - marginLeft: '20px', - }, - treeContainer: { - marginTop: '37px', - overflowX: 'visible', - overflowY: 'auto', - minWidth: '240px', - whiteSpace: 'nowrap', - } -}); - export interface ProjectTreeProps { projects: Array>; - toggleProjectTreeItem: (id: string, status: TreeItemStatus) => void; + toggleProjectTreeItemOpen: (id: string, status: TreeItemStatus) => void; + toggleProjectTreeItemActive: (id: string) => void; } class ProjectTree extends React.Component> { render(): ReactElement { - const {classes, projects} = this.props; - const {active, listItemText, row, treeContainer} = classes; + const { classes, projects, toggleProjectTreeItemOpen, toggleProjectTreeItemActive } = this.props; + const { active, listItemText, row, treeContainer } = classes; return (
, level: number) => - {level === 0 ? : } + - {project.data.name} - - }/> + {project.data.name} + } /> - }/> + } />
); } } +type CssRules = 'active' | 'listItemText' | 'row' | 'treeContainer'; + +const styles: StyleRulesCallback = (theme: Theme) => ({ + active: { + color: '#4285F6', + }, + listItemText: { + padding: '0px', + }, + row: { + display: 'flex', + alignItems: 'center', + marginLeft: '20px', + }, + treeContainer: { + minWidth: '240px', + whiteSpace: 'nowrap', + marginLeft: '13px', + } +}); + export default withStyles(styles)(ProjectTree); diff --git a/src/components/side-panel/side-panel.tsx b/src/components/side-panel/side-panel.tsx new file mode 100644 index 00000000..81ebd86b --- /dev/null +++ b/src/components/side-panel/side-panel.tsx @@ -0,0 +1,109 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as React from 'react'; +import { ReactElement } from 'react'; +import { StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core/styles'; +import List from "@material-ui/core/List/List"; +import ListItem from "@material-ui/core/ListItem/ListItem"; +import ListItemText from "@material-ui/core/ListItemText/ListItemText"; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import Collapse from "@material-ui/core/Collapse/Collapse"; + +import { Typography } from '@material-ui/core'; + +export interface SidePanelItem { + id: string; + name: string; + icon: string; + active?: boolean; + open?: boolean; +} + +interface SidePanelProps { + toggleSidePanelOpen: (id: string) => void; + toggleSidePanelActive: (id: string) => void; + sidePanelItems: SidePanelItem[]; +} + +class SidePanel extends React.Component> { + render(): ReactElement { + const { classes, toggleSidePanelOpen, toggleSidePanelActive, sidePanelItems } = this.props; + const { listItemText, leftSidePanelContainer, row, list, icon, projectIcon, active, activeArrow, inactiveArrow, arrowTransition, arrowRotate } = classes; + return ( +
+ + {sidePanelItems.map(it => ( + + toggleSidePanelActive(it.id)}> + + {it.name === "Projects" ? toggleSidePanelOpen(it.id)} className={`${it.active ? activeArrow : inactiveArrow} + ${it.open ? `fas fa-caret-down ${arrowTransition}` : `fas fa-caret-down ${arrowRotate}`}`} /> : null} + + + + {it.name}} /> + + + {it.name === "Projects" ? ( + + {this.props.children} + ) : null} + + ))} + +
+ ); + } +} + +type CssRules = 'active' | 'listItemText' | 'row' | 'leftSidePanelContainer' | 'list' | 'icon' | 'projectIcon' | + 'activeArrow' | 'inactiveArrow' | 'arrowRotate' | 'arrowTransition'; + +const styles: StyleRulesCallback = (theme: Theme) => ({ + active: { + color: '#4285F6', + }, + listItemText: { + padding: '0px', + }, + row: { + display: 'flex', + alignItems: 'center', + }, + activeArrow: { + color: '#4285F6', + position: 'absolute', + }, + inactiveArrow: { + position: 'absolute', + }, + arrowTransition: { + transition: 'all 0.1s ease', + }, + arrowRotate: { + transition: 'all 0.1s ease', + transform: 'rotate(-90deg)', + }, + leftSidePanelContainer: { + position: 'absolute', + top: '100px', + overflowY: 'auto', + minWidth: '240px', + whiteSpace: 'nowrap', + }, + list: { + paddingBottom: '5px', + paddingTop: '5px', + paddingLeft: '14px' + }, + icon: { + minWidth: '20px', + }, + projectIcon: { + marginLeft: '17px', + } +}); + +export default withStyles(styles)(SidePanel); \ No newline at end of file diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index 6731950c..5dd45878 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -9,38 +9,6 @@ import { StyleRulesCallback, Theme, withStyles, WithStyles } from '@material-ui/ 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' | 'inactiveArrow' | 'arrowRotate' | 'arrowTransition' | 'loader' | 'arrowVisibility'; - -const styles: StyleRulesCallback = (theme: Theme) => ({ - list: { - paddingBottom: '3px', - paddingTop: '3px', - }, - activeArrow: { - color: '#4285F6', - position: 'absolute', - }, - inactiveArrow: { - position: 'absolute', - }, - arrowTransition: { - transition: 'all 0.1s ease', - }, - arrowRotate: { - transition: 'all 0.1s ease', - transform: 'rotate(-90deg)', - }, - arrowVisibility: { - opacity: 0, - }, - loader: { - position: 'absolute', - transform: 'translate(0px)', - top: '3px' - } -}); export enum TreeItemStatus { Initial, @@ -61,27 +29,28 @@ export interface TreeItem { interface TreeProps { items?: Array>; render: (item: TreeItem, level?: number) => ReactElement<{}>; - toggleItem: (id: string, status: TreeItemStatus) => any; + toggleItemOpen: (id: string, status: TreeItemStatus) => void; + toggleItemActive: (id: string) => void; level?: number; } class Tree extends React.Component & WithStyles, {}> { renderArrow(status: TreeItemStatus, arrowClass: string, open: boolean, id: string) { - return this.props.toggleItem(id, status)} + const { arrowTransition, arrowVisibility, arrowRotate } = this.props.classes; + return this.props.toggleItemOpen(id, status)} 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}`}`} />; + ${arrowClass} + ${status === TreeItemStatus.Pending ? arrowVisibility : ''} + ${open ? `fas fa-caret-down ${arrowTransition}` : `fas fa-caret-down ${arrowRotate}`}`} />; } render(): ReactElement { const level = this.props.level ? this.props.level : 0; - const { classes, render, toggleItem, items } = this.props; + const { classes, render, toggleItemOpen, items, toggleItemActive } = this.props; const { list, inactiveArrow, activeArrow, loader } = classes; return {items && items.map((it: TreeItem, idx: number) =>
- + toggleItemActive(it.id)}> {it.status === TreeItemStatus.Pending ? : null} {it.toggled && it.items && it.items.length === 0 ? null : this.renderArrow(it.status, it.active ? activeArrow : inactiveArrow, it.open, it.id)} {render(it, level)} @@ -91,7 +60,8 @@ class Tree extends React.Component & WithStyles, {}> { }
)} @@ -99,5 +69,36 @@ class Tree extends React.Component & WithStyles, {}> { } } +type CssRules = 'list' | 'activeArrow' | 'inactiveArrow' | 'arrowRotate' | 'arrowTransition' | 'loader' | 'arrowVisibility'; + +const styles: StyleRulesCallback = (theme: Theme) => ({ + list: { + paddingBottom: '3px', + paddingTop: '3px', + }, + activeArrow: { + color: '#4285F6', + position: 'absolute', + }, + inactiveArrow: { + position: 'absolute', + }, + arrowTransition: { + transition: 'all 0.1s ease', + }, + arrowRotate: { + transition: 'all 0.1s ease', + transform: 'rotate(-90deg)', + }, + arrowVisibility: { + opacity: 0, + }, + loader: { + position: 'absolute', + transform: 'translate(0px)', + top: '3px' + } +}); + const StyledTree = withStyles(styles)(Tree); export default StyledTree; diff --git a/src/index.tsx b/src/index.tsx index ca92c381..cebe9354 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,7 +6,6 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { Provider } from "react-redux"; import Workbench from './views/workbench/workbench'; -import ProjectList from './components/project-list/project-list'; import './index.css'; import { Route } from "react-router"; import createBrowserHistory from "history/createBrowserHistory"; @@ -15,6 +14,7 @@ import { ConnectedRouter } from "react-router-redux"; import ApiToken from "./components/api-token/api-token"; import authActions from "./store/auth/auth-action"; import { authService, projectService } from "./services/services"; +import { sidePanelData } from './store/side-panel/side-panel-reducer'; const history = createBrowserHistory(); @@ -26,7 +26,8 @@ const store = configureStore({ }, auth: { user: undefined - } + }, + sidePanel: sidePanelData }, history); store.dispatch(authActions.INIT()); diff --git a/src/store/project/project-action.ts b/src/store/project/project-action.ts index 2856de66..b2c6b037 100644 --- a/src/store/project/project-action.ts +++ b/src/store/project/project-action.ts @@ -1,16 +1,17 @@ // Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 +import { default as unionize, ofType, UnionOf } from "unionize"; import { Project } from "../../models/project"; -import { default as unionize, ofType, UnionOf } from "unionize"; const actions = unionize({ CREATE_PROJECT: ofType(), REMOVE_PROJECT: ofType(), PROJECTS_REQUEST: ofType(), PROJECTS_SUCCESS: ofType<{ projects: Project[], parentItemId?: string }>(), - TOGGLE_PROJECT_TREE_ITEM: ofType() + TOGGLE_PROJECT_TREE_ITEM_OPEN: ofType(), + TOGGLE_PROJECT_TREE_ITEM_ACTIVE: ofType(), }, { tag: 'type', value: 'payload' diff --git a/src/store/project/project-reducer.ts b/src/store/project/project-reducer.ts index 8aa69ff2..fb1adbfa 100644 --- a/src/store/project/project-reducer.ts +++ b/src/store/project/project-reducer.ts @@ -2,10 +2,11 @@ // // SPDX-License-Identifier: AGPL-3.0 +import * as _ from "lodash"; + import { Project } from "../../models/project"; import actions, { ProjectAction } from "./project-action"; import { TreeItem, TreeItemStatus } from "../../components/tree/tree"; -import * as _ from "lodash"; export type ProjectState = Array>; @@ -69,14 +70,21 @@ const projectsReducer = (state: ProjectState = [], action: ProjectAction) => { PROJECTS_SUCCESS: ({ projects, parentItemId }) => { return updateProjectTree(state, projects, parentItemId); }, - TOGGLE_PROJECT_TREE_ITEM: itemId => { + TOGGLE_PROJECT_TREE_ITEM_OPEN: itemId => { const tree = _.cloneDeep(state); - resetTreeActivity(tree); const item = findTreeItem(tree, itemId); if (item) { + item.toggled = true; item.open = !item.open; + } + return tree; + }, + TOGGLE_PROJECT_TREE_ITEM_ACTIVE: itemId => { + const tree = _.cloneDeep(state); + resetTreeActivity(tree); + const item = findTreeItem(tree, itemId); + if (item) { item.active = true; - item.toggled = true; } return tree; }, diff --git a/src/store/side-panel/side-panel-action.ts b/src/store/side-panel/side-panel-action.ts new file mode 100644 index 00000000..08653460 --- /dev/null +++ b/src/store/side-panel/side-panel-action.ts @@ -0,0 +1,16 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import { default as unionize, ofType, UnionOf } from "unionize"; + +const actions = unionize({ + TOGGLE_SIDE_PANEL_ITEM_OPEN: ofType(), + TOGGLE_SIDE_PANEL_ITEM_ACTIVE: ofType(), +}, { + tag: 'type', + value: 'payload' +}); + +export type SidePanelAction = UnionOf; +export default actions; \ No newline at end of file diff --git a/src/store/side-panel/side-panel-reducer.ts b/src/store/side-panel/side-panel-reducer.ts new file mode 100644 index 00000000..96c97530 --- /dev/null +++ b/src/store/side-panel/side-panel-reducer.ts @@ -0,0 +1,86 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import * as _ from "lodash"; + +import actions, { SidePanelAction } from './side-panel-action'; +import { SidePanelItem } from '../../components/side-panel/side-panel'; + +export type SidePanelState = SidePanelItem[]; + +const sidePanelReducer = (state: SidePanelState = sidePanelData, action: SidePanelAction) => { + return actions.match(action, { + TOGGLE_SIDE_PANEL_ITEM_OPEN: () => { + const sidePanel = _.cloneDeep(state); + sidePanel[0].open = !sidePanel[0].open; + return sidePanel; + }, + TOGGLE_SIDE_PANEL_ITEM_ACTIVE: itemId => { + const sidePanel = _.cloneDeep(state); + resetSidePanelActivity(sidePanel); + sidePanel.map(it => { + if (it.id === itemId) { + it.active = true; + } + }); + resetProjectsCollapse(sidePanel); + return sidePanel; + }, + default: () => state + }); +}; + +export const sidePanelData = [ + { + id: "1", + name: "Projects", + icon: "fas fa-th fa-fw", + open: false, + active: false, + }, + { + id: "2", + name: "Shared with me", + icon: "fas fa-users fa-fw", + active: false, + }, + { + id: "3", + name: "Workflows", + icon: "fas fa-cogs fa-fw", + active: false, + }, + { + id: "4", + name: "Recent open", + icon: "icon-time fa-fw", + active: false, + }, + { + id: "5", + name: "Favorites", + icon: "fas fa-star fa-fw", + active: false, + }, + { + id: "6", + name: "Trash", + icon: "fas fa-trash-alt fa-fw", + active: false, + } +]; + +function resetSidePanelActivity(sidePanel: SidePanelItem[]) { + for (const t of sidePanel) { + t.active = false; + } +} + +function resetProjectsCollapse(sidePanel: SidePanelItem[]) { + if (!sidePanel[0].active) { + sidePanel[0].open = false; + } +} + +export default sidePanelReducer; diff --git a/src/store/store.ts b/src/store/store.ts index 499d89e8..49e0b5f7 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -6,24 +6,28 @@ import { createStore, applyMiddleware, compose, Middleware, combineReducers } fr import { routerMiddleware, routerReducer, RouterState } from "react-router-redux"; import thunkMiddleware from 'redux-thunk'; import { History } from "history"; + import projectsReducer, { ProjectState } from "./project/project-reducer"; +import sidePanelReducer, { SidePanelState } from './side-panel/side-panel-reducer'; import authReducer, { AuthState } from "./auth/auth-reducer"; const composeEnhancers = (process.env.NODE_ENV === 'development' && - window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || + window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose; export interface RootState { auth: AuthState; projects: ProjectState; router: RouterState; + sidePanel: SidePanelState; } const rootReducer = combineReducers({ auth: authReducer, projects: projectsReducer, - router: routerReducer + router: routerReducer, + sidePanel: sidePanelReducer }); diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 8884f3a8..dcc26ed9 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -3,33 +3,25 @@ // SPDX-License-Identifier: AGPL-3.0 import * as React from 'react'; - import { StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core/styles'; import Drawer from '@material-ui/core/Drawer'; -import AppBar from '@material-ui/core/AppBar'; -import Toolbar from '@material-ui/core/Toolbar'; -import Typography from '@material-ui/core/Typography'; import { connect, DispatchProp } from "react-redux"; + import ProjectList from "../../components/project-list/project-list"; import { Route, Switch } from "react-router"; -import { Link } from "react-router-dom"; -import Button from "@material-ui/core/Button/Button"; import authActions from "../../store/auth/auth-action"; -import IconButton from "@material-ui/core/IconButton/IconButton"; -import Menu from "@material-ui/core/Menu/Menu"; -import MenuItem from "@material-ui/core/MenuItem/MenuItem"; -import { AccountCircle } from "@material-ui/icons"; import { User } from "../../models/user"; -import Grid from "@material-ui/core/Grid/Grid"; import { RootState } from "../../store/store"; -import MainAppBar, { MainAppBarActionProps, MainAppBarMenuItems, MainAppBarMenuItem } from '../../components/main-app-bar/main-app-bar'; +import MainAppBar, { MainAppBarActionProps, MainAppBarMenuItem } from '../../components/main-app-bar/main-app-bar'; import { Breadcrumb } from '../../components/breadcrumbs/breadcrumbs'; import { push } from 'react-router-redux'; import projectActions from "../../store/project/project-action"; +import sidePanelActions from '../../store/side-panel/side-panel-action'; import ProjectTree from '../../components/project-tree/project-tree'; import { TreeItem, TreeItemStatus } from "../../components/tree/tree"; import { Project } from "../../models/project"; import { projectService } from '../../services/services'; +import SidePanel, {SidePanelItem} from '../../components/side-panel/side-panel'; const drawerWidth = 240; @@ -68,6 +60,7 @@ const styles: StyleRulesCallback = (theme: Theme) => ({ interface WorkbenchDataProps { projects: Array>; user?: User; + sidePanelItems: SidePanelItem[]; } interface WorkbenchActionProps { @@ -143,18 +136,32 @@ class Workbench extends React.Component { onMenuItemClick: (menuItem: NavMenuItem) => menuItem.action() }; - toggleProjectTreeItem = (itemId: string, status: TreeItemStatus) => { + toggleProjectTreeItemOpen = (itemId: string, status: TreeItemStatus) => { if (status === TreeItemStatus.Loaded) { - this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId)); + this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(itemId)); + this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId)); } else { this.props.dispatch(projectService.getProjectList(itemId)).then(() => { - this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId)); + this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(itemId)); + this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId)); }); } } + toggleProjectTreeItemActive = (itemId: string) => { + this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(itemId)); + } + + toggleSidePanelOpen = (itemId: string) => { + this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId)); + } + + toggleSidePanelActive = (itemId: string) => { + this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId)); + } + render() { - const { classes, user } = this.props; + const { classes, user, projects, sidePanelItems } = this.props; return (
@@ -173,9 +180,15 @@ class Workbench extends React.Component { paper: classes.drawerPaper, }}>
- + + + }
@@ -191,7 +204,8 @@ class Workbench extends React.Component { export default connect( (state: RootState) => ({ projects: state.projects, - user: state.auth.user + user: state.auth.user, + sidePanelItems: state.sidePanel, }) )( withStyles(styles)(Workbench) -- 2.30.2