From 9e2ed50e796576c4a9ac783b24f0d213fd74941f Mon Sep 17 00:00:00 2001 From: igoroctaviano Date: Mon, 29 Jun 2020 18:02:18 -0300 Subject: [PATCH 01/16] add new overlay --- .../src/getViewportModule.js | 15 +++- .../viewports/TrackedCornerstoneViewport.js | 14 +++- .../src/viewports/ViewportOverlay.js | 81 +++++++++++++++++++ .../services/ToolBarService/ToolBarService.js | 9 +++ platform/ui/tailwind.config.js | 2 + 5 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 extensions/measurement-tracking/src/viewports/ViewportOverlay.js diff --git a/extensions/measurement-tracking/src/getViewportModule.js b/extensions/measurement-tracking/src/getViewportModule.js index b494f418b..65203872e 100644 --- a/extensions/measurement-tracking/src/getViewportModule.js +++ b/extensions/measurement-tracking/src/getViewportModule.js @@ -12,8 +12,19 @@ const OHIFCornerstoneViewport = props => { ); }; -function getViewportModule({ commandsManager }) { - return [{ name: 'cornerstone-tracked', component: OHIFCornerstoneViewport }]; +function getViewportModule({ servicesManager }) { + const ExtendedOHIFCornerstoneSRViewport = props => { + const { ToolBarService } = servicesManager.services; + + return ( + + ); + }; + + return [{ name: 'cornerstone-tracked', component: ExtendedOHIFCornerstoneSRViewport }]; } export default getViewportModule; diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index 38e0938dd..df728c814 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -12,6 +12,8 @@ import { } from '@ohif/ui'; import { useTrackedMeasurements } from './../getContextModule'; +import ViewportOverlay from './ViewportOverlay'; + const { formatDate } = utils; // TODO -> Get this list from the list of tracked measurements. @@ -38,6 +40,8 @@ function TrackedCornerstoneViewport({ dataSource, displaySet, viewportIndex, + servicesManager, + ToolBarService }) { const [trackedMeasurements] = useTrackedMeasurements(); const [{ activeViewportIndex, viewports }] = useViewportGrid(); @@ -268,7 +272,15 @@ function TrackedCornerstoneViewport({ isStackPrefetchEnabled={true} // todo isPlaying={false} frameRate={24} - isOverlayVisible={false} + isOverlayVisible={true} + viewportOverlayComponent={props => { + return ( + + ); + }} />
{viewportDialogState.viewportIndex === viewportIndex && ( diff --git a/extensions/measurement-tracking/src/viewports/ViewportOverlay.js b/extensions/measurement-tracking/src/viewports/ViewportOverlay.js new file mode 100644 index 000000000..b042743cf --- /dev/null +++ b/extensions/measurement-tracking/src/viewports/ViewportOverlay.js @@ -0,0 +1,81 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cornerstone from 'cornerstone-core'; +import classnames from 'classnames'; + +const ViewportOverlay = ({ + imageId, + scale, + windowWidth, + windowCenter, + imageIndex, + stackSize, + activeTools +}) => { + const topLeft = 'top-viewport left-viewport'; + const topRight = 'top-viewport right-viewport-scrollbar'; + const bottomRight = 'bottom-viewport right-viewport-scrollbar'; + const bottomLeft = 'bottom-viewport left-viewport'; + const overlay = 'absolute pointer-events-none'; + + const isZoomActive = activeTools.includes('Zoom'); + const isWwwcActive = activeTools.includes('Wwwc'); + + if (!imageId) { + return null; + } + + const generalImageModule = cornerstone.metaData.get('generalImageModule', imageId) || {}; + const { instanceNumber } = generalImageModule; + + return ( +
+
+ {isZoomActive && ( +
+ Zoom: + {scale.toFixed(2)}x +
+ )} + {isWwwcActive && ( +
+ W: + {windowWidth.toFixed(0)} + L: + {windowCenter.toFixed(0)} +
+ )} +
+
+ {stackSize > 1 && ( +
+ I: + + {`${instanceNumber}/${stackSize}`} + +
+ )} +
+
+
+
+
+
+ ); +}; + +ViewportOverlay.propTypes = { + scale: PropTypes.number.isRequired, + windowWidth: PropTypes.number.isRequired, + windowCenter: PropTypes.number.isRequired, + imageId: PropTypes.string.isRequired, + imageIndex: PropTypes.number.isRequired, + stackSize: PropTypes.number.isRequired, + activeTools: PropTypes.arrayOf(PropTypes.string) +}; + +ViewportOverlay.defaultProps = { + activeTools: [] +}; + +export default ViewportOverlay; diff --git a/platform/core/src/services/ToolBarService/ToolBarService.js b/platform/core/src/services/ToolBarService/ToolBarService.js index 2cabbe54d..a9f7d34b4 100644 --- a/platform/core/src/services/ToolBarService/ToolBarService.js +++ b/platform/core/src/services/ToolBarService/ToolBarService.js @@ -27,6 +27,15 @@ export default class ToolBarService { return this.buttons; } + getActiveTools() { + return Object.keys(this.buttons).filter(key => { + const button = this.buttons[key]; + if (button && button.props && button.props.isActive) { + return button; + } + }); + } + setButtons(buttons) { this.buttons = buttons; this._broadcastChange(this.EVENTS.TOOL_BAR_MODIFIED, {}); diff --git a/platform/ui/tailwind.config.js b/platform/ui/tailwind.config.js index 3e32e4955..b01fd7e1a 100644 --- a/platform/ui/tailwind.config.js +++ b/platform/ui/tailwind.config.js @@ -328,6 +328,8 @@ module.exports = { '0': '0', auto: 'auto', full: '100%', + viewport: '0.5rem', + 'viewport-scrollbar': '1.3rem' }, letterSpacing: { tighter: '-0.05em', From 9403a091d9c12bab30029a5ad52d139eab23d6b2 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Mon, 29 Jun 2020 22:30:39 -0400 Subject: [PATCH 02/16] Update measurement tracking workflow to match requirements --- .../TrackedMeasurementsContext.jsx | 48 +++------ .../measurementTrackingMachine.js | 52 +++++---- .../promptBeginTracking.js | 57 ++++++++++ .../promptTrackNewSeries.js | 102 ++++++++++++++++++ .../promptTrackNewStudy.js | 96 +++++++++++++++++ 5 files changed, 300 insertions(+), 55 deletions(-) create mode 100644 extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js create mode 100644 extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js create mode 100644 extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx index 063995a13..5081deaad 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/TrackedMeasurementsContext.jsx @@ -6,6 +6,9 @@ import { machineConfiguration, defaultOptions, } from './measurementTrackingMachine'; +import promptBeginTracking from './promptBeginTracking'; +import promptTrackNewSeries from './promptTrackNewSeries'; +import promptTrackNewStudy from './promptTrackNewStudy'; const TrackedMeasurementsContext = React.createContext(); TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext'; @@ -19,41 +22,20 @@ function TrackedMeasurementsContextProvider( UIViewportDialogService, { children } ) { - function promptUser(message, ctx, evt) { - const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt; - - return new Promise(function(resolve, reject) { - /** - * TODO: Will have issues if "SeriesInstanceUID" exists in multiple displaySets? - * - * @param {number} result - -1 | 0 | 1 --> deny | cancel | accept - * @return resolve { userResponse: number, StudyInstanceUID: string, SeriesInstanceUID: string } - */ - const handleSubmit = result => { - UIViewportDialogService.hide(); - resolve({ userResponse: result, StudyInstanceUID, SeriesInstanceUID }); - }; - - UIViewportDialogService.show({ - viewportIndex, - type: 'info', - message, - actions: [ - { type: 'cancel', text: 'No', value: 0 }, - { type: 'secondary', text: 'No, do not ask again', value: -1 }, - { type: 'primary', text: 'Yes', value: 1 }, - ], - onSubmit: handleSubmit, - }); - }); - } - - // Set StateMachine behavior for prompts (invoked services) const machineOptions = Object.assign({}, defaultOptions); machineOptions.services = Object.assign({}, machineOptions.services, { - promptBeginTracking: promptUser.bind(null, 'Start tracking?'), - promptTrackNewStudy: promptUser.bind(null, 'New study?'), - promptTrackNewSeries: promptUser.bind(null, 'New series?'), + promptBeginTracking: promptBeginTracking.bind( + null, + UIViewportDialogService + ), + promptTrackNewSeries: promptTrackNewSeries.bind( + null, + UIViewportDialogService + ), + promptTrackNewStudy: promptTrackNewStudy.bind( + null, + UIViewportDialogService + ), }); diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js index 680fabf49..40ae9cac6 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/measurementTrackingMachine.js @@ -1,5 +1,13 @@ import { assign } from 'xstate'; +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + CREATE_REPORT: 1, + ADD_SERIES: 2, + SET_STUDY_AND_SERIES: 3, +}; + const machineConfiguration = { id: 'measurementTracking', initial: 'idle', @@ -24,11 +32,11 @@ const machineConfiguration = { { target: 'tracking', actions: ['setTrackedStudyAndSeries'], - cond: 'promptAccepted', + cond: 'shouldSetStudyAndSeries', }, { target: 'off', - cond: 'promptDeclined', + cond: 'shouldKillMachine', }, { target: 'idle', @@ -63,15 +71,21 @@ const machineConfiguration = { ], }, }, - promptTrackNewStudy: { + promptTrackNewSeries: { invoke: { - src: 'promptTrackNewStudy', + src: 'promptTrackNewSeries', onDone: [ { target: 'tracking', - actions: ['setTrackedStudyAndSeries'], - cond: 'promptAccepted', + actions: ['addTrackedSeries'], + cond: 'shouldAddSeries', }, + { + target: 'tracking', + actions: ['setTrackedStudyAndSeries'], + cond: 'shouldSetStudyAndSeries', + }, + // CREATE_REPORT && CANCEL { target: 'tracking', }, @@ -81,14 +95,14 @@ const machineConfiguration = { }, }, }, - promptTrackNewSeries: { + promptTrackNewStudy: { invoke: { - src: 'promptTrackNewSeries', + src: 'promptTrackNewStudy', onDone: [ { target: 'tracking', - actions: ['addTrackedSeries'], - cond: 'promptAccepted', + actions: ['setTrackedStudyAndSeries'], + cond: 'shouldSetStudyAndSeries', }, { target: 'tracking', @@ -135,9 +149,12 @@ const defaultOptions = { })), }, guards: { - promptAccepted: (ctx, evt) => evt.data && evt.data.userResponse === 1, - promptCanceled: (ctx, evt) => evt.data && evt.data.userResponse === 0, - promptDeclined: (ctx, evt) => evt.data && evt.data.userResponse === -1, + shouldKillMachine: (ctx, evt) => + evt.data && evt.data.userResponse === RESPONSE.NO_NEVER, + shouldAddSeries: (ctx, evt) => + evt.data && evt.data.userResponse === RESPONSE.ADD_SERIES, + shouldSetStudyAndSeries: (ctx, evt) => + evt.data && evt.data.userResponse === RESPONSE.SET_STUDY_AND_SERIES, // Has more than 1, or SeriesInstanceUID is not in list // --> Post removal would have non-empty trackedSeries array hasRemainingTrackedSeries: (ctx, evt) => @@ -149,13 +166,4 @@ const defaultOptions = { }, }; -// const measurementTrackingMachine = Machine( -// machineConfiguration, -// defaultOptions -// ); -// .transition(state, eventArgument).value -// const service = interpret(measurementTrackingMachine).start(); -// .send(event): nextState -// .state (getter) -// .onTransition(state => { state.vale }) export { defaultOptions, machineConfiguration }; diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js new file mode 100644 index 000000000..e1560150c --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js @@ -0,0 +1,57 @@ +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + CREATE_REPORT: 1, + ADD_SERIES: 2, + SET_STUDY_AND_SERIES: 3, +}; + +function promptUser(UIViewportDialogService, ctx, evt) { + const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt; + + return new Promise(async function(resolve, reject) { + let promptResult = await _askTrackMeasurements( + UIViewportDialogService, + viewportIndex + ); + + resolve({ + userResponse: promptResult, + StudyInstanceUID, + SeriesInstanceUID, + }); + }); +} + +function _askTrackMeasurements(UIViewportDialogService, viewportIndex) { + return new Promise(function(resolve, reject) { + const message = 'Track measurements for this series?'; + const actions = [ + { type: 'cancel', text: 'No', value: RESPONSE.CANCEL }, + { + type: 'secondary', + text: 'No, do not ask again', + value: RESPONSE.NO_NEVER, + }, + { + type: 'primary', + text: 'Yes', + value: RESPONSE.SET_STUDY_AND_SERIES, + }, + ]; + const onSubmit = result => { + UIViewportDialogService.hide(); + resolve(result); + }; + + UIViewportDialogService.show({ + viewportIndex, + type: 'info', + message, + actions, + onSubmit, + }); + }); +} + +export default promptUser; diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js new file mode 100644 index 000000000..fbbe31c15 --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js @@ -0,0 +1,102 @@ +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + CREATE_REPORT: 1, + ADD_SERIES: 2, + SET_STUDY_AND_SERIES: 3, +}; + +function promptUser(UIViewportDialogService, ctx, evt) { + const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt; + + return new Promise(async function(resolve, reject) { + let promptResult = await _askShouldAddMeasurements( + UIViewportDialogService, + viewportIndex + ); + + if (promptResult === RESPONSE.CREATE_REPORT) { + promptResult = await _askSaveDiscardOrCancel( + UIViewportDialogService, + viewportIndex + ); + } + + // TODO: Hook into @JamesAPetts createReport + if (promptResult === RESPONSE.CREATE_REPORT) { + window.alert('CREATE REPORT'); + } + + resolve({ + userResponse: promptResult, + StudyInstanceUID, + SeriesInstanceUID, + }); + }); +} + +function _askShouldAddMeasurements(UIViewportDialogService, viewportIndex) { + return new Promise(function(resolve, reject) { + const message = + 'Do you want to add this measurement to the existing report?'; + const actions = [ + { type: 'cancel', text: 'Cancel', value: RESPONSE.CANCEL }, + { + type: 'secondary', + text: 'Create new report', + value: RESPONSE.CREATE_REPORT, + }, + { + type: 'primary', + text: 'Add to existing report', + value: RESPONSE.ADD_SERIES, + }, + ]; + const onSubmit = result => { + UIViewportDialogService.hide(); + resolve(result); + }; + + UIViewportDialogService.show({ + viewportIndex, + type: 'info', + message, + actions, + onSubmit, + }); + }); +} + +function _askSaveDiscardOrCancel(UIViewportDialogService, viewportIndex) { + return new Promise(function(resolve, reject) { + const message = + 'You have existing tracked measurements. What would you like to do with your existing tracked measurements?'; + const actions = [ + { type: 'cancel', text: 'Cancel', value: RESPONSE.CANCEL }, + { + type: 'secondary', + text: 'Save in report', + value: RESPONSE.CREATE_REPORT, + }, + { + type: 'primary', + text: 'Discard', + value: RESPONSE.SET_STUDY_AND_SERIES, + }, + ]; + const onSubmit = result => { + UIViewportDialogService.hide(); + resolve(result); + }; + + UIViewportDialogService.show({ + viewportIndex, + type: 'info', // TODO: warn + message, + actions, + onSubmit, + }); + }); +} + +export default promptUser; diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js new file mode 100644 index 000000000..71265b6c4 --- /dev/null +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js @@ -0,0 +1,96 @@ +const RESPONSE = { + NO_NEVER: -1, + CANCEL: 0, + CREATE_REPORT: 1, + ADD_SERIES: 2, + SET_STUDY_AND_SERIES: 3, +}; + +function promptUser(UIViewportDialogService, ctx, evt) { + const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt; + + return new Promise(async function(resolve, reject) { + let promptResult = await _askTrackMeasurements( + UIViewportDialogService, + viewportIndex + ); + + if (promptResult === RESPONSE.SET_STUDY_AND_SERIES) { + promptResult = await _askSaveDiscardOrCancel( + UIViewportDialogService, + viewportIndex + ); + } + + // TODO: Hook into @JamesAPetts createReport + if (promptResult === RESPONSE.CREATE_REPORT) { + window.alert('CREATE REPORT'); + } + + resolve({ + userResponse: promptResult, + StudyInstanceUID, + SeriesInstanceUID, + }); + }); +} + +function _askTrackMeasurements(UIViewportDialogService, viewportIndex) { + return new Promise(function(resolve, reject) { + const message = 'Track measurements for this series?'; + const actions = [ + { type: 'cancel', text: 'No', value: RESPONSE.CANCEL }, + { + type: 'primary', + text: 'Yes', + value: RESPONSE.SET_STUDY_AND_SERIES, + }, + ]; + const onSubmit = result => { + UIViewportDialogService.hide(); + resolve(result); + }; + + UIViewportDialogService.show({ + viewportIndex, + type: 'info', + message, + actions, + onSubmit, + }); + }); +} + +function _askSaveDiscardOrCancel(UIViewportDialogService, viewportIndex) { + return new Promise(function(resolve, reject) { + const message = + 'Measurements cannot span across multiple studies. Do you want to save your tracked measurements?'; + const actions = [ + { type: 'cancel', text: 'Cancel', value: RESPONSE.CANCEL }, + { + type: 'secondary', + text: 'No, discard previosuly tracked series & measurements', + value: RESPONSE.SET_STUDY_AND_SERIES, + }, + { + type: 'primary', + text: 'Yes', + value: RESPONSE.CREATE_REPORT, + }, + ]; + const onSubmit = result => { + UIViewportDialogService.hide(); + resolve(result); + }; + + UIViewportDialogService.show({ + viewportIndex, + type: 'info', // TODO: warn + message, + actions, + onSubmit, + }); + }); +} + +export default promptUser; From a0b0f365dd6ac3e06cac2d6e488e40ff05b70178 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Mon, 29 Jun 2020 23:29:40 -0400 Subject: [PATCH 03/16] Handle for interaction outside of the ViewportDialog (default to cancel) --- .../promptBeginTracking.js | 4 +++ .../promptTrackNewSeries.js | 10 +++++- .../promptTrackNewStudy.js | 10 +++++- .../viewports/TrackedCornerstoneViewport.js | 1 + .../services/UIViewportDialogService/index.js | 10 +++++- .../components/Notification/Notification.jsx | 33 ++++++++++++++++--- .../ViewportDialogProvider.jsx | 3 ++ 7 files changed, 64 insertions(+), 7 deletions(-) diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js index e1560150c..657c24e66 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptBeginTracking.js @@ -50,6 +50,10 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) { message, actions, onSubmit, + onOutsideClick: () => { + UIViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, }); }); } diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js index fbbe31c15..4a6f7dcc1 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewSeries.js @@ -63,6 +63,10 @@ function _askShouldAddMeasurements(UIViewportDialogService, viewportIndex) { message, actions, onSubmit, + onOutsideClick: () => { + UIViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, }); }); } @@ -91,10 +95,14 @@ function _askSaveDiscardOrCancel(UIViewportDialogService, viewportIndex) { UIViewportDialogService.show({ viewportIndex, - type: 'info', // TODO: warn + type: 'warning', message, actions, onSubmit, + onOutsideClick: () => { + UIViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, }); }); } diff --git a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js index 71265b6c4..0f8842a31 100644 --- a/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js +++ b/extensions/measurement-tracking/src/contexts/TrackedMeasurementsContext/promptTrackNewStudy.js @@ -57,6 +57,10 @@ function _askTrackMeasurements(UIViewportDialogService, viewportIndex) { message, actions, onSubmit, + onOutsideClick: () => { + UIViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, }); }); } @@ -85,10 +89,14 @@ function _askSaveDiscardOrCancel(UIViewportDialogService, viewportIndex) { UIViewportDialogService.show({ viewportIndex, - type: 'info', // TODO: warn + type: 'warning', message, actions, onSubmit, + onOutsideClick: () => { + UIViewportDialogService.hide(); + resolve(RESPONSE.CANCEL); + }, }); }); } diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index 38e0938dd..55eb3ef29 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -277,6 +277,7 @@ function TrackedCornerstoneViewport({ type={viewportDialogState.type} actions={viewportDialogState.actions} onSubmit={viewportDialogState.onSubmit} + onOutsideClick={viewportDialogState.onOutsideClick} /> )}
diff --git a/platform/core/src/services/UIViewportDialogService/index.js b/platform/core/src/services/UIViewportDialogService/index.js index 8fdb91437..12cc526c5 100644 --- a/platform/core/src/services/UIViewportDialogService/index.js +++ b/platform/core/src/services/UIViewportDialogService/index.js @@ -26,13 +26,21 @@ const serviceImplementation = { * * @param {ViewportDialogProps} props { content, contentProps, viewportIndex } */ -function _show({ viewportIndex, type, message, actions, onSubmit }) { +function _show({ + viewportIndex, + type, + message, + actions, + onSubmit, + onOutsideClick, +}) { return serviceImplementation._show({ viewportIndex, type, message, actions, onSubmit, + onOutsideClick, }); } diff --git a/platform/ui/src/components/Notification/Notification.jsx b/platform/ui/src/components/Notification/Notification.jsx index f980f2f07..d76428373 100644 --- a/platform/ui/src/components/Notification/Notification.jsx +++ b/platform/ui/src/components/Notification/Notification.jsx @@ -1,16 +1,35 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import { Button, Icon } from '@ohif/ui'; -const Notification = ({ type, message, actions, onSubmit }) => { +const Notification = ({ type, message, actions, onSubmit, onOutsideClick }) => { + const notificationRef = useRef(null); + + useEffect(() => { + const notificationElement = notificationRef.current; + const handleClick = function(event) { + const isClickInside = notificationElement.contains(event.target); + + if (!isClickInside) { + onOutsideClick(); + } + }; + + document.addEventListener('mousedown', handleClick); + + return () => { + document.removeEventListener('mousedown', handleClick); + }; + }, [onOutsideClick]); + const iconsByType = { error: { icon: 'info', color: 'text-red-700', }, warning: { - icon: 'info', + icon: 'notificationwarning-diamond', color: 'text-yellow-500', }, info: { @@ -35,7 +54,10 @@ const Notification = ({ type, message, actions, onSubmit }) => { const { icon, color } = getIconData(); return ( -
+
{message} @@ -65,6 +87,7 @@ const Notification = ({ type, message, actions, onSubmit }) => { Notification.defaultProps = { type: 'info', + onOutsideClick: () => {}, }; Notification.propTypes = { @@ -78,6 +101,8 @@ Notification.propTypes = { }) ).isRequired, onSubmit: PropTypes.func.isRequired, + /** Can be used as a callback to dismiss the notification for clicks that occur outside of it */ + onOutsideClick: PropTypes.func, }; export default Notification; diff --git a/platform/ui/src/contextProviders/ViewportDialogProvider.jsx b/platform/ui/src/contextProviders/ViewportDialogProvider.jsx index 325ed3ea3..0fe8979fe 100644 --- a/platform/ui/src/contextProviders/ViewportDialogProvider.jsx +++ b/platform/ui/src/contextProviders/ViewportDialogProvider.jsx @@ -18,6 +18,9 @@ const DEFAULT_STATE = { onSubmit: () => { console.log('btn value?'); }, + onOutsideClick: () => { + console.warn('default: onOutsideClick') + }, onDismiss: () => { console.log('dismiss? -1'); }, From d266b944c20fe0a942ae12af235e54613d0d97b0 Mon Sep 17 00:00:00 2001 From: James Petts Date: Tue, 30 Jun 2020 11:41:50 +0100 Subject: [PATCH 04/16] [OHIF-192] - Viewport letter only if using multiple viewport layout. (#1822) * Viewport letter only in multi-viewport layout. * If SliceThickness not available print N/A --- .../dicom-sr/src/OHIFCornerstoneSRViewport.js | 13 +- .../PanelStudyBrowserTracking.jsx | 5 +- .../viewports/TrackedCornerstoneViewport.js | 22 ++- .../ViewportActionBar/ViewportActionBar.jsx | 130 ++++++++++-------- 4 files changed, 101 insertions(+), 69 deletions(-) diff --git a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js index af3bbc10c..b5b7caaf2 100644 --- a/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js +++ b/extensions/dicom-sr/src/OHIFCornerstoneSRViewport.js @@ -250,12 +250,19 @@ function OHIFCornerstoneSRViewport({ seriesDescription: SeriesDescription, modality: Modality, patientInformation: { - patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '', + patientName: PatientName + ? OHIF.utils.formatPN(PatientName.Alphabetic) + : '', patientSex: PatientSex || '', patientAge: PatientAge || '', MRN: PatientID || '', - thickness: `${SliceThickness}mm`, - spacing: PixelSpacing && PixelSpacing.length ? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed(2)}mm` : '', + thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '', + spacing: + PixelSpacing && PixelSpacing.length + ? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed( + 2 + )}mm` + : '', scanner: ManufacturerModelName || '', }, }} diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx index 438ad0d06..a3ef5f777 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx @@ -291,8 +291,11 @@ function _mapDisplaySets( const firstViewportIndexWithMatchingDisplaySetUid = viewports.findIndex( vp => vp.displaySetInstanceUID === ds.displaySetInstanceUID ); + const viewportIdentificator = - _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid] || ''; + viewports.length > 1 + ? _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid] + : ''; const array = componentType === 'thumbnailTracked' diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index 55eb3ef29..3665fd819 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -225,20 +225,25 @@ function TrackedCornerstoneViewport({ PatientAge, SliceThickness, PixelSpacing, - ManufacturerModelName + ManufacturerModelName, } = displaySet.images[0]; if (trackedSeries.includes(SeriesInstanceUID) !== isTracked) { setIsTracked(!isTracked); } + const label = + viewports.length > 1 + ? _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid] + : ''; + return ( <> alert(`Series ${direction}`)} showNavArrows={viewportIndex === activeViewportIndex} studyData={{ - label: _viewportLabels[firstViewportIndexWithMatchingDisplaySetUid], + label, isTracked: trackedSeries.includes(SeriesInstanceUID), isLocked: false, studyDate: formatDate(SeriesDate), // TODO: This is series date. Is that ok? @@ -246,12 +251,19 @@ function TrackedCornerstoneViewport({ seriesDescription: SeriesDescription, modality: Modality, patientInformation: { - patientName: PatientName ? OHIF.utils.formatPN(PatientName.Alphabetic) : '', + patientName: PatientName + ? OHIF.utils.formatPN(PatientName.Alphabetic) + : '', patientSex: PatientSex || '', patientAge: PatientAge || '', MRN: PatientID || '', - thickness: `${SliceThickness}mm`, - spacing: PixelSpacing && PixelSpacing.length ? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed(2)}mm` : '', + thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '', + spacing: + PixelSpacing && PixelSpacing.length + ? `${PixelSpacing[0].toFixed(2)}mm x ${PixelSpacing[1].toFixed( + 2 + )}mm` + : '', scanner: ManufacturerModelName || '', }, }} diff --git a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx index 12f052e76..d901edb6e 100644 --- a/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx +++ b/platform/ui/src/components/ViewportActionBar/ViewportActionBar.jsx @@ -39,7 +39,7 @@ const ViewportActionBar = ({ scanner, } = patientInformation; - const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo) + const onPatientInfoClick = () => setShowPatientInfo(!showPatientInfo); const renderIconStatus = () => { if (modality === 'SR') { @@ -64,26 +64,26 @@ const ViewportActionBar = ({ {!isTracked ? ( ) : ( - -
- -
-
- - Series is + +
+ +
+
+ + Series is tracked and can be viewed
in the measurement panel
-
- } - > - -
- )} +
+ } + > + + + )}
); }; @@ -196,54 +196,64 @@ function PatientInfo({ isSticky isDisabled={!isOpen} position="bottom-right" - content={isOpen && ( -
-
- -
-
- - {patientName} - -
-
- Sex - - {patientSex} - -
-
- Age - - {patientAge} - -
-
- MRN - {MRN} -
+ content={ + isOpen && ( +
+
+
-
-
- - Thickness - - - {thickness} - +
+ + {patientName} + +
+
+ Sex + + {patientSex} + +
+
+ Age + + {patientAge} + +
+
+ MRN + {MRN} +
-
- Spacing - {spacing} -
-
- Scanner - {scanner} +
+
+ + Thickness + + + {thickness ? thickness : 'N/A'} + +
+
+ + Spacing + + + {spacing} + +
+
+ + Scanner + + + {scanner} + +
-
- )} + ) + } >
From 364f2db1dc5e9177b6ba33974be5f57d5723c412 Mon Sep 17 00:00:00 2001 From: igoroctaviano Date: Tue, 30 Jun 2020 09:29:52 -0300 Subject: [PATCH 05/16] Remove servicesmanager --- .../src/viewports/TrackedCornerstoneViewport.js | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js index df728c814..f5579226a 100644 --- a/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js +++ b/extensions/measurement-tracking/src/viewports/TrackedCornerstoneViewport.js @@ -40,7 +40,6 @@ function TrackedCornerstoneViewport({ dataSource, displaySet, viewportIndex, - servicesManager, ToolBarService }) { const [trackedMeasurements] = useTrackedMeasurements(); From eb8f3a63f2c3260996794888ab86c36fd0802fa0 Mon Sep 17 00:00:00 2001 From: James Petts Date: Tue, 30 Jun 2020 15:10:27 +0100 Subject: [PATCH 06/16] OHIF-197 (#1833) * Viewport letter only in multi-viewport layout. * If SliceThickness not available print N/A * Create displaySet in local session on SR generation. * remove empty line * remove console log * Give storeMeasurements an onSuccess handler instead of always creating dataSets. Co-authored-by: Danny Brown --- .../default/src/DicomWebDataSource/index.js | 6 ++- .../PanelMeasurementTableTracking/index.js | 19 +++++--- platform/core/src/DICOMSR/dataExchange.js | 47 +++---------------- .../core/src/DataSources/IWebApiDataSource.js | 2 + .../DicomMetadataStore/DicomMetadataStore.js | 18 +++---- 5 files changed, 34 insertions(+), 58 deletions(-) diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index 93a7b1b2f..e1b1a5f3a 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -10,7 +10,10 @@ import { DicomMetadataStore, IWebApiDataSource, utils } from '@ohif/core'; import getImageId from './utils/getImageId'; import * as dcmjs from 'dcmjs'; -import { retrieveStudyMetadata } from './retrieveStudyMetadata.js'; +import { + retrieveStudyMetadata, + deleteStudyMetadataPromise, +} from './retrieveStudyMetadata.js'; const { DicomMetaDictionary, DicomDict } = dcmjs.data; @@ -187,6 +190,7 @@ function createDicomWebApi(dicomWebConfig) { storeInstances(instances); }); }, + deleteStudyMetadataPromise, getImageIdsForDisplaySet(displaySet) { const images = displaySet.images; const imageIds = []; diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index 10184832a..b7cf5ae8f 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -24,7 +24,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { measurementChangeTimestamp, 200 ); - const { MeasurementService } = servicesManager.services; + const { MeasurementService, DisplaySetService } = servicesManager.services; const [ trackedMeasurements, sendTrackedMeasurementsEvent, @@ -105,7 +105,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { const activeMeasurementItem = 0; - const onExportClick = () => { + const exportReport = () => { const measurements = MeasurementService.getMeasurements(); const trackedMeasurements = measurements.filter( m => @@ -117,7 +117,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { DICOMSR.downloadReport(trackedMeasurements, dataSource); }; - const onCreateReportClick = () => { + const createReport = () => { const measurements = MeasurementService.getMeasurements(); const trackedMeasurements = measurements.filter( m => @@ -125,13 +125,18 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { trackedSeries.includes(m.referenceSeriesUID) ); - 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]; - DICOMSR.storeMeasurements(trackedMeasurements, dataSource); + DICOMSR.storeMeasurements( + trackedMeasurements, + dataSource, + naturalizedReport => { + DisplaySetService.makeDisplaySets([naturalizedReport]); + } + ); }; return ( @@ -154,8 +159,8 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
diff --git a/platform/core/src/DICOMSR/dataExchange.js b/platform/core/src/DICOMSR/dataExchange.js index 2b3cfe172..8f86f743c 100644 --- a/platform/core/src/DICOMSR/dataExchange.js +++ b/platform/core/src/DICOMSR/dataExchange.js @@ -43,45 +43,6 @@ const retrieveMeasurements = server => { return retrieveMeasurementFromSR(latestSeries, studies, serverUrl); }; -/** - * Function to be registered into MeasurementAPI to store measurements into DICOM Structured Reports - * - * @param {Object} measurementData - OHIF measurementData object - * @param {Object} filter - * @param {serverType} server - * @returns {Object} With message to be displayed on success - */ -const storeMeasurementsOld = async (measurementData, filter, server) => { - log.info('[DICOMSR] storeMeasurements'); - - if (!server || server.type !== 'dicomWeb') { - log.error('[DICOMSR] DicomWeb server is required!'); - return Promise.reject({}); - } - - const serverUrl = server.wadoRoot; - const firstMeasurementKey = Object.keys(measurementData)[0]; - const firstMeasurement = measurementData[firstMeasurementKey][0]; - const StudyInstanceUID = - firstMeasurement && firstMeasurement.StudyInstanceUID; - - try { - await stowSRFromMeasurements(measurementData, serverUrl); - if (StudyInstanceUID) { - studies.deleteStudyMetadataPromise(StudyInstanceUID); - } - - return { - message: 'Measurements saved successfully', - }; - } catch (error) { - log.error( - `[DICOMSR] Error while saving the measurements: ${error.message}` - ); - throw new Error('Error while saving the measurements.'); - } -}; - /** * * @param {object[]} measurementData An array of measurements from the measurements service @@ -119,7 +80,7 @@ const generateReport = measurementData => { * that you wish to serialize. * @param {object} dataSource The dataSource that you wish to use to persist the data. */ -const storeMeasurements = async (measurementData, dataSource) => { +const storeMeasurements = async (measurementData, dataSource, onSuccess) => { // 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'); @@ -136,7 +97,11 @@ const storeMeasurements = async (measurementData, dataSource) => { await dataSource.store.dicom(naturalizedReport); if (StudyInstanceUID) { - studies.deleteStudyMetadataPromise(StudyInstanceUID); + dataSource.deleteStudyMetadataPromise(StudyInstanceUID); + } + + if (onSuccess) { + onSuccess(naturalizedReport); } return { diff --git a/platform/core/src/DataSources/IWebApiDataSource.js b/platform/core/src/DataSources/IWebApiDataSource.js index 10285d364..2ba208ea5 100644 --- a/platform/core/src/DataSources/IWebApiDataSource.js +++ b/platform/core/src/DataSources/IWebApiDataSource.js @@ -17,6 +17,7 @@ function create({ retrieve, store, retrieveSeriesMetadata, + deleteStudyMetadataPromise, getImageIdsForDisplaySet, }) { const defaultQuery = { @@ -59,6 +60,7 @@ function create({ store: store || defaultStore, getImageIdsForDisplaySet, retrieveSeriesMetadata, + deleteStudyMetadataPromise, }; } diff --git a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js index 8fb176959..7768905fa 100644 --- a/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js +++ b/platform/core/src/services/DicomMetadataStore/DicomMetadataStore.js @@ -72,16 +72,16 @@ const BaseImplementation = { study = _model.studies[_model.studies.length - 1]; } - // TODO: Worth identifying why this is being called many times with series - // that are already "added"? - const didAddSeries = study.addSeries(instances); + study.addSeries(instances); - if (didAddSeries) { - this._broadcastEvent(EVENTS.INSTANCES_ADDED, { - StudyInstanceUID, - SeriesInstanceUID, - }); - } + // Broadcast an event even if we used cached data. + // This is because the mode needs to listen to instances that are added to build up its active displaySets. + // It will see there are cached displaySets and end early if this Series has already been fired in this + // Mode session for some reason. + this._broadcastEvent(EVENTS.INSTANCES_ADDED, { + StudyInstanceUID, + SeriesInstanceUID, + }); }, addStudy(study) { const { StudyInstanceUID } = study; From 2c11609c556bb7b5363eb2cd701a2b74cac38f2f Mon Sep 17 00:00:00 2001 From: James Petts Date: Tue, 30 Jun 2020 16:27:35 +0100 Subject: [PATCH 07/16] OHIF-198 - Jump to thumbnail when a new displaySet is added (#1834) * Viewport letter only in multi-viewport layout. * If SliceThickness not available print N/A * Create displaySet in local session on SR generation. * Jump to thumbnail when a new displaySet is added with the madeInClient property. * Check if scrollIntoView exists. --- .../default/src/Panels/PanelStudyBrowser.jsx | 7 +- .../dicom-sr/src/getSopClassHandlerModule.js | 5 +- .../PanelMeasurementTableTracking/index.js | 6 +- .../PanelStudyBrowserTracking.jsx | 83 ++++++++++++++++++- .../DisplaySetService/DisplaySetService.js | 13 ++- .../ui/src/components/Thumbnail/Thumbnail.jsx | 2 + .../ThumbnailList/ThumbnailList.jsx | 3 + .../ThumbnailNoImage/ThumbnailNoImage.jsx | 5 +- .../ThumbnailTracked/ThumbnailTracked.jsx | 2 + 9 files changed, 113 insertions(+), 13 deletions(-) diff --git a/extensions/default/src/Panels/PanelStudyBrowser.jsx b/extensions/default/src/Panels/PanelStudyBrowser.jsx index 8e813217f..2d66fc634 100644 --- a/extensions/default/src/Panels/PanelStudyBrowser.jsx +++ b/extensions/default/src/Panels/PanelStudyBrowser.jsx @@ -28,6 +28,8 @@ function PanelStudyBrowser({ const [displaySets, setDisplaySets] = useState([]); const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({}); + console.log(DisplaySetService); + // ~~ studyDisplayList useEffect(() => { // Fetch all studies for the patient in each primary study @@ -98,8 +100,9 @@ function PanelStudyBrowser({ // DISPLAY_SETS_ADDED returns an array of DisplaySets that were added const SubscriptionDisplaySetsAdded = DisplaySetService.subscribe( DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, - newDisplaySets => { - newDisplaySets.forEach(async dSet => { + data => { + const { displaySetsAdded } = data; + displaySetsAdded.forEach(async dSet => { const newImageSrcEntry = {}; const displaySet = DisplaySetService.getDisplaySetByUID( dSet.displaySetInstanceUID diff --git a/extensions/dicom-sr/src/getSopClassHandlerModule.js b/extensions/dicom-sr/src/getSopClassHandlerModule.js index 9e57e3dcf..80a597a08 100644 --- a/extensions/dicom-sr/src/getSopClassHandlerModule.js +++ b/extensions/dicom-sr/src/getSopClassHandlerModule.js @@ -103,10 +103,11 @@ function _getDisplaySetsFromSeries( // Subscribe to new displaySets as the source may come in after. DisplaySetService.subscribe( DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, - newDisplaySets => { + data => { + const { displaySetsAdded } = data; // If there are still some measurements that have not yet been loaded into cornerstone, // See if we can load them onto any of the new displaySets. - newDisplaySets.forEach(newDisplaySet => { + displaySetsAdded.forEach(newDisplaySet => { _checkIfCanAddMeasurementsToDisplaySet( displaySet, newDisplaySet, diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index b7cf5ae8f..968a51c1f 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -117,7 +117,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { DICOMSR.downloadReport(trackedMeasurements, dataSource); }; - const createReport = () => { + const createReport = async () => { const measurements = MeasurementService.getMeasurements(); const trackedMeasurements = measurements.filter( m => @@ -134,7 +134,9 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { trackedMeasurements, dataSource, naturalizedReport => { - DisplaySetService.makeDisplaySets([naturalizedReport]); + DisplaySetService.makeDisplaySets([naturalizedReport], { + madeInClient: true, + }); } ); }; diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx index a3ef5f777..b45cd3691 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.jsx @@ -34,6 +34,7 @@ function PanelStudyBrowserTracking({ const [studyDisplayList, setStudyDisplayList] = useState([]); const [displaySets, setDisplaySets] = useState([]); const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({}); + const [jumpToDisplaySet, setJumpToDisplaySet] = useState(null); // TODO: Should this be somewhere else? Feels more like a mode "lifecycle" setup/destroy? useEffect(() => { @@ -136,18 +137,26 @@ function PanelStudyBrowserTracking({ // DISPLAY_SETS_ADDED returns an array of DisplaySets that were added const SubscriptionDisplaySetsAdded = DisplaySetService.subscribe( DisplaySetService.EVENTS.DISPLAY_SETS_ADDED, - newDisplaySets => { - newDisplaySets.forEach(async dSet => { + data => { + const { displaySetsAdded, options } = data; + displaySetsAdded.forEach(async dSet => { + const displaySetInstanceUID = dSet.displaySetInstanceUID; + const newImageSrcEntry = {}; const displaySet = DisplaySetService.getDisplaySetByUID( - dSet.displaySetInstanceUID + displaySetInstanceUID ); + + if (options.madeInClient) { + setJumpToDisplaySet(displaySetInstanceUID); + } + const imageIds = dataSource.getImageIdsForDisplaySet(displaySet); const imageId = imageIds[Math.floor(imageIds.length / 2)]; // TODO: Is it okay that imageIds are not returned here for SR displaysets? if (imageId) { // When the image arrives, render it and store the result in the thumbnailImgSrcMap - newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc( + newImageSrcEntry[displaySetInstanceUID] = await getImageSrc( imageId ); setThumbnailImageSrcMap(prevState => { @@ -214,6 +223,51 @@ function PanelStudyBrowserTracking({ } } + useEffect(() => { + if (jumpToDisplaySet) { + // Get element by displaySetInstanceUID + const displaySetInstanceUID = jumpToDisplaySet; + const element = document.getElementById( + `thumbnail-${displaySetInstanceUID}` + ); + + if (element && typeof element.scrollIntoView === 'function') { + // TODO: Any way to support IE here? + element.scrollIntoView({ behavior: 'smooth' }); + + setJumpToDisplaySet(null); + } + } + }, [jumpToDisplaySet, expandedStudyInstanceUIDs, activeTabName]); + + useEffect(() => { + if (!jumpToDisplaySet) { + return; + } + + const displaySetInstanceUID = jumpToDisplaySet; + // Set the activeTabName and expand the study + const thumbnailLocation = _findTabAndStudyOfDisplaySet( + displaySetInstanceUID, + tabs + ); + if (!thumbnailLocation) { + console.warn('jumpToThumbnail: displaySet thumbnail not found.'); + + return; + } + const { tabName, StudyInstanceUID } = thumbnailLocation; + setActiveTabName(tabName); + const studyExpanded = expandedStudyInstanceUIDs.includes(StudyInstanceUID); + if (!studyExpanded) { + const updatedExpandedStudyInstanceUIDs = [ + ...expandedStudyInstanceUIDs, + StudyInstanceUID, + ]; + setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs); + } + }, [jumpToDisplaySet]); + return ( displaySet.displaySetInstanceUID === displaySetInstanceUid ); - makeDisplaySets = (input, batch = false) => { + makeDisplaySets = (input, { batch = false, madeInClient = false } = {}) => { if (!input || !input.length) { throw new Error('No instances were provided.'); } @@ -78,11 +78,20 @@ export default class DisplaySetService { displaySetsAdded = displaySets; } + const options = {}; + + if (madeInClient) { + options.madeInClient = true; + } + // TODO: This is tricky. How do we know we're not resetting to the same/existing DSs? // TODO: This is likely run anytime we touch DicomMetadataStore. How do we prevent uneccessary broadcasts? if (displaySetsAdded && displaySetsAdded.length) { - this._broadcastEvent(EVENTS.DISPLAY_SETS_ADDED, displaySetsAdded); this._broadcastEvent(EVENTS.DISPLAY_SETS_CHANGED, this.activeDisplaySets); + this._broadcastEvent(EVENTS.DISPLAY_SETS_ADDED, { + displaySetsAdded, + options, + }); } }; diff --git a/platform/ui/src/components/Thumbnail/Thumbnail.jsx b/platform/ui/src/components/Thumbnail/Thumbnail.jsx index 44460fa8a..7813928a0 100644 --- a/platform/ui/src/components/Thumbnail/Thumbnail.jsx +++ b/platform/ui/src/components/Thumbnail/Thumbnail.jsx @@ -9,6 +9,7 @@ import { Icon } from '@ohif/ui'; * */ const Thumbnail = ({ + displaySetInstanceUID, className, imageSrc, imageAltText, @@ -36,6 +37,7 @@ const Thumbnail = ({ className, 'flex flex-col flex-1 px-3 mb-8 cursor-pointer outline-none' )} + id={`thumbnail-${displaySetInstanceUID}`} onClick={onClick} onKeyDown={onClick} role="button" diff --git a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx index 303159df8..0b1febd82 100644 --- a/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx +++ b/platform/ui/src/components/ThumbnailList/ThumbnailList.jsx @@ -33,6 +33,7 @@ const ThumbnailList = ({ return (
Date: Tue, 30 Jun 2020 12:33:38 -0300 Subject: [PATCH 08/16] Add messages to sr pacs --- .../PanelMeasurementTableTracking/index.js | 47 +++-- platform/ui/src/assets/icons/close.svg | 2 +- .../src/components/IconButton/IconButton.jsx | 2 +- .../ui/src/components/Snackbar/Snackbar.css | 172 ++++++++++++++++++ .../components/Snackbar/SnackbarContainer.jsx | 24 +-- .../src/components/Snackbar/SnackbarItem.jsx | 33 ++-- .../src/contextProviders/DialogProvider.css | 8 + .../src/contextProviders/DialogProvider.jsx | 35 +++- 8 files changed, 275 insertions(+), 48 deletions(-) create mode 100644 platform/ui/src/components/Snackbar/Snackbar.css create mode 100644 platform/ui/src/contextProviders/DialogProvider.css diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index 10184832a..20987a38e 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -24,7 +24,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { measurementChangeTimestamp, 200 ); - const { MeasurementService } = servicesManager.services; + const { MeasurementService, UINotificationService, UIDialogService } = servicesManager.services; const [ trackedMeasurements, sendTrackedMeasurementsEvent, @@ -117,21 +117,40 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { DICOMSR.downloadReport(trackedMeasurements, dataSource); }; - const onCreateReportClick = () => { - const measurements = MeasurementService.getMeasurements(); - const trackedMeasurements = measurements.filter( - m => - trackedStudy === m.referenceStudyUID && - trackedSeries.includes(m.referenceSeriesUID) - ); + const onCreateReportClick = async () => { + // TODO: Create a loading service that uses the dialog service with these options? + const loadingDialogId = UIDialogService.create({ + showOverlay: true, + isDraggable: false, + centralize: true, + // TODO: Create a loading indicator component + zeplin design? + content: () =>
Loading...
+ }); + try { + const measurements = MeasurementService.getMeasurements(); + const trackedMeasurements = measurements.filter( + m => + trackedStudy === m.referenceStudyUID && + trackedSeries.includes(m.referenceSeriesUID) + ); - 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 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]; - DICOMSR.storeMeasurements(trackedMeasurements, dataSource); + const { message } = await DICOMSR.storeMeasurements(trackedMeasurements, dataSource); + UINotificationService.show({ title: 'STOW SR', message, type: 'success' }); + } catch (error) { + UINotificationService.show({ + title: 'STOW SR', + message: error.message || 'Failed to store measurements', + type: 'error', + }); + } finally { + UIDialogService.dismiss({ id: loadingDialogId }); + } }; return ( @@ -148,7 +167,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { title="Measurements" amount={displayMeasurements.length} data={displayMeasurements} - onClick={() => {}} + onClick={() => { }} onEdit={id => alert(`Edit: ${id}`)} />
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..0c3a0546a --- /dev/null +++ b/platform/ui/src/components/Snackbar/Snackbar.css @@ -0,0 +1,172 @@ +/* TODO: Create tailwind styles for this component */ +.sb-topLeft { + top: 0; + bottom: auto; + left: 0; + right: auto; +} + +.sb-topCenter { + left: 50%; + top: 0; + bottom: auto; + transform: translateX(-50%); +} + +.sb-topRight { + top: 0; + bottom: auto; + left: auto; + right: 0; +} + +.sb-bottomLeft { + top: auto; + bottom: 0px; + left: 0px; + right: auto; +} + +.sb-bottomCenter { + left: 50%; + bottom: 0; + top: auto; + transform: translateX(-50%); +} + +.sb-bottomRight { + top: auto; + bottom: 0px; + left: auto; + right: 0px; + margin: 10px 0 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 { + height: 20px; + opacity: 1; + overflow: hidden; + padding: 2px; + text-align: center; + text-shadow: none; + width: 20px; + cursor: pointer; + position: absolute; + right: 5px; + top: 5px; + transition: all 0.3s ease; + background: rgba(255, 255, 255, 0.6); + border-radius: 100%; +} + +.sb-closeBtn:hover { + background: #fff; +} + +.sb-closeIcon { + display: block; + font-size: 0; + height: 100%; + line-height: 0; + overflow: hidden; + position: relative; + width: 100%; +} + +.sb-closeIcon:after, +.sb-closeIcon:before { + content: ' '; + display: block; + height: 2px; + transition: all 0.3s ease; + width: 12px; + background-color: #222; + opacity: 1; + position: 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 { + font-size: 16px; + font-weight: bold; +} + +.sb-message { + font-size: 14px; + word-break: normal; +} + +.sb-item { + position: relative; + transition: height 300ms ease; + animation: fadein 1s; + padding: 20px; + color: white; + overflow: hidden; + border-radius: 4px; + 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); +} + +.sb-item a { + color: white; + text-decoration: underline; +} + +.sb-hidden { + padding-top: 0; + padding-bottom: 0; + margin-bottom: 0; + height: 0; + opacity: 0; + transition: all 300ms ease; +} + +@keyframes fadein { + from { + opacity: 0; + top: 30px; + } + to { + opacity: 1; + top: 0; + } +} + +/* Internet Explorer */ +@-ms-keyframes fadein { + from { + opacity: 0; + top: 30px; + } + to { + opacity: 1; + 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..dc0f2bcef 100644 --- a/platform/ui/src/components/Snackbar/SnackbarItem.jsx +++ b/platform/ui/src/components/Snackbar/SnackbarItem.jsx @@ -1,25 +1,36 @@ 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' + }; + 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}
); From 5ca6757b449b0eb0adafabd1e3c1bd423ae83435 Mon Sep 17 00:00:00 2001 From: igoroctaviano Date: Tue, 30 Jun 2020 14:02:23 -0300 Subject: [PATCH 09/16] Add error message of generate reportt --- .../PanelMeasurementTableTracking/index.js | 17 +++++++---------- platform/core/src/DICOMSR/dataExchange.js | 19 +++++++------------ .../ui/src/components/Snackbar/Snackbar.css | 14 -------------- .../src/components/Snackbar/SnackbarItem.jsx | 4 +++- 4 files changed, 17 insertions(+), 37 deletions(-) diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index e6473ed68..9a40f8ca5 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -136,17 +136,14 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { // Would need some way of saying which one is the "push" dataSource const dataSource = dataSources[0]; - const { message } = await DICOMSR.storeMeasurements( - trackedMeasurements, - dataSource, - naturalizedReport => { - DisplaySetService.makeDisplaySets([naturalizedReport], { - madeInClient: true, - }); - } - ); + const naturalizedReport = await DICOMSR.storeMeasurements(trackedMeasurements, dataSource); - UINotificationService.show({ title: 'STOW SR', message, type: 'success' }); + DisplaySetService.makeDisplaySets([naturalizedReport], { madeInClient: true }); + UINotificationService.show({ + title: 'STOW SR', + message: 'Measurements saved successfully', + type: 'success' + }); } catch (error) { UINotificationService.show({ title: 'STOW SR', 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/components/Snackbar/Snackbar.css b/platform/ui/src/components/Snackbar/Snackbar.css index 0c3a0546a..93db56bfd 100644 --- a/platform/ui/src/components/Snackbar/Snackbar.css +++ b/platform/ui/src/components/Snackbar/Snackbar.css @@ -134,20 +134,6 @@ 0 3px 5px -1px rgba(0, 0, 0, 0.14); } -.sb-item a { - color: white; - text-decoration: underline; -} - -.sb-hidden { - padding-top: 0; - padding-bottom: 0; - margin-bottom: 0; - height: 0; - opacity: 0; - transition: all 300ms ease; -} - @keyframes fadein { from { opacity: 0; diff --git a/platform/ui/src/components/Snackbar/SnackbarItem.jsx b/platform/ui/src/components/Snackbar/SnackbarItem.jsx index dc0f2bcef..1c45790f2 100644 --- a/platform/ui/src/components/Snackbar/SnackbarItem.jsx +++ b/platform/ui/src/components/Snackbar/SnackbarItem.jsx @@ -19,10 +19,12 @@ const SnackbarItem = ({ options, onClose }) => { [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 (
From 04baaf941b9fee78749c2a0dd75b30fa27189b62 Mon Sep 17 00:00:00 2001 From: igoroctaviano Date: Tue, 30 Jun 2020 14:25:10 -0300 Subject: [PATCH 10/16] Update snackbar styles --- .../ui/src/components/Snackbar/Snackbar.css | 77 ++++--------------- platform/ui/tailwind.config.js | 2 + 2 files changed, 19 insertions(+), 60 deletions(-) diff --git a/platform/ui/src/components/Snackbar/Snackbar.css b/platform/ui/src/components/Snackbar/Snackbar.css index 93db56bfd..0f0083def 100644 --- a/platform/ui/src/components/Snackbar/Snackbar.css +++ b/platform/ui/src/components/Snackbar/Snackbar.css @@ -1,45 +1,29 @@ /* TODO: Create tailwind styles for this component */ .sb-topLeft { - top: 0; - bottom: auto; - left: 0; - right: auto; + @apply top-0 left-0 bottom-auto right-auto; } .sb-topCenter { - left: 50%; - top: 0; - bottom: auto; transform: translateX(-50%); + @apply top-0 bottom-auto left-1/2; } .sb-topRight { - top: 0; - bottom: auto; - left: auto; - right: 0; + @apply right-0 top-0 left-auto bottom-auto; } .sb-bottomLeft { - top: auto; - bottom: 0px; - left: 0px; - right: auto; + @apply right-auto left-0 bottom-0 top-auto; } .sb-bottomCenter { - left: 50%; - bottom: 0; - top: auto; + @apply top-auto bottom-0 left-1/2; transform: translateX(-50%); } .sb-bottomRight { - top: auto; - bottom: 0px; - left: auto; - right: 0px; margin: 10px 0 0; + @apply top-auto bottom-0 left-auto right-0; } .sb-topLeft .sb-item, @@ -55,20 +39,12 @@ } .sb-closeBtn { - height: 20px; - opacity: 1; - overflow: hidden; - padding: 2px; - text-align: center; text-shadow: none; width: 20px; - cursor: pointer; - position: absolute; + height: 20px; right: 5px; top: 5px; - transition: all 0.3s ease; - background: rgba(255, 255, 255, 0.6); - border-radius: 100%; + @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 { @@ -76,31 +52,20 @@ } .sb-closeIcon { - display: block; - font-size: 0; - height: 100%; - line-height: 0; - overflow: hidden; - position: relative; - width: 100%; + @apply w-full relative overflow-hidden h-full block leading-none; } .sb-closeIcon:after, .sb-closeIcon:before { content: ' '; - display: block; height: 2px; - transition: all 0.3s ease; width: 12px; - background-color: #222; - opacity: 1; - position: absolute; + @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%; } @@ -113,46 +78,38 @@ } .sb-title { - font-size: 16px; - font-weight: bold; + @apply break-normal text-lg font-bold; } .sb-message { - font-size: 14px; - word-break: normal; + @apply break-normal text-base; } .sb-item { - position: relative; - transition: height 300ms ease; animation: fadein 1s; - padding: 20px; - color: white; - overflow: hidden; - border-radius: 4px; 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 { - opacity: 0; top: 30px; + @apply opacity-0; } to { - opacity: 1; - top: 0; + @apply opacity-100 top-0; } } /* Internet Explorer */ @-ms-keyframes fadein { from { - opacity: 0; top: 30px; + @apply opacity-0; } to { - opacity: 1; + @apply opacity-100; top: 0; } } 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', From 92314513f6f54aa27d23de0f6e94c9604260a866 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 15:20:14 -0400 Subject: [PATCH 11/16] Delete "basic" example mode, replace with segmentation mode --- .../.webpack/webpack.dev.js | 0 .../.webpack/webpack.prod.js | 0 modes/{example => segmentation}/LICENSE | 0 modes/{example => segmentation}/babel.config.js | 0 modes/{example => segmentation}/package.json | 4 ++-- modes/{example => segmentation}/src/index.js | 15 +++++++-------- .../src/toolbarButtons.js | 0 7 files changed, 9 insertions(+), 10 deletions(-) rename modes/{example => segmentation}/.webpack/webpack.dev.js (100%) rename modes/{example => segmentation}/.webpack/webpack.prod.js (100%) rename modes/{example => segmentation}/LICENSE (100%) rename modes/{example => segmentation}/babel.config.js (100%) rename modes/{example => segmentation}/package.json (90%) rename modes/{example => segmentation}/src/index.js (88%) rename modes/{example => segmentation}/src/toolbarButtons.js (100%) diff --git a/modes/example/.webpack/webpack.dev.js b/modes/segmentation/.webpack/webpack.dev.js similarity index 100% rename from modes/example/.webpack/webpack.dev.js rename to modes/segmentation/.webpack/webpack.dev.js diff --git a/modes/example/.webpack/webpack.prod.js b/modes/segmentation/.webpack/webpack.prod.js similarity index 100% rename from modes/example/.webpack/webpack.prod.js rename to modes/segmentation/.webpack/webpack.prod.js diff --git a/modes/example/LICENSE b/modes/segmentation/LICENSE similarity index 100% rename from modes/example/LICENSE rename to modes/segmentation/LICENSE diff --git a/modes/example/babel.config.js b/modes/segmentation/babel.config.js similarity index 100% rename from modes/example/babel.config.js rename to modes/segmentation/babel.config.js diff --git a/modes/example/package.json b/modes/segmentation/package.json similarity index 90% rename from modes/example/package.json rename to modes/segmentation/package.json index e98a3601e..66ba594e8 100644 --- a/modes/example/package.json +++ b/modes/segmentation/package.json @@ -1,7 +1,7 @@ { - "name": "@ohif/mode-example", + "name": "@ohif/mode-segmentation", "version": "0.0.1", - "description": "Example mode for OHIF", + "description": "Segmentation mode for OHIF", "author": "OHIF", "license": "MIT", "repository": "OHIF/Viewers", diff --git a/modes/example/src/index.js b/modes/segmentation/src/index.js similarity index 88% rename from modes/example/src/index.js rename to modes/segmentation/src/index.js index 597f179ca..fc56ce4ed 100644 --- a/modes/example/src/index.js +++ b/modes/segmentation/src/index.js @@ -3,8 +3,10 @@ import { hotkeys } from '@ohif/core'; export default function mode({ modeConfiguration }) { return { - id: 'example-mode', - displayName: 'Basic Viewer', + // TODO: Mode uses 'id' for route when it should use `slug`, if provided, and + // the route path + id: 'segmentation', + displayName: 'Segmentation', validationTags: { study: [], series: [], @@ -15,7 +17,7 @@ export default function mode({ modeConfiguration }) { }, routes: [ { - path: 'viewer', + path: 'segmentation', init: ({ servicesManager, extensionManager }) => { const { ToolBarService } = servicesManager.services; ToolBarService.init(extensionManager); @@ -51,7 +53,6 @@ export default function mode({ modeConfiguration }) { return { id: 'org.ohif.default.layoutTemplateModule.viewerLayout', props: { - // named slots leftPanels: ['org.ohif.default.panelModule.seriesList'], rightPanels: ['org.ohif.default.panelModule.measure'], viewports: [ @@ -69,10 +70,8 @@ export default function mode({ modeConfiguration }) { ], extensions: ['org.ohif.default', 'org.ohif.cornerstone'], sopClassHandlers: ['org.ohif.default.sopClassHandlerModule.stack'], - hotkeys: [ - ...hotkeys.defaults.hotkeyBindings - ] + hotkeys: [...hotkeys.defaults.hotkeyBindings], }; } -window.exampleMode = mode({}); +window.segmentationMode = mode({}); diff --git a/modes/example/src/toolbarButtons.js b/modes/segmentation/src/toolbarButtons.js similarity index 100% rename from modes/example/src/toolbarButtons.js rename to modes/segmentation/src/toolbarButtons.js From e82b6b3c5e798dd7f3684bf7d3b284825275b9b1 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 15:30:47 -0400 Subject: [PATCH 12/16] Rename longitudinal mode to "Basic Viewer"; matching design --- modes/longitudinal/src/index.js | 8 +++----- platform/viewer/src/App.jsx | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js index 2bf100c09..db09204ba 100644 --- a/modes/longitudinal/src/index.js +++ b/modes/longitudinal/src/index.js @@ -20,8 +20,8 @@ export default function mode({ modeConfiguration }) { return { // TODO: We're using this as a route segment // We should not be. - id: 'longitudinal-workflow', - displayName: 'Comparison', + id: 'viewer', + displayName: 'Basic Viewer', validationTags: { study: [], series: [], @@ -95,9 +95,7 @@ export default function mode({ modeConfiguration }) { 'org.ohif.dicom-sr', ], sopClassHandlers: [ohif.sopClassHandler, dicomsr.sopClassHandler], - hotkeys: [ - ...hotkeys.defaults.hotkeyBindings - ] + hotkeys: [...hotkeys.defaults.hotkeyBindings], }; } diff --git a/platform/viewer/src/App.jsx b/platform/viewer/src/App.jsx index 1cb815d60..1e72bd772 100644 --- a/platform/viewer/src/App.jsx +++ b/platform/viewer/src/App.jsx @@ -20,8 +20,8 @@ import createRoutes from './routes'; import appInit from './appInit.js'; // TODO: Temporarily for testing -import '@ohif/mode-example'; import '@ohif/mode-longitudinal'; +import '@ohif/mode-segmentation'; /** * ENV Variable to determine routing behavior @@ -50,7 +50,7 @@ function App({ config, defaultExtensions }) { dataSources, extensionManager, servicesManager, - hotkeysManager + hotkeysManager, }); const { UIDialogService, From eb0f38ebf25df7c53e0dddb4115a5e0353f9b1ee Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 15:48:24 -0400 Subject: [PATCH 13/16] Prevent default measurementpanel crash --- extensions/default/src/ActionButtons.jsx | 37 ----- .../default/src/PanelMeasurementTable.js | 150 ++++++++++++++---- 2 files changed, 115 insertions(+), 72 deletions(-) delete mode 100644 extensions/default/src/ActionButtons.jsx diff --git a/extensions/default/src/ActionButtons.jsx b/extensions/default/src/ActionButtons.jsx deleted file mode 100644 index 0e3e8a3b6..000000000 --- a/extensions/default/src/ActionButtons.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Button, ButtonGroup, Icon, IconButton } from '@ohif/ui'; - -function ActionButtons() { - return ( - - alert('Export')}> - - - - - - - - ); -} - -export default ActionButtons; diff --git a/extensions/default/src/PanelMeasurementTable.js b/extensions/default/src/PanelMeasurementTable.js index 6da4696fd..cd83598b2 100644 --- a/extensions/default/src/PanelMeasurementTable.js +++ b/extensions/default/src/PanelMeasurementTable.js @@ -1,55 +1,135 @@ -import React from 'react'; -import { StudySummary, MeasurementTable } from '@ohif/ui'; -import ActionButtons from './ActionButtons.jsx'; +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { MeasurementTable } from '@ohif/ui'; +import { DicomMetadataStore } from '@ohif/core'; export default function PanelMeasurementTable({ servicesManager, - commandsManager, + // commandsManager, }) { const { MeasurementService } = servicesManager.services; + const [displayMeasurements, setDisplayMeasurements] = useState([]); - console.log('MeasurementTable rendering!!!!!!!!!!!!!'); + useEffect(() => { + const measurements = MeasurementService.getMeasurements(); + const mappedMeasurements = measurements.map((m, index) => + _mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES) + ); + setDisplayMeasurements(mappedMeasurements); + }, [MeasurementService]); - const descriptionData = { - date: '07-Sep-2010', - modality: 'CT', - description: 'CHEST/ABD/PELVIS W CONTRAST', - }; - - const activeMeasurementItem = 0; - - const measurementTableData = { - title: 'Measurements', - amount: 10, - data: new Array(10).fill({}).map((el, i) => ({ - id: i + 1, - label: 'Label short description', - displayText: '24.0 x 24.0 mm (S:4, I:22)', - isActive: activeMeasurementItem === i + 1, - })), - onClick: id => setActiveMeasurementItem(s => (s === id ? null : id)), - onEdit: id => alert(`Edit: ${id}`), - }; + // const activeMeasurementItem = 0; return ( <>
- {}} onEdit={id => alert(`Edit: ${id}`)} />
-
- -
); } + +PanelMeasurementTable.propTypes = { + servicesManager: PropTypes.shape({ + services: PropTypes.shape({ + MeasurementService: PropTypes.shape({ + getMeasurements: PropTypes.func.isRequired, + VALUE_TYPES: PropTypes.object.isRequired, + }).isRequired, + }).isRequired, + }).isRequired, +}; + +function _mapMeasurementToDisplay(measurement, index, types) { + const { + id, + label, + description, + // Reference IDs + referenceStudyUID, + referenceSeriesUID, + SOPInstanceUID, + } = measurement; + const instance = DicomMetadataStore.getInstance( + referenceStudyUID, + referenceSeriesUID, + SOPInstanceUID + ); + const { PixelSpacing, SeriesNumber, InstanceNumber } = instance; + + return { + id: index + 1, + label: '(empty)', // 'Label short description', + displayText: + _getDisplayText( + measurement, + PixelSpacing, + SeriesNumber, + InstanceNumber, + types + ) || [], + // TODO: handle one layer down + isActive: false, // activeMeasurementItem === i + 1, + }; +} + +function _getDisplayText( + measurement, + pixelSpacing, + seriesNumber, + instanceNumber, + types +) { + const { type, points } = measurement; + const hasPixelSpacing = + pixelSpacing !== undefined && + Array.isArray(pixelSpacing) && + pixelSpacing.length === 2; + const [rowPixelSpacing, colPixelSpacing] = hasPixelSpacing + ? pixelSpacing + : [1, 1]; + const unit = hasPixelSpacing ? 'mm' : 'px'; + + switch (type) { + case types.POLYLINE: { + const { length } = measurement; + const roundedLength = _round(length, 1); + + return [ + `${roundedLength} ${unit} (S:${seriesNumber}, I:${instanceNumber})`, + ]; + } + case types.BIDIRECTIONAL: { + const { shortestDiameter, longestDiameter } = measurement; + const roundedShortestDiameter = _round(shortestDiameter, 1); + const roundedLongestDiameter = _round(longestDiameter, 1); + + return [ + `l: ${roundedLongestDiameter} ${unit} (S:${seriesNumber}, I:${instanceNumber})`, + `s: ${roundedShortestDiameter} ${unit}`, + ]; + } + case types.ELLIPSE: { + const { area } = measurement; + const roundedArea = _round(area, 1); + + return [ + `${roundedArea} ${unit}2 (S:${seriesNumber}, I:${instanceNumber})`, + ]; + } + case types.POINT: { + const { text } = measurement; + return [`${text} (S:${seriesNumber}, I:${instanceNumber})`]; + } + } +} + +function _round(value, decimals) { + return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals); +} From e3ced7e29455f871f01e6f5fcf71cea66b0b4f92 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 15:48:42 -0400 Subject: [PATCH 14/16] Add segmentation mode at appInit --- platform/viewer/src/appInit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js index eb14613ce..3c35dd487 100644 --- a/platform/viewer/src/appInit.js +++ b/platform/viewer/src/appInit.js @@ -71,8 +71,8 @@ function appInit(appConfigOrFunc, defaultExtensions) { // TODO: Remove this if (!appConfig.modes.length) { - appConfig.modes.push(window.exampleMode); appConfig.modes.push(window.longitudinalMode); + appConfig.modes.push(window.segmentationMode); } return { @@ -80,7 +80,7 @@ function appInit(appConfigOrFunc, defaultExtensions) { commandsManager, extensionManager, servicesManager, - hotkeysManager + hotkeysManager, }; } From 064df8a5098ec776a37438fad11bea35523042a3 Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 15:49:05 -0400 Subject: [PATCH 15/16] Fixing lint errors in PanelMeasurementTableTracking --- .../PanelMeasurementTableTracking/index.js | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js index 9a40f8ca5..e8381f39f 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking/index.js @@ -21,7 +21,12 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { measurementChangeTimestamp, 200 ); - const { MeasurementService, UINotificationService, UIDialogService, DisplaySetService } = servicesManager.services; + const { + MeasurementService, + UINotificationService, + UIDialogService, + DisplaySetService, + } = servicesManager.services; const [ trackedMeasurements, sendTrackedMeasurementsEvent, @@ -31,9 +36,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { DISPLAY_STUDY_SUMMARY_INITIAL_VALUE ); const [displayMeasurements, setDisplayMeasurements] = useState([]); - // TODO: measurements subscribtion - // Initial? useEffect(() => { const measurements = MeasurementService.getMeasurements(); const filteredMeasurements = measurements.filter( @@ -120,7 +123,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { isDraggable: false, centralize: true, // TODO: Create a loading indicator component + zeplin design? - content: () =>
Loading...
+ content: () =>
Loading...
, }); try { @@ -136,13 +139,18 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { // Would need some way of saying which one is the "push" dataSource const dataSource = dataSources[0]; - const naturalizedReport = await DICOMSR.storeMeasurements(trackedMeasurements, dataSource); + const naturalizedReport = await DICOMSR.storeMeasurements( + trackedMeasurements, + dataSource + ); - DisplaySetService.makeDisplaySets([naturalizedReport], { madeInClient: true }); + DisplaySetService.makeDisplaySets([naturalizedReport], { + madeInClient: true, + }); UINotificationService.show({ title: 'STOW SR', message: 'Measurements saved successfully', - type: 'success' + type: 'success', }); } catch (error) { UINotificationService.show({ @@ -169,7 +177,7 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { title="Measurements" amount={displayMeasurements.length} data={displayMeasurements} - onClick={() => { }} + onClick={() => {}} onEdit={id => alert(`Edit: ${id}`)} />
@@ -183,7 +191,16 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) { ); } -PanelMeasurementTableTracking.propTypes = {}; +PanelMeasurementTableTracking.propTypes = { + servicesManager: PropTypes.shape({ + services: PropTypes.shape({ + MeasurementService: PropTypes.shape({ + getMeasurements: PropTypes.func.isRequired, + VALUE_TYPES: PropTypes.object.isRequired, + }).isRequired, + }).isRequired, + }).isRequired, +}; // TODO: This could be a MeasurementService mapper function _mapMeasurementToDisplay(measurement, index, types) { @@ -203,9 +220,6 @@ function _mapMeasurementToDisplay(measurement, index, types) { ); const { PixelSpacing, SeriesNumber, InstanceNumber } = instance; - console.log('mapping....', measurement); - console.log(instance); - return { id: index + 1, label: '(empty)', // 'Label short description', @@ -234,15 +248,7 @@ function _getDisplayText( instanceNumber, types ) { - // TODO: determination of shape influences text - // Length: 'xx.x unit (S:x, I:x)' - // Rectangle: 'xx.x x xx.x unit (S:x, I:x)', - // Ellipse? - // Bidirectional? - // Freehand? - const { type, points } = measurement; - const hasPixelSpacing = pixelSpacing !== undefined && Array.isArray(pixelSpacing) && @@ -253,18 +259,16 @@ function _getDisplayText( const unit = hasPixelSpacing ? 'mm' : 'px'; switch (type) { - case types.POLYLINE: + case types.POLYLINE: { const { length } = measurement; - const roundedLength = _round(length, 1); return [ `${roundedLength} ${unit} (S:${seriesNumber}, I:${instanceNumber})`, ]; - - case types.BIDIRECTIONAL: + } + case types.BIDIRECTIONAL: { const { shortestDiameter, longestDiameter } = measurement; - const roundedShortestDiameter = _round(shortestDiameter, 1); const roundedLongestDiameter = _round(longestDiameter, 1); @@ -272,16 +276,19 @@ function _getDisplayText( `l: ${roundedLongestDiameter} ${unit} (S:${seriesNumber}, I:${instanceNumber})`, `s: ${roundedShortestDiameter} ${unit}`, ]; - case types.ELLIPSE: + } + case types.ELLIPSE: { const { area } = measurement; - const roundedArea = _round(area, 1); + return [ `${roundedArea} ${unit}2 (S:${seriesNumber}, I:${instanceNumber})`, ]; - case types.POINT: + } + case types.POINT: { const { text } = measurement; return [`${text} (S:${seriesNumber}, I:${instanceNumber})`]; + } } } From 236b89a5515c89fd17f3deee792f153eeee2960b Mon Sep 17 00:00:00 2001 From: dannyrb Date: Tue, 30 Jun 2020 16:04:07 -0400 Subject: [PATCH 16/16] Make sure measurements table updates --- .../default/src/PanelMeasurementTable.js | 43 +++++++++++++++++-- extensions/default/src/debounce.js | 21 +++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 extensions/default/src/debounce.js diff --git a/extensions/default/src/PanelMeasurementTable.js b/extensions/default/src/PanelMeasurementTable.js index cd83598b2..0967ac378 100644 --- a/extensions/default/src/PanelMeasurementTable.js +++ b/extensions/default/src/PanelMeasurementTable.js @@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { MeasurementTable } from '@ohif/ui'; import { DicomMetadataStore } from '@ohif/core'; +import debounce from './debounce.js'; export default function PanelMeasurementTable({ servicesManager, @@ -11,11 +12,34 @@ export default function PanelMeasurementTable({ const [displayMeasurements, setDisplayMeasurements] = useState([]); useEffect(() => { - const measurements = MeasurementService.getMeasurements(); - const mappedMeasurements = measurements.map((m, index) => - _mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES) + const debouncedSetDisplayMeasurements = debounce( + setDisplayMeasurements, + 100 ); - setDisplayMeasurements(mappedMeasurements); + // ~~ Initial + setDisplayMeasurements(_getMappedMeasurements(MeasurementService)); + + // ~~ Subscription + const added = MeasurementService.EVENTS.MEASUREMENT_ADDED; + const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED; + const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED; + const subscriptions = []; + + [added, updated, removed].forEach(evt => { + subscriptions.push( + MeasurementService.subscribe(evt, () => { + debouncedSetDisplayMeasurements( + _getMappedMeasurements(MeasurementService) + ); + }).unsubscribe + ); + }); + + return () => { + subscriptions.forEach(unsub => { + unsub(); + }); + }; }, [MeasurementService]); // const activeMeasurementItem = 0; @@ -40,12 +64,23 @@ PanelMeasurementTable.propTypes = { services: PropTypes.shape({ MeasurementService: PropTypes.shape({ getMeasurements: PropTypes.func.isRequired, + subscribe: PropTypes.func.isRequired, + EVENTS: PropTypes.object.isRequired, VALUE_TYPES: PropTypes.object.isRequired, }).isRequired, }).isRequired, }).isRequired, }; +function _getMappedMeasurements(MeasurementService) { + const measurements = MeasurementService.getMeasurements(); + const mappedMeasurements = measurements.map((m, index) => + _mapMeasurementToDisplay(m, index, MeasurementService.VALUE_TYPES) + ); + + return mappedMeasurements; +} + function _mapMeasurementToDisplay(measurement, index, types) { const { id, diff --git a/extensions/default/src/debounce.js b/extensions/default/src/debounce.js new file mode 100644 index 000000000..83a16b099 --- /dev/null +++ b/extensions/default/src/debounce.js @@ -0,0 +1,21 @@ +// Returns a function, that, as long as it continues to be invoked, will not +// be triggered. The function will be called after it stops being called for +// N milliseconds. If `immediate` is passed, trigger the function on the +// leading edge, instead of the trailing. +function debounce(func, wait, immediate) { + var timeout; + return function() { + var context = this, + args = arguments; + var later = function() { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; +} + +export default debounce;