15768: multiselect popover pops over Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa...
[arvados-workbench2.git] / src / components / data-table-multiselect-popover / data-table-multiselect-tree.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from 'react';
6 import { Tree, toggleNodeSelection, getNode, initTreeNode, getNodeChildrenIds, selectNode, deselectNodes } from 'models/tree';
7 import { Tree as TreeComponent, TreeItem, TreeItemStatus } from 'components/tree/tree';
8 import { noop, map } from 'lodash/fp';
9 import { toggleNodeCollapse } from 'models/tree';
10 import { countNodes, countChildren } from 'models/tree';
11
12 export interface DataTableFilterItem {
13     name: string;
14 }
15
16 export type DataTableFilters = Tree<DataTableFilterItem>;
17
18 export interface DataTableFilterProps {
19     filters: DataTableFilters;
20     onChange?: (filters: DataTableFilters) => void;
21
22     /**
23      * When set to true, only one filter can be selected at a time.
24      */
25     mutuallyExclusive?: boolean;
26 }
27
28 export class DataTableFiltersTree extends React.Component<DataTableFilterProps> {
29     render() {
30         const { filters } = this.props;
31         const hasSubfilters = countNodes(filters) !== countChildren('')(filters);
32         return (
33             <TreeComponent
34                 levelIndentation={hasSubfilters ? 20 : 0}
35                 itemRightPadding={20}
36                 items={filtersToTree(filters)}
37                 render={this.props.mutuallyExclusive ? renderRadioItem : renderItem}
38                 showSelection
39                 useRadioButtons={this.props.mutuallyExclusive}
40                 disableRipple
41                 onContextMenu={noop}
42                 toggleItemActive={this.props.mutuallyExclusive ? this.toggleRadioButtonFilter : this.toggleFilter}
43                 toggleItemOpen={this.toggleOpen}
44             />
45         );
46     }
47
48     /**
49      * Handler for when a tree item is toggled via a radio button.
50      * Ensures mutual exclusivity among filter tree items.
51      */
52     toggleRadioButtonFilter = (_: any, item: TreeItem<DataTableFilterItem>) => {
53         const { onChange = noop } = this.props;
54
55         // If the filter is already selected, do nothing.
56         if (item.selected) {
57             return;
58         }
59
60         // Otherwise select this node and deselect the others
61         const filters = selectNode(item.id)(this.props.filters);
62         const toDeselect = Object.keys(this.props.filters).filter((id) => id !== item.id);
63         onChange(deselectNodes(toDeselect)(filters));
64     };
65
66     toggleFilter = (_: React.MouseEvent, item: TreeItem<DataTableFilterItem>) => {
67         const { onChange = noop } = this.props;
68         onChange(toggleNodeSelection(item.id)(this.props.filters));
69     };
70
71     toggleOpen = (_: React.MouseEvent, item: TreeItem<DataTableFilterItem>) => {
72         const { onChange = noop } = this.props;
73         onChange(toggleNodeCollapse(item.id)(this.props.filters));
74     };
75 }
76
77 const renderItem = (item: TreeItem<DataTableFilterItem>) => (
78     <span>
79         {item.data.name}
80         {item.initialState !== item.selected ? <>*</> : null}
81     </span>
82 );
83
84 const renderRadioItem = (item: TreeItem<DataTableFilterItem>) => <span>{item.data.name}</span>;
85
86 const filterToTreeItem =
87     (filters: DataTableFilters) =>
88     (id: string): TreeItem<any> => {
89         const node = getNode(id)(filters) || initTreeNode({ id: '', value: 'InvalidNode' });
90         const items = getNodeChildrenIds(node.id)(filters).map(filterToTreeItem(filters));
91         const isIndeterminate = !node.selected && items.some((i) => i.selected || i.indeterminate);
92
93         return {
94             active: node.active,
95             data: node.value,
96             id: node.id,
97             items: items.length > 0 ? items : undefined,
98             open: node.expanded,
99             selected: node.selected,
100             initialState: node.initialState,
101             indeterminate: isIndeterminate,
102             status: TreeItemStatus.LOADED,
103         };
104     };
105
106 const filtersToTree = (filters: DataTableFilters): TreeItem<DataTableFilterItem>[] => map(filterToTreeItem(filters), getNodeChildrenIds('')(filters));