feat(actions): simplify action corner api through the toolbarService (#5033)

This commit is contained in:
Alireza authored and GitHub committed 2025-05-15 12:08:12 -04:00
1 parent f71b88e639
commit 7c15bb8901
246 files changed
+7930 -4270

No files matched your search

@@ -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', {
@@ -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;
@@ -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;
+21 -22
View File
@@ -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
@@ -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;