Merge branch 'master'
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Fri, 3 Aug 2018 07:10:55 +0000 (09:10 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Fri, 3 Aug 2018 07:10:55 +0000 (09:10 +0200)
Feature #13855

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

34 files changed:
package.json
src/common/api/common-resource-service.ts
src/common/api/server-api.ts [deleted file]
src/common/custom-theme.ts
src/index.tsx
src/services/auth-service/auth-service.ts
src/services/services.ts
src/store/auth/auth-action.ts
src/store/auth/auth-actions.test.ts [new file with mode: 0644]
src/store/auth/auth-reducer.test.ts
src/store/auth/auth-reducer.ts
src/store/collection-panel/collection-panel-action.ts
src/store/collections/collections-reducer.ts [new file with mode: 0644]
src/store/collections/creator/collection-creator-action.ts
src/store/collections/creator/collection-creator-reducer.test.ts
src/store/collections/creator/collection-creator-reducer.ts
src/store/collections/updator/collection-updator-action.ts [new file with mode: 0644]
src/store/collections/updator/collection-updator-reducer.ts [new file with mode: 0644]
src/store/details-panel/details-panel-action.ts
src/store/favorite-panel/favorite-panel-middleware-service.ts
src/store/favorites/favorites-actions.ts
src/store/project-panel/project-panel-middleware-service.ts
src/store/project/project-action.ts
src/store/store.ts
src/views-components/api-token/api-token.tsx
src/views-components/context-menu/action-sets/collection-action-set.ts
src/views-components/create-collection-dialog/create-collection-dialog.tsx
src/views-components/dialog-create/dialog-collection-create.tsx
src/views-components/dialog-update/dialog-collection-update.tsx [new file with mode: 0644]
src/views-components/update-collection-dialog/update-collection-dialog..tsx [new file with mode: 0644]
src/views/collection-panel/collection-panel.tsx
src/views/workbench/workbench.test.tsx
src/views/workbench/workbench.tsx
yarn.lock

index fa4bd309df7ad1b6c873128d70192e0c9b97fc88..0b0ebcd770c82cd2f289f232d60b4ff3441554ba 100644 (file)
@@ -6,11 +6,13 @@
     "@material-ui/core": "1.4.0",
     "@material-ui/icons": "1.1.0",
     "@types/lodash": "4.14.112",
+    "@types/react-copy-to-clipboard": "4.2.5",
     "@types/redux-form": "7.4.1",
     "axios": "0.18.0",
     "classnames": "2.2.6",
     "lodash": "4.17.10",
     "react": "16.4.1",
+    "react-copy-to-clipboard": "5.0.1",
     "react-dom": "16.4.1",
     "react-redux": "5.0.7",
     "react-router": "4.3.1",
index 3956fb7390983824a402456abc2144850b85cda2..8ad8fe916ee48e7ef7cf373c105530a224b1c1a2 100644 (file)
@@ -100,8 +100,11 @@ export class CommonResourceService<T extends Resource> {
                 }));
     }
 
-    update(uuid: string) {
-        throw new Error("Not implemented");
+    update(uuid: string, data: any) {
+        return CommonResourceService.defaultResponse(
+            this.serverApi
+                .put<T>(this.resourceType + uuid, data));
+        
     }
 }
 
diff --git a/src/common/api/server-api.ts b/src/common/api/server-api.ts
deleted file mode 100644 (file)
index bcd2f65..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import Axios, { AxiosInstance } from "axios";
-
-export const API_HOST = process.env.REACT_APP_ARVADOS_API_HOST;
-
-export const authClient: AxiosInstance = Axios.create();
-export const apiClient: AxiosInstance = Axios.create();
-
-export function setServerApiAuthorizationHeader(token: string) {
-    [authClient, apiClient].forEach(client => {
-        client.defaults.headers.common = {
-            Authorization: `OAuth2 ${token}`
-        };
-    });
-}
-
-export function removeServerApiAuthorizationHeader() {
-    [authClient, apiClient].forEach(client => {
-        delete client.defaults.headers.common.Authorization;
-    });
-}
-
-export const setBaseUrl = (url: string) => {
-    authClient.defaults.baseURL = url;
-    apiClient.defaults.baseURL = url + "/arvados/v1";
-};
index e5d2e5e78ca80bed2cada42c923bd88d4461c96c..ecad39134d1652e07d11b58d8da524fdad7cca29 100644 (file)
@@ -96,6 +96,23 @@ const themeOptions: ArvadosThemeOptions = {
             root: {
                 padding: '8px 16px'
             }
+        },
+        MuiInput: {
+            underline: {
+                '&:after': {
+                    borderBottomColor: purple800
+                },
+                '&:hover:not($disabled):not($focused):not($error):before': {
+                    borderBottom: '1px solid inherit'
+                }
+            }
+        },
+        MuiFormLabel: {
+            focused: {
+                "&$focused:not($error)": {
+                    color: purple800
+                }
+            }
         }
     },
     mixins: {
index c950c55727927e98268206e2c1abfae4317bbb37..467aee08dd378915152892fbdcecb67ef4f86c86 100644 (file)
@@ -12,13 +12,12 @@ import createBrowserHistory from "history/createBrowserHistory";
 import { configureStore } from "./store/store";
 import { ConnectedRouter } from "react-router-redux";
 import { ApiToken } from "./views-components/api-token/api-token";
-import { authActions } from "./store/auth/auth-action";
-import { authService } from "./services/services";
+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';
-import { setBaseUrl } from './common/api/server-api';
 import { addMenuActionSet, ContextMenuKind } from "./views-components/context-menu/context-menu";
 import { rootProjectActionSet } from "./views-components/context-menu/action-sets/root-project-action-set";
 import { projectActionSet } from "./views-components/context-menu/action-sets/project-action-set";
@@ -38,14 +37,14 @@ addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
 
 fetchConfig()
     .then(config => {
-
-        setBaseUrl(config.API_HOST);
-
         const history = createBrowserHistory();
-        const store = configureStore(history);
+        const services = createServices(config.API_HOST);
+        const store = configureStore(history, services);
+
+        store.dispatch(initAuth());
+        store.dispatch(getProjectList(services.authService.getUuid()));
 
-        store.dispatch(authActions.INIT());
-        store.dispatch<any>(getProjectList(authService.getUuid()));
+        const Token = (props: any) => <ApiToken authService={services.authService} {...props}/>;
 
         const App = () =>
             <MuiThemeProvider theme={CustomTheme}>
@@ -53,7 +52,7 @@ fetchConfig()
                     <ConnectedRouter history={history}>
                         <div>
                             <Route path="/" component={Workbench} />
-                            <Route path="/token" component={ApiToken} />
+                            <Route path="/token" component={Token} />
                         </div>
                     </ConnectedRouter>
                 </Provider>
index 551d435f25d208d07a835270b4602a97b228460a..f96edc79a08acd2e1079a4785b6aafd9f31a6edb 100644 (file)
@@ -2,7 +2,6 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { API_HOST } from "../../common/api/server-api";
 import { User } from "../../models/user";
 import { AxiosInstance } from "../../../node_modules/axios";
 
@@ -25,8 +24,8 @@ export interface UserDetailsResponse {
 export class AuthService {
 
     constructor(
-        protected authClient: AxiosInstance,
-        protected apiClient: AxiosInstance) { }
+        protected apiClient: AxiosInstance,
+        protected baseUrl: string) { }
 
     public saveApiToken(token: string) {
         localStorage.setItem(API_TOKEN_KEY, token);
@@ -78,12 +77,12 @@ export class AuthService {
 
     public login() {
         const currentUrl = `${window.location.protocol}//${window.location.host}/token`;
-        window.location.assign(`${this.authClient.defaults.baseURL || ""}/login?return_to=${currentUrl}`);
+        window.location.assign(`${this.baseUrl || ""}/login?return_to=${currentUrl}`);
     }
 
     public logout() {
         const currentUrl = `${window.location.protocol}//${window.location.host}`;
-        window.location.assign(`${this.authClient.defaults.baseURL || ""}/logout?return_to=${currentUrl}`);
+        window.location.assign(`${this.baseUrl || ""}/logout?return_to=${currentUrl}`);
     }
 
     public getUserDetails = (): Promise<User> => {
index a05ad9c2288a7e415c37ce474e91468f6deeeeb8..9e1adbf6e4e20d8a1f59637ae4a02f356905dc18 100644 (file)
@@ -4,17 +4,46 @@
 
 import { AuthService } from "./auth-service/auth-service";
 import { GroupsService } from "./groups-service/groups-service";
-import { authClient, apiClient } from "../common/api/server-api";
 import { ProjectService } from "./project-service/project-service";
 import { LinkService } from "./link-service/link-service";
 import { FavoriteService } from "./favorite-service/favorite-service";
+import { AxiosInstance } from "axios";
 import { CollectionService } from "./collection-service/collection-service";
+import Axios from "axios";
 import { CollectionFilesService } from "./collection-files-service/collection-files-service";
 
-export const authService = new AuthService(authClient, apiClient);
-export const groupsService = new GroupsService(apiClient);
-export const projectService = new ProjectService(apiClient);
-export const collectionService = new CollectionService(apiClient);
-export const collectionFilesService = new CollectionFilesService(collectionService);
-export const linkService = new LinkService(apiClient);
-export const favoriteService = new FavoriteService(linkService, groupsService);
\ No newline at end of file
+export interface ServiceRepository {
+    apiClient: AxiosInstance;
+
+    authService: AuthService;
+    groupsService: GroupsService;
+    projectService: ProjectService;
+    linkService: LinkService;
+    favoriteService: FavoriteService;
+    collectionService: CollectionService;
+    collectionFilesService: CollectionFilesService;
+}
+
+export const createServices = (baseUrl: string): ServiceRepository => {
+    const apiClient = Axios.create();
+    apiClient.defaults.baseURL = `${baseUrl}/arvados/v1`;
+
+    const authService = new AuthService(apiClient, baseUrl);
+    const groupsService = new GroupsService(apiClient);
+    const projectService = new ProjectService(apiClient);
+    const linkService = new LinkService(apiClient);
+    const favoriteService = new FavoriteService(linkService, groupsService);
+    const collectionService = new CollectionService(apiClient);
+    const collectionFilesService = new CollectionFilesService(collectionService);
+
+    return {
+        apiClient,
+        authService,
+        groupsService,
+        projectService,
+        linkService,
+        favoriteService,
+        collectionService,
+        collectionFilesService
+    };
+};
index e9930a02836da5d66c7e062813a5fd292784f973..6b81c31796a41dce91cce146f560978cd5510d56 100644 (file)
@@ -4,14 +4,16 @@
 
 import { ofType, default as unionize, UnionOf } from "unionize";
 import { Dispatch } from "redux";
-import { authService } from "../../services/services";
 import { User } from "../../models/user";
+import { RootState } from "../store";
+import { ServiceRepository } from "../../services/services";
+import { AxiosInstance } from "axios";
 
 export const authActions = unionize({
     SAVE_API_TOKEN: ofType<string>(),
     LOGIN: {},
     LOGOUT: {},
-    INIT: {},
+    INIT: ofType<{ user: User, token: string }>(),
     USER_DETAILS_REQUEST: {},
     USER_DETAILS_SUCCESS: ofType<User>()
 }, {
@@ -19,11 +21,52 @@ export const authActions = unionize({
     value: 'payload'
 });
 
-export const getUserDetails = () => (dispatch: Dispatch): Promise<User> => {
+function setAuthorizationHeader(client: AxiosInstance, token: string) {
+    client.defaults.headers.common = {
+        Authorization: `OAuth2 ${token}`
+    };
+}
+
+function removeAuthorizationHeader(client: AxiosInstance) {
+    delete client.defaults.headers.common.Authorization;
+}
+
+export const initAuth = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    const user = services.authService.getUser();
+    const token = services.authService.getApiToken();
+    if (token) {
+        setAuthorizationHeader(services.apiClient, token);
+    }
+    if (token && user) {
+        dispatch(authActions.INIT({ user, token }));
+    }
+};
+
+export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    services.authService.saveApiToken(token);
+    setAuthorizationHeader(services.apiClient, token);
+    dispatch(authActions.SAVE_API_TOKEN(token));
+};
+
+export const login = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    services.authService.login();
+    dispatch(authActions.LOGIN());
+};
+
+export const logout = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    services.authService.removeApiToken();
+    services.authService.removeUser();
+    removeAuthorizationHeader(services.apiClient);
+    services.authService.logout();
+    dispatch(authActions.LOGOUT());
+};
+
+export const getUserDetails = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<User> => {
     dispatch(authActions.USER_DETAILS_REQUEST());
-    return authService.getUserDetails().then(details => {
-        dispatch(authActions.USER_DETAILS_SUCCESS(details));
-        return details;
+    return services.authService.getUserDetails().then(user => {
+        services.authService.saveUser(user);
+        dispatch(authActions.USER_DETAILS_SUCCESS(user));
+        return user;
     });
 };
 
diff --git a/src/store/auth/auth-actions.test.ts b/src/store/auth/auth-actions.test.ts
new file mode 100644 (file)
index 0000000..1ded88e
--- /dev/null
@@ -0,0 +1,74 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { authReducer, AuthState } from "./auth-reducer";
+import { AuthAction, initAuth } from "./auth-action";
+import {
+    API_TOKEN_KEY,
+    USER_EMAIL_KEY,
+    USER_FIRST_NAME_KEY,
+    USER_LAST_NAME_KEY,
+    USER_OWNER_UUID_KEY,
+    USER_UUID_KEY
+} from "../../services/auth-service/auth-service";
+
+import 'jest-localstorage-mock';
+import { createServices } from "../../services/services";
+import { configureStore, RootStore } from "../store";
+import createBrowserHistory from "history/createBrowserHistory";
+
+describe('auth-actions', () => {
+    let reducer: (state: AuthState | undefined, action: AuthAction) => any;
+    let store: RootStore;
+
+    beforeEach(() => {
+        store = configureStore(createBrowserHistory(), createServices("/arvados/v1"));
+        localStorage.clear();
+        reducer = authReducer(createServices("/arvados/v1"));
+    });
+
+    it('should initialise state with user and api token from local storage', () => {
+
+        localStorage.setItem(API_TOKEN_KEY, "token");
+        localStorage.setItem(USER_EMAIL_KEY, "test@test.com");
+        localStorage.setItem(USER_FIRST_NAME_KEY, "John");
+        localStorage.setItem(USER_LAST_NAME_KEY, "Doe");
+        localStorage.setItem(USER_UUID_KEY, "uuid");
+        localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid");
+
+        store.dispatch(initAuth());
+
+        expect(store.getState().auth).toEqual({
+            apiToken: "token",
+            user: {
+                email: "test@test.com",
+                firstName: "John",
+                lastName: "Doe",
+                uuid: "uuid",
+                ownerUuid: "ownerUuid"
+            }
+        });
+    });
+
+    // TODO: Add remaining action tests
+    /*
+    it('should fire external url to login', () => {
+        const initialState = undefined;
+        window.location.assign = jest.fn();
+        reducer(initialState, authActions.LOGIN());
+        expect(window.location.assign).toBeCalledWith(
+            `/login?return_to=${window.location.protocol}//${window.location.host}/token`
+        );
+    });
+
+    it('should fire external url to logout', () => {
+        const initialState = undefined;
+        window.location.assign = jest.fn();
+        reducer(initialState, authActions.LOGOUT());
+        expect(window.location.assign).toBeCalledWith(
+            `/logout?return_to=${location.protocol}//${location.host}`
+        );
+    });
+    */
+});
index 778b500d364b87fe5478b939833af69ff24ca628..0e05263d4301a7b3d94f0408670da9a9cc019fd5 100644 (file)
@@ -2,66 +2,44 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { authReducer } from "./auth-reducer";
-import { authActions } from "./auth-action";
-import {
-    API_TOKEN_KEY,
-    USER_EMAIL_KEY,
-    USER_FIRST_NAME_KEY,
-    USER_LAST_NAME_KEY,
-    USER_OWNER_UUID_KEY,
-    USER_UUID_KEY
-} from "../../services/auth-service/auth-service";
+import { authReducer, AuthState } from "./auth-reducer";
+import { AuthAction, authActions } from "./auth-action";
 
 import 'jest-localstorage-mock';
+import { createServices } from "../../services/services";
 
 describe('auth-reducer', () => {
+    let reducer: (state: AuthState | undefined, action: AuthAction) => any;
+
     beforeAll(() => {
         localStorage.clear();
+        reducer = authReducer(createServices("/arvados/v1"));
     });
 
-    it('should return default state on initialisation', () => {
-        const initialState = undefined;
-        const state = authReducer(initialState, authActions.INIT());
-        expect(state).toEqual({
-            apiToken: undefined,
-            user: undefined
-        });
-    });
-
-    it('should read user and api token from local storage on init if they are there', () => {
+    it('should correctly initialise state', () => {
         const initialState = undefined;
-
-        localStorage.setItem(API_TOKEN_KEY, "token");
-        localStorage.setItem(USER_EMAIL_KEY, "test@test.com");
-        localStorage.setItem(USER_FIRST_NAME_KEY, "John");
-        localStorage.setItem(USER_LAST_NAME_KEY, "Doe");
-        localStorage.setItem(USER_UUID_KEY, "uuid");
-        localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid");
-
-        const state = authReducer(initialState, authActions.INIT());
+        const user = {
+            email: "test@test.com",
+            firstName: "John",
+            lastName: "Doe",
+            uuid: "uuid",
+            ownerUuid: "ownerUuid"
+        };
+        const state = reducer(initialState, authActions.INIT({user, token: "token"}));
         expect(state).toEqual({
             apiToken: "token",
-            user: {
-                email: "test@test.com",
-                firstName: "John",
-                lastName: "Doe",
-                uuid: "uuid",
-                ownerUuid: "ownerUuid"
-            }
+            user
         });
     });
 
-    it('should store token in local storage', () => {
+    it('should save api token', () => {
         const initialState = undefined;
 
-        const state = authReducer(initialState, authActions.SAVE_API_TOKEN("token"));
+        const state = reducer(initialState, authActions.SAVE_API_TOKEN("token"));
         expect(state).toEqual({
             apiToken: "token",
             user: undefined
         });
-
-        expect(localStorage.getItem(API_TOKEN_KEY)).toBe("token");
     });
 
     it('should set user details on success fetch', () => {
@@ -75,7 +53,7 @@ describe('auth-reducer', () => {
             ownerUuid: "ownerUuid"
         };
 
-        const state = authReducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
+        const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
         expect(state).toEqual({
             apiToken: undefined,
             user: {
@@ -86,25 +64,5 @@ describe('auth-reducer', () => {
                 ownerUuid: "ownerUuid",
             }
         });
-
-        expect(localStorage.getItem(API_TOKEN_KEY)).toBe("token");
-    });
-
-    it('should fire external url to login', () => {
-        const initialState = undefined;
-        window.location.assign = jest.fn();
-        authReducer(initialState, authActions.LOGIN());
-        expect(window.location.assign).toBeCalledWith(
-            `/login?return_to=${window.location.protocol}//${window.location.host}/token`
-        );
-    });
-
-    it('should fire external url to logout', () => {
-        const initialState = undefined;
-        window.location.assign = jest.fn();
-        authReducer(initialState, authActions.LOGOUT());
-        expect(window.location.assign).toBeCalledWith(
-            `/logout?return_to=${location.protocol}//${location.host}`
-        );
     });
 });
index 366385d50b506de529139f3f75151762f6a6285a..1546212b08fe846834bb84d61cf098dedf2c40c3 100644 (file)
@@ -4,42 +4,28 @@
 
 import { authActions, AuthAction } from "./auth-action";
 import { User } from "../../models/user";
-import { authService } from "../../services/services";
-import { removeServerApiAuthorizationHeader, setServerApiAuthorizationHeader } from "../../common/api/server-api";
+import { ServiceRepository } from "../../services/services";
 
 export interface AuthState {
     user?: User;
     apiToken?: string;
 }
 
-export const authReducer = (state: AuthState = {}, action: AuthAction) => {
+export const authReducer = (services: ServiceRepository) => (state: AuthState = {}, action: AuthAction) => {
     return authActions.match(action, {
         SAVE_API_TOKEN: (token: string) => {
-            authService.saveApiToken(token);
-            setServerApiAuthorizationHeader(token);
             return {...state, apiToken: token};
         },
-        INIT: () => {
-            const user = authService.getUser();
-            const token = authService.getApiToken();
-            if (token) {
-                setServerApiAuthorizationHeader(token);
-            }
-            return {user, apiToken: token};
+        INIT: ({ user, token }) => {
+            return { user, apiToken: token };
         },
         LOGIN: () => {
-            authService.login();
             return state;
         },
         LOGOUT: () => {
-            authService.removeApiToken();
-            authService.removeUser();
-            removeServerApiAuthorizationHeader();
-            authService.logout();
             return {...state, apiToken: undefined};
         },
         USER_DETAILS_SUCCESS: (user: User) => {
-            authService.saveUser(user);
             return {...state, user};
         },
         default: () => state
index ab2db17d249852dc3b68ef162901dfe858e9f8b9..419f04049b83ffaceff9f4c871c5319487163fa6 100644 (file)
@@ -6,28 +6,27 @@ import { unionize, ofType, UnionOf } from "unionize";
 import { Dispatch } from "redux";
 import { ResourceKind } from "../../models/resource";
 import { CollectionResource } from "../../models/collection";
-import { collectionService, collectionFilesService } from "../../services/services";
 import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions";
 import { createTree } from "../../models/tree";
-import { mapManifestToCollectionFilesTree } from "../../services/collection-files-service/collection-manifest-mapper";
-import { parseKeepManifestText } from "../../services/collection-files-service/collection-manifest-parser";
+import { RootState } from "../store";
+import { ServiceRepository } from "../../services/services";
 
 export const collectionPanelActions = unionize({
     LOAD_COLLECTION: ofType<{ uuid: string, kind: ResourceKind }>(),
-    LOAD_COLLECTION_SUCCESS: ofType<{ item: CollectionResource }>(),
+    LOAD_COLLECTION_SUCCESS: ofType<{ item: CollectionResource }>()
 }, { tag: 'type', value: 'payload' });
 
 export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
 
 export const loadCollection = (uuid: string, kind: ResourceKind) =>
-    (dispatch: Dispatch) => {
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid, kind }));
         dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
-        return collectionService
+        return services.collectionService
             .get(uuid)
             .then(item => {
                 dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item }));
-                return collectionFilesService.getFiles(item.uuid);
+                return services.collectionFilesService.getFiles(item.uuid);
             })
             .then(files => {
                 dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files }));
diff --git a/src/store/collections/collections-reducer.ts b/src/store/collections/collections-reducer.ts
new file mode 100644 (file)
index 0000000..966cf29
--- /dev/null
@@ -0,0 +1,17 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { combineReducers } from 'redux';
+import * as creator from "./creator/collection-creator-reducer";
+import * as updator from "./updator/collection-updator-reducer";
+
+export type CollectionsState = {
+    creator: creator.CollectionCreatorState;
+    updator: updator.CollectionUpdatorState;
+};
+
+export const collectionsReducer = combineReducers({
+    creator: creator.collectionCreationReducer,
+    updator: updator.collectionCreationReducer
+});
\ No newline at end of file
index b30f8b80876ef60db1fea4bd84ff1a2863db291e..2f2b83850e5f226aeb9e3a857e1c5f937aa5e3e8 100644 (file)
@@ -6,8 +6,8 @@ import { default as unionize, ofType, UnionOf } from "unionize";
 import { Dispatch } from "redux";
 
 import { RootState } from "../../store";
-import { collectionService } from '../../../services/services';
 import { CollectionResource } from '../../../models/collection';
+import { ServiceRepository } from "../../../services/services";
 
 export const collectionCreateActions = unionize({
     OPEN_COLLECTION_CREATOR: ofType<{ ownerUuid: string }>(),
@@ -20,13 +20,13 @@ export const collectionCreateActions = unionize({
     });
 
 export const createCollection = (collection: Partial<CollectionResource>) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
-        const { ownerUuid } = getState().collectionCreation.creator;
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { ownerUuid } = getState().collections.creator;
         const collectiontData = { ownerUuid, ...collection };
         dispatch(collectionCreateActions.CREATE_COLLECTION(collectiontData));
-        return collectionService
+        return services.collectionService
             .create(collectiontData)
             .then(collection => dispatch(collectionCreateActions.CREATE_COLLECTION_SUCCESS(collection)));
     };
 
-export type CollectionCreateAction = UnionOf<typeof collectionCreateActions>;
\ No newline at end of file
+export type CollectionCreateAction = UnionOf<typeof collectionCreateActions>;
index 0da18c81ef319b28b81674b68fb7feff423f5d43..fde58c433602aea0725ce09cb59f3006483bc4b2 100644 (file)
@@ -8,36 +8,24 @@ import { collectionCreateActions } from "./collection-creator-action";
 describe('collection-reducer', () => {
 
     it('should open collection creator dialog', () => {
-        const initialState = {
-            creator: { opened: false, ownerUuid: "" }
-        };
-        const collection = {
-            creator: { opened: true, ownerUuid: "" },
-        };
-
-        const state = collectionCreationReducer(initialState, collectionCreateActions.OPEN_COLLECTION_CREATOR(initialState.creator));
+        const initialState = { opened: false, ownerUuid: "" };
+        const collection = { opened: true, ownerUuid: "" };
+
+        const state = collectionCreationReducer(initialState, collectionCreateActions.OPEN_COLLECTION_CREATOR(initialState));
         expect(state).toEqual(collection);
     });
 
     it('should close collection creator dialog', () => {
-        const initialState = {
-            creator: { opened: true, ownerUuid: "" }
-        };
-        const collection = {
-            creator: { opened: false, ownerUuid: "" },
-        };
+        const initialState = { opened: true, ownerUuid: "" };
+        const collection = { opened: false, ownerUuid: "" };
 
         const state = collectionCreationReducer(initialState, collectionCreateActions.CLOSE_COLLECTION_CREATOR());
         expect(state).toEqual(collection);
     });
 
     it('should reset collection creator dialog props', () => {
-        const initialState = {
-            creator: { opened: true, ownerUuid: "test" }
-        };
-        const collection = {
-            creator: { opened: false, ownerUuid: "" },
-        };
+        const initialState = { opened: true, ownerUuid: "test" };
+        const collection = { opened: false, ownerUuid: "" };
 
         const state = collectionCreationReducer(initialState, collectionCreateActions.CREATE_COLLECTION_SUCCESS());
         expect(state).toEqual(collection);
index 769766e192f1178886a9c746a6e459fa30ea67a5..1a3cb0d2ea5f1082de69e0c796321df6295f6d07 100644 (file)
@@ -4,9 +4,7 @@
 
 import { collectionCreateActions, CollectionCreateAction } from './collection-creator-action';
 
-export type CollectionCreatorState = {
-    creator: CollectionCreator
-};
+export type CollectionCreatorState = CollectionCreator;
 
 interface CollectionCreator {
     opened: boolean;
@@ -15,17 +13,12 @@ interface CollectionCreator {
 
 const updateCreator = (state: CollectionCreatorState, creator?: Partial<CollectionCreator>) => ({
     ...state,
-    creator: {
-        ...state.creator,
-        ...creator
-    }
+    ...creator
 });
 
 const initialState: CollectionCreatorState = {
-    creator: {
-        opened: false,
-        ownerUuid: ""
-    }
+    opened: false,
+    ownerUuid: ''
 };
 
 export const collectionCreationReducer = (state: CollectionCreatorState = initialState, action: CollectionCreateAction) => {
diff --git a/src/store/collections/updator/collection-updator-action.ts b/src/store/collections/updator/collection-updator-action.ts
new file mode 100644 (file)
index 0000000..e12bfe5
--- /dev/null
@@ -0,0 +1,47 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { default as unionize, ofType, UnionOf } from "unionize";
+import { Dispatch } from "redux";
+
+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";
+
+export const collectionUpdatorActions = unionize({
+    OPEN_COLLECTION_UPDATOR: ofType<{ uuid: string }>(),
+    CLOSE_COLLECTION_UPDATOR: ofType<{}>(),
+    UPDATE_COLLECTION_SUCCESS: ofType<{}>(),
+}, {
+        tag: 'type',
+        value: 'payload'
+    });
+
+
+export const COLLECTION_FORM_NAME = 'collectionEditDialog';
+    
+export const openUpdator = (uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        dispatch(collectionUpdatorActions.OPEN_COLLECTION_UPDATOR({ uuid }));
+        const item = getState().collectionPanel.item;
+        if(item) {
+            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.updator;
+        return services.collectionService
+            .update(uuid, collection)
+            .then(collection => {
+                    dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection as CollectionResource }));
+                    dispatch(collectionUpdatorActions.UPDATE_COLLECTION_SUCCESS());
+                }
+            );
+    };
+
+export type CollectionUpdatorAction = UnionOf<typeof collectionUpdatorActions>;
\ No newline at end of file
diff --git a/src/store/collections/updator/collection-updator-reducer.ts b/src/store/collections/updator/collection-updator-reducer.ts
new file mode 100644 (file)
index 0000000..b9d0250
--- /dev/null
@@ -0,0 +1,31 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { collectionUpdatorActions, CollectionUpdatorAction } from './collection-updator-action';
+
+export type CollectionUpdatorState = CollectionUpdator;
+
+interface CollectionUpdator {
+    opened: boolean;
+    uuid: string;
+}
+
+const updateCollection = (state: CollectionUpdatorState, updator?: Partial<CollectionUpdator>) => ({
+    ...state,
+    ...updator
+});
+
+const initialState: CollectionUpdatorState = {
+    opened: false,
+    uuid: ''
+};
+
+export const collectionCreationReducer = (state: CollectionUpdatorState = initialState, action: CollectionUpdatorAction) => {
+    return collectionUpdatorActions.match(action, {
+        OPEN_COLLECTION_UPDATOR: ({ uuid }) => updateCollection(state, { uuid, opened: true }),
+        CLOSE_COLLECTION_UPDATOR: () => updateCollection(state, { opened: false }),
+        UPDATE_COLLECTION_SUCCESS: () => updateCollection(state, { opened: false, uuid: "" }),
+        default: () => state
+    });
+};
index 03212b9fc915bacfbc08f22d367d393b1f26b14b..c4acf5aa9b3710fac0a3f61a905132651f8709da 100644 (file)
@@ -3,10 +3,10 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { unionize, ofType, UnionOf } from "unionize";
-import { CommonResourceService } from "../../common/api/common-resource-service";
 import { Dispatch } from "redux";
-import { apiClient } from "../../common/api/server-api";
 import { Resource, ResourceKind } from "../../models/resource";
+import { RootState } from "../store";
+import { ServiceRepository } from "../../services/services";
 
 export const detailsPanelActions = unionize({
     TOGGLE_DETAILS_PANEL: ofType<{}>(),
@@ -17,23 +17,20 @@ export const detailsPanelActions = unionize({
 export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
 
 export const loadDetails = (uuid: string, kind: ResourceKind) =>
-    (dispatch: Dispatch) => {
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(detailsPanelActions.LOAD_DETAILS({ uuid, kind }));
-        getService(kind)
-            .get(uuid)
-            .then(project => {
-                dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item: project }));
-            });
+        const item = await getService(services, kind).get(uuid);
+        dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item }));
     };
 
-const getService = (kind: ResourceKind) => {
+const getService = (services: ServiceRepository, kind: ResourceKind) => {
     switch (kind) {
         case ResourceKind.PROJECT:
-            return new CommonResourceService(apiClient, "groups");
+            return services.projectService;
         case ResourceKind.COLLECTION:
-            return new CommonResourceService(apiClient, "collections");
+            return services.collectionService;
         default:
-            return new CommonResourceService(apiClient, "");
+            return services.projectService;
     }
 };
 
index 8908fff7083edc1d261239d057128a3bb60b2010..62d9ae2ab6330632f7a063f50989793eb3149a3c 100644 (file)
@@ -8,7 +8,7 @@ import { RootState } from "../store";
 import { DataColumns } from "../../components/data-table/data-table";
 import { FavoritePanelItem, resourceToDataItem } from "../../views/favorite-panel/favorite-panel-item";
 import { FavoriteOrderBuilder } from "../../services/favorite-service/favorite-order-builder";
-import { favoriteService, authService } from "../../services/services";
+import { ServiceRepository } from "../../services/services";
 import { SortDirection } from "../../components/data-table/data-column";
 import { FilterBuilder } from "../../common/api/filter-builder";
 import { LinkResource } from "../../models/link";
@@ -17,7 +17,7 @@ import { favoritePanelActions } from "./favorite-panel-action";
 import { Dispatch, MiddlewareAPI } from "redux";
 
 export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService {
-    constructor(id: string) {
+    constructor(private services: ServiceRepository, id: string) {
         super(id);
     }
 
@@ -30,8 +30,8 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
         const typeFilters = getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
         const order = FavoriteOrderBuilder.create();
         if (typeFilters.length > 0) {
-            favoriteService
-                .list(authService.getUuid()!, {
+            this.services.favoriteService
+                .list(this.services.authService.getUuid()!, {
                     limit: dataExplorer.rowsPerPage,
                     offset: dataExplorer.page * dataExplorer.rowsPerPage,
                     order: sortColumn!.name === FavoritePanelColumnNames.NAME
index eb4f649025d3b03d27b3c342b410519c2f074734..38229dff8390424fd26686da2158d6f59b22ceea 100644 (file)
@@ -4,10 +4,10 @@
 
 import { unionize, ofType, UnionOf } from "unionize";
 import { Dispatch } from "redux";
-import { favoriteService } from "../../services/services";
 import { RootState } from "../store";
 import { checkFavorite } from "./favorites-reducer";
 import { snackbarActions } from "../snackbar/snackbar-actions";
+import { ServiceRepository } from "../../services/services";
 
 export const favoritesActions = unionize({
     TOGGLE_FAVORITE: ofType<{ resourceUuid: string }>(),
@@ -18,14 +18,14 @@ export const favoritesActions = unionize({
 export type FavoritesAction = UnionOf<typeof favoritesActions>;
 
 export const toggleFavorite = (resource: { uuid: string; name: string }) =>
-    (dispatch: Dispatch, getState: () => RootState): Promise<any> => {
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
         const userUuid = getState().auth.user!.uuid;
         dispatch(favoritesActions.TOGGLE_FAVORITE({ resourceUuid: resource.uuid }));
         dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Working..." }));
         const isFavorite = checkFavorite(resource.uuid, getState().favorites);
         const promise: any = isFavorite
-            ? favoriteService.delete({ userUuid, resourceUuid: resource.uuid })
-            : favoriteService.create({ userUuid, resource });
+            ? services.favoriteService.delete({ userUuid, resourceUuid: resource.uuid })
+            : services.favoriteService.create({ userUuid, resource });
 
         return promise
             .then(() => {
@@ -41,12 +41,12 @@ export const toggleFavorite = (resource: { uuid: string; name: string }) =>
     };
 
 export const checkPresenceInFavorites = (resourceUuids: string[]) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const userUuid = getState().auth.user!.uuid;
         dispatch(favoritesActions.CHECK_PRESENCE_IN_FAVORITES(resourceUuids));
-        favoriteService
+        services.favoriteService
             .checkPresenceInFavorites(userUuid, resourceUuids)
-            .then(results => {
+            .then((results: any) => {
                 dispatch(favoritesActions.UPDATE_FAVORITES(results));
             });
     };
index 761ec188bde282019e7421b34654ef732b33806b..8d3f06a59b3732637a9c2c73997ecbb8f8956151 100644 (file)
@@ -6,7 +6,7 @@ import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-mi
 import { ProjectPanelColumnNames, ProjectPanelFilter } from "../../views/project-panel/project-panel";
 import { RootState } from "../store";
 import { DataColumns } from "../../components/data-table/data-table";
-import { groupsService } from "../../services/services";
+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 } from "../../common/api/order-builder";
@@ -18,7 +18,7 @@ import { projectPanelActions } from "./project-panel-action";
 import { Dispatch, MiddlewareAPI } from "redux";
 
 export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
-    constructor(id: string) {
+    constructor(private services: ServiceRepository, id: string) {
         super(id);
     }
 
@@ -31,7 +31,7 @@ export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService
         const sortColumn = dataExplorer.columns.find(({ sortDirection }) => Boolean(sortDirection && sortDirection !== "none"));
         const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC ? SortDirection.ASC : SortDirection.DESC;
         if (typeFilters.length > 0) {
-            groupsService
+            this.services.groupsService
                 .contents(state.projects.currentItemId, {
                     limit: dataExplorer.rowsPerPage,
                     offset: dataExplorer.page * dataExplorer.rowsPerPage,
index cf38456109be0b25625214773f771c5eabc51713..2f9963bf9b3510f8b726475ba2fa35eee7864f58 100644 (file)
@@ -4,11 +4,11 @@
 import { default as unionize, ofType, UnionOf } from "unionize";
 
 import { ProjectResource } from "../../models/project";
-import { projectService } from "../../services/services";
 import { Dispatch } from "redux";
 import { FilterBuilder } from "../../common/api/filter-builder";
 import { RootState } from "../store";
 import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { ServiceRepository } from "../../services/services";
 
 export const projectActions = unionize({
     OPEN_PROJECT_CREATOR: ofType<{ ownerUuid: string }>(),
@@ -26,9 +26,9 @@ export const projectActions = unionize({
         value: 'payload'
     });
 
-export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch, getState: () => RootState) => {
+export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
     dispatch(projectActions.PROJECTS_REQUEST(parentUuid));
-    return projectService.list({
+    return services.projectService.list({
         filters: FilterBuilder
             .create<ProjectResource>()
             .addEqual("ownerUuid", parentUuid)
@@ -40,11 +40,11 @@ export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch,
 };
 
 export const createProject = (project: Partial<ProjectResource>) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const { ownerUuid } = getState().projects.creator;
         const projectData = { ownerUuid, ...project };
         dispatch(projectActions.CREATE_PROJECT(projectData));
-        return projectService
+        return services.projectService
             .create(projectData)
             .then(project => dispatch(projectActions.CREATE_PROJECT_SUCCESS(project)));
     };
index 3eda005484124dd14b3955f17f2d49ccb4c8764d..6e57465cc388a8e3e83d720ff9b9606918482a50 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { createStore, applyMiddleware, compose, Middleware, combineReducers } from 'redux';
+import { createStore, applyMiddleware, compose, Middleware, combineReducers, Store, Action, Dispatch } from 'redux';
 import { routerMiddleware, routerReducer, RouterState } from "react-router-redux";
 import thunkMiddleware from 'redux-thunk';
 import { History } from "history";
@@ -23,9 +23,10 @@ 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 { CollectionCreatorState, collectionCreationReducer } from './collections/creator/collection-creator-reducer';
 import { CollectionPanelState, collectionPanelReducer } from './collection-panel/collection-panel-reducer';
 import { DialogState, dialogReducer } from './dialog/dialog-reducer';
+import { CollectionsState, collectionsReducer } from './collections/collections-reducer';
+import { ServiceRepository } from "../services/services";
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -35,7 +36,7 @@ const composeEnhancers =
 export interface RootState {
     auth: AuthState;
     projects: ProjectState;
-    collectionCreation: CollectionCreatorState;
+    collections: CollectionsState;
     router: RouterState;
     dataExplorer: DataExplorerState;
     sidePanel: SidePanelState;
@@ -48,34 +49,36 @@ export interface RootState {
     dialog: DialogState;
 }
 
-const rootReducer = combineReducers({
-    auth: authReducer,
-    projects: projectsReducer,
-    collectionCreation: collectionCreationReducer,
-    router: routerReducer,
-    dataExplorer: dataExplorerReducer,
-    sidePanel: sidePanelReducer,
-    collectionPanel: collectionPanelReducer,
-    detailsPanel: detailsPanelReducer,
-    contextMenu: contextMenuReducer,
-    form: formReducer,
-    favorites: favoritesReducer,
-    snackbar: snackbarReducer,
-    collectionPanelFiles: collectionPanelFilesReducer,
-    dialog: dialogReducer
-});
+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
+    });
 
-export function configureStore(history: History) {
     const projectPanelMiddleware = dataExplorerMiddleware(
-        new ProjectPanelMiddlewareService(PROJECT_PANEL_ID)
+        new ProjectPanelMiddlewareService(services, PROJECT_PANEL_ID)
     );
     const favoritePanelMiddleware = dataExplorerMiddleware(
-        new FavoritePanelMiddlewareService(FAVORITE_PANEL_ID)
+        new FavoritePanelMiddlewareService(services, FAVORITE_PANEL_ID)
     );
 
     const middlewares: Middleware[] = [
         routerMiddleware(history),
-        thunkMiddleware,
+        thunkMiddleware.withExtraArgument(services),
         projectPanelMiddleware,
         favoritePanelMiddleware
     ];
index 1d017ccdffe754ab0fa7ca1dc2777b5fcd985c61..0ae41c657e34b925acb909084b848b9bb4cce31c 100644 (file)
@@ -5,12 +5,13 @@
 import { Redirect, RouteProps } from "react-router";
 import * as React from "react";
 import { connect, DispatchProp } from "react-redux";
-import { authActions, getUserDetails } from "../../store/auth/auth-action";
-import { authService } from "../../services/services";
+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";
 
 interface ApiTokenProps {
+    authService: AuthService;
 }
 
 export const ApiToken = connect()(
@@ -18,9 +19,9 @@ export const ApiToken = connect()(
         componentDidMount() {
             const search = this.props.location ? this.props.location.search : "";
             const apiToken = getUrlParameter(search, 'api_token');
-            this.props.dispatch(authActions.SAVE_API_TOKEN(apiToken));
+            this.props.dispatch(saveApiToken(apiToken));
             this.props.dispatch<any>(getUserDetails()).then(() => {
-                const rootUuid = authService.getRootUuid();
+                const rootUuid = this.props.authService.getRootUuid();
                 this.props.dispatch(getProjectList(rootUuid));
             });
         }
index fbb5c864b8a2a608a5b5904229054b50728a9f66..566f8f12e41c85cef26b7b945544b8c050811996 100644 (file)
@@ -5,8 +5,8 @@
 import { ContextMenuActionSet } from "../context-menu-action-set";
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "../../../store/favorites/favorites-actions";
-import { dataExplorerActions } from "../../../store/data-explorer/data-explorer-action";
 import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, ProvenanceGraphIcon, AdvancedIcon, RemoveIcon } from "../../../components/icon/icon";
+import { openUpdator } from "../../../store/collections/updator/collection-updator-action";
 import { favoritePanelActions } from "../../../store/favorite-panel/favorite-panel-action";
 
 export const collectionActionSet: ContextMenuActionSet = [[
@@ -14,7 +14,7 @@ export const collectionActionSet: ContextMenuActionSet = [[
         icon: RenameIcon,
         name: "Edit collection",
         execute: (dispatch, resource) => {
-            // add code
+            dispatch<any>(openUpdator(resource.uuid));
         }
     },
     {
@@ -35,7 +35,7 @@ export const collectionActionSet: ContextMenuActionSet = [[
         component: ToggleFavoriteAction,
         execute: (dispatch, resource) => {
             dispatch<any>(toggleFavorite(resource)).then(() => {
-                dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+                dispatch<any>(favoritePanelActions.REQUEST_ITEMS());           
             });
         }
     },
index e4474a562585e5c50d8aaaabc0f88f21d8785bd4..0ba2b22add6e33179a04cba44b8e5e3228be6310 100644 (file)
@@ -14,7 +14,7 @@ import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
 import { snackbarActions } from "../../store/snackbar/snackbar-actions";
 
 const mapStateToProps = (state: RootState) => ({
-    open: state.collectionCreation.creator.opened
+    open: state.collections.creator.opened
 });
 
 const mapDispatchToProps = (dispatch: Dispatch) => ({
@@ -33,7 +33,7 @@ const addCollection = (data: { name: string, description: string }) =>
     (dispatch: Dispatch) => {
         return dispatch<any>(createCollection(data)).then(() => {
             dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Created a new collection",
+                message: "Collection has been successfully created.",
                 hideDuration: 2000
             }));
             dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
index d0f793bfd854887df5b3afc1dbf6685d5b4290c0..3e3b74aa92747d9adf5053d543097a1110ee3ace 100644 (file)
@@ -14,7 +14,7 @@ import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress }
 
 import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
 
-type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "dialog" | "dialogTitle" | "createProgress" | "dialogActions";
+type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "createProgress" | "dialogActions";
 
 const styles: StyleRulesCallback<CssRules> = theme => ({
     button: {
@@ -27,17 +27,9 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
     formContainer: {
         display: "flex",
         flexDirection: "column",
-        marginTop: "20px",
-    },
-    dialogTitle: {
-        paddingBottom: "0"
     },
     textField: {
-        marginTop: "32px",
-    },
-    dialog: {
-        minWidth: "600px",
-        minHeight: "320px"
+        marginBottom: theme.spacing.unit * 3
     },
     createProgress: {
         position: "absolute",
@@ -45,7 +37,7 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
         right: "110px"
     },
     dialogActions: {
-        marginBottom: "24px"
+        marginBottom: theme.spacing.unit * 3
     }
 });
 interface DialogCollectionCreateProps {
@@ -77,39 +69,41 @@ export const DialogCollectionCreate = compose(
                 <Dialog
                     open={open}
                     onClose={handleClose}
+                    fullWidth={true}
+                    maxWidth='sm'
                     disableBackdropClick={true}
                     disableEscapeKeyDown={true}>
-                    <div className={classes.dialog}>
-                        <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
-                            <DialogTitle id="form-dialog-title" className={classes.dialogTitle}>Create a collection</DialogTitle>
-                            <DialogContent className={classes.formContainer}>
-                                <Field name="name"
-                                       component={this.renderTextField}
-                                       floatinglabeltext="Collection Name"
-                                       validate={COLLECTION_NAME_VALIDATION}
-                                       className={classes.textField}
-                                       label="Collection Name"/>
-                                <Field name="description"
-                                       component={this.renderTextField}
-                                       floatinglabeltext="Description - optional"
-                                       validate={COLLECTION_DESCRIPTION_VALIDATION}
-                                       className={classes.textField}
-                                       label="Description - optional"/>
-                            </DialogContent>
-                            <DialogActions className={classes.dialogActions}>
-                                <Button onClick={handleClose} className={classes.button} color="primary"
-                                        disabled={submitting}>CANCEL</Button>
-                                <Button type="submit"
-                                        className={classes.lastButton}
-                                        color="primary"
-                                        disabled={invalid || submitting || pristine}
-                                        variant="contained">
-                                    CREATE A COLLECTION
-                                </Button>
-                                {submitting && <CircularProgress size={20} className={classes.createProgress}/>}
-                            </DialogActions>
-                        </form>
-                    </div>
+                    <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
+                        <DialogTitle id="form-dialog-title">Create a collection</DialogTitle>
+                        <DialogContent className={classes.formContainer}>
+                            <Field name="name"
+                                    disabled={submitting}
+                                    component={this.renderTextField}
+                                    floatinglabeltext="Collection Name"
+                                    validate={COLLECTION_NAME_VALIDATION}
+                                    className={classes.textField}
+                                    label="Collection Name"/>
+                            <Field name="description"
+                                    disabled={submitting}
+                                    component={this.renderTextField}
+                                    floatinglabeltext="Description - optional"
+                                    validate={COLLECTION_DESCRIPTION_VALIDATION}
+                                    className={classes.textField}
+                                    label="Description - optional"/>
+                        </DialogContent>
+                        <DialogActions className={classes.dialogActions}>
+                            <Button onClick={handleClose} className={classes.button} color="primary"
+                                    disabled={submitting}>CANCEL</Button>
+                            <Button type="submit"
+                                    className={classes.lastButton}
+                                    color="primary"
+                                    disabled={invalid || submitting || pristine}
+                                    variant="contained">
+                                CREATE A COLLECTION
+                            </Button>
+                            {submitting && <CircularProgress size={20} className={classes.createProgress}/>}
+                        </DialogActions>
+                    </form>
                 </Dialog>
             );
         }
diff --git a/src/views-components/dialog-update/dialog-collection-update.tsx b/src/views-components/dialog-update/dialog-collection-update.tsx
new file mode 100644 (file)
index 0000000..80a82b2
--- /dev/null
@@ -0,0 +1,131 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { reduxForm, Field } from 'redux-form';
+import { compose } from 'redux';
+import { ArvadosTheme } from '../../common/custom-theme';
+import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '../../../node_modules/@material-ui/core';
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
+import { COLLECTION_FORM_NAME } from '../../store/collections/updator/collection-updator-action';
+
+type CssRules = 'content' | 'actions' | 'textField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    content: {
+        display: 'flex',
+        flexDirection: 'column'
+    },
+    actions: {
+        margin: 0,
+        padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3 - theme.spacing.unit / 2}px 
+                ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`
+    },
+    textField: {
+        marginBottom: theme.spacing.unit * 3
+    },
+    buttonWrapper: {
+        position: 'relative'
+    },
+    saveButton: {
+        boxShadow: 'none'
+    },
+    circularProgress: {
+        position: 'absolute',
+        top: 0,
+        bottom: 0,
+        left: 0,
+        right: 0,
+        margin: 'auto'
+    }
+});
+
+interface DialogCollectionDataProps {
+    open: boolean;
+    handleSubmit: any;
+    submitting: boolean;
+    invalid: boolean;
+    pristine: boolean;
+}
+
+interface DialogCollectionAction {
+    handleClose: () => void;
+    onSubmit: (data: { name: string, description: string }) => void;
+}
+
+type DialogCollectionProps = DialogCollectionDataProps & DialogCollectionAction & WithStyles<CssRules>;
+
+interface TextFieldProps {
+    label: string;
+    floatinglabeltext: string;
+    className?: string;
+    input?: string;
+    meta?: any;
+}
+
+export const DialogCollectionUpdate = compose(
+    reduxForm({ form: COLLECTION_FORM_NAME }),
+    withStyles(styles))(
+
+        class DialogCollectionUpdate extends React.Component<DialogCollectionProps> {
+
+            render() {
+                const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine } = this.props;
+                return (
+                    <Dialog open={open}
+                        onClose={handleClose}
+                        fullWidth={true}
+                        maxWidth='sm'
+                        disableBackdropClick={true}
+                        disableEscapeKeyDown={true}>
+
+                        <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
+                            <DialogTitle>Edit Collection</DialogTitle>
+                            <DialogContent className={classes.content}>
+                                <Field name="name"
+                                    disabled={submitting}
+                                    component={this.renderTextField}
+                                    floatinglabeltext="Collection Name"
+                                    validate={COLLECTION_NAME_VALIDATION}
+                                    className={classes.textField}
+                                    label="Collection Name" />
+                                <Field name="description"
+                                    disabled={submitting}
+                                    component={this.renderTextField}
+                                    floatinglabeltext="Description - optional"
+                                    validate={COLLECTION_DESCRIPTION_VALIDATION}
+                                    className={classes.textField}
+                                    label="Description - optional" />
+                            </DialogContent>
+                            <DialogActions className={classes.actions}>
+                                <Button onClick={handleClose} color="primary"
+                                    disabled={submitting}>CANCEL</Button>
+                                <div className={classes.buttonWrapper}>
+                                    <Button type="submit" className={classes.saveButton}
+                                        color="primary"
+                                        disabled={invalid || submitting || pristine}
+                                        variant="contained">
+                                        SAVE
+                                    </Button>
+                                    {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
+                                </div>
+                            </DialogActions>
+                        </form>
+                    </Dialog>
+                );
+            }
+
+            renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
+                <TextField
+                    helperText={touched && error}
+                    label={label}
+                    className={this.props.classes.textField}
+                    error={touched && !!error}
+                    autoComplete='off'
+                    {...input}
+                    {...custom}
+                />
+            )
+        }
+    );
\ No newline at end of file
diff --git a/src/views-components/update-collection-dialog/update-collection-dialog..tsx b/src/views-components/update-collection-dialog/update-collection-dialog..tsx
new file mode 100644 (file)
index 0000000..a91277e
--- /dev/null
@@ -0,0 +1,44 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
+import { SubmissionError } from "redux-form";
+import { RootState } from "../../store/store";
+import { snackbarActions } from "../../store/snackbar/snackbar-actions";
+import { collectionUpdatorActions, updateCollection } from "../../store/collections/updator/collection-updator-action";
+import { dataExplorerActions } from "../../store/data-explorer/data-explorer-action";
+import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
+import { DialogCollectionUpdate } from "../dialog-update/dialog-collection-update";
+
+const mapStateToProps = (state: RootState) => ({
+    open: state.collections.updator.opened
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+    handleClose: () => {
+        dispatch(collectionUpdatorActions.CLOSE_COLLECTION_UPDATOR());
+    },
+    onSubmit: (data: { name: string, description: string }) => {
+        return dispatch<any>(editCollection(data))
+            .catch((e: any) => {
+                if(e.errors) {
+                    throw new SubmissionError({ name: e.errors.join("").includes("UniqueViolation") ? "Collection with this name already exists." : "" });
+                }
+            });
+    }
+});
+
+const editCollection = (data: { name: string, description: string }) =>
+    (dispatch: Dispatch) => {
+        return dispatch<any>(updateCollection(data)).then(() => {
+            dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: "Collection has been successfully updated.",
+                hideDuration: 2000
+            }));
+            dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+        });
+    };
+
+export const UpdateCollectionDialog = connect(mapStateToProps, mapDispatchToProps)(DialogCollectionUpdate);
\ No newline at end of file
index a7d2682ae61015403ed0b8a10b84f1127fae9c86..05104eecbb2616d149fc54010e39f963d2c9e1c4 100644 (file)
@@ -5,18 +5,19 @@
 import * as React from 'react';
 import { 
     StyleRulesCallback, WithStyles, withStyles, Card, 
-    CardHeader, IconButton, CardContent, Grid
+    CardHeader, IconButton, CardContent, Grid, Chip
 } from '@material-ui/core';
 import { connect } from 'react-redux';
 import { RouteComponentProps } from 'react-router';
 import { ArvadosTheme } from '../../common/custom-theme';
 import { RootState } from '../../store/store';
-import { MoreOptionsIcon, CollectionIcon } from '../../components/icon/icon';
+import { MoreOptionsIcon, CollectionIcon, CopyIcon } from '../../components/icon/icon';
 import { DetailsAttribute } from '../../components/details-attribute/details-attribute';
 import { CollectionResource } from '../../models/collection';
 import { CollectionPanelFiles } from '../../views-components/collection-panel-files/collection-panel-files';
+import * as CopyToClipboard from 'react-copy-to-clipboard';
 
-type CssRules = 'card' | 'iconHeader';
+type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     card: {
@@ -25,6 +26,14 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     iconHeader: {
         fontSize: '1.875rem',
         color: theme.customs.colors.yellow700
+    },
+    tag: {
+        marginRight: theme.spacing.unit
+    },
+    copyIcon: {
+        marginLeft: theme.spacing.unit,
+        fontSize: '1.125rem',
+        cursor: 'pointer'
     }
 });
 
@@ -40,6 +49,7 @@ interface CollectionPanelActionProps {
 type CollectionPanelProps = CollectionPanelDataProps & CollectionPanelActionProps 
                             & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
 
+
 export const CollectionPanel = withStyles(styles)(
     connect((state: RootState) => ({ item: state.collectionPanel.item }))(
         class extends React.Component<CollectionPanelProps> { 
@@ -57,12 +67,17 @@ export const CollectionPanel = withStyles(styles)(
                                         <MoreOptionsIcon />
                                     </IconButton> 
                                 }
-                                title={item && item.name } />
+                                title={item && item.name } 
+                                subheader={item && item.description} />
                             <CardContent>
                                 <Grid container direction="column">
                                     <Grid item xs={6}>
-                                    <DetailsAttribute label='Collection UUID' value={item && item.uuid} />
-                                        <DetailsAttribute label='Content size' value='54 MB' />
+                                    <DetailsAttribute label='Collection UUID' value={item && item.uuid}>
+                                        <CopyToClipboard text={item && item.uuid}>
+                                            <CopyIcon className={classes.copyIcon} />
+                                        </CopyToClipboard>
+                                    </DetailsAttribute>
+                                    <DetailsAttribute label='Content size' value='54 MB' />
                                     <DetailsAttribute label='Owner' value={item && item.ownerUuid} />
                                     </Grid>
                                 </Grid>
@@ -74,7 +89,9 @@ export const CollectionPanel = withStyles(styles)(
                             <CardContent>
                                 <Grid container direction="column">
                                     <Grid item xs={4}>
-                                        Tags
+                                        <Chip label="Tag 1" className={classes.tag}/>
+                                        <Chip label="Tag 2" className={classes.tag}/>
+                                        <Chip label="Tag 3" className={classes.tag}/>
                                     </Grid>
                                 </Grid>
                             </CardContent>
index 538b8e780f352489a4c0e15e6bf5493e2f0ee96d..8e0b353f6558f26200de817d8e9edb3586361a0d 100644 (file)
@@ -11,12 +11,13 @@ import createBrowserHistory from "history/createBrowserHistory";
 import { ConnectedRouter } from "react-router-redux";
 import { MuiThemeProvider } from '@material-ui/core/styles';
 import { CustomTheme } from '../../common/custom-theme';
+import { createServices } from "../../services/services";
 
 const history = createBrowserHistory();
 
 it('renders without crashing', () => {
     const div = document.createElement('div');
-    const store = configureStore(createBrowserHistory());
+    const store = configureStore(createBrowserHistory(), createServices("/arvados/v1"));
     ReactDOM.render(
         <MuiThemeProvider theme={CustomTheme}>
             <Provider store={store}>
index b543bef7f20cc220cd743c1532925a46239723c0..3611d7b10fb12210f9573f9ebf2df41476d62693 100644 (file)
@@ -7,7 +7,7 @@ import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/st
 import Drawer from '@material-ui/core/Drawer';
 import { connect, DispatchProp } from "react-redux";
 import { Route, Switch, RouteComponentProps } from "react-router";
-import { authActions } from "../../store/auth/auth-action";
+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';
@@ -25,7 +25,6 @@ import { ProjectPanel } from "../project-panel/project-panel";
 import { DetailsPanel } from '../../views-components/details-panel/details-panel';
 import { ArvadosTheme } from '../../common/custom-theme';
 import { CreateProjectDialog } from "../../views-components/create-project-dialog/create-project-dialog";
-import { authService } from '../../services/services';
 
 import { detailsPanelActions, loadDetails } from "../../store/details-panel/details-panel-action";
 import { contextMenuActions } from "../../store/context-menu/context-menu-actions";
@@ -44,9 +43,11 @@ import { loadCollection } from '../../store/collection-panel/collection-panel-ac
 import { getCollectionUrl } from '../../models/collection';
 import { RemoveDialog } from '../../views-components/remove-dialog/remove-dialog';
 import { RenameDialog } from '../../views-components/rename-dialog/rename-dialog';
+import { UpdateCollectionDialog } from '../../views-components/update-collection-dialog/update-collection-dialog.';
+import { AuthService } from "../../services/auth-service/auth-service";
 
-const drawerWidth = 240;
-const appBarHeight = 100;
+const DRAWER_WITDH = 240;
+const APP_BAR_HEIGHT = 100;
 
 type CssRules = 'root' | 'appBar' | 'drawerPaper' | 'content' | 'contentWrapper' | 'toolbar';
 
@@ -67,7 +68,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
     drawerPaper: {
         position: 'relative',
-        width: drawerWidth,
+        width: DRAWER_WITDH,
         display: 'flex',
         flexDirection: 'column',
     },
@@ -76,7 +77,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         display: "flex",
         flexGrow: 1,
         minWidth: 0,
-        paddingTop: appBarHeight
+        paddingTop: APP_BAR_HEIGHT
     },
     content: {
         padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3}px`,
@@ -94,10 +95,14 @@ interface WorkbenchDataProps {
     sidePanelItems: SidePanelItem[];
 }
 
+interface WorkbenchServiceProps {
+    authService: AuthService;
+}
+
 interface WorkbenchActionProps {
 }
 
-type WorkbenchProps = WorkbenchDataProps & WorkbenchActionProps & DispatchProp & WithStyles<CssRules>;
+type WorkbenchProps = WorkbenchDataProps & WorkbenchServiceProps & WorkbenchActionProps & DispatchProp<any> & WithStyles<CssRules>;
 
 interface NavBreadcrumb extends Breadcrumb {
     itemId: string;
@@ -143,7 +148,7 @@ export const Workbench = withStyles(styles)(
                         },
                         {
                             label: "Logout",
-                            action: () => this.props.dispatch(authActions.LOGOUT())
+                            action: () => this.props.dispatch(logout())
                         },
                         {
                             label: "My account",
@@ -159,7 +164,7 @@ export const Workbench = withStyles(styles)(
                     anonymousMenu: [
                         {
                             label: "Sign in",
-                            action: () => this.props.dispatch(authActions.LOGIN())
+                            action: () => this.props.dispatch(login())
                         }
                     ]
                 }
@@ -196,22 +201,22 @@ export const Workbench = withStyles(styles)(
                                     toggleActive={this.toggleSidePanelActive}
                                     sidePanelItems={this.props.sidePanelItems}
                                     onContextMenu={(event) => this.openContextMenu(event, {
-                                        uuid: authService.getUuid() || "",
+                                        uuid: this.props.authService.getUuid() || "",
                                         name: "",
                                         kind: ContextMenuKind.ROOT_PROJECT
                                     })}>
                                     <ProjectTree
                                         projects={this.props.projects}
-                                        toggleOpen={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.OPEN))}
+                                        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<any>(setProjectItem(itemId, ItemMode.ACTIVE));
-                                            this.props.dispatch<any>(loadDetails(itemId, ResourceKind.PROJECT));
-                                            this.props.dispatch<any>(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
+                                            this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE));
+                                            this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
+                                            this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
                                         }} />
                                 </SidePanel>
                             </Drawer>}
@@ -231,6 +236,7 @@ export const Workbench = withStyles(styles)(
                         <CreateCollectionDialog />
                         <RemoveDialog />
                         <RenameDialog />
+                        <UpdateCollectionDialog />
                         <CurrentTokenDialog
                             currentToken={this.props.currentToken}
                             open={this.state.isCurrentTokenDialogOpen}
@@ -251,7 +257,7 @@ export const Workbench = withStyles(styles)(
                 {...props} />
 
             renderProjectPanel = (props: RouteComponentProps<{ id: string }>) => <ProjectPanel
-                onItemRouteChange={itemId => this.props.dispatch<any>(setProjectItem(itemId, ItemMode.ACTIVE))}
+                onItemRouteChange={itemId => this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE))}
                 onContextMenu={(event, item) => {
 
                     const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE;
@@ -264,23 +270,23 @@ export const Workbench = withStyles(styles)(
                 onProjectCreationDialogOpen={this.handleProjectCreationDialogOpen}
                 onCollectionCreationDialogOpen={this.handleCollectionCreationDialogOpen}
                 onItemClick={item => {
-                    this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
+                    this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
                 }}
                 onItemDoubleClick={item => {
                     switch (item.kind) {
                         case ResourceKind.COLLECTION:
-                            this.props.dispatch<any>(loadCollection(item.uuid, item.kind as ResourceKind));
+                            this.props.dispatch(loadCollection(item.uuid, item.kind as ResourceKind));
                             this.props.dispatch(push(getCollectionUrl(item.uuid)));
-                        default:
-                            this.props.dispatch<any>(setProjectItem(item.uuid, ItemMode.ACTIVE));
-                            this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
+                        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<any>(favoritePanelActions.REQUEST_ITEMS())}
+                onItemRouteChange={() => this.props.dispatch(favoritePanelActions.REQUEST_ITEMS())}
                 onContextMenu={(event, item) => {
                     const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE;
                     this.openContextMenu(event, {
@@ -291,17 +297,17 @@ export const Workbench = withStyles(styles)(
                 }}
                 onDialogOpen={this.handleProjectCreationDialogOpen}
                 onItemClick={item => {
-                    this.props.dispatch<any>(loadDetails(item.uuid, item.kind as ResourceKind));
+                    this.props.dispatch(loadDetails(item.uuid, item.kind as ResourceKind));
                 }}
                 onItemDoubleClick={item => {
                     switch (item.kind) {
                         case ResourceKind.COLLECTION:
-                            this.props.dispatch<any>(loadCollection(item.uuid, item.kind as ResourceKind));
+                            this.props.dispatch(loadCollection(item.uuid, item.kind as ResourceKind));
                             this.props.dispatch(push(getCollectionUrl(item.uuid)));
                         default:
-                            this.props.dispatch<any>(loadDetails(item.uuid, ResourceKind.PROJECT));
-                            this.props.dispatch<any>(setProjectItem(item.uuid, ItemMode.ACTIVE));
-                            this.props.dispatch<any>(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
+                            this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
+                            this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
+                            this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
                     }
 
                 }}
@@ -309,8 +315,8 @@ export const Workbench = withStyles(styles)(
 
             mainAppBarActions: MainAppBarActionProps = {
                 onBreadcrumbClick: ({ itemId }: NavBreadcrumb) => {
-                    this.props.dispatch<any>(setProjectItem(itemId, ItemMode.BOTH));
-                    this.props.dispatch<any>(loadDetails(itemId, ResourceKind.PROJECT));
+                    this.props.dispatch(setProjectItem(itemId, ItemMode.BOTH));
+                    this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
                 },
                 onSearch: searchText => {
                     this.setState({ searchText });
index 1eed5d2fda1e5adb4dff764d193c58d4e664a708..968562f7d160ce3a154ec11cb965bafdecb26f0f 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
   version "10.5.2"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707"
 
+"@types/react-copy-to-clipboard@4.2.5":
+  version "4.2.5"
+  resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-4.2.5.tgz#bda288b4256288676019b75ca86f1714bbd206d4"
+  dependencies:
+    "@types/react" "*"
+
 "@types/react-dom@16.0.6":
   version "16.0.6"
   resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.6.tgz#f1a65a4e7be8ed5d123f8b3b9eacc913e35a1a3c"
@@ -1817,6 +1823,12 @@ copy-descriptor@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
 
+copy-to-clipboard@^3:
+  version "3.0.8"
+  resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9"
+  dependencies:
+    toggle-selection "^1.0.3"
+
 core-js@^1.0.0:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
@@ -6098,6 +6110,13 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
+react-copy-to-clipboard@5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e"
+  dependencies:
+    copy-to-clipboard "^3"
+    prop-types "^15.5.8"
+
 react-dev-utils@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-5.0.1.tgz#1f396e161fe44b595db1b186a40067289bf06613"
@@ -7406,6 +7425,10 @@ to-regex@^3.0.1, to-regex@^3.0.2:
     regex-not "^1.0.2"
     safe-regex "^1.1.0"
 
+toggle-selection@^1.0.3:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
+
 toposort@^1.0.0:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"