Add trash view and trahsing/untrashing project/collection
authorDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 21 Aug 2018 19:46:18 +0000 (21:46 +0200)
committerDaniel Kos <daniel.kos@contractors.roche.com>
Wed, 29 Aug 2018 20:57:35 +0000 (22:57 +0200)
Feature #13828

Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos@contractors.roche.com>

25 files changed:
package.json
src/components/icon/icon.tsx
src/services/collection-service/collection-service.ts
src/services/groups-service/groups-service.ts
src/services/services.ts
src/services/trash-service/trash-service.test.ts [deleted file]
src/services/trash-service/trash-service.ts [deleted file]
src/store/collections/collection-trash-actions.ts [new file with mode: 0644]
src/store/context-menu/context-menu-reducer.ts
src/store/navigation/navigation-action.ts
src/store/project/project-action.ts
src/store/project/project-reducer.test.ts
src/store/project/project-reducer.ts
src/store/side-panel/side-panel-reducer.ts
src/store/trash-panel/trash-panel-middleware-service.ts
src/views-components/context-menu/action-sets/collection-action-set.ts
src/views-components/context-menu/action-sets/collection-resource-action-set.ts
src/views-components/context-menu/action-sets/project-action-set.ts
src/views-components/context-menu/actions/trash-action.tsx [new file with mode: 0644]
src/views/collection-panel/collection-panel.tsx
src/views/favorite-panel/favorite-panel-item.ts
src/views/project-panel/project-panel-item.ts
src/views/trash-panel/trash-panel-item.ts
src/views/trash-panel/trash-panel.tsx
src/views/workbench/workbench.tsx

index e2b6c4e1ec21f4017c0096a75bba61191a923dbd..0264998b68e09133be53ef28dda8d4d90c0b7222 100644 (file)
@@ -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"
index 0f0442a53ebd1b7be4b485694b45e86e3a99be7a..86a1a68c3ec1e4ef59a16b77a099041eb6f8d79a 100644 (file)
@@ -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} />;
index 9feec699e52dfd07070105c75c251d96b3107541..ad493b5a21483dc04677a52998070c5615a62e95 100644 (file)
@@ -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);
+    }
+
 }
index 39cc74a9c2406a64041999cf0dceb2071b291cf8..4756aa3edcf22c1b000a6b7894fdc265970c9a44 100644 (file)
@@ -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 {
index 91997552c0ee1998cb358567706af50020d83689..f63194d3d0ad634d54bdddae90f845b608bea6d9 100644 (file)
@@ -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 (file)
index f22d066..0000000
+++ /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 (file)
index fc02d2f..0000000
+++ /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 (file)
index 0000000..c6d4ee0
--- /dev/null
@@ -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
+                }));
+            });
+        }
+    };
index ac14c35534dba5b5fd0576ec0f10bac47631b7d8..8026c1d1d66125f2ee5a597dc5e3eb95b2cb8089 100644 (file)
@@ -20,6 +20,8 @@ export interface ContextMenuResource {
     kind: string;
     name: string;
     description?: string;
+    isTrashed?: boolean;
+    ownerUuid?: string;
 }
 
 const initialState = {
index 50ec93d25e8091e2186e0eccf7a6f403d0ca90ae..ad70d9d58b4c92897554e4efadb8c9019d07300c 100644 (file)
@@ -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 }));
         });
     };
 
index 2017658916cbfe7f5bc408c5eaaea4d547e6cc20..bb5da19941e26f21cd8e60a2e218e8ba07fcdbd2 100644 (file)
@@ -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>;
index bb60e396946a588f8d93a1c1f7e6803461ba82eb..cf4701091c52e1ef33f74337562a5fd305568b26 100644 (file)
@@ -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);
     });
 });
index bb0748657ee1e64b3d28b5e8bc923fc062ca6ea9..6b473cc53c35a8e0905eb356fc68e6d33af3849e 100644 (file)
@@ -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
     });
 };
index b68ce7a1c20026df63768ab0e7bfdff2edd858ab..56785e2310137e258eec255611ec8a80c797b9d6 100644 (file)
@@ -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());
index 2d1dbf7b90c57351e58c4739054b30bc9faa4187..3a4da39606ae9a9051883804187e4874ea3f9c22 100644 (file)
@@ -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,
index 4561f9d308879b981c8a9a72dc32c29dc701ddcc..c8fb3cbc927d7c51f1f93fcea2b12ea407e1fd0d 100644 (file)
@@ -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 = [[
     {
@@ -39,6 +41,12 @@ export const collectionActionSet: ContextMenuActionSet = [[
             });
         }
     },
+    {
+        component: ToggleTrashAction,
+        execute: (dispatch, resource) => {
+            dispatch<any>(toggleCollectionTrashed(resource));
+        }
+    },
     {
         icon: CopyIcon,
         name: "Copy to project",
index 7d8364bd70ea22b4c29bb69c5c11b95699f6765b..dbc9e23698b1d3814889d814bf98adb69b94b837 100644 (file)
@@ -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 = [[
     {
@@ -39,6 +41,12 @@ export const collectionResourceActionSet: ContextMenuActionSet = [[
             });
         }
     },
+    {
+        component: ToggleTrashAction,
+        execute: (dispatch, resource) => {
+            dispatch<any>(toggleCollectionTrashed(resource));
+        }
+    },
     {
         icon: CopyIcon,
         name: "Copy to project",
index 1b000c88fcee77ec2c39a844d3476001fca725a7..d2412e7eda495394e339d25cee97f76cff517d02 100644 (file)
@@ -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 (file)
index 0000000..d6c8b2f
--- /dev/null
@@ -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 >);
index 374cb95159483f5d4896fcbd539fddfc61931ea3..9e32700d034e8eeb5d87bb8c4db65006ae279730 100644 (file)
@@ -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);
                 }
             }
-
         }
     )
 );
index 842b6d6bbcc05469aa0566dbee43f6e4cbd608cd..d2e23315bd6c43f81a033c9e77a0c8290edf42c1 100644 (file)
@@ -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
     };
 }
index f0318591f5066d388286040ea6ccd96fe1b431d0..ecc5a7d55b160502e29a7e47256047e7495ee624 100644 (file)
@@ -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
     };
 }
index 89164581d2996cf3b56757df2de1146a401deb3e..a2f59ac89c29d264586b1e12e0e9690a11639b40 100644 (file)
@@ -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
index c5a302efb3835cc69ddbfbe572f30f7eb441cacd..fa73c0b0f83a2c746ef973800d3d80cba08ab94d 100644 (file)
@@ -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> = [
index a3f7624f9f3805c277d0b9bf7fc968d458e32660..8028f2c30ffc10cd8c883e2676aedd5a9175531f 100644 (file)
@@ -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({