diff --git a/extensions/cornerstone/src/index.js b/extensions/cornerstone/src/index.js index 0d30d2232..431948332 100644 --- a/extensions/cornerstone/src/index.js +++ b/extensions/cornerstone/src/index.js @@ -39,7 +39,9 @@ export default { const onNewImageHandler = jumpData => { commandsManager.runCommand('jumpToImage', jumpData); }; - return ; + return ( + + ); }; return ExtendedOHIFCornerstoneViewport; diff --git a/extensions/dicom-microscopy/src/DicomMicroscopyViewport.js b/extensions/dicom-microscopy/src/DicomMicroscopyViewport.js index acbfbca48..2290895dc 100644 --- a/extensions/dicom-microscopy/src/DicomMicroscopyViewport.js +++ b/extensions/dicom-microscopy/src/DicomMicroscopyViewport.js @@ -92,8 +92,8 @@ class DicomMicroscopyViewport extends Component { {this.state.error ? (

{JSON.stringify(this.state.error)}

) : ( -
- )} +
+ )}
); } diff --git a/extensions/dicom-rt/src/components/RTPanel/RTPanel.js b/extensions/dicom-rt/src/components/RTPanel/RTPanel.js index e6399bb02..80eb785d2 100644 --- a/extensions/dicom-rt/src/components/RTPanel/RTPanel.js +++ b/extensions/dicom-rt/src/components/RTPanel/RTPanel.js @@ -31,7 +31,18 @@ const refreshViewport = () => { * @param {number} props.isOpen - isOpen * @returns component */ -const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }) => { +const RTPanel = ({ + studies, + viewports, + activeIndex, + isOpen, + onContourItemClick, + activeContexts = [], + contexts = {} +}) => { + const isVTK = () => activeContexts.includes(contexts.VTK); + const isCornerstone = () => activeContexts.includes(contexts.CORNERSTONE); + const [selectedContour, setSelectedContour] = useState(); const DEFAULT_SET_INDEX = 0; const DEFAULT_STATE = { @@ -119,33 +130,35 @@ const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick } onClick={() => { setSelectedContour(isSameContour ? null : ROINumber); - const enabledElements = cornerstone.getEnabledElements(); - const element = enabledElements[activeIndex].element; - const toolState = cornerstoneTools.getToolState(element, 'stack'); + if (isCornerstone()) { + const enabledElements = cornerstone.getEnabledElements(); + const element = enabledElements[activeIndex].element; + const toolState = cornerstoneTools.getToolState(element, 'stack'); - if (!toolState) { - return; + if (!toolState) { + return; + } + + const imageIds = toolState.data[0].imageIds; + + const module = cornerstoneTools.getModule('rtstruct'); + const imageId = module.getters.imageIdOfCenterFrameOfROIContour( + state.selectedSet.SeriesInstanceUID, + ROINumber, + imageIds + ); + + const frameIndex = imageIds.indexOf(imageId); + const SOPInstanceUID = cornerstone.metaData.get('SOPInstanceUID', imageId); + const StudyInstanceUID = cornerstone.metaData.get('StudyInstanceUID', imageId); + + onContourItemClick({ + StudyInstanceUID, + SOPInstanceUID, + frameIndex, + activeViewportIndex: activeIndex + }); } - - const imageIds = toolState.data[0].imageIds; - - const module = cornerstoneTools.getModule('rtstruct'); - const imageId = module.getters.imageIdOfCenterFrameOfROIContour( - state.selectedSet.SeriesInstanceUID, - ROINumber, - imageIds - ); - - const frameIndex = imageIds.indexOf(imageId); - const SOPInstanceUID = cornerstone.metaData.get('SOPInstanceUID', imageId); - const StudyInstanceUID = cornerstone.metaData.get('StudyInstanceUID', imageId); - - onContourItemClick({ - StudyInstanceUID, - SOPInstanceUID, - frameIndex, - activeViewportIndex: activeIndex - }); }} label={`${ROIName} ${interpretedType}`} index={ROINumber} diff --git a/extensions/dicom-rt/src/index.js b/extensions/dicom-rt/src/index.js index e73bf1069..fae258a11 100644 --- a/extensions/dicom-rt/src/index.js +++ b/extensions/dicom-rt/src/index.js @@ -20,14 +20,21 @@ export default { preRegistration({ servicesManager, configuration = {} }) { init({ servicesManager, configuration }); }, - getPanelModule({ commandsManager }) { + getPanelModule({ commandsManager, api }) { const ExtendedRTPanel = props => { + const { activeContexts } = api.hooks.useAppContext(); + const contourItemClickHandler = contourData => { commandsManager.runCommand('jumpToImage', contourData); }; return ( - + ); }; diff --git a/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js b/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js index 084f7a0c9..2af848454 100644 --- a/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js +++ b/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js @@ -49,8 +49,8 @@ const SegmentationPanel = ({ onConfigurationChange, onDisplaySetLoadFailure, onSelectedSegmentationChange, - activeContexts, - contexts, + activeContexts = [], + contexts = {}, }) => { const isVTK = () => activeContexts.includes(contexts.VTK); const isCornerstone = () => activeContexts.includes(contexts.CORNERSTONE); diff --git a/extensions/dicom-segmentation/src/index.js b/extensions/dicom-segmentation/src/index.js index 0eb6ef545..a01184b66 100644 --- a/extensions/dicom-segmentation/src/index.js +++ b/extensions/dicom-segmentation/src/index.js @@ -46,7 +46,7 @@ export default { const onSegmentVisibilityChangeHandler = (segmentNumber, visible) => { commandsManager.runCommand('setSegmentConfiguration', { segmentNumber, - visible + visible, }); }; @@ -55,7 +55,7 @@ export default { globalOpacity: configuration.fillAlpha, outlineThickness: configuration.outlineWidth, renderOutline: configuration.renderOutline, - visible: configuration.renderFill + visible: configuration.renderFill, }); }; diff --git a/extensions/vtk/src/index.js b/extensions/vtk/src/index.js index f66061b43..4b0dee69e 100644 --- a/extensions/vtk/src/index.js +++ b/extensions/vtk/src/index.js @@ -1,3 +1,4 @@ +import React from 'react'; import asyncComponent from './asyncComponent.js'; import commandsModule from './commandsModule.js'; import toolbarModule from './toolbarModule.js'; @@ -16,7 +17,8 @@ const vtkExtension = { id: 'vtk', getViewportModule({ commandsManager }) { - return withCommandsManager(OHIFVTKViewport, commandsManager); + const ExtendedVTKViewport = props => ; + return withCommandsManager(ExtendedVTKViewport, commandsManager); }, getToolbarModule() { return toolbarModule; diff --git a/platform/ui/package.json b/platform/ui/package.json index ea67c6537..97a2732aa 100644 --- a/platform/ui/package.json +++ b/platform/ui/package.json @@ -53,6 +53,7 @@ "react-dnd-html5-backend": "^9.4.0", "react-dnd-touch-backend": "^9.4.0", "react-draggable": "^4.1.0", + "react-error-boundary": "^2.2.1", "react-i18next": "^10.11.0", "react-modal": "^3.11.1", "react-with-direction": "1.3.0" diff --git a/platform/ui/src/components/errorBoundary/ErrorBoundary.js b/platform/ui/src/components/errorBoundary/ErrorBoundary.js new file mode 100644 index 000000000..d2acec560 --- /dev/null +++ b/platform/ui/src/components/errorBoundary/ErrorBoundary.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ErrorBoundary } from 'react-error-boundary'; +import './ErrorFallback.css'; + +const ErrorFallback = ({ error, componentStack, resetErrorBoundary }) => { + return ( +
+

Something went wrong.

+
{error.message}
+
{componentStack}
+
+ ); +}; + +const OHIFErrorBoundary = ({ + context = 'OHIF', + onReset = () => {}, + onError = () => {}, + fallbackComponent, + children, +}) => { + const onErrorHandler = (error, componentStack) => { + console.error(`${context} Error Boundary`, error, componentStack); + onError(error, componentStack); + }; + + const onResetHandler = () => { + onReset(); + }; + + return ( + + {children} + + ); +}; + +OHIFErrorBoundary.propTypes = { + context: PropTypes.string, + onReset: PropTypes.func, + onError: PropTypes.func, + children: PropTypes.node.isRequired, + fallbackComponent: PropTypes.element, +}; + +export default OHIFErrorBoundary; diff --git a/platform/ui/src/components/errorBoundary/ErrorFallback.css b/platform/ui/src/components/errorBoundary/ErrorFallback.css new file mode 100644 index 000000000..c000e83c8 --- /dev/null +++ b/platform/ui/src/components/errorBoundary/ErrorFallback.css @@ -0,0 +1,4 @@ +.ErrorFallback { + padding: 10px; + color: var(--active-color); +} diff --git a/platform/ui/src/components/errorBoundary/index.js b/platform/ui/src/components/errorBoundary/index.js new file mode 100644 index 000000000..e5d6dda21 --- /dev/null +++ b/platform/ui/src/components/errorBoundary/index.js @@ -0,0 +1 @@ +export { default as ErrorBoundary } from './ErrorBoundary'; diff --git a/platform/ui/src/components/index.js b/platform/ui/src/components/index.js index 3624e4115..c3853be93 100644 --- a/platform/ui/src/components/index.js +++ b/platform/ui/src/components/index.js @@ -7,7 +7,6 @@ import { AboutContent } from './content/aboutContent/AboutContent'; import { TabComponents, TabFooter } from './tabComponents'; import { HotkeyField } from './customForm'; import { LanguageSwitcher } from './languageSwitcher'; - import { Checkbox } from './checkbox'; import { CineDialog } from './cineDialog'; import { ViewportDownloadForm } from './content/viewportDownloadForm'; @@ -25,8 +24,10 @@ import { } from './studyList'; import { ToolbarSection } from './toolbarSection'; import { Tooltip } from './tooltip'; +import { ErrorBoundary } from './errorBoundary'; export { + ErrorBoundary, ContextMenu, Checkbox, CineDialog, diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js index 451ac244e..36237ac06 100644 --- a/platform/ui/src/index.js +++ b/platform/ui/src/index.js @@ -29,6 +29,7 @@ import { Tooltip, AboutContent, OHIFModal, + ErrorBoundary } from './components'; import { useDebounce, useMedia } from './hooks'; @@ -123,6 +124,7 @@ export { DialogProvider, withDialog, useDialog, + ErrorBoundary, // Hooks useDebounce, useMedia, diff --git a/platform/viewer/cypress/integration/pwa/OHIFExtensionMicroscopy.spec.js b/platform/viewer/cypress/integration/pwa/OHIFExtensionMicroscopy.spec.js index 5e0ac5386..c650e86f9 100644 --- a/platform/viewer/cypress/integration/pwa/OHIFExtensionMicroscopy.spec.js +++ b/platform/viewer/cypress/integration/pwa/OHIFExtensionMicroscopy.spec.js @@ -1,7 +1,7 @@ describe('OHIF Microscopy Extension', () => { before(() => { cy.openStudyModality('SM'); - cy.expectMinimumThumbnails(6); + cy.expectMinimumThumbnails(2); }); it('checks if series thumbnails are being displayed', () => { diff --git a/platform/viewer/src/App.js b/platform/viewer/src/App.js index 47d869c9d..07e62c283 100644 --- a/platform/viewer/src/App.js +++ b/platform/viewer/src/App.js @@ -13,6 +13,7 @@ import { ModalProvider, DialogProvider, OHIFModal, + ErrorBoundary } from '@ohif/ui'; import { @@ -167,53 +168,57 @@ class App extends Component { if (this._userManager) { return ( - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + ); } return ( - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + ); } diff --git a/platform/viewer/src/OHIFStandaloneViewer.js b/platform/viewer/src/OHIFStandaloneViewer.js index baad5d759..fd5d875e5 100644 --- a/platform/viewer/src/OHIFStandaloneViewer.js +++ b/platform/viewer/src/OHIFStandaloneViewer.js @@ -5,7 +5,7 @@ import { Route, Switch } from 'react-router-dom'; import { NProgress } from '@tanem/react-nprogress'; import { CSSTransition } from 'react-transition-group'; import { connect } from 'react-redux'; -import { ViewerbaseDragDropContext } from '@ohif/ui'; +import { ViewerbaseDragDropContext, ErrorBoundary } from '@ohif/ui'; import { SignoutCallbackComponent } from 'redux-oidc'; import asyncComponent from './components/AsyncComponent.js'; import * as RoutesUtil from './routes/routesUtil'; @@ -191,7 +191,9 @@ class OHIFStandaloneViewer extends Component { {match === null ? ( <> ) : ( - + + + )} )} diff --git a/platform/viewer/src/components/ErrorBoundaryDialog/ErrorBoundaryDialog.js b/platform/viewer/src/components/ErrorBoundaryDialog/ErrorBoundaryDialog.js new file mode 100644 index 000000000..24a5807c0 --- /dev/null +++ b/platform/viewer/src/components/ErrorBoundaryDialog/ErrorBoundaryDialog.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { ErrorBoundary } from '@ohif/ui'; +import { servicesManager } from './../../App'; + +const { UIModalService } = servicesManager.services; + +const ErrorBoundaryDialog = ({ context, children }) => { + const handleOnError = (error, componentStack) => { + const ErrorDialog = () => ( +
+
+

+ {context}: {error.message} +

+
+
{componentStack}
+
+ ); + + UIModalService.show({ + content: ErrorDialog, + title: `${context}: ${error.message}`, + }); + }; + + const fallbackComponent = () => ( +
+

+ Error rendering {context}.
Check the browser console for more + details. +

+
+ ); + + return ( + + {children} + + ); +}; + +ErrorBoundaryDialog.propTypes = { + context: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, +}; + +export default ErrorBoundaryDialog; diff --git a/platform/viewer/src/components/ErrorBoundaryDialog/index.js b/platform/viewer/src/components/ErrorBoundaryDialog/index.js new file mode 100644 index 000000000..0fe70549f --- /dev/null +++ b/platform/viewer/src/components/ErrorBoundaryDialog/index.js @@ -0,0 +1,3 @@ +import ErrorBoundaryDialog from './ErrorBoundaryDialog'; + +export default ErrorBoundaryDialog; diff --git a/platform/viewer/src/components/ViewportGrid/ViewportGrid.js b/platform/viewer/src/components/ViewportGrid/ViewportGrid.js index e59449956..cb52c06c6 100644 --- a/platform/viewer/src/components/ViewportGrid/ViewportGrid.js +++ b/platform/viewer/src/components/ViewportGrid/ViewportGrid.js @@ -12,7 +12,7 @@ import EmptyViewport from './EmptyViewport.js'; const { loadAndCacheDerivedDisplaySets } = utils; -const ViewportGrid = function (props) { +const ViewportGrid = function(props) { const { activeViewportIndex, availablePlugins, @@ -24,7 +24,7 @@ const ViewportGrid = function (props) { studies, viewportData, children, - isStudyLoaded + isStudyLoaded, } = props; const rowSize = 100 / numRows; @@ -54,7 +54,7 @@ const ViewportGrid = function (props) { }); }); } - }, [studies, viewportData, isStudyLoaded]); + }, [studies, viewportData, isStudyLoaded, snackbar]); const getViewportPanes = () => layout.viewports.map((layout, viewportIndex) => { diff --git a/platform/viewer/src/components/ViewportGrid/ViewportPane.js b/platform/viewer/src/components/ViewportGrid/ViewportPane.js index 15a6bcd65..f1995f297 100644 --- a/platform/viewer/src/components/ViewportGrid/ViewportPane.js +++ b/platform/viewer/src/components/ViewportGrid/ViewportPane.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import './ViewportPane.css'; -const ViewportPane = function(props) { +const ViewportPane = function (props) { const { children, onDrop, viewportIndex, className: propClassName } = props; const [{ hovered, highlighted }, drop] = useDrop({ accept: 'thumbnail', diff --git a/platform/viewer/src/connectedComponents/Viewer.js b/platform/viewer/src/connectedComponents/Viewer.js index 7bb799df6..632d2909b 100644 --- a/platform/viewer/src/connectedComponents/Viewer.js +++ b/platform/viewer/src/connectedComponents/Viewer.js @@ -2,8 +2,7 @@ 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 OHIF, { MODULE_TYPES, DICOMSR } from '@ohif/core'; import { withDialog } from '@ohif/ui'; import moment from 'moment'; import ConnectedHeader from './ConnectedHeader.js'; @@ -11,6 +10,7 @@ import ToolbarRow from './ToolbarRow.js'; import ConnectedStudyBrowser from './ConnectedStudyBrowser.js'; import ConnectedViewerMain from './ConnectedViewerMain.js'; import SidePanel from './../components/SidePanel.js'; +import ErrorBoundaryDialog from './../components/ErrorBoundaryDialog'; import { extensionManager } from './../App.js'; // Contexts @@ -26,6 +26,7 @@ class Viewer extends Component { PropTypes.shape({ StudyInstanceUID: PropTypes.string.isRequired, StudyDate: PropTypes.string, + PatientID: PropTypes.string, displaySets: PropTypes.arrayOf( PropTypes.shape({ displaySetInstanceUID: PropTypes.string.isRequired, @@ -256,42 +257,44 @@ class Viewer extends Component { {/* 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]; + + { + 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); - this.setState(updatedState); - }} - studies={this.props.studies} - /> + 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} + /> + {/**/} {/**/} @@ -299,37 +302,46 @@ class Viewer extends Component { {/* VIEWPORTS + SIDEPANELS */}
{/* LEFT */} - - {VisiblePanelLeft ? ( - - ) : ( + + + {VisiblePanelLeft ? ( + + ) : ( )} - + + {/* MAIN */}
- + + +
{/* RIGHT */} - - {VisiblePanelRight && ( - - )} - + + + {VisiblePanelRight && ( + + )} + +
); @@ -349,7 +361,7 @@ export default withDialog(Viewer); * @param {Study[]} studies * @param {DisplaySet[]} studies[].displaySets */ -const _mapStudiesToThumbnails = function (studies) { +const _mapStudiesToThumbnails = function(studies) { return studies.map(study => { const { StudyInstanceUID } = study; diff --git a/yarn.lock b/yarn.lock index 2e4be842c..f774efc57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1247,7 +1247,7 @@ pirates "^4.0.0" source-map-support "^0.5.9" -"@babel/runtime@7.1.2", "@babel/runtime@7.5.5", "@babel/runtime@7.6.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@7.1.2", "@babel/runtime@7.5.5", "@babel/runtime@7.6.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.6": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== @@ -15796,6 +15796,13 @@ react-dropzone@^10.1.7: file-selector "^0.1.11" prop-types "^15.7.2" +react-error-boundary@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-2.2.1.tgz#32ed74386a90482060cc2fea948bb7135465c4cb" + integrity sha512-8SZMkJRFUb0JuluHKwuUtkh5vvVWBg3O/bJIWgNMhMJv529aG//TmcphR3cSMhCWjz1vFZDb4taJd6pmwP5mEQ== + dependencies: + "@babel/runtime" "^7.9.6" + react-error-overlay@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.1.tgz#417addb0814a90f3a7082eacba7cee588d00da89"