15951: Accepts filter toggle actions when clicking on the entire row.
[arvados.git] / src / components / data-table-filters / data-table-filters-popover.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import * as React from "react";
6 import {
7     WithStyles,
8     withStyles,
9     ButtonBase,
10     StyleRulesCallback,
11     Theme,
12     Popover,
13     Button,
14     Card,
15     CardActions,
16     Typography,
17     CardContent,
18     Tooltip,
19     IconButton
20 } from "@material-ui/core";
21 import * as classnames from "classnames";
22 import { DefaultTransformOrigin } from "~/components/popover/helpers";
23 import { createTree } from '~/models/tree';
24 import { DataTableFilters, DataTableFiltersTree } from "./data-table-filters-tree";
25 import { getNodeDescendants } from '~/models/tree';
26
27 export type CssRules = "root" | "icon" | "iconButton" | "active" | "checkbox";
28
29 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
30     root: {
31         cursor: "pointer",
32         display: "inline-flex",
33         justifyContent: "flex-start",
34         flexDirection: "inherit",
35         alignItems: "center",
36         "&:hover": {
37             color: theme.palette.text.primary,
38         },
39         "&:focus": {
40             color: theme.palette.text.primary,
41         },
42     },
43     active: {
44         color: theme.palette.text.primary,
45         '& $iconButton': {
46             opacity: 1,
47         },
48     },
49     icon: {
50         fontSize: 12,
51         userSelect: 'none',
52         width: 16,
53         height: 15,
54         marginTop: 1
55     },
56     iconButton: {
57         color: theme.palette.text.primary,
58         opacity: 0.7,
59     },
60     checkbox: {
61         width: 24,
62         height: 24
63     }
64 });
65
66 enum SelectionMode {
67     ALL = 'all',
68     NONE = 'none'
69 }
70
71 export interface DataTableFilterProps {
72     name: string;
73     filters: DataTableFilters;
74     onChange?: (filters: DataTableFilters) => void;
75
76     /**
77      * When set to true, only one filter can be selected at a time.
78      */
79     mutuallyExclusive?: boolean;
80
81     /**
82      * By default `all` filters selection means that label should be grayed out.
83      * Use `none` when label is supposed to be grayed out when no filter is selected.
84      */
85     defaultSelection?: SelectionMode;
86 }
87
88 interface DataTableFilterState {
89     anchorEl?: HTMLElement;
90     filters: DataTableFilters;
91     prevFilters: DataTableFilters;
92 }
93
94 export const DataTableFiltersPopover = withStyles(styles)(
95     class extends React.Component<DataTableFilterProps & WithStyles<CssRules>, DataTableFilterState> {
96         state: DataTableFilterState = {
97             anchorEl: undefined,
98             filters: createTree(),
99             prevFilters: createTree(),
100         };
101         icon = React.createRef<HTMLElement>();
102
103         render() {
104             const { name, classes, defaultSelection = SelectionMode.ALL, children } = this.props;
105             const isActive = getNodeDescendants('')(this.state.filters)
106                 .some(f => defaultSelection === SelectionMode.ALL
107                     ? !f.selected
108                     : f.selected
109                 );
110             return <>
111                 <Tooltip title='Filters'>
112                     <ButtonBase
113                         className={classnames([classes.root, { [classes.active]: isActive }])}
114                         component="span"
115                         onClick={this.open}
116                         disableRipple>
117                         {children}
118                         <IconButton component='span' classes={{ root: classes.iconButton }} tabIndex={-1}>
119                             <i className={classnames(["fas fa-filter", classes.icon])}
120                                 data-fa-transform="shrink-3"
121                                 ref={this.icon} />
122                         </IconButton>
123                     </ButtonBase>
124                 </Tooltip>
125                 <Popover
126                     anchorEl={this.state.anchorEl}
127                     open={!!this.state.anchorEl}
128                     anchorOrigin={DefaultTransformOrigin}
129                     transformOrigin={DefaultTransformOrigin}
130                     onClose={this.cancel}>
131                     <Card>
132                         <CardContent>
133                             <Typography variant="caption">
134                                 {name}
135                             </Typography>
136                         </CardContent>
137                         <DataTableFiltersTree
138                             filters={this.state.filters}
139                             mutuallyExclusive={this.props.mutuallyExclusive}
140                             onChange={filters => {
141                                 this.setState({ filters });
142                                 if (this.props.mutuallyExclusive) {
143                                     const { onChange } = this.props;
144                                     if (onChange) {
145                                         onChange(filters);
146                                     }
147                                     this.setState({ anchorEl: undefined });
148                                 }
149                             }} />
150                         {this.props.mutuallyExclusive ||
151                         <CardActions>
152                             <Button
153                                 color="primary"
154                                 variant='contained'
155                                 size="small"
156                                 onClick={this.submit}>
157                                 Ok
158                             </Button>
159                             <Button
160                                 color="primary"
161                                 variant="outlined"
162                                 size="small"
163                                 onClick={this.cancel}>
164                                 Cancel
165                             </Button>
166                         </CardActions >
167                         }
168                     </Card>
169                 </Popover>
170             </>;
171         }
172
173         static getDerivedStateFromProps(props: DataTableFilterProps, state: DataTableFilterState): DataTableFilterState {
174             return props.filters !== state.prevFilters
175                 ? { ...state, filters: props.filters, prevFilters: props.filters }
176                 : state;
177         }
178
179         open = () => {
180             this.setState({ anchorEl: this.icon.current || undefined });
181         }
182
183         submit = () => {
184             const { onChange } = this.props;
185             if (onChange) {
186                 onChange(this.state.filters);
187             }
188             this.setState({ anchorEl: undefined });
189         }
190
191         cancel = () => {
192             this.setState(prev => ({
193                 ...prev,
194                 filters: prev.prevFilters,
195                 anchorEl: undefined
196             }));
197         }
198
199         setFilters = (filters: DataTableFilters) => {
200             this.setState({ filters });
201         }
202
203     }
204 );