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