X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/4ad6191d53207a8b2d4c0c8a30b18119daaa5fbc..5bf94d55b564bfbd052a61ab8219aa063b2a80c6:/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 3d8ae0c3d3..194950b134 100644 --- a/services/workbench2/src/components/multiselect-toolbar/MultiselectToolbar.tsx +++ b/services/workbench2/src/components/multiselect-toolbar/MultiselectToolbar.tsx @@ -10,93 +10,167 @@ import { RootState } from "store/store"; import { Dispatch } from "redux"; import { TCheckedList } from "components/data-table/data-table"; import { ContextMenuResource } from "store/context-menu/context-menu-actions"; -import { Resource, extractUuidKind } from "models/resource"; +import { Resource, ResourceKind, extractUuidKind } from "models/resource"; import { getResource } from "store/resources/resources"; import { ResourcesState } from "store/resources/resources"; -import { ContextMenuAction, ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set"; -import { RestoreFromTrashIcon, TrashIcon } from "components/icon/icon"; -import { multiselectActionsFilters, TMultiselectActionsFilters, contextMenuActionConsts } from "./ms-toolbar-action-filters"; +import { MultiSelectMenuAction, MultiSelectMenuActionSet } from "views-components/multiselect-toolbar/ms-menu-actions"; +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"; import { ContainerRequestResource } from "models/container-request"; +import { FavoritesState } from "store/favorites/favorites-reducer"; +import { resourceIsFrozen } from "common/frozen-resources"; +import { getResourceWithEditableStatus } from "store/resources/resources"; +import { GroupResource } from "models/group"; +import { EditableResource } from "models/resource"; +import { User } from "models/user"; +import { GroupClass } from "models/group"; +import { isProcessCancelable } from "store/processes/process"; +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 { IntersectionObserverWrapper } from "./ms-toolbar-overflow-wrapper"; +import { ContextMenuKind, sortMenuItems, menuDirection } from 'views-components/context-menu/menu-item-sort'; -type CssRules = "root" | "button"; +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", - overflow: "hidden", - transition: "width 150ms", + margin: "1rem auto auto 0.3rem", + overflow: 'hidden', }, button: { width: "2.5rem", height: "2.5rem ", + paddingLeft: 0, + border: "1px solid transparent", + }, + iconContainer: { + height: '100%', + }, + icon: { + marginLeft: '-0.5rem', + }, + divider: { + display: "flex", + alignItems: "center", }, }); export type MultiselectToolbarProps = { checkedList: TCheckedList; - resources: ResourcesState; + singleSelectedUuid: string | null + iconProps: IconProps + user: User | null + disabledButtons: Set executeMulti: (action: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState) => void; }; +type IconProps = { + resources: ResourcesState; + favorites: FavoritesState; + publicFavorites: PublicFavoritesState; +} + export const MultiselectToolbar = connect( mapStateToProps, mapDispatchToProps )( withStyles(styles)((props: MultiselectToolbarProps & WithStyles) => { - const { classes, checkedList } = props; - const currentResourceKinds = Array.from(selectedToKindSet(checkedList)); - + const { classes, checkedList, singleSelectedUuid, iconProps, user, disabledButtons } = props; + const singleResourceKind = singleSelectedUuid ? [resourceToMsResourceKind(singleSelectedUuid, iconProps.resources, user)] : null + const currentResourceKinds = singleResourceKind ? singleResourceKind : Array.from(selectedToKindSet(checkedList)); const currentPathIsTrash = window.location.pathname === "/trash"; - const buttons = + + const rawActions = currentPathIsTrash && selectedToKindSet(checkedList).size ? [msToggleTrashAction] - : selectActionsByKind(currentResourceKinds, multiselectActionsFilters); + : selectActionsByKind(currentResourceKinds as string[], multiselectActionsFilters).filter((action) => + singleSelectedUuid === null ? action.isForMulti : true + ); + + const actions: ContextMenuAction[] | MultiSelectMenuAction[] = sortMenuItems( + singleResourceKind && singleResourceKind.length ? (singleResourceKind[0] as ContextMenuKind) : ContextMenuKind.MULTI, + rawActions, + menuDirection.HORIZONTAL + ); return ( - {buttons.length ? ( - buttons.map((btn, i) => - btn.name === "ToggleTrashAction" ? ( + style={{ width: `${(actions.length * 2.5) + 6}rem`}} + data-cy='multiselect-toolbar' + > + {actions.length ? ( + + {actions.map((action, i) =>{ + const { hasAlts, useAlts, name, altName, icon, altIcon } = action; + return action.name === ContextMenuActionNames.DIVIDER ? ( + action.component && ( +
+ +
+ ) + ) : hasAlts ? ( - props.executeMulti(btn, checkedList, props.resources)}> - {currentPathIsTrash ? : } - + + props.executeMulti(action, checkedList, iconProps.resources)} + className={classes.icon} + > + {currentPathIsTrash || (useAlts && useAlts(singleSelectedUuid, iconProps)) ? altIcon && altIcon({}) : icon({})} + + ) : ( - props.executeMulti(btn, checkedList, props.resources)}> - {btn.icon ? btn.icon({}) : <>} - + + props.executeMulti(action, checkedList, iconProps.resources)} + className={classes.icon} + > + {action.icon({})} + + - ) - ) + ); + })} +
) : ( <> )}
- ); + ) }) ); @@ -130,14 +204,75 @@ function groupByKind(checkedList: TCheckedList, resources: ResourcesState): Reco return result; } -function filterActions(actionArray: ContextMenuActionSet, filters: Set): Array { +function filterActions(actionArray: MultiSelectMenuActionSet, filters: Set): Array { return actionArray[0].filter(action => filters.has(action.name as string)); } -function selectActionsByKind(currentResourceKinds: Array, filterSet: TMultiselectActionsFilters) { - const rawResult: Set = new Set(); +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 isEditable = (user.isAdmin || (resource || ({} as EditableResource)).isEditable) && !readonly && !isFrozen; + + switch (kind) { + case ResourceKind.PROJECT: + if (isFrozen) { + return isAdmin ? ContextMenuKind.FROZEN_PROJECT_ADMIN : ContextMenuKind.FROZEN_PROJECT; + } + + return isAdmin && !readonly + ? resource && resource.groupClass !== GroupClass.FILTER + ? ContextMenuKind.PROJECT_ADMIN + : ContextMenuKind.FILTER_GROUP_ADMIN + : isEditable + ? resource && resource.groupClass !== GroupClass.FILTER + ? ContextMenuKind.PROJECT + : ContextMenuKind.FILTER_GROUP + : ContextMenuKind.READONLY_PROJECT; + case ResourceKind.COLLECTION: + const c = getResource(uuid)(resources); + if (c === undefined) { + return; + } + const isOldVersion = c.uuid !== c.currentVersionUuid; + const isTrashed = c.isTrashed; + return isOldVersion + ? ContextMenuKind.OLD_VERSION_COLLECTION + : isTrashed && isEditable + ? ContextMenuKind.TRASHED_COLLECTION + : isAdmin && isEditable + ? ContextMenuKind.COLLECTION_ADMIN + : isEditable + ? ContextMenuKind.COLLECTION + : ContextMenuKind.READONLY_COLLECTION; + case ResourceKind.PROCESS: + return isAdmin && isEditable + ? resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process) + ? ContextMenuKind.RUNNING_PROCESS_ADMIN + : ContextMenuKind.PROCESS_ADMIN + : readonly + ? ContextMenuKind.READONLY_PROCESS_RESOURCE + : resource && isProcessCancelable(getProcess(resource.uuid)(resources) as Process) + ? ContextMenuKind.RUNNING_PROCESS_RESOURCE + : ContextMenuKind.PROCESS_RESOURCE; + case ResourceKind.USER: + return ContextMenuKind.ROOT_PROJECT; + case ResourceKind.LINK: + return ContextMenuKind.LINK; + case ResourceKind.WORKFLOW: + return isEditable ? ContextMenuKind.WORKFLOW : ContextMenuKind.READONLY_WORKFLOW; + default: + return; + } +}; + +function selectActionsByKind(currentResourceKinds: Array, filterSet: TMultiselectActionsFilters): MultiSelectMenuAction[] { + const rawResult: Set = new Set(); const resultNames = new Set(); - const allFiltersArray: ContextMenuAction[][] = []; + const allFiltersArray: MultiSelectMenuAction[][] = [] currentResourceKinds.forEach(kind => { if (filterSet[kind]) { const actions = filterActions(...filterSet[kind]); @@ -152,38 +287,36 @@ function selectActionsByKind(currentResourceKinds: Array, filterSet: TMu }); const filteredNameSet = allFiltersArray.map(filterArray => { - const resultSet = new Set(); - filterArray.forEach(action => resultSet.add(action.name || "")); + const resultSet = new Set(); + filterArray.forEach(action => resultSet.add(action.name as string || "")); return resultSet; }); const filteredResult = Array.from(rawResult).filter(action => { for (let i = 0; i < filteredNameSet.length; i++) { - if (!filteredNameSet[i].has(action.name)) return false; + if (!filteredNameSet[i].has(action.name as string)) return false; } 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(state: RootState) { +function mapStateToProps({auth, multiselect, resources, favorites, publicFavorites}: RootState) { return { - checkedList: state.multiselect.checkedList as TCheckedList, - resources: state.resources, - }; + checkedList: multiselect.checkedList as TCheckedList, + singleSelectedUuid: isExactlyOneSelected(multiselect.checkedList), + user: auth && auth.user ? auth.user : null, + disabledButtons: new Set(multiselect.disabledButtons), + iconProps: { + resources, + favorites, + publicFavorites + } + } } function mapDispatchToProps(dispatch: Dispatch) { @@ -191,13 +324,13 @@ function mapDispatchToProps(dispatch: Dispatch) { executeMulti: (selectedAction: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState): void => { const kindGroups = groupByKind(checkedList, resources); switch (selectedAction.name) { - case contextMenuActionConsts.MOVE_TO: - case contextMenuActionConsts.REMOVE: + case ContextMenuActionNames.MOVE_TO: + case ContextMenuActionNames.REMOVE: const firstResource = getResource(selectedToArray(checkedList)[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 contextMenuActionConsts.COPY_TO_CLIPBOARD: + case ContextMenuActionNames.COPY_LINK_TO_CLIPBOARD: const selectedResources = selectedToArray(checkedList).map(uuid => getResource(uuid)(resources)); dispatch(copyToClipboardAction(selectedResources)); break;