[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 <igoroctaviano@gmail.com> Co-authored-by: Danny Brown <danny.ri.brown@gmail.com>
This commit is contained in:
15 files changed
+236
-41
No files matched your search
@@ -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 },
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
|
||||
@@ -191,7 +191,6 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
||||
id: 'enter-annotation',
|
||||
centralize: true,
|
||||
isDraggable: false,
|
||||
useLastPosition: false,
|
||||
showOverlay: true,
|
||||
content: Dialog,
|
||||
contentProps: {
|
||||
|
||||
+118
-20
@@ -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: () => (
|
||||
<div className="p-4 bg-primary-dark text-white">
|
||||
<p>This is a destructive action.</p>
|
||||
<p>Are you sure you want to continue?</p>
|
||||
</div>
|
||||
),
|
||||
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);
|
||||
|
||||
@@ -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({
|
||||
<PanelStudyBrowserTracking
|
||||
MeasurementService={servicesManager.services.MeasurementService}
|
||||
DisplaySetService={servicesManager.services.DisplaySetService}
|
||||
UIDialogService={servicesManager.services.UIDialogService}
|
||||
UINotificationService={servicesManager.services.UINotificationService}
|
||||
dataSource={dataSource}
|
||||
getImageSrc={_getImageSrcFromImageId}
|
||||
getStudiesForPatientByStudyInstanceUID={
|
||||
|
||||
Reference in new issue
Block a user