Remove connected components roped in from a rebase

This commit is contained in:
dannyrb 2020-06-14 21:14:31 -04:00
parent fab7334696
commit ae107f4d3b
11 changed files with 0 additions and 1809 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 (
<div className="ToolContextMenu">
<ContextMenu items={dropdownItems} onClick={onClickHandler} />
</div>
);
};
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;

View File

@ -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 (
<>
<div className="ToolbarRow">
<div className="pull-left m-t-1 p-y-1" style={{ padding: '10px' }}>
<RoundedButtonGroup
options={this.buttonGroups.left}
value={this.props.selectedLeftSidePanel || ''}
onValueChanged={onPressLeft}
/>
</div>
{buttonComponents}
<ConnectedLayoutButton />
<div
className="pull-right m-t-1 rm-x-1"
style={{ marginLeft: 'auto' }}
>
{this.buttonGroups.right.length && (
<RoundedButtonGroup
options={this.buttonGroups.right}
value={this.props.selectedRightSidePanel || ''}
onValueChanged={onPressRight}
/>
)}
</div>
</div>
</>
);
}
}
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 (
<CustomComponent
parentContext={parentContext}
toolbarClickCallback={_handleToolbarButtonClick.bind(this)}
button={button}
key={button.id}
activeButtons={activeButtonsIds}
isActive={isActive}
/>
);
}
}
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 (
<ExpandableToolMenu
key={button.id}
label={button.label}
icon={button.icon}
buttons={childButtons}
activeCommand={activeCommand}
/>
);
}
function _getDefaultButtonComponent(button, activeButtons) {
return (
<ToolbarButton
key={button.id}
label={button.label}
icon={button.icon}
onClick={_handleToolbarButtonClick.bind(this, button)}
isActive={activeButtons.map(button => 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)))
);

View File

@ -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 */}
<WhiteLabelingContext.Consumer>
{whiteLabeling => (
<UserManagerContext.Consumer>
{userManager => (
<AppContext.Consumer>
{appContext => (
<ConnectedHeader
linkText={
appContext.appConfig.showStudyList
? 'Study List'
: undefined
}
linkPath={
appContext.appConfig.showStudyList ? '/' : undefined
}
userManager={userManager}
>
{whiteLabeling &&
whiteLabeling.createLogoComponentFn &&
whiteLabeling.createLogoComponentFn(React)}
</ConnectedHeader>
)}
</AppContext.Consumer>
)}
</UserManagerContext.Consumer>
)}
</WhiteLabelingContext.Consumer>
{/* TOOLBAR */}
<ToolbarRow
isLeftSidePanelOpen={this.state.isLeftSidePanelOpen}
isRightSidePanelOpen={this.state.isRightSidePanelOpen}
selectedLeftSidePanel={
this.state.isLeftSidePanelOpen
? this.state.selectedLeftSidePanel
: ''
}
selectedRightSidePanel={
this.state.isRightSidePanelOpen
? this.state.selectedRightSidePanel
: ''
}
handleSidePanelChange={(side, selectedPanel) => {
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}
/>
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
{/*<StudyPrefetcher studies={this.props.studies} />*/}
{/* VIEWPORTS + SIDEPANELS */}
<div className="FlexboxLayout">
{/* LEFT */}
<SidePanel from="left" isOpen={this.state.isLeftSidePanelOpen}>
{VisiblePanelLeft ? (
<VisiblePanelLeft
viewports={this.props.viewports}
studies={this.props.studies}
activeIndex={this.props.activeViewportIndex}
/>
) : (
<ConnectedStudyBrowser
studies={this.state.thumbnails}
studyMetadata={this.props.studies}
/>
)}
</SidePanel>
{/* MAIN */}
<div className={classNames('main-content')}>
<ConnectedViewerMain
studies={this.props.studies}
isStudyLoaded={this.props.isStudyLoaded}
/>
</div>
{/* RIGHT */}
<SidePanel from="right" isOpen={this.state.isRightSidePanelOpen}>
{VisiblePanelRight && (
<VisiblePanelRight
isOpen={this.state.isRightSidePanelOpen}
viewports={this.props.viewports}
studies={this.props.studies}
activeIndex={this.props.activeViewportIndex}
/>
)}
</SidePanel>
</div>
</>
);
}
}
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,
};
});
};

View File

@ -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 (
<Dropzone onDrop={onDrop} noDrag>
{({ getRootProps, getInputProps }) => (
<span {...getRootProps()} className="link-dialog">
{dir ? (
<span>
{i18n('Load folders')}
<input
{...getInputProps()}
webkitdirectory="true"
mozdirectory="true"
/>
</span>
) : (
<span>
{i18n('Load files')}
<input {...getInputProps()} />
</span>
)}
</span>
)}
</Dropzone>
);
};
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 <div>Error: {JSON.stringify(this.state.error)}</div>;
}
return (
<Dropzone onDrop={onDrop} noClick>
{({ getRootProps, getInputProps }) => (
<div {...getRootProps()} style={{ width: '100%', height: '100%' }}>
{this.state.studies ? (
<ConnectedViewer
studies={this.state.studies}
studyInstanceUIDs={
this.state.studies &&
this.state.studies.map(a => a.StudyInstanceUID)
}
/>
) : (
<div className={'drag-drop-instructions'}>
<div className={'drag-drop-contents'}>
{this.state.loading ? (
<h3>{this.props.t('Loading...')}</h3>
) : (
<>
<h3>
{this.props.t(
'Drag and Drop DICOM files here to load them in the Viewer'
)}
</h3>
<h4>{linksDialogMessage(onDrop, this.props.t)}</h4>
</>
)}
</div>
</div>
)}
</div>
)}
</Dropzone>
);
}
}
export default withTranslation('Common')(ViewerLocalFileData);

View File

@ -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 (
<div className="ViewerMain">
{this.state.displaySets.length && (
<ConnectedViewportGrid
isStudyLoaded={this.props.isStudyLoaded}
studies={this.props.studies}
viewportData={viewportData}
setViewportData={this.setViewportData}
>
{/* Children to add to each viewport that support children */}
</ConnectedViewportGrid>
)}
</div>
);
}
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;

View File

@ -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 <div>Error: {JSON.stringify(error)}</div>;
}
return (
<ConnectedViewer
studies={studies}
isStudyLoaded={isStudyLoaded}
studyInstanceUIDs={studyInstanceUIDs}
/>
);
}
ViewerRetrieveStudyData.propTypes = {
studyInstanceUIDs: PropTypes.array.isRequired,
seriesInstanceUIDs: PropTypes.array,
server: PropTypes.object,
clearViewportSpecificData: PropTypes.func.isRequired,
};
export default ViewerRetrieveStudyData;

View File

@ -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;
}

View File

@ -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);
});
});