Merge branch 'master'
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 27 Aug 2018 12:09:16 +0000 (14:09 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 27 Aug 2018 12:09:16 +0000 (14:09 +0200)
Feature #14102

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

65 files changed:
src/common/unionize.ts [new file with mode: 0644]
src/components/breadcrumbs/breadcrumbs.tsx
src/components/side-panel/side-panel.tsx [deleted file]
src/components/tree/tree.tsx
src/index.tsx
src/models/resource.ts
src/models/user.ts
src/routes/routes.ts [new file with mode: 0644]
src/services/ancestors-service/ancestors-service.ts [new file with mode: 0644]
src/services/services.ts
src/services/user-service/user-service.ts [new file with mode: 0644]
src/store/auth/auth-action.ts
src/store/breadcrumbs/breadcrumbs-actions.ts [new file with mode: 0644]
src/store/collection-panel/collection-panel-action.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
src/store/collections/creator/collection-creator-action.ts [new file with mode: 0644]
src/store/collections/updater/collection-updater-action.ts [new file with mode: 0644]
src/store/collections/uploader/collection-uploader-actions.ts
src/store/context-menu/context-menu-actions.ts
src/store/data-explorer/data-explorer-action.ts
src/store/data-explorer/data-explorer-middleware-service.ts
src/store/details-panel/details-panel-action.ts
src/store/details-panel/details-panel-reducer.ts
src/store/dialog/dialog-actions.ts
src/store/favorite-panel/favorite-panel-action.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/properties/properties-actions.ts [new file with mode: 0644]
src/store/properties/properties-reducer.ts [new file with mode: 0644]
src/store/properties/properties.ts [new file with mode: 0644]
src/store/resources/resources-actions.ts [new file with mode: 0644]
src/store/resources/resources-reducer.ts [new file with mode: 0644]
src/store/resources/resources.ts [new file with mode: 0644]
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/snackbar/snackbar-actions.ts
src/store/snackbar/snackbar-reducer.ts
src/store/store.ts
src/store/tree-picker/tree-picker-actions.ts
src/store/tree-picker/tree-picker-reducer.ts
src/store/tree-picker/tree-picker.ts
src/views-components/api-token/api-token.tsx
src/views-components/breadcrumbs/breadcrumbs.ts [new file with mode: 0644]
src/views-components/context-menu/action-sets/project-action-set.ts
src/views-components/context-menu/action-sets/root-project-action-set.ts
src/views-components/data-explorer/data-explorer.tsx
src/views-components/data-explorer/renderers.tsx
src/views-components/details-panel/details-panel.tsx
src/views-components/main-app-bar/main-app-bar.tsx
src/views-components/project-tree-picker/project-tree-picker.tsx
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-components/tree-picker/tree-picker.ts
src/views/collection-panel/collection-panel.tsx
src/views/favorite-panel/favorite-panel-item.ts [deleted file]
src/views/favorite-panel/favorite-panel.tsx
src/views/project-panel/project-panel-item.ts [deleted file]
src/views/project-panel/project-panel.tsx
src/views/workbench/workbench.tsx

diff --git a/src/common/unionize.ts b/src/common/unionize.ts
new file mode 100644 (file)
index 0000000..b684431
--- /dev/null
@@ -0,0 +1,14 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export * from 'unionize';
+import { unionize as originalUnionize, SingleValueRec } from 'unionize';
+
+export function unionize<Record extends SingleValueRec>(record: Record) {
+    return originalUnionize(record, {
+        tag: 'type',
+        value: 'payload'
+    });
+}
+
index da549dba46757a9932d7655f43c25e56431d4380..444ac75ef51b97c0f1df5ba8cf55b828bbf571c1 100644 (file)
@@ -25,7 +25,7 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
     }
 });
 
-interface BreadcrumbsProps {
+export interface BreadcrumbsProps {
     items: Breadcrumb[];
     onClick: (breadcrumb: Breadcrumb) => void;
     onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => void;
diff --git a/src/components/side-panel/side-panel.tsx b/src/components/side-panel/side-panel.tsx
deleted file mode 100644 (file)
index 84e5c54..0000000
+++ /dev/null
@@ -1,128 +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 { ArvadosTheme } from '~/common/custom-theme';
-import { List, ListItem, ListItemIcon, Collapse } from "@material-ui/core";
-import { SidePanelRightArrowIcon, IconType } from '../icon/icon';
-import * as classnames from "classnames";
-import { ListItemTextIcon } from '../list-item-text-icon/list-item-text-icon';
-import { Dispatch } from "redux";
-import { RouteComponentProps, withRouter } from "react-router";
-
-type CssRules = 'active' | 'row' | 'root' | 'list' | 'iconClose' | 'iconOpen' | 'toggableIconContainer' | 'toggableIcon';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    root: {
-        overflowY: 'auto',
-        minWidth: '240px',
-        whiteSpace: 'nowrap',
-        marginTop: '52px',
-        display: 'flex',
-        flexGrow: 1,
-    },
-    list: {
-        padding: '5px 0px 5px 14px',
-        minWidth: '240px',
-    },
-    row: {
-        display: 'flex',
-        alignItems: 'center',
-    },
-    toggableIconContainer: {
-        color: theme.palette.grey["700"],
-        height: '14px',
-        width: '14px'
-    },
-    toggableIcon: {
-        fontSize: '14px'
-    },
-    active: {
-        color: theme.palette.primary.main,
-    },
-    iconClose: {
-        transition: 'all 0.1s ease',
-    },
-    iconOpen: {
-        transition: 'all 0.1s ease',
-        transform: 'rotate(90deg)',
-    }
-});
-
-export interface SidePanelItem {
-    id: string;
-    name: string;
-    url: string;
-    icon: IconType;
-    open?: boolean;
-    margin?: boolean;
-    openAble?: boolean;
-    activeAction?: (dispatch: Dispatch, uuid?: string) => void;
-}
-
-interface SidePanelDataProps {
-    toggleOpen: (id: string) => void;
-    toggleActive: (id: string) => void;
-    sidePanelItems: SidePanelItem[];
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: SidePanelItem) => void;
-}
-
-type SidePanelProps = RouteComponentProps<{}> & SidePanelDataProps & WithStyles<CssRules>;
-
-export const SidePanel = withStyles(styles)(withRouter(
-    class extends React.Component<SidePanelProps> {
-        render() {
-            const { classes, toggleOpen, toggleActive, sidePanelItems, children } = this.props;
-            const { root, row, list, toggableIconContainer } = classes;
-
-            const path = this.props.location.pathname.split('/');
-            const activeUrl = path.length > 1 ? "/" + path[1] : "/";
-            return (
-                <div className={root}>
-                    <List>
-                        {sidePanelItems.map(it => {
-                            const active = it.url === activeUrl;
-                            return <span key={it.name}>
-                                <ListItem button className={list} onClick={() => toggleActive(it.id)}
-                                          onContextMenu={this.handleRowContextMenu(it)}>
-                                    <span className={row}>
-                                        {it.openAble ? (
-                                            <i onClick={() => toggleOpen(it.id)} className={toggableIconContainer}>
-                                                <ListItemIcon
-                                                    className={this.getToggableIconClassNames(it.open, active)}>
-                                                    < SidePanelRightArrowIcon/>
-                                                </ListItemIcon>
-                                            </i>
-                                        ) : null}
-                                        <ListItemTextIcon icon={it.icon} name={it.name} isActive={active}
-                                                          hasMargin={it.margin}/>
-                                    </span>
-                                </ListItem>
-                                {it.openAble ? (
-                                    <Collapse in={it.open} timeout="auto" unmountOnExit>
-                                        {children}
-                                    </Collapse>
-                                ) : null}
-                            </span>;
-                        })}
-                    </List>
-                </div>
-            );
-        }
-
-        getToggableIconClassNames = (isOpen?: boolean, isActive ?: boolean) => {
-            const { iconOpen, iconClose, active, toggableIcon } = this.props.classes;
-            return classnames(toggableIcon, {
-                [iconOpen]: isOpen,
-                [iconClose]: !isOpen,
-                [active]: isActive
-            });
-        }
-
-        handleRowContextMenu = (item: SidePanelItem) =>
-            (event: React.MouseEvent<HTMLElement>) =>
-                item.openAble ? this.props.onContextMenu(event, item) : null
-    }
-));
index 8d657f8dd63dabff570abafaa831d8411a9d1eab..c892d7d2c8fb12b86da3e4904b9dc9c8198148e1 100644 (file)
@@ -107,7 +107,7 @@ export const Tree = withStyles(styles)(
                             onContextMenu={this.handleRowContextMenu(it)}>
                             {it.status === TreeItemStatus.PENDING ?
                                 <CircularProgress size={10} className={loader} /> : null}
-                            <i onClick={() => this.props.toggleItemOpen(it.id, it.status)}
+                            <i onClick={this.handleToggleItemOpen(it.id, it.status)}
                                 className={toggableIconContainer}>
                                 <ListItemIcon className={this.getToggableIconClassNames(it.open, it.active)}>
                                     {this.getProperArrowAnimation(it.status, it.items!)}
@@ -171,5 +171,10 @@ export const Tree = withStyles(styles)(
                 }
                 : undefined;
         }
+
+        handleToggleItemOpen = (id: string, status: TreeItemStatus) => (event: React.MouseEvent<HTMLElement>) => {
+            event.stopPropagation();
+            this.props.toggleItemOpen(id, status);
+        }
     }
 );
index bee08c80a7fa598d7ffd42ad87abd14335f872ef..ad77162986e01978a1c3cc30f58c002f627388bf 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,9 @@ 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';
+import { Routes } from '~/routes/routes';
 
 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 +49,25 @@ addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
 addMenuActionSet(ContextMenuKind.COLLECTION_RESOURCE, collectionResourceActionSet);
 
 fetchConfig()
-    .then(config => {
+    .then((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={Routes.TOKEN} component={TokenComponent} />
+                            <Route path={Routes.ROOT} component={WorkbenchComponent} />
                         </div>
                     </ConnectedRouter>
                 </Provider>
@@ -73,6 +77,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 6a76b070452146734b2a9f64a36bf3d4eac6ff4f..ff95c1a9b8df59872d6819dccdbcfb2b8ee92441 100644 (file)
@@ -20,5 +20,39 @@ export enum ResourceKind {
     GROUP = "arvados#group",
     PROCESS = "arvados#containerRequest",
     PROJECT = "arvados#group",
-    WORKFLOW = "arvados#workflow"
+    WORKFLOW = "arvados#workflow",
+    USER = "arvados#user",
 }
+
+export enum ResourceObjectType {
+    USER = 'tpzed',
+    GROUP = 'j7d0g',
+    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 = RESOURCE_UUID_REGEX.exec(uuid);
+    return match
+        ? match[0].split('-')[1]
+        : undefined;
+};
+
+export const extractUuidKind = (uuid: string = '') => {
+    const objectType = extractUuidObjectType(uuid);
+    switch (objectType) {
+        case ResourceObjectType.USER:
+            return ResourceKind.USER;
+        case ResourceObjectType.GROUP:
+            return ResourceKind.GROUP;
+        case ResourceObjectType.COLLECTION:
+            return ResourceKind.COLLECTION;
+        default:
+            return undefined;
+    }
+};
index 4cc29ba779cc6dfd677c965d0c485e7bc18fb0fe..c2f21e582798dacd5597872696ff7fc1685d62e7 100644 (file)
@@ -2,6 +2,8 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { Resource, ResourceKind } from '~/models/resource';
+
 export interface User {
     email: string;
     firstName: string;
@@ -12,4 +14,18 @@ export interface User {
 
 export const getUserFullname = (user?: User) => {
     return user ? `${user.firstName} ${user.lastName}` : "";
-};
\ No newline at end of file
+};
+
+export interface UserResource extends Resource {
+    kind: ResourceKind.USER;
+    email: string;
+    username: string;
+    firstName: string;
+    lastName: string;
+    identityUrl: string;
+    isAdmin: boolean;
+    prefs: string;
+    defaultOwnerUuid: string;
+    isActive: boolean;
+    writableBy: string[];
+}
\ No newline at end of file
diff --git a/src/routes/routes.ts b/src/routes/routes.ts
new file mode 100644 (file)
index 0000000..cacdd4b
--- /dev/null
@@ -0,0 +1,71 @@
+// 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: '/',
+    TOKEN: '/token',
+    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<CollectionRouteParams>(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());
+    }
+};
diff --git a/src/services/ancestors-service/ancestors-service.ts b/src/services/ancestors-service/ancestors-service.ts
new file mode 100644 (file)
index 0000000..1cd42fb
--- /dev/null
@@ -0,0 +1,44 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { GroupsService } from "~/services/groups-service/groups-service";
+import { UserService } from '../user-service/user-service';
+import { GroupResource } from '~/models/group';
+import { UserResource } from '~/models/user';
+import { extractUuidObjectType, ResourceObjectType } from "~/models/resource";
+
+export class AncestorService {
+    constructor(
+        private groupsService: GroupsService,
+        private userService: UserService
+    ) { }
+
+    async ancestors(uuid: string, rootUuid: string): Promise<Array<UserResource | GroupResource>> {
+        const service = this.getService(extractUuidObjectType(uuid));
+        if (service) {
+            const resource = await service.get(uuid);
+            if (uuid === rootUuid) {
+                return [resource];
+            } else {
+                return [
+                    ...await this.ancestors(resource.ownerUuid, rootUuid),
+                    resource
+                ];
+            }
+        } else {
+            return [];
+        }
+    }
+
+    private getService = (objectType?: string) => {
+        switch (objectType) {
+            case ResourceObjectType.GROUP:
+                return this.groupsService;
+            case ResourceObjectType.USER:
+                return this.userService;
+            default:
+                return undefined;
+        }
+    }
+}
\ No newline at end of file
index 0e1f4b438bc239ee02578b9d8aebe751b18f096c..6295527bfb40d5c326b56a4a4d09dfe27d22e85e 100644 (file)
@@ -14,6 +14,9 @@ import { CollectionFilesService } from "./collection-files-service/collection-fi
 import { KeepService } from "./keep-service/keep-service";
 import { WebDAV } from "../common/webdav";
 import { Config } from "../common/config";
+import { UserService } from './user-service/user-service';
+import { AncestorService } from "~/services/ancestors-service/ancestors-service";
+import { ResourceKind } from "~/models/resource";
 
 export type ServiceRepository = ReturnType<typeof createServices>;
 
@@ -33,6 +36,8 @@ export const createServices = (config: Config) => {
     const collectionService = new CollectionService(apiClient, webdavClient, authService);
     const tagService = new TagService(linkService);
     const collectionFilesService = new CollectionFilesService(collectionService);
+    const userService = new UserService(apiClient);
+    const ancestorsService = new AncestorService(groupsService, userService);
 
     return {
         apiClient,
@@ -45,6 +50,21 @@ export const createServices = (config: Config) => {
         favoriteService,
         collectionService,
         tagService,
-        collectionFilesService
+        collectionFilesService,
+        userService,
+        ancestorsService,
     };
 };
+
+export const getResourceService = (kind?: ResourceKind) => (serviceRepository: ServiceRepository) => {
+    switch (kind) {
+        case ResourceKind.USER:
+            return serviceRepository.userService;
+        case ResourceKind.GROUP:
+            return serviceRepository.groupsService;
+        case ResourceKind.COLLECTION:
+            return serviceRepository.collectionService;
+        default:
+            return undefined;
+    }
+};
\ No newline at end of file
diff --git a/src/services/user-service/user-service.ts b/src/services/user-service/user-service.ts
new file mode 100644 (file)
index 0000000..3c09a87
--- /dev/null
@@ -0,0 +1,13 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { AxiosInstance } from "axios";
+import { CommonResourceService } from "~/common/api/common-resource-service";
+import { UserResource } from "~/models/user";
+
+export class UserService extends CommonResourceService<UserResource> {
+    constructor(serverApi: AxiosInstance) {
+        super(serverApi, "users");
+    }
+}
\ No newline at end of file
index 00af5ce5b0bb7614f4fbc97316a61dd712759ba3..ac2e0b7e2f68c6e699e294710d582582701b5fc2 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { ofType, default as unionize, UnionOf } from "unionize";
+import { ofType, unionize, UnionOf } from '~/common/unionize';
 import { Dispatch } from "redux";
 import { User } from "~/models/user";
 import { RootState } from "../store";
@@ -16,9 +16,6 @@ export const authActions = unionize({
     INIT: ofType<{ user: User, token: string }>(),
     USER_DETAILS_REQUEST: {},
     USER_DETAILS_SUCCESS: ofType<User>()
-}, {
-    tag: 'type',
-    value: 'payload'
 });
 
 function setAuthorizationHeader(services: ServiceRepository, token: string) {
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 06d4d2762288ee5b6a14108a7ac01e336242a494..5b2690bfaf7a763f8e56620a25eee66f598847d5 100644 (file)
@@ -2,16 +2,17 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { unionize, ofType, UnionOf } from "unionize";
 import { Dispatch } from "redux";
 import { loadCollectionFiles } from "./collection-panel-files/collection-panel-files-actions";
-import { CollectionResource } from "~/models/collection";
+import { CollectionResource } from '~/models/collection';
 import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions";
 import { createTree } from "~/models/tree";
 import { RootState } from "../store";
 import { ServiceRepository } from "~/services/services";
 import { TagResource, TagProperty } from "~/models/tag";
 import { snackbarActions } from "../snackbar/snackbar-actions";
+import { resourcesActions } from "~/store/resources/resources-actions";
+import { unionize, ofType, UnionOf } from '~/common/unionize';
 
 export const collectionPanelActions = unionize({
     LOAD_COLLECTION: ofType<{ uuid: string }>(),
@@ -22,22 +23,21 @@ export const collectionPanelActions = unionize({
     CREATE_COLLECTION_TAG_SUCCESS: ofType<{ tag: TagResource }>(),
     DELETE_COLLECTION_TAG: ofType<{ uuid: string }>(),
     DELETE_COLLECTION_TAG_SUCCESS: ofType<{ uuid: string }>()
-}, { tag: 'type', value: 'payload' });
+});
 
 export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
 
 export const COLLECTION_TAG_FORM_NAME = 'collectionTagForm';
 
-export const loadCollection = (uuid: string) =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+export const loadCollectionPanel = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid }));
         dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
-        return services.collectionService
-            .get(uuid)
-            .then(item => {
-                dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item }));
-                dispatch<any>(loadCollectionFiles(uuid));
-            });
+        const collection = await services.collectionService.get(uuid);
+        dispatch(resourcesActions.SET_RESOURCES([collection]));
+        dispatch<any>(loadCollectionFiles(collection.uuid));
+        dispatch<any>(loadCollectionTags(collection.uuid));
+        return collection;
     };
 
 export const loadCollectionTags = (uuid: string) =>
@@ -50,7 +50,6 @@ export const loadCollectionTags = (uuid: string) =>
             });
     };
 
-
 export const createCollectionTag = (data: TagProperty) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(collectionPanelActions.CREATE_COLLECTION_TAG({ data }));
index 97abfef09f2822c51912097f7643be5a68bc293b..01b4fe4fac2f489e1aebf7402d3569322745d85d 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
 import { Dispatch } from "redux";
 import { CollectionFilesTree, CollectionFileType } from "~/models/collection-file";
 import { ServiceRepository } from "~/services/services";
@@ -22,7 +22,7 @@ export const collectionPanelFilesAction = unionize({
     TOGGLE_COLLECTION_FILE_SELECTION: ofType<{ id: string }>(),
     SELECT_ALL_COLLECTION_FILES: ofType<{}>(),
     UNSELECT_ALL_COLLECTION_FILES: ofType<{}>(),
-}, { tag: 'type', value: 'payload' });
+});
 
 export type CollectionPanelFilesAction = UnionOf<typeof collectionPanelFilesAction>;
 
diff --git a/src/store/collections/creator/collection-creator-action.ts b/src/store/collections/creator/collection-creator-action.ts
new file mode 100644 (file)
index 0000000..8c42f2d
--- /dev/null
@@ -0,0 +1,34 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from "~/common/unionize";
+import { Dispatch } from "redux";
+
+import { RootState } from "../../store";
+import { CollectionResource } from '~/models/collection';
+import { ServiceRepository } from "~/services/services";
+import { uploadCollectionFiles } from '../uploader/collection-uploader-actions';
+import { reset } from "redux-form";
+
+export const collectionCreateActions = unionize({
+    OPEN_COLLECTION_CREATOR: ofType<{ ownerUuid: string }>(),
+    CLOSE_COLLECTION_CREATOR: ofType<{}>(),
+    CREATE_COLLECTION: ofType<{}>(),
+    CREATE_COLLECTION_SUCCESS: ofType<{}>(),
+});
+
+export type CollectionCreateAction = UnionOf<typeof collectionCreateActions>;
+
+export const createCollection = (collection: Partial<CollectionResource>, files: File[]) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { ownerUuid } = getState().collections.creator;
+        const collectiontData = { ownerUuid, ...collection };
+        dispatch(collectionCreateActions.CREATE_COLLECTION(collectiontData));
+        const newCollection = await services.collectionService.create(collectiontData);
+        await dispatch<any>(uploadCollectionFiles(newCollection.uuid));
+        dispatch(collectionCreateActions.CREATE_COLLECTION_SUCCESS(collection));
+        dispatch(reset('collectionCreateDialog'));
+        return newCollection;
+    };
+
diff --git a/src/store/collections/updater/collection-updater-action.ts b/src/store/collections/updater/collection-updater-action.ts
new file mode 100644 (file)
index 0000000..1ca1a83
--- /dev/null
@@ -0,0 +1,43 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { unionize, ofType, UnionOf } from '~/common/unionize';
+import { RootState } from "../../store";
+import { ServiceRepository } from "~/services/services";
+import { CollectionResource } from '~/models/collection';
+import { initialize } from 'redux-form';
+import { collectionPanelActions } from "../../collection-panel/collection-panel-action";
+import { ContextMenuResource } from "../../context-menu/context-menu-reducer";
+import { resourcesActions } from "~/store/resources/resources-actions";
+
+export const collectionUpdaterActions = unionize({
+    OPEN_COLLECTION_UPDATER: ofType<{ uuid: string }>(),
+    CLOSE_COLLECTION_UPDATER: ofType<{}>(),
+    UPDATE_COLLECTION_SUCCESS: ofType<{}>(),
+});
+
+export const COLLECTION_FORM_NAME = 'collectionEditDialog';
+
+export const openUpdater = (item: ContextMenuResource) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        if (item) {
+            dispatch(collectionUpdaterActions.OPEN_COLLECTION_UPDATER({ uuid: item.uuid }));
+            dispatch(initialize(COLLECTION_FORM_NAME, { name: item.name, description: item.description }));
+        }
+    };
+
+export const updateCollection = (collection: Partial<CollectionResource>) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { uuid } = getState().collections.updater;
+        return services.collectionService
+            .update(uuid, collection)
+            .then(collection => {
+                dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection as CollectionResource }));
+                dispatch(collectionUpdaterActions.UPDATE_COLLECTION_SUCCESS());
+                dispatch(resourcesActions.SET_RESOURCES([collection]));
+            });
+    };
+
+export type CollectionUpdaterAction = UnionOf<typeof collectionUpdaterActions>;
index 0fa55d836cd9510d1840ffb450b9e57801a4a34b..58dcdc4cd1b31ffb05ccfcdecaedb5557d1893a3 100644 (file)
@@ -2,7 +2,7 @@
 //\r
 // SPDX-License-Identifier: AGPL-3.0\r
 \r
-import { default as unionize, ofType, UnionOf } from "unionize";\r
+import { unionize, ofType, UnionOf } from "~/common/unionize";\r
 import { Dispatch } from 'redux';\r
 import { RootState } from '~/store/store';\r
 import { ServiceRepository } from '~/services/services';\r
@@ -26,9 +26,6 @@ export const collectionUploaderActions = unionize({
     START_UPLOAD: ofType(),\r
     SET_UPLOAD_PROGRESS: ofType<{ fileId: number, loaded: number, total: number, currentTime: number }>(),\r
     CLEAR_UPLOAD: ofType()\r
-}, {\r
-    tag: 'type',\r
-    value: 'payload'\r
 });\r
 \r
 export type CollectionUploaderAction = UnionOf<typeof collectionUploaderActions>;\r
index 8e5eb1e795791260474d92e0fffe6e596560081d..26e25c3df3a6db4511f39b126667d4321d2b8b6e 100644 (file)
@@ -2,15 +2,67 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from '~/common/unionize';
 import { ContextMenuPosition, ContextMenuResource } from "./context-menu-reducer";
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { getResource } from '../resources/resources';
+import { ProjectResource } from '~/models/project';
+import { UserResource } from '../../models/user';
+import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
+import { extractUuidKind, ResourceKind } from '~/models/resource';
 
 export const contextMenuActions = unionize({
     OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
     CLOSE_CONTEXT_MENU: ofType<{}>()
-}, {
-        tag: 'type',
-        value: 'payload'
-    });
+});
 
 export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
+
+export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: { name: string; uuid: string; description?: string; kind: ContextMenuKind; }) =>
+    (dispatch: Dispatch) => {
+        event.preventDefault();
+        dispatch(
+            contextMenuActions.OPEN_CONTEXT_MENU({
+                position: { x: event.clientX, y: event.clientY },
+                resource
+            })
+        );
+    };
+
+export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const userResource = getResource<UserResource>(projectUuid)(getState().resources);
+        if (userResource) {
+            dispatch<any>(openContextMenu(event, {
+                name: '',
+                uuid: userResource.uuid,
+                kind: ContextMenuKind.ROOT_PROJECT
+            }));
+        }
+    };
+
+export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const projectResource = getResource<ProjectResource>(projectUuid)(getState().resources);
+        if (projectResource) {
+            dispatch<any>(openContextMenu(event, {
+                name: projectResource.name,
+                uuid: projectResource.uuid,
+                kind: ContextMenuKind.PROJECT
+            }));
+        }
+    };
+
+export const openSidePanelContextMenu = (event: React.MouseEvent<HTMLElement>, id: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        if (!isSidePanelTreeCategory(id)) {
+            const kind = extractUuidKind(id);
+            if (kind === ResourceKind.USER) {
+                dispatch<any>(openRootProjectContextMenu(event, id));
+            } else if (kind === ResourceKind.PROJECT) {
+                dispatch<any>(openProjectContextMenu(event, id));
+            }
+        }
+    };
index abb293fdcf2dd062cfdd0ffb20b6f6ed1ad5a0c0..e637043dacde193f4af1983706289f1dc4b3255c 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
 import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
 import { DataColumns } from "~/components/data-table/data-table";
 
@@ -17,7 +17,7 @@ export const dataExplorerActions = unionize({
     TOGGLE_COLUMN: ofType<{ id: string, columnName: string }>(),
     TOGGLE_SORT: ofType<{ id: string, columnName: string }>(),
     SET_SEARCH_VALUE: ofType<{ id: string, searchValue: string }>(),
-}, { tag: "type", value: "payload" });
+});
 
 export type DataExplorerAction = UnionOf<typeof dataExplorerActions>;
 
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 b8021fb6a0d81d12588b2efe921a0d3142c7df6c..2724a3e3465dbbac374a029f1f68c321dce2a9b1 100644 (file)
@@ -2,48 +2,17 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { unionize, ofType, UnionOf } from "unionize";
-import { Dispatch } from "redux";
-import { Resource, ResourceKind } from "~/models/resource";
-import { RootState } from "../store";
-import { ServiceRepository } from "~/services/services";
+import { unionize, ofType, UnionOf } from '~/common/unionize';
 
 export const detailsPanelActions = unionize({
     TOGGLE_DETAILS_PANEL: ofType<{}>(),
-    LOAD_DETAILS: ofType<{ uuid: string, kind: ResourceKind }>(),
-    LOAD_DETAILS_SUCCESS: ofType<{ item: Resource }>(),
-    UPDATE_DETAILS: ofType<{ item: Resource }>()
-}, { tag: 'type', value: 'payload' });
+    LOAD_DETAILS_PANEL: ofType<string>()
+});
 
 export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
 
-export const loadDetails = (uuid: string, kind: ResourceKind) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(detailsPanelActions.LOAD_DETAILS({ uuid, kind }));
-        const item = await getService(services, kind).get(uuid);
-        dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item }));
-    };
-
-export const updateDetails = (item: Resource) => 
-    async (dispatch: Dispatch, getState: () => RootState) => {
-        const currentItem = getState().detailsPanel.item;
-        if (currentItem && (currentItem.uuid === item.uuid)) {
-            dispatch(detailsPanelActions.UPDATE_DETAILS({ item }));
-            dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item }));
-        }
-    };
-
-
-const getService = (services: ServiceRepository, kind: ResourceKind) => {
-    switch (kind) {
-        case ResourceKind.PROJECT:
-            return services.projectService;
-        case ResourceKind.COLLECTION:
-            return services.collectionService;
-        default:
-            return services.projectService;
-    }
-};
+export const loadDetailsPanel = (uuid: string) => detailsPanelActions.LOAD_DETAILS_PANEL(uuid);
+
 
 
 
index f22add3d49b08810a2c5cf248ca4f90503dc0a1d..091b2fa2cf1adcfa55f19454fdae2f37d9fef7d9 100644 (file)
@@ -3,21 +3,20 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { detailsPanelActions, DetailsPanelAction } from "./details-panel-action";
-import { Resource } from "~/models/resource";
 
 export interface DetailsPanelState {
-    item: Resource | null;
+    resourceUuid: string;
     isOpened: boolean;
 }
 
 const initialState = {
-    item: null,
+    resourceUuid: '',
     isOpened: false
 };
 
 export const detailsPanelReducer = (state: DetailsPanelState = initialState, action: DetailsPanelAction) =>
     detailsPanelActions.match(action, {
         default: () => state,
-        LOAD_DETAILS_SUCCESS: ({ item }) => ({ ...state, item }),
+        LOAD_DETAILS_PANEL: resourceUuid => ({ ...state, resourceUuid }),
         TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened })
     });
index df4418f41363b32ba92e75b12c5a07547ee9b06d..22629b692f2bff6dea8c7f78ee97ea18fe0308ca 100644 (file)
@@ -2,14 +2,11 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
 
 export const dialogActions = unionize({
     OPEN_DIALOG: ofType<{ id: string, data: any }>(),
     CLOSE_DIALOG: ofType<{ id: string }>()
-}, {
-        tag: 'type',
-        value: 'payload'
-    });
+});
 
 export type DialogAction = UnionOf<typeof dialogActions>;
index aa1ec8d03041668f4b1d1296685cba13b23f3349..067d5ceedb90bbbdc6f947d5b6b2afe5fe2fbf00 100644 (file)
@@ -6,3 +6,5 @@ import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
 
 export const FAVORITE_PANEL_ID = "favoritePanel";
 export const favoritePanelActions = bindDataExplorerActions(FAVORITE_PANEL_ID);
+
+export const loadFavoritePanel = () => favoritePanelActions.REQUEST_ITEMS();
index 1c2f062252b2101224d62b690d955849c327b698..e5857dd363e75433fb9bdb3822e738c39ea7fa48 100644 (file)
@@ -6,16 +6,18 @@ import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-mi
 import { FavoritePanelColumnNames, FavoritePanelFilter } from "~/views/favorite-panel/favorite-panel";
 import { RootState } from "../store";
 import { DataColumns } from "~/components/data-table/data-table";
-import { FavoritePanelItem, resourceToDataItem } from "~/views/favorite-panel/favorite-panel-item";
 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,53 +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<FavoritePanelItem, 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(favoritePanelActions.SET_ITEMS({
-                    items: response.items.map(resourceToDataItem),
-                    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..e5a8e591d20d1527b0137fffc3a4c35c8cd4b1ff 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
 import { Dispatch } from "redux";
 import { RootState } from "../store";
 import { checkFavorite } from "./favorites-reducer";
@@ -13,7 +13,7 @@ export const favoritesActions = unionize({
     TOGGLE_FAVORITE: ofType<{ resourceUuid: string }>(),
     CHECK_PRESENCE_IN_FAVORITES: ofType<string[]>(),
     UPDATE_FAVORITES: ofType<Record<string, boolean>>()
-}, { tag: 'type', value: 'payload' });
+});
 
 export type FavoritesAction = UnionOf<typeof favoritesActions>;
 
@@ -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 50ec93d25e8091e2186e0eccf7a6f403d0ca90ae..3256846be67ecc3c2975f60aa874543a60e15400 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 } from "~/models/resource";
-import { projectPanelActions } from "../project-panel/project-panel-action";
+import { ResourceKind, Resource, extractUuidKind } from '~/models/resource';
 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 { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
+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 { 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';
+import { setCollectionBreadcrumbs, setProjectBreadcrumbs, setSidePanelBreadcrumbs } from '../breadcrumbs/breadcrumbs-actions';
 
-export const getResourceUrl = (resourceKind: ResourceKind, resourceUuid: string): string => {
-    switch (resourceKind) {
-        case ResourceKind.PROJECT: return getProjectUrl(resourceUuid);
-        case ResourceKind.COLLECTION: return getCollectionUrl(resourceUuid);
-        default:
-            return '';
-    }
-};
+export const navigateTo = (uuid: string) =>
+    async (dispatch: Dispatch) => {
+        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);
+        }
+    };
 
-export enum ItemMode {
-    BOTH,
-    OPEN,
-    ACTIVE
-}
+export const loadWorkbench = () =>
+    async (dispatch: Dispatch, getState: () => RootState) => {
+        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));
+                    }
+                }
+            } else {
+                dispatch(userIsNotAuthenticated);
+            }
+        } else {
+            dispatch(userIsNotAuthenticated);
+        }
+    };
 
-export const setProjectItem = (itemId: string, itemMode: ItemMode) =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const { projects, router } = getState();
-        const treeItem = findTreeItem(projects.items, itemId);
+export const navigateToFavorites = push(Routes.FAVORITES);
 
-        if (treeItem) {
-            const resourceUrl = getResourceUrl(treeItem.data.kind, treeItem.data.uuid);
+export const loadFavorites = () =>
+    (dispatch: Dispatch) => {
+        dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.FAVORITES));
+        dispatch<any>(loadFavoritePanel());
+        dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
+    };
 
-            if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
-                if (router.location && !router.location.pathname.includes(resourceUrl)) {
-                    dispatch(push(resourceUrl));
-                }
-                dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(treeItem.data.uuid));
-            }
 
-            const promise = treeItem.status === TreeItemStatus.LOADED
-                ? Promise.resolve()
-                : dispatch<any>(getProjectList(itemId));
+export const navigateToProject = compose(push, getProjectUrl);
 
-            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());
-            }
-        }
+export const loadProject = (uuid: string) =>
+    async (dispatch: Dispatch) => {
+        await dispatch<any>(activateSidePanelTreeItem(uuid));
+        dispatch<any>(setProjectBreadcrumbs(uuid));
+        dispatch<any>(openProjectPanel(uuid));
+        dispatch(loadDetailsPanel(uuid));
     };
 
-export const restoreBranch = (itemId: string) =>
-    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));
-        });
+export const navigateToCollection = compose(push, getCollectionUrl);
+
+export const loadCollection = (uuid: string) =>
+    async (dispatch: Dispatch) => {
+        const collection = await dispatch<any>(loadCollectionPanel(uuid));
+        await dispatch<any>(activateSidePanelTreeItem(collection.ownerUuid));
+        dispatch<any>(setCollectionBreadcrumbs(collection.uuid));
+        dispatch(loadDetailsPanel(uuid));
     };
 
-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];
-    }
-};
+export const cannotNavigateToResource = ({ kind, uuid }: Resource) =>
+    snackbarActions.OPEN_SNACKBAR({
+        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.`
+    });
+
+export const userIsNotAuthenticated = snackbarActions.OPEN_SNACKBAR({
+    message: 'User is not authenticated'
+});
 
-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 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 663add3e2eb68c9549aac05f334cd301a156535c..da7f5b33e0d96e928f534f9491e501004fabd3f7 100644 (file)
 //
 // 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";
 import { ServiceRepository } from "~/services/services";
-import { ProjectPanelItem, resourceToDataItem } from "~/views/project-panel/project-panel-item";
 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 { 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<ProjectPanelItem, 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(projectPanelActions.SET_ITEMS({
-                    items: response.items.map(resourceToDataItem),
-                    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 ff66a9cf8ac035b9628bba060ac5cb30ef0878c1..4f03ae1c7dbe174713b29fc3dbed21b5582d27e8 100644 (file)
@@ -1,14 +1,15 @@
 // Copyright (C) The Arvados Authors. All rights reserved.
 //
 // SPDX-License-Identifier: AGPL-3.0
-import { default as unionize, ofType, UnionOf } from "unionize";
 
+import { unionize, ofType, UnionOf } from '~/common/unionize';
 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 { resourcesActions } from '~/store/resources/resources-actions';
 
 export const projectActions = unionize({
     REMOVE_PROJECT: ofType<string>(),
@@ -17,9 +18,6 @@ export const projectActions = unionize({
     TOGGLE_PROJECT_TREE_ITEM_OPEN: ofType<string>(),
     TOGGLE_PROJECT_TREE_ITEM_ACTIVE: ofType<string>(),
     RESET_PROJECT_TREE_ACTIVITY: ofType<string>()
-}, {
-    tag: 'type',
-    value: 'payload'
 });
 
 export const getProjectList = (parentUuid: string = '') => 
@@ -31,7 +29,8 @@ 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)));
+            dispatch<any>(resourcesActions.SET_RESOURCES(projects));
             return projects;
         });
     };
diff --git a/src/store/properties/properties-actions.ts b/src/store/properties/properties-actions.ts
new file mode 100644 (file)
index 0000000..8647a9c
--- /dev/null
@@ -0,0 +1,12 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from '~/common/unionize';
+
+export const propertiesActions = unionize({
+    SET_PROPERTY: ofType<{ key: string, value: any }>(),
+    DELETE_PROPERTY: ofType<string>(),
+});
+
+export type PropertiesAction = UnionOf<typeof propertiesActions>;
diff --git a/src/store/properties/properties-reducer.ts b/src/store/properties/properties-reducer.ts
new file mode 100644 (file)
index 0000000..27fdf85
--- /dev/null
@@ -0,0 +1,14 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { PropertiesState, setProperty, deleteProperty } from './properties';
+import { PropertiesAction, propertiesActions } from './properties-actions';
+
+
+export const propertiesReducer = (state: PropertiesState = {}, action: PropertiesAction) =>
+    propertiesActions.match(action, {
+        SET_PROPERTY: ({ key, value }) => setProperty(key, value)(state),
+        DELETE_PROPERTY: key => deleteProperty(key)(state),
+        default: () => state,
+    });
\ No newline at end of file
diff --git a/src/store/properties/properties.ts b/src/store/properties/properties.ts
new file mode 100644 (file)
index 0000000..bee5b19
--- /dev/null
@@ -0,0 +1,23 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export type PropertiesState = { [key: string]: any };
+
+export const getProperty = <T>(id: string) =>
+    (state: PropertiesState): T | undefined =>
+        state[id];
+
+export const setProperty = <T>(id: string, data: T) =>
+    (state: PropertiesState) => ({
+        ...state,
+        [id]: data
+    });
+
+export const deleteProperty = (id: string) =>
+    (state: PropertiesState) => {
+        const newState = { ...state };
+        delete newState[id];
+        return newState;
+    };
+
diff --git a/src/store/resources/resources-actions.ts b/src/store/resources/resources-actions.ts
new file mode 100644 (file)
index 0000000..0034e7a
--- /dev/null
@@ -0,0 +1,31 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from '~/common/unionize';
+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>;
+
+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/resources/resources-reducer.ts b/src/store/resources/resources-reducer.ts
new file mode 100644 (file)
index 0000000..22108e0
--- /dev/null
@@ -0,0 +1,13 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ResourcesState, setResource, deleteResource } from './resources';
+import { ResourcesAction, resourcesActions } from './resources-actions';
+
+export const resourcesReducer = (state: ResourcesState = {}, action: ResourcesAction) =>
+    resourcesActions.match(action, {
+        SET_RESOURCES: resources => resources.reduce((state, resource) => setResource(resource.uuid, resource)(state), state),
+        DELETE_RESOURCES: ids => ids.reduce((state, id) => deleteResource(id)(state), state),
+        default: () => state,
+    });
\ No newline at end of file
diff --git a/src/store/resources/resources.ts b/src/store/resources/resources.ts
new file mode 100644 (file)
index 0000000..add4efe
--- /dev/null
@@ -0,0 +1,37 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Resource } from "~/models/resource";
+import { ResourceKind } from '../../models/resource';
+
+export type ResourcesState = { [key: string]: Resource };
+
+export const getResource = <T extends Resource = Resource>(id: string) =>
+    (state: ResourcesState): T | undefined =>
+        state[id] as T;
+
+export const setResource = <T extends Resource>(id: string, data: T) =>
+    (state: ResourcesState) => ({
+        ...state,
+        [id]: data
+    });
+
+export const deleteResource = (id: string) =>
+    (state: ResourcesState) => {
+        const newState = {...state};
+        delete newState[id];
+        return newState;
+    };
+
+export const filterResources = (filter: (resource: Resource) => boolean) =>
+    (state: ResourcesState) =>
+        Object
+            .keys(state)
+            .map(id => getResource(id)(state))
+            .filter(filter);
+
+export const filterResourcesByKind = (kind: ResourceKind) =>
+    (state: ResourcesState) =>
+        filterResources(resource => resource.kind === kind)(state);
+
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..574bda9
--- /dev/null
@@ -0,0 +1,158 @@
+// 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, 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 { getTreePicker, TreePicker } from '../tree-picker/tree-picker';
+import { TreeItemStatus } from "~/components/tree/tree";
+import { getNodeAncestors, getNodeValue, getNodeAncestorsIds } from '~/models/tree';
+import { ProjectResource } from '~/models/project';
+
+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';
+
+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,
+    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().treePicker);
+        if (node && !node.selected) {
+            dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId: SIDE_PANEL_TREE }));
+        }
+        if (!isSidePanelTreeCategory(nodeId)) {
+            await dispatch<any>(activateSidePanelTreeProject(nodeId));
+        }
+    };
+
+export const activateSidePanelTreeProject = (nodeId: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { treePicker } = getState();
+        const node = getSidePanelTreeNode(nodeId)(treePicker);
+        if (node && node.status !== TreeItemStatus.LOADED) {
+            await dispatch<any>(loadSidePanelTreeProjects(nodeId));
+        } else if (node === undefined) {
+            await dispatch<any>(activateSidePanelTreeBranch(nodeId));
+        }
+        dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({
+            nodeIds: getSidePanelTreeNodeAncestorsIds(nodeId)(treePicker),
+            pickerId: SIDE_PANEL_TREE
+        }));
+        dispatch<any>(expandSidePanelTreeItem(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));
+        }
+        dispatch(treePickerActions.EXPAND_TREE_PICKER_NODES({
+            nodeIds: ancestors.map(ancestor => ancestor.uuid),
+            pickerId: SIDE_PANEL_TREE
+        }));
+        dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId, pickerId: SIDE_PANEL_TREE }));
+    };
+
+export const toggleSidePanelTreeItemCollapse = (nodeId: string) =>
+    async (dispatch: Dispatch, getState: () => RootState) => {
+        const node = getSidePanelTreeNode(nodeId)(getState().treePicker);
+        if (node && node.status === TreeItemStatus.INITIAL) {
+            await dispatch<any>(loadSidePanelTreeProjects(node.nodeId));
+        }
+        dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId: SIDE_PANEL_TREE }));
+    };
+
+export const expandSidePanelTreeItem = (nodeId: string) =>
+    async (dispatch: Dispatch, getState: () => RootState) => {
+        const node = getSidePanelTreeNode(nodeId)(getState().treePicker);
+        if (node && node.collapsed) {
+            dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ nodeId, pickerId: SIDE_PANEL_TREE }));
+        }
+    };
+
+const getSidePanelTreeNode = (nodeId: string) => (treePicker: TreePicker) => {
+    const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(treePicker);
+    return sidePanelTree
+        ? getNodeValue(nodeId)(sidePanelTree)
+        : undefined;
+};
+
+const getSidePanelTreeNodeAncestorsIds = (nodeId: string) => (treePicker: TreePicker) => {
+    const sidePanelTree = getTreePicker(SIDE_PANEL_TREE)(treePicker);
+    return sidePanelTree
+        ? getNodeAncestorsIds(nodeId)(sidePanelTree)
+        : [];
+};
index ecea3535e35040fdbb8095830ea60e21bacff95e..8c7ef4a7a1ff7ff6d8d3f1023e8d7d93cefd6f08 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, navigateTo } 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>(navigateTo(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 2f6175ad68ac9040028ccc77ed28c918426d7cc7..55d9f3a8651b86afecc516aa48dc1af0abc412e1 100644 (file)
@@ -2,11 +2,11 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
 
 export const snackbarActions = unionize({
     OPEN_SNACKBAR: ofType<{message: string; hideDuration?: number}>(),
     CLOSE_SNACKBAR: ofType<{}>()
-}, { tag: 'type', value: 'payload' });
+});
 
 export type SnackbarAction = UnionOf<typeof snackbarActions>;
index 403c19f0071d4eac0949becc10fbb389082c40bb..fc2f4a1964e27627ff5d02e63150713bbe78eb3b 100644 (file)
@@ -20,7 +20,7 @@ const initialState: SnackbarState = {
 
 export const snackbarReducer = (state = initialState, action: SnackbarAction) => {
     return snackbarActions.match(action, {
-        OPEN_SNACKBAR: data => ({ ...data, open: true }),
+        OPEN_SNACKBAR: data => ({ ...initialState, ...data, open: true }),
         CLOSE_SNACKBAR: () => initialState,
         default: () => state,
     });
index a4bf9d6e3b9692d00e92bfcdc59ec93fa66c89bb..0e548c587dbcd90a7119d51f65f6c32514513b3d 100644 (file)
@@ -3,75 +3,44 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { createStore, applyMiddleware, compose, Middleware, combineReducers, Store, Action, Dispatch } from 'redux';
-import { routerMiddleware, routerReducer, RouterState } from "react-router-redux";
+import { routerMiddleware, routerReducer } from "react-router-redux";
 import thunkMiddleware from 'redux-thunk';
 import { History } from "history";
 
-import { projectsReducer, ProjectState } from "./project/project-reducer";
-import { sidePanelReducer, SidePanelState } from './side-panel/side-panel-reducer';
-import { authReducer, AuthState } from "./auth/auth-reducer";
-import { dataExplorerReducer, DataExplorerState } from './data-explorer/data-explorer-reducer';
-import { detailsPanelReducer, DetailsPanelState } from './details-panel/details-panel-reducer';
-import { contextMenuReducer, ContextMenuState } from './context-menu/context-menu-reducer';
+import { projectsReducer } from "./project/project-reducer";
+import { authReducer } from "./auth/auth-reducer";
+import { dataExplorerReducer } from './data-explorer/data-explorer-reducer';
+import { detailsPanelReducer } from './details-panel/details-panel-reducer';
+import { contextMenuReducer } from './context-menu/context-menu-reducer';
 import { reducer as formReducer } from 'redux-form';
-import { FavoritesState, favoritesReducer } from './favorites/favorites-reducer';
-import { snackbarReducer, SnackbarState } from './snackbar/snackbar-reducer';
-import { CollectionPanelFilesState } from './collection-panel/collection-panel-files/collection-panel-files-state';
+import { favoritesReducer } from './favorites/favorites-reducer';
+import { snackbarReducer } from './snackbar/snackbar-reducer';
 import { collectionPanelFilesReducer } from './collection-panel/collection-panel-files/collection-panel-files-reducer';
 import { dataExplorerMiddleware } from "./data-explorer/data-explorer-middleware";
 import { FAVORITE_PANEL_ID } from "./favorite-panel/favorite-panel-action";
 import { PROJECT_PANEL_ID } from "./project-panel/project-panel-action";
 import { ProjectPanelMiddlewareService } from "./project-panel/project-panel-middleware-service";
 import { FavoritePanelMiddlewareService } from "./favorite-panel/favorite-panel-middleware-service";
-import { CollectionPanelState, collectionPanelReducer } from './collection-panel/collection-panel-reducer';
-import { DialogState, dialogReducer } from './dialog/dialog-reducer';
-import { CollectionsState, collectionsReducer } from './collections/collections-reducer';
+import { collectionPanelReducer } from './collection-panel/collection-panel-reducer';
+import { dialogReducer } from './dialog/dialog-reducer';
+import { collectionsReducer } from './collections/collections-reducer';
 import { ServiceRepository } from "~/services/services";
 import { treePickerReducer } from './tree-picker/tree-picker-reducer';
-import { TreePicker } from './tree-picker/tree-picker';
+import { resourcesReducer } from '~/store/resources/resources-reducer';
+import { propertiesReducer } from './properties/properties-reducer';
+import { RootState } from './store';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
         window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
     compose;
 
-export interface RootState {
-    auth: AuthState;
-    projects: ProjectState;
-    collections: CollectionsState;
-    router: RouterState;
-    dataExplorer: DataExplorerState;
-    sidePanel: SidePanelState;
-    collectionPanel: CollectionPanelState;
-    detailsPanel: DetailsPanelState;
-    contextMenu: ContextMenuState;
-    favorites: FavoritesState;
-    snackbar: SnackbarState;
-    collectionPanelFiles: CollectionPanelFilesState;
-    dialog: DialogState;
-    treePicker: TreePicker;
-}
+export type RootState = ReturnType<ReturnType<typeof createRootReducer>>;
 
 export type RootStore = Store<RootState, Action> & { dispatch: Dispatch<any> };
 
 export function configureStore(history: History, services: ServiceRepository): RootStore {
-    const rootReducer = combineReducers({
-        auth: authReducer(services),
-        projects: projectsReducer,
-        collections: collectionsReducer,
-        router: routerReducer,
-        dataExplorer: dataExplorerReducer,
-        sidePanel: sidePanelReducer,
-        collectionPanel: collectionPanelReducer,
-        detailsPanel: detailsPanelReducer,
-        contextMenu: contextMenuReducer,
-        form: formReducer,
-        favorites: favoritesReducer,
-        snackbar: snackbarReducer,
-        collectionPanelFiles: collectionPanelFilesReducer,
-        dialog: dialogReducer,
-        treePicker: treePickerReducer,
-    });
+    const rootReducer = createRootReducer(services);
 
     const projectPanelMiddleware = dataExplorerMiddleware(
         new ProjectPanelMiddlewareService(services, PROJECT_PANEL_ID)
@@ -89,3 +58,22 @@ export function configureStore(history: History, services: ServiceRepository): R
     const enhancer = composeEnhancers(applyMiddleware(...middlewares));
     return createStore(rootReducer, enhancer);
 }
+
+const createRootReducer = (services: ServiceRepository) => combineReducers({
+    auth: authReducer(services),
+    projects: projectsReducer,
+    collections: collectionsReducer,
+    router: routerReducer,
+    dataExplorer: dataExplorerReducer,
+    collectionPanel: collectionPanelReducer,
+    detailsPanel: detailsPanelReducer,
+    contextMenu: contextMenuReducer,
+    form: formReducer,
+    favorites: favoritesReducer,
+    snackbar: snackbarReducer,
+    collectionPanelFiles: collectionPanelFilesReducer,
+    dialog: dialogReducer,
+    treePicker: treePickerReducer,
+    resources: resourcesReducer,
+    properties: propertiesReducer,
+});
index 34f1303717e0097a723c4851af8e94481f4f213d..5b04389af6850888a3bad224dc11fc507005d5e3 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { default as unionize, ofType, UnionOf } from "unionize";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
 
 import { TreePickerNode } from "./tree-picker";
 
@@ -11,10 +11,8 @@ export const treePickerActions = unionize({
     LOAD_TREE_PICKER_NODE_SUCCESS: ofType<{ nodeId: string, nodes: Array<TreePickerNode>, pickerId: string }>(),
     TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ nodeId: string, pickerId: string }>(),
     TOGGLE_TREE_PICKER_NODE_SELECT: ofType<{ nodeId: string, pickerId: string }>(),
+    EXPAND_TREE_PICKER_NODES: ofType<{ nodeIds: string[], pickerId: string }>(),
     RESET_TREE_PICKER: ofType<{ pickerId: string }>()
-}, {
-        tag: 'type',
-        value: 'payload'
-    });
+});
 
 export type TreePickerAction = UnionOf<typeof treePickerActions>;
index e7173d22ac73832403cb486c345f41b6f106946b..8eaada6fc3e092e477c042db44c36ade006df9f5 100644 (file)
@@ -18,8 +18,10 @@ export const treePickerReducer = (state: TreePicker = {}, action: TreePickerActi
             updateOrCreatePicker(state, pickerId, setNodeValueWith(toggleCollapse)(nodeId)),
         TOGGLE_TREE_PICKER_NODE_SELECT: ({ nodeId, pickerId }) =>
             updateOrCreatePicker(state, pickerId, mapTreeValues(toggleSelect(nodeId))),
-        RESET_TREE_PICKER: ({ pickerId }) => 
+        RESET_TREE_PICKER: ({ pickerId }) =>
             updateOrCreatePicker(state, pickerId, createTree),
+        EXPAND_TREE_PICKER_NODES: ({ pickerId, nodeIds }) =>
+            updateOrCreatePicker(state, pickerId, mapTreeValues(expand(nodeIds))),
         default: () => state
     });
 
@@ -29,6 +31,11 @@ const updateOrCreatePicker = (state: TreePicker, pickerId: string, func: (value:
     return { ...state, [pickerId]: updatedPicker };
 };
 
+const expand = (ids: string[]) => (node: TreePickerNode): TreePickerNode =>
+    ids.some(id => id === node.nodeId)
+        ? { ...node, collapsed: false }
+        : node;
+
 const setPending = (value: TreePickerNode): TreePickerNode =>
     ({ ...value, status: TreeItemStatus.PENDING });
 
index c815ad4f900468ed74f2bd0b33d062e5e377f219..259a4b8d53de78e1b7d9992ae85b4d69d5fe40ca 100644 (file)
@@ -7,9 +7,9 @@ import { TreeItemStatus } from "~/components/tree/tree";
 
 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;
@@ -21,3 +21,5 @@ export const createTreePickerNode = (data: { nodeId: string, value: any }) => ({
     collapsed: true,
     status: TreeItemStatus.INITIAL
 });
+
+export const getTreePicker = <Value = {}>(id: string) => (state: TreePicker): Tree<TreePickerNode<Value>> | 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() {
diff --git a/src/views-components/breadcrumbs/breadcrumbs.ts b/src/views-components/breadcrumbs/breadcrumbs.ts
new file mode 100644 (file)
index 0000000..c2f3389
--- /dev/null
@@ -0,0 +1,30 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { Breadcrumbs as BreadcrumbsComponent, BreadcrumbsProps } from '~/components/breadcrumbs/breadcrumbs';
+import { RootState } from '~/store/store';
+import { Dispatch } from 'redux';
+import { navigateTo } from '~/store/navigation/navigation-action';
+import { getProperty } from '../../store/properties/properties';
+import { ResourceBreadcrumb, BREADCRUMBS } from '../../store/breadcrumbs/breadcrumbs-actions';
+import { openSidePanelContextMenu } from '~/store/context-menu/context-menu-actions';
+
+type BreadcrumbsDataProps = Pick<BreadcrumbsProps, 'items'>;
+type BreadcrumbsActionProps = Pick<BreadcrumbsProps, 'onClick' | 'onContextMenu'>;
+
+const mapStateToProps = () => ({ properties }: RootState): BreadcrumbsDataProps => ({
+    items: getProperty<ResourceBreadcrumb[]>(BREADCRUMBS)(properties) || []
+});
+
+const mapDispatchToProps = (dispatch: Dispatch): BreadcrumbsActionProps => ({
+    onClick: ({ uuid }: ResourceBreadcrumb) => {
+        dispatch<any>(navigateTo(uuid));
+    },
+    onContextMenu: (event, breadcrumb: ResourceBreadcrumb) => {
+        dispatch<any>(openSidePanelContextMenu(event, breadcrumb.uuid));
+    }
+});
+
+export const Breadcrumbs = connect(mapStateToProps(), mapDispatchToProps)(BreadcrumbsComponent);
\ No newline at end of file
index bc5d739ab342a5e501f54fa41e97d32889b04c9d..af10aedf804201b2a1c8bb662e1dcf4d15313936 100644 (file)
@@ -2,15 +2,13 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { reset, initialize } from "redux-form";
-
 import { ContextMenuActionSet } from "../context-menu-action-set";
 import { NewProjectIcon, RenameIcon, CopyIcon, MoveToIcon } from "~/components/icon/icon";
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "~/store/favorites/favorites-actions";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
 import { openMoveProjectDialog } from '~/store/projects/project-move-actions';
-import { PROJECT_CREATE_FORM_NAME, openProjectCreateDialog } from '~/store/projects/project-create-actions';
+import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
 import { openProjectUpdateDialog } from '~/store/projects/project-update-actions';
 
 export const projectActionSet: ContextMenuActionSet = [[
@@ -18,7 +16,6 @@ export const projectActionSet: ContextMenuActionSet = [[
         icon: NewProjectIcon,
         name: "New project",
         execute: (dispatch, resource) => {
-            dispatch(reset(PROJECT_CREATE_FORM_NAME));
             dispatch<any>(openProjectCreateDialog(resource.uuid));
         }
     },
index 2c71abc41ed6b88cd0fa5094ccf807e38e148c13..386c5162f3c6ab9858c55f9b25468acbd98b353c 100644 (file)
@@ -2,20 +2,16 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { reset } from "redux-form";
-
 import { ContextMenuActionSet } from "../context-menu-action-set";
-import { projectActions } from "~/store/project/project-action";
-import { COLLECTION_CREATE_FORM_NAME, openCollectionCreateDialog } from '~/store/collections/collection-create-actions';
+import { openCollectionCreateDialog } from '~/store/collections/collection-create-actions';
 import { NewProjectIcon, CollectionIcon } from "~/components/icon/icon";
-import { PROJECT_CREATE_FORM_NAME, openProjectCreateDialog } from '~/store/projects/project-create-actions';
+import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
 
 export const rootProjectActionSet: ContextMenuActionSet =  [[
     {
         icon: NewProjectIcon,
         name: "New project",
         execute: (dispatch, resource) => {
-            dispatch(reset(PROJECT_CREATE_FORM_NAME));
             dispatch<any>(openProjectCreateDialog(resource.uuid));
         }
     },
@@ -23,7 +19,6 @@ export const rootProjectActionSet: ContextMenuActionSet =  [[
         icon: CollectionIcon,
         name: "New Collection",
         execute: (dispatch, resource) => {
-            dispatch(reset(COLLECTION_CREATE_FORM_NAME));
             dispatch<any>(openCollectionCreateDialog(resource.uuid));
         }
     }
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 }));
         },
index 1b07642ab727da755b898553ebc29a768f6bc025..abf1839286997117c0ecc56d2a5e87f917456e23 100644 (file)
@@ -9,9 +9,14 @@ import { ResourceKind } from '~/models/resource';
 import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '~/components/icon/icon';
 import { formatDate, formatFileSize } from '~/common/formatters';
 import { resourceLabel } from '~/common/labels';
+import { connect } from 'react-redux';
+import { RootState } from '~/store/store';
+import { getResource } from '../../store/resources/resources';
+import { GroupContentsResource } from '~/services/groups-service/groups-service';
+import { ProcessResource } from '~/models/process';
 
 
-export const renderName = (item: {name: string; uuid: string, kind: string}) =>
+export const renderName = (item: { name: string; uuid: string, kind: string }) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
         <Grid item>
             {renderIcon(item)}
@@ -28,8 +33,13 @@ export const renderName = (item: {name: string; uuid: string, kind: string}) =>
         </Grid>
     </Grid>;
 
+export const ResourceName = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+        return resource || { name: '', uuid: '', kind: '' };
+    })(renderName);
 
-export const renderIcon = (item: {kind: string}) => {
+export const renderIcon = (item: { kind: string }) => {
     switch (item.kind) {
         case ResourceKind.PROJECT:
             return <ProjectIcon />;
@@ -46,22 +56,52 @@ export const renderDate = (date: string) => {
     return <Typography noWrap>{formatDate(date)}</Typography>;
 };
 
+export const ResourceLastModifiedDate = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+        return { date: resource ? resource.modifiedAt : '' };
+    })((props: { date: string }) => renderDate(props.date));
+
 export const renderFileSize = (fileSize?: number) =>
     <Typography noWrap>
         {formatFileSize(fileSize)}
     </Typography>;
 
+export const ResourceFileSize = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+        return {};
+    })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
+
 export const renderOwner = (owner: string) =>
     <Typography noWrap color="primary" >
         {owner}
     </Typography>;
 
+export const ResourceOwner = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+        return { owner: resource ? resource.ownerUuid : '' };
+    })((props: { owner: string }) => renderOwner(props.owner));
+
 export const renderType = (type: string) =>
     <Typography noWrap>
         {resourceLabel(type)}
     </Typography>;
 
-export const renderStatus = (item: {status?: string}) =>
+export const ResourceType = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+        return { type: resource ? resource.kind : '' };
+    })((props: { type: string }) => renderType(props.type));
+
+export const renderStatus = (item: { status?: string }) =>
     <Typography noWrap align="center" >
         {item.status || "-"}
     </Typography>;
+
+export const ProcessStatus = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource(props.uuid)(state.resources) as ProcessResource | undefined;
+        return { status: resource ? resource.state : '-' };
+    })((props: { status: string }) => renderType(props.status));
index 21d57aea4831a306a4333c1135fd71b39d7db3a4..7aae7860ac39999df6262fdb886198f49400a4e2 100644 (file)
@@ -20,6 +20,7 @@ import { ProcessDetails } from "./process-details";
 import { EmptyDetails } from "./empty-details";
 import { DetailsData } from "./details-data";
 import { DetailsResource } from "~/models/details";
+import { getResource } from '../../store/resources/resources';
 
 type CssRules = 'drawerPaper' | 'container' | 'opened' | 'headerContainer' | 'headerIcon' | 'headerTitle' | 'tabContainer';
 
@@ -70,10 +71,13 @@ const getItem = (resource: DetailsResource): DetailsData => {
     }
 };
 
-const mapStateToProps = ({ detailsPanel }: RootState) => ({
-    isOpened: detailsPanel.isOpened,
-    item: getItem(detailsPanel.item as DetailsResource)
-});
+const mapStateToProps = ({ detailsPanel, resources }: RootState) => {
+    const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource;
+    return {
+        isOpened: detailsPanel.isOpened,
+        item: getItem(resource)
+    };
+};
 
 const mapDispatchToProps = (dispatch: Dispatch) => ({
     onCloseDrawer: () => {
@@ -110,7 +114,7 @@ export const DetailsPanel = withStyles(styles)(
                 const { tabsValue } = this.state;
                 return (
                     <Typography component="div"
-                                className={classnames([classes.container, { [classes.opened]: isOpened }])}>
+                        className={classnames([classes.container, { [classes.opened]: isOpened }])}>
                         <Drawer variant="permanent" anchor="right" classes={{ paper: classes.drawerPaper }}>
                             <Typography component="div" className={classes.headerContainer}>
                                 <Grid container alignItems='center' justify='space-around'>
@@ -124,14 +128,14 @@ export const DetailsPanel = withStyles(styles)(
                                     </Grid>
                                     <Grid item>
                                         <IconButton color="inherit" onClick={onCloseDrawer}>
-                                            {<CloseIcon/>}
+                                            {<CloseIcon />}
                                         </IconButton>
                                     </Grid>
                                 </Grid>
                             </Typography>
                             <Tabs value={tabsValue} onChange={this.handleChange}>
-                                <Tab disableRipple label="Details"/>
-                                <Tab disableRipple label="Activity" disabled/>
+                                <Tab disableRipple label="Details" />
+                                <Tab disableRipple label="Activity" disabled />
                             </Tabs>
                             {tabsValue === 0 && this.renderTabContainer(
                                 <Grid container direction="column">
@@ -139,7 +143,7 @@ export const DetailsPanel = withStyles(styles)(
                                 </Grid>
                             )}
                             {tabsValue === 1 && this.renderTabContainer(
-                                <Grid container direction="column"/>
+                                <Grid container direction="column" />
                             )}
                         </Drawer>
                     </Typography>
index 54d6a5da0ec8c5306734ba27861a5288fd21fd89..73a8608c8afe7f5a8dc0b97955a3059e176a1a27 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;
@@ -31,9 +31,7 @@ interface MainAppBarDataProps {
 
 export interface MainAppBarActionProps {
     onSearch: (searchText: string) => void;
-    onBreadcrumbClick: (breadcrumb: Breadcrumb) => void;
     onMenuItemClick: (menuItem: MainAppBarMenuItem) => void;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: Breadcrumb) => void;
     onDetailsPanelToggle: () => void;
 }
 
@@ -68,15 +66,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 92c54515b5355152fd2eb634a225309c18ce8388..3859180f0e2d81f0308070d399a1b836d5fa277a 100644 (file)
@@ -18,9 +18,10 @@ import { ServiceRepository } from "~/services/services";
 import { FilterBuilder } from "~/common/api/filter-builder";
 import { WrappedFieldProps } from 'redux-form';
 
-type ProjectTreePickerProps = Pick<TreePickerProps, 'toggleItemActive' | 'toggleItemOpen'>;
+type ProjectTreePickerProps = Pick<TreePickerProps, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen'>;
 
 const mapDispatchToProps = (dispatch: Dispatch, props: { onChange: (projectUuid: string) => void }): ProjectTreePickerProps => ({
+    onContextMenu: () => { return; },
     toggleItemActive: (nodeId, status, pickerId) => {
         getNotSelectedTreePickerKind(pickerId)
             .forEach(pickerId => dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECT({ nodeId: '', pickerId })));
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..d0b00d6
--- /dev/null
@@ -0,0 +1,69 @@
+// 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';
+import { openSidePanelContextMenu } from '~/store/context-menu/context-menu-actions';
+
+export interface SidePanelTreeProps {
+    onItemActivation: (id: string) => void;
+}
+
+type SidePanelTreeActionProps = Pick<TreePickerProps, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen'>;
+
+const mapDispatchToProps = (dispatch: Dispatch, props: SidePanelTreeProps): SidePanelTreeActionProps => ({
+    onContextMenu: (event, id) => {
+        dispatch<any>(openSidePanelContextMenu(event, id));
+    },
+    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 b90f2e420656dd1365b37869c65779830726a589..8b7630ab8cc62609ca9a3898653f1956bd821553 100644 (file)
@@ -11,25 +11,34 @@ import { Dispatch } from "redux";
 
 export interface TreePickerProps {
     pickerId: string;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, nodeId: string, pickerId: string) => void;
     toggleItemOpen: (nodeId: string, status: TreeItemStatus, pickerId: string) => void;
     toggleItemActive: (nodeId: string, status: TreeItemStatus, pickerId: string) => void;
 }
 
-const mapStateToProps = (state: RootState, props: TreePickerProps): Pick<TreeProps<any>, 'items'> => {
-    const tree = state.treePicker[props.pickerId] || createTree();
-    return {
-        items: getNodeChildrenIds('')(tree)
-            .map(treePickerToTreeItems(tree))
+const memoizedMapStateToProps = () => {
+    let prevTree: Ttree<TreePickerNode>;
+    let mappedProps: Pick<TreeProps<any>, 'items'>;
+    return (state: RootState, props: TreePickerProps): Pick<TreeProps<any>, 'items'> => {
+        const tree = state.treePicker[props.pickerId] || createTree();
+        if(tree !== prevTree){
+            prevTree = tree;
+            mappedProps = {
+                items: getNodeChildrenIds('')(tree)
+                    .map(treePickerToTreeItems(tree))
+            };
+        }
+        return mappedProps;
     };
 };
 
 const mapDispatchToProps = (dispatch: Dispatch, props: TreePickerProps): Pick<TreeProps<any>, 'onContextMenu' | 'toggleItemOpen' | 'toggleItemActive'> => ({
-    onContextMenu: () => { return; },
+    onContextMenu: (event, item) => props.onContextMenu(event, item.id, props.pickerId),
     toggleItemActive: (id, status) => props.toggleItemActive(id, status, props.pickerId),
     toggleItemOpen: (id, status) => props.toggleItemOpen(id, status, props.pickerId)
 });
 
-export const TreePicker = connect(mapStateToProps, mapDispatchToProps)(Tree);
+export const TreePicker = connect(memoizedMapStateToProps(), mapDispatchToProps)(Tree);
 
 const treePickerToTreeItems = (tree: Ttree<TreePickerNode>) =>
     (id: string): TreeItem<any> => {
index 559d4a9a86072ee3bc0f0750b94166bf72017d43..d22dd0de289122cdf088504a614f1042c6254161 100644 (file)
@@ -20,6 +20,9 @@ import { TagResource } from '~/models/tag';
 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 { contextMenuActions } from '~/store/context-menu/context-menu-actions';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
 
 type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'label' | 'value';
 
@@ -55,81 +58,96 @@ interface CollectionPanelDataProps {
     tags: TagResource[];
 }
 
-interface CollectionPanelActionProps {
-    onItemRouteChange: (collectionId: string) => void;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: CollectionResource) => void;
-}
-
-type CollectionPanelProps = CollectionPanelDataProps & CollectionPanelActionProps & DispatchProp
-                            & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+type CollectionPanelProps = CollectionPanelDataProps & DispatchProp
+    & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
 
 
 export const CollectionPanel = withStyles(styles)(
-    connect((state: RootState) => ({
-        item: state.collectionPanel.item,
-        tags: state.collectionPanel.tags
-    }))(
+    connect((state: RootState, props: RouteComponentProps<{ id: string }>) => {
+        const collection = getResource(props.match.params.id)(state.resources);
+        return {
+            item: collection,
+            tags: state.collectionPanel.tags
+        };
+    })(
         class extends React.Component<CollectionPanelProps> {
 
             render() {
-                const { classes, item, tags, onContextMenu } = this.props;
+                const { classes, item, tags } = this.props;
                 return <div>
-                        <Card className={classes.card}>
-                            <CardHeader
-                                avatar={ <CollectionIcon className={classes.iconHeader} /> }
-                                action={
-                                    <IconButton
-                                        aria-label="More options"
-                                        onClick={event => onContextMenu(event, item)}>
-                                        <MoreOptionsIcon />
-                                    </IconButton>
-                                }
-                                title={item && item.name }
-                                subheader={item && item.description} />
-                            <CardContent>
-                                <Grid container direction="column">
-                                    <Grid item xs={6}>
-                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value}
-                                                label='Collection UUID'
-                                                value={item && item.uuid}>
-                                            <Tooltip title="Copy uuid">
-                                                <CopyToClipboard text={item && item.uuid} onCopy={() => this.onCopy() }>
-                                                    <CopyIcon className={classes.copyIcon} />
-                                                </CopyToClipboard>
-                                            </Tooltip>
-                                        </DetailsAttribute>
-                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value} 
-                                            label='Number of files' value='14' />
-                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value} 
-                                            label='Content size' value='54 MB' />
-                                        <DetailsAttribute classLabel={classes.label} classValue={classes.value} 
-                                            label='Owner' value={item && item.ownerUuid} />
-                                    </Grid>
+                    <Card className={classes.card}>
+                        <CardHeader
+                            avatar={<CollectionIcon className={classes.iconHeader} />}
+                            action={
+                                <IconButton
+                                    aria-label="More options"
+                                    onClick={this.handleContextMenu}>
+                                    <MoreOptionsIcon />
+                                </IconButton>
+                            }
+                            title={item && item.name}
+                            subheader={item && item.description} />
+                        <CardContent>
+                            <Grid container direction="column">
+                                <Grid item xs={6}>
+                                    <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                                        label='Collection UUID'
+                                        value={item && item.uuid}>
+                                        <Tooltip title="Copy uuid">
+                                            <CopyToClipboard text={item && item.uuid} onCopy={() => this.onCopy()}>
+                                                <CopyIcon className={classes.copyIcon} />
+                                            </CopyToClipboard>
+                                        </Tooltip>
+                                    </DetailsAttribute>
+                                    <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                                        label='Number of files' value='14' />
+                                    <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                                        label='Content size' value='54 MB' />
+                                    <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+                                        label='Owner' value={item && item.ownerUuid} />
                                 </Grid>
-                            </CardContent>
-                        </Card>
+                            </Grid>
+                        </CardContent>
+                    </Card>
 
-                        <Card className={classes.card}>
-                            <CardHeader title="Properties" />
-                            <CardContent>
-                                <Grid container direction="column">
-                                    <Grid item xs={12}><CollectionTagForm /></Grid>
-                                    <Grid item xs={12}>
-                                        {
-                                            tags.map(tag => {
-                                                return <Chip key={tag.etag} className={classes.tag}
-                                                    onDelete={this.handleDelete(tag.uuid)}
-                                                    label={renderTagLabel(tag)}  />;
-                                            })
-                                        }
-                                    </Grid>
+                    <Card className={classes.card}>
+                        <CardHeader title="Properties" />
+                        <CardContent>
+                            <Grid container direction="column">
+                                <Grid item xs={12}><CollectionTagForm /></Grid>
+                                <Grid item xs={12}>
+                                    {
+                                        tags.map(tag => {
+                                            return <Chip key={tag.etag} className={classes.tag}
+                                                onDelete={this.handleDelete(tag.uuid)}
+                                                label={renderTagLabel(tag)} />;
+                                        })
+                                    }
                                 </Grid>
-                            </CardContent>
-                        </Card>
-                        <div className={classes.card}>
-                            <CollectionPanelFiles/>
-                        </div>
-                    </div>;
+                            </Grid>
+                        </CardContent>
+                    </Card>
+                    <div className={classes.card}>
+                        <CollectionPanelFiles />
+                    </div>
+                </div>;
+            }
+
+            handleContextMenu = (event: React.MouseEvent<any>) => {
+                event.preventDefault();
+                const { uuid, name, description } = this.props.item;
+                const resource = {
+                    uuid,
+                    name,
+                    description,
+                    kind: ContextMenuKind.COLLECTION
+                };
+                this.props.dispatch(
+                    contextMenuActions.OPEN_CONTEXT_MENU({
+                        position: { x: event.clientX, y: event.clientY },
+                        resource
+                    })
+                );
             }
 
             handleDelete = (uuid: string) => () => {
@@ -143,12 +161,6 @@ export const CollectionPanel = withStyles(styles)(
                 }));
             }
 
-            componentWillReceiveProps({ match, item, onItemRouteChange }: CollectionPanelProps) {
-                if (!item || match.params.id !== item.uuid) {
-                    onItemRouteChange(match.params.id);
-                }
-            }
-
         }
     )
 );
diff --git a/src/views/favorite-panel/favorite-panel-item.ts b/src/views/favorite-panel/favorite-panel-item.ts
deleted file mode 100644 (file)
index 842b6d6..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { GroupContentsResource } from "~/services/groups-service/groups-service";
-import { ResourceKind } from "~/models/resource";
-
-export interface FavoritePanelItem {
-    uuid: string;
-    name: string;
-    kind: string;
-    url: string;
-    owner: string;
-    lastModified: string;
-    fileSize?: number;
-    status?: string;
-}
-
-export function resourceToDataItem(r: GroupContentsResource): FavoritePanelItem {
-    return {
-        uuid: r.uuid,
-        name: r.name,
-        kind: r.kind,
-        url: "",
-        owner: r.ownerUuid,
-        lastModified: r.modifiedAt,
-        status:  r.kind === ResourceKind.PROCESS ? r.state : undefined
-    };
-}
index 125ea27ddf1635836bdd592091025d637efd028a..3fd33aefcbf71918c731e3de7812a7dbeb61e26e 100644 (file)
@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { FavoritePanelItem } from './favorite-panel-item';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
 import { DispatchProp, connect } from 'react-redux';
@@ -16,9 +15,14 @@ import { SortDirection } from '~/components/data-table/data-column';
 import { ResourceKind } from '~/models/resource';
 import { resourceLabel } from '~/common/labels';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '~/views-components/data-explorer/renderers';
-import { FAVORITE_PANEL_ID } from "~/store/favorite-panel/favorite-panel-action";
+import { FAVORITE_PANEL_ID, loadFavoritePanel } from "~/store/favorite-panel/favorite-panel-action";
+import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner, ResourceName } from '~/views-components/data-explorer/renderers';
 import { FavoriteIcon } from '~/components/icon/icon';
+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 { navigateTo } from '~/store/navigation/navigation-action';
 
 type CssRules = "toolbar" | "button";
 
@@ -45,14 +49,14 @@ export interface FavoritePanelFilter extends DataTableFilterItem {
     type: ResourceKind | ContainerRequestState;
 }
 
-export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
+export const favoritePanelColumns: DataColumns<string, FavoritePanelFilter> = [
     {
         name: FavoritePanelColumnNames.NAME,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.ASC,
         filters: [],
-        render: renderName,
+        render: uuid => <ResourceName uuid={uuid} />,
         width: "450px"
     },
     {
@@ -77,7 +81,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
                 type: ContainerRequestState.UNCOMMITTED
             }
         ],
-        render: renderStatus,
+        render: uuid => <ProcessStatus uuid={uuid} />,
         width: "75px"
     },
     {
@@ -102,7 +106,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
                 type: ResourceKind.PROJECT
             }
         ],
-        render: item => renderType(item.kind),
+        render: uuid => <ResourceType uuid={uuid} />,
         width: "125px"
     },
     {
@@ -111,7 +115,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: [],
-        render: item => renderOwner(item.owner),
+        render: uuid => <ResourceOwner uuid={uuid} />,
         width: "200px"
     },
     {
@@ -120,7 +124,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: [],
-        render: item => renderFileSize(item.fileSize),
+        render: uuid => <ResourceFileSize uuid={uuid} />,
         width: "50px"
     },
     {
@@ -129,7 +133,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: [],
-        render: item => renderDate(item.lastModified),
+        render: uuid => <ResourceLastModifiedDate uuid={uuid} />,
         width: "150px"
     }
 ];
@@ -139,36 +143,45 @@ interface FavoritePanelDataProps {
 }
 
 interface FavoritePanelActionProps {
-    onItemClick: (item: FavoritePanelItem) => void;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: FavoritePanelItem) => void;
+    onItemClick: (item: string) => void;
+    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: string) => void;
     onDialogOpen: (ownerUuid: string) => void;
-    onItemDoubleClick: (item: FavoritePanelItem) => void;
-    onItemRouteChange: (itemId: string) => void;
+    onItemDoubleClick: (item: string) => void;
 }
 
+const mapDispatchToProps = (dispatch: Dispatch): FavoritePanelActionProps => ({
+    onContextMenu: (event, resourceUuid) => {
+        event.preventDefault();
+        dispatch(
+            contextMenuActions.OPEN_CONTEXT_MENU({
+                position: { x: event.clientX, y: event.clientY },
+                resource: { name: '', uuid: resourceUuid, kind: ContextMenuKind.RESOURCE }
+            })
+        );
+    },
+    onDialogOpen: (ownerUuid: string) => { return; },
+    onItemClick: (resourceUuid: string) => {
+        dispatch<any>(loadDetailsPanel(resourceUuid));
+    },
+    onItemDoubleClick: uuid => {
+        dispatch<any>(navigateTo(uuid));
+    }
+});
+
 type FavoritePanelProps = FavoritePanelDataProps & FavoritePanelActionProps & DispatchProp
-                        & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+    & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
 
 export const FavoritePanel = withStyles(styles)(
-    connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
+    connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }), mapDispatchToProps)(
         class extends React.Component<FavoritePanelProps> {
             render() {
                 return <DataExplorer
                     id={FAVORITE_PANEL_ID}
-                    columns={columns}
                     onRowClick={this.props.onItemClick}
                     onRowDoubleClick={this.props.onItemDoubleClick}
                     onContextMenu={this.props.onContextMenu}
-                    extractKey={(item: FavoritePanelItem) => item.uuid}
                     defaultIcon={FavoriteIcon}
-                    defaultMessages={['Your favorites list is empty.']}/>
-                ;
-            }
-
-            componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: FavoritePanelProps) {
-                if (match.params.id !== currentItemId) {
-                    onItemRouteChange(match.params.id);
-                }
+                    defaultMessages={['Your favorites list is empty.']} />;
             }
         }
     )
diff --git a/src/views/project-panel/project-panel-item.ts b/src/views/project-panel/project-panel-item.ts
deleted file mode 100644 (file)
index f031859..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { GroupContentsResource } from "~/services/groups-service/groups-service";
-import { ResourceKind } from "~/models/resource";
-
-export interface ProjectPanelItem {
-    uuid: string;
-    name: string;
-    description?: string;
-    kind: string;
-    url: string;
-    owner: string;
-    lastModified: string;
-    fileSize?: number;
-    status?: string;
-}
-
-export function resourceToDataItem(r: GroupContentsResource): ProjectPanelItem {
-    return {
-        uuid: r.uuid,
-        name: r.name,
-        description: r.description,
-        kind: r.kind,
-        url: "",
-        owner: r.ownerUuid,
-        lastModified: r.modifiedAt,
-        status:  r.kind === ResourceKind.PROCESS ? r.state : undefined
-    };
-}
index 0f958d2cfbbb87684c47c1d77a16319f3e494e28..f9dcc396651caa9f93cea42c7f26f106c6f97c8b 100644 (file)
@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { ProjectPanelItem } from './project-panel-item';
 import { Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
 import { DispatchProp, connect } from 'react-redux';
@@ -16,9 +15,22 @@ import { SortDirection } from '~/components/data-table/data-column';
 import { ResourceKind } from '~/models/resource';
 import { resourceLabel } from '~/common/labels';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '~/views-components/data-explorer/renderers';
-import { restoreBranch } from '~/store/navigation/navigation-action';
+import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner } from '~/views-components/data-explorer/renderers';
 import { ProjectIcon } from '~/components/icon/icon';
+import { ResourceName } from '~/views-components/data-explorer/renderers';
+import { ResourcesState, getResource } from '~/store/resources/resources';
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { contextMenuActions } from '~/store/context-menu/context-menu-actions';
+import { CollectionResource } from '~/models/collection';
+import { ProjectResource } from '~/models/project';
+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 { navigateTo } 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";
 
@@ -50,14 +62,14 @@ export interface ProjectPanelFilter extends DataTableFilterItem {
     type: ResourceKind | ContainerRequestState;
 }
 
-export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
+export const projectPanelColumns: DataColumns<string, ProjectPanelFilter> = [
     {
         name: ProjectPanelColumnNames.NAME,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.ASC,
         filters: [],
-        render: renderName,
+        render: uuid => <ResourceName uuid={uuid} />,
         width: "450px"
     },
     {
@@ -82,7 +94,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
                 type: ContainerRequestState.UNCOMMITTED
             }
         ],
-        render: renderStatus,
+        render: uuid => <ProcessStatus uuid={uuid} />,
         width: "75px"
     },
     {
@@ -107,7 +119,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
                 type: ResourceKind.PROJECT
             }
         ],
-        render: item => renderType(item.kind),
+        render: uuid => <ResourceType uuid={uuid} />,
         width: "125px"
     },
     {
@@ -116,7 +128,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: [],
-        render: item => renderOwner(item.owner),
+        render: uuid => <ResourceOwner uuid={uuid} />,
         width: "200px"
     },
     {
@@ -125,7 +137,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: [],
-        render: item => renderFileSize(item.fileSize),
+        render: uuid => <ResourceFileSize uuid={uuid} />,
         width: "50px"
     },
     {
@@ -134,7 +146,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
         configurable: true,
         sortDirection: SortDirection.NONE,
         filters: [],
-        render: item => renderDate(item.lastModified),
+        render: uuid => <ResourceLastModifiedDate uuid={uuid} />,
         width: "150px"
     }
 ];
@@ -143,22 +155,17 @@ export const PROJECT_PANEL_ID = "projectPanel";
 
 interface ProjectPanelDataProps {
     currentItemId: string;
+    resources: ResourcesState;
 }
 
-interface ProjectPanelActionProps {
-    onItemClick: (item: ProjectPanelItem) => void;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, item: ProjectPanelItem) => void;
-    onProjectCreationDialogOpen: (ownerUuid: string) => void;
-    onCollectionCreationDialogOpen: (ownerUuid: string) => void;
-    onItemDoubleClick: (item: ProjectPanelItem) => void;
-    onItemRouteChange: (itemId: string) => void;
-}
-
-type ProjectPanelProps = ProjectPanelDataProps & ProjectPanelActionProps & DispatchProp
+type ProjectPanelProps = ProjectPanelDataProps & DispatchProp
     & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
 
 export const ProjectPanel = withStyles(styles)(
-    connect((state: RootState) => ({ currentItemId: state.projects.currentItemId }))(
+    connect((state: RootState) => ({
+        currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
+        resources: state.resources
+    }))(
         class extends React.Component<ProjectPanelProps> {
             render() {
                 const { classes } = this.props;
@@ -176,35 +183,60 @@ export const ProjectPanel = withStyles(styles)(
                     </div>
                     <DataExplorer
                         id={PROJECT_PANEL_ID}
-                        columns={columns}
-                        onRowClick={this.props.onItemClick}
-                        onRowDoubleClick={this.props.onItemDoubleClick}
-                        onContextMenu={this.props.onContextMenu}
-                        extractKey={(item: ProjectPanelItem) => item.uuid}
+                        onRowClick={this.handleRowClick}
+                        onRowDoubleClick={this.handleRowDoubleClick}
+                        onContextMenu={this.handleContextMenu}
                         defaultIcon={ProjectIcon}
                         defaultMessages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']} />
                 </div>;
             }
 
             handleNewProjectClick = () => {
-                this.props.onProjectCreationDialogOpen(this.props.currentItemId);
+                this.props.dispatch<any>(openProjectCreator(this.props.currentItemId));
             }
 
             handleNewCollectionClick = () => {
-                this.props.onCollectionCreationDialogOpen(this.props.currentItemId);
+                this.props.dispatch(reset(COLLECTION_CREATE_DIALOG));
+                this.props.dispatch(collectionCreateActions.OPEN_COLLECTION_CREATOR({ ownerUuid: this.props.currentItemId }));
             }
 
-            componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: ProjectPanelProps) {
-                if (match.params.id !== currentItemId) {
-                    onItemRouteChange(match.params.id);
+            handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+                event.preventDefault();
+                const resource = getResource(resourceUuid)(this.props.resources) as CollectionResource | ProjectResource | undefined;
+                if (resource) {
+                    let kind: ContextMenuKind;
+
+                    if (resource.kind === ResourceKind.PROJECT) {
+                        kind = ContextMenuKind.PROJECT;
+                    } else if (resource.kind === ResourceKind.COLLECTION) {
+                        kind = ContextMenuKind.COLLECTION_RESOURCE;
+                    } else {
+                        kind = ContextMenuKind.RESOURCE;
+                    }
+                    if (kind !== ContextMenuKind.RESOURCE) {
+                        this.props.dispatch(
+                            contextMenuActions.OPEN_CONTEXT_MENU({
+                                position: { x: event.clientX, y: event.clientY },
+                                resource: {
+                                    uuid: resource.uuid,
+                                    name: resource.name || '',
+                                    description: resource.description,
+                                    kind,
+                                }
+                            })
+                        );
+                    }
                 }
             }
 
-            componentDidMount() {
-                if (this.props.match.params.id && this.props.currentItemId === '') {
-                    this.props.dispatch<any>(restoreBranch(this.props.match.params.id));
-                }
+            handleRowDoubleClick = (uuid: string) => {
+                this.props.dispatch<any>(navigateTo(uuid));
             }
+
+            handleRowClick = (uuid: string) => {
+                this.props.dispatch(loadDetailsPanel(uuid));
+            }
+
         }
     )
 );
index 973788b70663412ae465059b66c8442daf341255..37c247ef23c6719debc5e9e8c5a74e67b20dbcbe 100644 (file)
@@ -4,47 +4,24 @@
 
 import * as React from 'react';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
-import Drawer from '@material-ui/core/Drawer';
 import { connect, DispatchProp } from "react-redux";
-import { Route, RouteComponentProps, 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";
 import { MainAppBar, MainAppBarActionProps, MainAppBarMenuItem } from '~/views-components/main-app-bar/main-app-bar';
-import { Breadcrumb } from '~/components/breadcrumbs/breadcrumbs';
 import { push } from 'react-router-redux';
-import { reset } from 'redux-form';
-import { ProjectTree } from '~/views-components/project-tree/project-tree';
 import { TreeItem } from "~/components/tree/tree";
-import { getTreePath } from '~/store/project/project-reducer';
-import { sidePanelActions } from '~/store/side-panel/side-panel-action';
-import { SidePanel, SidePanelItem } from '~/components/side-panel/side-panel';
-import { ItemMode, setProjectItem } from "~/store/navigation/navigation-action";
-import { projectActions } from "~/store/project/project-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 { detailsPanelActions, loadDetails } from "~/store/details-panel/details-panel-action";
-import { contextMenuActions } from "~/store/context-menu/context-menu-actions";
+import { detailsPanelActions } from "~/store/details-panel/details-panel-action";
 import { ProjectResource } from '~/models/project';
-import { ResourceKind } from '~/models/resource';
-import { ContextMenu, ContextMenuKind } from "~/views-components/context-menu/context-menu";
+import { ContextMenu } 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';
 import { Snackbar } from '~/views-components/snackbar/snackbar';
-import { favoritePanelActions } from '~/store/favorite-panel/favorite-panel-action';
 import { CollectionPanel } from '../collection-panel/collection-panel';
-import { loadCollection, loadCollectionTags } from '~/store/collection-panel/collection-panel-action';
-import { getCollectionUrl } from '~/models/collection';
-
-import { PROJECT_CREATE_FORM_NAME, openProjectCreateDialog } from '~/store/projects/project-create-actions';
-import { COLLECTION_CREATE_FORM_NAME, openCollectionCreateDialog } from '~/store/collections/collection-create-actions';
-import { CreateCollectionDialog } from '~/views-components/dialog-forms/create-collection-dialog';
-import { UpdateCollectionDialog } from '~/views-components/dialog-forms/update-collection-dialog';
-import { CreateProjectDialog } from '~/views-components/dialog-forms/create-project-dialog';
-import { UpdateProjectDialog } from '~/views-components/dialog-forms/update-project-dialog';
-
-import { ProjectPanel } from "~/views/project-panel/project-panel";
 import { AuthService } from "~/services/auth-service/auth-service";
 import { RenameFileDialog } from '~/views-components/rename-file-dialog/rename-file-dialog';
 import { FileRemoveDialog } from '~/views-components/file-remove-dialog/file-remove-dialog';
@@ -52,14 +29,21 @@ 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 { CollectionPartialCopyDialog } from '~/views-components/collection-partial-copy-dialog/collection-partial-copy-dialog';
+import { SidePanel } from '~/views-components/side-panel/side-panel';
+import { Routes } from '~/routes/routes';
+import { Breadcrumbs } from '~/views-components/breadcrumbs/breadcrumbs';
+import { CreateProjectDialog } from '~/views-components/dialog-forms/create-project-dialog';
+import { CreateCollectionDialog } from '~/views-components/dialog-forms/create-collection-dialog';
+import { CopyCollectionDialog } from '~/views-components/dialog-forms/copy-collection-dialog';
+import { UpdateCollectionDialog } from '~/views-components/dialog-forms/update-collection-dialog';
+import { UpdateProjectDialog } from '~/views-components/dialog-forms/update-project-dialog';
 import { MoveProjectDialog } from '~/views-components/dialog-forms/move-project-dialog';
 import { MoveCollectionDialog } from '~/views-components/dialog-forms/move-collection-dialog';
-import { CopyCollectionDialog } from '~/views-components/dialog-forms/copy-collection-dialog';
 
-const DRAWER_WITDH = 240;
+
 const APP_BAR_HEIGHT = 100;
 
-type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar';
+type CssRules = 'root' | 'appBar' | 'content' | 'contentWrapper';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
@@ -76,12 +60,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         position: "absolute",
         width: "100%"
     },
-    drawerPaper: {
-        position: 'relative',
-        width: DRAWER_WITDH,
-        display: 'flex',
-        flexDirection: 'column',
-    },
     contentWrapper: {
         backgroundColor: theme.palette.background.default,
         display: "flex",
@@ -95,7 +73,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         flexGrow: 1,
         position: 'relative'
     },
-    toolbar: theme.mixins.toolbar
 });
 
 interface WorkbenchDataProps {
@@ -103,7 +80,6 @@ interface WorkbenchDataProps {
     currentProjectId: string;
     user?: User;
     currentToken?: string;
-    sidePanelItems: SidePanelItem[];
 }
 
 interface WorkbenchGeneralProps {
@@ -116,10 +92,6 @@ interface WorkbenchActionProps {
 
 type WorkbenchProps = WorkbenchDataProps & WorkbenchGeneralProps & WorkbenchActionProps & DispatchProp<any> & WithStyles<CssRules>;
 
-interface NavBreadcrumb extends Breadcrumb {
-    itemId: string;
-}
-
 interface NavMenuItem extends MainAppBarMenuItem {
     action: () => void;
 }
@@ -142,12 +114,10 @@ export const Workbench = withStyles(styles)(
             currentProjectId: state.projects.currentItemId,
             user: state.auth.user,
             currentToken: state.auth.apiToken,
-            sidePanelItems: state.sidePanel
         })
     )(
         class extends React.Component<WorkbenchProps, WorkbenchState> {
             state = {
-                isCreationDialogOpen: false,
                 isCurrentTokenDialogOpen: false,
                 anchorEl: null,
                 searchText: "",
@@ -183,62 +153,25 @@ export const Workbench = withStyles(styles)(
             };
 
             render() {
-                const path = getTreePath(this.props.projects, this.props.currentProjectId);
-                const breadcrumbs = path.map(item => ({
-                    label: item.data.name,
-                    itemId: item.data.uuid,
-                    status: item.status
-                }));
-
                 const { classes, user } = this.props;
                 return (
                     <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}
                                 buildInfo={this.props.buildInfo}
                                 {...this.mainAppBarActions} />
                         </div>
-                        {user &&
-                            <Drawer
-                                variant="permanent"
-                                classes={{
-                                    paper: classes.drawerPaper,
-                                }}>
-                                <div className={classes.toolbar} />
-                                <SidePanel
-                                    toggleOpen={this.toggleSidePanelOpen}
-                                    toggleActive={this.toggleSidePanelActive}
-                                    sidePanelItems={this.props.sidePanelItems}
-                                    onContextMenu={(event) => this.openContextMenu(event, {
-                                        uuid: this.props.authService.getUuid() || "",
-                                        name: "",
-                                        kind: ContextMenuKind.ROOT_PROJECT
-                                    })}>
-                                    <ProjectTree
-                                        projects={this.props.projects}
-                                        toggleOpen={itemId => this.props.dispatch(setProjectItem(itemId, ItemMode.OPEN))}
-                                        onContextMenu={(event, item) => this.openContextMenu(event, {
-                                            uuid: item.data.uuid,
-                                            name: item.data.name,
-                                            kind: ContextMenuKind.PROJECT
-                                        })}
-                                        toggleActive={itemId => {
-                                            this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE));
-                                            this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
-                                        }} />
-                                </SidePanel>
-                            </Drawer>}
+                        {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" render={this.renderProjectPanel} />
-                                    <Route path="/favorites" render={this.renderFavoritePanel} />
-                                    <Route path="/collections/:id" render={this.renderCollectionPanel} />
+                                    <Route path={Routes.PROJECTS} component={ProjectPanel} />
+                                    <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
+                                    <Route path={Routes.FAVORITES} component={FavoritePanel} />
                                 </Switch>
                             </div>
                             {user && <DetailsPanel />}
@@ -266,91 +199,7 @@ export const Workbench = withStyles(styles)(
                 );
             }
 
-            renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
-                onItemRouteChange={(collectionId) => {
-                    this.props.dispatch<any>(loadCollection(collectionId));
-                    this.props.dispatch<any>(loadCollectionTags(collectionId));
-                }}
-                onContextMenu={(event, item) => {
-                    this.openContextMenu(event, {
-                        uuid: item.uuid,
-                        name: item.name,
-                        description: item.description,
-                        kind: ContextMenuKind.COLLECTION
-                    });
-                }}
-                {...props} />
-
-            renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
-                onItemRouteChange={itemId => this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE))}
-                onContextMenu={(event, item) => {
-                    let kind: ContextMenuKind;
-
-                    if (item.kind === ResourceKind.PROJECT) {
-                        kind = ContextMenuKind.PROJECT;
-                    } else if (item.kind === ResourceKind.COLLECTION) {
-                        kind = ContextMenuKind.COLLECTION_RESOURCE;
-                    } else {
-                        kind = ContextMenuKind.RESOURCE;
-                    }
-
-                    this.openContextMenu(event, {
-                        uuid: item.uuid,
-                        name: item.name,
-                        description: item.description,
-                        kind
-                    });
-                }}
-                onProjectCreationDialogOpen={this.handleProjectCreationDialogOpen}
-                onCollectionCreationDialogOpen={this.handleCollectionCreationDialogOpen}
-                onItemClick={item => {
-                    this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
-                }}
-                onItemDoubleClick={item => {
-                    switch (item.kind) {
-                        case ResourceKind.COLLECTION:
-                            this.props.dispatch(loadCollection(item.uuid));
-                            this.props.dispatch(push(getCollectionUrl(item.uuid)));
-                        default:
-                            this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
-                            this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
-                    }
-
-                }}
-                {...props} />
-
-            renderFavoritePanel = (props: RouteComponentProps<{ id: string }>) => <FavoritePanel
-                onItemRouteChange={() => this.props.dispatch(favoritePanelActions.REQUEST_ITEMS())}
-                onContextMenu={(event, item) => {
-                    const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE;
-                    this.openContextMenu(event, {
-                        uuid: item.uuid,
-                        name: item.name,
-                        kind,
-                    });
-                }}
-                onDialogOpen={this.handleProjectCreationDialogOpen}
-                onItemClick={item => {
-                    this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
-                }}
-                onItemDoubleClick={item => {
-                    switch (item.kind) {
-                        case ResourceKind.COLLECTION:
-                            this.props.dispatch(loadCollection(item.uuid));
-                            this.props.dispatch(push(getCollectionUrl(item.uuid)));
-                        default:
-                            this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
-                            this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
-                    }
-
-                }}
-                {...props} />
-
             mainAppBarActions: MainAppBarActionProps = {
-                onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
-                    this.props.dispatch(setProjectItem(itemId, ItemMode.BOTH));
-                    this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
-                },
                 onSearch: searchText => {
                     this.setState({ searchText });
                     this.props.dispatch(push(`/search?q=${searchText}`));
@@ -359,48 +208,8 @@ export const Workbench = withStyles(styles)(
                 onDetailsPanelToggle: () => {
                     this.props.dispatch(detailsPanelActions.TOGGLE_DETAILS_PANEL());
                 },
-                onContextMenu: (event: React.MouseEvent<HTMLElement>, breadcrumb: NavBreadcrumb) => {
-                    this.openContextMenu(event, {
-                        uuid: breadcrumb.itemId,
-                        name: breadcrumb.label,
-                        kind: ContextMenuKind.PROJECT
-                    });
-                }
             };
 
-            toggleSidePanelOpen = (itemId: string) => {
-                this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(itemId));
-            }
-
-            toggleSidePanelActive = (itemId: string) => {
-                this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
-
-                const panelItem = this.props.sidePanelItems.find(it => it.id === itemId);
-                if (panelItem && panelItem.activeAction) {
-                    panelItem.activeAction(this.props.dispatch, this.props.authService.getUuid());
-                }
-            }
-
-            handleProjectCreationDialogOpen = (itemUuid: string) => {
-                this.props.dispatch(reset(PROJECT_CREATE_FORM_NAME));
-                this.props.dispatch<any>(openProjectCreateDialog(itemUuid));
-            }
-
-            handleCollectionCreationDialogOpen = (itemUuid: string) => {
-                this.props.dispatch(reset(COLLECTION_CREATE_FORM_NAME));
-                this.props.dispatch<any>(openCollectionCreateDialog(itemUuid));
-            }
-
-            openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: { name: string; uuid: string; description?: string; kind: ContextMenuKind; }) => {
-                event.preventDefault();
-                this.props.dispatch(
-                    contextMenuActions.OPEN_CONTEXT_MENU({
-                        position: { x: event.clientX, y: event.clientY },
-                        resource
-                    })
-                );
-            }
-
             toggleCurrentTokenModal = () => {
                 this.setState({ isCurrentTokenDialogOpen: !this.state.isCurrentTokenDialogOpen });
             }