1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React, { useEffect, useState } 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 import { AuthState } from "store/auth/auth-reducer";
39 const WIDTH_TRANSITION = 150
41 type CssRules = "root" | "transition" | "button" | "iconContainer";
43 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
50 margin: "1rem auto auto 0.5rem",
51 transition: `width ${WIDTH_TRANSITION}ms`,
53 scrollBehavior: 'smooth',
54 '&::-webkit-scrollbar': {
58 '&::-webkit-scrollbar-track': {
62 '&::-webkit-scrollbar-thumb': {
63 backgroundColor: '#757575',
73 margin: "1rem auto auto 0.5rem",
75 transition: `width ${WIDTH_TRANSITION}ms`,
86 export type MultiselectToolbarProps = {
87 checkedList: TCheckedList;
88 singleSelectedUuid: string | null
89 inputSelectedUuid?: string
92 disabledButtons: Set<string>
94 executeMulti: (action: ContextMenuAction | MultiSelectMenuAction, inputSelectedUuid: string | undefined, checkedList: TCheckedList, resources: ResourcesState) => void;
98 resources: ResourcesState;
99 favorites: FavoritesState;
100 publicFavorites: PublicFavoritesState;
103 export const MultiselectToolbar = connect(
107 withStyles(styles)((props: MultiselectToolbarProps & WithStyles<CssRules>) => {
108 const { classes, checkedList, inputSelectedUuid, iconProps, user, disabledButtons } = props;
109 const singleSelectedUuid = inputSelectedUuid ?? props.singleSelectedUuid
110 const singleResourceKind = singleSelectedUuid ? [resourceToMsResourceKind(singleSelectedUuid, iconProps.resources, user)] : null
111 const currentResourceKinds = singleResourceKind ? singleResourceKind : Array.from(selectedToKindSet(checkedList));
112 const currentPathIsTrash = window.location.pathname === "/trash";
113 const [isTransitioning, setIsTransitioning] = useState(false);
114 let transitionTimeout;
116 const handleTransition = () => {
117 setIsTransitioning(true)
118 transitionTimeout = setTimeout(() => {
119 setIsTransitioning(false)
120 }, WIDTH_TRANSITION);
126 if(transitionTimeout) clearTimeout(transitionTimeout)
128 // eslint-disable-next-line
132 currentPathIsTrash && selectedToKindSet(checkedList).size
133 ? [msToggleTrashAction]
134 : selectActionsByKind(currentResourceKinds as string[], multiselectActionsFilters).filter((action) =>
135 singleSelectedUuid === null ? action.isForMulti : true
141 className={isTransitioning ? classes.transition: classes.root}
142 style={{ width: `${(actions.length * 2.5) + 1}rem` }}
143 data-cy='multiselect-toolbar'
146 actions.map((action, i) =>{
147 const { hasAlts, useAlts, name, altName, icon, altIcon } = action;
150 className={classes.button}
151 title={currentPathIsTrash || (useAlts && useAlts(singleSelectedUuid, iconProps)) ? altName : name}
155 <span className={classes.iconContainer}>
157 data-cy='multiselect-button'
158 disabled={disabledButtons.has(name)}
159 onClick={() => props.executeMulti(action, inputSelectedUuid, checkedList, iconProps.resources)}
161 {currentPathIsTrash || (useAlts && useAlts(singleSelectedUuid, iconProps)) ? altIcon && altIcon({}) : icon({})}
167 className={classes.button}
172 <span className={classes.iconContainer}>
174 data-cy='multiselect-button'
175 onClick={() => props.executeMulti(action, inputSelectedUuid, checkedList, iconProps.resources)}
192 export function selectedToArray(checkedList: TCheckedList): Array<string> {
193 const arrayifiedSelectedList: Array<string> = [];
194 for (const [key, value] of Object.entries(checkedList)) {
195 if (value === true) {
196 arrayifiedSelectedList.push(key);
199 return arrayifiedSelectedList;
202 export function selectedToKindSet(checkedList: TCheckedList): Set<string> {
203 const setifiedList = new Set<string>();
204 for (const [key, value] of Object.entries(checkedList)) {
205 if (value === true) {
206 setifiedList.add(extractUuidKind(key) as string);
212 function groupByKind(checkedList: TCheckedList, resources: ResourcesState): Record<string, ContextMenuResource[]> {
214 selectedToArray(checkedList).forEach(uuid => {
215 const resource = getResource(uuid)(resources) as ContainerRequestResource | Resource;
216 if (!result[resource.kind]) result[resource.kind] = [];
217 result[resource.kind].push(resource);
222 function filterActions(actionArray: MultiSelectMenuActionSet, filters: Set<string>): Array<MultiSelectMenuAction> {
223 return actionArray[0].filter(action => filters.has(action.name as string));
226 const resourceToMsResourceKind = (uuid: string, resources: ResourcesState, user: User | null, readonly = false): (msMenuResourceKind | ResourceKind) | undefined => {
228 const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, user.uuid)(resources);
229 const { isAdmin } = user;
230 const kind = extractUuidKind(uuid);
232 const isFrozen = resourceIsFrozen(resource, resources);
233 const isEditable = (user.isAdmin || (resource || ({} as EditableResource)).isEditable) && !readonly && !isFrozen;
236 case ResourceKind.PROJECT:
238 return isAdmin ? msMenuResourceKind.FROZEN_PROJECT_ADMIN : msMenuResourceKind.FROZEN_PROJECT;
241 return isAdmin && !readonly
242 ? resource && resource.groupClass !== GroupClass.FILTER
243 ? msMenuResourceKind.PROJECT_ADMIN
244 : msMenuResourceKind.FILTER_GROUP_ADMIN
246 ? resource && resource.groupClass !== GroupClass.FILTER
247 ? msMenuResourceKind.PROJECT
248 : msMenuResourceKind.FILTER_GROUP
249 : msMenuResourceKind.READONLY_PROJECT;
250 case ResourceKind.COLLECTION:
251 const c = getResource<CollectionResource>(uuid)(resources);
252 if (c === undefined) {
255 const isOldVersion = c.uuid !== c.currentVersionUuid;
256 const isTrashed = c.isTrashed;
258 ? msMenuResourceKind.OLD_VERSION_COLLECTION
259 : isTrashed && isEditable
260 ? msMenuResourceKind.TRASHED_COLLECTION
261 : isAdmin && isEditable
262 ? msMenuResourceKind.COLLECTION_ADMIN
264 ? msMenuResourceKind.COLLECTION
265 : msMenuResourceKind.READONLY_COLLECTION;
266 case ResourceKind.PROCESS:
267 return isAdmin && isEditable
268 ? resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process)
269 ? msMenuResourceKind.RUNNING_PROCESS_ADMIN
270 : msMenuResourceKind.PROCESS_ADMIN
272 ? msMenuResourceKind.READONLY_PROCESS_RESOURCE
273 : resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process)
274 ? msMenuResourceKind.RUNNING_PROCESS_RESOURCE
275 : msMenuResourceKind.PROCESS_RESOURCE;
276 case ResourceKind.USER:
277 return isAdmin ? msMenuResourceKind.ROOT_PROJECT_ADMIN : msMenuResourceKind.ROOT_PROJECT;
278 case ResourceKind.LINK:
279 return msMenuResourceKind.LINK;
280 case ResourceKind.WORKFLOW:
281 return isEditable ? msMenuResourceKind.WORKFLOW : msMenuResourceKind.READONLY_WORKFLOW;
287 function selectActionsByKind(currentResourceKinds: Array<string>, filterSet: TMultiselectActionsFilters) {
288 const rawResult: Set<MultiSelectMenuAction> = new Set();
289 const resultNames = new Set();
290 const allFiltersArray: MultiSelectMenuAction[][] = []
291 currentResourceKinds.forEach(kind => {
292 if (filterSet[kind]) {
293 const actions = filterActions(...filterSet[kind]);
294 allFiltersArray.push(actions);
295 actions.forEach(action => {
296 if (!resultNames.has(action.name)) {
297 rawResult.add(action);
298 resultNames.add(action.name);
304 const filteredNameSet = allFiltersArray.map(filterArray => {
305 const resultSet = new Set<string>();
306 filterArray.forEach(action => resultSet.add(action.name as string || ""));
310 const filteredResult = Array.from(rawResult).filter(action => {
311 for (let i = 0; i < filteredNameSet.length; i++) {
312 if (!filteredNameSet[i].has(action.name as string)) return false;
317 return filteredResult.sort((a, b) => {
318 const nameA = a.name || "";
319 const nameB = b.name || "";
331 //--------------------------------------------------//
333 function mapStateToProps({auth, multiselect, resources, favorites, publicFavorites}: RootState) {
335 checkedList: multiselect.checkedList as TCheckedList,
336 singleSelectedUuid: isExactlyOneSelected(multiselect.checkedList),
337 user: auth && auth.user ? auth.user : null,
338 disabledButtons: new Set<string>(multiselect.disabledButtons),
348 function mapDispatchToProps(dispatch: Dispatch) {
350 executeMulti: (selectedAction: ContextMenuAction, inputSelectedUuid: string | undefined, checkedList: TCheckedList, resources: ResourcesState): void => {
351 const kindGroups = inputSelectedUuid ? groupByKind({[inputSelectedUuid]: true}, resources) : groupByKind(checkedList, resources);
352 const currentList = inputSelectedUuid ? [inputSelectedUuid] : selectedToArray(checkedList)
353 switch (selectedAction.name) {
354 case MultiSelectMenuActionNames.MOVE_TO:
355 case MultiSelectMenuActionNames.REMOVE:
356 const firstResource = getResource(currentList[0])(resources) as ContainerRequestResource | Resource;
357 const action = findActionByName(selectedAction.name as string, kindToActionSet[firstResource.kind]);
358 if (action) action.execute(dispatch, kindGroups[firstResource.kind]);
360 case MultiSelectMenuActionNames.COPY_TO_CLIPBOARD:
361 const selectedResources = currentList.map(uuid => getResource(uuid)(resources));
362 dispatch<any>(copyToClipboardAction(selectedResources));
365 for (const kind in kindGroups) {
366 const action = findActionByName(selectedAction.name as string, kindToActionSet[kind]);
367 if (action) action.execute(dispatch, kindGroups[kind]);