Skip to content

Commit 4a56d25

Browse files
Fran McDadeFran McDade
Fran McDade
authored and
Fran McDade
committed
feat: request access via duos in addition to dbgap (#4127)
1 parent 59d2d7b commit 4a56d25

File tree

7 files changed

+216
-13
lines changed

7 files changed

+216
-13
lines changed

app/apis/azul/anvil-cmg/common/entities.ts

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface DatasetEntity {
4747
consent_group: (string | null)[];
4848
dataset_id: string;
4949
description?: string;
50+
duos_id: string | null;
5051
registered_identifier: (string | null)[];
5152
title: string;
5253
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {
2+
ButtonProps,
3+
ListItemTextProps,
4+
MenuProps,
5+
SvgIconProps,
6+
} from "@mui/material";
7+
import {
8+
TEXT_BODY_500,
9+
TEXT_BODY_SMALL_400_2_LINES,
10+
} from "@databiosphere/findable-ui/lib/theme/common/typography";
11+
12+
export const BUTTON_PROPS: ButtonProps = {
13+
color: "primary",
14+
variant: "contained",
15+
};
16+
17+
export const LIST_ITEM_TEXT_PROPS: ListItemTextProps = {
18+
primaryTypographyProps: { variant: TEXT_BODY_500 },
19+
secondaryTypographyProps: { variant: TEXT_BODY_SMALL_400_2_LINES },
20+
};
21+
22+
export const MENU_PROPS: Partial<MenuProps> = {
23+
variant: "menu",
24+
};
25+
26+
export const SVG_ICON_PROPS: SvgIconProps = {
27+
fontSize: "small",
28+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Button } from "@mui/material";
2+
import styled from "@emotion/styled";
3+
import { css } from "@emotion/react";
4+
import { primaryDark } from "@databiosphere/findable-ui/lib/styles/common/mixins/colors";
5+
import { DropdownMenu } from "@databiosphere/findable-ui/lib/components/common/DropdownMenu/dropdownMenu";
6+
7+
interface Props {
8+
open?: boolean;
9+
}
10+
11+
export const StyledDropdownMenu = styled(DropdownMenu)`
12+
.MuiPaper-menu {
13+
max-width: 324px;
14+
15+
.MuiListItemText-root {
16+
display: grid;
17+
gap: 4px;
18+
white-space: normal;
19+
}
20+
}
21+
`;
22+
23+
export const StyledButton = styled(Button, {
24+
shouldForwardProp: (prop) => prop !== "open",
25+
})<Props>`
26+
padding-right: 8px;
27+
28+
.MuiButton-endIcon {
29+
margin-left: -6px;
30+
}
31+
32+
${(props) =>
33+
props.open &&
34+
css`
35+
background-color: ${primaryDark(props)};
36+
`}
37+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { Props } from "./types";
2+
import { ListItemText, MenuItem } from "@mui/material";
3+
import { Actions } from "@databiosphere/findable-ui/lib/components/Layout/components/BackPage/components/BackPageHero/components/Actions/actions";
4+
import { CallToActionButton } from "@databiosphere/findable-ui/lib/components/common/Button/components/CallToActionButton/callToActionButton";
5+
import ArrowDropDownRoundedIcon from "@mui/icons-material/ArrowDropDownRounded";
6+
import { StyledButton, StyledDropdownMenu } from "./requestAccess.styles";
7+
import {
8+
ANCHOR_TARGET,
9+
REL_ATTRIBUTE,
10+
} from "@databiosphere/findable-ui/lib/components/Links/common/entities";
11+
import {
12+
BUTTON_PROPS,
13+
LIST_ITEM_TEXT_PROPS,
14+
MENU_PROPS,
15+
SVG_ICON_PROPS,
16+
} from "./constants";
17+
import { getRequestAccessOptions } from "./utils";
18+
19+
export const RequestAccess = ({
20+
datasetsResponse,
21+
}: Props): JSX.Element | null => {
22+
const options = getRequestAccessOptions(datasetsResponse);
23+
// If there are no request access options, return null.
24+
if (options.length === 0) return null;
25+
// If there is only one request access option, render a CallToActionButton.
26+
if (options.length === 1)
27+
return (
28+
<Actions>
29+
<CallToActionButton
30+
callToAction={{ label: "Request Access", url: options[0].href }}
31+
/>
32+
</Actions>
33+
);
34+
// Otherwise, render a dropdown menu for multiple request access options.
35+
return (
36+
<Actions>
37+
<StyledDropdownMenu
38+
{...MENU_PROPS}
39+
button={(props) => (
40+
<StyledButton
41+
{...BUTTON_PROPS}
42+
endIcon={<ArrowDropDownRoundedIcon {...SVG_ICON_PROPS} />}
43+
{...props}
44+
>
45+
Request Access
46+
</StyledButton>
47+
)}
48+
>
49+
{({ closeMenu }): JSX.Element[] => [
50+
...options.map(({ href, primary, secondary }, i) => (
51+
<MenuItem
52+
key={i}
53+
component="a"
54+
href={href}
55+
rel={REL_ATTRIBUTE.NO_OPENER_NO_REFERRER}
56+
target={ANCHOR_TARGET.BLANK}
57+
onClick={closeMenu}
58+
>
59+
<ListItemText
60+
{...LIST_ITEM_TEXT_PROPS}
61+
primary={primary}
62+
secondary={secondary}
63+
/>
64+
</MenuItem>
65+
)),
66+
]}
67+
</StyledDropdownMenu>
68+
</Actions>
69+
);
70+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { DatasetsResponse } from "../../../../../../apis/azul/anvil-cmg/common/responses";
2+
3+
export interface Props {
4+
datasetsResponse: DatasetsResponse;
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { DatasetsResponse } from "../../../../../../apis/azul/anvil-cmg/common/responses";
2+
import { takeArrayValueAt } from "../../../../../../viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders";
3+
import {
4+
processEntityArrayValue,
5+
processEntityValue,
6+
} from "../../../../../../apis/azul/common/utils";
7+
import { LABEL } from "@databiosphere/findable-ui/lib/apis/azul/common/entities";
8+
import { ListItemTextProps } from "@mui/material";
9+
10+
/**
11+
* Generates a list of request access menu options based on the provided dataset response.
12+
* This function extracts identifiers (DUOS ID and dbGaP ID) from the datasets response and returns an array of menu option objects.
13+
* 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.
14+
* @param datasetsResponse - Response model return from datasets API.
15+
* @returns menu option objects with `href`, `primary`, and `secondary` properties.
16+
*/
17+
export function getRequestAccessOptions(
18+
datasetsResponse: DatasetsResponse
19+
): (Pick<ListItemTextProps, "primary" | "secondary"> & { href: string })[] {
20+
// Get the dbGaP ID and DUOS ID from the datasets response.
21+
const dbGapId = takeArrayValueAt(
22+
processEntityArrayValue(
23+
datasetsResponse.datasets,
24+
"registered_identifier",
25+
LABEL.EMPTY
26+
),
27+
0
28+
);
29+
const duosId = processEntityValue(
30+
datasetsResponse.datasets,
31+
"duos_id",
32+
LABEL.EMPTY
33+
);
34+
const options = [];
35+
if (duosId) {
36+
// If a DUOS ID is present, add a menu option for DUOS.
37+
options.push({
38+
href: `https://duos.org/dataset/${duosId}`,
39+
primary: "DUOS",
40+
secondary:
41+
"Request access via DUOS, which streamlines data access for NHGRI-sponsored studies, both registered and unregistered in dbGaP.",
42+
});
43+
}
44+
if (dbGapId) {
45+
// If a dbGaP ID is present, add a menu option for dbGaP.
46+
options.push({
47+
href: `https://dbgap.ncbi.nlm.nih.gov/aa/wga.cgi?adddataset=${dbGapId}`,
48+
primary: "dbGaP",
49+
secondary:
50+
"Request access via the dbGaP Authorized Access portal for studies registered in dbGaP, following the standard data access process.",
51+
});
52+
}
53+
return options;
54+
}

app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts

+21-13
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import {
3939
ChipProps as MChipProps,
4040
FadeProps as MFadeProps,
4141
} from "@mui/material";
42-
import React from "react";
42+
import React, { ReactNode } from "react";
4343
import {
4444
ANVIL_CMG_CATEGORY_KEY,
4545
ANVIL_CMG_CATEGORY_LABEL,
@@ -112,6 +112,7 @@ import { Unused, Void } from "../../../common/entities";
112112
import { SUMMARY_DISPLAY_TEXT } from "./summaryMapper/constants";
113113
import { mapExportSummary } from "./summaryMapper/summaryMapper";
114114
import { ExportEntity } from "app/components/Export/components/AnVILExplorer/components/ExportEntity/exportEntity";
115+
import { RequestAccess } from "../../../../components/Detail/components/AnVILCMG/components/RequestAccess/requestAccess";
115116

116117
/**
117118
* Build props for activity type BasicCell component from the given activities response.
@@ -512,6 +513,7 @@ export const buildDatasetHero = (
512513
datasetsResponse: DatasetsResponse
513514
): React.ComponentProps<typeof C.BackPageHero> => {
514515
return {
516+
actions: getDatasetRequestAccess(datasetsResponse),
515517
breadcrumbs: getDatasetBreadcrumbs(datasetsResponse),
516518
callToAction: getDatasetCallToAction(datasetsResponse),
517519
title: getDatasetTitle(datasetsResponse),
@@ -1079,10 +1081,7 @@ function getDatasetCallToAction(
10791081
): CallToAction | undefined {
10801082
const isReady = isResponseReady(datasetsResponse);
10811083
const isAccessGranted = isDatasetAccessible(datasetsResponse);
1082-
const registeredIdentifier = getDatasetRegisteredIdentifier(datasetsResponse);
1083-
if (!isReady) {
1084-
return;
1085-
}
1084+
if (!isReady) return;
10861085
// Display export button if user is authorized to access the dataset.
10871086
if (isAccessGranted) {
10881087
return {
@@ -1091,14 +1090,6 @@ function getDatasetCallToAction(
10911090
url: `/datasets/${getDatasetEntryId(datasetsResponse)}/export`,
10921091
};
10931092
}
1094-
// Display request access button if user is not authorized to access the dataset.
1095-
if (registeredIdentifier === LABEL.UNSPECIFIED) {
1096-
return {
1097-
label: "Request Access",
1098-
target: ANCHOR_TARGET.BLANK,
1099-
url: `https://dbgap.ncbi.nlm.nih.gov/aa/wga.cgi?adddataset=${registeredIdentifier}`,
1100-
};
1101-
}
11021093
// Otherwise, display nothing.
11031094
}
11041095

@@ -1116,6 +1107,23 @@ export function getDatasetRegisteredIdentifier(
11161107
);
11171108
}
11181109

1110+
/**
1111+
* Returns the `actions` prop for the Hero component from the given datasets response.
1112+
* @param datasetsResponse - Response model return from datasets API.
1113+
* @returns react node to be used as the `actions` props for the Hero component.
1114+
*/
1115+
function getDatasetRequestAccess(
1116+
datasetsResponse: DatasetsResponse
1117+
): ReactNode {
1118+
const isReady = isResponseReady(datasetsResponse);
1119+
const isAccessGranted = isDatasetAccessible(datasetsResponse);
1120+
if (!isReady) return null;
1121+
// Display nothing if user is authorized to access the dataset.
1122+
if (isAccessGranted) return null;
1123+
// Display request access button if user is not authorized to access the dataset.
1124+
return RequestAccess({ datasetsResponse });
1125+
}
1126+
11191127
/**
11201128
* Returns StatusBadge component props from the given datasets response.
11211129
* @param datasetsResponse - Response model return from datasets API.

0 commit comments

Comments
 (0)