927da8c36d35f7264b5774f4712e4723a816b10f
[arvados-workbench2.git] / src / components / multiselect-toolbar / 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 { MultiSelectMenuAction, MultiSelectMenuActionSet, MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-action-set";
17 import { ContextMenuAction } from "views-components/context-menu/context-menu-action-set";
18 import { multiselectActionsFilters, TMultiselectActionsFilters } from "./ms-toolbar-action-filters";
19 import { kindToActionSet, findActionByName } from "./ms-kind-action-differentiator";
20 import { msToggleTrashAction } from "views-components/multiselect-toolbar/ms-project-action-set";
21 import { copyToClipboardAction } from "store/open-in-new-tab/open-in-new-tab.actions";
22 import { ContainerRequestResource } from "models/container-request";
23 import { FavoritesState } from "store/favorites/favorites-reducer";
24
25 type CssRules = "root" | "button";
26
27 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
28     root: {
29         display: "flex",
30         flexDirection: "row",
31         width: 0,
32         padding: 0,
33         margin: "1rem auto auto 0.5rem",
34         overflow: "hidden",
35         transition: "width 150ms",
36     },
37     button: {
38         width: "2.5rem",
39         height: "2.5rem ",
40     },
41 });
42
43 export type MultiselectToolbarProps = {
44     checkedList: TCheckedList;
45     selectedUuid: string | null
46     resources: ResourcesState;
47     executeMulti: (action: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState) => void;
48     favorites: FavoritesState
49 };
50
51 export const MultiselectToolbar = connect(
52     mapStateToProps,
53     mapDispatchToProps
54 )(
55     withStyles(styles)((props: MultiselectToolbarProps & WithStyles<CssRules>) => {
56         const { classes, checkedList, resources, selectedUuid: singleSelectedUuid, favorites } = props;
57         const currentResourceKinds = Array.from(selectedToKindSet(checkedList));
58
59         const currentPathIsTrash = window.location.pathname === "/trash";
60
61         const actions =
62             currentPathIsTrash && selectedToKindSet(checkedList).size
63                 ? [msToggleTrashAction]
64                 : selectActionsByKind(currentResourceKinds, multiselectActionsFilters)
65                 .filter((action) => (singleSelectedUuid === null ? action.isForMulti : true));
66
67         return (
68             <React.Fragment>
69                 <Toolbar
70                     className={classes.root}
71                     style={{ width: `${actions.length * 2.5}rem` }}
72                 >
73                     {actions.length ? (
74                         actions.map((action, i) =>
75                             action.hasAlts ? (
76                                 <Tooltip
77                                     className={classes.button}
78                                     title={action.useAlts(singleSelectedUuid, resources, favorites) ? action.altName: action.name}
79                                     key={i}
80                                     disableFocusListener
81                                 >
82                                     <IconButton onClick={() => props.executeMulti(action, checkedList, resources)}>
83                                         {action.useAlts(singleSelectedUuid, resources, favorites) ? action.altIcon && action.altIcon({}):  action.icon({})}
84                                     </IconButton>
85                                 </Tooltip>
86                             ) : (
87                                 <Tooltip
88                                     className={classes.button}
89                                     title={action.name}
90                                     key={i}
91                                     disableFocusListener
92                                 >
93                                     <IconButton onClick={() => props.executeMulti(action, checkedList, resources)}>{action.icon({})}</IconButton>
94                                 </Tooltip>
95                             )
96                         )
97                     ) : (
98                         <></>
99                     )}
100                 </Toolbar>
101             </React.Fragment>
102         )
103     })
104 );
105
106 export function selectedToArray(checkedList: TCheckedList): Array<string> {
107     const arrayifiedSelectedList: Array<string> = [];
108     for (const [key, value] of Object.entries(checkedList)) {
109         if (value === true) {
110             arrayifiedSelectedList.push(key);
111         }
112     }
113     return arrayifiedSelectedList;
114 }
115
116 export function selectedToKindSet(checkedList: TCheckedList): Set<string> {
117     const setifiedList = new Set<string>();
118     for (const [key, value] of Object.entries(checkedList)) {
119         if (value === true) {
120             setifiedList.add(extractUuidKind(key) as string);
121         }
122     }
123     return setifiedList;
124 }
125
126 function groupByKind(checkedList: TCheckedList, resources: ResourcesState): Record<string, ContextMenuResource[]> {
127     const result = {};
128     selectedToArray(checkedList).forEach(uuid => {
129         const resource = getResource(uuid)(resources) as ContainerRequestResource | Resource;
130         if (!result[resource.kind]) result[resource.kind] = [];
131         result[resource.kind].push(resource);
132     });
133     return result;
134 }
135
136 function filterActions(actionArray: MultiSelectMenuActionSet, filters: Set<string>): Array<MultiSelectMenuAction> {
137     return actionArray[0].filter(action => filters.has(action.name as string));
138 }
139
140 function selectActionsByKind(currentResourceKinds: Array<string>, filterSet: TMultiselectActionsFilters) {
141     const rawResult: Set<MultiSelectMenuAction> = new Set();
142     const resultNames = new Set();
143     const allFiltersArray: MultiSelectMenuAction[][] = []
144     currentResourceKinds.forEach(kind => {
145         if (filterSet[kind]) {
146             const actions = filterActions(...filterSet[kind]);
147             allFiltersArray.push(actions);
148             actions.forEach(action => {
149                 if (!resultNames.has(action.name)) {
150                     rawResult.add(action);
151                     resultNames.add(action.name);
152                 }
153             });
154         }
155     });
156
157     const filteredNameSet = allFiltersArray.map(filterArray => {
158         const resultSet = new Set<string>();
159         filterArray.forEach(action => resultSet.add(action.name as string || ""));
160         return resultSet;
161     });
162
163     const filteredResult = Array.from(rawResult).filter(action => {
164         for (let i = 0; i < filteredNameSet.length; i++) {
165             if (!filteredNameSet[i].has(action.name as string)) return false;
166         }
167         return true;
168     });
169
170     return filteredResult.sort((a, b) => {
171         const nameA = a.name || "";
172         const nameB = b.name || "";
173         if (nameA < nameB) {
174             return -1;
175         }
176         if (nameA > nameB) {
177             return 1;
178         }
179         return 0;
180     });
181 }
182
183 const isExactlyOneSelected = (checkedList: TCheckedList) => {
184     let tally = 0;
185     let current = '';
186     for (const uuid in checkedList) {
187         if (checkedList[uuid] === true) {
188             tally++;
189             current = uuid;
190         }
191     }
192     return tally === 1 ? current : null
193 };
194
195 //--------------------------------------------------//
196
197 function mapStateToProps(state: RootState) {
198     return {
199         checkedList: state.multiselect.checkedList as TCheckedList,
200         selectedUuid: isExactlyOneSelected(state.multiselect.checkedList),
201         resources: state.resources,
202         favorites: state.favorites
203     };
204 }
205
206 function mapDispatchToProps(dispatch: Dispatch) {
207     return {
208         executeMulti: (selectedAction: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState): void => {
209             const kindGroups = groupByKind(checkedList, resources);
210             switch (selectedAction.name) {
211                 case MultiSelectMenuActionNames.MOVE_TO:
212                 case MultiSelectMenuActionNames.REMOVE:
213                     const firstResource = getResource(selectedToArray(checkedList)[0])(resources) as ContainerRequestResource | Resource;
214                     const action = findActionByName(selectedAction.name as string, kindToActionSet[firstResource.kind]);
215                     if (action) action.execute(dispatch, kindGroups[firstResource.kind]);
216                     break;
217                 case MultiSelectMenuActionNames.COPY_TO_CLIPBOARD:
218                     const selectedResources = selectedToArray(checkedList).map(uuid => getResource(uuid)(resources));
219                     dispatch<any>(copyToClipboardAction(selectedResources));
220                     break;
221                 default:
222                     for (const kind in kindGroups) {
223                         const action = findActionByName(selectedAction.name as string, kindToActionSet[kind]);
224                         if (action) action.execute(dispatch, kindGroups[kind]);
225                     }
226                     break;
227             }
228         },
229     };
230 }