diff --git a/app/apis/azul/anvil-cmg/common/entities.ts b/app/apis/azul/anvil-cmg/common/entities.ts index 146eec12c..737600bf7 100644 --- a/app/apis/azul/anvil-cmg/common/entities.ts +++ b/app/apis/azul/anvil-cmg/common/entities.ts @@ -47,6 +47,7 @@ export interface DatasetEntity { consent_group: (string | null)[]; dataset_id: string; description?: string; + duos_id: string | null; registered_identifier: (string | null)[]; title: string; } diff --git a/app/components/Detail/components/AnVILCMG/components/RequestAccess/constants.ts b/app/components/Detail/components/AnVILCMG/components/RequestAccess/constants.ts new file mode 100644 index 000000000..ddb3d4f19 --- /dev/null +++ b/app/components/Detail/components/AnVILCMG/components/RequestAccess/constants.ts @@ -0,0 +1,28 @@ +import { + ButtonProps, + ListItemTextProps, + MenuProps, + SvgIconProps, +} from "@mui/material"; +import { + TEXT_BODY_500, + TEXT_BODY_SMALL_400_2_LINES, +} from "@databiosphere/findable-ui/lib/theme/common/typography"; + +export const BUTTON_PROPS: ButtonProps = { + color: "primary", + variant: "contained", +}; + +export const LIST_ITEM_TEXT_PROPS: ListItemTextProps = { + primaryTypographyProps: { variant: TEXT_BODY_500 }, + secondaryTypographyProps: { variant: TEXT_BODY_SMALL_400_2_LINES }, +}; + +export const MENU_PROPS: Partial = { + variant: "menu", +}; + +export const SVG_ICON_PROPS: SvgIconProps = { + fontSize: "small", +}; diff --git a/app/components/Detail/components/AnVILCMG/components/RequestAccess/requestAccess.styles.ts b/app/components/Detail/components/AnVILCMG/components/RequestAccess/requestAccess.styles.ts new file mode 100644 index 000000000..268b8e291 --- /dev/null +++ b/app/components/Detail/components/AnVILCMG/components/RequestAccess/requestAccess.styles.ts @@ -0,0 +1,37 @@ +import { Button } from "@mui/material"; +import styled from "@emotion/styled"; +import { css } from "@emotion/react"; +import { primaryDark } from "@databiosphere/findable-ui/lib/styles/common/mixins/colors"; +import { DropdownMenu } from "@databiosphere/findable-ui/lib/components/common/DropdownMenu/dropdownMenu"; + +interface Props { + open?: boolean; +} + +export const StyledDropdownMenu = styled(DropdownMenu)` + .MuiPaper-menu { + max-width: 324px; + + .MuiListItemText-root { + display: grid; + gap: 4px; + white-space: normal; + } + } +`; + +export const StyledButton = styled(Button, { + shouldForwardProp: (prop) => prop !== "open", +})` + padding-right: 8px; + + .MuiButton-endIcon { + margin-left: -6px; + } + + ${(props) => + props.open && + css` + background-color: ${primaryDark(props)}; + `} +`; diff --git a/app/components/Detail/components/AnVILCMG/components/RequestAccess/requestAccess.tsx b/app/components/Detail/components/AnVILCMG/components/RequestAccess/requestAccess.tsx new file mode 100644 index 000000000..ac99a1d5c --- /dev/null +++ b/app/components/Detail/components/AnVILCMG/components/RequestAccess/requestAccess.tsx @@ -0,0 +1,70 @@ +import { Props } from "./types"; +import { ListItemText, MenuItem } from "@mui/material"; +import { Actions } from "@databiosphere/findable-ui/lib/components/Layout/components/BackPage/components/BackPageHero/components/Actions/actions"; +import { CallToActionButton } from "@databiosphere/findable-ui/lib/components/common/Button/components/CallToActionButton/callToActionButton"; +import ArrowDropDownRoundedIcon from "@mui/icons-material/ArrowDropDownRounded"; +import { StyledButton, StyledDropdownMenu } from "./requestAccess.styles"; +import { + ANCHOR_TARGET, + REL_ATTRIBUTE, +} from "@databiosphere/findable-ui/lib/components/Links/common/entities"; +import { + BUTTON_PROPS, + LIST_ITEM_TEXT_PROPS, + MENU_PROPS, + SVG_ICON_PROPS, +} from "./constants"; +import { getRequestAccessOptions } from "./utils"; + +export const RequestAccess = ({ + datasetsResponse, +}: Props): JSX.Element | null => { + const options = getRequestAccessOptions(datasetsResponse); + // If there are no request access options, return null. + if (options.length === 0) return null; + // If there is only one request access option, render a CallToActionButton. + if (options.length === 1) + return ( + + + + ); + // Otherwise, render a dropdown menu for multiple request access options. + return ( + + ( + } + {...props} + > + Request Access + + )} + > + {({ closeMenu }): JSX.Element[] => [ + ...options.map(({ href, primary, secondary }, i) => ( + + + + )), + ]} + + + ); +}; diff --git a/app/components/Detail/components/AnVILCMG/components/RequestAccess/types.ts b/app/components/Detail/components/AnVILCMG/components/RequestAccess/types.ts new file mode 100644 index 000000000..c5712dbb5 --- /dev/null +++ b/app/components/Detail/components/AnVILCMG/components/RequestAccess/types.ts @@ -0,0 +1,5 @@ +import { DatasetsResponse } from "../../../../../../apis/azul/anvil-cmg/common/responses"; + +export interface Props { + datasetsResponse: DatasetsResponse; +} diff --git a/app/components/Detail/components/AnVILCMG/components/RequestAccess/utils.ts b/app/components/Detail/components/AnVILCMG/components/RequestAccess/utils.ts new file mode 100644 index 000000000..e33c94241 --- /dev/null +++ b/app/components/Detail/components/AnVILCMG/components/RequestAccess/utils.ts @@ -0,0 +1,54 @@ +import { DatasetsResponse } from "../../../../../../apis/azul/anvil-cmg/common/responses"; +import { takeArrayValueAt } from "../../../../../../viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders"; +import { + processEntityArrayValue, + processEntityValue, +} from "../../../../../../apis/azul/common/utils"; +import { LABEL } from "@databiosphere/findable-ui/lib/apis/azul/common/entities"; +import { ListItemTextProps } from "@mui/material"; + +/** + * Generates a list of request access menu options based on the provided dataset response. + * This function extracts identifiers (DUOS ID and dbGaP ID) from the datasets response and returns an array of menu option objects. + * Each menu option contains a link `href` and title `primary` and description text `secondary`, to be used in Material UI's `MenuItem` and `ListItemText` component. + * @param datasetsResponse - Response model return from datasets API. + * @returns menu option objects with `href`, `primary`, and `secondary` properties. + */ +export function getRequestAccessOptions( + datasetsResponse: DatasetsResponse +): (Pick & { href: string })[] { + // Get the dbGaP ID and DUOS ID from the datasets response. + const dbGapId = takeArrayValueAt( + processEntityArrayValue( + datasetsResponse.datasets, + "registered_identifier", + LABEL.EMPTY + ), + 0 + ); + const duosId = processEntityValue( + datasetsResponse.datasets, + "duos_id", + LABEL.EMPTY + ); + const options = []; + if (duosId) { + // If a DUOS ID is present, add a menu option for DUOS. + options.push({ + href: `https://duos.org/dataset/${duosId}`, + primary: "DUOS", + secondary: + "Request access via DUOS, which streamlines data access for NHGRI-sponsored studies, both registered and unregistered in dbGaP.", + }); + } + if (dbGapId) { + // If a dbGaP ID is present, add a menu option for dbGaP. + options.push({ + href: `https://dbgap.ncbi.nlm.nih.gov/aa/wga.cgi?adddataset=${dbGapId}`, + primary: "dbGaP", + secondary: + "Request access via the dbGaP Authorized Access portal for studies registered in dbGaP, following the standard data access process.", + }); + } + return options; +} diff --git a/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts b/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts index 6da95403a..4ea659b87 100644 --- a/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts +++ b/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts @@ -39,7 +39,7 @@ import { ChipProps as MChipProps, FadeProps as MFadeProps, } from "@mui/material"; -import React from "react"; +import React, { ReactNode } from "react"; import { ANVIL_CMG_CATEGORY_KEY, ANVIL_CMG_CATEGORY_LABEL, @@ -112,6 +112,7 @@ import { Unused, Void } from "../../../common/entities"; import { SUMMARY_DISPLAY_TEXT } from "./summaryMapper/constants"; import { mapExportSummary } from "./summaryMapper/summaryMapper"; import { ExportEntity } from "app/components/Export/components/AnVILExplorer/components/ExportEntity/exportEntity"; +import { RequestAccess } from "../../../../components/Detail/components/AnVILCMG/components/RequestAccess/requestAccess"; /** * Build props for activity type BasicCell component from the given activities response. @@ -512,6 +513,7 @@ export const buildDatasetHero = ( datasetsResponse: DatasetsResponse ): React.ComponentProps => { return { + actions: getDatasetRequestAccess(datasetsResponse), breadcrumbs: getDatasetBreadcrumbs(datasetsResponse), callToAction: getDatasetCallToAction(datasetsResponse), title: getDatasetTitle(datasetsResponse), @@ -1079,10 +1081,7 @@ function getDatasetCallToAction( ): CallToAction | undefined { const isReady = isResponseReady(datasetsResponse); const isAccessGranted = isDatasetAccessible(datasetsResponse); - const registeredIdentifier = getDatasetRegisteredIdentifier(datasetsResponse); - if (!isReady) { - return; - } + if (!isReady) return; // Display export button if user is authorized to access the dataset. if (isAccessGranted) { return { @@ -1091,14 +1090,6 @@ function getDatasetCallToAction( url: `/datasets/${getDatasetEntryId(datasetsResponse)}/export`, }; } - // Display request access button if user is not authorized to access the dataset. - if (registeredIdentifier === LABEL.UNSPECIFIED) { - return { - label: "Request Access", - target: ANCHOR_TARGET.BLANK, - url: `https://dbgap.ncbi.nlm.nih.gov/aa/wga.cgi?adddataset=${registeredIdentifier}`, - }; - } // Otherwise, display nothing. } @@ -1116,6 +1107,23 @@ export function getDatasetRegisteredIdentifier( ); } +/** + * Returns the `actions` prop for the Hero component from the given datasets response. + * @param datasetsResponse - Response model return from datasets API. + * @returns react node to be used as the `actions` props for the Hero component. + */ +function getDatasetRequestAccess( + datasetsResponse: DatasetsResponse +): ReactNode { + const isReady = isResponseReady(datasetsResponse); + const isAccessGranted = isDatasetAccessible(datasetsResponse); + if (!isReady) return null; + // Display nothing if user is authorized to access the dataset. + if (isAccessGranted) return null; + // Display request access button if user is not authorized to access the dataset. + return RequestAccess({ datasetsResponse }); +} + /** * Returns StatusBadge component props from the given datasets response. * @param datasetsResponse - Response model return from datasets API.