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