X-Git-Url: https://git.arvados.org/arvados-workbench2.git/blobdiff_plain/95b86eb6b3184b787b570a906347ccaac32195c6..f7a4118c87ae1df4a42efdad3695e6fcb5d60854:/src/components/data-table-filters/data-table-filters-popover.tsx diff --git a/src/components/data-table-filters/data-table-filters-popover.tsx b/src/components/data-table-filters/data-table-filters-popover.tsx index b79d36b4..557abd82 100644 --- a/src/components/data-table-filters/data-table-filters-popover.tsx +++ b/src/components/data-table-filters/data-table-filters-popover.tsx @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0 -import * as React from "react"; +import React, { useEffect } from 'react'; import { WithStyles, withStyles, @@ -15,53 +15,75 @@ import { 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'; + Tooltip, + IconButton, +} from '@material-ui/core'; +import 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'; +import debounce from 'lodash/debounce'; -export type CssRules = "root" | "icon" | "active" | "checkbox"; +export type CssRules = 'root' | 'icon' | 'iconButton' | 'active' | 'checkbox'; const styles: StyleRulesCallback = (theme: Theme) => ({ root: { - cursor: "pointer", - display: "inline-flex", - justifyContent: "flex-start", - flexDirection: "inherit", - alignItems: "center", - "&:hover": { + cursor: 'pointer', + display: 'inline-flex', + justifyContent: 'flex-start', + flexDirection: 'inherit', + alignItems: 'center', + '&:hover': { color: theme.palette.text.primary, }, - "&:focus": { + '&:focus': { color: theme.palette.text.primary, }, }, active: { color: theme.palette.text.primary, - '& $icon': { + '& $iconButton': { opacity: 1, }, }, icon: { - marginRight: 4, - marginLeft: 4, + fontSize: 12, + userSelect: 'none', + width: 16, + height: 15, + marginTop: 1, + }, + iconButton: { + color: theme.palette.text.primary, opacity: 0.7, - userSelect: "none", - width: 16 }, checkbox: { width: 24, - height: 24 - } + height: 24, + }, }); +enum SelectionMode { + ALL = 'all', + NONE = 'none', +} + export interface DataTableFilterProps { name: string; filters: DataTableFilters; onChange?: (filters: DataTableFilters) => void; + + /** + * When set to true, only one filter can be selected at a time. + */ + mutuallyExclusive?: boolean; + + /** + * By default `all` filters selection means that label should be grayed out. + * Use `none` when label is supposed to be grayed out when no filter is selected. + */ + defaultSelection?: SelectionMode; } interface DataTableFilterState { @@ -80,86 +102,90 @@ export const DataTableFiltersPopover = withStyles(styles)( icon = React.createRef(); render() { - const { name, classes, children } = this.props; - const isActive = getNodeDescendants('')(this.state.filters).some(f => f.selected); - return <> - - - {children} - - - - - - - - {name} - - - this.setState({ filters })} /> - - - - - - - ; + const { name, classes, defaultSelection = SelectionMode.ALL, children } = this.props; + const isActive = getNodeDescendants('')(this.state.filters).some((f) => (defaultSelection === SelectionMode.ALL ? !f.selected : f.selected)); + return ( + <> + + + {children} + + + + + + + + + {name} + + + <> + {this.props.mutuallyExclusive || ( + + + + )} + + + + + + ); } static getDerivedStateFromProps(props: DataTableFilterProps, state: DataTableFilterState): DataTableFilterState { - return props.filters !== state.prevFilters - ? { ...state, filters: props.filters, prevFilters: props.filters } - : state; + return props.filters !== state.prevFilters ? { ...state, filters: props.filters, prevFilters: props.filters } : state; } open = () => { this.setState({ anchorEl: this.icon.current || undefined }); - } + }; - submit = () => { + onChange = (filters) => { + this.setState({ filters }); + if (this.props.mutuallyExclusive) { + // Mutually exclusive filters apply immediately + const { onChange } = this.props; + if (onChange) { + onChange(filters); + } + this.close(); + } else { + // Non-mutually exclusive filters are debounced + this.submit(); + } + }; + + submit = debounce(() => { const { onChange } = this.props; if (onChange) { onChange(this.state.filters); } - this.setState({ anchorEl: undefined }); - } + }, 1000); - cancel = () => { - this.setState(prev => ({ + MountHandler = () => { + useEffect(() => { + return () => { + this.submit.cancel(); + }; + }, []); + return null; + }; + + close = () => { + this.setState((prev) => ({ ...prev, - filters: prev.prevFilters, - anchorEl: undefined + anchorEl: undefined, })); - } - - setFilters = (filters: DataTableFilters) => { - this.setState({ filters }); - } - + }; } );