Merge branch 'master' into 13854-tags-card
authorJanicki Artur <artur.janicki@contractors.roche.com>
Tue, 7 Aug 2018 09:33:53 +0000 (11:33 +0200)
committerJanicki Artur <artur.janicki@contractors.roche.com>
Tue, 7 Aug 2018 09:33:53 +0000 (11:33 +0200)
refs #13854

Arvados-DCO-1.1-Signed-off-by: Janicki Artur <artur.janicki@contractors.roche.com>

51 files changed:
package.json
src/components/collection-panel-files/collection-panel-files.tsx [new file with mode: 0644]
src/components/file-tree/file-tree-data.ts [new file with mode: 0644]
src/components/file-tree/file-tree-item.tsx [new file with mode: 0644]
src/components/file-tree/file-tree.tsx [new file with mode: 0644]
src/components/icon/icon.tsx
src/components/list-item-text-icon/list-item-text-icon.tsx
src/components/side-panel/side-panel.tsx
src/components/tree/tree.test.tsx
src/components/tree/tree.tsx
src/index.tsx
src/models/collection-file.ts [new file with mode: 0644]
src/models/keep-manifest.ts [new file with mode: 0644]
src/models/object-types.ts [new file with mode: 0644]
src/models/tree.test.ts [new file with mode: 0644]
src/models/tree.ts [new file with mode: 0644]
src/services/collection-files-service/collection-files-service.ts [new file with mode: 0644]
src/services/collection-files-service/collection-manifest-mapper.test.ts [new file with mode: 0644]
src/services/collection-files-service/collection-manifest-mapper.ts [new file with mode: 0644]
src/services/collection-files-service/collection-manifest-parser.test.ts [new file with mode: 0644]
src/services/collection-files-service/collection-manifest-parser.ts [new file with mode: 0644]
src/services/services.ts
src/store/collection-panel/collection-panel-action.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts [new file with mode: 0644]
src/store/collection-panel/collection-panel-files/collection-panel-files-reducer.test.ts [new file with mode: 0644]
src/store/collection-panel/collection-panel-files/collection-panel-files-reducer.ts [new file with mode: 0644]
src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts [new file with mode: 0644]
src/store/details-panel/details-panel-action.ts
src/store/dialog/dialog-actions.ts [new file with mode: 0644]
src/store/dialog/dialog-reducer.test.ts [new file with mode: 0644]
src/store/dialog/dialog-reducer.ts [new file with mode: 0644]
src/store/dialog/with-dialog.ts [new file with mode: 0644]
src/store/navigation/navigation-action.ts
src/store/store.ts
src/validators/create-collection/create-collection-validator.tsx [new file with mode: 0644]
src/validators/create-project/create-project-validator.tsx
src/views-components/collection-panel-files/collection-panel-files.ts [new file with mode: 0644]
src/views-components/context-menu/action-sets/collection-files-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/action-sets/collection-files-item-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/actions/favorite-action.tsx
src/views-components/context-menu/context-menu.tsx
src/views-components/create-project-dialog/create-project-dialog.tsx
src/views-components/data-explorer/data-explorer.tsx
src/views-components/dialog-create/dialog-collection-create.tsx
src/views-components/dialog-update/dialog-collection-update.tsx
src/views-components/remove-dialog/remove-dialog.tsx [new file with mode: 0644]
src/views-components/rename-dialog/rename-dialog.tsx [new file with mode: 0644]
src/views/collection-panel/collection-panel.tsx
src/views/project-panel/project-panel.tsx
src/views/workbench/workbench.tsx
yarn.lock

index 0b0ebcd770c82cd2f289f232d60b4ff3441554ba..f940e54ee3e4b138ee7000daf58eb41ca5e38d67 100644 (file)
@@ -3,17 +3,17 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
-    "@material-ui/core": "1.4.0",
-    "@material-ui/icons": "1.1.0",
-    "@types/lodash": "4.14.112",
+    "@material-ui/core": "1.4.2",
+    "@material-ui/icons": "2.0.0",
+    "@types/lodash": "4.14.116",
     "@types/react-copy-to-clipboard": "4.2.5",
-    "@types/redux-form": "7.4.1",
+    "@types/redux-form": "7.4.4",
     "axios": "0.18.0",
     "classnames": "2.2.6",
     "lodash": "4.17.10",
-    "react": "16.4.1",
+    "react": "16.4.2",
     "react-copy-to-clipboard": "5.0.1",
-    "react-dom": "16.4.1",
+    "react-dom": "16.4.2",
     "react-redux": "5.0.7",
     "react-router": "4.3.1",
     "react-router-dom": "4.3.1",
     "@types/classnames": "^2.2.4",
     "@types/enzyme": "3.1.12",
     "@types/enzyme-adapter-react-16": "1.0.2",
-    "@types/jest": "23.3.0",
-    "@types/node": "10.5.2",
+    "@types/jest": "23.3.1",
+    "@types/node": "10.5.5",
     "@types/react": "16.4",
     "@types/react-dom": "16.0.6",
-    "@types/react-redux": "6.0.4",
+    "@types/react-redux": "6.0.6",
     "@types/react-router": "4.0.29",
-    "@types/react-router-dom": "4.2.7",
+    "@types/react-router-dom": "4.3.0",
     "@types/react-router-redux": "5.0.15",
     "@types/redux-devtools": "3.0.44",
-    "@types/redux-form": "7.4.1",
+    "@types/redux-form": "7.4.4",
     "axios-mock-adapter": "1.15.0",
     "enzyme": "3.3.0",
     "enzyme-adapter-react-16": "1.1.1",
     "jest-localstorage-mock": "2.2.0",
     "redux-devtools": "3.4.1",
     "redux-form": "7.4.2",
-    "typescript": "2.9.2"
+    "typescript": "3.0.1"
   },
   "moduleNameMapper": {
     "^~/(.*)$": "<rootDir>/src/$1"
diff --git a/src/components/collection-panel-files/collection-panel-files.tsx b/src/components/collection-panel-files/collection-panel-files.tsx
new file mode 100644 (file)
index 0000000..17bbe85
--- /dev/null
@@ -0,0 +1,68 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { TreeItem, TreeItemStatus } from '../tree/tree';
+import { FileTreeData } from '../file-tree/file-tree-data';
+import { FileTree } from '../file-tree/file-tree';
+import { IconButton, Grid, Typography, StyleRulesCallback, withStyles, WithStyles, CardHeader, CardContent, Card, Button } from '@material-ui/core';
+import { CustomizeTableIcon } from '../icon/icon';
+
+export interface CollectionPanelFilesProps {
+    items: Array<TreeItem<FileTreeData>>;
+    onUploadDataClick: () => void;
+    onItemMenuOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
+    onOptionsMenuOpen: (event: React.MouseEvent<HTMLElement>) => void;
+    onSelectionToggle: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
+    onCollapseToggle: (id: string, status: TreeItemStatus) => void;
+}
+
+type CssRules = 'root' | 'cardSubheader' | 'nameHeader' | 'fileSizeHeader';
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+    root: {
+        paddingBottom: theme.spacing.unit
+    },
+    cardSubheader: {
+        paddingTop: 0,
+        paddingBottom: 0
+    },
+    nameHeader: {
+        marginLeft: '75px'
+    },
+    fileSizeHeader: {
+        marginRight: '65px'
+    }
+});
+
+export const CollectionPanelFiles = withStyles(styles)(
+    ({ onItemMenuOpen, onOptionsMenuOpen, classes, ...treeProps }: CollectionPanelFilesProps & WithStyles<CssRules>) =>
+        <Card className={classes.root}>
+            <CardHeader
+                title="Files"
+                action={
+                    <Button 
+                        variant='raised' 
+                        color='primary'
+                        size='small'>
+                        Upload data
+                    </Button>
+                } />
+            <CardHeader
+                className={classes.cardSubheader}
+                action={
+                    <IconButton onClick={onOptionsMenuOpen}>
+                        <CustomizeTableIcon />
+                    </IconButton>
+                } />
+            <Grid container justify="space-between">
+                <Typography variant="caption" className={classes.nameHeader}>
+                    Name
+                    </Typography>
+                <Typography variant="caption" className={classes.fileSizeHeader}>
+                    File size
+                    </Typography>
+            </Grid>
+            <FileTree onMenuOpen={onItemMenuOpen} {...treeProps} />
+        </Card>);
diff --git a/src/components/file-tree/file-tree-data.ts b/src/components/file-tree/file-tree-data.ts
new file mode 100644 (file)
index 0000000..4be4ace
--- /dev/null
@@ -0,0 +1,9 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export interface FileTreeData {
+    name: string;
+    type: string;
+    size?: number;
+}
diff --git a/src/components/file-tree/file-tree-item.tsx b/src/components/file-tree/file-tree-item.tsx
new file mode 100644 (file)
index 0000000..5255ded
--- /dev/null
@@ -0,0 +1,73 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { TreeItem } from "../tree/tree";
+import { ProjectIcon, MoreOptionsIcon, DefaultIcon, CollectionIcon } from "../icon/icon";
+import { Typography, IconButton, StyleRulesCallback, withStyles, WithStyles } from "@material-ui/core";
+import { formatFileSize } from "../../common/formatters";
+import { ListItemTextIcon } from "../list-item-text-icon/list-item-text-icon";
+import { FileTreeData } from "./file-tree-data";
+
+type CssRules = "root" | "spacer" | "sizeInfo" | "button";
+
+const fileTreeItemStyle: StyleRulesCallback<CssRules> = theme => ({
+    root: {
+        display: "flex",
+        alignItems: "center",
+        paddingRight: `${theme.spacing.unit * 1.5}px`
+    },
+    spacer: {
+        flex: "1"
+    },
+    sizeInfo: {
+        width: `${theme.spacing.unit * 8}px`
+    },
+    button: {
+        width: theme.spacing.unit * 3,
+        height: theme.spacing.unit * 3,
+        marginRight: theme.spacing.unit
+    }
+});
+
+export interface FileTreeItemProps {
+    item: TreeItem<FileTreeData>;
+    onMoreClick: (event: React.MouseEvent<any>, item: TreeItem<FileTreeData>) => void;
+}
+export const FileTreeItem = withStyles(fileTreeItemStyle)(
+    class extends React.Component<FileTreeItemProps & WithStyles<CssRules>> {
+        render() {
+            const { classes, item } = this.props;
+            return <div className={classes.root}>
+                <ListItemTextIcon
+                    icon={getIcon(item)}
+                    name={item.data.name} />
+                <div className={classes.spacer} />
+                <Typography
+                    className={classes.sizeInfo}
+                    variant="caption">{formatFileSize(item.data.size)}</Typography>
+                <IconButton
+                    className={classes.button}
+                    onClick={this.handleClick}>
+                    <MoreOptionsIcon />
+                </IconButton>
+            </div >;
+        }
+
+        handleClick = (event: React.MouseEvent<any>) => {
+            this.props.onMoreClick(event, this.props.item);
+        }
+    });
+
+const getIcon = (item: TreeItem<FileTreeData>) => {
+    switch(item.data.type){
+        case 'directory':
+            return ProjectIcon;
+        case 'file':
+            return CollectionIcon;
+        default:
+            return DefaultIcon;
+    }
+};
+
diff --git a/src/components/file-tree/file-tree.tsx b/src/components/file-tree/file-tree.tsx
new file mode 100644 (file)
index 0000000..06fc8b7
--- /dev/null
@@ -0,0 +1,51 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Tree, TreeItem, TreeItemStatus } from "../tree/tree";
+import { FileTreeData } from "./file-tree-data";
+import { FileTreeItem } from "./file-tree-item";
+
+export interface FileTreeProps {
+    items: Array<TreeItem<FileTreeData>>;
+    onMenuOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
+    onSelectionToggle: (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => void;
+    onCollapseToggle: (id: string, status: TreeItemStatus) => void;
+}
+
+export class FileTree extends React.Component<FileTreeProps> {
+    render() {
+        return <Tree
+            showSelection={true}
+            items={this.props.items}
+            disableRipple={true}
+            render={this.renderItem}
+            onContextMenu={this.handleContextMenu}
+            toggleItemActive={this.handleToggleActive}
+            toggleItemOpen={this.handleToggle}
+            onSelectionChange={this.handleSelectionChange} />;
+    }
+
+    handleContextMenu = (event: React.MouseEvent<any>, item: TreeItem<FileTreeData>) => {
+        event.stopPropagation();
+        this.props.onMenuOpen(event, item);
+    }
+
+    handleToggle = (id: string, status: TreeItemStatus) => {
+        this.props.onCollapseToggle(id, status);
+    }
+
+    handleToggleActive = () => { return; };
+
+    handleSelectionChange = (event: React.MouseEvent<HTMLElement>, item: TreeItem<FileTreeData>) => {
+        event.stopPropagation();
+        this.props.onSelectionToggle(event, item);
+    }
+
+    renderItem = (item: TreeItem<FileTreeData>) =>
+        <FileTreeItem
+            item={item}
+            onMoreClick={this.handleContextMenu} />
+
+}
index f8e53d3e97d38f8522feaaaf4d5b5e4cc0df6c86..1dc8669ecfdc927f550459819fc1f9aae1561d16 100644 (file)
@@ -11,7 +11,7 @@ import Code from '@material-ui/icons/Code';
 import ChevronLeft from '@material-ui/icons/ChevronLeft';
 import ChevronRight from '@material-ui/icons/ChevronRight';
 import Close from '@material-ui/icons/Close';
-import ContentCopy from '@material-ui/icons/ContentCopy';
+import ContentCopy from '@material-ui/icons/FileCopyOutlined';
 import CreateNewFolder from '@material-ui/icons/CreateNewFolder';
 import Delete from '@material-ui/icons/Delete';
 import DeviceHub from '@material-ui/icons/DeviceHub';
@@ -73,4 +73,4 @@ export const SidePanelRightArrowIcon: IconType = (props) => <PlayArrow {...props
 export const TrashIcon: IconType = (props) => <Delete {...props} />;
 export const UserPanelIcon: IconType = (props) => <Person {...props} />;
 export const UsedByIcon: IconType = (props) => <Folder {...props} />;
-export const WorkflowIcon: IconType = (props) => <Code {...props} />;
\ No newline at end of file
+export const WorkflowIcon: IconType = (props) => <Code {...props} />;
index 8f9d4744dd3145364d0aea2350e5e995c51af25b..6f8a2c4302e635bd9b4222801d0ddbb9ce5f82bb 100644 (file)
@@ -23,7 +23,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         color: theme.palette.primary.main,
     },
     hasMargin: {
-        marginLeft: '18px',
+        marginLeft: `${theme.spacing.unit}px`,
     },
 });
 
index de6b3c8149faa99a8cb83362a39a25dd186b2aa3..ec648e11b2e6d84922e57a54a6e57f973f381e1c 100644 (file)
@@ -34,7 +34,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     toggableIconContainer: {
         color: theme.palette.grey["700"],
         height: '14px',
-        position: 'absolute'
+        width: '14px'
     },
     toggableIcon: {
         fontSize: '14px'
index 58484c37a2358a538cd6f8af2827506d0f809b92..45981d8962c7119f614b48a9ab866bceed9c65f9 100644 (file)
@@ -10,6 +10,7 @@ import ListItem from "@material-ui/core/ListItem/ListItem";
 import { Tree, TreeItem } from './tree';
 import { ProjectResource } from '../../models/project';
 import { mockProjectResource } from '../../models/test-utils';
+import { Checkbox } from '@material-ui/core';
 
 Enzyme.configure({ adapter: new Adapter() });
 
@@ -48,4 +49,51 @@ describe("Tree component", () => {
             items={[project]} />);
         expect(wrapper.find('i')).toHaveLength(1);
     });
+
+    it("should render checkbox", () => {
+        const project: TreeItem<ProjectResource> = {
+            data: mockProjectResource(),
+            id: "3",
+            open: true,
+            active: true,
+            status: 1,
+        };
+        const wrapper = mount(<Tree
+            showSelection={true}
+            render={() => <div />}
+            toggleItemOpen={jest.fn()}
+            toggleItemActive={jest.fn()}
+            onContextMenu={jest.fn()}
+            items={[project]} />);
+        expect(wrapper.find(Checkbox)).toHaveLength(1);
+    });
+
+    it("call onSelectionChanged with associated item", () => {
+        const project: TreeItem<ProjectResource> = {
+            data: mockProjectResource(),
+            id: "3",
+            open: true,
+            active: true,
+            status: 1,
+        };
+        const spy = jest.fn();
+        const onSelectionChanged = (event: any, item: TreeItem<any>) => spy(item);
+        const wrapper = mount(<Tree
+            showSelection={true}
+            render={() => <div />}
+            toggleItemOpen={jest.fn()}
+            toggleItemActive={jest.fn()}
+            onContextMenu={jest.fn()}
+            onSelectionChange={onSelectionChanged}
+            items={[project]} />);
+        wrapper.find(Checkbox).prop('onClick')();
+        expect(spy).toHaveBeenLastCalledWith({
+            data: mockProjectResource(),
+            id: "3",
+            open: true,
+            active: true,
+            status: 1,
+        });
+    });
+
 });
index 20526005a1830491a5e844bdf64d3cf8d6148d68..ea15b6b1bd8df4a8b830bbdc6aec48f6c56e7ea2 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { List, ListItem, ListItemIcon, Collapse } from "@material-ui/core";
+import { List, ListItem, ListItemIcon, Collapse, Checkbox } from "@material-ui/core";
 import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
 import { ReactElement } from "react";
 import CircularProgress from '@material-ui/core/CircularProgress';
@@ -12,12 +12,24 @@ import * as classnames from "classnames";
 import { ArvadosTheme } from '../../common/custom-theme';
 import { SidePanelRightArrowIcon } from '../icon/icon';
 
-type CssRules = 'list' | 'active' | 'loader' | 'toggableIconContainer' | 'iconClose' | 'iconOpen' | 'toggableIcon';
+type CssRules = 'list'
+    | 'listItem'
+    | 'active'
+    | 'loader'
+    | 'toggableIconContainer'
+    | 'iconClose'
+    | 'renderContainer'
+    | 'iconOpen'
+    | 'toggableIcon'
+    | 'checkbox';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     list: {
         padding: '3px 0px'
     },
+    listItem: {
+        padding: '3px 0px',
+    },
     loader: {
         position: 'absolute',
         transform: 'translate(0px)',
@@ -26,11 +38,14 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     toggableIconContainer: {
         color: theme.palette.grey["700"],
         height: '14px',
-        position: 'absolute'
+        width: '14px',
     },
     toggableIcon: {
         fontSize: '14px'
     },
+    renderContainer: {
+        flex: 1
+    },
     active: {
         color: theme.palette.primary.main,
     },
@@ -40,6 +55,12 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     iconOpen: {
         transition: 'all 0.1s ease',
         transform: 'rotate(90deg)',
+    },
+    checkbox: {
+        width: theme.spacing.unit * 3,
+        height: theme.spacing.unit * 3,
+        margin: `0 ${theme.spacing.unit}px`,
+        color: theme.palette.grey["500"]
     }
 });
 
@@ -54,6 +75,7 @@ export interface TreeItem<T> {
     id: string;
     open: boolean;
     active: boolean;
+    selected?: boolean;
     status: TreeItemStatus;
     items?: Array<TreeItem<T>>;
 }
@@ -65,18 +87,22 @@ interface TreeProps<T> {
     toggleItemActive: (id: string, status: TreeItemStatus) => void;
     level?: number;
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
+    showSelection?: boolean;
+    onSelectionChange?: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
+    disableRipple?: boolean;
 }
 
 export const Tree = withStyles(styles)(
     class Component<T> extends React.Component<TreeProps<T> & WithStyles<CssRules>, {}> {
         render(): ReactElement<any> {
             const level = this.props.level ? this.props.level : 0;
-            const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu } = this.props;
-            const { list, loader, toggableIconContainer } = classes;
+            const { classes, render, toggleItemOpen, items, toggleItemActive, onContextMenu, disableRipple } = this.props;
+            const { list, listItem, loader, toggableIconContainer, renderContainer } = classes;
             return <List component="div" className={list}>
                 {items && items.map((it: TreeItem<T>, idx: number) =>
                     <div key={`item/${level}/${idx}`}>
-                        <ListItem button className={list} style={{ paddingLeft: (level + 1) * 20 }}
+                        <ListItem button className={listItem} style={{ paddingLeft: (level + 1) * 20 }}
+                            disableRipple={disableRipple}
                             onClick={() => toggleItemActive(it.id, it.status)}
                             onContextMenu={this.handleRowContextMenu(it)}>
                             {it.status === TreeItemStatus.PENDING ?
@@ -87,17 +113,28 @@ export const Tree = withStyles(styles)(
                                     {it.status !== TreeItemStatus.INITIAL && it.items && it.items.length === 0 ? <span /> : <SidePanelRightArrowIcon />}
                                 </ListItemIcon>
                             </i>
-                            {render(it, level)}
+                            {this.props.showSelection &&
+                                <Checkbox
+                                    checked={it.selected}
+                                    className={classes.checkbox}
+                                    color="primary"
+                                    onClick={this.handleCheckboxChange(it)} />}
+                            <div className={renderContainer}>
+                                {render(it, level)}
+                            </div>
                         </ListItem>
                         {it.items && it.items.length > 0 &&
                             <Collapse in={it.open} timeout="auto" unmountOnExit>
                                 <Tree
+                                    showSelection={this.props.showSelection}
                                     items={it.items}
                                     render={render}
+                                    disableRipple={disableRipple}
                                     toggleItemOpen={toggleItemOpen}
                                     toggleItemActive={toggleItemActive}
                                     level={level + 1}
-                                    onContextMenu={onContextMenu} />
+                                    onContextMenu={onContextMenu}
+                                    onSelectionChange={this.props.onSelectionChange} />
                             </Collapse>}
                     </div>)}
             </List>;
@@ -115,5 +152,14 @@ export const Tree = withStyles(styles)(
         handleRowContextMenu = (item: TreeItem<T>) =>
             (event: React.MouseEvent<HTMLElement>) =>
                 this.props.onContextMenu(event, item)
+
+        handleCheckboxChange = (item: TreeItem<T>) => {
+            const { onSelectionChange } = this.props;
+            return onSelectionChange
+                ? (event: React.MouseEvent<HTMLElement>) => {
+                    onSelectionChange(event, item);
+                }
+                : undefined;
+        }
     }
 );
index 887efc560f69a5d811aefeafd18cb542148b74dd..467aee08dd378915152892fbdcecb67ef4f86c86 100644 (file)
@@ -23,12 +23,16 @@ import { rootProjectActionSet } from "./views-components/context-menu/action-set
 import { projectActionSet } from "./views-components/context-menu/action-sets/project-action-set";
 import { resourceActionSet } from './views-components/context-menu/action-sets/resource-action-set';
 import { favoriteActionSet } from "./views-components/context-menu/action-sets/favorite-action-set";
+import { collectionFilesActionSet } from './views-components/context-menu/action-sets/collection-files-action-set';
+import { collectionFilesItemActionSet } from './views-components/context-menu/action-sets/collection-files-item-action-set';
 import { collectionActionSet } from './views-components/context-menu/action-sets/collection-action-set';
 
 addMenuActionSet(ContextMenuKind.ROOT_PROJECT, rootProjectActionSet);
 addMenuActionSet(ContextMenuKind.PROJECT, projectActionSet);
 addMenuActionSet(ContextMenuKind.RESOURCE, resourceActionSet);
 addMenuActionSet(ContextMenuKind.FAVORITE, favoriteActionSet);
+addMenuActionSet(ContextMenuKind.COLLECTION_FILES, collectionFilesActionSet);
+addMenuActionSet(ContextMenuKind.COLLECTION_FILES_ITEM, collectionFilesItemActionSet);
 addMenuActionSet(ContextMenuKind.COLLECTION, collectionActionSet); 
 
 fetchConfig()
diff --git a/src/models/collection-file.ts b/src/models/collection-file.ts
new file mode 100644 (file)
index 0000000..a140080
--- /dev/null
@@ -0,0 +1,44 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Tree } from './tree';
+
+export type CollectionFilesTree = Tree<CollectionDirectory | CollectionFile>;
+
+export enum CollectionFileType {
+    DIRECTORY = 'directory',
+    FILE = 'file'
+}
+
+export interface CollectionDirectory {
+    parentId: string;
+    id: string;
+    name: string;
+    type: CollectionFileType.DIRECTORY;
+}
+
+export interface CollectionFile {
+    parentId: string;
+    id: string;
+    name: string;
+    size: number;
+    type: CollectionFileType.FILE;
+}
+
+export const createCollectionDirectory = (data: Partial<CollectionDirectory>): CollectionDirectory => ({
+    id: '',
+    name: '',
+    parentId: '',
+    type: CollectionFileType.DIRECTORY,
+    ...data
+});
+
+export const createCollectionFile = (data: Partial<CollectionFile>): CollectionFile => ({
+    id: '',
+    name: '',
+    parentId: '',
+    size: 0,
+    type: CollectionFileType.FILE,
+    ...data
+});
\ No newline at end of file
diff --git a/src/models/keep-manifest.ts b/src/models/keep-manifest.ts
new file mode 100644 (file)
index 0000000..6dc6445
--- /dev/null
@@ -0,0 +1,17 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export type KeepManifest = KeepManifestStream[];
+
+export interface KeepManifestStream {
+    name: string;
+    locators: string[];
+    files: Array<KeepManifestStreamFile>;
+}
+
+export interface KeepManifestStreamFile {
+    name: string;
+    position: string;
+    size: number;
+}
diff --git a/src/models/object-types.ts b/src/models/object-types.ts
new file mode 100644 (file)
index 0000000..f0f17e0
--- /dev/null
@@ -0,0 +1,23 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+const USER_UUID_REGEX = /.*tpzed.*/;
+const GROUP_UUID_REGEX = /.*-j7d0g-.*/;
+
+export enum ObjectTypes {
+    USER = "User",
+    GROUP = "Group",
+    UNKNOWN = "Unknown"
+}
+
+export const getUuidObjectType = (uuid: string) => {
+    switch (true) {
+        case USER_UUID_REGEX.test(uuid):
+            return ObjectTypes.USER;
+        case GROUP_UUID_REGEX.test(uuid):
+            return ObjectTypes.GROUP;
+        default:
+            return ObjectTypes.UNKNOWN;
+    }
+};
\ No newline at end of file
diff --git a/src/models/tree.test.ts b/src/models/tree.test.ts
new file mode 100644 (file)
index 0000000..708cf40
--- /dev/null
@@ -0,0 +1,88 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as Tree from './tree';
+
+describe('Tree', () => {
+    let tree: Tree.Tree<string>;
+
+    beforeEach(() => {
+        tree = Tree.createTree();
+    });
+
+    it('sets new node', () => {
+        const newTree = Tree.setNode({ children: [], id: 'Node 1', parent: '', value: 'Value 1' })(tree);
+        expect(Tree.getNode('Node 1')(newTree)).toEqual({ children: [], id: 'Node 1', parent: '', value: 'Value 1' });
+    });
+
+    it('adds new node reference to parent children', () => {
+        const [newTree] = [tree]
+            .map(Tree.setNode({ children: [], id: 'Node 1', parent: '', value: 'Value 1' }))
+            .map(Tree.setNode({ children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 2' }));
+
+        expect(Tree.getNode('Node 1')(newTree)).toEqual({ children: ['Node 2'], id: 'Node 1', parent: '', value: 'Value 1' });
+    });
+
+    it('gets node ancestors', () => {
+        const newTree = [
+            { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+            { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
+            { children: [], id: 'Node 3', parent: 'Node 2', value: 'Value 1' }
+        ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+        expect(Tree.getNodeAncestors('Node 3')(newTree)).toEqual(['Node 1', 'Node 2']);
+    });
+
+    it('gets node descendants', () => {
+        const newTree = [
+            { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+            { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
+            { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' },
+            { children: [], id: 'Node 3', parent: 'Node 1', value: 'Value 1' },
+            { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
+        ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+        expect(Tree.getNodeDescendants('Node 1')(newTree)).toEqual(['Node 2', 'Node 3', 'Node 2.1', 'Node 3.1']);
+    });
+
+    it('gets root descendants', () => {
+        const newTree = [
+            { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+            { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
+            { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' },
+            { children: [], id: 'Node 3', parent: 'Node 1', value: 'Value 1' },
+            { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
+        ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+        expect(Tree.getNodeDescendants('')(newTree)).toEqual(['Node 1', 'Node 2', 'Node 3', 'Node 2.1', 'Node 3.1']);
+    });
+
+    it('gets node children', () => {
+        const newTree = [
+            { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+            { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
+            { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' },
+            { children: [], id: 'Node 3', parent: 'Node 1', value: 'Value 1' },
+            { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
+        ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+        expect(Tree.getNodeChildren('Node 1')(newTree)).toEqual(['Node 2', 'Node 3']);
+    });
+
+    it('gets root children', () => {
+        const newTree = [
+            { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+            { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 1' },
+            { children: [], id: 'Node 2.1', parent: 'Node 2', value: 'Value 1' },
+            { children: [], id: 'Node 3', parent: '', value: 'Value 1' },
+            { children: [], id: 'Node 3.1', parent: 'Node 3', value: 'Value 1' }
+        ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+        expect(Tree.getNodeChildren('')(newTree)).toEqual(['Node 1', 'Node 3']);
+    });
+
+    it('maps tree', () => {
+        const newTree = [
+            { children: [], id: 'Node 1', parent: '', value: 'Value 1' },
+            { children: [], id: 'Node 2', parent: 'Node 1', value: 'Value 2' },
+        ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+        const mappedTree = Tree.mapTreeValues<string, number>(value => parseInt(value.split(' ')[1], 10))(newTree);
+        expect(Tree.getNode('Node 2')(mappedTree)).toEqual({ children: [], id: 'Node 2', parent: 'Node 1', value: 2 }, );
+    });
+});
\ No newline at end of file
diff --git a/src/models/tree.ts b/src/models/tree.ts
new file mode 100644 (file)
index 0000000..8b66e50
--- /dev/null
@@ -0,0 +1,107 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export type Tree<T> = Record<string, TreeNode<T>>;
+
+export const TREE_ROOT_ID = '';
+
+export interface TreeNode<T> {
+    children: string[];
+    value: T;
+    id: string;
+    parent: string;
+}
+
+export const createTree = <T>(): Tree<T> => ({});
+
+export const getNode = (id: string) => <T>(tree: Tree<T>): TreeNode<T> | undefined => tree[id];
+
+export const setNode = <T>(node: TreeNode<T>) => (tree: Tree<T>): Tree<T> => {
+    const [newTree] = [tree]
+        .map(tree => getNode(node.id)(tree) === node
+            ? tree
+            : {...tree, [node.id]: node})
+        .map(addChild(node.parent, node.id));
+    return newTree;
+};
+
+export const getNodeValue = (id: string) => <T>(tree: Tree<T>) => {
+    const node = getNode(id)(tree);
+    return node ? node.value : undefined;
+};
+
+export const setNodeValue = (id: string) => <T>(value: T) => (tree: Tree<T>) => {
+    const node = getNode(id)(tree);
+    return node
+        ? setNode(mapNodeValue(() => value)(node))(tree)
+        : tree;
+};
+
+export const setNodeValueWith = <T>(mapFn: (value: T) => T) => (id: string) => (tree: Tree<T>) => {
+    const node = getNode(id)(tree);
+    return node
+        ? setNode(mapNodeValue(mapFn)(node))(tree)
+        : tree;
+};
+
+export const mapTreeValues = <T, R>(mapFn: (value: T) => R) => (tree: Tree<T>): Tree<R> =>
+    getNodeDescendants('')(tree)
+        .map(id => getNode(id)(tree))
+        .map(mapNodeValue(mapFn))
+        .reduce((newTree, node) => setNode(node)(newTree), createTree<R>());
+
+export const mapTree = <T, R>(mapFn: (node: TreeNode<T>) => TreeNode<R>) => (tree: Tree<T>): Tree<R> =>
+    getNodeDescendants('')(tree)
+        .map(id => getNode(id)(tree))
+        .map(mapFn)
+        .reduce((newTree, node) => setNode(node)(newTree), createTree<R>());
+
+export const getNodeAncestors = (id: string) => <T>(tree: Tree<T>): string[] => {
+    const node = getNode(id)(tree);
+    return node && node.parent
+        ? [...getNodeAncestors(node.parent)(tree), node.parent]
+        : [];
+};
+
+export const getNodeDescendants = (id: string, limit = Infinity) => <T>(tree: Tree<T>): string[] => {
+    const node = getNode(id)(tree);
+    const children = node ? node.children :
+        id === TREE_ROOT_ID
+            ? getRootNodeChildren(tree)
+            : [];
+
+    return children
+        .concat(limit < 1
+            ? []
+            : children
+                .map(id => getNodeDescendants(id, limit - 1)(tree))
+                .reduce((nodes, nodeChildren) => [...nodes, ...nodeChildren], []));
+};
+
+export const getNodeChildren = (id: string) => <T>(tree: Tree<T>): string[] =>
+    getNodeDescendants(id, 0)(tree);
+
+const mapNodeValue = <T, R>(mapFn: (value: T) => R) => (node: TreeNode<T>): TreeNode<R> =>
+    ({ ...node, value: mapFn(node.value) });
+
+const getRootNodeChildren = <T>(tree: Tree<T>) =>
+    Object
+        .keys(tree)
+        .filter(id => getNode(id)(tree)!.parent === TREE_ROOT_ID);
+
+const addChild = (parentId: string, childId: string) => <T>(tree: Tree<T>): Tree<T> => {
+    const node = getNode(parentId)(tree);
+    if (node) {
+        const children = node.children.some(id => id === childId)
+            ? node.children
+            : [...node.children, childId];
+
+        const newNode = children === node.children
+            ? node
+            : { ...node, children };
+
+        return setNode(newNode)(tree);
+    }
+    return tree;
+};
diff --git a/src/services/collection-files-service/collection-files-service.ts b/src/services/collection-files-service/collection-files-service.ts
new file mode 100644 (file)
index 0000000..96c9e99
--- /dev/null
@@ -0,0 +1,25 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { CollectionService } from "../collection-service/collection-service";
+import { parseKeepManifestText } from "./collection-manifest-parser";
+import { mapManifestToCollectionFilesTree } from "./collection-manifest-mapper";
+
+export class CollectionFilesService {
+    
+    constructor(private collectionService: CollectionService) { }
+
+    getFiles(collectionUuid: string) {
+        return this.collectionService
+            .get(collectionUuid)
+            .then(collection =>
+                mapManifestToCollectionFilesTree(
+                    parseKeepManifestText(
+                        collection.manifestText
+                    )
+                )
+            );
+    }
+
+}
\ No newline at end of file
diff --git a/src/services/collection-files-service/collection-manifest-mapper.test.ts b/src/services/collection-files-service/collection-manifest-mapper.test.ts
new file mode 100644 (file)
index 0000000..ad8f872
--- /dev/null
@@ -0,0 +1,59 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { parseKeepManifestText } from "./collection-manifest-parser";
+import { mapManifestToFiles, mapManifestToDirectories } from "./collection-manifest-mapper";
+
+test('mapManifestToFiles', () => {
+    const manifestText = `. 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt\n./c d41d8cd98f00b204e9800998ecf8427e+0 0:0:d`;
+    const manifest = parseKeepManifestText(manifestText);
+    const files = mapManifestToFiles(manifest);
+    expect(files).toEqual([{
+        parentId: '',
+        id: '/a',
+        name: 'a',
+        size: 0,
+        type: 'file'
+    }, {
+        parentId: '',
+        id: '/b',
+        name: 'b',
+        size: 0,
+        type: 'file'
+    }, {
+        parentId: '',
+        id: '/output.txt',
+        name: 'output.txt',
+        size: 33,
+        type: 'file'
+    }, {
+        parentId: '/c',
+        id: '/c/d',
+        name: 'd',
+        size: 0,
+        type: 'file'
+    },]);
+});
+
+test('mapManifestToDirectories', () => {
+    const manifestText = `./c/user/results 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt\n`;
+    const manifest = parseKeepManifestText(manifestText);
+    const directories = mapManifestToDirectories(manifest);
+    expect(directories).toEqual([{
+        parentId: "",
+        id: '/c',
+        name: 'c',
+        type: 'directory'
+    }, {
+        parentId: '/c',
+        id: '/c/user',
+        name: 'user',
+        type: 'directory'
+    }, {
+        parentId: '/c/user',
+        id: '/c/user/results',
+        name: 'results',
+        type: 'directory'
+    },]);
+});
\ No newline at end of file
diff --git a/src/services/collection-files-service/collection-manifest-mapper.ts b/src/services/collection-files-service/collection-manifest-mapper.ts
new file mode 100644 (file)
index 0000000..c2a8ae8
--- /dev/null
@@ -0,0 +1,73 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { uniqBy } from 'lodash';
+import { KeepManifestStream, KeepManifestStreamFile, KeepManifest } from "../../models/keep-manifest";
+import { TreeNode, setNode, createTree } from '../../models/tree';
+import { CollectionFilesTree, CollectionFile, CollectionDirectory, createCollectionDirectory, createCollectionFile } from '../../models/collection-file';
+
+export const mapManifestToCollectionFilesTree = (manifest: KeepManifest): CollectionFilesTree =>
+    manifestToCollectionFiles(manifest)
+        .map(mapCollectionFileToTreeNode)
+        .reduce((tree, node) => setNode(node)(tree), createTree<CollectionFile>());
+
+
+export const mapCollectionFileToTreeNode = (file: CollectionFile): TreeNode<CollectionFile> => ({
+    children: [],
+    id: file.id,
+    parent: file.parentId,
+    value: file
+});
+
+export const manifestToCollectionFiles = (manifest: KeepManifest): Array<CollectionDirectory | CollectionFile> => ([
+    ...mapManifestToDirectories(manifest),
+    ...mapManifestToFiles(manifest)
+]);
+
+export const mapManifestToDirectories = (manifest: KeepManifest): CollectionDirectory[] =>
+    uniqBy(
+        manifest
+            .map(mapStreamDirectory)
+            .map(splitDirectory)
+            .reduce((all, splitted) => ([...all, ...splitted]), []),
+        directory => directory.id);
+
+export const mapManifestToFiles = (manifest: KeepManifest): CollectionFile[] =>
+    manifest
+        .map(stream => stream.files.map(mapStreamFile(stream)))
+        .reduce((all, current) => ([...all, ...current]), []);
+
+const splitDirectory = (directory: CollectionDirectory): CollectionDirectory[] => {
+    return directory.name
+        .split('/')
+        .slice(1)
+        .map(mapPathComponentToDirectory);
+};
+
+const mapPathComponentToDirectory = (component: string, index: number, components: string[]): CollectionDirectory =>
+    createCollectionDirectory({
+        parentId: index === 0 ? '' : joinPathComponents(components, index),
+        id: joinPathComponents(components, index + 1),
+        name: component,
+    });
+
+const joinPathComponents = (components: string[], index: number) =>
+    `/${components.slice(0, index).join('/')}`;
+
+const mapStreamDirectory = (stream: KeepManifestStream): CollectionDirectory =>
+    createCollectionDirectory({
+        parentId: '',
+        id: stream.name,
+        name: stream.name,
+    });
+
+const mapStreamFile = (stream: KeepManifestStream) =>
+    (file: KeepManifestStreamFile): CollectionFile =>
+        createCollectionFile({
+            parentId: stream.name,
+            id: `${stream.name}/${file.name}`,
+            name: file.name,
+            size: file.size,
+        });
+
diff --git a/src/services/collection-files-service/collection-manifest-parser.test.ts b/src/services/collection-files-service/collection-manifest-parser.test.ts
new file mode 100644 (file)
index 0000000..eddc9c6
--- /dev/null
@@ -0,0 +1,34 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { parseKeepManifestText, parseKeepManifestStream } from "./collection-manifest-parser";
+
+describe('parseKeepManifestText', () => {
+    it('should parse text into streams', () => {
+        const manifestText = `. 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt\n./c d41d8cd98f00b204e9800998ecf8427e+0 0:0:d\n`;
+        const manifest = parseKeepManifestText(manifestText);
+        expect(manifest[0].name).toBe('');
+        expect(manifest[1].name).toBe('/c');
+        expect(manifest.length).toBe(2);
+    });
+});
+
+describe('parseKeepManifestStream', () => {
+    const streamText = './c 930625b054ce894ac40596c3f5a0d947+33 0:0:a 0:0:b 0:33:output.txt';
+    const stream = parseKeepManifestStream(streamText);
+
+    it('should parse stream name', () => {
+        expect(stream.name).toBe('/c');
+    });
+    it('should parse stream locators', () => {
+        expect(stream.locators).toEqual(['930625b054ce894ac40596c3f5a0d947+33']);
+    });
+    it('should parse stream files', () => {
+        expect(stream.files).toEqual([
+            {name: 'a', position: '0', size: 0},
+            {name: 'b', position: '0', size: 0},
+            {name: 'output.txt', position: '0', size: 33},
+        ]);
+    });
+});
\ No newline at end of file
diff --git a/src/services/collection-files-service/collection-manifest-parser.ts b/src/services/collection-files-service/collection-manifest-parser.ts
new file mode 100644 (file)
index 0000000..df334d4
--- /dev/null
@@ -0,0 +1,46 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { KeepManifestStream, KeepManifestStreamFile } from "../../models/keep-manifest";
+
+/**
+ * Documentation [http://doc.arvados.org/api/storage.html](http://doc.arvados.org/api/storage.html)
+ */
+export const parseKeepManifestText = (text: string) =>
+    text
+        .split(/\n/)
+        .filter(streamText => streamText.length > 0)
+        .map(parseKeepManifestStream);
+
+/**
+ * Documentation [http://doc.arvados.org/api/storage.html](http://doc.arvados.org/api/storage.html)
+ */
+export const parseKeepManifestStream = (stream: string): KeepManifestStream => {
+    const tokens = stream.split(' ');
+    return {
+        name: streamName(tokens),
+        locators: locators(tokens),
+        files: files(tokens)
+    };
+};
+
+const FILE_LOCATOR_REGEXP = /^([0-9a-f]{32})\+([0-9]+)(\+[A-Z][-A-Za-z0-9@_]*)*$/;
+
+const FILE_REGEXP = /([0-9]+):([0-9]+):(.*)/;
+
+const streamName = (tokens: string[]) => tokens[0].slice(1);
+
+const locators = (tokens: string[]) => tokens.filter(isFileLocator);
+
+const files = (tokens: string[]) => tokens.filter(isFile).map(parseFile);
+
+const isFileLocator = (token: string) => FILE_LOCATOR_REGEXP.test(token);
+
+const isFile = (token: string) => FILE_REGEXP.test(token);
+
+const parseFile = (token: string): KeepManifestStreamFile => {
+    const match = FILE_REGEXP.exec(token);
+    const [position, size, name] = match!.slice(1);
+    return { name, position, size: parseInt(size, 10) };
+};
\ No newline at end of file
index f56a30f7183245f13f493701a1440d30129c8597..87c668f2ae4ca3ff3e9b36e0f6bb16d3c51a96e8 100644 (file)
@@ -8,11 +8,10 @@ import { ProjectService } from "./project-service/project-service";
 import { LinkService } from "./link-service/link-service";
 import { FavoriteService } from "./favorite-service/favorite-service";
 import { AxiosInstance } from "axios";
-import { CommonResourceService } from "../common/api/common-resource-service";
-import { Resource } from "../models/resource";
 import { CollectionService } from "./collection-service/collection-service";
 import { TagService } from "./tag-service/tag-service";
 import Axios from "axios";
+import { CollectionFilesService } from "./collection-files-service/collection-files-service";
 
 export interface ServiceRepository {
     apiClient: AxiosInstance;
@@ -22,8 +21,9 @@ export interface ServiceRepository {
     projectService: ProjectService;
     linkService: LinkService;
     favoriteService: FavoriteService;
-    collectionService: CommonResourceService<Resource>;
     tagService: TagService;
+    collectionService: CollectionService;
+    collectionFilesService: CollectionFilesService;
 }
 
 export const createServices = (baseUrl: string): ServiceRepository => {
@@ -37,7 +37,8 @@ export const createServices = (baseUrl: string): ServiceRepository => {
     const favoriteService = new FavoriteService(linkService, groupsService);
     const collectionService = new CollectionService(apiClient);
     const tagService = new TagService(linkService);
-    
+    const collectionFilesService = new CollectionFilesService(collectionService);
+
     return {
         apiClient,
         authService,
@@ -46,6 +47,7 @@ export const createServices = (baseUrl: string): ServiceRepository => {
         linkService,
         favoriteService,
         collectionService,
-        tagService
+        tagService,
+        collectionFilesService
     };
 };
index f9994d734e375da7e7c2ee185ceba4787e8a7d50..f2774f6fb384f2d72256e57a93bd9cda92d71493 100644 (file)
@@ -6,6 +6,8 @@ import { unionize, ofType, UnionOf } from "unionize";
 import { Dispatch } from "redux";
 import { ResourceKind } from "../../models/resource";
 import { CollectionResource } from "../../models/collection";
+import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions";
+import { createTree } from "../../models/tree";
 import { RootState } from "../store";
 import { ServiceRepository } from "../../services/services";
 import { TagResource, TagProperty } from "../../models/tag";
@@ -29,10 +31,15 @@ export const COLLECTION_TAG_FORM_NAME = 'collectionTagForm';
 export const loadCollection = (uuid: string, kind: ResourceKind) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid, kind }));
+        dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
         return services.collectionService
             .get(uuid)
             .then(item => {
-                dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: item as CollectionResource }));
+                dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item }));
+                return services.collectionFilesService.getFiles(item.uuid);
+            })
+            .then(files => {
+                dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(files));
             });
     };
 
diff --git a/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts b/src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
new file mode 100644 (file)
index 0000000..463d49c
--- /dev/null
@@ -0,0 +1,16 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { default as unionize, ofType, UnionOf } from "unionize";
+import { CollectionFilesTree } from "../../../models/collection-file";
+
+export const collectionPanelFilesAction = unionize({
+    SET_COLLECTION_FILES: ofType<CollectionFilesTree>(),
+    TOGGLE_COLLECTION_FILE_COLLAPSE: ofType<{ id: string }>(),
+    TOGGLE_COLLECTION_FILE_SELECTION: ofType<{ id: string }>(),
+    SELECT_ALL_COLLECTION_FILES: ofType<{}>(),
+    UNSELECT_ALL_COLLECTION_FILES: ofType<{}>(),
+}, { tag: 'type', value: 'payload' });
+
+export type CollectionPanelFilesAction = UnionOf<typeof collectionPanelFilesAction>;
\ No newline at end of file
diff --git a/src/store/collection-panel/collection-panel-files/collection-panel-files-reducer.test.ts b/src/store/collection-panel/collection-panel-files/collection-panel-files-reducer.test.ts
new file mode 100644 (file)
index 0000000..1a6bb7d
--- /dev/null
@@ -0,0 +1,116 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { collectionPanelFilesReducer } from "./collection-panel-files-reducer";
+import { collectionPanelFilesAction } from "./collection-panel-files-actions";
+import { CollectionFile, CollectionDirectory, createCollectionFile, createCollectionDirectory } from "../../../models/collection-file";
+import { createTree, setNode, getNodeValue, mapTreeValues, Tree } from "../../../models/tree";
+import { CollectionPanelFile, CollectionPanelDirectory } from "./collection-panel-files-state";
+
+describe('CollectionPanelFilesReducer', () => {
+
+    const files: Array<CollectionFile | CollectionDirectory> = [
+        createCollectionDirectory({ id: 'Directory 1', name: 'Directory 1', parentId: '' }),
+        createCollectionDirectory({ id: 'Directory 2', name: 'Directory 2', parentId: 'Directory 1' }),
+        createCollectionDirectory({ id: 'Directory 3', name: 'Directory 3', parentId: '' }),
+        createCollectionDirectory({ id: 'Directory 4', name: 'Directory 4', parentId: 'Directory 3' }),
+        createCollectionFile({ id: 'file1.txt', name: 'file1.txt', parentId: 'Directory 2' }),
+        createCollectionFile({ id: 'file2.txt', name: 'file2.txt', parentId: 'Directory 2' }),
+        createCollectionFile({ id: 'file3.txt', name: 'file3.txt', parentId: 'Directory 3' }),
+        createCollectionFile({ id: 'file4.txt', name: 'file4.txt', parentId: 'Directory 3' }),
+        createCollectionFile({ id: 'file5.txt', name: 'file5.txt', parentId: 'Directory 4' }),
+    ];
+
+    const collectionFilesTree = files.reduce((tree, file) => setNode({
+        children: [],
+        id: file.id,
+        parent: file.parentId,
+        value: file
+    })(tree), createTree<CollectionFile | CollectionDirectory>());
+
+    const collectionPanelFilesTree = collectionPanelFilesReducer(
+        createTree<CollectionPanelFile | CollectionPanelDirectory>(),
+        collectionPanelFilesAction.SET_COLLECTION_FILES(collectionFilesTree));
+
+    it('SET_COLLECTION_FILES', () => {
+        expect(getNodeValue('Directory 1')(collectionPanelFilesTree)).toEqual({
+            ...createCollectionDirectory({ id: 'Directory 1', name: 'Directory 1', parentId: '' }),
+            collapsed: true,
+            selected: false
+        });
+    });
+
+    it('TOGGLE_COLLECTION_FILE_COLLAPSE', () => {
+        const newTree = collectionPanelFilesReducer(
+            collectionPanelFilesTree,
+            collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_COLLAPSE({ id: 'Directory 3' }));
+
+        const value = getNodeValue('Directory 3')(newTree)! as CollectionPanelDirectory;
+        expect(value.collapsed).toBe(false);
+    });
+
+    it('TOGGLE_COLLECTION_FILE_SELECTION', () => {
+        const newTree = collectionPanelFilesReducer(
+            collectionPanelFilesTree,
+            collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: 'Directory 3' }));
+
+        const value = getNodeValue('Directory 3')(newTree);
+        expect(value!.selected).toBe(true);
+    });
+
+    it('TOGGLE_COLLECTION_FILE_SELECTION ancestors', () => {
+        const newTree = collectionPanelFilesReducer(
+            collectionPanelFilesTree,
+            collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: 'Directory 2' }));
+
+        const value = getNodeValue('Directory 1')(newTree);
+        expect(value!.selected).toBe(true);
+    });
+
+    it('TOGGLE_COLLECTION_FILE_SELECTION descendants', () => {
+        const newTree = collectionPanelFilesReducer(
+            collectionPanelFilesTree,
+            collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: 'Directory 2' }));
+        expect(getNodeValue('file1.txt')(newTree)!.selected).toBe(true);
+        expect(getNodeValue('file2.txt')(newTree)!.selected).toBe(true);
+    });
+
+    it('TOGGLE_COLLECTION_FILE_SELECTION unselect ancestors', () => {
+        const [newTree] = [collectionPanelFilesTree]
+            .map(tree => collectionPanelFilesReducer(
+                tree,
+                collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: 'Directory 2' })))
+            .map(tree => collectionPanelFilesReducer(
+                tree,
+                collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: 'file1.txt' })));
+
+        expect(getNodeValue('Directory 2')(newTree)!.selected).toBe(false);
+    });
+
+    it('SELECT_ALL_COLLECTION_FILES', () => {
+        const newTree = collectionPanelFilesReducer(
+            collectionPanelFilesTree,
+            collectionPanelFilesAction.SELECT_ALL_COLLECTION_FILES());
+
+        mapTreeValues((v: CollectionPanelFile | CollectionPanelDirectory) => {
+            expect(v.selected).toEqual(true);
+            return v;
+        })(newTree);
+    });
+
+    it('SELECT_ALL_COLLECTION_FILES', () => {
+        const [newTree] = [collectionPanelFilesTree]
+            .map(tree => collectionPanelFilesReducer(
+                tree,
+                collectionPanelFilesAction.SELECT_ALL_COLLECTION_FILES()))
+            .map(tree => collectionPanelFilesReducer(
+                tree,
+                collectionPanelFilesAction.UNSELECT_ALL_COLLECTION_FILES()));
+
+        mapTreeValues((v: CollectionPanelFile | CollectionPanelDirectory) => {
+            expect(v.selected).toEqual(false);
+            return v;
+        })(newTree);
+    });
+});
diff --git a/src/store/collection-panel/collection-panel-files/collection-panel-files-reducer.ts b/src/store/collection-panel/collection-panel-files/collection-panel-files-reducer.ts
new file mode 100644 (file)
index 0000000..ca518f0
--- /dev/null
@@ -0,0 +1,74 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, mapCollectionFileToCollectionPanelFile } 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";
+
+export const collectionPanelFilesReducer = (state: CollectionPanelFilesState = createTree(), action: CollectionPanelFilesAction) => {
+    return collectionPanelFilesAction.match(action, {
+        SET_COLLECTION_FILES: files =>
+            mapTree(mapCollectionFileToCollectionPanelFile)(files),
+
+        TOGGLE_COLLECTION_FILE_COLLAPSE: data =>
+            toggleCollapse(data.id)(state),
+
+        TOGGLE_COLLECTION_FILE_SELECTION: data => [state]
+            .map(toggleSelected(data.id))
+            .map(toggleAncestors(data.id))
+            .map(toggleDescendants(data.id))[0],
+
+        SELECT_ALL_COLLECTION_FILES: () =>
+            mapTreeValues(v => ({ ...v, selected: true }))(state),
+
+        UNSELECT_ALL_COLLECTION_FILES: () =>
+            mapTreeValues(v => ({ ...v, selected: false }))(state),
+
+        default: () => state
+    }) as CollectionPanelFilesState;
+};
+
+const toggleCollapse = (id: string) => (tree: CollectionPanelFilesState) =>
+    setNodeValueWith((v: CollectionPanelDirectory | CollectionPanelFile) =>
+        v.type === CollectionFileType.DIRECTORY
+            ? { ...v, collapsed: !v.collapsed }
+            : v)(id)(tree);
+
+
+const toggleSelected = (id: string) => (tree: CollectionPanelFilesState) =>
+    setNodeValueWith((v: CollectionPanelDirectory | CollectionPanelFile) => ({ ...v, selected: !v.selected }))(id)(tree);
+
+
+const toggleDescendants = (id: string) => (tree: CollectionPanelFilesState) => {
+    const node = getNode(id)(tree);
+    if (node && node.value.type === CollectionFileType.DIRECTORY) {
+        return getNodeDescendants(id)(tree)
+            .reduce((newTree, id) =>
+                setNodeValueWith(v => ({ ...v, selected: node.value.selected }))(id)(newTree), tree);
+    }
+    return tree;
+};
+
+const toggleAncestors = (id: string) => (tree: CollectionPanelFilesState) => {
+    const ancestors = getNodeAncestors(id)(tree).reverse();
+    return ancestors.reduce((newTree, parent) => parent ? toggleParentNode(parent)(newTree) : newTree, tree);
+};
+
+const toggleParentNode = (id: string) => (tree: CollectionPanelFilesState) => {
+    const node = getNode(id)(tree);
+    if (node) {
+        const parentNode = getNode(node.id)(tree);
+        if (parentNode) {
+            const selected = parentNode.children
+                .map(id => getNode(id)(tree))
+                .every(node => node !== undefined && node.value.selected);
+            return setNodeValueWith(v => ({ ...v, selected }))(parentNode.id)(tree);
+        }
+        return setNode(node)(tree);
+    }
+    return tree;
+};
+
+
diff --git a/src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts b/src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts
new file mode 100644 (file)
index 0000000..d6f2fa4
--- /dev/null
@@ -0,0 +1,26 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { CollectionFile, CollectionDirectory, CollectionFileType } from '../../../models/collection-file';
+import { Tree, TreeNode } from '../../../models/tree';
+
+export type CollectionPanelFilesState = Tree<CollectionPanelDirectory | CollectionPanelFile>;
+
+export interface CollectionPanelDirectory extends CollectionDirectory {
+    collapsed: boolean;
+    selected: boolean;
+}
+
+export interface CollectionPanelFile extends CollectionFile {
+    selected: boolean;
+}
+
+export const mapCollectionFileToCollectionPanelFile = (node: TreeNode<CollectionDirectory | CollectionFile>): TreeNode<CollectionPanelDirectory | CollectionPanelFile> => {
+    return {
+        ...node,
+        value: node.value.type === CollectionFileType.DIRECTORY
+            ? { ...node.value, selected: false, collapsed: true }
+            : { ...node.value, selected: false }
+    };
+};
\ No newline at end of file
index cb5a709e3ffe861c36656b84f726b25cc367cf0e..c4acf5aa9b3710fac0a3f61a905132651f8709da 100644 (file)
@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { unionize, ofType, UnionOf } from "unionize";
-import { CommonResourceService } from "../../common/api/common-resource-service";
 import { Dispatch } from "redux";
 import { Resource, ResourceKind } from "../../models/resource";
 import { RootState } from "../store";
@@ -18,13 +17,10 @@ export const detailsPanelActions = unionize({
 export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
 
 export const loadDetails = (uuid: string, kind: ResourceKind) =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(detailsPanelActions.LOAD_DETAILS({ uuid, kind }));
-        getService(services, kind)
-            .get(uuid)
-            .then(project => {
-                dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item: project }));
-            });
+        const item = await getService(services, kind).get(uuid);
+        dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item }));
     };
 
 const getService = (services: ServiceRepository, kind: ResourceKind) => {
diff --git a/src/store/dialog/dialog-actions.ts b/src/store/dialog/dialog-actions.ts
new file mode 100644 (file)
index 0000000..df4418f
--- /dev/null
@@ -0,0 +1,15 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { default as unionize, ofType, UnionOf } from "unionize";
+
+export const dialogActions = unionize({
+    OPEN_DIALOG: ofType<{ id: string, data: any }>(),
+    CLOSE_DIALOG: ofType<{ id: string }>()
+}, {
+        tag: 'type',
+        value: 'payload'
+    });
+
+export type DialogAction = UnionOf<typeof dialogActions>;
diff --git a/src/store/dialog/dialog-reducer.test.ts b/src/store/dialog/dialog-reducer.test.ts
new file mode 100644 (file)
index 0000000..f0bf9be
--- /dev/null
@@ -0,0 +1,30 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { dialogReducer } from "./dialog-reducer";
+import { dialogActions } from "./dialog-actions";
+
+describe('DialogReducer', () => {
+    it('OPEN_DIALOG', () => {
+        const id = 'test id';
+        const data = 'test data';
+        const state = dialogReducer({}, dialogActions.OPEN_DIALOG({ id, data }));
+        expect(state[id]).toEqual({ open: true, data });
+    });
+
+    it('CLOSE_DIALOG', () => {
+        const id = 'test id';
+        const state = dialogReducer({}, dialogActions.CLOSE_DIALOG({ id }));
+        expect(state[id]).toEqual({ open: false });
+    });
+    
+    it('CLOSE_DIALOG persist data', () => {
+        const id = 'test id';
+        const [newState] = [{}]
+            .map(state => dialogReducer(state, dialogActions.OPEN_DIALOG({ id, data: 'test data' })))
+            .map(state => dialogReducer(state, dialogActions.CLOSE_DIALOG({ id })));
+        
+        expect(newState[id]).toEqual({ open: false, data: 'test data' });
+    });
+});
diff --git a/src/store/dialog/dialog-reducer.ts b/src/store/dialog/dialog-reducer.ts
new file mode 100644 (file)
index 0000000..e49f65d
--- /dev/null
@@ -0,0 +1,22 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { DialogAction, dialogActions } from "./dialog-actions";
+
+export type DialogState = Record<string, Dialog>;
+
+export interface Dialog {
+    open: boolean;
+    data?: any;
+}
+
+export const dialogReducer = (state: DialogState = {}, action: DialogAction) =>
+    dialogActions.match(action, {
+        OPEN_DIALOG: ({ id, data }) => ({ ...state, [id]: { open: true, data } }),
+        CLOSE_DIALOG: ({ id }) => ({ 
+            ...state, 
+            [id]: state[id] ? { ...state[id], open: false } : { open: false } }),
+        default: () => state,
+    });
+
diff --git a/src/store/dialog/with-dialog.ts b/src/store/dialog/with-dialog.ts
new file mode 100644 (file)
index 0000000..42ae73e
--- /dev/null
@@ -0,0 +1,33 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { connect } from 'react-redux';
+import { DialogState } from './dialog-reducer';
+import { Dispatch } from 'redux';
+import { dialogActions } from './dialog-actions';
+
+export type WithDialog<T> = {
+    open: boolean;
+    data?: T;
+};
+
+export type WithDialogActions = {
+    closeDialog: () => void;
+};
+
+export const withDialog = (id: string) =>
+    <T>(component: React.ComponentType<WithDialog<T> & WithDialogActions>) =>
+        connect(mapStateToProps(id), mapDispatchToProps(id))(component);
+
+export const mapStateToProps = (id: string) => <T>(state: { dialog: DialogState }): WithDialog<T> => {
+    const dialog = state.dialog[id];
+    return dialog ? dialog : { open: false };
+};
+
+export const mapDispatchToProps = (id: string) => (dispatch: Dispatch): WithDialogActions => ({
+    closeDialog: () => {
+        dispatch(dialogActions.CLOSE_DIALOG({ id }));
+    }
+});
\ No newline at end of file
index ffb0f7acf6f5fc3e69b1c0295936f6ee8c4eacc5..defddadaada04a4b313b46121976353a16ea9a34 100644 (file)
@@ -11,7 +11,12 @@ import { RootState } from "../store";
 import { Resource, ResourceKind } from "../../models/resource";
 import { projectPanelActions } from "../project-panel/project-panel-action";
 import { getCollectionUrl } from "../../models/collection";
-import { getProjectUrl } from "../../models/project";
+import { getProjectUrl, ProjectResource } from "../../models/project";
+import { ProjectService } from "../../services/project-service/project-service";
+import { ServiceRepository } from "../../services/services";
+import { sidePanelActions } from "../side-panel/side-panel-action";
+import { SidePanelIdentifiers } from "../side-panel/side-panel-reducer";
+import { getUuidObjectType, ObjectTypes } from "../../models/object-types";
 
 export const getResourceUrl = <T extends Resource>(resource: T): string => {
     switch (resource.kind) {
@@ -59,3 +64,32 @@ export const setProjectItem = (itemId: string, itemMode: ItemMode) =>
         }
     };
 
+export const restoreBranch = (itemId: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const ancestors = await loadProjectAncestors(itemId, services.projectService);
+        const uuids = ancestors.map(ancestor => ancestor.uuid);
+        await loadBranch(uuids, dispatch);
+        dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelIdentifiers.PROJECTS));
+        dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
+        uuids.forEach(uuid => {
+            dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(uuid));
+        });
+    };
+
+export const loadProjectAncestors = async (uuid: string, projectService: ProjectService): Promise<Array<ProjectResource>> => {
+    if (getUuidObjectType(uuid) === ObjectTypes.USER) {
+        return [];
+    } else {
+        const currentProject = await projectService.get(uuid);
+        const ancestors = await loadProjectAncestors(currentProject.ownerUuid, projectService);
+        return [...ancestors, currentProject];
+    }
+};
+
+const loadBranch = async (uuids: string[], dispatch: Dispatch): Promise<any> => {
+    const [uuid, ...rest] = uuids;
+    if (uuid) {
+        await dispatch<any>(getProjectList(uuid));
+        return loadBranch(rest, dispatch);
+    }
+};
index 02dcc9b27d6c1dba16404890134920866a948c2a..aeb6a09cd388af3e559a41142590adcfc31234c1 100644 (file)
@@ -16,12 +16,15 @@ import { contextMenuReducer, ContextMenuState } from './context-menu/context-men
 import { reducer as formReducer } from 'redux-form';
 import { FavoritesState, favoritesReducer } from './favorites/favorites-reducer';
 import { snackbarReducer, SnackbarState } from './snackbar/snackbar-reducer';
+import { CollectionPanelFilesState } from './collection-panel/collection-panel-files/collection-panel-files-state';
+import { collectionPanelFilesReducer } from './collection-panel/collection-panel-files/collection-panel-files-reducer';
 import { dataExplorerMiddleware } from "./data-explorer/data-explorer-middleware";
 import { FAVORITE_PANEL_ID } from "./favorite-panel/favorite-panel-action";
 import { PROJECT_PANEL_ID } from "./project-panel/project-panel-action";
 import { ProjectPanelMiddlewareService } from "./project-panel/project-panel-middleware-service";
 import { FavoritePanelMiddlewareService } from "./favorite-panel/favorite-panel-middleware-service";
 import { CollectionPanelState, collectionPanelReducer } from './collection-panel/collection-panel-reducer';
+import { DialogState, dialogReducer } from './dialog/dialog-reducer';
 import { CollectionsState, collectionsReducer } from './collections/collections-reducer';
 import { ServiceRepository } from "../services/services";
 
@@ -42,25 +45,29 @@ export interface RootState {
     contextMenu: ContextMenuState;
     favorites: FavoritesState;
     snackbar: SnackbarState;
+    collectionPanelFiles: CollectionPanelFilesState;
+    dialog: DialogState;
 }
 
 export type RootStore = Store<RootState, Action> & { dispatch: Dispatch<any> };
 
 export function configureStore(history: History, services: ServiceRepository): RootStore {
-       const rootReducer = combineReducers({
-           auth: authReducer(services),
-           projects: projectsReducer,
+    const rootReducer = combineReducers({
+        auth: authReducer(services),
+        projects: projectsReducer,
         collections: collectionsReducer,
-           router: routerReducer,
-           dataExplorer: dataExplorerReducer,
-           sidePanel: sidePanelReducer,
-           collectionPanel: collectionPanelReducer,
-           detailsPanel: detailsPanelReducer,
-           contextMenu: contextMenuReducer,
-           form: formReducer,
-           favorites: favoritesReducer,
-           snackbar: snackbarReducer,
-       });
+        router: routerReducer,
+        dataExplorer: dataExplorerReducer,
+        sidePanel: sidePanelReducer,
+        collectionPanel: collectionPanelReducer,
+        detailsPanel: detailsPanelReducer,
+        contextMenu: contextMenuReducer,
+        form: formReducer,
+        favorites: favoritesReducer,
+        snackbar: snackbarReducer,
+        collectionPanelFiles: collectionPanelFilesReducer,
+        dialog: dialogReducer
+    });
 
     const projectPanelMiddleware = dataExplorerMiddleware(
         new ProjectPanelMiddlewareService(services, PROJECT_PANEL_ID)
diff --git a/src/validators/create-collection/create-collection-validator.tsx b/src/validators/create-collection/create-collection-validator.tsx
new file mode 100644 (file)
index 0000000..2d8e1f5
--- /dev/null
@@ -0,0 +1,9 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { require } from '../require';
+import { maxLength } from '../max-length';
+
+export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
+export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
\ No newline at end of file
index 527043d9d8567024c60546091f6b4103dcdbe684..928efdd2205e439c82fbd936e0e476e13f046139 100644 (file)
@@ -7,5 +7,3 @@ import { maxLength } from '../max-length';
 
 export const PROJECT_NAME_VALIDATION = [require, maxLength(255)];
 export const PROJECT_DESCRIPTION_VALIDATION = [maxLength(255)];
-export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
-export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
diff --git a/src/views-components/collection-panel-files/collection-panel-files.ts b/src/views-components/collection-panel-files/collection-panel-files.ts
new file mode 100644 (file)
index 0000000..0009fc0
--- /dev/null
@@ -0,0 +1,86 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// 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 { 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 { 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 { contextMenuActions } from "../../store/context-menu/context-menu-actions";
+import { ContextMenuKind } from "../context-menu/context-menu";
+import { Tree, getNodeChildren, getNode } from "../../models/tree";
+import { CollectionFileType } from "../../models/collection-file";
+
+const memoizedMapStateToProps = () => {
+    let prevState: CollectionPanelFilesState;
+    let prevTree: Array<TreeItem<FileTreeData>>;
+
+    return (state: RootState): Pick<CollectionPanelFilesProps, "items"> => {
+        if (prevState !== state.collectionPanelFiles) {
+            prevState = state.collectionPanelFiles;
+            prevTree = getNodeChildren('')(state.collectionPanelFiles)
+                .map(collectionItemToTreeItem(state.collectionPanelFiles));
+        }
+        return {
+            items: prevTree
+        };
+    };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
+    onUploadDataClick: () => { return; },
+    onCollapseToggle: (id) => {
+        dispatch(collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_COLLAPSE({ id }));
+    },
+    onSelectionToggle: (event, item) => {
+        dispatch(collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: item.id }));
+    },
+    onItemMenuOpen: (event, item) => {
+        event.preventDefault();
+        dispatch(contextMenuActions.OPEN_CONTEXT_MENU({
+            position: { x: event.clientX, y: event.clientY },
+            resource: { kind: ContextMenuKind.COLLECTION_FILES_ITEM, name: item.data.name, uuid: item.id }
+        }));
+    },
+    onOptionsMenuOpen: (event) =>
+        dispatch(contextMenuActions.OPEN_CONTEXT_MENU({
+            position: { x: event.clientX, y: event.clientY },
+            resource: { kind: ContextMenuKind.COLLECTION_FILES, name: '', uuid: '' }
+        }))
+});
+
+
+export const CollectionPanelFiles = connect(memoizedMapStateToProps(), mapDispatchToProps)(Component);
+
+const collectionItemToTreeItem = (tree: Tree<CollectionPanelDirectory | CollectionPanelFile>) =>
+    (id: string): TreeItem<FileTreeData> => {
+        const node = getNode(id)(tree) || {
+            id: '',
+            children: [],
+            parent: '',
+            value: {
+                name: 'Invalid node',
+                type: CollectionFileType.DIRECTORY,
+                selected: false,
+                collapsed: true
+            }
+        };
+        return {
+            active: false,
+            data: {
+                name: node.value.name,
+                size: node.value.type === CollectionFileType.FILE ? node.value.size : undefined,
+                type: node.value.type
+            },
+            id: node.id,
+            items: getNodeChildren(node.id)(tree)
+                .map(collectionItemToTreeItem(tree)),
+            open: node.value.type === CollectionFileType.DIRECTORY ? !node.value.collapsed : false,
+            selected: node.value.selected,
+            status: TreeItemStatus.LOADED
+        };
+    };
diff --git a/src/views-components/context-menu/action-sets/collection-files-action-set.ts b/src/views-components/context-menu/action-sets/collection-files-action-set.ts
new file mode 100644 (file)
index 0000000..9396b9e
--- /dev/null
@@ -0,0 +1,35 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// 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 { openRemoveDialog } from "../../remove-dialog/remove-dialog";
+
+
+export const collectionFilesActionSet: ContextMenuActionSet = [[{
+    name: "Select all",
+    execute: (dispatch) => {
+        dispatch(collectionPanelFilesAction.SELECT_ALL_COLLECTION_FILES());
+    }
+},{
+    name: "Unselect all",
+    execute: (dispatch) => {
+        dispatch(collectionPanelFilesAction.UNSELECT_ALL_COLLECTION_FILES());
+    }
+},{
+    name: "Remove selected",
+    execute: (dispatch, resource) => {
+        dispatch(openRemoveDialog('selected files'));
+    }
+},{
+    name: "Download selected",
+    execute: (dispatch, resource) => {
+        return;
+    }
+},{
+    name: "Create a new collection with selected",
+    execute: (dispatch, resource) => {
+        return;
+    }
+}]];
diff --git a/src/views-components/context-menu/action-sets/collection-files-item-action-set.ts b/src/views-components/context-menu/action-sets/collection-files-item-action-set.ts
new file mode 100644 (file)
index 0000000..0b623ec
--- /dev/null
@@ -0,0 +1,29 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { RenameIcon, DownloadIcon, RemoveIcon } from "../../../components/icon/icon";
+import { openRemoveDialog } from "../../remove-dialog/remove-dialog";
+import { openRenameDialog } from "../../rename-dialog/rename-dialog";
+
+
+export const collectionFilesItemActionSet: ContextMenuActionSet = [[{
+    name: "Rename",
+    icon: RenameIcon,
+    execute: (dispatch, resource) => {
+        dispatch(openRenameDialog('the item'));
+    }
+},{
+    name: "Download",
+    icon: DownloadIcon,
+    execute: (dispatch, resource) => {
+        return;
+    }
+},{
+    name: "Remove",
+    icon: RemoveIcon,
+    execute: (dispatch, resource) => {
+        dispatch(openRemoveDialog('selected file'));
+    }
+}]];
index 05e03fb34b754456c76a13787e5e15b02695b767..55fe8cfdba19efe1c42b088e7ccc87471aaea97f 100644 (file)
@@ -9,7 +9,7 @@ import { connect } from "react-redux";
 import { RootState } from "../../../store/store";
 
 const mapStateToProps = (state: RootState) => ({
-    isFavorite: state.contextMenu.resource && state.favorites[state.contextMenu.resource.uuid] === true
+    isFavorite: state.contextMenu.resource !== undefined && state.favorites[state.contextMenu.resource.uuid] === true
 });
 
 export const ToggleFavoriteAction = connect(mapStateToProps)((props: { isFavorite: boolean }) =>
index 4ec702914169e27d839df59b8a7d94bb497d4b26..8b00893703839c5bbd9fe64d932348ff08474f4b 100644 (file)
@@ -60,5 +60,7 @@ export enum ContextMenuKind {
     PROJECT = "Project",
     RESOURCE = "Resource",
     FAVORITE = "Favorite",
+    COLLECTION_FILES = "CollectionFiles",
+    COLLECTION_FILES_ITEM = "CollectionFilesItem",
     COLLECTION = 'Collection'
 }
index 1a521890d71be9ac684cb57335dee9d935e29526..aa0dc7bc89a49ea3584806556d268435a05e737d 100644 (file)
@@ -21,7 +21,7 @@ const addProject = (data: { name: string, description: string }) =>
         const { ownerUuid } = getState().projects.creator;
         return dispatch<any>(createProject(data)).then(() => {
             dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Created a new project",
+                message: "Project has been successfully created.",
                 hideDuration: 2000
             }));
             dispatch(projectPanelActions.REQUEST_ITEMS());
index 6449bf8d5697ac75b1204e3d3e361e4eb95ef86d..e13e8af0ec44934369a0ae5db0fdcb6b46987b2f 100644 (file)
@@ -24,40 +24,46 @@ interface Props {
 const mapStateToProps = (state: RootState, { id }: Props) =>
     getDataExplorer(state.dataExplorer, id);
 
-const mapDispatchToProps = (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => {
-    dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
-    return {
-        onSearch: (searchValue: string) => {
-            dispatch(dataExplorerActions.SET_SEARCH_VALUE({ id, searchValue }));
-        },
+const mapDispatchToProps = () => {
+    let prevColumns: DataColumns<any>;
+    return (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => {
+        if (columns !== prevColumns) {
+            prevColumns = columns;
+            dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
+        }
+        return {
+            onSearch: (searchValue: string) => {
+                dispatch(dataExplorerActions.SET_SEARCH_VALUE({ id, searchValue }));
+            },
 
-        onColumnToggle: (column: DataColumn<any>) => {
-            dispatch(dataExplorerActions.TOGGLE_COLUMN({ id, columnName: column.name }));
-        },
+            onColumnToggle: (column: DataColumn<any>) => {
+                dispatch(dataExplorerActions.TOGGLE_COLUMN({ id, columnName: column.name }));
+            },
 
-        onSortToggle: (column: DataColumn<any>) => {
-            dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
-        },
+            onSortToggle: (column: DataColumn<any>) => {
+                dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
+            },
 
-        onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
-            dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
-        },
+            onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
+                dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
+            },
 
-        onChangePage: (page: number) => {
-            dispatch(dataExplorerActions.SET_PAGE({ id, page }));
-        },
+            onChangePage: (page: number) => {
+                dispatch(dataExplorerActions.SET_PAGE({ id, page }));
+            },
 
-        onChangeRowsPerPage: (rowsPerPage: number) => {
-            dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
-        },
+            onChangeRowsPerPage: (rowsPerPage: number) => {
+                dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
+            },
 
-        onRowClick,
+            onRowClick,
 
-        onRowDoubleClick,
+            onRowDoubleClick,
 
-        onContextMenu,
+            onContextMenu,
+        };
     };
 };
 
-export const DataExplorer = connect(mapStateToProps, mapDispatchToProps)(DataExplorerComponent);
+export const DataExplorer = connect(mapStateToProps, mapDispatchToProps())(DataExplorerComponent);
 
index 3e3b74aa92747d9adf5053d543097a1110ee3ace..874ce138c76e456ff798e3d63edb88a4d3aa79e2 100644 (file)
@@ -12,7 +12,7 @@ import DialogContent from '@material-ui/core/DialogContent';
 import DialogTitle from '@material-ui/core/DialogTitle';
 import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
 
-import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-collection/create-collection-validator';
 
 type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "createProgress" | "dialogActions";
 
index 80a82b27fd97bb27a65dd954603e1cb7c2a965d8..08eee418bdc1f2bf016c0e575e0d751b3d96137c 100644 (file)
@@ -7,7 +7,7 @@ import { reduxForm, Field } from 'redux-form';
 import { compose } from 'redux';
 import { ArvadosTheme } from '../../common/custom-theme';
 import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '../../../node_modules/@material-ui/core';
-import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-project/create-project-validator';
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '../../validators/create-collection/create-collection-validator';
 import { COLLECTION_FORM_NAME } from '../../store/collections/updator/collection-updator-action';
 
 type CssRules = 'content' | 'actions' | 'textField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
diff --git a/src/views-components/remove-dialog/remove-dialog.tsx b/src/views-components/remove-dialog/remove-dialog.tsx
new file mode 100644 (file)
index 0000000..f08727f
--- /dev/null
@@ -0,0 +1,34 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from "@material-ui/core";
+import { withDialog } from "../../store/dialog/with-dialog";
+import { dialogActions } from "../../store/dialog/dialog-actions";
+
+export const REMOVE_DIALOG = 'removeCollectionFilesDialog';
+
+export const RemoveDialog = withDialog(REMOVE_DIALOG)(
+    (props) =>
+        <Dialog open={props.open}>
+            <DialogTitle>{`Removing ${props.data}`}</DialogTitle>
+            <DialogContent>
+                {`Are you sure you want to remove ${props.data}?`}
+            </DialogContent>
+            <DialogActions>
+                <Button
+                    variant='flat'
+                    color='primary'
+                    onClick={props.closeDialog}>
+                    Cancel
+                </Button>
+                <Button variant='raised' color='primary'>
+                    Remove
+                </Button>
+            </DialogActions>
+        </Dialog>
+);
+
+export const openRemoveDialog = (removedDataName: string) =>
+    dialogActions.OPEN_DIALOG({ id: REMOVE_DIALOG, data: removedDataName });
diff --git a/src/views-components/rename-dialog/rename-dialog.tsx b/src/views-components/rename-dialog/rename-dialog.tsx
new file mode 100644 (file)
index 0000000..8fe38cb
--- /dev/null
@@ -0,0 +1,37 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Typography } from "@material-ui/core";
+import { withDialog } from "../../store/dialog/with-dialog";
+import { dialogActions } from "../../store/dialog/dialog-actions";
+
+export const RENAME_DIALOG = 'nameDialog';
+
+export const RenameDialog = withDialog(RENAME_DIALOG)(
+    (props) =>
+        <Dialog open={props.open}>
+            <DialogTitle>{`Rename`}</DialogTitle>
+            <DialogContent>
+                <Typography variant='body1' gutterBottom>
+                    {`Please, enter a new name for ${props.data}`}
+                </Typography>
+                <TextField fullWidth={true} placeholder='New name' />
+            </DialogContent>
+            <DialogActions>
+                <Button
+                    variant='flat'
+                    color='primary'
+                    onClick={props.closeDialog}>
+                    Cancel
+                </Button>
+                <Button variant='raised' color='primary'>
+                    Ok
+                </Button>
+            </DialogActions>
+        </Dialog>
+);
+
+export const openRenameDialog = (originalName: string, ) =>
+    dialogActions.OPEN_DIALOG({ id: RENAME_DIALOG, data: originalName });
index aac5661e14fd3687c3909ee261d48bad0fd89a00..489d28473b0803fe3887380c850186f05c707680 100644 (file)
@@ -3,9 +3,9 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { 
-    StyleRulesCallback, WithStyles, withStyles, Card, 
-    CardHeader, IconButton, CardContent, Grid, Chip, TextField, Button
+import {
+    StyleRulesCallback, WithStyles, withStyles, Card,
+    CardHeader, IconButton, CardContent, Grid, Chip
 } from '@material-ui/core';
 import { connect, DispatchProp } from "react-redux";
 import { RouteComponentProps } from 'react-router';
@@ -14,6 +14,7 @@ import { RootState } from '../../store/store';
 import { MoreOptionsIcon, CollectionIcon, CopyIcon } from '../../components/icon/icon';
 import { DetailsAttribute } from '../../components/details-attribute/details-attribute';
 import { CollectionResource } from '../../models/collection';
+import { CollectionPanelFiles } from '../../views-components/collection-panel-files/collection-panel-files';
 import * as CopyToClipboard from 'react-copy-to-clipboard';
 import { TagResource } from '../../models/tag';
 import { CollectionTagForm } from './collection-tag-form';
@@ -69,16 +70,16 @@ export const CollectionPanel = withStyles(styles)(
                 const { classes, item, tags, onContextMenu } = this.props;
                 return <div>
                         <Card className={classes.card}>
-                            <CardHeader 
+                            <CardHeader
                                 avatar={ <CollectionIcon className={classes.iconHeader} /> }
-                                action={ 
+                                action={
                                     <IconButton
                                         aria-label="More options"
                                         onClick={event => onContextMenu(event, item)}>
                                         <MoreOptionsIcon />
-                                    </IconButton> 
+                                    </IconButton>
                                 }
-                                title={item && item.name } 
+                                title={item && item.name }
                                 subheader={item && item.description} />
                             <CardContent>
                                 <Grid container direction="column">
@@ -115,17 +116,9 @@ export const CollectionPanel = withStyles(styles)(
                                 </Grid>
                             </CardContent>
                         </Card>
-
-                        <Card className={classes.card}>
-                            <CardHeader title="Files" />
-                            <CardContent>
-                                <Grid container direction="column">
-                                    <Grid item xs={4}>
-                                        Files
-                                    </Grid>
-                                </Grid>
-                            </CardContent>
-                        </Card>
+                        <div className={classes.card}>
+                            <CollectionPanelFiles/>
+                        </div>
                     </div>;
             }
 
@@ -146,4 +139,4 @@ export const CollectionPanel = withStyles(styles)(
 const renderTagLabel = (tag: TagResource) => {
     const { properties } = tag;
     return `${properties.key}: ${properties.value}`;
-};
\ No newline at end of file
+};
index 5c3fb2b0421837c43cf2ae721de6306c50358bd3..0cd75ca3f8f5ae5e14fd5b853d48a624bd45c0cc 100644 (file)
@@ -17,6 +17,7 @@ import { ResourceKind } from '../../models/resource';
 import { resourceLabel } from '../../common/labels';
 import { ArvadosTheme } from '../../common/custom-theme';
 import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '../../views-components/data-explorer/renderers';
+import { restoreBranch } from '../../store/navigation/navigation-action';
 
 type CssRules = "toolbar" | "button";
 
@@ -176,11 +177,18 @@ export const ProjectPanel = withStyles(styles)(
             handleNewCollectionClick = () => {
                 this.props.onCollectionCreationDialogOpen(this.props.currentItemId);
             }
+
             componentWillReceiveProps({ match, currentItemId, onItemRouteChange }: ProjectPanelProps) {
                 if (match.params.id !== currentItemId) {
                     onItemRouteChange(match.params.id);
                 }
             }
+
+            componentDidMount() {
+                if (this.props.match.params.id && this.props.currentItemId === '') {
+                    this.props.dispatch<any>(restoreBranch(this.props.match.params.id));
+                }
+            }
         }
     )
 );
index 8491d172249a657b8c7957ea3e8024a0e65a5907..69d809869995d3c3344a043b0901076ec9633add 100644 (file)
@@ -34,13 +34,14 @@ import { ResourceKind } from '../../models/resource';
 import { ContextMenu, ContextMenuKind } from "../../views-components/context-menu/context-menu";
 import { FavoritePanel } from "../favorite-panel/favorite-panel";
 import { CurrentTokenDialog } from '../../views-components/current-token-dialog/current-token-dialog';
-import { dataExplorerActions } from '../../store/data-explorer/data-explorer-action';
 import { Snackbar } from '../../views-components/snackbar/snackbar';
 import { favoritePanelActions } from '../../store/favorite-panel/favorite-panel-action';
 import { CreateCollectionDialog } from '../../views-components/create-collection-dialog/create-collection-dialog';
 import { CollectionPanel } from '../collection-panel/collection-panel';
 import { loadCollection, loadCollectionTags } from '../../store/collection-panel/collection-panel-action';
 import { getCollectionUrl } from '../../models/collection';
+import { RemoveDialog } from '../../views-components/remove-dialog/remove-dialog';
+import { RenameDialog } from '../../views-components/rename-dialog/rename-dialog';
 import { UpdateCollectionDialog } from '../../views-components/update-collection-dialog/update-collection-dialog.';
 import { AuthService } from "../../services/auth-service/auth-service";
 
@@ -232,6 +233,8 @@ export const Workbench = withStyles(styles)(
                         <Snackbar />
                         <CreateProjectDialog />
                         <CreateCollectionDialog />
+                        <RemoveDialog />
+                        <RenameDialog />
                         <UpdateCollectionDialog />
                         <CurrentTokenDialog
                             currentToken={this.props.currentToken}
index 1eed5d2fda1e5adb4dff764d193c58d4e664a708..ee49986a76142a6638e0bc529c858221153c6e3b 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -23,9 +23,9 @@
     core-js "^2.5.7"
     regenerator-runtime "^0.12.0"
 
-"@material-ui/core@1.4.0":
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.4.0.tgz#e535fef84576b096c46e1fb7d6c4c61895155fd3"
+"@material-ui/core@1.4.2":
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.4.2.tgz#8a1282e985d4922a4d2b4f7e287d8a716a2fc108"
   dependencies:
     "@babel/runtime" "^7.0.0-beta.42"
     "@types/jss" "^9.5.3"
     jss-vendor-prefixer "^7.0.0"
     keycode "^2.1.9"
     normalize-scroll-left "^0.1.2"
-    popper.js "^1.0.0"
+    popper.js "^1.14.1"
     prop-types "^15.6.0"
     react-event-listener "^0.6.0"
     react-jss "^8.1.0"
     react-transition-group "^2.2.1"
     recompose "^0.27.0"
-    scroll "^2.0.3"
     warning "^4.0.1"
 
-"@material-ui/icons@1.1.0":
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-1.1.0.tgz#4d025df7b0ba6ace8d6710079ed76013a4d26595"
+"@material-ui/icons@2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-2.0.0.tgz#f2c4e80d0cb4bbbd433127781da67d93393535f8"
   dependencies:
-    recompose "^0.26.0 || ^0.27.0"
+    "@babel/runtime" "^7.0.0-beta.42"
+    recompose "^0.27.0"
 
 "@types/cheerio@*":
   version "0.22.8"
@@ -87,9 +87,9 @@
   version "4.6.2"
   resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0"
 
-"@types/jest@23.3.0":
-  version "23.3.0"
-  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.0.tgz#5dd70033b616a6228042244ebd992f6426808810"
+"@types/jest@23.3.1":
+  version "23.3.1"
+  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.1.tgz#a4319aedb071d478e6f407d1c4578ec8156829cf"
 
 "@types/jss@^9.5.3":
   version "9.5.4"
     csstype "^2.0.0"
     indefinite-observable "^1.0.1"
 
-"@types/lodash@4.14.112":
-  version "4.14.112"
-  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.112.tgz#4a8d8e5716b97a1ac01fe1931ad1e4cba719de5a"
+"@types/lodash@4.14.116":
+  version "4.14.116"
+  resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.116.tgz#5ccf215653e3e8c786a58390751033a9adca0eb9"
 
-"@types/node@*", "@types/node@10.5.2":
+"@types/node@*":
   version "10.5.2"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707"
 
+"@types/node@10.5.5":
+  version "10.5.5"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.5.tgz#8e84d24e896cd77b0d4f73df274027e3149ec2ba"
+
+"@types/react-copy-to-clipboard@4.2.5":
+  version "4.2.5"
+  resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-4.2.5.tgz#bda288b4256288676019b75ca86f1714bbd206d4"
+  dependencies:
+    "@types/react" "*"
+
 "@types/react-dom@16.0.6":
   version "16.0.6"
   resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.6.tgz#f1a65a4e7be8ed5d123f8b3b9eacc913e35a1a3c"
     "@types/node" "*"
     "@types/react" "*"
 
-"@types/react-redux@6.0.4":
-  version "6.0.4"
-  resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-6.0.4.tgz#c1cfce0a0bd88983c75dbf393576f8dc59181586"
+"@types/react-redux@6.0.6":
+  version "6.0.6"
+  resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-6.0.6.tgz#87f1d0a6ea901b93fcaf95fa57641ff64079d277"
   dependencies:
     "@types/react" "*"
     redux "^4.0.0"
 
-"@types/react-router-dom@4.2.7":
-  version "4.2.7"
-  resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.2.7.tgz#9d36bfe175f916dd8d7b6b0237feed6cce376b4c"
+"@types/react-router-dom@4.3.0":
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.0.tgz#c91796d02deb3a5b24bc1c5db4a255df0d18b8b5"
   dependencies:
     "@types/history" "*"
     "@types/react" "*"
     "@types/react" "*"
     redux "^3.6.0"
 
-"@types/redux-form@7.4.1":
-  version "7.4.1"
-  resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-7.4.1.tgz#df84bbda5f06e4d517210797c3cfdc573c3bda36"
+"@types/redux-form@7.4.4":
+  version "7.4.4"
+  resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-7.4.4.tgz#2cf62b8eb1dc1b1df95b6b25c2763db196e5c190"
   dependencies:
     "@types/react" "*"
     redux "^3.6.0 || ^4.0.0"
@@ -1817,6 +1827,12 @@ copy-descriptor@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
 
+copy-to-clipboard@^3:
+  version "3.0.8"
+  resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9"
+  dependencies:
+    toggle-selection "^1.0.3"
+
 core-js@^1.0.0:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
@@ -2272,10 +2288,6 @@ dom-urls@^1.1.0:
   dependencies:
     urijs "^1.16.1"
 
-dom-walk@^0.1.0:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018"
-
 domain-browser@^1.1.1:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@@ -3162,13 +3174,6 @@ global-prefix@^1.0.1:
     is-windows "^1.0.1"
     which "^1.2.14"
 
-global@~4.3.0:
-  version "4.3.2"
-  resolved "https://registry.yarnpkg.com/global/-/global-4.3.2.tgz#e76989268a6c74c38908b1305b10fc0e394e9d0f"
-  dependencies:
-    min-document "^2.19.0"
-    process "~0.5.1"
-
 globals@^9.18.0:
   version "9.18.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
@@ -4873,12 +4878,6 @@ mimic-fn@^1.0.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
 
-min-document@^2.19.0:
-  version "2.19.0"
-  resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685"
-  dependencies:
-    dom-walk "^0.1.0"
-
 minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
@@ -5577,9 +5576,9 @@ pn@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
 
-popper.js@^1.0.0:
-  version "1.14.3"
-  resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095"
+popper.js@^1.14.1:
+  version "1.14.4"
+  resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.4.tgz#8eec1d8ff02a5a3a152dd43414a15c7b79fd69b6"
 
 portfinder@^1.0.9:
   version "1.0.13"
@@ -5918,10 +5917,6 @@ process@^0.11.10:
   version "0.11.10"
   resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
 
-process@~0.5.1:
-  version "0.5.2"
-  resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf"
-
 promise-inflight@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
@@ -6038,12 +6033,6 @@ raf@3.4.0, raf@^3.4.0:
   dependencies:
     performance-now "^2.1.0"
 
-rafl@~1.2.1:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/rafl/-/rafl-1.2.2.tgz#fe930f758211020d47e38815f5196a8be4150740"
-  dependencies:
-    global "~4.3.0"
-
 railroad-diagrams@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
@@ -6098,6 +6087,13 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
+react-copy-to-clipboard@5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e"
+  dependencies:
+    copy-to-clipboard "^3"
+    prop-types "^15.5.8"
+
 react-dev-utils@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-5.0.1.tgz#1f396e161fe44b595db1b186a40067289bf06613"
@@ -6121,9 +6117,9 @@ react-dev-utils@^5.0.1:
     strip-ansi "3.0.1"
     text-table "0.2.0"
 
-react-dom@16.4.1:
-  version "16.4.1"
-  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.1.tgz#7f8b0223b3a5fbe205116c56deb85de32685dad6"
+react-dom@16.4.2:
+  version "16.4.2"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.2.tgz#4afed569689f2c561d2b8da0b819669c38a0bda4"
   dependencies:
     fbjs "^0.8.16"
     loose-envify "^1.1.0"
@@ -6273,9 +6269,9 @@ react-transition-group@^2.2.1:
     prop-types "^15.6.2"
     react-lifecycles-compat "^3.0.4"
 
-react@16.4.1:
-  version "16.4.1"
-  resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32"
+react@16.4.2:
+  version "16.4.2"
+  resolved "https://registry.yarnpkg.com/react/-/react-16.4.2.tgz#2cd90154e3a9d9dd8da2991149fdca3c260e129f"
   dependencies:
     fbjs "^0.8.16"
     loose-envify "^1.1.0"
@@ -6348,7 +6344,7 @@ realpath-native@^1.0.0:
   dependencies:
     util.promisify "^1.0.0"
 
-"recompose@^0.26.0 || ^0.27.0", recompose@^0.27.0:
+recompose@^0.27.0:
   version "0.27.1"
   resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.27.1.tgz#1a49e931f183634516633bbb4f4edbfd3f38a7ba"
   dependencies:
@@ -6747,12 +6743,6 @@ schema-utils@^0.4.5:
     ajv "^6.1.0"
     ajv-keywords "^3.1.0"
 
-scroll@^2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/scroll/-/scroll-2.0.3.tgz#0951b785544205fd17753bc3d294738ba16fc2ab"
-  dependencies:
-    rafl "~1.2.1"
-
 select-hose@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -7406,6 +7396,10 @@ to-regex@^3.0.1, to-regex@^3.0.2:
     regex-not "^1.0.2"
     safe-regex "^1.1.0"
 
+toggle-selection@^1.0.3:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
+
 toposort@^1.0.0:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
@@ -7545,9 +7539,9 @@ typedarray@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
 
-typescript@2.9.2:
-  version "2.9.2"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
+typescript@3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.1.tgz#43738f29585d3a87575520a4b93ab6026ef11fdb"
 
 ua-parser-js@^0.7.18:
   version "0.7.18"