diff --git a/CHANGELOG-dataset-detail-workspace-updates.md b/CHANGELOG-dataset-detail-workspace-updates.md new file mode 100644 index 0000000000..72b079ef21 --- /dev/null +++ b/CHANGELOG-dataset-detail-workspace-updates.md @@ -0,0 +1 @@ +- Update menu choices for the workspace buttons in unified views dataset detail pages. \ No newline at end of file diff --git a/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/HelperPanel.tsx b/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/HelperPanel.tsx index b32f154d8b..c5167951d3 100644 --- a/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/HelperPanel.tsx +++ b/context/app/static/js/components/detailPage/ProcessedData/HelperPanel/HelperPanel.tsx @@ -1,34 +1,26 @@ import React, { PropsWithChildren } from 'react'; +import { animated } from '@react-spring/web'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; -import { formatDate } from 'date-fns/format'; import Divider from '@mui/material/Divider'; -import { useIsDesktop } from 'js/hooks/media-queries'; +import Fade from '@mui/material/Fade'; import SchemaRounded from '@mui/icons-material/SchemaRounded'; -import { WorkspacesIcon } from 'js/shared-styles/icons'; import CloudDownloadRounded from '@mui/icons-material/CloudDownloadRounded'; -import { useAppContext } from 'js/components/Contexts'; + +import { useIsDesktop } from 'js/hooks/media-queries'; +import { WorkspacesIcon } from 'js/shared-styles/icons'; import { useAnimatedSidebarPosition } from 'js/shared-styles/sections/TableOfContents/hooks'; -import { animated } from '@react-spring/web'; -import { useEventCallback } from '@mui/material/utils'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; -import AddRounded from '@mui/icons-material/AddRounded'; -import NewWorkspaceDialog from 'js/components/workspaces/NewWorkspaceDialog'; -import { useCreateWorkspaceForm } from 'js/components/workspaces/NewWorkspaceDialog/useCreateWorkspaceForm'; -import { useOpenDialog } from 'js/components/workspaces/WorkspacesDropdownMenu/WorkspacesDropdownMenu'; -import SelectableTableProvider from 'js/shared-styles/tables/SelectableTableProvider/SelectableTableProvider'; -import AddDatasetsFromSearchDialog from 'js/components/workspaces/AddDatasetsFromSearchDialog'; import { LineClamp } from 'js/shared-styles/text'; -import Fade from '@mui/material/Fade'; import { SecondaryBackgroundTooltip } from 'js/shared-styles/tooltips'; + +import { formatDate } from 'date-fns/format'; import { HelperPanelPortal } from '../../DetailLayout/DetailLayout'; import useProcessedDataStore from '../store'; import StatusIcon from '../../StatusIcon'; import { getDateLabelAndValue } from '../../utils'; import { HelperPanelButton } from './styles'; import { useTrackEntityPageEvent } from '../../useTrackEntityPageEvent'; +import ProcessedDataWorkspaceMenu from '../ProcessedDataWorkspaceMenu'; function useCurrentDataset() { return useProcessedDataStore((state) => state.currentDataset); @@ -100,108 +92,25 @@ function HelperPanelBody() { ); } -function WorkspaceButton() { - const currentDataset = useCurrentDataset(); - const { isWorkspacesUser } = useAppContext(); - const [anchorEl, setAnchorEl] = React.useState(null); - const open = Boolean(anchorEl); - const track = useTrackEntityPageEvent(); - - const handleClick = (event: React.MouseEvent) => { - track({ - action: 'Open Workspace Menu', - label: currentDataset?.hubmap_id, - }); - setAnchorEl(event.currentTarget); - }; - const handleClose = () => { - setAnchorEl(null); - }; - - const { - control, - errors, - removeDatasets, - setDialogIsOpen: setOpenCreateWorkspace, - dialogIsOpen: createWorkspaceIsOpen, - ...rest - } = useCreateWorkspaceForm({ - defaultName: currentDataset?.hubmap_id, - initialSelectedDatasets: currentDataset ? [currentDataset.uuid] : [], - }); - - const openEditWorkspaceDialog = useOpenDialog('ADD_DATASETS_FROM_SEARCH'); - - const trackCreateWorkspace = useEventCallback(() => { - track({ - action: 'Start Creating Workspace', - label: currentDataset?.hubmap_id, - }); - setOpenCreateWorkspace(true); - handleClose(); - }); - - const trackAddToWorkspace = useEventCallback(() => { - track({ - action: 'Start Adding Dataset to Existing Workspace', - label: currentDataset?.hubmap_id, - }); - openEditWorkspaceDialog(); - handleClose(); - }); - - if (!isWorkspacesUser || currentDataset?.status !== 'Published') { - return null; - } - // The selectable table provider is used here since a lot of the workspace logic relies on the selected rows - return ( - - } - onClick={handleClick} - aria-controls={open ? 'basic-menu' : undefined} - aria-haspopup="true" - aria-expanded={open ? 'true' : undefined} - > - Workspace - - - - - - - Launch New Workspace - - - - - - Add to Workspace - - - - - - ); -} - function HelperPanelActions() { const currentDataset = useCurrentDataset(); const track = useTrackEntityPageEvent(); if (!currentDataset) { return null; } + + const { hubmap_id, uuid, status } = currentDataset; + return ( <> - + + }>Workspace + + } + datasetDetails={{ hubmap_id, uuid, status }} + /> } diff --git a/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataWorkspaceMenu.tsx b/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataWorkspaceMenu.tsx new file mode 100644 index 0000000000..e0ace6f404 --- /dev/null +++ b/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataWorkspaceMenu.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { useEventCallback } from '@mui/material/utils'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import AddRounded from '@mui/icons-material/AddRounded'; + +import SelectableTableProvider from 'js/shared-styles/tables/SelectableTableProvider'; +import { WorkspacesIcon } from 'js/shared-styles/icons'; +import { useOpenDialog } from 'js/components/workspaces/WorkspacesDropdownMenu/WorkspacesDropdownMenu'; +import { useCreateWorkspaceForm } from 'js/components/workspaces/NewWorkspaceDialog/useCreateWorkspaceForm'; +import { useAppContext, useFlaskDataContext } from 'js/components/Contexts'; +import { useTrackEntityPageEvent } from 'js/components/detailPage/useTrackEntityPageEvent'; +import NewWorkspaceDialog from 'js/components/workspaces/NewWorkspaceDialog/NewWorkspaceDialog'; +import AddDatasetsFromSearchDialog from 'js/components/workspaces/AddDatasetsFromSearchDialog/AddDatasetsFromSearchDialog'; + +interface ProcessedDataWorkspaceMenuProps { + button: React.ReactNode; + datasetDetails: { hubmap_id: string; uuid: string; status: string }; +} + +function ProcessedDataWorkspaceMenu({ + button, + datasetDetails: { hubmap_id, uuid, status }, +}: ProcessedDataWorkspaceMenuProps) { + const { + entity: { mapped_data_access_level }, + } = useFlaskDataContext(); + + const { isWorkspacesUser } = useAppContext(); + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const track = useTrackEntityPageEvent(); + + const handleOpen = (event: React.MouseEvent) => { + track({ + action: 'Open Workspace Menu', + label: hubmap_id, + }); + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const { + control, + errors, + removeDatasets, + setDialogIsOpen: setOpenCreateWorkspace, + dialogIsOpen: createWorkspaceIsOpen, + ...rest + } = useCreateWorkspaceForm({ + defaultName: hubmap_id, + initialSelectedDatasets: [uuid].filter(Boolean), + }); + + const openEditWorkspaceDialog = useOpenDialog('ADD_DATASETS_FROM_SEARCH'); + + const createWorkspace = useEventCallback(() => { + track({ + action: 'Start Creating Workspace', + label: hubmap_id, + }); + setOpenCreateWorkspace(true); + handleClose(); + }); + + const addToWorkspace = useEventCallback(() => { + track({ + action: 'Start Adding Dataset to Existing Workspace', + label: hubmap_id, + }); + openEditWorkspaceDialog(); + handleClose(); + }); + + // Clone the button element and add the onClick handler + const buttonWithClickHandler = React.cloneElement(button as React.ReactElement, { + onClick: handleOpen, + 'aria-controls': open ? 'basic-menu' : undefined, + 'aria-haspopup': 'true', + 'aria-expanded': open ? 'true' : undefined, + }); + + const showWorkspaceButton = mapped_data_access_level && hubmap_id && isWorkspacesUser && status === 'Published'; + + if (!showWorkspaceButton) { + return null; + } + + const options = [ + { + children: 'Launch New Workspace', + onClick: createWorkspace, + icon: , + }, + { + children: 'Add to Workspace', + onClick: addToWorkspace, + icon: , + }, + ]; + + // The selectable table provider is used here since a lot of the workspace logic relies on the selected rows + return ( + + {buttonWithClickHandler} + <> + + {options.map(({ children, onClick, icon }) => ( + + {icon} + {children} + + ))} + + + + + + ); +} + +export default ProcessedDataWorkspaceMenu; diff --git a/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/ProcessedDataset.tsx b/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/ProcessedDataset.tsx index 6a2fa5015d..368d7d1618 100644 --- a/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/ProcessedDataset.tsx +++ b/context/app/static/js/components/detailPage/ProcessedData/ProcessedDataset/ProcessedDataset.tsx @@ -138,7 +138,13 @@ function VisualizationAccordion() { This visualization includes various interactive elements such as scatter plots, spatial imaging plots, heat maps, genome browser tracks, and more. - + ); } diff --git a/context/app/static/js/components/detailPage/entityHeader/EntityHeaderActionButtons/EntityHeaderActionButtons.tsx b/context/app/static/js/components/detailPage/entityHeader/EntityHeaderActionButtons/EntityHeaderActionButtons.tsx index 1fe2e28731..dae7879851 100644 --- a/context/app/static/js/components/detailPage/entityHeader/EntityHeaderActionButtons/EntityHeaderActionButtons.tsx +++ b/context/app/static/js/components/detailPage/entityHeader/EntityHeaderActionButtons/EntityHeaderActionButtons.tsx @@ -13,12 +13,11 @@ import EditSavedStatusDialog from 'js/components/savedLists/EditSavedStatusDialo import useSavedEntitiesStore, { SavedEntitiesStore } from 'js/stores/useSavedEntitiesStore'; import { Entity } from 'js/components/types'; import { AllEntityTypes } from 'js/shared-styles/icons/entityIconMap'; -import NewWorkspaceDialog from 'js/components/workspaces/NewWorkspaceDialog'; -import { useCreateWorkspaceForm } from 'js/components/workspaces/NewWorkspaceDialog/useCreateWorkspaceForm'; -import { useAppContext, useFlaskDataContext } from 'js/components/Contexts'; -import WorkspacesIcon from 'assets/svg/workspaces.svg'; +import { useFlaskDataContext } from 'js/components/Contexts'; import { sectionIconMap } from 'js/shared-styles/icons/sectionIconMap'; import { useIsLargeDesktop } from 'js/hooks/media-queries'; +import ProcessedDataWorkspaceMenu from 'js/components/detailPage/ProcessedData/ProcessedDataWorkspaceMenu'; +import WorkspacesIcon from 'assets/svg/workspaces.svg'; function ActionButton({ icon: Icon, @@ -89,33 +88,6 @@ function WorkspaceSVGIcon({ color = 'primary', ...props }: SvgIconProps) { return ; } -function CreateWorkspaceButton({ - uuid, - hubmap_id, - mapped_data_access_level, -}: Pick) { - const { setDialogIsOpen, removeDatasets, ...rest } = useCreateWorkspaceForm({ - defaultName: hubmap_id, - initialSelectedDatasets: [uuid], - }); - - const disabled = mapped_data_access_level === 'Protected'; - - return ( - <> - { - setDialogIsOpen(true); - }} - icon={WorkspaceSVGIcon} - tooltip={disabled ? 'Protected datasets are not available in workspaces.' : 'Launch a new workspace.'} - disabled={disabled} - /> - - - ); -} - const useSavedEntitiesSelector = (state: SavedEntitiesStore) => state.savedEntities; function SaveEditEntityButton({ entity_type, uuid }: Pick & { entity_type: AllEntityTypes }) { @@ -210,27 +182,36 @@ function EntityHeaderActionButtons({ entity_type?: AllEntityTypes; }) { const isLargeDesktop = useIsLargeDesktop(); - const { isWorkspacesUser } = useAppContext(); const { - entity: { mapped_data_access_level, hubmap_id, uuid }, + entity: { mapped_data_access_level, uuid, hubmap_id, status }, } = useFlaskDataContext(); if (!(entity_type && uuid)) { return null; } - const isDataset = entity_type === 'Dataset'; - const showWorkspaceButton = mapped_data_access_level && hubmap_id && isDataset && isWorkspacesUser; + const disabled = mapped_data_access_level === 'Protected'; return ( {isLargeDesktop && } - {showWorkspaceButton && ( - - )} + + } + datasetDetails={{ hubmap_id, uuid, status }} + /> ); }