feat(toolbar): new Toolbar to enable reactive state synchronization (#3983)
This commit is contained in:
1 parent
79d5c36bda
commit
566b25a544
155 files changed
+6311
-6577
No files matched your search
+32
-32
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -10,6 +10,9 @@ window.config = {
|
||||
strictZSpacingForVolumeViewport: true,
|
||||
// filterQueryParam: false,
|
||||
defaultDataSourceName: 'e2e',
|
||||
investigationalUseDialog: {
|
||||
option: 'never',
|
||||
},
|
||||
dataSources: [
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
|
||||
@@ -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 }],
|
||||
|
||||
@@ -49,6 +49,8 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
|
||||
appConfig,
|
||||
});
|
||||
|
||||
servicesManager.setExtensionManager(extensionManager);
|
||||
|
||||
servicesManager.registerServices([
|
||||
UINotificationService.REGISTRATION,
|
||||
UIModalService.REGISTRATION,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user