Merge branch 'master' into 14258-collection-filtering
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 26 Nov 2018 22:56:12 +0000 (23:56 +0100)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Mon, 26 Nov 2018 22:56:12 +0000 (23:56 +0100)
refs #14258

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

28 files changed:
src/components/data-explorer/data-explorer.tsx
src/components/data-table-filters/data-table-filters-popover.tsx [new file with mode: 0644]
src/components/data-table-filters/data-table-filters-tree.tsx [new file with mode: 0644]
src/components/data-table-filters/data-table-filters.tsx
src/components/data-table/data-column.ts
src/components/data-table/data-table.test.tsx
src/components/data-table/data-table.tsx
src/components/tree/tree.tsx
src/models/collection.ts
src/models/tree.ts
src/services/api/filter-builder.test.ts
src/services/api/filter-builder.ts
src/store/data-explorer/data-explorer-action.ts
src/store/data-explorer/data-explorer-middleware-service.ts
src/store/data-explorer/data-explorer-middleware.test.ts
src/store/data-explorer/data-explorer-reducer.ts
src/store/favorite-panel/favorite-panel-middleware-service.ts
src/store/project-panel/project-panel-action.ts
src/store/project-panel/project-panel-middleware-service.ts
src/store/resource-type-filters/resource-type-filters.test.ts [new file with mode: 0644]
src/store/resource-type-filters/resource-type-filters.ts [new file with mode: 0644]
src/store/trash-panel/trash-panel-middleware-service.ts
src/views-components/data-explorer/data-explorer.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/project-panel/project-panel.tsx
src/views/search-results-panel/search-results-panel-view.tsx
src/views/trash-panel/trash-panel.tsx
src/views/workflow-panel/workflow-panel-view.tsx

index f863ba13617adcc50c6ded0aac032445787079eb..cb979c7bd216b31d7e7d6760c08da9471a159472 100644 (file)
@@ -7,9 +7,10 @@ import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, WithStyles, Table
 import { ColumnSelector } from "../column-selector/column-selector";
 import { DataTable, DataColumns } from "../data-table/data-table";
 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";
+import { createTree } from '~/models/tree';
+import { DataTableFilters } from '../data-table-filters/data-table-filters-tree';
 import { MoreOptionsIcon } from '~/components/icon/icon';
 
 type CssRules = 'searchBox' | "toolbar" | "footer" | "root" | 'moreOptionsButton';
@@ -53,7 +54,7 @@ interface DataExplorerActionProps<T> {
     onColumnToggle: (column: DataColumn<T>) => void;
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
     onSortToggle: (column: DataColumn<T>) => void;
-    onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<T>) => void;
+    onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
     onChangePage: (page: number) => void;
     onChangeRowsPerPage: (rowsPerPage: number) => void;
     extractKey?: (item: T) => React.Key;
@@ -137,7 +138,7 @@ export const DataExplorer = withStyles(styles)(
             selected: true,
             configurable: false,
             sortDirection: SortDirection.NONE,
-            filters: [],
+            filters: createTree(),
             key: "context-actions",
             render: this.renderContextMenuTrigger
         };
diff --git a/src/components/data-table-filters/data-table-filters-popover.tsx b/src/components/data-table-filters/data-table-filters-popover.tsx
new file mode 100644 (file)
index 0000000..b79d36b
--- /dev/null
@@ -0,0 +1,165 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import {
+    WithStyles,
+    withStyles,
+    ButtonBase,
+    StyleRulesCallback,
+    Theme,
+    Popover,
+    Button,
+    Card,
+    CardActions,
+    Typography,
+    CardContent,
+    Tooltip
+} from "@material-ui/core";
+import * as classnames from "classnames";
+import { DefaultTransformOrigin } from "~/components/popover/helpers";
+import { createTree } from '~/models/tree';
+import { DataTableFilters, DataTableFiltersTree } from "./data-table-filters-tree";
+import { getNodeDescendants } from '~/models/tree';
+
+export type CssRules = "root" | "icon" | "active" | "checkbox";
+
+const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
+    root: {
+        cursor: "pointer",
+        display: "inline-flex",
+        justifyContent: "flex-start",
+        flexDirection: "inherit",
+        alignItems: "center",
+        "&:hover": {
+            color: theme.palette.text.primary,
+        },
+        "&:focus": {
+            color: theme.palette.text.primary,
+        },
+    },
+    active: {
+        color: theme.palette.text.primary,
+        '& $icon': {
+            opacity: 1,
+        },
+    },
+    icon: {
+        marginRight: 4,
+        marginLeft: 4,
+        opacity: 0.7,
+        userSelect: "none",
+        width: 16
+    },
+    checkbox: {
+        width: 24,
+        height: 24
+    }
+});
+
+export interface DataTableFilterProps {
+    name: string;
+    filters: DataTableFilters;
+    onChange?: (filters: DataTableFilters) => void;
+}
+
+interface DataTableFilterState {
+    anchorEl?: HTMLElement;
+    filters: DataTableFilters;
+    prevFilters: DataTableFilters;
+}
+
+export const DataTableFiltersPopover = withStyles(styles)(
+    class extends React.Component<DataTableFilterProps & WithStyles<CssRules>, DataTableFilterState> {
+        state: DataTableFilterState = {
+            anchorEl: undefined,
+            filters: createTree(),
+            prevFilters: createTree(),
+        };
+        icon = React.createRef<HTMLElement>();
+
+        render() {
+            const { name, classes, children } = this.props;
+            const isActive = getNodeDescendants('')(this.state.filters).some(f => f.selected);
+            return <>
+                <Tooltip title='Filters'>
+                    <ButtonBase
+                        className={classnames([classes.root, { [classes.active]: isActive }])}
+                        component="span"
+                        onClick={this.open}
+                        disableRipple>
+                        {children}
+                        <i className={classnames(["fas fa-filter", classes.icon])}
+                            data-fa-transform="shrink-3"
+                            ref={this.icon} />
+                    </ButtonBase>
+                </Tooltip>
+                <Popover
+                    anchorEl={this.state.anchorEl}
+                    open={!!this.state.anchorEl}
+                    anchorOrigin={DefaultTransformOrigin}
+                    transformOrigin={DefaultTransformOrigin}
+                    onClose={this.cancel}>
+                    <Card>
+                        <CardContent>
+                            <Typography variant="caption">
+                                {name}
+                            </Typography>
+                        </CardContent>
+                        <DataTableFiltersTree
+                            filters={this.state.filters}
+                            onChange={filters => this.setState({ filters })} />
+                        <CardActions>
+                            <Button
+                                color="primary"
+                                variant="raised"
+                                size="small"
+                                onClick={this.submit}>
+                                Ok
+                            </Button>
+                            <Button
+                                color="primary"
+                                variant="outlined"
+                                size="small"
+                                onClick={this.cancel}>
+                                Cancel
+                            </Button>
+                        </CardActions >
+                    </Card>
+                </Popover>
+            </>;
+        }
+
+        static getDerivedStateFromProps(props: DataTableFilterProps, state: DataTableFilterState): DataTableFilterState {
+            return props.filters !== state.prevFilters
+                ? { ...state, filters: props.filters, prevFilters: props.filters }
+                : state;
+        }
+
+        open = () => {
+            this.setState({ anchorEl: this.icon.current || undefined });
+        }
+
+        submit = () => {
+            const { onChange } = this.props;
+            if (onChange) {
+                onChange(this.state.filters);
+            }
+            this.setState({ anchorEl: undefined });
+        }
+
+        cancel = () => {
+            this.setState(prev => ({
+                ...prev,
+                filters: prev.prevFilters,
+                anchorEl: undefined
+            }));
+        }
+
+        setFilters = (filters: DataTableFilters) => {
+            this.setState({ filters });
+        }
+
+    }
+);
diff --git a/src/components/data-table-filters/data-table-filters-tree.tsx b/src/components/data-table-filters/data-table-filters-tree.tsx
new file mode 100644 (file)
index 0000000..b13224b
--- /dev/null
@@ -0,0 +1,73 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Tree, toggleNodeSelection, getNode, initTreeNode, getNodeChildrenIds } from '~/models/tree';
+import { Tree as TreeComponent, TreeItem, TreeItemStatus } from '~/components/tree/tree';
+import { noop, map } from "lodash/fp";
+import { toggleNodeCollapse } from '~/models/tree';
+import { countNodes, countChildren } from '~/models/tree';
+
+export interface DataTableFilterItem {
+    name: string;
+}
+
+export type DataTableFilters = Tree<DataTableFilterItem>;
+
+export interface DataTableFilterProps {
+    filters: DataTableFilters;
+    onChange?: (filters: DataTableFilters) => void;
+}
+
+export class DataTableFiltersTree extends React.Component<DataTableFilterProps> {
+
+    render() {
+        const { filters } = this.props;
+        const hasSubfilters = countNodes(filters) !== countChildren('')(filters);
+        return <TreeComponent
+            levelIndentation={hasSubfilters ? 20 : 0}
+            itemRightPadding={20}
+            items={filtersToTree(filters)}
+            render={renderItem}
+            showSelection
+            onContextMenu={noop}
+            toggleItemActive={noop}
+            toggleItemOpen={this.toggleOpen}
+            toggleItemSelection={this.toggleFilter}
+        />;
+    }
+
+    toggleFilter = (_: React.MouseEvent, item: TreeItem<DataTableFilterItem>) => {
+        const { onChange = noop } = this.props;
+        onChange(toggleNodeSelection(item.id)(this.props.filters));
+    }
+
+    toggleOpen = (_: React.MouseEvent, item: TreeItem<DataTableFilterItem>) => {
+        const { onChange = noop } = this.props;
+        onChange(toggleNodeCollapse(item.id)(this.props.filters));
+    }
+}
+
+const renderItem = (item: TreeItem<DataTableFilterItem>) =>
+    <span>{item.data.name}</span>;
+
+const filterToTreeItem = (filters: DataTableFilters) =>
+    (id: string): TreeItem<any> => {
+        const node = getNode(id)(filters) || initTreeNode({ id: '', value: 'InvalidNode' });
+        const items = getNodeChildrenIds(node.id)(filters)
+            .map(filterToTreeItem(filters));
+
+        return {
+            active: node.active,
+            data: node.value,
+            id: node.id,
+            items: items.length > 0 ? items : undefined,
+            open: node.expanded,
+            selected: node.selected,
+            status: TreeItemStatus.LOADED,
+        };
+    };
+
+const filtersToTree = (filters: DataTableFilters): TreeItem<DataTableFilterItem>[] =>
+    map(filterToTreeItem(filters), getNodeChildrenIds('')(filters));
index 11607e4b22d108277f4f2dd8c32028ec88bcb36c..7033d369e7dcdb0f1daa0e9168fac1427719db3c 100644 (file)
@@ -23,6 +23,10 @@ import {
 } from "@material-ui/core";
 import * as classnames from "classnames";
 import { DefaultTransformOrigin } from "../popover/helpers";
+import { createTree, initTreeNode, mapTree } from '~/models/tree';
+import { DataTableFilters as DataTableFiltersModel, DataTableFiltersTree } from "./data-table-filters-tree";
+import { pipe } from 'lodash/fp';
+import { setNode } from '~/models/tree';
 
 export type CssRules = "root" | "icon" | "active" | "checkbox";
 
@@ -74,14 +78,27 @@ interface DataTableFilterState {
     anchorEl?: HTMLElement;
     filters: DataTableFilterItem[];
     prevFilters: DataTableFilterItem[];
+    filtersTree: DataTableFiltersModel;
 }
 
+const filters: DataTableFiltersModel = pipe(
+    createTree,
+    setNode(initTreeNode({ id: 'Project', value: { name: 'Project' } })),
+    setNode(initTreeNode({ id: 'Process', value: { name: 'Process' } })),
+    setNode(initTreeNode({ id: 'Data collection', value: { name: 'Data collection' } })),
+    setNode(initTreeNode({ id: 'General', parent: 'Data collection', value: { name: 'General' } })),
+    setNode(initTreeNode({ id: 'Output', parent: 'Data collection', value: { name: 'Output' } })),
+    setNode(initTreeNode({ id: 'Log', parent: 'Data collection', value: { name: 'Log' } })),
+    mapTree(node => ({...node, selected: true})),
+)();
+
 export const DataTableFilters = withStyles(styles)(
     class extends React.Component<DataTableFilterProps & WithStyles<CssRules>, DataTableFilterState> {
         state: DataTableFilterState = {
             anchorEl: undefined,
             filters: [],
-            prevFilters: []
+            prevFilters: [],
+            filtersTree: filters,
         };
         icon = React.createRef<HTMLElement>();
 
@@ -128,6 +145,9 @@ export const DataTableFilters = withStyles(styles)(
                                 </ListItem>
                             )}
                         </List>
+                        <DataTableFiltersTree
+                            filters={this.state.filtersTree}
+                            onChange={filtersTree => this.setState({ filtersTree })} />
                         <CardActions>
                             <Button
                                 color="primary"
index a5f95506a816da0decfd4754705ea4cb8c0cddc6..28e93beed0cebadc1fbaa54324cc2729a3a86ed5 100644 (file)
@@ -3,15 +3,16 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from "react";
-import { DataTableFilterItem } from "../data-table-filters/data-table-filters";
+import { DataTableFilters } from "../data-table-filters/data-table-filters-tree";
+import { createTree } from '~/models/tree';
 
-export interface DataColumn<T, F extends DataTableFilterItem = DataTableFilterItem> {
+export interface DataColumn<T> {
     key?: React.Key;
     name: string;
     selected: boolean;
     configurable: boolean;
     sortDirection?: SortDirection;
-    filters: F[];
+    filters: DataTableFilters;
     render: (item: T) => React.ReactElement<any>;
     renderHeader?: () => React.ReactElement<any>;
 }
@@ -34,13 +35,13 @@ export const resetSortDirection = <T>(column: DataColumn<T>): DataColumn<T> => {
     return column.sortDirection ? { ...column, sortDirection: SortDirection.NONE } : column;
 };
 
-export const createDataColumn = <T, F extends DataTableFilterItem>(dataColumn: Partial<DataColumn<T, F>>): DataColumn<T, F> => ({
+export const createDataColumn = <T>(dataColumn: Partial<DataColumn<T>>): DataColumn<T> => ({
     key: '',
     name: '',
     selected: true,
     configurable: true,
     sortDirection: SortDirection.NONE,
-    filters: [],
+    filters: createTree(),
     render: () => React.createElement('span'),
     ...dataColumn,
 });
index c2f5d4acc1a85f10e6565ee164942e8fda7bc833..d0b83b969695d00dfc4e31170e0d072f112e9055 100644 (file)
@@ -4,12 +4,15 @@
 
 import * as React from "react";
 import { mount, configure } from "enzyme";
+import { pipe } from 'lodash/fp';
 import { TableHead, TableCell, Typography, TableBody, Button, TableSortLabel } from "@material-ui/core";
 import * as Adapter from "enzyme-adapter-react-16";
 import { DataTable, DataColumns } from "./data-table";
-import { DataTableFilters } from "../data-table-filters/data-table-filters";
+import { DataTableFilters } from "~/components/data-table-filters/data-table-filters";
 import { SortDirection, createDataColumn } from "./data-column";
-import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { DataTableFiltersPopover } from '~/components/data-table-filters/data-table-filters-popover';
+import { createTree, setNode, initTreeNode } from '~/models/tree';
+import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters-tree";
 
 configure({ adapter: new Adapter() });
 
@@ -139,12 +142,12 @@ describe("<DataTable />", () => {
     it("passes sorting props to <TableSortLabel />", () => {
         const columns: DataColumns<string> = [
             createDataColumn({
-            name: "Column 1",
-            sortDirection: SortDirection.ASC,
-            selected: true,
-            configurable: true,
-            render: (item) => <Typography>{item}</Typography>
-        })];
+                name: "Column 1",
+                sortDirection: SortDirection.ASC,
+                selected: true,
+                configurable: true,
+                render: (item) => <Typography>{item}</Typography>
+            })];
         const onSortToggle = jest.fn();
         const dataTable = mount(<DataTable
             columns={columns}
@@ -180,13 +183,17 @@ describe("<DataTable />", () => {
         expect(dataTable.find(DataTableFilters)).toHaveLength(0);
     });
 
-    it("passes filter props to <DataTableFilter />", () => {
+    it("passes filter props to <DataTableFiltersPopover />", () => {
+        const filters = pipe(
+            () => createTree<DataTableFilterItem>(),
+            setNode(initTreeNode({ id: 'filter', value: { name: 'filter' } }))
+        );
         const columns: DataColumns<string> = [{
             name: "Column 1",
             sortDirection: SortDirection.ASC,
             selected: true,
             configurable: true,
-            filters: [{ name: "Filter 1", selected: true }],
+            filters: filters(),
             render: (item) => <Typography>{item}</Typography>
         }];
         const onFiltersChange = jest.fn();
@@ -198,8 +205,8 @@ describe("<DataTable />", () => {
             onRowDoubleClick={jest.fn()}
             onSortToggle={jest.fn()}
             onContextMenu={jest.fn()} />);
-        expect(dataTable.find(DataTableFilters).prop("filters")).toBe(columns[0].filters);
-        dataTable.find(DataTableFilters).prop("onChange")([]);
+        expect(dataTable.find(DataTableFiltersPopover).prop("filters")).toBe(columns[0].filters);
+        dataTable.find(DataTableFiltersPopover).prop("onChange")([]);
         expect(onFiltersChange).toHaveBeenCalledWith([], columns[0]);
     });
 
index 25d81c62fa7dc89a8135c8a718c5dc02800d22a6..d9157a6a5d0e14851f141309c8a89f96af818c50 100644 (file)
@@ -5,10 +5,12 @@
 import * as React from 'react';
 import { Table, TableBody, TableRow, TableCell, TableHead, TableSortLabel, StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core';
 import { DataColumn, SortDirection } from './data-column';
-import { DataTableFilters, DataTableFilterItem } from "../data-table-filters/data-table-filters";
 import { DataTableDefaultView } from '../data-table-default-view/data-table-default-view';
+import { DataTableFilters } from '../data-table-filters/data-table-filters-tree';
+import { DataTableFiltersPopover } from '../data-table-filters/data-table-filters-popover';
+import { countNodes } from '~/models/tree';
 
-export type DataColumns<T, F extends DataTableFilterItem = DataTableFilterItem> = Array<DataColumn<T, F>>;
+export type DataColumns<T> = Array<DataColumn<T>>;
 
 export interface DataTableDataProps<T> {
     items: T[];
@@ -17,7 +19,7 @@ export interface DataTableDataProps<T> {
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
     onRowDoubleClick: (event: React.MouseEvent<HTMLTableRowElement>, item: T) => void;
     onSortToggle: (column: DataColumn<T>) => void;
-    onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<T>) => void;
+    onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
     extractKey?: (item: T) => React.Key;
     working?: boolean;
     defaultView?: React.ReactNode;
@@ -81,15 +83,15 @@ export const DataTable = withStyles(styles)(
             return <TableCell key={key || index}>
                 {renderHeader ?
                     renderHeader() :
-                    filters.length > 0
-                        ? <DataTableFilters
+                    countNodes(filters) > 0
+                        ? <DataTableFiltersPopover
                             name={`${name} filters`}
                             onChange={filters =>
                                 onFiltersChange &&
                                 onFiltersChange(filters, column)}
                             filters={filters}>
                             {name}
-                        </DataTableFilters>
+                        </DataTableFiltersPopover>
                         : sortDirection
                             ? <TableSortLabel
                                 active={sortDirection !== SortDirection.NONE}
index 4cbefbd2b0eb0cbb1a0b9cc8d5927cc66a05e90c..c64a722139b3d4c5a9690e1ee0120e673c52cf7f 100644 (file)
@@ -88,6 +88,8 @@ export interface TreeProps<T> {
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
     render: (item: TreeItem<T>, level?: number) => ReactElement<{}>;
     showSelection?: boolean | ((item: TreeItem<T>) => boolean);
+    levelIndentation?: number;
+    itemRightPadding?: number;
     toggleItemActive: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
     toggleItemOpen: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
     toggleItemSelection?: (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>) => void;
@@ -103,10 +105,16 @@ export const Tree = withStyles(styles)(
                 ? this.props.showSelection
                 : () => this.props.showSelection ? true : false;
 
+            const { levelIndentation = 20, itemRightPadding = 20 } = this.props;
+
             return <List component="div" className={list}>
                 {items && items.map((it: TreeItem<T>, idx: number) =>
                     <div key={`item/${level}/${idx}`}>
-                        <ListItem button className={listItem} style={{ paddingLeft: (level + 1) * 20 }}
+                        <ListItem button className={listItem} 
+                            style={{ 
+                                paddingLeft: (level + 1) * levelIndentation,
+                                paddingRight: itemRightPadding,
+                            }}
                             disableRipple={disableRipple}
                             onClick={event => toggleItemActive(event, it)}
                             onContextMenu={this.handleRowContextMenu(it)}>
index 53c6230143b9a78c8881d8b92baeb5e4ea305c1d..2b16ea2523a4de9d4cb4a776e402fc6ce2e7168e 100644 (file)
@@ -22,3 +22,9 @@ export interface CollectionResource extends TrashableResource {
 export const getCollectionUrl = (uuid: string) => {
     return `/collections/${uuid}`;
 };
+
+export enum CollectionType {
+    GENERAL = 'nil',
+    OUTPUT = 'output',
+    LOG = 'log',
+}
index fe52a97b0fcdd3806579318d968f2efc8206f364..bec2f758a478335e6ef437afe6592dde144ce729 100644 (file)
@@ -95,6 +95,12 @@ export const getNodeAncestorsIds = (id: string) => <T>(tree: Tree<T>): string[]
 export const getNodeDescendants = (id: string, limit = Infinity) => <T>(tree: Tree<T>) =>
     mapIdsToNodes(getNodeDescendantsIds(id, limit)(tree))(tree);
 
+export const countNodes = <T>(tree: Tree<T>) =>
+    getNodeDescendantsIds('')(tree).length;
+
+export const countChildren = (id: string) => <T>(tree: Tree<T>) =>
+    getNodeChildren('')(tree).length;
+
 export const getNodeDescendantsIds = (id: string, limit = Infinity) => <T>(tree: Tree<T>): string[] => {
     const node = getNode(id)(tree);
     const children = node ? node.children :
@@ -120,19 +126,19 @@ export const mapIdsToNodes = (ids: string[]) => <T>(tree: Tree<T>) =>
     ids.map(id => getNode(id)(tree)).filter((node): node is TreeNode<T> => node !== undefined);
 
 export const activateNode = (id: string) => <T>(tree: Tree<T>) =>
-    mapTree(node => node.id === id ? { ...node, active: true } : { ...node, active: false })(tree);
+    mapTree((node: TreeNode<T>) => node.id === id ? { ...node, active: true } : { ...node, active: false })(tree);
 
 export const deactivateNode = <T>(tree: Tree<T>) =>
-    mapTree(node => node.active ? { ...node, active: false } : node)(tree);
+    mapTree((node: TreeNode<T>) => node.active ? { ...node, active: false } : node)(tree);
 
 export const expandNode = (...ids: string[]) => <T>(tree: Tree<T>) =>
-    mapTree(node => ids.some(id => id === node.id) ? { ...node, expanded: true } : node)(tree);
+    mapTree((node: TreeNode<T>) => ids.some(id => id === node.id) ? { ...node, expanded: true } : node)(tree);
 
 export const collapseNode = (...ids: string[]) => <T>(tree: Tree<T>) =>
-    mapTree(node => ids.some(id => id === node.id) ? { ...node, expanded: false } : node)(tree);
+    mapTree((node: TreeNode<T>) => ids.some(id => id === node.id) ? { ...node, expanded: false } : node)(tree);
 
 export const toggleNodeCollapse = (...ids: string[]) => <T>(tree: Tree<T>) =>
-    mapTree(node => ids.some(id => id === node.id) ? { ...node, expanded: !node.expanded } : node)(tree);
+    mapTree((node: TreeNode<T>) => ids.some(id => id === node.id) ? { ...node, expanded: !node.expanded } : node)(tree);
 
 export const setNodeStatus = (id: string) => (status: TreeNodeStatus) => <T>(tree: Tree<T>) => {
     const node = getNode(id)(tree);
@@ -175,6 +181,10 @@ export const deselectNodes = (id: string | string[]) => <T>(tree: Tree<T>) => {
     return ids.reduce((tree, id) => deselectNode(id)(tree), tree);
 };
 
+export const getSelectedNodes = <T>(tree: Tree<T>) =>
+    getNodeDescendants('')(tree)
+        .filter(node => node.selected);
+
 export const initTreeNode = <T>(data: Pick<TreeNode<T>, 'id' | 'value'> & { parent?: string }): TreeNode<T> => ({
     children: [],
     active: false,
index 5f646de5f72911af6708ae78b80e2155ffe8a1a1..2ddd6bf0d337a658019192d6f5878bd838a08e46 100644 (file)
@@ -60,6 +60,12 @@ describe("FilterBuilder", () => {
         ).toEqual(`["etag","in",["etagValue1","etagValue2"]]`);
     });
 
+    it("should add 'not in' rule for set", () => {
+        expect(
+            filters.addNotIn("etag", ["etagValue1", "etagValue2"]).getFilters()
+        ).toEqual(`["etag","not in",["etagValue1","etagValue2"]]`);
+    });
+
     it("should add multiple rules", () => {
         expect(
             filters
@@ -73,6 +79,6 @@ describe("FilterBuilder", () => {
         expect(new FilterBuilder()
             .addIn("etag", ["etagValue1", "etagValue2"], "myPrefix")
             .getFilters())
-            .toEqual(`["my_prefix.etag","in",["etagValue1","etagValue2"]]`);
+            .toEqual(`["myPrefix.etag","in",["etagValue1","etagValue2"]]`);
     });
 });
index e36765ba5b5a6a145f7bae29529238438fb13420..1ebf488636115c7dfb2cd9bcbf420d79ee82fa1b 100644 (file)
@@ -31,6 +31,10 @@ export class FilterBuilder {
         return this.addCondition(field, "in", value, "", "", resourcePrefix);
     }
 
+    public addNotIn(field: string, value?: string | string[], resourcePrefix?: string) {
+        return this.addCondition(field, "not in", value, "", "", resourcePrefix);
+    }
+
     public addGt(field: string, value?: string, resourcePrefix?: string) {
         return this.addCondition(field, ">", value, "", "", resourcePrefix);
     }
@@ -62,7 +66,7 @@ export class FilterBuilder {
             }
 
             const resPrefix = resourcePrefix
-                ? _.snakeCase(resourcePrefix) + "."
+                ? resourcePrefix + "."
                 : "";
 
             this.filters += `${this.filters ? "," : ""}["${resPrefix}${_.snakeCase(field)}","${cond}",${value}]`;
index a58d20edb17eea49270caf32a5cc959bef7ecb82..7797ae6cefc27759d3f994b0d7f14f9225b5f193 100644 (file)
@@ -3,14 +3,14 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { unionize, ofType, UnionOf } from "~/common/unionize";
-import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters";
 import { DataColumns } from "~/components/data-table/data-table";
+import { DataTableFilters } from '~/components/data-table-filters/data-table-filters-tree';
 
 export const dataExplorerActions = unionize({
     RESET_PAGINATION: ofType<{ id: string }>(),
     REQUEST_ITEMS: ofType<{ id: string }>(),
     SET_COLUMNS: ofType<{ id: string, columns: DataColumns<any> }>(),
-    SET_FILTERS: ofType<{ id: string, columnName: string, filters: DataTableFilterItem[] }>(),
+    SET_FILTERS: ofType<{ id: string, columnName: string, filters: DataTableFilters }>(),
     SET_ITEMS: ofType<{ id: string, items: any[], page: number, rowsPerPage: number, itemsAvailable: number }>(),
     SET_PAGE: ofType<{ id: string, page: number }>(),
     SET_ROWS_PER_PAGE: ofType<{ id: string, rowsPerPage: number }>(),
@@ -28,7 +28,7 @@ export const bindDataExplorerActions = (id: string) => ({
         dataExplorerActions.REQUEST_ITEMS({ id }),
     SET_COLUMNS: (payload: { columns: DataColumns<any> }) =>
         dataExplorerActions.SET_COLUMNS({ ...payload, id }),
-    SET_FILTERS: (payload: { columnName: string, filters: DataTableFilterItem[] }) =>
+    SET_FILTERS: (payload: { columnName: string, filters: DataTableFilters }) =>
         dataExplorerActions.SET_FILTERS({ ...payload, id }),
     SET_ITEMS: (payload: { items: any[], page: number, rowsPerPage: number, itemsAvailable: number }) =>
         dataExplorerActions.SET_ITEMS({ ...payload, id }),
index 934af7be94ca04ebe369a7911403c09810592684..80ab514cb41440b9a3356f62ed6379a4ec08896c 100644 (file)
@@ -5,9 +5,10 @@
 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";
 import { DataExplorer } from './data-explorer-reducer';
 import { ListResults } from '~/services/common-service/common-resource-service';
+import { createTree } from "~/models/tree";
+import { DataTableFilters } from "~/components/data-table-filters/data-table-filters-tree";
 
 export abstract class DataExplorerMiddlewareService {
     protected readonly id: string;
@@ -20,20 +21,19 @@ 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) : [];
+    public getColumnFilters<T>(columns: DataColumns<T>, columnName: string): DataTableFilters {
+        return getDataExplorerColumnFilters(columns, columnName);
     }
 
     abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>): void;
 }
 
-export const getDataExplorerColumnFilters = <T, F extends DataTableFilterItem>(columns: DataColumns<T, F>, columnName: string): F[] => {
+export const getDataExplorerColumnFilters = <T>(columns: DataColumns<T>, columnName: string): DataTableFilters => {
     const column = columns.find(c => c.name === columnName);
-    return column ? column.filters.filter(f => f.selected) : [];
+    return column ? column.filters : createTree();
 };
 
-export const dataExplorerToListParams = <R>(dataExplorer: DataExplorer) => ({
+export const dataExplorerToListParams = (dataExplorer: DataExplorer) => ({
     limit: dataExplorer.rowsPerPage,
     offset: dataExplorer.page * dataExplorer.rowsPerPage,
 });
index 814d5855c0b6a45b0ee0224d73e382f13b561560..00931bf8e3c95cd5d98404ac74a14b136253536a 100644 (file)
@@ -8,6 +8,8 @@ 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";
+import { createTree } from '~/models/tree';
+import { DataTableFilterItem } from "~/components/data-table-filters/data-table-filters-tree";
 
 
 describe("DataExplorerMiddleware", () => {
@@ -20,7 +22,7 @@ describe("DataExplorerMiddleware", () => {
                 selected: true,
                 configurable: false,
                 sortDirection: SortDirection.NONE,
-                filters: [],
+                filters: createTree<DataTableFilterItem>(),
                 render: jest.fn()
             }],
             requestItems: jest.fn(),
@@ -48,7 +50,7 @@ describe("DataExplorerMiddleware", () => {
                 selected: true,
                 configurable: false,
                 sortDirection: SortDirection.NONE,
-                filters: [],
+                filters: createTree<DataTableFilterItem>(),
                 render: jest.fn()
             }],
             requestItems: jest.fn(),
@@ -115,7 +117,7 @@ describe("DataExplorerMiddleware", () => {
         };
         const next = jest.fn();
         const middleware = dataExplorerMiddleware(service)(api)(next);
-        middleware(dataExplorerActions.SET_FILTERS({ id: service.getId(), columnName: "", filters: [] }));
+        middleware(dataExplorerActions.SET_FILTERS({ id: service.getId(), columnName: "", filters: createTree() }));
         expect(api.dispatch).toHaveBeenCalledTimes(2);
     });
 
index 1657ab70ed7bc1fbe13bc4c691f38d41084f45c2..613bf278edd81d67ad73cc84402ab991aa8353a7 100644 (file)
@@ -4,8 +4,8 @@
 
 import { DataColumn, toggleSortDirection, resetSortDirection, SortDirection } from "~/components/data-table/data-column";
 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";
+import { DataTableFilters } from "~/components/data-table-filters/data-table-filters-tree";
 
 export interface DataExplorer {
     columns: DataColumns<any>;
@@ -103,7 +103,7 @@ const toggleColumn = (columnName: string) =>
         ? { ...column, selected: !column.selected }
         : column;
 
-const setFilters = (columnName: string, filters: DataTableFilterItem[]) =>
+const setFilters = (columnName: string, filters: DataTableFilters) =>
     (column: DataColumn<any>) => column.name === columnName
         ? { ...column, filters }
         : column;
index d7d54dedb03ca5500cb0a5da269cbe7ed6c7fc18..87f49f34f281221da6bb3ea7e33df5b3852d520d 100644 (file)
@@ -2,8 +2,8 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { DataExplorerMiddlewareService } from "../data-explorer/data-explorer-middleware-service";
-import { FavoritePanelColumnNames, FavoritePanelFilter } from "~/views/favorite-panel/favorite-panel";
+import { DataExplorerMiddlewareService } from "~/store/data-explorer/data-explorer-middleware-service";
+import { FavoritePanelColumnNames } from "~/views/favorite-panel/favorite-panel";
 import { RootState } from "../store";
 import { DataColumns } from "~/components/data-table/data-table";
 import { ServiceRepository } from "~/services/services";
@@ -21,6 +21,8 @@ import { progressIndicatorActions } from '~/store/progress-indicator/progress-in
 import { getDataExplorer } from "~/store/data-explorer/data-explorer-reducer";
 import { loadMissingProcessesInformation } from "~/store/project-panel/project-panel-middleware-service";
 import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
+import { getDataExplorerColumnFilters } from '~/store/data-explorer/data-explorer-middleware-service';
+import { serializeSimpleObjectTypeFilters } from '../resource-type-filters/resource-type-filters';
 
 export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -32,9 +34,10 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
         if (!dataExplorer) {
             api.dispatch(favoritesPanelDataExplorerIsNotSet());
         } else {
-            const columns = dataExplorer.columns as DataColumns<string, FavoritePanelFilter>;
+            const columns = dataExplorer.columns as DataColumns<string>;
             const sortColumn = getSortColumn(dataExplorer);
-            const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
+            const typeFilters = serializeSimpleObjectTypeFilters(getDataExplorerColumnFilters(columns, FavoritePanelColumnNames.TYPE));
+
 
             const linkOrder = new OrderBuilder<LinkResource>();
             const contentOrder = new OrderBuilder<GroupContentsResource>();
@@ -59,9 +62,10 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
                         linkOrder: linkOrder.getOrder(),
                         contentOrder: contentOrder.getOrder(),
                         filters: new FilterBuilder()
-                            .addIsA("headUuid", typeFilters.map(filter => filter.type))
                             .addILike("name", dataExplorer.searchValue)
-                            .getFilters()
+                            .addIsA("headUuid", typeFilters)
+                            .getFilters(),
+
                     });
                 api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
                 api.dispatch(resourcesActions.SET_RESOURCES(response.items));
index ef720923b317c0c0c25a4724b63988c0a6c52c1b..21598fad1c6072eb7de02ac7d64215af1f42bfa9 100644 (file)
@@ -2,10 +2,9 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
-import { propertiesActions } from "~/store/properties/properties-actions";
 import { Dispatch } from 'redux';
-import { ServiceRepository } from "~/services/services";
+import { bindDataExplorerActions } from "~/store/data-explorer/data-explorer-action";
+import { propertiesActions } from "~/store/properties/properties-actions";
 import { RootState } from '~/store/store';
 import { getProperty } from "~/store/properties/properties";
 
@@ -15,7 +14,7 @@ export const IS_PROJECT_PANEL_TRASHED = 'isProjectPanelTrashed';
 export const projectPanelActions = bindDataExplorerActions(PROJECT_PANEL_ID);
 
 export const openProjectPanel = (projectUuid: string) =>
-    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    (dispatch: Dispatch) => {
         dispatch(propertiesActions.SET_PROPERTY({ key: PROJECT_PANEL_CURRENT_UUID, value: projectUuid }));
         dispatch(projectPanelActions.REQUEST_ITEMS());
     };
index 458444f3b943b053af7debd291858d4720cb66bc..36672e99ac19003ede9f9ade9240d870de496d14 100644 (file)
@@ -14,7 +14,7 @@ import { DataColumns } from "~/components/data-table/data-table";
 import { ServiceRepository } from "~/services/services";
 import { SortDirection } from "~/components/data-table/data-column";
 import { OrderBuilder, OrderDirection } from "~/services/api/order-builder";
-import { FilterBuilder } from "~/services/api/filter-builder";
+import { FilterBuilder, joinFilters } from "~/services/api/filter-builder";
 import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
 import { updateFavorites } from "../favorites/favorites-actions";
 import { PROJECT_PANEL_CURRENT_UUID, IS_PROJECT_PANEL_TRASHED, projectPanelActions } from './project-panel-action';
@@ -32,6 +32,7 @@ import { getResource } from "~/store/resources/resources";
 import { CollectionResource } from "~/models/collection";
 import { resourcesDataActions } from "~/store/resources-data/resources-data-actions";
 import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
+import { serializeResourceTypeFilters } from '../resource-type-filters/resource-type-filters';
 
 export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -115,15 +116,22 @@ export const getParams = (dataExplorer: DataExplorer, isProjectTrashed: boolean)
 });
 
 export const getFilters = (dataExplorer: DataExplorer) => {
-    const columns = dataExplorer.columns as DataColumns<string, ProjectPanelFilter>;
-    const typeFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.TYPE);
-    const statusFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.STATUS);
-    return new FilterBuilder()
-        .addIsA("uuid", typeFilters.map(f => f.type))
+    const columns = dataExplorer.columns as DataColumns<string>;
+    const typeFilters = serializeResourceTypeFilters(getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.TYPE));
+
+    // TODO: Extract group contents name filter
+    const nameFilters = new FilterBuilder()
         .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
         .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
         .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
         .getFilters();
+
+    return joinFilters(
+        typeFilters,
+        nameFilters,
+    );
+    // TODO: Restore process status filters
+    // const statusFilters = getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.STATUS);
 };
 
 export const getOrder = (dataExplorer: DataExplorer) => {
diff --git a/src/store/resource-type-filters/resource-type-filters.test.ts b/src/store/resource-type-filters/resource-type-filters.test.ts
new file mode 100644 (file)
index 0000000..02f017e
--- /dev/null
@@ -0,0 +1,50 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { getInitialResourceTypeFilters, serializeResourceTypeFilters, ObjectTypeFilter, CollectionTypeFilter } from './resource-type-filters';
+import { ResourceKind } from '~/models/resource';
+import { deselectNode } from '~/models/tree';
+import { pipe } from 'lodash/fp';
+
+describe("serializeResourceTypeFilters", () => {
+    it("should serialize all filters", () => {
+        const filters = getInitialResourceTypeFilters();
+        const serializedFilters = serializeResourceTypeFilters(filters);
+        expect(serializedFilters)
+            .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.PROCESS}","${ResourceKind.COLLECTION}"]]`);
+    });
+
+    it("should serialize all but collection filters", () => {
+        const filters = deselectNode(ObjectTypeFilter.COLLECTION)(getInitialResourceTypeFilters());
+        const serializedFilters = serializeResourceTypeFilters(filters);
+        expect(serializedFilters)
+            .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.PROCESS}"]]`);
+    });
+
+    it("should serialize output collections and projects", () => {
+        const filters = pipe(
+            () => getInitialResourceTypeFilters(),
+            deselectNode(ObjectTypeFilter.PROCESS),
+            deselectNode(CollectionTypeFilter.GENERAL_COLLECTION),
+            deselectNode(CollectionTypeFilter.LOG_COLLECTION),
+        )();
+
+        const serializedFilters = serializeResourceTypeFilters(filters);
+        expect(serializedFilters)
+            .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.COLLECTION}"]],["collections.properties.type","in",["output"]]`);
+    });
+
+    it("should serialize general and log collections", () => {
+        const filters = pipe(
+            () => getInitialResourceTypeFilters(),
+            deselectNode(ObjectTypeFilter.PROJECT),
+            deselectNode(ObjectTypeFilter.PROCESS),
+            deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION)
+        )();
+
+        const serializedFilters = serializeResourceTypeFilters(filters);
+        expect(serializedFilters)
+            .toEqual(`["uuid","is_a",["${ResourceKind.COLLECTION}"]],["collections.properties.type","not in",["output"]]`);
+    });
+});
diff --git a/src/store/resource-type-filters/resource-type-filters.ts b/src/store/resource-type-filters/resource-type-filters.ts
new file mode 100644 (file)
index 0000000..78777be
--- /dev/null
@@ -0,0 +1,141 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { difference, pipe, values, includes, __ } from 'lodash/fp';
+import { createTree, setNode, TreeNodeStatus, TreeNode, Tree } from '~/models/tree';
+import { DataTableFilterItem, DataTableFilters } from '~/components/data-table-filters/data-table-filters-tree';
+import { ResourceKind } from '~/models/resource';
+import { FilterBuilder } from '~/services/api/filter-builder';
+import { getSelectedNodes } from '~/models/tree';
+import { CollectionType } from '~/models/collection';
+import { GroupContentsResourcePrefix } from '~/services/groups-service/groups-service';
+
+export enum ObjectTypeFilter {
+    PROJECT = 'Project',
+    PROCESS = 'Process',
+    COLLECTION = 'Data Collection',
+}
+
+export enum CollectionTypeFilter {
+    GENERAL_COLLECTION = 'General',
+    OUTPUT_COLLECTION = 'Output',
+    LOG_COLLECTION = 'Log',
+}
+
+const initFilter = (name: string, parent = '') =>
+    setNode<DataTableFilterItem>({
+        id: name,
+        value: { name },
+        parent,
+        children: [],
+        active: false,
+        selected: true,
+        expanded: false,
+        status: TreeNodeStatus.LOADED,
+    });
+
+export const getSimpleObjectTypeFilters = pipe(
+    (): DataTableFilters => createTree<DataTableFilterItem>(),
+    initFilter(ObjectTypeFilter.PROJECT),
+    initFilter(ObjectTypeFilter.PROCESS),
+    initFilter(ObjectTypeFilter.COLLECTION),
+);
+
+export const getInitialResourceTypeFilters = pipe(
+    (): DataTableFilters => createTree<DataTableFilterItem>(),
+    initFilter(ObjectTypeFilter.PROJECT),
+    initFilter(ObjectTypeFilter.PROCESS),
+    initFilter(ObjectTypeFilter.COLLECTION),
+    initFilter(CollectionTypeFilter.GENERAL_COLLECTION, ObjectTypeFilter.COLLECTION),
+    initFilter(CollectionTypeFilter.OUTPUT_COLLECTION, ObjectTypeFilter.COLLECTION),
+    initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION),
+);
+
+
+const createFiltersBuilder = (filters: DataTableFilters) =>
+    ({ fb: new FilterBuilder(), selectedFilters: getSelectedNodes(filters) });
+
+const getMatchingFilters = (values: string[], filters: TreeNode<DataTableFilterItem>[]) =>
+    filters
+        .map(f => f.id)
+        .filter(includes(__, values));
+
+const objectTypeToResourceKind = (type: ObjectTypeFilter) => {
+    switch (type) {
+        case ObjectTypeFilter.PROJECT:
+            return ResourceKind.PROJECT;
+        case ObjectTypeFilter.PROCESS:
+            return ResourceKind.PROCESS;
+        case ObjectTypeFilter.COLLECTION:
+            return ResourceKind.COLLECTION;
+    }
+};
+
+const serializeObjectTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => {
+    const collectionFilters = getMatchingFilters(values(CollectionTypeFilter), selectedFilters);
+    const typeFilters = pipe(
+        () => new Set(getMatchingFilters(values(ObjectTypeFilter), selectedFilters)),
+        set => collectionFilters.length > 0
+            ? set.add(ObjectTypeFilter.COLLECTION)
+            : set,
+        set => Array.from(set)
+    )();
+
+    return {
+        fb: typeFilters.length > 0
+            ? fb.addIsA('uuid', typeFilters.map(objectTypeToResourceKind))
+            : fb,
+        selectedFilters,
+    };
+};
+
+const collectionTypeToPropertyValue = (type: CollectionTypeFilter) => {
+    switch (type) {
+        case CollectionTypeFilter.GENERAL_COLLECTION:
+            return CollectionType.GENERAL;
+        case CollectionTypeFilter.OUTPUT_COLLECTION:
+            return CollectionType.OUTPUT;
+        case CollectionTypeFilter.LOG_COLLECTION:
+            return CollectionType.LOG;
+    }
+};
+
+const serializeCollectionTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => pipe(
+    () => getMatchingFilters(values(CollectionTypeFilter), selectedFilters),
+    filters => filters.map(collectionTypeToPropertyValue),
+    mappedFilters => ({
+        fb: buildCollectiomTypeFilters({ fb, filters: mappedFilters }),
+        selectedFilters
+    })
+)();
+
+const COLLECTION_TYPES = values(CollectionType);
+
+const NON_GENERAL_COLLECTION_TYPES = difference(COLLECTION_TYPES, [CollectionType.GENERAL]);
+
+const COLLECTION_PROPERTIES_PREFIX = `${GroupContentsResourcePrefix.COLLECTION}.properties`;
+
+const buildCollectiomTypeFilters = ({ fb, filters }: { fb: FilterBuilder, filters: CollectionType[] }) => {
+    switch (true) {
+        case filters.length === 0 || filters.length === COLLECTION_TYPES.length:
+            return fb;
+        case includes(CollectionType.GENERAL, filters):
+            return fb.addNotIn('type', difference(NON_GENERAL_COLLECTION_TYPES, filters), COLLECTION_PROPERTIES_PREFIX);
+        default:
+            return fb.addIn('type', filters, COLLECTION_PROPERTIES_PREFIX);
+    }
+};
+
+export const serializeResourceTypeFilters = pipe(
+    createFiltersBuilder,
+    serializeObjectTypeFilters,
+    serializeCollectionTypeFilters,
+    ({ fb }) => fb.getFilters(),
+);
+
+export const serializeSimpleObjectTypeFilters = (filters: Tree<DataTableFilterItem>) => {
+    return getSelectedNodes(filters)
+        .map(f => f.id)
+        .map(objectTypeToResourceKind);
+};
index 9afc57b49f74e5808f7254f9d6b3362c74b03ee8..f52421a1d6581581cdf97f9613d848d6b3f74e15 100644 (file)
@@ -23,6 +23,9 @@ import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions
 import { updateResources } from "~/store/resources/resources-actions";
 import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
+import { serializeResourceTypeFilters } from '~/store//resource-type-filters/resource-type-filters';
+import { getDataExplorerColumnFilters } from '~/store/data-explorer/data-explorer-middleware-service';
+import { joinFilters } from '../../services/api/filter-builder';
 
 export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -31,9 +34,22 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
 
     async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
         const dataExplorer = api.getState().dataExplorer[this.getId()];
-        const columns = dataExplorer.columns as DataColumns<string, TrashPanelFilter>;
+        const columns = dataExplorer.columns as DataColumns<string>;
         const sortColumn = getSortColumn(dataExplorer);
-        const typeFilters = this.getColumnFilters(columns, TrashPanelColumnNames.TYPE);
+
+        const typeFilters = serializeResourceTypeFilters(getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.TYPE));
+
+        const otherFilters = new FilterBuilder()
+            .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
+            .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROCESS)
+            .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
+            .addEqual("is_trashed", true)
+            .getFilters();
+
+        const filters = joinFilters(
+            typeFilters,
+            otherFilters,
+        );
 
         const order = new OrderBuilder<ProjectResource>();
 
@@ -55,12 +71,7 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
                 .contents(userUuid, {
                     ...dataExplorerToListParams(dataExplorer),
                     order: order.getOrder(),
-                    filters: new FilterBuilder()
-                        .addIsA("uuid", typeFilters.map(f => f.type))
-                        .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.COLLECTION)
-                        .addILike("name", dataExplorer.searchValue, GroupContentsResourcePrefix.PROJECT)
-                        .addEqual("is_trashed", true)
-                        .getFilters(),
+                    filters,
                     recursive: true,
                     includeTrash: true
                 });
index 59555707d11a2e509d886abf978b3ab4a2972580..710d202dfe25997c66dcde62f44ead0d9e469b10 100644 (file)
@@ -9,8 +9,8 @@ import { getDataExplorer } from "~/store/data-explorer/data-explorer-reducer";
 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";
+import { DataTableFilters } from '~/components/data-table-filters/data-table-filters-tree';
 
 interface Props {
     id: string;
@@ -44,7 +44,7 @@ const mapDispatchToProps = () => {
             dispatch(dataExplorerActions.TOGGLE_SORT({ id, columnName: column.name }));
         },
 
-        onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<any>) => {
+        onFiltersChange: (filters: DataTableFilters, column: DataColumn<any>) => {
             dispatch(dataExplorerActions.SET_FILTERS({ id, columnName: column.name, filters }));
         },
 
index 33c901cb784e19e51b03051cfaa640d1facd571b..4682d3fc299bf6cdc6b3e8e24768a8fdc5f5f161 100644 (file)
@@ -11,7 +11,6 @@ import { RouteComponentProps } from 'react-router';
 import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
 import { SortDirection } from '~/components/data-table/data-column';
 import { ResourceKind } from '~/models/resource';
-import { resourceLabel } from '~/common/labels';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { FAVORITE_PANEL_ID } from "~/store/favorite-panel/favorite-panel-action";
 import {
@@ -28,9 +27,11 @@ import { openContextMenu, resourceKindToContextMenuKind } from '~/store/context-
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { navigateTo } from '~/store/navigation/navigation-action';
 import { ContainerRequestState } from "~/models/container-request";
-import { FavoritesState } from '../../store/favorites/favorites-reducer';
+import { FavoritesState } from '~/store/favorites/favorites-reducer';
 import { RootState } from '~/store/store';
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { createTree } from '~/models/tree';
+import { getSimpleObjectTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
 
 type CssRules = "toolbar" | "button";
 
@@ -57,57 +58,41 @@ export interface FavoritePanelFilter extends DataTableFilterItem {
     type: ResourceKind | ContainerRequestState;
 }
 
-export const favoritePanelColumns: DataColumns<string, FavoritePanelFilter> = [
+export const favoritePanelColumns: DataColumns<string> = [
     {
         name: FavoritePanelColumnNames.NAME,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.ASC,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceName uuid={uuid} />
     },
     {
         name: "Status",
         selected: true,
         configurable: true,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ProcessStatus uuid={uuid} />
     },
     {
         name: FavoritePanelColumnNames.TYPE,
         selected: true,
         configurable: true,
-        filters: [
-            {
-                name: resourceLabel(ResourceKind.COLLECTION),
-                selected: true,
-                type: ResourceKind.COLLECTION
-            },
-            {
-                name: resourceLabel(ResourceKind.PROCESS),
-                selected: true,
-                type: ResourceKind.PROCESS
-            },
-            {
-                name: resourceLabel(ResourceKind.PROJECT),
-                selected: true,
-                type: ResourceKind.PROJECT
-            }
-        ],
+        filters: getSimpleObjectTypeFilters(),
         render: uuid => <ResourceType uuid={uuid} />
     },
     {
         name: FavoritePanelColumnNames.OWNER,
         selected: true,
         configurable: true,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceOwner uuid={uuid} />
     },
     {
         name: FavoritePanelColumnNames.FILE_SIZE,
         selected: true,
         configurable: true,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceFileSize uuid={uuid} />
     },
     {
@@ -115,7 +100,7 @@ export const favoritePanelColumns: DataColumns<string, FavoritePanelFilter> = [
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceLastModifiedDate uuid={uuid} />
     }
 ];
index 1221d0d1fba4df866500dc7ed4669a7fe8f58f38..b8811476668fea67ef2abc23422759af88176086 100644 (file)
@@ -3,16 +3,18 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
+import withStyles from "@material-ui/core/styles/withStyles";
 import { DispatchProp, connect } from 'react-redux';
-import { DataColumns } from '~/components/data-table/data-table';
 import { RouteComponentProps } from 'react-router';
+import { StyleRulesCallback, WithStyles } from "@material-ui/core";
+
+import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
+import { DataColumns } from '~/components/data-table/data-table';
 import { RootState } from '~/store/store';
 import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
 import { ContainerRequestState } from '~/models/container-request';
 import { SortDirection } from '~/components/data-table/data-column';
 import { ResourceKind, Resource } from '~/models/resource';
-import { resourceLabel } from '~/common/labels';
 import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner } from '~/views-components/data-explorer/renderers';
 import { ProjectIcon } from '~/components/icon/icon';
 import { ResourceName } from '~/views-components/data-explorer/renderers';
@@ -24,9 +26,9 @@ import { navigateTo } from '~/store/navigation/navigation-action';
 import { getProperty } from '~/store/properties/properties';
 import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
-import { StyleRulesCallback, WithStyles } from "@material-ui/core";
 import { ArvadosTheme } from "~/common/custom-theme";
-import withStyles from "@material-ui/core/styles/withStyles";
+import { createTree } from '~/models/tree';
+import { getInitialResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
 
 type CssRules = 'root' | "button";
 
@@ -54,57 +56,41 @@ export interface ProjectPanelFilter extends DataTableFilterItem {
     type: ResourceKind | ContainerRequestState;
 }
 
-export const projectPanelColumns: DataColumns<string, ProjectPanelFilter> = [
+export const projectPanelColumns: DataColumns<string> = [
     {
         name: ProjectPanelColumnNames.NAME,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.ASC,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceName uuid={uuid} />
     },
     {
         name: "Status",
         selected: true,
         configurable: true,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ProcessStatus uuid={uuid} />,
     },
     {
         name: ProjectPanelColumnNames.TYPE,
         selected: true,
         configurable: true,
-        filters: [
-            {
-                name: resourceLabel(ResourceKind.COLLECTION),
-                selected: true,
-                type: ResourceKind.COLLECTION
-            },
-            {
-                name: resourceLabel(ResourceKind.PROCESS),
-                selected: true,
-                type: ResourceKind.PROCESS
-            },
-            {
-                name: resourceLabel(ResourceKind.PROJECT),
-                selected: true,
-                type: ResourceKind.PROJECT
-            }
-        ],
+        filters: getInitialResourceTypeFilters(),
         render: uuid => <ResourceType uuid={uuid} />
     },
     {
         name: ProjectPanelColumnNames.OWNER,
         selected: true,
         configurable: true,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceOwner uuid={uuid} />
     },
     {
         name: ProjectPanelColumnNames.FILE_SIZE,
         selected: true,
         configurable: true,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceFileSize uuid={uuid} />
     },
     {
@@ -112,13 +98,18 @@ export const projectPanelColumns: DataColumns<string, ProjectPanelFilter> = [
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceLastModifiedDate uuid={uuid} />
     }
 ];
 
 export const PROJECT_PANEL_ID = "projectPanel";
 
+const DEFAUL_VIEW_MESSAGES = [
+    'Your project is empty.',
+    'Please create a project or create a collection and upload a data.',
+];
+
 interface ProjectPanelDataProps {
     currentItemId: string;
     resources: ResourcesState;
@@ -145,11 +136,8 @@ export const ProjectPanel = withStyles(styles)(
                         dataTableDefaultView={
                             <DataTableDefaultView
                                 icon={ProjectIcon}
-                                messages={[
-                                    'Your project is empty.',
-                                    'Please create a project or create a collection and upload a data.'
-                                ]}/>
-                        }/>
+                                messages={DEFAUL_VIEW_MESSAGES} />
+                        } />
                 </div>;
             }
 
index 009b2abef409b23d432220eeb30cdb9680250768..ea658ee72573c1e6554a6053b4e75ae96c474295 100644 (file)
@@ -20,7 +20,9 @@ import {
     ResourceOwner,
     ResourceType
 } from '~/views-components/data-explorer/renderers';
-
+import { createTree } from '~/models/tree';
+import { getInitialResourceTypeFilters } from '../../store/resource-type-filters/resource-type-filters';
+// TODO: code clean up
 export enum SearchResultsPanelColumnNames {
     NAME = "Name",
     PROJECT = "Project",
@@ -48,64 +50,48 @@ export interface WorkflowPanelFilter extends DataTableFilterItem {
     type: ResourceKind | ContainerRequestState;
 }
 
-export const searchResultsPanelColumns: DataColumns<string, WorkflowPanelFilter> = [
+export const searchResultsPanelColumns: DataColumns<string> = [
     {
         name: SearchResultsPanelColumnNames.NAME,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.ASC,
-        filters: [],
+        filters: createTree(),
         render: (uuid: string) => <ResourceName uuid={uuid} />
     },
     {
         name: SearchResultsPanelColumnNames.PROJECT,
         selected: true,
         configurable: true,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceFileSize uuid={uuid} />
     },
     {
         name: SearchResultsPanelColumnNames.STATUS,
         selected: true,
         configurable: true,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ProcessStatus uuid={uuid} />
     },
     {
         name: SearchResultsPanelColumnNames.TYPE,
         selected: true,
         configurable: true,
-        filters: [
-            {
-                name: resourceLabel(ResourceKind.COLLECTION),
-                selected: true,
-                type: ResourceKind.COLLECTION
-            },
-            {
-                name: resourceLabel(ResourceKind.PROCESS),
-                selected: true,
-                type: ResourceKind.PROCESS
-            },
-            {
-                name: resourceLabel(ResourceKind.PROJECT),
-                selected: true,
-                type: ResourceKind.PROJECT
-            }
-        ],
+        filters: getInitialResourceTypeFilters(),
         render: (uuid: string) => <ResourceType uuid={uuid} />,
     },
     {
         name: SearchResultsPanelColumnNames.OWNER,
         selected: true,
         configurable: true,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceOwner uuid={uuid} />
     },
     {
         name: SearchResultsPanelColumnNames.FILE_SIZE,
         selected: true,
         configurable: true,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceFileSize uuid={uuid} />
     },
     {
@@ -113,7 +99,7 @@ export const searchResultsPanelColumns: DataColumns<string, WorkflowPanelFilter>
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceLastModifiedDate uuid={uuid} />
     }
 ];
index a0cf3e4fd40f2add68c0603c9d1e2760d3010c24..ae12425e1516a1dde5e05b31624a2537b97c5704 100644 (file)
@@ -33,7 +33,9 @@ import { ContextMenuKind } from "~/views-components/context-menu/context-menu";
 import { Dispatch } from "redux";
 import { PanelDefaultView } from '~/components/panel-default-view/panel-default-view';
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
-
+import { createTree } from '~/models/tree';
+import { getInitialResourceTypeFilters } from '../../store/resource-type-filters/resource-type-filters';
+// TODO: code clean up
 type CssRules = "toolbar" | "button";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
@@ -79,13 +81,13 @@ export const ResourceRestore =
         </Tooltip>
     );
 
-export const trashPanelColumns: DataColumns<string, TrashPanelFilter> = [
+export const trashPanelColumns: DataColumns<string> = [
     {
         name: TrashPanelColumnNames.NAME,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.ASC,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceName uuid={uuid} />
     },
     {
@@ -93,18 +95,7 @@ export const trashPanelColumns: DataColumns<string, TrashPanelFilter> = [
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
-        filters: [
-            {
-                name: resourceLabel(ResourceKind.COLLECTION),
-                selected: true,
-                type: ResourceKind.COLLECTION
-            },
-            {
-                name: resourceLabel(ResourceKind.PROJECT),
-                selected: true,
-                type: ResourceKind.PROJECT
-            }
-        ],
+        filters: getInitialResourceTypeFilters(),
         render: uuid => <ResourceType uuid={uuid} />,
     },
     {
@@ -112,7 +103,7 @@ export const trashPanelColumns: DataColumns<string, TrashPanelFilter> = [
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceFileSize uuid={uuid} />
     },
     {
@@ -120,7 +111,7 @@ export const trashPanelColumns: DataColumns<string, TrashPanelFilter> = [
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceTrashDate uuid={uuid} />
     },
     {
@@ -128,7 +119,7 @@ export const trashPanelColumns: DataColumns<string, TrashPanelFilter> = [
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceDeleteDate uuid={uuid} />
     },
     {
@@ -136,7 +127,7 @@ export const trashPanelColumns: DataColumns<string, TrashPanelFilter> = [
         selected: true,
         configurable: false,
         sortDirection: SortDirection.NONE,
-        filters: [],
+        filters: createTree(),
         render: uuid => <ResourceRestore uuid={uuid} />
     }
 ];
index 18a254bd36afcc678bab48f41f87a59b460d4e14..b8e0e436d77612370a1eeb5f0c4ef4f2750f841a 100644 (file)
@@ -19,6 +19,7 @@ import { DataTableFilterItem } from '~/components/data-table-filters/data-table-
 import { Grid, Paper } from '@material-ui/core';
 import { WorkflowDetailsCard } from './workflow-description-card';
 import { WorkflowResource } from '../../models/workflow';
+import { createTree } from '~/models/tree';
 
 export enum WorkflowPanelColumnNames {
     NAME = "Name",
@@ -61,36 +62,38 @@ const resourceStatus = (type: string) => {
     }
 };
 
-export const workflowPanelColumns: DataColumns<string, WorkflowPanelFilter> = [
+export const workflowPanelColumns: DataColumns<string> = [
     {
         name: WorkflowPanelColumnNames.NAME,
         selected: true,
         configurable: true,
         sortDirection: SortDirection.ASC,
-        filters: [],
+        filters: createTree(),
         render: (uuid: string) => <RosurceWorkflowName uuid={uuid} />
     },
     {
         name: WorkflowPanelColumnNames.AUTHORISATION,
         selected: true,
         configurable: true,
-        filters: [
-            {
-                name: resourceStatus(ResourceStatus.PUBLIC),
-                selected: true,
-                type: ResourceStatus.PUBLIC
-            },
-            {
-                name: resourceStatus(ResourceStatus.PRIVATE),
-                selected: true,
-                type: ResourceStatus.PRIVATE
-            },
-            {
-                name: resourceStatus(ResourceStatus.SHARED),
-                selected: true,
-                type: ResourceStatus.SHARED
-            }
-        ],
+        filters: createTree(),
+        // TODO: restore filters
+        // filters: [
+        //     {
+        //         name: resourceStatus(ResourceStatus.PUBLIC),
+        //         selected: true,
+        //         type: ResourceStatus.PUBLIC
+        //     },
+        //     {
+        //         name: resourceStatus(ResourceStatus.PRIVATE),
+        //         selected: true,
+        //         type: ResourceStatus.PRIVATE
+        //     },
+        //     {
+        //         name: resourceStatus(ResourceStatus.SHARED),
+        //         selected: true,
+        //         type: ResourceStatus.SHARED
+        //     }
+        // ],
         render: (uuid: string) => <ResourceWorkflowStatus uuid={uuid} />,
     },
     {
@@ -98,14 +101,14 @@ export const workflowPanelColumns: DataColumns<string, WorkflowPanelFilter> = [
         selected: true,
         configurable: true,
         sortDirection: SortDirection.NONE,
-        filters: [],
+        filters: createTree(),
         render: (uuid: string) => <ResourceLastModifiedDate uuid={uuid} />
     },
     {
         name: '',
         selected: true,
         configurable: false,
-        filters: [],
+        filters: createTree(),
         render: (uuid: string) => <ResourceShare uuid={uuid} />
     }
 ];