Merge branch 'master' into 14039-details-view-improvements
authorJanicki Artur <artur.janicki@contractors.roche.com>
Mon, 20 Aug 2018 05:52:56 +0000 (07:52 +0200)
committerJanicki Artur <artur.janicki@contractors.roche.com>
Mon, 20 Aug 2018 05:52:56 +0000 (07:52 +0200)
refs #14039

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

54 files changed:
.env
package.json
src/common/api/common-resource-service.ts
src/common/api/filter-builder.test.ts
src/common/api/filter-builder.ts
src/common/api/order-builder.test.ts
src/common/api/order-builder.ts
src/common/xml.ts [new file with mode: 0644]
src/components/context-menu/context-menu.tsx
src/components/data-explorer/data-explorer.tsx
src/components/data-table/data-column.ts
src/components/side-panel/side-panel.tsx
src/index.tsx
src/models/collection-file.ts
src/services/collection-files-service/collection-manifest-mapper.test.ts
src/services/collection-service/collection-service.ts
src/services/favorite-service/favorite-order-builder.ts [deleted file]
src/services/favorite-service/favorite-service.test.ts
src/services/favorite-service/favorite-service.ts
src/services/groups-service/groups-service.ts
src/services/project-service/project-service.test.ts
src/services/project-service/project-service.ts
src/services/services.ts
src/services/tag-service/tag-service.ts
src/store/auth/auth-action.ts
src/store/collection-panel/collection-panel-action.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-actions.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-reducer.ts
src/store/collection-panel/collection-panel-files/collection-panel-files-state.ts
src/store/data-explorer/data-explorer-middleware-service.ts
src/store/data-explorer/data-explorer-middleware.test.ts
src/store/data-explorer/data-explorer-reducer.test.tsx
src/store/data-explorer/data-explorer-reducer.ts
src/store/favorite-panel/favorite-panel-middleware-service.ts
src/store/navigation/navigation-action.ts
src/store/project-panel/project-panel-middleware-service.ts
src/store/project/project-action.ts
src/store/side-panel/side-panel-action.ts
src/store/side-panel/side-panel-reducer.test.ts
src/store/side-panel/side-panel-reducer.ts
src/views-components/context-menu/action-sets/collection-files-action-set.ts
src/views-components/context-menu/action-sets/collection-files-item-action-set.ts
src/views-components/context-menu/actions/download-action.tsx [new file with mode: 0644]
src/views-components/context-menu/actions/download-collection-file-action.tsx [new file with mode: 0644]
src/views-components/context-menu/actions/favorite-action.tsx
src/views-components/data-explorer/data-explorer.tsx
src/views-components/file-remove-dialog/file-remove-dialog.ts
src/views-components/file-remove-dialog/multiple-files-remove-dialog.ts
src/views-components/main-app-bar/main-app-bar.tsx
src/views-components/project-tree-picker/project-tree-picker.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/project-panel/project-panel.tsx
src/views/workbench/workbench.tsx
yarn.lock

diff --git a/.env b/.env
index de1444c0bded77d30caae5614f392b9ba6b1fe12..df56fb28a76b3fbd7d61e647a59af98a3b2e7308 100644 (file)
--- a/.env
+++ b/.env
@@ -4,5 +4,5 @@
 
 REACT_APP_ARVADOS_CONFIG_URL=/config.json
 REACT_APP_ARVADOS_API_HOST=qr1hi.arvadosapi.com
-REACT_APP_ARVADOS_KEEP_WEB_HOST=download.qr1hi.arvadosapi.com
+REACT_APP_ARVADOS_KEEP_WEB_HOST=collections.qr1hi.arvadosapi.com
 HTTPS=true
\ No newline at end of file
index e2b6c4e1ec21f4017c0096a75bba61191a923dbd..b9484c5c0f644f0881527f60c885f2ea947e3f14 100644 (file)
@@ -3,19 +3,19 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
-    "@material-ui/core": "1.4.2",
-    "@material-ui/icons": "2.0.0",
+    "@material-ui/core": "1.5.0",
+    "@material-ui/icons": "2.0.2",
     "@types/lodash": "4.14.116",
-    "@types/react-copy-to-clipboard": "4.2.5",
-    "@types/react-dropzone": "4.2.1",
-    "@types/redux-form": "7.4.4",
+    "@types/react-copy-to-clipboard": "4.2.6",
+    "@types/react-dropzone": "4.2.2",
+    "@types/redux-form": "7.4.5",
     "axios": "0.18.0",
     "classnames": "2.2.6",
     "lodash": "4.17.10",
     "react": "16.4.2",
     "react-copy-to-clipboard": "5.0.1",
     "react-dom": "16.4.2",
-    "react-dropzone": "4.2.13",
+    "react-dropzone": "5.0.1",
     "react-redux": "5.0.7",
     "react-router": "4.3.1",
     "react-router-dom": "4.3.1",
   },
   "devDependencies": {
     "@types/classnames": "^2.2.4",
-    "@types/enzyme": "3.1.12",
-    "@types/enzyme-adapter-react-16": "1.0.2",
+    "@types/enzyme": "3.1.13",
+    "@types/enzyme-adapter-react-16": "1.0.3",
     "@types/jest": "23.3.1",
-    "@types/node": "10.5.5",
+    "@types/node": "10.7.1",
     "@types/react": "16.4",
-    "@types/react-dom": "16.0.6",
+    "@types/react-dom": "16.0.7",
     "@types/react-redux": "6.0.6",
     "@types/react-router": "4.0.29",
     "@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.4",
+    "@types/redux-form": "7.4.5",
     "axios-mock-adapter": "1.15.0",
-    "enzyme": "3.3.0",
-    "enzyme-adapter-react-16": "1.1.1",
+    "enzyme": "3.4.4",
+    "enzyme-adapter-react-16": "1.2.0",
     "jest-localstorage-mock": "2.2.0",
     "redux-devtools": "3.4.1",
     "redux-form": "7.4.2",
index 36017f0f11f5b2455e27a74330a67e43c2c8cbe5..caa4d760c9e08ef1c7af63bf86e90b34a03d4ebb 100644 (file)
@@ -3,16 +3,14 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as _ from "lodash";
-import { FilterBuilder } from "./filter-builder";
-import { OrderBuilder } from "./order-builder";
 import { AxiosInstance, AxiosPromise } from "axios";
 import { Resource } from "~/models/resource";
 
 export interface ListArguments {
     limit?: number;
     offset?: number;
-    filters?: FilterBuilder;
-    order?: OrderBuilder;
+    filters?: string;
+    order?: string;
     select?: string[];
     distinct?: boolean;
     count?: string;
@@ -90,8 +88,8 @@ export class CommonResourceService<T extends Resource> {
         const { filters, order, ...other } = args;
         const params = {
             ...other,
-            filters: filters ? filters.serialize() : undefined,
-            order: order ? order.getOrder() : undefined
+            filters: filters ? `[${filters}]` : undefined,
+            order: order ? order : undefined
         };
         return CommonResourceService.defaultResponse(
             this.serverApi
index d129a806d0a2fd8937d05caed3cecad1b69f7d86..de2ba4cba65bbd008f581d20c4db5c16b1cbdc05 100644 (file)
@@ -9,49 +9,49 @@ describe("FilterBuilder", () => {
     let filters: FilterBuilder;
 
     beforeEach(() => {
-        filters = FilterBuilder.create();
+        filters = new FilterBuilder();
     });
 
     it("should add 'equal' rule", () => {
         expect(
-            filters.addEqual("etag", "etagValue").serialize()
-        ).toEqual(`[["etag","=","etagValue"]]`);
+            filters.addEqual("etag", "etagValue").getFilters()
+        ).toEqual(`["etag","=","etagValue"]`);
     });
 
     it("should add 'like' rule", () => {
         expect(
-            filters.addLike("etag", "etagValue").serialize()
-        ).toEqual(`[["etag","like","%etagValue%"]]`);
+            filters.addLike("etag", "etagValue").getFilters()
+        ).toEqual(`["etag","like","%etagValue%"]`);
     });
 
     it("should add 'ilike' rule", () => {
         expect(
-            filters.addILike("etag", "etagValue").serialize()
-        ).toEqual(`[["etag","ilike","%etagValue%"]]`);
+            filters.addILike("etag", "etagValue").getFilters()
+        ).toEqual(`["etag","ilike","%etagValue%"]`);
     });
 
     it("should add 'is_a' rule", () => {
         expect(
-            filters.addIsA("etag", "etagValue").serialize()
-        ).toEqual(`[["etag","is_a","etagValue"]]`);
+            filters.addIsA("etag", "etagValue").getFilters()
+        ).toEqual(`["etag","is_a","etagValue"]`);
     });
 
     it("should add 'is_a' rule for set", () => {
         expect(
-            filters.addIsA("etag", ["etagValue1", "etagValue2"]).serialize()
-        ).toEqual(`[["etag","is_a",["etagValue1","etagValue2"]]]`);
+            filters.addIsA("etag", ["etagValue1", "etagValue2"]).getFilters()
+        ).toEqual(`["etag","is_a",["etagValue1","etagValue2"]]`);
     });
 
     it("should add 'in' rule", () => {
         expect(
-            filters.addIn("etag", "etagValue").serialize()
-        ).toEqual(`[["etag","in","etagValue"]]`);
+            filters.addIn("etag", "etagValue").getFilters()
+        ).toEqual(`["etag","in","etagValue"]`);
     });
 
     it("should add 'in' rule for set", () => {
         expect(
-            filters.addIn("etag", ["etagValue1", "etagValue2"]).serialize()
-        ).toEqual(`[["etag","in",["etagValue1","etagValue2"]]]`);
+            filters.addIn("etag", ["etagValue1", "etagValue2"]).getFilters()
+        ).toEqual(`["etag","in",["etagValue1","etagValue2"]]`);
     });
 
     it("should add multiple rules", () => {
@@ -59,28 +59,14 @@ describe("FilterBuilder", () => {
             filters
                 .addIn("etag", ["etagValue1", "etagValue2"])
                 .addEqual("href", "hrefValue")
-                .serialize()
-        ).toEqual(`[["etag","in",["etagValue1","etagValue2"]],["href","=","hrefValue"]]`);
-    });
-
-    it("should concatenate multiple builders", () => {
-        expect(
-            filters
-                .concat(FilterBuilder.create().addIn("etag", ["etagValue1", "etagValue2"]))
-                .concat(FilterBuilder.create().addEqual("href", "hrefValue"))
-                .serialize()
-        ).toEqual(`[["etag","in",["etagValue1","etagValue2"]],["href","=","hrefValue"]]`);
+                .getFilters()
+        ).toEqual(`["etag","in",["etagValue1","etagValue2"]],["href","=","hrefValue"]`);
     });
 
     it("should add attribute prefix", () => {
-        expect(FilterBuilder
-            .create("myPrefix")
-            .addIn("etag", ["etagValue1", "etagValue2"])
-            .serialize())
-            .toEqual(`[["my_prefix.etag","in",["etagValue1","etagValue2"]]]`);
+        expect(new FilterBuilder()
+            .addIn("etag", ["etagValue1", "etagValue2"], "myPrefix")
+            .getFilters())
+            .toEqual(`["my_prefix.etag","in",["etagValue1","etagValue2"]]`);
     });
-
-
-
-
 });
index e5aab3ac72825c37d0b503287bf07968eb93dc1d..b5558dbb16a291f8ecb81fdbd642f742bb99b9d3 100644 (file)
@@ -4,58 +4,48 @@
 
 import * as _ from "lodash";
 
-export class FilterBuilder {
-    static create(resourcePrefix = "") {
-        return new FilterBuilder(resourcePrefix);
-    }
-
-    constructor(
-        private resourcePrefix = "",
-        private filters = "") { }
+export function joinFilters(filters0?: string, filters1?: string) {
+    return [filters0, filters1].filter(s => s).join(",");
+}
 
-    public addEqual(field: string, value?: string) {
-        return this.addCondition(field, "=", value);
-    }
+export class FilterBuilder {
+    constructor(private filters = "") { }
 
-    public addLike(field: string, value?: string) {
-        return this.addCondition(field, "like", value, "%", "%");
+    public addEqual(field: string, value?: string, resourcePrefix?: string) {
+        return this.addCondition(field, "=", value, "", "", resourcePrefix );
     }
 
-    public addILike(field: string, value?: string) {
-        return this.addCondition(field, "ilike", value, "%", "%");
+    public addLike(field: string, value?: string, resourcePrefix?: string) {
+        return this.addCondition(field, "like", value, "%", "%", resourcePrefix);
     }
 
-    public addIsA(field: string, value?: string | string[]) {
-        return this.addCondition(field, "is_a", value);
+    public addILike(field: string, value?: string, resourcePrefix?: string) {
+        return this.addCondition(field, "ilike", value, "%", "%", resourcePrefix);
     }
 
-    public addIn(field: string, value?: string | string[]) {
-        return this.addCondition(field, "in", value);
+    public addIsA(field: string, value?: string | string[], resourcePrefix?: string) {
+        return this.addCondition(field, "is_a", value, "", "", resourcePrefix);
     }
 
-    public concat(filterBuilder: FilterBuilder) {
-        return new FilterBuilder(this.resourcePrefix, this.filters + (this.filters && filterBuilder.filters ? "," : "") + filterBuilder.getFilters());
+    public addIn(field: string, value?: string | string[], resourcePrefix?: string) {
+        return this.addCondition(field, "in", value, "", "", resourcePrefix);
     }
 
     public getFilters() {
         return this.filters;
     }
 
-    public serialize() {
-        return "[" + this.filters + "]";
-    }
-
-    private addCondition(field: string, cond: string, value?: string | string[], prefix: string = "", postfix: string = "") {
+    private addCondition(field: string, cond: string, value?: string | string[], prefix: string = "", postfix: string = "", resourcePrefix?: string) {
         if (value) {
             value = typeof value === "string"
                 ? `"${prefix}${value}${postfix}"`
                 : `["${value.join(`","`)}"]`;
 
-            const resourcePrefix = this.resourcePrefix
-                ? _.snakeCase(this.resourcePrefix) + "."
+            const resPrefix = resourcePrefix
+                ? _.snakeCase(resourcePrefix) + "."
                 : "";
 
-            this.filters += `${this.filters ? "," : ""}["${resourcePrefix}${_.snakeCase(field.toString())}","${cond}",${value}]`;
+            this.filters += `${this.filters ? "," : ""}["${resPrefix}${_.snakeCase(field)}","${cond}",${value}]`;
         }
         return this;
     }
index f53bddb5cc51e047540029564579041c4798c647..f56e0634357e2ad4ff3a8b27eba6fedec8ee493c 100644 (file)
@@ -6,22 +6,10 @@ import { OrderBuilder } from "./order-builder";
 
 describe("OrderBuilder", () => {
     it("should build correct order query", () => {
-        const order = OrderBuilder
-            .create()
+        const order = new OrderBuilder()
             .addAsc("kind")
             .addDesc("modifiedAt")
             .getOrder();
-        expect(order).toEqual(["kind asc", "modified_at desc"]);
-    });
-
-    it("should combine results with other builder", () => {
-        const order = OrderBuilder
-            .create()
-            .addAsc("kind")
-            .concat(OrderBuilder
-                .create("properties")
-                .addDesc("modifiedAt"))
-            .getOrder();
-        expect(order).toEqual(["kind asc", "properties.modified_at desc"]);
+        expect(order).toEqual("kind asc,modified_at desc");
     });
 });
index ed990541c61bb960e4b1a5074a4530570d1617eb..196b06952e55c911d9cd0bac6ed881151918bf98 100644 (file)
@@ -3,40 +3,28 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as _ from "lodash";
-import { Resource } from "../../models/resource";
+import { Resource } from "~/models/resource";
 
-export class OrderBuilder<T extends Resource = Resource> {
-
-    static create<T extends Resource = Resource>(prefix?: string){
-        return new OrderBuilder<T>([], prefix);
-    }
+export enum OrderDirection { ASC, DESC }
 
-    private constructor(
-        private order: string[] = [],
-        private prefix = ""){}
+export class OrderBuilder<T extends Resource = Resource> {
 
-    private addRule (direction: string, attribute: keyof T) {
-        const prefix = this.prefix ? this.prefix + "." : "";
-        const order = [...this.order, `${prefix}${_.snakeCase(attribute.toString())} ${direction}`];
-        return new OrderBuilder<T>(order, prefix);
-    }
+    constructor(private order: string[] = []) {}
 
-    addAsc(attribute: keyof T) {
-        return this.addRule("asc", attribute);
+    addOrder(direction: OrderDirection, attribute: keyof T, prefix?: string) {
+        this.order.push(`${prefix ? prefix + "." : ""}${_.snakeCase(attribute.toString())} ${direction === OrderDirection.ASC ? "asc" : "desc"}`);
+        return this;
     }
 
-    addDesc(attribute: keyof T) {
-        return this.addRule("desc", attribute);
+    addAsc(attribute: keyof T, prefix?: string) {
+        return this.addOrder(OrderDirection.ASC, attribute, prefix);
     }
 
-    concat(orderBuilder: OrderBuilder){
-        return new OrderBuilder<T>(
-            this.order.concat(orderBuilder.getOrder()),
-            this.prefix
-        );
+    addDesc(attribute: keyof T, prefix?: string) {
+        return this.addOrder(OrderDirection.DESC, attribute, prefix);
     }
 
     getOrder() {
-        return this.order.slice();
+        return this.order.join(",");
     }
 }
diff --git a/src/common/xml.ts b/src/common/xml.ts
new file mode 100644 (file)
index 0000000..c810de9
--- /dev/null
@@ -0,0 +1,8 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const getTagValue = (document: Document | Element, tagName: string, defaultValue: string) => {
+    const [el] = Array.from(document.getElementsByTagName(tagName));
+    return el ? el.innerHTML : defaultValue;
+};
index 95bbeafb4f23774c4a358b19282a60120375f751..4068251bdc04c82487cbf141fbdea910692496a8 100644 (file)
@@ -36,21 +36,23 @@ export class ContextMenu extends React.PureComponent<ContextMenuProps> {
                 {items.map((group, groupIndex) =>
                     <React.Fragment key={groupIndex}>
                         {group.map((item, actionIndex) =>
-                            <ListItem
-                                button
-                                key={actionIndex}
-                                onClick={() => onItemClick(item)}>
-                                {item.icon &&
-                                    <ListItemIcon>
-                                        <item.icon />
-                                    </ListItemIcon>}
-                                {item.name &&
-                                    <ListItemText>
-                                        {item.name}
-                                    </ListItemText>}
-                                {item.component &&
-                                    <item.component />}
-                            </ListItem>)}
+                            item.component
+                                ? <item.component
+                                    key={actionIndex}
+                                    onClick={() => onItemClick(item)} />
+                                : <ListItem
+                                    button
+                                    key={actionIndex}
+                                    onClick={() => onItemClick(item)}>
+                                    {item.icon &&
+                                        <ListItemIcon>
+                                            <item.icon />
+                                        </ListItemIcon>}
+                                    {item.name &&
+                                        <ListItemText>
+                                            {item.name}
+                                        </ListItemText>}
+                                </ListItem>)}
                         {groupIndex < items.length - 1 && <Divider />}
                     </React.Fragment>)}
             </List>
index 2811bd4d1f7e7fada857e6677e7f50c1c576fba6..af14db9c9e08818f3a9dc945718d1296f707d5e8 100644 (file)
@@ -7,7 +7,7 @@ import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, WithStyles, Table
 import MoreVertIcon from "@material-ui/icons/MoreVert";
 import { ColumnSelector } from "../column-selector/column-selector";
 import { DataTable, DataColumns } from "../data-table/data-table";
-import { DataColumn } from "../data-table/data-column";
+import { DataColumn, SortDirection } from "../data-table/data-column";
 import { DataTableFilterItem } from '../data-table-filters/data-table-filters';
 import { SearchInput } from '../search-input/search-input';
 import { ArvadosTheme } from "~/common/custom-theme";
@@ -51,6 +51,7 @@ interface DataExplorerDataProps<T> {
 }
 
 interface DataExplorerActionProps<T> {
+    onSetColumns: (columns: DataColumns<T>) => void;
     onSearch: (value: string) => void;
     onRowClick: (item: T) => void;
     onRowDoubleClick: (item: T) => void;
@@ -67,11 +68,16 @@ type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T
 
 export const DataExplorer = withStyles(styles)(
     class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
+        componentDidMount() {
+            if (this.props.onSetColumns) {
+                this.props.onSetColumns(this.props.columns);
+            }
+        }
         render() {
-            const { 
-                columns, onContextMenu, onFiltersChange, onSortToggle, extractKey, 
-                rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch, 
-                items, itemsAvailable, onRowClick, onRowDoubleClick, defaultIcon, defaultMessages, classes 
+            const {
+                columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
+                rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
+                items, itemsAvailable, onRowClick, onRowDoubleClick, defaultIcon, defaultMessages, classes
             } = this.props;
             return <div>
                 { items.length > 0 ? (
@@ -111,7 +117,7 @@ export const DataExplorer = withStyles(styles)(
                         </Toolbar>
                     </Paper>
                 ) : (
-                    <DefaultView 
+                    <DefaultView
                         classRoot={classes.defaultRoot}
                         icon={defaultIcon}
                         classIcon={classes.defaultIcon}
@@ -140,6 +146,8 @@ export const DataExplorer = withStyles(styles)(
             name: "Actions",
             selected: true,
             configurable: false,
+            sortDirection: SortDirection.NONE,
+            filters: [],
             key: "context-actions",
             render: this.renderContextMenuTrigger,
             width: "auto"
index ac35c0239556b01692ae7a960eabb2257dd3e930..d4e23ab5b5eb95afc9f68414639e8348b235f545 100644 (file)
@@ -6,12 +6,12 @@ import { DataTableFilterItem } from "../data-table-filters/data-table-filters";
 import * as React from "react";
 
 export interface DataColumn<T, F extends DataTableFilterItem = DataTableFilterItem> {
+    key?: React.Key;
     name: string;
     selected: boolean;
     configurable: boolean;
-    key?: React.Key;
-    sortDirection?: SortDirection;
-    filters?: F[];
+    sortDirection: SortDirection;
+    filters: F[];
     render: (item: T) => React.ReactElement<any>;
     renderHeader?: () => React.ReactElement<any>;
     width?: string;
index 206cb6322b84a1d181d451f76d8886328837a969..84e5c5476f5f8266c3464f82c880b9f052a5af42 100644 (file)
@@ -3,7 +3,6 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { ReactElement } from 'react';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { List, ListItem, ListItemIcon, Collapse } from "@material-ui/core";
@@ -11,6 +10,7 @@ import { SidePanelRightArrowIcon, IconType } from '../icon/icon';
 import * as classnames from "classnames";
 import { ListItemTextIcon } from '../list-item-text-icon/list-item-text-icon';
 import { Dispatch } from "redux";
+import { RouteComponentProps, withRouter } from "react-router";
 
 type CssRules = 'active' | 'row' | 'root' | 'list' | 'iconClose' | 'iconOpen' | 'toggableIconContainer' | 'toggableIcon';
 
@@ -54,8 +54,8 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 export interface SidePanelItem {
     id: string;
     name: string;
+    url: string;
     icon: IconType;
-    active?: boolean;
     open?: boolean;
     margin?: boolean;
     openAble?: boolean;
@@ -69,30 +69,34 @@ interface SidePanelDataProps {
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: SidePanelItem) => void;
 }
 
-type SidePanelProps = SidePanelDataProps & WithStyles<CssRules>;
+type SidePanelProps = RouteComponentProps<{}> & SidePanelDataProps & WithStyles<CssRules>;
 
-export const SidePanel = withStyles(styles)(
+export const SidePanel = withStyles(styles)(withRouter(
     class extends React.Component<SidePanelProps> {
-        render(): ReactElement<any> {
+        render() {
             const { classes, toggleOpen, toggleActive, sidePanelItems, children } = this.props;
             const { root, row, list, toggableIconContainer } = classes;
+
+            const path = this.props.location.pathname.split('/');
+            const activeUrl = path.length > 1 ? "/" + path[1] : "/";
             return (
                 <div className={root}>
                     <List>
-                        {sidePanelItems.map(it => (
-                            <span key={it.name}>
+                        {sidePanelItems.map(it => {
+                            const active = it.url === activeUrl;
+                            return <span key={it.name}>
                                 <ListItem button className={list} onClick={() => toggleActive(it.id)}
                                           onContextMenu={this.handleRowContextMenu(it)}>
                                     <span className={row}>
                                         {it.openAble ? (
                                             <i onClick={() => toggleOpen(it.id)} className={toggableIconContainer}>
                                                 <ListItemIcon
-                                                    className={this.getToggableIconClassNames(it.open, it.active)}>
+                                                    className={this.getToggableIconClassNames(it.open, active)}>
                                                     < SidePanelRightArrowIcon/>
                                                 </ListItemIcon>
                                             </i>
                                         ) : null}
-                                        <ListItemTextIcon icon={it.icon} name={it.name} isActive={it.active}
+                                        <ListItemTextIcon icon={it.icon} name={it.name} isActive={active}
                                                           hasMargin={it.margin}/>
                                     </span>
                                 </ListItem>
@@ -101,8 +105,8 @@ export const SidePanel = withStyles(styles)(
                                         {children}
                                     </Collapse>
                                 ) : null}
-                            </span>
-                        ))}
+                            </span>;
+                        })}
                     </List>
                 </div>
             );
@@ -121,4 +125,4 @@ export const SidePanel = withStyles(styles)(
             (event: React.MouseEvent<HTMLElement>) =>
                 item.openAble ? this.props.onContextMenu(event, item) : null
     }
-);
+));
index cfdbb46cf55882743fd714e7520a9c64884a8d22..443e76f3e62a42597d804aefd779b32b3f65ffa9 100644 (file)
@@ -28,6 +28,14 @@ import { collectionFilesItemActionSet } from './views-components/context-menu/ac
 import { collectionActionSet } from './views-components/context-menu/action-sets/collection-action-set';
 import { collectionResourceActionSet } from './views-components/context-menu/action-sets/collection-resource-action-set';
 
+const getBuildNumber = () => "BN-" + (process.env.BUILD_NUMBER || "dev");
+const getGitCommit = () => "GIT-" + (process.env.GIT_COMMIT || "latest").substr(0, 7);
+const getBuildInfo = () => getBuildNumber() + " / " + getGitCommit();
+
+const buildInfo = getBuildInfo();
+
+console.log(`Starting arvados [${buildInfo}]`);
+
 addMenuActionSet(ContextMenuKind.ROOT_PROJECT, rootProjectActionSet);
 addMenuActionSet(ContextMenuKind.PROJECT, projectActionSet);
 addMenuActionSet(ContextMenuKind.RESOURCE, resourceActionSet);
@@ -45,9 +53,9 @@ fetchConfig()
 
         store.dispatch(initAuth());
         store.dispatch(getProjectList(services.authService.getUuid()));
-        
+
         const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props}/>;
-        const WorkbenchComponent = (props: any) => <Workbench authService={services.authService} {...props}/>;
+        const WorkbenchComponent = (props: any) => <Workbench authService={services.authService} buildInfo={buildInfo} {...props}/>;
 
         const App = () =>
             <MuiThemeProvider theme={CustomTheme}>
index a4b656c51397c80bbd0319b73bf7f18d6d12323b..d74ada6008b982fe40daca7e601fbfbb99f1ea57 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { Tree } from './tree';
+import { Tree, createTree, setNode } from './tree';
 
 export type CollectionFilesTree = Tree<CollectionDirectory | CollectionFile>;
 
@@ -13,6 +13,7 @@ export enum CollectionFileType {
 
 export interface CollectionDirectory {
     path: string;
+    url: string;
     id: string;
     name: string;
     type: CollectionFileType.DIRECTORY;
@@ -20,6 +21,7 @@ export interface CollectionDirectory {
 
 export interface CollectionFile {
     path: string;
+    url: string;
     id: string;
     name: string;
     size: number;
@@ -34,6 +36,7 @@ export const createCollectionDirectory = (data: Partial<CollectionDirectory>): C
     id: '',
     name: '',
     path: '',
+    url: '',
     type: CollectionFileType.DIRECTORY,
     ...data
 });
@@ -42,7 +45,21 @@ export const createCollectionFile = (data: Partial<CollectionFile>): CollectionF
     id: '',
     name: '',
     path: '',
+    url: '',
     size: 0,
     type: CollectionFileType.FILE,
     ...data
 });
+
+export const createCollectionFilesTree = (data: Array<CollectionDirectory | CollectionFile>) => {
+    const directories = data.filter(item => item.type === CollectionFileType.DIRECTORY);
+    directories.sort((a, b) => a.path.localeCompare(b.path));
+    const files = data.filter(item => item.type === CollectionFileType.FILE);
+    return [...directories, ...files]
+        .reduce((tree, item) => setNode({
+            children: [],
+            id: item.id,
+            parent: item.path,
+            value: item
+        })(tree), createTree<CollectionDirectory | CollectionFile>());
+};
\ No newline at end of file
index f08ea7bd1dc6a30e5a51895848efedf211492f9a..698a6bb7f4ba6f55a46a8a5a5d6e986eaff92747 100644 (file)
@@ -14,25 +14,29 @@ test('mapManifestToFiles', () => {
         id: '/a',
         name: 'a',
         size: 0,
-        type: 'file'
+        type: 'file',
+        url: ''
     }, {
         path: '',
         id: '/b',
         name: 'b',
         size: 0,
-        type: 'file'
+        type: 'file',
+        url: ''
     }, {
         path: '',
         id: '/output.txt',
         name: 'output.txt',
         size: 33,
-        type: 'file'
+        type: 'file',
+        url: ''
     }, {
         path: '/c',
         id: '/c/d',
         name: 'd',
         size: 0,
-        type: 'file'
+        type: 'file',
+        url: ''
     },]);
 });
 
@@ -44,17 +48,20 @@ test('mapManifestToDirectories', () => {
         path: "",
         id: '/c',
         name: 'c',
-        type: 'directory'
+        type: 'directory',
+        url: ''
     }, {
         path: '/c',
         id: '/c/user',
         name: 'user',
-        type: 'directory'
+        type: 'directory',
+        url: ''
     }, {
         path: '/c/user',
         id: '/c/user/results',
         name: 'results',
-        type: 'directory'
+        type: 'directory',
+        url: ''
     },]);
 });
 
index f60e81f1975da8b4c37921f7490b9c0fb150c008..9feec699e52dfd07070105c75c251d96b3107541 100644 (file)
@@ -2,23 +2,81 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import * as _ from "lodash";
 import { CommonResourceService } from "~/common/api/common-resource-service";
 import { CollectionResource } from "~/models/collection";
 import axios, { AxiosInstance } from "axios";
 import { KeepService } from "../keep-service/keep-service";
+import { WebDAV } from "~/common/webdav";
+import { AuthService } from "../auth-service/auth-service";
+import { mapTree, getNodeChildren, getNode, TreeNode } from "../../models/tree";
+import { getTagValue } from "~/common/xml";
 import { FilterBuilder } from "~/common/api/filter-builder";
-import { CollectionFile, createCollectionFile } from "~/models/collection-file";
+import { CollectionFile, createCollectionFile, CollectionFileType, CollectionDirectory, createCollectionDirectory } from '~/models/collection-file';
 import { parseKeepManifestText, stringifyKeepManifest } from "../collection-files-service/collection-manifest-parser";
-import * as _ from "lodash";
 import { KeepManifestStream } from "~/models/keep-manifest";
+import { createCollectionFilesTree } from '~/models/collection-file';
 
 export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
 
 export class CollectionService extends CommonResourceService<CollectionResource> {
-    constructor(serverApi: AxiosInstance, private keepService: KeepService) {
+    constructor(serverApi: AxiosInstance, private keepService: KeepService, private webdavClient: WebDAV, private authService: AuthService) {
         super(serverApi, "collections");
     }
 
+    async files(uuid: string) {
+        const request = await this.webdavClient.propfind(`/c=${uuid}`);
+        if (request.responseXML != null) {
+            const files = this.extractFilesData(request.responseXML);
+            const tree = createCollectionFilesTree(files);
+            const sortedTree = mapTree(node => {
+                const children = getNodeChildren(node.id)(tree).map(id => getNode(id)(tree)) as TreeNode<CollectionDirectory | CollectionFile>[];
+                children.sort((a, b) =>
+                    a.value.type !== b.value.type
+                        ? a.value.type === CollectionFileType.DIRECTORY ? -1 : 1
+                        : a.value.name.localeCompare(b.value.name)
+                );
+                return { ...node, children: children.map(child => child.id) };
+            })(tree);
+            return sortedTree;
+        }
+        return Promise.reject();
+    }
+
+    async deleteFile(collectionUuid: string, filePath: string) {
+        return this.webdavClient.delete(`/c=${collectionUuid}${filePath}`);
+    }
+
+    extractFilesData(document: Document) {
+        const collectionUrlPrefix = /\/c=[0-9a-zA-Z\-]*/;
+        return Array
+            .from(document.getElementsByTagName('D:response'))
+            .slice(1) // omit first element which is collection itself
+            .map(element => {
+                const name = getTagValue(element, 'D:displayname', '');
+                const size = parseInt(getTagValue(element, 'D:getcontentlength', '0'), 10);
+                const pathname = getTagValue(element, 'D:href', '');
+                const nameSuffix = `/${name || ''}`;
+                const directory = pathname
+                    .replace(collectionUrlPrefix, '')
+                    .replace(nameSuffix, '');
+                const href = this.webdavClient.defaults.baseURL + pathname + '?api_token=' + this.authService.getApiToken();
+
+                const data = {
+                    url: href,
+                    id: `${directory}/${name}`,
+                    name,
+                    path: directory,
+                };
+
+                return getTagValue(element, 'D:resourcetype', '')
+                    ? createCollectionDirectory(data)
+                    : createCollectionFile({ ...data, size });
+
+            });
+    }
+
+
     private readFile(file: File): Promise<ArrayBuffer> {
         return new Promise<ArrayBuffer>(resolve => {
             const reader = new FileReader();
@@ -84,10 +142,10 @@ export class CollectionService extends CommonResourceService<CollectionResource>
     }
 
     uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress): Promise<CollectionResource | never> {
-        const filters = FilterBuilder.create()
+        const filters = new FilterBuilder()
             .addEqual("service_type", "proxy");
 
-        return this.keepService.list({ filters }).then(data => {
+        return this.keepService.list({ filters: filters.getFilters() }).then(data => {
             if (data.items && data.items.length > 0) {
                 const serviceHost =
                     (data.items[0].serviceSslFlag ? "https://" : "http://") +
diff --git a/src/services/favorite-service/favorite-order-builder.ts b/src/services/favorite-service/favorite-order-builder.ts
deleted file mode 100644 (file)
index fc6cbdc..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { LinkResource } from "~/models/link";
-import { GroupContentsResource, GroupContentsResourcePrefix } from "../groups-service/groups-service";
-import { OrderBuilder } from "~/common/api/order-builder";
-
-export class FavoriteOrderBuilder {
-
-    static create(
-        linkOrder = OrderBuilder.create<LinkResource>(),
-        contentOrder = OrderBuilder.create<GroupContentsResource>()) {
-        return new FavoriteOrderBuilder(linkOrder, contentOrder);
-    }
-
-    private constructor(
-        private linkOrder: OrderBuilder<LinkResource>,
-        private contentOrder: OrderBuilder<GroupContentsResource>
-    ) { }
-
-    addAsc(attribute: "name") {
-        const linkOrder = this.linkOrder.addAsc(attribute);
-        const contentOrder = this.contentOrder
-            .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION).addAsc(attribute))
-            .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS).addAsc(attribute))
-            .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT).addAsc(attribute));
-        return FavoriteOrderBuilder.create(linkOrder, contentOrder);
-    }
-
-    addDesc(attribute: "name") {
-        const linkOrder = this.linkOrder.addDesc(attribute);
-        const contentOrder = this.contentOrder
-            .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION).addDesc(attribute))
-            .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS).addDesc(attribute))
-            .concat(OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT).addDesc(attribute));
-        return FavoriteOrderBuilder.create(linkOrder, contentOrder);
-    }
-
-    getLinkOrder() {
-        return this.linkOrder;
-    }
-
-    getContentOrder() {
-        return this.contentOrder;
-    }
-
-}
index de59ff8d14e44f76b77d1c6e108947333d0978b7..d4bf9ac7f68c2bf14bccd1dd685e8db29d13cab3 100644 (file)
@@ -38,8 +38,7 @@ describe("FavoriteService", () => {
 
     it("unmarks resource as favorite", async () => {
         const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ uuid: "linkUuid" }] }));
-        const filters = FilterBuilder
-            .create()
+        const filters = new FilterBuilder()
             .addEqual('tailUuid', "userUuid")
             .addEqual('headUuid', "resourceUuid")
             .addEqual('linkClass', LinkClass.STAR);
@@ -49,35 +48,33 @@ describe("FavoriteService", () => {
 
         const newFavorite = await favoriteService.delete({ userUuid: "userUuid", resourceUuid: "resourceUuid" });
 
-        expect(list.mock.calls[0][0].filters.getFilters()).toEqual(filters.getFilters());
+        expect(list.mock.calls[0][0].filters).toEqual(filters.getFilters());
         expect(linkService.delete).toHaveBeenCalledWith("linkUuid");
         expect(newFavorite[0].uuid).toEqual("linkUuid");
     });
 
     it("lists favorite resources", async () => {
         const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ headUuid: "headUuid" }] }));
-        const listFilters = FilterBuilder
-            .create()
+        const listFilters = new FilterBuilder()
             .addEqual('tailUuid', "userUuid")
             .addEqual('linkClass', LinkClass.STAR);
         const contents = jest.fn().mockReturnValue(Promise.resolve({ items: [{ uuid: "resourceUuid" }] }));
-        const contentFilters = FilterBuilder.create().addIn('uuid', ["headUuid"]);
+        const contentFilters = new FilterBuilder().addIn('uuid', ["headUuid"]);
         linkService.list = list;
         groupService.contents = contents;
         const favoriteService = new FavoriteService(linkService, groupService);
 
         const favorites = await favoriteService.list("userUuid");
 
-        expect(list.mock.calls[0][0].filters.getFilters()).toEqual(listFilters.getFilters());
+        expect(list.mock.calls[0][0].filters).toEqual(listFilters.getFilters());
         expect(contents.mock.calls[0][0]).toEqual("userUuid");
-        expect(contents.mock.calls[0][1].filters.getFilters()).toEqual(contentFilters.getFilters());
+        expect(contents.mock.calls[0][1].filters).toEqual(contentFilters.getFilters());
         expect(favorites).toEqual({ items: [{ uuid: "resourceUuid" }] });
     });
 
     it("checks if resources are present in favorites", async () => {
         const list = jest.fn().mockReturnValue(Promise.resolve({ items: [{ headUuid: "foo" }] }));
-        const listFilters = FilterBuilder
-            .create()
+        const listFilters = new FilterBuilder()
             .addIn("headUuid", ["foo", "oof"])
             .addEqual("tailUuid", "userUuid")
             .addEqual("linkClass", LinkClass.STAR);
@@ -86,7 +83,7 @@ describe("FavoriteService", () => {
 
         const favorites = await favoriteService.checkPresenceInFavorites("userUuid", ["foo", "oof"]);
 
-        expect(list.mock.calls[0][0].filters.getFilters()).toEqual(listFilters.getFilters());
+        expect(list.mock.calls[0][0].filters).toEqual(listFilters.getFilters());
         expect(favorites).toEqual({ foo: true, oof: false });
     });
 
index d948819550f1b812c54c9341f1a6aa04c35eae2b..7a49c8ccbac3555af028536815f38ade60c08de5 100644 (file)
@@ -4,24 +4,23 @@
 
 import { LinkService } from "../link-service/link-service";
 import { GroupsService, GroupContentsResource } from "../groups-service/groups-service";
-import { LinkResource, LinkClass } from "~/models/link";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { LinkClass } from "~/models/link";
+import { FilterBuilder, joinFilters } from "~/common/api/filter-builder";
 import { ListResults } from "~/common/api/common-resource-service";
-import { FavoriteOrderBuilder } from "./favorite-order-builder";
-import { OrderBuilder } from "~/common/api/order-builder";
 
 export interface FavoriteListArguments {
     limit?: number;
     offset?: number;
-    filters?: FilterBuilder;
-    order?: FavoriteOrderBuilder;
+    filters?: string;
+    linkOrder?: string;
+    contentOrder?: string;
 }
 
 export class FavoriteService {
     constructor(
         private linkService: LinkService,
         private groupsService: GroupsService
-    ) { }
+    ) {}
 
     create(data: { userUuid: string; resource: { uuid: string; name: string } }) {
         return this.linkService.create({
@@ -36,36 +35,36 @@ export class FavoriteService {
     delete(data: { userUuid: string; resourceUuid: string; }) {
         return this.linkService
             .list({
-                filters: FilterBuilder
-                    .create()
+                filters: new FilterBuilder()
                     .addEqual('tailUuid', data.userUuid)
                     .addEqual('headUuid', data.resourceUuid)
                     .addEqual('linkClass', LinkClass.STAR)
+                    .getFilters()
             })
             .then(results => Promise.all(
                 results.items.map(item => this.linkService.delete(item.uuid))));
     }
 
-    list(userUuid: string, { filters, limit, offset, order }: FavoriteListArguments = {}): Promise<ListResults<GroupContentsResource>> {
-        const listFilter = FilterBuilder
-            .create()
+    list(userUuid: string, { filters, limit, offset, linkOrder, contentOrder }: FavoriteListArguments = {}): Promise<ListResults<GroupContentsResource>> {
+        const listFilters = new FilterBuilder()
             .addEqual('tailUuid', userUuid)
-            .addEqual('linkClass', LinkClass.STAR);
+            .addEqual('linkClass', LinkClass.STAR)
+            .getFilters();
 
         return this.linkService
             .list({
-                filters: filters ? filters.concat(listFilter) : listFilter,
+                filters: joinFilters(filters, listFilters),
                 limit,
                 offset,
-                order: order ? order.getLinkOrder() : OrderBuilder.create<LinkResource>()
+                order: linkOrder
             })
             .then(results => {
                 const uuids = results.items.map(item => item.headUuid);
                 return this.groupsService.contents(userUuid, {
                     limit,
                     offset,
-                    order: order ? order.getContentOrder() : OrderBuilder.create<GroupContentsResource>(),
-                    filters: FilterBuilder.create().addIn('uuid', uuids),
+                    order: contentOrder,
+                    filters: new FilterBuilder().addIn('uuid', uuids).getFilters(),
                     recursive: true
                 });
             });
@@ -74,11 +73,11 @@ export class FavoriteService {
     checkPresenceInFavorites(userUuid: string, resourceUuids: string[]): Promise<Record<string, boolean>> {
         return this.linkService
             .list({
-                filters: FilterBuilder
-                    .create()
+                filters: new FilterBuilder()
                     .addIn("headUuid", resourceUuids)
                     .addEqual("tailUuid", userUuid)
                     .addEqual("linkClass", LinkClass.STAR)
+                    .getFilters()
             })
             .then(({ items }) => resourceUuids.reduce((results, uuid) => {
                 const isFavorite = items.some(item => item.headUuid === uuid);
index e4c3167550990995ff0c3a3e1504b5383e883afa..822c810ef7ed0203666bdba829c0b13fced7d5ba 100644 (file)
@@ -4,8 +4,6 @@
 
 import * as _ from "lodash";
 import { CommonResourceService, ListResults } from "~/common/api/common-resource-service";
-import { FilterBuilder } from "~/common/api/filter-builder";
-import { OrderBuilder } from "~/common/api/order-builder";
 import { AxiosInstance } from "axios";
 import { GroupResource } from "~/models/group";
 import { CollectionResource } from "~/models/collection";
@@ -15,8 +13,8 @@ import { ProcessResource } from "~/models/process";
 export interface ContentsArguments {
     limit?: number;
     offset?: number;
-    order?: OrderBuilder;
-    filters?: FilterBuilder;
+    order?: string;
+    filters?: string;
     recursive?: boolean;
 }
 
@@ -35,8 +33,8 @@ export class GroupsService<T extends GroupResource = GroupResource> extends Comm
         const { filters, order, ...other } = args;
         const params = {
             ...other,
-            filters: filters ? filters.serialize() : undefined,
-            order: order ? order.getOrder() : undefined
+            filters: filters ? `[${filters}]` : undefined,
+            order: order ? order : undefined
         };
         return this.serverApi
             .get(this.resourceType + `${uuid}/contents/`, {
index 688a476b47c620ca4e04a75414b6d3221bbf7ab7..a717736c3a68f3fb3e5ca9692696fcad5d72d095 100644 (file)
@@ -25,10 +25,10 @@ describe("CommonResourceService", () => {
         const resource = await projectService.list();
         expect(axiosInstance.get).toHaveBeenCalledWith("/groups/", {
             params: {
-                filters: FilterBuilder
-                    .create()
+                filters: "[" + new FilterBuilder()
                     .addEqual("groupClass", "project")
-                    .serialize()
+                    .getFilters() + "]",
+                order: undefined
             }
         });
     });
index 3ffaa35f8c9145fb6337f04c7bfb0ec868bd2f1f..e916f3c0a4da9d8be388a8577d3d304115385007 100644 (file)
@@ -6,7 +6,7 @@ import { GroupsService } from "../groups-service/groups-service";
 import { ProjectResource } from "~/models/project";
 import { GroupClass } from "~/models/group";
 import { ListArguments } from "~/common/api/common-resource-service";
-import { FilterBuilder } from "~/common/api/filter-builder";
+import { FilterBuilder, joinFilters } from "~/common/api/filter-builder";
 
 export class ProjectService extends GroupsService<ProjectResource> {
 
@@ -18,18 +18,12 @@ export class ProjectService extends GroupsService<ProjectResource> {
     list(args: ListArguments = {}) {
         return super.list({
             ...args,
-            filters: this.addProjectFilter(args.filters)
+            filters: joinFilters(
+                args.filters,
+                new FilterBuilder()
+                    .addEqual("groupClass", GroupClass.PROJECT)
+                    .getFilters()
+            )
         });
     }
-
-    private addProjectFilter(filters?: FilterBuilder) {
-        return FilterBuilder
-            .create()
-            .concat(filters
-                ? filters
-                : FilterBuilder.create())
-            .concat(FilterBuilder
-                .create()
-                .addEqual("groupClass", GroupClass.PROJECT));
-    }
 }
index 99f802dfe079521074c2fa143297e0d1bca0cb2d..61dd399206384c159d0ffe6982e704fe054a405a 100644 (file)
@@ -30,7 +30,7 @@ export const createServices = (config: Config) => {
     const projectService = new ProjectService(apiClient);
     const linkService = new LinkService(apiClient);
     const favoriteService = new FavoriteService(linkService, groupsService);
-    const collectionService = new CollectionService(apiClient, keepService);
+    const collectionService = new CollectionService(apiClient, keepService, webdavClient, authService);
     const tagService = new TagService(linkService);
     const collectionFilesService = new CollectionFilesService(collectionService);
 
index 78fdceed0443cc87df088e9c8883ccb36759ecb0..52e481a7975b38890c88c9d9309f68bf960e59fa 100644 (file)
@@ -25,15 +25,15 @@ export class TagService {
     }
 
     list(uuid: string) {
-        const filters = FilterBuilder
-            .create()
+        const filters = new FilterBuilder()
             .addEqual("headUuid", uuid)
             .addEqual("tailUuid", TagTailType.COLLECTION)
-            .addEqual("linkClass", LinkClass.TAG);
+            .addEqual("linkClass", LinkClass.TAG)
+            .getFilters();
 
-        const order = OrderBuilder
-            .create<TagResource>()
-            .addAsc('createdAt');
+        const order = new OrderBuilder<TagResource>()
+            .addAsc('createdAt')
+            .getOrder();
 
         return this.linkService
             .list({ filters, order })
index 33a5df90450e2cde772c73c8d80c7b06854c4f12..00af5ce5b0bb7614f4fbc97316a61dd712759ba3 100644 (file)
@@ -17,9 +17,9 @@ export const authActions = unionize({
     USER_DETAILS_REQUEST: {},
     USER_DETAILS_SUCCESS: ofType<User>()
 }, {
-        tag: 'type',
-        value: 'payload'
-    });
+    tag: 'type',
+    value: 'payload'
+});
 
 function setAuthorizationHeader(services: ServiceRepository, token: string) {
     services.apiClient.defaults.headers.common = {
index 0772210c8497fae4655a7cc7a004fb8e4d3067b2..06d4d2762288ee5b6a14108a7ac01e336242a494 100644 (file)
@@ -4,7 +4,7 @@
 
 import { unionize, ofType, UnionOf } from "unionize";
 import { Dispatch } from "redux";
-import { ResourceKind } from "~/models/resource";
+import { loadCollectionFiles } from "./collection-panel-files/collection-panel-files-actions";
 import { CollectionResource } from "~/models/collection";
 import { collectionPanelFilesAction } from "./collection-panel-files/collection-panel-files-actions";
 import { createTree } from "~/models/tree";
@@ -14,7 +14,7 @@ import { TagResource, TagProperty } from "~/models/tag";
 import { snackbarActions } from "../snackbar/snackbar-actions";
 
 export const collectionPanelActions = unionize({
-    LOAD_COLLECTION: ofType<{ uuid: string, kind: ResourceKind }>(),
+    LOAD_COLLECTION: ofType<{ uuid: string }>(),
     LOAD_COLLECTION_SUCCESS: ofType<{ item: CollectionResource }>(),
     LOAD_COLLECTION_TAGS: ofType<{ uuid: string }>(),
     LOAD_COLLECTION_TAGS_SUCCESS: ofType<{ tags: TagResource[] }>(),
@@ -28,18 +28,15 @@ export type CollectionPanelAction = UnionOf<typeof collectionPanelActions>;
 
 export const COLLECTION_TAG_FORM_NAME = 'collectionTagForm';
 
-export const loadCollection = (uuid: string, kind: ResourceKind) =>
+export const loadCollection = (uuid: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid, kind }));
+        dispatch(collectionPanelActions.LOAD_COLLECTION({ uuid }));
         dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES({ files: createTree() }));
         return services.collectionService
             .get(uuid)
             .then(item => {
                 dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item }));
-                return services.collectionFilesService.getFiles(item.uuid);
-            })
-            .then(files => {
-                dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(files));
+                dispatch<any>(loadCollectionFiles(uuid));
             });
     };
 
index 09821083ee4c5bda6fce5f6bb6faf1524dad6eb4..cedfbebef5aded305b3237e125ee80857c96361b 100644 (file)
@@ -3,7 +3,14 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { default as unionize, ofType, UnionOf } from "unionize";
-import { CollectionFilesTree } from "~/models/collection-file";
+import { Dispatch } from "redux";
+import { CollectionFilesTree, CollectionFileType } from "~/models/collection-file";
+import { ServiceRepository } from "~/services/services";
+import { RootState } from "../../store";
+import { snackbarActions } from "../../snackbar/snackbar-actions";
+import { dialogActions } from "../../dialog/dialog-actions";
+import { getNodeValue, getNodeDescendants } from "~/models/tree";
+import { CollectionPanelDirectory, CollectionPanelFile } from "./collection-panel-files-state";
 
 export const collectionPanelFilesAction = unionize({
     SET_COLLECTION_FILES: ofType<CollectionFilesTree>(),
@@ -14,3 +21,69 @@ export const collectionPanelFilesAction = unionize({
 }, { tag: 'type', value: 'payload' });
 
 export type CollectionPanelFilesAction = UnionOf<typeof collectionPanelFilesAction>;
+
+export const loadCollectionFiles = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const files = await services.collectionService.files(uuid);
+        dispatch(collectionPanelFilesAction.SET_COLLECTION_FILES(files));
+    };
+
+export const removeCollectionFiles = (filePaths: string[]) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { item } = getState().collectionPanel;
+        if (item) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
+            const promises = filePaths.map(filePath => services.collectionService.deleteFile(item.uuid, filePath));
+            await Promise.all(promises);
+            dispatch<any>(loadCollectionFiles(item.uuid));
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000 }));
+        }
+    };
+
+export const removeCollectionsSelectedFiles = () =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const tree = getState().collectionPanelFiles;
+        const allFiles = getNodeDescendants('')(tree)
+            .map(id => getNodeValue(id)(tree))
+            .filter(file => file !== undefined) as Array<CollectionPanelDirectory | CollectionPanelFile>;
+
+        const selectedDirectories = allFiles.filter(file => file.selected && file.type === CollectionFileType.DIRECTORY);
+        const selectedFiles = allFiles.filter(file => file.selected && !selectedDirectories.some(dir => dir.id === file.path));
+        const paths = [...selectedDirectories, ...selectedFiles].map(file => file.id);
+        dispatch<any>(removeCollectionFiles(paths));
+    };
+
+export const FILE_REMOVE_DIALOG = 'fileRemoveDialog';
+export const openFileRemoveDialog = (filePath: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const file = getNodeValue(filePath)(getState().collectionPanelFiles);
+        if (file) {
+            const title = file.type === CollectionFileType.DIRECTORY
+                ? 'Removing directory'
+                : 'Removing file';
+            const text = file.type === CollectionFileType.DIRECTORY
+                ? 'Are you sure you want to remove this directory?'
+                : 'Are you sure you want to remove this file?';
+
+            dispatch(dialogActions.OPEN_DIALOG({
+                id: FILE_REMOVE_DIALOG,
+                data: {
+                    title,
+                    text,
+                    confirmButtonLabel: 'Remove',
+                    filePath
+                }
+            }));
+        }
+    };
+
+export const MULTIPLE_FILES_REMOVE_DIALOG = 'multipleFilesRemoveDialog';
+export const openMultipleFilesRemoveDialog = () =>
+    dialogActions.OPEN_DIALOG({
+        id: MULTIPLE_FILES_REMOVE_DIALOG,
+        data: {
+            title: 'Removing files',
+            text: 'Are you sure you want to remove selected files?',
+            confirmButtonLabel: 'Remove'
+        }
+    });
index 2a3aac744737691bbf6c2a472f002eca0ff363db..08b60308c42bb9d4f3dfdeb72eae23f4b45946de 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, mapCollectionFileToCollectionPanelFile } from "./collection-panel-files-state";
+import { CollectionPanelFilesState, CollectionPanelFile, CollectionPanelDirectory, mapCollectionFileToCollectionPanelFile, mergeCollectionPanelFilesStates } from "./collection-panel-files-state";
 import { CollectionPanelFilesAction, collectionPanelFilesAction } from "./collection-panel-files-actions";
 import { createTree, mapTreeValues, getNode, setNode, getNodeAncestors, getNodeDescendants, setNodeValueWith, mapTree } from "~/models/tree";
 import { CollectionFileType } from "~/models/collection-file";
@@ -10,7 +10,7 @@ import { CollectionFileType } from "~/models/collection-file";
 export const collectionPanelFilesReducer = (state: CollectionPanelFilesState = createTree(), action: CollectionPanelFilesAction) => {
     return collectionPanelFilesAction.match(action, {
         SET_COLLECTION_FILES: files =>
-            mapTree(mapCollectionFileToCollectionPanelFile)(files),
+            mergeCollectionPanelFilesStates(state, mapTree(mapCollectionFileToCollectionPanelFile)(files)),
 
         TOGGLE_COLLECTION_FILE_COLLAPSE: data =>
             toggleCollapse(data.id)(state),
index f7955eb6d0fe8a544003095dce3de8bd4988fc63..35b81d2e121e134b1b10f8aad6223a0267da9765 100644 (file)
@@ -2,8 +2,8 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { Tree, TreeNode, mapTreeValues, getNodeValue } from '~/models/tree';
 import { CollectionFile, CollectionDirectory, CollectionFileType } from '~/models/collection-file';
-import { Tree, TreeNode } from '~/models/tree';
 
 export type CollectionPanelFilesState = Tree<CollectionPanelDirectory | CollectionPanelFile>;
 
@@ -24,3 +24,14 @@ export const mapCollectionFileToCollectionPanelFile = (node: TreeNode<Collection
             : { ...node.value, selected: false }
     };
 };
+
+export const mergeCollectionPanelFilesStates = (oldState: CollectionPanelFilesState, newState: CollectionPanelFilesState) => {
+    return mapTreeValues((value: CollectionPanelDirectory | CollectionPanelFile) => {
+        const oldValue = getNodeValue(value.id)(oldState);
+        return oldValue
+            ? oldValue.type === CollectionFileType.DIRECTORY
+                ? { ...value, collapsed: oldValue.collapsed, selected: oldValue.selected }
+                : { ...value, selected: oldValue.selected }
+            : value;
+    })(newState);
+}; 
index 14be4ea7b8c074551989d0dde93ae7350ff5154d..7c64020ef3b46b7e9f2ff85b4e078741a352bab4 100644 (file)
@@ -4,6 +4,8 @@
 
 import { Dispatch, MiddlewareAPI } from "redux";
 import { RootState } from "../store";
+import { DataColumns } from "~/components/data-table/data-table";
+import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
 
 export abstract class DataExplorerMiddlewareService {
     protected readonly id: string;
@@ -16,5 +18,10 @@ export abstract class DataExplorerMiddlewareService {
         return this.id;
     }
 
+    public getColumnFilters<T, F extends DataTableFilterItem>(columns: DataColumns<T, F>, columnName: string): F[] {
+        const column = columns.find(c => c.name === columnName);
+        return column ? column.filters.filter(f => f.selected) : [];
+    }
+
     abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>): void;
 }
index d93ccbf446200a1eaafec7bf2304cde82cd625de..2a88817cecc8ad9256b6ee9f33b3c212df83a78b 100644 (file)
@@ -7,6 +7,7 @@ import { dataExplorerMiddleware } from "./data-explorer-middleware";
 import { MiddlewareAPI } from "redux";
 import { DataColumns } from "~/components/data-table/data-table";
 import { dataExplorerActions } from "./data-explorer-action";
+import { SortDirection } from "~/components/data-table/data-column";
 
 
 describe("DataExplorerMiddleware", () => {
@@ -18,6 +19,8 @@ describe("DataExplorerMiddleware", () => {
                 name: "Column",
                 selected: true,
                 configurable: false,
+                sortDirection: SortDirection.NONE,
+                filters: [],
                 render: jest.fn()
             }],
             requestItems: jest.fn(),
@@ -44,6 +47,8 @@ describe("DataExplorerMiddleware", () => {
                 name: "Column",
                 selected: true,
                 configurable: false,
+                sortDirection: SortDirection.NONE,
+                filters: [],
                 render: jest.fn()
             }],
             requestItems: jest.fn(),
index 6b1c90798962032ddc273005b1f1b0c7d41de123..0bc44ba85bf06195bb34ef88500ec807c4b99e54 100644 (file)
@@ -12,9 +12,11 @@ describe('data-explorer-reducer', () => {
     it('should set columns', () => {
         const columns: DataColumns<any> = [{
             name: "Column 1",
+            filters: [],
             render: jest.fn(),
             selected: true,
-            configurable: true
+            configurable: true,
+            sortDirection: SortDirection.NONE
         }];
         const state = dataExplorerReducer(undefined,
             dataExplorerActions.SET_COLUMNS({ id: "Data explorer", columns }));
@@ -24,12 +26,14 @@ describe('data-explorer-reducer', () => {
     it('should toggle sorting', () => {
         const columns: DataColumns<any> = [{
             name: "Column 1",
+            filters: [],
             render: jest.fn(),
             selected: true,
             configurable: true,
             sortDirection: SortDirection.ASC
         }, {
             name: "Column 2",
+            filters: [],
             render: jest.fn(),
             selected: true,
             configurable: true,
@@ -44,9 +48,11 @@ describe('data-explorer-reducer', () => {
     it('should set filters', () => {
         const columns: DataColumns<any> = [{
             name: "Column 1",
+            filters: [],
             render: jest.fn(),
             selected: true,
-            configurable: true
+            configurable: true,
+            sortDirection: SortDirection.NONE
         }];
 
         const filters: DataTableFilterItem[] = [{
index 175cd0b2817fd0ea419f8474f1326bb16bd2dc20..cc800244abc1d0d9c1673bd03743d1eff7024f14 100644 (file)
@@ -67,9 +67,23 @@ export const getDataExplorer = (state: DataExplorerState, id: string) =>
 const update = (state: DataExplorerState, id: string, updateFn: (dataExplorer: DataExplorer) => DataExplorer) =>
     ({ ...state, [id]: updateFn(getDataExplorer(state, id)) });
 
+const canUpdateColumns = (prevColumns: DataColumns<any>, nextColumns: DataColumns<any>) => {
+    if (prevColumns.length !== nextColumns.length) {
+        return true;
+    }
+    for (let i = 0; i < nextColumns.length; i++) {
+        const pc = prevColumns[i];
+        const nc = nextColumns[i];
+        if (pc.key !== nc.key || pc.name !== nc.name) {
+            return true;
+        }
+    }
+    return false;
+};
+
 const setColumns = (columns: DataColumns<any>) =>
     (dataExplorer: DataExplorer) =>
-        ({ ...dataExplorer, columns });
+        ({ ...dataExplorer, columns: canUpdateColumns(dataExplorer.columns, columns) ? columns : dataExplorer.columns });
 
 const mapColumns = (mapFn: (column: DataColumn<any>) => DataColumn<any>) =>
     (dataExplorer: DataExplorer) =>
index 653184104cd0f3383a9f8252ff8bba75636f7769..1c2f062252b2101224d62b690d955849c327b698 100644 (file)
@@ -3,17 +3,19 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-middleware-service";
-import { FavoritePanelFilter, FavoritePanelColumnNames } from "~/views/favorite-panel/favorite-panel";
+import { FavoritePanelColumnNames, FavoritePanelFilter } from "~/views/favorite-panel/favorite-panel";
 import { RootState } from "../store";
 import { DataColumns } from "~/components/data-table/data-table";
 import { FavoritePanelItem, resourceToDataItem } from "~/views/favorite-panel/favorite-panel-item";
-import { FavoriteOrderBuilder } from "~/services/favorite-service/favorite-order-builder";
 import { ServiceRepository } from "~/services/services";
 import { SortDirection } from "~/components/data-table/data-column";
 import { FilterBuilder } from "~/common/api/filter-builder";
 import { checkPresenceInFavorites } from "../favorites/favorites-actions";
 import { favoritePanelActions } from "./favorite-panel-action";
 import { Dispatch, MiddlewareAPI } from "redux";
+import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
+import { LinkResource } from "~/models/link";
+import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
 
 export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -23,47 +25,51 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
     requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
         const dataExplorer = api.getState().dataExplorer[this.getId()];
         const columns = dataExplorer.columns as DataColumns<FavoritePanelItem, FavoritePanelFilter>;
-        const sortColumn = dataExplorer.columns.find(
-            ({ sortDirection }) => sortDirection !== undefined && sortDirection !== "none"
-        );
-        const typeFilters = getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
-        const order = FavoriteOrderBuilder.create();
-        if (typeFilters.length > 0) {
-            this.services.favoriteService
-                .list(this.services.authService.getUuid()!, {
-                    limit: dataExplorer.rowsPerPage,
-                    offset: dataExplorer.page * dataExplorer.rowsPerPage,
-                    order: sortColumn!.name === FavoritePanelColumnNames.NAME
-                        ? sortColumn!.sortDirection === SortDirection.ASC
-                            ? order.addDesc("name")
-                            : order.addAsc("name")
-                        : order,
-                    filters: FilterBuilder
-                        .create()
-                        .addIsA("headUuid", typeFilters.map(filter => filter.type))
-                        .addILike("name", dataExplorer.searchValue)
-                })
-                .then(response => {
-                    api.dispatch(favoritePanelActions.SET_ITEMS({
-                        items: response.items.map(resourceToDataItem),
-                        itemsAvailable: response.itemsAvailable,
-                        page: Math.floor(response.offset / response.limit),
-                        rowsPerPage: response.limit
-                    }));
-                    api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
-                });
-        } else {
-            api.dispatch(favoritePanelActions.SET_ITEMS({
-                items: [],
-                itemsAvailable: 0,
-                page: 0,
-                rowsPerPage: dataExplorer.rowsPerPage
-            }));
+        const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
+        const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
+
+        const linkOrder = new OrderBuilder<LinkResource>();
+        const contentOrder = new OrderBuilder<GroupContentsResource>();
+
+        if (sortColumn && sortColumn.name === FavoritePanelColumnNames.NAME) {
+            const direction = sortColumn.sortDirection === SortDirection.ASC
+                ? OrderDirection.ASC
+                : OrderDirection.DESC;
+
+            linkOrder.addOrder(direction, "name");
+            contentOrder
+                .addOrder(direction, "name", GroupContentsResourcePrefix.COLLECTION)
+                .addOrder(direction, "name", GroupContentsResourcePrefix.PROCESS)
+                .addOrder(direction, "name", GroupContentsResourcePrefix.PROJECT);
         }
+
+        this.services.favoriteService
+            .list(this.services.authService.getUuid()!, {
+                limit: dataExplorer.rowsPerPage,
+                offset: dataExplorer.page * dataExplorer.rowsPerPage,
+                linkOrder: linkOrder.getOrder(),
+                contentOrder: contentOrder.getOrder(),
+                filters: new FilterBuilder()
+                    .addIsA("headUuid", typeFilters.map(filter => filter.type))
+                    .addILike("name", dataExplorer.searchValue)
+                    .getFilters()
+            })
+            .then(response => {
+                api.dispatch(favoritePanelActions.SET_ITEMS({
+                    items: response.items.map(resourceToDataItem),
+                    itemsAvailable: response.itemsAvailable,
+                    page: Math.floor(response.offset / response.limit),
+                    rowsPerPage: response.limit
+                }));
+                api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
+            })
+            .catch(() => {
+                api.dispatch(favoritePanelActions.SET_ITEMS({
+                    items: [],
+                    itemsAvailable: 0,
+                    page: 0,
+                    rowsPerPage: dataExplorer.rowsPerPage
+                }));
+            });
     }
 }
-
-const getColumnFilters = (columns: DataColumns<FavoritePanelItem, FavoritePanelFilter>, columnName: string) => {
-    const column = columns.find(c => c.name === columnName);
-    return column && column.filters ? column.filters.filter(f => f.selected) : [];
-};
index 79d24471491fcd5e2ced64a8bdcd8edb6b38bd87..50ec93d25e8091e2186e0eccf7a6f403d0ca90ae 100644 (file)
@@ -3,26 +3,27 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from "redux";
-import { projectActions, getProjectList } from "../project/project-action";
+import { getProjectList, projectActions } from "../project/project-action";
 import { push } from "react-router-redux";
 import { TreeItemStatus } from "~/components/tree/tree";
 import { findTreeItem } from "../project/project-reducer";
 import { RootState } from "../store";
-import { Resource, ResourceKind } from "~/models/resource";
+import { ResourceKind } from "~/models/resource";
 import { projectPanelActions } from "../project-panel/project-panel-action";
 import { getCollectionUrl } from "~/models/collection";
 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 { SidePanelId } 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) {
-        case ResourceKind.PROJECT: return getProjectUrl(resource.uuid);
-        case ResourceKind.COLLECTION: return getCollectionUrl(resource.uuid);
-        default: return resource.href;
+export const getResourceUrl = (resourceKind: ResourceKind, resourceUuid: string): string => {
+    switch (resourceKind) {
+        case ResourceKind.PROJECT: return getProjectUrl(resourceUuid);
+        case ResourceKind.COLLECTION: return getCollectionUrl(resourceUuid);
+        default:
+            return '';
     }
 };
 
@@ -38,7 +39,7 @@ export const setProjectItem = (itemId: string, itemMode: ItemMode) =>
         const treeItem = findTreeItem(projects.items, itemId);
 
         if (treeItem) {
-            const resourceUrl = getResourceUrl(treeItem.data);
+            const resourceUrl = getResourceUrl(treeItem.data.kind, treeItem.data.uuid);
 
             if (itemMode === ItemMode.ACTIVE || itemMode === ItemMode.BOTH) {
                 if (router.location && !router.location.pathname.includes(resourceUrl)) {
@@ -74,8 +75,7 @@ export const restoreBranch = (itemId: string) =>
         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));
+        dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(SidePanelId.PROJECTS));
         uuids.forEach(uuid => {
             dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(uuid));
         });
index cc5207b8d1b2af4b6bf6d8ebc02ad01ccb669e37..663add3e2eb68c9549aac05f334cd301a156535c 100644 (file)
@@ -9,12 +9,13 @@ import { DataColumns } from "~/components/data-table/data-table";
 import { ServiceRepository } from "~/services/services";
 import { ProjectPanelItem, resourceToDataItem } from "~/views/project-panel/project-panel-item";
 import { SortDirection } from "~/components/data-table/data-column";
-import { OrderBuilder } from "~/common/api/order-builder";
+import { OrderBuilder, OrderDirection } from "~/common/api/order-builder";
 import { FilterBuilder } from "~/common/api/filter-builder";
-import { GroupContentsResourcePrefix, GroupContentsResource } from "~/services/groups-service/groups-service";
+import { GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
 import { checkPresenceInFavorites } from "../favorites/favorites-actions";
 import { projectPanelActions } from "./project-panel-action";
 import { Dispatch, MiddlewareAPI } from "redux";
+import { ProjectResource } from "~/models/project";
 
 export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -25,71 +26,53 @@ export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService
         const state = api.getState();
         const dataExplorer = state.dataExplorer[this.getId()];
         const columns = dataExplorer.columns as DataColumns<ProjectPanelItem, ProjectPanelFilter>;
-        const typeFilters = getColumnFilters(columns, ProjectPanelColumnNames.TYPE);
-        const statusFilters = getColumnFilters(columns, ProjectPanelColumnNames.STATUS);
-        const sortColumn = dataExplorer.columns.find(({ sortDirection }) => Boolean(sortDirection && sortDirection !== "none"));
-        const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC ? SortDirection.ASC : SortDirection.DESC;
-        if (typeFilters.length > 0) {
-            this.services.groupsService
-                .contents(state.projects.currentItemId, {
-                    limit: dataExplorer.rowsPerPage,
-                    offset: dataExplorer.page * dataExplorer.rowsPerPage,
-                    order: sortColumn
-                        ? sortColumn.name === ProjectPanelColumnNames.NAME
-                            ? getOrder("name", sortDirection)
-                            : getOrder("createdAt", sortDirection)
-                        : OrderBuilder.create(),
-                    filters: FilterBuilder
-                        .create()
-                        .concat(FilterBuilder
-                            .create()
-                            .addIsA("uuid", typeFilters.map(f => f.type)))
-                        .concat(FilterBuilder
-                            .create(GroupContentsResourcePrefix.PROCESS)
-                            .addIn("state", statusFilters.map(f => f.type)))
-                        .concat(getSearchFilter(dataExplorer.searchValue))
-                })
-                .then(response => {
-                    api.dispatch(projectPanelActions.SET_ITEMS({
-                        items: response.items.map(resourceToDataItem),
-                        itemsAvailable: response.itemsAvailable,
-                        page: Math.floor(response.offset / response.limit),
-                        rowsPerPage: response.limit
-                    }));
-                    api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
-                });
-        } else {
-            api.dispatch(projectPanelActions.SET_ITEMS({
-                items: [],
-                itemsAvailable: 0,
-                page: 0,
-                rowsPerPage: dataExplorer.rowsPerPage
-            }));
-        }
-    }
-}
+        const typeFilters = this.getColumnFilters(columns, ProjectPanelColumnNames.TYPE);
+        const statusFilters = this.getColumnFilters(columns, ProjectPanelColumnNames.STATUS);
+        const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
 
-const getColumnFilters = (columns: DataColumns<ProjectPanelItem, ProjectPanelFilter>, columnName: string) => {
-    const column = columns.find(c => c.name === columnName);
-    return column && column.filters ? column.filters.filter(f => f.selected) : [];
-};
+        const order = new OrderBuilder<ProjectResource>();
 
-const getOrder = (attribute: "name" | "createdAt", direction: SortDirection) =>
-    [
-        OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION),
-        OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS),
-        OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT)
-    ].reduce((acc, b) =>
-        acc.concat(direction === SortDirection.ASC
-            ? b.addAsc(attribute)
-            : b.addDesc(attribute)), OrderBuilder.create());
+        if (sortColumn) {
+            const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+                ? OrderDirection.ASC
+                : OrderDirection.DESC;
 
-const getSearchFilter = (searchValue: string) =>
-    searchValue
-        ? [
-            FilterBuilder.create(GroupContentsResourcePrefix.COLLECTION),
-            FilterBuilder.create(GroupContentsResourcePrefix.PROCESS),
-            FilterBuilder.create(GroupContentsResourcePrefix.PROJECT)]
-            .reduce((acc, b) =>
-                acc.concat(b.addILike("name", searchValue)), FilterBuilder.create())
-        : FilterBuilder.create();
+            const columnName = sortColumn && sortColumn.name === ProjectPanelColumnNames.NAME ? "name" : "createdAt";
+            order
+                .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.COLLECTION)
+                .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROCESS)
+                .addOrder(sortDirection, columnName, GroupContentsResourcePrefix.PROJECT);
+        }
+
+        this.services.groupsService
+            .contents(state.projects.currentItemId, {
+                limit: dataExplorer.rowsPerPage,
+                offset: dataExplorer.page * dataExplorer.rowsPerPage,
+                order: order.getOrder(),
+                filters: new FilterBuilder()
+                    .addIsA("uuid", typeFilters.map(f => f.type))
+                    .addIn("state", statusFilters.map(f => f.type), GroupContentsResourcePrefix.PROCESS)
+                    .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
+                    .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
+                    .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
+                    .getFilters()
+            })
+            .then(response => {
+                api.dispatch(projectPanelActions.SET_ITEMS({
+                    items: response.items.map(resourceToDataItem),
+                    itemsAvailable: response.itemsAvailable,
+                    page: Math.floor(response.offset / response.limit),
+                    rowsPerPage: response.limit
+                }));
+                api.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
+            })
+            .catch(() => {
+                api.dispatch(projectPanelActions.SET_ITEMS({
+                    items: [],
+                    itemsAvailable: 0,
+                    page: 0,
+                    rowsPerPage: dataExplorer.rowsPerPage
+                }));
+            });
+    }
+}
index 96ad62853bf5c0b5be6157b62eebaee74b9cc2d0..2017658916cbfe7f5bc408c5eaaea4d547e6cc20 100644 (file)
@@ -37,9 +37,9 @@ export const getProjectList = (parentUuid: string = '') =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(projectActions.PROJECTS_REQUEST(parentUuid));
         return services.projectService.list({
-            filters: FilterBuilder
-                .create()
+            filters: new FilterBuilder()
                 .addEqual("ownerUuid", parentUuid)
+                .getFilters()
         }).then(({ items: projects }) => {
             dispatch(projectActions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
             dispatch<any>(checkPresenceInFavorites(projects.map(project => project.uuid)));
index 0dd6aad19e3ca0ee2e2311cb333a72a83a889176..ecea3535e35040fdbb8095830ea60e21bacff95e 100644 (file)
@@ -3,11 +3,10 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { default as unionize, ofType, UnionOf } from "unionize";
+import { SidePanelId } from "~/store/side-panel/side-panel-reducer";
 
 export const sidePanelActions = unionize({
-    TOGGLE_SIDE_PANEL_ITEM_OPEN: ofType<string>(),
-    TOGGLE_SIDE_PANEL_ITEM_ACTIVE: ofType<string>(),
-    RESET_SIDE_PANEL_ACTIVITY: ofType<{}>(),
+    TOGGLE_SIDE_PANEL_ITEM_OPEN: ofType<SidePanelId>()
 }, {
     tag: 'type',
     value: 'payload'
index 4872a72cf750486565eeaee5f8a36b94f4dc9233..a76e33a49b0508e52e2a8e1cc60cadb2f7d2e97d 100644 (file)
@@ -7,39 +7,14 @@ import { sidePanelActions } from "./side-panel-action";
 import { ProjectsIcon } from "~/components/icon/icon";
 
 describe('side-panel-reducer', () => {
-
-    it('should toggle activity on side-panel', () => {
-        const initialState = [
-            {
-                id: "1",
-                name: "Projects",
-                icon: ProjectsIcon,
-                open: false,
-                active: false,
-            }
-        ];
-        const project = [
-            {
-                id: "1",
-                name: "Projects",
-                icon: ProjectsIcon,
-                open: false,
-                active: true,
-            }
-        ];
-
-        const state = sidePanelReducer(initialState, sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(initialState[0].id));
-        expect(state).toEqual(project);
-    });
-
     it('should open side-panel item', () => {
         const initialState = [
             {
                 id: "1",
                 name: "Projects",
+                url: "/projects",
                 icon: ProjectsIcon,
-                open: false,
-                active: false,
+                open: false
             }
         ];
         const project = [
@@ -48,35 +23,11 @@ describe('side-panel-reducer', () => {
                 name: "Projects",
                 icon: ProjectsIcon,
                 open: true,
-                active: false,
+                url: "/projects"
             }
         ];
 
         const state = sidePanelReducer(initialState, sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_OPEN(initialState[0].id));
         expect(state).toEqual(project);
     });
-
-    it('should remove activity on side-panel item', () => {
-        const initialState = [
-            {
-                id: "1",
-                name: "Projects",
-                icon: ProjectsIcon,
-                open: false,
-                active: true,
-            }
-        ];
-        const project = [
-            {
-                id: "1",
-                name: "Projects",
-                icon: ProjectsIcon,
-                open: false,
-                active: false,
-            }
-        ];
-
-        const state = sidePanelReducer(initialState, sidePanelActions.RESET_SIDE_PANEL_ACTIVITY(initialState[0].id));
-        expect(state).toEqual(project);
-    });
 });
index 8c73a8f596b6c9a96d0359dd804e7838ce307827..db1cbe5de51a7133b0a26f00a4f427223872e5d9 100644 (file)
@@ -2,7 +2,6 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import * as _ from "lodash";
 import { sidePanelActions, SidePanelAction } from './side-panel-action';
 import { SidePanelItem } from '~/components/side-panel/side-panel';
 import { ProjectsIcon, ShareMeIcon, WorkflowIcon, RecentIcon, FavoriteIcon, TrashIcon } from "~/components/icon/icon";
@@ -12,37 +11,20 @@ import { favoritePanelActions } from "../favorite-panel/favorite-panel-action";
 import { projectPanelActions } from "../project-panel/project-panel-action";
 import { projectActions } from "../project/project-action";
 import { getProjectUrl } from "../../models/project";
+import { columns as projectPanelColumns } from "../../views/project-panel/project-panel";
+import { columns as favoritePanelColumns } from "../../views/favorite-panel/favorite-panel";
 
 export type SidePanelState = SidePanelItem[];
 
-export const sidePanelReducer = (state: SidePanelState = sidePanelData, action: SidePanelAction) => {
-    if (state.length === 0) {
-        return sidePanelData;
-    } else {
-        return sidePanelActions.match(action, {
-            TOGGLE_SIDE_PANEL_ITEM_OPEN: itemId =>
-                state.map(it => ({...it, open: itemId === it.id && it.open === false})),
-            TOGGLE_SIDE_PANEL_ITEM_ACTIVE: itemId => {
-                const sidePanel = _.cloneDeep(state);
-                resetSidePanelActivity(sidePanel);
-                sidePanel.forEach(it => {
-                    if (it.id === itemId) {
-                        it.active = true;
-                    }
-                });
-                return sidePanel;
-            },
-            RESET_SIDE_PANEL_ACTIVITY: () => {
-                const sidePanel = _.cloneDeep(state);
-                resetSidePanelActivity(sidePanel);
-                return sidePanel;
-            },
-            default: () => state
-        });
-    }
+export const sidePanelReducer = (state: SidePanelState = sidePanelItems, action: SidePanelAction) => {
+    return sidePanelActions.match(action, {
+        TOGGLE_SIDE_PANEL_ITEM_OPEN: itemId =>
+            state.map(it => ({...it, open: itemId === it.id && it.open === false})),
+        default: () => state
+    });
 };
 
-export enum SidePanelIdentifiers {
+export enum SidePanelId {
     PROJECTS = "Projects",
     SHARED_WITH_ME = "SharedWithMe",
     WORKFLOWS = "Workflows",
@@ -51,61 +33,75 @@ export enum SidePanelIdentifiers {
     TRASH = "Trash"
 }
 
-export const sidePanelData = [
+export const sidePanelItems = [
     {
-        id: SidePanelIdentifiers.PROJECTS,
+        id: SidePanelId.PROJECTS,
         name: "Projects",
+        url: "/projects",
         icon: ProjectsIcon,
         open: false,
         active: false,
         margin: true,
         openAble: true,
         activeAction: (dispatch: Dispatch, uuid: string) => {
-            dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(uuid));
             dispatch(push(getProjectUrl(uuid)));
+            dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(uuid));
+            dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
             dispatch(projectPanelActions.RESET_PAGINATION());
-            dispatch(projectPanelActions.REQUEST_ITEMS()); 
+            dispatch(projectPanelActions.REQUEST_ITEMS());
         }
     },
     {
-        id: SidePanelIdentifiers.SHARED_WITH_ME,
+        id: SidePanelId.SHARED_WITH_ME,
         name: "Shared with me",
+        url: "/shared",
         icon: ShareMeIcon,
         active: false,
+        activeAction: (dispatch: Dispatch) => {
+            dispatch(push("/shared"));
+        }
     },
     {
-        id: SidePanelIdentifiers.WORKFLOWS,
+        id: SidePanelId.WORKFLOWS,
         name: "Workflows",
+        url: "/workflows",
         icon: WorkflowIcon,
         active: false,
+        activeAction: (dispatch: Dispatch) => {
+            dispatch(push("/workflows"));
+        }
     },
     {
-        id: SidePanelIdentifiers.RECENT_OPEN,
+        id: SidePanelId.RECENT_OPEN,
         name: "Recent open",
+        url: "/recent",
         icon: RecentIcon,
         active: false,
+        activeAction: (dispatch: Dispatch) => {
+            dispatch(push("/recent"));
+        }
     },
     {
-        id: SidePanelIdentifiers.FAVORITES,
+        id: SidePanelId.FAVORITES,
         name: "Favorites",
+        url: "/favorites",
         icon: FavoriteIcon,
         active: false,
         activeAction: (dispatch: Dispatch) => {
             dispatch(push("/favorites"));
+            dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
             dispatch(favoritePanelActions.RESET_PAGINATION());
             dispatch(favoritePanelActions.REQUEST_ITEMS());
         }
     },
     {
-        id: SidePanelIdentifiers.TRASH,
+        id: SidePanelId.TRASH,
         name: "Trash",
+        url: "/trash",
         icon: TrashIcon,
         active: false,
+        activeAction: (dispatch: Dispatch) => {
+            dispatch(push("/trash"));
+        }
     }
 ];
-
-function resetSidePanelActivity(sidePanel: SidePanelItem[]) {
-    for (const t of sidePanel) {
-        t.active = false;
-    }
-}
index 0bed68e917739eb875fc4e015c556547659f6dd8..653da011f7ed03153cfd3a360c192bf0b36d146c 100644 (file)
@@ -3,8 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { ContextMenuActionSet } from "../context-menu-action-set";
-import { collectionPanelFilesAction } from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
-import { openMultipleFilesRemoveDialog } from "~/views-components/file-remove-dialog/multiple-files-remove-dialog";
+import { collectionPanelFilesAction, openMultipleFilesRemoveDialog } from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
 import { createCollectionWithSelected } from "~/views-components/create-collection-dialog-with-selected/create-collection-dialog-with-selected";
 
 
@@ -20,7 +19,7 @@ export const collectionFilesActionSet: ContextMenuActionSet = [[{
     }
 }, {
     name: "Remove selected",
-    execute: (dispatch, resource) => {
+    execute: (dispatch) => {
         dispatch(openMultipleFilesRemoveDialog());
     }
 }, {
index 8728ad31e19702674fb718711855bc959c1000c3..a3bfa0b95cb30736ae8c995fcfa00e3deb463b0f 100644 (file)
@@ -5,7 +5,8 @@
 import { ContextMenuActionSet } from "../context-menu-action-set";
 import { RenameIcon, DownloadIcon, RemoveIcon } from "~/components/icon/icon";
 import { openRenameFileDialog } from "../../rename-file-dialog/rename-file-dialog";
-import { openFileRemoveDialog } from "../../file-remove-dialog/file-remove-dialog";
+import { DownloadCollectionFileAction } from "../actions/download-collection-file-action";
+import { openFileRemoveDialog } from "../../../store/collection-panel/collection-panel-files/collection-panel-files-actions";
 
 
 export const collectionFilesItemActionSet: ContextMenuActionSet = [[{
@@ -15,15 +16,12 @@ export const collectionFilesItemActionSet: ContextMenuActionSet = [[{
         dispatch<any>(openRenameFileDialog(resource.name));
     }
 }, {
-    name: "Download",
-    icon: DownloadIcon,
-    execute: (dispatch, resource) => {
-        return;
-    }
+    component: DownloadCollectionFileAction,
+    execute: () => { return; }
 }, {
     name: "Remove",
     icon: RemoveIcon,
     execute: (dispatch, resource) => {
-        dispatch(openFileRemoveDialog(resource.uuid));
+        dispatch<any>(openFileRemoveDialog(resource.uuid));
     }
 }]];
diff --git a/src/views-components/context-menu/actions/download-action.tsx b/src/views-components/context-menu/actions/download-action.tsx
new file mode 100644 (file)
index 0000000..1f6979d
--- /dev/null
@@ -0,0 +1,29 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { ListItemIcon, ListItemText, Button, ListItem } from "@material-ui/core";
+import { DownloadIcon } from "../../../components/icon/icon";
+
+export const DownloadAction = (props: { href?: string, download?: string, onClick?: () => void }) => {
+    const targetProps = props.download ? {} : { target: '_blank' };
+    const downloadProps = props.download ? { download: props.download } : {};
+    return props.href
+        ? <a
+            style={{ textDecoration: 'none' }}
+            href={props.href}
+            onClick={props.onClick}
+            {...targetProps}
+            {...downloadProps}>
+            <ListItem button>
+                <ListItemIcon>
+                    <DownloadIcon />
+                </ListItemIcon>
+                <ListItemText>
+                    Download
+            </ListItemText>
+            </ListItem>
+        </a >
+        : null;
+};
\ No newline at end of file
diff --git a/src/views-components/context-menu/actions/download-collection-file-action.tsx b/src/views-components/context-menu/actions/download-collection-file-action.tsx
new file mode 100644 (file)
index 0000000..460e620
--- /dev/null
@@ -0,0 +1,25 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { RootState } from "../../../store/store";
+import { DownloadAction } from "./download-action";
+import { getNodeValue } from "../../../models/tree";
+import { CollectionFileType } from "../../../models/collection-file";
+
+const mapStateToProps = (state: RootState) => {
+    const { resource } = state.contextMenu;
+    if (resource) {
+        const file = getNodeValue(resource.uuid)(state.collectionPanelFiles);
+        if (file) {
+            return {
+                href: file.url,
+                download: file.type === CollectionFileType.DIRECTORY ? undefined : file.name
+            };
+        }
+    }
+    return {};
+};
+
+export const DownloadCollectionFileAction = connect(mapStateToProps)(DownloadAction);
index 21f037d9e5451fd3a0267ece7ddc1db6f9e3c8fc..1e817ba3a0bc884a31ad6c0f1d77366ccefe1bcc 100644 (file)
@@ -3,25 +3,28 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from "react";
-import { ListItemIcon, ListItemText } from "@material-ui/core";
+import { ListItemIcon, ListItemText, ListItem } from "@material-ui/core";
 import { AddFavoriteIcon, RemoveFavoriteIcon } from "~/components/icon/icon";
 import { connect } from "react-redux";
 import { RootState } from "~/store/store";
 
-const mapStateToProps = (state: RootState) => ({
-    isFavorite: state.contextMenu.resource !== undefined && state.favorites[state.contextMenu.resource.uuid] === true
+const mapStateToProps = (state: RootState, props: { onClick: () => {} }) => ({
+    isFavorite: state.contextMenu.resource !== undefined && state.favorites[state.contextMenu.resource.uuid] === true,
+    onClick: props.onClick
 });
 
-export const ToggleFavoriteAction = connect(mapStateToProps)((props: { isFavorite: boolean }) =>
-    <>
+export const ToggleFavoriteAction = connect(mapStateToProps)((props: { isFavorite: boolean, onClick: () => void }) =>
+    <ListItem
+        button
+        onClick={props.onClick}>
         <ListItemIcon>
             {props.isFavorite
                 ? <RemoveFavoriteIcon />
                 : <AddFavoriteIcon />}
         </ListItemIcon>
-        <ListItemText>
+        <ListItemText style={{ textDecoration: 'none' }}>
             {props.isFavorite
                 ? <>Remove from favorites</>
                 : <>Add to favorites</>}
         </ListItemText>
-    </>);
+    </ListItem >);
index 68eeb3c144210dbbb247e94f634014d8d3726a98..d548f607f550637cd5a120fc358c620d913bd29b 100644 (file)
@@ -21,48 +21,50 @@ interface Props {
     extractKey?: (item: any) => React.Key;
 }
 
-const mapStateToProps = (state: RootState, { id }: Props) =>
-    getDataExplorer(state.dataExplorer, id);
+const mapStateToProps = (state: RootState, { id, columns }: Props) => {
+    const s = getDataExplorer(state.dataExplorer, id);
+    if (s.columns.length === 0) {
+        s.columns = columns;
+    }
+    return s;
+};
 
 const mapDispatchToProps = () => {
-    let prevColumns: DataColumns<any>;
-    return (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => {
-        if (columns !== prevColumns) {
-            prevColumns = columns;
+    return (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
+        onSetColumns: (columns: DataColumns<any>) => {
             dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
-        }
-        return {
-            onSearch: (searchValue: string) => {
-                dispatch(dataExplorerActions.SET_SEARCH_VALUE({ id, searchValue }));
-            },
+        },
+
+        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);
index 04497933c87effdbe6b81ba540bd4adf7ae90f52..c83181c2823bd502b0b4209f1a285f9a3319528f 100644 (file)
@@ -5,34 +5,29 @@
 import { Dispatch } from "redux";
 import { connect } from "react-redux";
 import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog";
-import { withDialog } from "~/store/dialog/with-dialog";
-import { dialogActions } from "~/store/dialog/dialog-actions";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { withDialog, WithDialogProps } from '~/store/dialog/with-dialog';
+import { RootState } from '~/store/store';
+import { removeCollectionFiles, FILE_REMOVE_DIALOG } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
 
-const FILE_REMOVE_DIALOG = 'fileRemoveDialog';
+const mapStateToProps = (state: RootState, props: WithDialogProps<{ filePath: string }>) => ({
+    filePath: props.data.filePath
+});
 
-const mapDispatchToProps = (dispatch: Dispatch) => ({
-    onConfirm: () => {
-        // TODO: dispatch action that removes single file
-        dispatch(dialogActions.CLOSE_DIALOG({ id: FILE_REMOVE_DIALOG }));
-        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing file...', hideDuration: 2000 }));
-        setTimeout(() => {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'File removed.', hideDuration: 2000 }));
-        }, 1000);
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<{ filePath: string }>) => ({
+    onConfirm: (filePath: string) => {
+        props.closeDialog();
+        dispatch<any>(removeCollectionFiles([filePath]));
     }
 });
 
-export const openFileRemoveDialog = (fileId: string) =>
-    dialogActions.OPEN_DIALOG({
-        id: FILE_REMOVE_DIALOG,
-        data: {
-            title: 'Removing file',
-            text: 'Are you sure you want to remove this file?',
-            confirmButtonLabel: 'Remove',
-            fileId
-        }
+const mergeProps = (
+    stateProps: { filePath: string },
+    dispatchProps: { onConfirm: (filePath: string) => void },
+    props: WithDialogProps<{ filePath: string }>) => ({
+        onConfirm: () => dispatchProps.onConfirm(stateProps.filePath),
+        ...props
     });
 
 export const [FileRemoveDialog] = [ConfirmationDialog]
-    .map(withDialog(FILE_REMOVE_DIALOG))
-    .map(connect(undefined, mapDispatchToProps));
+    .map(connect(mapStateToProps, mapDispatchToProps, mergeProps))
+    .map(withDialog(FILE_REMOVE_DIALOG));
index 1362de6b8ac424c479c460674cb1eb43fe5996a7..03dae1dd8bc719b3b5062f0eec22aee855c150b3 100644 (file)
@@ -4,34 +4,17 @@
 
 import { Dispatch } from "redux";
 import { connect } from "react-redux";
+import { MULTIPLE_FILES_REMOVE_DIALOG, removeCollectionsSelectedFiles } from "../../store/collection-panel/collection-panel-files/collection-panel-files-actions";
 import { ConfirmationDialog } from "~/components/confirmation-dialog/confirmation-dialog";
-import { withDialog } from "~/store/dialog/with-dialog";
-import { dialogActions } from "~/store/dialog/dialog-actions";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { withDialog, WithDialogProps } from "~/store/dialog/with-dialog";
 
-const MULTIPLE_FILES_REMOVE_DIALOG = 'multipleFilesRemoveDialog';
-
-const mapDispatchToProps = (dispatch: Dispatch) => ({
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
     onConfirm: () => {
-        // TODO: dispatch action that removes multiple files
-        dispatch(dialogActions.CLOSE_DIALOG({ id: MULTIPLE_FILES_REMOVE_DIALOG }));
-        dispatch(snackbarActions.OPEN_SNACKBAR({message: 'Removing files...', hideDuration: 2000}));
-        setTimeout(() => {
-            dispatch(snackbarActions.OPEN_SNACKBAR({message: 'Files removed.', hideDuration: 2000}));
-        }, 1000);
+        props.closeDialog();
+        dispatch<any>(removeCollectionsSelectedFiles());
     }
 });
 
-export const openMultipleFilesRemoveDialog = () =>
-    dialogActions.OPEN_DIALOG({
-        id: MULTIPLE_FILES_REMOVE_DIALOG,
-        data: {
-            title: 'Removing files',
-            text: 'Are you sure you want to remove selected files?',
-            confirmButtonLabel: 'Remove'
-        }
-    });
-
 export const [MultipleFilesRemoveDialog] = [ConfirmationDialog]
-    .map(withDialog(MULTIPLE_FILES_REMOVE_DIALOG))
-    .map(connect(undefined, mapDispatchToProps));
+    .map(connect(undefined, mapDispatchToProps))
+    .map(withDialog(MULTIPLE_FILES_REMOVE_DIALOG));
index 8bce325425f7c52a07cbbb958c8f316b48cdb29f..54d6a5da0ec8c5306734ba27861a5288fd21fd89 100644 (file)
@@ -26,6 +26,7 @@ interface MainAppBarDataProps {
     breadcrumbs: Breadcrumb[];
     user?: User;
     menuItems: MainAppBarMenuItems;
+    buildInfo: string;
 }
 
 export interface MainAppBarActionProps {
@@ -44,10 +45,10 @@ export const MainAppBar: React.SFC<MainAppBarProps> = (props) => {
             <Grid container justify="space-between">
                 <Grid item xs={3}>
                     <Typography variant="headline" color="inherit" noWrap>
-                        Arvados
+                        Arvados 2
                     </Typography>
                     <Typography variant="body1" color="inherit" noWrap >
-                        Workbench 2
+                        {props.buildInfo}
                     </Typography>
                 </Grid>
                 <Grid item xs={6} container alignItems="center">
index e09d78abf67098adfc917e296f31a521ae6e4e56..9143c47a2da5efe25b4d983015946706a8487c7f 100644 (file)
@@ -48,9 +48,9 @@ export const loadProjectTreePickerProjects = (id: string) =>
 
         const ownerUuid = id.length === 0 ? services.authService.getUuid() || '' : id;
 
-        const filters = FilterBuilder
-            .create()
-            .addEqual('ownerUuid', ownerUuid);
+        const filters = new FilterBuilder()
+            .addEqual('ownerUuid', ownerUuid)
+            .getFilters();
 
         const { items } = await services.projectService.list({ filters });
 
index 618f0aba426b1ec6d44c00189437414ef5a2e901..125ea27ddf1635836bdd592091025d637efd028a 100644 (file)
@@ -51,6 +51,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
         selected: true,
         configurable: true,
         sortDirection: SortDirection.ASC,
+        filters: [],
         render: renderName,
         width: "450px"
     },
@@ -58,6 +59,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
         name: "Status",
         selected: true,
         configurable: true,
+        sortDirection: SortDirection.NONE,
         filters: [
             {
                 name: ContainerRequestState.COMMITTED,
@@ -82,6 +84,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
         name: FavoritePanelColumnNames.TYPE,
         selected: true,
         configurable: true,
+        sortDirection: SortDirection.NONE,
         filters: [
             {
                 name: resourceLabel(ResourceKind.COLLECTION),
@@ -106,6 +109,8 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
         name: FavoritePanelColumnNames.OWNER,
         selected: true,
         configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: [],
         render: item => renderOwner(item.owner),
         width: "200px"
     },
@@ -113,6 +118,8 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
         name: FavoritePanelColumnNames.FILE_SIZE,
         selected: true,
         configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: [],
         render: item => renderFileSize(item.fileSize),
         width: "50px"
     },
@@ -121,6 +128,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
+        filters: [],
         render: item => renderDate(item.lastModified),
         width: "150px"
     }
@@ -151,7 +159,7 @@ export const FavoritePanel = withStyles(styles)(
                     onRowClick={this.props.onItemClick}
                     onRowDoubleClick={this.props.onItemDoubleClick}
                     onContextMenu={this.props.onContextMenu}
-                    extractKey={(item: FavoritePanelItem) => item.uuid} 
+                    extractKey={(item: FavoritePanelItem) => item.uuid}
                     defaultIcon={FavoriteIcon}
                     defaultMessages={['Your favorites list is empty.']}/>
                 ;
index e9179a118ddc759de11d4fccba3109c6309fc600..0f958d2cfbbb87684c47c1d77a16319f3e494e28 100644 (file)
@@ -56,6 +56,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
         selected: true,
         configurable: true,
         sortDirection: SortDirection.ASC,
+        filters: [],
         render: renderName,
         width: "450px"
     },
@@ -63,6 +64,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
         name: "Status",
         selected: true,
         configurable: true,
+        sortDirection: SortDirection.NONE,
         filters: [
             {
                 name: ContainerRequestState.COMMITTED,
@@ -87,6 +89,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
         name: ProjectPanelColumnNames.TYPE,
         selected: true,
         configurable: true,
+        sortDirection: SortDirection.NONE,
         filters: [
             {
                 name: resourceLabel(ResourceKind.COLLECTION),
@@ -111,6 +114,8 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
         name: ProjectPanelColumnNames.OWNER,
         selected: true,
         configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: [],
         render: item => renderOwner(item.owner),
         width: "200px"
     },
@@ -118,6 +123,8 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
         name: ProjectPanelColumnNames.FILE_SIZE,
         selected: true,
         configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: [],
         render: item => renderFileSize(item.fileSize),
         width: "50px"
     },
@@ -126,6 +133,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
+        filters: [],
         render: item => renderDate(item.lastModified),
         width: "150px"
     }
index a38afb7ac30e32b8f06f265a1635693aead54ca9..a2d61d5cd17a209351fa5bf3f228479df5087092 100644 (file)
@@ -29,7 +29,6 @@ import { CreateProjectDialog } from "~/views-components/create-project-dialog/cr
 
 import { detailsPanelActions, loadDetails } from "~/store/details-panel/details-panel-action";
 import { contextMenuActions } from "~/store/context-menu/context-menu-actions";
-import { SidePanelIdentifiers } from '~/store/side-panel/side-panel-reducer';
 import { ProjectResource } from '~/models/project';
 import { ResourceKind } from '~/models/resource';
 import { ContextMenu, ContextMenuKind } from "~/views-components/context-menu/context-menu";
@@ -101,14 +100,15 @@ interface WorkbenchDataProps {
     sidePanelItems: SidePanelItem[];
 }
 
-interface WorkbenchServiceProps {
+interface WorkbenchGeneralProps {
     authService: AuthService;
+    buildInfo: string;
 }
 
 interface WorkbenchActionProps {
 }
 
-type WorkbenchProps = WorkbenchDataProps & WorkbenchServiceProps & WorkbenchActionProps & DispatchProp<any> & WithStyles<CssRules>;
+type WorkbenchProps = WorkbenchDataProps & WorkbenchGeneralProps & WorkbenchActionProps & DispatchProp<any> & WithStyles<CssRules>;
 
 interface NavBreadcrumb extends Breadcrumb {
     itemId: string;
@@ -193,6 +193,7 @@ export const Workbench = withStyles(styles)(
                                 searchText={this.state.searchText}
                                 user={this.props.user}
                                 menuItems={this.state.menuItems}
+                                buildInfo={this.props.buildInfo}
                                 {...this.mainAppBarActions} />
                         </div>
                         {user &&
@@ -222,7 +223,6 @@ export const Workbench = withStyles(styles)(
                                         toggleActive={itemId => {
                                             this.props.dispatch(setProjectItem(itemId, ItemMode.ACTIVE));
                                             this.props.dispatch(loadDetails(itemId, ResourceKind.PROJECT));
-                                            this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
                                         }} />
                                 </SidePanel>
                             </Drawer>}
@@ -257,7 +257,7 @@ export const Workbench = withStyles(styles)(
 
             renderCollectionPanel = (props: RouteComponentProps<{ id: string }>) => <CollectionPanel
                 onItemRouteChange={(collectionId) => {
-                    this.props.dispatch<any>(loadCollection(collectionId, ResourceKind.COLLECTION));
+                    this.props.dispatch<any>(loadCollection(collectionId));
                     this.props.dispatch<any>(loadCollectionTags(collectionId));
                 }}
                 onContextMenu={(event, item) => {
@@ -298,7 +298,7 @@ export const Workbench = withStyles(styles)(
                 onItemDoubleClick={item => {
                     switch (item.kind) {
                         case ResourceKind.COLLECTION:
-                            this.props.dispatch(loadCollection(item.uuid, item.kind as ResourceKind));
+                            this.props.dispatch(loadCollection(item.uuid));
                             this.props.dispatch(push(getCollectionUrl(item.uuid)));
                         default:
                             this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
@@ -325,12 +325,11 @@ export const Workbench = withStyles(styles)(
                 onItemDoubleClick={item => {
                     switch (item.kind) {
                         case ResourceKind.COLLECTION:
-                            this.props.dispatch(loadCollection(item.uuid, item.kind as ResourceKind));
+                            this.props.dispatch(loadCollection(item.uuid));
                             this.props.dispatch(push(getCollectionUrl(item.uuid)));
                         default:
                             this.props.dispatch(loadDetails(item.uuid, ResourceKind.PROJECT));
                             this.props.dispatch(setProjectItem(item.uuid, ItemMode.ACTIVE));
-                            this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(SidePanelIdentifiers.PROJECTS));
                     }
 
                 }}
@@ -363,7 +362,6 @@ export const Workbench = withStyles(styles)(
             }
 
             toggleSidePanelActive = (itemId: string) => {
-                this.props.dispatch(sidePanelActions.TOGGLE_SIDE_PANEL_ITEM_ACTIVE(itemId));
                 this.props.dispatch(projectActions.RESET_PROJECT_TREE_ACTIVITY(itemId));
 
                 const panelItem = this.props.sidePanelItems.find(it => it.id === itemId);
index ae2b1c571627921abf4fc87cee07613423376134..67c12647b4625337eae116a1f6feea7f9322da8e 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
     esutils "^2.0.2"
     js-tokens "^3.0.0"
 
-"@babel/runtime@^7.0.0-beta.42":
-  version "7.0.0-beta.54"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.54.tgz#39ebb42723fe7ca4b3e1b00e967e80138d47cadf"
+"@babel/runtime@7.0.0-beta.42":
+  version "7.0.0-beta.42"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.42.tgz#352e40c92e0460d3e82f49bd7e79f6cda76f919f"
+  dependencies:
+    core-js "^2.5.3"
+    regenerator-runtime "^0.11.1"
+
+"@babel/runtime@7.0.0-beta.56":
+  version "7.0.0-beta.56"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.56.tgz#cda612dffd5b1719a7b8e91e3040bd6ae64de8b0"
   dependencies:
-    core-js "^2.5.7"
     regenerator-runtime "^0.12.0"
 
-"@material-ui/core@1.4.2":
-  version "1.4.2"
-  resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.4.2.tgz#8a1282e985d4922a4d2b4f7e287d8a716a2fc108"
+"@material-ui/core@1.5.0":
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.5.0.tgz#00884bb4139d98786d05a97803d19426d4afa55d"
   dependencies:
-    "@babel/runtime" "^7.0.0-beta.42"
+    "@babel/runtime" "7.0.0-beta.42"
     "@types/jss" "^9.5.3"
     "@types/react-transition-group" "^2.0.8"
     brcast "^3.0.1"
     normalize-scroll-left "^0.1.2"
     popper.js "^1.14.1"
     prop-types "^15.6.0"
-    react-event-listener "^0.6.0"
+    react-event-listener "^0.6.2"
     react-jss "^8.1.0"
     react-transition-group "^2.2.1"
-    recompose "^0.27.0"
+    recompose "^0.28.0"
     warning "^4.0.1"
 
-"@material-ui/icons@2.0.0":
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-2.0.0.tgz#f2c4e80d0cb4bbbd433127781da67d93393535f8"
+"@material-ui/icons@2.0.2":
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-2.0.2.tgz#0150c38cda089ef284e9b4a730dfe6e88a0b5de6"
   dependencies:
-    "@babel/runtime" "^7.0.0-beta.42"
-    recompose "^0.27.0"
+    "@babel/runtime" "7.0.0-beta.42"
+    recompose "^0.28.0"
 
 "@types/cheerio@*":
   version "0.22.8"
   version "2.2.6"
   resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.6.tgz#dbe8a666156d556ed018e15a4c65f08937c3f628"
 
-"@types/enzyme-adapter-react-16@1.0.2":
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.2.tgz#15ae37c64d6221a6f4b3a4aacc357cf773859de4"
+"@types/enzyme-adapter-react-16@1.0.3":
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.3.tgz#0cf7025b036694ca8d596fe38f24162e7117acf1"
   dependencies:
     "@types/enzyme" "*"
 
-"@types/enzyme@*", "@types/enzyme@3.1.12":
+"@types/enzyme@*":
   version "3.1.12"
   resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.12.tgz#293bb07c1ef5932d37add3879e72e0f5bc614f3c"
   dependencies:
     "@types/cheerio" "*"
     "@types/react" "*"
 
+"@types/enzyme@3.1.13":
+  version "3.1.13"
+  resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.1.13.tgz#4bbc5c81fa40c9fc7efee25c4a23cb37119a33ea"
+  dependencies:
+    "@types/cheerio" "*"
+    "@types/react" "*"
+
 "@types/history@*":
   version "4.6.2"
   resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0"
   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/node@10.7.1":
+  version "10.7.1"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-10.7.1.tgz#b704d7c259aa40ee052eec678758a68d07132a2e"
 
-"@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"
+"@types/react-copy-to-clipboard@4.2.6":
+  version "4.2.6"
+  resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-4.2.6.tgz#d1374550dec803f17f26ec71b62783c5737bfc02"
   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/react-dom@16.0.7":
+  version "16.0.7"
+  resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.7.tgz#54d0f867a76b90597e8432030d297982f25c20ba"
   dependencies:
     "@types/node" "*"
     "@types/react" "*"
 
-"@types/react-dropzone@4.2.1":
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/@types/react-dropzone/-/react-dropzone-4.2.1.tgz#4a973b63a8a227e263ff4eece053f643220f28fc"
+"@types/react-dropzone@4.2.2":
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/@types/react-dropzone/-/react-dropzone-4.2.2.tgz#af0a2595169700c8ab1114e9096285499beaff40"
   dependencies:
     "@types/react" "*"
 
     "@types/react" "*"
     redux "^3.6.0"
 
-"@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"
+"@types/redux-form@7.4.5":
+  version "7.4.5"
+  resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-7.4.5.tgz#fae0fa6cfbc613867093d1e0f6a84db17177305e"
   dependencies:
     "@types/react" "*"
     redux "^3.6.0 || ^4.0.0"
@@ -407,6 +420,14 @@ array-unique@^0.3.2:
   version "0.3.2"
   resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
 
+array.prototype.flat@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz#812db8f02cad24d3fab65dd67eabe3b8903494a4"
+  dependencies:
+    define-properties "^1.1.2"
+    es-abstract "^1.10.0"
+    function-bind "^1.1.1"
+
 arrify@^1.0.0, arrify@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
@@ -471,7 +492,7 @@ atob@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.1.tgz#ae2d5a729477f289d60dd7f96a6314a22dd6c22a"
 
-attr-accept@^1.0.3:
+attr-accept@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-1.1.3.tgz#48230c79f93790ef2775fcec4f0db0f5db41ca52"
   dependencies:
@@ -1849,7 +1870,7 @@ core-js@^1.0.0:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
 
-core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.7:
+core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.3:
   version "2.5.7"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
 
@@ -2440,39 +2461,42 @@ entities@^1.1.1, entities@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
 
-enzyme-adapter-react-16@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.1.1.tgz#a8f4278b47e082fbca14f5bfb1ee50ee650717b4"
+enzyme-adapter-react-16@1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.2.0.tgz#c6e80f334e0a817873262d7d01ee9e4747e3c97e"
   dependencies:
-    enzyme-adapter-utils "^1.3.0"
-    lodash "^4.17.4"
-    object.assign "^4.0.4"
+    enzyme-adapter-utils "^1.5.0"
+    function.prototype.name "^1.1.0"
+    object.assign "^4.1.0"
     object.values "^1.0.4"
-    prop-types "^15.6.0"
+    prop-types "^15.6.2"
+    react-is "^16.4.2"
     react-reconciler "^0.7.0"
     react-test-renderer "^16.0.0-0"
 
-enzyme-adapter-utils@^1.3.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.4.0.tgz#c403b81e8eb9953658569e539780964bdc98de62"
+enzyme-adapter-utils@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.5.0.tgz#a020ab3ae79bb1c85e1d51f48f35e995e0eed810"
   dependencies:
+    function.prototype.name "^1.1.0"
     object.assign "^4.1.0"
-    prop-types "^15.6.0"
+    prop-types "^15.6.2"
 
-enzyme@3.3.0:
-  version "3.3.0"
-  resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.3.0.tgz#0971abd167f2d4bf3f5bd508229e1c4b6dc50479"
+enzyme@3.4.4:
+  version "3.4.4"
+  resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.4.4.tgz#92c7c6b9e59d4ef0c3d36a75dccc0e41a5c14d21"
   dependencies:
+    array.prototype.flat "^1.2.1"
     cheerio "^1.0.0-rc.2"
-    function.prototype.name "^1.0.3"
-    has "^1.0.1"
+    function.prototype.name "^1.1.0"
+    has "^1.0.3"
     is-boolean-object "^1.0.0"
-    is-callable "^1.1.3"
+    is-callable "^1.1.4"
     is-number-object "^1.0.3"
     is-string "^1.0.4"
     is-subset "^0.1.1"
     lodash "^4.17.4"
-    object-inspect "^1.5.0"
+    object-inspect "^1.6.0"
     object-is "^1.0.1"
     object.assign "^4.1.0"
     object.entries "^1.0.4"
@@ -2492,7 +2516,7 @@ error-ex@^1.2.0:
   dependencies:
     is-arrayish "^0.2.1"
 
-es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
+es-abstract@^1.10.0, es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
   version "1.12.0"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165"
   dependencies:
@@ -3082,7 +3106,7 @@ function-bind@^1.1.0, function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
 
-function.prototype.name@^1.0.3:
+function.prototype.name@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.0.tgz#8bd763cc0af860a859cc5d49384d74b932cd2327"
   dependencies:
@@ -3319,7 +3343,7 @@ has-values@^1.0.0:
     is-number "^3.0.0"
     kind-of "^4.0.0"
 
-has@^1.0.1:
+has@^1.0.1, has@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
   dependencies:
@@ -3689,7 +3713,7 @@ is-builtin-module@^1.0.0:
   dependencies:
     builtin-modules "^1.0.0"
 
-is-callable@^1.1.1, is-callable@^1.1.3:
+is-callable@^1.1.1, is-callable@^1.1.3, is-callable@^1.1.4:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
 
@@ -5230,7 +5254,7 @@ object-copy@^0.1.0:
     define-property "^0.2.5"
     kind-of "^3.0.3"
 
-object-inspect@^1.5.0:
+object-inspect@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b"
 
@@ -5248,7 +5272,7 @@ object-visit@^1.0.0:
   dependencies:
     isobject "^3.0.0"
 
-object.assign@^4.0.4, object.assign@^4.1.0:
+object.assign@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
   dependencies:
@@ -6138,22 +6162,22 @@ react-dom@16.4.2:
     object-assign "^4.1.1"
     prop-types "^15.6.0"
 
-react-dropzone@4.2.13:
-  version "4.2.13"
-  resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-4.2.13.tgz#31393c079b4e5ddcc176c095cebc3545d1248b9d"
+react-dropzone@5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-5.0.1.tgz#3ed201215794c0f650c6f25a8311a9d96d35ebb6"
   dependencies:
-    attr-accept "^1.0.3"
+    attr-accept "^1.1.3"
     prop-types "^15.5.7"
 
 react-error-overlay@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4"
 
-react-event-listener@^0.6.0:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.1.tgz#41c7a80a66b398c27dd511e22712b02f3d4eccca"
+react-event-listener@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.6.2.tgz#df405e9578be052b77a76e4c3914686637caecff"
   dependencies:
-    "@babel/runtime" "^7.0.0-beta.42"
+    "@babel/runtime" "7.0.0-beta.42"
     prop-types "^15.6.0"
     warning "^4.0.1"
 
@@ -6161,6 +6185,10 @@ react-is@^16.4.1:
   version "16.4.1"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e"
 
+react-is@^16.4.2:
+  version "16.4.2"
+  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.2.tgz#84891b56c2b6d9efdee577cc83501dfc5ecead88"
+
 react-jss@^8.1.0:
   version "8.6.1"
   resolved "https://registry.yarnpkg.com/react-jss/-/react-jss-8.6.1.tgz#a06e2e1d2c4d91b4d11befda865e6c07fbd75252"
@@ -6363,11 +6391,11 @@ realpath-native@^1.0.0:
   dependencies:
     util.promisify "^1.0.0"
 
-recompose@^0.27.0:
-  version "0.27.1"
-  resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.27.1.tgz#1a49e931f183634516633bbb4f4edbfd3f38a7ba"
+recompose@^0.28.0:
+  version "0.28.2"
+  resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.28.2.tgz#19e679227bdf979e0d31b73ffe7ae38c9194f4a7"
   dependencies:
-    babel-runtime "^6.26.0"
+    "@babel/runtime" "7.0.0-beta.56"
     change-emitter "^0.1.2"
     fbjs "^0.8.1"
     hoist-non-react-statics "^2.3.1"
@@ -6453,7 +6481,7 @@ regenerate@^1.2.1:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
 
-regenerator-runtime@^0.11.0:
+regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1:
   version "0.11.1"
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"