feat(actions): simplify action corner api through the toolbarService (#5033)
This commit is contained in:
1 parent
f71b88e639
commit
7c15bb8901
246 files changed
+7930
-4270
No files matched your search
+13
-1
@@ -35,7 +35,12 @@ function TrackedMeasurementsContextProvider(
|
||||
|
||||
const [viewportGrid, viewportGridService] = useViewportGrid();
|
||||
const { activeViewportId, viewports } = viewportGrid;
|
||||
const { measurementService, displaySetService, customizationService } = servicesManager.services;
|
||||
const {
|
||||
measurementService,
|
||||
displaySetService,
|
||||
customizationService,
|
||||
trackedMeasurementsService,
|
||||
} = servicesManager.services as AppTypes.Services;
|
||||
|
||||
const machineOptions = Object.assign({}, defaultOptions);
|
||||
machineOptions.actions = Object.assign({}, machineOptions.actions, {
|
||||
@@ -272,6 +277,13 @@ function TrackedMeasurementsContextProvider(
|
||||
measurementTrackingMachine
|
||||
);
|
||||
|
||||
// Update TrackedMeasurementsService when trackedSeries changes in context
|
||||
useEffect(() => {
|
||||
if (trackedMeasurements?.context?.trackedSeries && trackedMeasurementsService) {
|
||||
trackedMeasurementsService.updateTrackedSeries(trackedMeasurements.context.trackedSeries);
|
||||
}
|
||||
}, [trackedMeasurements?.context?.trackedSeries, trackedMeasurementsService]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update the state machine with the active viewport ID
|
||||
sendTrackedMeasurementsEvent('UPDATE_ACTIVE_VIEWPORT_ID', {
|
||||
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
import { hydrateStructuredReport } from '@ohif/extension-cornerstone-dicom-sr';
|
||||
import { measurementTrackingMode } from './promptBeginTracking';
|
||||
|
||||
const RESPONSE = {
|
||||
NO_NEVER: -1,
|
||||
CANCEL: 0,
|
||||
CREATE_REPORT: 1,
|
||||
ADD_SERIES: 2,
|
||||
SET_STUDY_AND_SERIES: 3,
|
||||
NO_NOT_FOR_SERIES: 4,
|
||||
HYDRATE_REPORT: 5,
|
||||
};
|
||||
|
||||
function promptHydrateStructuredReport(
|
||||
{ servicesManager, extensionManager, commandsManager, appConfig },
|
||||
ctx,
|
||||
evt
|
||||
) {
|
||||
const { uiViewportDialogService, displaySetService, customizationService } =
|
||||
servicesManager.services;
|
||||
const { viewportId, displaySetInstanceUID } = evt;
|
||||
const srDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
return new Promise(async function (resolve, reject) {
|
||||
const standardMode = appConfig?.measurementTrackingMode === measurementTrackingMode.STANDARD;
|
||||
|
||||
const promptResult = standardMode
|
||||
? await _askTrackMeasurements(uiViewportDialogService, customizationService, viewportId)
|
||||
: RESPONSE.HYDRATE_REPORT;
|
||||
|
||||
// Need to do action here... So we can set state...
|
||||
let StudyInstanceUID, SeriesInstanceUIDs;
|
||||
|
||||
if (promptResult === RESPONSE.HYDRATE_REPORT) {
|
||||
console.warn('!! HYDRATING STRUCTURED REPORT');
|
||||
const hydrationResult = hydrateStructuredReport(
|
||||
{ servicesManager, extensionManager, commandsManager, appConfig },
|
||||
displaySetInstanceUID
|
||||
);
|
||||
|
||||
StudyInstanceUID = hydrationResult.StudyInstanceUID;
|
||||
SeriesInstanceUIDs = hydrationResult.SeriesInstanceUIDs;
|
||||
}
|
||||
|
||||
resolve({
|
||||
userResponse: promptResult,
|
||||
displaySetInstanceUID: evt.displaySetInstanceUID,
|
||||
srSeriesInstanceUID: srDisplaySet.SeriesInstanceUID,
|
||||
viewportId,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUIDs,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _askTrackMeasurements(uiViewportDialogService, customizationService, viewportId) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const message = customizationService.getCustomization('viewportNotification.hydrateSRMessage');
|
||||
const actions = [
|
||||
{
|
||||
id: 'no-hydrate',
|
||||
type: 'secondary',
|
||||
text: 'No',
|
||||
value: RESPONSE.CANCEL,
|
||||
},
|
||||
{
|
||||
id: 'yes-hydrate',
|
||||
type: 'primary',
|
||||
text: 'Yes',
|
||||
value: RESPONSE.HYDRATE_REPORT,
|
||||
},
|
||||
];
|
||||
const onSubmit = result => {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
uiViewportDialogService.show({
|
||||
viewportId,
|
||||
type: 'info',
|
||||
message,
|
||||
actions,
|
||||
onSubmit,
|
||||
onOutsideClick: () => {
|
||||
uiViewportDialogService.hide();
|
||||
resolve(RESPONSE.CANCEL);
|
||||
},
|
||||
onKeyPress: event => {
|
||||
if (event.key === 'Enter') {
|
||||
const action = actions.find(action => action.value === RESPONSE.HYDRATE_REPORT);
|
||||
onSubmit(action.value);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default promptHydrateStructuredReport;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { utils } from '@ohif/extension-cornerstone';
|
||||
|
||||
function promptHydrateStructuredReport({ servicesManager, commandsManager }, ctx, evt) {
|
||||
const { displaySetService } = servicesManager.services;
|
||||
const { viewportId, displaySetInstanceUID } = evt;
|
||||
const srDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
|
||||
|
||||
const hydrateCallback = async () => {
|
||||
return commandsManager.runCommand('hydrateSecondaryDisplaySet', {
|
||||
displaySet: srDisplaySet,
|
||||
viewportId,
|
||||
});
|
||||
};
|
||||
|
||||
// For SR we need to use the whole context
|
||||
const enhancedSrDisplaySet = {
|
||||
...srDisplaySet,
|
||||
displaySetInstanceUID,
|
||||
};
|
||||
|
||||
return utils.promptHydrationDialog({
|
||||
servicesManager,
|
||||
viewportId,
|
||||
displaySet: enhancedSrDisplaySet,
|
||||
hydrateCallback,
|
||||
type: 'SR',
|
||||
});
|
||||
}
|
||||
|
||||
export default promptHydrateStructuredReport;
|
||||
@@ -1,17 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
import getContextModule from './getContextModule';
|
||||
import getPanelModule from './getPanelModule';
|
||||
import getViewportModule from './getViewportModule';
|
||||
import { id } from './id.js';
|
||||
import { ViewportActionButton } from '@ohif/ui-next';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { measurementTrackingMode } from './contexts/TrackedMeasurementsContext/promptBeginTracking';
|
||||
import getCustomizationModule from './getCustomizationModule';
|
||||
import {
|
||||
onDoubleClickHandler,
|
||||
customOnDropHandlerCallback,
|
||||
} from './customizations/studyBrowserCustomization';
|
||||
import { TrackedMeasurementsService } from './services';
|
||||
// Import types to ensure they're included in the build
|
||||
import './types';
|
||||
|
||||
const measurementTrackingExtension = {
|
||||
/**
|
||||
@@ -23,8 +22,25 @@ const measurementTrackingExtension = {
|
||||
getPanelModule,
|
||||
getViewportModule,
|
||||
|
||||
/**
|
||||
* Service configuration
|
||||
*/
|
||||
preRegistration({ servicesManager }) {
|
||||
servicesManager.registerService(TrackedMeasurementsService.REGISTRATION);
|
||||
},
|
||||
|
||||
onModeEnter({ servicesManager }) {
|
||||
const { toolbarService, customizationService } = servicesManager.services;
|
||||
const { customizationService, toolbarService, trackedMeasurementsService } =
|
||||
servicesManager.services;
|
||||
|
||||
toolbarService.registerEventForToolbarUpdate(trackedMeasurementsService, [
|
||||
trackedMeasurementsService.EVENTS.TRACKED_SERIES_CHANGED,
|
||||
trackedMeasurementsService.EVENTS.SERIES_ADDED,
|
||||
trackedMeasurementsService.EVENTS.SERIES_REMOVED,
|
||||
trackedMeasurementsService.EVENTS.TRACKING_ENABLED,
|
||||
trackedMeasurementsService.EVENTS.TRACKING_DISABLED,
|
||||
]);
|
||||
|
||||
customizationService.setCustomizations({
|
||||
'studyBrowser.thumbnailDoubleClickCallback': {
|
||||
$set: onDoubleClickHandler,
|
||||
@@ -33,23 +49,6 @@ const measurementTrackingExtension = {
|
||||
$set: customOnDropHandlerCallback,
|
||||
},
|
||||
});
|
||||
toolbarService.addButtons(
|
||||
[
|
||||
{
|
||||
// A button for loading tracked, SR measurements.
|
||||
// Note that the command run is registered in TrackedMeasurementsContext
|
||||
// because it must be bound to a React context's data.
|
||||
id: 'loadSRMeasurements',
|
||||
component: props => (
|
||||
<ViewportActionButton {...props}>{i18n.t('Common:LOAD')}</ViewportActionButton>
|
||||
),
|
||||
props: {
|
||||
commands: ['loadTrackedSRMeasurements'],
|
||||
},
|
||||
},
|
||||
],
|
||||
true // replace the button if it is already defined
|
||||
);
|
||||
},
|
||||
getCustomizationModule,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { utils } from '@ohif/core';
|
||||
import { useSystem, utils } from '@ohif/core';
|
||||
import { AccordionTrigger, MeasurementTable, ScrollArea, useViewportGrid } from '@ohif/ui-next';
|
||||
import {
|
||||
PanelMeasurement,
|
||||
StudyMeasurements,
|
||||
StudyMeasurementsActions,
|
||||
StudySummaryFromMetadata,
|
||||
AccordionGroup,
|
||||
StudyMeasurementsActions,
|
||||
MeasurementsOrAdditionalFindings,
|
||||
} from '@ohif/extension-cornerstone';
|
||||
|
||||
@@ -18,7 +18,9 @@ const { filterAnd, filterPlanarMeasurement, filterMeasurementsBySeriesUID } =
|
||||
|
||||
function PanelMeasurementTableTracking(props) {
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
const { measurementService, uiModalService } = props.servicesManager.services;
|
||||
const { servicesManager } = useSystem();
|
||||
const { measurementService, uiModalService } = servicesManager.services;
|
||||
|
||||
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
|
||||
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
|
||||
const measurementFilter = trackedStudy
|
||||
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
import { PubSubService } from '@ohif/core';
|
||||
|
||||
const EVENTS = {
|
||||
TRACKED_SERIES_CHANGED: 'event::trackedmeasurements:trackedserieschanged',
|
||||
SERIES_ADDED: 'event::trackedmeasurements:seriesadded',
|
||||
SERIES_REMOVED: 'event::trackedmeasurements:seriesremoved',
|
||||
TRACKING_ENABLED: 'event::trackedmeasurements:trackingenabled',
|
||||
TRACKING_DISABLED: 'event::trackedmeasurements:trackingdisabled',
|
||||
};
|
||||
|
||||
/**
|
||||
* Service class for accessing tracked measurements data.
|
||||
* This service provides a robust way to access tracked series information
|
||||
* from anywhere in the application, including outside of React components.
|
||||
*/
|
||||
export class TrackedMeasurementsService extends PubSubService {
|
||||
public static readonly REGISTRATION = {
|
||||
name: 'trackedMeasurementsService',
|
||||
altName: 'TrackedMeasurementsService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return new TrackedMeasurementsService();
|
||||
},
|
||||
};
|
||||
|
||||
private _trackedSeries: string[] = [];
|
||||
|
||||
constructor() {
|
||||
super(EVENTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the tracked series and notifies subscribers
|
||||
* @param trackedSeries Array of series UIDs being tracked
|
||||
*/
|
||||
public updateTrackedSeries(trackedSeries: string[]): void {
|
||||
if (!trackedSeries) {
|
||||
trackedSeries = [];
|
||||
}
|
||||
|
||||
const hasChanged =
|
||||
this._trackedSeries.length !== trackedSeries.length ||
|
||||
this._trackedSeries.some((seriesUID, index) => seriesUID !== trackedSeries[index]);
|
||||
|
||||
if (hasChanged) {
|
||||
const oldSeries = [...this._trackedSeries];
|
||||
this._trackedSeries = [...trackedSeries];
|
||||
|
||||
const wasEmpty = oldSeries.length === 0;
|
||||
const isEmpty = trackedSeries.length === 0;
|
||||
|
||||
if (wasEmpty && !isEmpty) {
|
||||
this._broadcastEvent(EVENTS.TRACKING_ENABLED, {
|
||||
trackedSeries: this.getTrackedSeries(),
|
||||
});
|
||||
} else if (!wasEmpty && isEmpty) {
|
||||
this._broadcastEvent(EVENTS.TRACKING_DISABLED, {
|
||||
trackedSeries: this.getTrackedSeries(),
|
||||
});
|
||||
}
|
||||
|
||||
this._broadcastEvent(EVENTS.TRACKED_SERIES_CHANGED, {
|
||||
trackedSeries: this.getTrackedSeries(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single series to tracking
|
||||
* @param seriesInstanceUID Series instance UID to add to tracking
|
||||
*/
|
||||
public addTrackedSeries(seriesInstanceUID: string): void {
|
||||
if (!seriesInstanceUID || this.isSeriesTracked(seriesInstanceUID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wasEmpty = this._trackedSeries.length === 0;
|
||||
this._trackedSeries = [...this._trackedSeries, seriesInstanceUID];
|
||||
|
||||
this._broadcastEvent(EVENTS.SERIES_ADDED, {
|
||||
seriesInstanceUID,
|
||||
trackedSeries: this.getTrackedSeries(),
|
||||
});
|
||||
|
||||
if (wasEmpty) {
|
||||
this._broadcastEvent(EVENTS.TRACKING_ENABLED, {
|
||||
trackedSeries: this.getTrackedSeries(),
|
||||
});
|
||||
}
|
||||
|
||||
this._broadcastEvent(EVENTS.TRACKED_SERIES_CHANGED, {
|
||||
trackedSeries: this.getTrackedSeries(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single series from tracking
|
||||
* @param seriesInstanceUID Series instance UID to remove from tracking
|
||||
*/
|
||||
public removeTrackedSeries(seriesInstanceUID: string): void {
|
||||
if (!seriesInstanceUID || !this.isSeriesTracked(seriesInstanceUID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._trackedSeries = this._trackedSeries.filter(uid => uid !== seriesInstanceUID);
|
||||
|
||||
this._broadcastEvent(EVENTS.SERIES_REMOVED, {
|
||||
seriesInstanceUID,
|
||||
trackedSeries: this.getTrackedSeries(),
|
||||
});
|
||||
|
||||
if (this._trackedSeries.length === 0) {
|
||||
this._broadcastEvent(EVENTS.TRACKING_DISABLED, {
|
||||
trackedSeries: this.getTrackedSeries(),
|
||||
});
|
||||
}
|
||||
|
||||
this._broadcastEvent(EVENTS.TRACKED_SERIES_CHANGED, {
|
||||
trackedSeries: this.getTrackedSeries(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currently tracked series
|
||||
* @returns Array of series UIDs being tracked
|
||||
*/
|
||||
public getTrackedSeries(): string[] {
|
||||
return [...this._trackedSeries];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific series is being tracked
|
||||
* @param seriesInstanceUID Series instance UID to check
|
||||
* @returns boolean indicating if series is tracked
|
||||
*/
|
||||
public isSeriesTracked(seriesInstanceUID: string): boolean {
|
||||
return this._trackedSeries.includes(seriesInstanceUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the service state
|
||||
*/
|
||||
public reset(): void {
|
||||
const wasTracking = this._trackedSeries.length > 0;
|
||||
this._trackedSeries = [];
|
||||
|
||||
if (wasTracking) {
|
||||
this._broadcastEvent(EVENTS.TRACKING_DISABLED, {
|
||||
trackedSeries: [],
|
||||
});
|
||||
|
||||
this._broadcastEvent(EVENTS.TRACKED_SERIES_CHANGED, {
|
||||
trackedSeries: [],
|
||||
});
|
||||
}
|
||||
|
||||
super.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any series are being tracked
|
||||
* @returns boolean indicating if tracking is active
|
||||
*/
|
||||
public isTrackingEnabled(): boolean {
|
||||
return this._trackedSeries.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export default TrackedMeasurementsService;
|
||||
@@ -0,0 +1 @@
|
||||
export * from './TrackedMeasurementsService';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './TrackedMeasurementsService';
|
||||
@@ -0,0 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
import { TrackedMeasurementsService } from '../services/TrackedMeasurementsService';
|
||||
|
||||
declare global {
|
||||
namespace AppTypes {
|
||||
export type TrackedMeasurementsServiceType = TrackedMeasurementsService;
|
||||
export interface Services {
|
||||
trackedMeasurementsService?: TrackedMeasurementsService;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AppTypes';
|
||||
@@ -2,32 +2,28 @@ import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ViewportActionArrows } from '@ohif/ui-next';
|
||||
import { useViewportGrid, Icons, Tooltip, TooltipTrigger, TooltipContent } from '@ohif/ui-next';
|
||||
import { OHIFCornerstoneViewport } from '@ohif/extension-cornerstone';
|
||||
|
||||
import { annotation } from '@cornerstonejs/tools';
|
||||
import { useTrackedMeasurements } from './../getContextModule';
|
||||
import { BaseVolumeViewport, Enums } from '@cornerstonejs/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSystem } from '@ohif/core';
|
||||
|
||||
function TrackedCornerstoneViewport(
|
||||
props: withAppTypes<{ viewportId: string; displaySets: AppTypes.DisplaySet[] }>
|
||||
) {
|
||||
const { displaySets, viewportId, servicesManager, extensionManager } = props;
|
||||
const { servicesManager } = useSystem();
|
||||
const { displaySets, viewportId } = props as {
|
||||
displaySets: AppTypes.DisplaySet[];
|
||||
viewportId: string;
|
||||
servicesManager: AppTypes.Services;
|
||||
};
|
||||
|
||||
const {
|
||||
measurementService,
|
||||
cornerstoneViewportService,
|
||||
viewportGridService,
|
||||
viewportActionCornersService,
|
||||
} = servicesManager.services;
|
||||
const { measurementService, cornerstoneViewportService, viewportGridService, toolbarService } =
|
||||
servicesManager.services;
|
||||
|
||||
// Todo: handling more than one displaySet on the same viewport
|
||||
const displaySet = displaySets[0];
|
||||
const { t } = useTranslation('Common');
|
||||
|
||||
const [viewportGrid] = useViewportGrid();
|
||||
const { activeViewportId } = viewportGrid;
|
||||
|
||||
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
|
||||
|
||||
const [isTracked, setIsTracked] = useState(false);
|
||||
@@ -193,39 +189,9 @@ function TrackedCornerstoneViewport(
|
||||
[measurementService, servicesManager, trackedMeasurementUID, trackedMeasurements, viewportId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const statusComponent = _getStatusComponent(isTracked, t);
|
||||
const arrowsComponent = _getArrowsComponent(
|
||||
isTracked,
|
||||
switchMeasurement,
|
||||
viewportId === activeViewportId
|
||||
);
|
||||
|
||||
viewportActionCornersService.addComponents([
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportStatusComponent',
|
||||
component: statusComponent,
|
||||
indexPriority: -100,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
{
|
||||
viewportId,
|
||||
id: 'viewportActionArrowsComponent',
|
||||
component: arrowsComponent,
|
||||
indexPriority: 0,
|
||||
location: viewportActionCornersService.LOCATIONS.topRight,
|
||||
},
|
||||
]);
|
||||
}, [activeViewportId, isTracked, switchMeasurement, viewportActionCornersService, viewportId]);
|
||||
|
||||
const getCornerstoneViewport = () => {
|
||||
const { component: Component } = extensionManager.getModuleEntry(
|
||||
'@ohif/extension-cornerstone.viewportModule.cornerstone'
|
||||
);
|
||||
|
||||
return (
|
||||
<Component
|
||||
<OHIFCornerstoneViewport
|
||||
{...props}
|
||||
onElementEnabled={evt => {
|
||||
props.onElementEnabled?.(evt);
|
||||
@@ -316,30 +282,4 @@ const _getArrowsComponent = (isTracked, switchMeasurement, isActiveViewport) =>
|
||||
);
|
||||
};
|
||||
|
||||
function _getStatusComponent(isTracked, t) {
|
||||
if (!isTracked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Icons.StatusTracking className="text-muted-foreground mt-0.5 ml-0.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
align="start"
|
||||
side="bottom"
|
||||
>
|
||||
{isTracked ? (
|
||||
<>{t('Series is tracked and can be viewed in the measurement panel')}</>
|
||||
) : (
|
||||
<>{t('Measurements for untracked series will not be shown in the measurements panel')}</>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default TrackedCornerstoneViewport;
|
||||
Reference in new issue
Block a user