feat(toolbar): new Toolbar to enable reactive state synchronization (#3983)

This commit is contained in:
Alireza authored and GitHub committed 2024-03-27 16:01:32 -04:00
1 parent 79d5c36bda
commit 566b25a544
155 files changed
+6311 -6577

No files matched your search

@@ -72,7 +72,7 @@ describe('OHIF Cornerstone Toolbar', () => {
// Assign an alias to the button element
cy.get('@wwwcBtnPrimary').as('wwwcButton');
cy.get('@wwwcButton').click();
cy.get('@wwwcButton').should('have.class', 'active');
cy.get('@wwwcButton').should('have.class', 'bg-primary-light');
//drags the mouse inside the viewport to be able to interact with series
cy.get('@viewport')
@@ -97,7 +97,7 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('@panButton').click();
// Assert that the button has the 'active' class
cy.get('@panButton').should('have.class', 'active');
cy.get('@panButton').should('have.class', 'bg-primary-light');
// Trigger the pan actions on the viewport
cy.get('@viewport')
@@ -418,44 +418,44 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('@moreBtnSecondary').click();
//Click on Flip button
cy.get('[data-cy="flip-horizontal"]').click();
cy.get('[data-cy="flipHorizontal"]').click();
cy.waitDicomImage();
cy.get('@viewportInfoMidLeft').should('contains.text', 'L');
cy.get('@viewportInfoMidTop').should('contains.text', 'A');
});
it('checks if stack sync is preserved on new display set and uses FOR', () => {
// Active stack image sync and reference lines
cy.get('[data-cy="MoreTools-split-button-secondary"]').click();
cy.get('[data-cy="ImageSliceSync"]').click();
// Add reference lines as that sometimes throws an exception
cy.get('[data-cy="MoreTools-split-button-secondary"]').click();
cy.get('[data-cy="ReferenceLines"]').click();
// it('checks if stack sync is preserved on new display set and uses FOR', () => {
// // Active stack image sync and reference lines
// cy.get('[data-cy="MoreTools-split-button-secondary"]').click();
// cy.get('[data-cy="ImageSliceSync"]').click();
// // Add reference lines as that sometimes throws an exception
// cy.get('[data-cy="MoreTools-split-button-secondary"]').click();
// cy.get('[data-cy="ReferenceLines"]').click();
cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick();
cy.get('body').type('{downarrow}{downarrow}');
// cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick();
// cy.get('body').type('{downarrow}{downarrow}');
// Change the layout and double load the first
cy.setLayout(2, 1);
cy.get('body').type('{rightarrow}');
cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick();
cy.waitDicomImage();
// // Change the layout and double load the first
// cy.setLayout(2, 1);
// cy.get('body').type('{rightarrow}');
// cy.get('[data-cy="study-browser-thumbnail"]:nth-child(2)').dblclick();
// cy.waitDicomImage();
// Now navigate down once and check that the left hand pane navigated
cy.get('body').focus().type('{downarrow}');
// // Now navigate down once and check that the left hand pane navigated
// cy.get('body').focus().type('{downarrow}');
// The following lines assist in troubleshooting when/if this test were to fail.
cy.get('[data-cy="viewport-pane"]')
.eq(0)
.find('[data-cy="viewport-overlay-top-right"]')
.should('contains.text', 'I:2 (2/20)');
cy.get('[data-cy="viewport-pane"]')
.eq(1)
.find('[data-cy="viewport-overlay-top-right"]')
.should('contains.text', 'I:2 (2/20)');
// // The following lines assist in troubleshooting when/if this test were to fail.
// cy.get('[data-cy="viewport-pane"]')
// .eq(0)
// .find('[data-cy="viewport-overlay-top-right"]')
// .should('contains.text', 'I:2 (2/20)');
// cy.get('[data-cy="viewport-pane"]')
// .eq(1)
// .find('[data-cy="viewport-overlay-top-right"]')
// .should('contains.text', 'I:2 (2/20)');
cy.get('body').type('{leftarrow}');
cy.setLayout(1, 1);
cy.get('@viewportInfoTopRight').should('contains.text', 'I:2 (2/20)');
});
// cy.get('body').type('{leftarrow}');
// cy.setLayout(1, 1);
// cy.get('@viewportInfoTopRight').should('contains.text', 'I:2 (2/20)');
// });
});
@@ -18,7 +18,7 @@ describe('OHIF General Viewer', function () {
cy.get('@zoomBtn')
.click()
.then($zoomBtn => {
cy.wrap($zoomBtn).should('have.class', 'active');
cy.wrap($zoomBtn).should('have.class', 'bg-primary-light');
});
const zoomLevelInitial = cy.get('@viewportInfoTopLeft').then($viewportInfo => {
@@ -7,8 +7,7 @@ describe('OHIF MPR', () => {
});
it('should not go MPR for non reconstructible displaySets', () => {
cy.get('[data-cy="MPR"]').click();
cy.get('.cornerstone-canvas').should('have.length', 1);
cy.get('[data-cy="MPR"]').should('have.class', 'ohif-disabled');
});
it('should go MPR for reconstructible displaySets and come back', () => {
@@ -88,7 +87,6 @@ describe('OHIF MPR', () => {
});
it('should correctly render Crosshairs for MPR', () => {
cy.get('[data-cy="Crosshairs"]').should('not.exist');
cy.get(':nth-child(3) > [data-cy="study-browser-thumbnail"]').dblclick();
cy.get('[data-cy="MPR"]').click();
cy.get('[data-cy="Crosshairs"]').click();
@@ -125,13 +123,7 @@ describe('OHIF MPR', () => {
cy.get('[data-cy="MPR"]').click();
cy.get('[data-cy="Crosshairs"]').click();
// wait for the crosshairs tool to be active
cy.get('[data-cy="Crosshairs"].active');
// Click the crosshairs button to deactivate it.
cy.get('[data-cy="Crosshairs"]').click();
// wait for the window level button to be active
cy.get('[data-cy="WindowLevel-split-button-primary"].active');
});
});
+1 -1
View File
@@ -273,7 +273,7 @@ Cypress.Commands.add(
}
});
cy.get('@lengthButton').should('have.class', 'active');
cy.get('@lengthButton').should('have.class', 'bg-primary-light');
cy.get('@viewport').then($viewport => {
const [x1, y1] = firstClick;
+3
View File
@@ -10,6 +10,9 @@ window.config = {
strictZSpacingForVolumeViewport: true,
// filterQueryParam: false,
defaultDataSourceName: 'e2e',
investigationalUseDialog: {
option: 'never',
},
dataSources: [
{
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
+2
View File
@@ -16,6 +16,7 @@ import {
ViewportGridProvider,
CineProvider,
UserAuthenticationProvider,
ToolboxProvider,
} from '@ohif/ui';
// Viewer Project
// TODO: Should this influence study list?
@@ -69,6 +70,7 @@ function App({ config, defaultExtensions, defaultModes }) {
[UserAuthenticationProvider, { service: userAuthenticationService }],
[I18nextProvider, { i18n }],
[ThemeWrapper],
[ToolboxProvider],
[ViewportGridProvider, { service: viewportGridService }],
[ViewportDialogProvider, { service: uiViewportDialogService }],
[CineProvider, { service: cineService }],
+2
View File
@@ -49,6 +49,8 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
appConfig,
});
servicesManager.setExtensionManager(extensionManager);
servicesManager.registerServices([
UINotificationService.REGISTRATION,
UIModalService.REGISTRATION,
+17 -2
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useCallback, useRef } from 'react';
import React, { useEffect, useCallback, useRef, useState } from 'react';
import ReactResizeDetector from 'react-resize-detector';
import PropTypes from 'prop-types';
import { ServicesManager, Types, MeasurementService } from '@ohif/core';
@@ -15,12 +15,15 @@ function ViewerViewportGrid(props) {
const { layout, activeViewportId, viewports } = viewportGrid;
const { numCols, numRows } = layout;
const elementRef = useRef(null);
const layoutHash = useRef(null);
// TODO -> Need some way of selecting which displaySets hit the viewports.
const { displaySetService, measurementService, hangingProtocolService, uiNotificationService } = (
servicesManager as ServicesManager
).services;
const generateLayoutHash = () => `${numCols}-${numRows}`;
/**
* This callback runs after the viewports structure has changed in any way.
* On initial display, that means if it has changed by applying a HangingProtocol,
@@ -135,6 +138,16 @@ function ViewerViewportGrid(props) {
};
}, []);
// Check viewport readiness in useEffect
useEffect(() => {
const allReady = viewportGridService.getGridViewportsReady();
const sameLayoutHash = layoutHash.current === generateLayoutHash();
if (allReady && !sameLayoutHash) {
layoutHash.current = generateLayoutHash();
viewportGridService.publishViewportsReady();
}
}, [viewportGridService, generateLayoutHash]);
useEffect(() => {
const { unsubscribe } = measurementService.subscribe(
MeasurementService.EVENTS.JUMP_TO_MEASUREMENT_LAYOUT,
@@ -327,6 +340,9 @@ function ViewerViewportGrid(props) {
viewportOptions={viewportOptions}
displaySetOptions={displaySetOptions}
needsRerendering={displaySetsNeedsRerendering}
onReady={() => {
viewportGridService.setViewportIsReady(viewportId, true);
}}
/>
</div>
</ViewportPane>
@@ -360,7 +376,6 @@ function ViewerViewportGrid(props) {
}}
targetRef={elementRef.current}
/>
{/* {ViewportPanes} */}
{getViewportPanes()}
</ViewportGrid>
</div>
+67 -196
View File
@@ -1,141 +1,18 @@
import React, { useEffect, useState, useRef } from 'react';
import React, { useEffect, useState, useRef, useContext, createContext } from 'react';
import { useParams, useLocation, useNavigate } from 'react-router';
import PropTypes from 'prop-types';
// TODO: DicomMetadataStore should be injected?
import { DicomMetadataStore, ServicesManager, utils, log, Enums } from '@ohif/core';
import { ServicesManager, utils } from '@ohif/core';
import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui';
import { useSearchParams } from '@hooks';
import { useAppConfig } from '@state';
import ViewportGrid from '@components/ViewportGrid';
import Compose from './Compose';
import getStudies from './studiesList';
import { history } from '../../utils/history';
import loadModules from '../../pluginImports';
import isSeriesFilterUsed from '../../utils/isSeriesFilterUsed';
import { defaultRouteInit } from './defaultRouteInit';
import { updateAuthServiceAndCleanUrl } from './updateAuthServiceAndCleanUrl';
const { getSplitParam, sortingCriteria } = utils;
/**
* Initialize the route.
*
* @param props.servicesManager to read services from
* @param props.studyInstanceUIDs for a list of studies to read
* @param props.dataSource to read the data from
* @param props.filters filters from query params to read the data from
* @returns array of subscriptions to cancel
*/
function defaultRouteInit(
{ servicesManager, studyInstanceUIDs, dataSource, filters, appConfig },
hangingProtocolId
) {
const { displaySetService, hangingProtocolService, uiNotificationService, customizationService } =
servicesManager.services;
/**
* Function to apply the hanging protocol when the minimum number of display sets were
* received or all display sets retrieval were completed
* @returns
*/
function applyHangingProtocol() {
const displaySets = displaySetService.getActiveDisplaySets();
if (!displaySets || !displaySets.length) {
return;
}
// Gets the studies list to use
const studies = getStudies(studyInstanceUIDs, displaySets);
// study being displayed, and is thus the "active" study.
const activeStudy = studies[0];
// run the hanging protocol matching on the displaySets with the predefined
// hanging protocol in the mode configuration
hangingProtocolService.run({ studies, activeStudy, displaySets }, hangingProtocolId);
}
const unsubscriptions = [];
const issuedWarningSeries = [];
const { unsubscribe: instanceAddedUnsubscribe } = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
function ({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) {
const seriesMetadata = DicomMetadataStore.getSeries(StudyInstanceUID, SeriesInstanceUID);
// checks if the series filter was used, if it exists
const seriesInstanceUIDs = filters?.seriesInstanceUID;
if (
seriesInstanceUIDs?.length &&
!isSeriesFilterUsed(seriesMetadata.instances, filters) &&
!issuedWarningSeries.includes(seriesInstanceUIDs[0])
) {
// stores the series instance filter so it shows only once the warning
issuedWarningSeries.push(seriesInstanceUIDs[0]);
uiNotificationService.show({
title: 'Series filter',
message: `Each of the series in filter: ${seriesInstanceUIDs} are not part of the current study. The entire study is being displayed`,
type: 'error',
duration: 7000,
});
}
displaySetService.makeDisplaySets(seriesMetadata.instances, madeInClient);
}
);
unsubscriptions.push(instanceAddedUnsubscribe);
log.time(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS);
log.time(Enums.TimingEnum.STUDY_TO_FIRST_IMAGE);
const allRetrieves = studyInstanceUIDs.map(StudyInstanceUID =>
dataSource.retrieve.series.metadata({
StudyInstanceUID,
filters,
returnPromises: true,
sortCriteria:
customizationService.get('sortingCriteria') ||
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria,
})
);
// log the error if this fails, otherwise it's so difficult to tell what went wrong...
allRetrieves.forEach(retrieve => {
retrieve.catch(error => {
console.error(error);
});
});
Promise.allSettled(allRetrieves).then(promises => {
log.timeEnd(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS);
log.time(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);
log.time(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES);
const allPromises = [];
const remainingPromises = [];
function startRemainingPromises(remainingPromises) {
remainingPromises.forEach(p => p.forEach(p => p.start()));
}
promises.forEach(promise => {
const retrieveSeriesMetadataPromise = promise.value;
if (Array.isArray(retrieveSeriesMetadataPromise)) {
const { requiredSeries, remaining } = hangingProtocolService.filterSeriesRequiredForRun(
hangingProtocolId,
retrieveSeriesMetadataPromise
);
const requiredSeriesPromises = requiredSeries.map(promise => promise.start());
allPromises.push(Promise.allSettled(requiredSeriesPromises));
remainingPromises.push(remaining);
}
});
Promise.allSettled(allPromises).then(applyHangingProtocol);
startRemainingPromises(remainingPromises);
applyHangingProtocol();
});
return unsubscriptions;
}
const { getSplitParam } = utils;
export default function ModeRoute({
mode,
@@ -166,7 +43,7 @@ export default function ModeRoute({
// The URL's query search parameters where the keys are all lower case.
const lowerCaseSearchParams = useSearchParams({ lowerCaseKeys: true });
const [studyInstanceUIDs, setStudyInstanceUIDs] = useState();
const [studyInstanceUIDs, setStudyInstanceUIDs] = useState(null);
const [refresh, setRefresh] = useState(false);
const [ExtensionDependenciesLoaded, setExtensionDependenciesLoaded] = useState(false);
@@ -193,25 +70,7 @@ export default function ModeRoute({
const token = lowerCaseSearchParams.get('token');
if (token) {
// if a token is passed in, set the userAuthenticationService to use it
// for the Authorization header for all requests
userAuthenticationService.setServiceImplementation({
getAuthorizationHeader: () => ({
Authorization: 'Bearer ' + token,
}),
});
// Create a URL object with the current location
const urlObj = new URL(window.location.origin + window.location.pathname + location.search);
// Remove the token from the URL object
urlObj.searchParams.delete('token');
const cleanUrl = urlObj.toString();
// Update the browser's history without the token
if (window.history && window.history.replaceState) {
window.history.replaceState(null, '', cleanUrl);
}
updateAuthServiceAndCleanUrl(token, location, userAuthenticationService);
}
// Preserve the old array interface for hotkeys
@@ -228,32 +87,6 @@ export default function ModeRoute({
// Only handling one route per mode for now
const route = mode.routes[0];
// For each extension, look up their context modules
// TODO: move to extension manager.
let contextModules = [];
Object.keys(extensions).forEach(extensionId => {
const allRegisteredModuleIds = Object.keys(extensionManager.modulesMap);
const moduleIds = allRegisteredModuleIds.filter(id =>
id.includes(`${extensionId}.contextModule.`)
);
if (!moduleIds || !moduleIds.length) {
return;
}
const modules = moduleIds.map(extensionManager.getModuleEntry);
contextModules = contextModules.concat(modules);
});
const contextModuleProviders = contextModules.map(a => a.provider);
const CombinedContextProvider = ({ children }) =>
Compose({ components: contextModuleProviders, children });
function ViewportGridWithDataSource(props) {
return ViewportGrid({ ...props, dataSource });
}
useEffect(() => {
const loadExtensions = async () => {
const loadedExtensions = await loadModules(Object.keys(extensions));
@@ -298,7 +131,7 @@ export default function ModeRoute({
}, [location, ExtensionDependenciesLoaded]);
useEffect(() => {
if (!ExtensionDependenciesLoaded) {
if (!ExtensionDependenciesLoaded || !studyInstanceUIDs?.length) {
return;
}
@@ -313,7 +146,7 @@ export default function ModeRoute({
setRefresh(!refresh);
}
};
if (studyInstanceUIDs?.length && studyInstanceUIDs[0] !== undefined) {
if (Array.isArray(studyInstanceUIDs) && studyInstanceUIDs[0]) {
retrieveLayoutData();
}
return () => {
@@ -322,7 +155,7 @@ export default function ModeRoute({
}, [studyInstanceUIDs, ExtensionDependenciesLoaded]);
useEffect(() => {
if (!hotkeys || !ExtensionDependenciesLoaded) {
if (!hotkeys || !ExtensionDependenciesLoaded || !studyInstanceUIDs?.length) {
return;
}
@@ -339,10 +172,10 @@ export default function ModeRoute({
return () => {
hotkeysManager.destroy();
};
}, [ExtensionDependenciesLoaded]);
}, [ExtensionDependenciesLoaded, hotkeys, studyInstanceUIDs]);
useEffect(() => {
if (!layoutTemplateData.current || !ExtensionDependenciesLoaded) {
if (!layoutTemplateData.current || !ExtensionDependenciesLoaded || !studyInstanceUIDs?.length) {
return;
}
@@ -477,7 +310,21 @@ export default function ModeRoute({
refresh,
]);
const renderLayoutData = props => {
if (!studyInstanceUIDs || !layoutTemplateData.current || !ExtensionDependenciesLoaded) {
return null;
}
const ViewportGridWithDataSource = props => {
return ViewportGrid({ ...props, dataSource });
};
const CombinedExtensionsContextProvider = createCombinedContextProvider(
extensionManager,
servicesManager,
commandsManager
);
const getLayoutComponent = props => {
const layoutTemplateModuleEntry = extensionManager.getModuleEntry(
layoutTemplateData.current.id
);
@@ -486,27 +333,51 @@ export default function ModeRoute({
return <LayoutComponent {...props} />;
};
const LayoutComponent = getLayoutComponent({
...layoutTemplateData.current.props,
ViewportGridComp: ViewportGridWithDataSource,
});
return (
<ImageViewerProvider
// initialState={{ StudyInstanceUIDs: StudyInstanceUIDs }}
StudyInstanceUIDs={studyInstanceUIDs}
// reducer={reducer}
>
<CombinedContextProvider>
<DragAndDropProvider>
{layoutTemplateData.current &&
studyInstanceUIDs?.[0] !== undefined &&
ExtensionDependenciesLoaded &&
renderLayoutData({
...layoutTemplateData.current.props,
ViewportGridComp: ViewportGridWithDataSource,
})}
</DragAndDropProvider>
</CombinedContextProvider>
<ImageViewerProvider StudyInstanceUIDs={studyInstanceUIDs}>
{CombinedExtensionsContextProvider ? (
<CombinedExtensionsContextProvider>
<DragAndDropProvider>{LayoutComponent}</DragAndDropProvider>
</CombinedExtensionsContextProvider>
) : (
<DragAndDropProvider>{LayoutComponent}</DragAndDropProvider>
)}
</ImageViewerProvider>
);
}
/**
* Creates a combined context provider using the context modules from the extension manager.
* @param {object} extensionManager - The extension manager instance.
* @param {object} servicesManager - The services manager instance.
* @param {object} commandsManager - The commands manager instance.
* @returns {React.Component} - A React component that provides combined contexts to its children.
*/
function createCombinedContextProvider(extensionManager, servicesManager, commandsManager) {
const extensionsContextModules = extensionManager.getModulesByType(
extensionManager.constructor.MODULE_TYPES.CONTEXT
);
if (!extensionsContextModules?.length) {
return;
}
const contextModuleProviders = extensionsContextModules.flatMap(({ module }) => {
return module.map(aContextModule => {
return aContextModule.provider;
});
});
return ({ children }) => {
return Compose({ components: contextModuleProviders, children });
};
}
ModeRoute.propTypes = {
mode: PropTypes.object.isRequired,
dataSourceName: PropTypes.string,
@@ -0,0 +1,129 @@
import getStudies from './studiesList';
import { DicomMetadataStore, log } from '@ohif/core';
import isSeriesFilterUsed from '../../utils/isSeriesFilterUsed';
import { utils, Enums } from '@ohif/core';
const { sortingCriteria } = utils;
/**
* Initialize the route.
*
* @param props.servicesManager to read services from
* @param props.studyInstanceUIDs for a list of studies to read
* @param props.dataSource to read the data from
* @param props.filters filters from query params to read the data from
* @returns array of subscriptions to cancel
*/
export function defaultRouteInit(
{ servicesManager, studyInstanceUIDs, dataSource, filters, appConfig },
hangingProtocolId
) {
const { displaySetService, hangingProtocolService, uiNotificationService, customizationService } =
servicesManager.services;
/**
* Function to apply the hanging protocol when the minimum number of display sets were
* received or all display sets retrieval were completed
* @returns
*/
function applyHangingProtocol() {
const displaySets = displaySetService.getActiveDisplaySets();
if (!displaySets || !displaySets.length) {
return;
}
// Gets the studies list to use
const studies = getStudies(studyInstanceUIDs, displaySets);
// study being displayed, and is thus the "active" study.
const activeStudy = studies[0];
// run the hanging protocol matching on the displaySets with the predefined
// hanging protocol in the mode configuration
hangingProtocolService.run({ studies, activeStudy, displaySets }, hangingProtocolId);
}
const unsubscriptions = [];
const issuedWarningSeries = [];
const { unsubscribe: instanceAddedUnsubscribe } = DicomMetadataStore.subscribe(
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
function ({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) {
const seriesMetadata = DicomMetadataStore.getSeries(StudyInstanceUID, SeriesInstanceUID);
// checks if the series filter was used, if it exists
const seriesInstanceUIDs = filters?.seriesInstanceUID;
if (
seriesInstanceUIDs?.length &&
!isSeriesFilterUsed(seriesMetadata.instances, filters) &&
!issuedWarningSeries.includes(seriesInstanceUIDs[0])
) {
// stores the series instance filter so it shows only once the warning
issuedWarningSeries.push(seriesInstanceUIDs[0]);
uiNotificationService.show({
title: 'Series filter',
message: `Each of the series in filter: ${seriesInstanceUIDs} are not part of the current study. The entire study is being displayed`,
type: 'error',
duration: 7000,
});
}
displaySetService.makeDisplaySets(seriesMetadata.instances, madeInClient);
}
);
unsubscriptions.push(instanceAddedUnsubscribe);
log.time(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS);
log.time(Enums.TimingEnum.STUDY_TO_FIRST_IMAGE);
const allRetrieves = studyInstanceUIDs.map(StudyInstanceUID =>
dataSource.retrieve.series.metadata({
StudyInstanceUID,
filters,
returnPromises: true,
sortCriteria:
customizationService.get('sortingCriteria') ||
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria,
})
);
// log the error if this fails, otherwise it's so difficult to tell what went wrong...
allRetrieves.forEach(retrieve => {
retrieve.catch(error => {
console.error(error);
});
});
Promise.allSettled(allRetrieves).then(promises => {
log.timeEnd(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS);
log.time(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);
log.time(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES);
const allPromises = [];
const remainingPromises = [];
function startRemainingPromises(remainingPromises) {
remainingPromises.forEach(p => p.forEach(p => p.start()));
}
promises.forEach(promise => {
const retrieveSeriesMetadataPromise = promise.value;
if (Array.isArray(retrieveSeriesMetadataPromise)) {
const { requiredSeries, remaining } = hangingProtocolService.filterSeriesRequiredForRun(
hangingProtocolId,
retrieveSeriesMetadataPromise
);
const requiredSeriesPromises = requiredSeries.map(promise => promise.start());
allPromises.push(Promise.allSettled(requiredSeriesPromises));
remainingPromises.push(remaining);
}
});
Promise.allSettled(allPromises).then(applyHangingProtocol);
startRemainingPromises(remainingPromises);
applyHangingProtocol();
});
return unsubscriptions;
}
@@ -0,0 +1,35 @@
/**
* Updates the user authentication service with the provided token and cleans the token from the URL.
* @param token - The token to set in the user authentication service.
* @param location - The location object from the router.
* @param userAuthenticationService - The user authentication service instance.
*/
export function updateAuthServiceAndCleanUrl(
token: string,
location: any,
userAuthenticationService: any
): void {
if (!token) {
return;
}
// if a token is passed in, set the userAuthenticationService to use it
// for the Authorization header for all requests
userAuthenticationService.setServiceImplementation({
getAuthorizationHeader: () => ({
Authorization: 'Bearer ' + token,
}),
});
// Create a URL object with the current location
const urlObj = new URL(window.location.origin + window.location.pathname + location.search);
// Remove the token from the URL object
urlObj.searchParams.delete('token');
const cleanUrl = urlObj.toString();
// Update the browser's history without the token
if (window.history && window.history.replaceState) {
window.history.replaceState(null, '', cleanUrl);
}
}
@@ -419,7 +419,7 @@ function WorkList({
/>
} // launch-arrow | launch-info
onClick={() => {}}
data-cy={`mode-${mode.routeName}-${studyInstanceUid}`}
dataCY={`mode-${mode.routeName}-${studyInstanceUid}`}
className={isValidMode ? 'text-[13px]' : 'bg-[#222d44] text-[13px]'}
>
{mode.displayName}