X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/6182baa054abdd54389c91aca9ac710bd3869f08..28aa75d2683d5f0472335ecef3b2ff505e72f30d:/services/workbench2/src/components/multiselect-toolbar/MultiselectToolbar.tsx diff --git a/services/workbench2/src/components/multiselect-toolbar/MultiselectToolbar.tsx b/services/workbench2/src/components/multiselect-toolbar/MultiselectToolbar.tsx index f92c0dcf4e..75a6e6e2c5 100644 --- a/services/workbench2/src/components/multiselect-toolbar/MultiselectToolbar.tsx +++ b/services/workbench2/src/components/multiselect-toolbar/MultiselectToolbar.tsx @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0 -import React, { useEffect, useState } from "react"; +import React from "react"; import { connect } from "react-redux"; import { StyleRulesCallback, withStyles, WithStyles, Toolbar, Tooltip, IconButton } from "@material-ui/core"; import { ArvadosTheme } from "common/custom-theme"; @@ -14,9 +14,8 @@ import { Resource, ResourceKind, extractUuidKind } from "models/resource"; import { getResource } from "store/resources/resources"; import { ResourcesState } from "store/resources/resources"; import { MultiSelectMenuAction, MultiSelectMenuActionSet } from "views-components/multiselect-toolbar/ms-menu-actions"; -import { MultiSelectMenuActionNames } from "views-components/multiselect-toolbar/ms-menu-actions"; -import { ContextMenuAction } from "views-components/context-menu/context-menu-action-set"; -import { multiselectActionsFilters, TMultiselectActionsFilters, msMenuResourceKind } from "./ms-toolbar-action-filters"; +import { ContextMenuAction, ContextMenuActionNames } from "views-components/context-menu/context-menu-action-set"; +import { multiselectActionsFilters, TMultiselectActionsFilters } from "./ms-toolbar-action-filters"; import { kindToActionSet, findActionByName } from "./ms-kind-action-differentiator"; import { msToggleTrashAction } from "views-components/multiselect-toolbar/ms-project-action-set"; import { copyToClipboardAction } from "store/open-in-new-tab/open-in-new-tab.actions"; @@ -33,62 +32,52 @@ import { CollectionResource } from "models/collection"; import { getProcess } from "store/processes/process"; import { Process } from "store/processes/process"; import { PublicFavoritesState } from "store/public-favorites/public-favorites-reducer"; -import { isExactlyOneSelected } from "store/multiselect/multiselect-actions"; +import { AuthState } from "store/auth/auth-reducer"; +import { IntersectionObserverWrapper } from "./ms-toolbar-overflow-wrapper"; +import classNames from "classnames"; +import { ContextMenuKind, sortMenuItems, menuDirection } from 'views-components/context-menu/menu-item-sort'; -const WIDTH_TRANSITION = 150 - -type CssRules = "root" | "transition" | "button" | "iconContainer"; +type CssRules = "root" | "button" | "iconContainer" | "icon" | "divider"; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ root: { display: "flex", flexDirection: "row", width: 0, - height: '2.7rem', - padding: 0, - margin: "1rem auto auto 0.5rem", - transition: `width ${WIDTH_TRANSITION}ms`, - overflowY: 'auto', - scrollBehavior: 'smooth', - '&::-webkit-scrollbar': { - width: 0, - height: 2 - }, - '&::-webkit-scrollbar-track': { - width: 0, - height: 2 - }, - '&::-webkit-scrollbar-thumb': { - backgroundColor: '#757575', - borderRadius: 2 - } - }, - transition: { - display: "flex", - flexDirection: "row", - width: 0, - height: '2.7rem', + height: '2.5rem', padding: 0, - margin: "1rem auto auto 0.5rem", + margin: 0, overflow: 'hidden', - transition: `width ${WIDTH_TRANSITION}ms`, }, button: { width: "2.5rem", height: "2.5rem ", + paddingLeft: 0, + border: "1px solid transparent", }, iconContainer: { - height: '100%' - } + height: '100%', + }, + icon: { + marginLeft: '-0.5rem', + }, + divider: { + display: "flex", + alignItems: "center", + }, }); export type MultiselectToolbarProps = { checkedList: TCheckedList; - singleSelectedUuid: string | null + selectedResourceUuid: string | null; iconProps: IconProps user: User | null disabledButtons: Set - executeMulti: (action: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState) => void; + auth: AuthState; + location: string; + isSubPanel?: boolean; + injectedStyles?: string; + executeMulti: (action: ContextMenuAction | MultiSelectMenuAction, checkedList: TCheckedList, resources: ResourcesState) => void; }; type IconProps = { @@ -97,80 +86,105 @@ type IconProps = { publicFavorites: PublicFavoritesState; } +const disallowedPaths = [ + "/favorites", + "/public-favorites", + "/trash", + "/group", +] + +const isPathDisallowed = (location: string): boolean => { + return disallowedPaths.some(path => location.includes(path)) +} + export const MultiselectToolbar = connect( mapStateToProps, mapDispatchToProps )( withStyles(styles)((props: MultiselectToolbarProps & WithStyles) => { - const { classes, checkedList, singleSelectedUuid, iconProps, user, disabledButtons } = props; - const singleResourceKind = singleSelectedUuid ? [resourceToMsResourceKind(singleSelectedUuid, iconProps.resources, user)] : null + const { classes, checkedList, iconProps, user, disabledButtons, location, isSubPanel, injectedStyles } = props; + const selectedResourceUuid = isPathDisallowed(location) ? null : props.selectedResourceUuid; + const singleResourceKind = selectedResourceUuid && !isSubPanel ? [resourceToMsResourceKind(selectedResourceUuid, iconProps.resources, user)] : null const currentResourceKinds = singleResourceKind ? singleResourceKind : Array.from(selectedToKindSet(checkedList)); const currentPathIsTrash = window.location.pathname === "/trash"; - const [isTransitioning, setIsTransitioning] = useState(false); - - const handleTransition = () => { - setIsTransitioning(true) - setTimeout(() => { - setIsTransitioning(false) - }, WIDTH_TRANSITION); - } - - useEffect(()=>{ - handleTransition() - }, [checkedList]) - const actions = + const rawActions = currentPathIsTrash && selectedToKindSet(checkedList).size ? [msToggleTrashAction] : selectActionsByKind(currentResourceKinds as string[], multiselectActionsFilters).filter((action) => - singleSelectedUuid === null ? action.isForMulti : true + selectedResourceUuid === null ? action.isForMulti : true ); + + const actions: ContextMenuAction[] | MultiSelectMenuAction[] = sortMenuItems( + singleResourceKind && singleResourceKind.length ? (singleResourceKind[0] as ContextMenuKind) : ContextMenuKind.MULTI, + rawActions, + menuDirection.HORIZONTAL + ); + + const targetResources = selectedResourceUuid ? {[selectedResourceUuid]: true} as TCheckedList : checkedList return ( {actions.length ? ( - actions.map((action, i) =>{ - const { hasAlts, useAlts, name, altName, icon, altIcon } = action; - return hasAlts ? ( - - - props.executeMulti(action, checkedList, iconProps.resources)} - > - {currentPathIsTrash || (useAlts && useAlts(singleSelectedUuid, iconProps)) ? altIcon && altIcon({}) : icon({})} - - - - ) : ( - - - props.executeMulti(action, checkedList, iconProps.resources)} + + {actions.map((action, i) =>{ + const { hasAlts, useAlts, name, altName, icon, altIcon } = action; + return action.name === ContextMenuActionNames.DIVIDER ? ( + action.component && ( +
- {action.icon({})} - - - - ); - }) + +
+ ) + ) : hasAlts ? ( + + + props.executeMulti(action, targetResources, iconProps.resources)} + className={classes.icon} + > + {currentPathIsTrash || (useAlts && useAlts(selectedResourceUuid, iconProps)) ? altIcon && altIcon({}) : icon({})} + + + + ) : ( + + + { + props.executeMulti(action, targetResources, iconProps.resources)}} + className={classes.icon} + > + {action.icon({})} + + + + ); + })} +
) : ( <> )} @@ -214,30 +228,30 @@ function filterActions(actionArray: MultiSelectMenuActionSet, filters: Set filters.has(action.name as string)); } -const resourceToMsResourceKind = (uuid: string, resources: ResourcesState, user: User | null, readonly = false): (msMenuResourceKind | ResourceKind) | undefined => { +const resourceToMsResourceKind = (uuid: string, resources: ResourcesState, user: User | null, readonly = false): (ContextMenuKind | ResourceKind) | undefined => { if (!user) return; const resource = getResourceWithEditableStatus(uuid, user.uuid)(resources); const { isAdmin } = user; const kind = extractUuidKind(uuid); - const isFrozen = resourceIsFrozen(resource, resources); + const isFrozen = resource?.kind && resource.kind === ResourceKind.PROJECT ? resourceIsFrozen(resource, resources) : false; const isEditable = (user.isAdmin || (resource || ({} as EditableResource)).isEditable) && !readonly && !isFrozen; switch (kind) { case ResourceKind.PROJECT: if (isFrozen) { - return isAdmin ? msMenuResourceKind.FROZEN_PROJECT_ADMIN : msMenuResourceKind.FROZEN_PROJECT; + return isAdmin ? ContextMenuKind.FROZEN_PROJECT_ADMIN : ContextMenuKind.FROZEN_PROJECT; } return isAdmin && !readonly ? resource && resource.groupClass !== GroupClass.FILTER - ? msMenuResourceKind.PROJECT_ADMIN - : msMenuResourceKind.FILTER_GROUP_ADMIN + ? ContextMenuKind.PROJECT_ADMIN + : ContextMenuKind.FILTER_GROUP_ADMIN : isEditable ? resource && resource.groupClass !== GroupClass.FILTER - ? msMenuResourceKind.PROJECT - : msMenuResourceKind.FILTER_GROUP - : msMenuResourceKind.READONLY_PROJECT; + ? ContextMenuKind.PROJECT + : ContextMenuKind.FILTER_GROUP + : ContextMenuKind.READONLY_PROJECT; case ResourceKind.COLLECTION: const c = getResource(uuid)(resources); if (c === undefined) { @@ -246,36 +260,36 @@ const resourceToMsResourceKind = (uuid: string, resources: ResourcesState, user: const isOldVersion = c.uuid !== c.currentVersionUuid; const isTrashed = c.isTrashed; return isOldVersion - ? msMenuResourceKind.OLD_VERSION_COLLECTION + ? ContextMenuKind.OLD_VERSION_COLLECTION : isTrashed && isEditable - ? msMenuResourceKind.TRASHED_COLLECTION + ? ContextMenuKind.TRASHED_COLLECTION : isAdmin && isEditable - ? msMenuResourceKind.COLLECTION_ADMIN + ? ContextMenuKind.COLLECTION_ADMIN : isEditable - ? msMenuResourceKind.COLLECTION - : msMenuResourceKind.READONLY_COLLECTION; + ? ContextMenuKind.COLLECTION + : ContextMenuKind.READONLY_COLLECTION; case ResourceKind.PROCESS: return isAdmin && isEditable ? resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process) - ? msMenuResourceKind.RUNNING_PROCESS_ADMIN - : msMenuResourceKind.PROCESS_ADMIN + ? ContextMenuKind.RUNNING_PROCESS_ADMIN + : ContextMenuKind.PROCESS_ADMIN : readonly - ? msMenuResourceKind.READONLY_PROCESS_RESOURCE + ? ContextMenuKind.READONLY_PROCESS_RESOURCE : resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process) - ? msMenuResourceKind.RUNNING_PROCESS_RESOURCE - : msMenuResourceKind.PROCESS_RESOURCE; + ? ContextMenuKind.RUNNING_PROCESS_RESOURCE + : ContextMenuKind.PROCESS_RESOURCE; case ResourceKind.USER: - return msMenuResourceKind.ROOT_PROJECT; + return isAdmin ? ContextMenuKind.ROOT_PROJECT_ADMIN : ContextMenuKind.ROOT_PROJECT; case ResourceKind.LINK: - return msMenuResourceKind.LINK; + return ContextMenuKind.LINK; case ResourceKind.WORKFLOW: - return isEditable ? msMenuResourceKind.WORKFLOW : msMenuResourceKind.READONLY_WORKFLOW; + return isEditable ? ContextMenuKind.WORKFLOW : ContextMenuKind.READONLY_WORKFLOW; default: return; } }; -function selectActionsByKind(currentResourceKinds: Array, filterSet: TMultiselectActionsFilters) { +function selectActionsByKind(currentResourceKinds: Array, filterSet: TMultiselectActionsFilters): MultiSelectMenuAction[] { const rawResult: Set = new Set(); const resultNames = new Set(); const allFiltersArray: MultiSelectMenuAction[][] = [] @@ -305,28 +319,20 @@ function selectActionsByKind(currentResourceKinds: Array, filterSet: TMu return true; }); - return filteredResult.sort((a, b) => { - const nameA = a.name || ""; - const nameB = b.name || ""; - if (nameA < nameB) { - return -1; - } - if (nameA > nameB) { - return 1; - } - return 0; - }); + return filteredResult; } //--------------------------------------------------// -function mapStateToProps({auth, multiselect, resources, favorites, publicFavorites}: RootState) { +function mapStateToProps({auth, multiselect, resources, favorites, publicFavorites, selectedResourceUuid}: RootState) { return { checkedList: multiselect.checkedList as TCheckedList, - singleSelectedUuid: isExactlyOneSelected(multiselect.checkedList), user: auth && auth.user ? auth.user : null, disabledButtons: new Set(multiselect.disabledButtons), + auth, + selectedResourceUuid, + location: window.location.pathname, iconProps: { resources, favorites, @@ -339,15 +345,16 @@ function mapDispatchToProps(dispatch: Dispatch) { return { executeMulti: (selectedAction: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState): void => { const kindGroups = groupByKind(checkedList, resources); + const currentList = selectedToArray(checkedList) switch (selectedAction.name) { - case MultiSelectMenuActionNames.MOVE_TO: - case MultiSelectMenuActionNames.REMOVE: - const firstResource = getResource(selectedToArray(checkedList)[0])(resources) as ContainerRequestResource | Resource; + case ContextMenuActionNames.MOVE_TO: + case ContextMenuActionNames.REMOVE: + const firstResource = getResource(currentList[0])(resources) as ContainerRequestResource | Resource; const action = findActionByName(selectedAction.name as string, kindToActionSet[firstResource.kind]); if (action) action.execute(dispatch, kindGroups[firstResource.kind]); break; - case MultiSelectMenuActionNames.COPY_TO_CLIPBOARD: - const selectedResources = selectedToArray(checkedList).map(uuid => getResource(uuid)(resources)); + case ContextMenuActionNames.COPY_LINK_TO_CLIPBOARD: + const selectedResources = currentList.map(uuid => getResource(uuid)(resources)); dispatch(copyToClipboardAction(selectedResources)); break; default: