Merge branch '21764-project-picker-crash' into main. Closes #21764
[arvados.git] / services / workbench2 / src / views-components / tree-picker / tree-picker.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { connect } from "react-redux";
6 import { Tree as TreeComponent, TreeProps, TreeItem, TreeItemStatus } from "components/tree/tree";
7 import { RootState } from "store/store";
8 import { getNodeChildrenIds, Tree, createTree, getNode, TreeNodeStatus } from 'models/tree';
9 import { Dispatch } from "redux";
10 import { initTreeNode } from '../../models/tree';
11 import { ResourcesState } from "store/resources/resources";
12 import { Resource } from "models/resource";
13
14 type Callback<T> = (event: React.MouseEvent<HTMLElement>, item: TreeItem<T>, pickerId: string) => void;
15 export interface TreePickerProps<T> {
16     pickerId: string;
17     onContextMenu: Callback<T>;
18     toggleItemOpen: Callback<T>;
19     toggleItemActive: Callback<T>;
20     toggleItemSelection: Callback<T>;
21 }
22
23 const flatTree = <T>(itemsIdMap: Map<string, TreeItem<T>>, depth: number, items?: TreeItem<T>[]): TreeItem<T>[] => {
24     return items ? items
25         .map((item: TreeItem<T>) => addToItemsIdMap(item, itemsIdMap))
26         .reduce((acc: Array<TreeItem<T>>, next: TreeItem<T>) => {
27             const { items } = next;
28             acc.push({ ...next, depth });
29             acc.push(...(next.open ? flatTree(itemsIdMap, depth + 1, items) : []));
30             return acc;
31         }, [] as TreeItem<T>[]) : [];
32 };
33
34 const addToItemsIdMap = <T>(item: TreeItem<T>, itemsIdMap: Map<string, TreeItem<T>>): TreeItem<T> => {
35     itemsIdMap[item.id] = item;
36     return item;
37 };
38
39 const mapStateToProps =
40     <T>(state: RootState, props: TreePickerProps<T>): Pick<TreeProps<T>, 'items' | 'disableRipple' | 'itemsMap'> => {
41         const itemsIdMap: Map<string, TreeItem<T>> = new Map();
42         const tree: Tree<T> = state.treePicker[props.pickerId] || createTree<T>();
43         return {
44             disableRipple: true,
45             items: getNodeChildrenIds('')(tree)
46                 .map(treePickerToTreeItems(tree, state.resources))
47                 .map(item => addToItemsIdMap(item, itemsIdMap))
48                 .map(parentItem => ({
49                     ...parentItem,
50                     flatTree: true,
51                     items: flatTree(itemsIdMap, 2, parentItem.items || []),
52                 })),
53             itemsMap: itemsIdMap,
54         };
55     };
56
57 const mapDispatchToProps = <T>(_: Dispatch, props: TreePickerProps<T>): Pick<TreeProps<T>, 'onContextMenu' | 'toggleItemOpen' | 'toggleItemActive' | 'toggleItemSelection'> => ({
58     onContextMenu: (event, item) => props.onContextMenu(event, item, props.pickerId),
59     toggleItemActive: (event, item) => props.toggleItemActive(event, item, props.pickerId),
60     toggleItemOpen: (event, item) => props.toggleItemOpen(event, item, props.pickerId),
61     toggleItemSelection: (event, item) => props.toggleItemSelection(event, item, props.pickerId),
62 });
63
64 export const TreePicker = connect(mapStateToProps, mapDispatchToProps)(TreeComponent);
65
66 const treePickerToTreeItems = <T>(tree: Tree<T>, resources: ResourcesState) =>
67     (id: string): TreeItem<any> => {
68         const node = getNode(id)(tree) || initTreeNode({ id: '', value: 'InvalidNode' });
69         const items = getNodeChildrenIds(node.id)(tree)
70             .map(treePickerToTreeItems(tree, resources));
71         const resource = resources[node.id] as (Resource | undefined);
72
73         return {
74             active: node.active,
75             data: resource
76                 ? {
77                     ...resource,
78                     name: typeof node.value === "string"
79                         ? node.value
80                         : typeof (node.value as any).name === "string"
81                             ? (node.value as any).name
82                             : ""
83                   }
84                 : node.value,
85             id: node.id,
86             items: items.length > 0 ? items : undefined,
87             open: node.expanded,
88             selected: node.selected,
89             status: treeNodeStatusToTreeItem(node.status),
90         };
91     };
92
93 export const treeNodeStatusToTreeItem = (status: TreeNodeStatus) => {
94     switch (status) {
95         case TreeNodeStatus.INITIAL:
96             return TreeItemStatus.INITIAL;
97         case TreeNodeStatus.PENDING:
98             return TreeItemStatus.PENDING;
99         case TreeNodeStatus.LOADED:
100             return TreeItemStatus.LOADED;
101     }
102 };