Skip to content

Commit

Permalink
[JN-1523] preserving study sidebar during portal navigation (#1305)
Browse files Browse the repository at this point in the history
  • Loading branch information
devonbush authored Dec 12, 2024
1 parent d6a8a74 commit 360bdde
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 42 deletions.
2 changes: 0 additions & 2 deletions ui-admin/src/navbar/AdminSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ const AdminSidebar = ({ config }: { config: Config }) => {
if (portalList.length) {
studyList = portalList.flatMap(portal => portal.portalStudies.map(ps => ps.study))
}

const currentStudy = studyList.find(study => study.shortcode === studyShortcode)

const color = ZONE_COLORS[config.deploymentZone] || ZONE_COLORS['prod']

// automatically collapse the sidebar for mobile-first routes
Expand Down
16 changes: 13 additions & 3 deletions ui-admin/src/navbar/StudySidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@ import {
useNavigate
} from 'react-router-dom'
import {
siteContentPath,
studyKitsPath,
studyParticipantsPath
} from 'portal/PortalRouter'
import StudySelector from './StudySelector'
import React from 'react'
import {
adminTasksPath,
adminTasksPath, studyEnvMailingListPath, studyEnvSiteContentPath,
studyEnvDataBrowserPath,
studyEnvDatasetListViewPath,
studyEnvExportIntegrationsPath,
Expand All @@ -33,6 +32,7 @@ import {
studyPublishingPath
} from 'study/StudyRouter'
import { sidebarNavLinkClasses } from './AdminSidebar'
import { portalUsersPath } from '../user/AdminUserRouter'


/** shows menu options related to the current study */
Expand Down Expand Up @@ -75,6 +75,10 @@ export const StudySidebar = ({ study, portalList, portalShortcode }:
<NavLink to={studyEnvImportPath(portalShortcode, study.shortcode, 'sandbox')}
className={sidebarNavLinkClasses} style={navStyleFunc}>Import Participants</NavLink>
</li>
<li className="mb-2">
<NavLink to={studyEnvMailingListPath({ ...studyParams, envName: 'live' })}
className={sidebarNavLinkClasses} style={navStyleFunc}>Mailing List</NavLink>
</li>
</ul>}/>
<CollapsableMenu header={'Analytics & Data'} content={<ul className="list-unstyled">
<li className="mb-2">
Expand All @@ -98,7 +102,7 @@ export const StudySidebar = ({ study, portalList, portalShortcode }:
</ul>}/>
<CollapsableMenu header={'Design & Build'} content={<ul className="list-unstyled">
<li className="mb-2">
<NavLink to={siteContentPath(portalShortcode, 'sandbox')}
<NavLink to={studyEnvSiteContentPath({ ...studyParams, envName: 'sandbox' })}
className={sidebarNavLinkClasses} style={navStyleFunc}>Website</NavLink>
</li>
<li className="mb-2">
Expand All @@ -120,6 +124,12 @@ export const StudySidebar = ({ study, portalList, portalShortcode }:
className={sidebarNavLinkClasses} style={navStyleFunc}>Site Settings</NavLink>
</li>
</ul>}/>
<CollapsableMenu header={'Manage'} content={<ul className="list-unstyled">
<li className="mb-2">
<NavLink to={portalUsersPath({ portalShortcode, studyShortcode: study.shortcode, envName: 'live' })}
className={sidebarNavLinkClasses} style={navStyleFunc}>Team Members</NavLink>
</li>
</ul>}/>
</div>
</div>
}
38 changes: 30 additions & 8 deletions ui-admin/src/portal/PortalRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, { useContext } from 'react'
import React, { useContext, useEffect } from 'react'
import {
Link,
Route,
Routes, useNavigate,
useParams
} from 'react-router-dom'
import StudyRouter from '../study/StudyRouter'
import StudyRouter, { studyShortcodeFromPath } from '../study/StudyRouter'
import PortalDashboard from './dashboard/PortalDashboard'
import {
LoadedPortalContextT,
Expand All @@ -22,7 +22,6 @@ import {
PortalEnvironment
} from '@juniper/ui-core'
import SiteContentLoader from './siteContent/SiteContentLoader'
import { PortalAdminUserRouter } from 'user/AdminUserRouter'
import { NavBreadcrumb } from 'navbar/AdminNavbar'
import Select from 'react-select'
import { portalEnvPath } from 'study/StudyEnvironmentRouter'
Expand All @@ -43,13 +42,34 @@ export type PortalEnvContext = {
/** controls routes for within a portal */
export default function PortalRouter() {
const portalContext = useContext(PortalContext) as LoadedPortalContextT
const portal = portalContext.portal

// if there isn't a study selected, default to the first
const params = useParams()
const studyShortcode = studyShortcodeFromPath(params['*'])
const navigate = useNavigate()

let effectivePath = portalHomePath(portalContext.portal.shortcode, studyShortcode, 'live')
const currentStudy = portal.portalStudies.find(pStudy =>
pStudy.study.shortcode === studyShortcode)?.study ||
portal?.portalStudies.sort((a, b) => a.createdAt - b.createdAt)[0]?.study
if (!studyShortcode && currentStudy) {
effectivePath =`/${portal.shortcode}/studies/${currentStudy?.shortcode}/env/live/portalDashboard`
}

useEffect(() => {
if (!studyShortcode && currentStudy) {
navigate(effectivePath)
}
}, [])

return <>
<NavBreadcrumb value={portalHomePath(portalContext.portal.shortcode)}>
<NavBreadcrumb value={effectivePath}>
<Link className='me-2' to={''}>
<FontAwesomeIcon icon={faHome}/> Home
</Link>
<FontAwesomeIcon icon={faChevronRight} className="fa-xs text-muted me-2"/>
<Link className='me-2' to={portalHomePath(portalContext.portal.shortcode)}>
<Link className='me-2' to={effectivePath}>
{portalContext.portal.name}
</Link>
</NavBreadcrumb>
Expand All @@ -58,7 +78,6 @@ export default function PortalRouter() {
<Route path=":studyShortcode/*" element={<StudyRouter portalContext={portalContext}/>}/>
</Route>
<Route path="env/:portalEnv/*" element={<PortalEnvRouter portalContext={portalContext}/>}/>
<Route path="users/*" element={<PortalAdminUserRouter portal={portalContext.portal}/>}/>
<Route index element={<PortalDashboard portal={portalContext.portal}/>}/>
<Route path="*" element={<div>Unmatched portal route</div>}/>
</Routes>
Expand Down Expand Up @@ -138,8 +157,11 @@ function PortalEnvRouter({ portalContext }: {portalContext: LoadedPortalContextT
}

/** admin homepage for a given portal */
export const portalHomePath = (portalShortcode: string) => {
return `/${portalShortcode}`
export const portalHomePath = (portalShortcode: string, studyShortcode?: string, envName?: string) => {
if (!studyShortcode || !envName) {
return `/${portalShortcode}`
}
return `/${portalShortcode}/studies/${studyShortcode}/env/${envName}/portalDashboard`
}

/** path to portal-specific user list */
Expand Down
12 changes: 6 additions & 6 deletions ui-admin/src/portal/dashboard/widgets/MailingListWidget.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { Link } from 'react-router-dom'
import { Button } from 'components/forms/Button'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArrowUpRightFromSquare, faCaretUp, faLightbulb } from '@fortawesome/free-solid-svg-icons'
import { faCaretUp, faLightbulb, faUserPen } from '@fortawesome/free-solid-svg-icons'
import LoadingSpinner from 'util/LoadingSpinner'
import React, { useState } from 'react'
import Api, { MailingListContact } from 'api/api'
import { useLoadingEffect } from 'api/api-utils'
import { Portal } from '@juniper/ui-core'
import { mailingListPath } from 'portal/PortalRouter'
import pluralize from 'pluralize'
import { InfoCard, InfoCardBody, InfoCardHeader } from 'components/InfoCard'
import { studyEnvMailingListPath, useStudyEnvParamsFromPath } from '../../../study/StudyEnvironmentRouter'

export const MailingListWidget = ({ portal }: { portal: Portal }) => {
const [contacts, setContacts] = useState<MailingListContact[]>([])

const studyEnvParams = useStudyEnvParamsFromPath()
const recentContacts = contacts.filter(contact => {
const lastWeek = new Date()
lastWeek.setDate(lastWeek.getDate() - 7)
return new Date((contact.createdAt || 0) * 1000) > lastWeek
})

const { isLoading: isMailingListLoading } = useLoadingEffect(async () => {
const result = await Api.fetchMailingList(portal.shortcode, 'live')
const result = await Api.fetchMailingList(portal.shortcode, studyEnvParams.envName || 'live')
setContacts(result)
}, [portal.shortcode])

Expand All @@ -30,10 +30,10 @@ export const MailingListWidget = ({ portal }: { portal: Portal }) => {
<InfoCardHeader>
<div className="d-flex align-items-center justify-content-between w-100">
<span className="fw-bold">Mailing List</span>
<Link to={mailingListPath(portal.shortcode, 'live')}>
<Link to={studyEnvMailingListPath(studyEnvParams)}>
<Button tooltip={'View mailing list'}
variant="light" className="border">
<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="fa-lg"/>
<FontAwesomeIcon icon={faUserPen} className="fa-lg"/> Manage
</Button>
</Link>
</div>
Expand Down
17 changes: 9 additions & 8 deletions ui-admin/src/portal/dashboard/widgets/StudyTeamWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUserPen } from '@fortawesome/free-solid-svg-icons'
import React, { useState } from 'react'
import { Portal } from '@juniper/ui-core'
import { useNavigate } from 'react-router-dom'
import { Link } from 'react-router-dom'
import { portalUsersPath } from 'user/AdminUserRouter'
import { useLoadingEffect } from 'api/api-utils'
import Api from 'api/api'
import { AdminUser, Role } from 'api/adminUser'
import LoadingSpinner from 'util/LoadingSpinner'
import { InfoCard, InfoCardBody, InfoCardHeader } from 'components/InfoCard'
import { useStudyEnvParamsFromPath } from '../../../study/StudyEnvironmentRouter'

export const StudyTeamWidget = ({ portal }: { portal: Portal }) => {
const [users, setUsers] = useState<AdminUser[]>([])
const navigate = useNavigate()
const [roles, setRoles] = useState<Role[]>([])

const studyEnvParams = useStudyEnvParamsFromPath()
const { isLoading: isLoadingRoles } = useLoadingEffect(async () => {
const fetchedRoles = await Api.fetchRoles()
setRoles(fetchedRoles)
Expand All @@ -31,11 +31,12 @@ export const StudyTeamWidget = ({ portal }: { portal: Portal }) => {
<InfoCardHeader>
<div className="d-flex align-items-center justify-content-between w-100">
<span className="fw-bold">Team Members</span>
<Button onClick={() => navigate(portalUsersPath(portal.shortcode))}
tooltip={'View all team members'}
variant="light" className="border">
<FontAwesomeIcon icon={faUserPen} className="fa-lg"/> Manage
</Button>
<Link to={portalUsersPath(studyEnvParams)}>
<Button tooltip={'View all team members'}
variant="light" className="border">
<FontAwesomeIcon icon={faUserPen} className="fa-lg"/> Manage
</Button>
</Link>
</div>
</InfoCardHeader>
<InfoCardBody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const StudyTrendsWidget = ({ portal, study }: { portal: Portal, study: St
}, [])

const getMetricsLast7Days = async (
study: Study, metric: string, setMetricData: (data: BasicMetricDatum[]) => void
study: Study, metric: string, setMetricData: (data: BasicMetricDatum[], envName: string) => void
) => {
const result = await Api.fetchMetric(portal.shortcode, study.shortcode, 'live', metric)
// TODO: api doesn't currently honor time ranges, so we'll filter down after fetching
Expand Down
23 changes: 19 additions & 4 deletions ui-admin/src/portal/dashboard/widgets/WebsiteWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@ import { DropdownButton } from 'study/participants/survey/SurveyResponseView'
import { useNavigate } from 'react-router-dom'
import { siteContentPath, siteMediaPath } from 'portal/PortalRouter'
import { InfoCard, InfoCardBody, InfoCardHeader } from 'components/InfoCard'
import {
studyEnvSiteContentPath,
studyEnvSiteMediaPath,
useStudyEnvParamsFromPath
} from '../../../study/StudyEnvironmentRouter'

export const WebsiteWidget = ({ portal }: { portal: Portal }) => {
const livePortalUrl = portal.portalEnvironments.find(env =>
env.environmentName === 'live')?.portalEnvironmentConfig.participantHostname
const { studyShortcode } = useStudyEnvParamsFromPath()

return (
<InfoCard>
Expand All @@ -26,7 +32,7 @@ export const WebsiteWidget = ({ portal }: { portal: Portal }) => {
variant="light" className="border m-1">
<FontAwesomeIcon icon={faEye} className="fa-lg"/> Preview
</Button>
<CustomizeWebsiteDropdown portal={portal}/>
<CustomizeWebsiteDropdown portal={portal} studyShortcode={studyShortcode}/>
</div>
</div>
</InfoCardHeader>
Expand Down Expand Up @@ -55,7 +61,7 @@ export const WebsiteWidget = ({ portal }: { portal: Portal }) => {
)
}

const CustomizeWebsiteDropdown = ({ portal }: { portal: Portal }) => {
const CustomizeWebsiteDropdown = ({ portal, studyShortcode }: { portal: Portal, studyShortcode?: string }) => {
const navigate = useNavigate()

return (
Expand All @@ -74,14 +80,23 @@ const CustomizeWebsiteDropdown = ({ portal }: { portal: Portal }) => {
</Button>
<div className="dropdown-menu" aria-labelledby="customizeWebsiteMenu">
<DropdownButton
onClick={() => navigate(siteContentPath(portal.shortcode, 'sandbox'))}
onClick={() => navigate(studyShortcode ?
studyEnvSiteContentPath({
portalShortcode: portal.shortcode, studyShortcode, envName: 'sandbox'
}) :
siteContentPath(portal.shortcode, 'sandbox'))}
icon={faPencil}
label="Edit website"
description="Design your portal website"
/>
<div className="dropdown-divider my-1"></div>
<DropdownButton
onClick={() => navigate(siteMediaPath(portal.shortcode, 'sandbox'))}
onClick={() => navigate(studyShortcode ?
studyEnvSiteMediaPath({
portalShortcode: portal.shortcode, studyShortcode, envName: 'sandbox'
}) :
siteMediaPath(portal.shortcode, 'sandbox')
)}
icon={faImage}
label="Manage media"
description="Add or update images and files"
Expand Down
42 changes: 37 additions & 5 deletions ui-admin/src/study/StudyEnvironmentRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import PreRegView from './surveys/PreRegView'
import {
ApiProvider,
I18nProvider,
StudyEnvParams
StudyEnvParams,
OptionalStudyEnvParams, ENVIRONMENT_NAMES, EnvironmentName
} from '@juniper/ui-core'
import DashboardSettings from 'dashboard/DashboardSettings'
import { previewApi } from 'util/apiContextUtils'
Expand All @@ -52,6 +53,10 @@ import ExportIntegrationView from './export/integrations/ExportIntegrationView'
import ExportIntegrationJobList from './export/integrations/ExportIntegrationJobList'
import LoadedSettingsView from './settings/SettingsView'
import { ENVIRONMENT_ICON_MAP } from 'util/publishUtils'
import SiteContentLoader from '../portal/siteContent/SiteContentLoader'
import PortalDashboard from '../portal/dashboard/PortalDashboard'
import { mailingListPath } from '../portal/PortalRouter'
import { PortalAdminUserRouter } from '../user/AdminUserRouter'

export type StudyEnvContextT = { study: Study, currentEnv: StudyEnvironment, currentEnvPath: string, portal: Portal }

Expand Down Expand Up @@ -92,6 +97,7 @@ function StudyEnvironmentRouter({ study }: { study: Study }) {
const portalEnvContext = {
...portalContext, portalEnv
}
const studyEnvParams = paramsFromContext(studyEnvContext)

return <div className="StudyView d-flex flex-column flex-grow-1" key={studyEnvContext.currentEnvPath}>
<NavBreadcrumb value={currentEnvPath}>
Expand All @@ -110,13 +116,18 @@ function StudyEnvironmentRouter({ study }: { study: Study }) {
<ApiProvider api={previewApi(portal.shortcode, currentEnv.environmentName)}>
<I18nProvider defaultLanguage={'en'} portalShortcode={portal.shortcode}>
<Routes>
<Route path="portalDashboard" element={<PortalDashboard portal={portalContext.portal}/>}/>
<Route path="users/*" element={<PortalAdminUserRouter portal={portal} studyEnvParams={studyEnvParams}/>}/>
<Route path="notificationContent/*" element={<TriggerList studyEnvContext={studyEnvContext}
portalContext={portalContext}/>}/>
<Route path="participants/*" element={<ParticipantsRouter studyEnvContext={studyEnvContext}/>}/>
<Route path="families/*" element={<FamilyRouter studyEnvContext={studyEnvContext}/>}/>
<Route path="kits/scan" element={<KitScanner studyEnvContext={studyEnvContext}/>}/>
<Route path="kits/*" element={<KitsRouter studyEnvContext={studyEnvContext}/>}/>
<Route path="siteContent" element={<SiteContentLoader portalEnvContext={portalEnvContext}/>}/>
<Route path="media" element={<SiteMediaList portalContext={portalContext} portalEnv={portalEnv}/>}/>
<Route path="mailingList" element={<MailingListView portalContext={portalContext}
portalEnv={portalEnv}/>}/>
<Route path="alerts" element={<DashboardSettings currentEnv={portalEnv}
portalContext={portalContext}/>}/>
<Route path="metrics" element={<StudyEnvMetricsView studyEnvContext={studyEnvContext}/>}/>
Expand Down Expand Up @@ -177,12 +188,16 @@ export const paramsFromContext = (studyEnvContext: StudyEnvContextT): StudyEnvPa

/** gets the current study environment from the url. It's up to the caller to handle if any of the params are
* not present. If the caller knows the params will be there, the return can be cast to StudyEnvParams */
export const useStudyEnvParamsFromPath = () => {
export const useStudyEnvParamsFromPath = (): OptionalStudyEnvParams => {
const params = useParams<StudyParams & PortalParams>()
const envName = params.studyEnv
if (envName && !ENVIRONMENT_NAMES.includes(envName as EnvironmentName)) {
throw new Error(`invalid environment name in url: ${envName}`)
}
return {
studyShortcode: params.studyShortcode,
portalShortcode: params.portalShortcode,
envName: params.studyEnv
portalShortcode: params.portalShortcode!,
envName: params.studyEnv as EnvironmentName
}
}

Expand Down Expand Up @@ -295,7 +310,24 @@ export const portalPublishHistoryPath = (portalShortcode: string, studyShortcode
return `/${portalShortcode}/studies/${studyShortcode}/publishing/history`
}

const baseStudyEnvPath = (params: StudyEnvParams) => {
/** below are duplicates of portal-level routes, but with the current study preserved */
export const studyEnvMailingListPath = (studyEnvParams: OptionalStudyEnvParams) => {
if (!studyEnvParams.studyShortcode || !studyEnvParams.envName) {
return `/${mailingListPath(studyEnvParams.portalShortcode, 'live')}`
}
return `${baseStudyEnvPath(studyEnvParams)}/mailingList`
}

export const studyEnvSiteContentPath = (studyEnvParams: StudyEnvParams) => {
return `${baseStudyEnvPath(studyEnvParams)}/siteContent`
}

export const studyEnvSiteMediaPath = (studyEnvParams: StudyEnvParams) => {
return `${baseStudyEnvPath(studyEnvParams)}/media`
}


export const baseStudyEnvPath = (params: StudyEnvParams) => {
return `${studyEnvPath(params.portalShortcode,
params.studyShortcode,
params.envName)}`
Expand Down
Loading

0 comments on commit 360bdde

Please sign in to comment.