21224: disabled eslint in useeffect Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa...
[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 { isExactlyOneSelected } from "store/multiselect/multiselect-actions";
37
38 const WIDTH_TRANSITION = 150
39
40 type CssRules = "root" | "transition" | "button" | "iconContainer";
41
42 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
43     root: {
44         display: "flex",
45         flexDirection: "row",
46         width: 0,
47         height: '2.7rem',
48         padding: 0,
49         margin: "1rem auto auto 0.5rem",
50         transition: `width ${WIDTH_TRANSITION}ms`,
51         overflowY: 'auto',
52         scrollBehavior: 'smooth',
53         '&::-webkit-scrollbar': {
54             width: 0,
55             height: 2
56         },
57         '&::-webkit-scrollbar-track': {
58             width: 0,
59             height: 2
60         },
61         '&::-webkit-scrollbar-thumb': {
62             backgroundColor: '#757575',
63             borderRadius: 2
64         }
65     },
66     transition: {
67         display: "flex",
68         flexDirection: "row",
69         width: 0,
70         height: '2.7rem',
71         padding: 0,
72         margin: "1rem auto auto 0.5rem",
73         overflow: 'hidden',
74         transition: `width ${WIDTH_TRANSITION}ms`,
75     },
76     button: {
77         width: "2.5rem",
78         height: "2.5rem ",
79     },
80     iconContainer: {
81         height: '100%'
82     }
83 });
84
85 export type MultiselectToolbarProps = {
86     checkedList: TCheckedList;
87     singleSelectedUuid: string | null
88     inputSelectedUuid?: string
89     iconProps: IconProps
90     user: User | null
91     disabledButtons: Set<string>
92     executeMulti: (action: ContextMenuAction, inputSelectedUuid: string | undefined, checkedList: TCheckedList, resources: ResourcesState) => void;
93 };
94
95 type IconProps = {
96     resources: ResourcesState;
97     favorites: FavoritesState;
98     publicFavorites: PublicFavoritesState;
99 }
100
101 export const MultiselectToolbar = connect(
102     mapStateToProps,
103     mapDispatchToProps
104 )(
105     withStyles(styles)((props: MultiselectToolbarProps & WithStyles<CssRules>) => {
106         const { classes, checkedList, inputSelectedUuid, iconProps, user, disabledButtons } = props;
107         const singleSelectedUuid = inputSelectedUuid ?? props.singleSelectedUuid
108         const singleResourceKind = singleSelectedUuid ? [resourceToMsResourceKind(singleSelectedUuid, iconProps.resources, user)] : null
109         const currentResourceKinds = singleResourceKind ? singleResourceKind : Array.from(selectedToKindSet(checkedList));
110         const currentPathIsTrash = window.location.pathname === "/trash";
111         const [isTransitioning, setIsTransitioning] = useState(false);
112         let transitionTimeout;
113         
114         const handleTransition = () => {
115             setIsTransitioning(true)
116             transitionTimeout = setTimeout(() => {
117                 setIsTransitioning(false)
118             }, WIDTH_TRANSITION);
119         }
120         
121         useEffect(()=>{
122                 handleTransition()
123                 return () => {
124                     if(transitionTimeout) clearTimeout(transitionTimeout)
125                 };
126             // eslint-disable-next-line
127         }, [checkedList])
128
129         const actions =
130             currentPathIsTrash && selectedToKindSet(checkedList).size
131                 ? [msToggleTrashAction]
132                 : selectActionsByKind(currentResourceKinds as string[], multiselectActionsFilters).filter((action) =>
133                         singleSelectedUuid === null ? action.isForMulti : true
134                     );
135
136         return (
137             <React.Fragment>
138                 <Toolbar
139                     className={isTransitioning ? classes.transition: classes.root}
140                     style={{ width: `${(actions.length * 2.5) + 1}rem` }}
141                     data-cy='multiselect-toolbar'
142                     >
143                     {actions.length ? (
144                         actions.map((action, i) =>{
145                             const { hasAlts, useAlts, name, altName, icon, altIcon } = action;
146                         return hasAlts ? (
147                             <Tooltip
148                                 className={classes.button}
149                                 title={currentPathIsTrash || (useAlts && useAlts(singleSelectedUuid, iconProps)) ? altName : name}
150                                 key={i}
151                                 disableFocusListener
152                             >
153                                 <span className={classes.iconContainer}>
154                                     <IconButton
155                                         data-cy='multiselect-button'
156                                         disabled={disabledButtons.has(name)}
157                                         onClick={() => props.executeMulti(action, inputSelectedUuid, checkedList, iconProps.resources)}
158                                     >
159                                         {currentPathIsTrash || (useAlts && useAlts(singleSelectedUuid, iconProps)) ? altIcon && altIcon({}) : icon({})}
160                                     </IconButton>
161                                 </span>
162                             </Tooltip>
163                         ) : (
164                             <Tooltip
165                                 className={classes.button}
166                                 title={action.name}
167                                 key={i}
168                                 disableFocusListener
169                             >
170                                 <span className={classes.iconContainer}>
171                                     <IconButton
172                                         data-cy='multiselect-button'
173                                         onClick={() => props.executeMulti(action, inputSelectedUuid, checkedList, iconProps.resources)}
174                                     >
175                                         {action.icon({})}
176                                     </IconButton>
177                                 </span>
178                             </Tooltip>
179                         );
180                         })
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 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}: RootState) {
332     return {
333         checkedList: multiselect.checkedList as TCheckedList,
334         singleSelectedUuid: isExactlyOneSelected(multiselect.checkedList),
335         user: auth && auth.user ? auth.user : null,
336         disabledButtons: new Set<string>(multiselect.disabledButtons),
337         iconProps: {
338             resources,
339             favorites,
340             publicFavorites
341         }
342     }
343 }
344
345 function mapDispatchToProps(dispatch: Dispatch) {
346     return {
347         executeMulti: (selectedAction: ContextMenuAction, inputSelectedUuid: string | undefined, checkedList: TCheckedList, resources: ResourcesState): void => {
348             const kindGroups = inputSelectedUuid ? groupByKind({[inputSelectedUuid]: true}, resources) : groupByKind(checkedList, resources);
349             const currentList = inputSelectedUuid ? [inputSelectedUuid] : selectedToArray(checkedList)
350             switch (selectedAction.name) {
351                 case MultiSelectMenuActionNames.MOVE_TO:
352                 case MultiSelectMenuActionNames.REMOVE:
353                     const firstResource = getResource(currentList[0])(resources) as ContainerRequestResource | Resource;
354                     const action = findActionByName(selectedAction.name as string, kindToActionSet[firstResource.kind]);
355                     if (action) action.execute(dispatch, kindGroups[firstResource.kind]);
356                     break;
357                 case MultiSelectMenuActionNames.COPY_TO_CLIPBOARD:
358                     const selectedResources = currentList.map(uuid => getResource(uuid)(resources));
359                     dispatch<any>(copyToClipboardAction(selectedResources));
360                     break;
361                 default:
362                     for (const kind in kindGroups) {
363                         const action = findActionByName(selectedAction.name as string, kindToActionSet[kind]);
364                         if (action) action.execute(dispatch, kindGroups[kind]);
365                     }
366                     break;
367             }
368         },
369     };
370 }