refs #13828 Merge branch 'origin/13828-trash-view'
authorDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 4 Sep 2018 07:41:44 +0000 (09:41 +0200)
committerDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 4 Sep 2018 07:49:11 +0000 (09:49 +0200)
# Conflicts:
# src/index.tsx
# src/routes/routes.ts
# src/store/context-menu/context-menu-actions.ts
# src/store/store.ts
# src/store/workbench/workbench-actions.ts
# src/views/workbench/workbench.tsx

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

76 files changed:
src/common/formatters.ts
src/components/data-explorer/data-explorer.test.tsx
src/components/data-explorer/data-explorer.tsx
src/components/icon/icon.tsx
src/index.tsx
src/models/collection.ts
src/models/group.ts
src/models/project.ts
src/models/resource.ts
src/models/test-utils.ts
src/routes/routes.ts
src/services/ancestors-service/ancestors-service.ts
src/services/api/filter-builder.test.ts [moved from src/common/api/filter-builder.test.ts with 100% similarity]
src/services/api/filter-builder.ts [moved from src/common/api/filter-builder.ts with 100% similarity]
src/services/api/order-builder.test.ts [moved from src/common/api/order-builder.test.ts with 100% similarity]
src/services/api/order-builder.ts [moved from src/common/api/order-builder.ts with 94% similarity]
src/services/api/url-builder.ts [moved from src/common/api/url-builder.ts with 100% similarity]
src/services/collection-files-service/collection-files-service.ts
src/services/collection-service/collection-service.ts
src/services/common-service/common-resource-service.test.ts [moved from src/common/api/common-resource-service.test.ts with 98% similarity]
src/services/common-service/common-resource-service.ts [moved from src/common/api/common-resource-service.ts with 98% similarity]
src/services/common-service/trashable-resource-service.ts [new file with mode: 0644]
src/services/container-request-service/container-request-service.ts
src/services/container-service/container-service.ts
src/services/favorite-service/favorite-service.test.ts
src/services/favorite-service/favorite-service.ts
src/services/groups-service/groups-service.test.ts
src/services/groups-service/groups-service.ts
src/services/keep-service/keep-service.ts
src/services/link-service/link-service.ts
src/services/log-service/log-service.ts
src/services/project-service/project-service.test.ts
src/services/project-service/project-service.ts
src/services/tag-service/tag-service.ts
src/services/user-service/user-service.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
src/store/collections/collection-copy-actions.ts
src/store/collections/collection-create-actions.ts
src/store/collections/collection-move-actions.ts
src/store/collections/collection-partial-copy-actions.ts
src/store/collections/collection-update-actions.ts
src/store/context-menu/context-menu-actions.ts
src/store/context-menu/context-menu-reducer.ts
src/store/data-explorer/data-explorer-middleware-service.ts
src/store/favorite-panel/favorite-panel-middleware-service.ts
src/store/navigation/navigation-action.ts
src/store/process-logs-panel/process-logs-panel-actions.ts
src/store/processes/processes-actions.ts
src/store/project-panel/project-panel-middleware-service.ts
src/store/projects/project-create-actions.ts
src/store/projects/project-move-actions.ts
src/store/projects/project-update-actions.ts
src/store/side-panel-tree/side-panel-tree-actions.ts
src/store/side-panel/side-panel-action.ts
src/store/store.ts
src/store/trash-panel/trash-panel-action.ts [new file with mode: 0644]
src/store/trash-panel/trash-panel-middleware-service.ts [new file with mode: 0644]
src/store/trash/trash-actions.ts [new file with mode: 0644]
src/store/workbench/workbench-actions.ts
src/views-components/collection-panel-files/collection-panel-files.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/action-sets/trash-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/actions/trash-action.tsx [new file with mode: 0644]
src/views-components/context-menu/context-menu-action-set.ts
src/views-components/context-menu/context-menu.tsx
src/views-components/data-explorer/renderers.tsx
src/views-components/project-tree-picker/project-tree-picker.tsx
src/views/collection-panel/collection-panel.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/project-panel/project-panel.tsx
src/views/trash-panel/trash-panel.tsx [new file with mode: 0644]
src/views/workbench/workbench.tsx
src/websocket/websocket-service.ts
src/websocket/websocket.ts

index 0402f3903763e75ac04ee16c555f05d787cbbc77..e2097878a9f98276ffe1d78cf49ac6ca4468d894 100644 (file)
@@ -2,10 +2,13 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-export const formatDate = (isoDate: string) => {
-    const date = new Date(isoDate);
-    const text = date.toLocaleString();
-    return text === 'Invalid Date' ? "" : text;
+export const formatDate = (isoDate?: string) => {
+    if (isoDate) {
+        const date = new Date(isoDate);
+        const text = date.toLocaleString();
+        return text === 'Invalid Date' ? "" : text;
+    }
+    return "";
 };
 
 export const formatFileSize = (size?: number) => {
index 3e447b4015480091b66e6ee39f5b6d5560ea6cc6..882c178be5ef5550629412f5116a7ab3f4e6d50f 100644 (file)
@@ -124,4 +124,5 @@ const mockDataExplorerProps = () => ({
     defaultIcon: ProjectIcon,
     onSetColumns: jest.fn(),
     defaultMessages: ['testing'],
+    contextMenuColumn: true
 });
index af14db9c9e08818f3a9dc945718d1296f707d5e8..ea400b100edfac2b8d06a8ea7e9fb45b4fef3e61 100644 (file)
@@ -48,6 +48,7 @@ interface DataExplorerDataProps<T> {
     page: number;
     defaultIcon: IconType;
     defaultMessages: string[];
+    contextMenuColumn: boolean;
 }
 
 interface DataExplorerActionProps<T> {
@@ -95,7 +96,7 @@ export const DataExplorer = withStyles(styles)(
                             </Grid>
                         </Toolbar>
                         <DataTable
-                            columns={[...columns, this.contextMenuColumn]}
+                            columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
                             items={items}
                             onRowClick={(_, item: T) => onRowClick(item)}
                             onContextMenu={onContextMenu}
@@ -142,7 +143,7 @@ export const DataExplorer = withStyles(styles)(
                 </IconButton>
             </Grid>
 
-        contextMenuColumn = {
+        contextMenuColumn: DataColumn<any> = {
             name: "Actions",
             selected: true,
             configurable: false,
index 8bd9e22772c42ea593767f8614882ccac96fe9da..90861bfed6aca96b1a4fc9e49db749f628e543c0 100644 (file)
@@ -33,6 +33,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/History';
 import Search from '@material-ui/icons/Search';
 import SettingsApplications from '@material-ui/icons/SettingsApplications';
 import Star from '@material-ui/icons/Star';
@@ -69,6 +70,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 20d2c1e9e1f1fc2bf0ee31023a58583e92dc5988..4ce80d31e9d0f0360e6dd7051b113f9dbfe4d40c 100644 (file)
@@ -31,6 +31,7 @@ import { processActionSet } from './views-components/context-menu/action-sets/pr
 import { addRouteChangeHandlers } from './routes/routes';
 import { loadWorkbench } from './store/workbench/workbench-actions';
 import { Routes } from '~/routes/routes';
+import { trashActionSet } from "~/views-components/context-menu/action-sets/trash-action-set";
 import { ServiceRepository } from '~/services/services';
 import { initWebSocket } from '~/websocket/websocket';
 import { Config } from '~/common/config';
@@ -52,6 +53,7 @@ addMenuActionSet(ContextMenuKind.COLLECTION_FILES_ITEM, collectionFilesItemActio
 addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet);
 addMenuActionSet(ContextMenuKind.COLLECTION_RESOURCE, collectionResourceActionSet);
 addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
+addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
 
 fetchConfig()
     .then((config) => {
index 0e96f7fd3dd6f1473f62d8c2a2e3226aa42f6507..f8e38f9a0fac227bd2cfeccd62654723f9c66ef5 100644 (file)
@@ -2,9 +2,9 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { Resource, ResourceKind } from "./resource";
+import { ResourceKind, TrashableResource } from "./resource";
 
-export interface CollectionResource extends Resource {
+export interface CollectionResource extends TrashableResource {
     kind: ResourceKind.COLLECTION;
     name: string;
     description: string;
@@ -14,11 +14,8 @@ export interface CollectionResource extends Resource {
     replicationDesired: number;
     replicationConfirmed: number;
     replicationConfirmedAt: string;
-    trashAt: string;
-    deleteAt: string;
-    isTrashed: boolean;
 }
 
 export const getCollectionUrl = (uuid: string) => {
     return `/collections/${uuid}`;
-};
\ No newline at end of file
+};
index 5e8d7a1e0b4a2e76ceaf9c80d035f55417aa55d6..e2d0367a5e40fab7fef6fc7e98058c9e3840ee89 100644 (file)
@@ -2,20 +2,17 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { Resource, ResourceKind } from "./resource";
+import { ResourceKind, TrashableResource } from "./resource";
 
-export interface GroupResource extends Resource {
+export interface GroupResource extends TrashableResource {
     kind: ResourceKind.GROUP;
     name: string;
     groupClass: GroupClass | null;
     description: string;
     properties: string;
     writeableBy: string[];
-    trashAt: string;
-    deleteAt: string;
-    isTrashed: boolean;
 }
 
 export enum GroupClass {
     PROJECT = "project"
-}
\ No newline at end of file
+}
index b919450774f687084a700d8b4d041772aa142669..8e101ce29ffeaac99cb7c2073aae96b8b79ac9e8 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { GroupResource, GroupClass } from "./group";
+import { GroupClass, GroupResource } from "./group";
 
 export interface ProjectResource extends GroupResource {
     groupClass: GroupClass.PROJECT;
index 3c6c11bc57f81b62669a693362d5c3a75b9e2a24..698bcf73188ec21c375a349d3f2605953bbd2074 100644 (file)
@@ -10,10 +10,16 @@ export interface Resource {
     modifiedByUserUuid: string;
     modifiedAt: string;
     href: string;
-    kind: string;
+    kind: ResourceKind;
     etag: string;
 }
 
+export interface TrashableResource extends Resource {
+    trashAt: string;
+    deleteAt: string;
+    isTrashed: boolean;
+}
+
 export enum ResourceKind {
     COLLECTION = "arvados#collection",
     CONTAINER = "arvados#container",
@@ -24,6 +30,7 @@ export enum ResourceKind {
     PROJECT = "arvados#group",
     USER = "arvados#user",
     WORKFLOW = "arvados#workflow",
+    NONE = "arvados#none"
 }
 
 export enum ResourceObjectType {
index 6723430c34872848c26a6b9fa8be172b780f5603..b08ce5a0525bd913b4d774262f3548b664e8e9ac 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { GroupResource, GroupClass } from "./group";
+import { GroupClass, GroupResource } from "./group";
 import { Resource, ResourceKind } from "./resource";
 import { ProjectResource } from "./project";
 
@@ -34,7 +34,7 @@ export const mockCommonResource = (data: Partial<Resource>): Resource => ({
     createdAt: "",
     etag: "",
     href: "",
-    kind: "",
+    kind: ResourceKind.NONE,
     modifiedAt: "",
     modifiedByClientUuid: "",
     modifiedByUserUuid: "",
index 6901d8755588acb9d5f1600f30cca934ca05566c..05a8ab099ce1db395fb42f67561f330f3adae289 100644 (file)
@@ -6,9 +6,9 @@ import { History, Location } from 'history';
 import { RootStore } from '~/store/store';
 import { matchPath } from 'react-router';
 import { ResourceKind, RESOURCE_UUID_PATTERN, extractUuidKind } from '~/models/resource';
-import { getProjectUrl } from '../models/project';
+import { getProjectUrl } from '~/models/project';
 import { getCollectionUrl } from '~/models/collection';
-import { loadProject, loadFavorites, loadCollection, loadProcessLog } from '~/store/workbench/workbench-actions';
+import { loadProject, loadFavorites, loadCollection, loadTrash, loadProcessLog } from '~/store/workbench/workbench-actions';
 import { loadProcess } from '~/store/processes/processes-actions';
 
 export const Routes = {
@@ -18,6 +18,7 @@ export const Routes = {
     COLLECTIONS: `/collections/:id(${RESOURCE_UUID_PATTERN})`,
     PROCESSES: `/processes/:id(${RESOURCE_UUID_PATTERN})`,
     FAVORITES: '/favorites',
+    TRASH: '/trash',
     PROCESS_LOGS: `/process-logs/:id(${RESOURCE_UUID_PATTERN})`
 };
 
@@ -43,15 +44,18 @@ export const addRouteChangeHandlers = (history: History, store: RootStore) => {
     history.listen(handler);
 };
 
+export interface ResourceRouteParams {
+    id: string;
+}
+
 export const matchRootRoute = (route: string) =>
     matchPath(route, { path: Routes.ROOT, exact: true });
 
 export const matchFavoritesRoute = (route: string) =>
     matchPath(route, { path: Routes.FAVORITES });
 
-export interface ResourceRouteParams {
-    id: string;
-}
+export const matchTrashRoute = (route: string) =>
+    matchPath(route, { path: Routes.TRASH });
 
 export const matchProjectRoute = (route: string) =>
     matchPath<ResourceRouteParams>(route, { path: Routes.PROJECTS });
@@ -69,6 +73,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     const projectMatch = matchProjectRoute(pathname);
     const collectionMatch = matchCollectionRoute(pathname);
     const favoriteMatch = matchFavoritesRoute(pathname);
+    const trashMatch = matchTrashRoute(pathname);
     const processMatch = matchProcessRoute(pathname);
     const processLogMatch = matchProcessLogRoute(pathname);
     
@@ -78,6 +83,8 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
         store.dispatch(loadCollection(collectionMatch.params.id));
     } else if (favoriteMatch) {
         store.dispatch(loadFavorites());
+    } else if (trashMatch) {
+        store.dispatch(loadTrash());
     } else if (processMatch) {
         store.dispatch(loadProcess(processMatch.params.id));
     } else if (processLogMatch) {
index 1cd42fb523945c3a6a5918b24b0474780dabe908..f90b4a3053ca1744c2228a51c4c45177bf644c87 100644 (file)
@@ -6,7 +6,7 @@ import { GroupsService } from "~/services/groups-service/groups-service";
 import { UserService } from '../user-service/user-service';
 import { GroupResource } from '~/models/group';
 import { UserResource } from '~/models/user';
-import { extractUuidObjectType, ResourceObjectType } from "~/models/resource";
+import { extractUuidObjectType, ResourceObjectType, TrashableResource } from "~/models/resource";
 
 export class AncestorService {
     constructor(
@@ -14,7 +14,7 @@ export class AncestorService {
         private userService: UserService
     ) { }
 
-    async ancestors(uuid: string, rootUuid: string): Promise<Array<UserResource | GroupResource>> {
+    async ancestors(uuid: string, rootUuid: string): Promise<Array<UserResource | GroupResource | TrashableResource>> {
         const service = this.getService(extractUuidObjectType(uuid));
         if (service) {
             const resource = await service.get(uuid);
@@ -41,4 +41,4 @@ export class AncestorService {
                 return undefined;
         }
     }
-}
\ No newline at end of file
+}
similarity index 94%
rename from src/common/api/order-builder.ts
rename to src/services/api/order-builder.ts
index 196b06952e55c911d9cd0bac6ed881151918bf98..03f2696a5614178daa4aaec77e0d346d045faf75 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as _ from "lodash";
-import { Resource } from "~/models/resource";
+import { Resource } from "src/models/resource";
 
 export enum OrderDirection { ASC, DESC }
 
index 1d9a537f4a559cfdf06ab01d29eb9a011943f1ed..6f88a729aba7ebba03e73709fe5ca97189f64460 100644 (file)
@@ -5,7 +5,7 @@
 import { CollectionService } from "../collection-service/collection-service";
 import { parseKeepManifestText, stringifyKeepManifest } from "./collection-manifest-parser";
 import { mapManifestToCollectionFilesTree } from "./collection-manifest-mapper";
-import { CommonResourceService } from "~/common/api/common-resource-service";
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import * as _ from "lodash";
 
 export class CollectionFilesService {
index c0d61bd27b4d5d789a09a0096c16154ddcbbb461..6e6f2a97d439748a3fb96d9741cff45aa965e073 100644 (file)
@@ -2,7 +2,6 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { CommonResourceService } from "~/common/api/common-resource-service";
 import { CollectionResource } from "~/models/collection";
 import { AxiosInstance } from "axios";
 import { CollectionFile, CollectionDirectory } from "~/models/collection-file";
@@ -11,10 +10,11 @@ import { AuthService } from "../auth-service/auth-service";
 import { mapTreeValues } from "~/models/tree";
 import { parseFilesResponse } from "./collection-service-files-response";
 import { fileToArrayBuffer } from "~/common/file";
+import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
 
 export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
 
-export class CollectionService extends CommonResourceService<CollectionResource> {
+export class CollectionService extends TrashableResourceService<CollectionResource> {
     constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService) {
         super(serverApi, "collections");
     }
@@ -67,5 +67,4 @@ export class CollectionService extends CommonResourceService<CollectionResource>
         return this.webdavClient.put(fileURL, fileContent, requestConfig);
 
     }
-
 }
similarity index 98%
rename from src/common/api/common-resource-service.test.ts
rename to src/services/common-service/common-resource-service.test.ts
index a1d5e0868f4e0f0c333251022aed668e2edc7ded..d67d5dbf403ab66ea59c6267dce0d0fa90dacd9c 100644 (file)
@@ -5,7 +5,7 @@
 import { CommonResourceService } from "./common-resource-service";
 import axios, { AxiosInstance } from "axios";
 import MockAdapter from "axios-mock-adapter";
-import { Resource } from "~/models/resource";
+import { Resource } from "src/models/resource";
 
 export const mockResourceService = <R extends Resource, C extends CommonResourceService<R>>(Service: new (client: AxiosInstance) => C) => {
     const axiosInstance = axios.create();
similarity index 98%
rename from src/common/api/common-resource-service.ts
rename to src/services/common-service/common-resource-service.ts
index 8c4b65d14866caf2b31b6479216819645bb299a4..7b36b71cf42d7ce6ba289712eac1f2955b047ec4 100644 (file)
@@ -4,7 +4,7 @@
 
 import * as _ from "lodash";
 import { AxiosInstance, AxiosPromise } from "axios";
-import { Resource } from "~/models/resource";
+import { Resource } from "src/models/resource";
 
 export interface ListArguments {
     limit?: number;
diff --git a/src/services/common-service/trashable-resource-service.ts b/src/services/common-service/trashable-resource-service.ts
new file mode 100644 (file)
index 0000000..23e7366
--- /dev/null
@@ -0,0 +1,32 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as _ from "lodash";
+import { AxiosInstance } from "axios";
+import { TrashableResource } from "src/models/resource";
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
+
+export class TrashableResourceService<T extends TrashableResource> extends CommonResourceService<T> {
+
+    constructor(serverApi: AxiosInstance, resourceType: string) {
+        super(serverApi, resourceType);
+    }
+
+    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);
+    }
+}
index 8cf8e74a0d72de55864d3761df5937b5b54980e5..01805ff903ee4396da0ae64dc283045da2119c98 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { CommonResourceService } from "~/common/api/common-resource-service";
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { AxiosInstance } from "axios";
 import { ContainerRequestResource } from '../../models/container-request';
 
index 698c7f52b8285ad2a65c58ecece17095b32d4d8e..0ace1f60af6a1e72fbf674727493209796a5f360 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { CommonResourceService } from "~/common/api/common-resource-service";
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { AxiosInstance } from "axios";
 import { ContainerResource } from '../../models/container';
 
index d4bf9ac7f68c2bf14bccd1dd685e8db29d13cab3..beaf869eeedb0a0594e20c81e32e2206f2cac2ae 100644 (file)
@@ -6,8 +6,8 @@ import { LinkService } from "../link-service/link-service";
 import { GroupsService } from "../groups-service/groups-service";
 import { FavoriteService } from "./favorite-service";
 import { LinkClass } from "~/models/link";
-import { mockResourceService } from "~/common/api/common-resource-service.test";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { mockResourceService } from "~/services/common-service/common-resource-service.test";
+import { FilterBuilder } from "~/services/api/filter-builder";
 
 describe("FavoriteService", () => {
 
index 7a49c8ccbac3555af028536815f38ade60c08de5..4601054315fab0a196c1fa4de6c12056558e4a57 100644 (file)
@@ -5,8 +5,8 @@
 import { LinkService } from "../link-service/link-service";
 import { GroupsService, GroupContentsResource } from "../groups-service/groups-service";
 import { LinkClass } from "~/models/link";
-import { FilterBuilder, joinFilters } from "~/common/api/filter-builder";
-import { ListResults } from "~/common/api/common-resource-service";
+import { FilterBuilder, joinFilters } from "~/services/api/filter-builder";
+import { ListResults } from "~/services/common-service/common-resource-service";
 
 export interface FavoriteListArguments {
     limit?: number;
index c3be8bdaa8a6ecfa58a1553b7de57483481ff86b..e1157f4b177e5ca18c9764c9bb249cf1467d7074 100644 (file)
@@ -16,7 +16,7 @@ describe("GroupsService", () => {
 
     it("#contents", async () => {
         axiosMock
-            .onGet("/groups/1/contents/")
+            .onGet("/groups/1/contents")
             .reply(200, {
                 kind: "kind",
                 offset: 2,
index 822c810ef7ed0203666bdba829c0b13fced7d5ba..b285e9285518505cf6e459d357ba72ee58acec05 100644 (file)
@@ -3,12 +3,13 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as _ from "lodash";
-import { CommonResourceService, ListResults } from "~/common/api/common-resource-service";
+import { CommonResourceService, ListResults } from "~/services/common-service/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 { TrashableResource } from "~/models/resource";
+import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
 
 export interface ContentsArguments {
     limit?: number;
@@ -16,6 +17,7 @@ export interface ContentsArguments {
     order?: string;
     filters?: string;
     recursive?: boolean;
+    includeTrash?: boolean;
 }
 
 export type GroupContentsResource =
@@ -23,7 +25,7 @@ export type GroupContentsResource =
     ProjectResource |
     ProcessResource;
 
-export class GroupsService<T extends GroupResource = GroupResource> extends CommonResourceService<T> {
+export class GroupsService<T extends TrashableResource = TrashableResource> extends TrashableResourceService<T> {
 
     constructor(serverApi: AxiosInstance) {
         super(serverApi, "groups");
@@ -37,7 +39,7 @@ 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);
index fd49823a4730a29b8a1c45e3271de991650d9d09..77d06d933d37ba334e5205a70adde87d127d83d4 100644 (file)
@@ -2,7 +2,7 @@
 //\r
 // SPDX-License-Identifier: AGPL-3.0\r
 \r
-import { CommonResourceService } from "~/common/api/common-resource-service";\r
+import { CommonResourceService } from "~/services/common-service/common-resource-service";\r
 import { AxiosInstance } from "axios";\r
 import { KeepResource } from "~/models/keep";\r
 \r
index 8724904ef2a5eb0d61ed9ba101f82d5eda3bff60..c77def5f8c2eb461b8533ded52b6ae522cdc03e4 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { CommonResourceService } from "~/common/api/common-resource-service";
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { LinkResource } from "~/models/link";
 import { AxiosInstance } from "axios";
 
index c92475d55db24f77e81c8be0dd221aa6f409b663..8f6c66c8a2ddbf24f9e02fb0fe84874036b23f6c 100644 (file)
@@ -2,9 +2,9 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { CommonResourceService } from "~/common/api/common-resource-service";
 import { AxiosInstance } from "axios";
 import { LogResource } from '~/models/log';
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
 
 export class LogService extends CommonResourceService<LogResource> {
     constructor(serverApi: AxiosInstance) {
index a717736c3a68f3fb3e5ca9692696fcad5d72d095..11c2f61f3d87f5d9f7190726fc22d658d078f73d 100644 (file)
@@ -4,7 +4,7 @@
 
 import axios from "axios";
 import { ProjectService } from "./project-service";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { FilterBuilder } from "~/services/api/filter-builder";
 
 describe("CommonResourceService", () => {
     const axiosInstance = axios.create();
index e916f3c0a4da9d8be388a8577d3d304115385007..2dc3eeb0a87f61ee5af40454802d4d8b09e9e860 100644 (file)
@@ -5,8 +5,8 @@
 import { GroupsService } from "../groups-service/groups-service";
 import { ProjectResource } from "~/models/project";
 import { GroupClass } from "~/models/group";
-import { ListArguments } from "~/common/api/common-resource-service";
-import { FilterBuilder, joinFilters } from "~/common/api/filter-builder";
+import { ListArguments } from "~/services/common-service/common-resource-service";
+import { FilterBuilder, joinFilters } from "~/services/api/filter-builder";
 
 export class ProjectService extends GroupsService<ProjectResource> {
 
index 52e481a7975b38890c88c9d9309f68bf960e59fa..9f5b34fd412a725de32e2279b670067fa764bb96 100644 (file)
@@ -4,9 +4,9 @@
 
 import { LinkService } from "../link-service/link-service";
 import { LinkClass } from "~/models/link";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { FilterBuilder } from "~/services/api/filter-builder";
 import { TagTailType, TagResource } from "~/models/tag";
-import { OrderBuilder } from "~/common/api/order-builder";
+import { OrderBuilder } from "~/services/api/order-builder";
 
 export class TagService {
 
index 3c09a87d78512439887389ccaee4d0afdc698f32..31cc4bbbbce8820b357dcab978a2efc2f8fb381f 100644 (file)
@@ -3,11 +3,11 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { AxiosInstance } from "axios";
-import { CommonResourceService } from "~/common/api/common-resource-service";
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { UserResource } from "~/models/user";
 
 export class UserService extends CommonResourceService<UserResource> {
     constructor(serverApi: AxiosInstance) {
         super(serverApi, "users");
     }
-}
\ No newline at end of file
+}
index 413fedfc1c08eccd17fe24f4c22aecd03d5a000e..f214fd2f7b41297677efac451836bf09568eee1a 100644 (file)
@@ -12,7 +12,7 @@ import { dialogActions } from '../../dialog/dialog-actions';
 import { getNodeValue } from "~/models/tree";
 import { filterCollectionFilesBySelection } from './collection-panel-files-state';
 import { startSubmit, initialize, stopSubmit, reset } from 'redux-form';
-import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
 import { getDialog } from "~/store/dialog/dialog-reducer";
 import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
 
index 15ea85532d3f66a7f8a1c2a798c8c805227a4675..87ba0424be5df6bba68f31dbade878b57559077c 100644 (file)
@@ -8,7 +8,7 @@ import { initialize, startSubmit, stopSubmit } from 'redux-form';
 import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
-import { getCommonResourceServiceError, CommonResourceServiceError } from '~/common/api/common-resource-service';
+import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
 
 export const COLLECTION_COPY_FORM_NAME = 'collectionCopyFormName';
 
@@ -46,4 +46,4 @@ export const copyCollection = (resource: CollectionCopyFormDialogData) =>
             }
             return ;
         }
-    };
\ No newline at end of file
+    };
index 5a1246a7b0891b4a387cfb7a92dd538b4c34cc8a..254d6a8aa6850cb318057f5bbbccd228e397653e 100644 (file)
@@ -7,7 +7,7 @@ import { reset, startSubmit, stopSubmit, initialize } from 'redux-form';
 import { RootState } from '~/store/store';
 import { dialogActions } from "~/store/dialog/dialog-actions";
 import { ServiceRepository } from '~/services/services';
-import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
 import { uploadCollectionFiles } from './collection-upload-actions';
 import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions';
 
index dcd7b1aa8068136d3713eaf8c1f481dddac7b57e..420bcd01f1d90ed3d505414291d65b8f0bb89193 100644 (file)
@@ -7,7 +7,7 @@ import { dialogActions } from "~/store/dialog/dialog-actions";
 import { startSubmit, stopSubmit, initialize } from 'redux-form';
 import { ServiceRepository } from '~/services/services';
 import { RootState } from '~/store/store';
-import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 import { projectPanelActions } from '~/store/project-panel/project-panel-action';
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
index a063abae4107a28438da4e503d2c33d7700a1ca6..dedf75e16cda4d972720b4a685d2fecefe04d2d1 100644 (file)
@@ -10,7 +10,7 @@ import { dialogActions } from '~/store/dialog/dialog-actions';
 import { ServiceRepository } from '~/services/services';
 import { filterCollectionFilesBySelection } from '../collection-panel/collection-panel-files/collection-panel-files-state';
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
-import { getCommonResourceServiceError, CommonResourceServiceError } from '~/common/api/common-resource-service';
+import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
 
 export const COLLECTION_PARTIAL_COPY_FORM_NAME = 'COLLECTION_PARTIAL_COPY_DIALOG';
 
@@ -68,4 +68,4 @@ export const copyCollectionPartial = ({ name, description, projectUuid }: Collec
                 }
             }
         }
-    };
\ No newline at end of file
+    };
index 75e03d5066d7bd6d19422152e8881da2cb8c8db1..bf05d4ddc5a3a02075e42c402d22f22090061f1f 100644 (file)
@@ -6,15 +6,11 @@ import { Dispatch } from "redux";
 import { initialize, startSubmit, stopSubmit } from 'redux-form';
 import { RootState } from "~/store/store";
 import { collectionPanelActions } from "~/store/collection-panel/collection-panel-action";
-import { loadDetailsPanel } from "~/store/details-panel/details-panel-action";
 import { dialogActions } from "~/store/dialog/dialog-actions";
-import { dataExplorerActions } from "~/store/data-explorer/data-explorer-action";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
-import { ContextMenuResource } from '~/store/context-menu/context-menu-reducer';
-import { PROJECT_PANEL_ID } from "~/views/project-panel/project-panel";
-import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
 import { ServiceRepository } from "~/services/services";
 import { CollectionResource } from '~/models/collection';
+import { ContextMenuResource } from "~/store/context-menu/context-menu-actions";
 
 export interface CollectionUpdateFormDialogData {
     uuid: string;
@@ -46,4 +42,4 @@ export const updateCollection = (collection: Partial<CollectionResource>) =>
             }
             return;
         }
-    };
\ No newline at end of file
+    };
index 3440a3053dad930dd1db230e0b52e9100e690935..2b0e6f8f8bd2aad4192397459c79927862264166 100644 (file)
@@ -3,16 +3,15 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { unionize, ofType, UnionOf } from '~/common/unionize';
-import { ContextMenuPosition, ContextMenuResource } from "./context-menu-reducer";
+import { ContextMenuPosition } from "./context-menu-reducer";
 import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
 import { Dispatch } from 'redux';
 import { RootState } from '~/store/store';
 import { getResource } from '../resources/resources';
 import { ProjectResource } from '~/models/project';
-import { UserResource } from '../../models/user';
+import { UserResource } from '~/models/user';
 import { isSidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
-import { extractUuidKind, ResourceKind } from '~/models/resource';
-import { matchProcessRoute } from '~/routes/routes';
+import { extractUuidKind, ResourceKind, TrashableResource } from '~/models/resource';
 
 export const contextMenuActions = unionize({
     OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
@@ -21,7 +20,17 @@ export const contextMenuActions = unionize({
 
 export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
 
-export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: { name: string; uuid: string; description?: string; kind: ContextMenuKind; }) =>
+export type ContextMenuResource = {
+    name: string;
+    uuid: string;
+    ownerUuid: string;
+    description?: string;
+    kind: ResourceKind,
+    menuKind: ContextMenuKind;
+    isTrashed?: boolean;
+};
+
+export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource) =>
     (dispatch: Dispatch) => {
         event.preventDefault();
         dispatch(
@@ -34,24 +43,30 @@ export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource:
 
 export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-        const userResource = getResource<UserResource>(projectUuid)(getState().resources);
-        if (userResource) {
+        const res = getResource<UserResource>(projectUuid)(getState().resources);
+        if (res) {
             dispatch<any>(openContextMenu(event, {
                 name: '',
-                uuid: userResource.uuid,
-                kind: ContextMenuKind.ROOT_PROJECT
+                uuid: res.uuid,
+                ownerUuid: res.uuid,
+                kind: res.kind,
+                menuKind: ContextMenuKind.ROOT_PROJECT,
+                isTrashed: false
             }));
         }
     };
 
 export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-        const projectResource = getResource<ProjectResource>(projectUuid)(getState().resources);
-        if (projectResource) {
+        const res = getResource<ProjectResource>(projectUuid)(getState().resources);
+        if (res) {
             dispatch<any>(openContextMenu(event, {
-                name: projectResource.name,
-                uuid: projectResource.uuid,
-                kind: ContextMenuKind.PROJECT
+                name: res.name,
+                uuid: res.uuid,
+                kind: res.kind,
+                menuKind: ContextMenuKind.PROJECT,
+                ownerUuid: res.ownerUuid,
+                isTrashed: res.isTrashed
             }));
         }
     };
@@ -78,10 +93,12 @@ export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>) =>
         // const uuid = match ? match.params.id : '';
         const uuid = pathname.split('/').slice(-1)[0];
         const resource = {
-            uuid,
+            uuid: '',
+            ownerUuid: '',
+            kind: ResourceKind.PROCESS,
             name: '',
             description: '',
-            kind: ContextMenuKind.PROCESS
+            menuKind: ContextMenuKind.PROCESS
         };
         dispatch<any>(openContextMenu(event, resource));
     };
index ac14c35534dba5b5fd0576ec0f10bac47631b7d8..03d9cc7843a8df8e5f068a74274d0d6cb7a682e0 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { contextMenuActions, ContextMenuAction } from "./context-menu-actions";
+import { contextMenuActions, ContextMenuAction, ContextMenuResource } from "./context-menu-actions";
 
 export interface ContextMenuState {
     open: boolean;
@@ -15,13 +15,6 @@ export interface ContextMenuPosition {
     y: number;
 }
 
-export interface ContextMenuResource {
-    uuid: string;
-    kind: string;
-    name: string;
-    description?: string;
-}
-
 const initialState = {
     open: false,
     position: { x: 0, y: 0 }
index 059c078429833487c553aeb744eaf75ab8a771c7..934af7be94ca04ebe369a7911403c09810592684 100644 (file)
@@ -7,7 +7,7 @@ import { RootState } from "../store";
 import { DataColumns } from "~/components/data-table/data-table";
 import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
 import { DataExplorer } from './data-explorer-reducer';
-import { ListArguments, ListResults } from '~/common/api/common-resource-service';
+import { ListResults } from '~/services/common-service/common-resource-service';
 
 export abstract class DataExplorerMiddlewareService {
     protected readonly id: string;
@@ -42,4 +42,4 @@ export const listResultsToDataExplorerItemsMeta = <R>({ itemsAvailable, offset,
     itemsAvailable,
     page: Math.floor(offset / limit),
     rowsPerPage: limit
-});
\ No newline at end of file
+});
index e5857dd363e75433fb9bdb3822e738c39ea7fa48..f808b97e8835882a682694799a0b52cdc63155f9 100644 (file)
@@ -8,11 +8,11 @@ import { RootState } from "../store";
 import { DataColumns } from "~/components/data-table/data-table";
 import { ServiceRepository } from "~/services/services";
 import { SortDirection } from "~/components/data-table/data-column";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { FilterBuilder } from "~/services/api/filter-builder";
 import { updateFavorites } from "../favorites/favorites-actions";
 import { favoritePanelActions } from "./favorite-panel-action";
 import { Dispatch, MiddlewareAPI } from "redux";
-import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
+import { OrderBuilder, OrderDirection } from "~/services/api/order-builder";
 import { LinkResource } from "~/models/link";
 import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
 import { resourcesActions } from "~/store/resources/resources-actions";
index 6298e2511c48d12a77e549e70cc79135a3f24510..ddb9d29ffdaa05a79f591773412f2723f4500337 100644 (file)
@@ -28,6 +28,8 @@ export const navigateTo = (uuid: string) =>
 
 export const navigateToFavorites = push(Routes.FAVORITES);
 
+export const navigateToTrash = push(Routes.TRASH);
+
 export const navigateToProject = compose(push, getProjectUrl);
 
 export const navigateToCollection = compose(push, getCollectionUrl);
index 62c9a25dd34a306b6d2b8395b054cc6a837309c6..79c6434c5bdf23d63a6efa6bfc4c775a4690e950 100644 (file)
@@ -8,14 +8,14 @@ import { LogEventType } from '~/models/log';
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { Dispatch } from 'redux';
-import { FilterBuilder } from '~/common/api/filter-builder';
 import { groupBy } from 'lodash';
 import { loadProcess } from '~/store/processes/processes-actions';
-import { OrderBuilder } from '~/common/api/order-builder';
 import { LogResource } from '~/models/log';
 import { LogService } from '~/services/log-service/log-service';
-import { ResourceEventMessage } from '../../websocket/resource-event-message';
+import { ResourceEventMessage } from '~/websocket/resource-event-message';
 import { getProcess } from '~/store/processes/process';
+import { FilterBuilder } from "~/services/api/filter-builder";
+import { OrderBuilder } from "~/services/api/order-builder";
 
 export const processLogsPanelActions = unionize({
     RESET_PROCESS_LOGS_PANEL: ofType<{}>(),
index d94cc01e4cad412fab9056885eba44efaf2ab018..031683a7e8af5a48fbf6067de0455c9ee31f2dd3 100644 (file)
@@ -6,7 +6,7 @@ import { Dispatch } from "redux";
 import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { updateResources } from '~/store/resources/resources-actions';
-import { FilterBuilder } from '~/common/api/filter-builder';
+import { FilterBuilder } from '~/services/api/filter-builder';
 import { ContainerRequestResource } from '../../models/container-request';
 import { Process } from './process';
 
index da7f5b33e0d96e928f534f9491e501004fabd3f7..aade4172f99201cc10205109bd8740c37d9f72ee 100644 (file)
@@ -8,8 +8,8 @@ import { RootState } from "../store";
 import { DataColumns } from "~/components/data-table/data-table";
 import { ServiceRepository } from "~/services/services";
 import { SortDirection } from "~/components/data-table/data-column";
-import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { OrderBuilder, OrderDirection } from "~/services/api/order-builder";
+import { FilterBuilder } from "~/services/api/filter-builder";
 import { GroupContentsResourcePrefix, GroupContentsResource } from "~/services/groups-service/groups-service";
 import { updateFavorites } from "../favorites/favorites-actions";
 import { projectPanelActions, PROJECT_PANEL_CURRENT_UUID } from './project-panel-action';
@@ -19,7 +19,7 @@ import { updateResources } from "~/store/resources/resources-actions";
 import { getProperty } from "~/store/properties/properties";
 import { snackbarActions } from '../snackbar/snackbar-actions';
 import { DataExplorer, getDataExplorer } from '../data-explorer/data-explorer-reducer';
-import { ListResults } from '~/common/api/common-resource-service';
+import { ListResults } from '~/services/common-service/common-resource-service';
 
 export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
index 76f20590763ff8c0791117faedeee73614ce7aa3..6d704c207edb6c5b1cd463466923303cf251ae72 100644 (file)
@@ -6,7 +6,7 @@ import { Dispatch } from "redux";
 import { reset, startSubmit, stopSubmit, initialize } from 'redux-form';
 import { RootState } from '~/store/store';
 import { dialogActions } from "~/store/dialog/dialog-actions";
-import { getCommonResourceServiceError, CommonResourceServiceError } from '~/common/api/common-resource-service';
+import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
 import { ProjectResource } from '~/models/project';
 import { ServiceRepository } from '~/services/services';
 
index 6e24314ccc6f00a75bbcbb06629defb9a716dff2..c251bdf8f8724d3a6fd7969dce4aae0b011d1ab2 100644 (file)
@@ -7,7 +7,7 @@ import { dialogActions } from "~/store/dialog/dialog-actions";
 import { startSubmit, stopSubmit, initialize } from 'redux-form';
 import { ServiceRepository } from '~/services/services';
 import { RootState } from '~/store/store';
-import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
 
index 39b97b24f782e7d0ac1e99a46b441f8828352fb8..afa2e35e8d7c6555055ba053c0bfaeadb6145d84 100644 (file)
@@ -6,10 +6,10 @@ import { Dispatch } from "redux";
 import { initialize, startSubmit, stopSubmit } from 'redux-form';
 import { RootState } from "~/store/store";
 import { dialogActions } from "~/store/dialog/dialog-actions";
-import { ContextMenuResource } from '~/store/context-menu/context-menu-reducer';
-import { getCommonResourceServiceError, CommonResourceServiceError } from "~/common/api/common-resource-service";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
 import { ServiceRepository } from "~/services/services";
 import { ProjectResource } from '~/models/project';
+import { ContextMenuResource } from "~/store/context-menu/context-menu-actions";
 
 export interface ProjectUpdateFormDialogData {
     uuid: string;
@@ -40,4 +40,4 @@ export const updateProject = (project: Partial<ProjectResource>) =>
             }
             return ;
         }
-    };
\ No newline at end of file
+    };
index 8fbc375c9a706016b2a98f660c8a8749e1aa65b8..c102ab3cd408407fc3cf93f052ceb7cc0a384223 100644 (file)
@@ -7,7 +7,7 @@ import { treePickerActions } from "~/store/tree-picker/tree-picker-actions";
 import { createTreePickerNode, TreePickerNode } from '~/store/tree-picker/tree-picker';
 import { RootState } from '../store';
 import { ServiceRepository } from '~/services/services';
-import { FilterBuilder } from '~/common/api/filter-builder';
+import { FilterBuilder } from '~/services/api/filter-builder';
 import { resourcesActions } from '../resources/resources-actions';
 import { getTreePicker, TreePicker } from '../tree-picker/tree-picker';
 import { TreeItemStatus } from "~/components/tree/tree";
index 8c7ef4a7a1ff7ff6d8d3f1023e8d7d93cefd6f08..3b66157dcd83262b1edfc35e5e3b131e7d885cd0 100644 (file)
@@ -4,7 +4,7 @@
 
 import { Dispatch } from 'redux';
 import { isSidePanelTreeCategory, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
-import { navigateToFavorites, navigateTo } from '../navigation/navigation-action';
+import { navigateToFavorites, navigateTo, navigateToTrash } from '../navigation/navigation-action';
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 
 export const navigateFromSidePanel = (id: string) =>
@@ -20,6 +20,8 @@ const getSidePanelTreeCategoryAction = (id: string) => {
     switch (id) {
         case SidePanelTreeCategory.FAVORITES:
             return navigateToFavorites;
+        case SidePanelTreeCategory.TRASH:
+            return navigateToTrash;
         default:
             return sidePanelTreeCategoryNotAvailable(id);
     }
@@ -29,4 +31,4 @@ const sidePanelTreeCategoryNotAvailable = (id: string) =>
     snackbarActions.OPEN_SNACKBAR({
         message: `${id} not available`,
         hideDuration: 3000,
-    });
\ No newline at end of file
+    });
index d0c0dd67b10453d3a11191ecd7418e10dc7b00bc..01aca598be44d384a052e4d4d5f85c6955e2d9b8 100644 (file)
@@ -28,6 +28,8 @@ import { resourcesReducer } from '~/store/resources/resources-reducer';
 import { propertiesReducer } from './properties/properties-reducer';
 import { RootState } from './store';
 import { fileUploaderReducer } from './file-uploader/file-uploader-reducer';
+import { TrashPanelMiddlewareService } from "~/store/trash-panel/trash-panel-middleware-service";
+import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-action";
 import { processLogsPanelReducer } from './process-logs-panel/process-logs-panel-reducer';
 
 const composeEnhancers =
@@ -48,12 +50,16 @@ export function configureStore(history: History, services: ServiceRepository): R
     const favoritePanelMiddleware = dataExplorerMiddleware(
         new FavoritePanelMiddlewareService(services, FAVORITE_PANEL_ID)
     );
+    const trashPanelMiddleware = dataExplorerMiddleware(
+        new TrashPanelMiddlewareService(services, TRASH_PANEL_ID)
+    );
 
     const middlewares: Middleware[] = [
         routerMiddleware(history),
         thunkMiddleware.withExtraArgument(services),
         projectPanelMiddleware,
-        favoritePanelMiddleware
+        favoritePanelMiddleware,
+        trashPanelMiddleware
     ];
     const enhancer = composeEnhancers(applyMiddleware(...middlewares));
     return createStore(rootReducer, enhancer);
diff --git a/src/store/trash-panel/trash-panel-action.ts b/src/store/trash-panel/trash-panel-action.ts
new file mode 100644 (file)
index 0000000..6be9322
--- /dev/null
@@ -0,0 +1,11 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
+import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+
+export const TRASH_PANEL_ID = "trashPanel";
+export const trashPanelActions = bindDataExplorerActions(TRASH_PANEL_ID);
+
+export const loadTrashPanel = () => trashPanelActions.REQUEST_ITEMS();
diff --git a/src/store/trash-panel/trash-panel-middleware-service.ts b/src/store/trash-panel/trash-panel-middleware-service.ts
new file mode 100644 (file)
index 0000000..19ed3be
--- /dev/null
@@ -0,0 +1,84 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import {
+    DataExplorerMiddlewareService, dataExplorerToListParams,
+    listResultsToDataExplorerItemsMeta
+} from "../data-explorer/data-explorer-middleware-service";
+import { RootState } from "../store";
+import { DataColumns } from "~/components/data-table/data-table";
+import { ServiceRepository } from "~/services/services";
+import { SortDirection } from "~/components/data-table/data-column";
+import { FilterBuilder } from "~/services/api/filter-builder";
+import { trashPanelActions } from "./trash-panel-action";
+import { Dispatch, MiddlewareAPI } from "redux";
+import { OrderBuilder, OrderDirection } from "~/services/api/order-builder";
+import { GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
+import { TrashPanelColumnNames, TrashPanelFilter } from "~/views/trash-panel/trash-panel";
+import { ProjectResource } from "~/models/project";
+import { ProjectPanelColumnNames } from "~/views/project-panel/project-panel";
+import { updateFavorites } from "~/store/favorites/favorites-actions";
+import { TrashableResource } from "~/models/resource";
+import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { updateResources } from "~/store/resources/resources-actions";
+
+export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
+    constructor(private services: ServiceRepository, id: string) {
+        super(id);
+    }
+
+    async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+        const dataExplorer = api.getState().dataExplorer[this.getId()];
+        const columns = dataExplorer.columns as DataColumns<string, TrashPanelFilter>;
+        const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+        const typeFilters = this.getColumnFilters(columns, TrashPanelColumnNames.TYPE);
+
+        const order = new OrderBuilder<ProjectResource>();
+
+        if (sortColumn) {
+            const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+                ? OrderDirection.ASC
+                : OrderDirection.DESC;
+
+            const columnName = sortColumn && sortColumn.name === ProjectPanelColumnNames.NAME ? "name" : "createdAt";
+            order
+                .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.COLLECTION)
+                .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT);
+        }
+
+        try {
+            const userUuid = this.services.authService.getUuid()!;
+            const listResults = await this.services.groupsService
+                .contents(userUuid, {
+                    ...dataExplorerToListParams(dataExplorer),
+                    order: order.getOrder(),
+                    filters: new FilterBuilder()
+                        .addIsA("uuid", typeFilters.map(f => f.type))
+                        .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
+                        .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
+                        .getFilters(),
+                    recursive: true,
+                    includeTrash: true
+                });
+
+            const items = listResults.items
+                .filter(it => (it as TrashableResource).isTrashed)
+                .map(it => it.uuid);
+
+            api.dispatch(trashPanelActions.SET_ITEMS({
+                ...listResultsToDataExplorerItemsMeta(listResults),
+                items
+            }));
+            api.dispatch<any>(updateFavorites(items));
+            api.dispatch(updateResources(listResults.items));
+        } catch (e) {
+            api.dispatch(couldNotFetchTrashContents());
+        }
+    }
+}
+
+const couldNotFetchTrashContents = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Could not fetch trash contents.'
+    });
diff --git a/src/store/trash/trash-actions.ts b/src/store/trash/trash-actions.ts
new file mode 100644 (file)
index 0000000..cd6df55
--- /dev/null
@@ -0,0 +1,66 @@
+// 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 { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
+import { activateSidePanelTreeItem, loadSidePanelTreeProjects } from "~/store/side-panel-tree/side-panel-tree-actions";
+import { projectPanelActions } from "~/store/project-panel/project-panel-action";
+import { ResourceKind, TrashableResource } from "~/models/resource";
+
+export const toggleProjectTrashed = (uuid: string, ownerUuid: string, isTrashed: boolean) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+        if (isTrashed) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
+            await services.groupsService.untrash(uuid);
+            dispatch<any>(activateSidePanelTreeItem(uuid));
+            dispatch(trashPanelActions.REQUEST_ITEMS());
+            dispatch(snackbarActions.CLOSE_SNACKBAR());
+            dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: "Restored from trash",
+                hideDuration: 2000
+            }));
+        } else {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
+            await services.groupsService.trash(uuid);
+            dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
+            dispatch(snackbarActions.CLOSE_SNACKBAR());
+            dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: "Added to trash",
+                hideDuration: 2000
+            }));
+        }
+    };
+
+export const toggleCollectionTrashed = (uuid: string, isTrashed: boolean) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+        if (isTrashed) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
+            await services.collectionService.untrash(uuid);
+            dispatch(trashPanelActions.REQUEST_ITEMS());
+            dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: "Restored from trash",
+                hideDuration: 2000
+            }));
+        } else {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
+            await services.collectionService.trash(uuid);
+            dispatch(projectPanelActions.REQUEST_ITEMS());
+            dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: "Added to trash",
+                hideDuration: 2000
+            }));
+        }
+    };
+
+export const toggleTrashed = (kind: ResourceKind, uuid: string, ownerUuid: string, isTrashed: boolean) =>
+    (dispatch: Dispatch) => {
+        if (kind === ResourceKind.PROJECT) {
+            dispatch<any>(toggleProjectTrashed(uuid, ownerUuid, isTrashed!!));
+        } else if (kind === ResourceKind.COLLECTION) {
+            dispatch<any>(toggleCollectionTrashed(uuid, isTrashed!!));
+        }
+    };
index 8c7ec9a30f96a25ed699a38d78a0a6c67eb32760..80f50fe153744382832c81aa34d63f8976ea2869 100644 (file)
@@ -30,6 +30,8 @@ import * as collectionUpdateActions from '~/store/collections/collection-update-
 import * as collectionMoveActions from '~/store/collections/collection-move-actions';
 import * as processesActions from '../processes/processes-actions';
 import { getProcess } from '../processes/process';
+import { trashPanelColumns } from "~/views/trash-panel/trash-panel";
+import { loadTrashPanel, trashPanelActions } from "~/store/trash-panel/trash-panel-action";
 import { initProcessLogsPanel } from '../process-logs-panel/process-logs-panel-actions';
 
 
@@ -42,6 +44,7 @@ export const loadWorkbench = () =>
             if (userResource) {
                 dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
                 dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
+                dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
                 dispatch<any>(initSidePanelTree());
                 if (router.location) {
                     const match = matchRootRoute(router.location.pathname);
@@ -64,6 +67,12 @@ export const loadFavorites = () =>
         dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
     };
 
+export const loadTrash = () =>
+    (dispatch: Dispatch) => {
+        dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
+        dispatch<any>(loadTrashPanel());
+        dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.TRASH));
+    };
 
 export const loadProject = (uuid: string) =>
     async (dispatch: Dispatch) => {
@@ -204,10 +213,10 @@ export const couldNotLoadUser = snackbarActions.OPEN_SNACKBAR({
     message: 'Could not load user'
 });
 
-const reloadProjectMatchingUuid = (matchingUuids: string[]) =>
+export const reloadProjectMatchingUuid = (matchingUuids: string[]) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const currentProjectPanelUuid = getProjectPanelCurrentUuid(getState());
         if (currentProjectPanelUuid && matchingUuids.some(uuid => uuid === currentProjectPanelUuid)) {
             dispatch<any>(loadProject(currentProjectPanelUuid));
         }
-    };
\ No newline at end of file
+    };
index ccb18c8f98d715eac025855d502a2c03bc021880..edc100f783d282745172da4bf68cfe6873436b84 100644 (file)
@@ -3,18 +3,26 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { connect } from "react-redux";
-import { CollectionPanelFiles as Component, CollectionPanelFilesProps } from "~/components/collection-panel-files/collection-panel-files";
+import {
+    CollectionPanelFiles as Component,
+    CollectionPanelFilesProps
+} from "~/components/collection-panel-files/collection-panel-files";
 import { RootState } from "~/store/store";
-import { TreeItemStatus, TreeItem } from "~/components/tree/tree";
-import { CollectionPanelFilesState, CollectionPanelDirectory, CollectionPanelFile } from "~/store/collection-panel/collection-panel-files/collection-panel-files-state";
+import { TreeItem, TreeItemStatus } from "~/components/tree/tree";
+import {
+    CollectionPanelDirectory,
+    CollectionPanelFile,
+    CollectionPanelFilesState
+} from "~/store/collection-panel/collection-panel-files/collection-panel-files-state";
 import { FileTreeData } from "~/components/file-tree/file-tree-data";
 import { Dispatch } from "redux";
 import { collectionPanelFilesAction } from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
 import { ContextMenuKind } from "../context-menu/context-menu";
-import { Tree, getNodeChildrenIds, getNode } from "~/models/tree";
+import { getNode, getNodeChildrenIds, Tree } from "~/models/tree";
 import { CollectionFileType } from "~/models/collection-file";
 import { openContextMenu } from '~/store/context-menu/context-menu-actions';
 import { openUploadCollectionFilesDialog } from '~/store/collections/collection-upload-actions';
+import { ResourceKind } from "~/models/resource";
 
 const memoizedMapStateToProps = () => {
     let prevState: CollectionPanelFilesState;
@@ -43,10 +51,10 @@ const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps,
         dispatch(collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: item.id }));
     },
     onItemMenuOpen: (event, item) => {
-        dispatch<any>(openContextMenu(event, { kind: ContextMenuKind.COLLECTION_FILES_ITEM, name: item.data.name, uuid: item.id }));
+        dispatch<any>(openContextMenu(event, { menuKind: ContextMenuKind.COLLECTION_FILES_ITEM, kind: ResourceKind.COLLECTION, name: item.data.name, uuid: item.id, ownerUuid: '' }));
     },
     onOptionsMenuOpen: (event) => {
-        dispatch<any>(openContextMenu(event, { kind: ContextMenuKind.COLLECTION_FILES, name: '', uuid: '' }));
+        dispatch<any>(openContextMenu(event, { menuKind: ContextMenuKind.COLLECTION_FILES, kind: ResourceKind.COLLECTION, name: '', uuid: '', ownerUuid: '' }));
     },
 });
 
index b3fdc3fbab642561d823cddd003a70b9ab288149..67249fc716ae80d6ddd2f5b1b0c8d4b0892aeeec 100644 (file)
@@ -10,6 +10,8 @@ import { openCollectionUpdateDialog } from "~/store/collections/collection-updat
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
 import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
 import { openCollectionCopyDialog } from "~/store/collections/collection-copy-actions";
+import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
+import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
 
 export const collectionActionSet: ContextMenuActionSet = [[
     {
@@ -39,6 +41,12 @@ export const collectionActionSet: ContextMenuActionSet = [[
             });
         }
     },
+    {
+        component: ToggleTrashAction,
+        execute: (dispatch, resource) => {
+            dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
+        }
+    },
     {
         icon: CopyIcon,
         name: "Copy to project",
index a299b9370ca93f39a437338b486ab639935b323e..b7d3e64f29f97db9620243d2ee93fdabd37ce84f 100644 (file)
@@ -4,12 +4,14 @@
 
 import { ContextMenuActionSet } from "../context-menu-action-set";
 import { ToggleFavoriteAction } from "../actions/favorite-action";
+import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
 import { toggleFavorite } from "~/store/favorites/favorites-actions";
 import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, RemoveIcon } from "~/components/icon/icon";
 import { openCollectionUpdateDialog } from "~/store/collections/collection-update-actions";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
 import { openMoveCollectionDialog } from '~/store/collections/collection-move-actions';
 import { openCollectionCopyDialog } from '~/store/collections/collection-copy-actions';
+import { toggleCollectionTrashed } from "~/store/trash/trash-actions";
 
 export const collectionResourceActionSet: ContextMenuActionSet = [[
     {
@@ -39,6 +41,12 @@ export const collectionResourceActionSet: ContextMenuActionSet = [[
             });
         }
     },
+    {
+        component: ToggleTrashAction,
+        execute: (dispatch, resource) => {
+            dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
+        }
+    },
     {
         icon: CopyIcon,
         name: "Copy to project",
index af10aedf804201b2a1c8bb662e1dcf4d15313936..e5a1915472c7d39ef3f8d9e67c5160c8d437b1ca 100644 (file)
@@ -10,6 +10,8 @@ import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-acti
 import { openMoveProjectDialog } from '~/store/projects/project-move-actions';
 import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
 import { openProjectUpdateDialog } from '~/store/projects/project-update-actions';
+import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
+import { toggleProjectTrashed } from "~/store/trash/trash-actions";
 
 export const projectActionSet: ContextMenuActionSet = [[
     {
@@ -34,6 +36,12 @@ export const projectActionSet: ContextMenuActionSet = [[
             });
         }
     },
+    {
+        component: ToggleTrashAction,
+        execute: (dispatch, resource) => {
+            dispatch<any>(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!));
+        }
+    },
     {
         icon: MoveToIcon,
         name: "Move to",
diff --git a/src/views-components/context-menu/action-sets/trash-action-set.ts b/src/views-components/context-menu/action-sets/trash-action-set.ts
new file mode 100644 (file)
index 0000000..fafd5fe
--- /dev/null
@@ -0,0 +1,16 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { ToggleTrashAction } from "~/views-components/context-menu/actions/trash-action";
+import { toggleTrashed } from "~/store/trash/trash-actions";
+
+export const trashActionSet: ContextMenuActionSet = [[
+    {
+        component: ToggleTrashAction,
+        execute: (dispatch, resource) => {
+            dispatch<any>(toggleTrashed(resource.kind, resource.uuid, resource.ownerUuid, resource.isTrashed!!));
+        }
+    },
+]];
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..e465bb6
--- /dev/null
@@ -0,0 +1,27 @@
+// 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 cbcc0b72f201b76190ea4e7384af8d3433512ba6..c89cd3a5dcf2aa09e4a03953a48c9d960b76ba77 100644 (file)
@@ -4,7 +4,7 @@
 
 import { Dispatch } from "redux";
 import { ContextMenuItem } from "~/components/context-menu/context-menu";
-import { ContextMenuResource } from "~/store/context-menu/context-menu-reducer";
+import { ContextMenuResource } from "~/store/context-menu/context-menu-actions";
 
 export interface ContextMenuAction extends ContextMenuItem {
     execute(dispatch: Dispatch, resource: ContextMenuResource): void;
index d92948c84ae27db97540147cee89de4c59e3ba6e..a545b2bdddfb447ebe5bf43ee768a841d52d36e8 100644 (file)
@@ -4,10 +4,9 @@
 
 import { connect } from "react-redux";
 import { RootState } from "~/store/store";
-import { contextMenuActions } from "~/store/context-menu/context-menu-actions";
+import { contextMenuActions, ContextMenuResource } from "~/store/context-menu/context-menu-actions";
 import { ContextMenu as ContextMenuComponent, ContextMenuProps, ContextMenuItem } from "~/components/context-menu/context-menu";
 import { createAnchorAt } from "~/components/popover/helpers";
-import { ContextMenuResource } from "~/store/context-menu/context-menu-reducer";
 import { ContextMenuActionSet, ContextMenuAction } from "./context-menu-action-set";
 import { Dispatch } from "redux";
 
@@ -52,7 +51,7 @@ export const addMenuActionSet = (name: string, itemSet: ContextMenuActionSet) =>
 };
 
 const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => {
-    return resource ? menuActionSets.get(resource.kind) || [] : [];
+    return resource ? menuActionSets.get(resource.menuKind) || [] : [];
 };
 
 export enum ContextMenuKind {
@@ -60,6 +59,7 @@ export enum ContextMenuKind {
     PROJECT = "Project",
     RESOURCE = "Resource",
     FAVORITE = "Favorite",
+    TRASH = "Trash",
     COLLECTION_FILES = "CollectionFiles",
     COLLECTION_FILES_ITEM = "CollectionFilesItem",
     COLLECTION = 'Collection',
index abf1839286997117c0ecc56d2a5e87f917456e23..c96813df95bb25f8b4f773804ed6eda5df7255b2 100644 (file)
@@ -5,13 +5,13 @@
 import * as React from 'react';
 import { Grid, Typography } from '@material-ui/core';
 import { FavoriteStar } from '../favorite-star/favorite-star';
-import { ResourceKind } from '~/models/resource';
+import { ResourceKind, TrashableResource } from '~/models/resource';
 import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '~/components/icon/icon';
 import { formatDate, formatFileSize } from '~/common/formatters';
 import { resourceLabel } from '~/common/labels';
 import { connect } from 'react-redux';
 import { RootState } from '~/store/store';
-import { getResource } from '../../store/resources/resources';
+import { getResource } from '~/store/resources/resources';
 import { GroupContentsResource } from '~/services/groups-service/groups-service';
 import { ProcessResource } from '~/models/process';
 
@@ -35,7 +35,7 @@ export const renderName = (item: { name: string; uuid: string, kind: string }) =
 
 export const ResourceName = connect(
     (state: RootState, props: { uuid: string }) => {
-        const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+        const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
         return resource || { name: '', uuid: '', kind: '' };
     })(renderName);
 
@@ -52,16 +52,28 @@ export const renderIcon = (item: { kind: string }) => {
     }
 };
 
-export const renderDate = (date: string) => {
+export const renderDate = (date?: string) => {
     return <Typography noWrap>{formatDate(date)}</Typography>;
 };
 
 export const ResourceLastModifiedDate = connect(
     (state: RootState, props: { uuid: string }) => {
-        const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+        const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
         return { date: resource ? resource.modifiedAt : '' };
     })((props: { date: string }) => renderDate(props.date));
 
+export const ResourceTrashDate = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<TrashableResource>(props.uuid)(state.resources);
+        return { date: resource ? resource.trashAt : '' };
+    })((props: { date: string }) => renderDate(props.date));
+
+export const ResourceDeleteDate = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<TrashableResource>(props.uuid)(state.resources);
+        return { date: resource ? resource.deleteAt : '' };
+    })((props: { date: string }) => renderDate(props.date));
+
 export const renderFileSize = (fileSize?: number) =>
     <Typography noWrap>
         {formatFileSize(fileSize)}
@@ -69,7 +81,7 @@ export const renderFileSize = (fileSize?: number) =>
 
 export const ResourceFileSize = connect(
     (state: RootState, props: { uuid: string }) => {
-        const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+        const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
         return {};
     })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
 
@@ -80,7 +92,7 @@ export const renderOwner = (owner: string) =>
 
 export const ResourceOwner = connect(
     (state: RootState, props: { uuid: string }) => {
-        const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+        const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
         return { owner: resource ? resource.ownerUuid : '' };
     })((props: { owner: string }) => renderOwner(props.owner));
 
@@ -91,7 +103,7 @@ export const renderType = (type: string) =>
 
 export const ResourceType = connect(
     (state: RootState, props: { uuid: string }) => {
-        const resource = getResource(props.uuid)(state.resources) as GroupContentsResource | undefined;
+        const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
         return { type: resource ? resource.kind : '' };
     })((props: { type: string }) => renderType(props.type));
 
@@ -102,6 +114,6 @@ export const renderStatus = (item: { status?: string }) =>
 
 export const ProcessStatus = connect(
     (state: RootState, props: { uuid: string }) => {
-        const resource = getResource(props.uuid)(state.resources) as ProcessResource | undefined;
+        const resource = getResource<ProcessResource>(props.uuid)(state.resources);
         return { status: resource ? resource.state : '-' };
     })((props: { status: string }) => renderType(props.status));
index 3859180f0e2d81f0308070d399a1b836d5fa277a..9139ee7c20c531a65668995b0fbe4245323848f8 100644 (file)
@@ -15,7 +15,7 @@ import { ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon } from "~/componen
 import { createTreePickerNode } from "~/store/tree-picker/tree-picker";
 import { RootState } from "~/store/store";
 import { ServiceRepository } from "~/services/services";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { FilterBuilder } from "~/services/api/filter-builder";
 import { WrappedFieldProps } from 'redux-form';
 
 type ProjectTreePickerProps = Pick<TreePickerProps, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen'>;
index 348b548bdb4845eef161ce30210bcf5fde7833d0..748151c6c366d30f3ef650eca3d90738d0369b7a 100644 (file)
@@ -21,7 +21,7 @@ import { CollectionTagForm } from './collection-tag-form';
 import { deleteCollectionTag } from '~/store/collection-panel/collection-panel-action';
 import { snackbarActions } from '~/store/snackbar/snackbar-actions';
 import { getResource } from '~/store/resources/resources';
-import { contextMenuActions, openContextMenu } from '~/store/context-menu/context-menu-actions';
+import { openContextMenu } from '~/store/context-menu/context-menu-actions';
 import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
 
 type CssRules = 'card' | 'iconHeader' | 'tag' | 'copyIcon' | 'label' | 'value';
@@ -71,7 +71,6 @@ export const CollectionPanel = withStyles(styles)(
         };
     })(
         class extends React.Component<CollectionPanelProps> {
-
             render() {
                 const { classes, item, tags } = this.props;
                 return <div>
@@ -134,12 +133,14 @@ export const CollectionPanel = withStyles(styles)(
             }
 
             handleContextMenu = (event: React.MouseEvent<any>) => {
-                const { uuid, name, description } = this.props.item;
+                const { uuid, ownerUuid, name, description, kind } = this.props.item;
                 const resource = {
                     uuid,
+                    ownerUuid,
                     name,
                     description,
-                    kind: ContextMenuKind.COLLECTION
+                    kind,
+                    menuKind: ContextMenuKind.COLLECTION
                 };
                 this.props.dispatch<any>(openContextMenu(event, resource));
             }
@@ -154,7 +155,6 @@ export const CollectionPanel = withStyles(styles)(
                     hideDuration: 2000
                 }));
             }
-
         }
     )
 );
index 9fbae5ced889902d4771af6dbc7bb821e6f15360..2cb30198e7242c54fca8020bdf5579dc57535a66 100644 (file)
@@ -5,23 +5,29 @@
 import * as React from 'react';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
-import { DispatchProp, connect } from 'react-redux';
+import { connect, DispatchProp } from 'react-redux';
 import { DataColumns } from '~/components/data-table/data-table';
 import { RouteComponentProps } from 'react-router';
 import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
-import { ContainerRequestState } from '~/models/container-request';
 import { SortDirection } from '~/components/data-table/data-column';
 import { ResourceKind } from '~/models/resource';
 import { resourceLabel } from '~/common/labels';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { FAVORITE_PANEL_ID } from "~/store/favorite-panel/favorite-panel-action";
-import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner, ResourceName } from '~/views-components/data-explorer/renderers';
+import {
+    ProcessStatus,
+    ResourceFileSize,
+    ResourceLastModifiedDate,
+    ResourceName,
+    ResourceOwner,
+    ResourceType
+} from '~/views-components/data-explorer/renderers';
 import { FavoriteIcon } from '~/components/icon/icon';
 import { Dispatch } from 'redux';
-import { contextMenuActions, openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
-import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
-import { loadDetailsPanel } from '../../store/details-panel/details-panel-action';
+import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-menu/context-menu-actions';
+import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { navigateTo } from '~/store/navigation/navigation-action';
+import { ContainerRequestState } from "~/models/container-request";
 
 type CssRules = "toolbar" | "button";
 
@@ -152,7 +158,13 @@ const mapDispatchToProps = (dispatch: Dispatch): FavoritePanelActionProps => ({
     onContextMenu: (event, resourceUuid) => {
         const kind = resourceKindToContextMenuKind(resourceUuid);
         if (kind) {
-            dispatch<any>(openContextMenu(event, { name: '', uuid: resourceUuid, kind }));
+            dispatch<any>(openContextMenu(event, {
+                name: '',
+                uuid: resourceUuid,
+                ownerUuid: '',
+                kind: ResourceKind.NONE,
+                menuKind: kind
+            }));
         }
     },
     onDialogOpen: (ownerUuid: string) => { return; },
@@ -177,7 +189,8 @@ export const FavoritePanel = withStyles(styles)(
                     onRowDoubleClick={this.props.onItemDoubleClick}
                     onContextMenu={this.props.onContextMenu}
                     defaultIcon={FavoriteIcon}
-                    defaultMessages={['Your favorites list is empty.']} />;
+                    defaultMessages={['Your favorites list is empty.']}
+                    contextMenuColumn={true}/>;
             }
         }
     )
index 06946430e71909d711f1bdc6c32b1ac4c0f80021..1cb72a963b0ff709bff33203ad21859eda374c01 100644 (file)
@@ -20,9 +20,7 @@ import { ProjectIcon } from '~/components/icon/icon';
 import { ResourceName } from '~/views-components/data-explorer/renderers';
 import { ResourcesState, getResource } from '~/store/resources/resources';
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
-import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
-import { contextMenuActions, resourceKindToContextMenuKind, openContextMenu } from '~/store/context-menu/context-menu-actions';
-import { CollectionResource } from '~/models/collection';
+import { resourceKindToContextMenuKind, openContextMenu } from '~/store/context-menu/context-menu-actions';
 import { ProjectResource } from '~/models/project';
 import { navigateTo } from '~/store/navigation/navigation-action';
 import { getProperty } from '~/store/properties/properties';
@@ -185,7 +183,8 @@ export const ProjectPanel = withStyles(styles)(
                         onRowDoubleClick={this.handleRowDoubleClick}
                         onContextMenu={this.handleContextMenu}
                         defaultIcon={ProjectIcon}
-                        defaultMessages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']} />
+                        defaultMessages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']}
+                        contextMenuColumn={true}/>
                 </div>;
             }
 
@@ -198,9 +197,17 @@ export const ProjectPanel = withStyles(styles)(
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
-                const kind = resourceKindToContextMenuKind(resourceUuid);
-                if (kind) {
-                    this.props.dispatch<any>(openContextMenu(event, { name: '', uuid: resourceUuid, kind }));
+                const menuKind = resourceKindToContextMenuKind(resourceUuid);
+                const resource = getResource<ProjectResource>(resourceUuid)(this.props.resources);
+                if (menuKind && resource) {
+                    this.props.dispatch<any>(openContextMenu(event, {
+                        name: resource.name,
+                        uuid: resource.uuid,
+                        ownerUuid: resource.ownerUuid,
+                        isTrashed: resource.isTrashed,
+                        kind: resource.kind,
+                        menuKind
+                    }));
                 }
             }
 
diff --git a/src/views/trash-panel/trash-panel.tsx b/src/views/trash-panel/trash-panel.tsx
new file mode 100644 (file)
index 0000000..08df05c
--- /dev/null
@@ -0,0 +1,199 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { IconButton, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
+import { connect, DispatchProp } from 'react-redux';
+import { DataColumns } from '~/components/data-table/data-table';
+import { RootState } from '~/store/store';
+import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
+import { SortDirection } from '~/components/data-table/data-column';
+import { ResourceKind, TrashableResource } from '~/models/resource';
+import { resourceLabel } from '~/common/labels';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { RestoreFromTrashIcon, TrashIcon } from '~/components/icon/icon';
+import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-action";
+import { getProperty } from "~/store/properties/properties";
+import { PROJECT_PANEL_CURRENT_UUID } from "~/store/project-panel/project-panel-action";
+import { openContextMenu } from "~/store/context-menu/context-menu-actions";
+import { getResource, ResourcesState } from "~/store/resources/resources";
+import {
+    ResourceDeleteDate,
+    ResourceFileSize,
+    ResourceName,
+    ResourceTrashDate,
+    ResourceType
+} from "~/views-components/data-explorer/renderers";
+import { navigateTo } from "~/store/navigation/navigation-action";
+import { loadDetailsPanel } from "~/store/details-panel/details-panel-action";
+import { toggleCollectionTrashed, toggleProjectTrashed, toggleTrashed } from "~/store/trash/trash-actions";
+import { ContextMenuKind } from "~/views-components/context-menu/context-menu";
+import { Dispatch } from "redux";
+
+type CssRules = "toolbar" | "button";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    toolbar: {
+        paddingBottom: theme.spacing.unit * 3,
+        textAlign: "right"
+    },
+    button: {
+        marginLeft: theme.spacing.unit
+    },
+});
+
+export enum TrashPanelColumnNames {
+    NAME = "Name",
+    TYPE = "Type",
+    FILE_SIZE = "File size",
+    TRASHED_DATE = "Trashed date",
+    TO_BE_DELETED = "To be deleted"
+}
+
+export interface TrashPanelFilter extends DataTableFilterItem {
+    type: ResourceKind;
+}
+
+export const ResourceRestore =
+    connect((state: RootState, props: { uuid: string, dispatch?: Dispatch<any> }) => {
+        const resource = getResource<TrashableResource>(props.uuid)(state.resources);
+        return { resource, dispatch: props.dispatch };
+    })((props: { resource?: TrashableResource, dispatch?: Dispatch<any> }) =>
+        <IconButton onClick={() => {
+            if (props.resource && props.dispatch) {
+                props.dispatch(toggleTrashed(
+                    props.resource.kind,
+                    props.resource.uuid,
+                    props.resource.ownerUuid,
+                    props.resource.isTrashed
+                ));
+            }
+        }}>
+            <RestoreFromTrashIcon/>
+        </IconButton>
+    );
+
+export const trashPanelColumns: DataColumns<string, TrashPanelFilter> = [
+    {
+        name: TrashPanelColumnNames.NAME,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.ASC,
+        filters: [],
+        render: uuid => <ResourceName uuid={uuid}/>,
+        width: "450px"
+    },
+    {
+        name: TrashPanelColumnNames.TYPE,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: [
+            {
+                name: resourceLabel(ResourceKind.COLLECTION),
+                selected: true,
+                type: ResourceKind.COLLECTION
+            },
+            {
+                name: resourceLabel(ResourceKind.PROCESS),
+                selected: true,
+                type: ResourceKind.PROCESS
+            },
+            {
+                name: resourceLabel(ResourceKind.PROJECT),
+                selected: true,
+                type: ResourceKind.PROJECT
+            }
+        ],
+        render: uuid => <ResourceType uuid={uuid}/>,
+        width: "125px"
+    },
+    {
+        name: TrashPanelColumnNames.FILE_SIZE,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: [],
+        render: uuid => <ResourceFileSize uuid={uuid} />,
+        width: "50px"
+    },
+    {
+        name: TrashPanelColumnNames.TRASHED_DATE,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: [],
+        render: uuid => <ResourceTrashDate uuid={uuid} />,
+        width: "50px"
+    },
+    {
+        name: TrashPanelColumnNames.TO_BE_DELETED,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: [],
+        render: uuid => <ResourceDeleteDate uuid={uuid} />,
+        width: "50px"
+    },
+    {
+        name: '',
+        selected: true,
+        configurable: false,
+        sortDirection: SortDirection.NONE,
+        filters: [],
+        render: uuid => <ResourceRestore uuid={uuid}/>,
+        width: "50px"
+    }
+];
+
+interface TrashPanelDataProps {
+    currentItemId: string;
+    resources: ResourcesState;
+}
+
+type TrashPanelProps = TrashPanelDataProps & DispatchProp & WithStyles<CssRules>;
+
+export const TrashPanel = withStyles(styles)(
+    connect((state: RootState) => ({
+        currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
+        resources: state.resources
+    }))(
+        class extends React.Component<TrashPanelProps> {
+            render() {
+                return <DataExplorer
+                    id={TRASH_PANEL_ID}
+                    onRowClick={this.handleRowClick}
+                    onRowDoubleClick={this.handleRowDoubleClick}
+                    onContextMenu={this.handleContextMenu}
+                    defaultIcon={TrashIcon}
+                    defaultMessages={['Your trash list is empty.']}
+                    contextMenuColumn={false}/>
+                ;
+            }
+
+            handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+                const resource = getResource<TrashableResource>(resourceUuid)(this.props.resources);
+                if (resource) {
+                    this.props.dispatch<any>(openContextMenu(event, {
+                        name: '',
+                        uuid: resource.uuid,
+                        ownerUuid: resource.ownerUuid,
+                        isTrashed: resource.isTrashed,
+                        kind: resource.kind,
+                        menuKind: ContextMenuKind.TRASH
+                    }));
+                }
+            }
+
+            handleRowDoubleClick = (uuid: string) => {
+                this.props.dispatch<any>(navigateTo(uuid));
+            }
+
+            handleRowClick = (uuid: string) => {
+                this.props.dispatch(loadDetailsPanel(uuid));
+            }
+        }
+    )
+);
index 21396d1d491306106bbe7210709ccd43ab69f2ef..3c281087c2addf20ad8f3f6e7e31500673a28782 100644 (file)
@@ -39,6 +39,8 @@ import { MoveCollectionDialog } from '~/views-components/dialog-forms/move-colle
 import { FilesUploadCollectionDialog } from '~/views-components/dialog-forms/files-upload-collection-dialog';
 import { PartialCopyCollectionDialog } from '~/views-components/dialog-forms/partial-copy-collection-dialog';
 
+import { TrashPanel } from "~/views/trash-panel/trash-panel";
+
 const APP_BAR_HEIGHT = 100;
 
 type CssRules = 'root' | 'appBar' | 'content' | 'contentWrapper';
@@ -167,6 +169,7 @@ export const Workbench = withStyles(styles)(
                                     <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
                                     <Route path={Routes.FAVORITES} component={FavoritePanel} />
                                     <Route path={Routes.PROCESSES} component={ProcessPanel} />
+                                    <Route path={Routes.TRASH} component={TrashPanel} />
                                     <Route path={Routes.PROCESS_LOGS} component={ProcessLogPanel} />
                                 </Switch>
                             </div>
index 77c1fd323f9c24f2ff88514712cadaaaef3a4a1d..a5ce13d7982aec92130cae83110fda3e0f5134b9 100644 (file)
@@ -4,8 +4,8 @@
 
 import { AuthService } from '~/services/auth-service/auth-service';
 import { ResourceEventMessage } from './resource-event-message';
-import { CommonResourceService } from '~/common/api/common-resource-service';
 import { camelCase } from 'lodash';
+import { CommonResourceService } from "~/services/common-service/common-resource-service";
 
 type MessageListener = (message: ResourceEventMessage) => void;
 
index 634fa8f979b4387b0acdc8c23740ce33459e4ddc..e3f1e192023e8acda5aa84db991010b48a83d3e4 100644 (file)
@@ -9,10 +9,10 @@ import { WebSocketService } from './websocket-service';
 import { ResourceEventMessage } from './resource-event-message';
 import { ResourceKind } from '~/models/resource';
 import { loadProcess } from '~/store/processes/processes-actions';
-import { loadContainers } from '../store/processes/processes-actions';
-import { FilterBuilder } from '~/common/api/filter-builder';
-import { LogEventType } from '../models/log';
+import { loadContainers } from '~/store/processes/processes-actions';
+import { LogEventType } from '~/models/log';
 import { addProcessLogsPanelItem } from '../store/process-logs-panel/process-logs-panel-actions';
+import { FilterBuilder } from "~/services/api/filter-builder";
 
 export const initWebSocket = (config: Config, authService: AuthService, store: RootStore) => {
     const webSocketService = new WebSocketService(config.websocketUrl, authService);