Merge branch 'master'
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Thu, 2 Aug 2018 14:47:22 +0000 (16:47 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Thu, 2 Aug 2018 14:47:22 +0000 (16:47 +0200)
Feature #13855

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

27 files changed:
src/components/column-selector/column-selector.test.tsx
src/components/column-selector/column-selector.tsx
src/components/data-explorer/data-explorer.tsx
src/components/data-table/data-column.ts
src/store/data-explorer/data-explorer-action.ts
src/store/data-explorer/data-explorer-middleware-service.ts [new file with mode: 0644]
src/store/data-explorer/data-explorer-middleware.test.ts [new file with mode: 0644]
src/store/data-explorer/data-explorer-middleware.ts [new file with mode: 0644]
src/store/data-explorer/data-explorer-reducer.ts
src/store/favorite-panel/favorite-panel-action.ts [new file with mode: 0644]
src/store/favorite-panel/favorite-panel-middleware-service.ts [new file with mode: 0644]
src/store/favorite-panel/favorite-panel-middleware.ts [deleted file]
src/store/navigation/navigation-action.ts
src/store/project-panel/project-panel-action.ts [new file with mode: 0644]
src/store/project-panel/project-panel-middleware-service.ts [new file with mode: 0644]
src/store/project-panel/project-panel-middleware.ts [deleted file]
src/store/side-panel/side-panel-reducer.ts
src/store/store.ts
src/views-components/context-menu/action-sets/collection-action-set.ts
src/views-components/context-menu/action-sets/favorite-action-set.ts
src/views-components/context-menu/action-sets/project-action-set.ts
src/views-components/create-project-dialog/create-project-dialog.tsx
src/views-components/data-explorer/data-explorer.tsx
src/views-components/data-explorer/renderers.tsx [new file with mode: 0644]
src/views/favorite-panel/favorite-panel.tsx
src/views/project-panel/project-panel.tsx
src/views/workbench/workbench.tsx

index 01dba85c0621e1ac50718a340cb9059f8c147a2a..02265fc4fd5f2f16da793a9a5cb72081a417ca71 100644 (file)
@@ -17,7 +17,8 @@ describe("<ColumnSelector />", () => {
             {
                 name: "Column 1",
                 render: () => <span />,
-                selected: true
+                selected: true,
+                configurable: true
             },
             {
                 name: "Column 2",
@@ -42,17 +43,20 @@ describe("<ColumnSelector />", () => {
             {
                 name: "Column 1",
                 render: () => <span />,
-                selected: true
+                selected: true,
+                configurable: true
             },
             {
                 name: "Column 2",
                 render: () => <span />,
-                selected: false
+                selected: false,
+                configurable: true
             },
             {
                 name: "Column 3",
                 render: () => <span />,
-                selected: true
+                selected: true,
+                configurable: true
             }
         ];
         const columnsConfigurator = mount(<ColumnSelector columns={columns} onColumnToggle={jest.fn()} />);
@@ -67,7 +71,8 @@ describe("<ColumnSelector />", () => {
             {
                 name: "Column 1",
                 render: () => <span />,
-                selected: true
+                selected: true,
+                configurable: true
             }
         ];
         const onColumnToggle = jest.fn();
index 0f496e25cd2c396f9fa20bcabf7e3c0915e49573..f2e42dd251bfbfa07e3c926de0a24cd7291c2038 100644 (file)
@@ -5,7 +5,7 @@
 import * as React from 'react';
 import { WithStyles, StyleRulesCallback, withStyles, IconButton, Paper, List, Checkbox, ListItemText, ListItem } from '@material-ui/core';
 import MenuIcon from "@material-ui/icons/Menu";
-import { DataColumn, isColumnConfigurable } from '../data-table/data-column';
+import { DataColumn } from '../data-table/data-column';
 import { Popover } from "../popover/popover";
 import { IconButtonProps } from '@material-ui/core/IconButton';
 import { DataColumns } from '../data-table/data-table';
@@ -33,8 +33,8 @@ export const ColumnSelector = withStyles(styles)(
         <Paper>
             <List dense>
                 {columns
-                    .filter(isColumnConfigurable)
-                    .map((column, index) => (
+                    .filter(column => column.configurable)
+                    .map((column, index) =>
                         <ListItem
                             button
                             key={index}
@@ -48,7 +48,7 @@ export const ColumnSelector = withStyles(styles)(
                                 {column.name}
                             </ListItemText>
                         </ListItem>
-                    ))}
+                    )}
             </List>
         </Paper>
     </Popover>
index 4699fd6de0c143f4871febb38b421be8e4ee737f..46d5fb50f3783c89c9852892716891c6d260732c 100644 (file)
@@ -29,7 +29,7 @@ interface DataExplorerDataProps<T> {
     columns: DataColumns<T>;
     searchValue: string;
     rowsPerPage: number;
-    rowsPerPageOptions?: number[];
+    rowsPerPageOptions: number[];
     page: number;
     onSearch: (value: string) => void;
     onRowClick: (item: T) => void;
@@ -105,8 +105,8 @@ export const DataExplorer = withStyles(styles)(
         contextMenuColumn = {
             name: "Actions",
             selected: true,
+            configurable: false,
             key: "context-actions",
-            renderHeader: () => null,
             render: this.renderContextMenuTrigger,
             width: "auto"
         };
index 96ef952493aa895e24b778b22716b024c9ade851..a5000b935bcea4b1420b14408516d03718dacb67 100644 (file)
@@ -7,12 +7,12 @@ import { DataTableFilterItem } from "../data-table-filters/data-table-filters";
 export interface DataColumn<T, F extends DataTableFilterItem = DataTableFilterItem> {
     name: string;
     selected: boolean;
-    configurable?: boolean;
+    configurable: boolean;
     key?: React.Key;
     sortDirection?: SortDirection;
     filters?: F[];
-    render: (item: T) => React.ReactElement<void>;
-    renderHeader?: () => React.ReactElement<void> | null;
+    render: (item: T) => React.ReactElement<any>;
+    renderHeader?: () => React.ReactElement<any>;
     width?: string;
 }
 
@@ -22,10 +22,6 @@ export enum SortDirection {
     NONE = "none"
 }
 
-export const isColumnConfigurable = <T>(column: DataColumn<T>) => {
-    return column.configurable === undefined || column.configurable;
-};
-
 export const toggleSortDirection = <T>(column: DataColumn<T>): DataColumn<T> => {
     return column.sortDirection
         ? column.sortDirection === SortDirection.ASC
index 053f4194ec8ddab68b3583bf346f21ea39f470ad..6dd7af92856a909b968fa328e0b9011df457a073 100644 (file)
@@ -20,3 +20,26 @@ export const dataExplorerActions = unionize({
 }, { tag: "type", value: "payload" });
 
 export type DataExplorerAction = UnionOf<typeof dataExplorerActions>;
+
+export const bindDataExplorerActions = (id: string) => ({
+    RESET_PAGINATION: () =>
+        dataExplorerActions.RESET_PAGINATION({ id }),
+    REQUEST_ITEMS: () =>
+        dataExplorerActions.REQUEST_ITEMS({ id }),
+    SET_COLUMNS: (payload: { columns: DataColumns<any> }) =>
+        dataExplorerActions.SET_COLUMNS({ ...payload, id }),
+    SET_FILTERS: (payload: { columnName: string, filters: DataTableFilterItem[] }) =>
+        dataExplorerActions.SET_FILTERS({ ...payload, id }),
+    SET_ITEMS: (payload: { items: any[], page: number, rowsPerPage: number, itemsAvailable: number }) =>
+        dataExplorerActions.SET_ITEMS({ ...payload, id }),
+    SET_PAGE: (payload: { page: number }) =>
+        dataExplorerActions.SET_PAGE({ ...payload, id }),
+    SET_ROWS_PER_PAGE: (payload: { rowsPerPage: number }) =>
+        dataExplorerActions.SET_ROWS_PER_PAGE({ ...payload, id }),
+    TOGGLE_COLUMN: (payload: { columnName: string }) =>
+        dataExplorerActions.TOGGLE_COLUMN({ ...payload, id }),
+    TOGGLE_SORT: (payload: { columnName: string }) =>
+        dataExplorerActions.TOGGLE_SORT({ ...payload, id }),
+    SET_SEARCH_VALUE: (payload: { searchValue: string }) =>
+        dataExplorerActions.SET_SEARCH_VALUE({ ...payload, id }),
+});
diff --git a/src/store/data-explorer/data-explorer-middleware-service.ts b/src/store/data-explorer/data-explorer-middleware-service.ts
new file mode 100644 (file)
index 0000000..14be4ea
--- /dev/null
@@ -0,0 +1,20 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch, MiddlewareAPI } from "redux";
+import { RootState } from "../store";
+
+export abstract class DataExplorerMiddlewareService {
+    protected readonly id: string;
+
+    protected constructor(id: string) {
+        this.id = id;
+    }
+
+    public getId() {
+        return this.id;
+    }
+
+    abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>): void;
+}
diff --git a/src/store/data-explorer/data-explorer-middleware.test.ts b/src/store/data-explorer/data-explorer-middleware.test.ts
new file mode 100644 (file)
index 0000000..6b8297b
--- /dev/null
@@ -0,0 +1,210 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { DataExplorerMiddlewareService } from "./data-explorer-middleware-service";
+import { dataExplorerMiddleware } from "./data-explorer-middleware";
+import { MiddlewareAPI } from "redux";
+import { DataColumns } from "../../components/data-table/data-table";
+import { dataExplorerActions } from "./data-explorer-action";
+
+
+describe("DataExplorerMiddleware", () => {
+    
+    it("handles only actions that are identified by service id", () => {
+        const config = {
+            id: "ServiceId",
+            columns: [{
+                name: "Column",
+                selected: true,
+                configurable: false,
+                render: jest.fn()
+            }],
+            requestItems: jest.fn(),
+            setApi: jest.fn()
+        };
+        const service = new ServiceMock(config);
+        const api = {
+            getState: jest.fn(),
+            dispatch: jest.fn()
+        };
+        const next = jest.fn();
+        const middleware = dataExplorerMiddleware(service)(api)(next);
+        middleware(dataExplorerActions.SET_PAGE({ id: "OtherId", page: 0 }));
+        middleware(dataExplorerActions.SET_PAGE({ id: "ServiceId", page: 0 }));
+        middleware(dataExplorerActions.SET_PAGE({ id: "OtherId", page: 0 }));
+        expect(api.dispatch).toHaveBeenCalledWith(dataExplorerActions.REQUEST_ITEMS({ id: "ServiceId" }));
+        expect(api.dispatch).toHaveBeenCalledTimes(1);
+    });
+
+    it("handles REQUEST_ITEMS action", () => {
+        const config = {
+            id: "ServiceId",
+            columns: [{
+                name: "Column",
+                selected: true,
+                configurable: false,
+                render: jest.fn()
+            }],
+            requestItems: jest.fn(),
+            setApi: jest.fn()
+        };
+        const service = new ServiceMock(config);
+        const api = {
+            getState: jest.fn(),
+            dispatch: jest.fn()
+        };
+        const next = jest.fn();
+        const middleware = dataExplorerMiddleware(service)(api)(next);
+        middleware(dataExplorerActions.REQUEST_ITEMS({ id: "ServiceId" }));
+        expect(config.requestItems).toHaveBeenCalled();
+    });
+
+    it("handles SET_PAGE action", () => {
+        const config = {
+            id: "ServiceId",
+            columns: [],
+            requestItems: jest.fn(),
+            setApi: jest.fn()
+        };
+        const service = new ServiceMock(config);
+        const api = {
+            getState: jest.fn(),
+            dispatch: jest.fn()
+        };
+        const next = jest.fn();
+        const middleware = dataExplorerMiddleware(service)(api)(next);
+        middleware(dataExplorerActions.SET_PAGE({ id: service.getId(), page: 0 }));
+        expect(api.dispatch).toHaveBeenCalledTimes(1);
+    });
+
+    it("handles SET_ROWS_PER_PAGE action", () => {
+        const config = {
+            id: "ServiceId",
+            columns: [],
+            requestItems: jest.fn(),
+            setApi: jest.fn()
+        };
+        const service = new ServiceMock(config);
+        const api = {
+            getState: jest.fn(),
+            dispatch: jest.fn()
+        };
+        const next = jest.fn();
+        const middleware = dataExplorerMiddleware(service)(api)(next);
+        middleware(dataExplorerActions.SET_ROWS_PER_PAGE({ id: service.getId(), rowsPerPage: 0 }));
+        expect(api.dispatch).toHaveBeenCalledTimes(1);
+    });
+
+    it("handles SET_FILTERS action", () => {
+        const config = {
+            id: "ServiceId",
+            columns: [],
+            requestItems: jest.fn(),
+            setApi: jest.fn()
+        };
+        const service = new ServiceMock(config);
+        const api = {
+            getState: jest.fn(),
+            dispatch: jest.fn()
+        };
+        const next = jest.fn();
+        const middleware = dataExplorerMiddleware(service)(api)(next);
+        middleware(dataExplorerActions.SET_FILTERS({ id: service.getId(), columnName: "", filters: [] }));
+        expect(api.dispatch).toHaveBeenCalledTimes(2);
+    });
+
+    it("handles SET_ROWS_PER_PAGE action", () => {
+        const config = {
+            id: "ServiceId",
+            columns: [],
+            requestItems: jest.fn(),
+            setApi: jest.fn()
+        };
+        const service = new ServiceMock(config);
+        const api = {
+            getState: jest.fn(),
+            dispatch: jest.fn()
+        };
+        const next = jest.fn();
+        const middleware = dataExplorerMiddleware(service)(api)(next);
+        middleware(dataExplorerActions.SET_ROWS_PER_PAGE({ id: service.getId(), rowsPerPage: 0 }));
+        expect(api.dispatch).toHaveBeenCalledTimes(1);
+    });
+
+    it("handles TOGGLE_SORT action", () => {
+        const config = {
+            id: "ServiceId",
+            columns: [],
+            requestItems: jest.fn(),
+            setApi: jest.fn()
+        };
+        const service = new ServiceMock(config);
+        const api = {
+            getState: jest.fn(),
+            dispatch: jest.fn()
+        };
+        const next = jest.fn();
+        const middleware = dataExplorerMiddleware(service)(api)(next);
+        middleware(dataExplorerActions.TOGGLE_SORT({ id: service.getId(), columnName: "" }));
+        expect(api.dispatch).toHaveBeenCalledTimes(1);
+    });
+
+    it("handles SET_SEARCH_VALUE action", () => {
+        const config = {
+            id: "ServiceId",
+            columns: [],
+            requestItems: jest.fn(),
+            setApi: jest.fn()
+        };
+        const service = new ServiceMock(config);
+        const api = {
+            getState: jest.fn(),
+            dispatch: jest.fn()
+        };
+        const next = jest.fn();
+        const middleware = dataExplorerMiddleware(service)(api)(next);
+        middleware(dataExplorerActions.SET_SEARCH_VALUE({ id: service.getId(), searchValue: "" }));
+        expect(api.dispatch).toHaveBeenCalledTimes(2);
+    });
+
+    it("forwards other actions", () => {
+        const config = {
+            id: "ServiceId",
+            columns: [],
+            requestItems: jest.fn(),
+            setApi: jest.fn()
+        };
+        const service = new ServiceMock(config);
+        const api = {
+            getState: jest.fn(),
+            dispatch: jest.fn()
+        };
+        const next = jest.fn();
+        const middleware = dataExplorerMiddleware(service)(api)(next);
+        middleware(dataExplorerActions.SET_COLUMNS({ id: service.getId(), columns: [] }));
+        middleware(dataExplorerActions.SET_ITEMS({ id: service.getId(), items: [], rowsPerPage: 0, itemsAvailable: 0, page: 0 }));
+        middleware(dataExplorerActions.TOGGLE_COLUMN({ id: service.getId(), columnName: "" }));
+        expect(api.dispatch).toHaveBeenCalledTimes(0);
+        expect(next).toHaveBeenCalledTimes(3);
+    });
+
+});
+
+class ServiceMock extends DataExplorerMiddlewareService {
+    constructor(private config: {
+        id: string,
+        columns: DataColumns<any>,
+        requestItems: (api: MiddlewareAPI) => void
+    }) {
+        super(config.id);
+    }
+
+    getColumns() {
+        return this.config.columns;
+    }
+
+    requestItems(api: MiddlewareAPI) {
+        this.config.requestItems(api);
+    }
+}
diff --git a/src/store/data-explorer/data-explorer-middleware.ts b/src/store/data-explorer/data-explorer-middleware.ts
new file mode 100644 (file)
index 0000000..146867c
--- /dev/null
@@ -0,0 +1,44 @@
+
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Middleware } from "redux";
+import { dataExplorerActions, bindDataExplorerActions } from "./data-explorer-action";
+import { DataExplorerMiddlewareService } from "./data-explorer-middleware-service";
+
+export const dataExplorerMiddleware = (service: DataExplorerMiddlewareService): Middleware => api => next => {
+    const handleAction = <T extends { id: string }>(handler: (data: T) => void) =>
+        (data: T) => {
+            if (data.id === service.getId()) {
+                handler(data);
+            }
+        };
+    const actions = bindDataExplorerActions(service.getId());
+
+    return action => {
+        dataExplorerActions.match(action, {
+            SET_PAGE: handleAction(() => {
+                api.dispatch(actions.REQUEST_ITEMS());
+            }),
+            SET_ROWS_PER_PAGE: handleAction(() => {
+                api.dispatch(actions.REQUEST_ITEMS());
+            }),
+            SET_FILTERS: handleAction(() => {
+                api.dispatch(actions.RESET_PAGINATION());
+                api.dispatch(actions.REQUEST_ITEMS());
+            }),
+            TOGGLE_SORT: handleAction(() => {
+                api.dispatch(actions.REQUEST_ITEMS());
+            }),
+            SET_SEARCH_VALUE: handleAction(() => {
+                api.dispatch(actions.RESET_PAGINATION());
+                api.dispatch(actions.REQUEST_ITEMS());
+            }),
+            REQUEST_ITEMS: handleAction(() => {
+                service.requestItems(api);
+            }),
+            default: () => next(action)
+        });
+    };
+};
index c112454b94f31451100d047d1e5f46f489e28e30..1fde652d07b3941bfa9b02207352101a72a4ba4a 100644 (file)
@@ -7,13 +7,13 @@ import { dataExplorerActions, DataExplorerAction } from "./data-explorer-action"
 import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
 import { DataColumns } from "../../components/data-table/data-table";
 
-interface DataExplorer {
+export interface DataExplorer {
     columns: DataColumns<any>;
     items: any[];
     itemsAvailable: number;
     page: number;
     rowsPerPage: number;
-    rowsPerPageOptions?: number[];
+    rowsPerPageOptions: number[];
     searchValue: string;
 }
 
@@ -27,7 +27,7 @@ export const initialDataExplorer: DataExplorer = {
     searchValue: ""
 };
 
-export type DataExplorerState = Record<string, DataExplorer | undefined>;
+export type DataExplorerState = Record<string, DataExplorer>;
 
 export const dataExplorerReducer = (state: DataExplorerState = {}, action: DataExplorerAction) =>
     dataExplorerActions.match(action, {
diff --git a/src/store/favorite-panel/favorite-panel-action.ts b/src/store/favorite-panel/favorite-panel-action.ts
new file mode 100644 (file)
index 0000000..aa1ec8d
--- /dev/null
@@ -0,0 +1,8 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
+
+export const FAVORITE_PANEL_ID = "favoritePanel";
+export const favoritePanelActions = bindDataExplorerActions(FAVORITE_PANEL_ID);
diff --git a/src/store/favorite-panel/favorite-panel-middleware-service.ts b/src/store/favorite-panel/favorite-panel-middleware-service.ts
new file mode 100644 (file)
index 0000000..8908fff
--- /dev/null
@@ -0,0 +1,70 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// 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 { 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 { favoriteService, authService } from "../../services/services";
+import { SortDirection } from "../../components/data-table/data-column";
+import { FilterBuilder } from "../../common/api/filter-builder";
+import { LinkResource } from "../../models/link";
+import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { favoritePanelActions } from "./favorite-panel-action";
+import { Dispatch, MiddlewareAPI } from "redux";
+
+export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService {
+    constructor(id: string) {
+        super(id);
+    }
+
+    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) {
+            favoriteService
+                .list(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<LinkResource>()
+                        .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 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) : [];
+};
diff --git a/src/store/favorite-panel/favorite-panel-middleware.ts b/src/store/favorite-panel/favorite-panel-middleware.ts
deleted file mode 100644 (file)
index 548a117..0000000
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { Middleware } from "redux";
-import { dataExplorerActions } from "../data-explorer/data-explorer-action";
-import { favoriteService } from "../../services/services";
-import { RootState } from "../store";
-import { getDataExplorer } from "../data-explorer/data-explorer-reducer";
-import { FilterBuilder } from "../../common/api/filter-builder";
-import { DataColumns } from "../../components/data-table/data-table";
-import {
-    columns,
-    FAVORITE_PANEL_ID,
-    FavoritePanelColumnNames,
-    FavoritePanelFilter
-} from "../../views/favorite-panel/favorite-panel";
-import { FavoritePanelItem, resourceToDataItem } from "../../views/favorite-panel/favorite-panel-item";
-import { LinkResource } from "../../models/link";
-import { checkPresenceInFavorites } from "../favorites/favorites-actions";
-import { OrderBuilder } from "../../common/api/order-builder";
-import { SortDirection } from "../../components/data-table/data-column";
-import { GroupContentsResource, GroupContentsResourcePrefix } from "../../services/groups-service/groups-service";
-import { FavoriteOrderBuilder } from "../../services/favorite-service/favorite-order-builder";
-
-export const favoritePanelMiddleware: Middleware = store => next => {
-    next(dataExplorerActions.SET_COLUMNS({ id: FAVORITE_PANEL_ID, columns }));
-
-    return action => {
-
-        const handlePanelAction = <T extends { id: string }>(handler: (data: T) => void) =>
-            (data: T) => {
-                next(action);
-                if (data.id === FAVORITE_PANEL_ID) {
-                    handler(data);
-                }
-            };
-
-        dataExplorerActions.match(action, {
-            SET_PAGE: handlePanelAction(() => {
-                store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
-            }),
-            SET_ROWS_PER_PAGE: handlePanelAction(() => {
-                store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
-            }),
-            SET_FILTERS: handlePanelAction(() => {
-                store.dispatch(dataExplorerActions.RESET_PAGINATION({ id: FAVORITE_PANEL_ID }));
-                store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
-            }),
-            TOGGLE_SORT: handlePanelAction(() => {
-                store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
-            }),
-            SET_SEARCH_VALUE: handlePanelAction(() => {
-                store.dispatch(dataExplorerActions.RESET_PAGINATION({ id: FAVORITE_PANEL_ID }));
-                store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
-            }),
-            REQUEST_ITEMS: handlePanelAction(() => {
-                const state = store.getState() as RootState;
-                const dataExplorer = getDataExplorer(state.dataExplorer, FAVORITE_PANEL_ID);
-                const columns = dataExplorer.columns as DataColumns<FavoritePanelItem, FavoritePanelFilter>;
-                const sortColumn = dataExplorer.columns.find(({ sortDirection }) => Boolean(sortDirection && sortDirection !== "none"));
-                const typeFilters = getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
-                const order = FavoriteOrderBuilder.create();
-                if (typeFilters.length > 0) {
-                    favoriteService
-                        .list(state.projects.currentItemId, {
-                            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<LinkResource>()
-                                .addIsA("headUuid", typeFilters.map(filter => filter.type))
-                                .addILike("name", dataExplorer.searchValue)
-                        })
-                        .then(response => {
-                            store.dispatch(dataExplorerActions.SET_ITEMS({
-                                id: FAVORITE_PANEL_ID,
-                                items: response.items.map(resourceToDataItem),
-                                itemsAvailable: response.itemsAvailable,
-                                page: Math.floor(response.offset / response.limit),
-                                rowsPerPage: response.limit
-                            }));
-                            store.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
-                        });
-                } else {
-                    store.dispatch(dataExplorerActions.SET_ITEMS({
-                        id: FAVORITE_PANEL_ID,
-                        items: [],
-                        itemsAvailable: 0,
-                        page: 0,
-                        rowsPerPage: dataExplorer.rowsPerPage
-                    }));
-                }
-            }),
-            default: () => next(action)
-        });
-    };
-};
-
-const getOrder = (direction: SortDirection) => {
-    const order = OrderBuilder.create<LinkResource>();
-    const addRule = (builder: OrderBuilder<GroupContentsResource | LinkResource>, direction: SortDirection) =>
-        direction === SortDirection.ASC
-            ? builder.addAsc("name")
-            : builder.addDesc("name");
-
-    return [
-        OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION),
-        OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS),
-        OrderBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT)
-    ].reduce((acc, b) =>
-        acc.concat(addRule(b, direction)), addRule(OrderBuilder.create(), direction));
-};
-
-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 f8687ed754ad51f4604b4541361c5e3aff318fb5..ffb0f7acf6f5fc3e69b1c0295936f6ee8c4eacc5 100644 (file)
@@ -7,10 +7,9 @@ import { projectActions, getProjectList } from "../project/project-action";
 import { push } from "react-router-redux";
 import { TreeItemStatus } from "../../components/tree/tree";
 import { findTreeItem } from "../project/project-reducer";
-import { dataExplorerActions } from "../data-explorer/data-explorer-action";
-import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
 import { RootState } from "../store";
 import { Resource, ResourceKind } from "../../models/resource";
+import { projectPanelActions } from "../project-panel/project-panel-action";
 import { getCollectionUrl } from "../../models/collection";
 import { getProjectUrl } from "../../models/project";
 
@@ -53,8 +52,8 @@ export const setProjectItem = (itemId: string, itemMode: ItemMode) =>
                     if (itemMode === ItemMode.OPEN || itemMode === ItemMode.BOTH) {
                         dispatch(projectActions.TOGGLE_PROJECT_TREE_ITEM_OPEN(treeItem.data.uuid));
                     }
-                    dispatch(dataExplorerActions.RESET_PAGINATION({id: PROJECT_PANEL_ID}));
-                    dispatch(dataExplorerActions.REQUEST_ITEMS({id: PROJECT_PANEL_ID}));
+                    dispatch(projectPanelActions.RESET_PAGINATION());
+                    dispatch(projectPanelActions.REQUEST_ITEMS());
                 }));
 
         }
diff --git a/src/store/project-panel/project-panel-action.ts b/src/store/project-panel/project-panel-action.ts
new file mode 100644 (file)
index 0000000..33cedd7
--- /dev/null
@@ -0,0 +1,8 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
+
+export const PROJECT_PANEL_ID = "projectPanel";
+export const projectPanelActions = bindDataExplorerActions(PROJECT_PANEL_ID);
diff --git a/src/store/project-panel/project-panel-middleware-service.ts b/src/store/project-panel/project-panel-middleware-service.ts
new file mode 100644 (file)
index 0000000..761ec18
--- /dev/null
@@ -0,0 +1,96 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-middleware-service";
+import { ProjectPanelColumnNames, ProjectPanelFilter } from "../../views/project-panel/project-panel";
+import { RootState } from "../store";
+import { DataColumns } from "../../components/data-table/data-table";
+import { groupsService } 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 { FilterBuilder } from "../../common/api/filter-builder";
+import { ProcessResource } from "../../models/process";
+import { GroupContentsResourcePrefix, GroupContentsResource } from "../../services/groups-service/groups-service";
+import { checkPresenceInFavorites } from "../favorites/favorites-actions";
+import { projectPanelActions } from "./project-panel-action";
+import { Dispatch, MiddlewareAPI } from "redux";
+
+export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
+    constructor(id: string) {
+        super(id);
+    }
+
+    requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+        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) {
+            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<ProcessResource>(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 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 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());
+
+const getSearchFilter = (searchValue: string) =>
+    searchValue
+        ? [
+            FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION),
+            FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS),
+            FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT)]
+            .reduce((acc, b) =>
+                acc.concat(b.addILike("name", searchValue)), FilterBuilder.create())
+        : FilterBuilder.create();
diff --git a/src/store/project-panel/project-panel-middleware.ts b/src/store/project-panel/project-panel-middleware.ts
deleted file mode 100644 (file)
index b7ab03c..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { Middleware } from "redux";
-import { dataExplorerActions } from "../data-explorer/data-explorer-action";
-import { PROJECT_PANEL_ID, columns, ProjectPanelFilter, ProjectPanelColumnNames } from "../../views/project-panel/project-panel";
-import { groupsService } from "../../services/services";
-import { RootState } from "../store";
-import { getDataExplorer } from "../data-explorer/data-explorer-reducer";
-import { resourceToDataItem, ProjectPanelItem } from "../../views/project-panel/project-panel-item";
-import { FilterBuilder } from "../../common/api/filter-builder";
-import { DataColumns } from "../../components/data-table/data-table";
-import { ProcessResource } from "../../models/process";
-import { OrderBuilder } from "../../common/api/order-builder";
-import { GroupContentsResource, GroupContentsResourcePrefix } from "../../services/groups-service/groups-service";
-import { SortDirection } from "../../components/data-table/data-column";
-import { checkPresenceInFavorites } from "../favorites/favorites-actions";
-
-export const projectPanelMiddleware: Middleware = store => next => {
-    next(dataExplorerActions.SET_COLUMNS({ id: PROJECT_PANEL_ID, columns }));
-
-    return action => {
-
-        const handleProjectPanelAction = <T extends { id: string }>(handler: (data: T) => void) =>
-            (data: T) => {
-                next(action);
-                if (data.id === PROJECT_PANEL_ID) {
-                    handler(data);
-                }
-            };
-
-        dataExplorerActions.match(action, {
-            SET_PAGE: handleProjectPanelAction(() => {
-                store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
-            }),
-            SET_ROWS_PER_PAGE: handleProjectPanelAction(() => {
-                store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
-            }),
-            SET_FILTERS: handleProjectPanelAction(() => {
-                store.dispatch(dataExplorerActions.RESET_PAGINATION({ id: PROJECT_PANEL_ID }));
-                store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
-            }),
-            TOGGLE_SORT: handleProjectPanelAction(() => {
-                store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
-            }),
-            SET_SEARCH_VALUE: handleProjectPanelAction(() => {
-                store.dispatch(dataExplorerActions.RESET_PAGINATION({ id: PROJECT_PANEL_ID }));
-                store.dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
-            }),
-            REQUEST_ITEMS: handleProjectPanelAction(() => {
-                const state = store.getState() as RootState;
-                const dataExplorer = getDataExplorer(state.dataExplorer, PROJECT_PANEL_ID);
-                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) {
-                    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<ProcessResource>(GroupContentsResourcePrefix.PROCESS)
-                                    .addIn("state", statusFilters.map(f => f.type)))
-                                .concat(getSearchFilter(dataExplorer.searchValue))
-                        })
-                        .then(response => {
-                            store.dispatch(dataExplorerActions.SET_ITEMS({
-                                id: PROJECT_PANEL_ID,
-                                items: response.items.map(resourceToDataItem),
-                                itemsAvailable: response.itemsAvailable,
-                                page: Math.floor(response.offset / response.limit),
-                                rowsPerPage: response.limit
-                            }));
-                            store.dispatch<any>(checkPresenceInFavorites(response.items.map(item => item.uuid)));
-                        });
-                } else {
-                    store.dispatch(dataExplorerActions.SET_ITEMS({
-                        id: PROJECT_PANEL_ID,
-                        items: [],
-                        itemsAvailable: 0,
-                        page: 0,
-                        rowsPerPage: dataExplorer.rowsPerPage
-                    }));
-                }
-            }),
-            default: () => next(action)
-        });
-    };
-};
-
-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 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());
-
-const getSearchFilter = (searchValue: string) =>
-    searchValue
-        ? [
-            FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.COLLECTION),
-            FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROCESS),
-            FilterBuilder.create<GroupContentsResource>(GroupContentsResourcePrefix.PROJECT)]
-            .reduce((acc, b) =>
-                acc.concat(b.addILike("name", searchValue)), FilterBuilder.create())
-        : FilterBuilder.create();
-
-
index 9d56102b149719e74454a69b7d3bf1d917ab7d8c..cae42ac38456f2d4d7b8835865dcf04367941dd6 100644 (file)
@@ -6,10 +6,9 @@ import * as _ from "lodash";
 import { sidePanelActions, SidePanelAction } from './side-panel-action';
 import { SidePanelItem } from '../../components/side-panel/side-panel';
 import { ProjectsIcon, ShareMeIcon, WorkflowIcon, RecentIcon, FavoriteIcon, TrashIcon } from "../../components/icon/icon";
-import { dataExplorerActions } from "../data-explorer/data-explorer-action";
 import { Dispatch } from "redux";
-import { FAVORITE_PANEL_ID } from "../../views/favorite-panel/favorite-panel";
 import { push } from "react-router-redux";
+import { favoritePanelActions } from "../favorite-panel/favorite-panel-action";
 
 export type SidePanelState = SidePanelItem[];
 
@@ -84,8 +83,8 @@ export const sidePanelData = [
         active: false,
         activeAction: (dispatch: Dispatch) => {
             dispatch(push("/favorites"));
-            dispatch(dataExplorerActions.RESET_PAGINATION({id: FAVORITE_PANEL_ID}));
-            dispatch(dataExplorerActions.REQUEST_ITEMS({id: FAVORITE_PANEL_ID}));
+            dispatch(favoritePanelActions.RESET_PAGINATION());
+            dispatch(favoritePanelActions.REQUEST_ITEMS());
         }
     },
     {
index 0ce461602bbe73c2b41d3b9934fae9b29117fc23..3eda005484124dd14b3955f17f2d49ccb4c8764d 100644 (file)
@@ -11,15 +11,18 @@ import { projectsReducer, ProjectState } from "./project/project-reducer";
 import { sidePanelReducer, SidePanelState } from './side-panel/side-panel-reducer';
 import { authReducer, AuthState } from "./auth/auth-reducer";
 import { dataExplorerReducer, DataExplorerState } from './data-explorer/data-explorer-reducer';
-import { projectPanelMiddleware } from './project-panel/project-panel-middleware';
 import { detailsPanelReducer, DetailsPanelState } from './details-panel/details-panel-reducer';
 import { contextMenuReducer, ContextMenuState } from './context-menu/context-menu-reducer';
-import { favoritePanelMiddleware } from "./favorite-panel/favorite-panel-middleware";
 import { reducer as formReducer } from 'redux-form';
 import { FavoritesState, favoritesReducer } from './favorites/favorites-reducer';
 import { snackbarReducer, SnackbarState } from './snackbar/snackbar-reducer';
 import { CollectionPanelFilesState } from './collection-panel/collection-panel-files/collection-panel-files-state';
 import { collectionPanelFilesReducer } from './collection-panel/collection-panel-files/collections-panel-files-reducer';
+import { dataExplorerMiddleware } from "./data-explorer/data-explorer-middleware";
+import { FAVORITE_PANEL_ID } from "./favorite-panel/favorite-panel-action";
+import { PROJECT_PANEL_ID } from "./project-panel/project-panel-action";
+import { ProjectPanelMiddlewareService } from "./project-panel/project-panel-middleware-service";
+import { FavoritePanelMiddlewareService } from "./favorite-panel/favorite-panel-middleware-service";
 import { CollectionCreatorState, collectionCreationReducer } from './collections/creator/collection-creator-reducer';
 import { CollectionPanelState, collectionPanelReducer } from './collection-panel/collection-panel-reducer';
 import { DialogState, dialogReducer } from './dialog/dialog-reducer';
@@ -62,8 +65,14 @@ const rootReducer = combineReducers({
     dialog: dialogReducer
 });
 
-
 export function configureStore(history: History) {
+    const projectPanelMiddleware = dataExplorerMiddleware(
+        new ProjectPanelMiddlewareService(PROJECT_PANEL_ID)
+    );
+    const favoritePanelMiddleware = dataExplorerMiddleware(
+        new FavoritePanelMiddlewareService(FAVORITE_PANEL_ID)
+    );
+
     const middlewares: Middleware[] = [
         routerMiddleware(history),
         thunkMiddleware,
index 0822b781e4e3dd1ad39177d6c17c77820316b3a7..fbb5c864b8a2a608a5b5904229054b50728a9f66 100644 (file)
@@ -6,8 +6,8 @@ import { ContextMenuActionSet } from "../context-menu-action-set";
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "../../../store/favorites/favorites-actions";
 import { dataExplorerActions } from "../../../store/data-explorer/data-explorer-action";
-import { FAVORITE_PANEL_ID } from "../../../views/favorite-panel/favorite-panel";
 import { RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon, ProvenanceGraphIcon, AdvancedIcon, RemoveIcon } from "../../../components/icon/icon";
+import { favoritePanelActions } from "../../../store/favorite-panel/favorite-panel-action";
 
 export const collectionActionSet: ContextMenuActionSet = [[
     {
@@ -35,7 +35,7 @@ export const collectionActionSet: ContextMenuActionSet = [[
         component: ToggleFavoriteAction,
         execute: (dispatch, resource) => {
             dispatch<any>(toggleFavorite(resource)).then(() => {
-                dispatch<any>(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }));
+                dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
             });
         }
     },
index 9682d4bf9a57778f8969239d320334aff9c616d5..72c72fa9cb0c6f44eecc548399877eedccfb06ce 100644 (file)
@@ -5,14 +5,13 @@
 import { ContextMenuActionSet } from "../context-menu-action-set";
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "../../../store/favorites/favorites-actions";
-import { dataExplorerActions } from "../../../store/data-explorer/data-explorer-action";
-import { FAVORITE_PANEL_ID } from "../../../views/favorite-panel/favorite-panel";
+import { favoritePanelActions } from "../../../store/favorite-panel/favorite-panel-action";
 
 export const favoriteActionSet: ContextMenuActionSet = [[{
     component: ToggleFavoriteAction,
     execute: (dispatch, resource) => {
         dispatch<any>(toggleFavorite(resource)).then(() => {
-            dispatch<any>(dataExplorerActions.REQUEST_ITEMS({ id : FAVORITE_PANEL_ID }));
+            dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
         });
     }
 }]];
index 25e0ba37315cbc9124471167b496a8e615e7f7ce..df298e4b1f8c1c472bae5fdc7c2b8d5ba8f464ad 100644 (file)
@@ -7,8 +7,7 @@ import { projectActions } from "../../../store/project/project-action";
 import { NewProjectIcon } from "../../../components/icon/icon";
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "../../../store/favorites/favorites-actions";
-import { dataExplorerActions } from "../../../store/data-explorer/data-explorer-action";
-import { FAVORITE_PANEL_ID } from "../../../views/favorite-panel/favorite-panel";
+import { favoritePanelActions } from "../../../store/favorite-panel/favorite-panel-action";
 
 export const projectActionSet: ContextMenuActionSet = [[{
     icon: NewProjectIcon,
@@ -20,7 +19,7 @@ export const projectActionSet: ContextMenuActionSet = [[{
     component: ToggleFavoriteAction,
     execute: (dispatch, resource) => {
         dispatch<any>(toggleFavorite(resource)).then(() => {
-            dispatch<any>(dataExplorerActions.REQUEST_ITEMS({ id : FAVORITE_PANEL_ID }));
+            dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
         });
     }
 }]];
index eacb1eb5c09a3d58823d1230c3eb269fe2f22c7a..1a521890d71be9ac684cb57335dee9d935e29526 100644 (file)
@@ -9,8 +9,7 @@ import { SubmissionError } from "redux-form";
 import { RootState } from "../../store/store";
 import { DialogProjectCreate } from "../dialog-create/dialog-project-create";
 import { projectActions, createProject, getProjectList } from "../../store/project/project-action";
-import { dataExplorerActions } from "../../store/data-explorer/data-explorer-action";
-import { PROJECT_PANEL_ID } from "../../views/project-panel/project-panel";
+import { projectPanelActions } from "../../store/project-panel/project-panel-action";
 import { snackbarActions } from "../../store/snackbar/snackbar-actions";
 
 const mapStateToProps = (state: RootState) => ({
@@ -25,7 +24,7 @@ const addProject = (data: { name: string, description: string }) =>
                 message: "Created a new project",
                 hideDuration: 2000
             }));
-            dispatch(dataExplorerActions.REQUEST_ITEMS({ id: PROJECT_PANEL_ID }));
+            dispatch(projectPanelActions.REQUEST_ITEMS());
             dispatch<any>(getProjectList(ownerUuid));
         });
     };
index 2645504c612200b27dd3a2f7b778b85ba8f9174e..6449bf8d5697ac75b1204e3d3e361e4eb95ef86d 100644 (file)
@@ -10,9 +10,11 @@ import { Dispatch } from "redux";
 import { dataExplorerActions } from "../../store/data-explorer/data-explorer-action";
 import { DataColumn } from "../../components/data-table/data-column";
 import { DataTableFilterItem } from "../../components/data-table-filters/data-table-filters";
+import { DataColumns } from "../../components/data-table/data-table";
 
 interface Props {
     id: string;
+    columns: DataColumns<any>;
     onRowClick: (item: any) => void;
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
     onRowDoubleClick: (item: any) => void;
@@ -22,37 +24,40 @@ interface Props {
 const mapStateToProps = (state: RootState, { id }: Props) =>
     getDataExplorer(state.dataExplorer, id);
 
-const mapDispatchToProps = (dispatch: Dispatch, { id, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
-    onSearch: (searchValue: string) => {
-        dispatch(dataExplorerActions.SET_SEARCH_VALUE({ id, searchValue }));
-    },
+const mapDispatchToProps = (dispatch: Dispatch, { id, columns, onRowClick, onRowDoubleClick, onContextMenu }: Props) => {
+    dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
+    return {
+        onSearch: (searchValue: string) => {
+            dispatch(dataExplorerActions.SET_SEARCH_VALUE({ id, searchValue }));
+        },
 
-    onColumnToggle: (column: DataColumn<any>) => {
-        dispatch(dataExplorerActions.TOGGLE_COLUMN({ id, columnName: column.name }));
-    },
+        onColumnToggle: (column: DataColumn<any>) => {
+            dispatch(dataExplorerActions.TOGGLE_COLUMN({ id, columnName: column.name }));
+        },
 
-    onSortToggle: (column: DataColumn<any>) => {
-        dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
-    },
+        onSortToggle: (column: DataColumn<any>) => {
+            dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
+        },
 
-    onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
-        dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
-    },
+        onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
+            dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
+        },
 
-    onChangePage: (page: number) => {
-        dispatch(dataExplorerActions.SET_PAGE({ id, page }));
-    },
+        onChangePage: (page: number) => {
+            dispatch(dataExplorerActions.SET_PAGE({ id, page }));
+        },
 
-    onChangeRowsPerPage: (rowsPerPage: number) => {
-        dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
-    },
+        onChangeRowsPerPage: (rowsPerPage: number) => {
+            dispatch(dataExplorerActions.SET_ROWS_PER_PAGE({ id, rowsPerPage }));
+        },
 
-    onRowClick,
+        onRowClick,
 
-    onRowDoubleClick,
+        onRowDoubleClick,
 
-    onContextMenu,
-});
+        onContextMenu,
+    };
+};
 
 export const DataExplorer = connect(mapStateToProps, mapDispatchToProps)(DataExplorerComponent);
 
diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
new file mode 100644 (file)
index 0000000..2b99f02
--- /dev/null
@@ -0,0 +1,67 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { Grid, Typography } from '@material-ui/core';
+import { FavoriteStar } from '../favorite-star/favorite-star';
+import { ResourceKind } from '../../models/resource';
+import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '../../components/icon/icon';
+import { formatDate, formatFileSize } from '../../common/formatters';
+import { resourceLabel } from '../../common/labels';
+
+
+export const renderName = (item: {name: string; uuid: string, kind: string}) =>
+    <Grid container alignItems="center" wrap="nowrap" spacing={16}>
+        <Grid item>
+            {renderIcon(item)}
+        </Grid>
+        <Grid item>
+            <Typography color="primary">
+                {item.name}
+            </Typography>
+        </Grid>
+        <Grid item>
+            <Typography variant="caption">
+                <FavoriteStar resourceUuid={item.uuid} />
+            </Typography>
+        </Grid>
+    </Grid>;
+
+
+export const renderIcon = (item: {kind: string}) => {
+    switch (item.kind) {
+        case ResourceKind.PROJECT:
+            return <ProjectIcon />;
+        case ResourceKind.COLLECTION:
+            return <CollectionIcon />;
+        case ResourceKind.PROCESS:
+            return <ProcessIcon />;
+        default:
+            return <DefaultIcon />;
+    }
+};
+
+export const renderDate = (date: string) => {
+    return <Typography noWrap>{formatDate(date)}</Typography>;
+};
+
+export const renderFileSize = (fileSize?: number) =>
+    <Typography noWrap>
+        {formatFileSize(fileSize)}
+    </Typography>;
+
+export const renderOwner = (owner: string) =>
+    <Typography noWrap color="primary" >
+        {owner}
+    </Typography>;
+
+export const renderType = (type: string) =>
+    <Typography noWrap>
+        {resourceLabel(type)}
+    </Typography>;
+
+export const renderStatus = (item: {status?: string}) =>
+    <Typography noWrap align="center" >
+        {item.status || "-"}
+    </Typography>;
\ No newline at end of file
index 36a3410763129b043c95c2d55af45a39814f0a15..f99afecb4cde7823d015d67ee278c14dab870b5b 100644 (file)
@@ -4,8 +4,7 @@
 
 import * as React from 'react';
 import { FavoritePanelItem } from './favorite-panel-item';
-import { Grid, Typography, Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
-import { formatDate, formatFileSize } from '../../common/formatters';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 import { DataExplorer } from "../../views-components/data-explorer/data-explorer";
 import { DispatchProp, connect } from 'react-redux';
 import { DataColumns } from '../../components/data-table/data-table';
@@ -16,9 +15,9 @@ import { ContainerRequestState } from '../../models/container-request';
 import { SortDirection } from '../../components/data-table/data-column';
 import { ResourceKind } from '../../models/resource';
 import { resourceLabel } from '../../common/labels';
-import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon } from '../../components/icon/icon';
 import { ArvadosTheme } from '../../common/custom-theme';
-import { FavoriteStar } from "../../views-components/favorite-star/favorite-star";
+import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '../../views-components/data-explorer/renderers';
+import { FAVORITE_PANEL_ID } from "../../store/favorite-panel/favorite-panel-action";
 
 type CssRules = "toolbar" | "button";
 
@@ -32,61 +31,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
 });
 
-const renderName = (item: FavoritePanelItem) =>
-    <Grid container alignItems="center" wrap="nowrap" spacing={16}>
-        <Grid item>
-            {renderIcon(item)}
-        </Grid>
-        <Grid item>
-            <Typography color="primary">
-                {item.name}
-            </Typography>
-        </Grid>
-        <Grid item>
-            <Typography variant="caption">
-                <FavoriteStar resourceUuid={item.uuid} />
-            </Typography>
-        </Grid>
-    </Grid>;
-
-
-const renderIcon = (item: FavoritePanelItem) => {
-    switch (item.kind) {
-        case ResourceKind.PROJECT:
-            return <ProjectIcon />;
-        case ResourceKind.COLLECTION:
-            return <CollectionIcon />;
-        case ResourceKind.PROCESS:
-            return <ProcessIcon />;
-        default:
-            return <DefaultIcon />;
-    }
-};
-
-const renderDate = (date: string) => {
-    return <Typography noWrap>{formatDate(date)}</Typography>;
-};
-
-const renderFileSize = (fileSize?: number) =>
-    <Typography noWrap>
-        {formatFileSize(fileSize)}
-    </Typography>;
-
-const renderOwner = (owner: string) =>
-    <Typography noWrap color="primary" >
-        {owner}
-    </Typography>;
-
-const renderType = (type: string) =>
-    <Typography noWrap>
-        {resourceLabel(type)}
-    </Typography>;
-
-const renderStatus = (item: FavoritePanelItem) =>
-    <Typography noWrap align="center" >
-        {item.status || "-"}
-    </Typography>;
-
 export enum FavoritePanelColumnNames {
     NAME = "Name",
     STATUS = "Status",
@@ -104,6 +48,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
     {
         name: FavoritePanelColumnNames.NAME,
         selected: true,
+        configurable: true,
         sortDirection: SortDirection.ASC,
         render: renderName,
         width: "450px"
@@ -111,6 +56,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
     {
         name: "Status",
         selected: true,
+        configurable: true,
         filters: [
             {
                 name: ContainerRequestState.COMMITTED,
@@ -134,6 +80,7 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
     {
         name: FavoritePanelColumnNames.TYPE,
         selected: true,
+        configurable: true,
         filters: [
             {
                 name: resourceLabel(ResourceKind.COLLECTION),
@@ -157,26 +104,27 @@ export const columns: DataColumns<FavoritePanelItem, FavoritePanelFilter> = [
     {
         name: FavoritePanelColumnNames.OWNER,
         selected: true,
+        configurable: true,
         render: item => renderOwner(item.owner),
         width: "200px"
     },
     {
         name: FavoritePanelColumnNames.FILE_SIZE,
         selected: true,
+        configurable: true,
         render: item => renderFileSize(item.fileSize),
         width: "50px"
     },
     {
         name: FavoritePanelColumnNames.LAST_MODIFIED,
         selected: true,
+        configurable: true,
         sortDirection: SortDirection.NONE,
         render: item => renderDate(item.lastModified),
         width: "150px"
     }
 ];
 
-export const FAVORITE_PANEL_ID = "favoritePanel";
-
 interface FavoritePanelDataProps {
     currentItemId: string;
 }
@@ -198,6 +146,7 @@ export const FavoritePanel = withStyles(styles)(
             render() {
                 return <DataExplorer
                     id={FAVORITE_PANEL_ID}
+                    columns={columns}
                     onRowClick={this.props.onItemClick}
                     onRowDoubleClick={this.props.onItemDoubleClick}
                     onContextMenu={this.props.onContextMenu}
index de040effb81dcc975445c6bb74bfb2a79e7013d4..5c3fb2b0421837c43cf2ae721de6306c50358bd3 100644 (file)
@@ -4,8 +4,7 @@
 
 import * as React from 'react';
 import { ProjectPanelItem } from './project-panel-item';
-import { Grid, Typography, Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
-import { formatDate, formatFileSize } from '../../common/formatters';
+import { Button, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
 import { DataExplorer } from "../../views-components/data-explorer/data-explorer";
 import { DispatchProp, connect } from 'react-redux';
 import { DataColumns } from '../../components/data-table/data-table';
@@ -16,9 +15,8 @@ import { ContainerRequestState } from '../../models/container-request';
 import { SortDirection } from '../../components/data-table/data-column';
 import { ResourceKind } from '../../models/resource';
 import { resourceLabel } from '../../common/labels';
-import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, FavoriteIcon } from '../../components/icon/icon';
 import { ArvadosTheme } from '../../common/custom-theme';
-import { FavoriteStar } from '../../views-components/favorite-star/favorite-star';
+import { renderName, renderStatus, renderType, renderOwner, renderFileSize, renderDate } from '../../views-components/data-explorer/renderers';
 
 type CssRules = "toolbar" | "button";
 
@@ -32,61 +30,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
 });
 
-const renderName = (item: ProjectPanelItem) =>
-    <Grid container alignItems="center" wrap="nowrap" spacing={16}>
-        <Grid item>
-            {renderIcon(item)}
-        </Grid>
-        <Grid item>
-            <Typography color="default">
-                {item.name}
-            </Typography>
-        </Grid>
-        <Grid item>
-            <Typography variant="caption">
-                <FavoriteStar resourceUuid={item.uuid} />
-            </Typography>
-        </Grid>
-    </Grid>;
-
-
-const renderIcon = (item: ProjectPanelItem) => {
-    switch (item.kind) {
-        case ResourceKind.PROJECT:
-            return <ProjectIcon />;
-        case ResourceKind.COLLECTION:
-            return <CollectionIcon />;
-        case ResourceKind.PROCESS:
-            return <ProcessIcon />;
-        default:
-            return <DefaultIcon />;
-    }
-};
-
-const renderDate = (date: string) => {
-    return <Typography noWrap>{formatDate(date)}</Typography>;
-};
-
-const renderFileSize = (fileSize?: number) =>
-    <Typography noWrap>
-        {formatFileSize(fileSize)}
-    </Typography>;
-
-const renderOwner = (owner: string) =>
-    <Typography noWrap color="primary" >
-        {owner}
-    </Typography>;
-
-const renderType = (type: string) =>
-    <Typography noWrap>
-        {resourceLabel(type)}
-    </Typography>;
-
-const renderStatus = (item: ProjectPanelItem) =>
-    <Typography noWrap align="center" >
-        {item.status || "-"}
-    </Typography>;
-
 export enum ProjectPanelColumnNames {
     NAME = "Name",
     STATUS = "Status",
@@ -104,6 +47,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
     {
         name: ProjectPanelColumnNames.NAME,
         selected: true,
+        configurable: true,
         sortDirection: SortDirection.ASC,
         render: renderName,
         width: "450px"
@@ -111,6 +55,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
     {
         name: "Status",
         selected: true,
+        configurable: true,
         filters: [
             {
                 name: ContainerRequestState.COMMITTED,
@@ -134,6 +79,7 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
     {
         name: ProjectPanelColumnNames.TYPE,
         selected: true,
+        configurable: true,
         filters: [
             {
                 name: resourceLabel(ResourceKind.COLLECTION),
@@ -157,18 +103,21 @@ export const columns: DataColumns<ProjectPanelItem, ProjectPanelFilter> = [
     {
         name: ProjectPanelColumnNames.OWNER,
         selected: true,
+        configurable: true,
         render: item => renderOwner(item.owner),
         width: "200px"
     },
     {
         name: ProjectPanelColumnNames.FILE_SIZE,
         selected: true,
+        configurable: true,
         render: item => renderFileSize(item.fileSize),
         width: "50px"
     },
     {
         name: ProjectPanelColumnNames.LAST_MODIFIED,
         selected: true,
+        configurable: true,
         sortDirection: SortDirection.NONE,
         render: item => renderDate(item.lastModified),
         width: "150px"
@@ -212,6 +161,7 @@ export const ProjectPanel = withStyles(styles)(
                     </div>
                     <DataExplorer
                         id={PROJECT_PANEL_ID}
+                        columns={columns}
                         onRowClick={this.props.onItemClick}
                         onRowDoubleClick={this.props.onItemDoubleClick}
                         onContextMenu={this.props.onContextMenu}
@@ -233,4 +183,4 @@ export const ProjectPanel = withStyles(styles)(
             }
         }
     )
-);
\ No newline at end of file
+);
index 36e8bec3cb843fdcbca9df8163d26755ff790e2e..b543bef7f20cc220cd743c1532925a46239723c0 100644 (file)
@@ -33,10 +33,11 @@ import { SidePanelIdentifiers } from '../../store/side-panel/side-panel-reducer'
 import { ProjectResource } from '../../models/project';
 import { ResourceKind } from '../../models/resource';
 import { ContextMenu, ContextMenuKind } from "../../views-components/context-menu/context-menu";
-import { FavoritePanel, FAVORITE_PANEL_ID } from "../favorite-panel/favorite-panel";
+import { FavoritePanel } from "../favorite-panel/favorite-panel";
 import { CurrentTokenDialog } from '../../views-components/current-token-dialog/current-token-dialog';
 import { dataExplorerActions } from '../../store/data-explorer/data-explorer-action';
 import { Snackbar } from '../../views-components/snackbar/snackbar';
+import { favoritePanelActions } from '../../store/favorite-panel/favorite-panel-action';
 import { CreateCollectionDialog } from '../../views-components/create-collection-dialog/create-collection-dialog';
 import { CollectionPanel } from '../collection-panel/collection-panel';
 import { loadCollection } from '../../store/collection-panel/collection-panel-action';
@@ -279,7 +280,7 @@ export const Workbench = withStyles(styles)(
                 {...props} />
 
             renderFavoritePanel = (props: RouteComponentProps<{ id: string }>) => <FavoritePanel
-                onItemRouteChange={() => this.props.dispatch<any>(dataExplorerActions.REQUEST_ITEMS({ id: FAVORITE_PANEL_ID }))}
+                onItemRouteChange={() => this.props.dispatch<any>(favoritePanelActions.REQUEST_ITEMS())}
                 onContextMenu={(event, item) => {
                     const kind = item.kind === ResourceKind.PROJECT ? ContextMenuKind.PROJECT : ContextMenuKind.RESOURCE;
                     this.openContextMenu(event, {