Create data table filter component
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Wed, 20 Jun 2018 10:59:11 +0000 (12:59 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Wed, 20 Jun 2018 10:59:11 +0000 (12:59 +0200)
Feature #13633

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

package.json
src/components/data-table-filters/data-table-filters.tsx [new file with mode: 0644]
src/components/data-table/data-column.ts
src/components/data-table/data-table.tsx
src/views-components/data-explorer/data-explorer.tsx
yarn.lock

index fda2ead62ec4895668d94e3aa0e7410d4a570904..1f08e6cb8505c12b784e53c1e65f7add0add7716 100644 (file)
@@ -7,6 +7,7 @@
     "@material-ui/icons": "1.1.0",
     "@types/lodash": "4.14.109",
     "axios": "0.18.0",
+    "classnames": "^2.2.6",
     "lodash": "4.17.10",
     "react": "16.4.1",
     "react-dom": "16.4.1",
     "lint": "tslint src/** -t verbose"
   },
   "devDependencies": {
+    "@types/classnames": "^2.2.4",
     "@types/enzyme": "3.1.10",
     "@types/enzyme-adapter-react-16": "1.0.2",
     "@types/jest": "23.1.0",
     "@types/node": "10.3.3",
-    "@types/react": "16.3.18",
+    "@types/react": "16.4.1",
     "@types/react-dom": "16.0.6",
     "@types/react-redux": "6.0.2",
     "@types/react-router": "4.0.26",
diff --git a/src/components/data-table-filters/data-table-filters.tsx b/src/components/data-table-filters/data-table-filters.tsx
new file mode 100644 (file)
index 0000000..cf0260b
--- /dev/null
@@ -0,0 +1,189 @@
+// 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,
+    List,
+    ListItem,
+    Checkbox,
+    ListItemText,
+    Button,
+    Card,
+    CardActions,
+    Typography,
+    CardContent
+} from "@material-ui/core";
+import * as classnames from "classnames";
+import { DefaultTransformOrigin } from "../popover/helpers";
+
+export interface DataTableFilterItem {
+    name: string;
+    selected: boolean;
+}
+
+export interface DataTableFilterProps {
+    name: string;
+    filters: DataTableFilterItem[];
+    onChange?: (filters: DataTableFilterItem[]) => void;
+}
+
+interface DataTableFilterState {
+    anchorEl?: HTMLElement;
+    filters: DataTableFilterItem[];
+    prevFilters: DataTableFilterItem[];
+}
+
+class DataTableFilter extends React.Component<DataTableFilterProps & WithStyles<CssRules>, DataTableFilterState> {
+    state: DataTableFilterState = {
+        anchorEl: undefined,
+        filters: [],
+        prevFilters: []
+    };
+    icon = React.createRef<HTMLElement>();
+
+    render() {
+        const { name, classes, children } = this.props;
+        return <>
+            <ButtonBase
+                className={classnames([
+                    classes.root,
+                    { [classes.active]: this.state.filters.filter(({ selected }) => !selected).length > 0 }])}
+                component="span"
+                onClick={this.open}
+                disableRipple>
+                {children}
+                <i className={classnames(["fas fa-filter", classes.icon])}
+                    data-fa-transform="shrink-3"
+                    ref={this.icon} />
+            </ButtonBase>
+            <Popover
+                anchorEl={this.state.anchorEl}
+                open={!!this.state.anchorEl}
+                anchorOrigin={DefaultTransformOrigin}
+                transformOrigin={DefaultTransformOrigin}
+                onClose={this.cancel}>
+                <Card>
+                    <CardContent>
+                        <Typography variant="caption">
+                            {name}
+                        </Typography>
+                    </CardContent>
+                    <List dense>
+                        {this.state.filters.map((filter, index) =>
+                            <ListItem
+                                button
+                                key={index}
+                                onClick={this.toggleFilter(filter)}>
+                                <Checkbox
+                                    disableRipple
+                                    color="primary"
+                                    checked={filter.selected}
+                                    className={classes.checkbox} />
+                                <ListItemText>
+                                    {filter.name}
+                                </ListItemText>
+                            </ListItem>
+                        )}
+                    </List>
+                    <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
+        }));
+    }
+
+    toggleFilter = (toggledFilter: DataTableFilterItem) => () => {
+        this.setState(prev => ({
+            ...prev,
+            filters: prev.filters.map(filter =>
+                filter === toggledFilter
+                    ? { ...filter, selected: !filter.selected }
+                    : filter)
+        }));
+    }
+}
+
+
+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 default withStyles(styles)(DataTableFilter);
index f3d9576dd66d5cac933dffdc5e96c67464c9610e..7ac568a25b5301901ffc95318f1581852dc8dfc5 100644 (file)
@@ -2,6 +2,8 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { DataTableFilterItem } from "../data-table-filters/data-table-filters";
+
 export interface DataColumn<T> {
     name: string;
     selected: boolean;
@@ -9,6 +11,8 @@ export interface DataColumn<T> {
     key?: React.Key;
     sortDirection?: SortDirection;
     onSortToggle?: () => void;
+    filters?: DataTableFilterItem[];
+    onFiltersChange?: (filters: DataTableFilterItem[]) => void;
     render: (item: T) => React.ReactElement<void>;
     renderHeader?: () => React.ReactElement<void> | null;
 }
index 43efdd0ada3bc97c50284926a3b75758375fb2c8..26f6faf5dde7330b7aa749f1a0be9bf75cb35d4c 100644 (file)
@@ -5,6 +5,7 @@
 import * as React from 'react';
 import { Table, TableBody, TableRow, TableCell, TableHead, TableSortLabel, StyleRulesCallback, Theme, WithStyles, withStyles, Typography } from '@material-ui/core';
 import { DataColumn } from './data-column';
+import DataTableFilters from "../data-table-filters/data-table-filters";
 
 export type DataColumns<T> = Array<DataColumn<T>>;
 
@@ -25,16 +26,23 @@ class DataTable<T> extends React.Component<DataTableProps<T> & WithStyles<CssRul
                         <TableRow>
                             {columns
                                 .filter(column => column.selected)
-                                .map(({ name, renderHeader, key, sortDirection, onSortToggle }, index) =>
+                                .map(({ name, renderHeader, key, sortDirection, onSortToggle, filters, onFiltersChange }, index) =>
                                     <TableCell key={key || index}>
                                         {renderHeader ?
                                             renderHeader() :
-                                            <TableSortLabel
-                                                active={!!sortDirection}
-                                                direction={sortDirection}
-                                                onClick={() => onSortToggle && onSortToggle()}>
-                                                {name}
-                                            </TableSortLabel>}
+                                            filters ?
+                                                <DataTableFilters
+                                                    name={`${name} filters`}
+                                                    onChange={onFiltersChange}
+                                                    filters={filters}>
+                                                    {name}
+                                                </DataTableFilters> :
+                                                <TableSortLabel
+                                                    active={!!sortDirection}
+                                                    direction={sortDirection}
+                                                    onClick={() => onSortToggle && onSortToggle()}>
+                                                    {name}
+                                                </TableSortLabel>}
                                     </TableCell>
                                 )}
                         </TableRow>
index ffb21f93fb2c8caa1f7978d4cdda98afced87017..dab686ee461242b454300c341b160e38e8490f15 100644 (file)
@@ -49,10 +49,26 @@ class DataExplorer extends React.Component<DataExplorerProps, DataExplorerState>
         }, {
             name: "Status",
             selected: true,
+            onFiltersChange: console.log,
+            filters: [{
+                name: "In progress", 
+                selected: true
+            }, {
+                name: "Complete",
+                selected: true
+            }],
             render: item => renderStatus(item.status)
         }, {
             name: "Type",
             selected: true,
+            onFiltersChange: console.log,
+            filters: [{
+                name: "Collection", 
+                selected: true
+            }, {
+                name: "Group",
+                selected: true
+            }],
             render: item => renderType(item.type)
         }, {
             name: "Owner",
index eee6c8604b3bf2a54334135c3ef643abbb5037e8..05a6ef11e56e24d192864ba5fbae4d846ed0da0a 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
   version "0.22.7"
   resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.7.tgz#4a92eafedfb2b9f4437d3a4410006d81114c66ce"
 
+"@types/classnames@^2.2.4":
+  version "2.2.4"
+  resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.4.tgz#d3ee9ebf714aa34006707b8f4a58fd46b642305a"
+
 "@types/enzyme-adapter-react-16@1.0.2":
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.2.tgz#15ae37c64d6221a6f4b3a4aacc357cf773859de4"
   dependencies:
     "@types/react" "*"
 
-"@types/react@*", "@types/react@16.3.18":
+"@types/react@*":
   version "16.3.18"
   resolved "https://registry.yarnpkg.com/@types/react/-/react-16.3.18.tgz#bf195aed4d77dc86f06e4c9bb760214a3b822b8d"
   dependencies:
     csstype "^2.2.0"
 
+"@types/react@16.4.1":
+  version "16.4.1"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-16.4.1.tgz#c53bbfb4a78933db587da085ac60dbf5fcf73f8f"
+  dependencies:
+    csstype "^2.2.0"
+
 "@types/redux-devtools@3.0.44":
   version "3.0.44"
   resolved "https://registry.yarnpkg.com/@types/redux-devtools/-/redux-devtools-3.0.44.tgz#2781b87067b8aec3102d4cb4a478feb340df5259"
@@ -1559,7 +1569,7 @@ class-utils@^0.3.5:
     isobject "^3.0.0"
     static-extend "^0.1.1"
 
-classnames@^2.2.5:
+classnames@^2.2.5, classnames@^2.2.6:
   version "2.2.6"
   resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
 
@@ -2007,10 +2017,14 @@ cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
   dependencies:
     cssom "0.3.x"
 
-csstype@^2.0.0, csstype@^2.2.0, csstype@^2.5.2:
+csstype@^2.0.0, csstype@^2.5.2:
   version "2.5.3"
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.3.tgz#2504152e6e1cc59b32098b7f5d6a63f16294c1f7"
 
+csstype@^2.2.0:
+  version "2.5.5"
+  resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.5.tgz#4125484a3d42189a863943f23b9e4b80fedfa106"
+
 currently-unhandled@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"