Merge branch 'master' into 13990-collection-files-service-based-on-webdav
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Tue, 14 Aug 2018 09:56:59 +0000 (11:56 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Tue, 14 Aug 2018 09:56:59 +0000 (11:56 +0200)
refs #13990

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

18 files changed:
.env
src/common/xml.ts [new file with mode: 0644]
src/components/context-menu/context-menu.tsx
src/models/collection-file.ts
src/services/collection-service/collection-service.ts
src/services/services.ts
src/store/collection-panel/collection-panel-action.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-reducer.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts
src/views-components/context-menu/action-sets/collection-files-action-set.ts
src/views-components/context-menu/action-sets/collection-files-item-action-set.ts
src/views-components/context-menu/actions/download-action.tsx [new file with mode: 0644]
src/views-components/context-menu/actions/download-collection-file-action.tsx [new file with mode: 0644]
src/views-components/context-menu/actions/favorite-action.tsx
src/views-components/file-remove-dialog/file-remove-dialog.ts
src/views-components/file-remove-dialog/multiple-files-remove-dialog.ts
src/views/workbench/workbench.tsx

diff --git a/.env b/.env
index de1444c0bded77d30caae5614f392b9ba6b1fe12..df56fb28a76b3fbd7d61e647a59af98a3b2e7308 100644 (file)
--- a/.env
+++ b/.env
@@ -4,5 +4,5 @@
 
 REACT_APP_ARVADOS_CONFIG_URL=/config.json
 REACT_APP_ARVADOS_API_HOST=qr1hi.arvadosapi.com
-REACT_APP_ARVADOS_KEEP_WEB_HOST=download.qr1hi.arvadosapi.com
+REACT_APP_ARVADOS_KEEP_WEB_HOST=collections.qr1hi.arvadosapi.com
 HTTPS=true
\ No newline at end of file
diff --git a/src/common/xml.ts b/src/common/xml.ts
new file mode 100644 (file)
index 0000000..c810de9
--- /dev/null
@@ -0,0 +1,8 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const getTagValue = (document: Document | Element, tagName: string, defaultValue: string) => {
+    const [el] = Array.from(document.getElementsByTagName(tagName));
+    return el ? el.innerHTML : defaultValue;
+};
index 95bbeafb4f23774c4a358b19282a60120375f751..4068251bdc04c82487cbf141fbdea910692496a8 100644 (file)
@@ -36,21 +36,23 @@ export class ContextMenu extends React.PureComponent<ContextMenuProps> {
                 {items.map((group, groupIndex) =>
                     <React.Fragment key={groupIndex}>
                         {group.map((item, actionIndex) =>
-                            <ListItem
-                                button
-                                key={actionIndex}
-                                onClick={() => onItemClick(item)}>
-                                {item.icon &&
-                                    <ListItemIcon>
-                                        <item.icon />
-                                    </ListItemIcon>}
-                                {item.name &&
-                                    <ListItemText>
-                                        {item.name}
-                                    </ListItemText>}
-                                {item.component &&
-                                    <item.component />}
-                            </ListItem>)}
+                            item.component
+                                ? <item.component
+                                    key={actionIndex}
+                                    onClick={() => onItemClick(item)} />
+                                : <ListItem
+                                    button
+                                    key={actionIndex}
+                                    onClick={() => onItemClick(item)}>
+                                    {item.icon &&
+                                        <ListItemIcon>
+                                            <item.icon />
+                                        </ListItemIcon>}
+                                    {item.name &&
+                                        <ListItemText>
+                                            {item.name}
+                                        </ListItemText>}
+                                </ListItem>)}
                         {groupIndex < items.length - 1 && <Divider />}
                     </React.Fragment>)}
             </List>
index a4b656c51397c80bbd0319b73bf7f18d6d12323b..d74ada6008b982fe40daca7e601fbfbb99f1ea57 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { Tree } from './tree';
+import { Tree, createTree, setNode } from './tree';
 
 export type CollectionFilesTree = Tree<CollectionDirectory | CollectionFile>;
 
@@ -13,6 +13,7 @@ export enum CollectionFileType {
 
 export interface CollectionDirectory {
     path: string;
+    url: string;
     id: string;
     name: string;
     type: CollectionFileType.DIRECTORY;
@@ -20,6 +21,7 @@ export interface CollectionDirectory {
 
 export interface CollectionFile {
     path: string;
+    url: string;
     id: string;
     name: string;
     size: number;
@@ -34,6 +36,7 @@ export const createCollectionDirectory = (data: Partial<CollectionDirectory>): C
     id: '',
     name: '',
     path: '',
+    url: '',
     type: CollectionFileType.DIRECTORY,
     ...data
 });
@@ -42,7 +45,21 @@ export const createCollectionFile = (data: Partial<CollectionFile>): CollectionF
     id: '',
     name: '',
     path: '',
+    url: '',
     size: 0,
     type: CollectionFileType.FILE,
     ...data
 });
+
+export const createCollectionFilesTree = (data: Array<CollectionDirectory | CollectionFile>) => {
+    const directories = data.filter(item => item.type === CollectionFileType.DIRECTORY);
+    directories.sort((a, b) => a.path.localeCompare(b.path));
+    const files = data.filter(item => item.type === CollectionFileType.FILE);
+    return [...directories, ...files]
+        .reduce((tree, item) => setNode({
+            children: [],
+            id: item.id,
+            parent: item.path,
+            value: item
+        })(tree), createTree<CollectionDirectory | CollectionFile>());
+};
\ No newline at end of file
index f60e81f1975da8b4c37921f7490b9c0fb150c008..d07ef216ac0c51b7e14dbbccff967642e6c3db22 100644 (file)
@@ -2,23 +2,81 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import * as _ from "lodash";
 import { CommonResourceService } from "~/common/api/common-resource-service";
 import { CollectionResource } from "~/models/collection";
 import axios, { AxiosInstance } from "axios";
 import { KeepService } from "../keep-service/keep-service";
+import { WebDAV } from "~/common/webdav";
+import { AuthService } from "../auth-service/auth-service";
+import { mapTree, getNodeChildren, getNode, TreeNode } from "../../models/tree";
+import { getTagValue } from "~/common/xml";
 import { FilterBuilder } from "~/common/api/filter-builder";
-import { CollectionFile, createCollectionFile } from "~/models/collection-file";
+import { CollectionFile, createCollectionFile, CollectionFileType, CollectionDirectory, createCollectionDirectory } from '~/models/collection-file';
 import { parseKeepManifestText, stringifyKeepManifest } from "../collection-files-service/collection-manifest-parser";
-import * as _ from "lodash";
 import { KeepManifestStream } from "~/models/keep-manifest";
+import { createCollectionFilesTree } from '~/models/collection-file';
 
 export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
 
 export class CollectionService extends CommonResourceService<CollectionResource> {
-    constructor(serverApi: AxiosInstance, private keepService: KeepService) {
+    constructor(serverApi: AxiosInstance, private keepService: KeepService, private webdavClient: WebDAV, private authService: AuthService) {
         super(serverApi, "collections");
     }
 
+    async files(uuid: string) {
+        const request = await this.webdavClient.propfind(`/c=${uuid}`);
+        if (request.responseXML != null) {
+            const files = this.extractFilesData(request.responseXML);
+            const tree = createCollectionFilesTree(files);
+            const sortedTree = mapTree(node => {
+                const children = getNodeChildren(node.id)(tree).map(id => getNode(id)(tree)) as TreeNode<CollectionDirectory | CollectionFile>[];
+                children.sort((a, b) =>
+                    a.value.type !== b.value.type
+                        ? a.value.type === CollectionFileType.DIRECTORY ? -1 : 1
+                        : a.value.name.localeCompare(b.value.name)
+                );
+                return { ...node, children: children.map(child => child.id) };
+            })(tree);
+            return sortedTree;
+        }
+        return Promise.reject();
+    }
+
+    async deleteFile(collectionUuid: string, filePath: string) {
+        return this.webdavClient.delete(`/c=${collectionUuid}${filePath}`);
+    }
+
+    extractFilesData(document: Document) {
+        const collectionUrlPrefix = /\/c=[0-9a-zA-Z\-]*/;
+        return Array
+            .from(document.getElementsByTagName('D:response'))
+            .slice(1) // omit first element which is collection itself
+            .map(element => {
+                const name = getTagValue(element, 'D:displayname', '');
+                const size = parseInt(getTagValue(element, 'D:getcontentlength', '0'), 10);
+                const pathname = getTagValue(element, 'D:href', '');
+                const nameSuffix = `/${name || ''}`;
+                const directory = pathname
+                    .replace(collectionUrlPrefix, '')
+                    .replace(nameSuffix, '');
+                const href = this.webdavClient.defaults.baseURL + pathname + '?api_token=' + this.authService.getApiToken();
+
+                const data = {
+                    url: href,
+                    id: `${directory}/${name}`,
+                    name,
+                    path: directory,
+                };
+
+                return getTagValue(element, 'D:resourcetype', '')
+                    ? createCollectionDirectory(data)
+                    : createCollectionFile({ ...data, size });
+
+            });
+    }
+
+
     private readFile(file: File): Promise<ArrayBuffer> {
         return new Promise<ArrayBuffer>(resolve => {
             const reader = new FileReader();
index 99f802dfe079521074c2fa143297e0d1bca0cb2d..61dd399206384c159d0ffe6982e704fe054a405a 100644 (file)
@@ -30,7 +30,7 @@ export const createServices = (config: Config) => {
     const projectService = new ProjectService(apiClient);
     const linkService = new LinkService(apiClient);
     const favoriteService = new FavoriteService(linkService, groupsService);
-    const collectionService = new CollectionService(apiClient, keepService);
+    const collectionService = new CollectionService(apiClient, keepService, webdavClient, authService);
     const tagService = new TagService(linkService);
     const collectionFilesService = new CollectionFilesService(collectionService);
 
index 0772210c8497fae4655a7cc7a004fb8e4d3067b2..06d4d2762288ee5b6a14108a7ac01e336242a494 100644 (file)
@@ -4,7 +4,7 @@
 
 import { unionize, ofType, UnionOf } from "unionize";
 import { Dispatch } from "redux";
-import { ResourceKind } from "~/models/resource";
+import { loadCollectionFiles } from "./collection-panel-files/collection-panel-files-actions";
 import { CollectionResource } from "~/models/collection";
 import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions";
 import { createTree } from "~/models/tree";
@@ -14,7 +14,7 @@ import { TagResource, TagProperty } from "~/models/tag";
 import { snackbarActions } from "../snackbar/snackbar-actions";
 
 export const collectionPanelActions = unionize({
-    LOAD_COLLECTION: ofType<{ uuid: string, kind: ResourceKind }>(),
+    LOAD_COLLECTION: ofType<{ uuid: string }>(),
     LOAD_COLLECTION_SUCCESS: ofType<{ item: CollectionResource }>(),
     LOAD_COLLECTION_TAGS: ofType<{ uuid: string }>(),
     LOAD_COLLECTION_TAGS_SUCCESS: ofType<{ tags: TagResource[] }>(),
@@ -28,18 +28,15 @@ export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
 
 export const COLLECTION_TAG_FORM_NAME = 'collectionTagForm';
 
-export const loadCollection = (uuid: string, kind: ResourceKind) =>
+export const loadCollection = (uuid: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid, kind }));
+        dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid }));
         dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
         return services.collectionService
             .get(uuid)
             .then(item => {
                 dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item }));
-                return services.collectionFilesService.getFiles(item.uuid);
-            })
-            .then(files => {
-                dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(files));
+                dispatch<any>(loadCollectionFiles(uuid));
             });
     };
 
index 09821083ee4c5bda6fce5f6bb6faf1524dad6eb4..cedfbebef5aded305b3237e125ee80857c96361b 100644 (file)
@@ -3,7 +3,14 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { default as unionize, ofType, UnionOf } from "unionize";
-import { CollectionFilesTree } from "~/models/collection-file";
+import { Dispatch } from "redux";
+import { CollectionFilesTree, CollectionFileType } from "~/models/collection-file";
+import { ServiceRepository } from "~/services/services";
+import { RootState } from "../../store";
+import { snackbarActions } from "../../snackbar/snackbar-actions";
+import { dialogActions } from "../../dialog/dialog-actions";
+import { getNodeValue, getNodeDescendants } from "~/models/tree";
+import { CollectionPanelDirectory, CollectionPanelFile } from "./collection-panel-files-state";
 
 export const collectionPanelFilesAction = unionize({
     SET_COLLECTION_FILES: ofType<CollectionFilesTree>(),
@@ -14,3 +21,69 @@ export const collectionPanelFilesAction = unionize({
 }, { tag: 'type', value: 'payload' });
 
 export type CollectionPanelFilesAction = UnionOf<typeof collectionPanelFilesAction>;
+
+export const loadCollectionFiles = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const files = await services.collectionService.files(uuid);
+        dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(files));
+    };
+
+export const removeCollectionFiles = (filePaths: string[]) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { item } = getState().collectionPanel;
+        if (item) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
+            const promises = filePaths.map(filePath => services.collectionService.deleteFile(item.uuid, filePath));
+            await Promise.all(promises);
+            dispatch<any>(loadCollectionFiles(item.uuid));
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000 }));
+        }
+    };
+
+export const removeCollectionsSelectedFiles = () =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const tree = getState().collectionPanelFiles;
+        const allFiles = getNodeDescendants('')(tree)
+            .map(id => getNodeValue(id)(tree))
+            .filter(file => file !== undefined) as Array<CollectionPanelDirectory | CollectionPanelFile>;
+
+        const selectedDirectories = allFiles.filter(file => file.selected && file.type === CollectionFileType.DIRECTORY);
+        const selectedFiles = allFiles.filter(file => file.selected && !selectedDirectories.some(dir => dir.id === file.path));
+        const paths = [...selectedDirectories, ...selectedFiles].map(file => file.id);
+        dispatch<any>(removeCollectionFiles(paths));
+    };
+
+export const FILE_REMOVE_DIALOG = 'fileRemoveDialog';
+export const openFileRemoveDialog = (filePath: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const file = getNodeValue(filePath)(getState().collectionPanelFiles);
+        if (file) {
+            const title = file.type === CollectionFileType.DIRECTORY
+                ? 'Removing directory'
+                : 'Removing file';
+            const text = file.type === CollectionFileType.DIRECTORY
+                ? 'Are you sure you want to remove this directory?'
+                : 'Are you sure you want to remove this file?';
+
+            dispatch(dialogActions.OPEN_DIALOG({
+                id: FILE_REMOVE_DIALOG,
+                data: {
+                    title,
+                    text,
+                    confirmButtonLabel: 'Remove',
+                    filePath
+                }
+            }));
+        }
+    };
+
+export const MULTIPLE_FILES_REMOVE_DIALOG = 'multipleFilesRemoveDialog';
+export const openMultipleFilesRemoveDialog = () =>
+    dialogActions.OPEN_DIALOG({
+        id: MULTIPLE_FILES_REMOVE_DIALOG,
+        data: {
+            title: 'Removing files',
+            text: 'Are you sure you want to remove selected files?',
+            confirmButtonLabel: 'Remove'
+        }
+    });
index 2a3aac744737691bbf6c2a472f002eca0ff363db..08b60308c42bb9d4f3dfdeb72eae23f4b45946de 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, mapCollectionFileToCollectionPanelFile } from "./collection-panel-files-state";
+import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, mapCollectionFileToCollectionPanelFile, mergeCollectionPanelFilesStates } from "./collection-panel-files-state";
 import { CollectionPanelFilesAction, collectionPanelFilesAction } from "./collection-panel-files-actions";
 import { createTree, mapTreeValues, getNode, setNode, getNodeAncestors, getNodeDescendants, setNodeValueWith, mapTree } from "~/models/tree";
 import { CollectionFileType } from "~/models/collection-file";
@@ -10,7 +10,7 @@ import { CollectionFileType } from "~/models/collection-file";
 export const collectionPanelFilesReducer = (state: CollectionPanelFilesState = createTree(), action: CollectionPanelFilesAction) => {
     return collectionPanelFilesAction.match(action, {
         SET_COLLECTION_FILES: files =>
-            mapTree(mapCollectionFileToCollectionPanelFile)(files),
+            mergeCollectionPanelFilesStates(state, mapTree(mapCollectionFileToCollectionPanelFile)(files)),
 
         TOGGLE_COLLECTION_FILE_COLLAPSE: data =>
             toggleCollapse(data.id)(state),
index f7955eb6d0fe8a544003095dce3de8bd4988fc63..35b81d2e121e134b1b10f8aad6223a0267da9765 100644 (file)
@@ -2,8 +2,8 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { Tree, TreeNode, mapTreeValues, getNodeValue } from '~/models/tree';
 import { CollectionFile, CollectionDirectory, CollectionFileType } from '~/models/collection-file';
-import { Tree, TreeNode } from '~/models/tree';
 
 export type CollectionPanelFilesState = Tree<CollectionPanelDirectory | CollectionPanelFile>;
 
@@ -24,3 +24,14 @@ export const mapCollectionFileToCollectionPanelFile = (node: TreeNode<Collection
             : { ...node.value, selected: false }
     };
 };
+
+export const mergeCollectionPanelFilesStates = (oldState: CollectionPanelFilesState, newState: CollectionPanelFilesState) => {
+    return mapTreeValues((value: CollectionPanelDirectory | CollectionPanelFile) => {
+        const oldValue = getNodeValue(value.id)(oldState);
+        return oldValue
+            ? oldValue.type === CollectionFileType.DIRECTORY
+                ? { ...value, collapsed: oldValue.collapsed, selected: oldValue.selected }
+                : { ...value, selected: oldValue.selected }
+            : value;
+    })(newState);
+}; 
index 0bed68e917739eb875fc4e015c556547659f6dd8..653da011f7ed03153cfd3a360c192bf0b36d146c 100644 (file)
@@ -3,8 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { ContextMenuActionSet } from "../context-menu-action-set";
-import { collectionPanelFilesAction } from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
-import { openMultipleFilesRemoveDialog } from "~/views-components/file-remove-dialog/multiple-files-remove-dialog";
+import { collectionPanelFilesAction, openMultipleFilesRemoveDialog } from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
 import { createCollectionWithSelected } from "~/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected";
 
 
@@ -20,7 +19,7 @@ export const collectionFilesActionSet: ContextMenuActionSet = [[{
     }
 }, {
     name: "Remove selected",
-    execute: (dispatch, resource) => {
+    execute: (dispatch) => {
         dispatch(openMultipleFilesRemoveDialog());
     }
 }, {
index 8728ad31e19702674fb718711855bc959c1000c3..a3bfa0b95cb30736ae8c995fcfa00e3deb463b0f 100644 (file)
@@ -5,7 +5,8 @@
 import { ContextMenuActionSet } from "../context-menu-action-set";
 import { RenameIcon, DownloadIcon, RemoveIcon } from "~/components/icon/icon";
 import { openRenameFileDialog } from "../../rename-file-dialog/rename-file-dialog";
-import { openFileRemoveDialog } from "../../file-remove-dialog/file-remove-dialog";
+import { DownloadCollectionFileAction } from "../actions/download-collection-file-action";
+import { openFileRemoveDialog } from "../../../store/collection-panel/collection-panel-files/collection-panel-files-actions";
 
 
 export const collectionFilesItemActionSet: ContextMenuActionSet = [[{
@@ -15,15 +16,12 @@ export const collectionFilesItemActionSet: ContextMenuActionSet = [[{
         dispatch<any>(openRenameFileDialog(resource.name));
     }
 }, {
-    name: "Download",
-    icon: DownloadIcon,
-    execute: (dispatch, resource) => {
-        return;
-    }
+    component: DownloadCollectionFileAction,
+    execute: () => { return; }
 }, {
     name: "Remove",
     icon: RemoveIcon,
     execute: (dispatch, resource) => {
-        dispatch(openFileRemoveDialog(resource.uuid));
+        dispatch<any>(openFileRemoveDialog(resource.uuid));
     }
 }]];
diff --git a/src/views-components/context-menu/actions/download-action.tsx b/src/views-components/context-menu/actions/download-action.tsx
new file mode 100644 (file)
index 0000000..1f6979d
--- /dev/null
@@ -0,0 +1,29 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { ListItemIcon, ListItemText, Button, ListItem } from "@material-ui/core";
+import { DownloadIcon } from "../../../components/icon/icon";
+
+export const DownloadAction = (props: { href?: string, download?: string, onClick?: () => void }) => {
+    const targetProps = props.download ? {} : { target: '_blank' };
+    const downloadProps = props.download ? { download: props.download } : {};
+    return props.href
+        ? <a
+            style={{ textDecoration: 'none' }}
+            href={props.href}
+            onClick={props.onClick}
+            {...targetProps}
+            {...downloadProps}>
+            <ListItem button>
+                <ListItemIcon>
+                    <DownloadIcon />
+                </ListItemIcon>
+                <ListItemText>
+                    Download
+            </ListItemText>
+            </ListItem>
+        </a >
+        : null;
+};
\ No newline at end of file
diff --git a/src/views-components/context-menu/actions/download-collection-file-action.tsx b/src/views-components/context-menu/actions/download-collection-file-action.tsx
new file mode 100644 (file)
index 0000000..460e620
--- /dev/null
@@ -0,0 +1,25 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { RootState } from "../../../store/store";
+import { DownloadAction } from "./download-action";
+import { getNodeValue } from "../../../models/tree";
+import { CollectionFileType } from "../../../models/collection-file";
+
+const mapStateToProps = (state: RootState) => {
+    const { resource } = state.contextMenu;
+    if (resource) {
+        const file = getNodeValue(resource.uuid)(state.collectionPanelFiles);
+        if (file) {
+            return {
+                href: file.url,
+                download: file.type === CollectionFileType.DIRECTORY ? undefined : file.name
+            };
+        }
+    }
+    return {};
+};
+
+export const DownloadCollectionFileAction = connect(mapStateToProps)(DownloadAction);
index 21f037d9e5451fd3a0267ece7ddc1db6f9e3c8fc..1e817ba3a0bc884a31ad6c0f1d77366ccefe1bcc 100644 (file)
@@ -3,25 +3,28 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from "react";
-import { ListItemIcon, ListItemText } from "@material-ui/core";
+import { ListItemIcon, ListItemText, ListItem } from "@material-ui/core";
 import { AddFavoriteIcon, RemoveFavoriteIcon } from "~/components/icon/icon";
 import { connect } from "react-redux";
 import { RootState } from "~/store/store";
 
-const mapStateToProps = (state: RootState) => ({
-    isFavorite: state.contextMenu.resource !== undefined && state.favorites[state.contextMenu.resource.uuid] === true
+const mapStateToProps = (state: RootState, props: { onClick: () => {} }) => ({
+    isFavorite: state.contextMenu.resource !== undefined && state.favorites[state.contextMenu.resource.uuid] === true,
+    onClick: props.onClick
 });
 
-export const ToggleFavoriteAction = connect(mapStateToProps)((props: { isFavorite: boolean }) =>
-    <>
+export const ToggleFavoriteAction = connect(mapStateToProps)((props: { isFavorite: boolean, onClick: () => void }) =>
+    <ListItem
+        button
+        onClick={props.onClick}>
         <ListItemIcon>
             {props.isFavorite
                 ? <RemoveFavoriteIcon />
                 : <AddFavoriteIcon />}
         </ListItemIcon>
-        <ListItemText>
+        <ListItemText style={{ textDecoration: 'none' }}>
             {props.isFavorite
                 ? <>Remove from favorites</>
                 : <>Add to favorites</>}
         </ListItemText>
-    </>);
+    </ListItem >);
index 04497933c87effdbe6b81ba540bd4adf7ae90f52..c83181c2823bd502b0b4209f1a285f9a3319528f 100644 (file)
@@ -5,34 +5,29 @@
 import { Dispatch } from "redux";
 import { connect } from "react-redux";
 import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog";
-import { withDialog } from "~/store/dialog/with-dialog";
-import { dialogActions } from "~/store/dialog/dialog-actions";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { withDialog, WithDialogProps } from '~/store/dialog/with-dialog';
+import { RootState } from '~/store/store';
+import { removeCollectionFiles, FILE_REMOVE_DIALOG } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
 
-const FILE_REMOVE_DIALOG = 'fileRemoveDialog';
+const mapStateToProps = (state: RootState, props: WithDialogProps<{ filePath: string }>) => ({
+    filePath: props.data.filePath
+});
 
-const mapDispatchToProps = (dispatch: Dispatch) => ({
-    onConfirm: () => {
-        // TODO: dispatch action that removes single file
-        dispatch(dialogActions.CLOSE_DIALOG({ id: FILE_REMOVE_DIALOG }));
-        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing file...', hideDuration: 2000 }));
-        setTimeout(() => {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'File removed.', hideDuration: 2000 }));
-        }, 1000);
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<{ filePath: string }>) => ({
+    onConfirm: (filePath: string) => {
+        props.closeDialog();
+        dispatch<any>(removeCollectionFiles([filePath]));
     }
 });
 
-export const openFileRemoveDialog = (fileId: string) =>
-    dialogActions.OPEN_DIALOG({
-        id: FILE_REMOVE_DIALOG,
-        data: {
-            title: 'Removing file',
-            text: 'Are you sure you want to remove this file?',
-            confirmButtonLabel: 'Remove',
-            fileId
-        }
+const mergeProps = (
+    stateProps: { filePath: string },
+    dispatchProps: { onConfirm: (filePath: string) => void },
+    props: WithDialogProps<{ filePath: string }>) => ({
+        onConfirm: () => dispatchProps.onConfirm(stateProps.filePath),
+        ...props
     });
 
 export const [FileRemoveDialog] = [ConfirmationDialog]
-    .map(withDialog(FILE_REMOVE_DIALOG))
-    .map(connect(undefined, mapDispatchToProps));
+    .map(connect(mapStateToProps, mapDispatchToProps, mergeProps))
+    .map(withDialog(FILE_REMOVE_DIALOG));
index 1362de6b8ac424c479c460674cb1eb43fe5996a7..03dae1dd8bc719b3b5062f0eec22aee855c150b3 100644 (file)
@@ -4,34 +4,17 @@
 
 import { Dispatch } from "redux";
 import { connect } from "react-redux";
+import { MULTIPLE_FILES_REMOVE_DIALOG, removeCollectionsSelectedFiles } from "../../store/collection-panel/collection-panel-files/collection-panel-files-actions";
 import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog";
-import { withDialog } from "~/store/dialog/with-dialog";
-import { dialogActions } from "~/store/dialog/dialog-actions";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
 
-const MULTIPLE_FILES_REMOVE_DIALOG = 'multipleFilesRemoveDialog';
-
-const mapDispatchToProps = (dispatch: Dispatch) => ({
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
     onConfirm: () => {
-        // TODO: dispatch action that removes multiple files
-        dispatch(dialogActions.CLOSE_DIALOG({ id: MULTIPLE_FILES_REMOVE_DIALOG }));
-        dispatch(snackbarActions.OPEN_SNACKBAR({message: 'Removing files...', hideDuration: 2000}));
-        setTimeout(() => {
-            dispatch(snackbarActions.OPEN_SNACKBAR({message: 'Files removed.', hideDuration: 2000}));
-        }, 1000);
+        props.closeDialog();
+        dispatch<any>(removeCollectionsSelectedFiles());
     }
 });
 
-export const openMultipleFilesRemoveDialog = () =>
-    dialogActions.OPEN_DIALOG({
-        id: MULTIPLE_FILES_REMOVE_DIALOG,
-        data: {
-            title: 'Removing files',
-            text: 'Are you sure you want to remove selected files?',
-            confirmButtonLabel: 'Remove'
-        }
-    });
-
 export const [MultipleFilesRemoveDialog] = [ConfirmationDialog]
-    .map(withDialog(MULTIPLE_FILES_REMOVE_DIALOG))
-    .map(connect(undefined, mapDispatchToProps));
+    .map(connect(undefined, mapDispatchToProps))
+    .map(withDialog(MULTIPLE_FILES_REMOVE_DIALOG));
index a0be3729964eb87a33b33d802cedbc265a4376d1..58641fb36bc5e8fdf629bd2006b009d98660a834 100644 (file)
@@ -255,7 +255,7 @@ export const Workbench = withStyles(styles)(
 
             renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
                 onItemRouteChange={(collectionId) => {
-                    this.props.dispatch<any>(loadCollection(collectionId, ResourceKind.COLLECTION));
+                    this.props.dispatch<any>(loadCollection(collectionId));
                     this.props.dispatch<any>(loadCollectionTags(collectionId));
                 }}
                 onContextMenu={(event, item) => {
@@ -296,7 +296,7 @@ export const Workbench = withStyles(styles)(
                 onItemDoubleClick={item => {
                     switch (item.kind) {
                         case ResourceKind.COLLECTION:
-                            this.props.dispatch(loadCollection(item.uuid, item.kind as ResourceKind));
+                            this.props.dispatch(loadCollection(item.uuid));
                             this.props.dispatch(push(getCollectionUrl(item.uuid)));
                         default:
                             this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
@@ -323,7 +323,7 @@ export const Workbench = withStyles(styles)(
                 onItemDoubleClick={item => {
                     switch (item.kind) {
                         case ResourceKind.COLLECTION:
-                            this.props.dispatch(loadCollection(item.uuid, item.kind as ResourceKind));
+                            this.props.dispatch(loadCollection(item.uuid));
                             this.props.dispatch(push(getCollectionUrl(item.uuid)));
                         default:
                             this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));