21128: more css tweaks Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox@curii...
[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, 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 type CssRules = "root" | "button" | "iconContainer";
39
40 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
41     root: {
42         display: "flex",
43         flexDirection: "row",
44         width: 0,
45         height: '2.7rem',
46         padding: 0,
47         margin: "1rem auto auto 0.5rem",
48         overflowY: 'auto',
49         transition: "width 150ms",
50         scrollBehavior: 'smooth',
51         '&::-webkit-scrollbar': {
52             width: 0,
53             height: 2
54         },
55         '&::-webkit-scrollbar-track': {
56             width: 0,
57             height: 2
58         },
59         '&::-webkit-scrollbar-thumb': {
60             backgroundColor: '#757575',
61             borderRadius: 2
62         }
63     },
64     button: {
65         width: "2.5rem",
66         height: "2.5rem ",
67     },
68     iconContainer: {
69         height: '100%'
70     }
71 });
72
73 export type MultiselectToolbarProps = {
74     checkedList: TCheckedList;
75     singleSelectedUuid: string | null
76     iconProps: IconProps
77     user: User | null
78     disabledButtons: Set<string>
79     executeMulti: (action: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState) => void;
80 };
81
82 type IconProps = {
83     resources: ResourcesState;
84     favorites: FavoritesState;
85     publicFavorites: PublicFavoritesState;
86 }
87
88 export const MultiselectToolbar = connect(
89     mapStateToProps,
90     mapDispatchToProps
91 )(
92     withStyles(styles)((props: MultiselectToolbarProps & WithStyles<CssRules>) => {
93         const { classes, checkedList, singleSelectedUuid, iconProps, user , disabledButtons} = props;
94         const singleResourceKind = singleSelectedUuid ? [resourceToMsResourceKind(singleSelectedUuid, iconProps.resources, user)] : null
95         const currentResourceKinds = singleResourceKind ? singleResourceKind : Array.from(selectedToKindSet(checkedList));
96         const currentPathIsTrash = window.location.pathname === "/trash";
97         
98         const actions =
99         currentPathIsTrash && selectedToKindSet(checkedList).size
100         ? [msToggleTrashAction]
101         : selectActionsByKind(currentResourceKinds as string[], multiselectActionsFilters)
102         .filter((action) => (singleSelectedUuid === null ? action.isForMulti : true));
103         
104         return (
105             <React.Fragment>
106                 <Toolbar
107                     className={classes.root}
108                     style={{ width: `${(actions.length * 2.5) + 1}rem` }}
109                     >
110                     {actions.length ? (
111                         actions.map((action, i) =>{
112                             const { hasAlts, useAlts, name, altName, icon, altIcon } = action;
113                         return hasAlts ? (
114                             <Tooltip
115                             className={classes.button}
116                             title={currentPathIsTrash || (useAlts && useAlts(singleSelectedUuid, iconProps)) ? altName : name}
117                             key={i}
118                             disableFocusListener
119                             >
120                                     <span className={classes.iconContainer}>
121                                         <IconButton disabled={disabledButtons.has(name)} onClick={() => props.executeMulti(action, checkedList, iconProps.resources)}>
122                                             {currentPathIsTrash || (useAlts && useAlts(singleSelectedUuid, iconProps)) ? altIcon && altIcon({}) : icon({})}
123                                         </IconButton>
124                                     </span>
125                                 </Tooltip>
126                             ) : (
127                                 <Tooltip
128                                     className={classes.button}
129                                     title={action.name}
130                                     key={i}
131                                     disableFocusListener
132                                 >
133                                     <span className={classes.iconContainer}>
134                                         <IconButton onClick={() => props.executeMulti(action, checkedList, iconProps.resources)}>{action.icon({})}</IconButton>
135                                     </span>
136                                 </Tooltip>
137                             )
138                         })
139                     ) : (
140                         <></>
141                     )}
142                 </Toolbar>
143             </React.Fragment>
144         )
145     })
146 );
147
148 export function selectedToArray(checkedList: TCheckedList): Array<string> {
149     const arrayifiedSelectedList: Array<string> = [];
150     for (const [key, value] of Object.entries(checkedList)) {
151         if (value === true) {
152             arrayifiedSelectedList.push(key);
153         }
154     }
155     return arrayifiedSelectedList;
156 }
157
158 export function selectedToKindSet(checkedList: TCheckedList): Set<string> {
159     const setifiedList = new Set<string>();
160     for (const [key, value] of Object.entries(checkedList)) {
161         if (value === true) {
162             setifiedList.add(extractUuidKind(key) as string);
163         }
164     }
165     return setifiedList;
166 }
167
168 function groupByKind(checkedList: TCheckedList, resources: ResourcesState): Record<string, ContextMenuResource[]> {
169     const result = {};
170     selectedToArray(checkedList).forEach(uuid => {
171         const resource = getResource(uuid)(resources) as ContainerRequestResource | Resource;
172         if (!result[resource.kind]) result[resource.kind] = [];
173         result[resource.kind].push(resource);
174     });
175     return result;
176 }
177
178 function filterActions(actionArray: MultiSelectMenuActionSet, filters: Set<string>): Array<MultiSelectMenuAction> {
179     return actionArray[0].filter(action => filters.has(action.name as string));
180 }
181
182 const resourceToMsResourceKind = (uuid: string, resources: ResourcesState, user: User | null, readonly = false): (msMenuResourceKind | ResourceKind) | undefined => {
183     if (!user) return;
184     const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, user.uuid)(resources);
185     const { isAdmin } = user;
186     const kind = extractUuidKind(uuid);
187
188     const isFrozen = resourceIsFrozen(resource, resources);
189     const isEditable = (user.isAdmin || (resource || ({} as EditableResource)).isEditable) && !readonly && !isFrozen;
190
191     switch (kind) {
192         case ResourceKind.PROJECT:
193             if (isFrozen) {
194                 return isAdmin ? msMenuResourceKind.FROZEN_PROJECT_ADMIN : msMenuResourceKind.FROZEN_PROJECT;
195             }
196
197             return isAdmin && !readonly
198                 ? resource && resource.groupClass !== GroupClass.FILTER
199                     ? msMenuResourceKind.PROJECT_ADMIN
200                     : msMenuResourceKind.FILTER_GROUP_ADMIN
201                 : isEditable
202                 ? resource && resource.groupClass !== GroupClass.FILTER
203                     ? msMenuResourceKind.PROJECT
204                     : msMenuResourceKind.FILTER_GROUP
205                 : msMenuResourceKind.READONLY_PROJECT;
206         case ResourceKind.COLLECTION:
207             const c = getResource<CollectionResource>(uuid)(resources);
208             if (c === undefined) {
209                 return;
210             }
211             const isOldVersion = c.uuid !== c.currentVersionUuid;
212             const isTrashed = c.isTrashed;
213             return isOldVersion
214                 ? msMenuResourceKind.OLD_VERSION_COLLECTION
215                 : isTrashed && isEditable
216                 ? msMenuResourceKind.TRASHED_COLLECTION
217                 : isAdmin && isEditable
218                 ? msMenuResourceKind.COLLECTION_ADMIN
219                 : isEditable
220                 ? msMenuResourceKind.COLLECTION
221                 : msMenuResourceKind.READONLY_COLLECTION;
222         case ResourceKind.PROCESS:
223             return isAdmin && isEditable
224                 ? resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process)
225                     ? msMenuResourceKind.RUNNING_PROCESS_ADMIN
226                     : msMenuResourceKind.PROCESS_ADMIN
227                 : readonly
228                 ? msMenuResourceKind.READONLY_PROCESS_RESOURCE
229                 : resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process)
230                 ? msMenuResourceKind.RUNNING_PROCESS_RESOURCE
231                 : msMenuResourceKind.PROCESS_RESOURCE;
232         case ResourceKind.USER:
233             return msMenuResourceKind.ROOT_PROJECT;
234         case ResourceKind.LINK:
235             return msMenuResourceKind.LINK;
236         case ResourceKind.WORKFLOW:
237             return isEditable ? msMenuResourceKind.WORKFLOW : msMenuResourceKind.READONLY_WORKFLOW;
238         default:
239             return;
240     }
241 }; 
242
243 function selectActionsByKind(currentResourceKinds: Array<string>, filterSet: TMultiselectActionsFilters) {
244     const rawResult: Set<MultiSelectMenuAction> = new Set();
245     const resultNames = new Set();
246     const allFiltersArray: MultiSelectMenuAction[][] = []
247     currentResourceKinds.forEach(kind => {
248         if (filterSet[kind]) {
249             const actions = filterActions(...filterSet[kind]);
250             allFiltersArray.push(actions);
251             actions.forEach(action => {
252                 if (!resultNames.has(action.name)) {
253                     rawResult.add(action);
254                     resultNames.add(action.name);
255                 }
256             });
257         }
258     });
259
260     const filteredNameSet = allFiltersArray.map(filterArray => {
261         const resultSet = new Set<string>();
262         filterArray.forEach(action => resultSet.add(action.name as string || ""));
263         return resultSet;
264     });
265
266     const filteredResult = Array.from(rawResult).filter(action => {
267         for (let i = 0; i < filteredNameSet.length; i++) {
268             if (!filteredNameSet[i].has(action.name as string)) return false;
269         }
270         return true;
271     });
272
273     return filteredResult.sort((a, b) => {
274         const nameA = a.name || "";
275         const nameB = b.name || "";
276         if (nameA < nameB) {
277             return -1;
278         }
279         if (nameA > nameB) {
280             return 1;
281         }
282         return 0;
283     });
284 }
285
286
287 //--------------------------------------------------//
288
289 function mapStateToProps({auth, multiselect, resources, favorites, publicFavorites}: RootState) {
290     return {
291         checkedList: multiselect.checkedList as TCheckedList,
292         singleSelectedUuid: isExactlyOneSelected(multiselect.checkedList),
293         user: auth && auth.user ? auth.user : null,
294         disabledButtons: new Set<string>(multiselect.disabledButtons),
295         iconProps: {
296             resources,
297             favorites,
298             publicFavorites
299         }
300     }
301 }
302
303 function mapDispatchToProps(dispatch: Dispatch) {
304     return {
305         executeMulti: (selectedAction: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState): void => {
306             const kindGroups = groupByKind(checkedList, resources);
307             switch (selectedAction.name) {
308                 case MultiSelectMenuActionNames.MOVE_TO:
309                 case MultiSelectMenuActionNames.REMOVE:
310                     const firstResource = getResource(selectedToArray(checkedList)[0])(resources) as ContainerRequestResource | Resource;
311                     const action = findActionByName(selectedAction.name as string, kindToActionSet[firstResource.kind]);
312                     if (action) action.execute(dispatch, kindGroups[firstResource.kind]);
313                     break;
314                 case MultiSelectMenuActionNames.COPY_TO_CLIPBOARD:
315                     const selectedResources = selectedToArray(checkedList).map(uuid => getResource(uuid)(resources));
316                     dispatch<any>(copyToClipboardAction(selectedResources));
317                     break;
318                 default:
319                     for (const kind in kindGroups) {
320                         const action = findActionByName(selectedAction.name as string, kindToActionSet[kind]);
321                         if (action) action.execute(dispatch, kindGroups[kind]);
322                     }
323                     break;
324             }
325         },
326     };
327 }