21128: filter groups works Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox@curii...
[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) => {
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         default:
157             return [resource.kind]
158     }
159 }; 
160
161 function selectActionsByKind(currentResourceKinds: Array<string>, filterSet: TMultiselectActionsFilters) {
162     const rawResult: Set<MultiSelectMenuAction> = new Set();
163     const resultNames = new Set();
164     const allFiltersArray: MultiSelectMenuAction[][] = []
165     currentResourceKinds.forEach(kind => {
166         if (filterSet[kind]) {
167             const actions = filterActions(...filterSet[kind]);
168             allFiltersArray.push(actions);
169             actions.forEach(action => {
170                 if (!resultNames.has(action.name)) {
171                     rawResult.add(action);
172                     resultNames.add(action.name);
173                 }
174             });
175         }
176     });
177
178     const filteredNameSet = allFiltersArray.map(filterArray => {
179         const resultSet = new Set<string>();
180         filterArray.forEach(action => resultSet.add(action.name as string || ""));
181         return resultSet;
182     });
183
184     const filteredResult = Array.from(rawResult).filter(action => {
185         for (let i = 0; i < filteredNameSet.length; i++) {
186             if (!filteredNameSet[i].has(action.name as string)) return false;
187         }
188         return true;
189     });
190
191     return filteredResult.sort((a, b) => {
192         const nameA = a.name || "";
193         const nameB = b.name || "";
194         if (nameA < nameB) {
195             return -1;
196         }
197         if (nameA > nameB) {
198             return 1;
199         }
200         return 0;
201     });
202 }
203
204 export const isExactlyOneSelected = (checkedList: TCheckedList) => {
205     let tally = 0;
206     let current = '';
207     for (const uuid in checkedList) {
208         if (checkedList[uuid] === true) {
209             tally++;
210             current = uuid;
211         }
212     }
213     return tally === 1 ? current : null
214 };
215
216 //--------------------------------------------------//
217
218 function mapStateToProps({multiselect, resources, favorites}: RootState) {
219     return {
220         checkedList: multiselect.checkedList as TCheckedList,
221         selectedUuid: isExactlyOneSelected(multiselect.checkedList),
222         iconProps: {
223             resources,
224             favorites
225         }
226     }
227 }
228
229 function mapDispatchToProps(dispatch: Dispatch) {
230     return {
231         executeMulti: (selectedAction: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState): void => {
232             const kindGroups = groupByKind(checkedList, resources);
233             switch (selectedAction.name) {
234                 case MultiSelectMenuActionNames.MOVE_TO:
235                 case MultiSelectMenuActionNames.REMOVE:
236                     const firstResource = getResource(selectedToArray(checkedList)[0])(resources) as ContainerRequestResource | Resource;
237                     const action = findActionByName(selectedAction.name as string, kindToActionSet[firstResource.kind]);
238                     if (action) action.execute(dispatch, kindGroups[firstResource.kind]);
239                     break;
240                 case MultiSelectMenuActionNames.COPY_TO_CLIPBOARD:
241                     const selectedResources = selectedToArray(checkedList).map(uuid => getResource(uuid)(resources));
242                     dispatch<any>(copyToClipboardAction(selectedResources));
243                     break;
244                 default:
245                     for (const kind in kindGroups) {
246                         const action = findActionByName(selectedAction.name as string, kindToActionSet[kind]);
247                         if (action) action.execute(dispatch, kindGroups[kind]);
248                     }
249                     break;
250             }
251         },
252     };
253 }