[arvados-workbench2] updated: 2.6.0-12-g750b2c39
git repository hosting
git at public.arvados.org
Thu Apr 27 19:40:16 UTC 2023
Summary of changes:
.../data-table-multiselect-popover.tsx | 192 +++++++++++++++++++++
.../data-table-multiselect-tree.tsx} | 72 ++++----
src/components/data-table/data-table.tsx | 56 ++++--
3 files changed, 266 insertions(+), 54 deletions(-)
create mode 100644 src/components/data-table-multiselect-popover/data-table-multiselect-popover.tsx
copy src/components/{data-table-filters/data-table-filters-tree.tsx => data-table-multiselect-popover/data-table-multiselect-tree.tsx} (66%)
via 750b2c391b5418bfd3db88e59429abad9fde6c1e (commit)
from ccc29ca7f0afff5b6d038553e1feb2c4b1666392 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
commit 750b2c391b5418bfd3db88e59429abad9fde6c1e
Author: Lisa Knox <lisaknox83 at gmail.com>
Date: Thu Apr 27 15:40:11 2023 -0400
15768: multiselect popover pops over Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox at curii.com>
diff --git a/src/components/data-table-multiselect-popover/data-table-multiselect-popover.tsx b/src/components/data-table-multiselect-popover/data-table-multiselect-popover.tsx
new file mode 100644
index 00000000..74b083a9
--- /dev/null
+++ b/src/components/data-table-multiselect-popover/data-table-multiselect-popover.tsx
@@ -0,0 +1,192 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { useEffect } from 'react';
+import {
+ WithStyles,
+ withStyles,
+ ButtonBase,
+ StyleRulesCallback,
+ Theme,
+ Popover,
+ Button,
+ Card,
+ CardActions,
+ Typography,
+ CardContent,
+ 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-multiselect-tree';
+import { getNodeDescendants } from 'models/tree';
+import debounce from 'lodash/debounce';
+import { green, grey } from '@material-ui/core/colors';
+
+export type CssRules = 'root' | 'icon' | 'iconButton' | 'active' | 'checkbox';
+
+const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
+ root: {
+ // border: '1px dashed green',
+ margin: 0,
+ borderRadius: '7px',
+ '&:hover': {
+ backgroundColor: grey[200],
+ },
+ '&:focus': {
+ color: theme.palette.text.primary,
+ },
+ },
+ active: {
+ color: theme.palette.text.primary,
+ '& $iconButton': {
+ opacity: 1,
+ },
+ },
+ icon: {
+ // border: '1px solid red',
+ cursor: 'pointer',
+ fontSize: 20,
+ userSelect: 'none',
+ '&:hover': {
+ color: theme.palette.text.primary,
+ },
+ },
+ iconButton: {
+ color: theme.palette.text.primary,
+ opacity: 0.6,
+ padding: 1,
+ paddingBottom: 5,
+ },
+ checkbox: {
+ width: 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 {
+ anchorEl?: HTMLElement;
+ filters: DataTableFilters;
+ prevFilters: DataTableFilters;
+}
+
+export const DataTableMultiselectPopover = 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, defaultSelection = SelectionMode.ALL, children } = this.props;
+ const isActive = getNodeDescendants('')(this.state.filters).some((f) => (defaultSelection === SelectionMode.ALL ? !f.selected : f.selected));
+ return (
+ <>
+ <Tooltip disableFocusListener title='Multiselect Actions'>
+ <ButtonBase className={classnames([classes.root, { [classes.active]: isActive }])} component='span' onClick={this.open} disableRipple>
+ {children}
+ <IconButton component='span' classes={{ root: classes.iconButton }} tabIndex={-1}>
+ <i className={classnames(['fas fa-sort-down', classes.icon])} data-fa-transform='shrink-3' ref={this.icon} />
+ </IconButton>
+ </ButtonBase>
+ </Tooltip>
+ <Popover
+ anchorEl={this.state.anchorEl}
+ open={!!this.state.anchorEl}
+ anchorOrigin={DefaultTransformOrigin}
+ transformOrigin={DefaultTransformOrigin}
+ onClose={this.close}
+ >
+ <Card>
+ <CardContent>
+ <Typography variant='caption'>{'foo'}</Typography>
+ </CardContent>
+ <DataTableFiltersTree filters={this.state.filters} mutuallyExclusive={this.props.mutuallyExclusive} onChange={this.onChange} />
+ {this.props.mutuallyExclusive || (
+ <CardActions>
+ <Button color='primary' variant='outlined' size='small' onClick={this.close}>
+ Close
+ </Button>
+ </CardActions>
+ )}
+ </Card>
+ </Popover>
+ <this.MountHandler />
+ </>
+ );
+ }
+
+ 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 });
+ };
+
+ 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);
+ }
+ }, 1000);
+
+ MountHandler = () => {
+ useEffect(() => {
+ return () => {
+ this.submit.cancel();
+ };
+ }, []);
+ return null;
+ };
+
+ close = () => {
+ this.setState((prev) => ({
+ ...prev,
+ anchorEl: undefined,
+ }));
+ };
+ }
+);
diff --git a/src/components/data-table-multiselect-popover/data-table-multiselect-tree.tsx b/src/components/data-table-multiselect-popover/data-table-multiselect-tree.tsx
new file mode 100644
index 00000000..326d0399
--- /dev/null
+++ b/src/components/data-table-multiselect-popover/data-table-multiselect-tree.tsx
@@ -0,0 +1,106 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { Tree, toggleNodeSelection, getNode, initTreeNode, getNodeChildrenIds, selectNode, deselectNodes } 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;
+
+ /**
+ * When set to true, only one filter can be selected at a time.
+ */
+ mutuallyExclusive?: boolean;
+}
+
+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={this.props.mutuallyExclusive ? renderRadioItem : renderItem}
+ showSelection
+ useRadioButtons={this.props.mutuallyExclusive}
+ disableRipple
+ onContextMenu={noop}
+ toggleItemActive={this.props.mutuallyExclusive ? this.toggleRadioButtonFilter : this.toggleFilter}
+ toggleItemOpen={this.toggleOpen}
+ />
+ );
+ }
+
+ /**
+ * Handler for when a tree item is toggled via a radio button.
+ * Ensures mutual exclusivity among filter tree items.
+ */
+ toggleRadioButtonFilter = (_: any, item: TreeItem<DataTableFilterItem>) => {
+ const { onChange = noop } = this.props;
+
+ // If the filter is already selected, do nothing.
+ if (item.selected) {
+ return;
+ }
+
+ // Otherwise select this node and deselect the others
+ const filters = selectNode(item.id)(this.props.filters);
+ const toDeselect = Object.keys(this.props.filters).filter((id) => id !== item.id);
+ onChange(deselectNodes(toDeselect)(filters));
+ };
+
+ 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}
+ {item.initialState !== item.selected ? <>*</> : null}
+ </span>
+);
+
+const renderRadioItem = (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));
+ const isIndeterminate = !node.selected && items.some((i) => i.selected || i.indeterminate);
+
+ return {
+ active: node.active,
+ data: node.value,
+ id: node.id,
+ items: items.length > 0 ? items : undefined,
+ open: node.expanded,
+ selected: node.selected,
+ initialState: node.initialState,
+ indeterminate: isIndeterminate,
+ status: TreeItemStatus.LOADED,
+ };
+ };
+
+const filtersToTree = (filters: DataTableFilters): TreeItem<DataTableFilterItem>[] => map(filterToTreeItem(filters), getNodeChildrenIds('')(filters));
diff --git a/src/components/data-table/data-table.tsx b/src/components/data-table/data-table.tsx
index 265b0986..9627681f 100644
--- a/src/components/data-table/data-table.tsx
+++ b/src/components/data-table/data-table.tsx
@@ -21,6 +21,7 @@ import classnames from 'classnames';
import { DataColumn, SortDirection } from './data-column';
import { DataTableDefaultView } from '../data-table-default-view/data-table-default-view';
import { DataTableFilters } from '../data-table-filters/data-table-filters-tree';
+import { DataTableMultiselectPopover } from '../data-table-multiselect-popover/data-table-multiselect-popover';
import { DataTableFiltersPopover } from '../data-table-filters/data-table-filters-popover';
import { countNodes, getTreeDirty } from 'models/tree';
import { IconType, PendingIcon } from 'components/icon/icon';
@@ -52,7 +53,19 @@ export interface DataTableDataProps<I> {
currentRoute?: string;
}
-type CssRules = 'tableBody' | 'root' | 'content' | 'noItemsInfo' | 'checkBoxCell' | 'tableCell' | 'arrow' | 'arrowButton' | 'tableCellWorkflows' | 'loader';
+type CssRules =
+ | 'tableBody'
+ | 'root'
+ | 'content'
+ | 'noItemsInfo'
+ | 'checkBoxHead'
+ | 'checkBoxCell'
+ | 'checkBox'
+ | 'tableCell'
+ | 'arrow'
+ | 'arrowButton'
+ | 'tableCellWorkflows'
+ | 'loader';
const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
root: {
@@ -74,8 +87,15 @@ const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
textAlign: 'center',
padding: theme.spacing.unit,
},
+ checkBoxHead: {
+ padding: '0',
+ display: 'flex',
+ },
checkBoxCell: {
padding: '0',
+ paddingLeft: '10px',
+ },
+ checkBox: {
cursor: 'pointer',
},
tableCell: {
@@ -121,16 +141,12 @@ export const DataTable = withStyles(styles)(
}
componentDidUpdate(prevProps: Readonly<DataTableProps<T>>) {
- console.log(this.state);
+ // console.log(this.state.checkedList);
if (!arraysAreCongruent(prevProps.items, this.props.items)) {
this.initializeCheckedList(this.props.items);
}
}
- componentWillUnmount(): void {
- console.log('UNMOUNT');
- }
-
checkBoxColumn: DataColumn<any, any> = {
name: 'checkBoxColumn',
selected: true,
@@ -140,7 +156,7 @@ export const DataTable = withStyles(styles)(
<input
type='checkbox'
name={uuid}
- color='primary'
+ className={this.props.classes.checkBox}
checked={this.state.checkedList[uuid] ?? false}
onChange={() => this.handleCheck(uuid)}
onDoubleClick={(ev) => ev.stopPropagation()}
@@ -167,16 +183,18 @@ export const DataTable = withStyles(styles)(
};
handleCheck = (uuid: string): void => {
- const newCheckedList = { ...this.state.checkedList };
- newCheckedList[uuid] = !this.state.checkedList[uuid];
+ const { checkedList } = this.state;
+ const newCheckedList = { ...checkedList };
+ newCheckedList[uuid] = !checkedList[uuid];
this.setState({ checkedList: newCheckedList });
- console.log(newCheckedList);
+ // console.log(newCheckedList);
};
handleInvertSelect = (): void => {
- const newCheckedList = { ...this.state.checkedList };
+ const { checkedList } = this.state;
+ const newCheckedList = { ...checkedList };
for (const key in newCheckedList) {
- newCheckedList[key] = !this.state.checkedList[key];
+ newCheckedList[key] = !checkedList[key];
}
this.setState({ checkedList: newCheckedList });
};
@@ -216,9 +234,17 @@ export const DataTable = withStyles(styles)(
const { onSortToggle, onFiltersChange, classes } = this.props;
return index === 0 ? (
<TableCell key={key || index} className={classes.checkBoxCell}>
- <Tooltip title={this.state.isSelected ? 'Deselect All' : 'Select All'}>
- <input type='checkbox' checked={this.state.isSelected} onChange={this.handleSelectorSelect}></input>
- </Tooltip>
+ <div className={classes.checkBoxHead}>
+ <Tooltip title={this.state.isSelected ? 'Deselect All' : 'Select All'}>
+ <input type='checkbox' className={classes.checkBox} checked={this.state.isSelected} onChange={this.handleSelectorSelect}></input>
+ </Tooltip>
+ <DataTableMultiselectPopover
+ name={`${name} filters`}
+ mutuallyExclusive={column.mutuallyExclusiveFilters}
+ onChange={(filters) => onFiltersChange && onFiltersChange(filters, column)}
+ filters={filters}
+ ></DataTableMultiselectPopover>
+ </div>
</TableCell>
) : (
<TableCell className={classes.tableCell} key={key || index}>
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list