From 397aa4d0e361e95dcbd83e0557a9cd84c0b8440a Mon Sep 17 00:00:00 2001 From: Ghadeer Albattarni <165973963+GhadeerAlbattarni@users.noreply.github.com> Date: Tue, 12 May 2026 13:22:25 -0400 Subject: [PATCH] fix(measurement-tracking): restore tracked state on undo after Delete all (#5994) * fix: always show delete confirmation when measurements exist --- .../cornerstone/src/initMeasurementService.ts | 29 +++- .../default/src/ViewerLayout/ViewerHeader.tsx | 2 + .../TrackedMeasurementsContext.tsx | 25 +++- .../measurementTrackingMachine.js | 11 ++ .../panels/PanelMeasurementTableTracking.tsx | 35 ++--- .../MeasurementService/MeasurementService.ts | 6 +- tests/MeasurementPanel.spec.ts | 139 +++++++++++++++++- tests/pages/MainToolbarPageObject.ts | 18 +++ tests/pages/RightPanelPageObject.ts | 1 + tests/pages/ViewportPageObject.ts | 2 +- 10 files changed, 238 insertions(+), 30 deletions(-) diff --git a/extensions/cornerstone/src/initMeasurementService.ts b/extensions/cornerstone/src/initMeasurementService.ts index bedf84ecd..7837aa8d4 100644 --- a/extensions/cornerstone/src/initMeasurementService.ts +++ b/extensions/cornerstone/src/initMeasurementService.ts @@ -1,4 +1,4 @@ -import { eventTarget, Types } from '@cornerstonejs/core'; +import { eventTarget, Types, utilities as csUtils } from '@cornerstonejs/core'; import { Enums, annotation, cancelActiveManipulations } from '@cornerstonejs/tools'; import { DicomMetadataStore } from '@ohif/core'; @@ -17,6 +17,8 @@ const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } const { removeAnnotation } = annotation.state; const csToolsEvents = Enums.Events; +const { DefaultHistoryMemo } = csUtils.HistoryMemo; + const initMeasurementService = ( measurementService, displaySetService, @@ -354,7 +356,7 @@ const connectMeasurementServiceToTools = ({ const { MEASUREMENT_REMOVED, MEASUREMENTS_CLEARED, MEASUREMENT_UPDATED, RAW_MEASUREMENT_ADDED } = measurementService.EVENTS; - measurementService.subscribe(MEASUREMENTS_CLEARED, ({ measurements }) => { + measurementService.subscribe(MEASUREMENTS_CLEARED, ({ measurements, trackingContext }) => { if (!Object.keys(measurements).length) { return; } @@ -373,6 +375,29 @@ const connectMeasurementServiceToTools = ({ options: { deleting: true }, }); } + + // If tracking context was provided, push a memo that keeps XState in sync with + // Cornerstone's annotation history across unlimited undo/redo cycles: + // undo → re-populate trackedStudy/trackedSeries so the panel reflects the + // restored annotations. + // redo → wipe trackedStudy/trackedSeries because the annotations have been + // re-deleted by their own Cornerstone memos; no measurements are + // deleted here (they are already gone), so CLEAR_TRACKING_CONTEXT is + // used instead of UNTRACK_ALL to avoid a double-delete. + if (trackingContext) { + DefaultHistoryMemo.push({ + id: csUtils.uuidv4(), + operationType: 'trackingState', + restoreMemo(undo?: boolean) { + if (undo === true) { + commandsManager.run('restoreTrackedSeries', trackingContext); + } else if (undo === false) { + commandsManager.run('clearTrackedSeries'); + } + }, + }); + } + commandsManager.run('endRecordingForAnnotationGroup'); // trigger a render diff --git a/extensions/default/src/ViewerLayout/ViewerHeader.tsx b/extensions/default/src/ViewerLayout/ViewerHeader.tsx index eb3b7789d..e2c0a34e6 100644 --- a/extensions/default/src/ViewerLayout/ViewerHeader.tsx +++ b/extensions/default/src/ViewerLayout/ViewerHeader.tsx @@ -101,6 +101,7 @@ function ViewerHeader({ appConfig }: withAppTypes<{ appConfig: AppTypes.Config }