From 68332701abc675a7a41c12d4f0c0e2540088e0b0 Mon Sep 17 00:00:00 2001 From: James Petts Date: Wed, 19 Aug 2020 20:27:34 +0100 Subject: [PATCH] [OHIF-309] - Users should be able to delete an SR from the side panel when the behavior is supported by the data source (#1952) * Remove trailing zeroes from patient age * WIP reject + remove displaySet. * ohif-316: Update DICOM SR thumbnail design to include a delete action (#1948) * add "supportsReject" to netlify config Co-authored-by: Igor Octaviano Co-authored-by: Danny Brown --- extensions/cornerstone/src/init.js | 3 +- .../src/DicomWebDataSource/dcm4cheeReject.js | 35 +++++ .../default/src/DicomWebDataSource/index.js | 13 +- .../dicom-sr/src/OHIFCornerstoneSRViewport.js | 17 +++ .../PanelMeasurementTableTracking/index.js | 1 - .../PanelStudyBrowserTracking.jsx | 138 +++++++++++++++--- .../PanelStudyBrowserTracking/index.jsx | 6 +- .../core/src/DataSources/IWebApiDataSource.js | 4 + .../DisplaySetService/DisplaySetService.js | 20 +++ .../src/services/DisplaySetService/EVENTS.js | 1 + platform/ui/src/components/Dialog/Header.js | 13 +- .../ThumbnailList/ThumbnailList.jsx | 9 +- .../ThumbnailNoImage/ThumbnailNoImage.jsx | 11 +- platform/viewer/public/config/default.js | 3 +- platform/viewer/public/config/netlify.js | 3 +- 15 files changed, 236 insertions(+), 41 deletions(-) create mode 100644 extensions/default/src/DicomWebDataSource/dcm4cheeReject.js diff --git a/extensions/cornerstone/src/init.js b/extensions/cornerstone/src/init.js index b10bff02f..e65efa36b 100644 --- a/extensions/cornerstone/src/init.js +++ b/extensions/cornerstone/src/init.js @@ -236,9 +236,8 @@ export default function init({ id: dialogId, centralize: true, isDraggable: false, - content: Dialog, - useLastPosition: false, showOverlay: true, + content: Dialog, contentProps: { title: 'Enter your annotation', value: { label }, diff --git a/extensions/default/src/DicomWebDataSource/dcm4cheeReject.js b/extensions/default/src/DicomWebDataSource/dcm4cheeReject.js new file mode 100644 index 000000000..542267abe --- /dev/null +++ b/extensions/default/src/DicomWebDataSource/dcm4cheeReject.js @@ -0,0 +1,35 @@ +export default function (wadoRoot) { + return { + series: (StudyInstanceUID, SeriesInstanceUID) => { + return new Promise((resolve, reject) => { + // Reject because of Quality. (Seems the most sensible out of the options) + const CodeValueAndCodeSchemeDesignator = `113001%5EDCM`; + + const url = `${wadoRoot}/studies/${StudyInstanceUID}/series/${SeriesInstanceUID}/reject/${CodeValueAndCodeSchemeDesignator}`; + + const xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + + //Send the proper header information along with the request + // TODO -> Auth when we re-add authorization. + + console.log(xhr); + + xhr.onreadystatechange = function () { + //Call a function when the state changes. + if (xhr.readyState == 4) { + switch (xhr.status) { + case 204: + resolve(xhr.responseText); + + break; + case 404: + reject('Your dataSource does not support reject functionality'); + } + } + }; + xhr.send(); + }); + }, + }; +} diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index 4fa32d5e5..fa8cdfda0 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -6,6 +6,7 @@ import { processResults, processSeriesResults, } from './qido.js'; +import dcm4cheeReject from './dcm4cheeReject'; import { DicomMetadataStore, IWebApiDataSource, utils } from '@ohif/core'; import getImageId from './utils/getImageId'; @@ -34,6 +35,7 @@ const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1'; * @param {boolean} qidoSupportsIncludeField - Whether QIDO supports the "Include" option to request additional fields in response * @param {string} imageRengering - wadors | ? (unsure of where/how this is used) * @param {string} thumbnailRendering - wadors | ? (unsure of where/how this is used) + * @param {bool} supportsReject - Whether the server supports reject calls (i.e. DCM4CHEE) * @param {bool} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking */ function createDicomWebApi(dicomWebConfig) { @@ -43,6 +45,7 @@ function createDicomWebApi(dicomWebConfig) { enableStudyLazyLoad, supportsFuzzyMatching, supportsWildcard, + supportsReject, } = dicomWebConfig; const qidoConfig = { @@ -59,7 +62,7 @@ function createDicomWebApi(dicomWebConfig) { const qidoDicomWebClient = new api.DICOMwebClient(qidoConfig); const wadoDicomWebClient = new api.DICOMwebClient(wadoConfig); - return IWebApiDataSource.create({ + const implementation = { query: { studies: { mapParams: mapParams.bind(), @@ -234,7 +237,13 @@ function createDicomWebApi(dicomWebConfig) { return imageIds; }, - }); + }; + + if (supportsReject) { + implementation.reject = dcm4cheeReject(wadoRoot); + } + + return IWebApiDataSource.create(implementation); } export { createDicomWebApi }; diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index 22ddc0ec2..bfda0b808 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -43,6 +43,23 @@ function OHIFCornerstoneSRViewport({ const [isHydrated, setIsHydrated] = useState(displaySet.isHydrated); const { viewports, activeViewportIndex } = viewportGrid; + useEffect(() => { + const onDisplaySetsRemovedSubscription = DisplaySetService.subscribe( + DisplaySetService.EVENTS.DISPLAY_SETS_REMOVED, ({ displaySetInstanceUIDs }) => { + const activeViewport = viewports[activeViewportIndex]; + if (displaySetInstanceUIDs.includes(activeViewport.displaySetInstanceUID)) { + viewportGridService.setDisplaysetForViewport({ + viewportIndex: activeViewportIndex, + displaySetInstanceUID: undefined, + }); + } + }); + + return () => { + onDisplaySetsRemovedSubscription.unsubscribe(); + }; + }, []); + // Optional hook into tracking extension, if present. let trackedMeasurements; let sendTrackedMeasurementsEvent; diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index cfb617a10..d298fc1a5 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -191,7 +191,6 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { id: 'enter-annotation', centralize: true, isDraggable: false, - useLastPosition: false, showOverlay: true, content: Dialog, contentProps: { diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx index 65390acf5..b0d58a289 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import { utils } from '@ohif/core'; -import { StudyBrowser, useImageViewer, useViewportGrid } from '@ohif/ui'; +import { StudyBrowser, useImageViewer, useViewportGrid, Dialog } from '@ohif/ui'; import { useTrackedMeasurements } from '../../getContextModule'; const { formatDate } = utils; @@ -13,6 +13,8 @@ const { formatDate } = utils; function PanelStudyBrowserTracking({ MeasurementService, DisplaySetService, + UIDialogService, + UINotificationService, getImageSrc, getStudiesForPatientByStudyInstanceUID, requestDisplaySetCreationForStudy, @@ -136,7 +138,11 @@ function PanelStudyBrowserTracking({ thumbnailImageSrcMap, trackedSeries, viewports, - isSingleViewport + isSingleViewport, + dataSource, + DisplaySetService, + UIDialogService, + UINotificationService ); setDisplaySets(mappedDisplaySets); @@ -146,6 +152,7 @@ function PanelStudyBrowserTracking({ trackedSeries, thumbnailImageSrcMap, viewports, + dataSource, ]); // ~~ subscriptions --> displaySets @@ -193,7 +200,11 @@ function PanelStudyBrowserTracking({ thumbnailImageSrcMap, trackedSeries, viewports, - isSingleViewport + isSingleViewport, + dataSource, + DisplaySetService, + UIDialogService, + UINotificationService ); setDisplaySets(mappedDisplaySets); @@ -227,10 +238,10 @@ function PanelStudyBrowserTracking({ ); const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy ? [ - ...expandedStudyInstanceUIDs.filter( - stdyUid => stdyUid !== StudyInstanceUID - ), - ] + ...expandedStudyInstanceUIDs.filter( + stdyUid => stdyUid !== StudyInstanceUID + ), + ] : [...expandedStudyInstanceUIDs, StudyInstanceUID]; setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); @@ -304,7 +315,7 @@ function PanelStudyBrowserTracking({ SeriesInstanceUID: displaySet.SeriesInstanceUID, }); }} - onClickThumbnail={() => {}} + onClickThumbnail={() => { }} onDoubleClickThumbnail={onDoubleClickThumbnailHandler} activeDisplaySetInstanceUID={activeDisplaySetInstanceUID} /> @@ -359,8 +370,13 @@ function _mapDisplaySets( thumbnailImageSrcMap, trackedSeriesInstanceUIDs, viewports, // TODO: make array of `displaySetInstanceUIDs`? - isSingleViewport + isSingleViewport, + dataSource, + DisplaySetService, + UIDialogService, + UINotificationService ) { + console.log(displaySets.length); const thumbnailDisplaySets = []; const thumbnailNoImageDisplaySets = []; displaySets.forEach(ds => { @@ -369,19 +385,21 @@ function _mapDisplaySets( const viewportIdentificator = isSingleViewport ? [] : viewports.reduce((acc, viewportData, index) => { - if (viewportData.displaySetInstanceUID === ds.displaySetInstanceUID) { - acc.push(_viewportLabels[index]); - } - return acc; - }, []); + if (viewportData.displaySetInstanceUID === ds.displaySetInstanceUID) { + acc.push(_viewportLabels[index]); + } + return acc; + }, []); const array = componentType === 'thumbnailTracked' ? thumbnailDisplaySets : thumbnailNoImageDisplaySets; - array.push({ - displaySetInstanceUID: ds.displaySetInstanceUID, + const { displaySetInstanceUID } = ds; + + const thumbnailProps = { + displaySetInstanceUID, description: ds.SeriesDescription, seriesNumber: ds.SeriesNumber, modality: ds.Modality, @@ -392,12 +410,71 @@ function _mapDisplaySets( imageSrc, dragData: { type: 'displayset', - displaySetInstanceUID: ds.displaySetInstanceUID, + displaySetInstanceUID, // .. Any other data to pass }, isTracked: trackedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID), viewportIdentificator, - }); + }; + + if (componentType === 'thumbnailNoImage') { + if (dataSource.reject && dataSource.reject.series) { + thumbnailProps.canReject = true; + thumbnailProps.onReject = () => { + UIDialogService.create({ + id: 'ds-reject-sr', + centralize: true, + isDraggable: false, + showOverlay: true, + content: Dialog, + contentProps: { + title: 'Reject Report', + body: () => ( +
+

This is a destructive action.

+

Are you sure you want to continue?

+
+ ), + actions: [ + { id: 'cancel', text: 'Cancel', type: 'secondary' }, + { id: 'save', text: 'Save', type: 'primary' }, + ], + onClose: () => UIDialogService.dismiss({ id: 'ds-reject-sr' }), + onSubmit: async ({ action }) => { + switch (action.id) { + case 'save': + try { + await dataSource.reject.series(ds.StudyInstanceUID, ds.SeriesInstanceUID); + DisplaySetService.deleteDisplaySet(displaySetInstanceUID); + UIDialogService.dismiss({ id: 'ds-reject-sr' }); + UINotificationService.show({ + title: 'Reject Report', + message: 'Report rejected successfully', + type: 'success', + }); + } catch (error) { + UIDialogService.dismiss({ id: 'ds-reject-sr' }); + UINotificationService.show({ + title: 'Reject Report', + message: 'Failed to reject report', + type: 'error', + }); + } + break; + case 'cancel': + UIDialogService.dismiss({ id: 'ds-reject-sr' }); + break; + } + }, + }, + }); + }; + } else { + thumbnailProps.canReject = false; + } + } + + array.push(thumbnailProps); }); return [...thumbnailDisplaySets, ...thumbnailNoImageDisplaySets]; @@ -442,14 +519,35 @@ function _createStudyBrowserTabs( const recentStudies = []; const allStudies = []; + // Iterate over each study... studyDisplayList.forEach(study => { - const displaySetsForStudy = utils.sortBySeriesDate(displaySets.filter( + // Find it's display sets + const displaySetsForStudy = displaySets.filter( ds => ds.StudyInstanceUID === study.studyInstanceUid - )); + ); + + // Sort them + const sortedDisplaySetsForStudy = utils.sortBySeriesDate(displaySetsForStudy); + + /* Sort by series number, then by series date + displaySetsForStudy.sort((a, b) => { + if (a.seriesNumber !== b.seriesNumber) { + return a.seriesNumber - b.seriesNumber; + } + + const seriesDateA = Date.parse(a.seriesDate); + const seriesDateB = Date.parse(b.seriesDate); + + return seriesDateA - seriesDateB; + }); + */ + + // Map the study to it's tab/view representation const tabStudy = Object.assign({}, study, { displaySets: displaySetsForStudy, }); + // Add the "tab study" to the 'primary', 'recent', and/or 'all' tab group(s) if (primaryStudyInstanceUIDs.includes(study.studyInstanceUid)) { primaryStudies.push(tabStudy); allStudies.push(tabStudy); diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.jsx index f7ac34814..dbb365567 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.jsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/index.jsx @@ -18,9 +18,7 @@ function WrappedPanelStudyBrowserTracking({ extensionManager, servicesManager, }) { - // TODO: This should be made available a different way; route should have - // already determined our datasource - const dataSource = extensionManager.getDataSources('dicomweb')[0]; + const dataSource = extensionManager.getActiveDataSource()[0]; const _getStudiesForPatientByStudyInstanceUID = getStudiesForPatientByStudyInstanceUID.bind( null, dataSource @@ -37,6 +35,8 @@ function WrappedPanelStudyBrowserTracking({ ds.displaySetInstanceUID === displaySetInstanceUID + ); + + const activeDisplaySetsIndex = activeDisplaySets.findIndex( + ds => ds.displaySetInstanceUID === displaySetInstanceUID + ); + + displaySetCache.splice(displaySetCacheIndex, 1); + activeDisplaySets.splice(activeDisplaySetsIndex, 1); + + this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets); + this._broadcastEvent(EVENTS.DISPLAY_SETS_REMOVED, { + displaySetInstanceUIDs: [displaySetInstanceUID], + }); + } + /** * @param {string} displaySetInstanceUID * @returns {object} displaySet diff --git a/platform/core/src/services/DisplaySetService/EVENTS.js b/platform/core/src/services/DisplaySetService/EVENTS.js index 6b964f703..44e07d1f6 100644 --- a/platform/core/src/services/DisplaySetService/EVENTS.js +++ b/platform/core/src/services/DisplaySetService/EVENTS.js @@ -1,6 +1,7 @@ const EVENTS = { DISPLAY_SETS_ADDED: 'event::displaySetService:displaySetsAdded', DISPLAY_SETS_CHANGED: 'event::displaySetService:displaySetsChanged', + DISPLAY_SETS_REMOVED: 'event::displaySetService:displaySetsRemoved', }; export default EVENTS; diff --git a/platform/ui/src/components/Dialog/Header.js b/platform/ui/src/components/Dialog/Header.js index 72d292b9b..f74e651ac 100644 --- a/platform/ui/src/components/Dialog/Header.js +++ b/platform/ui/src/components/Dialog/Header.js @@ -5,14 +5,13 @@ import PropTypes from 'prop-types'; import { Typography, Icon } from '..'; const CloseButton = ({ onClick }) => { - const theme = 'bg-transparent fill-primary-active'; - const outline = 'outline-none focus:outline-none'; - const flex = 'flex h-full'; - return ( - + ); }; diff --git a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx index 21ce687ea..0c37b8d8f 100644 --- a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx +++ b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx @@ -24,6 +24,8 @@ const ThumbnailList = ({ seriesDate, viewportIdentificator, isTracked, + canReject, + onReject, imageSrc, imageAltText, }) => { @@ -82,6 +84,8 @@ const ThumbnailList = ({ modalityTooltip={_getModalityTooltip(modality)} seriesDate={seriesDate} description={description} + canReject={canReject} + onReject={onReject} onClick={() => onThumbnailClick(displaySetInstanceUID)} onDoubleClick={() => onThumbnailDoubleClick(displaySetInstanceUID) @@ -113,7 +117,10 @@ ThumbnailList.propTypes = { 'thumbnailTracked', 'thumbnailNoImage', ]).isRequired, - viewportIdentificator: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + viewportIdentificator: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.array, + ]), isTracked: PropTypes.bool, /** * Data the thumbnail should expose to a receiving drop target. Use a matching diff --git a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx index 24322be6e..d5db2f37f 100644 --- a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx +++ b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.jsx @@ -12,12 +12,14 @@ const ThumbnailNoImage = ({ modalityTooltip, onClick, onDoubleClick, + canReject, + onReject, dragData, isActive, }) => { const [collectedProps, drag, dragPreview] = useDrag({ item: { ...dragData }, - canDrag: function(monitor) { + canDrag: function (monitor) { return Object.keys(dragData).length !== 0; }, }); @@ -51,8 +53,11 @@ const ThumbnailNoImage = ({ {seriesDate} -
- {description} +
+ {canReject && } +
+ {description} +
diff --git a/platform/viewer/public/config/default.js b/platform/viewer/public/config/default.js index ac5bcd8d1..bae63f6ef 100644 --- a/platform/viewer/public/config/default.js +++ b/platform/viewer/public/config/default.js @@ -16,11 +16,12 @@ window.config = { qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', qidoSupportsIncludeField: true, + supportsReject: true, imageRendering: 'wadors', thumbnailRendering: 'wadors', enableStudyLazyLoad: true, supportsFuzzyMatching: true, - supportsWildcard: true + supportsWildcard: true, }, }, ], diff --git a/platform/viewer/public/config/netlify.js b/platform/viewer/public/config/netlify.js index 7b6f0a7a0..b8134cb3c 100644 --- a/platform/viewer/public/config/netlify.js +++ b/platform/viewer/public/config/netlify.js @@ -16,11 +16,12 @@ window.config = { qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs', qidoSupportsIncludeField: true, + supportsReject: true, imageRendering: 'wadors', thumbnailRendering: 'wadors', enableStudyLazyLoad: true, supportsFuzzyMatching: true, - supportsWildcard: true + supportsWildcard: true, }, }, ],