diff --git a/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts b/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts index 89abd66df..8799aa4ad 100644 --- a/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts +++ b/extensions/cornerstone-dicom-rt/src/utils/promptHydrateRT.ts @@ -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', diff --git a/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts b/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts index 9d3a8c8b6..b6aa263d3 100644 --- a/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts +++ b/extensions/cornerstone-dicom-seg/src/utils/promptHydrateSEG.ts @@ -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', diff --git a/extensions/cornerstone/src/components/AccordionGroup/AccordionGroup.tsx b/extensions/cornerstone/src/components/AccordionGroup/AccordionGroup.tsx index 2091089a8..337614a29 100644 --- a/extensions/cornerstone/src/components/AccordionGroup/AccordionGroup.tsx +++ b/extensions/cornerstone/src/components/AccordionGroup/AccordionGroup.tsx @@ -179,7 +179,10 @@ function Trigger(props) { >
Are you sure you want to untrack this series?
+{message}
This action cannot be undone and will delete all your existing measurements.
diff --git a/platform/app/src/appInit.js b/platform/app/src/appInit.js index db1c54498..b4e510ccc 100644 --- a/platform/app/src/appInit.js +++ b/platform/app/src/appInit.js @@ -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, diff --git a/platform/app/src/components/ViewportGrid.tsx b/platform/app/src/components/ViewportGrid.tsx index e595a4fe0..0c4561af0 100644 --- a/platform/app/src/components/ViewportGrid.tsx +++ b/platform/app/src/components/ViewportGrid.tsx @@ -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 diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index 81d39148b..1683bbba5 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -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; diff --git a/platform/core/src/services/ViewportGridService/ViewportGridService.ts b/platform/core/src/services/ViewportGridService/ViewportGridService.ts index ee828b74a..f39fec02b 100644 --- a/platform/core/src/services/ViewportGridService/ViewportGridService.ts +++ b/platform/core/src/services/ViewportGridService/ViewportGridService.ts @@ -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; diff --git a/platform/core/src/types/AppTypes.ts b/platform/core/src/types/AppTypes.ts index 826e5de13..16125b04c 100644 --- a/platform/core/src/types/AppTypes.ts +++ b/platform/core/src/types/AppTypes.ts @@ -114,6 +114,7 @@ declare global { days?: number; }; groupEnabledModesFirst?: boolean; + measurementTrackingMode?: 'standard' | 'simplified' | 'none'; disableConfirmationPrompts?: boolean; showPatientInfo?: 'visible' | 'visibleCollapsed' | 'disabled' | 'visibleReadOnly'; requestTransferSyntaxUID?: string; diff --git a/platform/docs/docs/assets/img/promptAddSeriesContent.png b/platform/docs/docs/assets/img/promptAddSeriesContent.png new file mode 100644 index 000000000..53c721160 Binary files /dev/null and b/platform/docs/docs/assets/img/promptAddSeriesContent.png differ diff --git a/platform/docs/docs/assets/img/promptBeginTrackingContent.png b/platform/docs/docs/assets/img/promptBeginTrackingContent.png new file mode 100644 index 000000000..d5a2aa261 Binary files /dev/null and b/platform/docs/docs/assets/img/promptBeginTrackingContent.png differ diff --git a/platform/docs/docs/assets/img/promptDiscardDirtyContent.png b/platform/docs/docs/assets/img/promptDiscardDirtyContent.png new file mode 100644 index 000000000..aedd30e3e Binary files /dev/null and b/platform/docs/docs/assets/img/promptDiscardDirtyContent.png differ diff --git a/platform/docs/docs/assets/img/promptDiscardSeriesContent.png b/platform/docs/docs/assets/img/promptDiscardSeriesContent.png new file mode 100644 index 000000000..cad713435 Binary files /dev/null and b/platform/docs/docs/assets/img/promptDiscardSeriesContent.png differ diff --git a/platform/docs/docs/assets/img/promptDiscardStudyContent.png b/platform/docs/docs/assets/img/promptDiscardStudyContent.png new file mode 100644 index 000000000..7cbafd216 Binary files /dev/null and b/platform/docs/docs/assets/img/promptDiscardStudyContent.png differ diff --git a/platform/docs/docs/assets/img/promptRtstructContent.png b/platform/docs/docs/assets/img/promptRtstructContent.png new file mode 100644 index 000000000..71af4eb47 Binary files /dev/null and b/platform/docs/docs/assets/img/promptRtstructContent.png differ diff --git a/platform/docs/docs/assets/img/promptSRTrackingContent.png b/platform/docs/docs/assets/img/promptSRTrackingContent.png new file mode 100644 index 000000000..365f62411 Binary files /dev/null and b/platform/docs/docs/assets/img/promptSRTrackingContent.png differ diff --git a/platform/docs/docs/assets/img/promptTrackStudyContent.png b/platform/docs/docs/assets/img/promptTrackStudyContent.png new file mode 100644 index 000000000..d5a2aa261 Binary files /dev/null and b/platform/docs/docs/assets/img/promptTrackStudyContent.png differ diff --git a/platform/docs/docs/assets/img/promptsegmentationContent.png b/platform/docs/docs/assets/img/promptsegmentationContent.png new file mode 100644 index 000000000..0091b988b Binary files /dev/null and b/platform/docs/docs/assets/img/promptsegmentationContent.png differ diff --git a/platform/docs/docs/configuration/configurationFiles.md b/platform/docs/docs/configuration/configurationFiles.md index d3289dbb0..be6829c74 100644 --- a/platform/docs/docs/configuration/configurationFiles.md +++ b/platform/docs/docs/configuration/configurationFiles.md @@ -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 diff --git a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx index 6a8cefe73..a7dd8c8a5 100644 --- a/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx +++ b/platform/docs/docs/platform/services/customization-service/sampleCustomizations.tsx @@ -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 = [ diff --git a/platform/i18n/src/locales/en-US/MeasurementTable.json b/platform/i18n/src/locales/en-US/MeasurementTable.json index 008a8c8a4..424317274 100644 --- a/platform/i18n/src/locales/en-US/MeasurementTable.json +++ b/platform/i18n/src/locales/en-US/MeasurementTable.json @@ -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?" }