21224: removed vestigial inputSelectedUuid param in toolbar Arvados-DCO-1.1-Signed...
[arvados.git] / services / workbench2 / 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, { useEffect, useState } 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 } from "views-components/multiselect-toolbar/ms-menu-actions";
17 import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions";
18 import { ContextMenuAction } from "views-components/context-menu/context-menu-action-set";
19 import { multiselectActionsFilters, TMultiselectActionsFilters, msMenuResourceKind } from "./ms-toolbar-action-filters";
20 import { kindToActionSet, findActionByName } from "./ms-kind-action-differentiator";
21 import { msToggleTrashAction } from "views-components/multiselect-toolbar/ms-project-action-set";
22 import { copyToClipboardAction } from "store/open-in-new-tab/open-in-new-tab.actions";
23 import { ContainerRequestResource } from "models/container-request";
24 import { FavoritesState } from "store/favorites/favorites-reducer";
25 import { resourceIsFrozen } from "common/frozen-resources";
26 import { getResourceWithEditableStatus } from "store/resources/resources";
27 import { GroupResource } from "models/group";
28 import { EditableResource } from "models/resource";
29 import { User } from "models/user";
30 import { GroupClass } from "models/group";
31 import { isProcessCancelable } from "store/processes/process";
32 import { CollectionResource } from "models/collection";
33 import { getProcess } from "store/processes/process";
34 import { Process } from "store/processes/process";
35 import { PublicFavoritesState } from "store/public-favorites/public-favorites-reducer";
36 import { AuthState } from "store/auth/auth-reducer";
37 import { IntersectionObserverWrapper } from "./ms-toolbar-overflow-wrapper";
38
39 const WIDTH_TRANSITION = 150
40
41 type CssRules = "root" | "transition" | "button" | "iconContainer" | "icon";
42
43 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
44     root: {
45         display: "flex",
46         flexDirection: "row",
47         width: 0,
48         height: '2.7rem',
49         padding: 0,
50         margin: "1rem auto auto 0.3rem",
51         transition: `width ${WIDTH_TRANSITION}ms`,
52         overflow: 'hidden',
53     },
54     transition: {
55         display: "flex",
56         flexDirection: "row",
57         height: '2.7rem',
58         padding: 0,
59         margin: "1rem auto auto 0.3rem",
60         overflow: 'hidden',
61         transition: `width ${WIDTH_TRANSITION}ms`,
62     },
63     button: {
64         width: "2.5rem",
65         height: "2.5rem ",
66         paddingLeft: 0,
67         border: "1px solid transparent",
68     },
69     iconContainer: {
70         height: '100%',
71     },
72     icon: {
73         marginLeft: '-0.5rem',
74     }
75 });
76
77 export type MultiselectToolbarProps = {
78     checkedList: TCheckedList;
79     selectedResourceUuid: string | null;
80     iconProps: IconProps
81     user: User | null
82     disabledButtons: Set<string>
83     auth: AuthState;
84     location: string;
85     isSubPanel?: boolean;
86     executeMulti: (action: ContextMenuAction | MultiSelectMenuAction, checkedList: TCheckedList, resources: ResourcesState) => void;
87 };
88
89 type IconProps = {
90     resources: ResourcesState;
91     favorites: FavoritesState;
92     publicFavorites: PublicFavoritesState;
93 }
94
95 export const MultiselectToolbar = connect(
96     mapStateToProps,
97     mapDispatchToProps
98 )(
99     withStyles(styles)((props: MultiselectToolbarProps & WithStyles<CssRules>) => {
100         const { classes, checkedList, iconProps, user, disabledButtons, selectedResourceUuid, location, isSubPanel } = props;
101         const singleResourceKind = selectedResourceUuid && !isSubPanel ? [resourceToMsResourceKind(selectedResourceUuid, iconProps.resources, user)] : null
102         const currentResourceKinds = singleResourceKind ? singleResourceKind : Array.from(selectedToKindSet(checkedList));
103         const currentPathIsTrash = location.includes("/trash");
104         const [isTransitioning, setIsTransitioning] = useState(false);
105         let transitionTimeout;
106         
107         const handleTransition = () => {
108             setIsTransitioning(true)
109             transitionTimeout = setTimeout(() => {
110                 setIsTransitioning(false)
111             }, WIDTH_TRANSITION);
112         }
113         
114         useEffect(()=>{
115                 handleTransition()
116                 return () => {
117                     if(transitionTimeout) clearTimeout(transitionTimeout)
118                 };
119             // eslint-disable-next-line
120         }, [checkedList])
121
122         const actions =
123             currentPathIsTrash && selectedToKindSet(checkedList).size
124                 ? [msToggleTrashAction]
125                 : selectActionsByKind(currentResourceKinds as string[], multiselectActionsFilters).filter((action) =>
126                         selectedResourceUuid === null ? action.isForMulti : true
127                     );
128
129         return (
130             <React.Fragment>
131                 <Toolbar
132                     className={isTransitioning ? classes.transition: classes.root}
133                     style={{ width: `${(actions.length * 2.5) + 6}rem`}}
134                     data-cy='multiselect-toolbar'
135                     >
136                     {actions.length ? (
137                         <IntersectionObserverWrapper menuLength={actions.length}>
138                             {actions.map((action, i) =>{
139                                 const { hasAlts, useAlts, name, altName, icon, altIcon } = action;
140                             return hasAlts ? (
141                                 <Tooltip
142                                     className={classes.button}
143                                     data-targetid={name}
144                                     title={currentPathIsTrash || (useAlts && useAlts(selectedResourceUuid, iconProps)) ? altName : name}
145                                     key={i}
146                                     disableFocusListener
147                                     >
148                                     <span className={classes.iconContainer}>
149                                         <IconButton
150                                             data-cy='multiselect-button'
151                                             disabled={disabledButtons.has(name)}
152                                             onClick={() => props.executeMulti(action, checkedList, iconProps.resources)}
153                                             className={classes.icon}
154                                         >
155                                             {currentPathIsTrash || (useAlts && useAlts(selectedResourceUuid, iconProps)) ? altIcon && altIcon({}) : icon({})}
156                                         </IconButton>
157                                     </span>
158                                 </Tooltip>
159                             ) : (
160                                 <Tooltip
161                                     className={classes.button}
162                                     data-targetid={name}
163                                     title={action.name}
164                                     key={i}
165                                     disableFocusListener
166                                     >
167                                     <span className={classes.iconContainer}>
168                                         <IconButton
169                                             data-cy='multiselect-button'
170                                             onClick={() => {
171                                                 props.executeMulti(action, checkedList, iconProps.resources)}}
172                                             className={classes.icon}
173                                         >
174                                             {action.icon({})}
175                                         </IconButton>
176                                     </span>
177                                 </Tooltip>
178                             );
179                             })}
180                         </IntersectionObserverWrapper>
181                     ) : (
182                         <></>
183                     )}
184                 </Toolbar>
185             </React.Fragment>
186         )
187     })
188 );
189
190 export function selectedToArray(checkedList: TCheckedList): Array<string> {
191     const arrayifiedSelectedList: Array<string> = [];
192     for (const [key, value] of Object.entries(checkedList)) {
193         if (value === true) {
194             arrayifiedSelectedList.push(key);
195         }
196     }
197     return arrayifiedSelectedList;
198 }
199
200 export function selectedToKindSet(checkedList: TCheckedList): Set<string> {
201     const setifiedList = new Set<string>();
202     for (const [key, value] of Object.entries(checkedList)) {
203         if (value === true) {
204             setifiedList.add(extractUuidKind(key) as string);
205         }
206     }
207     return setifiedList;
208 }
209
210 function groupByKind(checkedList: TCheckedList, resources: ResourcesState): Record<string, ContextMenuResource[]> {
211     const result = {};
212     selectedToArray(checkedList).forEach(uuid => {
213         const resource = getResource(uuid)(resources) as ContainerRequestResource | Resource;
214         if (!result[resource.kind]) result[resource.kind] = [];
215         result[resource.kind].push(resource);
216     });
217     return result;
218 }
219
220 function filterActions(actionArray: MultiSelectMenuActionSet, filters: Set<string>): Array<MultiSelectMenuAction> {
221     return actionArray[0].filter(action => filters.has(action.name as string));
222 }
223
224 const resourceToMsResourceKind = (uuid: string, resources: ResourcesState, user: User | null, readonly = false): (msMenuResourceKind | ResourceKind) | undefined => {
225     if (!user) return;
226     const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, user.uuid)(resources);
227     const { isAdmin } = user;
228     const kind = extractUuidKind(uuid);
229
230     const isFrozen = resourceIsFrozen(resource, resources);
231     const isEditable = (user.isAdmin || (resource || ({} as EditableResource)).isEditable) && !readonly && !isFrozen;
232
233     switch (kind) {
234         case ResourceKind.PROJECT:
235             if (isFrozen) {
236                 return isAdmin ? msMenuResourceKind.FROZEN_PROJECT_ADMIN : msMenuResourceKind.FROZEN_PROJECT;
237             }
238
239             return isAdmin && !readonly
240                 ? resource && resource.groupClass !== GroupClass.FILTER
241                     ? msMenuResourceKind.PROJECT_ADMIN
242                     : msMenuResourceKind.FILTER_GROUP_ADMIN
243                 : isEditable
244                 ? resource && resource.groupClass !== GroupClass.FILTER
245                     ? msMenuResourceKind.PROJECT
246                     : msMenuResourceKind.FILTER_GROUP
247                 : msMenuResourceKind.READONLY_PROJECT;
248         case ResourceKind.COLLECTION:
249             const c = getResource<CollectionResource>(uuid)(resources);
250             if (c === undefined) {
251                 return;
252             }
253             const isOldVersion = c.uuid !== c.currentVersionUuid;
254             const isTrashed = c.isTrashed;
255             return isOldVersion
256                 ? msMenuResourceKind.OLD_VERSION_COLLECTION
257                 : isTrashed && isEditable
258                 ? msMenuResourceKind.TRASHED_COLLECTION
259                 : isAdmin && isEditable
260                 ? msMenuResourceKind.COLLECTION_ADMIN
261                 : isEditable
262                 ? msMenuResourceKind.COLLECTION
263                 : msMenuResourceKind.READONLY_COLLECTION;
264         case ResourceKind.PROCESS:
265             return isAdmin && isEditable
266                 ? resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process)
267                     ? msMenuResourceKind.RUNNING_PROCESS_ADMIN
268                     : msMenuResourceKind.PROCESS_ADMIN
269                 : readonly
270                 ? msMenuResourceKind.READONLY_PROCESS_RESOURCE
271                 : resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process)
272                 ? msMenuResourceKind.RUNNING_PROCESS_RESOURCE
273                 : msMenuResourceKind.PROCESS_RESOURCE;
274         case ResourceKind.USER:
275             return isAdmin ? msMenuResourceKind.ROOT_PROJECT_ADMIN : msMenuResourceKind.ROOT_PROJECT;
276         case ResourceKind.LINK:
277             return msMenuResourceKind.LINK;
278         case ResourceKind.WORKFLOW:
279             return isEditable ? msMenuResourceKind.WORKFLOW : msMenuResourceKind.READONLY_WORKFLOW;
280         default:
281             return;
282     }
283 }; 
284
285 function selectActionsByKind(currentResourceKinds: Array<string>, filterSet: TMultiselectActionsFilters) {
286     const rawResult: Set<MultiSelectMenuAction> = new Set();
287     const resultNames = new Set();
288     const allFiltersArray: MultiSelectMenuAction[][] = []
289     currentResourceKinds.forEach(kind => {
290         if (filterSet[kind]) {
291             const actions = filterActions(...filterSet[kind]);
292             allFiltersArray.push(actions);
293             actions.forEach(action => {
294                 if (!resultNames.has(action.name)) {
295                     rawResult.add(action);
296                     resultNames.add(action.name);
297                 }
298             });
299         }
300     });
301
302     const filteredNameSet = allFiltersArray.map(filterArray => {
303         const resultSet = new Set<string>();
304         filterArray.forEach(action => resultSet.add(action.name as string || ""));
305         return resultSet;
306     });
307
308     const filteredResult = Array.from(rawResult).filter(action => {
309         for (let i = 0; i < filteredNameSet.length; i++) {
310             if (!filteredNameSet[i].has(action.name as string)) return false;
311         }
312         return true;
313     });
314
315     return filteredResult.sort((a, b) => {
316         const nameA = a.name || "";
317         const nameB = b.name || "";
318         if (nameA < nameB) {
319             return -1;
320         }
321         if (nameA > nameB) {
322             return 1;
323         }
324         return 0;
325     });
326 }
327
328
329 //--------------------------------------------------//
330
331 function mapStateToProps({auth, multiselect, resources, favorites, publicFavorites, selectedResourceUuid}: RootState) {
332     return {
333         checkedList: multiselect.checkedList as TCheckedList,
334         user: auth && auth.user ? auth.user : null,
335         disabledButtons: new Set<string>(multiselect.disabledButtons),
336         auth,
337         selectedResourceUuid,
338         location: window.location.pathname,
339         iconProps: {
340             resources,
341             favorites,
342             publicFavorites
343         }
344     }
345 }
346
347 function mapDispatchToProps(dispatch: Dispatch) {
348     return {
349         executeMulti: (selectedAction: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState): void => {
350             const kindGroups = groupByKind(checkedList, resources);
351             const currentList = selectedToArray(checkedList)
352             switch (selectedAction.name) {
353                 case MultiSelectMenuActionNames.MOVE_TO:
354                 case MultiSelectMenuActionNames.REMOVE:
355                     const firstResource = getResource(currentList[0])(resources) as ContainerRequestResource | Resource;
356                     const action = findActionByName(selectedAction.name as string, kindToActionSet[firstResource.kind]);
357                     if (action) action.execute(dispatch, kindGroups[firstResource.kind]);
358                     break;
359                 case MultiSelectMenuActionNames.COPY_TO_CLIPBOARD:
360                     const selectedResources = currentList.map(uuid => getResource(uuid)(resources));
361                     dispatch<any>(copyToClipboardAction(selectedResources));
362                     break;
363                 default:
364                     for (const kind in kindGroups) {
365                         const action = findActionByName(selectedAction.name as string, kindToActionSet[kind]);
366                         if (action) action.execute(dispatch, kindGroups[kind]);
367                     }
368                     break;
369             }
370         },
371     };
372 }