Merge branch '18203-Support-setting-multi-properties-at-once' into main
[arvados-workbench2.git] / src / components / data-table-filters / data-table-filters-tree.tsx
index 3615451ebaf433d3a295c33f37e1a9bb5b8e8bf1..7b97865bba4b546085d6c90ab7cb68a7b5821e45 100644 (file)
@@ -2,12 +2,12 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import * as React from "react";
-import { Tree, toggleNodeSelection, getNode, initTreeNode, getNodeChildrenIds } from '~/models/tree';
-import { Tree as TreeComponent, TreeItem, TreeItemStatus } from '~/components/tree/tree';
+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';
+import { toggleNodeCollapse } from 'models/tree';
+import { countNodes, countChildren } from 'models/tree';
 
 export interface DataTableFilterItem {
     name: string;
@@ -18,6 +18,11 @@ 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> {
@@ -26,17 +31,39 @@ export class DataTableFiltersTree extends React.Component<DataTableFilterProps>
         const { filters } = this.props;
         const hasSubfilters = countNodes(filters) !== countChildren('')(filters);
         return <TreeComponent
-            levelIndentation={hasSubfilters ? undefined : 0}
+            levelIndentation={hasSubfilters ? 20 : 0}
+            itemRightPadding={20}
             items={filtersToTree(filters)}
-            render={renderItem}
+            render={this.props.mutuallyExclusive ? renderRadioItem : renderItem}
             showSelection
+            useRadioButtons={this.props.mutuallyExclusive}
+            disableRipple
             onContextMenu={noop}
-            toggleItemActive={noop}
+            toggleItemActive={
+                this.props.mutuallyExclusive
+                    ? this.toggleRadioButtonFilter
+                    : this.toggleFilter
+            }
             toggleItemOpen={this.toggleOpen}
-            toggleItemSelection={this.toggleFilter}
         />;
     }
 
+    /**
+     * 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));
@@ -49,13 +76,24 @@ export class DataTableFiltersTree extends React.Component<DataTableFilterProps>
 }
 
 const renderItem = (item: TreeItem<DataTableFilterItem>) =>
-    <span>{item.data.name}</span>;
+    <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,
@@ -64,6 +102,8 @@ const filterToTreeItem = (filters: DataTableFilters) =>
             items: items.length > 0 ? items : undefined,
             open: node.expanded,
             selected: node.selected,
+            initialState: node.initialState,
+            indeterminate: isIndeterminate,
             status: TreeItemStatus.LOADED,
         };
     };