diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index 968a51c1f..9a40f8ca5 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -5,9 +5,6 @@ import { DicomMetadataStore, DICOMSR } from '@ohif/core'; import { useDebounce } from '@hooks'; import ActionButtons from './ActionButtons'; import { useTrackedMeasurements } from '../../getContextModule'; -import cornerstoneTools from 'cornerstone-tools'; -import cornerstone from 'cornerstone-core'; -import dcmjs from 'dcmjs'; const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = { key: undefined, // @@ -24,7 +21,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { measurementChangeTimestamp, 200 ); - const { MeasurementService, DisplaySetService } = servicesManager.services; + const { MeasurementService, UINotificationService, UIDialogService, DisplaySetService } = servicesManager.services; const [ trackedMeasurements, sendTrackedMeasurementsEvent, @@ -118,27 +115,44 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { }; const createReport = async () => { - const measurements = MeasurementService.getMeasurements(); - const trackedMeasurements = measurements.filter( - m => - trackedStudy === m.referenceStudyUID && - trackedSeries.includes(m.referenceSeriesUID) - ); + const loadingDialogId = UIDialogService.create({ + showOverlay: true, + isDraggable: false, + centralize: true, + // TODO: Create a loading indicator component + zeplin design? + content: () =>
Loading...
+ }); - const dataSources = extensionManager.getDataSources(); - // TODO -> Eventually deal with multiple dataSources. - // Would need some way of saying which one is the "push" dataSource - const dataSource = dataSources[0]; + try { + const measurements = MeasurementService.getMeasurements(); + const trackedMeasurements = measurements.filter( + m => + trackedStudy === m.referenceStudyUID && + trackedSeries.includes(m.referenceSeriesUID) + ); - DICOMSR.storeMeasurements( - trackedMeasurements, - dataSource, - naturalizedReport => { - DisplaySetService.makeDisplaySets([naturalizedReport], { - madeInClient: true, - }); - } - ); + const dataSources = extensionManager.getDataSources(); + // TODO -> Eventually deal with multiple dataSources. + // Would need some way of saying which one is the "push" dataSource + const dataSource = dataSources[0]; + + 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 ( @@ -155,7 +169,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { title="Measurements" amount={displayMeasurements.length} data={displayMeasurements} - onClick={() => {}} + onClick={() => { }} onEdit={id => alert(`Edit: ${id}`)} /> diff --git a/platform/core/src/DICOMSR/dataExchange.js b/platform/core/src/DICOMSR/dataExchange.js index 8f86f743c..b0dad9044 100644 --- a/platform/core/src/DICOMSR/dataExchange.js +++ b/platform/core/src/DICOMSR/dataExchange.js @@ -79,8 +79,9 @@ const generateReport = measurementData => { * @param {object[]} measurementData An array of measurements from the measurements service * that you wish to serialize. * @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, // But it is good enough for now whilst we only have cornerstone as a datasource. log.info('[DICOMSR] storeMeasurements'); @@ -90,28 +91,22 @@ const storeMeasurements = async (measurementData, dataSource, onSuccess) => { return Promise.reject({}); } - const naturalizedReport = generateReport(measurementData); - const { StudyInstanceUID } = naturalizedReport; - try { + const naturalizedReport = generateReport(measurementData); + const { StudyInstanceUID } = naturalizedReport; + await dataSource.store.dicom(naturalizedReport); if (StudyInstanceUID) { dataSource.deleteStudyMetadataPromise(StudyInstanceUID); } - if (onSuccess) { - onSuccess(naturalizedReport); - } - - return { - message: 'Measurements saved successfully', - }; + return naturalizedReport; } catch (error) { log.error( `[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.'); } }; diff --git a/platform/ui/src/assets/icons/close.svg b/platform/ui/src/assets/icons/close.svg index 83b259fca..03181c71f 100644 --- a/platform/ui/src/assets/icons/close.svg +++ b/platform/ui/src/assets/icons/close.svg @@ -1,4 +1,4 @@ - + diff --git a/platform/ui/src/components/IconButton/IconButton.jsx b/platform/ui/src/components/IconButton/IconButton.jsx index 290bfa273..38b94a87f 100644 --- a/platform/ui/src/components/IconButton/IconButton.jsx +++ b/platform/ui/src/components/IconButton/IconButton.jsx @@ -113,7 +113,7 @@ const IconButton = ({ }; IconButton.defaultProps = { - onClick: () => {}, + onClick: () => { }, color: 'default', disabled: false, fullWidth: false, diff --git a/platform/ui/src/components/Snackbar/Snackbar.css b/platform/ui/src/components/Snackbar/Snackbar.css new file mode 100644 index 000000000..0f0083def --- /dev/null +++ b/platform/ui/src/components/Snackbar/Snackbar.css @@ -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; + } +} diff --git a/platform/ui/src/components/Snackbar/SnackbarContainer.jsx b/platform/ui/src/components/Snackbar/SnackbarContainer.jsx index eb0e83fb2..40c76fc00 100644 --- a/platform/ui/src/components/Snackbar/SnackbarContainer.jsx +++ b/platform/ui/src/components/Snackbar/SnackbarContainer.jsx @@ -2,16 +2,18 @@ import React from 'react'; import SnackbarItem from './SnackbarItem'; import { useSnackbar } from '../../contextProviders'; +import './Snackbar.css'; + const SnackbarContainer = () => { const { snackbarItems, hide } = useSnackbar(); - const renderItem = item => { - return ; - }; - - if (!snackbarItems) { - return null; - } + const renderItem = item => ( + + ); const renderItems = () => { const items = { @@ -23,11 +25,9 @@ const SnackbarContainer = () => { bottomRight: [], }; - snackbarItems.map(item => { - items[item.position].push(item); - }); + snackbarItems.forEach(item => items[item.position].push(item)); - return ( + return snackbarItems && (
{Object.keys(items).map(pos => { if (!items[pos].length) { @@ -35,7 +35,7 @@ const SnackbarContainer = () => { } return ( -
+
{items[pos].map((item, index) => (
{renderItem(item)}
))} diff --git a/platform/ui/src/components/Snackbar/SnackbarItem.jsx b/platform/ui/src/components/Snackbar/SnackbarItem.jsx index abeef2ccf..1c45790f2 100644 --- a/platform/ui/src/components/Snackbar/SnackbarItem.jsx +++ b/platform/ui/src/components/Snackbar/SnackbarItem.jsx @@ -1,25 +1,38 @@ import React, { useEffect } from 'react'; +import classNames from 'classnames'; + +import SnackbarTypes from './SnackbarTypes'; const SnackbarItem = ({ options, onClose }) => { - const handleClose = () => { - onClose(options.id); - }; + const handleClose = () => onClose(options.id); useEffect(() => { if (options.autoClose) { - setTimeout(() => { - handleClose(); - }, options.duration); + setTimeout(() => 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 ( -
- - x +
+ + x - {options.title &&
{options.title}
} - {options.message &&
{options.message}
} + {options.title &&
{options.title}
} + {options.message &&
{options.message}
}
); }; diff --git a/platform/ui/src/contextProviders/DialogProvider.css b/platform/ui/src/contextProviders/DialogProvider.css new file mode 100644 index 000000000..09438e95b --- /dev/null +++ b/platform/ui/src/contextProviders/DialogProvider.css @@ -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; +} diff --git a/platform/ui/src/contextProviders/DialogProvider.jsx b/platform/ui/src/contextProviders/DialogProvider.jsx index eb3c207cb..58ed46432 100644 --- a/platform/ui/src/contextProviders/DialogProvider.jsx +++ b/platform/ui/src/contextProviders/DialogProvider.jsx @@ -5,11 +5,14 @@ import React, { useCallback, useEffect, } from 'react'; + import PropTypes from 'prop-types'; import Draggable from 'react-draggable'; import classNames from 'classnames'; + import { utils } from '@ohif/core'; +import './DialogProvider.css'; const DialogContext = createContext(null); @@ -150,6 +153,7 @@ const DialogProvider = ({ children, service }) => { onStart, onStop, onDrag, + showOverlay, } = dialog; let position = @@ -158,13 +162,13 @@ const DialogProvider = ({ children, service }) => { position = centerPositions.find(position => position.id === id); } - return ( + const dragableItem = () => ( { const e = event || window.event; const target = e.target || e.srcElement; @@ -215,6 +219,21 @@ const DialogProvider = ({ children, service }) => {
); + + 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 ( +
+ {component} +
+ ); + }; + + return showOverlay ? withOverlay(dragableItem()) : dragableItem(); }); /** @@ -236,13 +255,11 @@ const DialogProvider = ({ children, service }) => { return ( -
- {dialogs.some(dialog => dialog.showOverlay) ? ( -
{renderDialogs()}
- ) : ( - renderDialogs() - )} -
+ {!isEmpty() && +
+ {renderDialogs()} +
+ } {children}
); diff --git a/platform/ui/tailwind.config.js b/platform/ui/tailwind.config.js index b01fd7e1a..e70bbe86b 100644 --- a/platform/ui/tailwind.config.js +++ b/platform/ui/tailwind.config.js @@ -329,6 +329,7 @@ module.exports = { auto: 'auto', full: '100%', viewport: '0.5rem', + '1/2': '50%', 'viewport-scrollbar': '1.3rem' }, letterSpacing: { @@ -683,6 +684,7 @@ module.exports = { transitionProperty: { none: 'none', all: 'all', + 'height': 'height', default: 'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform', colors: 'background-color, border-color, color, fill, stroke',