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