diff --git a/package.json b/package.json index 16f1cf8a6..e05f8ff06 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "@ohif/extension-dicom-pdf": "0.0.7", "@ohif/extension-vtk": "0.1.0", "@ohif/i18n": "0.1.0", + "@tanem/react-nprogress": "^1.1.25", "classnames": "^2.2.6", "cornerstone-core": "^2.2.8", "cornerstone-math": "^0.1.8", @@ -103,6 +104,7 @@ "react-resize-detector": "^4.2.0", "react-router": "^5.0.1", "react-router-dom": "^5.0.1", + "react-transition-group": "^4.1.1", "react-viewerbase": "0.12.0", "redux": "^4.0.1", "redux-logger": "^3.0.6", diff --git a/src/OHIFStandaloneViewer.css b/src/OHIFStandaloneViewer.css index b7a86669a..e76d6dd7f 100644 --- a/src/OHIFStandaloneViewer.css +++ b/src/OHIFStandaloneViewer.css @@ -1,17 +1,32 @@ html, body, #root { - background-color: black; - height: 100%; - width: 100%; - margin: 0; + background-color: black; + height: 100%; + width: 100%; + margin: 0; + padding: 0; + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-family: Roboto, OpenSans, HelveticaNeue-Light, Helvetica Neue Light, + Helvetica Neue, Helvetica, Arial, Lucida Grande, sans-serif; } -body { - box-sizing: border-box; - margin: 0; - padding: 0; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - font-family: Roboto,OpenSans,HelveticaNeue-Light,Helvetica Neue Light,Helvetica Neue,Helvetica,Arial,Lucida Grande,sans-serif; +#root .fade-enter { + opacity: 0; +} + +#root .fade-enter-active { + opacity: 1; + transition: opacity 200ms; +} + +#root .fade-exit { + opacity: 1; +} + +#root .fade-exit-active { + opacity: 0; + transition: opacity 200ms; } diff --git a/src/OHIFStandaloneViewer.js b/src/OHIFStandaloneViewer.js index 79caae389..9f31abee7 100644 --- a/src/OHIFStandaloneViewer.js +++ b/src/OHIFStandaloneViewer.js @@ -1,16 +1,19 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +import { withRouter, matchPath } from 'react-router'; +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 'react-viewerbase'; // import asyncComponent from './components/AsyncComponent.js' import IHEInvokeImageDisplay from './routes/IHEInvokeImageDisplay.js'; import ViewerRouting from './routes/ViewerRouting.js'; import StudyListRouting from './studylist/StudyListRouting.js'; import StandaloneRouting from './routes/StandaloneRouting.js'; -import CallbackPage from './CallbackPage.js'; -import { withRouter } from 'react-router'; -import { Route, Switch } from 'react-router-dom'; -import { connect } from 'react-redux'; -import { ViewerbaseDragDropContext } from 'react-viewerbase'; - +import CallbackPage from './routes/CallbackPage.js'; +import NotFound from './routes/NotFound.js'; +import { Bar, Container } from './components/LoadingBar/'; import './OHIFStandaloneViewer.css'; import './variables.css'; import './theme-tide.css'; @@ -32,6 +35,10 @@ import './theme-tide.css'; const reload = () => window.location.reload(); class OHIFStandaloneViewer extends Component { + state = { + isLoading: false, + }; + static propTypes = { history: PropTypes.object.isRequired, user: PropTypes.object, @@ -79,24 +86,84 @@ class OHIFStandaloneViewer extends Component { ); } + /** + * Note: this approach for routing is caused by the conflict between + * react-transition-group and react-router's component. + * + * See http://reactcommunity.org/react-transition-group/with-react-router/ + */ + const routes = [ + { + path: '/', + Component: StudyListRouting, + }, + { + path: '/studylist', + Component: StudyListRouting, + }, + { + path: '/viewer', + Component: StandaloneRouting, + }, + { + path: '/viewer/:studyInstanceUids', + Component: ViewerRouting, + }, + { + path: '/study/:studyInstanceUid/series/:seriesInstanceUids', + Component: ViewerRouting, + }, + { + path: '/IHEInvokeImageDisplay', + Component: IHEInvokeImageDisplay, + }, + ]; + + const currentPath = this.props.location.pathname; + const noMatchingRoutes = !routes.find(r => + matchPath(currentPath, { + path: r.path, + exact: true, + }) + ); + return ( - + <> + + {({ isFinished, progress, animationDuration }) => ( + + + + )} + - - - - - - -
Sorry, this page does not exist.
} /> -
+ {!noMatchingRoutes && + routes.map(({ path, Component }) => ( + + {({ match }) => ( + { + this.setState({ isLoading: true }); + }} + onEntered={() => { + this.setState({ isLoading: false }); + }} + > + {match === null ? <> : } + + )} + + ))} + {noMatchingRoutes && } + ); } } diff --git a/src/components/LoadingBar/Bar.js b/src/components/LoadingBar/Bar.js new file mode 100644 index 000000000..50effbde5 --- /dev/null +++ b/src/components/LoadingBar/Bar.js @@ -0,0 +1,32 @@ +import React from 'react'; + +const Bar = ({ progress, animationDuration }) => ( +
+
+
+); + +export default Bar; diff --git a/src/components/LoadingBar/Container.js b/src/components/LoadingBar/Container.js new file mode 100644 index 000000000..cd8270e73 --- /dev/null +++ b/src/components/LoadingBar/Container.js @@ -0,0 +1,15 @@ +import React from 'react'; + +const Container = ({ children, isFinished, animationDuration }) => ( +
+ {children} +
+); + +export default Container; diff --git a/src/components/LoadingBar/index.js b/src/components/LoadingBar/index.js new file mode 100644 index 000000000..7943b896a --- /dev/null +++ b/src/components/LoadingBar/index.js @@ -0,0 +1,4 @@ +import Bar from './Bar.js'; +import Container from './Container.js'; + +export { Bar, Container }; diff --git a/src/connectedComponents/FlexboxLayout.js b/src/connectedComponents/FlexboxLayout.js index 3467fc403..732e5828d 100644 --- a/src/connectedComponents/FlexboxLayout.js +++ b/src/connectedComponents/FlexboxLayout.js @@ -9,7 +9,7 @@ import PropTypes from 'prop-types'; class FlexboxLayout extends Component { static propTypes = { - studies: PropTypes.array.isRequired, + studies: PropTypes.array, leftSidebarOpen: PropTypes.bool.isRequired, rightSidebarOpen: PropTypes.bool.isRequired, }; @@ -19,11 +19,23 @@ class FlexboxLayout extends Component { }; componentDidMount() { - const studiesForBrowser = this.getStudiesForBrowser(); + if (this.props.studies) { + const studiesForBrowser = this.getStudiesForBrowser(); - this.setState({ - studiesForBrowser, - }); + this.setState({ + studiesForBrowser, + }); + } + } + + componentDidUpdate(prevProps) { + if (this.props.studies !== prevProps.studies) { + const studiesForBrowser = this.getStudiesForBrowser(); + + this.setState({ + studiesForBrowser, + }); + } } getStudiesForBrowser = () => { diff --git a/src/connectedComponents/Viewer.js b/src/connectedComponents/Viewer.js index 1faf3be60..0fd4610d2 100644 --- a/src/connectedComponents/Viewer.js +++ b/src/connectedComponents/Viewer.js @@ -165,9 +165,25 @@ class Viewer extends Component { onMeasurementsUpdated: this.onMeasurementsUpdated, }); - const patientId = studies[0] && studies[0].patientId; - timepointApi.retrieveTimepoints({ patientId }); - measurementApi.retrieveMeasurements(patientId, [currentTimepointId]); + this.currentTimepointId = currentTimepointId; + this.timepointApi = timepointApi; + this.measurementApi = measurementApi; + + if (studies) { + const patientId = studies[0] && studies[0].patientId; + timepointApi.retrieveTimepoints({ patientId }); + measurementApi.retrieveMeasurements(patientId, [currentTimepointId]); + } + } + + componentDidUpdate(prevProps) { + if (this.props.studies !== prevProps.studies) { + const { studies } = this.props; + const patientId = studies[0] && studies[0].patientId; + const currentTimepointId = this.currentTimepointId; + this.timepointApi.retrieveTimepoints({ patientId }); + this.measurementApi.retrieveMeasurements(patientId, [currentTimepointId]); + } } render() { diff --git a/src/connectedComponents/ViewerMain.js b/src/connectedComponents/ViewerMain.js index 8e412def1..02393911d 100644 --- a/src/connectedComponents/ViewerMain.js +++ b/src/connectedComponents/ViewerMain.js @@ -9,7 +9,7 @@ import React from 'react'; class ViewerMain extends Component { static propTypes = { activeViewportIndex: PropTypes.number.isRequired, - studies: PropTypes.array.isRequired, + studies: PropTypes.array, viewportSpecificData: PropTypes.object.isRequired, layout: PropTypes.object.isRequired, setViewportSpecificData: PropTypes.func.isRequired, @@ -59,11 +59,23 @@ class ViewerMain extends Component { //window.addEventListener('beforeunload', unloadHandlers.beforeUnload); // Get all the display sets for the viewer studies - const displaySets = this.getDisplaySets(this.props.studies); + if (this.props.studies) { + const displaySets = this.getDisplaySets(this.props.studies); - this.setState({ - displaySets, - }); + this.setState({ + displaySets, + }); + } + } + + componentDidUpdate(prevProps) { + if (this.props.studies !== prevProps.studies) { + const displaySets = this.getDisplaySets(this.props.studies); + + this.setState({ + displaySets, + }); + } } getViewportData = () => { @@ -125,14 +137,16 @@ class ViewerMain extends Component { render() { return (
- - {/* Children to add to each viewport that support children */} - - + > + {/* Children to add to each viewport that support children */} + + + )}
); } diff --git a/src/connectedComponents/ViewerRetrieveStudyData.js b/src/connectedComponents/ViewerRetrieveStudyData.js index 8622f4bd4..79c40b840 100644 --- a/src/connectedComponents/ViewerRetrieveStudyData.js +++ b/src/connectedComponents/ViewerRetrieveStudyData.js @@ -75,8 +75,6 @@ class ViewerRetrieveStudyData extends Component { render() { if (this.state.error) { return
Error: {JSON.stringify(this.state.error)}
; - } else if (!this.state.studies) { - return
Loading...
; } return ( diff --git a/src/CallbackPage.js b/src/routes/CallbackPage.js similarity index 100% rename from src/CallbackPage.js rename to src/routes/CallbackPage.js diff --git a/src/routes/NotFound.css b/src/routes/NotFound.css new file mode 100644 index 000000000..6f6b0780d --- /dev/null +++ b/src/routes/NotFound.css @@ -0,0 +1,9 @@ +.not-found { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + + color: white; +} diff --git a/src/routes/NotFound.js b/src/routes/NotFound.js new file mode 100644 index 000000000..2844b35da --- /dev/null +++ b/src/routes/NotFound.js @@ -0,0 +1,16 @@ +import React from 'react'; +import './NotFound.css'; +import { Link } from 'react-router-dom'; + +export default function NotFound() { + return ( +
+
+

Sorry, this page does not exist.

+
+ Go back to the Study List +
+
+
+ ); +} diff --git a/yarn.lock b/yarn.lock index 5a046bc4a..19b0a595f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1472,6 +1472,15 @@ "@svgr/plugin-svgo" "^4.0.3" loader-utils "^1.1.0" +"@tanem/react-nprogress@^1.1.25": + version "1.1.25" + resolved "https://registry.yarnpkg.com/@tanem/react-nprogress/-/react-nprogress-1.1.25.tgz#1c8e75a30181d5f9da3f99f8c97414d25bd7d398" + integrity sha512-SpHAd1ln3KeJf1UWIioPKXclzZ82UrmIH0piGY0J3llx30OAROEML6EWbIpHledMg7qAuB4NpJbHG0uPldBJmg== + dependencies: + "@babel/runtime" "^7.4.5" + hoist-non-react-statics "^3.3.0" + prop-types "^15.7.2" + "@types/babel__core@^7.1.0": version "7.1.2" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f" @@ -12509,6 +12518,16 @@ react-transition-group@^2.0.0, react-transition-group@^2.2.0: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" +react-transition-group@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.1.1.tgz#16efe9ac8c68306f6bef59c7da5a96b4dfd9fb32" + integrity sha512-K/N1wqJ2GRP2yj3WBqEUYa0KV5fiaAWpUfU9SpHOHefeKvyrO+VrnMBML21M19QZoVbDZKmuQFHZYoMMi1xuJA== + dependencies: + "@babel/runtime" "^7.4.5" + dom-helpers "^3.4.0" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react-viewerbase@0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/react-viewerbase/-/react-viewerbase-0.12.0.tgz#0315c4360ec51619e36defce403cebd29c430485"