Merge branch '19143-project-list-workflows'
[arvados-workbench2.git] / src / views-components / tree-picker / tree-picker.ts
index 0cd55f121ffa9a7ac88bc8229fc2ae53643b97aa..86c76e0824ce6c78ffb66ea6c487a28ce9db313f 100644 (file)
@@ -3,47 +3,95 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { connect } from "react-redux";
-import { Tree, TreeProps, TreeItem, TreeItemStatus } from "~/components/tree/tree";
-import { RootState } from "~/store/store";
-import { createTreePickerNode, TreePickerNode } from "~/store/tree-picker/tree-picker";
-import { getNodeValue, getNodeChildrenIds, Tree as Ttree, createTree } from "~/models/tree";
+import { Tree, TreeProps, TreeItem, TreeItemStatus } from "components/tree/tree";
+import { RootState } from "store/store";
+import { getNodeChildrenIds, Tree as Ttree, createTree, getNode, TreeNodeStatus } from 'models/tree';
 import { Dispatch } from "redux";
+import { initTreeNode } from '../../models/tree';
 
-export interface TreePickerProps {
+type Callback<T> = (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>, pickerId: string) => void;
+export interface TreePickerProps<T> {
     pickerId: string;
-    onContextMenu: (event: React.MouseEvent<HTMLElement>, nodeId: string, pickerId: string) => void;
-    toggleItemOpen: (nodeId: string, status: TreeItemStatus, pickerId: string) => void;
-    toggleItemActive: (nodeId: string, status: TreeItemStatus, pickerId: string) => void;
+    onContextMenu: Callback<T>;
+    toggleItemOpen: Callback<T>;
+    toggleItemActive: Callback<T>;
+    toggleItemSelection: Callback<T>;
 }
 
-const mapStateToProps = (state: RootState, props: TreePickerProps): Pick<TreeProps<any>, 'items'> => {
-    const tree = state.treePicker[props.pickerId] || createTree();
-    return {
-        items: getNodeChildrenIds('')(tree)
-            .map(treePickerToTreeItems(tree))
+const flatTree = (itemsIdMap: Map<string, any>, depth: number, items?: any): [] => {
+    return items ? items
+        .map((item: any) => addToItemsIdMap(item, itemsIdMap))
+        .reduce((prev: Array<any>, next: any) => {
+            const { items } = next;
+            prev.push({ ...next, depth });
+            prev.push(...(next.open ? flatTree(itemsIdMap, depth + 1, items) : []));
+            return prev;
+        }, []) : [];
+};
+
+const addToItemsIdMap = <T>(item: TreeItem<T>, itemsIdMap: Map<string, TreeItem<T>>) => {
+    itemsIdMap[item.id] = item;
+    return item;
+};
+
+const memoizedMapStateToProps = () => {
+    let prevTree: Ttree<any>;
+    let mappedProps: Pick<TreeProps<any>, 'items' | 'disableRipple' | 'itemsMap'>;
+    return <T>(state: RootState, props: TreePickerProps<T>): Pick<TreeProps<T>, 'items' | 'disableRipple' | 'itemsMap'> => {
+        const itemsIdMap: Map<string, TreeItem<T>> = new Map();
+        const tree = state.treePicker[props.pickerId] || createTree();
+        if (tree !== prevTree) {
+            prevTree = tree;
+            mappedProps = {
+                disableRipple: true,
+                items: getNodeChildrenIds('')(tree)
+                    .map(treePickerToTreeItems(tree))
+                    .map(item => addToItemsIdMap(item, itemsIdMap))
+                    .map(parentItem => ({
+                        ...parentItem,
+                        flatTree: true,
+                        items: flatTree(itemsIdMap, 2, parentItem.items || []),
+                    })),
+                itemsMap: itemsIdMap,
+            };
+        }
+        return mappedProps;
     };
 };
 
-const mapDispatchToProps = (dispatch: Dispatch, props: TreePickerProps): Pick<TreeProps<any>, 'onContextMenu' | 'toggleItemOpen' | 'toggleItemActive'> => ({
-    onContextMenu: (event, item) => props.onContextMenu(event, item.id, props.pickerId),
-    toggleItemActive: (id, status) => props.toggleItemActive(id, status, props.pickerId),
-    toggleItemOpen: (id, status) => props.toggleItemOpen(id, status, props.pickerId)
+const mapDispatchToProps = (_: Dispatch, props: TreePickerProps<any>): Pick<TreeProps<any>, 'onContextMenu' | 'toggleItemOpen' | 'toggleItemActive' | 'toggleItemSelection'> => ({
+    onContextMenu: (event, item) => props.onContextMenu(event, item, props.pickerId),
+    toggleItemActive: (event, item) => props.toggleItemActive(event, item, props.pickerId),
+    toggleItemOpen: (event, item) => props.toggleItemOpen(event, item, props.pickerId),
+    toggleItemSelection: (event, item) => props.toggleItemSelection(event, item, props.pickerId),
 });
 
-export const TreePicker = connect(mapStateToProps, mapDispatchToProps)(Tree);
+export const TreePicker = connect(memoizedMapStateToProps(), mapDispatchToProps)(Tree);
 
-const treePickerToTreeItems = (tree: Ttree<TreePickerNode>) =>
+const treePickerToTreeItems = (tree: Ttree<any>) =>
     (id: string): TreeItem<any> => {
-        const node: TreePickerNode = getNodeValue(id)(tree) || createTreePickerNode({ nodeId: '', value: 'InvalidNode' });
-        const items = getNodeChildrenIds(node.nodeId)(tree)
+        const node = getNode(id)(tree) || initTreeNode({ id: '', value: 'InvalidNode' });
+        const items = getNodeChildrenIds(node.id)(tree)
             .map(treePickerToTreeItems(tree));
         return {
-            active: node.selected,
+            active: node.active,
             data: node.value,
-            id: node.nodeId,
+            id: node.id,
             items: items.length > 0 ? items : undefined,
-            open: !node.collapsed,
-            status: node.status
+            open: node.expanded,
+            selected: node.selected,
+            status: treeNodeStatusToTreeItem(node.status),
         };
     };
 
+export const treeNodeStatusToTreeItem = (status: TreeNodeStatus) => {
+    switch (status) {
+        case TreeNodeStatus.INITIAL:
+            return TreeItemStatus.INITIAL;
+        case TreeNodeStatus.PENDING:
+            return TreeItemStatus.PENDING;
+        case TreeNodeStatus.LOADED:
+            return TreeItemStatus.LOADED;
+    }
+};
+