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