15768: initial Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox@curii.com>
[arvados.git] / src / components / multiselectToolbar / 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, extractUuidKind } from 'models/resource';
14 import { getResource } from 'store/resources/resources';
15 import { ResourcesState } from 'store/resources/resources';
16 import { ContextMenuAction, ContextMenuActionSet } from 'views-components/context-menu/context-menu-action-set';
17 import { RestoreFromTrashIcon, TrashIcon } from 'components/icon/icon';
18 import { multiselectActionsFilters, TMultiselectActionsFilters } from './ms-toolbar-action-filters';
19 import { kindToActionSet, findActionByName } from './ms-kind-action-differentiator';
20 import { toggleTrashAction } from 'views-components/context-menu/action-sets/project-action-set';
21
22 type CssRules = 'root' | 'button';
23
24 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
25     root: {
26         display: 'flex',
27         flexDirection: 'row',
28         width: 0,
29         padding: 0,
30         margin: '1rem auto auto 0.5rem',
31         overflow: 'hidden',
32         transition: 'width 150ms',
33         // borderBottom: '1px solid gray',
34     },
35     button: {
36         width: '1rem',
37         margin: 'auto 5px',
38     },
39 });
40
41 export type MultiselectToolbarProps = {
42     isVisible: boolean;
43     checkedList: TCheckedList;
44     resources: ResourcesState;
45     executeMulti: (action: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState) => void;
46 };
47
48 export const MultiselectToolbar = connect(
49     mapStateToProps,
50     mapDispatchToProps
51 )(
52     withStyles(styles)((props: MultiselectToolbarProps & WithStyles<CssRules>) => {
53         const { classes, checkedList } = props;
54         const currentResourceKinds = Array.from(selectedToKindSet(checkedList));
55
56         const currentPathIsTrash = window.location.pathname === '/trash';
57         const buttons =
58             currentPathIsTrash && selectedToKindSet(checkedList).size
59                 ? [toggleTrashAction]
60                 : selectActionsByKind(currentResourceKinds, multiselectActionsFilters);
61
62         return (
63             <Toolbar className={classes.root} style={{ width: `${buttons.length * 2.12}rem` }}>
64                 {buttons.length ? (
65                     buttons.map((btn, i) =>
66                         btn.name === 'ToggleTrashAction' ? (
67                             <Tooltip className={classes.button} title={currentPathIsTrash ? 'Restore' : 'Move to trash'} key={i} disableFocusListener>
68                                 <IconButton onClick={() => props.executeMulti(btn, checkedList, props.resources)}>
69                                     {currentPathIsTrash ? <RestoreFromTrashIcon /> : <TrashIcon />}
70                                 </IconButton>
71                             </Tooltip>
72                         ) : (
73                             <Tooltip className={classes.button} title={btn.name} key={i} disableFocusListener>
74                                 <IconButton onClick={() => props.executeMulti(btn, checkedList, props.resources)}>
75                                     {btn.icon ? btn.icon({}) : <></>}
76                                 </IconButton>
77                             </Tooltip>
78                         )
79                     )
80                 ) : (
81                     <></>
82                 )}
83             </Toolbar>
84         );
85     })
86 );
87
88 function selectedToArray(checkedList: TCheckedList): Array<string> {
89     const arrayifiedSelectedList: Array<string> = [];
90     for (const [key, value] of Object.entries(checkedList)) {
91         if (value === true) {
92             arrayifiedSelectedList.push(key);
93         }
94     }
95     return arrayifiedSelectedList;
96 }
97
98 function selectedToKindSet(checkedList: TCheckedList): Set<string> {
99     const setifiedList = new Set<string>();
100     for (const [key, value] of Object.entries(checkedList)) {
101         if (value === true) {
102             setifiedList.add(extractUuidKind(key) as string);
103         }
104     }
105     return setifiedList;
106 }
107
108 function filterActions(actionArray: ContextMenuActionSet, filters: Set<string>): Array<ContextMenuAction> {
109     return actionArray[0].filter((action) => filters.has(action.name as string));
110 }
111
112 function selectActionsByKind(currentResourceKinds: Array<string>, filterSet: TMultiselectActionsFilters) {
113     const rawResult: Set<ContextMenuAction> = new Set();
114     const resultNames = new Set();
115     const allFiltersArray: ContextMenuAction[][] = [];
116     currentResourceKinds.forEach((kind) => {
117         if (filterSet[kind]) {
118             const actions = filterActions(...filterSet[kind]);
119             allFiltersArray.push(actions);
120             actions.forEach((action) => {
121                 if (!resultNames.has(action.name)) {
122                     rawResult.add(action);
123                     resultNames.add(action.name);
124                 }
125             });
126         }
127     });
128
129     const filteredNameSet = allFiltersArray.map((filterArray) => {
130         const resultSet = new Set();
131         filterArray.forEach((action) => resultSet.add(action.name || ''));
132         return resultSet;
133     });
134
135     const filteredResult = Array.from(rawResult).filter((action) => {
136         for (let i = 0; i < filteredNameSet.length; i++) {
137             if (!filteredNameSet[i].has(action.name)) return false;
138         }
139         return true;
140     });
141
142     return filteredResult.sort((a, b) => {
143         const nameA = a.name || '';
144         const nameB = b.name || '';
145         if (nameA < nameB) {
146             return -1;
147         }
148         if (nameA > nameB) {
149             return 1;
150         }
151         return 0;
152     });
153 }
154
155 //--------------------------------------------------//
156
157 function mapStateToProps(state: RootState) {
158     const { isVisible, checkedList } = state.multiselect;
159     return {
160         isVisible: isVisible,
161         checkedList: checkedList as TCheckedList,
162         resources: state.resources,
163     };
164 }
165
166 function mapDispatchToProps(dispatch: Dispatch) {
167     return {
168         executeMulti: (selectedAction: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState): void => {
169             const kindGroups = groupByKind(checkedList, resources);
170             for (const kind in kindGroups) {
171                 const actionSet = kindToActionSet[kind];
172                 const action = findActionByName(selectedAction.name as string, actionSet);
173
174                 if (action) action.execute(dispatch, kindGroups[kind]);
175                 // if (action && action.name === 'ToggleTrashAction') action.execute(dispatch, kindGroups[kind]);
176             }
177         },
178     };
179 }
180
181 function groupByKind(checkedList: TCheckedList, resources: ResourcesState): Record<string, ContextMenuResource[]> {
182     const result = {};
183     selectedToArray(checkedList).forEach((uuid) => {
184         const resource = getResource(uuid)(resources) as Resource;
185         if (!result[resource.kind]) result[resource.kind] = [];
186         result[resource.kind].push(resource);
187     });
188     return result;
189 }