15768: project multimove works Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox...
[arvados.git] / src / components / multiselectToolbar / MultiselectToolbar.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 { connect } from "react-redux";
7 import { StyleRulesCallback, withStyles, WithStyles, Toolbar, Tooltip, IconButton } from "@material-ui/core";
8 import { ArvadosTheme } from "common/custom-theme";
9 import { RootState } from "store/store";
10 import { Dispatch } from "redux";
11 import { TCheckedList } from "components/data-table/data-table";
12 import { ContextMenuResource } from "store/context-menu/context-menu-actions";
13 import { Resource, extractUuidKind } from "models/resource";
14 import { getResource } from "store/resources/resources";
15 import { ResourcesState } from "store/resources/resources";
16 import { ContextMenuAction, ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
17 import { RestoreFromTrashIcon, TrashIcon } from "components/icon/icon";
18 import { multiselectActionsFilters, TMultiselectActionsFilters } from "./ms-toolbar-action-filters";
19 import { kindToActionSet, findActionByName } from "./ms-kind-action-differentiator";
20 import { toggleTrashAction } from "views-components/context-menu/action-sets/project-action-set";
21
22 type CssRules = "root" | "button";
23
24 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
25     root: {
26         display: "flex",
27         flexDirection: "row",
28         width: 0,
29         padding: 0,
30         margin: "1rem auto auto 0.5rem",
31         overflow: "hidden",
32         transition: "width 150ms",
33     },
34     button: {
35         width: "1rem",
36         margin: "auto 5px",
37     },
38 });
39
40 export type MultiselectToolbarProps = {
41     isVisible: boolean;
42     checkedList: TCheckedList;
43     resources: ResourcesState;
44     executeMulti: (action: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState) => void;
45 };
46
47 export const MultiselectToolbar = connect(
48     mapStateToProps,
49     mapDispatchToProps
50 )(
51     withStyles(styles)((props: MultiselectToolbarProps & WithStyles<CssRules>) => {
52         const { classes, checkedList } = props;
53         const currentResourceKinds = Array.from(selectedToKindSet(checkedList));
54
55         const currentPathIsTrash = window.location.pathname === "/trash";
56         const buttons =
57             currentPathIsTrash && selectedToKindSet(checkedList).size
58                 ? [toggleTrashAction]
59                 : selectActionsByKind(currentResourceKinds, multiselectActionsFilters);
60
61         return (
62             <Toolbar
63                 className={classes.root}
64                 style={{ width: `${buttons.length * 2.12}rem` }}>
65                 {buttons.length ? (
66                     buttons.map((btn, i) =>
67                         btn.name === "ToggleTrashAction" ? (
68                             <Tooltip
69                                 className={classes.button}
70                                 title={currentPathIsTrash ? "Restore" : "Move to trash"}
71                                 key={i}
72                                 disableFocusListener>
73                                 <IconButton onClick={() => props.executeMulti(btn, checkedList, props.resources)}>
74                                     {currentPathIsTrash ? <RestoreFromTrashIcon /> : <TrashIcon />}
75                                 </IconButton>
76                             </Tooltip>
77                         ) : (
78                             <Tooltip
79                                 className={classes.button}
80                                 title={btn.name}
81                                 key={i}
82                                 disableFocusListener>
83                                 <IconButton onClick={() => props.executeMulti(btn, checkedList, props.resources)}>
84                                     {btn.icon ? btn.icon({}) : <></>}
85                                 </IconButton>
86                             </Tooltip>
87                         )
88                     )
89                 ) : (
90                     <></>
91                 )}
92             </Toolbar>
93         );
94     })
95 );
96
97 export function selectedToArray(checkedList: TCheckedList): Array<string> {
98     const arrayifiedSelectedList: Array<string> = [];
99     for (const [key, value] of Object.entries(checkedList)) {
100         if (value === true) {
101             arrayifiedSelectedList.push(key);
102         }
103     }
104     return arrayifiedSelectedList;
105 }
106
107 function selectedToKindSet(checkedList: TCheckedList): Set<string> {
108     const setifiedList = new Set<string>();
109     for (const [key, value] of Object.entries(checkedList)) {
110         if (value === true) {
111             setifiedList.add(extractUuidKind(key) as string);
112         }
113     }
114     return setifiedList;
115 }
116
117 function filterActions(actionArray: ContextMenuActionSet, filters: Set<string>): Array<ContextMenuAction> {
118     return actionArray[0].filter(action => filters.has(action.name as string));
119 }
120
121 function selectActionsByKind(currentResourceKinds: Array<string>, filterSet: TMultiselectActionsFilters) {
122     const rawResult: Set<ContextMenuAction> = new Set();
123     const resultNames = new Set();
124     const allFiltersArray: ContextMenuAction[][] = [];
125     currentResourceKinds.forEach(kind => {
126         if (filterSet[kind]) {
127             const actions = filterActions(...filterSet[kind]);
128             allFiltersArray.push(actions);
129             actions.forEach(action => {
130                 if (!resultNames.has(action.name)) {
131                     rawResult.add(action);
132                     resultNames.add(action.name);
133                 }
134             });
135         }
136     });
137
138     const filteredNameSet = allFiltersArray.map(filterArray => {
139         const resultSet = new Set();
140         filterArray.forEach(action => resultSet.add(action.name || ""));
141         return resultSet;
142     });
143
144     const filteredResult = Array.from(rawResult).filter(action => {
145         for (let i = 0; i < filteredNameSet.length; i++) {
146             if (!filteredNameSet[i].has(action.name)) return false;
147         }
148         return true;
149     });
150
151     return filteredResult.sort((a, b) => {
152         const nameA = a.name || "";
153         const nameB = b.name || "";
154         if (nameA < nameB) {
155             return -1;
156         }
157         if (nameA > nameB) {
158             return 1;
159         }
160         return 0;
161     });
162 }
163
164 //--------------------------------------------------//
165
166 function mapStateToProps(state: RootState) {
167     const { isVisible, checkedList } = state.multiselect;
168     return {
169         isVisible: isVisible,
170         checkedList: checkedList as TCheckedList,
171         resources: state.resources,
172     };
173 }
174
175 function mapDispatchToProps(dispatch: Dispatch) {
176     return {
177         executeMulti: (selectedAction: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState): void => {
178             const kindGroups = groupByKind(checkedList, resources);
179             for (const kind in kindGroups) {
180                 const actionSet = kindToActionSet[kind];
181                 const action = findActionByName(selectedAction.name as string, actionSet);
182
183                 if (action) action.execute(dispatch, kindGroups[kind]);
184                 // if (action && action.name === 'ToggleTrashAction') action.execute(dispatch, kindGroups[kind]);
185             }
186         },
187     };
188 }
189
190 function groupByKind(checkedList: TCheckedList, resources: ResourcesState): Record<string, ContextMenuResource[]> {
191     const result = {};
192     selectedToArray(checkedList).forEach(uuid => {
193         const resource = getResource(uuid)(resources) as Resource;
194         if (!result[resource.kind]) result[resource.kind] = [];
195         result[resource.kind].push(resource);
196     });
197     return result;
198 }