feat: extension panels
* Snapshot -- Switching branches * fix classes and hooks for panel componet * Restore button functionality * feat: support for expandableToolMenu * Shift cornerstoneTools config up a layer, and use globalToolSync * Remaining initCornerstoneTools changes * Pull in Segmentation Plugin * Shift MeasurementsPanel to an extension * Note regarding where data is coming from * Make sure measurement callback is available * Bump core version * Shift MeasurementsTable to a local extensions directory * Register appCommands as an extension * Update package dependencies and yarn lock * Support for panel width * Ability to pass props to our panel component * Add a safety check around calling our tacked on method for tool options * Left sidebar plugins + fix activeIndex prop passed to sidebar. * fix: viewer height Now in a shared container w/ top bar, so we need to factor that in when calculating height * lock file latest * Update thumbnails if studies has changes * bump cornerstone version to resolve globalToolSynch history issue * Default panel to open; studies optional * Bump minor version for ohif-cornestone-extension * Simplify button logic * Accommodate odd roundedButtonGroup value change emit * Map viewers + selectedViewport instead of pulling off the window
This commit is contained in:
1 parent
514609708d
commit
5ac9048ef0
36 files changed
+554
-463
No files matched your search
@@ -1,16 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import FlexboxLayout from './FlexboxLayout';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
leftSidebarOpen: state.ui.leftSidebarOpen,
|
||||
rightSidebarOpen: state.ui.rightSidebarOpen,
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedFlexboxLayout = connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)(FlexboxLayout);
|
||||
|
||||
export default ConnectedFlexboxLayout;
|
||||
@@ -1,353 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { MeasurementTable } from 'react-viewerbase';
|
||||
import OHIF from 'ohif-core';
|
||||
import moment from 'moment';
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import jumpToRowItem from '../lib/jumpToRowItem.js';
|
||||
import getMeasurementLocationCallback from '../lib/getMeasurementLocationCallback';
|
||||
|
||||
const { setViewportSpecificData } = OHIF.redux.actions;
|
||||
const { MeasurementApi } = OHIF.measurements;
|
||||
|
||||
function groupBy(list, props) {
|
||||
return list.reduce((a, b) => {
|
||||
(a[b[props]] = a[b[props]] || []).push(b);
|
||||
return a;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function getAllTools() {
|
||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
let tools = [];
|
||||
config.measurementTools.forEach(
|
||||
toolGroup => (tools = tools.concat(toolGroup.childTools))
|
||||
);
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
function getMeasurementText(measurementData) {
|
||||
const { location, description } = measurementData;
|
||||
let text = '...';
|
||||
if (location) {
|
||||
text = location;
|
||||
if (description) {
|
||||
text += `(${description})`;
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function getDataForEachMeasurementNumber(
|
||||
measurementNumberList,
|
||||
timepoints,
|
||||
displayFunction
|
||||
) {
|
||||
const data = [];
|
||||
// on each measurement number we should get each measurement data by available timepoint
|
||||
measurementNumberList.forEach(measurement => {
|
||||
timepoints.forEach(timepoint => {
|
||||
const eachData = {
|
||||
displayText: '...',
|
||||
};
|
||||
if (measurement.timepointId === timepoint.timepointId) {
|
||||
eachData.displayText = displayFunction(measurement);
|
||||
}
|
||||
data.push(eachData);
|
||||
});
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function convertMeasurementsToTableData(toolCollections, timepoints) {
|
||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
const toolGroups = config.measurementTools;
|
||||
const tools = getAllTools();
|
||||
|
||||
const tableMeasurements = toolGroups.map(toolGroup => {
|
||||
return {
|
||||
groupName: toolGroup.name,
|
||||
groupId: toolGroup.id,
|
||||
measurements: [],
|
||||
};
|
||||
});
|
||||
|
||||
Object.keys(toolCollections).forEach(toolId => {
|
||||
const toolMeasurements = toolCollections[toolId];
|
||||
const tool = tools.find(tool => tool.id === toolId);
|
||||
const { displayFunction } = tool.options.measurementTable;
|
||||
|
||||
// Group by measurementNumber so we can display then all in the same line
|
||||
const groupedMeasurements = groupBy(toolMeasurements, 'measurementNumber');
|
||||
|
||||
Object.keys(groupedMeasurements).forEach(groupedMeasurementsIndex => {
|
||||
const measurementNumberList =
|
||||
groupedMeasurements[groupedMeasurementsIndex];
|
||||
const measurementData = measurementNumberList[0];
|
||||
const {
|
||||
measurementNumber,
|
||||
lesionNamingNumber,
|
||||
toolType,
|
||||
} = measurementData;
|
||||
const measurementId = measurementData._id;
|
||||
|
||||
//check if all measurements with same measurementNumber will have same LABEL
|
||||
const tableMeasurement = {
|
||||
itemNumber: lesionNamingNumber,
|
||||
label: getMeasurementText(measurementData),
|
||||
measurementId,
|
||||
measurementNumber,
|
||||
lesionNamingNumber,
|
||||
toolType,
|
||||
hasWarnings: false, //TODO
|
||||
warningTitle: '', //TODO
|
||||
isSplitLesion: false, //TODO
|
||||
warningList: [], //TODO
|
||||
data: getDataForEachMeasurementNumber(
|
||||
measurementNumberList,
|
||||
timepoints,
|
||||
displayFunction
|
||||
),
|
||||
};
|
||||
|
||||
// find the group object for the tool
|
||||
const toolGroupMeasurements = tableMeasurements.find(group => {
|
||||
return group.groupId === tool.toolGroup;
|
||||
});
|
||||
// inject the new measurement for this measurementNumer
|
||||
toolGroupMeasurements.measurements.push(tableMeasurement);
|
||||
});
|
||||
});
|
||||
|
||||
// Sort measurements by lesion naming number
|
||||
tableMeasurements.forEach(tm => {
|
||||
tm.measurements.sort((m1, m2) =>
|
||||
m1.lesionNamingNumber > m2.lesionNamingNumber ? 1 : -1
|
||||
);
|
||||
});
|
||||
|
||||
return tableMeasurements;
|
||||
}
|
||||
|
||||
function convertTimepointsToTableData(timepoints) {
|
||||
if (!timepoints || !timepoints.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Study date:',
|
||||
date: moment(timepoints[0].latestDate).format('DD-MMM-YY'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { timepoints, measurements } = state.timepointManager;
|
||||
return {
|
||||
timepoints: convertTimepointsToTableData(timepoints),
|
||||
measurementCollection: convertMeasurementsToTableData(
|
||||
measurements,
|
||||
timepoints
|
||||
),
|
||||
timepointManager: state.timepointManager,
|
||||
viewports: state.viewports,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
dispatchRelabel: (event, measurementData, viewportsState) => {
|
||||
const activeViewportIndex =
|
||||
(viewportsState && viewportsState.activeViewportIndex) || 0;
|
||||
|
||||
const enabledElements = cornerstone.getEnabledElements();
|
||||
if (!enabledElements || enabledElements.length <= activeViewportIndex) {
|
||||
OHIF.log.error('Failed to find the enabled element');
|
||||
return;
|
||||
}
|
||||
|
||||
const { element } = enabledElements[activeViewportIndex];
|
||||
|
||||
const eventData = {
|
||||
event: {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
},
|
||||
element,
|
||||
};
|
||||
|
||||
const { toolType, measurementId } = measurementData;
|
||||
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
|
||||
return measurement._id === measurementId;
|
||||
});
|
||||
|
||||
const options = {
|
||||
skipAddLabelButton: true,
|
||||
editLocation: true,
|
||||
};
|
||||
|
||||
// Clone the tool not to set empty location initially
|
||||
const toolForLocation = Object.assign({}, tool, { location: null });
|
||||
getMeasurementLocationCallback(eventData, toolForLocation, options);
|
||||
},
|
||||
dispatchEditDescription: (event, measurementData, viewportsState) => {
|
||||
const activeViewportIndex =
|
||||
(viewportsState && viewportsState.activeViewportIndex) || 0;
|
||||
|
||||
const enabledElements = cornerstone.getEnabledElements();
|
||||
if (!enabledElements || enabledElements.length <= activeViewportIndex) {
|
||||
OHIF.log.error('Failed to find the enabled element');
|
||||
return;
|
||||
}
|
||||
|
||||
const { element } = enabledElements[activeViewportIndex];
|
||||
|
||||
const eventData = {
|
||||
event: {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
},
|
||||
element,
|
||||
};
|
||||
|
||||
const { toolType, measurementId } = measurementData;
|
||||
const tool = MeasurementApi.Instance.tools[toolType].find(measurement => {
|
||||
return measurement._id === measurementId;
|
||||
});
|
||||
|
||||
const options = {
|
||||
editDescriptionOnDialog: true,
|
||||
};
|
||||
|
||||
getMeasurementLocationCallback(eventData, tool, options);
|
||||
},
|
||||
dispatchJumpToRowItem: (
|
||||
measurementData,
|
||||
viewportsState,
|
||||
timepointManagerState,
|
||||
options
|
||||
) => {
|
||||
const actionData = jumpToRowItem(
|
||||
measurementData,
|
||||
viewportsState,
|
||||
timepointManagerState,
|
||||
dispatch,
|
||||
options
|
||||
);
|
||||
|
||||
actionData.viewportSpecificData.forEach(viewportSpecificData => {
|
||||
const { viewportIndex, displaySet } = viewportSpecificData;
|
||||
|
||||
dispatch(setViewportSpecificData(viewportIndex, displaySet));
|
||||
});
|
||||
|
||||
const { toolType, measurementNumber } = measurementData;
|
||||
const measurementApi = MeasurementApi.Instance;
|
||||
|
||||
Object.keys(measurementApi.tools).forEach(toolType => {
|
||||
const measurements = measurementApi.tools[toolType];
|
||||
|
||||
measurements.forEach(measurement => {
|
||||
measurement.active = false;
|
||||
});
|
||||
});
|
||||
|
||||
const measurementsToActive = measurementApi.tools[toolType].filter(
|
||||
measurement => {
|
||||
return measurement.measurementNumber === measurementNumber;
|
||||
}
|
||||
);
|
||||
|
||||
measurementsToActive.forEach(measurementToActive => {
|
||||
measurementToActive.active = true;
|
||||
});
|
||||
|
||||
measurementApi.syncMeasurementsAndToolData();
|
||||
|
||||
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||
cornerstone.updateImage(enabledElement.element);
|
||||
});
|
||||
|
||||
// Needs to update viewports.layout state to set layout
|
||||
//const layout = actionData.layout;
|
||||
//dispatch(setLayout(layout))
|
||||
|
||||
// Needs to update viewports.activeViewportIndex to the first updated viewport
|
||||
//const viewportIndex = actionData.viewportIndex;
|
||||
//dispatch(setViewportActive(viewportIndex));
|
||||
|
||||
// Needs to update timepointsManager.measurements state to set active measurementId
|
||||
// TODO: Not yet implemented
|
||||
//dispatch(setActiveMeasurement(measurementData.measurementId))
|
||||
|
||||
// (later): Needs to set some property on state.extensions.cornerstone to synchronize viewport scrolling
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
|
||||
return {
|
||||
timepoints: propsFromState.timepoints,
|
||||
measurementCollection: propsFromState.measurementCollection,
|
||||
selectedMeasurementNumber: ownProps.selectedMeasurementNumber,
|
||||
...propsFromDispatch,
|
||||
onItemClick: (event, measurementData) => {
|
||||
// TODO: Add timepointId to .data for measurementData?
|
||||
// TODO: Tooltype should be on the level below? This should
|
||||
// provide the entire row item?
|
||||
|
||||
const viewportsState = propsFromState.viewports;
|
||||
const timepointManagerState = propsFromState.timepointManager;
|
||||
|
||||
// TODO: invertViewportTimepointsOrder should be stored in / read from user preferences
|
||||
// TODO: childToolKey should come from the measurement table when it supports child tools
|
||||
const options = {
|
||||
invertViewportTimepointsOrder: false,
|
||||
childToolKey: null,
|
||||
};
|
||||
|
||||
propsFromDispatch.dispatchJumpToRowItem(
|
||||
measurementData,
|
||||
viewportsState,
|
||||
timepointManagerState,
|
||||
options
|
||||
);
|
||||
},
|
||||
onRelabelClick: (event, measurementData) => {
|
||||
const viewportsState = propsFromState.viewports;
|
||||
propsFromDispatch.dispatchRelabel(event, measurementData, viewportsState);
|
||||
},
|
||||
onEditDescriptionClick: (event, measurementData) => {
|
||||
const viewportsState = propsFromState.viewports;
|
||||
propsFromDispatch.dispatchEditDescription(
|
||||
event,
|
||||
measurementData,
|
||||
viewportsState
|
||||
);
|
||||
},
|
||||
onDeleteClick: (event, measurementData) => {
|
||||
const { MeasurementHandlers } = OHIF.measurements;
|
||||
|
||||
MeasurementHandlers.onRemoved({
|
||||
detail: {
|
||||
toolType: measurementData.toolType,
|
||||
measurementData: {
|
||||
_id: measurementData.measurementId,
|
||||
lesionNamingNumber: measurementData.lesionNamingNumber,
|
||||
measurementNumber: measurementData.measurementNumber,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedMeasurementTable = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
mergeProps
|
||||
)(MeasurementTable);
|
||||
|
||||
export default ConnectedMeasurementTable;
|
||||
@@ -1,8 +1,5 @@
|
||||
import {
|
||||
setLeftSidebarOpen,
|
||||
setRightSidebarOpen,
|
||||
} from './../store/layout/actions.js';
|
||||
|
||||
// TODO: REPLACE THIS WITH A CONTEXT PROVIDER
|
||||
// EVERYTHING IN `VIEWER.JS` COULD USE THIS FOR APPROPRIATE CONTEXT
|
||||
import ToolbarRow from './ToolbarRow';
|
||||
import { connect } from 'react-redux';
|
||||
import { getActiveContexts } from './../store/layout/selectors.js';
|
||||
@@ -10,25 +7,9 @@ import { getActiveContexts } from './../store/layout/selectors.js';
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
activeContexts: getActiveContexts(state),
|
||||
leftSidebarOpen: state.ui.leftSidebarOpen,
|
||||
rightSidebarOpen: state.ui.rightSidebarOpen,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setLeftSidebarOpen: state => {
|
||||
dispatch(setLeftSidebarOpen(state));
|
||||
},
|
||||
setRightSidebarOpen: state => {
|
||||
dispatch(setRightSidebarOpen(state));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedToolbarRow = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ToolbarRow);
|
||||
const ConnectedToolbarRow = connect(mapStateToProps)(ToolbarRow);
|
||||
|
||||
export default ConnectedToolbarRow;
|
||||
@@ -4,6 +4,13 @@ import OHIF from 'ohif-core';
|
||||
|
||||
const { setTimepoints, setMeasurements } = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
viewports: state.viewports.viewportSpecificData,
|
||||
activeViewportIndex: state.viewports.activeViewportIndex,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onTimepointsUpdated: timepoints => {
|
||||
@@ -16,7 +23,7 @@ const mapDispatchToProps = dispatch => {
|
||||
};
|
||||
|
||||
const ConnectedViewer = connect(
|
||||
null,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Viewer);
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
.FlexboxLayout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-flow: row nowrap;
|
||||
align-items: stretch;
|
||||
height: calc(100% - var(--toolbar-height));
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
height: 100%;
|
||||
transition: var(--sidebar-transition);
|
||||
}
|
||||
|
||||
.sidebar-left {
|
||||
border-right: var(--ui-border-thickness) solid var(--ui-border-color);
|
||||
flex: 1;
|
||||
margin-left: calc(var(--left-sidebar-menu-width) * -1);
|
||||
max-width: var(--left-sidebar-menu-width);
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.sidebar-left.sidebar-open {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
order: 2;
|
||||
overflow: hidden;
|
||||
transition: var(--sidebar-transition);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-right {
|
||||
flex: 1;
|
||||
margin-right: calc(var(--right-sidebar-menu-width) * -1);
|
||||
max-width: var(--right-sidebar-menu-width);
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.sidebar-right.sidebar-open {
|
||||
margin-right: 0;
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import './FlexboxLayout.css';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import ConnectedMeasurementTable from './ConnectedMeasurementTable';
|
||||
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
||||
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class FlexboxLayout extends Component {
|
||||
static propTypes = {
|
||||
studies: PropTypes.array,
|
||||
leftSidebarOpen: PropTypes.bool.isRequired,
|
||||
rightSidebarOpen: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
studiesForBrowser: [],
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.studies) {
|
||||
const studiesForBrowser = this.getStudiesForBrowser();
|
||||
|
||||
this.setState({
|
||||
studiesForBrowser,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.studies !== prevProps.studies) {
|
||||
const studiesForBrowser = this.getStudiesForBrowser();
|
||||
|
||||
this.setState({
|
||||
studiesForBrowser,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getStudiesForBrowser = () => {
|
||||
const { studies } = this.props;
|
||||
|
||||
// TODO[react]:
|
||||
// - Add sorting of display sets
|
||||
// - Add useMiddleSeriesInstanceAsThumbnail
|
||||
// - Add showStackLoadingProgressBar option
|
||||
return studies.map(study => {
|
||||
const { studyInstanceUid } = study;
|
||||
|
||||
const thumbnails = study.displaySets.map(displaySet => {
|
||||
const {
|
||||
displaySetInstanceUid,
|
||||
seriesDescription,
|
||||
seriesNumber,
|
||||
instanceNumber,
|
||||
numImageFrames,
|
||||
// TODO: This is undefined
|
||||
// modality,
|
||||
} = displaySet;
|
||||
|
||||
let imageId;
|
||||
let altImageText = ' '; // modality
|
||||
|
||||
if (displaySet.images && displaySet.images.length) {
|
||||
imageId = displaySet.images[0].getImageId();
|
||||
} else {
|
||||
altImageText = 'SR';
|
||||
}
|
||||
|
||||
return {
|
||||
imageId,
|
||||
altImageText,
|
||||
displaySetInstanceUid,
|
||||
seriesDescription,
|
||||
seriesNumber,
|
||||
instanceNumber,
|
||||
numImageFrames,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
studyInstanceUid,
|
||||
thumbnails,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
let mainContentClassName = 'main-content';
|
||||
if (this.props.leftSidebarOpen) {
|
||||
mainContentClassName += ' sidebar-left-open';
|
||||
}
|
||||
|
||||
if (this.props.rightSidebarOpen) {
|
||||
mainContentClassName += ' sidebar-right-open';
|
||||
}
|
||||
|
||||
// TODO[react]: Make ConnectedMeasurementTable extension with state.timepointManager
|
||||
return (
|
||||
<div className="FlexboxLayout">
|
||||
<div
|
||||
className={
|
||||
this.props.leftSidebarOpen
|
||||
? 'sidebar-menu sidebar-left sidebar-open'
|
||||
: 'sidebar-menu sidebar-left'
|
||||
}
|
||||
>
|
||||
<ConnectedStudyBrowser studies={this.state.studiesForBrowser} />
|
||||
</div>
|
||||
<div className={mainContentClassName}>
|
||||
<ConnectedViewerMain studies={this.props.studies} />
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
this.props.rightSidebarOpen
|
||||
? 'sidebar-menu sidebar-right sidebar-open'
|
||||
: 'sidebar-menu sidebar-right'
|
||||
}
|
||||
>
|
||||
<ConnectedMeasurementTable />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FlexboxLayout;
|
||||
@@ -1,7 +1,11 @@
|
||||
import './ToolbarRow.css';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { RoundedButtonGroup, ToolbarButton } from 'react-viewerbase';
|
||||
import {
|
||||
RoundedButtonGroup,
|
||||
ToolbarButton,
|
||||
ExpandableToolMenu,
|
||||
} from 'react-viewerbase';
|
||||
import { commandsManager, extensionManager } from './../App.js';
|
||||
|
||||
import ConnectedCineDialog from './ConnectedCineDialog';
|
||||
@@ -11,19 +15,17 @@ import { MODULE_TYPES } from 'ohif-core';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
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 = {
|
||||
leftSidebarOpen: PropTypes.bool.isRequired,
|
||||
rightSidebarOpen: PropTypes.bool.isRequired,
|
||||
setLeftSidebarOpen: PropTypes.func,
|
||||
setRightSidebarOpen: PropTypes.func,
|
||||
isLeftSidePanelOpen: PropTypes.bool.isRequired,
|
||||
isRightSidePanelOpen: PropTypes.bool.isRequired,
|
||||
selectedLeftSidePanel: PropTypes.string.isRequired,
|
||||
selectedRightSidePanel: PropTypes.string.isRequired,
|
||||
handleSidePanelChange: PropTypes.func,
|
||||
activeContexts: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
leftSidebarOpen: false,
|
||||
rightSidebarOpen: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -43,6 +45,43 @@ class ToolbarRow extends Component {
|
||||
};
|
||||
|
||||
this._handleBuiltIn = _handleBuiltIn.bind(this);
|
||||
|
||||
const panelModules = extensionManager.modules[MODULE_TYPES.PANEL];
|
||||
this.buttonGroups = {
|
||||
left: [
|
||||
// TODO: This should come from extensions, instead of being baked in
|
||||
{
|
||||
value: 'studies',
|
||||
icon: 'th-large',
|
||||
bottomLabel: 'Series',
|
||||
},
|
||||
],
|
||||
right: [],
|
||||
};
|
||||
|
||||
panelModules.forEach(panelExtension => {
|
||||
const panelModule = panelExtension.module;
|
||||
const defaultContexts = Array.from(panelModule.defaultContext);
|
||||
|
||||
// MENU OPTIONS
|
||||
panelModule.menuOptions.forEach(menuOption => {
|
||||
const contexts = Array.from(menuOption.context || defaultContexts);
|
||||
|
||||
const activeContextIncludesAnyPanelContexts = this.props.activeContexts.some(
|
||||
actx => contexts.includes(actx)
|
||||
);
|
||||
if (activeContextIncludesAnyPanelContexts) {
|
||||
const menuOptionEntry = {
|
||||
value: menuOption.target,
|
||||
icon: menuOption.icon,
|
||||
bottomLabel: menuOption.label,
|
||||
};
|
||||
const from = menuOption.from || 'right';
|
||||
|
||||
this.buttonGroups[from].push(menuOptionEntry);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -56,39 +95,7 @@ class ToolbarRow extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onLeftSidebarValueChanged = value => {
|
||||
this.props.setLeftSidebarOpen(!!value);
|
||||
};
|
||||
|
||||
onRightSidebarValueChanged = value => {
|
||||
this.props.setRightSidebarOpen(!!value);
|
||||
};
|
||||
|
||||
render() {
|
||||
const leftSidebarToggle = [
|
||||
{
|
||||
value: 'studies',
|
||||
icon: 'th-large',
|
||||
bottomLabel: 'Series',
|
||||
},
|
||||
];
|
||||
|
||||
const rightSidebarToggle = [
|
||||
{
|
||||
value: 'measurements',
|
||||
icon: 'list',
|
||||
bottomLabel: 'Measurements',
|
||||
},
|
||||
];
|
||||
|
||||
const leftSidebarValue = this.props.leftSidebarOpen
|
||||
? leftSidebarToggle[0].value
|
||||
: null;
|
||||
|
||||
const rightSidebarValue = this.props.rightSidebarOpen
|
||||
? rightSidebarToggle[0].value
|
||||
: null;
|
||||
|
||||
const buttonComponents = _getButtonComponents.call(
|
||||
this,
|
||||
this.state.toolbarButtons,
|
||||
@@ -102,14 +109,20 @@ class ToolbarRow extends Component {
|
||||
zIndex: 999,
|
||||
};
|
||||
|
||||
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={leftSidebarToggle}
|
||||
value={leftSidebarValue}
|
||||
onValueChanged={this.onLeftSidebarValueChanged}
|
||||
options={this.buttonGroups.left}
|
||||
value={this.props.selectedLeftSidePanel || ''}
|
||||
onValueChanged={onPressLeft}
|
||||
/>
|
||||
</div>
|
||||
{buttonComponents}
|
||||
@@ -119,11 +132,13 @@ class ToolbarRow extends Component {
|
||||
className="pull-right m-t-1 rm-x-1"
|
||||
style={{ marginLeft: 'auto' }}
|
||||
>
|
||||
<RoundedButtonGroup
|
||||
options={rightSidebarToggle}
|
||||
value={rightSidebarValue}
|
||||
onValueChanged={this.onRightSidebarValueChanged}
|
||||
/>
|
||||
{this.buttonGroups.right.length && (
|
||||
<RoundedButtonGroup
|
||||
options={this.buttonGroups.right}
|
||||
value={this.props.selectedRightSidePanel || ''}
|
||||
onValueChanged={onPressRight}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="CineDialogContainer" style={cineDialogContainerStyle}>
|
||||
@@ -140,36 +155,64 @@ class ToolbarRow extends Component {
|
||||
*/
|
||||
function _getButtonComponents(toolbarButtons, activeButtons) {
|
||||
return toolbarButtons.map((button, index) => {
|
||||
// TODO: If `button.buttons`, use `ExpandedToolMenu`
|
||||
// I don't believe any extensions currently leverage this
|
||||
if (button.buttons) {
|
||||
// Iterate over button definitions and update `onClick` behavior
|
||||
const childButtons = button.buttons.map(childButton => {
|
||||
childButton.onClick = _handleToolbarButtonClick.bind(this, childButton);
|
||||
return childButton;
|
||||
});
|
||||
|
||||
return (
|
||||
<ExpandableToolMenu
|
||||
key={button.id}
|
||||
text={button.label}
|
||||
icon={button.icon}
|
||||
buttons={childButtons}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
key={button.id}
|
||||
label={button.label}
|
||||
icon={button.icon}
|
||||
onClick={(evt, props) => {
|
||||
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') {
|
||||
this.setState({
|
||||
activeButtons: [button.id],
|
||||
});
|
||||
} else if (button.type === 'builtIn') {
|
||||
this._handleBuiltIn(button.options);
|
||||
}
|
||||
}}
|
||||
onClick={_handleToolbarButtonClick.bind(this, button)}
|
||||
isActive={activeButtons.includes(button.id)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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') {
|
||||
this.setState({
|
||||
activeButtons: [button.id],
|
||||
});
|
||||
} else if (button.type === 'builtIn') {
|
||||
this._handleBuiltIn(button.options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function _getVisibleToolbarButtons() {
|
||||
const toolbarModules = extensionManager.modules[MODULE_TYPES.TOOLBAR];
|
||||
const toolbarButtonDefinitions = [];
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
//import OHIF from 'ohif-core';
|
||||
//import { CineDialog } from 'react-viewerbase';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { MODULE_TYPES } from 'ohif-core';
|
||||
import OHIF from 'ohif-core';
|
||||
import moment from 'moment';
|
||||
import WhiteLabellingContext from '../WhiteLabellingContext.js';
|
||||
import ConnectedHeader from './ConnectedHeader.js';
|
||||
import ConnectedFlexboxLayout from './ConnectedFlexboxLayout.js';
|
||||
import ConnectedToolbarRow from './ConnectedToolbarRow.js';
|
||||
import ConnectedLabellingOverlay from './ConnectedLabellingOverlay';
|
||||
import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
||||
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
||||
import SidePanel from './../components/SidePanel.js';
|
||||
import { extensionManager } from './../App.js';
|
||||
import './Viewer.css';
|
||||
/**
|
||||
* Inits OHIF Hanging Protocol's onReady.
|
||||
@@ -53,6 +56,10 @@ class Viewer extends Component {
|
||||
studyInstanceUids: PropTypes.array,
|
||||
onTimepointsUpdated: PropTypes.func,
|
||||
onMeasurementsUpdated: PropTypes.func,
|
||||
// window.store.getState().viewports.viewportSpecificData
|
||||
viewports: PropTypes.object.isRequired,
|
||||
// window.store.getState().viewports.activeViewportIndex
|
||||
activeViewportIndex: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -75,6 +82,14 @@ class Viewer extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
state = {
|
||||
isLeftSidePanelOpen: true,
|
||||
isRightSidePanelOpen: false,
|
||||
selectedRightSidePanel: '',
|
||||
selectedLeftSidePanel: 'studies', // TODO: Don't hardcode this
|
||||
thumbnails: [],
|
||||
};
|
||||
|
||||
retrieveMeasurements = (patientId, timepointIds) => {
|
||||
OHIF.log.info('retrieveMeasurements');
|
||||
// TODO: Retrieve the measurements from the latest available SR
|
||||
@@ -171,8 +186,13 @@ class Viewer extends Component {
|
||||
|
||||
if (studies) {
|
||||
const patientId = studies[0] && studies[0].patientId;
|
||||
|
||||
timepointApi.retrieveTimepoints({ patientId });
|
||||
measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
|
||||
|
||||
this.setState({
|
||||
thumbnails: _mapStudiesToThumbnails(studies),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,14 +201,33 @@ class Viewer extends Component {
|
||||
const { studies } = this.props;
|
||||
const patientId = studies[0] && studies[0].patientId;
|
||||
const currentTimepointId = this.currentTimepointId;
|
||||
|
||||
this.timepointApi.retrieveTimepoints({ patientId });
|
||||
this.measurementApi.retrieveMeasurements(patientId, [currentTimepointId]);
|
||||
|
||||
this.setState({
|
||||
thumbnails: _mapStudiesToThumbnails(studies),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 */}
|
||||
<WhiteLabellingContext.Consumer>
|
||||
{whiteLabelling => (
|
||||
<ConnectedHeader home={false}>
|
||||
@@ -196,16 +235,132 @@ class Viewer extends Component {
|
||||
</ConnectedHeader>
|
||||
)}
|
||||
</WhiteLabellingContext.Consumer>
|
||||
<div id="viewer" className="Viewer">
|
||||
<ConnectedToolbarRow />
|
||||
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
|
||||
{/*<StudyPrefetcher studies={this.props.studies} />*/}
|
||||
<ConnectedFlexboxLayout studies={this.props.studies} />
|
||||
<ConnectedLabellingOverlay />
|
||||
|
||||
{/* TOOLBAR */}
|
||||
<ConnectedToolbarRow
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/*<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}
|
||||
activeIndex={this.props.activeViewportIndex}
|
||||
/>
|
||||
) : (
|
||||
<ConnectedStudyBrowser studies={this.state.thumbnails} />
|
||||
)}
|
||||
</SidePanel>
|
||||
|
||||
{/* MAIN */}
|
||||
<div className={classNames('main-content')}>
|
||||
<ConnectedViewerMain studies={this.props.studies} />
|
||||
</div>
|
||||
|
||||
{/* RIGHT */}
|
||||
<SidePanel from="right" isOpen={this.state.isRightSidePanelOpen}>
|
||||
{VisiblePanelRight && (
|
||||
<VisiblePanelRight
|
||||
viewports={this.props.viewports}
|
||||
activeIndex={this.props.activeViewportIndex}
|
||||
/>
|
||||
)}
|
||||
</SidePanel>
|
||||
</div>
|
||||
<ConnectedLabellingOverlay />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default 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 useMiddleSeriesInstanceAsThumbnail
|
||||
* - 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 = ' '; // modality
|
||||
|
||||
if (displaySet.images && displaySet.images.length) {
|
||||
imageId = displaySet.images[0].getImageId();
|
||||
} else {
|
||||
altImageText = 'SR';
|
||||
}
|
||||
|
||||
return {
|
||||
imageId,
|
||||
altImageText,
|
||||
displaySetInstanceUid,
|
||||
seriesDescription,
|
||||
seriesNumber,
|
||||
instanceNumber,
|
||||
numImageFrames,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
studyInstanceUid,
|
||||
thumbnails,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -15,27 +15,7 @@
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.ViewerMain>div {
|
||||
.ViewerMain > div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ViewerMain #imageViewerViewports .viewportContainer {
|
||||
/*theme('border', '%s solid $uiBorderColorDark' % $viewportBorderThickness)*/
|
||||
/*float: left;
|
||||
position: relative;*/
|
||||
outline: 0; /* Prevent blue outline in Chrome */
|
||||
}
|
||||
|
||||
.ViewerMain #imageViewerViewports .viewportContainer:hover,
|
||||
.ViewerMain #imageViewerViewports .viewportContainer:active,
|
||||
.ViewerMain #imageViewerViewports .viewportContainer:hover.active {
|
||||
outline: 0; /* Prevent blue outline in Chrome */
|
||||
}
|
||||
|
||||
/* &:hover
|
||||
//theme('border', '%s solid $uiBorderColor' % $viewportBorderThickness)
|
||||
|
||||
&.active, &:hover.active
|
||||
//theme('border', '%s solid $uiBorderColorActive' % $viewportBorderThickness)
|
||||
*/
|
||||
@@ -139,9 +139,9 @@ class ViewerMain extends Component {
|
||||
<div className="ViewerMain">
|
||||
{this.state.displaySets.length && (
|
||||
<ConnectedLayoutManager
|
||||
studies={this.props.studies}
|
||||
viewportData={this.getViewportData()}
|
||||
setViewportData={this.setViewportData}
|
||||
studies={this.props.studies}
|
||||
viewportData={this.getViewportData()}
|
||||
setViewportData={this.setViewportData}
|
||||
>
|
||||
{/* Children to add to each viewport that support children */}
|
||||
<ConnectedToolContextMenu />
|
||||
|
||||
Reference in new issue
Block a user