Merge pull request #1836 from OHIF/feat/ohif-179

OHIF-179: UI for successful posting of SR data to PACS.
This commit is contained in:
Danny Brown 2020-06-30 14:07:04 -04:00 committed by GitHub
commit 4d2774c87b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 234 additions and 70 deletions

View File

@ -5,9 +5,6 @@ import { DicomMetadataStore, DICOMSR } from '@ohif/core';
import { useDebounce } from '@hooks'; import { useDebounce } from '@hooks';
import ActionButtons from './ActionButtons'; import ActionButtons from './ActionButtons';
import { useTrackedMeasurements } from '../../getContextModule'; import { useTrackedMeasurements } from '../../getContextModule';
import cornerstoneTools from 'cornerstone-tools';
import cornerstone from 'cornerstone-core';
import dcmjs from 'dcmjs';
const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
key: undefined, // key: undefined, //
@ -24,7 +21,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
measurementChangeTimestamp, measurementChangeTimestamp,
200 200
); );
const { MeasurementService, DisplaySetService } = servicesManager.services; const { MeasurementService, UINotificationService, UIDialogService, DisplaySetService } = servicesManager.services;
const [ const [
trackedMeasurements, trackedMeasurements,
sendTrackedMeasurementsEvent, sendTrackedMeasurementsEvent,
@ -118,27 +115,44 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
}; };
const createReport = async () => { const createReport = async () => {
const measurements = MeasurementService.getMeasurements(); const loadingDialogId = UIDialogService.create({
const trackedMeasurements = measurements.filter( showOverlay: true,
m => isDraggable: false,
trackedStudy === m.referenceStudyUID && centralize: true,
trackedSeries.includes(m.referenceSeriesUID) // TODO: Create a loading indicator component + zeplin design?
); content: () => <div className="text-primary-active">Loading...</div>
});
const dataSources = extensionManager.getDataSources(); try {
// TODO -> Eventually deal with multiple dataSources. const measurements = MeasurementService.getMeasurements();
// Would need some way of saying which one is the "push" dataSource const trackedMeasurements = measurements.filter(
const dataSource = dataSources[0]; m =>
trackedStudy === m.referenceStudyUID &&
trackedSeries.includes(m.referenceSeriesUID)
);
DICOMSR.storeMeasurements( const dataSources = extensionManager.getDataSources();
trackedMeasurements, // TODO -> Eventually deal with multiple dataSources.
dataSource, // Would need some way of saying which one is the "push" dataSource
naturalizedReport => { const dataSource = dataSources[0];
DisplaySetService.makeDisplaySets([naturalizedReport], {
madeInClient: true, const naturalizedReport = await DICOMSR.storeMeasurements(trackedMeasurements, dataSource);
});
} DisplaySetService.makeDisplaySets([naturalizedReport], { madeInClient: true });
); UINotificationService.show({
title: 'STOW SR',
message: 'Measurements saved successfully',
type: 'success'
});
} catch (error) {
UINotificationService.show({
title: 'STOW SR',
message: error.message || 'Failed to store measurements',
type: 'error',
});
} finally {
UIDialogService.dismiss({ id: loadingDialogId });
}
}; };
return ( return (
@ -155,7 +169,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
title="Measurements" title="Measurements"
amount={displayMeasurements.length} amount={displayMeasurements.length}
data={displayMeasurements} data={displayMeasurements}
onClick={() => {}} onClick={() => { }}
onEdit={id => alert(`Edit: ${id}`)} onEdit={id => alert(`Edit: ${id}`)}
/> />
</div> </div>

View File

@ -79,8 +79,9 @@ const generateReport = measurementData => {
* @param {object[]} measurementData An array of measurements from the measurements service * @param {object[]} measurementData An array of measurements from the measurements service
* that you wish to serialize. * that you wish to serialize.
* @param {object} dataSource The dataSource that you wish to use to persist the data. * @param {object} dataSource The dataSource that you wish to use to persist the data.
* @return {object} The naturalized report
*/ */
const storeMeasurements = async (measurementData, dataSource, onSuccess) => { const storeMeasurements = async (measurementData, dataSource) => {
// TODO -> Eventually use the measurements directly and not the dcmjs adapter, // TODO -> Eventually use the measurements directly and not the dcmjs adapter,
// But it is good enough for now whilst we only have cornerstone as a datasource. // But it is good enough for now whilst we only have cornerstone as a datasource.
log.info('[DICOMSR] storeMeasurements'); log.info('[DICOMSR] storeMeasurements');
@ -90,28 +91,22 @@ const storeMeasurements = async (measurementData, dataSource, onSuccess) => {
return Promise.reject({}); return Promise.reject({});
} }
const naturalizedReport = generateReport(measurementData);
const { StudyInstanceUID } = naturalizedReport;
try { try {
const naturalizedReport = generateReport(measurementData);
const { StudyInstanceUID } = naturalizedReport;
await dataSource.store.dicom(naturalizedReport); await dataSource.store.dicom(naturalizedReport);
if (StudyInstanceUID) { if (StudyInstanceUID) {
dataSource.deleteStudyMetadataPromise(StudyInstanceUID); dataSource.deleteStudyMetadataPromise(StudyInstanceUID);
} }
if (onSuccess) { return naturalizedReport;
onSuccess(naturalizedReport);
}
return {
message: 'Measurements saved successfully',
};
} catch (error) { } catch (error) {
log.error( log.error(
`[DICOMSR] Error while saving the measurements: ${error.message}` `[DICOMSR] Error while saving the measurements: ${error.message}`
); );
throw new Error('Error while saving the measurements.'); throw new Error(error.message || 'Error while saving the measurements.');
} }
}; };

View File

@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 19 19"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19 19">
<g fill="currentColor" fill-rule="evenodd"> <g fill="currentColor" fill-rule="evenodd">
<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"> <g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5">
<path d="M.188.187L8.813 8.812M8.813.187L.188 8.812" transform="translate(5 5)"/> <path d="M.188.187L8.813 8.812M8.813.187L.188 8.812" transform="translate(5 5)"/>

Before

Width:  |  Height:  |  Size: 355 B

After

Width:  |  Height:  |  Size: 332 B

View File

@ -113,7 +113,7 @@ const IconButton = ({
}; };
IconButton.defaultProps = { IconButton.defaultProps = {
onClick: () => {}, onClick: () => { },
color: 'default', color: 'default',
disabled: false, disabled: false,
fullWidth: false, fullWidth: false,

View File

@ -0,0 +1,115 @@
/* TODO: Create tailwind styles for this component */
.sb-topLeft {
@apply top-0 left-0 bottom-auto right-auto;
}
.sb-topCenter {
transform: translateX(-50%);
@apply top-0 bottom-auto left-1/2;
}
.sb-topRight {
@apply right-0 top-0 left-auto bottom-auto;
}
.sb-bottomLeft {
@apply right-auto left-0 bottom-0 top-auto;
}
.sb-bottomCenter {
@apply top-auto bottom-0 left-1/2;
transform: translateX(-50%);
}
.sb-bottomRight {
margin: 10px 0 0;
@apply top-auto bottom-0 left-auto right-0;
}
.sb-topLeft .sb-item,
.sb-topCenter .sb-item,
.sb-topRight .sb-item {
margin: 10px 0 0;
}
.sb-bottomLeft .sb-item,
.sb-bottomCenter .sb-item,
.sb-bottomRight .sb-item {
margin: 0 0 10px;
}
.sb-closeBtn {
text-shadow: none;
width: 20px;
height: 20px;
right: 5px;
top: 5px;
@apply overflow-hidden opacity-100 rounded-full p-1 bg-white cursor-pointer absolute text-center duration-300 transition-all ease-in-out;
}
.sb-closeBtn:hover {
background: #fff;
}
.sb-closeIcon {
@apply w-full relative overflow-hidden h-full block leading-none;
}
.sb-closeIcon:after,
.sb-closeIcon:before {
content: ' ';
height: 2px;
width: 12px;
@apply duration-300 transition-all ease-in-out block bg-black opacity-100 absolute;
}
.sb-closeIcon:before {
left: 4px;
top: 3px;
transform: rotate(45deg);
transform-origin: 0px 50%;
}
.sb-closeIcon:after {
right: 3px;
top: 5px;
transform: rotate(-45deg);
transform-origin: calc(100% - 3px) 50%;
}
.sb-title {
@apply break-normal text-lg font-bold;
}
.sb-message {
@apply break-normal text-base;
}
.sb-item {
animation: fadein 1s;
box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.2), 0 1px 18px 0 rgba(0, 0, 0, 0.12),
0 3px 5px -1px rgba(0, 0, 0, 0.14);
@apply relative p-5 text-white overflow-hidden rounded-md transition-height ease-in-out duration-300;
}
@keyframes fadein {
from {
top: 30px;
@apply opacity-0;
}
to {
@apply opacity-100 top-0;
}
}
/* Internet Explorer */
@-ms-keyframes fadein {
from {
top: 30px;
@apply opacity-0;
}
to {
@apply opacity-100;
top: 0;
}
}

View File

@ -2,16 +2,18 @@ import React from 'react';
import SnackbarItem from './SnackbarItem'; import SnackbarItem from './SnackbarItem';
import { useSnackbar } from '../../contextProviders'; import { useSnackbar } from '../../contextProviders';
import './Snackbar.css';
const SnackbarContainer = () => { const SnackbarContainer = () => {
const { snackbarItems, hide } = useSnackbar(); const { snackbarItems, hide } = useSnackbar();
const renderItem = item => { const renderItem = item => (
return <SnackbarItem key={item.itemId} options={item} onClose={hide} />; <SnackbarItem
}; key={item.itemId}
options={item}
if (!snackbarItems) { onClose={hide}
return null; />
} );
const renderItems = () => { const renderItems = () => {
const items = { const items = {
@ -23,11 +25,9 @@ const SnackbarContainer = () => {
bottomRight: [], bottomRight: [],
}; };
snackbarItems.map(item => { snackbarItems.forEach(item => items[item.position].push(item));
items[item.position].push(item);
});
return ( return snackbarItems && (
<div> <div>
{Object.keys(items).map(pos => { {Object.keys(items).map(pos => {
if (!items[pos].length) { if (!items[pos].length) {
@ -35,7 +35,7 @@ const SnackbarContainer = () => {
} }
return ( return (
<div key={pos} className={`sb-container sb-${pos}`}> <div key={pos} className={`fixed z-50 p-6 box-border h-auto sb-${pos}`}>
{items[pos].map((item, index) => ( {items[pos].map((item, index) => (
<div key={item.id + index}>{renderItem(item)}</div> <div key={item.id + index}>{renderItem(item)}</div>
))} ))}

View File

@ -1,25 +1,38 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import classNames from 'classnames';
import SnackbarTypes from './SnackbarTypes';
const SnackbarItem = ({ options, onClose }) => { const SnackbarItem = ({ options, onClose }) => {
const handleClose = () => { const handleClose = () => onClose(options.id);
onClose(options.id);
};
useEffect(() => { useEffect(() => {
if (options.autoClose) { if (options.autoClose) {
setTimeout(() => { setTimeout(() => handleClose(), options.duration);
handleClose();
}, options.duration);
} }
}, []); }, []);
const typeClasses = {
[SnackbarTypes.INFO]: 'bg-primary-active',
[SnackbarTypes.WARNING]: 'bg-yellow-600',
[SnackbarTypes.SUCCESS]: 'bg-green-600',
[SnackbarTypes.ERROR]: 'bg-red-600'
};
const hidden = 'duration-300 transition-all ease-in-out h-0 opacity-0 pt-0 mb-0 pb-0';
return ( return (
<div> <div
<span onClick={handleClose}> className={classNames(
<span>x</span> `${options.visible ? '' : hidden} sb-item`,
typeClasses[options.type]
)}
>
<span className="sb-closeBtn" onClick={handleClose}>
<span className="sb-closeIcon">x</span>
</span> </span>
{options.title && <div>{options.title}</div>} {options.title && <div className="sb-title">{options.title}</div>}
{options.message && <div>{options.message}</div>} {options.message && <div className="sb-message">{options.message}</div>}
</div> </div>
); );
}; };

View File

@ -0,0 +1,8 @@
/* TODO: Find a better way to set the cursor for all contents of dialog. */
.DraggableItem.draggable div {
cursor: grab !important;
}
.DraggableItem.draggable.dragging div {
cursor: grabbing !important;
}

View File

@ -5,11 +5,14 @@ import React, {
useCallback, useCallback,
useEffect, useEffect,
} from 'react'; } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
import classNames from 'classnames'; import classNames from 'classnames';
import { utils } from '@ohif/core'; import { utils } from '@ohif/core';
import './DialogProvider.css';
const DialogContext = createContext(null); const DialogContext = createContext(null);
@ -150,6 +153,7 @@ const DialogProvider = ({ children, service }) => {
onStart, onStart,
onStop, onStop,
onDrag, onDrag,
showOverlay,
} = dialog; } = dialog;
let position = let position =
@ -158,13 +162,13 @@ const DialogProvider = ({ children, service }) => {
position = centerPositions.find(position => position.id === id); position = centerPositions.find(position => position.id === id);
} }
return ( const dragableItem = () => (
<Draggable <Draggable
key={id} key={id}
disabled={!isDraggable} disabled={!isDraggable}
position={position} position={position}
defaultPosition={position} defaultPosition={position}
bounds="parent" bounds='parent'
onStart={event => { onStart={event => {
const e = event || window.event; const e = event || window.event;
const target = e.target || e.srcElement; const target = e.target || e.srcElement;
@ -215,6 +219,21 @@ const DialogProvider = ({ children, service }) => {
</div> </div>
</Draggable> </Draggable>
); );
const withOverlay = component => {
const background = 'bg-black bg-opacity-50';
const overlay = 'fixed z-50 left-0 top-0 w-full h-full overflow-auto';
return (
<div
className={classNames(overlay, background)}
key={id}
>
{component}
</div>
);
};
return showOverlay ? withOverlay(dragableItem()) : dragableItem();
}); });
/** /**
@ -236,13 +255,11 @@ const DialogProvider = ({ children, service }) => {
return ( return (
<DialogContext.Provider value={{ create, dismiss, dismissAll, isEmpty }}> <DialogContext.Provider value={{ create, dismiss, dismissAll, isEmpty }}>
<div className="DraggableArea"> {!isEmpty() &&
{dialogs.some(dialog => dialog.showOverlay) ? ( <div className='w-full h-full absolute'>
<div className="Overlay active">{renderDialogs()}</div> {renderDialogs()}
) : ( </div>
renderDialogs() }
)}
</div>
{children} {children}
</DialogContext.Provider> </DialogContext.Provider>
); );

View File

@ -329,6 +329,7 @@ module.exports = {
auto: 'auto', auto: 'auto',
full: '100%', full: '100%',
viewport: '0.5rem', viewport: '0.5rem',
'1/2': '50%',
'viewport-scrollbar': '1.3rem' 'viewport-scrollbar': '1.3rem'
}, },
letterSpacing: { letterSpacing: {
@ -683,6 +684,7 @@ module.exports = {
transitionProperty: { transitionProperty: {
none: 'none', none: 'none',
all: 'all', all: 'all',
'height': 'height',
default: default:
'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform', 'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform',
colors: 'background-color, border-color, color, fill, stroke', colors: 'background-color, border-color, color, fill, stroke',