Merge branch 'master' into 13990-collection-files-service-based-on-webdav
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Fri, 17 Aug 2018 13:47:30 +0000 (15:47 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Fri, 17 Aug 2018 13:47:30 +0000 (15:47 +0200)
refs #13990

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

41 files changed:
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/components/data-explorer/data-explorer.tsx
src/components/data-table/data-column.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/tag-service/tag-service.ts
src/store/collections/updater/collection-updater-action.ts
src/store/data-explorer/data-explorer-middleware-service.ts
src/store/data-explorer/data-explorer-middleware.test.ts
src/store/details-panel/details-panel-action.ts
src/store/details-panel/details-panel-reducer.ts
src/store/favorite-panel/favorite-panel-middleware-service.ts
src/store/project-panel/project-panel-middleware-service.ts
src/store/project/project-action.ts
src/store/project/project-reducer.test.ts
src/store/project/project-reducer.ts
src/validators/create-collection/create-collection-validator.tsx [deleted file]
src/validators/create-project/create-project-validator.tsx [deleted file]
src/validators/validators.tsx
src/views-components/context-menu/action-sets/project-action-set.ts
src/views-components/dialog-create/dialog-collection-create-selected.tsx
src/views-components/dialog-create/dialog-collection-create.tsx
src/views-components/dialog-create/dialog-project-create.tsx
src/views-components/dialog-update/dialog-collection-update.tsx
src/views-components/dialog-update/dialog-project-update.tsx [new file with mode: 0644]
src/views-components/project-tree-picker/project-tree-picker.tsx
src/views-components/update-project-dialog/update-project-dialog.tsx [new file with mode: 0644]
src/views/collection-panel/collection-panel.tsx
src/views/collection-panel/collection-tag-form.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/project-panel/project-panel.tsx
src/views/workbench/workbench.tsx

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(",");
     }
 }
index 2811bd4d1f7e7fada857e6677e7f50c1c576fba6..681caa9478c15d194c0ab1f904a59369b015089a 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";
@@ -68,10 +68,10 @@ type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T
 export const DataExplorer = withStyles(styles)(
     class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
         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 +111,7 @@ export const DataExplorer = withStyles(styles)(
                         </Toolbar>
                     </Paper>
                 ) : (
-                    <DefaultView 
+                    <DefaultView
                         classRoot={classes.defaultRoot}
                         icon={defaultIcon}
                         classIcon={classes.defaultIcon}
@@ -140,6 +140,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 d07ef216ac0c51b7e14dbbccff967642e6c3db22..9feec699e52dfd07070105c75c251d96b3107541 100644 (file)
@@ -142,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 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 25b2f37ac1f72ecaca4172a8a173553841253abe..2f520d4a384f84b63982bfa12fd13dd8ece81e9b 100644 (file)
@@ -11,6 +11,7 @@ import { CollectionResource } from '~/models/collection';
 import { initialize } from 'redux-form';
 import { collectionPanelActions } from "../../collection-panel/collection-panel-action";
 import { ContextMenuResource } from "../../context-menu/context-menu-reducer";
+import { updateDetails } from "~/store/details-panel/details-panel-action";
 
 export const collectionUpdaterActions = unionize({
     OPEN_COLLECTION_UPDATER: ofType<{ uuid: string }>(),
@@ -40,6 +41,7 @@ export const updateCollection = (collection: Partial<CollectionResource>) =>
             .then(collection => {
                     dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: collection as CollectionResource }));
                     dispatch(collectionUpdaterActions.UPDATE_COLLECTION_SUCCESS());
+                    dispatch<any>(updateDetails(collection));
                 }
             );
     };
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 cadf517ac2376f50f9b643ac0cc5f0519371caa4..b8021fb6a0d81d12588b2efe921a0d3142c7df6c 100644 (file)
@@ -12,6 +12,7 @@ export const detailsPanelActions = unionize({
     TOGGLE_DETAILS_PANEL: ofType<{}>(),
     LOAD_DETAILS: ofType<{ uuid: string, kind: ResourceKind }>(),
     LOAD_DETAILS_SUCCESS: ofType<{ item: Resource }>(),
+    UPDATE_DETAILS: ofType<{ item: Resource }>()
 }, { tag: 'type', value: 'payload' });
 
 export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
@@ -23,6 +24,16 @@ export const loadDetails = (uuid: string, kind: ResourceKind) =>
         dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item }));
     };
 
+export const updateDetails = (item: Resource) => 
+    async (dispatch: Dispatch, getState: () => RootState) => {
+        const currentItem = getState().detailsPanel.item;
+        if (currentItem && (currentItem.uuid === item.uuid)) {
+            dispatch(detailsPanelActions.UPDATE_DETAILS({ item }));
+            dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item }));
+        }
+    };
+
+
 const getService = (services: ServiceRepository, kind: ResourceKind) => {
     switch (kind) {
         case ResourceKind.PROJECT:
index adc31e4bd6f60f55eff6e6f45ea6cae0ac6b275f..f22add3d49b08810a2c5cf248ca4f90503dc0a1d 100644 (file)
@@ -18,7 +18,6 @@ const initialState = {
 export const detailsPanelReducer = (state: DetailsPanelState = initialState, action: DetailsPanelAction) =>
     detailsPanelActions.match(action, {
         default: () => state,
-        LOAD_DETAILS: () => state,
         LOAD_DETAILS_SUCCESS: ({ item }) => ({ ...state, item }),
         TOGGLE_DETAILS_PANEL: () => ({ ...state, isOpened: !state.isOpened })
     });
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 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 bef50d1197f44939f9e547c577bb0e2f797e6647..2017658916cbfe7f5bc408c5eaaea4d547e6cc20 100644 (file)
@@ -9,12 +9,17 @@ import { FilterBuilder } from "~/common/api/filter-builder";
 import { RootState } from "../store";
 import { checkPresenceInFavorites } from "../favorites/favorites-actions";
 import { ServiceRepository } from "~/services/services";
+import { projectPanelActions } from "~/store/project-panel/project-panel-action";
+import { updateDetails } from "~/store/details-panel/details-panel-action";
 
 export const projectActions = unionize({
     OPEN_PROJECT_CREATOR: ofType<{ ownerUuid: string }>(),
     CLOSE_PROJECT_CREATOR: ofType<{}>(),
     CREATE_PROJECT: ofType<Partial<ProjectResource>>(),
     CREATE_PROJECT_SUCCESS: ofType<ProjectResource>(),
+    OPEN_PROJECT_UPDATER: ofType<{ uuid: string}>(),
+    CLOSE_PROJECT_UPDATER: ofType<{}>(),
+    UPDATE_PROJECT_SUCCESS: ofType<ProjectResource>(),
     REMOVE_PROJECT: ofType<string>(),
     PROJECTS_REQUEST: ofType<string>(),
     PROJECTS_SUCCESS: ofType<{ projects: ProjectResource[], parentItemId?: string }>(),
@@ -26,13 +31,15 @@ export const projectActions = unionize({
     value: 'payload'
 });
 
+export const PROJECT_FORM_NAME = 'projectEditDialog';
+
 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)));
@@ -50,4 +57,17 @@ export const createProject = (project: Partial<ProjectResource>) =>
             .then(project => dispatch(projectActions.CREATE_PROJECT_SUCCESS(project)));
     };
 
+export const updateProject = (project: Partial<ProjectResource>) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const { uuid } = getState().projects.updater;
+        return services.projectService
+            .update(uuid, project)
+            .then(project => {
+                dispatch(projectActions.UPDATE_PROJECT_SUCCESS(project));
+                dispatch(projectPanelActions.REQUEST_ITEMS());
+                dispatch<any>(getProjectList(project.ownerUuid));
+                dispatch<any>(updateDetails(project));
+            });
+    };
+
 export type ProjectAction = UnionOf<typeof projectActions>;
index cd96afce4a83241a60145d902188c955a800e191..bb60e396946a588f8d93a1c1f7e6803461ba82eb 100644 (file)
@@ -35,6 +35,10 @@ describe('project-reducer', () => {
             creator: {
                 opened: false,
                 ownerUuid: "",
+            },
+            updater: {
+                opened: false,
+                uuid: ''
             }
         });
     });
@@ -50,6 +54,7 @@ describe('project-reducer', () => {
             }],
             currentItemId: "1",
             creator: { opened: false, ownerUuid: "" },
+            updater: { opened: false, uuid: '' }
         };
         const project = {
             items: [{
@@ -61,6 +66,7 @@ describe('project-reducer', () => {
             }],
             currentItemId: "",
             creator: { opened: false, ownerUuid: "" },
+            updater: { opened: false, uuid: '' }
         };
 
         const state = projectsReducer(initialState, projectActions.RESET_PROJECT_TREE_ACTIVITY(initialState.items[0].id));
@@ -77,7 +83,8 @@ describe('project-reducer', () => {
                 status: TreeItemStatus.PENDING
             }],
             currentItemId: "1",
-            creator: { opened: false, ownerUuid: "" }
+            creator: { opened: false, ownerUuid: "" },
+            updater: { opened: false, uuid: '' }
         };
         const project = {
             items: [{
@@ -89,6 +96,7 @@ describe('project-reducer', () => {
             }],
             currentItemId: "1",
             creator: { opened: false, ownerUuid: "" },
+            updater: { opened: false, uuid: '' }
         };
 
         const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_ACTIVE(initialState.items[0].id));
@@ -106,7 +114,8 @@ describe('project-reducer', () => {
                 status: TreeItemStatus.PENDING,
             }],
             currentItemId: "1",
-            creator: { opened: false, ownerUuid: "" }
+            creator: { opened: false, ownerUuid: "" },
+            updater: { opened: false, uuid: '' }
         };
         const project = {
             items: [{
@@ -118,6 +127,8 @@ describe('project-reducer', () => {
             }],
             currentItemId: "1",
             creator: { opened: false, ownerUuid: "" },
+            updater: { opened: false, uuid: '' }
+
         };
 
         const state = projectsReducer(initialState, projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(initialState.items[0].id));
index 424900780d9e40030dccd5d1584e530a35f4de58..bb0748657ee1e64b3d28b5e8bc923fc062ca6ea9 100644 (file)
@@ -11,7 +11,8 @@ import { ProjectResource } from "~/models/project";
 export type ProjectState = {
     items: Array<TreeItem<ProjectResource>>,
     currentItemId: string,
-    creator: ProjectCreator
+    creator: ProjectCreator,
+    updater: ProjectUpdater
 };
 
 interface ProjectCreator {
@@ -20,6 +21,11 @@ interface ProjectCreator {
     error?: string;
 }
 
+interface ProjectUpdater {
+    opened: boolean;
+    uuid: string;
+}
+
 export function findTreeItem<T>(tree: Array<TreeItem<T>>, itemId: string): TreeItem<T> | undefined {
     let item;
     for (const t of tree) {
@@ -100,12 +106,24 @@ const updateCreator = (state: ProjectState, creator: Partial<ProjectCreator>) =>
     }
 });
 
+const updateProject = (state: ProjectState, updater?: Partial<ProjectUpdater>) => ({
+    ...state,
+    updater: {
+        ...state.updater,
+        ...updater
+    }
+});
+
 const initialState: ProjectState = {
     items: [],
     currentItemId: "",
     creator: {
         opened: false,
         ownerUuid: ""
+    },
+    updater: {
+        opened: false,
+        uuid: ''
     }
 };
 
@@ -116,6 +134,9 @@ export const projectsReducer = (state: ProjectState = initialState, action: Proj
         CLOSE_PROJECT_CREATOR: () => updateCreator(state, { opened: false }),
         CREATE_PROJECT: () => updateCreator(state, { error: undefined }),
         CREATE_PROJECT_SUCCESS: () => updateCreator(state, { opened: false, ownerUuid: "" }),
+        OPEN_PROJECT_UPDATER: ({ uuid }) => updateProject(state, { uuid, opened: true }),
+        CLOSE_PROJECT_UPDATER: () => updateProject(state, { opened: false, uuid: "" }),
+        UPDATE_PROJECT_SUCCESS: () => updateProject(state, { opened: false, uuid: "" }),
         REMOVE_PROJECT: () => state,
         PROJECTS_REQUEST: itemId => {
             const items = _.cloneDeep(state.items);
diff --git a/src/validators/create-collection/create-collection-validator.tsx b/src/validators/create-collection/create-collection-validator.tsx
deleted file mode 100644 (file)
index 2d8e1f5..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { require } from '../require';
-import { maxLength } from '../max-length';
-
-export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
-export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
\ No newline at end of file
diff --git a/src/validators/create-project/create-project-validator.tsx b/src/validators/create-project/create-project-validator.tsx
deleted file mode 100644 (file)
index ddea8be..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { require } from '../require';
-import { maxLength } from '../max-length';
-
-export const PROJECT_NAME_VALIDATION = [require, maxLength(255)];
-export const PROJECT_DESCRIPTION_VALIDATION = [maxLength(255)];
-export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
-export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
-export const COLLECTION_PROJECT_VALIDATION = [require];
index fdeb8fa8747118ea3c8bec26c473ef9e9e932f1c..edd07822942ace10ac40ae68e79b9222422dbe55 100644 (file)
@@ -6,4 +6,11 @@ import { require } from './require';
 import { maxLength } from './max-length';
 
 export const TAG_KEY_VALIDATION = [require, maxLength(255)];
-export const TAG_VALUE_VALIDATION = [require, maxLength(255)];
\ No newline at end of file
+export const TAG_VALUE_VALIDATION = [require, maxLength(255)];
+
+export const PROJECT_NAME_VALIDATION = [require, maxLength(255)];
+export const PROJECT_DESCRIPTION_VALIDATION = [maxLength(255)];
+
+export const COLLECTION_NAME_VALIDATION = [require, maxLength(255)];
+export const COLLECTION_DESCRIPTION_VALIDATION = [maxLength(255)];
+export const COLLECTION_PROJECT_VALIDATION = [require];
\ No newline at end of file
index 89446850a3b8322346d31665db7c98d8235e7528..1b000c88fcee77ec2c39a844d3476001fca725a7 100644 (file)
@@ -2,28 +2,39 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { reset } from "redux-form";
+import { reset, initialize } from "redux-form";
 
 import { ContextMenuActionSet } from "../context-menu-action-set";
-import { projectActions } from "~/store/project/project-action";
-import { NewProjectIcon } from "~/components/icon/icon";
+import { projectActions, PROJECT_FORM_NAME } from "~/store/project/project-action";
+import { NewProjectIcon, RenameIcon } from "~/components/icon/icon";
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "~/store/favorites/favorites-actions";
 import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
 import { PROJECT_CREATE_DIALOG } from "../../dialog-create/dialog-project-create";
 
-export const projectActionSet: ContextMenuActionSet = [[{
-    icon: NewProjectIcon,
-    name: "New project",
-    execute: (dispatch, resource) => {
-        dispatch(reset(PROJECT_CREATE_DIALOG));
-        dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
+export const projectActionSet: ContextMenuActionSet = [[
+    {
+        icon: NewProjectIcon,
+        name: "New project",
+        execute: (dispatch, resource) => {
+            dispatch(reset(PROJECT_CREATE_DIALOG));
+            dispatch(projectActions.OPEN_PROJECT_CREATOR({ ownerUuid: resource.uuid }));
+        }
+    },
+    {
+        icon: RenameIcon,
+        name: "Edit project",
+        execute: (dispatch, resource) => {
+            dispatch(projectActions.OPEN_PROJECT_UPDATER({ uuid: resource.uuid }));
+            dispatch(initialize(PROJECT_FORM_NAME, { name: resource.name, description: resource.description }));
+        }
+    },
+    {
+        component: ToggleFavoriteAction,
+        execute: (dispatch, resource) => {
+            dispatch<any>(toggleFavorite(resource)).then(() => {
+                dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+            });
+        }
     }
-}, {
-    component: ToggleFavoriteAction,
-    execute: (dispatch, resource) => {
-        dispatch<any>(toggleFavorite(resource)).then(() => {
-            dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
-        });
-    }
-}]];
+]];
index 0dc590ae04e5f3b61a42d97c404c1292a2b471d4..af2536df9512a66b89a1d013a5345c6b579bb690 100644 (file)
@@ -7,7 +7,7 @@ import { InjectedFormProps, Field, WrappedFieldProps } from "redux-form";
 import { Dialog, DialogTitle, DialogContent, DialogActions, Button, CircularProgress } from "@material-ui/core";
 import { WithDialogProps } from "~/store/dialog/with-dialog";
 import { TextField } from "~/components/text-field/text-field";
-import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION, COLLECTION_PROJECT_VALIDATION } from "~/validators/create-project/create-project-validator";
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION, COLLECTION_PROJECT_VALIDATION } from "~/validators/validators";
 import { ProjectTreePicker } from "../project-tree-picker/project-tree-picker";
 
 export const DialogCollectionCreateWithSelected = (props: WithDialogProps<string> & InjectedFormProps<{ name: string }>) =>
index 7f2e411ed08f525ab94e8bf8b3d94b84e541142a..af0e33f1b4260fab01a15bef29fef1985701ecdd 100644 (file)
@@ -9,13 +9,13 @@ import { TextField } from '~/components/text-field/text-field';
 import { Dialog, DialogActions, DialogContent, DialogTitle } from '@material-ui/core/';
 import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
 
-import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '~/validators/create-collection/create-collection-validator';
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '~/validators/validators';
 import { FileUpload } from "~/components/file-upload/file-upload";
 import { connect, DispatchProp } from "react-redux";
 import { RootState } from "~/store/store";
 import { collectionUploaderActions, UploadFile } from "~/store/collections/uploader/collection-uploader-actions";
 
-type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "createProgress" | "dialogActions";
+type CssRules = "button" | "lastButton" | "formContainer" | "createProgress" | "dialogActions";
 
 const styles: StyleRulesCallback<CssRules> = theme => ({
     button: {
@@ -29,9 +29,6 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
         display: "flex",
         flexDirection: "column",
     },
-    textField: {
-        marginBottom: theme.spacing.unit * 3
-    },
     createProgress: {
         position: "absolute",
         minWidth: "20px",
@@ -42,10 +39,8 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
     }
 });
 
-interface DialogCollectionCreateProps {
+interface DialogCollectionDataProps {
     open: boolean;
-    handleClose: () => void;
-    onSubmit: (data: { name: string, description: string }, files: UploadFile[]) => void;
     handleSubmit: any;
     submitting: boolean;
     invalid: boolean;
@@ -53,6 +48,13 @@ interface DialogCollectionCreateProps {
     files: UploadFile[];
 }
 
+interface DialogCollectionActionProps {
+    handleClose: () => void;
+    onSubmit: (data: { name: string, description: string }, files: UploadFile[]) => void;
+}
+
+type DialogCollectionProps = DialogCollectionDataProps & DialogCollectionActionProps & DispatchProp & WithStyles<CssRules>;
+
 export const COLLECTION_CREATE_DIALOG = "collectionCreateDialog";
 
 export const DialogCollectionCreate = compose(
@@ -61,7 +63,7 @@ export const DialogCollectionCreate = compose(
     })),
     reduxForm({ form: COLLECTION_CREATE_DIALOG }),
     withStyles(styles))(
-        class DialogCollectionCreate extends React.Component<DialogCollectionCreateProps & DispatchProp & WithStyles<CssRules>> {
+    class DialogCollectionCreate extends React.Component<DialogCollectionProps> {
             render() {
                 const { classes, open, handleClose, handleSubmit, onSubmit, submitting, invalid, pristine, files } = this.props;
                 const busy = submitting || files.reduce(
@@ -82,13 +84,11 @@ export const DialogCollectionCreate = compose(
                                     disabled={submitting}
                                     component={TextField}
                                     validate={COLLECTION_NAME_VALIDATION}
-                                    className={classes.textField}
                                     label="Collection Name" />
                                 <Field name="description"
                                     disabled={submitting}
                                     component={TextField}
                                     validate={COLLECTION_DESCRIPTION_VALIDATION}
-                                    className={classes.textField}
                                     label="Description - optional" />
                                 <FileUpload
                                     files={files}
index c3d8415c9a48dd77cd0cb608229cfb2d20cf9ee3..e77114b369a2137d5cdb3584703c68e5163e45f3 100644 (file)
@@ -9,9 +9,9 @@ import { TextField } from '~/components/text-field/text-field';
 import { Dialog, DialogActions, DialogContent, DialogTitle } from '@material-ui/core/';
 import { Button, StyleRulesCallback, WithStyles, withStyles, CircularProgress } from '@material-ui/core';
 
-import { PROJECT_NAME_VALIDATION, PROJECT_DESCRIPTION_VALIDATION } from '~/validators/create-project/create-project-validator';
+import { PROJECT_NAME_VALIDATION, PROJECT_DESCRIPTION_VALIDATION } from '~/validators/validators';
 
-type CssRules = "button" | "lastButton" | "formContainer" | "textField" | "dialog" | "dialogTitle" | "createProgress" | "dialogActions";
+type CssRules = "button" | "lastButton" | "formContainer" | "dialog" | "dialogTitle" | "createProgress" | "dialogActions";
 
 const styles: StyleRulesCallback<CssRules> = theme => ({
     button: {
@@ -29,9 +29,6 @@ const styles: StyleRulesCallback<CssRules> = theme => ({
     dialogTitle: {
         paddingBottom: "0"
     },
-    textField: {
-        marginTop: "32px",
-    },
     dialog: {
         minWidth: "600px",
         minHeight: "320px"
@@ -78,12 +75,10 @@ export const DialogProjectCreate = compose(
                                 <Field name="name"
                                        component={TextField}
                                        validate={PROJECT_NAME_VALIDATION}
-                                       className={classes.textField}
                                        label="Project Name"/>
                                 <Field name="description"
                                        component={TextField}
                                        validate={PROJECT_DESCRIPTION_VALIDATION}
-                                       className={classes.textField}
                                        label="Description - optional"/>
                             </DialogContent>
                             <DialogActions className={classes.dialogActions}>
index d97ff41bea913e2d46531134a95feb0e57edf4bc..18c43f2d008a1cbd407410d2389e3de8532a9fd0 100644 (file)
@@ -6,11 +6,12 @@ import * as React from 'react';
 import { reduxForm, Field } from 'redux-form';
 import { compose } from 'redux';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '@material-ui/core';
-import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '~/validators/create-collection/create-collection-validator';
+import { Dialog, DialogActions, DialogContent, DialogTitle, StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress } from '@material-ui/core';
+import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION } from '~/validators/validators';
 import { COLLECTION_FORM_NAME } from '~/store/collections/updater/collection-updater-action';
+import { TextField } from '~/components/text-field/text-field';
 
-type CssRules = 'content' | 'actions' | 'textField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
+type CssRules = 'content' | 'actions' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     content: {
@@ -22,9 +23,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3 - theme.spacing.unit / 2}px 
                 ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`
     },
-    textField: {
-        marginBottom: theme.spacing.unit * 3
-    },
     buttonWrapper: {
         position: 'relative'
     },
@@ -56,14 +54,6 @@ interface DialogCollectionAction {
 
 type DialogCollectionProps = DialogCollectionDataProps & DialogCollectionAction & WithStyles<CssRules>;
 
-interface TextFieldProps {
-    label: string;
-    floatinglabeltext: string;
-    className?: string;
-    input?: string;
-    meta?: any;
-}
-
 export const DialogCollectionUpdate = compose(
     reduxForm({ form: COLLECTION_FORM_NAME }),
     withStyles(styles))(
@@ -83,19 +73,15 @@ export const DialogCollectionUpdate = compose(
                         <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
                             <DialogTitle>Edit Collection</DialogTitle>
                             <DialogContent className={classes.content}>
-                                <Field name="name"
+                                <Field name='name'
                                     disabled={submitting}
-                                    component={this.renderTextField}
-                                    floatinglabeltext="Collection Name"
+                                    component={TextField}
                                     validate={COLLECTION_NAME_VALIDATION}
-                                    className={classes.textField}
                                     label="Collection Name" />
-                                <Field name="description"
+                                <Field name='description'
                                     disabled={submitting}
-                                    component={this.renderTextField}
-                                    floatinglabeltext="Description - optional"
+                                    component={TextField}
                                     validate={COLLECTION_DESCRIPTION_VALIDATION}
-                                    className={classes.textField}
                                     label="Description - optional" />
                             </DialogContent>
                             <DialogActions className={classes.actions}>
@@ -115,17 +101,5 @@ export const DialogCollectionUpdate = compose(
                     </Dialog>
                 );
             }
-
-            renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
-                <TextField
-                    helperText={touched && error}
-                    label={label}
-                    className={this.props.classes.textField}
-                    error={touched && !!error}
-                    autoComplete='off'
-                    {...input}
-                    {...custom}
-                />
-            )
         }
     );
diff --git a/src/views-components/dialog-update/dialog-project-update.tsx b/src/views-components/dialog-update/dialog-project-update.tsx
new file mode 100644 (file)
index 0000000..5dde00a
--- /dev/null
@@ -0,0 +1,101 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { reduxForm, Field } from 'redux-form';
+import { compose } from 'redux';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { StyleRulesCallback, WithStyles, withStyles, Dialog, DialogTitle, DialogContent, DialogActions, CircularProgress, Button } from '../../../node_modules/@material-ui/core';
+import { TextField } from '~/components/text-field/text-field';
+import { PROJECT_FORM_NAME } from '~/store/project/project-action';
+import { PROJECT_NAME_VALIDATION, PROJECT_DESCRIPTION_VALIDATION } from '~/validators/validators';
+
+type CssRules = 'content' | 'actions' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    content: {
+        display: 'flex',
+        flexDirection: 'column'
+    },
+    actions: {
+        margin: 0,
+        padding: `${theme.spacing.unit}px ${theme.spacing.unit * 3 - theme.spacing.unit / 2}px 
+                ${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px`
+    },
+    buttonWrapper: {
+        position: 'relative'
+    },
+    saveButton: {
+        boxShadow: 'none'
+    },
+    circularProgress: {
+        position: 'absolute',
+        top: 0,
+        bottom: 0,
+        left: 0,
+        right: 0,
+        margin: 'auto'
+    }
+});
+
+interface DialogProjectDataProps {
+    open: boolean;
+    handleSubmit: any;
+    submitting: boolean;
+    invalid: boolean;
+    pristine: boolean;
+}
+
+interface DialogProjectActionProps {
+    handleClose: () => void;
+    onSubmit: (data: { name: string, description: string }) => void;
+}
+
+type DialogProjectProps = DialogProjectDataProps & DialogProjectActionProps & WithStyles<CssRules>;
+
+export const DialogProjectUpdate = compose(
+    reduxForm({ form: PROJECT_FORM_NAME }),
+    withStyles(styles))(
+
+        class DialogProjectUpdate extends React.Component<DialogProjectProps> {
+            render() {
+                const { handleSubmit, handleClose, onSubmit, open, classes, submitting, invalid, pristine } = this.props;
+                return <Dialog open={open}
+                    onClose={handleClose}
+                    fullWidth={true}
+                    maxWidth='sm'
+                    disableBackdropClick={true}
+                    disableEscapeKeyDown={true}>
+                    <form onSubmit={handleSubmit((data: any) => onSubmit(data))}>
+                        <DialogTitle>Edit Collection</DialogTitle>
+                        <DialogContent className={classes.content}>
+                            <Field name='name' 
+                                disabled={submitting}
+                                component={TextField}
+                                validate={PROJECT_NAME_VALIDATION}
+                                label="Project Name" />
+                            <Field name='description' 
+                                disabled={submitting}
+                                component={TextField} 
+                                validate={PROJECT_DESCRIPTION_VALIDATION}
+                                label="Description - optional" />
+                        </DialogContent>
+                        <DialogActions className={classes.actions}>
+                            <Button onClick={handleClose} color="primary"
+                                disabled={submitting}>CANCEL</Button>
+                            <div className={classes.buttonWrapper}>
+                                <Button type="submit" className={classes.saveButton}
+                                    color="primary"
+                                    disabled={invalid || submitting || pristine}
+                                    variant="contained">
+                                    SAVE
+                                </Button>
+                                {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
+                            </div>
+                        </DialogActions>
+                    </form>
+                </Dialog>;
+            }
+        }
+    );
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 });
 
diff --git a/src/views-components/update-project-dialog/update-project-dialog.tsx b/src/views-components/update-project-dialog/update-project-dialog.tsx
new file mode 100644 (file)
index 0000000..0ea23c8
--- /dev/null
@@ -0,0 +1,42 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
+import { SubmissionError } from "redux-form";
+import { RootState } from "~/store/store";
+import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { DialogProjectUpdate } from "../dialog-update/dialog-project-update";
+import { projectActions, updateProject } from "~/store/project/project-action";
+
+const mapStateToProps = (state: RootState) => ({
+    open: state.projects.updater.opened
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+    handleClose: () => {
+        dispatch(projectActions.CLOSE_PROJECT_UPDATER());
+    },
+    onSubmit: (data: { name: string, description: string }) => {
+        return dispatch<any>(editProject(data))
+            .catch((e: any) => {
+                if (e.errors) {
+                    throw new SubmissionError({ name: e.errors.join("").includes("UniqueViolation") ? "Project with this name already exists." : "" });
+                }
+            });
+    }
+});
+
+const editProject = (data: { name: string, description: string }) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const { uuid } = getState().projects.updater;
+        return dispatch<any>(updateProject(data)).then(() => {
+            dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: "Project has been successfully updated.",
+                hideDuration: 2000
+            }));
+        });
+    };
+
+export const UpdateProjectDialog = connect(mapStateToProps, mapDispatchToProps)(DialogProjectUpdate);
index 5baa6d18753fd7e1f6a0198eb942d7c922538a59..374cb95159483f5d4896fcbd539fddfc61931ea3 100644 (file)
@@ -100,7 +100,7 @@ export const CollectionPanel = withStyles(styles)(
                         </Card>
 
                         <Card className={classes.card}>
-                            <CardHeader title="Tags" />
+                            <CardHeader title="Properties" />
                             <CardContent>
                                 <Grid container direction="column">
                                     <Grid item xs={12}><CollectionTagForm /></Grid>
index 8f2540412edbbd6318c9716d400e921e6086b9b8..83ad0ca42860a1e70c8e85900411af15eea3baf0 100644 (file)
@@ -6,20 +6,15 @@ import * as React from 'react';
 import { reduxForm, Field, reset } from 'redux-form';
 import { compose, Dispatch } from 'redux';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { StyleRulesCallback, withStyles, WithStyles, TextField, Button, CircularProgress } from '@material-ui/core';
+import { StyleRulesCallback, withStyles, WithStyles, Button, CircularProgress, Grid } from '@material-ui/core';
 import { TagProperty } from '~/models/tag';
+import { TextField } from '~/components/text-field/text-field';
 import { createCollectionTag, COLLECTION_TAG_FORM_NAME } from '~/store/collection-panel/collection-panel-action';
 import { TAG_VALUE_VALIDATION, TAG_KEY_VALIDATION } from '~/validators/validators';
 
-type CssRules = 'form' | 'textField' | 'buttonWrapper' | 'saveButton' | 'circularProgress';
+type CssRules = 'buttonWrapper' | 'saveButton' | 'circularProgress';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
-    form: {
-        marginBottom: theme.spacing.unit * 4
-    },
-    textField: {
-        marginRight: theme.spacing.unit
-    },
     buttonWrapper: {
         position: 'relative',
         display: 'inline-block'
@@ -47,14 +42,6 @@ interface CollectionTagFormActionProps {
     handleSubmit: any;
 }
 
-interface TextFieldProps {
-    label: string;
-    floatinglabeltext: string;
-    className?: string;
-    input?: string;
-    meta?: any;
-}
-
 type CollectionTagFormProps = CollectionTagFormDataProps & CollectionTagFormActionProps & WithStyles<CssRules>;
 
 export const CollectionTagForm = compose(
@@ -67,52 +54,41 @@ export const CollectionTagForm = compose(
     }),
     withStyles(styles))(
 
-    class CollectionTagForm extends React.Component<CollectionTagFormProps> {
+        class CollectionTagForm extends React.Component<CollectionTagFormProps> {
 
             render() {
                 const { classes, submitting, pristine, invalid, handleSubmit } = this.props;
                 return (
-                    <form className={classes.form} onSubmit={handleSubmit}>
-                        <Field name="key"
-                            disabled={submitting}
-                            component={this.renderTextField}
-                            floatinglabeltext="Key"
-                            validate={TAG_KEY_VALIDATION}
-                            className={classes.textField}
-                            label="Key" />
-                        <Field name="value"
-                            disabled={submitting}
-                            component={this.renderTextField}
-                            floatinglabeltext="Value"
-                            validate={TAG_VALUE_VALIDATION}
-                            className={classes.textField}
-                            label="Value" />
-                        <div className={classes.buttonWrapper}>
-                            <Button type="submit" className={classes.saveButton}
-                                color="primary"
-                                size='small'
-                                disabled={invalid || submitting || pristine}
-                                variant="contained">
-                                ADD
-                            </Button>
-                            {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
-                        </div>
+                    <form onSubmit={handleSubmit}>
+                        <Grid container justify="flex-start" alignItems="baseline" spacing={24}>
+                            <Grid item xs={3} component={"span"}>
+                                <Field name="key"
+                                    disabled={submitting}
+                                    component={TextField}
+                                    validate={TAG_KEY_VALIDATION}
+                                    label="Key" />
+                            </Grid>
+                            <Grid item xs={5} component={"span"}>
+                                <Field name="value"
+                                    disabled={submitting}
+                                    component={TextField}
+                                    validate={TAG_VALUE_VALIDATION}
+                                    label="Value" />
+                            </Grid>
+                            <Grid item component={"span"} className={classes.buttonWrapper}>
+                                <Button type="submit" className={classes.saveButton}
+                                    color="primary"
+                                    size='small'
+                                    disabled={invalid || submitting || pristine}
+                                    variant="contained">
+                                    ADD
+                                </Button>
+                                {submitting && <CircularProgress size={20} className={classes.circularProgress} />}
+                            </Grid>
+                        </Grid>
                     </form>
                 );
             }
-
-            renderTextField = ({ input, label, meta: { touched, error }, ...custom }: TextFieldProps) => (
-                <TextField
-                    helperText={touched && error}
-                    label={label}
-                    className={this.props.classes.textField}
-                    error={touched && !!error}
-                    autoComplete='off'
-                    {...input}
-                    {...custom}
-                />
-            )
-
         }
 
     );
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 58641fb36bc5e8fdf629bd2006b009d98660a834..5cf632faf174b655b91b29e456efaceba68d6849 100644 (file)
@@ -42,6 +42,7 @@ import { CollectionPanel } from '../collection-panel/collection-panel';
 import { loadCollection, loadCollectionTags } from '~/store/collection-panel/collection-panel-action';
 import { getCollectionUrl } from '~/models/collection';
 import { UpdateCollectionDialog } from '~/views-components/update-collection-dialog/update-collection-dialog.';
+import { UpdateProjectDialog } from '~/views-components/update-project-dialog/update-project-dialog';
 import { AuthService } from "~/services/auth-service/auth-service";
 import { RenameFileDialog } from '~/views-components/rename-file-dialog/rename-file-dialog';
 import { FileRemoveDialog } from '~/views-components/file-remove-dialog/file-remove-dialog';
@@ -245,6 +246,7 @@ export const Workbench = withStyles(styles)(
                         <FileRemoveDialog />
                         <MultipleFilesRemoveDialog />
                         <UpdateCollectionDialog />
+                        <UpdateProjectDialog />
                         <CurrentTokenDialog
                             currentToken={this.props.currentToken}
                             open={this.state.isCurrentTokenDialogOpen}