[ARVADOS-WORKBENCH2] updated: 1.2.0-40-gf1065fb

Git user git at public.curoverse.com
Wed Aug 29 16:57:40 EDT 2018


Summary of changes:
 package.json                                       |   4 +-
 src/components/icon/icon.tsx                       |   2 +
 .../collection-service/collection-service.ts       |  19 ++-
 src/services/groups-service/groups-service.ts      |  24 +++-
 src/services/services.ts                           |   3 -
 src/services/trash-service/trash-service.test.ts   |  17 ---
 src/services/trash-service/trash-service.ts        |  12 --
 src/store/collections/collection-trash-actions.ts  |  39 ++++++
 src/store/context-menu/context-menu-reducer.ts     |   2 +
 src/store/navigation/navigation-action.ts          |   8 +-
 src/store/project/project-action.ts                |  40 +++++-
 src/store/project/project-reducer.test.ts          |   4 +-
 src/store/project/project-reducer.ts               | 154 +++++++++------------
 src/store/side-panel/side-panel-reducer.ts         |   2 +-
 .../trash-panel/trash-panel-middleware-service.ts  |   4 +-
 .../action-sets/collection-action-set.ts           |   8 ++
 .../action-sets/collection-resource-action-set.ts  |   8 ++
 .../context-menu/action-sets/project-action-set.ts |   9 +-
 .../{favorite-action.tsx => trash-action.tsx}      |  21 ++-
 src/views/collection-panel/collection-panel.tsx    |   2 -
 src/views/favorite-panel/favorite-panel-item.ts    |   4 +-
 src/views/project-panel/project-panel-item.ts      |   4 +-
 src/views/trash-panel/trash-panel-item.ts          |   2 +
 src/views/trash-panel/trash-panel.tsx              |   3 +-
 src/views/workbench/workbench.tsx                  |  30 ++--
 25 files changed, 257 insertions(+), 168 deletions(-)
 delete mode 100644 src/services/trash-service/trash-service.test.ts
 delete mode 100644 src/services/trash-service/trash-service.ts
 create mode 100644 src/store/collections/collection-trash-actions.ts
 copy src/views-components/context-menu/actions/{favorite-action.tsx => trash-action.tsx} (50%)

       via  f1065fb7260dd562ea2e826e9cbc508d7932ecca (commit)
      from  efe0283919eb18e60ad876eaf6edef03c6cf04b3 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit f1065fb7260dd562ea2e826e9cbc508d7932ecca
Author: Daniel Kos <daniel.kos at contractors.roche.com>
Date:   Tue Aug 21 21:46:18 2018 +0200

    Add trash view and trahsing/untrashing project/collection
    
    Feature #13828
    
    Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos at contractors.roche.com>

diff --git a/package.json b/package.json
index e2b6c4e..0264998 100644
--- a/package.json
+++ b/package.json
@@ -26,8 +26,8 @@
     "unionize": "2.1.2"
   },
   "scripts": {
-    "start": "react-scripts-ts start",
-    "build": "react-scripts-ts build",
+    "start": "REACT_APP_BUILD_NUMBER=$BUILD_NUMBER REACT_APP_GIT_COMMIT=$GIT_COMMIT react-scripts-ts start",
+    "build": "REACT_APP_BUILD_NUMBER=$BUILD_NUMBER REACT_APP_GIT_COMMIT=$GIT_COMMIT react-scripts-ts build",
     "test": "react-scripts-ts test --env=jsdom",
     "eject": "react-scripts-ts eject",
     "lint": "tslint src/** -t verbose"
diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx
index 0f0442a..86a1a68 100644
--- a/src/components/icon/icon.tsx
+++ b/src/components/icon/icon.tsx
@@ -32,6 +32,7 @@ import Person from '@material-ui/icons/Person';
 import PersonAdd from '@material-ui/icons/PersonAdd';
 import PlayArrow from '@material-ui/icons/PlayArrow';
 import RateReview from '@material-ui/icons/RateReview';
+import RestoreFromTrash from '@material-ui/icons/RestoreFromTrash';
 import Search from '@material-ui/icons/Search';
 import SettingsApplications from '@material-ui/icons/SettingsApplications';
 import Star from '@material-ui/icons/Star';
@@ -67,6 +68,7 @@ export const RecentIcon: IconType = (props) => <AccessTime {...props} />;
 export const RemoveIcon: IconType = (props) => <Delete {...props} />;
 export const RemoveFavoriteIcon: IconType = (props) => <Star {...props} />;
 export const RenameIcon: IconType = (props) => <Edit {...props} />;
+export const RestoreFromTrashIcon: IconType = (props) => <RestoreFromTrash {...props} />;
 export const ReRunProcessIcon: IconType = (props) => <Cached {...props} />;
 export const SearchIcon: IconType = (props) => <Search {...props} />;
 export const ShareIcon: IconType = (props) => <PersonAdd {...props} />;
diff --git a/src/services/collection-service/collection-service.ts b/src/services/collection-service/collection-service.ts
index 9feec69..ad493b5 100644
--- a/src/services/collection-service/collection-service.ts
+++ b/src/services/collection-service/collection-service.ts
@@ -76,7 +76,6 @@ export class CollectionService extends CommonResourceService<CollectionResource>
             });
     }
 
-
     private readFile(file: File): Promise<ArrayBuffer> {
         return new Promise<ArrayBuffer>(resolve => {
             const reader = new FileReader();
@@ -163,4 +162,22 @@ export class CollectionService extends CommonResourceService<CollectionResource>
             }
         });
     }
+
+    trash(uuid: string): Promise<CollectionResource> {
+        return this.serverApi
+            .post(this.resourceType + `${uuid}/trash`)
+            .then(CommonResourceService.mapResponseKeys);
+    }
+
+    untrash(uuid: string): Promise<CollectionResource> {
+        const params = {
+            ensure_unique_name: true
+        };
+        return this.serverApi
+            .post(this.resourceType + `${uuid}/untrash`, {
+                params: CommonResourceService.mapKeys(_.snakeCase)(params)
+            })
+            .then(CommonResourceService.mapResponseKeys);
+    }
+
 }
diff --git a/src/services/groups-service/groups-service.ts b/src/services/groups-service/groups-service.ts
index 39cc74a..4756aa3 100644
--- a/src/services/groups-service/groups-service.ts
+++ b/src/services/groups-service/groups-service.ts
@@ -5,10 +5,10 @@
 import * as _ from "lodash";
 import { CommonResourceService, ListResults } from "~/common/api/common-resource-service";
 import { AxiosInstance } from "axios";
-import { GroupResource } from "~/models/group";
 import { CollectionResource } from "~/models/collection";
 import { ProjectResource } from "~/models/project";
 import { ProcessResource } from "~/models/process";
+import { TrashResource } from "~/models/resource";
 
 export interface ContentsArguments {
     limit?: number;
@@ -24,7 +24,7 @@ export type GroupContentsResource =
     ProjectResource |
     ProcessResource;
 
-export class GroupsService<T extends GroupResource = GroupResource> extends CommonResourceService<T> {
+export class GroupsService<T extends TrashResource = TrashResource> extends CommonResourceService<T> {
 
     constructor(serverApi: AxiosInstance) {
         super(serverApi, "groups");
@@ -38,11 +38,29 @@ export class GroupsService<T extends GroupResource = GroupResource> extends Comm
             order: order ? order : undefined
         };
         return this.serverApi
-            .get(this.resourceType + `${uuid}/contents/`, {
+            .get(this.resourceType + `${uuid}/contents`, {
                 params: CommonResourceService.mapKeys(_.snakeCase)(params)
             })
             .then(CommonResourceService.mapResponseKeys);
     }
+
+    trash(uuid: string): Promise<T> {
+        return this.serverApi
+            .post(this.resourceType + `${uuid}/trash`)
+            .then(CommonResourceService.mapResponseKeys);
+    }
+
+    untrash(uuid: string): Promise<T> {
+        const params = {
+            ensure_unique_name: true
+        };
+        return this.serverApi
+            .post(this.resourceType + `${uuid}/untrash`, {
+                params: CommonResourceService.mapKeys(_.snakeCase)(params)
+            })
+            .then(CommonResourceService.mapResponseKeys);
+    }
+
 }
 
 export enum GroupContentsResourcePrefix {
diff --git a/src/services/services.ts b/src/services/services.ts
index 9199755..f63194d 100644
--- a/src/services/services.ts
+++ b/src/services/services.ts
@@ -14,7 +14,6 @@ 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 { TrashService } from "~/services/trash-service/trash-service";
 
 export type ServiceRepository = ReturnType<typeof createServices>;
 
@@ -31,7 +30,6 @@ export const createServices = (config: Config) => {
     const projectService = new ProjectService(apiClient);
     const linkService = new LinkService(apiClient);
     const favoriteService = new FavoriteService(linkService, groupsService);
-    const trashService = new TrashService(apiClient);
     const collectionService = new CollectionService(apiClient, keepService, webdavClient, authService);
     const tagService = new TagService(linkService);
     const collectionFilesService = new CollectionFilesService(collectionService);
@@ -45,7 +43,6 @@ export const createServices = (config: Config) => {
         projectService,
         linkService,
         favoriteService,
-        trashService,
         collectionService,
         tagService,
         collectionFilesService
diff --git a/src/services/trash-service/trash-service.test.ts b/src/services/trash-service/trash-service.test.ts
deleted file mode 100644
index f22d066..0000000
--- a/src/services/trash-service/trash-service.test.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { GroupsService } from "../groups-service/groups-service";
-import { TrashService } from "./trash-service";
-import { mockResourceService } from "~/common/api/common-resource-service.test";
-
-describe("TrashService", () => {
-
-    let groupService: GroupsService;
-
-    beforeEach(() => {
-        groupService = mockResourceService(GroupsService);
-    });
-
-});
diff --git a/src/services/trash-service/trash-service.ts b/src/services/trash-service/trash-service.ts
deleted file mode 100644
index fc02d2f..0000000
--- a/src/services/trash-service/trash-service.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { GroupsService } from "../groups-service/groups-service";
-import { AxiosInstance } from "axios";
-
-export class TrashService extends GroupsService {
-    constructor(serverApi: AxiosInstance) {
-        super(serverApi);
-    }
-}
diff --git a/src/store/collections/collection-trash-actions.ts b/src/store/collections/collection-trash-actions.ts
new file mode 100644
index 0000000..c6d4ee0
--- /dev/null
+++ b/src/store/collections/collection-trash-actions.ts
@@ -0,0 +1,39 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { RootState } from "~/store/store";
+import { ServiceRepository } from "~/services/services";
+import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { sidePanelActions } from "~/store/side-panel/side-panel-action";
+import { SidePanelId } from "~/store/side-panel/side-panel-reducer";
+import { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
+import { getProjectList, projectActions } from "~/store/project/project-action";
+
+export const toggleCollectionTrashed = (resource: { uuid: string; name: string, isTrashed?: boolean, ownerUuid?: string }) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Working..." }));
+        if (resource.isTrashed) {
+            return services.collectionService.untrash(resource.uuid).then(() => {
+                dispatch<any>(getProjectList(resource.ownerUuid)).then(() => {
+                    dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelId.PROJECTS));
+                    dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN({ itemId: resource.ownerUuid!!, open: true, recursive: true }));
+                });
+                dispatch(trashPanelActions.REQUEST_ITEMS());
+                dispatch(snackbarActions.CLOSE_SNACKBAR());
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: "Restored from trash",
+                    hideDuration: 2000
+                }));
+            });
+        } else {
+            return services.collectionService.trash(resource.uuid).then(() => {
+                dispatch(snackbarActions.CLOSE_SNACKBAR());
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: "Added to trash",
+                    hideDuration: 2000
+                }));
+            });
+        }
+    };
diff --git a/src/store/context-menu/context-menu-reducer.ts b/src/store/context-menu/context-menu-reducer.ts
index ac14c35..8026c1d 100644
--- a/src/store/context-menu/context-menu-reducer.ts
+++ b/src/store/context-menu/context-menu-reducer.ts
@@ -20,6 +20,8 @@ export interface ContextMenuResource {
     kind: string;
     name: string;
     description?: string;
+    isTrashed?: boolean;
+    ownerUuid?: string;
 }
 
 const initialState = {
diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts
index 50ec93d..ad70d9d 100644
--- a/src/store/navigation/navigation-action.ts
+++ b/src/store/navigation/navigation-action.ts
@@ -45,7 +45,7 @@ export const setProjectItem = (itemId: string, itemMode: ItemMode) =>
                 if (router.location && !router.location.pathname.includes(resourceUrl)) {
                     dispatch(push(resourceUrl));
                 }
-                dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(treeItem.data.uuid));
+                dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE({ itemId: treeItem.data.uuid }));
             }
 
             const promise = treeItem.status === TreeItemStatus.LOADED
@@ -55,7 +55,7 @@ export const setProjectItem = (itemId: string, itemMode: ItemMode) =>
             promise
                 .then(() => dispatch<any>(() => {
                     if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) {
-                        dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(treeItem.data.uuid));
+                        dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN({ itemId: treeItem.data.uuid }));
                     }
                     dispatch(projectPanelActions.RESET_PAGINATION());
                     dispatch(projectPanelActions.REQUEST_ITEMS());
@@ -63,7 +63,7 @@ export const setProjectItem = (itemId: string, itemMode: ItemMode) =>
         } else {
             const uuid = services.authService.getUuid();
             if (itemId === uuid) {
-                dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(uuid));
+                dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE({ itemId: uuid }));
                 dispatch(projectPanelActions.RESET_PAGINATION());
                 dispatch(projectPanelActions.REQUEST_ITEMS());
             }
@@ -77,7 +77,7 @@ export const restoreBranch = (itemId: string) =>
         await loadBranch(uuids, dispatch);
         dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelId.PROJECTS));
         uuids.forEach(uuid => {
-            dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(uuid));
+            dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN({ itemId: uuid }));
         });
     };
 
diff --git a/src/store/project/project-action.ts b/src/store/project/project-action.ts
index 2017658..bb5da19 100644
--- a/src/store/project/project-action.ts
+++ b/src/store/project/project-action.ts
@@ -11,6 +11,10 @@ import { checkPresenceInFavorites } from "../favorites/favorites-actions";
 import { ServiceRepository } from "~/services/services";
 import { projectPanelActions } from "~/store/project-panel/project-panel-action";
 import { updateDetails } from "~/store/details-panel/details-panel-action";
+import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
+import { sidePanelActions } from "~/store/side-panel/side-panel-action";
+import { SidePanelId } from "~/store/side-panel/side-panel-reducer";
 
 export const projectActions = unionize({
     OPEN_PROJECT_CREATOR: ofType<{ ownerUuid: string }>(),
@@ -23,8 +27,8 @@ export const projectActions = unionize({
     REMOVE_PROJECT: ofType<string>(),
     PROJECTS_REQUEST: ofType<string>(),
     PROJECTS_SUCCESS: ofType<{ projects: ProjectResource[], parentItemId?: string }>(),
-    TOGGLE_PROJECT_TREE_ITEM_OPEN: ofType<string>(),
-    TOGGLE_PROJECT_TREE_ITEM_ACTIVE: ofType<string>(),
+    TOGGLE_PROJECT_TREE_ITEM_OPEN: ofType<{ itemId: string, open?: boolean, recursive?: boolean }>(),
+    TOGGLE_PROJECT_TREE_ITEM_ACTIVE: ofType<{ itemId: string, active?: boolean, recursive?: boolean }>(),
     RESET_PROJECT_TREE_ACTIVITY: ofType<string>()
 }, {
     tag: 'type',
@@ -33,7 +37,7 @@ export const projectActions = unionize({
 
 export const PROJECT_FORM_NAME = 'projectEditDialog';
 
-export const getProjectList = (parentUuid: string = '') => 
+export const getProjectList = (parentUuid: string = '') =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(projectActions.PROJECTS_REQUEST(parentUuid));
         return services.projectService.list({
@@ -70,4 +74,34 @@ export const updateProject = (project: Partial<ProjectResource>) =>
             });
     };
 
+export const toggleProjectTrashed = (resource: { uuid: string; name: string, isTrashed?: boolean, ownerUuid?: string }) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Working..." }));
+        if (resource.isTrashed) {
+            return services.groupsService.untrash(resource.uuid).then(() => {
+                dispatch<any>(getProjectList(resource.ownerUuid)).then(() => {
+                    dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelId.PROJECTS));
+                    dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN({ itemId: resource.ownerUuid!!, open: true, recursive: true }));
+                });
+                dispatch(trashPanelActions.REQUEST_ITEMS());
+                dispatch(snackbarActions.CLOSE_SNACKBAR());
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: "Restored from trash",
+                    hideDuration: 2000
+                }));
+            });
+        } else {
+            return services.groupsService.trash(resource.uuid).then(() => {
+                dispatch<any>(getProjectList(resource.ownerUuid)).then(() => {
+                    dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN({ itemId: resource.ownerUuid!!, open: true, recursive: true }));
+                });
+                dispatch(snackbarActions.CLOSE_SNACKBAR());
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: "Added to trash",
+                    hideDuration: 2000
+                }));
+            });
+        }
+    };
+
 export type ProjectAction = UnionOf<typeof projectActions>;
diff --git a/src/store/project/project-reducer.test.ts b/src/store/project/project-reducer.test.ts
index bb60e39..cf47010 100644
--- a/src/store/project/project-reducer.test.ts
+++ b/src/store/project/project-reducer.test.ts
@@ -99,7 +99,7 @@ describe('project-reducer', () => {
             updater: { opened: false, uuid: '' }
         };
 
-        const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState.items[0].id));
+        const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE({ itemId: initialState.items[0].id }));
         expect(state).toEqual(project);
     });
 
@@ -131,7 +131,7 @@ describe('project-reducer', () => {
 
         };
 
-        const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState.items[0].id));
+        const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN({ itemId: initialState.items[0].id }));
         expect(state).toEqual(project);
     });
 });
diff --git a/src/store/project/project-reducer.ts b/src/store/project/project-reducer.ts
index bb07486..6b473cc 100644
--- a/src/store/project/project-reducer.ts
+++ b/src/store/project/project-reducer.ts
@@ -2,8 +2,6 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import * as _ from "lodash";
-
 import { projectActions, ProjectAction } from "./project-action";
 import { TreeItem, TreeItemStatus } from "~/components/tree/tree";
 import { ProjectResource } from "~/models/project";
@@ -26,25 +24,25 @@ interface ProjectUpdater {
     uuid: string;
 }
 
-export function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeItem<T> | undefined {
-    let item;
+function rebuildTree<T>(tree: Array<TreeItem<T>>, action: (item: TreeItem<T>, visitedItems: TreeItem<T>[]) => void, visitedItems: TreeItem<T>[] = []): Array<TreeItem<T>> {
+    const newTree: Array<TreeItem<T>> = [];
     for (const t of tree) {
-        item = t.id === itemId
-            ? t
-            : findTreeItem(t.items ? t.items : [], itemId);
-        if (item) {
-            break;
-        }
+        const items = t.items
+            ? rebuildTree(t.items, action, visitedItems.concat(t))
+            : undefined;
+        const item: TreeItem<T> = { ...t, items };
+        action(item, visitedItems);
+        newTree.push(item);
     }
-    return item;
+    return newTree;
 }
 
-export function getActiveTreeItem<T>(tree: Array<TreeItem<T>>): TreeItem<T> | undefined {
+export function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeItem<T> | undefined {
     let item;
     for (const t of tree) {
-        item = t.active
+        item = t.id === itemId
             ? t
-            : getActiveTreeItem(t.items ? t.items : []);
+            : findTreeItem(t.items ? t.items : [], itemId);
         if (item) {
             break;
         }
@@ -66,38 +64,6 @@ export function getTreePath<T>(tree: Array<TreeItem<T>>, itemId: string): Array<
     return [];
 }
 
-function resetTreeActivity<T>(tree: Array<TreeItem<T>>) {
-    for (const t of tree) {
-        t.active = false;
-        resetTreeActivity(t.items ? t.items : []);
-    }
-}
-
-function updateProjectTree(tree: Array<TreeItem<ProjectResource>>, projects: ProjectResource[], parentItemId?: string): Array<TreeItem<ProjectResource>> {
-    let treeItem;
-    if (parentItemId) {
-        treeItem = findTreeItem(tree, parentItemId);
-        if (treeItem) {
-            treeItem.status = TreeItemStatus.LOADED;
-        }
-    }
-    const items = projects.map(p => ({
-        id: p.uuid,
-        open: false,
-        active: false,
-        status: TreeItemStatus.INITIAL,
-        data: p,
-        items: []
-    } as TreeItem<ProjectResource>));
-
-    if (treeItem) {
-        treeItem.items = items;
-        return tree;
-    }
-
-    return items;
-}
-
 const updateCreator = (state: ProjectState, creator: Partial<ProjectCreator>) => ({
     ...state,
     creator: {
@@ -127,7 +93,6 @@ const initialState: ProjectState = {
     }
 };
 
-
 export const projectsReducer = (state: ProjectState = initialState, action: ProjectAction) => {
     return projectActions.match(action, {
         OPEN_PROJECT_CREATOR: ({ ownerUuid }) => updateCreator(state, { ownerUuid, opened: true }),
@@ -139,55 +104,68 @@ export const projectsReducer = (state: ProjectState = initialState, action: Proj
         UPDATE_PROJECT_SUCCESS: () => updateProject(state, { opened: false, uuid: "" }),
         REMOVE_PROJECT: () => state,
         PROJECTS_REQUEST: itemId => {
-            const items = _.cloneDeep(state.items);
-            const item = findTreeItem(items, itemId);
-            if (item) {
-                item.status = TreeItemStatus.PENDING;
-                state.items = items;
-            }
-            return { ...state, items };
-        },
-        PROJECTS_SUCCESS: ({ projects, parentItemId }) => {
-            const items = _.cloneDeep(state.items);
             return {
                 ...state,
-                items: updateProjectTree(items, projects, parentItemId)
+                items: rebuildTree(state.items, item => {
+                    if (item.id === itemId) {
+                        item.status = TreeItemStatus.PENDING;
+                    }
+                })
             };
         },
-        TOGGLE_PROJECT_TREE_ITEM_OPEN: itemId => {
-            const items = _.cloneDeep(state.items);
-            const item = findTreeItem(items, itemId);
-            if (item) {
-                item.open = !item.open;
-            }
-            return {
-                ...state,
-                items,
-                currentItemId: itemId
-            };
-        },
-        TOGGLE_PROJECT_TREE_ITEM_ACTIVE: itemId => {
-            const items = _.cloneDeep(state.items);
-            resetTreeActivity(items);
-            const item = findTreeItem(items, itemId);
-            if (item) {
-                item.active = true;
-            }
-            return {
-                ...state,
-                items,
-                currentItemId: itemId
-            };
-        },
-        RESET_PROJECT_TREE_ACTIVITY: () => {
-            const items = _.cloneDeep(state.items);
-            resetTreeActivity(items);
+        PROJECTS_SUCCESS: ({ projects, parentItemId }) => {
+            const items = projects.map(p => ({
+               id: p.uuid,
+               open: false,
+               active: false,
+               status: TreeItemStatus.INITIAL,
+               data: p,
+               items: []
+            }));
             return {
                 ...state,
-                items,
-                currentItemId: ""
+                items: state.items.length > 0 ?
+                    rebuildTree(state.items, item => {
+                        if (item.id === parentItemId) {
+                           item.status = TreeItemStatus.LOADED;
+                           item.items = items;
+                        }
+                    }) : items
             };
         },
+        TOGGLE_PROJECT_TREE_ITEM_OPEN: ({ itemId, open, recursive }) => ({
+            ...state,
+            items: rebuildTree(state.items, (item, visitedItems) => {
+                if (item.id === itemId) {
+                    if (recursive && open !== undefined) {
+                        visitedItems.forEach(item => item.open = open);
+                    }
+                    item.open = open !== undefined ? open : !item.open;
+                }
+            }),
+            currentItemId: itemId
+        }),
+        TOGGLE_PROJECT_TREE_ITEM_ACTIVE: ({ itemId, active, recursive }) => ({
+            ...state,
+            items: rebuildTree(state.items, (item, visitedItems) => {
+                item.active = false;
+                if (item.id === itemId) {
+                    if (recursive && active !== undefined) {
+                        visitedItems.forEach(item => item.active = active);
+                    }
+
+                    item.active = active !== undefined ? active : true;
+                }
+            }),
+            currentItemId: itemId
+        }),
+        RESET_PROJECT_TREE_ACTIVITY: () => ({
+            ...state,
+            items: rebuildTree(state.items, item => {
+                item.active = false;
+            }),
+            currentItemId: ""
+        }),
         default: () => state
     });
 };
diff --git a/src/store/side-panel/side-panel-reducer.ts b/src/store/side-panel/side-panel-reducer.ts
index b68ce7a..56785e2 100644
--- a/src/store/side-panel/side-panel-reducer.ts
+++ b/src/store/side-panel/side-panel-reducer.ts
@@ -47,7 +47,7 @@ export const sidePanelItems = [
         openAble: true,
         activeAction: (dispatch: Dispatch, uuid: string) => {
             dispatch(push(getProjectUrl(uuid)));
-            dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(uuid));
+            dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE({ itemId: uuid }));
             dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
             dispatch(projectPanelActions.RESET_PAGINATION());
             dispatch(projectPanelActions.REQUEST_ITEMS());
diff --git a/src/store/trash-panel/trash-panel-middleware-service.ts b/src/store/trash-panel/trash-panel-middleware-service.ts
index 2d1dbf7..3a4da39 100644
--- a/src/store/trash-panel/trash-panel-middleware-service.ts
+++ b/src/store/trash-panel/trash-panel-middleware-service.ts
@@ -39,13 +39,12 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
             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 userUuid = this.services.authService.getUuid()!;
 
-        this.services.trashService
+        this.services.groupsService
             .contents(userUuid, {
                 limit: dataExplorer.rowsPerPage,
                 offset: dataExplorer.page * dataExplorer.rowsPerPage,
@@ -53,7 +52,6 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
                 filters: new FilterBuilder()
                     .addIsA("uuid", typeFilters.map(f => f.type))
                     .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
-                    .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
                     .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
                     .getFilters(),
                 recursive: true,
diff --git a/src/views-components/context-menu/action-sets/collection-action-set.ts b/src/views-components/context-menu/action-sets/collection-action-set.ts
index 4561f9d..c8fb3cb 100644
--- a/src/views-components/context-menu/action-sets/collection-action-set.ts
+++ b/src/views-components/context-menu/action-sets/collection-action-set.ts
@@ -8,6 +8,8 @@ import { toggleFavorite } from "~/store/favorites/favorites-actions";
 import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, ProvenanceGraphIcon, AdvancedIcon, RemoveIcon } from "~/components/icon/icon";
 import { openUpdater } from "~/store/collections/updater/collection-updater-action";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
+import { toggleCollectionTrashed } from "~/store/collections/collection-trash-actions";
 
 export const collectionActionSet: ContextMenuActionSet = [[
     {
@@ -40,6 +42,12 @@ export const collectionActionSet: ContextMenuActionSet = [[
         }
     },
     {
+        component: ToggleTrashAction,
+        execute: (dispatch, resource) => {
+            dispatch<any>(toggleCollectionTrashed(resource));
+        }
+    },
+    {
         icon: CopyIcon,
         name: "Copy to project",
         execute: (dispatch, resource) => {
diff --git a/src/views-components/context-menu/action-sets/collection-resource-action-set.ts b/src/views-components/context-menu/action-sets/collection-resource-action-set.ts
index 7d8364b..dbc9e23 100644
--- a/src/views-components/context-menu/action-sets/collection-resource-action-set.ts
+++ b/src/views-components/context-menu/action-sets/collection-resource-action-set.ts
@@ -8,6 +8,8 @@ import { toggleFavorite } from "~/store/favorites/favorites-actions";
 import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, RemoveIcon } from "~/components/icon/icon";
 import { openUpdater } from "~/store/collections/updater/collection-updater-action";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
+import { toggleCollectionTrashed } from "~/store/collections/collection-trash-actions";
 
 export const collectionResourceActionSet: ContextMenuActionSet = [[
     {
@@ -40,6 +42,12 @@ export const collectionResourceActionSet: ContextMenuActionSet = [[
         }
     },
     {
+        component: ToggleTrashAction,
+        execute: (dispatch, resource) => {
+            dispatch<any>(toggleCollectionTrashed(resource));
+        }
+    },
+    {
         icon: CopyIcon,
         name: "Copy to project",
         execute: (dispatch, resource) => {
diff --git a/src/views-components/context-menu/action-sets/project-action-set.ts b/src/views-components/context-menu/action-sets/project-action-set.ts
index 1b000c8..d2412e7 100644
--- a/src/views-components/context-menu/action-sets/project-action-set.ts
+++ b/src/views-components/context-menu/action-sets/project-action-set.ts
@@ -5,12 +5,13 @@
 import { reset, initialize } from "redux-form";
 
 import { ContextMenuActionSet } from "../context-menu-action-set";
-import { projectActions, PROJECT_FORM_NAME } from "~/store/project/project-action";
+import { projectActions, PROJECT_FORM_NAME, toggleProjectTrashed } from "~/store/project/project-action";
 import { NewProjectIcon, RenameIcon } 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 { PROJECT_CREATE_DIALOG } from "../../dialog-create/dialog-project-create";
+import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
 
 export const projectActionSet: ContextMenuActionSet = [[
     {
@@ -36,5 +37,11 @@ export const projectActionSet: ContextMenuActionSet = [[
                 dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
             });
         }
+    },
+    {
+        component: ToggleTrashAction,
+        execute: (dispatch, resource) => {
+            dispatch<any>(toggleProjectTrashed(resource));
+        }
     }
 ]];
diff --git a/src/views-components/context-menu/actions/trash-action.tsx b/src/views-components/context-menu/actions/trash-action.tsx
new file mode 100644
index 0000000..d6c8b2f
--- /dev/null
+++ b/src/views-components/context-menu/actions/trash-action.tsx
@@ -0,0 +1,29 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { ListItemIcon, ListItemText, ListItem } from "@material-ui/core";
+import { RestoreFromTrashIcon, TrashIcon } from "~/components/icon/icon";
+import { connect } from "react-redux";
+import { RootState } from "~/store/store";
+
+const mapStateToProps = (state: RootState, props: { onClick: () => {} }) => ({
+    isTrashed: state.contextMenu.resource && state.contextMenu.resource.isTrashed,
+    onClick: props.onClick
+});
+
+export const ToggleTrashAction = connect(mapStateToProps)((props: { isTrashed?: boolean, onClick: () => void }) =>
+    <ListItem button
+        onClick={props.onClick}>
+        <ListItemIcon>
+            {props.isTrashed
+                ? <RestoreFromTrashIcon/>
+                : <TrashIcon/>}
+        </ListItemIcon>
+        <ListItemText style={{ textDecoration: 'none' }}>
+            {props.isTrashed
+                ? <>Restore</>
+                : <>Move to trash</>}
+        </ListItemText>
+    </ListItem >);
diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx
index 374cb95..9e32700 100644
--- a/src/views/collection-panel/collection-panel.tsx
+++ b/src/views/collection-panel/collection-panel.tsx
@@ -65,7 +65,6 @@ export const CollectionPanel = withStyles(styles)(
         tags: state.collectionPanel.tags
     }))(
         class extends React.Component<CollectionPanelProps> {
-
             render() {
                 const { classes, item, tags, onContextMenu } = this.props;
                 return <div>
@@ -131,7 +130,6 @@ export const CollectionPanel = withStyles(styles)(
                     onItemRouteChange(match.params.id);
                 }
             }
-
         }
     )
 );
diff --git a/src/views/favorite-panel/favorite-panel-item.ts b/src/views/favorite-panel/favorite-panel-item.ts
index 842b6d6..d2e2331 100644
--- a/src/views/favorite-panel/favorite-panel-item.ts
+++ b/src/views/favorite-panel/favorite-panel-item.ts
@@ -14,6 +14,7 @@ export interface FavoritePanelItem {
     lastModified: string;
     fileSize?: number;
     status?: string;
+    isTrashed?: boolean;
 }
 
 export function resourceToDataItem(r: GroupContentsResource): FavoritePanelItem {
@@ -24,6 +25,7 @@ export function resourceToDataItem(r: GroupContentsResource): FavoritePanelItem
         url: "",
         owner: r.ownerUuid,
         lastModified: r.modifiedAt,
-        status:  r.kind === ResourceKind.PROCESS ? r.state : undefined
+        status:  r.kind === ResourceKind.PROCESS ? r.state : undefined,
+        isTrashed: r.kind === ResourceKind.GROUP || r.kind === ResourceKind.COLLECTION ? r.isTrashed: undefined
     };
 }
diff --git a/src/views/project-panel/project-panel-item.ts b/src/views/project-panel/project-panel-item.ts
index f031859..ecc5a7d 100644
--- a/src/views/project-panel/project-panel-item.ts
+++ b/src/views/project-panel/project-panel-item.ts
@@ -15,6 +15,7 @@ export interface ProjectPanelItem {
     lastModified: string;
     fileSize?: number;
     status?: string;
+    isTrashed?: boolean;
 }
 
 export function resourceToDataItem(r: GroupContentsResource): ProjectPanelItem {
@@ -26,6 +27,7 @@ export function resourceToDataItem(r: GroupContentsResource): ProjectPanelItem {
         url: "",
         owner: r.ownerUuid,
         lastModified: r.modifiedAt,
-        status:  r.kind === ResourceKind.PROCESS ? r.state : undefined
+        status: r.kind === ResourceKind.PROCESS ? r.state : undefined,
+        isTrashed: r.kind === ResourceKind.GROUP || r.kind === ResourceKind.COLLECTION ? r.isTrashed: undefined
     };
 }
diff --git a/src/views/trash-panel/trash-panel-item.ts b/src/views/trash-panel/trash-panel-item.ts
index 8916458..a2f59ac 100644
--- a/src/views/trash-panel/trash-panel-item.ts
+++ b/src/views/trash-panel/trash-panel-item.ts
@@ -9,6 +9,7 @@ export interface TrashPanelItem {
     uuid: string;
     name: string;
     kind: string;
+    owner: string;
     fileSize?: number;
     trashAt?: string;
     deleteAt?: string;
@@ -20,6 +21,7 @@ export function resourceToDataItem(r: GroupContentsResource): TrashPanelItem {
         uuid: r.uuid,
         name: r.name,
         kind: r.kind,
+        owner: r.ownerUuid,
         trashAt: (r as TrashResource).trashAt,
         deleteAt: (r as TrashResource).deleteAt,
         isTrashed: (r as TrashResource).isTrashed
diff --git a/src/views/trash-panel/trash-panel.tsx b/src/views/trash-panel/trash-panel.tsx
index c5a302e..fa73c0b 100644
--- a/src/views/trash-panel/trash-panel.tsx
+++ b/src/views/trash-panel/trash-panel.tsx
@@ -11,7 +11,6 @@ import { DataColumns } from '~/components/data-table/data-table';
 import { RouteComponentProps } from 'react-router';
 import { RootState } from '~/store/store';
 import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
-import { ProcessState } from '~/models/process';
 import { SortDirection } from '~/components/data-table/data-column';
 import { ResourceKind } from '~/models/resource';
 import { resourceLabel } from '~/common/labels';
@@ -41,7 +40,7 @@ export enum TrashPanelColumnNames {
 }
 
 export interface TrashPanelFilter extends DataTableFilterItem {
-    type: ResourceKind | ProcessState;
+    type: ResourceKind;
 }
 
 export const columns: DataColumns<TrashPanelItem, TrashPanelFilter> = [
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index a3f7624..8028f2c 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -219,6 +219,8 @@ export const Workbench = withStyles(styles)(
                                         toggleOpen={itemId => this.props.dispatch(setProjectItem(itemId, ItemMode.OPEN))}
                                         onContextMenu={(event, item) => this.openContextMenu(event, {
                                             uuid: item.data.uuid,
+                                            ownerUuid: item.data.ownerUuid || this.props.authService.getUuid(),
+                                            isTrashed: item.data.isTrashed,
                                             name: item.data.name,
                                             kind: ContextMenuKind.PROJECT
                                         })}
@@ -268,6 +270,7 @@ export const Workbench = withStyles(styles)(
                         uuid: item.uuid,
                         name: item.name,
                         description: item.description,
+                        isTrashed: item.isTrashed,
                         kind: ContextMenuKind.COLLECTION
                     });
                 }}
@@ -290,6 +293,8 @@ export const Workbench = withStyles(styles)(
                         uuid: item.uuid,
                         name: item.name,
                         description: item.description,
+                        isTrashed: item.isTrashed,
+                        ownerUuid: item.owner || this.props.authService.getUuid(),
                         kind
                     });
                 }}
@@ -318,6 +323,7 @@ export const Workbench = withStyles(styles)(
                     this.openContextMenu(event, {
                         uuid: item.uuid,
                         name: item.name,
+                        isTrashed: item.isTrashed,
                         kind,
                     });
                 }}
@@ -341,26 +347,28 @@ export const Workbench = withStyles(styles)(
             renderTrashPanel = (props: RouteComponentProps<{ id: string }>) => <TrashPanel
                 onItemRouteChange={() => this.props.dispatch(trashPanelActions.REQUEST_ITEMS())}
                 onContextMenu={(event, item) => {
-                    const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE;
+                    const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.COLLECTION;
                     this.openContextMenu(event, {
                         uuid: item.uuid,
                         name: item.name,
+                        isTrashed: item.isTrashed,
+                        ownerUuid: item.owner,
                         kind,
                     });
                 }}
                 onDialogOpen={this.handleProjectCreationDialogOpen}
                 onItemClick={item => {
-                    this.props.dispatch(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(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));
-                    }
+                    // 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} />
@@ -410,7 +418,7 @@ export const Workbench = withStyles(styles)(
                 this.props.dispatch(collectionCreateActions.OPEN_COLLECTION_CREATOR({ ownerUuid: itemUuid }));
             }
 
-            openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: { name: string; uuid: string; description?: string; kind: ContextMenuKind; }) => {
+            openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: { name: string; uuid: string; description?: string; isTrashed?: boolean, ownerUuid?: string, kind: ContextMenuKind; }) => {
                 event.preventDefault();
                 this.props.dispatch(
                     contextMenuActions.OPEN_CONTEXT_MENU({

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list