diff --git a/platform/viewer/src/connectedComponents/ConnectedStudyBrowser.js b/platform/viewer/src/connectedComponents/ConnectedStudyBrowser.js deleted file mode 100644 index 624837c67..000000000 --- a/platform/viewer/src/connectedComponents/ConnectedStudyBrowser.js +++ /dev/null @@ -1,57 +0,0 @@ -import OHIF from '@ohif/core'; -import { connect } from 'react-redux'; -import { StudyBrowser } from '@ohif/ui'; -import cloneDeep from 'lodash.clonedeep'; -import findDisplaySetByUID from './findDisplaySetByUID'; - -const { setActiveViewportSpecificData } = OHIF.redux.actions; - -// TODO -// - Determine in which display set is active from Redux (activeViewportIndex and layout viewportData) -// - Pass in errors and stack loading progress from Redux -const mapStateToProps = (state, ownProps) => { - // If we know that the stack loading progress details have changed, - // we can try to update the component state so that the thumbnail - // progress bar is updated - const stackLoadingProgressMap = state.loading.progress; - const studiesWithLoadingData = cloneDeep(ownProps.studies); - - studiesWithLoadingData.forEach(study => { - study.thumbnails.forEach(data => { - const { displaySetInstanceUID } = data; - const stackId = `StackProgress:${displaySetInstanceUID}`; - const stackProgressData = stackLoadingProgressMap[stackId]; - - let stackPercentComplete = 0; - if (stackProgressData) { - stackPercentComplete = stackProgressData.percentComplete; - } - - data.stackPercentComplete = stackPercentComplete; - }); - }); - - return { - studies: studiesWithLoadingData, - }; -}; - -const mapDispatchToProps = (dispatch, ownProps) => { - return { - onThumbnailClick: displaySetInstanceUID => { - const displaySet = findDisplaySetByUID( - ownProps.studyMetadata, - displaySetInstanceUID - ); - - dispatch(setActiveViewportSpecificData(displaySet)); - }, - }; -}; - -const ConnectedStudyBrowser = connect( - mapStateToProps, - mapDispatchToProps -)(StudyBrowser); - -export default ConnectedStudyBrowser; diff --git a/platform/viewer/src/connectedComponents/ConnectedViewerMain.js b/platform/viewer/src/connectedComponents/ConnectedViewerMain.js deleted file mode 100644 index bb121bcb4..000000000 --- a/platform/viewer/src/connectedComponents/ConnectedViewerMain.js +++ /dev/null @@ -1,37 +0,0 @@ -import OHIF from '@ohif/core'; -import ViewerMain from './ViewerMain'; -import { connect } from 'react-redux'; - -const { - setViewportSpecificData, - clearViewportSpecificData, -} = OHIF.redux.actions; - -const mapStateToProps = state => { - const { activeViewportIndex, layout, viewportSpecificData } = state.viewports; - - return { - activeViewportIndex, - layout, - viewportSpecificData, - viewports: state.viewports, - }; -}; - -const mapDispatchToProps = dispatch => { - return { - setViewportSpecificData: (viewportIndex, data) => { - dispatch(setViewportSpecificData(viewportIndex, data)); - }, - clearViewportSpecificData: () => { - dispatch(clearViewportSpecificData()); - }, - }; -}; - -const ConnectedViewerMain = connect( - mapStateToProps, - mapDispatchToProps -)(ViewerMain); - -export default ConnectedViewerMain; diff --git a/platform/viewer/src/connectedComponents/ConnectedViewerRetrieveStudyData.js b/platform/viewer/src/connectedComponents/ConnectedViewerRetrieveStudyData.js deleted file mode 100644 index 387bea839..000000000 --- a/platform/viewer/src/connectedComponents/ConnectedViewerRetrieveStudyData.js +++ /dev/null @@ -1,28 +0,0 @@ -import { connect } from 'react-redux'; -import ViewerRetrieveStudyData from './ViewerRetrieveStudyData.js'; -import OHIF from '@ohif/core'; - -const { clearViewportSpecificData } = OHIF.redux.actions; -const isActive = a => a.active === true; - -const mapStateToProps = (state, ownProps) => { - const activeServer = state.servers.servers.find(isActive); - - return { - server: ownProps.server || activeServer, - }; -}; -const mapDispatchToProps = dispatch => { - return { - clearViewportSpecificData: () => { - dispatch(clearViewportSpecificData()); - }, - }; -}; - -const ConnectedViewerRetrieveStudyData = connect( - mapStateToProps, - mapDispatchToProps -)(ViewerRetrieveStudyData); - -export default ConnectedViewerRetrieveStudyData; diff --git a/platform/viewer/src/connectedComponents/ToolContextMenu.js b/platform/viewer/src/connectedComponents/ToolContextMenu.js deleted file mode 100644 index 13dc3aeca..000000000 --- a/platform/viewer/src/connectedComponents/ToolContextMenu.js +++ /dev/null @@ -1,114 +0,0 @@ -import { ContextMenu } from '@ohif/ui'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { commandsManager } from './../App.jsx'; - -const toolTypes = [ - 'Angle', - 'Bidirectional', - 'Length', - 'FreehandMouse', - 'EllipticalRoi', - 'CircleRoi', - 'RectangleRoi', -]; - -const ToolContextMenu = ({ - onSetLabel, - onSetDescription, - isTouchEvent, - eventData, - onClose, - onDelete, -}) => { - const defaultDropdownItems = [ - { - label: 'Delete measurement', - actionType: 'Delete', - action: ({ nearbyToolData, eventData }) => - onDelete(nearbyToolData, eventData), - }, - { - label: 'Relabel', - actionType: 'setLabel', - action: ({ nearbyToolData, eventData }) => { - const { tool: measurementData } = nearbyToolData; - onSetLabel(eventData, measurementData); - }, - }, - { - actionType: 'setDescription', - action: ({ nearbyToolData, eventData }) => { - const { tool: measurementData } = nearbyToolData; - onSetDescription(eventData, measurementData); - }, - }, - ]; - - const getDropdownItems = (eventData, isTouchEvent = false) => { - const nearbyToolData = commandsManager.runCommand('getNearbyToolData', { - element: eventData.element, - canvasCoordinates: eventData.currentPoints.canvas, - availableToolTypes: toolTypes, - }); - - /* - * Annotate tools for touch events already have a press handle to edit it, - * has a better UX for deleting it. - */ - if ( - isTouchEvent && - nearbyToolData && - nearbyToolData.toolType === 'arrowAnnotate' - ) { - return; - } - - let dropdownItems = []; - if (nearbyToolData) { - defaultDropdownItems.forEach(item => { - item.params = { eventData, nearbyToolData }; - - if (item.actionType === 'setDescription') { - item.label = `${ - nearbyToolData.tool.description ? 'Edit' : 'Add' - } Description`; - } - - dropdownItems.push(item); - }); - } - - return dropdownItems; - }; - - const onClickHandler = ({ action, params }) => { - action(params); - if (onClose) { - onClose(); - } - }; - - const dropdownItems = getDropdownItems(eventData, isTouchEvent); - - return ( -
- -
- ); -}; - -ToolContextMenu.propTypes = { - isTouchEvent: PropTypes.bool.isRequired, - eventData: PropTypes.object, - onClose: PropTypes.func, - onSetDescription: PropTypes.func, - onSetLabel: PropTypes.func, - onDelete: PropTypes.func, -}; - -ToolContextMenu.defaultProps = { - isTouchEvent: false, -}; - -export default ToolContextMenu; diff --git a/platform/viewer/src/connectedComponents/ToolbarRow.js b/platform/viewer/src/connectedComponents/ToolbarRow.js deleted file mode 100644 index 3a9d7ad9e..000000000 --- a/platform/viewer/src/connectedComponents/ToolbarRow.js +++ /dev/null @@ -1,386 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { withTranslation } from 'react-i18next'; - -import { MODULE_TYPES } from '@ohif/core'; -import { - ExpandableToolMenu, - RoundedButtonGroup, - ToolbarButton, - withModal, - withDialog, -} from '@ohif/ui'; - -import './ToolbarRow.css'; -import { commandsManager, extensionManager } from './../App.jsx'; - -import ConnectedCineDialog from './ConnectedCineDialog'; -import ConnectedLayoutButton from './ConnectedLayoutButton'; -import { withAppContext } from '../context/AppContext'; - -class ToolbarRow extends Component { - // TODO: Simplify these? isOpen can be computed if we say "any" value for selected, - // closed if selected is null/undefined - static propTypes = { - isLeftSidePanelOpen: PropTypes.bool.isRequired, - isRightSidePanelOpen: PropTypes.bool.isRequired, - selectedLeftSidePanel: PropTypes.string.isRequired, - selectedRightSidePanel: PropTypes.string.isRequired, - handleSidePanelChange: PropTypes.func.isRequired, - activeContexts: PropTypes.arrayOf(PropTypes.string).isRequired, - studies: PropTypes.array, - t: PropTypes.func.isRequired, - // NOTE: withDialog, withModal HOCs - dialog: PropTypes.any, - modal: PropTypes.any, - }; - - static defaultProps = { - studies: [], - }; - - constructor(props) { - super(props); - - const toolbarButtonDefinitions = _getVisibleToolbarButtons.call(this); - // TODO: - // If it's a tool that can be active... Mark it as active? - // - Tools that are on/off? - // - Tools that can be bound to multiple buttons? - - // Normal ToolbarButtons... - // Just how high do we need to hoist this state? - // Why ToolbarRow instead of just Toolbar? Do we have any others? - this.state = { - toolbarButtons: toolbarButtonDefinitions, - activeButtons: [], - }; - - this.seriesPerStudyCount = []; - - this._handleBuiltIn = _handleBuiltIn.bind(this); - - this.updateButtonGroups(); - } - - updateButtonGroups() { - const panelModules = extensionManager.modules[MODULE_TYPES.PANEL]; - - this.buttonGroups = { - left: [], - right: [], - }; - - // ~ FIND MENU OPTIONS - panelModules.forEach(panelExtension => { - const panelModule = panelExtension.module; - const defaultContexts = Array.from(panelModule.defaultContext); - - panelModule.menuOptions.forEach(menuOption => { - const contexts = Array.from(menuOption.context || defaultContexts); - const hasActiveContext = this.props.activeContexts.some(actx => - contexts.includes(actx) - ); - - // It's a bit beefy to pass studies; probably only need to be reactive on `studyInstanceUIDs` and activeViewport? - // Note: This does not cleanly handle `studies` prop updating with panel open - const isDisabled = - typeof menuOption.isDisabled === 'function' && - menuOption.isDisabled(this.props.studies); - - if (hasActiveContext && !isDisabled) { - const menuOptionEntry = { - value: menuOption.target, - icon: menuOption.icon, - bottomLabel: menuOption.label, - }; - const from = menuOption.from || 'right'; - - this.buttonGroups[from].push(menuOptionEntry); - } - }); - }); - - // TODO: This should come from extensions, instead of being baked in - this.buttonGroups.left.unshift({ - value: 'studies', - icon: 'th-large', - bottomLabel: this.props.t('Series'), - }); - } - - componentDidUpdate(prevProps) { - const activeContextsChanged = - prevProps.activeContexts !== this.props.activeContexts; - - const prevStudies = prevProps.studies; - const studies = this.props.studies; - const seriesPerStudyCount = this.seriesPerStudyCount; - - let studiesUpdated = false; - - if (prevStudies.length !== studies.length) { - studiesUpdated = true; - } else { - for (let i = 0; i < studies.length; i++) { - if (studies[i].series.length !== seriesPerStudyCount[i]) { - seriesPerStudyCount[i] = studies[i].series.length; - - studiesUpdated = true; - break; - } - } - } - - if (studiesUpdated) { - this.updateButtonGroups(); - } - - if (activeContextsChanged) { - this.setState( - { - toolbarButtons: _getVisibleToolbarButtons.call(this), - }, - this.closeCineDialogIfNotApplicable - ); - } - } - - closeCineDialogIfNotApplicable = () => { - const { dialog } = this.props; - let { dialogId, activeButtons, toolbarButtons } = this.state; - if (dialogId) { - const cineButtonPresent = toolbarButtons.find( - button => button.options && button.options.behavior === 'CINE' - ); - if (!cineButtonPresent) { - dialog.dismiss({ id: dialogId }); - activeButtons = activeButtons.filter( - button => button.options && button.options.behavior !== 'CINE' - ); - this.setState({ dialogId: null, activeButtons }); - } - } - }; - - render() { - const buttonComponents = _getButtonComponents.call( - this, - this.state.toolbarButtons, - this.state.activeButtons - ); - - const onPress = (side, value) => { - this.props.handleSidePanelChange(side, value); - }; - const onPressLeft = onPress.bind(this, 'left'); - const onPressRight = onPress.bind(this, 'right'); - - return ( - <> -
-
- -
- {buttonComponents} - -
- {this.buttonGroups.right.length && ( - - )} -
-
- - ); - } -} - -function _getCustomButtonComponent(button, activeButtons) { - const CustomComponent = button.CustomComponent; - const isValidComponent = typeof CustomComponent === 'function'; - - // Check if its a valid customComponent. Later on an CustomToolbarComponent interface could be implemented. - if (isValidComponent) { - const parentContext = this; - const activeButtonsIds = activeButtons.map(button => button.id); - const isActive = activeButtonsIds.includes(button.id); - - return ( - - ); - } -} - -function _getExpandableButtonComponent(button, activeButtons) { - // Iterate over button definitions and update `onClick` behavior - let activeCommand; - const childButtons = button.buttons.map(childButton => { - childButton.onClick = _handleToolbarButtonClick.bind(this, childButton); - - if (activeButtons.map(button => button.id).indexOf(childButton.id) > -1) { - activeCommand = childButton.id; - } - - return childButton; - }); - - return ( - - ); -} - -function _getDefaultButtonComponent(button, activeButtons) { - return ( - button.id).includes(button.id)} - /> - ); -} -/** - * Determine which extension buttons should be showing, if they're - * active, and what their onClick behavior should be. - */ -function _getButtonComponents(toolbarButtons, activeButtons) { - const _this = this; - return toolbarButtons.map(button => { - const hasCustomComponent = button.CustomComponent; - const hasNestedButtonDefinitions = button.buttons && button.buttons.length; - - if (hasCustomComponent) { - return _getCustomButtonComponent.call(_this, button, activeButtons); - } - - if (hasNestedButtonDefinitions) { - return _getExpandableButtonComponent.call(_this, button, activeButtons); - } - - return _getDefaultButtonComponent.call(_this, button, activeButtons); - }); -} - -/** - * TODO: DEPRECATE - * This is used exclusively in `extensions/cornerstone/src` - * We have better ways with new UI Services to trigger "builtin" behaviors - * - * A handy way for us to handle different button types. IE. firing commands for - * buttons, or initiation built in behavior. - * - * @param {*} button - * @param {*} evt - * @param {*} props - */ -function _handleToolbarButtonClick(button, evt, props) { - const { activeButtons } = this.state; - - if (button.commandName) { - const options = Object.assign({ evt }, button.commandOptions); - commandsManager.runCommand(button.commandName, options); - } - - // TODO: Use Types ENUM - // TODO: We can update this to be a `getter` on the extension to query - // For the active tools after we apply our updates? - if (button.type === 'setToolActive') { - const toggables = activeButtons.filter( - ({ options }) => options && !options.togglable - ); - this.setState({ activeButtons: [...toggables, button] }); - } else if (button.type === 'builtIn') { - this._handleBuiltIn(button); - } -} - -/** - * - */ -function _getVisibleToolbarButtons() { - const toolbarModules = extensionManager.modules[MODULE_TYPES.TOOLBAR]; - const toolbarButtonDefinitions = []; - - toolbarModules.forEach(extension => { - const { definitions, defaultContext } = extension.module; - definitions.forEach(definition => { - const context = definition.context || defaultContext; - - if (this.props.activeContexts.includes(context)) { - toolbarButtonDefinitions.push(definition); - } - }); - }); - - return toolbarButtonDefinitions; -} - -function _handleBuiltIn(button) { - /* TODO: Keep cine button active until its unselected. */ - const { dialog, t } = this.props; - const { dialogId } = this.state; - const { id, options } = button; - - if (options.behavior === 'CINE') { - if (dialogId) { - dialog.dismiss({ id: dialogId }); - this.setState(state => ({ - dialogId: null, - activeButtons: [ - ...state.activeButtons.filter(button => button.id !== id), - ], - })); - } else { - const spacing = 20; - const { x, y } = document - .querySelector(`.ViewerMain`) - .getBoundingClientRect(); - const newDialogId = dialog.create({ - content: ConnectedCineDialog, - defaultPosition: { - x: x + spacing || 0, - y: y + spacing || 0, - }, - }); - this.setState(state => ({ - dialogId: newDialogId, - activeButtons: [...state.activeButtons, button], - })); - } - } - - if (options.behavior === 'DOWNLOAD_SCREEN_SHOT') { - commandsManager.runCommand('showDownloadViewportModal', { - title: t('Download High Quality Image'), - }); - } -} - -export default withTranslation(['Common', 'ViewportDownloadForm'])( - withModal(withDialog(withAppContext(ToolbarRow))) -); diff --git a/platform/viewer/src/connectedComponents/Viewer.js b/platform/viewer/src/connectedComponents/Viewer.js deleted file mode 100644 index ee85b8b9b..000000000 --- a/platform/viewer/src/connectedComponents/Viewer.js +++ /dev/null @@ -1,400 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; - -import { MODULE_TYPES } from '@ohif/core'; -import OHIF, { DICOMSR } from '@ohif/core'; -import { withDialog } from '@ohif/ui'; -import moment from 'moment'; -import ConnectedHeader from './ConnectedHeader.js'; -import ToolbarRow from './ToolbarRow.js'; -import ConnectedStudyBrowser from './ConnectedStudyBrowser.js'; -import ConnectedViewerMain from './ConnectedViewerMain.js'; -import SidePanel from './../components/SidePanel.js'; -import { extensionManager } from './../App.jsx'; - -// Contexts -import WhiteLabelingContext from '../context/WhiteLabelingContext.js'; -import UserManagerContext from '../context/UserManagerContext'; -import AppContext from '../context/AppContext'; - -import './Viewer.css'; - -class Viewer extends Component { - static propTypes = { - studies: PropTypes.arrayOf( - PropTypes.shape({ - StudyInstanceUID: PropTypes.string.isRequired, - StudyDate: PropTypes.string, - displaySets: PropTypes.arrayOf( - PropTypes.shape({ - displaySetInstanceUID: PropTypes.string.isRequired, - SeriesDescription: PropTypes.string, - SeriesNumber: PropTypes.number, - InstanceNumber: PropTypes.number, - numImageFrames: PropTypes.number, - Modality: PropTypes.string.isRequired, - images: PropTypes.arrayOf( - PropTypes.shape({ - getImageId: PropTypes.func.isRequired, - }) - ), - }) - ), - }) - ), - studyInstanceUIDs: PropTypes.array, - activeServer: PropTypes.shape({ - type: PropTypes.string, - wadoRoot: PropTypes.string, - }), - onTimepointsUpdated: PropTypes.func, - onMeasurementsUpdated: PropTypes.func, - // window.store.getState().viewports.viewportSpecificData - viewports: PropTypes.object.isRequired, - // window.store.getState().viewports.activeViewportIndex - activeViewportIndex: PropTypes.number.isRequired, - isStudyLoaded: PropTypes.bool, - dialog: PropTypes.object, - }; - - constructor(props) { - super(props); - - const { activeServer } = this.props; - const server = Object.assign({}, activeServer); - - OHIF.measurements.MeasurementApi.setConfiguration({ - dataExchange: { - retrieve: DICOMSR.retrieveMeasurements, - store: DICOMSR.storeMeasurements, - }, - server, - }); - - OHIF.measurements.TimepointApi.setConfiguration({ - dataExchange: { - retrieve: this.retrieveTimepoints, - store: this.storeTimepoints, - remove: this.removeTimepoint, - update: this.updateTimepoint, - disassociate: this.disassociateStudy, - }, - }); - } - - state = { - isLeftSidePanelOpen: true, - isRightSidePanelOpen: false, - selectedRightSidePanel: '', - selectedLeftSidePanel: 'studies', // TODO: Don't hardcode this - thumbnails: [], - }; - - componentWillUnmount() { - if (this.props.dialog) { - this.props.dialog.dismissAll(); - } - } - - retrieveTimepoints = filter => { - OHIF.log.info('retrieveTimepoints'); - - // Get the earliest and latest study date - let earliestDate = new Date().toISOString(); - let latestDate = new Date().toISOString(); - if (this.props.studies) { - latestDate = new Date('1000-01-01').toISOString(); - this.props.studies.forEach(study => { - const StudyDate = moment(study.StudyDate, 'YYYYMMDD').toISOString(); - if (StudyDate < earliestDate) { - earliestDate = StudyDate; - } - if (StudyDate > latestDate) { - latestDate = StudyDate; - } - }); - } - - // Return a generic timepoint - return Promise.resolve([ - { - timepointType: 'baseline', - timepointId: 'TimepointId', - studyInstanceUIDs: this.props.studyInstanceUIDs, - PatientID: filter.PatientID, - earliestDate, - latestDate, - isLocked: false, - }, - ]); - }; - - storeTimepoints = timepointData => { - OHIF.log.info('storeTimepoints'); - return Promise.resolve(); - }; - - updateTimepoint = (timepointData, query) => { - OHIF.log.info('updateTimepoint'); - return Promise.resolve(); - }; - - removeTimepoint = timepointId => { - OHIF.log.info('removeTimepoint'); - return Promise.resolve(); - }; - - disassociateStudy = (timepointIds, StudyInstanceUID) => { - OHIF.log.info('disassociateStudy'); - return Promise.resolve(); - }; - - onTimepointsUpdated = timepoints => { - if (this.props.onTimepointsUpdated) { - this.props.onTimepointsUpdated(timepoints); - } - }; - - onMeasurementsUpdated = measurements => { - if (this.props.onMeasurementsUpdated) { - this.props.onMeasurementsUpdated(measurements); - } - }; - - componentDidMount() { - const { studies, isStudyLoaded } = this.props; - const { TimepointApi, MeasurementApi } = OHIF.measurements; - const currentTimepointId = 'TimepointId'; - - const timepointApi = new TimepointApi(currentTimepointId, { - onTimepointsUpdated: this.onTimepointsUpdated, - }); - - const measurementApi = new MeasurementApi(timepointApi, { - onMeasurementsUpdated: this.onMeasurementsUpdated, - }); - - this.currentTimepointId = currentTimepointId; - this.timepointApi = timepointApi; - this.measurementApi = measurementApi; - - if (studies) { - const PatientID = studies[0] && studies[0].PatientID; - - timepointApi.retrieveTimepoints({ PatientID }); - if (isStudyLoaded) { - this.measurementApi.retrieveMeasurements(PatientID, [ - currentTimepointId, - ]); - } - this.setState({ - thumbnails: _mapStudiesToThumbnails(studies), - }); - } - } - - componentDidUpdate(prevProps) { - const { studies, isStudyLoaded } = this.props; - if (studies !== prevProps.studies) { - this.setState({ - thumbnails: _mapStudiesToThumbnails(studies), - }); - } - if (isStudyLoaded && isStudyLoaded !== prevProps.isStudyLoaded) { - const PatientID = studies[0] && studies[0].PatientID; - const { currentTimepointId } = this; - - this.timepointApi.retrieveTimepoints({ PatientID }); - this.measurementApi.retrieveMeasurements(PatientID, [currentTimepointId]); - } - } - - render() { - let VisiblePanelLeft, VisiblePanelRight; - const panelExtensions = extensionManager.modules[MODULE_TYPES.PANEL]; - - panelExtensions.forEach(panelExt => { - panelExt.module.components.forEach(comp => { - if (comp.id === this.state.selectedRightSidePanel) { - VisiblePanelRight = comp.component; - } else if (comp.id === this.state.selectedLeftSidePanel) { - VisiblePanelLeft = comp.component; - } - }); - }); - - return ( - <> - {/* HEADER */} - - {whiteLabeling => ( - - {userManager => ( - - {appContext => ( - - {whiteLabeling && - whiteLabeling.createLogoComponentFn && - whiteLabeling.createLogoComponentFn(React)} - - )} - - )} - - )} - - - {/* TOOLBAR */} - { - const sideClicked = side && side[0].toUpperCase() + side.slice(1); - const openKey = `is${sideClicked}SidePanelOpen`; - const selectedKey = `selected${sideClicked}SidePanel`; - const updatedState = Object.assign({}, this.state); - - const isOpen = updatedState[openKey]; - const prevSelectedPanel = updatedState[selectedKey]; - // RoundedButtonGroup returns `null` if selected button is clicked - const isSameSelectedPanel = - prevSelectedPanel === selectedPanel || selectedPanel === null; - - updatedState[selectedKey] = selectedPanel || prevSelectedPanel; - - const isClosedOrShouldClose = !isOpen || isSameSelectedPanel; - if (isClosedOrShouldClose) { - updatedState[openKey] = !updatedState[openKey]; - } - - this.setState(updatedState); - }} - studies={this.props.studies} - /> - - {/**/} - {/**/} - - {/* VIEWPORTS + SIDEPANELS */} -
- {/* LEFT */} - - {VisiblePanelLeft ? ( - - ) : ( - - )} - - - {/* MAIN */} -
- -
- - {/* RIGHT */} - - {VisiblePanelRight && ( - - )} - -
- - ); - } -} - -export default withDialog(Viewer); - -/** - * What types are these? Why do we have "mapping" dropped in here instead of in - * a mapping layer? - * - * TODO[react]: - * - Add sorting of display sets - * - Add showStackLoadingProgressBar option - * - * @param {Study[]} studies - * @param {DisplaySet[]} studies[].displaySets - */ -const _mapStudiesToThumbnails = function(studies) { - return studies.map(study => { - const { StudyInstanceUID } = study; - - const thumbnails = study.displaySets.map(displaySet => { - const { - displaySetInstanceUID, - SeriesDescription, - SeriesNumber, - InstanceNumber, - numImageFrames, - } = displaySet; - - let imageId; - let altImageText; - - if (displaySet.Modality && displaySet.Modality === 'SEG') { - // TODO: We want to replace this with a thumbnail showing - // the segmentation map on the image, but this is easier - // and better than what we have right now. - altImageText = 'SEG'; - } else if (displaySet.images && displaySet.images.length) { - const imageIndex = Math.floor(displaySet.images.length / 2); - - imageId = displaySet.images[imageIndex].getImageId(); - } else { - altImageText = displaySet.Modality ? displaySet.Modality : 'UN'; - } - - return { - imageId, - altImageText, - displaySetInstanceUID, - SeriesDescription, - SeriesNumber, - InstanceNumber, - numImageFrames, - }; - }); - - return { - StudyInstanceUID, - thumbnails, - }; - }); -}; diff --git a/platform/viewer/src/connectedComponents/ViewerLocalFileData.js b/platform/viewer/src/connectedComponents/ViewerLocalFileData.js deleted file mode 100644 index f2b6b116d..000000000 --- a/platform/viewer/src/connectedComponents/ViewerLocalFileData.js +++ /dev/null @@ -1,151 +0,0 @@ -import React, { Component } from 'react'; -import { metadata, utils } from '@ohif/core'; - -import ConnectedViewer from './ConnectedViewer.js'; -import PropTypes from 'prop-types'; -import { extensionManager } from './../App.jsx'; -import Dropzone from 'react-dropzone'; -import filesToStudies from '../lib/filesToStudies'; -import './ViewerLocalFileData.css'; -import { withTranslation } from 'react-i18next'; - -const { OHIFStudyMetadata } = metadata; -const { studyMetadataManager } = utils; - -const dropZoneLinkDialog = (onDrop, i18n, dir) => { - return ( - - {({ getRootProps, getInputProps }) => ( - - {dir ? ( - - {i18n('Load folders')} - - - ) : ( - - {i18n('Load files')} - - - )} - - )} - - ); -}; - -const linksDialogMessage = (onDrop, i18n) => { - return ( - <> - {i18n('Or click to ')} - {dropZoneLinkDialog(onDrop, i18n)} - {i18n(' or ')} - {dropZoneLinkDialog(onDrop, i18n, true)} - {i18n(' from dialog')} - - ); -}; - -class ViewerLocalFileData extends Component { - static propTypes = { - studies: PropTypes.array, - }; - - state = { - studies: null, - loading: false, - error: null, - }; - - updateStudies = studies => { - // Render the viewer when the data is ready - studyMetadataManager.purge(); - - // Map studies to new format, update metadata manager? - const updatedStudies = studies.map(study => { - const studyMetadata = new OHIFStudyMetadata( - study, - study.StudyInstanceUID - ); - const sopClassHandlerModules = - extensionManager.modules['sopClassHandlerModule']; - - study.displaySets = - study.displaySets || - studyMetadata.createDisplaySets(sopClassHandlerModules); - studyMetadata.setDisplaySets(study.displaySets); - - studyMetadata.forEachDisplaySet(displayset => { - displayset.localFile = true; - }); - - studyMetadataManager.add(studyMetadata); - - return study; - }); - - this.setState({ - studies: updatedStudies, - }); - }; - - render() { - const onDrop = async acceptedFiles => { - this.setState({ loading: true }); - - const studies = await filesToStudies(acceptedFiles); - const updatedStudies = this.updateStudies(studies); - - if (!updatedStudies) { - return; - } - - this.setState({ studies: updatedStudies, loading: false }); - }; - - if (this.state.error) { - return
Error: {JSON.stringify(this.state.error)}
; - } - - return ( - - {({ getRootProps, getInputProps }) => ( -
- {this.state.studies ? ( - a.StudyInstanceUID) - } - /> - ) : ( -
-
- {this.state.loading ? ( -

{this.props.t('Loading...')}

- ) : ( - <> -

- {this.props.t( - 'Drag and Drop DICOM files here to load them in the Viewer' - )} -

-

{linksDialogMessage(onDrop, this.props.t)}

- - )} -
-
- )} -
- )} -
- ); - } -} - -export default withTranslation('Common')(ViewerLocalFileData); diff --git a/platform/viewer/src/connectedComponents/ViewerMain.js b/platform/viewer/src/connectedComponents/ViewerMain.js deleted file mode 100644 index 6a9589495..000000000 --- a/platform/viewer/src/connectedComponents/ViewerMain.js +++ /dev/null @@ -1,193 +0,0 @@ -import './ViewerMain.css'; - -import { Component } from 'react'; -import { ConnectedViewportGrid } from './../components/ViewportGrid/index.js'; -import PropTypes from 'prop-types'; -import React from 'react'; -import memoize from 'lodash/memoize'; -import _values from 'lodash/values'; - -var values = memoize(_values); - -class ViewerMain extends Component { - static propTypes = { - activeViewportIndex: PropTypes.number.isRequired, - studies: PropTypes.array, - viewportSpecificData: PropTypes.object.isRequired, - layout: PropTypes.object.isRequired, - setViewportSpecificData: PropTypes.func.isRequired, - clearViewportSpecificData: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - - this.state = { - displaySets: [], - }; - } - - getDisplaySets(studies) { - const displaySets = []; - studies.forEach(study => { - study.displaySets.forEach(dSet => { - if (!dSet.plugin) { - dSet.plugin = 'cornerstone'; - } - displaySets.push(dSet); - }); - }); - - return displaySets; - } - - findDisplaySet(studies, StudyInstanceUID, displaySetInstanceUID) { - const study = studies.find(study => { - return study.StudyInstanceUID === StudyInstanceUID; - }); - - if (!study) { - return; - } - - return study.displaySets.find(displaySet => { - return displaySet.displaySetInstanceUID === displaySetInstanceUID; - }); - } - - componentDidMount() { - // Add beforeUnload event handler to check for unsaved changes - //window.addEventListener('beforeunload', unloadHandlers.beforeUnload); - - // Get all the display sets for the viewer studies - if (this.props.studies) { - const displaySets = this.getDisplaySets(this.props.studies); - this.setState({ displaySets }, this.fillEmptyViewportPanes); - } - } - - componentDidUpdate(prevProps) { - const prevViewportAmount = prevProps.layout.viewports.length; - const viewportAmount = this.props.layout.viewports.length; - const isVtk = this.props.layout.viewports.some(vp => !!vp.vtk); - - if ( - this.props.studies !== prevProps.studies || - (viewportAmount !== prevViewportAmount && !isVtk) - ) { - const displaySets = this.getDisplaySets(this.props.studies); - this.setState({ displaySets }, this.fillEmptyViewportPanes); - } - } - - fillEmptyViewportPanes = () => { - // TODO: Here is the entry point for filling viewports on load. - const dirtyViewportPanes = []; - const { layout, viewportSpecificData } = this.props; - const { displaySets } = this.state; - - if (!displaySets || !displaySets.length) { - return; - } - - for (let i = 0; i < layout.viewports.length; i++) { - const viewportPane = viewportSpecificData[i]; - const isNonEmptyViewport = - viewportPane && - viewportPane.StudyInstanceUID && - viewportPane.displaySetInstanceUID; - - if (isNonEmptyViewport) { - dirtyViewportPanes.push({ - StudyInstanceUID: viewportPane.StudyInstanceUID, - displaySetInstanceUID: viewportPane.displaySetInstanceUID, - }); - - continue; - } - - const foundDisplaySet = - displaySets.find( - ds => - !dirtyViewportPanes.some( - v => v.displaySetInstanceUID === ds.displaySetInstanceUID - ) - ) || displaySets[displaySets.length - 1]; - - dirtyViewportPanes.push(foundDisplaySet); - } - - dirtyViewportPanes.forEach((vp, i) => { - if (vp && vp.StudyInstanceUID) { - this.setViewportData({ - viewportIndex: i, - StudyInstanceUID: vp.StudyInstanceUID, - displaySetInstanceUID: vp.displaySetInstanceUID, - }); - } - }); - }; - - setViewportData = ({ - viewportIndex, - StudyInstanceUID, - displaySetInstanceUID, - }) => { - const displaySet = this.findDisplaySet( - this.props.studies, - StudyInstanceUID, - displaySetInstanceUID - ); - - this.props.setViewportSpecificData(viewportIndex, displaySet); - }; - - render() { - const { viewportSpecificData } = this.props; - const viewportData = values(viewportSpecificData); - - return ( -
- {this.state.displaySets.length && ( - - {/* Children to add to each viewport that support children */} - - )} -
- ); - } - - componentWillUnmount() { - // Clear the entire viewport specific data - const { viewportSpecificData } = this.props; - Object.keys(viewportSpecificData).forEach(viewportIndex => { - this.props.clearViewportSpecificData(viewportIndex); - }); - - // TODO: These don't have to be viewer specific? - // Could qualify for other routes? - // hotkeys.destroy(); - - // Remove beforeUnload event handler... - //window.removeEventListener('beforeunload', unloadHandlers.beforeUnload); - // Destroy the synchronizer used to update reference lines - //OHIF.viewer.updateImageSynchronizer.destroy(); - // TODO: Instruct all plugins to clean up themselves - // - // Clear references to all stacks in the StackManager - //StackManager.clearStacks(); - // @TypeSafeStudies - // Clears OHIF.viewer.Studies collection - //OHIF.viewer.Studies.removeAll(); - // @TypeSafeStudies - // Clears OHIF.viewer.StudyMetadataList collection - //OHIF.viewer.StudyMetadataList.removeAll(); - } -} - -export default ViewerMain; diff --git a/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js b/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js deleted file mode 100644 index 63fc55093..000000000 --- a/platform/viewer/src/connectedComponents/ViewerRetrieveStudyData.js +++ /dev/null @@ -1,375 +0,0 @@ -import React, { useState, useEffect, useContext } from 'react'; -import { metadata, studies, utils, log } from '@ohif/core'; -import usePrevious from '../customHooks/usePrevious'; - -import ConnectedViewer from './ConnectedViewer.js'; -import PropTypes from 'prop-types'; -import { extensionManager } from './../App.jsx'; -import { useSnackbarContext } from '@ohif/ui'; - -const { OHIFStudyMetadata, OHIFSeriesMetadata } = metadata; -const { retrieveStudiesMetadata, deleteStudyMetadataPromise } = studies; -const { studyMetadataManager, makeCancelable } = utils; - -// Contexts -import AppContext from '../context/AppContext'; - -const _promoteToFront = (list, value, searchMethod) => { - let response = [...list]; - let promoted = false; - const index = response.findIndex(searchMethod.bind(undefined, value)); - - if (index > 0) { - const first = response.splice(index, 1); - response = [...first, ...response]; - } - - if (index >= 0) { - promoted = true; - } - - return { - promoted, - data: response, - }; -}; - -/** - * Promote series to front if find found equivalent on filters object - * @param {Object} study - study reference to promote series against - * @param {Object} [filters] - Object containing filters to be applied - * @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against - * @param {boolean} isFilterStrategy - if filtering by query param strategy ON - */ -const _promoteList = (study, studyMetadata, filters, isFilterStrategy) => { - let promoted = false; - // Promote only if no filter should be applied - if (!isFilterStrategy) { - _sortStudyDisplaySet(study, studyMetadata); - promoted = _promoteStudyDisplaySet(study, studyMetadata, filters); - } - - return promoted; -}; - -const _promoteStudyDisplaySet = (study, studyMetadata, filters) => { - let promoted = false; - const queryParamsLength = Object.keys(filters).length; - const shouldPromoteToFront = queryParamsLength > 0; - - if (shouldPromoteToFront) { - const { seriesInstanceUID } = filters; - - const _seriesLookup = (valueToCompare, displaySet) => { - return displaySet.SeriesInstanceUID === valueToCompare; - }; - const promotedResponse = _promoteToFront( - studyMetadata.getDisplaySets(), - seriesInstanceUID, - _seriesLookup - ); - - study.displaySets = promotedResponse.data; - promoted = promotedResponse.promoted; - } - - return promoted; -}; - -/** - * Method to identify if query param (from url) was applied to given list - * @param {Object} study - study reference to promote series against - * @param {Object} [filters] - Object containing filters to be applied - * @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against - * @param {boolean} isFilterStrategy - if filtering by query param strategy ON - */ -const _isQueryParamApplied = (study, filters = {}, isFilterStrategy) => { - const { seriesInstanceUID } = filters; - let applied = true; - // skip in case no filter or no toast manager - if (!seriesInstanceUID) { - return applied; - } - - const { series = [], displaySets = [] } = study; - const firstSeries = isFilterStrategy ? series[0] : displaySets[0]; - - if (!firstSeries || firstSeries.SeriesInstanceUID !== seriesInstanceUID) { - applied = false; - } - - return applied; -}; -const _showUserMessage = (queryParamApplied, message, dialog = {}) => { - if (queryParamApplied) { - return; - } - - const { show: showUserMessage = () => {} } = dialog; - showUserMessage({ - message, - }); -}; - -const _addSeriesToStudy = (studyMetadata, series) => { - const sopClassHandlerModules = - extensionManager.modules['sopClassHandlerModule']; - const study = studyMetadata.getData(); - const seriesMetadata = new OHIFSeriesMetadata(series, study); - const existingSeries = studyMetadata.getSeriesByUID(series.SeriesInstanceUID); - if (existingSeries) { - studyMetadata.updateSeries(series.SeriesInstanceUID, seriesMetadata); - } else { - studyMetadata.addSeries(seriesMetadata); - } - - studyMetadata.createAndAddDisplaySetsForSeries( - sopClassHandlerModules, - seriesMetadata - ); - study.displaySets = studyMetadata.getDisplaySets(); - _updateStudyMetadataManager(study, studyMetadata); -}; - -const _updateStudyMetadataManager = (study, studyMetadata) => { - const { StudyInstanceUID } = study; - - if (!studyMetadataManager.get(StudyInstanceUID)) { - studyMetadataManager.add(studyMetadata); - } -}; - -const _updateStudyDisplaySets = (study, studyMetadata) => { - const sopClassHandlerModules = - extensionManager.modules['sopClassHandlerModule']; - - if (!study.displaySets) { - study.displaySets = studyMetadata.createDisplaySets(sopClassHandlerModules); - } - - studyMetadata.setDisplaySets(study.displaySets); -}; - -const _sortStudyDisplaySet = (study, studyMetadata) => { - studyMetadata.sortDisplaySets(study.displaySets); -}; - -function ViewerRetrieveStudyData({ - server, - studyInstanceUIDs, - seriesInstanceUIDs, - clearViewportSpecificData, -}) { - // hooks - const [error, setError] = useState(false); - const [studies, setStudies] = useState([]); - const [isStudyLoaded, setIsStudyLoaded] = useState(false); - const snackbarContext = useSnackbarContext(); - const { appConfig = {} } = useContext(AppContext); - const { - filterQueryParam: isFilterStrategy = false, - maxConcurrentMetadataRequests, - } = appConfig; - - let cancelableSeriesPromises; - let cancelableStudiesPromises; - /** - * Callback method when study is totally loaded - * @param {object} study study loaded - * @param {object} studyMetadata studyMetadata for given study - * @param {Object} [filters] - Object containing filters to be applied - * @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against - */ - const studyDidLoad = (study, studyMetadata, filters) => { - // User message - const promoted = _promoteList( - study, - studyMetadata, - filters, - isFilterStrategy - ); - - // Clear viewport to allow new promoted one to be displayed - if (promoted) { - clearViewportSpecificData(0); - } - - const isQueryParamApplied = _isQueryParamApplied( - study, - filters, - isFilterStrategy - ); - // Show message in case not promoted neither filtered but should to - _showUserMessage( - isQueryParamApplied, - 'Query parameters were not applied. Using original series list for given study.', - snackbarContext - ); - - setStudies([...studies, study]); - setIsStudyLoaded(true); - }; - - /** - * Method to process studies. It will update displaySet, studyMetadata, load remaining series, ... - * @param {Array} studiesData Array of studies retrieved from server - * @param {Object} [filters] - Object containing filters to be applied - * @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against - */ - const processStudies = (studiesData, filters) => { - if (Array.isArray(studiesData) && studiesData.length > 0) { - // Map studies to new format, update metadata manager? - const studies = studiesData.map(study => { - const studyMetadata = new OHIFStudyMetadata( - study, - study.StudyInstanceUID - ); - - _updateStudyDisplaySets(study, studyMetadata); - _updateStudyMetadataManager(study, studyMetadata); - - // Attempt to load remaning series if any - cancelableSeriesPromises[study.StudyInstanceUID] = makeCancelable( - loadRemainingSeries(studyMetadata) - ) - .then(result => { - if (result && !result.isCanceled) { - studyDidLoad(study, studyMetadata, filters); - } - }) - .catch(error => { - if (error && !error.isCanceled) { - setError(true); - log.error(error); - } - }); - - return study; - }); - - setStudies(studies); - } - }; - - const forceRerender = () => setStudies(studies => [...studies]); - - const loadRemainingSeries = async studyMetadata => { - const { seriesLoader } = studyMetadata.getData(); - if (!seriesLoader) return; - - const loadNextSeries = async () => { - if (!seriesLoader.hasNext()) return; - const series = await seriesLoader.next(); - _addSeriesToStudy(studyMetadata, series); - forceRerender(); - return loadNextSeries(); - }; - - const concurrentRequestsAllowed = - maxConcurrentMetadataRequests || studyMetadata.getSeriesCount(); - const promises = Array(concurrentRequestsAllowed) - .fill(null) - .map(loadNextSeries); - - return await Promise.all(promises); - }; - - const loadStudies = async () => { - try { - const filters = {}; - // Use the first, discard others - const seriesInstanceUID = seriesInstanceUIDs && seriesInstanceUIDs[0]; - - const retrieveParams = [server, studyInstanceUIDs]; - - if (seriesInstanceUID) { - filters.seriesInstanceUID = seriesInstanceUID; - // Query param filtering controlled by appConfig property - if (isFilterStrategy) { - retrieveParams.push(filters); - } - } - - cancelableStudiesPromises[studyInstanceUIDs] = makeCancelable( - retrieveStudiesMetadata(...retrieveParams) - ) - .then(result => { - if (result && !result.isCanceled) { - processStudies(result, filters); - } - }) - .catch(error => { - if (error && !error.isCanceled) { - setError(true); - log.error(error); - } - }); - } catch (error) { - if (error) { - setError(true); - log.error(error); - } - } - }; - - const purgeCancellablePromises = () => { - for (let studyInstanceUIDs in cancelableStudiesPromises) { - if ('cancel' in cancelableStudiesPromises[studyInstanceUIDs]) { - cancelableStudiesPromises[studyInstanceUIDs].cancel(); - } - } - - for (let studyInstanceUIDs in cancelableSeriesPromises) { - if ('cancel' in cancelableSeriesPromises[studyInstanceUIDs]) { - cancelableSeriesPromises[studyInstanceUIDs].cancel(); - deleteStudyMetadataPromise(studyInstanceUIDs); - studyMetadataManager.remove(studyInstanceUIDs); - } - } - }; - - const prevStudyInstanceUIDs = usePrevious(studyInstanceUIDs); - - useEffect(() => { - const hasStudyInstanceUIDsChanged = !( - prevStudyInstanceUIDs && - prevStudyInstanceUIDs.every(e => studyInstanceUIDs.includes(e)) - ); - - if (hasStudyInstanceUIDsChanged) { - studyMetadataManager.purge(); - purgeCancellablePromises(); - } - }, [studyInstanceUIDs]); - - useEffect(() => { - cancelableSeriesPromises = {}; - cancelableStudiesPromises = {}; - loadStudies(); - - return () => { - purgeCancellablePromises(); - }; - }, []); - - if (error) { - return
Error: {JSON.stringify(error)}
; - } - - return ( - - ); -} - -ViewerRetrieveStudyData.propTypes = { - studyInstanceUIDs: PropTypes.array.isRequired, - seriesInstanceUIDs: PropTypes.array, - server: PropTypes.object, - clearViewportSpecificData: PropTypes.func.isRequired, -}; - -export default ViewerRetrieveStudyData; diff --git a/platform/viewer/src/connectedComponents/findDisplaySetByUID.js b/platform/viewer/src/connectedComponents/findDisplaySetByUID.js deleted file mode 100644 index d50e437ae..000000000 --- a/platform/viewer/src/connectedComponents/findDisplaySetByUID.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Finds displaySet by UID across all displaySets inside studyMetadata - * @param {Array} studyMetadata - * @param {string} displaySetInstanceUID - */ -export default function findDisplaySetByUID( - studyMetadata, - displaySetInstanceUID -) { - if (!Array.isArray(studyMetadata)) return null; - - const allDisplaySets = studyMetadata.reduce((all, current) => { - let currentDisplaySet = []; - if (current && Array.isArray(current.displaySets)) { - currentDisplaySet = current.displaySets; - } - return all.concat(currentDisplaySet); - }, []); - - const bySetInstanceUID = ds => - ds.displaySetInstanceUID === displaySetInstanceUID; - - const displaySet = allDisplaySets.find(bySetInstanceUID); - return displaySet || null; -} diff --git a/platform/viewer/src/connectedComponents/findDisplaySetByUID.test.js b/platform/viewer/src/connectedComponents/findDisplaySetByUID.test.js deleted file mode 100644 index a53d1c544..000000000 --- a/platform/viewer/src/connectedComponents/findDisplaySetByUID.test.js +++ /dev/null @@ -1,43 +0,0 @@ -import findDisplaySetByUID from './findDisplaySetByUID'; - -describe('findDisplaySetByUID', () => { - test('returns null when studyMetadata isnt an array', () => { - const result = findDisplaySetByUID(undefined, 'hello'); - expect(result).toBeNull(); - }); - - test('returns null when no match found', () => { - const result = findDisplaySetByUID([], 'no-match'); - expect(result).toBeNull(); - }); - - test('it handles missing displaySet arrays', () => { - const expected = '9388-2291-a8fe'; - const studyMetadata = [ - { displaySets: null }, - { - displaySets: [{ displaySetInstanceUID: expected }], - }, - null, - 7, - ]; - const result = findDisplaySetByUID(studyMetadata, expected); - expect(result.displaySetInstanceUID).toBe(expected); - }); - - test('returns correct displaySet by UID', () => { - const expected = '1234-5678'; - const studyMetadata = [ - { displaySets: [{ displaySetInstanceUID: '0011-2239' }] }, - { - displaySets: [ - { displaySetInstanceUID: '0392-2211' }, - { displaySetInstanceUID: expected }, - ], - }, - { displaySets: [{ displaySetInstanceUID: '3384-9933' }] }, - ]; - const result = findDisplaySetByUID(studyMetadata, expected); - expect(result.displaySetInstanceUID).toBe(expected); - }); -});