feat: Automate tracking prompts for a less disruptive annotation workflow (#4822)

Co-authored-by: Devu Jayalekshmi <devu.jayalekshmi@trenser.com>
This commit is contained in:
nithin-trenser 2025-04-04 21:32:13 +05:30 committed by GitHub
parent c0391cea1b
commit 54fded554e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 948 additions and 70 deletions

View File

@ -13,13 +13,13 @@ function promptHydrateRT({
preHydrateCallbacks,
hydrateRTDisplaySet,
}: withAppTypes) {
const { uiViewportDialogService } = servicesManager.services;
const { uiViewportDialogService, customizationService } = servicesManager.services;
const extensionManager = servicesManager._extensionManager;
const appConfig = extensionManager._appConfig;
return new Promise(async function (resolve, reject) {
const promptResult = appConfig?.disableConfirmationPrompts
? RESPONSE.HYDRATE_SEG
: await _askHydrate(uiViewportDialogService, viewportId);
: await _askHydrate(uiViewportDialogService, customizationService, viewportId);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
@ -37,9 +37,13 @@ function promptHydrateRT({
});
}
function _askHydrate(uiViewportDialogService: AppTypes.UIViewportDialogService, viewportId) {
function _askHydrate(
uiViewportDialogService: AppTypes.UIViewportDialogService,
customizationService: AppTypes.CustomizationService,
viewportId
) {
return new Promise(function (resolve, reject) {
const message = 'Do you want to open this Segmentation?';
const message = customizationService.getCustomization('viewportNotification.hydrateRTMessage');
const actions = [
{
id: 'no-hydrate',

View File

@ -13,14 +13,14 @@ function promptHydrateSEG({
preHydrateCallbacks,
hydrateCallback,
}: withAppTypes) {
const { uiViewportDialogService } = servicesManager.services;
const { uiViewportDialogService, customizationService } = servicesManager.services;
const extensionManager = servicesManager._extensionManager;
const appConfig = extensionManager._appConfig;
return new Promise(async function (resolve, reject) {
const promptResult = appConfig?.disableConfirmationPrompts
? RESPONSE.HYDRATE_SEG
: await _askHydrate(uiViewportDialogService, viewportId);
: await _askHydrate(uiViewportDialogService, customizationService, viewportId);
if (promptResult === RESPONSE.HYDRATE_SEG) {
preHydrateCallbacks?.forEach(callback => {
@ -39,9 +39,13 @@ function promptHydrateSEG({
});
}
function _askHydrate(uiViewportDialogService, viewportId) {
function _askHydrate(
uiViewportDialogService: AppTypes.UIViewportDialogService,
customizationService: AppTypes.CustomizationService,
viewportId
) {
return new Promise(function (resolve, reject) {
const message = 'Do you want to open this Segmentation?';
const message = customizationService.getCustomization('viewportNotification.hydrateSEGMessage');
const actions = [
{
id: 'no-hydrate',

View File

@ -179,7 +179,10 @@ function Trigger(props) {
>
<div>
{React.cloneElement(children, childProps)}
<ChevronDownIcon key="chevronDown" className="text-primary h-4 w-4 shrink-0 transition-transform duration-200" />
<ChevronDownIcon
key="chevronDown"
className="text-primary h-4 w-4 shrink-0 transition-transform duration-200"
/>
</div>
</AccordionTrigger>
);

View File

@ -54,7 +54,7 @@ export const groupByNamedSets = (items, grouping) => {
};
export function MeasurementsOrAdditionalFindings(props): React.ReactNode {
const { items, children, grouping = {}, customHeader, group } = props;
const { items, children, grouping = {}, customHeader, group, actions } = props;
return (
<AccordionGroup
@ -72,6 +72,7 @@ export function MeasurementsOrAdditionalFindings(props): React.ReactNode {
<MeasurementTableNested
customHeader={customHeader}
allItems={items}
actions={actions}
/>
</AccordionGroup.Accordion>
</AccordionGroup>

View File

@ -53,6 +53,10 @@ export function StudyMeasurementsActions({ items, StudyInstanceUID, measurementF
className="pl-0.5"
onClick={e => {
e.stopPropagation();
if (actions?.onDelete) {
actions.onDelete();
return;
}
commandsManager.runCommand('clearMeasurements', {
measurementFilter,
});

View File

@ -7,7 +7,11 @@ import { toolNames } from './initCornerstoneTools';
import { onCompletedCalibrationLine } from './tools/CalibrationLineTool';
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
import getSOPInstanceAttributes from './utils/measurementServiceMappings/utils/getSOPInstanceAttributes';
import { triggerAnnotationRenderForViewportIds } from '@cornerstonejs/tools/utilities';
import {
setAnnotationLabel,
triggerAnnotationRenderForViewportIds,
} from '@cornerstonejs/tools/utilities';
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums;
const { removeAnnotation } = annotation.state;
@ -190,6 +194,7 @@ const connectToolsToMeasurementService = (servicesManager: AppTypes.ServicesMana
displaySetService,
cornerstoneViewportService,
customizationService,
viewportGridService,
} = servicesManager.services;
const csTools3DVer1MeasurementSource = initMeasurementService(
measurementService,
@ -197,7 +202,11 @@ const connectToolsToMeasurementService = (servicesManager: AppTypes.ServicesMana
cornerstoneViewportService,
customizationService
);
connectMeasurementServiceToTools(measurementService, cornerstoneViewportService);
connectMeasurementServiceToTools(
measurementService,
cornerstoneViewportService,
viewportGridService
);
const { annotationToMeasurement, remove } = csTools3DVer1MeasurementSource;
//
@ -323,7 +332,11 @@ const connectToolsToMeasurementService = (servicesManager: AppTypes.ServicesMana
return csTools3DVer1MeasurementSource;
};
const connectMeasurementServiceToTools = (measurementService, cornerstoneViewportService) => {
const connectMeasurementServiceToTools = (
measurementService,
cornerstoneViewportService,
viewportGridService
) => {
const { MEASUREMENT_REMOVED, MEASUREMENTS_CLEARED, MEASUREMENT_UPDATED, RAW_MEASUREMENT_ADDED } =
measurementService.EVENTS;
@ -366,7 +379,8 @@ const connectMeasurementServiceToTools = (measurementService, cornerstoneViewpor
}
if (data.label !== label) {
data.label = label;
const element = getActiveViewportEnabledElement(viewportGridService)?.viewport.element;
setAnnotationLabel(sourceAnnotation, element, label);
}
if (metadata.toolName === 'ArrowAnnotate') {

View File

@ -34,8 +34,9 @@ function PanelStudyBrowser({
dataSource,
customMapDisplaySets,
onClickUntrack,
onDoubleClickThumbnailHandlerCallBack,
}) {
const { servicesManager, commandsManager } = useSystem();
const { servicesManager, commandsManager, extensionManager } = useSystem();
const { displaySetService, customizationService } = servicesManager.services;
const navigate = useNavigate();
const studyMode = customizationService.getCustomization('studyBrowser.studyMode') || 'all';
@ -93,6 +94,7 @@ function PanelStudyBrowser({
commandsManager,
servicesManager,
isHangingProtocolLayout,
appConfig: extensionManager._appConfig,
};
const handlers = customHandler?.callbacks.map(callback => callback(setupArgs));
@ -100,6 +102,7 @@ function PanelStudyBrowser({
for (const handler of handlers) {
await handler(displaySetInstanceUID);
}
onDoubleClickThumbnailHandlerCallBack?.(displaySetInstanceUID);
},
[
activeViewportId,

View File

@ -1,5 +1,29 @@
import i18n from '@ohif/i18n';
import { Notification } from '@ohif/ui';
const beginTrackingMessage = i18n.t('MeasurementTable:Track measurements for this series?');
const trackNewSeriesMessage = i18n.t('Do you want to add this measurement to the existing report?');
const discardSeriesMessage = i18n.t(
'You have existing tracked measurements. What would you like to do with your existing tracked measurements?'
);
const trackNewStudyMessage = i18n.t('MeasurementTable:Track measurements for this series?');
const discardStudyMessage = i18n.t(
'Measurements cannot span across multiple studies. Do you want to save your tracked measurements?'
);
const hydrateSRMessage = i18n.t('Do you want to continue tracking measurements for this study?');
const hydrateRTMessage = i18n.t('Do you want to open this Segmentation?');
const hydrateSEGMessage = i18n.t('Do you want to open this Segmentation?');
const discardDirtyMessage = i18n.t('There are unsaved measurements. Do you want to save it?');
export default {
'ui.notificationComponent': Notification,
'viewportNotification.beginTrackingMessage': beginTrackingMessage,
'viewportNotification.trackNewSeriesMessage': trackNewSeriesMessage,
'viewportNotification.discardSeriesMessage': discardSeriesMessage,
'viewportNotification.trackNewStudyMessage': trackNewStudyMessage,
'viewportNotification.discardStudyMessage': discardStudyMessage,
'viewportNotification.hydrateSRMessage': hydrateSRMessage,
'viewportNotification.hydrateRTMessage': hydrateRTMessage,
'viewportNotification.hydrateSEGMessage': hydrateSEGMessage,
'viewportNotification.discardDirtyMessage': discardDirtyMessage,
};

View File

@ -1,4 +1,6 @@
import React from 'react';
import { setAnnotationLabel } from '@cornerstonejs/tools/utilities';
import { annotation } from '@cornerstonejs/tools';
import { LabellingFlow } from '@ohif/ui';
import { InputDialog } from '@ohif/ui-next';
@ -89,6 +91,7 @@ export async function callInputDialogAutoComplete({
uiDialogService,
labelConfig,
renderContent = LabellingFlow,
element,
}) {
const exclusive = labelConfig ? labelConfig.exclusive : false;
const dropDownItems = labelConfig ? labelConfig.items : [];
@ -97,7 +100,8 @@ export async function callInputDialogAutoComplete({
const labellingDoneCallback = newValue => {
uiDialogService.hide('select-annotation');
if (measurement && typeof newValue === 'string') {
measurement.label = newValue;
const sourceAnnotation = annotation.state.getAnnotation(measurement.uid);
setAnnotationLabel(sourceAnnotation, element, newValue);
}
resolve(newValue);
};

View File

@ -18,6 +18,7 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
const isBackupSave = evt.isBackupSave === undefined ? evt.data.isBackupSave : evt.isBackupSave;
const StudyInstanceUID = evt?.data?.StudyInstanceUID || ctx.trackedStudy;
const SeriesInstanceUID = evt?.data?.SeriesInstanceUID;
const { displaySetInstanceUID } = evt.data ?? evt;
const {
trackedSeries,
@ -76,6 +77,7 @@ async function promptSaveReport({ servicesManager, commandsManager, extensionMan
SeriesInstanceUID,
viewportId,
isBackupSave,
displaySetInstanceUID,
};
} catch (error) {
console.warn('Unable to save report', error);

View File

@ -3,14 +3,19 @@ import PropTypes from 'prop-types';
import { Machine } from 'xstate';
import { useMachine } from '@xstate/react';
import { useViewportGrid } from '@ohif/ui-next';
import { promptLabelAnnotation, promptSaveReport } from '@ohif/extension-default';
import { machineConfiguration, defaultOptions, RESPONSE } from './measurementTrackingMachine';
import promptBeginTracking from './promptBeginTracking';
import promptTrackNewSeries from './promptTrackNewSeries';
import promptTrackNewStudy from './promptTrackNewStudy';
import promptHydrateStructuredReport from './promptHydrateStructuredReport';
import { measurementTrackingMode } from './promptBeginTracking';
import hydrateStructuredReport from './hydrateStructuredReport';
import { useAppConfig } from '@state';
import {
promptBeginTrackingWrapper,
promptHydrateStructuredReportWrapper,
promptTrackNewSeriesWrapper,
promptTrackNewStudyWrapper,
promptLabelAnnotationWrapper,
promptSaveReportWrapper,
promptHasDirtyAnnotationsWrapper,
} from './promptWrapperFunctions';
const TrackedMeasurementsContext = React.createContext();
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
@ -35,7 +40,8 @@ function TrackedMeasurementsContextProvider(
const machineOptions = Object.assign({}, defaultOptions);
machineOptions.actions = Object.assign({}, machineOptions.actions, {
jumpToFirstMeasurementInActiveViewport: (ctx, evt) => {
const { trackedStudy, trackedSeries, activeViewportId } = ctx;
const { trackedStudy, trackedSeries } = ctx;
const { viewportId: activeViewportId } = evt.data;
const measurements = measurementService.getMeasurements();
const trackedMeasurements = measurements.filter(
m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID)
@ -82,7 +88,8 @@ function TrackedMeasurementsContextProvider(
},
jumpToSameImageInActiveViewport: (ctx, evt) => {
const { trackedStudy, trackedSeries, activeViewportId } = ctx;
const { trackedStudy, trackedSeries } = ctx;
const { viewportId: activeViewportId } = evt.data;
const measurements = measurementService.getMeasurements();
const trackedMeasurements = measurements.filter(
m => trackedStudy === m.referenceStudyUID && trackedSeries.includes(m.referenceSeriesUID)
@ -143,31 +150,64 @@ function TrackedMeasurementsContextProvider(
for (let i = 0; i < measurementIds.length; i++) {
measurementService.remove(measurementIds[i]);
}
measurementService.setIsMeasurementDeletedIndividually(false);
},
clearDisplaySetHydratedState: (ctx, evt) => {
const { displaySetInstanceUID } = evt.data ?? evt;
const displaysets = displaySetService.getActiveDisplaySets();
displaysets?.forEach(displayset => {
if (
displayset.Modality === 'SR' &&
displayset.displaySetInstanceUID !== displaySetInstanceUID &&
displayset.isHydrated
) {
displayset.isHydrated = false;
displayset.isLoaded = false;
}
});
},
updatedViewports: (ctx, evt) => {
const { hangingProtocolService } = servicesManager.services;
const { displaySetInstanceUID, viewportId } = evt.data ?? evt;
const updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportId,
displaySetInstanceUID
);
viewportGridService.setDisplaySetsForViewports(updatedViewports);
},
});
machineOptions.services = Object.assign({}, machineOptions.services, {
promptBeginTracking: promptBeginTracking.bind(null, {
promptBeginTracking: promptBeginTrackingWrapper.bind(null, {
servicesManager,
extensionManager,
appConfig,
}),
promptTrackNewSeries: promptTrackNewSeries.bind(null, {
promptTrackNewSeries: promptTrackNewSeriesWrapper.bind(null, {
servicesManager,
extensionManager,
appConfig,
}),
promptTrackNewStudy: promptTrackNewStudy.bind(null, {
promptTrackNewStudy: promptTrackNewStudyWrapper.bind(null, {
servicesManager,
extensionManager,
appConfig,
}),
promptSaveReport: promptSaveReport.bind(null, {
promptSaveReport: promptSaveReportWrapper.bind(null, {
servicesManager,
commandsManager,
extensionManager,
appConfig,
}),
promptHydrateStructuredReport: promptHydrateStructuredReport.bind(null, {
promptHydrateStructuredReport: promptHydrateStructuredReportWrapper.bind(null, {
servicesManager,
extensionManager,
commandsManager,
appConfig,
}),
promptHasDirtyAnnotations: promptHasDirtyAnnotationsWrapper.bind(null, {
servicesManager,
extensionManager,
commandsManager,
@ -179,7 +219,7 @@ function TrackedMeasurementsContextProvider(
commandsManager,
appConfig,
}),
promptLabelAnnotation: promptLabelAnnotation.bind(null, {
promptLabelAnnotation: promptLabelAnnotationWrapper.bind(null, {
servicesManager,
extensionManager,
commandsManager,
@ -194,6 +234,25 @@ function TrackedMeasurementsContextProvider(
const labelConfig = customizationService.getCustomization('measurementLabels');
return evt.data && evt.data.userResponse === RESPONSE.NO_NEVER && labelConfig?.labelOnMeasure;
},
isSimplifiedConfig: (ctx, evt, condMeta) => {
return appConfig?.measurementTrackingMode === measurementTrackingMode.SIMPLIFIED;
},
simplifiedAndLoadSR: (ctx, evt, condMeta) => {
return (
appConfig?.measurementTrackingMode === measurementTrackingMode.SIMPLIFIED &&
evt.data.isBackupSave === false
);
},
hasDirtyAndSimplified: (ctx, evt, condMeta) => {
const measurements = measurementService.getMeasurements();
const hasDirtyMeasurements =
measurements.some(measurement => measurement.isDirty) ||
(measurements.length && measurementService.getIsMeasurementDeletedIndividually());
return (
appConfig?.measurementTrackingMode === measurementTrackingMode.SIMPLIFIED &&
hasDirtyMeasurements
);
},
});
// TODO: IMPROVE
@ -266,7 +325,8 @@ function TrackedMeasurementsContextProvider(
// load function added by our sopClassHandler module
if (
displaySet.SOPClassHandlerId === SR_SOPCLASSHANDLERID &&
displaySet.isRehydratable === true
displaySet.isRehydratable === true &&
!displaySet.isHydrated
) {
const params = {
displaySetInstanceUID: displaySet.displaySetInstanceUID,

View File

@ -122,13 +122,27 @@ const machineConfiguration = {
UNTRACK_SERIES: [
{
target: 'tracking',
actions: ['removeTrackedSeries', 'setIsDirty'],
actions: ['removeTrackedSeries', 'setIsDirty', 'clearDisplaySetHydratedState'],
cond: 'hasRemainingTrackedSeries',
},
{
target: 'idle',
},
],
UNTRACK_ALL: [
{
target: 'tracking',
actions: [
'clearContext',
'setIsDirtyToClean',
'clearDisplaySetHydratedState',
'clearAllMeasurements',
],
},
{
target: 'idle',
},
],
SET_TRACKED_SERIES: [
{
target: 'tracking',
@ -146,6 +160,15 @@ const machineConfiguration = {
target: 'tracking',
},
],
CHECK_DIRTY: {
target: 'promptHasDirtyAnnotations',
cond: 'hasDirtyAndSimplified',
},
PROMPT_HYDRATE_SR: {
target: 'promptHydrateStructuredReport',
cond: 'isSimplifiedConfig',
actions: ['clearAllMeasurements', 'clearDisplaySetHydratedState'],
},
},
},
promptTrackNewSeries: {
@ -214,6 +237,16 @@ const machineConfiguration = {
invoke: {
src: 'promptSaveReport',
onDone: [
{
target: 'tracking',
actions: [
'clearAllMeasurements',
'clearDisplaySetHydratedState',
'setIsDirty',
'updatedViewports',
],
cond: 'simplifiedAndLoadSR',
},
// "clicked the save button"
// - should clear all measurements
// - show DICOM SR
@ -312,6 +345,28 @@ const machineConfiguration = {
],
},
},
promptHasDirtyAnnotations: {
invoke: {
src: 'promptHasDirtyAnnotations',
onDone: [
{
target: 'tracking',
actions: [
'clearAllMeasurements',
'clearDisplaySetHydratedState',
'setIsDirty',
'updatedViewports',
],
cond: 'shouldSetStudyAndSeries',
},
{
target: 'promptSaveReport',
cond: 'shouldPromptSaveReport',
},
{ target: 'tracking' },
],
},
},
},
strict: true,
};

View File

@ -9,17 +9,29 @@ const RESPONSE = {
SET_STUDY_AND_SERIES: 3,
};
export const measurementTrackingMode = {
STANDARD: 'standard',
SIMPLIFIED: 'simplified',
NONE: 'none',
};
function promptBeginTracking({ servicesManager, extensionManager }, ctx, evt) {
const { uiViewportDialogService } = servicesManager.services;
const { uiViewportDialogService, customizationService } = servicesManager.services;
const appConfig = extensionManager._appConfig;
// When the state change happens after a promise, the state machine sends the retult in evt.data;
// In case of direct transition to the state, the state machine sends the data in evt;
const { viewportId, StudyInstanceUID, SeriesInstanceUID } = evt.data || evt;
return new Promise(async function (resolve, reject) {
let promptResult = appConfig?.disableConfirmationPrompts
? RESPONSE.SET_STUDY_AND_SERIES
: await _askTrackMeasurements(uiViewportDialogService, viewportId);
const standardMode = appConfig?.measurementTrackingMode === measurementTrackingMode.STANDARD;
const noTrackingMode = appConfig?.measurementTrackingMode === measurementTrackingMode.NONE;
let promptResult;
promptResult = noTrackingMode
? RESPONSE.NO_NEVER
: standardMode
? await _askTrackMeasurements(uiViewportDialogService, customizationService, viewportId)
: RESPONSE.SET_STUDY_AND_SERIES;
resolve({
userResponse: promptResult,
@ -30,9 +42,11 @@ function promptBeginTracking({ servicesManager, extensionManager }, ctx, evt) {
});
}
function _askTrackMeasurements(uiViewportDialogService, viewportId) {
function _askTrackMeasurements(uiViewportDialogService, customizationService, viewportId) {
return new Promise(function (resolve, reject) {
const message = i18n.t('MeasurementTable:Track measurements for this series?');
const message = customizationService.getCustomization(
'viewportNotification.beginTrackingMessage'
);
const actions = [
{
id: 'prompt-begin-tracking-cancel',

View File

@ -0,0 +1,81 @@
const RESPONSE = {
NO_NEVER: -1,
CANCEL: 0,
CREATE_REPORT: 1,
ADD_SERIES: 2,
SET_STUDY_AND_SERIES: 3,
NO_NOT_FOR_SERIES: 4,
};
function promptHasDirtyAnnotations({ servicesManager }: withAppTypes, ctx, evt) {
const { viewportId, displaySetInstanceUID } = evt.data || evt;
return new Promise(async function (resolve, reject) {
const { uiViewportDialogService, customizationService } = servicesManager.services;
const promptResult = await _askSaveDiscardOrCancel(
uiViewportDialogService,
customizationService,
viewportId
);
resolve({
displaySetInstanceUID,
userResponse: promptResult,
viewportId,
isBackupSave: false,
});
});
}
function _askSaveDiscardOrCancel(
UIViewportDialogService: AppTypes.UIViewportDialogService,
customizationService: AppTypes.CustomizationService,
viewportId
) {
return new Promise(function (resolve, reject) {
const message = customizationService.getCustomization(
'viewportNotification.discardDirtyMessage'
);
const actions = [
{ id: 'cancel', type: 'cancel', text: 'Cancel', value: RESPONSE.CANCEL },
{
id: 'discard-existing',
type: 'secondary',
text: 'No, discard existing',
value: RESPONSE.SET_STUDY_AND_SERIES,
},
{
id: 'save-existing',
type: 'primary',
text: 'Yes',
value: RESPONSE.CREATE_REPORT,
},
];
const onSubmit = result => {
UIViewportDialogService.hide();
resolve(result);
};
UIViewportDialogService.show({
viewportId,
id: 'measurement-tracking-prompt-dirty-measurement',
type: 'info',
message,
actions,
onSubmit,
onOutsideClick: () => {
UIViewportDialogService.hide();
resolve(RESPONSE.CANCEL);
},
onKeyPress: event => {
if (event.key === 'Enter') {
const action = actions.find(action => action.id === 'save-existing');
onSubmit(action.value);
}
},
});
});
}
export default promptHasDirtyAnnotations;

View File

@ -1,5 +1,6 @@
import { hydrateStructuredReport } from '@ohif/extension-cornerstone-dicom-sr';
import { ButtonEnums } from '@ohif/ui';
import { measurementTrackingMode } from './promptBeginTracking';
const RESPONSE = {
NO_NEVER: -1,
@ -16,13 +17,16 @@ function promptHydrateStructuredReport(
ctx,
evt
) {
const { uiViewportDialogService, displaySetService } = servicesManager.services;
const { uiViewportDialogService, displaySetService, customizationService } =
servicesManager.services;
const { viewportId, displaySetInstanceUID } = evt;
const srDisplaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
return new Promise(async function (resolve, reject) {
const promptResult = appConfig?.disableConfirmationPrompts
? RESPONSE.HYDRATE_REPORT
: await _askTrackMeasurements(uiViewportDialogService, viewportId);
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;
@ -49,9 +53,9 @@ function promptHydrateStructuredReport(
});
}
function _askTrackMeasurements(uiViewportDialogService, viewportId) {
function _askTrackMeasurements(uiViewportDialogService, customizationService, viewportId) {
return new Promise(function (resolve, reject) {
const message = 'Do you want to continue tracking measurements for this study?';
const message = customizationService.getCustomization('viewportNotification.hydrateSRMessage');
const actions = [
{
id: 'no-hydrate',

View File

@ -1,4 +1,5 @@
import { ButtonEnums } from '@ohif/ui';
import { measurementTrackingMode } from './promptBeginTracking';
const RESPONSE = {
NO_NEVER: -1,
@ -10,17 +11,22 @@ const RESPONSE = {
};
function promptTrackNewSeries({ servicesManager, extensionManager }, ctx, evt) {
const { UIViewportDialogService } = servicesManager.services;
const { UIViewportDialogService, customizationService } = servicesManager.services;
// When the state change happens after a promise, the state machine sends the retult in evt.data;
// In case of direct transition to the state, the state machine sends the data in evt;
const { viewportId, StudyInstanceUID, SeriesInstanceUID } = evt.data || evt;
return new Promise(async function (resolve, reject) {
let promptResult = await _askShouldAddMeasurements(UIViewportDialogService, viewportId);
const appConfig = extensionManager._appConfig;
const showPrompt = appConfig?.measurementTrackingMode === measurementTrackingMode.STANDARD;
let promptResult = showPrompt
? await _askShouldAddMeasurements(UIViewportDialogService, customizationService, viewportId)
: RESPONSE.ADD_SERIES;
if (promptResult === RESPONSE.CREATE_REPORT) {
promptResult = ctx.isDirty
? await _askSaveDiscardOrCancel(UIViewportDialogService, viewportId)
? await _askSaveDiscardOrCancel(UIViewportDialogService, customizationService, viewportId)
: RESPONSE.SET_STUDY_AND_SERIES;
}
@ -34,9 +40,11 @@ function promptTrackNewSeries({ servicesManager, extensionManager }, ctx, evt) {
});
}
function _askShouldAddMeasurements(uiViewportDialogService, viewportId) {
function _askShouldAddMeasurements(uiViewportDialogService, customizationService, viewportId) {
return new Promise(function (resolve, reject) {
const message = 'Do you want to add this measurement to the existing report?';
const message = customizationService.getCustomization(
'viewportNotification.trackNewSeriesMessage'
);
const actions = [
{
type: ButtonEnums.type.secondary,
@ -73,10 +81,12 @@ function _askShouldAddMeasurements(uiViewportDialogService, viewportId) {
});
}
function _askSaveDiscardOrCancel(UIViewportDialogService, viewportId) {
function _askSaveDiscardOrCancel(UIViewportDialogService, customizationService, viewportId) {
return new Promise(function (resolve, reject) {
const message =
'You have existing tracked measurements. What would you like to do with your existing tracked measurements?';
const message = customizationService.getCustomization(
'viewportNotification.discardSeriesMessage'
);
const actions = [
{ type: 'cancel', text: 'Cancel', value: RESPONSE.CANCEL },
{

View File

@ -1,4 +1,5 @@
import i18n from 'i18next';
import { measurementTrackingMode } from './promptBeginTracking';
const RESPONSE = {
NO_NEVER: -1,
@ -10,18 +11,26 @@ const RESPONSE = {
};
function promptTrackNewStudy({ servicesManager, extensionManager }: withAppTypes, ctx, evt) {
const { uiViewportDialogService } = servicesManager.services;
const { uiViewportDialogService, customizationService } = servicesManager.services;
// When the state change happens after a promise, the state machine sends the retult in evt.data;
// In case of direct transition to the state, the state machine sends the data in evt;
const { viewportId, StudyInstanceUID, SeriesInstanceUID } = evt.data || evt;
return new Promise(async function (resolve, reject) {
let promptResult = await _askTrackMeasurements(uiViewportDialogService, viewportId);
const appConfig = extensionManager._appConfig;
const standardMode = appConfig?.measurementTrackingMode === measurementTrackingMode.STANDARD;
const simplifiedMode =
appConfig?.measurementTrackingMode === measurementTrackingMode.SIMPLIFIED;
let promptResult = standardMode
? await _askTrackMeasurements(uiViewportDialogService, customizationService, viewportId)
: RESPONSE.SET_STUDY_AND_SERIES;
if (promptResult === RESPONSE.SET_STUDY_AND_SERIES) {
promptResult = ctx.isDirty
? await _askSaveDiscardOrCancel(uiViewportDialogService, viewportId)
: RESPONSE.SET_STUDY_AND_SERIES;
promptResult =
ctx.isDirty && (standardMode || simplifiedMode)
? await _askSaveDiscardOrCancel(uiViewportDialogService, customizationService, viewportId)
: RESPONSE.SET_STUDY_AND_SERIES;
}
resolve({
@ -36,10 +45,13 @@ function promptTrackNewStudy({ servicesManager, extensionManager }: withAppTypes
function _askTrackMeasurements(
UIViewportDialogService: AppTypes.UIViewportDialogService,
customizationService: AppTypes.CustomizationService,
viewportId
) {
return new Promise(function (resolve, reject) {
const message = i18n.t('MeasurementTable:Track measurements for this series?');
const message = customizationService.getCustomization(
'viewportNotification.trackNewStudyMessage'
);
const actions = [
{ type: 'cancel', text: i18n.t('MeasurementTable:No'), value: RESPONSE.CANCEL },
{
@ -80,11 +92,13 @@ function _askTrackMeasurements(
function _askSaveDiscardOrCancel(
UIViewportDialogService: AppTypes.UIViewportDialogService,
customizationService: AppTypes.CustomizationService,
viewportId
) {
return new Promise(function (resolve, reject) {
const message =
'Measurements cannot span across multiple studies. Do you want to save your tracked measurements?';
const message = customizationService.getCustomization(
'viewportNotification.discardStudyMessage'
);
const actions = [
{ type: 'cancel', text: 'Cancel', value: RESPONSE.CANCEL },
{

View File

@ -0,0 +1,83 @@
const promptBeginTrackingWrapper = ({ servicesManager, extensionManager }, ctx, evt) => {
const { customizationService } = servicesManager.services;
const promptBeginTracking = customizationService.getCustomization(
'measurement.promptBeginTracking'
);
return promptBeginTracking({ servicesManager, extensionManager }, ctx, evt);
};
const promptHydrateStructuredReportWrapper = (
{ servicesManager, extensionManager, commandsManager, appConfig },
ctx,
evt
) => {
const { customizationService } = servicesManager.services;
const promptHydrateStructuredReport = customizationService.getCustomization(
'measurement.promptHydrateStructuredReport'
);
return promptHydrateStructuredReport(
{ servicesManager, extensionManager, commandsManager, appConfig },
ctx,
evt
);
};
const promptTrackNewSeriesWrapper = ({ servicesManager, extensionManager }, ctx, evt) => {
const { customizationService } = servicesManager.services;
const promptTrackNewSeries = customizationService.getCustomization(
'measurement.promptTrackNewSeries'
);
return promptTrackNewSeries({ servicesManager, extensionManager }, ctx, evt);
};
const promptTrackNewStudyWrapper = ({ servicesManager, extensionManager }, ctx, evt) => {
const { customizationService } = servicesManager.services;
const promptTrackNewStudy = customizationService.getCustomization(
'measurement.promptTrackNewStudy'
);
return promptTrackNewStudy({ servicesManager, extensionManager }, ctx, evt);
};
const promptLabelAnnotationWrapper = ({ servicesManager }, ctx, evt) => {
const { customizationService } = servicesManager.services;
const promptLabelAnnotation = customizationService.getCustomization(
'measurement.promptLabelAnnotation'
);
return promptLabelAnnotation({ servicesManager }, ctx, evt);
};
const promptSaveReportWrapper = (
{ servicesManager, commandsManager, extensionManager },
ctx,
evt
) => {
const { customizationService } = servicesManager.services;
const promptSaveReport = customizationService.getCustomization('measurement.promptSaveReport');
return promptSaveReport({ servicesManager, commandsManager, extensionManager }, ctx, evt);
};
const promptHasDirtyAnnotationsWrapper = (
{ servicesManager, commandsManager, extensionManager },
ctx,
evt
) => {
const { customizationService } = servicesManager.services;
const promptHasDirtyAnnotations = customizationService.getCustomization(
'measurement.promptHasDirtyAnnotations'
);
return promptHasDirtyAnnotations(
{ servicesManager, commandsManager, extensionManager },
ctx,
evt
);
};
export {
promptBeginTrackingWrapper,
promptHydrateStructuredReportWrapper,
promptTrackNewSeriesWrapper,
promptTrackNewStudyWrapper,
promptLabelAnnotationWrapper,
promptSaveReportWrapper,
promptHasDirtyAnnotationsWrapper,
};

View File

@ -0,0 +1,16 @@
import promptBeginTracking from '../contexts/TrackedMeasurementsContext/promptBeginTracking';
import promptHasDirtyAnnotations from '../contexts/TrackedMeasurementsContext/promptHasDirtyAnnotations';
import promptHydrateStructuredReport from '../contexts/TrackedMeasurementsContext/promptHydrateStructuredReport';
import promptTrackNewSeries from '../contexts/TrackedMeasurementsContext/promptTrackNewSeries';
import promptTrackNewStudy from '../contexts/TrackedMeasurementsContext/promptTrackNewStudy';
import { promptLabelAnnotation, promptSaveReport } from '@ohif/extension-default';
export default {
'measurement.promptBeginTracking': promptBeginTracking,
'measurement.promptHydrateStructuredReport': promptHydrateStructuredReport,
'measurement.promptTrackNewSeries': promptTrackNewSeries,
'measurement.promptTrackNewStudy': promptTrackNewStudy,
'measurement.promptLabelAnnotation': promptLabelAnnotation,
'measurement.promptSaveReport': promptSaveReport,
'measurement.promptHasDirtyAnnotations': promptHasDirtyAnnotations,
};

View File

@ -0,0 +1,64 @@
import { measurementTrackingMode } from '../contexts/TrackedMeasurementsContext/promptBeginTracking';
type CheckHasDirtyAndSimplifiedModeProps = {
servicesManager: AppTypes.ServicesManager;
appConfig: AppTypes.Config;
displaySetInstanceUID: string;
};
const onDoubleClickHandler = {
callbacks: [
({ activeViewportId, servicesManager, isHangingProtocolLayout, appConfig }) =>
async displaySetInstanceUID => {
const { hangingProtocolService, viewportGridService, uiNotificationService } =
servicesManager.services;
let updatedViewports = [];
const viewportId = activeViewportId;
const haveDirtyMeasurementsInSimplifiedMode = checkHasDirtyAndSimplifiedMode({
servicesManager,
appConfig,
displaySetInstanceUID,
});
try {
if (!haveDirtyMeasurementsInSimplifiedMode) {
updatedViewports = hangingProtocolService.getViewportsRequireUpdate(
viewportId,
displaySetInstanceUID,
isHangingProtocolLayout
);
viewportGridService.setDisplaySetsForViewports(updatedViewports);
}
} catch (error) {
console.warn(error);
uiNotificationService.show({
title: 'Thumbnail Double Click',
message: 'The selected display sets could not be added to the viewport.',
type: 'error',
duration: 3000,
});
}
},
],
};
const customOnDropHandlerCallback = async props => {
const handled = checkHasDirtyAndSimplifiedMode(props);
return Promise.resolve({ handled });
};
const checkHasDirtyAndSimplifiedMode = (props: CheckHasDirtyAndSimplifiedModeProps) => {
const { servicesManager, appConfig, displaySetInstanceUID } = props;
const simplifiedMode = appConfig.measurementTrackingMode === measurementTrackingMode.SIMPLIFIED;
const { measurementService, displaySetService } = servicesManager.services;
const measurements = measurementService.getMeasurements();
const haveDirtyMeasurements =
measurements.some(m => m.isDirty) ||
(measurements.length && measurementService.getIsMeasurementDeletedIndividually());
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
const hasDirtyAndSimplifiedMode =
displaySet.Modality === 'SR' && simplifiedMode && haveDirtyMeasurements;
return hasDirtyAndSimplifiedMode;
};
export { onDoubleClickHandler, customOnDropHandlerCallback };

View File

@ -0,0 +1,10 @@
import measurementTrackingPrompts from './customizations/measurementTrackingPrompts';
export default function getCustomizationModule() {
return [
{
name: 'default',
value: { ...measurementTrackingPrompts },
},
];
}

View File

@ -6,6 +6,12 @@ import getViewportModule from './getViewportModule';
import { id } from './id.js';
import { ViewportActionButton } from '@ohif/ui';
import i18n from '@ohif/i18n';
import { measurementTrackingMode } from './contexts/TrackedMeasurementsContext/promptBeginTracking';
import getCustomizationModule from './getCustomizationModule';
import {
onDoubleClickHandler,
customOnDropHandlerCallback,
} from './customizations/studyBrowserCustomization';
const measurementTrackingExtension = {
/**
@ -18,8 +24,15 @@ const measurementTrackingExtension = {
getViewportModule,
onModeEnter({ servicesManager }) {
const { toolbarService } = servicesManager.services;
const { toolbarService, customizationService } = servicesManager.services;
customizationService.setCustomizations({
'studyBrowser.thumbnailDoubleClickCallback': {
$set: onDoubleClickHandler,
},
customOnDropHandler: {
$set: customOnDropHandlerCallback,
},
});
toolbarService.addButtons(
[
{
@ -38,6 +51,9 @@ const measurementTrackingExtension = {
true // replace the button if it is already defined
);
},
getCustomizationModule,
};
export default measurementTrackingExtension;
export { measurementTrackingMode };

View File

@ -1,4 +1,5 @@
import React from 'react';
import { Dialog, ButtonEnums } from '@ohif/ui';
import { utils } from '@ohif/core';
import { AccordionTrigger, MeasurementTable, useViewportGrid } from '@ohif/ui-next';
import {
@ -11,18 +12,40 @@ import {
} from '@ohif/extension-cornerstone';
import { useTrackedMeasurements } from '../getContextModule';
import { UntrackSeriesModal } from './PanelStudyBrowserTracking/untrackSeriesModal';
const { filterAnd, filterPlanarMeasurement, filterMeasurementsBySeriesUID } =
utils.MeasurementFilters;
function PanelMeasurementTableTracking(props) {
const [viewportGrid] = useViewportGrid();
const { measurementService, uiModalService } = props.servicesManager.services;
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
const measurementFilter = trackedStudy
? filterAnd(filterPlanarMeasurement, filterMeasurementsBySeriesUID(trackedSeries))
: filterPlanarMeasurement;
const onUntrackConfirm = () => {
sendTrackedMeasurementsEvent('UNTRACK_ALL', {});
};
const onDelete = () => {
const hasDirtyMeasurements = measurementService
.getMeasurements()
.some(measurement => measurement.isDirty);
hasDirtyMeasurements
? uiModalService.show({
title: 'Untrack Study',
content: UntrackSeriesModal,
contentProps: {
onConfirm: onUntrackConfirm,
message: 'Are you sure you want to untrack study and delete all measurements?',
},
})
: onUntrackConfirm();
};
const EmptyComponent = () => (
<div data-cy="trackedMeasurements-panel">
<MeasurementTable
@ -43,6 +66,7 @@ function PanelMeasurementTableTracking(props) {
measurementFilter,
});
},
onDelete,
};
const Header = props => (
@ -51,7 +75,10 @@ function PanelMeasurementTableTracking(props) {
className="px-0"
>
<div data-cy="TrackingHeader">
<StudySummaryFromMetadata {...props} />
<StudySummaryFromMetadata
{...props}
actions={actions}
/>
</div>
</AccordionTrigger>
);
@ -68,7 +95,7 @@ function PanelMeasurementTableTracking(props) {
key="trackingMeasurementsHeader"
asChild={true}
>
<Header key="trackingHeadChild" />
<Header key="trackingHeadChild" />
</AccordionGroup.Trigger>
<MeasurementsOrAdditionalFindings
key="measurementsOrAdditionalFindings"

View File

@ -1,9 +1,10 @@
import React from 'react';
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useSystem } from '@ohif/core';
import PanelStudyBrowser from '@ohif/extension-default/src/Panels/StudyBrowser/PanelStudyBrowser';
import { UntrackSeriesModal } from './untrackSeriesModal';
import { useTrackedMeasurements } from '../../getContextModule';
import { useViewportGrid } from '@ohif/ui-next';
const thumbnailNoImageModalities = [
'SR',
@ -27,10 +28,34 @@ export default function PanelStudyBrowserTracking({
dataSource,
}) {
const { servicesManager } = useSystem();
const { displaySetService, uiModalService, measurementService } = servicesManager.services;
const { displaySetService, uiModalService, measurementService, viewportGridService } =
servicesManager.services;
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
const { trackedSeries } = trackedMeasurements.context;
const checkDirtyMeasurements = displaySetInstanceUID => {
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
if (displaySet.Modality === 'SR') {
const activeViewportId = viewportGridService.getActiveViewportId();
sendTrackedMeasurementsEvent('CHECK_DIRTY', {
viewportId: activeViewportId,
displaySetInstanceUID: displaySetInstanceUID,
});
}
};
useEffect(() => {
const subscriptionOndropFired = viewportGridService.subscribe(
viewportGridService.EVENTS.VIEWPORT_ONDROP_HANDLED,
({ eventData }) => {
checkDirtyMeasurements(eventData.displaySetInstanceUID);
}
);
return () => {
subscriptionOndropFired.unsubscribe();
};
}, []);
const onClickUntrack = displaySetInstanceUID => {
const onConfirm = () => {
const displaySet = displaySetService.getDisplaySetByUID(displaySetInstanceUID);
@ -50,6 +75,7 @@ export default function PanelStudyBrowserTracking({
content: UntrackSeriesModal,
contentProps: {
onConfirm,
message: 'Are you sure you want to untrack this series?',
},
});
};
@ -115,6 +141,7 @@ export default function PanelStudyBrowserTracking({
dataSource={dataSource}
customMapDisplaySets={mapDisplaySetsWithTracking}
onClickUntrack={onClickUntrack}
onDoubleClickThumbnailHandlerCallBack={checkDirtyMeasurements}
/>
);
}

View File

@ -1,11 +1,11 @@
import React from 'react';
import { FooterAction } from '@ohif/ui-next';
export function UntrackSeriesModal({ hide, onConfirm }) {
export function UntrackSeriesModal({ hide, onConfirm, message }) {
return (
<div className="text-foreground">
<div>
<p>Are you sure you want to untrack this series?</p>
<p>{message}</p>
<p className="mt-2">
This action cannot be undone and will delete all your existing measurements.
</p>

View File

@ -47,6 +47,7 @@ async function appInit(appConfigOrFunc, defaultExtensions, defaultModes) {
};
// Default the peer import function
appConfig.peerImport ||= peerImport;
appConfig.measurementTrackingMode ||= 'standard';
const extensionManager = new ExtensionManager({
commandsManager,

View File

@ -257,6 +257,7 @@ function ViewerViewportGrid(props: withAppTypes) {
}, [viewports, _getUpdatedViewports]);
const onDropHandler = (viewportId, { displaySetInstanceUID }) => {
const { viewportGridService } = servicesManager.services;
const customOnDropHandler = customizationService.getCustomization('customOnDropHandler');
const dropHandlerPromise = customOnDropHandler({
...props,
@ -264,13 +265,13 @@ function ViewerViewportGrid(props: withAppTypes) {
displaySetInstanceUID,
appConfig,
});
dropHandlerPromise.then(({ handled }) => {
if (!handled) {
const updatedViewports = _getUpdatedViewports(viewportId, displaySetInstanceUID);
viewportGridService.setDisplaySetsForViewports(updatedViewports);
}
});
viewportGridService.publishViewportOnDropHandled({ displaySetInstanceUID });
};
// Store previous isReferenceViewable values to avoid infinite loops

View File

@ -64,6 +64,7 @@ const MEASUREMENT_SCHEMA_KEYS = [
'isSelected',
'textBox',
'referencedImageId',
'isDirty',
];
const EVENTS = {
@ -92,6 +93,11 @@ const VALUE_TYPES = {
ROI_THRESHOLD_MANUAL: 'value_type::roiThresholdManual',
};
enum MeasurementChangeType {
HandlesUpdated = 'HandlesUpdated',
LabelChange = 'LabelChange',
}
export type MeasurementFilter = (measurement) => boolean;
/**
@ -121,6 +127,7 @@ class MeasurementService extends PubSubService {
private measurements = new Map();
private unmappedMeasurements = new Map();
private isMeasurementDeletedIndividually: boolean;
private sources = {};
private mappings = {};
@ -543,6 +550,11 @@ class MeasurementService extends PubSubService {
uid: internalUID,
};
newMeasurement.isDirty =
sourceAnnotationDetail.changeType === MeasurementChangeType.HandlesUpdated ||
sourceAnnotationDetail.changeType === MeasurementChangeType.LabelChange ||
oldMeasurement?.isDirty;
if (oldMeasurement) {
// TODO: Ultimately, each annotation should have a selected flag right from the source.
// For now, it is just added in OHIF here and in setMeasurementSelected.
@ -586,6 +598,7 @@ class MeasurementService extends PubSubService {
this.unmappedMeasurements.delete(measurementUID);
this.measurements.delete(measurementUID);
this.isMeasurementDeletedIndividually = true;
this._broadcastEvent(this.EVENTS.MEASUREMENT_REMOVED, {
source,
measurement: measurementUID,
@ -839,6 +852,14 @@ class MeasurementService extends PubSubService {
notYetUpdatedAtSource: true,
});
}
public setIsMeasurementDeletedIndividually = isDeletedIndividually => {
this.isMeasurementDeletedIndividually = isDeletedIndividually;
};
public getIsMeasurementDeletedIndividually = () => {
return this.isMeasurementDeletedIndividually;
};
}
export default MeasurementService;

View File

@ -7,6 +7,7 @@ class ViewportGridService extends PubSubService {
GRID_STATE_CHANGED: 'event::gridStateChanged',
GRID_SIZE_CHANGED: 'event::gridSizeChanged',
VIEWPORTS_READY: 'event::viewportsReady',
VIEWPORT_ONDROP_HANDLED: 'event::viewportOnDropHandled',
};
public static REGISTRATION = {
@ -141,6 +142,10 @@ class ViewportGridService extends PubSubService {
this._broadcastEvent(this.EVENTS.VIEWPORTS_READY, {});
}
public publishViewportOnDropHandled(eventData) {
this._broadcastEvent(this.EVENTS.VIEWPORT_ONDROP_HANDLED, { eventData });
}
public setActiveViewportId(id: string) {
if (id === this.getActiveViewportId()) {
return;

View File

@ -114,6 +114,7 @@ declare global {
days?: number;
};
groupEnabledModesFirst?: boolean;
measurementTrackingMode?: 'standard' | 'simplified' | 'none';
disableConfirmationPrompts?: boolean;
showPatientInfo?: 'visible' | 'visibleCollapsed' | 'disabled' | 'visibleReadOnly';
requestTransferSyntaxUID?: string;

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -125,7 +125,7 @@ Here are a list of some options available:
- `investigationalUseDialog`: This should contain an object with `option` value, it can be either `always` which always shows the dialog once per session, `never` which never shows the dialog, or `configure` which shows the dialog once and won't show it again until a set number of days defined by the user, if it's set to configure, you are required to add an additional property `days` which is the number of days to wait before showing the dialog again.
- `groupEnabledModesFirst`: boolean, if set to true, all valid modes for the study get grouped together first, then the rest of the modes. If false, all modes are shown in the order they are defined in the configuration.
- `experimentalStudyBrowserSort`: boolean, if set to true, you will get the experimental StudyBrowserSort component in the UI, which displays a list of sort functions that the displaySets can be sorted by, the sort reflects in all part of the app including the thumbnail/study panel. These sort functions are defined in the customizationModule and can be expanded by users.
- `disableConfirmationPrompts`: boolean, if set to true, it skips confirmation prompts for measurement tracking and hydration.
- `disableConfirmationPrompts`: boolean, if set to true, it skips confirmation prompts for segmentation related prompts.
- `showPatientInfo`: string, if set to 'visible', the patient info header will be shown and its initial state is expanded. If set to 'visibleCollapsed', the patient info header will be shown but it's initial state is collapsed. If set to 'disabled', the patient info header will never be shown, and if set to 'visibleReadOnly', the patient info header will be shown and always expanded.
- `requestTransferSyntaxUID` : Request a specific Transfer syntax from dicom web server ex: 1.2.840.10008.1.2.4.80 (applied only if acceptHeader is not set)
- `omitQuotationForMultipartRequest`: Some servers (e.g., .NET) require the `multipart/related` request to be sent without quotation marks. Defaults to `false`. If your server doesn't require this, then setting this flag to `true` might improve performance (by removing the need for preflight requests). Also note that

View File

@ -27,6 +27,15 @@ import windowLevelActionMenu from '../../../assets/img/windowLevelActionMenu.png
import viewPortNotificationImage from '../../../assets/img/viewport-notification.png';
import captureViewportModal from '../../../assets/img/captureViewportModal.png';
import aboutModal from '../../../assets/img/aboutModal.png';
import promptBeginTrackingContent from '../../../assets/img/promptBeginTrackingContent.png';
import promptAddSeriesContent from '../../../assets/img/promptAddSeriesContent.png';
import promptSRTrackingContent from '../../../assets/img/promptSRTrackingContent.png';
import promptsegmentationContent from '../../../assets/img/promptsegmentationContent.png';
import promptRtstructContent from '../../../assets/img/promptRtstructContent.png';
import promptDiscardStudyContent from '../../../assets/img/promptDiscardStudyContent.png';
import promptDiscardSeriesContent from '../../../assets/img/promptDiscardSeriesContent.png';
import promptDiscardDirtyContent from '../../../assets/img/promptDiscardDirtyContent.png';
import promptTrackStudyContent from '../../../assets/img/promptTrackStudyContent.png';
export const viewportOverlayCustomizations = [
{
@ -952,6 +961,296 @@ window.config = {
};
`,
},
{
id: 'viewportNotification.beginTrackingMessage',
description: 'Define the content to be displayed in begin tracking prompt',
default: 'Track measurements for this series?',
image: [promptBeginTrackingContent],
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'viewportNotification.beginTrackingMessage': {
$set: CustomizedComponent,
},
},
],
};
`,
},
{
id: 'viewportNotification.trackNewSeriesMessage',
description: 'Define the content to be displayed in track new series prompt',
default: 'Do you want to add this measurement to the existing report?',
image: [promptAddSeriesContent],
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'viewportNotification.trackNewSeriesMessage': {
$set: CustomizedComponent,
},
},
],
};
`,
},
{
id: 'viewportNotification.discardSeriesMessage',
description: 'Define the content to be displayed in discard series prompt',
default:
'You have existing tracked measurements. What would you like to do with your existing tracked measurements?',
image: [promptDiscardSeriesContent],
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'viewportNotification.discardSeriesMessage': {
$set: CustomizedComponent,
},
},
],
};
`,
},
{
id: 'viewportNotification.trackNewStudyMessage',
description: 'Define the content to be displayed in track new study prompt',
default: 'Track measurements for this series?',
image: [promptTrackStudyContent],
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'viewportNotification.trackNewStudyMessage': {
$set: CustomizedComponent,
},
},
],
};
`,
},
{
id: 'viewportNotification.discardStudyMessage',
description: 'Define the content to be displayed in discard study prompt',
default:
'Measurements cannot span across multiple studies. Do you want to save your tracked measurements?',
image: [promptDiscardStudyContent],
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'viewportNotification.discardStudyMessage': {
$set: CustomizedComponent,
},
},
],
};
`,
},
{
id: 'viewportNotification.hydrateSRMessage',
description: 'Define the content to be displayed in hydrate SR prompt',
default: 'Do you want to continue tracking measurements for this study?',
image: [promptSRTrackingContent],
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'viewportNotification.hydrateSRMessage': {
$set: CustomizedComponent,
},
},
],
};
`,
},
{
id: 'viewportNotification.hydrateRTMessage',
description: 'Define the content to be displayed in hydrate RT prompt',
default: 'Do you want to open this Segmentation?',
image: [promptRtstructContent],
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'viewportNotification.hydrateRTMessage': {
$set: CustomizedComponent,
},
},
],
};
`,
},
{
id: 'viewportNotification.hydrateSEGMessage',
description: 'Define the content to be displayed in hydrate SEG prompt',
default: 'Do you want to open this Segmentation?',
image: [promptsegmentationContent],
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'viewportNotification.hydrateSEGMessage': {
$set: CustomizedComponent,
},
},
],
};
`,
},
{
id: 'viewportNotification.discardDirtyMessage',
description: 'Define the content to be displayed in hydrate SR prompt',
default: 'There are unsaved measurements. Do you want to save it?',
image: [promptDiscardDirtyContent],
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'viewportNotification.discardDirtyMessage': {
$set: CustomizedComponent,
},
},
],
};
`,
},
{
id: 'measurement.promptBeginTracking',
description:
'Define the functionality to connect with the measurement tracking machine on begin measurement tracking',
default: 'promptBeginTracking',
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'measurement.promptBeginTracking': {
$set: customFunction,
},
},
],
};
`,
},
{
id: 'measurement.promptHydrateStructuredReport',
description:
'Define the functionality to connect with the measurement tracking machine on hydrate SR',
default: 'promptHydrateStructuredReport',
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'measurement.promptHydrateStructuredReport': {
$set: customFunction,
},
},
],
};
`,
},
{
id: 'measurement.promptTrackNewSeries',
description:
'Define the functionality to connect with the measurement tracking machine on tracking new series',
default: 'promptTrackNewSeries',
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'measurement.promptTrackNewSeries': {
$set: customFunction,
},
},
],
};
`,
},
{
id: 'measurement.promptTrackNewStudy',
description:
'Define the functionality to connect with the measurement tracking machine on tracking new study',
default: 'promptTrackNewStudy',
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'measurement.promptTrackNewStudy': {
$set: customFunction,
},
},
],
};
`,
},
{
id: 'measurement.promptLabelAnnotation',
description:
'Define the functionality to connect with the measurement tracking machine on begin measurement tracking',
default: 'promptLabelAnnotation',
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'measurement.promptLabelAnnotation': {
$set: customFunction,
},
},
],
};
`,
},
{
id: 'measurement.promptSaveReport',
description:
'Define the functionality to connect with the measurement tracking machine on save SR report',
default: 'promptSaveReport',
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'measurement.promptSaveReport': {
$set: customFunction,
},
},
],
};
`,
},
{
id: 'measurement.promptHasDirtyAnnotations',
description:
'Define the functionality to connect with the measurement tracking machine on there are dirty annotations',
default: 'promptHasDirtyAnnotations',
configuration: `
window.config = {
// rest of window config
customizationService: [
{
'measurement.promptHasDirtyAnnotations': {
$set: customFunction,
},
},
],
};
`,
},
];
export const segmentationCustomizations = [

View File

@ -8,5 +8,11 @@
"NonTargets": "NonTargets",
"Relabel": "Relabel",
"Targets": "Targets",
"Track measurements for this series?": "Track measurements for this series?"
"Track measurements for this series?": "Track measurements for this series?",
"Do you want to add this measurement to the existing report?": "Do you want to add this measurement to the existing report?",
"You have existing tracked measurements. What would you like to do with your existing tracked measurements?": "You have existing tracked measurements. What would you like to do with your existing tracked measurements?",
"Measurements cannot span across multiple studies. Do you want to save your tracked measurements?": "Measurements cannot span across multiple studies. Do you want to save your tracked measurements?",
"Do you want to continue tracking measurements for this study?": "Do you want to continue tracking measurements for this study?",
"Do you want to open this Segmentation?": "Do you want to open this Segmentation?",
"There are unsaved measurements. Do you want to save it?": "There are unsaved measurements. Do you want to save it?"
}