Create breadcrumbs actions
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Sat, 25 Aug 2018 20:48:14 +0000 (22:48 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Sat, 25 Aug 2018 20:48:14 +0000 (22:48 +0200)
Feature #14102

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

src/store/breadcrumbs/breadcrumbs-actions.ts [new file with mode: 0644]
src/store/navigation/navigation-action.ts
src/store/side-panel-tree/side-panel-tree-actions.ts
src/store/side-panel/side-panel-action.ts
src/store/tree-picker/tree-picker.ts
src/views-components/breadcrumbs/breadcrumbs.ts
src/views-components/main-app-bar/main-app-bar.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/project-panel/project-panel.tsx
src/views/workbench/workbench.tsx

diff --git a/src/store/breadcrumbs/breadcrumbs-actions.ts b/src/store/breadcrumbs/breadcrumbs-actions.ts
new file mode 100644 (file)
index 0000000..254a8d3
--- /dev/null
@@ -0,0 +1,46 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { Breadcrumb } from '~/components/breadcrumbs/breadcrumbs';
+import { getResource } from '~/store/resources/resources';
+import { TreePicker } from '../tree-picker/tree-picker';
+import { getSidePanelTreeBranch } from '../side-panel-tree/side-panel-tree-actions';
+import { propertiesActions } from '../properties/properties-actions';
+
+export const BREADCRUMBS = 'breadcrumbs';
+
+export interface ResourceBreadcrumb extends Breadcrumb {
+    uuid: string;
+}
+
+export const setBreadcrumbs = (breadcrumbs: Breadcrumb[]) =>
+    propertiesActions.SET_PROPERTY({ key: BREADCRUMBS, value: breadcrumbs });
+
+const getSidePanelTreeBreadcrumbs = (uuid: string) => (treePicker: TreePicker): ResourceBreadcrumb[] => {
+    const nodes = getSidePanelTreeBranch(uuid)(treePicker);
+    return nodes.map(node =>
+        typeof node.value === 'string'
+            ? { label: node.value, uuid: node.nodeId }
+            : { label: node.value.name, uuid: node.value.uuid });
+};
+
+export const setSidePanelBreadcrumbs = (uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const { treePicker } = getState();
+        const breadcrumbs = getSidePanelTreeBreadcrumbs(uuid)(treePicker);
+        dispatch(setBreadcrumbs(breadcrumbs));
+    };
+
+export const setProjectBreadcrumbs = setSidePanelBreadcrumbs;
+
+export const setCollectionBreadcrumbs = (collectionUuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const { resources } = getState();
+        const collection = getResource(collectionUuid)(resources);
+        if (collection) {
+            dispatch<any>(setProjectBreadcrumbs(collection.ownerUuid));
+        }
+    };
index db8efbfd989291a8dcf66dd8a49f72a2a338c75c..b5dc5e9d1488fdada0d598ad97bb67684d83c0dc 100644 (file)
@@ -5,7 +5,7 @@
 import { Dispatch, compose } from 'redux';
 import { push } from "react-router-redux";
 import { RootState } from "../store";
-import { ResourceKind, Resource } from '~/models/resource';
+import { ResourceKind, Resource, extractUuidKind } from '~/models/resource';
 import { getCollectionUrl } from "~/models/collection";
 import { getProjectUrl } from "~/models/project";
 import { getResource } from '~/store/resources/resources';
@@ -23,12 +23,18 @@ import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-acti
 import { projectPanelColumns } from '~/views/project-panel/project-panel';
 import { favoritePanelColumns } from '~/views/favorite-panel/favorite-panel';
 import { matchRootRoute } from '~/routes/routes';
+import { setCollectionBreadcrumbs, setProjectBreadcrumbs, setSidePanelBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions';
 
-export const navigateToResource = (uuid: string) =>
+export const navigateTo = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState) => {
-        const resource = getResource(uuid)(getState().resources);
-        if (resource) {
-            dispatch<any>(getResourceNavigationAction(resource));
+        const kind = extractUuidKind(uuid);
+        if (kind === ResourceKind.PROJECT || kind === ResourceKind.USER) {
+            dispatch<any>(navigateToProject(uuid));
+        } else if (kind === ResourceKind.COLLECTION) {
+            dispatch<any>(navigateToCollection(uuid));
+        }
+        if (uuid === SidePanelTreeCategory.FAVORITES) {
+            dispatch<any>(navigateToFavorites);
         }
     };
 
@@ -74,14 +80,16 @@ export const loadFavorites = () =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.FAVORITES));
         dispatch<any>(loadFavoritePanel());
+        dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
     };
 
 
 export const navigateToProject = compose(push, getProjectUrl);
 
 export const loadProject = (uuid: string) =>
-    (dispatch: Dispatch) => {
-        dispatch<any>(activateSidePanelTreeItem(uuid));
+    async (dispatch: Dispatch) => {
+        await dispatch<any>(activateSidePanelTreeItem(uuid));
+        dispatch<any>(setProjectBreadcrumbs(uuid));
         dispatch<any>(openProjectPanel(uuid));
         dispatch(loadDetailsPanel(uuid));
     };
@@ -91,7 +99,8 @@ export const navigateToCollection = compose(push, getCollectionUrl);
 export const loadCollection = (uuid: string) =>
     async (dispatch: Dispatch) => {
         const collection = await dispatch<any>(loadCollectionPanel(uuid));
-        dispatch<any>(activateSidePanelTreeItem(collection.ownerUuid));
+        await dispatch<any>(activateSidePanelTreeItem(collection.ownerUuid));
+        dispatch<any>(setCollectionBreadcrumbs(collection.uuid));
         dispatch(loadDetailsPanel(uuid));
     };
 
index 268f1e6c406e1e64d4091fe88a2f2fb95a03b34a..2fe5aba8921050a7d6a35941bed8ef949b7e2e52 100644 (file)
@@ -4,14 +4,15 @@
 
 import { Dispatch } from 'redux';
 import { treePickerActions } from "~/store/tree-picker/tree-picker-actions";
-import { createTreePickerNode } from '~/store/tree-picker/tree-picker';
+import { createTreePickerNode, TreePickerNode } from '~/store/tree-picker/tree-picker';
 import { RootState } from '../store';
 import { ServiceRepository } from '~/services/services';
 import { FilterBuilder } from '~/common/api/filter-builder';
 import { resourcesActions } from '../resources/resources-actions';
-import { getNodeValue } from '../../models/tree';
-import { getTreePicker } from '../tree-picker/tree-picker';
+import { getTreePicker, TreePicker } from '../tree-picker/tree-picker';
 import { TreeItemStatus } from "~/components/tree/tree";
+import { getNodeAncestors, getNodeValue } from '~/models/tree';
+import { ProjectResource } from '~/models/project';
 
 export enum SidePanelTreeCategory {
     PROJECTS = 'Projects',
@@ -24,6 +25,21 @@ export enum SidePanelTreeCategory {
 
 export const SIDE_PANEL_TREE = 'sidePanelTree';
 
+export const getSidePanelTree = (treePicker: TreePicker) =>
+    getTreePicker<ProjectResource | string>(SIDE_PANEL_TREE)(treePicker);
+
+export const getSidePanelTreeBranch = (uuid: string) => (treePicker: TreePicker): Array<TreePickerNode<ProjectResource | string>> => {
+    const tree = getSidePanelTree(treePicker);
+    if (tree) {
+        const ancestors = getNodeAncestors(uuid)(tree).map(node => node.value);
+        const node = getNodeValue(uuid)(tree);
+        if (node) {
+            return [...ancestors, node];
+        }
+    }
+    return [];
+};
+
 const SIDE_PANEL_CATEGORIES = [
     SidePanelTreeCategory.SHARED_WITH_ME,
     SidePanelTreeCategory.WORKFLOWS,
@@ -77,7 +93,7 @@ export const activateSidePanelTreeItem = (nodeId: string) =>
             dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId: SIDE_PANEL_TREE }));
         }
         if (!isSidePanelTreeCategory(nodeId)) {
-            dispatch<any>(activateSidePanelTreeProject(nodeId));
+            await dispatch<any>(activateSidePanelTreeProject(nodeId));
         }
     };
 
@@ -90,7 +106,7 @@ export const activateSidePanelTreeProject = (nodeId: string) =>
                 dispatch<any>(toggleSidePanelTreeItemCollapse(nodeId));
             }
         } else if (node === undefined) {
-            dispatch<any>(activateSidePanelTreeBranch(nodeId));
+            await dispatch<any>(activateSidePanelTreeBranch(nodeId));
         }
     };
 
index 4fc745b136481947ae29cc48a5b73c0bd06525e9..8c7ef4a7a1ff7ff6d8d3f1023e8d7d93cefd6f08 100644 (file)
@@ -4,7 +4,7 @@
 
 import { Dispatch } from 'redux';
 import { isSidePanelTreeCategory, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
-import { navigateToFavorites, navigateToResource } from '../navigation/navigation-action';
+import { navigateToFavorites, navigateTo } from '../navigation/navigation-action';
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 
 export const navigateFromSidePanel = (id: string) =>
@@ -12,7 +12,7 @@ export const navigateFromSidePanel = (id: string) =>
         if (isSidePanelTreeCategory(id)) {
             dispatch<any>(getSidePanelTreeCategoryAction(id));
         } else {
-            dispatch<any>(navigateToResource(id));
+            dispatch<any>(navigateTo(id));
         }
     };
 
index fd104fe4b25695624c6ee771aa14f44a4921bb93..259a4b8d53de78e1b7d9992ae85b4d69d5fe40ca 100644 (file)
@@ -4,13 +4,12 @@
 
 import { Tree } from "~/models/tree";
 import { TreeItemStatus } from "~/components/tree/tree";
-import { RootState } from '~/store/store';
 
 export type TreePicker = { [key: string]: Tree<TreePickerNode> };
 
-export interface TreePickerNode {
+export interface TreePickerNode<Value = any> {
     nodeId: string;
-    value: any;
+    value: Value;
     selected: boolean;
     collapsed: boolean;
     status: TreeItemStatus;
@@ -23,4 +22,4 @@ export const createTreePickerNode = (data: { nodeId: string, value: any }) => ({
     status: TreeItemStatus.INITIAL
 });
 
-export const getTreePicker = (id: string) => (state: TreePicker): Tree<TreePickerNode> | undefined => state[id];
\ No newline at end of file
+export const getTreePicker = <Value = {}>(id: string) => (state: TreePicker): Tree<TreePickerNode<Value>> | undefined => state[id];
\ No newline at end of file
index 306b29e7dbadd018461f6a5d27a95e8530f69c04..69eb9e3dfcf1abaa2929b8e6fbf2c5e556a9f9f1 100644 (file)
@@ -5,50 +5,25 @@
 import { connect } from "react-redux";
 import { Breadcrumbs as BreadcrumbsComponent, BreadcrumbsProps } from '~/components/breadcrumbs/breadcrumbs';
 import { RootState } from '~/store/store';
-import { Breadcrumb } from '~/components/breadcrumbs/breadcrumbs';
-import { matchProjectRoute } from '~/routes/routes';
-import { getTreePicker } from '~/store/tree-picker/tree-picker';
-import { SIDE_PANEL_TREE } from '~/store/side-panel-tree/side-panel-tree-actions';
-import { getNodeAncestors, getNode } from '~/models/tree';
 import { Dispatch } from 'redux';
-import { navigateToResource } from '~/store/navigation/navigation-action';
+import { navigateTo } from '~/store/navigation/navigation-action';
+import { getProperty } from '../../store/properties/properties';
+import { ResourceBreadcrumb, BREADCRUMBS } from '../../store/breadcrumbs/breadcrumbs-actions';
+
 
-interface ResourceBreadcrumb extends Breadcrumb {
-    uuid: string;
-}
 
 type BreadcrumbsDataProps = Pick<BreadcrumbsProps, 'items'>;
 type BreadcrumbsActionProps = Pick<BreadcrumbsProps, 'onClick' | 'onContextMenu'>;
 
-const memoizedMapStateToProps = () => {
-    let items: ResourceBreadcrumb[] = [];
-    return ({ router, treePicker }: RootState): BreadcrumbsDataProps => {
-        if (router.location) {
-            const projectMatch = matchProjectRoute(location.pathname);
-            const collectionMatch = matchProjectRoute(location.pathname);
-            const uuid = projectMatch && projectMatch.params.id
-                || collectionMatch && collectionMatch.params.id
-                || '';
-            const tree = getTreePicker(SIDE_PANEL_TREE)(treePicker);
-            if (tree) {
-                const ancestors = getNodeAncestors(uuid)(tree);
-                const node = getNode(uuid)(tree);
-                const nodes = node ? [...ancestors, node] : ancestors;
-                items = nodes.map(({ value }) =>
-                    typeof value.value === 'string'
-                        ? { label: value.value, uuid: value.nodeId }
-                        : { label: value.value.name, uuid: value.value.uuid });
-            }
-        }
-        return { items };
-    };
-};
+const mapStateToProps = () => ({ properties }: RootState): BreadcrumbsDataProps => ({
+    items: getProperty<ResourceBreadcrumb[]>(BREADCRUMBS)(properties) || []
+});
 
 const mapDispatchToProps = (dispatch: Dispatch): BreadcrumbsActionProps => ({
     onClick: ({ uuid }: ResourceBreadcrumb) => {
-        dispatch<any>(navigateToResource(uuid));
+        dispatch<any>(navigateTo(uuid));
     },
     onContextMenu: () => { return; }
 });
 
-export const Breadcrumbs = connect(memoizedMapStateToProps(), mapDispatchToProps)(BreadcrumbsComponent);
\ No newline at end of file
+export const Breadcrumbs = connect(mapStateToProps(), mapDispatchToProps)(BreadcrumbsComponent);
\ No newline at end of file
index 54d6a5da0ec8c5306734ba27861a5288fd21fd89..4c16754c9aab733e6be5a05dd815664a7e15ed97 100644 (file)
@@ -23,7 +23,7 @@ export interface MainAppBarMenuItems {
 interface MainAppBarDataProps {
     searchText: string;
     searchDebounce?: number;
-    breadcrumbs: Breadcrumb[];
+    breadcrumbs: React.ComponentType<any>;
     user?: User;
     menuItems: MainAppBarMenuItems;
     buildInfo: string;
@@ -68,15 +68,10 @@ export const MainAppBar: React.SFC<MainAppBarProps> = (props) => {
             </Grid>
         </Toolbar>
         <Toolbar >
-            {
-                props.user && <Breadcrumbs
-                    items={props.breadcrumbs}
-                    onClick={props.onBreadcrumbClick}
-                    onContextMenu={props.onContextMenu} />
-            }
-            { props.user && <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
-                    <DetailsIcon />
-                </IconButton>
+            {props.user && <props.breadcrumbs />}
+            {props.user && <IconButton color="inherit" onClick={props.onDetailsPanelToggle}>
+                <DetailsIcon />
+            </IconButton>
             }
         </Toolbar>
     </AppBar>;
index cdfe97054cc21ea52ae76f35424b051b0159df31..3fd33aefcbf71918c731e3de7812a7dbeb61e26e 100644 (file)
@@ -22,7 +22,7 @@ import { Dispatch } from 'redux';
 import { contextMenuActions } from '~/store/context-menu/context-menu-actions';
 import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
 import { loadDetailsPanel } from '../../store/details-panel/details-panel-action';
-import { navigateToResource } from '~/store/navigation/navigation-action';
+import { navigateTo } from '~/store/navigation/navigation-action';
 
 type CssRules = "toolbar" | "button";
 
@@ -164,7 +164,7 @@ const mapDispatchToProps = (dispatch: Dispatch): FavoritePanelActionProps => ({
         dispatch<any>(loadDetailsPanel(resourceUuid));
     },
     onItemDoubleClick: uuid => {
-        dispatch<any>(navigateToResource(uuid));
+        dispatch<any>(navigateTo(uuid));
     }
 });
 
index e1c9d83f9bbb53352a6218531a1118879734134c..f9dcc396651caa9f93cea42c7f26f106c6f97c8b 100644 (file)
@@ -28,10 +28,9 @@ import { openProjectCreator } from '~/store/project/project-action';
 import { reset } from 'redux-form';
 import { COLLECTION_CREATE_DIALOG } from '~/views-components/dialog-create/dialog-collection-create';
 import { collectionCreateActions } from '~/store/collections/creator/collection-creator-action';
-import { navigateToResource } from '~/store/navigation/navigation-action';
+import { navigateTo } from '~/store/navigation/navigation-action';
 import { getProperty } from '~/store/properties/properties';
 import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
-import { Breadcrumbs } from '~/views-components/breadcrumbs/breadcrumbs';
 
 type CssRules = 'root' | "toolbar" | "button";
 
@@ -182,7 +181,6 @@ export const ProjectPanel = withStyles(styles)(
                             New project
                         </Button>
                     </div>
-                    <Breadcrumbs />
                     <DataExplorer
                         id={PROJECT_PANEL_ID}
                         onRowClick={this.handleRowClick}
@@ -232,7 +230,7 @@ export const ProjectPanel = withStyles(styles)(
             }
 
             handleRowDoubleClick = (uuid: string) => {
-                this.props.dispatch<any>(navigateToResource(uuid));
+                this.props.dispatch<any>(navigateTo(uuid));
             }
 
             handleRowClick = (uuid: string) => {
index 8d8d937f5c52b4c61205be873bfdf5cfa9f3fb47..a4defc14448ac544d39314bf5d78ed762cb46e5d 100644 (file)
@@ -5,7 +5,7 @@
 import * as React from 'react';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
 import { connect, DispatchProp } from "react-redux";
-import { Route, Switch, Redirect } from "react-router";
+import { Route, Switch } from "react-router";
 import { login, logout } from "~/store/auth/auth-action";
 import { User } from "~/models/user";
 import { RootState } from "~/store/store";
@@ -18,9 +18,9 @@ import { ProjectPanel } from "~/views/project-panel/project-panel";
 import { DetailsPanel } from '~/views-components/details-panel/details-panel';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { CreateProjectDialog } from "~/views-components/create-project-dialog/create-project-dialog";
-import { detailsPanelActions, loadDetailsPanel } from "~/store/details-panel/details-panel-action";
+import { detailsPanelActions } from "~/store/details-panel/details-panel-action";
 import { openContextMenu } from '~/store/context-menu/context-menu-actions';
-import { ProjectResource, getProjectUrl } from '~/models/project';
+import { ProjectResource } from '~/models/project';
 import { ContextMenu, ContextMenuKind } from "~/views-components/context-menu/context-menu";
 import { FavoritePanel } from "../favorite-panel/favorite-panel";
 import { CurrentTokenDialog } from '~/views-components/current-token-dialog/current-token-dialog';
@@ -41,7 +41,9 @@ import { MoveProjectDialog } from '~/views-components/move-project-dialog/move-p
 import { MoveCollectionDialog } from '~/views-components/move-collection-dialog/move-collection-dialog';
 import { SidePanel } from '~/views-components/side-panel/side-panel';
 import { Routes } from '~/routes/routes';
-import { navigateToResource } from '~/store/navigation/navigation-action';
+import { navigateTo } from '~/store/navigation/navigation-action';
+import { Breadcrumbs } from '~/views-components/breadcrumbs/breadcrumbs';
+
 
 const APP_BAR_HEIGHT = 100;
 
@@ -173,7 +175,7 @@ export const Workbench = withStyles(styles)(
                     <div className={classes.root}>
                         <div className={classes.appBar}>
                             <MainAppBar
-                                breadcrumbs={breadcrumbs}
+                                breadcrumbs={Breadcrumbs}
                                 searchText={this.state.searchText}
                                 user={this.props.user}
                                 menuItems={this.state.menuItems}
@@ -216,7 +218,7 @@ export const Workbench = withStyles(styles)(
 
             mainAppBarActions: MainAppBarActionProps = {
                 onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
-                    this.props.dispatch(navigateToResource(itemId));
+                    this.props.dispatch(navigateTo(itemId));
                 },
                 onSearch: searchText => {
                     this.setState({ searchText });