Refactor to apply global navigation actions
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Fri, 24 Aug 2018 14:03:03 +0000 (16:03 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Fri, 24 Aug 2018 14:03:03 +0000 (16:03 +0200)
Feature #14102

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

28 files changed:
src/common/api/common-resource-service.ts
src/index.tsx
src/models/resource.ts
src/routes/routes.ts [new file with mode: 0644]
src/store/auth/auth-action.ts
src/store/collection-panel/collection-panel-action.ts
src/store/data-explorer/data-explorer-middleware-service.ts
src/store/favorite-panel/favorite-panel-middleware-service.ts
src/store/favorites/favorites-actions.ts
src/store/navigation/navigation-action.ts
src/store/project-panel/project-panel-action.ts
src/store/project-panel/project-panel-middleware-service.ts
src/store/project/project-action.ts
src/store/resources/resources-actions.ts
src/store/side-panel-tree/side-panel-tree-actions.ts [new file with mode: 0644]
src/store/side-panel/side-panel-action.ts
src/store/side-panel/side-panel-reducer.test.ts [deleted file]
src/store/side-panel/side-panel-reducer.ts [deleted file]
src/store/tree-picker/tree-picker.ts
src/views-components/api-token/api-token.tsx
src/views-components/data-explorer/data-explorer.tsx
src/views-components/navigation-panel/navigation-panel.tsx [deleted file]
src/views-components/side-panel-tree/side-panel-tree.tsx [new file with mode: 0644]
src/views-components/side-panel/side-panel.tsx [new file with mode: 0644]
src/views/collection-panel/collection-panel.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/project-panel/project-panel.tsx
src/views/workbench/workbench.tsx

index 2c9bfb51679f16bef721b4499f0cf00181ddc3f4..fe7494b4460faf57ee2b5e47f2691c208961ce76 100644 (file)
@@ -111,6 +111,7 @@ export class CommonResourceService<T extends Resource> {
                 .put<T>(this.resourceType + uuid, data && CommonResourceService.mapKeys(_.snakeCase)(data)));
 
     }
+
 }
 
 export const getCommonResourceServiceError = (errorResponse: any) => {
index bee08c80a7fa598d7ffd42ad87abd14335f872ef..ecc616629c47616728e0e5a7f1e540c81544d622 100644 (file)
@@ -7,14 +7,14 @@ import * as ReactDOM from 'react-dom';
 import { Provider } from "react-redux";
 import { Workbench } from './views/workbench/workbench';
 import './index.css';
-import { Route } from "react-router";
+import { Route } from 'react-router';
 import createBrowserHistory from "history/createBrowserHistory";
-import { configureStore } from "./store/store";
+import { History } from "history";
+import { configureStore, RootStore } from './store/store';
 import { ConnectedRouter } from "react-router-redux";
 import { ApiToken } from "./views-components/api-token/api-token";
 import { initAuth } from "./store/auth/auth-action";
 import { createServices } from "./services/services";
-import { getProjectList } from "./store/project/project-action";
 import { MuiThemeProvider } from '@material-ui/core/styles';
 import { CustomTheme } from './common/custom-theme';
 import { fetchConfig } from './common/config';
@@ -27,6 +27,8 @@ import { collectionFilesActionSet } from './views-components/context-menu/action
 import { collectionFilesItemActionSet } from './views-components/context-menu/action-sets/collection-files-item-action-set';
 import { collectionActionSet } from './views-components/context-menu/action-sets/collection-action-set';
 import { collectionResourceActionSet } from './views-components/context-menu/action-sets/collection-resource-action-set';
+import { addRouteChangeHandlers } from './routes/routes';
+import { loadWorkbench } from './store/navigation/navigation-action';
 
 const getBuildNumber = () => "BN-" + (process.env.REACT_APP_BUILD_NUMBER || "dev");
 const getGitCommit = () => "GIT-" + (process.env.REACT_APP_GIT_COMMIT || "latest").substr(0, 7);
@@ -46,24 +48,25 @@ addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
 addMenuActionSet(ContextMenuKind.COLLECTION_RESOURCE, collectionResourceActionSet);
 
 fetchConfig()
-    .then(config => {
+    .then(async (config) => {
         const history = createBrowserHistory();
         const services = createServices(config);
         const store = configureStore(history, services);
 
+        store.subscribe(initListener(history, store));
+
         store.dispatch(initAuth());
-        store.dispatch(getProjectList(services.authService.getUuid()));  
 
-        const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props}/>;
-        const WorkbenchComponent = (props: any) => <Workbench authService={services.authService} buildInfo={buildInfo} {...props}/>;
+        const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props} />;
+        const WorkbenchComponent = (props: any) => <Workbench authService={services.authService} buildInfo={buildInfo} {...props} />;
 
         const App = () =>
             <MuiThemeProvider theme={CustomTheme}>
                 <Provider store={store}>
                     <ConnectedRouter history={history}>
                         <div>
-                            <Route path="/" component={WorkbenchComponent} />
                             <Route path="/token" component={TokenComponent} />
+                            <Route path="/" component={WorkbenchComponent} />
                         </div>
                     </ConnectedRouter>
                 </Provider>
@@ -73,6 +76,20 @@ fetchConfig()
             <App />,
             document.getElementById('root') as HTMLElement
         );
+
+
     });
 
+const initListener = (history: History, store: RootStore) => {
+    let initialized = false;
+    return async () => {
+        const { router, auth } = store.getState();
+        if (router.location && auth.user && !initialized) {
+            initialized = true;
+            await store.dispatch(loadWorkbench());
+            addRouteChangeHandlers(history, store);
+        }
+    };
+};
+
 
index 3b30b08898701b6f0b4107ac5ce481995d180078..ff95c1a9b8df59872d6819dccdbcfb2b8ee92441 100644 (file)
@@ -30,16 +30,22 @@ export enum ResourceObjectType {
     COLLECTION = '4zz18'
 }
 
+export const RESOURCE_UUID_PATTERN = '.{5}-.{5}-.{15}';
+export const RESOURCE_UUID_REGEX = new RegExp(RESOURCE_UUID_PATTERN);
+
+export const isResourceUuid = (uuid: string) =>
+    RESOURCE_UUID_REGEX.test(uuid);
+
 export const extractUuidObjectType = (uuid: string) => {
-    const match = /(.{5})-(.{5})-(.{15})/.exec(uuid);
+    const match = RESOURCE_UUID_REGEX.exec(uuid);
     return match
-        ? match[2]
+        ? match[0].split('-')[1]
         : undefined;
 };
 
 export const extractUuidKind = (uuid: string = '') => {
     const objectType = extractUuidObjectType(uuid);
-    switch(objectType){
+    switch (objectType) {
         case ResourceObjectType.USER:
             return ResourceKind.USER;
         case ResourceObjectType.GROUP:
diff --git a/src/routes/routes.ts b/src/routes/routes.ts
new file mode 100644 (file)
index 0000000..2bdd17a
--- /dev/null
@@ -0,0 +1,70 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { History, Location } from 'history';
+import { RootStore } from '../store/store';
+import { matchPath } from 'react-router';
+import { ResourceKind, RESOURCE_UUID_PATTERN, extractUuidKind } from '~/models/resource';
+import { getProjectUrl } from '../models/project';
+import { getCollectionUrl } from '~/models/collection';
+import { loadProject, loadFavorites, loadCollection } from '../store/navigation/navigation-action';
+
+export const Routes = {
+    ROOT: '/',
+    PROJECTS: `/projects/:id(${RESOURCE_UUID_PATTERN})`,
+    COLLECTIONS: `/collections/:id(${RESOURCE_UUID_PATTERN})`,
+    FAVORITES: '/favorites',
+};
+
+export const getResourceUrl = (uuid: string) => {
+    const kind = extractUuidKind(uuid);
+    switch (kind) {
+        case ResourceKind.PROJECT:
+            return getProjectUrl(uuid);
+        case ResourceKind.COLLECTION:
+            return getCollectionUrl(uuid);
+        default:
+            return undefined;
+    }
+};
+
+export const addRouteChangeHandlers = (history: History, store: RootStore) => {
+    const handler = handleLocationChange(store);
+    handler(history.location);
+    history.listen(handler);
+};
+
+export const matchRootRoute = (route: string) =>
+    matchPath(route, { path: Routes.ROOT, exact: true });
+
+export const matchFavoritesRoute = (route: string) =>
+    matchPath(route, { path: Routes.FAVORITES });
+
+export interface ProjectRouteParams {
+    id: string;
+}
+
+export const matchProjectRoute = (route: string) =>
+    matchPath<ProjectRouteParams>(route, { path: Routes.PROJECTS });
+
+export interface CollectionRouteParams {
+    id: string;
+}
+
+export const matchCollectionRoute = (route: string) =>
+    matchPath<ProjectRouteParams>(route, { path: Routes.COLLECTIONS });
+
+
+const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
+    const projectMatch = matchProjectRoute(pathname);
+    const collectionMatch = matchCollectionRoute(pathname);
+    const favoriteMatch = matchFavoritesRoute(pathname);
+    if (projectMatch) {
+        store.dispatch(loadProject(projectMatch.params.id));
+    } else if (collectionMatch) {
+        store.dispatch(loadCollection(collectionMatch.params.id));
+    } else if (favoriteMatch) {
+        store.dispatch(loadFavorites());
+    }
+};
index 00af5ce5b0bb7614f4fbc97316a61dd712759ba3..72e2d3453299322775da2d421a5417e34d0eb5e0 100644 (file)
@@ -8,6 +8,8 @@ import { User } from "~/models/user";
 import { RootState } from "../store";
 import { ServiceRepository } from "~/services/services";
 import { AxiosInstance } from "axios";
+import { initSidePanelTree } from '../side-panel-tree/side-panel-tree-actions';
+import { updateResources } from '../resources/resources-actions';
 
 export const authActions = unionize({
     SAVE_API_TOKEN: ofType<string>(),
@@ -17,9 +19,9 @@ export const authActions = unionize({
     USER_DETAILS_REQUEST: {},
     USER_DETAILS_SUCCESS: ofType<User>()
 }, {
-    tag: 'type',
-    value: 'payload'
-});
+        tag: 'type',
+        value: 'payload'
+    });
 
 function setAuthorizationHeader(services: ServiceRepository, token: string) {
     services.apiClient.defaults.headers.common = {
@@ -34,16 +36,17 @@ function removeAuthorizationHeader(client: AxiosInstance) {
     delete client.defaults.headers.common.Authorization;
 }
 
-export const initAuth = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-    const user = services.authService.getUser();
-    const token = services.authService.getApiToken();
-    if (token) {
-        setAuthorizationHeader(services, token);
-    }
-    if (token && user) {
-        dispatch(authActions.INIT({ user, token }));
-    }
-};
+export const initAuth = () =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const user = services.authService.getUser();
+        const token = services.authService.getApiToken();
+        if (token) {
+            setAuthorizationHeader(services, token);
+        }
+        if (token && user) {
+            dispatch(authActions.INIT({ user, token }));
+        }
+    };
 
 export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
     services.authService.saveApiToken(token);
index d8ad6d0a949575bbcf53392097ace6ea1a983dcf..5b2690bfaf7a763f8e56620a25eee66f598847d5 100644 (file)
@@ -29,7 +29,7 @@ export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
 
 export const COLLECTION_TAG_FORM_NAME = 'collectionTagForm';
 
-export const loadCollection = (uuid: string) =>
+export const loadCollectionPanel = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid }));
         dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
@@ -37,6 +37,7 @@ export const loadCollection = (uuid: string) =>
         dispatch(resourcesActions.SET_RESOURCES([collection]));
         dispatch<any>(loadCollectionFiles(collection.uuid));
         dispatch<any>(loadCollectionTags(collection.uuid));
+        return collection;
     };
 
 export const loadCollectionTags = (uuid: string) =>
index 7c64020ef3b46b7e9f2ff85b4e078741a352bab4..059c078429833487c553aeb744eaf75ab8a771c7 100644 (file)
@@ -6,6 +6,8 @@ import { Dispatch, MiddlewareAPI } from "redux";
 import { RootState } from "../store";
 import { DataColumns } from "~/components/data-table/data-table";
 import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
+import { DataExplorer } from './data-explorer-reducer';
+import { ListArguments, ListResults } from '~/common/api/common-resource-service';
 
 export abstract class DataExplorerMiddlewareService {
     protected readonly id: string;
@@ -25,3 +27,19 @@ export abstract class DataExplorerMiddlewareService {
 
     abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>): void;
 }
+
+export const getDataExplorerColumnFilters = <T, F extends DataTableFilterItem>(columns: DataColumns<T, F>, columnName: string): F[] => {
+    const column = columns.find(c => c.name === columnName);
+    return column ? column.filters.filter(f => f.selected) : [];
+};
+
+export const dataExplorerToListParams = <R>(dataExplorer: DataExplorer) => ({
+    limit: dataExplorer.rowsPerPage,
+    offset: dataExplorer.page * dataExplorer.rowsPerPage,
+});
+
+export const listResultsToDataExplorerItemsMeta = <R>({ itemsAvailable, offset, limit }: ListResults<R>) => ({
+    itemsAvailable,
+    page: Math.floor(offset / limit),
+    rowsPerPage: limit
+});
\ No newline at end of file
index e4be32d8cdc8bc7513af377679264347bbde7ad2..e5857dd363e75433fb9bdb3822e738c39ea7fa48 100644 (file)
@@ -9,13 +9,15 @@ import { DataColumns } from "~/components/data-table/data-table";
 import { ServiceRepository } from "~/services/services";
 import { SortDirection } from "~/components/data-table/data-column";
 import { FilterBuilder } from "~/common/api/filter-builder";
-import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { updateFavorites } from "../favorites/favorites-actions";
 import { favoritePanelActions } from "./favorite-panel-action";
 import { Dispatch, MiddlewareAPI } from "redux";
 import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
 import { LinkResource } from "~/models/link";
 import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
 import { resourcesActions } from "~/store/resources/resources-actions";
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { getDataExplorer } from "~/store/data-explorer/data-explorer-reducer";
 
 export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -23,54 +25,64 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
     }
 
     requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
-        const dataExplorer = api.getState().dataExplorer[this.getId()];
-        const columns = dataExplorer.columns as DataColumns<string, FavoritePanelFilter>;
-        const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
-        const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
+        const dataExplorer = getDataExplorer(api.getState().dataExplorer, this.getId());
+        if (!dataExplorer) {
+            api.dispatch(favoritesPanelDataExplorerIsNotSet());
+         } else {
 
-        const linkOrder = new OrderBuilder<LinkResource>();
-        const contentOrder = new OrderBuilder<GroupContentsResource>();
+            const columns = dataExplorer.columns as DataColumns<string, FavoritePanelFilter>;
+            const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+            const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
 
-        if (sortColumn && sortColumn.name === FavoritePanelColumnNames.NAME) {
-            const direction = sortColumn.sortDirection === SortDirection.ASC
-                ? OrderDirection.ASC
-                : OrderDirection.DESC;
+            const linkOrder = new OrderBuilder<LinkResource>();
+            const contentOrder = new OrderBuilder<GroupContentsResource>();
 
-            linkOrder.addOrder(direction, "name");
-            contentOrder
-                .addOrder(direction, "name", GroupContentsResourcePrefix.COLLECTION)
-                .addOrder(direction, "name", GroupContentsResourcePrefix.PROCESS)
-                .addOrder(direction, "name", GroupContentsResourcePrefix.PROJECT);
-        }
+            if (sortColumn && sortColumn.name === FavoritePanelColumnNames.NAME) {
+                const direction = sortColumn.sortDirection === SortDirection.ASC
+                    ? OrderDirection.ASC
+                    : OrderDirection.DESC;
+
+                linkOrder.addOrder(direction, "name");
+                contentOrder
+                    .addOrder(direction, "name", GroupContentsResourcePrefix.COLLECTION)
+                    .addOrder(direction, "name", GroupContentsResourcePrefix.PROCESS)
+                    .addOrder(direction, "name", GroupContentsResourcePrefix.PROJECT);
+            }
 
-        this.services.favoriteService
-            .list(this.services.authService.getUuid()!, {
-                limit: dataExplorer.rowsPerPage,
-                offset: dataExplorer.page * dataExplorer.rowsPerPage,
-                linkOrder: linkOrder.getOrder(),
-                contentOrder: contentOrder.getOrder(),
-                filters: new FilterBuilder()
-                    .addIsA("headUuid", typeFilters.map(filter => filter.type))
-                    .addILike("name", dataExplorer.searchValue)
-                    .getFilters()
-            })
-            .then(response => {
-                api.dispatch(resourcesActions.SET_RESOURCES(response.items));
-                api.dispatch(favoritePanelActions.SET_ITEMS({
-                    items: response.items.map(resource => resource.uuid),
-                    itemsAvailable: response.itemsAvailable,
-                    page: Math.floor(response.offset / response.limit),
-                    rowsPerPage: response.limit
-                }));
-                api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
-            })
-            .catch(() => {
-                api.dispatch(favoritePanelActions.SET_ITEMS({
-                    items: [],
-                    itemsAvailable: 0,
-                    page: 0,
-                    rowsPerPage: dataExplorer.rowsPerPage
-                }));
-            });
+            this.services.favoriteService
+                .list(this.services.authService.getUuid()!, {
+                    limit: dataExplorer.rowsPerPage,
+                    offset: dataExplorer.page * dataExplorer.rowsPerPage,
+                    linkOrder: linkOrder.getOrder(),
+                    contentOrder: contentOrder.getOrder(),
+                    filters: new FilterBuilder()
+                        .addIsA("headUuid", typeFilters.map(filter => filter.type))
+                        .addILike("name", dataExplorer.searchValue)
+                        .getFilters()
+                })
+                .then(response => {
+                    api.dispatch(resourcesActions.SET_RESOURCES(response.items));
+                    api.dispatch(favoritePanelActions.SET_ITEMS({
+                        items: response.items.map(resource => resource.uuid),
+                        itemsAvailable: response.itemsAvailable,
+                        page: Math.floor(response.offset / response.limit),
+                        rowsPerPage: response.limit
+                    }));
+                    api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
+                })
+                .catch(() => {
+                    api.dispatch(favoritePanelActions.SET_ITEMS({
+                        items: [],
+                        itemsAvailable: 0,
+                        page: 0,
+                        rowsPerPage: dataExplorer.rowsPerPage
+                    }));
+                });
+        }
     }
 }
+
+const favoritesPanelDataExplorerIsNotSet = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Favorites panel is not ready.'
+    });
index 9e1b3ef1c20a3d27fdd1f46be629cfed77f85f37..57eecf8f54d1ec51f24714d4e4762b88d014a4f6 100644 (file)
@@ -40,7 +40,7 @@ export const toggleFavorite = (resource: { uuid: string; name: string }) =>
             });
     };
 
-export const checkPresenceInFavorites = (resourceUuids: string[]) =>
+export const updateFavorites = (resourceUuids: string[]) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const userUuid = getState().auth.user!.uuid;
         dispatch(favoritesActions.CHECK_PRESENCE_IN_FAVORITES(resourceUuids));
@@ -50,4 +50,3 @@ export const checkPresenceInFavorites = (resourceUuids: string[]) =>
                 dispatch(favoritesActions.UPDATE_FAVORITES(results));
             });
     };
-
index d440f19009982b7daee59692d844ca65d6900c8a..db8efbfd989291a8dcf66dd8a49f72a2a338c75c 100644 (file)
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { Dispatch } from "redux";
-import { getProjectList, projectActions } from "../project/project-action";
+import { Dispatch, compose } from 'redux';
 import { push } from "react-router-redux";
-import { TreeItemStatus } from "~/components/tree/tree";
-import { findTreeItem } from "../project/project-reducer";
 import { RootState } from "../store";
 import { ResourceKind, Resource } from '~/models/resource';
-import { projectPanelActions } from "../project-panel/project-panel-action";
 import { getCollectionUrl } from "~/models/collection";
-import { getProjectUrl, ProjectResource } from "~/models/project";
-import { ProjectService } from "~/services/project-service/project-service";
-import { ServiceRepository } from "~/services/services";
-import { sidePanelActions } from "../side-panel/side-panel-action";
-import { SidePanelId } from "../side-panel/side-panel-reducer";
-import { getUuidObjectType, ObjectTypes } from "~/models/object-types";
+import { getProjectUrl } from "~/models/project";
 import { getResource } from '~/store/resources/resources';
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
-import { loadCollection } from '~/store/collection-panel/collection-panel-action';
-import { GroupContentsResource } from "~/services/groups-service/groups-service";
+import { loadCollectionPanel } from '~/store/collection-panel/collection-panel-action';
 import { snackbarActions } from '../snackbar/snackbar-actions';
 import { resourceLabel } from "~/common/labels";
+import { loadFavoritePanel } from '../favorite-panel/favorite-panel-action';
+import { openProjectPanel, projectPanelActions } from '~/store/project-panel/project-panel-action';
+import { activateSidePanelTreeItem, initSidePanelTree, SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
+import { Routes } from '~/routes/routes';
+import { loadResource } from '../resources/resources-actions';
+import { ServiceRepository } from '~/services/services';
+import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action';
+import { projectPanelColumns } from '~/views/project-panel/project-panel';
+import { favoritePanelColumns } from '~/views/favorite-panel/favorite-panel';
+import { matchRootRoute } from '~/routes/routes';
 
-export const getResourceUrl = (resourceKind: ResourceKind, resourceUuid: string): string => {
-    switch (resourceKind) {
-        case ResourceKind.PROJECT: return getProjectUrl(resourceUuid);
-        case ResourceKind.COLLECTION: return getCollectionUrl(resourceUuid);
+export const navigateToResource = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState) => {
+        const resource = getResource(uuid)(getState().resources);
+        if (resource) {
+            dispatch<any>(getResourceNavigationAction(resource));
+        }
+    };
+
+const getResourceNavigationAction = (resource: Resource) => {
+    switch (resource.kind) {
+        case ResourceKind.COLLECTION:
+            return navigateToCollection(resource.uuid);
+        case ResourceKind.PROJECT:
+        case ResourceKind.USER:
+            return navigateToProject(resource.uuid);
         default:
-            return '';
+            return cannotNavigateToResource(resource);
     }
 };
 
-export enum ItemMode {
-    BOTH,
-    OPEN,
-    ACTIVE
-}
-
-export const setProjectItem = (itemId: string, itemMode: ItemMode) =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const { projects, router } = getState();
-        const treeItem = findTreeItem(projects.items, itemId);
-
-        if (treeItem) {
-            const resourceUrl = getResourceUrl(treeItem.data.kind, treeItem.data.uuid);
-
-            if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
-                if (router.location && !router.location.pathname.includes(resourceUrl)) {
-                    dispatch(push(resourceUrl));
+export const loadWorkbench = () =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { auth, router } = getState();
+        const { user } = auth;
+        if (user) {
+            const userResource = await dispatch<any>(loadResource(user.uuid));
+            if (userResource) {
+                dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
+                dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
+                dispatch<any>(initSidePanelTree());
+                if (router.location) {
+                    const match = matchRootRoute(router.location.pathname);
+                    if (match) {
+                        dispatch(navigateToProject(userResource.uuid));
+                    }
                 }
-                dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(treeItem.data.uuid));
+            } else {
+                dispatch(userIsNotAuthenticated);
             }
-
-            const promise = treeItem.status === TreeItemStatus.LOADED
-                ? Promise.resolve()
-                : dispatch<any>(getProjectList(itemId));
-
-            promise
-                .then(() => dispatch<any>(() => {
-                    if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) {
-                        dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(treeItem.data.uuid));
-                    }
-                    dispatch(projectPanelActions.RESET_PAGINATION());
-                    dispatch(projectPanelActions.REQUEST_ITEMS());
-                }));
         } else {
-            const uuid = services.authService.getUuid();
-            if (itemId === uuid) {
-                dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(uuid));
-                dispatch(projectPanelActions.RESET_PAGINATION());
-                dispatch(projectPanelActions.REQUEST_ITEMS());
-            }
+            dispatch(userIsNotAuthenticated);
         }
     };
 
-export const restoreBranch = (itemId: string) =>
+export const navigateToFavorites = push(Routes.FAVORITES);
+
+export const loadFavorites = () =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const ancestors = await loadProjectAncestors(itemId, services.projectService);
-        const uuids = ancestors.map(ancestor => ancestor.uuid);
-        await loadBranch(uuids, dispatch);
-        dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelId.PROJECTS));
-        uuids.forEach(uuid => {
-            dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(uuid));
-        });
+        dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.FAVORITES));
+        dispatch<any>(loadFavoritePanel());
     };
 
-export const loadProjectAncestors = async (uuid: string, projectService: ProjectService): Promise<Array<ProjectResource>> => {
-    if (getUuidObjectType(uuid) === ObjectTypes.USER) {
-        return [];
-    } else {
-        const currentProject = await projectService.get(uuid);
-        const ancestors = await loadProjectAncestors(currentProject.ownerUuid, projectService);
-        return [...ancestors, currentProject];
-    }
-};
 
-const loadBranch = async (uuids: string[], dispatch: Dispatch): Promise<any> => {
-    const [uuid, ...rest] = uuids;
-    if (uuid) {
-        await dispatch<any>(getProjectList(uuid));
-        return loadBranch(rest, dispatch);
-    }
-};
-
-export const navigateToResource = (uuid: string) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
-        const resource = getResource(uuid)(getState().resources);
-        resource
-            ? dispatch<any>(getResourceNavigationAction(resource))
-            : dispatch<any>(resourceIsNotLoaded(uuid));
-    };
+export const navigateToProject = compose(push, getProjectUrl);
 
-const getResourceNavigationAction = (resource: Resource) => {
-    switch (resource.kind) {
-        case ResourceKind.COLLECTION:
-            return navigateToCollection(resource);
-        case ResourceKind.PROJECT:
-            return navigateToProject(resource);
-        default:
-            return cannotNavigateToResource(resource);
-    }
-};
-
-export const navigateToProject = ({ uuid }: Resource) =>
+export const loadProject = (uuid: string) =>
     (dispatch: Dispatch) => {
-        dispatch<any>(setProjectItem(uuid, ItemMode.BOTH));
+        dispatch<any>(activateSidePanelTreeItem(uuid));
+        dispatch<any>(openProjectPanel(uuid));
         dispatch(loadDetailsPanel(uuid));
     };
 
-export const navigateToCollection = ({ uuid }: Resource) =>
-    (dispatch: Dispatch) => {
-        dispatch<any>(loadCollection(uuid));
-        dispatch(push(getCollectionUrl(uuid)));
+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));
+        dispatch(loadDetailsPanel(uuid));
     };
 
 export const cannotNavigateToResource = ({ kind, uuid }: Resource) =>
     snackbarActions.OPEN_SNACKBAR({
-        message: `${resourceLabel(kind)} identified by ${uuid} cannot be opened.`,
-        hideDuration: 3000
+        message: `${resourceLabel(kind)} identified by ${uuid} cannot be opened.`
     });
 
-
 export const resourceIsNotLoaded = (uuid: string) =>
     snackbarActions.OPEN_SNACKBAR({
-        message: `Resource identified by ${uuid} is not loaded.`,
-        hideDuration: 3000
+        message: `Resource identified by ${uuid} is not loaded.`
     });
+
+export const userIsNotAuthenticated = snackbarActions.OPEN_SNACKBAR({
+    message: 'User is not authenticated'
+});
+
+export const couldNotLoadUser = snackbarActions.OPEN_SNACKBAR({
+    message: 'Could not load user'
+});
\ No newline at end of file
index 33cedd711734d5d6b290d872a286b07a53b844bf..49041032c273000d2009eae452290fba987b83a0 100644 (file)
@@ -3,6 +3,20 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
-
+import { propertiesActions } from "~/store/properties/properties-actions";
+import { Dispatch } from 'redux';
+import { ServiceRepository } from "~/services/services";
+import { RootState } from '~/store/store';
+import { getProperty } from "~/store/properties/properties";
 export const PROJECT_PANEL_ID = "projectPanel";
+export const PROJECT_PANEL_CURRENT_UUID = "projectPanelCurrentUuid";
 export const projectPanelActions = bindDataExplorerActions(PROJECT_PANEL_ID);
+
+export const openProjectPanel = (projectUuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(propertiesActions.SET_PROPERTY({ key: PROJECT_PANEL_CURRENT_UUID, value: projectUuid }));
+        dispatch(projectPanelActions.REQUEST_ITEMS());
+    };
+
+export const getProjectPanelCurrentUuid = (state: RootState) => getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties);
+
index 0196ed425c34e9890ce3c12fe2c6a438ab184caf..da7f5b33e0d96e928f534f9491e501004fabd3f7 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-middleware-service";
+import { DataExplorerMiddlewareService, getDataExplorerColumnFilters, dataExplorerToListParams, listResultsToDataExplorerItemsMeta } from '../data-explorer/data-explorer-middleware-service';
 import { ProjectPanelColumnNames, ProjectPanelFilter } from "~/views/project-panel/project-panel";
 import { RootState } from "../store";
 import { DataColumns } from "~/components/data-table/data-table";
@@ -10,70 +10,98 @@ import { ServiceRepository } from "~/services/services";
 import { SortDirection } from "~/components/data-table/data-column";
 import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
 import { FilterBuilder } from "~/common/api/filter-builder";
-import { GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
-import { checkPresenceInFavorites } from "../favorites/favorites-actions";
-import { projectPanelActions } from "./project-panel-action";
+import { GroupContentsResourcePrefix, GroupContentsResource } from "~/services/groups-service/groups-service";
+import { updateFavorites } from "../favorites/favorites-actions";
+import { projectPanelActions, PROJECT_PANEL_CURRENT_UUID } from './project-panel-action';
 import { Dispatch, MiddlewareAPI } from "redux";
 import { ProjectResource } from "~/models/project";
-import { resourcesActions } from "~/store/resources/resources-actions";
+import { updateResources } from "~/store/resources/resources-actions";
+import { getProperty } from "~/store/properties/properties";
+import { snackbarActions } from '../snackbar/snackbar-actions';
+import { DataExplorer, getDataExplorer } from '../data-explorer/data-explorer-reducer';
+import { ListResults } from '~/common/api/common-resource-service';
 
 export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
         super(id);
     }
 
-    requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+    async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
         const state = api.getState();
-        const dataExplorer = state.dataExplorer[this.getId()];
-        const columns = dataExplorer.columns as DataColumns<string, ProjectPanelFilter>;
-        const typeFilters = this.getColumnFilters(columns, ProjectPanelColumnNames.TYPE);
-        const statusFilters = this.getColumnFilters(columns, ProjectPanelColumnNames.STATUS);
-        const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+        const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+        const projectUuid = getProperty<string>(PROJECT_PANEL_CURRENT_UUID)(state.properties);
+        if (!projectUuid) {
+            api.dispatch(projectPanelCurrentUuidIsNotSet());
+        } else if (!dataExplorer) {
+            api.dispatch(projectPanelDataExplorerIsNotSet());
+        } else {
+            try {
+                const response = await this.services.groupsService.contents(projectUuid, getParams(dataExplorer));
+                api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
+                api.dispatch(updateResources(response.items));
+                api.dispatch(setItems(response));
+            } catch (e) {
+                api.dispatch(couldNotFetchProjectContents());
+            }
+        }
+    }
+}
 
-        const order = new OrderBuilder<ProjectResource>();
+const setItems = (listResults: ListResults<GroupContentsResource>) =>
+    projectPanelActions.SET_ITEMS({
+        ...listResultsToDataExplorerItemsMeta(listResults),
+        items: listResults.items.map(resource => resource.uuid),
+    });
 
-        if (sortColumn) {
-            const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
-                ? OrderDirection.ASC
-                : OrderDirection.DESC;
+const getParams = (dataExplorer: DataExplorer) => ({
+    ...dataExplorerToListParams(dataExplorer),
+    order: getOrder(dataExplorer),
+    filters: getFilters(dataExplorer),
+});
 
-            const columnName = sortColumn && sortColumn.name === ProjectPanelColumnNames.NAME ? "name" : "createdAt";
-            order
-                .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.COLLECTION)
-                .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROCESS)
-                .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT);
-        }
+const getFilters = (dataExplorer: DataExplorer) => {
+    const columns = dataExplorer.columns as DataColumns<string, ProjectPanelFilter>;
+    const typeFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.TYPE);
+    const statusFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.STATUS);
+    return new FilterBuilder()
+        .addIsA("uuid", typeFilters.map(f => f.type))
+        .addIn("state", statusFilters.map(f => f.type), GroupContentsResourcePrefix.PROCESS)
+        .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
+        .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
+        .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
+        .getFilters();
+};
+
+const getOrder = (dataExplorer: DataExplorer) => {
+    const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+    const order = new OrderBuilder<ProjectResource>();
+    if (sortColumn) {
+        const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+            ? OrderDirection.ASC
+            : OrderDirection.DESC;
 
-        this.services.groupsService
-            .contents(state.projects.currentItemId, {
-                limit: dataExplorer.rowsPerPage,
-                offset: dataExplorer.page * dataExplorer.rowsPerPage,
-                order: order.getOrder(),
-                filters: new FilterBuilder()
-                    .addIsA("uuid", typeFilters.map(f => f.type))
-                    .addIn("state", statusFilters.map(f => f.type), GroupContentsResourcePrefix.PROCESS)
-                    .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
-                    .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
-                    .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
-                    .getFilters()
-            })
-            .then(response => {
-                api.dispatch(resourcesActions.SET_RESOURCES(response.items));
-                api.dispatch(projectPanelActions.SET_ITEMS({
-                    items: response.items.map(resource => resource.uuid),
-                    itemsAvailable: response.itemsAvailable,
-                    page: Math.floor(response.offset / response.limit),
-                    rowsPerPage: response.limit
-                }));
-                api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
-            })
-            .catch(() => {
-                api.dispatch(projectPanelActions.SET_ITEMS({
-                    items: [],
-                    itemsAvailable: 0,
-                    page: 0,
-                    rowsPerPage: dataExplorer.rowsPerPage
-                }));
-            });
+        const columnName = sortColumn && sortColumn.name === ProjectPanelColumnNames.NAME ? "name" : "createdAt";
+        return order
+            .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.COLLECTION)
+            .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROCESS)
+            .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT)
+            .getOrder();
+    } else {
+        return order.getOrder();
     }
-}
+};
+
+const projectPanelCurrentUuidIsNotSet = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Project panel is not opened.'
+    });
+
+const couldNotFetchProjectContents = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Could not fetch project contents.'
+    });
+
+const projectPanelDataExplorerIsNotSet = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Project panel is not ready.'
+    });
index da58ed28859a7bd4d94816b0fb7ed0f94d6ec79a..53e09cc64eb9bb14744f344a47ae4e36a5d9cdb5 100644 (file)
@@ -7,7 +7,7 @@ import { ProjectResource } from "~/models/project";
 import { Dispatch } from "redux";
 import { FilterBuilder } from "~/common/api/filter-builder";
 import { RootState } from "../store";
-import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { updateFavorites } from "../favorites/favorites-actions";
 import { ServiceRepository } from "~/services/services";
 import { projectPanelActions } from "~/store/project-panel/project-panel-action";
 import { resourcesActions } from "~/store/resources/resources-actions";
@@ -40,7 +40,7 @@ export const getProjectList = (parentUuid: string = '') =>
                 .getFilters()
         }).then(({ items: projects }) => {
             dispatch(projectActions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
-            dispatch<any>(checkPresenceInFavorites(projects.map(project => project.uuid)));
+            dispatch<any>(updateFavorites(projects.map(project => project.uuid)));
             return projects;
         });
     };
index 36f99362cccb52db39cdf74c29670bb75206a1e7..0034e7aa5faf8cdea6f22236f971ec14db657e18 100644 (file)
@@ -3,11 +3,29 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { unionize, ofType, UnionOf } from '~/common/unionize';
-import { Resource } from '~/models/resource';
+import { Resource, extractUuidKind } from '~/models/resource';
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { ServiceRepository } from '~/services/services';
+import { getResourceService } from '~/services/services';
 
 export const resourcesActions = unionize({
     SET_RESOURCES: ofType<Resource[]>(),
     DELETE_RESOURCES: ofType<string[]>()
 });
 
-export type ResourcesAction = UnionOf<typeof resourcesActions>;
\ No newline at end of file
+export type ResourcesAction = UnionOf<typeof resourcesActions>;
+
+export const updateResources = (resources: Resource[]) => resourcesActions.SET_RESOURCES(resources);
+
+export const loadResource = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const kind = extractUuidKind(uuid);
+        const service = getResourceService(kind)(services);
+        if (service) {
+            const resource = await service.get(uuid);
+            dispatch<any>(updateResources([resource]));
+            return resource;
+        }
+        return undefined;
+    };
\ No newline at end of file
diff --git a/src/store/side-panel-tree/side-panel-tree-actions.ts b/src/store/side-panel-tree/side-panel-tree-actions.ts
new file mode 100644 (file)
index 0000000..268f1e6
--- /dev/null
@@ -0,0 +1,119 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { treePickerActions } from "~/store/tree-picker/tree-picker-actions";
+import { createTreePickerNode } 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 { TreeItemStatus } from "~/components/tree/tree";
+
+export enum SidePanelTreeCategory {
+    PROJECTS = 'Projects',
+    SHARED_WITH_ME = 'Shared with me',
+    WORKFLOWS = 'Workflows',
+    RECENT_OPEN = 'Recent open',
+    FAVORITES = 'Favorites',
+    TRASH = 'Trash'
+}
+
+export const SIDE_PANEL_TREE = 'sidePanelTree';
+
+const SIDE_PANEL_CATEGORIES = [
+    SidePanelTreeCategory.SHARED_WITH_ME,
+    SidePanelTreeCategory.WORKFLOWS,
+    SidePanelTreeCategory.RECENT_OPEN,
+    SidePanelTreeCategory.FAVORITES,
+    SidePanelTreeCategory.TRASH,
+];
+
+export const isSidePanelTreeCategory = (id: string) => SIDE_PANEL_CATEGORIES.some(category => category === id);
+
+export const initSidePanelTree = () =>
+    (dispatch: Dispatch, getState: () => RootState, { authService }: ServiceRepository) => {
+        const rootProjectUuid = authService.getUuid() || '';
+        const nodes = SIDE_PANEL_CATEGORIES.map(nodeId => createTreePickerNode({ nodeId, value: nodeId }));
+        const projectsNode = createTreePickerNode({ nodeId: rootProjectUuid, value: SidePanelTreeCategory.PROJECTS });
+        dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+            nodeId: '',
+            pickerId: SIDE_PANEL_TREE,
+            nodes: [projectsNode, ...nodes]
+        }));
+        SIDE_PANEL_CATEGORIES.forEach(category => {
+            dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+                nodeId: category,
+                pickerId: SIDE_PANEL_TREE,
+                nodes: []
+            }));
+        });
+    };
+
+export const loadSidePanelTreeProjects = (projectUuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ nodeId: projectUuid, pickerId: SIDE_PANEL_TREE }));
+        const params = {
+            filters: new FilterBuilder()
+                .addEqual('ownerUuid', projectUuid)
+                .getFilters()
+        };
+        const { items } = await services.projectService.list(params);
+        dispatch(treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({
+            nodeId: projectUuid,
+            pickerId: SIDE_PANEL_TREE,
+            nodes: items.map(item => createTreePickerNode({ nodeId: item.uuid, value: item })),
+        }));
+        dispatch(resourcesActions.SET_RESOURCES(items));
+    };
+
+export const activateSidePanelTreeItem = (nodeId: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const node = getSidePanelTreeNode(nodeId)(getState());
+        if (node && !node.selected) {
+            dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId: SIDE_PANEL_TREE }));
+        }
+        if (!isSidePanelTreeCategory(nodeId)) {
+            dispatch<any>(activateSidePanelTreeProject(nodeId));
+        }
+    };
+
+export const activateSidePanelTreeProject = (nodeId: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const node = getSidePanelTreeNode(nodeId)(getState());
+        if (node && node.status !== TreeItemStatus.LOADED) {
+            await dispatch<any>(loadSidePanelTreeProjects(nodeId));
+            if (node.collapsed) {
+                dispatch<any>(toggleSidePanelTreeItemCollapse(nodeId));
+            }
+        } else if (node === undefined) {
+            dispatch<any>(activateSidePanelTreeBranch(nodeId));
+        }
+    };
+
+export const activateSidePanelTreeBranch = (nodeId: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const ancestors = await services.ancestorsService.ancestors(nodeId, services.authService.getUuid() || '');
+        for (const ancestor of ancestors) {
+            await dispatch<any>(loadSidePanelTreeProjects(ancestor.uuid));
+        }
+        for (const ancestor of ancestors) {
+            dispatch<any>(toggleSidePanelTreeItemCollapse(ancestor.uuid));
+        }
+        dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId: SIDE_PANEL_TREE }));
+    };
+
+const getSidePanelTreeNode = (nodeId: string) => (state: RootState) => {
+    const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(state.treePicker);
+    return sidePanelTree
+        ? getNodeValue(nodeId)(sidePanelTree)
+        : undefined;
+};
+
+export const toggleSidePanelTreeItemCollapse = (nodeId: string) =>
+    async (dispatch: Dispatch, getState: () => RootState) => {
+        dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId: SIDE_PANEL_TREE }));
+    };
index ecea3535e35040fdbb8095830ea60e21bacff95e..4fc745b136481947ae29cc48a5b73c0bd06525e9 100644 (file)
@@ -2,14 +2,31 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { default as unionize, ofType, UnionOf } from "unionize";
-import { SidePanelId } from "~/store/side-panel/side-panel-reducer";
+import { Dispatch } from 'redux';
+import { isSidePanelTreeCategory, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { navigateToFavorites, navigateToResource } from '../navigation/navigation-action';
+import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 
-export const sidePanelActions = unionize({
-    TOGGLE_SIDE_PANEL_ITEM_OPEN: ofType<SidePanelId>()
-}, {
-    tag: 'type',
-    value: 'payload'
-});
+export const navigateFromSidePanel = (id: string) =>
+    (dispatch: Dispatch) => {
+        if (isSidePanelTreeCategory(id)) {
+            dispatch<any>(getSidePanelTreeCategoryAction(id));
+        } else {
+            dispatch<any>(navigateToResource(id));
+        }
+    };
 
-export type SidePanelAction = UnionOf<typeof sidePanelActions>;
+const getSidePanelTreeCategoryAction = (id: string) => {
+    switch (id) {
+        case SidePanelTreeCategory.FAVORITES:
+            return navigateToFavorites;
+        default:
+            return sidePanelTreeCategoryNotAvailable(id);
+    }
+};
+
+const sidePanelTreeCategoryNotAvailable = (id: string) =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: `${id} not available`,
+        hideDuration: 3000,
+    });
\ No newline at end of file
diff --git a/src/store/side-panel/side-panel-reducer.test.ts b/src/store/side-panel/side-panel-reducer.test.ts
deleted file mode 100644 (file)
index a76e33a..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { sidePanelReducer } from "./side-panel-reducer";
-import { sidePanelActions } from "./side-panel-action";
-import { ProjectsIcon } from "~/components/icon/icon";
-
-describe('side-panel-reducer', () => {
-    it('should open side-panel item', () => {
-        const initialState = [
-            {
-                id: "1",
-                name: "Projects",
-                url: "/projects",
-                icon: ProjectsIcon,
-                open: false
-            }
-        ];
-        const project = [
-            {
-                id: "1",
-                name: "Projects",
-                icon: ProjectsIcon,
-                open: true,
-                url: "/projects"
-            }
-        ];
-
-        const state = sidePanelReducer(initialState, sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(initialState[0].id));
-        expect(state).toEqual(project);
-    });
-});
diff --git a/src/store/side-panel/side-panel-reducer.ts b/src/store/side-panel/side-panel-reducer.ts
deleted file mode 100644 (file)
index db1cbe5..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { sidePanelActions, SidePanelAction } from './side-panel-action';
-import { SidePanelItem } from '~/components/side-panel/side-panel';
-import { ProjectsIcon, ShareMeIcon, WorkflowIcon, RecentIcon, FavoriteIcon, TrashIcon } from "~/components/icon/icon";
-import { Dispatch } from "redux";
-import { push } from "react-router-redux";
-import { favoritePanelActions } from "../favorite-panel/favorite-panel-action";
-import { projectPanelActions } from "../project-panel/project-panel-action";
-import { projectActions } from "../project/project-action";
-import { getProjectUrl } from "../../models/project";
-import { columns as projectPanelColumns } from "../../views/project-panel/project-panel";
-import { columns as favoritePanelColumns } from "../../views/favorite-panel/favorite-panel";
-
-export type SidePanelState = SidePanelItem[];
-
-export const sidePanelReducer = (state: SidePanelState = sidePanelItems, action: SidePanelAction) => {
-    return sidePanelActions.match(action, {
-        TOGGLE_SIDE_PANEL_ITEM_OPEN: itemId =>
-            state.map(it => ({...it, open: itemId === it.id && it.open === false})),
-        default: () => state
-    });
-};
-
-export enum SidePanelId {
-    PROJECTS = "Projects",
-    SHARED_WITH_ME = "SharedWithMe",
-    WORKFLOWS = "Workflows",
-    RECENT_OPEN = "RecentOpen",
-    FAVORITES = "Favourites",
-    TRASH = "Trash"
-}
-
-export const sidePanelItems = [
-    {
-        id: SidePanelId.PROJECTS,
-        name: "Projects",
-        url: "/projects",
-        icon: ProjectsIcon,
-        open: false,
-        active: false,
-        margin: true,
-        openAble: true,
-        activeAction: (dispatch: Dispatch, uuid: string) => {
-            dispatch(push(getProjectUrl(uuid)));
-            dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(uuid));
-            dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
-            dispatch(projectPanelActions.RESET_PAGINATION());
-            dispatch(projectPanelActions.REQUEST_ITEMS());
-        }
-    },
-    {
-        id: SidePanelId.SHARED_WITH_ME,
-        name: "Shared with me",
-        url: "/shared",
-        icon: ShareMeIcon,
-        active: false,
-        activeAction: (dispatch: Dispatch) => {
-            dispatch(push("/shared"));
-        }
-    },
-    {
-        id: SidePanelId.WORKFLOWS,
-        name: "Workflows",
-        url: "/workflows",
-        icon: WorkflowIcon,
-        active: false,
-        activeAction: (dispatch: Dispatch) => {
-            dispatch(push("/workflows"));
-        }
-    },
-    {
-        id: SidePanelId.RECENT_OPEN,
-        name: "Recent open",
-        url: "/recent",
-        icon: RecentIcon,
-        active: false,
-        activeAction: (dispatch: Dispatch) => {
-            dispatch(push("/recent"));
-        }
-    },
-    {
-        id: SidePanelId.FAVORITES,
-        name: "Favorites",
-        url: "/favorites",
-        icon: FavoriteIcon,
-        active: false,
-        activeAction: (dispatch: Dispatch) => {
-            dispatch(push("/favorites"));
-            dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
-            dispatch(favoritePanelActions.RESET_PAGINATION());
-            dispatch(favoritePanelActions.REQUEST_ITEMS());
-        }
-    },
-    {
-        id: SidePanelId.TRASH,
-        name: "Trash",
-        url: "/trash",
-        icon: TrashIcon,
-        active: false,
-        activeAction: (dispatch: Dispatch) => {
-            dispatch(push("/trash"));
-        }
-    }
-];
index c815ad4f900468ed74f2bd0b33d062e5e377f219..fd104fe4b25695624c6ee771aa14f44a4921bb93 100644 (file)
@@ -4,6 +4,7 @@
 
 import { Tree } from "~/models/tree";
 import { TreeItemStatus } from "~/components/tree/tree";
+import { RootState } from '~/store/store';
 
 export type TreePicker = { [key: string]: Tree<TreePickerNode> };
 
@@ -21,3 +22,5 @@ export const createTreePickerNode = (data: { nodeId: string, value: any }) => ({
     collapsed: true,
     status: TreeItemStatus.INITIAL
 });
+
+export const getTreePicker = (id: string) => (state: TreePicker): Tree<TreePickerNode> | undefined => state[id];
\ No newline at end of file
index 3dc6d1a1acf51370c3c7b9c169745dd98ae184d8..4fa87a28b8471249c1dbb085e470a5b15545eff8 100644 (file)
@@ -9,6 +9,7 @@ import { getUserDetails, saveApiToken } from "~/store/auth/auth-action";
 import { getProjectList } from "~/store/project/project-action";
 import { getUrlParameter } from "~/common/url";
 import { AuthService } from "~/services/auth-service/auth-service";
+import { loadWorkbench } from '../../store/navigation/navigation-action';
 
 interface ApiTokenProps {
     authService: AuthService;
@@ -22,7 +23,7 @@ export const ApiToken = connect()(
             this.props.dispatch(saveApiToken(apiToken));
             this.props.dispatch<any>(getUserDetails()).then(() => {
                 const rootUuid = this.props.authService.getRootUuid();
-                this.props.dispatch(getProjectList(rootUuid));
+                this.props.dispatch(loadWorkbench());
             });
         }
         render() {
index d548f607f550637cd5a120fc358c620d913bd29b..16dd59933411f5d394b31a58d5262fbff0418cda 100644 (file)
@@ -14,23 +14,18 @@ import { DataColumns } from "~/components/data-table/data-table";
 
 interface Props {
     id: string;
-    columns: DataColumns<any>;
     onRowClick: (item: any) => void;
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
     onRowDoubleClick: (item: any) => void;
     extractKey?: (item: any) => React.Key;
 }
 
-const mapStateToProps = (state: RootState, { id, columns }: Props) => {
-    const s = getDataExplorer(state.dataExplorer, id);
-    if (s.columns.length === 0) {
-        s.columns = columns;
-    }
-    return s;
+const mapStateToProps = (state: RootState, { id }: Props) => {
+    return getDataExplorer(state.dataExplorer, id);
 };
 
 const mapDispatchToProps = () => {
-    return (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
+    return (dispatch: Dispatch, { id, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
         onSetColumns: (columns: DataColumns<any>) => {
             dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
         },
diff --git a/src/views-components/navigation-panel/navigation-panel.tsx b/src/views-components/navigation-panel/navigation-panel.tsx
deleted file mode 100644 (file)
index 283e9be..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import * as React from 'react';
-import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import Drawer from '@material-ui/core/Drawer';
-import { connect } from "react-redux";
-import { ProjectTree } from '~/views-components/project-tree/project-tree';
-import { SidePanel, SidePanelItem } from '~/components/side-panel/side-panel';
-import { ArvadosTheme } from '~/common/custom-theme';
-import { RootState } from '~/store/store';
-import { TreeItem } from '~/components/tree/tree';
-import { ProjectResource } from '~/models/project';
-import { sidePanelActions } from '../../store/side-panel/side-panel-action';
-import { Dispatch } from 'redux';
-import { projectActions } from '~/store/project/project-action';
-import { navigateToResource } from '../../store/navigation/navigation-action';
-import { openContextMenu } from '~/store/context-menu/context-menu-actions';
-import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
-
-
-const DRAWER_WITDH = 240;
-
-type CssRules = 'drawerPaper' | 'toolbar';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    drawerPaper: {
-        position: 'relative',
-        width: DRAWER_WITDH,
-        display: 'flex',
-        flexDirection: 'column',
-    },
-    toolbar: theme.mixins.toolbar
-});
-
-interface NavigationPanelDataProps {
-    projects: Array<TreeItem<ProjectResource>>;
-    sidePanelItems: SidePanelItem[];
-}
-
-interface NavigationPanelActionProps {
-    toggleSidePanelOpen: (panelItemId: string) => void;
-    toggleSidePanelActive: (panelItemId: string) => void;
-    toggleProjectOpen: (projectUuid: string) => void;
-    toggleProjectActive: (projectUuid: string) => void;
-    openRootContextMenu: (event: React.MouseEvent<any>) => void;
-    openProjectContextMenu: (event: React.MouseEvent<any>, item: TreeItem<ProjectResource>) => void;
-}
-
-type NavigationPanelProps = NavigationPanelDataProps & NavigationPanelActionProps & WithStyles<CssRules>;
-
-const mapStateToProps = (state: RootState): NavigationPanelDataProps => ({
-    projects: state.projects.items,
-    sidePanelItems: state.sidePanel
-});
-
-const mapDispatchToProps = (dispatch: Dispatch): NavigationPanelActionProps => ({
-    toggleSidePanelOpen: panelItemId => {
-        dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(panelItemId));
-    },
-    toggleSidePanelActive: panelItemId => {
-        dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(panelItemId));
-
-        // const panelItem = this.props.sidePanelItems.find(it => it.id === itemId);
-        // if (panelItem && panelItem.activeAction) {
-        //     panelItem.activeAction(this.props.dispatch, this.props.authService.getUuid());
-        // }
-    },
-    toggleProjectOpen: projectUuid => {
-        dispatch<any>(navigateToResource(projectUuid));
-    },
-    toggleProjectActive: projectUuid => {
-        dispatch<any>(navigateToResource(projectUuid));
-    },
-    openRootContextMenu: event => {
-        dispatch<any>(openContextMenu(event, {
-            uuid: "",
-            name: "",
-            kind: ContextMenuKind.ROOT_PROJECT
-        }));
-    },
-    openProjectContextMenu: (event, item) => {
-        dispatch<any>(openContextMenu(event, {
-            uuid: item.data.uuid,
-            name: item.data.name,
-            kind: ContextMenuKind.PROJECT
-        }));
-    }
-});
-
-export const NavigationPanel = withStyles(styles)(
-    connect(mapStateToProps, mapDispatchToProps)(
-        ({ classes, sidePanelItems, projects, ...actions }: NavigationPanelProps) => <Drawer
-            variant="permanent"
-            classes={{ paper: classes.drawerPaper }}>
-            <div className={classes.toolbar} />
-            <SidePanel
-                toggleOpen={actions.toggleSidePanelOpen}
-                toggleActive={actions.toggleSidePanelOpen}
-                sidePanelItems={sidePanelItems}
-                onContextMenu={actions.openRootContextMenu}>
-                <ProjectTree
-                    projects={projects}
-                    toggleOpen={actions.toggleProjectOpen}
-                    onContextMenu={actions.openProjectContextMenu}
-                    toggleActive={actions.toggleProjectActive} />
-            </SidePanel>
-        </Drawer>
-    )
-);
diff --git a/src/views-components/side-panel-tree/side-panel-tree.tsx b/src/views-components/side-panel-tree/side-panel-tree.tsx
new file mode 100644 (file)
index 0000000..6445515
--- /dev/null
@@ -0,0 +1,65 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { TreePicker, TreePickerProps } from "../tree-picker/tree-picker";
+import { TreeItem } from "~/components/tree/tree";
+import { ProjectResource } from "~/models/project";
+import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon";
+import { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon } from '~/components/icon/icon';
+import { RecentIcon, WorkflowIcon } from '~/components/icon/icon';
+import { activateSidePanelTreeItem, toggleSidePanelTreeItemCollapse, SIDE_PANEL_TREE, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
+
+export interface SidePanelTreeProps {
+    onItemActivation: (id: string) => void;
+}
+
+type SidePanelTreeActionProps = Pick<TreePickerProps, 'toggleItemActive' | 'toggleItemOpen'>;
+
+const mapDispatchToProps = (dispatch: Dispatch, props: SidePanelTreeProps): SidePanelTreeActionProps => ({
+    toggleItemActive: (nodeId) => {
+        dispatch<any>(activateSidePanelTreeItem(nodeId));
+        props.onItemActivation(nodeId);
+    },
+    toggleItemOpen: (nodeId) => {
+        dispatch<any>(toggleSidePanelTreeItemCollapse(nodeId));
+    }
+});
+
+export const SidePanelTree = connect(undefined, mapDispatchToProps)(
+    (props: SidePanelTreeActionProps) =>
+        <TreePicker {...props} render={renderSidePanelItem} pickerId={SIDE_PANEL_TREE} />);
+
+const renderSidePanelItem = (item: TreeItem<ProjectResource>) =>
+    <ListItemTextIcon
+        icon={getProjectPickerIcon(item)}
+        name={typeof item.data === 'string' ? item.data : item.data.name}
+        isActive={item.active}
+        hasMargin={true} />;
+
+const getProjectPickerIcon = (item: TreeItem<ProjectResource | string>) =>
+    typeof item.data === 'string'
+        ? getSidePanelIcon(item.data)
+        : ProjectIcon;
+
+const getSidePanelIcon = (category: string) => {
+    switch (category) {
+        case SidePanelTreeCategory.FAVORITES:
+            return FavoriteIcon;
+        case SidePanelTreeCategory.PROJECTS:
+            return ProjectsIcon;
+        case SidePanelTreeCategory.RECENT_OPEN:
+            return RecentIcon;
+        case SidePanelTreeCategory.SHARED_WITH_ME:
+            return ShareMeIcon;
+        case SidePanelTreeCategory.TRASH:
+            return TrashIcon;
+        case SidePanelTreeCategory.WORKFLOWS:
+            return WorkflowIcon;
+        default:
+            return ProjectIcon;
+    }
+};
diff --git a/src/views-components/side-panel/side-panel.tsx b/src/views-components/side-panel/side-panel.tsx
new file mode 100644 (file)
index 0000000..b81f39e
--- /dev/null
@@ -0,0 +1,45 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import Drawer from '@material-ui/core/Drawer';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { SidePanelTree, SidePanelTreeProps } from '~/views-components/side-panel-tree/side-panel-tree';
+import { compose, Dispatch } from 'redux';
+import { connect } from 'react-redux';
+import { navigateFromSidePanel } from '../../store/side-panel/side-panel-action';
+
+const DRAWER_WITDH = 240;
+
+type CssRules = 'drawerPaper' | 'toolbar';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    drawerPaper: {
+        position: 'relative',
+        width: DRAWER_WITDH,
+        display: 'flex',
+        flexDirection: 'column',
+        paddingTop: 58,
+        overflow: 'auto',
+    },
+    toolbar: theme.mixins.toolbar
+});
+
+const mapDispatchToProps = (dispatch: Dispatch): SidePanelTreeProps => ({
+    onItemActivation: id => {
+        dispatch<any>(navigateFromSidePanel(id));
+    }
+});
+
+export const SidePanel = compose(
+    withStyles(styles),
+    connect(undefined, mapDispatchToProps)
+)(({ classes, ...props }: WithStyles<CssRules> & SidePanelTreeProps) =>
+    <Drawer
+        variant="permanent"
+        classes={{ paper: classes.drawerPaper }}>
+        <div className={classes.toolbar} />
+        <SidePanelTree {...props} />
+    </Drawer>);
index 7621d95a05656c89c167e3be62c44669d6c1b21b..d22dd0de289122cdf088504a614f1042c6254161 100644 (file)
@@ -21,7 +21,6 @@ import { CollectionTagForm } from './collection-tag-form';
 import { deleteCollectionTag } from '~/store/collection-panel/collection-panel-action';
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 import { getResource } from '~/store/resources/resources';
-import { loadCollection } from '../../store/collection-panel/collection-panel-action';
 import { contextMenuActions } from '~/store/context-menu/context-menu-actions';
 import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
 
@@ -162,13 +161,6 @@ export const CollectionPanel = withStyles(styles)(
                 }));
             }
 
-            componentDidMount() {
-                const { match, item } = this.props;
-                if (!item && match.params.id) {
-                    this.props.dispatch<any>(loadCollection(match.params.id));
-                }
-            }
-
         }
     )
 );
index dfe107a811e248dc82b24903e0cb036e6d6e28f5..cdfe97054cc21ea52ae76f35424b051b0159df31 100644 (file)
@@ -49,7 +49,7 @@ export interface FavoritePanelFilter extends DataTableFilterItem {
     type: ResourceKind | ContainerRequestState;
 }
 
-export const columns: DataColumns<string, FavoritePanelFilter> = [
+export const favoritePanelColumns: DataColumns<string, FavoritePanelFilter> = [
     {
         name: FavoritePanelColumnNames.NAME,
         selected: true,
@@ -147,7 +147,6 @@ interface FavoritePanelActionProps {
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
     onDialogOpen: (ownerUuid: string) => void;
     onItemDoubleClick: (item: string) => void;
-    onMount: () => void;
 }
 
 const mapDispatchToProps = (dispatch: Dispatch): FavoritePanelActionProps => ({
@@ -166,10 +165,7 @@ const mapDispatchToProps = (dispatch: Dispatch): FavoritePanelActionProps => ({
     },
     onItemDoubleClick: uuid => {
         dispatch<any>(navigateToResource(uuid));
-    },
-    onMount: () => {
-        dispatch(loadFavoritePanel());
-    },
+    }
 });
 
 type FavoritePanelProps = FavoritePanelDataProps & FavoritePanelActionProps & DispatchProp
@@ -181,17 +177,12 @@ export const FavoritePanel = withStyles(styles)(
             render() {
                 return <DataExplorer
                     id={FAVORITE_PANEL_ID}
-                    columns={columns}
                     onRowClick={this.props.onItemClick}
                     onRowDoubleClick={this.props.onItemDoubleClick}
                     onContextMenu={this.props.onContextMenu}
                     defaultIcon={FavoriteIcon}
                     defaultMessages={['Your favorites list is empty.']} />;
             }
-
-            componentDidMount() {
-                this.props.onMount();
-            }
         }
     )
 );
index a2ae4cfd0d5ab6f18c7aa0a3ca6924277a4e9ba3..37712c7dcb47f3184bf9a72c8669cd964aa85167 100644 (file)
@@ -16,7 +16,6 @@ import { ResourceKind } from '~/models/resource';
 import { resourceLabel } from '~/common/labels';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner } from '~/views-components/data-explorer/renderers';
-import { restoreBranch, setProjectItem, ItemMode } from '~/store/navigation/navigation-action';
 import { ProjectIcon } from '~/components/icon/icon';
 import { ResourceName } from '~/views-components/data-explorer/renderers';
 import { ResourcesState, getResource } from '~/store/resources/resources';
@@ -30,6 +29,8 @@ 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 { getProperty } from '~/store/properties/properties';
+import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
 
 type CssRules = 'root' | "toolbar" | "button";
 
@@ -61,7 +62,7 @@ export interface ProjectPanelFilter extends DataTableFilterItem {
     type: ResourceKind | ContainerRequestState;
 }
 
-export const columns: DataColumns<string, ProjectPanelFilter> = [
+export const projectPanelColumns: DataColumns<string, ProjectPanelFilter> = [
     {
         name: ProjectPanelColumnNames.NAME,
         selected: true,
@@ -161,7 +162,10 @@ type ProjectPanelProps = ProjectPanelDataProps & DispatchProp
     & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
 
 export const ProjectPanel = withStyles(styles)(
-    connect((state: RootState) => ({ currentItemId: state.projects.currentItemId, resources: state.resources }))(
+    connect((state: RootState) => ({
+        currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
+        resources: state.resources
+    }))(
         class extends React.Component<ProjectPanelProps> {
             render() {
                 const { classes } = this.props;
@@ -179,7 +183,6 @@ export const ProjectPanel = withStyles(styles)(
                     </div>
                     <DataExplorer
                         id={PROJECT_PANEL_ID}
-                        columns={columns}
                         onRowClick={this.handleRowClick}
                         onRowDoubleClick={this.handleRowDoubleClick}
                         onContextMenu={this.handleContextMenu}
@@ -234,12 +237,6 @@ export const ProjectPanel = withStyles(styles)(
                 this.props.dispatch(loadDetailsPanel(uuid));
             }
 
-            async componentDidMount() {
-                if (this.props.match.params.id && this.props.currentItemId === '') {
-                    await this.props.dispatch<any>(restoreBranch(this.props.match.params.id));
-                    this.props.dispatch<any>(setProjectItem(this.props.match.params.id, ItemMode.BOTH));
-                }
-            }
         }
     )
 );
index 2dda4d23c5455d82577f77a8d29af6c7b5e33f3e..1c11e06aa9e5c6e45d0823bee8cd0cd65f446deb 100644 (file)
@@ -14,14 +14,13 @@ import { Breadcrumb } from '~/components/breadcrumbs/breadcrumbs';
 import { push } from 'react-router-redux';
 import { TreeItem } from "~/components/tree/tree";
 import { getTreePath } from '~/store/project/project-reducer';
-import { ItemMode, setProjectItem } from "~/store/navigation/navigation-action";
 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 { openContextMenu } from '~/store/context-menu/context-menu-actions';
-import { ProjectResource } from '~/models/project';
+import { ProjectResource, getProjectUrl } 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';
@@ -37,10 +36,12 @@ import { MultipleFilesRemoveDialog } from '~/views-components/file-remove-dialog
 import { DialogCollectionCreateWithSelectedFile } from '~/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected';
 import { UploadCollectionFilesDialog } from '~/views-components/upload-collection-files-dialog/upload-collection-files-dialog';
 import { ProjectCopyDialog } from '~/views-components/project-copy-dialog/project-copy-dialog';
-import { CollectionPartialCopyDialog } from '../../views-components/collection-partial-copy-dialog/collection-partial-copy-dialog';
+import { CollectionPartialCopyDialog } from '~/views-components/collection-partial-copy-dialog/collection-partial-copy-dialog';
 import { MoveProjectDialog } from '~/views-components/move-project-dialog/move-project-dialog';
 import { MoveCollectionDialog } from '~/views-components/move-collection-dialog/move-collection-dialog';
-import { NavigationPanel } from '~/views-components/navigation-panel/navigation-panel';
+import { SidePanel } from '~/views-components/side-panel/side-panel';
+import { Routes } from '~/routes/routes';
+import { navigateToResource } from '../../store/navigation/navigation-action';
 
 const APP_BAR_HEIGHT = 100;
 
@@ -165,6 +166,8 @@ export const Workbench = withStyles(styles)(
                     status: item.status
                 }));
 
+                const rootProjectUuid = this.props.authService.getUuid();
+
                 const { classes, user } = this.props;
                 return (
                     <div className={classes.root}>
@@ -177,14 +180,13 @@ export const Workbench = withStyles(styles)(
                                 buildInfo={this.props.buildInfo}
                                 {...this.mainAppBarActions} />
                         </div>
-                        {user && <NavigationPanel />}
+                        {user && <SidePanel />}
                         <main className={classes.contentWrapper}>
                             <div className={classes.content}>
                                 <Switch>
-                                    <Route path='/' exact render={() => <Redirect to={`/projects/${this.props.authService.getUuid()}`} />} />
-                                    <Route path="/projects/:id" component={ProjectPanel} />
-                                    <Route path="/favorites" component={FavoritePanel} />
-                                    <Route path="/collections/:id" component={CollectionPanel} />
+                                    <Route path={Routes.PROJECTS} component={ProjectPanel} />
+                                    <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
+                                    <Route path={Routes.FAVORITES} component={FavoritePanel} />
                                 </Switch>
                             </div>
                             {user && <DetailsPanel />}
@@ -214,8 +216,7 @@ export const Workbench = withStyles(styles)(
 
             mainAppBarActions: MainAppBarActionProps = {
                 onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
-                    this.props.dispatch(setProjectItem(itemId, ItemMode.BOTH));
-                    this.props.dispatch(loadDetailsPanel(itemId));
+                    this.props.dispatch(navigateToResource(itemId));
                 },
                 onSearch: searchText => {
                     this.setState({ searchText });