From 50ed96ff7377386faca081b201c6a4b02a9cb301 Mon Sep 17 00:00:00 2001 From: Bill Wallace Date: Wed, 5 Apr 2023 12:59:56 -0400 Subject: [PATCH] feat(URL): add param for initial series and sop uids to display (#3265) * feat: Allow navigating to a specified series and sop instance This was a feature in OHIF v2, so adding it to v3, albeit with new parameters. feat: Allow comma separated as well as repeated args params * docs * Test fixes * feat: Navigate to SOP selected - PR fixes * Updated docs * PR fixes --- .../default/src/DicomWebDataSource/index.js | 8 +-- ...taticWadoClient.js => StaticWadoClient.ts} | 51 ++++++++++++------- .../default/src/getHangingProtocolModule.js | 21 ++++++-- modes/basic-test-mode/src/index.js | 1 - platform/core/src/classes/ImageSet.js | 6 ++- .../HangingProtocolService.ts | 45 ++++++++++++++++ .../isDisplaySetFromUrl.ts | 46 +++++++++++++++++ platform/core/src/types/HangingProtocol.ts | 22 +++++--- platform/core/src/utils/index.js | 5 ++ platform/core/src/utils/index.test.js | 2 + platform/core/src/utils/splitComma.ts | 31 +++++++++++ platform/docs/docs/configuration/url.md | 39 ++++++++++++-- .../services/data/HangingProtocolService.md | 18 ++++++- .../customization/HangingProtocol.spec.js | 23 ++++++++- .../viewer/src/components/ViewportGrid.tsx | 7 +-- platform/viewer/src/routes/Mode/Mode.tsx | 6 ++- 16 files changed, 284 insertions(+), 47 deletions(-) rename extensions/default/src/DicomWebDataSource/utils/{StaticWadoClient.js => StaticWadoClient.ts} (79%) create mode 100644 platform/core/src/services/HangingProtocolService/isDisplaySetFromUrl.ts create mode 100644 platform/core/src/utils/splitComma.ts diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index 1af67936d..3534c727d 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -22,8 +22,8 @@ import { retrieveStudyMetadata, deleteStudyMetadataPromise, } from './retrieveStudyMetadata.js'; -import StaticWadoClient from './utils/StaticWadoClient.js'; -import getDirectURL from '../utils/getDirectURL.js'; +import StaticWadoClient from './utils/StaticWadoClient'; +import getDirectURL from '../utils/getDirectURL'; const { DicomMetaDictionary, DicomDict } = dcmjs.data; @@ -90,7 +90,9 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { const implementation = { initialize: ({ params, query }) => { const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params; - const queryStudyInstanceUIDs = query.getAll('StudyInstanceUIDs'); + const queryStudyInstanceUIDs = utils.splitComma( + query.getAll('StudyInstanceUIDs') + ); const StudyInstanceUIDs = (queryStudyInstanceUIDs.length && queryStudyInstanceUIDs) || diff --git a/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.js b/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.ts similarity index 79% rename from extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.js rename to extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.ts index afe645cc2..9521e379b 100644 --- a/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.js +++ b/extensions/default/src/DicomWebDataSource/utils/StaticWadoClient.ts @@ -9,18 +9,19 @@ import { api } from 'dicomweb-client'; */ export default class StaticWadoClient extends api.DICOMwebClient { static studyFilterKeys = { - StudyInstanceUID: '0020000D', - PatientName: '00100010', + studyinstanceuid: '0020000D', + patientname: '00100010', '00100020': 'mrn', - StudyDescription: '00081030', - StudyDate: '00080020', - ModalitiesInStudy: '00080061', - AccessionNumber: '00080050', + studydescription: '00081030', + studydate: '00080020', + modalitiesinstudy: '00080061', + accessionnumber: '00080050', }; static seriesFilterKeys = { - SeriesInstanceUID: '0020000E', - SeriesNumber: '00200011', + seriesinstanceuid: '0020000E', + seriesnumber: '00200011', + modality: '00080060', }; constructor(qidoConfig) { @@ -37,15 +38,18 @@ export default class StaticWadoClient extends api.DICOMwebClient { async searchForStudies(options) { if (!this.staticWado) return super.searchForStudies(options); - let searchResult = await super.searchForStudies(options); + const searchResult = await super.searchForStudies(options); const { queryParams } = options; + if (!queryParams) return searchResult; + + const lowerParams = this.toLowerParams(queryParams); const filtered = searchResult.filter(study => { for (const key of Object.keys(StaticWadoClient.studyFilterKeys)) { if ( !this.filterItem( key, - queryParams, + lowerParams, study, StaticWadoClient.studyFilterKeys ) @@ -61,16 +65,18 @@ export default class StaticWadoClient extends api.DICOMwebClient { async searchForSeries(options) { if (!this.staticWado) return super.searchForSeries(options); - let searchResult = await super.searchForSeries(options); + const searchResult = await super.searchForSeries(options); const { queryParams } = options; if (!queryParams) return searchResult; - const filtered = searchResult.filter(study => { + const lowerParams = this.toLowerParams(queryParams); + + const filtered = searchResult.filter(series => { for (const key of Object.keys(StaticWadoClient.seriesFilterKeys)) { if ( !this.filterItem( key, - queryParams, - study, + lowerParams, + series, StaticWadoClient.seriesFilterKeys ) ) { @@ -136,13 +142,13 @@ export default class StaticWadoClient extends api.DICOMwebClient { /** * Filters the return list by the query parameters. * - * @param {*} key - * @param {*} queryParams + * @param anyCaseKey - a possible search key + * @param queryParams - * @param {*} study * @param {*} sourceFilterMap * @returns */ - filterItem(key, queryParams, study, sourceFilterMap) { + filterItem(key: string, queryParams, study, sourceFilterMap) { const altKey = sourceFilterMap[key] || key; if (!queryParams) return true; const testValue = queryParams[key] || queryParams[altKey]; @@ -153,6 +159,15 @@ export default class StaticWadoClient extends api.DICOMwebClient { return this.compareDateRange(testValue, valueElem.Value[0]); } const value = valueElem.Value; - return this.compareValues(testValue, value) && true; + return this.compareValues(testValue, value); + } + + /** Converts the query parameters to lower case query parameters */ + toLowerParams(queryParams: Record): Record { + const lowerParams = {}; + Object.entries(queryParams).forEach(([key, value]) => { + lowerParams[key.toLowerCase()] = value; + }); + return lowerParams; } } diff --git a/extensions/default/src/getHangingProtocolModule.js b/extensions/default/src/getHangingProtocolModule.js index a975a8968..b575d9086 100644 --- a/extensions/default/src/getHangingProtocolModule.js +++ b/extensions/default/src/getHangingProtocolModule.js @@ -7,7 +7,7 @@ const defaultProtocol = { hasUpdatedPriorsInformation: false, name: 'Default', createdDate: '2021-02-23T19:22:08.894Z', - modifiedDate: '2021-02-23T19:22:08.894Z', + modifiedDate: '2023-04-01', availableTo: {}, editableBy: {}, protocolMatchingRules: [], @@ -33,8 +33,6 @@ const defaultProtocol = { }, displaySetSelectors: { defaultDisplaySetId: { - // Unused currently - imageMatchingRules: [], // Matches displaysets, NOT series seriesMatchingRules: [ // Try to match series with images by default, to prevent weird display @@ -45,6 +43,15 @@ const defaultProtocol = { greaterThan: { value: 0 }, }, }, + // This display set will select the specified items by preference + // It has no affect if nothing is specified in the URL. + { + attribute: 'isDisplaySetFromUrl', + weight: 10, + constraint: { + equals: true, + }, + }, ], // Can be used to select matching studies // studyMatchingRules: [], @@ -65,7 +72,13 @@ const defaultProtocol = { viewportOptions: { viewportType: 'stack', toolGroupId: 'default', - // initialImageOptions: { + // This will specify the initial image options index if it matches in the URL + // and will otherwise not specify anything. + initialImageOptions: { + custom: 'sopInstanceLocation', + }, + // Other options for initialImageOptions, which can be included in the default + // custom attribute, or can be provided directly. // index: 180, // preset: 'middle', // 'first', 'last', 'middle' // }, diff --git a/modes/basic-test-mode/src/index.js b/modes/basic-test-mode/src/index.js index 11e06ec24..d465b7e09 100644 --- a/modes/basic-test-mode/src/index.js +++ b/modes/basic-test-mode/src/index.js @@ -133,7 +133,6 @@ function modeFactory() { const { toolGroupService, syncGroupService, - toolbarService, segmentationService, cornerstoneViewportService, } = servicesManager.services; diff --git a/platform/core/src/classes/ImageSet.js b/platform/core/src/classes/ImageSet.js index 9f4ba7074..80838c14f 100644 --- a/platform/core/src/classes/ImageSet.js +++ b/platform/core/src/classes/ImageSet.js @@ -30,6 +30,10 @@ class ImageSet { writable: false, value: guid(), // Unique ID of the instance }); + + this.instances = images; + this.instance = images[0]; + this.StudyInstanceUID = this.instance?.StudyInstanceUID; } getUID() { @@ -56,7 +60,7 @@ class ImageSet { } } - getNumImages = () => this.images.length + getNumImages = () => this.images.length; getImage(index) { return this.images[index]; diff --git a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts index b366450d9..2d06acdaf 100644 --- a/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts +++ b/platform/core/src/services/HangingProtocolService/HangingProtocolService.ts @@ -6,6 +6,10 @@ import IDisplaySet from '../DisplaySetService/IDisplaySet'; import { CommandsManager } from '../../classes'; import ServicesManager from '../ServicesManager'; import * as HangingProtocol from '../../types/HangingProtocol'; +import { + isDisplaySetFromUrl, + sopInstanceLocation, +} from './isDisplaySetFromUrl'; type Protocol = HangingProtocol.Protocol | HangingProtocol.ProtocolGenerator; @@ -81,6 +85,14 @@ export default class HangingProtocolService extends PubSubService { // we can add more advanced checking here callback: displaySet => displaySet.isReconstructable ?? false, }, + isDisplaySetFromUrl: { + name: 'Checks if the display set is as specified in the URL', + callback: isDisplaySetFromUrl, + }, + sopInstanceLocation: { + name: 'Gets the position of the specified sop instance', + callback: sopInstanceLocation, + }, }; listeners = {}; registeredImageLoadStrategies = {}; @@ -648,6 +660,39 @@ export default class HangingProtocolService extends PubSubService { return viewportsToUpdate; } + /** + * Gets a computed options value, or a copy of the options + * This allows computing values such as the initial image index to use + * based on custom attribute functions, the same as the validators. + * Computing individual values is something that can be declared statically + * as long as the named functions are provided ahead of time, which is much + * simpler than recomputing the entire protocol. + */ + public getComputedOptions( + options: Record, + displaySetUIDs: string[] + ) { + const computed = { ...options }; + let displaySets; + for (const key in computed) { + const value = computed[key]; + if (!value) continue; + if (value.custom) { + if (!displaySets) { + displaySets = this.displaySets.filter( + displaySet => + displaySetUIDs.indexOf(displaySet.displaySetInstanceUID) !== -1 + ); + } + computed[key] = this.customAttributeRetrievalCallbacks[ + value.custom + ].callback.call(computed, displaySets); + if (computed[key] === undefined) computed[key] = computed.defaultValue; + } + } + return computed; + } + /** * It applied the protocol to the current studies and display sets based on the * protocolId that is provided. diff --git a/platform/core/src/services/HangingProtocolService/isDisplaySetFromUrl.ts b/platform/core/src/services/HangingProtocolService/isDisplaySetFromUrl.ts new file mode 100644 index 000000000..a271d5ecd --- /dev/null +++ b/platform/core/src/services/HangingProtocolService/isDisplaySetFromUrl.ts @@ -0,0 +1,46 @@ +import { getSplitParam } from '../../utils'; + +/** Indicates if the given display set is the one specified in the + * displaySet parameter in the URL + * The parameters are: + * initialSeriesInstanceUID + * initialSOPInstanceUID + */ +const isDisplaySetFromUrl = (displaySet): boolean => { + const params = new URLSearchParams(window.location.search); + const initialSeriesInstanceUID = getSplitParam( + 'initialseriesinstanceuid', + params + ); + const initialSOPInstanceUID = getSplitParam('initialsopinstanceuid', params); + if (!initialSeriesInstanceUID && !initialSOPInstanceUID) return false; + const isSeriesMatch = + !initialSeriesInstanceUID || + initialSeriesInstanceUID.some( + seriesUID => displaySet.SeriesInstanceUID === seriesUID + ); + const isSopMatch = + !initialSOPInstanceUID || + displaySet.instances?.some?.(instance => + initialSOPInstanceUID.some(sopUID => sopUID === instance.SOPInstanceUID) + ); + return isSeriesMatch && isSopMatch; +}; + +/** Returns the index location of the requested image, or the defaultValue in this. + * Returns undefined to fallback to the defaultValue + */ +function sopInstanceLocation(displaySets) { + const displaySet = displaySets[0]; + if (!displaySet) return; + const initialSOPInstanceUID = getSplitParam('initialsopinstanceuid'); + if (!initialSOPInstanceUID) return; + + const index = displaySet.instances.findIndex(instance => + initialSOPInstanceUID.includes(instance.SOPInstanceUID) + ); + // Need to return in the initial position specified format. + return index === -1 ? undefined : { index }; +} + +export { isDisplaySetFromUrl, sopInstanceLocation }; diff --git a/platform/core/src/types/HangingProtocol.ts b/platform/core/src/types/HangingProtocol.ts index 5cb26e20c..fec29f489 100644 --- a/platform/core/src/types/HangingProtocol.ts +++ b/platform/core/src/types/HangingProtocol.ts @@ -70,8 +70,8 @@ export type ConstraintValue = | boolean | [] | { - value: string | number | boolean | []; - }; + value: string | number | boolean | []; + }; export type Constraint = { // This value exactly @@ -135,19 +135,27 @@ export type SyncGroup = { target?: boolean; }; +/** Declares a custom option, that is a computed type value */ +export type CustomOptionAttribute = { + custom: string; + defaultValue?: T; +}; + +export type CustomOption = CustomOptionAttribute | T; + export type initialImageOptions = { index?: number; preset?: string; // todo: type more }; export type ViewportOptions = { - toolGroupId?: string; - viewportType?: string; + toolGroupId?: CustomOption; + viewportType?: CustomOption; id?: string; - orientation?: string; + orientation?: CustomOption; viewportId?: string; - initialImageOptions?: initialImageOptions; - syncGroups?: SyncGroup[]; + initialImageOptions?: CustomOption; + syncGroups?: CustomOption[]; customViewportProps?: Record; // Set to true to allow non-matching drag and drop or options provided // from options.displaySetSelectorsMap diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index 4c54c32ef..06e7537f3 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -33,6 +33,7 @@ import { seriesSortCriteria, } from './sortStudy'; import { subscribeToNextViewportGridChange } from './subscribeToNextViewportGridChange'; +import { splitComma, getSplitParam } from './splitComma'; // Commented out unused functionality. // Need to implement new mechanism for derived displaySets using the displaySetManager. @@ -71,6 +72,8 @@ const utils = { roundNumber, downloadCSVReport, subscribeToNextViewportGridChange, + splitComma, + getSplitParam, }; export { @@ -100,6 +103,8 @@ export { debounce, roundNumber, downloadCSVReport, + splitComma, + getSplitParam, }; export default utils; diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js index cae769191..b3c3e1e26 100644 --- a/platform/core/src/utils/index.test.js +++ b/platform/core/src/utils/index.test.js @@ -13,6 +13,8 @@ describe('Top level exports', () => { 'sortStudyInstances', 'sortStudySeries', 'sortingCriteria', + 'splitComma', + 'getSplitParam', 'isLowPriorityModality', 'writeScript', 'debounce', diff --git a/platform/core/src/utils/splitComma.ts b/platform/core/src/utils/splitComma.ts new file mode 100644 index 000000000..e8aab0b47 --- /dev/null +++ b/platform/core/src/utils/splitComma.ts @@ -0,0 +1,31 @@ +/** Splits a list of stirngs by commas within the strings */ +const splitComma = (strings: string[]): string[] => { + if (!strings) return null; + for (let i = 0; i < strings.length; i++) { + const comma = strings[i].indexOf(','); + if (comma !== -1) { + const splits = strings[i].split(/,/); + strings.splice(i, 1, ...splits); + } + } + return strings; +}; + +/** + * Returns an array of the comma split parameters from the given URL search params + * @param lowerCaseKey - lower case search parameter value + * @param params - URLSearchParams + * @returns Array of comma split items matching, or null + */ +const getSplitParam = ( + lowerCaseKey: string, + params = new URLSearchParams(window.location.search) +): string[] => { + return splitComma( + [...params] + .find(([key, value]) => key.toLowerCase() === lowerCaseKey && value) + ?.slice?.(1) + ); +}; + +export { splitComma, getSplitParam }; diff --git a/platform/docs/docs/configuration/url.md b/platform/docs/docs/configuration/url.md index f0e12ff36..a4ec17580 100644 --- a/platform/docs/docs/configuration/url.md +++ b/platform/docs/docs/configuration/url.md @@ -110,17 +110,46 @@ You can open more than one study in the Viewer by adding the `StudyInstanceUIDs` :::tip You can ues this feature to open a current and prior study in the Viewer. -Read more in the [Hanging Protocol Module](../platform/extensions/modules/hpModule.md#matching-on-prior-study-with-uid) section +Read more in the [Hanging Protocol Module](../platform/extensions/modules/hpModule.md#matching-on-prior-study-with-uid) section. You can also use commas to separate +values. ::: -### SeriesInstanceUIDs +### SeriesInstanceUID and initialSeriesInstanceUID -Sometimes you need to only open a specific series in a study, you can do that by +Sometimes you need to only retrieve a specific series in a study, you can do +that by providing series level QIDO query parameters in the URL such as +SeriesInstanceUID or SeriesNumber. This does NOT work with instance or study +level parameters. For example: ```js -/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095722.1&SeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125095748.1 +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1&SeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125113545.4 ``` -This will only open the viewer with one series (one displaySet). +This will only open the viewer with one series (one displaySet) loaded, and no +queries made for any other series. + +Alternatively, sometimes you want to just open the study on a specified series +and/or display a particular sop instance, which you can accomplish using: +`initialSeriesInstanceUID` and/or `initialSOPInstanceUID` +to select the series to open on, but allowing other +series to be present in the study browser panel. This is the same behaviour +as in OHIF 2.0, albeit on different URL parameters. For example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1&initialSeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125113545.4 +``` + +Note that you can combine these, if you want to load a specific set of series +plus show an initial one as the first one selected, for example: + +```js +http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1&SeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125113545.4&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125113546.1 +``` + +### hangingProtocolId + +You can select the initial hanging protocol to apply by using the +hangingProtocolId parameter. The selected parameter must be available in a +hangingProtocolModule registration, but does not have to be active. diff --git a/platform/docs/docs/platform/services/data/HangingProtocolService.md b/platform/docs/docs/platform/services/data/HangingProtocolService.md index 71dc86f49..21f615824 100644 --- a/platform/docs/docs/platform/services/data/HangingProtocolService.md +++ b/platform/docs/docs/platform/services/data/HangingProtocolService.md @@ -218,7 +218,8 @@ the display set is the same as the other viewports, but the from the display set selector which isn't already filling a view. ## Custom Attribute -In some situations, you might want to match based on a custom attribute and not the DICOM tags. For instance, +In some situations, you might want to match based on a custom +attribute and not the DICOM tags. For instance, if you have assigned a `timepointId` to each study, and you want to match based on it. Good news is that, in `OHIF-v3` you can define you custom attribute and use it for matching. @@ -294,3 +295,18 @@ function modeFactory() { }; } ``` + +### Custom Attributes for Viewport Options + +The custom attributes can also be used for viewport options. This example, +from the default hanging protocol navigates the image to the image +specified in the URL: + +```javascript +viewportOptions: { + initialImageOptions: { + // custom attribute name is selected by 'custom' + custom: 'sopInstanceLocation', + // This is the value returned if the above doesn't return anything + defaultValue: { index: 5 }, +``` diff --git a/platform/viewer/cypress/integration/customization/HangingProtocol.spec.js b/platform/viewer/cypress/integration/customization/HangingProtocol.spec.js index c7b8a8417..6d3f87384 100644 --- a/platform/viewer/cypress/integration/customization/HangingProtocol.spec.js +++ b/platform/viewer/cypress/integration/customization/HangingProtocol.spec.js @@ -1,5 +1,5 @@ describe('OHIF HP', () => { - beforeEach(() => { + const beforeSetup = () => { cy.checkStudyRouteInViewer( '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1', '&hangingProtocolId=@ohif/hp-extension.mn' @@ -7,15 +7,19 @@ describe('OHIF HP', () => { cy.expectMinimumThumbnails(3); cy.initCornerstoneToolsAliases(); cy.initCommonElementsAliases(); - }); + }; it('Should display 3 up', () => { + beforeSetup(); + cy.get('[data-cy="viewport-pane"]') .its('length') .should('be.eq', 3); }); it('Should navigate next/previous stage', () => { + beforeSetup(); + cy.get('body').type(','); cy.wait(250); cy.get('[data-cy="viewport-pane"]') @@ -28,4 +32,19 @@ describe('OHIF HP', () => { .its('length') .should('be.eq', 2); }); + + it('Should navigate to display set specified', () => { + // This filters by series instance UID, meaning there will only be 1 thumbnail + // It applies the initial SOP instance, navigating to that image + cy.checkStudyRouteInViewer( + '1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1', + '&SeriesInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125113545.4&initialSopInstanceUID=1.3.6.1.4.1.25403.345050719074.3824.20170125113546.1' + ); + cy.expectMinimumThumbnails(1); + cy.initCornerstoneToolsAliases(); + cy.initCommonElementsAliases(); + + // The specified series/sop UID's are index 101, so ensure that image is displayed + cy.get('@viewportInfoTopRight').should('contains.text', 'I:6'); + }); }); diff --git a/platform/viewer/src/components/ViewportGrid.tsx b/platform/viewer/src/components/ViewportGrid.tsx index 2eea5be09..03970597c 100644 --- a/platform/viewer/src/components/ViewportGrid.tsx +++ b/platform/viewer/src/components/ViewportGrid.tsx @@ -99,9 +99,10 @@ function ViewerViewportGrid(props) { return { displaySetInstanceUIDs: displaySetUIDsToHang, displaySetOptions: displaySetUIDsToHangOptions, - viewportOptions: { - ...viewportOptions, - }, + viewportOptions: hangingProtocolService.getComputedOptions( + viewportOptions, + displaySetUIDsToHang + ), }; }; diff --git a/platform/viewer/src/routes/Mode/Mode.tsx b/platform/viewer/src/routes/Mode/Mode.tsx index a48f05500..e0eb6d46e 100644 --- a/platform/viewer/src/routes/Mode/Mode.tsx +++ b/platform/viewer/src/routes/Mode/Mode.tsx @@ -279,8 +279,10 @@ export default function ModeRoute({ const filters = Array.from(query.keys()).reduce( (acc: Record, val: string) => { - if (val !== 'StudyInstanceUIDs') { - if (['seriesInstanceUID', 'SeriesInstanceUID'].includes(val)) { + const lowerVal = val.toLowerCase(); + if (lowerVal !== 'studyinstanceuids') { + // Not sure why the case matters here - it doesn't in the URL + if (lowerVal === 'seriesinstanceuid') { return { ...acc, seriesInstanceUID: query.get(val),