From a50c94f1b7210e8efee0d811cef1d1d403971551 Mon Sep 17 00:00:00 2001 From: Alejandro-Vega Date: Tue, 3 Sep 2024 12:45:52 -0400 Subject: [PATCH 01/29] Update approved study types and queries --- src/graphql/index.ts | 5 ++- src/graphql/listApprovedStudies.ts | 32 +++++++++++++++++-- .../listApprovedStudiesOfMyOrganization.ts | 2 +- src/types/ApprovedStudies.d.ts | 20 ++++++++++++ src/types/Submissions.d.ts | 2 ++ 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/graphql/index.ts b/src/graphql/index.ts index 1ae9f7470..47ae4a51c 100644 --- a/src/graphql/index.ts +++ b/src/graphql/index.ts @@ -142,7 +142,10 @@ export { query as LIST_CURATORS } from "./listActiveCurators"; export type { Response as ListCuratorsResp } from "./listActiveCurators"; export { query as LIST_APPROVED_STUDIES } from "./listApprovedStudies"; -export type { Response as ListApprovedStudiesResp } from "./listApprovedStudies"; +export type { + Input as ListApprovedStudiesInput, + Response as ListApprovedStudiesResp, +} from "./listApprovedStudies"; export { mutation as CREATE_ORG } from "./createOrganization"; export type { Input as CreateOrgInput, Response as CreateOrgResp } from "./createOrganization"; diff --git a/src/graphql/listApprovedStudies.ts b/src/graphql/listApprovedStudies.ts index 5160a60e9..693a0e819 100644 --- a/src/graphql/listApprovedStudies.ts +++ b/src/graphql/listApprovedStudies.ts @@ -1,8 +1,22 @@ import gql from "graphql-tag"; export const query = gql` - query listApprovedStudies { - listApprovedStudies { + query listApprovedStudies( + $first: Int + $offset: Int + $orderBy: String + $sortDirection: String + $controlledAccess: String + $study: String + ) { + listApprovedStudies( + first: $first + offset: $offset + orderBy: $orderBy + sortDirection: $sortDirection + controlledAccess: $controlledAccess + study: $study + ) { _id studyName studyAbbreviation @@ -11,6 +25,18 @@ export const query = gql` } `; +export type Input = { + first?: number; + offset?: number; + orderBy?: string; + sortDirection?: Order; + controlledAccess?: ControlledAccess; + study?: string; +}; + export type Response = { - listApprovedStudies: ApprovedStudy[]; + listApprovedStudies: { + total: number; + studies: ApprovedStudy[]; + }; }; diff --git a/src/graphql/listApprovedStudiesOfMyOrganization.ts b/src/graphql/listApprovedStudiesOfMyOrganization.ts index 3311921aa..2e616b4fc 100644 --- a/src/graphql/listApprovedStudiesOfMyOrganization.ts +++ b/src/graphql/listApprovedStudiesOfMyOrganization.ts @@ -13,5 +13,5 @@ export const query = gql` `; export type Response = { - listApprovedStudiesOfMyOrganization: ApprovedStudy[]; + listApprovedStudiesOfMyOrganization: ApprovedStudyOfMyOrganization[]; }; diff --git a/src/types/ApprovedStudies.d.ts b/src/types/ApprovedStudies.d.ts index b8364e56c..634482f0d 100644 --- a/src/types/ApprovedStudies.d.ts +++ b/src/types/ApprovedStudies.d.ts @@ -1,5 +1,6 @@ type ApprovedStudy = { _id: string; + originalOrg: string; /** * Study name * @@ -20,4 +21,23 @@ type ApprovedStudy = { * Boolean flag dictating whether the study has controlled access data */ controlledAccess: boolean; + /** + * Principal Investigator's name + */ + PI: string; + /** + * Open Researcher and Contributor ID. + * + * @example 0000-0001-2345-6789 + */ + ORCID: string; + /** + * Submission Request approval date or manual record creation date + */ + createdAt: string; }; + +type ApprovedStudyOfMyOrganization = Pick< + ApprovedStudy, + "_id" | "studyName" | "studyAbbreviation" | "dbGaPID" | "controlledAccess" +>; diff --git a/src/types/Submissions.d.ts b/src/types/Submissions.d.ts index 19ccbf902..f73afb476 100644 --- a/src/types/Submissions.d.ts +++ b/src/types/Submissions.d.ts @@ -372,3 +372,5 @@ type NodeDetailResult = { }; type NodeRelationship = "parent" | "child"; + +type ControlledAccess = "All" | "Controlled" | "Open"; From 8fef084febc11e0e053ea14b9c3caae34328179a Mon Sep 17 00:00:00 2001 From: Alejandro-Vega Date: Tue, 3 Sep 2024 12:47:14 -0400 Subject: [PATCH 02/29] Add route for studies and controller --- src/content/studies/Controller.tsx | 34 ++++++++++++++++++++++++++++++ src/router.tsx | 11 ++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/content/studies/Controller.tsx diff --git a/src/content/studies/Controller.tsx b/src/content/studies/Controller.tsx new file mode 100644 index 000000000..ea4e0f813 --- /dev/null +++ b/src/content/studies/Controller.tsx @@ -0,0 +1,34 @@ +import React, { FC } from "react"; +import { Navigate, useParams } from "react-router-dom"; +import { useAuthContext } from "../../components/Contexts/AuthContext"; +import { ApprovedStudiesProvider } from "../../components/Contexts/ApprovedStudiesListContext"; +import ListView from "./ListView"; +// import OrganizationView from "./OrganizationView"; + +/** + * Renders the correct view based on the URL and permissions-tier + * + * @param {void} props - React props + * @returns {FC} - React component + */ +const OrganizationController: FC = () => { + const { studyId } = useParams<{ studyId?: string }>(); + const { user } = useAuthContext(); + const isAdministrative = user?.role === "Admin"; + + if (!isAdministrative) { + return ; + } + + if (studyId) { + return null; // TODO: ; + } + + return ( + + + + ); +}; + +export default OrganizationController; diff --git a/src/router.tsx b/src/router.tsx index 1c165b529..ef1194e78 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -16,6 +16,7 @@ const DataSubmissions = LazyLoader(lazy(() => import("./content/dataSubmissions/ const Users = LazyLoader(lazy(() => import("./content/users/Controller"))); const DMN = LazyLoader(lazy(() => import("./content/modelNavigator/Controller"))); const Organizations = LazyLoader(lazy(() => import("./content/organizations/Controller"))); +const Studies = LazyLoader(lazy(() => import("./content/studies/Controller"))); const Status404 = LazyLoader(lazy(() => import("./content/status/Page404"))); const OperationDashboard = LazyLoader( lazy(() => import("./content/operationDashboard/Controller")) @@ -108,6 +109,16 @@ const routes: RouteObject[] = [ /> ), }, + { + path: "/studies/:studyId?", + element: ( + } + redirectPath="/studies" + redirectName="Studies Management" + /> + ), + }, { path: "/operation-dashboard", element: ( From 5f5c2f0f86acb50410c7513fa42aac2baba04712 Mon Sep 17 00:00:00 2001 From: Alejandro-Vega Date: Tue, 3 Sep 2024 12:48:08 -0400 Subject: [PATCH 03/29] Created an approved studies list context --- .../Contexts/ApprovedStudiesListContext.tsx | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/components/Contexts/ApprovedStudiesListContext.tsx diff --git a/src/components/Contexts/ApprovedStudiesListContext.tsx b/src/components/Contexts/ApprovedStudiesListContext.tsx new file mode 100644 index 000000000..294225b7a --- /dev/null +++ b/src/components/Contexts/ApprovedStudiesListContext.tsx @@ -0,0 +1,95 @@ +import React, { FC, createContext, useContext, useEffect, useState } from "react"; +import { useQuery } from "@apollo/client"; +import { LIST_APPROVED_STUDIES, ListApprovedStudiesResp } from "../../graphql"; + +export type ContextState = { + status: Status; + data: ApprovedStudy[]; +}; + +export enum Status { + LOADING = "LOADING", + LOADED = "LOADED", + ERROR = "ERROR", +} + +const initialState: ContextState = { + status: Status.LOADING, + data: [], +}; + +/** + * Approved Studies List Context + * + * NOTE: Do NOT use this context directly. Use the useApprovedStudiesListContext hook instead. + * this is exported for testing purposes only. + * + * @see ContextState ApprovedStudies context state + * @see useApprovedStudiesListContext ApprovedStudies context hook + */ +export const Context = createContext(null); +Context.displayName = "ApprovedStudiesListContext"; + +/** + * Approved Studies Context Hook + * + * + * @see ApprovedStudiesProvider Must be wrapped in a ApprovedStudiesProvider component + * @see ContextState Approved Studies context state returned by the hook + * @returns {ContextState} - Approved Studies context + */ +export const useApprovedStudiesListContext = (): ContextState => { + const context = useContext(Context); + + if (!context) { + throw new Error( + "ApprovedStudiesListContext cannot be used outside of the ApprovedStudiesProvider component" + ); + } + + return context; +}; + +type ProviderProps = { + preload: boolean; + children: React.ReactNode; +}; + +/** + * Creates a approved studies context provider + * + * @see useApprovedStudiesListContext + * @param {ProviderProps} props + * @returns {JSX.Element} Context provider + */ +export const ApprovedStudiesProvider: FC = ({ + preload, + children, +}: ProviderProps) => { + const [state, setState] = useState(initialState); + + const { data, loading, error } = preload + ? useQuery(LIST_APPROVED_STUDIES, { + context: { clientName: "backend" }, + fetchPolicy: "no-cache", + }) + : { data: null, loading: false, error: null }; + + useEffect(() => { + if (loading) { + setState({ status: Status.LOADING, data: [] }); + return; + } + if (error) { + setState({ status: Status.ERROR, data: [] }); + return; + } + + setState({ + status: Status.LOADED, + data: data?.listApprovedStudies?.studies || [], + }); + }, [loading, error, data]); + + return {children}; +}; From a71bb8ee93e892fd830f6dfdeb7f9132605fc99d Mon Sep 17 00:00:00 2001 From: Alejandro-Vega Date: Tue, 3 Sep 2024 12:48:54 -0400 Subject: [PATCH 04/29] Update navbar to include new manage studies --- src/components/Header/HeaderTabletAndMobile.tsx | 8 ++++++++ src/components/Header/components/NavbarDesktop.tsx | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/components/Header/HeaderTabletAndMobile.tsx b/src/components/Header/HeaderTabletAndMobile.tsx index 6bf80130f..af7034aa1 100644 --- a/src/components/Header/HeaderTabletAndMobile.tsx +++ b/src/components/Header/HeaderTabletAndMobile.tsx @@ -184,6 +184,14 @@ const Header = () => { className: "navMobileSubItem", }); } + if (user?.role === "Admin") { + navbarSublists[displayName].splice(1, 0, { + name: "Manage Studies", + link: "/studies", + id: "navbar-dropdown-item-studies-manage", + className: "navMobileSubItem", + }); + } if (user?.role && GenerateApiTokenRoles.includes(user?.role)) { navbarSublists[displayName].splice(1, 0, { name: "API Token", diff --git a/src/components/Header/components/NavbarDesktop.tsx b/src/components/Header/components/NavbarDesktop.tsx index 41f236ed2..e90b96f84 100644 --- a/src/components/Header/components/NavbarDesktop.tsx +++ b/src/components/Header/components/NavbarDesktop.tsx @@ -541,6 +541,18 @@ const NavBar = () => { )} + {user?.role === "Admin" && ( + + setClickedTitle("")} + > + Manage Studies + + + )} {user?.role && GenerateApiTokenRoles.includes(user?.role) ? (