diff --git a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js index a0043fefcda..3eddd4371d5 100644 --- a/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js +++ b/extensions/cornerstone-dicom-seg/src/getSopClassHandlerModule.js @@ -121,7 +121,7 @@ function _load(segDisplaySet, servicesManager, extensionManager, headers) { }); } - const suppressEvents = true; + const suppressEvents = false; segmentationService .createSegmentationForSEGDisplaySet(segDisplaySet, null, suppressEvents) .then(() => { diff --git a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx index 141bb8f5935..f96acbf0d09 100644 --- a/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx +++ b/extensions/cornerstone-dicom-seg/src/panels/PanelSegmentation.tsx @@ -1,5 +1,5 @@ import { createReportAsync } from '@ohif/extension-default'; -import React, { useEffect, useState, useCallback, useReducer } from 'react'; +import React, { useEffect, useState, useCallback, useReducer, useRef } from 'react'; import PropTypes from 'prop-types'; import { SegmentationGroupTable, LegacyButtonGroup, LegacyButton } from '@ohif/ui'; @@ -33,8 +33,18 @@ export default function PanelSegmentation({ uiDialogService, displaySetService, userAuthenticationService, - CropDisplayAreaService, + CacheAPIService, + uiViewportDialogService, } = servicesManager.services; + const utilityModule = extensionManager.getModuleEntry( + '@gradienthealth/ohif-gradienthealth-extension.utilityModule.version' + ); + const { + getObjectVersions, + confirmSEGVersionRestore, + restoreObjectVersion, + parseUrlToBucketAndFileName, + } = utilityModule.exports; const { t } = useTranslation('PanelSegmentation'); @@ -44,7 +54,9 @@ export default function PanelSegmentation({ ); const [segmentations, setSegmentations] = useState(() => segmentationService.getSegmentations()); + const [versionsMap, setVersionsMap] = useState(new Map()); const [savedStatusStates, dispatch] = useReducer(savedStatusReducer, {}); + const componentWillUnMount = useRef(false); useEffect(() => { // ~~ Subscription @@ -69,6 +81,20 @@ export default function PanelSegmentation({ }; }, []); + useEffect(() => { + updateVersions(segmentations); + const { unsubscribe } = segmentationService.subscribe( + segmentationService.EVENTS.SEGMENTATION_ADDED, + ({ segmentation: newSegmentation }) => { + updateVersions([newSegmentation]); + } + ); + + return () => { + unsubscribe(); + }; + }, []); + useEffect(() => { let changedSegmentations: any[] = [], timerId; @@ -125,6 +151,7 @@ export default function PanelSegmentation({ const savedSegmentations = Object.keys(payload).filter( id => payload[id] === SAVED_STATUS_ICON.SAVED ); + updateVersions(changedSegmentations); changedSegmentations = changedSegmentations.filter( cs => !savedSegmentations.includes(cs.id) ); @@ -138,6 +165,30 @@ export default function PanelSegmentation({ }; }, []); + useEffect(() => { + return () => { + componentWillUnMount.current = true; + }; + }, []); + + useEffect(() => { + const loadActiveSegLiveVersion = () => { + const activeSegmentation = segmentations?.find(segmentation => segmentation.isActive); + if (activeSegmentation) { + const liveVersion = versionsMap + .get(activeSegmentation.id) + ?.find(version => !version.timeDeleted); + liveVersion && onVersionClick(activeSegmentation.id, liveVersion); + } + }; + + return () => { + if (componentWillUnMount.current) { + loadActiveSegLiveVersion(); + } + }; + }, [segmentations, versionsMap]); + const setSegmentationActive = segmentationId => { setReferencedDisplaySet(segmentationId); @@ -153,6 +204,14 @@ export default function PanelSegmentation({ // Set referenced displaySet of the segmentation to the viewport // if it is not displayed in any of the viewports. const setReferencedDisplaySet = segmentationId => { + const activeSegmentation = segmentations.find(segmentation => segmentation.isActive); + if (activeSegmentation.id !== segmentationId) { + const liveVersion = versionsMap + .get(activeSegmentation.id) + ?.find(version => !version.timeDeleted); + liveVersion && onVersionClick(activeSegmentation.id, liveVersion); + } + const segDisplayset = displaySetService.getDisplaySetByUID(segmentationId); if (!segDisplayset) { return; @@ -346,6 +405,7 @@ export default function PanelSegmentation({ }); dispatch({ payload: { [segmentationId]: SAVED_STATUS_ICON.SAVED } }); + updateVersions([segmentations.find(segmentation => segmentation.id === segmentationId)]); } catch (error) { console.warn(error.message); dispatch({ payload: { [segmentationId]: SAVED_STATUS_ICON.ERROR } }); @@ -372,6 +432,109 @@ export default function PanelSegmentation({ }); }; + const onVersionClick = (segmentationId, version) => { + const headers = userAuthenticationService.getAuthorizationHeader(); + const displaySet = displaySetService.getDisplaySetByUID(segmentationId); + const referencedDisplaySetInstanceUID = displaySet.referencedDisplaySetInstanceUID; + const referencedDisplaySet = displaySetService.getDisplaySetByUID( + referencedDisplaySetInstanceUID + ); + const { activeViewportId } = viewportGridService.getState(); + + const url = new URL(displaySet.instances[0].url); + + if (url.searchParams.get('generation') === version.generation) { + return; + } + + url.searchParams.set('generation', version.generation); + const newUrl = url.toString(); + const { bucket, fileName } = parseUrlToBucketAndFileName(newUrl); + const imageIdToFileUriMap = CacheAPIService.getImageIdToFileUriMap(); + const imageId = imageIdToFileUriMap.get(newUrl) || newUrl; + + displaySet.instances[0].url = displaySet.instance.url = newUrl; + displaySet.instance.imageId = imageId; + displaySet.instance.getImageId = () => imageId; + + const liveVersion = versionsMap.get(segmentationId).find(version => !version.timeDeleted); + displaySet.isLoaded = false; + const toolGroupIds = getToolGroupIds(segmentationId); + segmentationService.remove(segmentationId); + + displaySet + .load({ headers: headers }) + .then(() => { + const promises = toolGroupIds.map(toolGroupId => + segmentationService.addSegmentationRepresentationToToolGroup( + toolGroupId, + segmentationId, + true + ) + ); + return Promise.all(promises); + }) + .then(async () => { + referencedDisplaySet.needsRerendering = true; + viewportGridService.setDisplaySetsForViewport({ + viewportId: activeViewportId, + displaySetInstanceUIDs: [referencedDisplaySetInstanceUID], + }); + + if (liveVersion.generation === version.generation) { + return; + } + + return confirmSEGVersionRestore(activeViewportId, servicesManager); + }) + .then(status => { + switch (status) { + case 1: + restoreObjectVersion(bucket, fileName, version.generation, headers).then(() => + updateVersions([ + segmentations.find(segmentation => segmentation.id === segmentationId), + ]) + ); + break; + case 0: + onVersionClick(segmentationId, liveVersion); + break; + default: + uiViewportDialogService.hide(); // This case is when we are load live version. + } + }) + .catch(error => console.warn(error)); + }; + + const updateVersions = updatedSegmentations => { + const headers = userAuthenticationService.getAuthorizationHeader(); + const newVersionsMap = new Map(); + + const promises = updatedSegmentations.map(async segmentation => { + const displaySet = displaySetService.getDisplaySetByUID(segmentation.id); + if (displaySet) { + const url = new URL(displaySet.instances[0].url); + url.searchParams.delete('generation'); + const { bucket, fileName } = parseUrlToBucketAndFileName(url.toString()); + return getObjectVersions(bucket, fileName, headers).then(versions => ({ + id: segmentation.id, + versions, + })); + } + }); + + Promise.all(promises).then(results => { + results.forEach(result => { + if (result) { + newVersionsMap.set(result.id, result.versions); + } + }); + setVersionsMap( + prevState => new Map([...Array.from(prevState), ...Array.from(newVersionsMap)]) + ); + }); + }; + const params = new URLSearchParams(window.location.search); const showAddSegmentation = params.get('disableAddSegmentation') !== 'true'; @@ -381,6 +544,7 @@ export default function PanelSegmentation({ @@ -427,7 +592,7 @@ export default function PanelSegmentation({ setFillAlphaInactive={value => _setSegmentationConfiguration(selectedSegmentationId, 'fillAlphaInactive', value) } - CropDisplayAreaService={CropDisplayAreaService} + servicesManager={servicesManager} /> diff --git a/platform/ui/src/components/ObjectVersionList/ObjectVersionList.tsx b/platform/ui/src/components/ObjectVersionList/ObjectVersionList.tsx new file mode 100644 index 00000000000..16369378f05 --- /dev/null +++ b/platform/ui/src/components/ObjectVersionList/ObjectVersionList.tsx @@ -0,0 +1,77 @@ +import React, { useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment'; + +export default function ObjectVersionsList({ show, versions, onVersionSelect, onClose }) { + const element = useRef(null); + + const handleClick = e => { + if (element.current && !element.current.contains(e.target)) { + onClose(); + } + }; + + useEffect(() => { + document.addEventListener('click', handleClick); + + if (!show) { + document.removeEventListener('click', handleClick); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [show]); + + const filteredVersions = versions.sort( + (a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime() + ); + + return ( + <> + {show ? ( +
+
+ {filteredVersions.length ? ( + filteredVersions.map((version, index) => ( +
{ + evt.preventDefault(); + onVersionSelect(version); + }} + > + {index === 0 + ? 'Current Version' + : moment(version.updated).format('MMMM D, h:mm A')} +
+ )) + ) : ( +
+ No noncurrent versions Found +
+ )} +
+
+ ) : null} + + ); +} + +ObjectVersionsList.propTypes = { + show: PropTypes.bool, + versions: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + generation: PropTypes.string, + updated: PropTypes.string, + }) + ).isRequired, + onVersionSelect: PropTypes.func, + onClose: PropTypes.func, +}; diff --git a/platform/ui/src/components/ObjectVersionList/index.ts b/platform/ui/src/components/ObjectVersionList/index.ts new file mode 100644 index 00000000000..6ccb7beb432 --- /dev/null +++ b/platform/ui/src/components/ObjectVersionList/index.ts @@ -0,0 +1,2 @@ +import ObjectVersionsList from './ObjectVersionList'; +export default ObjectVersionsList; diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx index 401a95b3505..65f040225ac 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationDropDownRow.tsx @@ -1,10 +1,11 @@ -import React from 'react'; -import { Icon, Dropdown } from '../../components'; +import React, { useState } from 'react'; +import { Icon, Dropdown, ObjectVersionsList } from '../../components'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; function SegmentationDropDownRow({ segmentation, + versions, savedStatusState, activeSegmentationId, disableEditing, @@ -18,16 +19,34 @@ function SegmentationDropDownRow({ onSegmentationDelete, onSegmentAdd, onToggleShowSegments, + onVersionClick, showSegments, + CacheAPIService, }) { + const [showVersionHistory, setShowVersionHistory] = useState(false); const { t } = useTranslation('SegmentationTable'); if (!segmentation) { return null; } + const cacheVersions = () => { + const versionUrls = + versions?.map(version => { + return `dicomweb:https://storage.googleapis.com/${version.bucket}/${version.name}?generation=${version.generation}`; + }) || []; + + CacheAPIService.cacheFiles(versionUrls); + }; + return (
+ onVersionClick(segmentation.id, version)} + onClose={() => setShowVersionHistory(false)} + />
{ e.stopPropagation(); @@ -78,6 +97,14 @@ function SegmentationDropDownRow({ ] : []), ...[ + { + title: t('Show Version History'), + onClick: () => { + cacheVersions(); + onSegmentationClick(segmentation.id); + setShowVersionHistory(true); + }, + }, { title: t('Download DICOM SEG'), onClick: () => { @@ -157,6 +184,7 @@ SegmentationDropDownRow.propTypes = { label: PropTypes.string.isRequired, isVisible: PropTypes.bool.isRequired, }), + versions: PropTypes.array, savedStatusState: PropTypes.string, activeSegmentationId: PropTypes.string, disableEditing: PropTypes.bool, @@ -170,7 +198,9 @@ SegmentationDropDownRow.propTypes = { onSegmentationDelete: PropTypes.func, onSegmentAdd: PropTypes.func, onToggleShowSegments: PropTypes.func, + onVersionClick: PropTypes.func, showSegments: PropTypes.bool, + CacheAPIService: PropTypes.any, }; SegmentationDropDownRow.defaultProps = { diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroup.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroup.tsx index cea71e8f14e..faf969f27ad 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroup.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroup.tsx @@ -6,6 +6,7 @@ import SegmentationGroupSegment from './SegmentationGroupSegment'; const SegmentationGroup = ({ segmentation, + versions, savedStatusState, activeSegmentationId, disableEditing, @@ -24,9 +25,12 @@ const SegmentationGroup = ({ onToggleSegmentVisibility, onToggleSegmentLock, onSegmentColorClick, + onVersionClick, showDeleteSegment, - CropDisplayAreaService, + servicesManager, }) => { + const { CacheAPIService, CropDisplayAreaService } = servicesManager.services; + const [showSegments, toggleShowSegments] = useState(true); return ( @@ -37,6 +41,7 @@ const SegmentationGroup = ({
{showSegments && ( @@ -111,6 +118,7 @@ SegmentationGroup.propTypes = { }) ), }), + versions: PropTypes.array, savedStatusState: PropTypes.string, activeSegmentationId: PropTypes.string, disableEditing: PropTypes.bool, @@ -130,7 +138,8 @@ SegmentationGroup.propTypes = { onToggleSegmentVisibility: PropTypes.func.isRequired, onToggleSegmentLock: PropTypes.func.isRequired, onSegmentColorClick: PropTypes.func.isRequired, - CropDisplayAreaService: PropTypes.any, + onVersionClick: PropTypes.func, + servicesManager: PropTypes.any, }; export default SegmentationGroup; diff --git a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx index a7a22f23aeb..7c5529e6455 100644 --- a/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx +++ b/platform/ui/src/components/SegmentationGroupTable/SegmentationGroupTable.tsx @@ -9,6 +9,7 @@ import SegmentationGroup from './SegmentationGroup'; const SegmentationGroupTable = ({ segmentations, + versionsMap, savedStatusStates, // segmentation initial config segmentationConfig, @@ -34,6 +35,8 @@ const SegmentationGroupTable = ({ onToggleSegmentVisibility, onToggleSegmentLock, onSegmentColorClick, + // object version handlers + onVersionClick, // segmentation config handlers setFillAlpha, setFillAlphaInactive, @@ -42,7 +45,7 @@ const SegmentationGroupTable = ({ setRenderFill, setRenderInactiveSegmentations, setRenderOutline, - CropDisplayAreaService, + servicesManager, }) => { const [isConfigOpen, setIsConfigOpen] = useState(false); const [activeSegmentationId, setActiveSegmentationId] = useState(null); @@ -115,6 +118,7 @@ const SegmentationGroupTable = ({ key={segmentation.id} activeSegmentationId={activeSegmentationId} segmentation={segmentation} + versions={versionsMap?.get(segmentation.id)} savedStatusState={savedStatusStates[segmentation.id]} disableEditing={disableEditing} showAddSegment={showAddSegment} @@ -132,8 +136,9 @@ const SegmentationGroupTable = ({ onToggleSegmentVisibility={onToggleSegmentVisibility} onToggleSegmentLock={onToggleSegmentLock} onSegmentColorClick={onSegmentColorClick} + onVersionClick={onVersionClick} showDeleteSegment={showDeleteSegment} - CropDisplayAreaService={CropDisplayAreaService} + servicesManager={servicesManager} /> )) )} @@ -159,6 +164,7 @@ SegmentationGroupTable.propTypes = { ), }) ), + versionsMap: PropTypes.object, savedStatusStates: PropTypes.object, segmentationConfig: PropTypes.object.isRequired, disableEditing: PropTypes.bool, @@ -180,6 +186,7 @@ SegmentationGroupTable.propTypes = { onToggleSegmentVisibility: PropTypes.func.isRequired, onToggleSegmentLock: PropTypes.func.isRequired, onSegmentColorClick: PropTypes.func.isRequired, + onVersionClick: PropTypes.func, setFillAlpha: PropTypes.func.isRequired, setFillAlphaInactive: PropTypes.func.isRequired, setOutlineWidthActive: PropTypes.func.isRequired, @@ -187,7 +194,7 @@ SegmentationGroupTable.propTypes = { setRenderFill: PropTypes.func.isRequired, setRenderInactiveSegmentations: PropTypes.func.isRequired, setRenderOutline: PropTypes.func.isRequired, - CropDisplayAreaService: PropTypes.any, + servicesManager: PropTypes.any, }; SegmentationGroupTable.defaultProps = { diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js index b51bc92cf88..d07e9aa823b 100644 --- a/platform/ui/src/components/index.js +++ b/platform/ui/src/components/index.js @@ -79,6 +79,7 @@ import PanelSection from './PanelSection'; import AdvancedToolbox from './AdvancedToolbox'; import InputDoubleRange from './InputDoubleRange'; import LegacyButtonGroup from './LegacyButtonGroup'; +import ObjectVersionsList from './ObjectVersionList'; export { AboutModal, @@ -126,6 +127,7 @@ export { Modal, NavBar, Notification, + ObjectVersionsList, ProgressLoadingBar, PanelSection, Select,