From 2b65892c82132be9c2c777720acf8e94c2cf0b40 Mon Sep 17 00:00:00 2001 From: Igor Octaviano Date: Mon, 22 Jun 2026 19:40:20 -0300 Subject: [PATCH] feat: abstract routing configuration and URI parameter access (#48) Introduce src/utils/routes.ts as a single source of truth for route templates, path builders, and URI parameter parsing. Previously the /studies/:studyInstanceUID/series/:seriesInstanceUID template (and its GCP DICOM store variant) was hardcoded and re-parsed by hand across App, CaseViewer, Worklist, Header, and OidcManager, making routing changes error-prone. Components now consume RoutePaths for definitions and the buildStudyPath/buildSeriesPath/buildLogoutPath builders plus parse/predicate helpers instead of ad-hoc string interpolation, split(), includes(), and regex replacement. Add unit tests for the new routing helpers. Closes #48 --- src/App.tsx | 13 +-- src/auth/OidcManager.tsx | 5 +- src/components/CaseViewer.tsx | 35 +++---- src/components/Header.tsx | 28 ++---- src/components/Worklist.tsx | 3 +- src/utils/__tests__/routes.test.ts | 115 +++++++++++++++++++++++ src/utils/routes.ts | 144 +++++++++++++++++++++++++++++ 7 files changed, 296 insertions(+), 47 deletions(-) create mode 100644 src/utils/__tests__/routes.test.ts create mode 100644 src/utils/routes.ts diff --git a/src/App.tsx b/src/App.tsx index 70f0cde4..d90f7a42 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,6 +28,7 @@ import NotificationMiddleware, { NotificationMiddlewareContext, } from './services/NotificationMiddleware' import { CustomError, errorTypes } from './utils/CustomError' +import { getProjectStorePath, isProjectsPath, RoutePaths } from './utils/routes' import { joinUrl, normalizeServerUrl } from './utils/url' function ParametrizedCaseViewer({ @@ -103,8 +104,8 @@ function _createClientMapping({ } }) } else { - if (window.location.pathname.includes('/projects/')) { - const pathname = window.location.pathname.split('/study/')[0] + if (isProjectsPath(window.location.pathname)) { + const pathname = getProjectStorePath(window.location.pathname) const pathUrl = `${gcpBaseUrl}${pathname}/dicomWeb` serverSettings.url = pathUrl } @@ -540,7 +541,7 @@ class App extends React.Component {
{ } /> @@ -593,7 +594,7 @@ class App extends React.Component { } /> @@ -623,7 +624,7 @@ class App extends React.Component { } />
{ console.info(`switch to series "${seriesInstanceUID}"`) - let urlPath = `/studies/${studyInstanceUID}/series/${seriesInstanceUID}` + let urlPath = buildSeriesPath(studyInstanceUID, seriesInstanceUID) - if (location.pathname.includes('/projects/')) { - urlPath = location.pathname - if (!location.pathname.includes('/series/')) { - urlPath += `/series/${seriesInstanceUID}` - } else { - urlPath = urlPath.replace( - /\/series\/[^/]+/, - `/series/${seriesInstanceUID}`, - ) - } + if (isProjectsPath(location.pathname)) { + urlPath = withSeriesInProjectPath(location.pathname, seriesInstanceUID) } if ( - location.pathname.includes('/series/') && + hasSeriesInPath(location.pathname) && location.search !== null && location.search !== undefined ) { @@ -259,13 +259,8 @@ function Viewer(props: ViewerProps): JSX.Element | null { * Otherwise select the first series correspondent to * the first slide contained in the study. */ - let selectedSeriesInstanceUID: string - if (location.pathname.includes('series/')) { - const seriesFragment = location.pathname.split('series/')[1] - selectedSeriesInstanceUID = seriesFragment.includes('/') - ? seriesFragment.split('/')[0] - : seriesFragment - } else { + let selectedSeriesInstanceUID = parseSeriesInstanceUID(location.pathname) + if (selectedSeriesInstanceUID === '') { selectedSeriesInstanceUID = volumeInstances[0].SeriesInstanceUID } @@ -316,7 +311,7 @@ function Viewer(props: ViewerProps): JSX.Element | null { = { container: { textAlign: 'center', @@ -213,12 +216,7 @@ class Header extends React.Component { } } const pathNorm = trimmedUrl.startsWith('/') ? trimmedUrl : `/${trimmedUrl}` - return ( - pathNorm.includes('/projects/') && - pathNorm.includes('/locations/') && - pathNorm.includes('/datasets/') && - pathNorm.includes('/dicomStores/') - ) + return isGcpDicomStorePath(pathNorm) } static handleUserMenuButtonClick(e: React.SyntheticEvent): void { @@ -367,13 +365,9 @@ class Header extends React.Component { handleDicomTagBrowserButtonClick = (): void => { const width = window.innerWidth - 200 - let seriesInstanceUID = '' - if (this.props.location.pathname.includes('series/')) { - const seriesFragment = this.props.location.pathname.split('series/')[1] - seriesInstanceUID = seriesFragment.includes('/') - ? seriesFragment.split('/')[0] - : seriesFragment - } + const seriesInstanceUID = parseSeriesInstanceUID( + this.props.location.pathname, + ) Modal.info({ title: 'DICOM Tag Browser', @@ -627,9 +621,7 @@ class Header extends React.Component { ) - const showDicomTagBrowser = DICOM_TAG_BROWSER_PATHS.some((path) => - this.props.location.pathname.includes(path), - ) + const showDicomTagBrowser = isViewerPath(this.props.location.pathname) const dicomTagBrowserButton = showDicomTagBrowser ? (