Merge branch '13628-connect-breadcrumbs-with-project-tree'
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 18 Jun 2018 11:13:31 +0000 (13:13 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 18 Jun 2018 11:13:31 +0000 (13:13 +0200)
refs #13628

Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski@contractors.roche.com>

__mocks__/popper.js.js [new file with mode: 0644]
src/components/breadcrumbs/breadcrumbs.test.tsx
src/components/breadcrumbs/breadcrumbs.tsx
src/store/project/project-reducer.test.ts
src/store/project/project-reducer.ts
src/views/workbench/workbench.tsx

diff --git a/__mocks__/popper.js.js b/__mocks__/popper.js.js
new file mode 100644 (file)
index 0000000..07c7856
--- /dev/null
@@ -0,0 +1,30 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export default class Popper {
+    static placements = [
+        'auto',
+        'auto-end',
+        'auto-start',
+        'bottom',
+        'bottom-end',
+        'bottom-start',
+        'left',
+        'left-end',
+        'left-start',
+        'right',
+        'right-end',
+        'right-start',
+        'top',
+        'top-end',
+        'top-start'
+    ];
+
+    constructor() {
+        return {
+            destroy: jest.fn(),
+            scheduleUpdate: jest.fn()
+        };
+    }
+}
\ No newline at end of file
index 77beb49478783c6ea308d27072ad7b20da342684..b525554a2c8031d80464c5d2f070b55cfbf9b57c 100644 (file)
@@ -22,29 +22,29 @@ describe("<Breadcrumbs />", () => {
 
     it("renders one item", () => {
         const items = [
-            {label: 'breadcrumb 1'}
+            { label: 'breadcrumb 1' }
         ];
-        const breadcrumbs = mount(<Breadcrumbs items={items} onClick={onClick}  />);
+        const breadcrumbs = mount(<Breadcrumbs items={items} onClick={onClick} />);
         expect(breadcrumbs.find(Button)).toHaveLength(1);
         expect(breadcrumbs.find(ChevronRightIcon)).toHaveLength(0);
     });
-    
+
     it("renders multiple items", () => {
         const items = [
-            {label: 'breadcrumb 1'},
-            {label: 'breadcrumb 2'}
+            { label: 'breadcrumb 1' },
+            { label: 'breadcrumb 2' }
         ];
-        const breadcrumbs = mount(<Breadcrumbs items={items} onClick={onClick}  />);
+        const breadcrumbs = mount(<Breadcrumbs items={items} onClick={onClick} />);
         expect(breadcrumbs.find(Button)).toHaveLength(2);
         expect(breadcrumbs.find(ChevronRightIcon)).toHaveLength(1);
     });
-    
+
     it("calls onClick with clicked item", () => {
         const items = [
-            {label: 'breadcrumb 1'},
-            {label: 'breadcrumb 2'}
+            { label: 'breadcrumb 1' },
+            { label: 'breadcrumb 2' }
         ];
-        const breadcrumbs = mount(<Breadcrumbs items={items} onClick={onClick}  />);
+        const breadcrumbs = mount(<Breadcrumbs items={items} onClick={onClick} />);
         breadcrumbs.find(Button).at(1).simulate('click');
         expect(onClick).toBeCalledWith(items[1]);
     });
index 25f30a1bdae5da9abc02fb6a9f7bcb511ce7a942..41f71981e57eea29d54064f5888c78961f283f89 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { Button, Grid, StyleRulesCallback, WithStyles } from '@material-ui/core';
+import { Button, Grid, StyleRulesCallback, WithStyles, Typography, Tooltip } from '@material-ui/core';
 import ChevronRightIcon from '@material-ui/icons/ChevronRight';
 import { withStyles } from '@material-ui/core';
 
@@ -17,19 +17,27 @@ interface BreadcrumbsProps {
 }
 
 const Breadcrumbs: React.SFC<BreadcrumbsProps & WithStyles<CssRules>> = ({ classes, onClick, items }) => {
-    return <Grid container alignItems="center">
+    return <Grid container alignItems="center" wrap="nowrap">
         {
             items.map((item, index) => {
                 const isLastItem = index === items.length - 1;
                 return (
                     <React.Fragment key={index}>
-                        <Button
-                            color="inherit"
-                            className={isLastItem ? classes.currentItem : classes.item}
-                            onClick={() => onClick(item)}
-                        >
-                            {item.label}
-                        </Button>
+                        <Tooltip title={item.label}>
+                            <Button
+                                color="inherit"
+                                className={isLastItem ? classes.currentItem : classes.item}
+                                onClick={() => onClick(item)}
+                            >
+                                <Typography
+                                    noWrap
+                                    color="inherit"
+                                    className={classes.label}
+                                >
+                                    {item.label}
+                                </Typography>
+                            </Button>
+                        </Tooltip>
                         {
                             !isLastItem && <ChevronRightIcon color="inherit" className={classes.item} />
                         }
@@ -40,7 +48,7 @@ const Breadcrumbs: React.SFC<BreadcrumbsProps & WithStyles<CssRules>> = ({ class
     </Grid>;
 };
 
-type CssRules = "item" | "currentItem";
+type CssRules = "item" | "currentItem" | "label";
 
 const styles: StyleRulesCallback<CssRules> = theme => {
     const { unit } = theme.spacing;
@@ -50,6 +58,9 @@ const styles: StyleRulesCallback<CssRules> = theme => {
         },
         currentItem: {
             opacity: 1
+        },
+        label: {
+            textTransform: "none"
         }
     };
 };
index cfb73fa8b45daa4be4dbb1a0dfeee5790d5461f3..f964e0ea311f333af3486a69ed6188a1cddb9e0c 100644 (file)
@@ -2,8 +2,9 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import projectsReducer from "./project-reducer";
+import projectsReducer, { getTreePath } from "./project-reducer";
 import actions from "./project-action";
+import { TreeItem, TreeItemStatus } from "../../components/tree/tree";
 
 describe('project-reducer', () => {
     it('should add new project to the list', () => {
@@ -35,7 +36,7 @@ describe('project-reducer', () => {
         };
 
         const projects = [project, project];
-        const state = projectsReducer(initialState, actions.PROJECTS_SUCCESS({projects, parentItemId: undefined}));
+        const state = projectsReducer(initialState, actions.PROJECTS_SUCCESS({ projects, parentItemId: undefined }));
         expect(state).toEqual([{
                 active: false,
                 open: false,
@@ -54,3 +55,53 @@ describe('project-reducer', () => {
         ]);
     });
 });
+
+describe("findTreeBranch", () => {
+
+    const createTreeItem = (id: string, items?: Array<TreeItem<string>>): TreeItem<string> => ({
+        id,
+        items,
+        active: false,
+        data: "",
+        open: false,
+        status: TreeItemStatus.Initial
+    });
+
+    it("should return an array that matches path to the given item", () => {
+        const tree: Array<TreeItem<string>> = [
+            createTreeItem("1", [
+                createTreeItem("1.1", [
+                    createTreeItem("1.1.1"),
+                    createTreeItem("1.1.2")
+                ])
+            ]),
+            createTreeItem("2", [
+                createTreeItem("2.1", [
+                    createTreeItem("2.1.1"),
+                    createTreeItem("2.1.2")
+                ])
+            ])
+        ];
+        const branch = getTreePath(tree, "2.1.1");
+        expect(branch.map(item => item.id)).toEqual(["2", "2.1", "2.1.1"]);
+    });
+
+    it("should return empty array if item is not found", () => {
+        const tree: Array<TreeItem<string>> = [
+            createTreeItem("1", [
+                createTreeItem("1.1", [
+                    createTreeItem("1.1.1"),
+                    createTreeItem("1.1.2")
+                ])
+            ]),
+            createTreeItem("2", [
+                createTreeItem("2.1", [
+                    createTreeItem("2.1.1"),
+                    createTreeItem("2.1.2")
+                ])
+            ])
+        ];
+        expect(getTreePath(tree, "3")).toHaveLength(0);
+    });
+
+});
index 7563ea90c229febd989ae33af0602daea5c42401..4f7545fc979ea93b9fbe4fd6ee2f4e74559e6a87 100644 (file)
@@ -22,6 +22,20 @@ export function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeI
     return item;
 }
 
+export function getTreePath<T>(tree: Array<TreeItem<T>>, itemId: string): Array<TreeItem<T>> {
+    for(const item of tree){
+        if(item.id === itemId){
+            return [item];
+        } else {
+            const branch = getTreePath(item.items || [], itemId);
+            if(branch.length > 0){
+                return [item, ...branch];
+            }
+        }
+    }
+    return [];
+}
+
 function resetTreeActivity<T>(tree: Array<TreeItem<T>>) {
     for (const t of tree) {
         t.active = false;
index 90df260b974d983842f67aa2d5ab90a7b8fd63c8..ee4ac7f5398ac71e32756db016018c9a6a083465 100644 (file)
@@ -19,6 +19,7 @@ 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 { getTreePath } from '../../store/project/project-reducer';
 import DataExplorer from '../data-explorer/data-explorer';
 
 const drawerWidth = 240;
@@ -66,7 +67,8 @@ interface WorkbenchActionProps {
 type WorkbenchProps = WorkbenchDataProps & WorkbenchActionProps & DispatchProp & WithStyles<CssRules>;
 
 interface NavBreadcrumb extends Breadcrumb {
-    path: string;
+    itemId: string;
+    status: TreeItemStatus;
 }
 
 interface NavMenuItem extends MainAppBarMenuItem {
@@ -88,15 +90,7 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
     state = {
         anchorEl: null,
         searchText: "",
-        breadcrumbs: [
-            {
-                label: "Projects",
-                path: "/projects"
-            }, {
-                label: "Project 1",
-                path: "/projects/project-1"
-            }
-        ],
+        breadcrumbs: [],
         menuItems: {
             accountMenu: [
                 {
@@ -125,7 +119,9 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
 
 
     mainAppBarActions: MainAppBarActionProps = {
-        onBreadcrumbClick: (breadcrumb: NavBreadcrumb) => this.props.dispatch(push(breadcrumb.path)),
+        onBreadcrumbClick: ({ itemId, status }: NavBreadcrumb) => {
+            this.toggleProjectTreeItem(itemId, status);
+        },
         onSearch: searchText => {
             this.setState({ searchText });
             this.props.dispatch(push(`/search?q=${searchText}`));
@@ -142,6 +138,14 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
     }
 
     openProjectItem = (itemId: string) => {
+        const branch = getTreePath(this.props.projects, itemId);
+        this.setState({
+            breadcrumbs: branch.map(item => ({
+                label: item.data.name,
+                itemId: item.data.uuid,
+                status: item.status
+            }))
+        });
         this.props.dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM(itemId));
         this.props.dispatch(push(`/project/${itemId}`));
     }