Compare commits
46
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86c479ac04 | ||
|
|
460e375f0a | ||
|
|
78344c2c66 | ||
|
|
b451ce4407 | ||
|
|
21b586b08f | ||
|
|
3bbeb1c521 | ||
|
|
7668b69f20 | ||
|
|
3a12138134 | ||
|
|
97786b808c | ||
|
|
cd1baa1b36 | ||
|
|
3ae3bee452 | ||
|
|
fd017c4512 | ||
|
|
c680720ce5 | ||
|
|
dfc4e89a91 | ||
|
|
13d337aaab | ||
|
|
4f26940005 | ||
|
|
9d08e81aa5 | ||
|
|
1a539d04c2 | ||
|
|
5cc78c867f | ||
|
|
cefa480979 | ||
|
|
66537bf1b9 | ||
|
|
862d426c71 | ||
|
|
e62f403fe9 | ||
|
|
dea0ceab57 | ||
|
|
a6c1e6c717 | ||
|
|
b03739428f | ||
|
|
2a52d9f05d | ||
|
|
d5e21bb5bb | ||
|
|
04ffe3ec76 | ||
|
|
9cdf8b690b | ||
|
|
8d49b31d03 | ||
|
|
e4cfae7ad9 | ||
|
|
e53041f630 | ||
|
|
35c54eac56 | ||
|
|
e7be573328 | ||
|
|
8a7706b286 | ||
|
|
c217b8bbde | ||
|
|
74f3cbc4b2 | ||
|
|
d091cd604c | ||
|
|
3a4280163d | ||
|
|
2d75e01ea0 | ||
|
|
5c5a49486d | ||
|
|
a8a57132a7 | ||
|
|
92c8996ed4 | ||
|
|
f07dca5fa4 | ||
|
|
c99e0d8a23 |
No files matched your search
+66
-4
@@ -13,7 +13,7 @@ version: 2.1
|
||||
##
|
||||
orbs:
|
||||
codecov: codecov/codecov@1.0.5
|
||||
cypress: cypress-io/cypress@1.10.0
|
||||
cypress: cypress-io/cypress@1.11.0
|
||||
|
||||
defaults: &defaults
|
||||
docker:
|
||||
@@ -198,7 +198,7 @@ jobs:
|
||||
- run: chmod +x ~/repo/.circleci/build-and-publish-docs.sh
|
||||
- run: ~/repo/.circleci/build-and-publish-docs.sh
|
||||
|
||||
DOCKER_PUBLISH:
|
||||
DOCKER_MASTER_PUBLISH:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
@@ -227,6 +227,43 @@ jobs:
|
||||
docker push ohif/$IMAGE_NAME:latest
|
||||
fi
|
||||
|
||||
DOCKER_PR_PUBLISH:
|
||||
<<: *defaults
|
||||
steps:
|
||||
# Enable yarn workspaces
|
||||
- run: yarn config set workspaces-experimental true
|
||||
|
||||
# Checkout code and ALL Git Tags
|
||||
- checkout:
|
||||
post:
|
||||
- git fetch --all
|
||||
|
||||
- restore_cache:
|
||||
name: Restore Yarn and Cypress Package Cache
|
||||
keys:
|
||||
# when lock file changes, use increasingly general patterns to restore cache
|
||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- yarn-packages-
|
||||
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: yarn install --frozen-lockfile
|
||||
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: false
|
||||
|
||||
- run:
|
||||
name: Build and push Docker image
|
||||
command: |
|
||||
# Remove npm config
|
||||
rm -f ./.npmrc
|
||||
# Set our version number using vars
|
||||
echo $CIRCLE_BUILD_NUM
|
||||
# Build our image, auth, and push
|
||||
docker build --tag ohif/viewer:PR_BUILD-$CIRCLE_BUILD_NUM .
|
||||
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
|
||||
docker push ohif/viewer:PR_BUILD-$CIRCLE_BUILD_NUM
|
||||
|
||||
VIEWER_DEMO_BUILD:
|
||||
<<: *defaults
|
||||
steps:
|
||||
@@ -288,10 +325,13 @@ workflows:
|
||||
# E2E: PWA
|
||||
- cypress/run:
|
||||
name: 'E2E: PWA'
|
||||
executor: cypress/browsers-chrome76
|
||||
browser: chrome
|
||||
pre-steps:
|
||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||
add wait-on' # Use yarn latest
|
||||
yarn: true
|
||||
record: false
|
||||
store_artifacts: false
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true yarn run build
|
||||
@@ -307,14 +347,18 @@ workflows:
|
||||
# E2E: script-tag
|
||||
- cypress/run:
|
||||
name: 'E2E: Script Tag'
|
||||
executor: cypress/browsers-chrome76
|
||||
browser: chrome
|
||||
pre-steps:
|
||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||
add wait-on' # Use yarn latest
|
||||
yarn: true
|
||||
record: false
|
||||
store_artifacts: false
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true yarn run build:package
|
||||
start: yarn run test:e2e:serve
|
||||
spec: 'cypress/integration/common/**/*'
|
||||
wait-on: 'http://localhost:3000'
|
||||
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
no-workspace: true # Don't persist workspace
|
||||
@@ -324,6 +368,17 @@ workflows:
|
||||
requires:
|
||||
- UNIT_TESTS
|
||||
|
||||
PR_OPTIONAL_DOCKER_PUBLISH:
|
||||
jobs:
|
||||
# https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval
|
||||
- AWAIT_APPROVAL:
|
||||
type: approval
|
||||
# Update hub.docker.org
|
||||
- DOCKER_PR_PUBLISH:
|
||||
context: Docker Hub
|
||||
requires:
|
||||
- AWAIT_APPROVAL
|
||||
|
||||
# MERGE TO MASTER
|
||||
RELEASE:
|
||||
jobs:
|
||||
@@ -334,16 +389,19 @@ workflows:
|
||||
# E2E: PWA + Persist
|
||||
- cypress/run:
|
||||
name: 'E2E: PWA'
|
||||
executor: cypress/browsers-chrome76
|
||||
browser: chrome
|
||||
pre-steps:
|
||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||
add wait-on' # Use yarn latest
|
||||
yarn: true
|
||||
record: true
|
||||
record: false
|
||||
store_artifacts: true
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true yarn run build
|
||||
start: yarn run test:e2e:serve
|
||||
wait-on: 'http://localhost:3000'
|
||||
command: 'yarn run test:e2e:ci'
|
||||
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
no-workspace: true # Don't persist workspace
|
||||
post-steps:
|
||||
@@ -356,14 +414,18 @@ workflows:
|
||||
# E2E: script-tag
|
||||
- cypress/run:
|
||||
name: 'E2E: Script Tag'
|
||||
executor: cypress/browsers-chrome76
|
||||
browser: chrome
|
||||
pre-steps:
|
||||
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
|
||||
add wait-on' # Use yarn latest
|
||||
yarn: true
|
||||
record: false
|
||||
store_artifacts: false
|
||||
working_directory: platform/viewer
|
||||
build: npx cross-env QUICK_BUILD=true yarn run build:package
|
||||
start: yarn run test:e2e:serve
|
||||
spec: 'cypress/integration/common/**/*'
|
||||
wait-on: 'http://localhost:3000'
|
||||
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
no-workspace: true # Don't persist workspace
|
||||
@@ -393,6 +455,6 @@ workflows:
|
||||
requires:
|
||||
- VIEWER_DEMO_BUILD
|
||||
# Update hub.docker.org
|
||||
- DOCKER_PUBLISH:
|
||||
- DOCKER_MASTER_PUBLISH:
|
||||
requires:
|
||||
- NPM_PUBLISH
|
||||
@@ -0,0 +1 @@
|
||||
PERCY_TOKEN=<your token here>
|
||||
@@ -0,0 +1,29 @@
|
||||
### Request Checklist
|
||||
|
||||
- [] Brief description of changes
|
||||
- [] Links to any relevant issues
|
||||
- [] Required status checks are passing
|
||||
- [] `@mention` a maintainer to request a review
|
||||
|
||||
#### Special
|
||||
|
||||
> If this PR is not on a branch for this repo, it is "untrusted" and does not
|
||||
> have access to env vars. Reviewers can kick off a trusted docker deploy by:
|
||||
>
|
||||
> 1. Verifying there are no malicious changes to CI build
|
||||
> 2. Using the [git-push-fork-to-upstream-branch][script] script
|
||||
> 3. Executing the manual approval step in the CI workflow
|
||||
|
||||
- [] Request docker publish of PR
|
||||
|
||||
_Want to improve this process? Consider PR'ing the ability for reviewers to add
|
||||
a label to trigger this process._
|
||||
|
||||
<!--
|
||||
Links
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[blog]: https://circleci.com/blog/triggering-trusted-ci-jobs-on-untrusted-forks/
|
||||
[script]: https://github.com/jklukas/git-push-fork-to-upstream-branch
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -21,6 +21,7 @@ npm-debug.log
|
||||
package-lock.json
|
||||
yarn-error.log
|
||||
.DS_Store
|
||||
.env
|
||||
|
||||
# Common Example Data Directories
|
||||
sampledata/
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// ~~ ENV
|
||||
const dotenv = require('dotenv');
|
||||
//
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const PACKAGE = require('../platform/viewer/package.json');
|
||||
// ~~ RULES
|
||||
const loadShadersRule = require('./rules/loadShaders.js');
|
||||
const loadWebWorkersRule = require('./rules/loadWebWorkers.js');
|
||||
@@ -9,6 +13,10 @@ const TerserJSPlugin = require('terser-webpack-plugin');
|
||||
// ~~ ENV VARS
|
||||
const NODE_ENV = process.env.NODE_ENV;
|
||||
const QUICK_BUILD = process.env.QUICK_BUILD;
|
||||
const BUILD_NUM = process.env.CIRCLE_BUILD_NUM || '0';
|
||||
|
||||
//
|
||||
dotenv.config();
|
||||
|
||||
module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
||||
if (!process.env.NODE_ENV) {
|
||||
@@ -68,6 +76,8 @@ module.exports = (env, argv, { SRC_DIR, DIST_DIR }) => {
|
||||
'process.env.DEBUG': JSON.stringify(process.env.DEBUG),
|
||||
'process.env.APP_CONFIG': JSON.stringify(process.env.APP_CONFIG || ''),
|
||||
'process.env.PUBLIC_URL': JSON.stringify(process.env.PUBLIC_URL || '/'),
|
||||
'process.env.VERSION_NUMBER': JSON.stringify(PACKAGE.version || ''),
|
||||
'process.env.BUILD_NUM': JSON.stringify(BUILD_NUM),
|
||||
}),
|
||||
],
|
||||
// Fix: https://github.com/webpack-contrib/css-loader/issues/447#issuecomment-285598881
|
||||
|
||||
@@ -44,6 +44,7 @@ RUN yarn config set workspaces-experimental true
|
||||
RUN yarn install
|
||||
|
||||
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
||||
ENV QUICK_BUILD true
|
||||
# ENV GENERATE_SOURCEMAP=false
|
||||
# ENV REACT_APP_CONFIG=config/default.js
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
[![Netlify Status][netlify-image]][netlify-url]
|
||||
[![CircleCI][circleci-image]][circleci-url]
|
||||
[![codecov][codecov-image]][codecov-url]
|
||||
[![This project is using Percy.io for visual regression testing.][percy-image]](percy-url)
|
||||
[](#contributors)
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
@@ -288,6 +289,8 @@ MIT © [OHIF](https://github.com/OHIF)
|
||||
[docker-image-url]: https://hub.docker.com/r/ohif/viewer
|
||||
[license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square
|
||||
[license-url]: LICENSE
|
||||
[percy-image]: https://percy.io/static/images/percy-badge.svg
|
||||
[percy-url]: https://percy.io/Open-Health-Imaging-Foundation/OHIF-Viewer
|
||||
<!-- Links -->
|
||||
[monorepo]: https://en.wikipedia.org/wiki/Monorepo
|
||||
[how-to-fork]: https://help.github.com/en/articles/fork-a-repo
|
||||
|
||||
@@ -48,7 +48,7 @@ If everything worked as expected, you should have a new `dist/` directory in the
|
||||
project's folder. It should roughly resemble the following:
|
||||
|
||||
```bash
|
||||
dist/
|
||||
<root>platform/viewer/dist/
|
||||
├── app-config.js
|
||||
├── app.bundle.js
|
||||
├── app.css
|
||||
|
||||
@@ -3,6 +3,98 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.52.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.17...@ohif/extension-vtk@0.52.18) (2019-10-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092)) ([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.16...@ohif/extension-vtk@0.52.17) (2019-10-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Fix crosshairs location when using MIP ([#1080](https://github.com/OHIF/Viewers/issues/1080)) ([b451ce4](https://github.com/OHIF/Viewers/commit/b451ce440796f13b0891739d2130e8ee71993d3d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.15...@ohif/extension-vtk@0.52.16) (2019-10-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Switch to orhtographic view for 2D MPR ([#1074](https://github.com/OHIF/Viewers/issues/1074)) ([13d337a](https://github.com/OHIF/Viewers/commit/13d337aaabb8dadf6366c6262c5e47e7781edd08))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.14...@ohif/extension-vtk@0.52.15) (2019-10-23)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* ⚡️ Throttle UI refresh of synced WL value in vtk port ([#1070](https://github.com/OHIF/Viewers/issues/1070)) ([9d08e81](https://github.com/OHIF/Viewers/commit/9d08e81aa5def63a5e34f464aae099f7c29f0ac5))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.13...@ohif/extension-vtk@0.52.14) (2019-10-22)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.12...@ohif/extension-vtk@0.52.13) (2019-10-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.11...@ohif/extension-vtk@0.52.12) (2019-10-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.10...@ohif/extension-vtk@0.52.11) (2019-10-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.9...@ohif/extension-vtk@0.52.10) (2019-10-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.8...@ohif/extension-vtk@0.52.9) (2019-10-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.7...@ohif/extension-vtk@0.52.8) (2019-10-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "0.52.8",
|
||||
"version": "0.52.18",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -48,11 +48,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"react-vtkjs-viewport": "^0.1.6"
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"react-vtkjs-viewport": "^0.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^1.3.2",
|
||||
"@ohif/ui": "^0.53.4",
|
||||
"@ohif/core": "^1.5.1",
|
||||
"@ohif/ui": "^0.56.0",
|
||||
"cornerstone-tools": "^4.0.9",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import OHIF from "@ohif/core";
|
||||
import { View2D } from "react-vtkjs-viewport";
|
||||
import { connect } from "react-redux";
|
||||
import OHIF from '@ohif/core';
|
||||
import { View2D } from 'react-vtkjs-viewport';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
|
||||
|
||||
@@ -24,7 +24,7 @@ const mapStateToProps = (state, ownProps) => {
|
||||
// Hopefully this doesn't break anything under the hood for this one
|
||||
// activeTool: activeButton && activeButton.command,
|
||||
...dataFromStore,
|
||||
enableStackPrefetch: isActive
|
||||
enableStackPrefetch: isActive,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
|
||||
setViewportSpecificData: data => {
|
||||
dispatch(setViewportSpecificData(viewportIndex, data));
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -63,10 +63,10 @@ const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
|
||||
// Store the API details for later
|
||||
//setViewportSpecificData({ vtkApi: api });
|
||||
|
||||
if (afterCreation && typeof afterCreation === "function") {
|
||||
if (afterCreation && typeof afterCreation === 'function') {
|
||||
afterCreation(api);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
return props;
|
||||
};
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
}
|
||||
|
||||
.loadingIndicator {
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
font-size: 18px;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
font-size: 8px;
|
||||
height: 20%;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
/* Necessary for click-through to cornerstone element below */
|
||||
/* Necessary for click-through to vtkjs element below */
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import './LoadingIndicator.css';
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class LoadingIndicator extends PureComponent {
|
||||
@@ -35,7 +35,8 @@ class LoadingIndicator extends PureComponent {
|
||||
<div className="imageViewerLoadingIndicator loadingIndicator">
|
||||
<div className="indicatorContents">
|
||||
<p>
|
||||
Loading... <i className="fa fa-spin fa-circle-o-notch fa-fw" />{' '}
|
||||
{this.props.t('Reformatting')}...
|
||||
<i className="fa fa-spin fa-circle-o-notch fa-fw" />
|
||||
{percComplete}
|
||||
</p>
|
||||
</div>
|
||||
@@ -46,4 +47,4 @@ class LoadingIndicator extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
export default LoadingIndicator;
|
||||
export default withTranslation('Common')(LoadingIndicator);
|
||||
@@ -11,7 +11,6 @@ import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
|
||||
import vtkImageData from 'vtk.js/Sources/Common/DataModel/ImageData';
|
||||
import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
|
||||
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
|
||||
import vtkViewportSubscriptionManager from './utils/vtkViewportSubscriptionManager.js';
|
||||
|
||||
const { StackManager } = OHIF.utils;
|
||||
|
||||
@@ -172,56 +171,58 @@ class OHIFVTKViewport extends Component {
|
||||
default:
|
||||
imageDataObject = getImageData(stack.imageIds, displaySetInstanceUid);
|
||||
|
||||
const loadImageDataPromise = loadImageData(imageDataObject);
|
||||
|
||||
return loadImageDataPromise.then(() => {
|
||||
return {
|
||||
data: imageDataObject.vtkImageData,
|
||||
};
|
||||
});
|
||||
return imageDataObject;
|
||||
}
|
||||
};
|
||||
|
||||
getOrCreateVolume(data, displaySetInstanceUid) {
|
||||
getOrCreateVolume(imageDataObject, displaySetInstanceUid) {
|
||||
if (volumeCache[displaySetInstanceUid]) {
|
||||
return volumeCache[displaySetInstanceUid];
|
||||
}
|
||||
|
||||
const { vtkImageData, imageMetaData0 } = imageDataObject;
|
||||
const { windowWidth, windowCenter, modality } = imageMetaData0;
|
||||
|
||||
let lower;
|
||||
let upper;
|
||||
|
||||
if (modality === 'PT') {
|
||||
// For PET just set the range to 0-5 SUV
|
||||
lower = 0;
|
||||
upper = 5;
|
||||
} else {
|
||||
lower = windowCenter - windowWidth / 2.0;
|
||||
upper = windowCenter + windowWidth / 2.0;
|
||||
}
|
||||
|
||||
const volumeActor = vtkVolume.newInstance();
|
||||
const volumeMapper = vtkVolumeMapper.newInstance();
|
||||
|
||||
volumeActor.setMapper(volumeMapper);
|
||||
volumeMapper.setInputData(data);
|
||||
volumeMapper.setInputData(vtkImageData);
|
||||
|
||||
const range = data
|
||||
.getPointData()
|
||||
.getScalars()
|
||||
.getRange();
|
||||
|
||||
// TODO: For PET we might want to just set this to 0-5 SUV
|
||||
volumeActor
|
||||
.getProperty()
|
||||
.getRGBTransferFunction(0)
|
||||
.setRange(range[0], range[1]);
|
||||
.setRange(lower, upper);
|
||||
|
||||
// TODO: Should look into implementing autoAdjustSampleDistance in vtk
|
||||
const sampleDistance =
|
||||
1.2 *
|
||||
Math.sqrt(
|
||||
data
|
||||
.getSpacing()
|
||||
.map(v => v * v)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
);
|
||||
const spacing = vtkImageData.getSpacing();
|
||||
// Set the sample distance to half the mean length of one side. This is where the divide by 6 comes from.
|
||||
// https://github.com/Kitware/VTK/blob/6b559c65bb90614fb02eb6d1b9e3f0fca3fe4b0b/Rendering/VolumeOpenGL2/vtkSmartVolumeMapper.cxx#L344
|
||||
const sampleDistance = (spacing[0] + spacing[1] + spacing[2]) / 6;
|
||||
|
||||
volumeMapper.setSampleDistance(sampleDistance);
|
||||
|
||||
// Be generous to surpress warnings, as the logging really hurts performance.
|
||||
// TODO: maybe we should auto adjust samples to 1000.
|
||||
volumeMapper.setMaximumSamplesPerRay(4000);
|
||||
|
||||
volumeCache[displaySetInstanceUid] = volumeActor;
|
||||
|
||||
return volumeActor;
|
||||
}
|
||||
|
||||
async setStateFromProps() {
|
||||
setStateFromProps() {
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const {
|
||||
studyInstanceUid,
|
||||
@@ -238,8 +239,7 @@ class OHIFVTKViewport extends Component {
|
||||
}
|
||||
|
||||
const sopClassUid = sopClassUids[0];
|
||||
|
||||
let { data, labelmap } = await this.getViewportData(
|
||||
const imageDataObject = this.getViewportData(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid,
|
||||
@@ -248,29 +248,51 @@ class OHIFVTKViewport extends Component {
|
||||
frameIndex
|
||||
);
|
||||
|
||||
this.imageDataObject = imageDataObject;
|
||||
|
||||
// TODO: Temporarily disabling this since it is not yet
|
||||
// being used and hurts performance significantly.
|
||||
/*if (!labelmap) {
|
||||
labelmap = createLabelMapImageData(data);
|
||||
}*/
|
||||
|
||||
const volumeActor = this.getOrCreateVolume(data, displaySetInstanceUid);
|
||||
const volumeActor = this.getOrCreateVolume(
|
||||
imageDataObject,
|
||||
displaySetInstanceUid
|
||||
);
|
||||
|
||||
this.setState({
|
||||
volumes: [volumeActor],
|
||||
paintFilterBackgroundImageData: data,
|
||||
paintFilterLabelMapImageData: labelmap,
|
||||
paintFilterBackgroundImageData: imageDataObject.vtkImageData,
|
||||
paintFilterLabelMapImageData: null, // TODO
|
||||
percentComplete: 0,
|
||||
});
|
||||
|
||||
this.setState(
|
||||
{
|
||||
paintFilterBackgroundImageData: imageDataObject.vtkImageData,
|
||||
paintFilterLabelMapImageData: null, // TODO
|
||||
percentComplete: 0,
|
||||
},
|
||||
() => {
|
||||
this.loadProgressively(imageDataObject);
|
||||
|
||||
// TODO: There must be a better way to do this.
|
||||
// We do this so that if all the data is available the react-vtkjs-viewport
|
||||
// Will render _something_ before the volumes are set and the volume
|
||||
// Construction that happens in react-vtkjs-viewport locks up the CPU.
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
volumes: [volumeActor],
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setStateFromProps();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
vtkViewportSubscriptionManager.unsubscribe(this.props.viewportIndex);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { studies, displaySet } = this.props.viewportData;
|
||||
const prevDisplaySet = prevProps.viewportData.displaySet;
|
||||
@@ -285,6 +307,37 @@ class OHIFVTKViewport extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
loadProgressively(imageDataObject) {
|
||||
loadImageData(imageDataObject);
|
||||
|
||||
const { isLoading, insertPixelDataPromises } = imageDataObject;
|
||||
|
||||
const numberOfFrames = insertPixelDataPromises.length;
|
||||
|
||||
if (!isLoading) {
|
||||
this.setState({ isLoaded: true });
|
||||
return;
|
||||
}
|
||||
|
||||
insertPixelDataPromises.forEach(promise => {
|
||||
promise.then(numberProcessed => {
|
||||
const percentComplete = Math.floor(
|
||||
(numberProcessed * 100) / numberOfFrames
|
||||
);
|
||||
|
||||
if (percentComplete !== this.state.percentComplete) {
|
||||
this.setState({
|
||||
percentComplete,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(insertPixelDataPromises).then(() => {
|
||||
this.setState({ isLoaded: true });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let childrenWithProps = null;
|
||||
|
||||
@@ -302,21 +355,23 @@ class OHIFVTKViewport extends Component {
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.state.volumes ? (
|
||||
<ConnectedVTKViewport
|
||||
volumes={this.state.volumes}
|
||||
paintFilterLabelMapImageData={
|
||||
this.state.paintFilterLabelMapImageData
|
||||
}
|
||||
paintFilterBackgroundImageData={
|
||||
this.state.paintFilterBackgroundImageData
|
||||
}
|
||||
viewportIndex={this.props.viewportIndex}
|
||||
/>
|
||||
) : (
|
||||
<div style={style}>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
<div style={style}>
|
||||
{!this.state.isLoaded && (
|
||||
<LoadingIndicator percentComplete={this.state.percentComplete} />
|
||||
)}
|
||||
{this.state.volumes && (
|
||||
<ConnectedVTKViewport
|
||||
volumes={this.state.volumes}
|
||||
paintFilterLabelMapImageData={
|
||||
this.state.paintFilterLabelMapImageData
|
||||
}
|
||||
paintFilterBackgroundImageData={
|
||||
this.state.paintFilterBackgroundImageData
|
||||
}
|
||||
viewportIndex={this.props.viewportIndex}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{childrenWithProps}
|
||||
</>
|
||||
|
||||
@@ -4,66 +4,17 @@ import {
|
||||
vtkInteractorStyleMPRSlice,
|
||||
vtkInteractorStyleMPRRotate,
|
||||
vtkSVGCrosshairsWidget,
|
||||
vtkSVGWidgetManager,
|
||||
} from 'react-vtkjs-viewport';
|
||||
|
||||
import setMPRLayout from './utils/setMPRLayout.js';
|
||||
import setViewportToVTK from './utils/setViewportToVTK.js';
|
||||
import vtkViewportSubscriptionManager from './utils/vtkViewportSubscriptionManager.js';
|
||||
import vtkCoordinate from 'vtk.js/Sources/Rendering/Core/Coordinate';
|
||||
import vtkMath from 'vtk.js/Sources/Common/Core/Math';
|
||||
import vtkMatrixBuilder from 'vtk.js/Sources/Common/Core/MatrixBuilder';
|
||||
import Constants from 'vtk.js/Sources/Rendering/Core/VolumeMapper/Constants.js';
|
||||
import throttle from 'lodash.throttle';
|
||||
|
||||
const { BlendMode } = Constants;
|
||||
|
||||
// TODO: Put this somewhere else
|
||||
let apis = {};
|
||||
let currentSlabThickness = 0.1;
|
||||
let defaultSlabThickness = 0.1;
|
||||
|
||||
function getCrosshairCallbackForIndex(index) {
|
||||
return ({ worldPos }) => {
|
||||
// Set camera focal point to world coordinate for linked views
|
||||
apis.forEach((api, viewportIndex) => {
|
||||
if (viewportIndex !== index) {
|
||||
// We are basically doing the same as getSlice but with the world coordinate
|
||||
// that we want to jump to instead of the camera focal point.
|
||||
// I would rather do the camera adjustment directly but I keep
|
||||
// doing it wrong and so this is good enough for now.
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
|
||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||
const sliceNormal = istyle.getSliceNormal();
|
||||
const transform = vtkMatrixBuilder
|
||||
.buildFromDegree()
|
||||
.identity()
|
||||
.rotateFromDirections(sliceNormal, [1, 0, 0]);
|
||||
|
||||
const mutatedWorldPos = worldPos.slice();
|
||||
transform.apply(mutatedWorldPos);
|
||||
const slice = mutatedWorldPos[0];
|
||||
|
||||
istyle.setSlice(slice);
|
||||
|
||||
renderWindow.render();
|
||||
}
|
||||
|
||||
const renderer = api.genericRenderWindow.getRenderer();
|
||||
const wPos = vtkCoordinate.newInstance();
|
||||
wPos.setCoordinateSystemToWorld();
|
||||
wPos.setValue(worldPos);
|
||||
|
||||
const displayPosition = wPos.getComputedDisplayValue(renderer);
|
||||
const { svgWidgetManager } = api;
|
||||
api.svgWidgets.crosshairsWidget.setPoint(
|
||||
displayPosition[0],
|
||||
displayPosition[1]
|
||||
);
|
||||
svgWidgetManager.render();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async function _getActiveViewportVTKApi(viewports) {
|
||||
const {
|
||||
@@ -105,8 +56,6 @@ async function _getActiveViewportVTKApi(viewports) {
|
||||
|
||||
function _setView(api, sliceNormal, viewUp) {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const renderer = api.genericRenderWindow.getRenderer();
|
||||
const camera = renderer.getActiveCamera();
|
||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||
istyle.setSliceNormal(...sliceNormal);
|
||||
istyle.setViewUp(...viewUp);
|
||||
@@ -114,41 +63,6 @@ function _setView(api, sliceNormal, viewUp) {
|
||||
renderWindow.render();
|
||||
}
|
||||
|
||||
function switchMPRInteractors(api, istyle) {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const renderer = api.genericRenderWindow.getRenderer();
|
||||
const camera = renderer.getActiveCamera();
|
||||
const currentIStyle = renderWindow.getInteractor().getInteractorStyle();
|
||||
|
||||
let currentViewport;
|
||||
if (currentIStyle.getViewport && istyle.getViewport) {
|
||||
currentViewport = currentIStyle.getViewport();
|
||||
}
|
||||
|
||||
let currentSlabThickness;
|
||||
if (currentIStyle.getSlabThickness && istyle.getSlabThickness) {
|
||||
currentSlabThickness = currentIStyle.getSlabThickness();
|
||||
defaultSlabThickness = currentSlabThickness;
|
||||
}
|
||||
|
||||
renderWindow.getInteractor().setInteractorStyle(istyle);
|
||||
|
||||
// TODO: Not sure why this is required the second time this function is called
|
||||
istyle.setInteractor(renderWindow.getInteractor());
|
||||
|
||||
if (currentViewport) {
|
||||
istyle.setViewport(currentViewport);
|
||||
}
|
||||
|
||||
if (istyle.getVolumeMapper() !== api.volumes[0]) {
|
||||
if (currentSlabThickness) {
|
||||
istyle.setSlabThickness(currentSlabThickness);
|
||||
}
|
||||
|
||||
istyle.setVolumeMapper(api.volumes[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
axial: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
@@ -175,52 +89,56 @@ const actions = {
|
||||
apis.forEach(api => {
|
||||
const istyle = vtkInteractorStyleMPRRotate.newInstance();
|
||||
|
||||
switchMPRInteractors(api, istyle);
|
||||
api.setInteractorStyle({ istyle });
|
||||
});
|
||||
},
|
||||
enableCrosshairsTool: () => {
|
||||
apis.forEach((api, index) => {
|
||||
apis.forEach((api, apiIndex) => {
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
|
||||
switchMPRInteractors(api, istyle);
|
||||
|
||||
istyle.setCallback(getCrosshairCallbackForIndex(index));
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
configuration: { apis, apiIndex },
|
||||
});
|
||||
});
|
||||
},
|
||||
enableLevelTool: () => {
|
||||
function updateVOI(apis, windowWidth, windowCenter) {
|
||||
apis.forEach(api => {
|
||||
api.updateVOI(windowWidth, windowCenter);
|
||||
});
|
||||
}
|
||||
|
||||
const throttledUpdateVOIs = throttle(updateVOI, 16, { trailing: true }); // ~ 60 fps
|
||||
|
||||
const callbacks = {
|
||||
setOnLevelsChanged: ({ windowCenter, windowWidth }) => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
|
||||
renderWindow.render();
|
||||
});
|
||||
|
||||
throttledUpdateVOIs(apis, windowWidth, windowCenter);
|
||||
},
|
||||
};
|
||||
|
||||
apis.forEach(api => {
|
||||
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
|
||||
|
||||
switchMPRInteractors(api, istyle);
|
||||
api.setInteractorStyle({ istyle, callbacks });
|
||||
});
|
||||
},
|
||||
setSlabThickness: ({ slabThickness }) => {
|
||||
currentSlabThickness = slabThickness;
|
||||
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||
|
||||
if (istyle.setSlabThickness) {
|
||||
istyle.setSlabThickness(currentSlabThickness);
|
||||
}
|
||||
|
||||
renderWindow.render();
|
||||
api.setSlabThickness(slabThickness);
|
||||
});
|
||||
},
|
||||
changeSlabThickness: ({ change }) => {
|
||||
currentSlabThickness += change;
|
||||
currentSlabThickness = Math.max(currentSlabThickness, 0.1);
|
||||
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||
const slabThickness = Math.max(api.getSlabThickness() + change, 0.1);
|
||||
|
||||
if (istyle.setSlabThickness) {
|
||||
istyle.setSlabThickness(currentSlabThickness);
|
||||
}
|
||||
|
||||
renderWindow.render();
|
||||
api.setSlabThickness(slabThickness);
|
||||
});
|
||||
},
|
||||
setBlendModeToComposite: () => {
|
||||
@@ -228,13 +146,15 @@ const actions = {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||
|
||||
const slabThickness = api.getSlabThickness();
|
||||
|
||||
const mapper = api.volumes[0].getMapper();
|
||||
if (mapper.setBlendModeToComposite) {
|
||||
mapper.setBlendModeToComposite();
|
||||
}
|
||||
|
||||
if (istyle.setSlabThickness) {
|
||||
istyle.setSlabThickness(defaultSlabThickness);
|
||||
istyle.setSlabThickness(slabThickness);
|
||||
}
|
||||
renderWindow.render();
|
||||
});
|
||||
@@ -252,7 +172,6 @@ const actions = {
|
||||
setBlendMode: ({ blendMode }) => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||
|
||||
api.volumes[0].getMapper().setBlendMode(blendMode);
|
||||
|
||||
@@ -260,78 +179,60 @@ const actions = {
|
||||
});
|
||||
},
|
||||
mpr2d: async ({ viewports }) => {
|
||||
// TODO push a lot of this backdoor logic lower down to the library level.
|
||||
const displaySet =
|
||||
viewports.viewportSpecificData[viewports.activeViewportIndex];
|
||||
|
||||
let apiByViewport;
|
||||
// Get current VOI if cornerstone viewport.
|
||||
const cornerstoneVOI = getVOIFromCornerstoneViewport(displaySet);
|
||||
|
||||
const viewportProps = [
|
||||
{
|
||||
//Axial
|
||||
orientation: {
|
||||
sliceNormal: [0, 0, 1],
|
||||
viewUp: [0, -1, 0],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Sagital
|
||||
orientation: {
|
||||
sliceNormal: [1, 0, 0],
|
||||
viewUp: [0, 0, 1],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Coronal
|
||||
orientation: {
|
||||
sliceNormal: [0, 1, 0],
|
||||
viewUp: [0, 0, 1],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
apiByViewport = await setMPRLayout(displaySet);
|
||||
apis = await setMPRLayout(displaySet, viewportProps, 1, 3);
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
apis = apiByViewport;
|
||||
if (cornerstoneVOI) {
|
||||
setVOI(cornerstoneVOI);
|
||||
}
|
||||
|
||||
const rgbTransferFunction = apiByViewport[0].volumes[0]
|
||||
.getProperty()
|
||||
.getRGBTransferFunction(0);
|
||||
|
||||
const onModifiedSubscription = rgbTransferFunction.onModified(() => {
|
||||
apiByViewport.forEach(a => {
|
||||
const renderWindow = a.genericRenderWindow.getRenderWindow();
|
||||
|
||||
renderWindow.render();
|
||||
});
|
||||
});
|
||||
|
||||
vtkViewportSubscriptionManager.pushSubscription(0, onModifiedSubscription);
|
||||
|
||||
apiByViewport.forEach((api, index) => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const renderer = api.genericRenderWindow.getRenderer();
|
||||
const camera = renderer.getActiveCamera();
|
||||
// Add widgets and set default interactorStyle of each viewport.
|
||||
apis.forEach((api, apiIndex) => {
|
||||
api.addSVGWidget(
|
||||
vtkSVGCrosshairsWidget.newInstance(),
|
||||
'crosshairsWidget'
|
||||
);
|
||||
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
renderWindow.getInteractor().setInteractorStyle(istyle);
|
||||
|
||||
istyle.setVolumeMapper(api.volumes[0]);
|
||||
istyle.setCallback(getCrosshairCallbackForIndex(index));
|
||||
|
||||
const svgWidgetManager = vtkSVGWidgetManager.newInstance();
|
||||
svgWidgetManager.setRenderer(renderer);
|
||||
svgWidgetManager.setScale(1);
|
||||
|
||||
const crosshairsWidget = vtkSVGCrosshairsWidget.newInstance();
|
||||
|
||||
svgWidgetManager.addWidget(crosshairsWidget);
|
||||
svgWidgetManager.render();
|
||||
|
||||
api.svgWidgetManager = svgWidgetManager;
|
||||
api.svgWidgets = {
|
||||
crosshairsWidget,
|
||||
};
|
||||
|
||||
switch (index) {
|
||||
default:
|
||||
case 0:
|
||||
//Axial
|
||||
istyle.setSliceNormal(0, 0, 1);
|
||||
istyle.setViewUp(0, -1, 0);
|
||||
|
||||
break;
|
||||
case 1:
|
||||
// sagittal
|
||||
istyle.setSliceNormal(1, 0, 0);
|
||||
istyle.setViewUp(0, 0, 1);
|
||||
break;
|
||||
case 2:
|
||||
// Coronal
|
||||
istyle.setSliceNormal(0, 1, 0);
|
||||
istyle.setViewUp(0, 0, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
renderWindow.render();
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
configuration: { apis, apiIndex },
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -421,3 +322,41 @@ export default {
|
||||
definitions,
|
||||
defaultContext: 'ACTIVE_VIEWPORT::VTK',
|
||||
};
|
||||
|
||||
function getVOIFromCornerstoneViewport(displaySet) {
|
||||
const cornerstoneElement = cornerstone.getEnabledElement(displaySet.dom);
|
||||
|
||||
if (cornerstoneElement) {
|
||||
const imageId = cornerstoneElement.image.imageId;
|
||||
|
||||
const { modality } = cornerstone.metaData.get(
|
||||
'generalSeriesModule',
|
||||
imageId
|
||||
);
|
||||
|
||||
if (modality !== 'PT') {
|
||||
const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi;
|
||||
|
||||
return {
|
||||
windowWidth,
|
||||
windowCenter,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setVOI(voi) {
|
||||
const { windowWidth, windowCenter } = voi;
|
||||
const lower = windowCenter - windowWidth / 2.0;
|
||||
const upper = windowCenter + windowWidth / 2.0;
|
||||
|
||||
const rgbTransferFunction = apis[0].volumes[0]
|
||||
.getProperty()
|
||||
.getRGBTransferFunction(0);
|
||||
|
||||
rgbTransferFunction.setRange(lower, upper);
|
||||
|
||||
apis.forEach(api => {
|
||||
api.updateVOI(windowWidth, windowCenter);
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,23 @@
|
||||
import setLayoutAndViewportData from './setLayoutAndViewportData.js';
|
||||
|
||||
export default function setMPRLayout(displaySet) {
|
||||
export default function setMPRLayout(
|
||||
displaySet,
|
||||
viewportPropsArray,
|
||||
numRows = 1,
|
||||
numColumns = 1
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const viewports = [];
|
||||
const numRows = 1;
|
||||
const numColumns = 3;
|
||||
const numViewports = numRows * numColumns;
|
||||
|
||||
if (viewportPropsArray && viewportPropsArray.length !== numViewports) {
|
||||
reject(
|
||||
new Error(
|
||||
'viewportProps is supplied but its length is not equal to numViewports'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const viewportSpecificData = {};
|
||||
|
||||
for (let i = 0; i < numViewports; i++) {
|
||||
@@ -17,8 +29,8 @@ export default function setMPRLayout(displaySet) {
|
||||
const apis = [];
|
||||
viewports.forEach((viewport, index) => {
|
||||
apis[index] = null;
|
||||
const viewportProps = viewportPropsArray[index];
|
||||
viewports[index] = Object.assign({}, viewports[index], {
|
||||
// plugin: 'vtk',
|
||||
vtk: {
|
||||
mode: 'mpr', // TODO: not used
|
||||
afterCreation: api => {
|
||||
@@ -28,6 +40,7 @@ export default function setMPRLayout(displaySet) {
|
||||
resolve(apis);
|
||||
}
|
||||
},
|
||||
...viewportProps,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
const subscriptions = [];
|
||||
|
||||
// This is pretty hacky right now, but it makes sure we don't keep adding subscriptions.
|
||||
// TODO -> Nuke this and move it up a layer once we have more vigorous layout support.
|
||||
|
||||
const vtkViewportSubscriptionManager = {
|
||||
subscriptions,
|
||||
pushSubscription(viewportIndex, subscription) {
|
||||
if (!Array.isArray(subscriptions[viewportIndex])) {
|
||||
subscriptions[viewportIndex] = [];
|
||||
}
|
||||
|
||||
subscriptions[viewportIndex].push(subscription);
|
||||
},
|
||||
unsubscribe(viewportIndex) {
|
||||
if (!subscriptions[viewportIndex]) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (subscriptions[viewportIndex].length) {
|
||||
subscriptions[viewportIndex].pop().unsubscribe();
|
||||
}
|
||||
|
||||
subscriptions[viewportIndex] = null;
|
||||
},
|
||||
};
|
||||
|
||||
export default vtkViewportSubscriptionManager;
|
||||
@@ -26,6 +26,7 @@
|
||||
"test:unit": "jest --collectCoverage",
|
||||
"test:unit:ci": "lerna run test:unit:ci --parallel --stream",
|
||||
"test:e2e": "lerna run test:e2e --stream",
|
||||
"test:e2e:ci": "lerna run test:e2e:ci --stream",
|
||||
"test:e2e:dist": "lerna run test:e2e:dist --stream",
|
||||
"test:e2e:serve": "lerna run test:e2e:serve --stream",
|
||||
"see-changed": "lerna changed",
|
||||
@@ -49,6 +50,7 @@
|
||||
"@babel/plugin-transform-runtime": "^7.5.0",
|
||||
"@babel/preset-env": "^7.5.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@percy/cypress": "^2.2.0",
|
||||
"babel-eslint": "9.x",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-inline-react-svg": "1.1.0",
|
||||
@@ -57,12 +59,15 @@
|
||||
"copy-webpack-plugin": "^5.0.3",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^3.2.0",
|
||||
"dotenv": "^8.1.0",
|
||||
"eslint": "5.16.0",
|
||||
"eslint-config-prettier": "^6.4.0",
|
||||
"eslint-config-react-app": "^4.0.1",
|
||||
"eslint-plugin-flowtype": "2.x",
|
||||
"eslint-plugin-import": "2.x",
|
||||
"eslint-plugin-jsx-a11y": "6.x",
|
||||
"eslint-plugin-node": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-react": "7.x",
|
||||
"eslint-plugin-react-hooks": "1.5.0",
|
||||
|
||||
@@ -3,6 +3,39 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.0...@ohif/core@1.5.1) (2019-10-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092)) ([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.4.0...@ohif/core@1.5.0) (2019-10-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Allow routes to load Google Cloud DICOM Stores in the Study List ([#1069](https://github.com/OHIF/Viewers/issues/1069)) ([21b586b](https://github.com/OHIF/Viewers/commit/21b586b08f3dde6613859712a9e0577dece564db))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.2...@ohif/core@1.4.0) (2019-10-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Only allow reconstruction of datasets that make sense ([#1010](https://github.com/OHIF/Viewers/issues/1010)) ([2d75e01](https://github.com/OHIF/Viewers/commit/2d75e01)), closes [#561](https://github.com/OHIF/Viewers/issues/561)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.3.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.3.1...@ohif/core@1.3.2) (2019-10-14)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "1.3.2",
|
||||
"version": "1.5.1",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { SeriesMetadata } from './SeriesMetadata';
|
||||
import { api } from 'dicomweb-client';
|
||||
// - createStacks
|
||||
import { isImage } from '../../utils/isImage';
|
||||
import isDisplaySetReconstructable from '../../utils/isDisplaySetReconstructable';
|
||||
import isLowPriorityModality from '../../utils/isLowPriorityModality';
|
||||
|
||||
export class StudyMetadata extends Metadata {
|
||||
@@ -146,6 +147,7 @@ export class StudyMetadata extends Metadata {
|
||||
|
||||
if (isMultiFrame(instance)) {
|
||||
displaySet = makeDisplaySet(series, [instance]);
|
||||
|
||||
displaySet.setAttributes({
|
||||
sopClassUids,
|
||||
isClip: true,
|
||||
@@ -620,6 +622,16 @@ const makeDisplaySet = (series, instances) => {
|
||||
imageSet.getImage(0).getRawValue('x00200013')
|
||||
);
|
||||
|
||||
const isReconstructable = isDisplaySetReconstructable(series, instances);
|
||||
|
||||
imageSet.isReconstructable = isReconstructable.value;
|
||||
|
||||
if (isReconstructable.missingFrames) {
|
||||
// TODO -> This is currently unused, but may be used for reconstructing
|
||||
// Volumes with gaps later on.
|
||||
imageSet.missingFrames = isReconstructable.missingFrames;
|
||||
}
|
||||
|
||||
return imageSet;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Checks if a series is reconstructable to a 3D volume.
|
||||
*
|
||||
* @param {Object} series The `OHIFSeriesMetadata` object.
|
||||
* @param {Object[]} instances The `OHIFInstanceMetadata` object
|
||||
*/
|
||||
export default function isDisplaySetReconstructable(series, instances) {
|
||||
// Can't reconstruct if we only have one image.
|
||||
|
||||
const modality = series._data.modality; // TODO -> Is there a better way to get this?
|
||||
const isMultiframe = instances[0].getRawValue('x00280008') > 1;
|
||||
|
||||
if (!constructableModalities.includes(modality)) {
|
||||
return { value: false };
|
||||
}
|
||||
|
||||
if (!isMultiframe && instances.length === 1) {
|
||||
return { values: false };
|
||||
}
|
||||
|
||||
if (isMultiframe) {
|
||||
return processMultiframe(instances[0]);
|
||||
} else {
|
||||
return processSingleframe(instances);
|
||||
}
|
||||
}
|
||||
|
||||
function processMultiframe(instance) {
|
||||
//TODO: deal with multriframe checks! return true for now.
|
||||
return { value: true };
|
||||
}
|
||||
|
||||
function processSingleframe(instances) {
|
||||
const firstImage = instances[0];
|
||||
const firstImageRows = firstImage.getTagValue('x00280010');
|
||||
const firstImageColumns = firstImage.getTagValue('x00280011');
|
||||
const firstImageSamplesPerPixel = firstImage.getTagValue('x00280002');
|
||||
// Note: No need to unpack iop, can compare string form.
|
||||
const firstImageOrientationPatient = firstImage.getTagValue('x00200037');
|
||||
|
||||
// Can't reconstruct if we:
|
||||
// -- Have a different dimensions within a displaySet.
|
||||
// -- Have a different number of components within a displaySet.
|
||||
// -- Have different orientations within a displaySet.
|
||||
for (let i = 1; i < instances.length; i++) {
|
||||
const instance = instances[i];
|
||||
const rows = instance.getTagValue('x00280010');
|
||||
const columns = instance.getTagValue('x00280011');
|
||||
const samplesPerPixel = instance.getTagValue('x00280002');
|
||||
const imageOrientationPatient = instance.getTagValue('x00200037');
|
||||
|
||||
if (
|
||||
rows !== firstImageRows ||
|
||||
columns !== firstImageColumns ||
|
||||
samplesPerPixel !== firstImageSamplesPerPixel ||
|
||||
imageOrientationPatient !== firstImageOrientationPatient
|
||||
) {
|
||||
return { value: false };
|
||||
}
|
||||
}
|
||||
|
||||
let missingFrames = 0;
|
||||
|
||||
// Check if frame spacing is approximately equal within a tolerance.
|
||||
// If spacing is on a uniform grid but we are missing frames,
|
||||
// Allow reconstruction, but pass back the number of missing frames.
|
||||
if (instances.length > 2) {
|
||||
const firstIpp = _getImagePositionPatient(firstImage);
|
||||
const lastIpp = _getImagePositionPatient(instances[instances.length - 1]);
|
||||
const averageSpacingBetweenFrames =
|
||||
_getPerpendicularDistance(firstIpp, lastIpp) / (instances.length - 1);
|
||||
|
||||
let previousIpp = firstIpp;
|
||||
|
||||
for (let i = 1; i < instances.length; i++) {
|
||||
const instance = instances[i];
|
||||
const ipp = _getImagePositionPatient(instance);
|
||||
|
||||
const spacingBetweenFrames = _getPerpendicularDistance(ipp, previousIpp);
|
||||
const spacingIssue = _getSpacingIssue(
|
||||
spacingBetweenFrames,
|
||||
averageSpacingBetweenFrames
|
||||
);
|
||||
|
||||
if (spacingIssue) {
|
||||
const issue = spacingIssue.issue;
|
||||
|
||||
if (issue === reconstructionIssues.MISSING_FRAMES) {
|
||||
missingFrames += spacingIssue.missingFrames;
|
||||
} else if (issue === reconstructionIssues.IRREGULAR_SPACING) {
|
||||
return { value: false };
|
||||
}
|
||||
}
|
||||
|
||||
previousIpp = ipp;
|
||||
}
|
||||
}
|
||||
|
||||
return { value: true, missingFrames };
|
||||
}
|
||||
|
||||
// TODO: Is 10% a reasonable tolerance for spacing?
|
||||
const tolerance = 0.1;
|
||||
|
||||
/**
|
||||
* Checks for spacing issues.
|
||||
*
|
||||
* @param {number} spacing The spacing between two frames.
|
||||
* @param {number} averageSpacing The average spacing between all frames.
|
||||
*
|
||||
* @returns {Object} An object containing the issue and extra information if necessary.
|
||||
*/
|
||||
function _getSpacingIssue(spacing, averageSpacing) {
|
||||
const equalWithinTolerance =
|
||||
Math.abs(spacing - averageSpacing) < averageSpacing * tolerance;
|
||||
|
||||
if (equalWithinTolerance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const multipleOfAverageSpacing = spacing / averageSpacing;
|
||||
|
||||
const numberOfSpacings = Math.round(multipleOfAverageSpacing);
|
||||
|
||||
const errorForEachSpacing =
|
||||
Math.abs(spacing - numberOfSpacings * averageSpacing) / numberOfSpacings;
|
||||
|
||||
if (errorForEachSpacing < tolerance * averageSpacing) {
|
||||
return {
|
||||
issue: reconstructionIssues.MISSING_FRAMES,
|
||||
missingFrames: numberOfSpacings - 1,
|
||||
};
|
||||
}
|
||||
|
||||
return { issue: reconstructionIssues.IRREGULAR_SPACING };
|
||||
}
|
||||
|
||||
function _getImagePositionPatient(instance) {
|
||||
return instance
|
||||
.getTagValue('x00200032')
|
||||
.split('\\')
|
||||
.map(element => Number(element));
|
||||
}
|
||||
|
||||
function _getPerpendicularDistance(a, b) {
|
||||
return Math.sqrt(
|
||||
Math.pow(a[0] - b[0], 2) +
|
||||
Math.pow(a[1] - b[1], 2) +
|
||||
Math.pow(a[2] - b[2], 2)
|
||||
);
|
||||
}
|
||||
|
||||
const constructableModalities = ['MR', 'CT', 'PT', 'NM'];
|
||||
const reconstructionIssues = {
|
||||
MISSING_FRAMES: 'missingframes',
|
||||
IRREGULAR_SPACING: 'irregularspacing',
|
||||
};
|
||||
@@ -6,7 +6,7 @@ const PARAM_PATTERN_IDENTIFIER = ':';
|
||||
function toLowerCaseFirstLetter(word) {
|
||||
return word[0].toLowerCase() + word.slice(1);
|
||||
}
|
||||
const getFilters = (location = {}) => {
|
||||
const getQueryFilters = (location = {}) => {
|
||||
const { search } = location;
|
||||
|
||||
if (!search) {
|
||||
@@ -55,13 +55,19 @@ const replaceParam = (path = '', paramKey, paramValue) => {
|
||||
return path;
|
||||
};
|
||||
|
||||
const isValidPath = path => {
|
||||
const paramPatternPiece = `/${PARAM_PATTERN_IDENTIFIER}`;
|
||||
return path.indexOf(paramPatternPiece) < 0;
|
||||
};
|
||||
|
||||
const queryString = {
|
||||
getQueryFilters: getFilters,
|
||||
getQueryFilters,
|
||||
};
|
||||
|
||||
const paramString = {
|
||||
parseParam: parseParam,
|
||||
replaceParam: replaceParam,
|
||||
isValidPath,
|
||||
parseParam,
|
||||
replaceParam,
|
||||
};
|
||||
|
||||
export { parse, queryString, paramString };
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.51.0](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.5...@ohif/i18n@0.51.0) (2019-10-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add browser info and app version ([#1046](https://github.com/OHIF/Viewers/issues/1046)) ([c217b8b](https://github.com/OHIF/Viewers/commit/c217b8b))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.50.5](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.4...@ohif/i18n@0.50.5) (2019-10-04)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/i18n",
|
||||
"version": "0.50.5",
|
||||
"version": "0.51.0",
|
||||
"description": "Internationalization library for The OHIF Viewer",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Browser": "Browser",
|
||||
"Build Number": "Build Number",
|
||||
"Latest Master Commits": "Latest Master Commits",
|
||||
"More details": "More details",
|
||||
"Name": "Name",
|
||||
"OHIF Viewer - About": "OHIF Viewer - About",
|
||||
"OS": "OS",
|
||||
"Report an issue": "Report an issue",
|
||||
"Repository URL": "Repository URL",
|
||||
"Value": "Value",
|
||||
"Version Information": "Version Information",
|
||||
"Visit the forum": "Visit the forum"
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import AboutModal from "./AboutModal.json";
|
||||
import Buttons from "./Buttons.json";
|
||||
import CineDialog from "./CineDialog.json";
|
||||
import Common from "./Common.json";
|
||||
@@ -8,6 +9,7 @@ import UserPreferencesModal from "./UserPreferencesModal.json";
|
||||
|
||||
export default {
|
||||
'en-US': {
|
||||
AboutModal,
|
||||
Buttons,
|
||||
CineDialog,
|
||||
Common,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"Browser": "Navegador",
|
||||
"Build Number": "Número da compilação",
|
||||
"Latest Master Commits": "Últimos Commits na Master",
|
||||
"More details": "Mais detalhes",
|
||||
"Name": "Nome",
|
||||
"OHIF Viewer - About": "OHIF Viewer - Sobre",
|
||||
"OS": "SO",
|
||||
"Report an issue": "Informar um problema",
|
||||
"Repository URL": "URL do Repositório",
|
||||
"Value": "Valor",
|
||||
"Version Information": "Informação da Versão",
|
||||
"Visit the forum": "Visite o fórum"
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import AboutModal from "./AboutModal.json";
|
||||
import Buttons from "./Buttons.json";
|
||||
import CineDialog from "./CineDialog.json";
|
||||
import Common from "./Common.json";
|
||||
@@ -6,6 +7,7 @@ import UserPreferencesModal from "./UserPreferencesModal.json";
|
||||
|
||||
export default {
|
||||
'pt-BR': {
|
||||
AboutModal,
|
||||
Buttons,
|
||||
CineDialog,
|
||||
Common,
|
||||
|
||||
@@ -3,6 +3,39 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.56.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.55.0...@ohif/ui@0.56.0) (2019-10-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Load spinner when selecting gcloud store. Add key on td ([#1034](https://github.com/OHIF/Viewers/issues/1034)) ([e62f403](https://github.com/OHIF/Viewers/commit/e62f403fe9e3df56713128e3d59045824b086d8d)), closes [#1057](https://github.com/OHIF/Viewers/issues/1057)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.55.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.54.0...@ohif/ui@0.55.0) (2019-10-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add browser info and app version ([#1046](https://github.com/OHIF/Viewers/issues/1046)) ([c217b8b](https://github.com/OHIF/Viewers/commit/c217b8b))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.54.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.4...@ohif/ui@0.54.0) (2019-10-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Notification Service ([#1011](https://github.com/OHIF/Viewers/issues/1011)) ([92c8996](https://github.com/OHIF/Viewers/commit/92c8996))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.53.3...@ohif/ui@0.53.4) (2019-10-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/ui",
|
||||
"version": "0.53.4",
|
||||
"version": "0.56.0",
|
||||
"description": "A set of React components for Medical Imaging Viewers",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
@@ -39,6 +39,7 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@ohif/i18n": "^0.2.3",
|
||||
"browser-detect": "^0.2.28",
|
||||
"classnames": "2.2.6",
|
||||
"i18next": "^17.0.3",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
.sb-container {
|
||||
position: fixed;
|
||||
width: var(--snackbar-size);
|
||||
padding: 20px;
|
||||
z-index: var(--snackbar-zIndex);
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.sb-topLeft {
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.sb-topCenter {
|
||||
left: 50%;
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sb-topRight {
|
||||
top: 0;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.sb-bottomLeft {
|
||||
top: auto;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.sb-bottomCenter {
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
top: auto;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.sb-bottomRight {
|
||||
top: auto;
|
||||
bottom: 0px;
|
||||
left: auto;
|
||||
right: 0px;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
.sb-topLeft .sb-item,
|
||||
.sb-topCenter .sb-item,
|
||||
.sb-topRight .sb-item {
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
.sb-bottomLeft .sb-item,
|
||||
.sb-bottomCenter .sb-item,
|
||||
.sb-bottomRight .sb-item {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.sb-closeBtn {
|
||||
height: 20px;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
padding: 2px;
|
||||
text-align: center;
|
||||
text-shadow: none;
|
||||
width: 20px;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.sb-closeBtn:hover {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.sb-closeIcon {
|
||||
display: block;
|
||||
font-size: 0;
|
||||
height: 100%;
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sb-closeIcon:after,
|
||||
.sb-closeIcon:before {
|
||||
content: ' ';
|
||||
display: block;
|
||||
height: 2px;
|
||||
transition: all 0.3s ease;
|
||||
width: 12px;
|
||||
background-color: #222;
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.sb-closeIcon:before {
|
||||
left: 4px;
|
||||
top: 3px;
|
||||
|
||||
transform: rotate(45deg);
|
||||
transform-origin: 0px 50%;
|
||||
}
|
||||
|
||||
.sb-closeIcon:after {
|
||||
right: 3px;
|
||||
top: 5px;
|
||||
transform: rotate(-45deg);
|
||||
transform-origin: calc(100% - 3px) 50%;
|
||||
}
|
||||
|
||||
.sb-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sb-message {
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.sb-item {
|
||||
position: relative;
|
||||
transition: height 300ms ease;
|
||||
animation: fadein 1s;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.2), 0 1px 18px 0 rgba(0, 0, 0, 0.12),
|
||||
0 3px 5px -1px rgba(0, 0, 0, 0.14);
|
||||
}
|
||||
|
||||
.sb-item a {
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.sb-container .sb-hidden {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
transition: all 300ms ease;
|
||||
}
|
||||
|
||||
.sb-success {
|
||||
background-color: var(--snackbar-success);
|
||||
}
|
||||
|
||||
.sb-error {
|
||||
background-color: var(--snackbar-error);
|
||||
}
|
||||
|
||||
.sb-warning {
|
||||
background-color: var(--snackbar-warning);
|
||||
}
|
||||
|
||||
.sb-info {
|
||||
background-color: var(--snackbar-info);
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
top: 30px;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Internet Explorer */
|
||||
@-ms-keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
top: 30px;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import SnackbarItem from './SnackbarItem';
|
||||
import './Snackbar.css';
|
||||
import { useSnackbarContext } from '../../utils/SnackbarProvider';
|
||||
|
||||
const SnackbarContainer = () => {
|
||||
const { snackbarItems, hide } = useSnackbarContext();
|
||||
|
||||
const renderItem = item => {
|
||||
return <SnackbarItem key={item.itemId} options={item} onClose={hide} />;
|
||||
};
|
||||
|
||||
if (!snackbarItems) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderItems = () => {
|
||||
const items = {
|
||||
topLeft: [],
|
||||
topCenter: [],
|
||||
topRight: [],
|
||||
bottomLeft: [],
|
||||
bottomCenter: [],
|
||||
bottomRight: [],
|
||||
};
|
||||
|
||||
snackbarItems.map(item => {
|
||||
items[item.position].push(item);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{Object.keys(items).map(pos => {
|
||||
if (!items[pos].length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={pos} className={`sb-container sb-${pos}`}>
|
||||
{items[pos].map(item => (
|
||||
<div key={item.id}>{renderItem(item)}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return <>{renderItems()}</>;
|
||||
};
|
||||
|
||||
export default SnackbarContainer;
|
||||
@@ -0,0 +1,31 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const SnackbarItem = ({ options, onClose }) => {
|
||||
const handleClose = () => {
|
||||
onClose(options.id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (options.autoClose) {
|
||||
setTimeout(() => {
|
||||
handleClose();
|
||||
}, options.duration);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${options.visible ? '' : 'sb-hidden'} sb-${
|
||||
options.type
|
||||
} sb-item`}
|
||||
>
|
||||
<span className="sb-closeBtn" onClick={handleClose}>
|
||||
<span className="sb-closeIcon">x</span>
|
||||
</span>
|
||||
{options.title && <div className="sb-title">{options.title}</div>}
|
||||
{options.message && <div className="sb-message">{options.message}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SnackbarItem;
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
INFO: 'info',
|
||||
WARNING: 'warning',
|
||||
SUCCESS: 'success',
|
||||
ERROR: 'error',
|
||||
};
|
||||
@@ -11,11 +11,79 @@ import { StudylistToolbar } from './StudyListToolbar.js';
|
||||
import { isInclusivelyBeforeDay } from 'react-dates';
|
||||
import moment from 'moment';
|
||||
import debounce from 'lodash.debounce';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
|
||||
const today = moment();
|
||||
const lastWeek = moment().subtract(7, 'day');
|
||||
const lastMonth = moment().subtract(1, 'month');
|
||||
function getPaginationFragment(
|
||||
props,
|
||||
searchData,
|
||||
nextPageCb,
|
||||
prevPageCb,
|
||||
changeRowsPerPageCb
|
||||
) {
|
||||
return (
|
||||
<PaginationArea
|
||||
pageOptions={props.pageOptions}
|
||||
currentPage={searchData.currentPage}
|
||||
nextPageFunc={nextPageCb}
|
||||
prevPageFunc={prevPageCb}
|
||||
onRowsPerPageChange={changeRowsPerPageCb}
|
||||
rowsPerPage={searchData.rowsPerPage}
|
||||
recordCount={props.studies.length}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getTableMeta(translate) {
|
||||
return {
|
||||
patientName: {
|
||||
displayText: translate('PatientName'),
|
||||
sort: 0,
|
||||
},
|
||||
patientId: {
|
||||
displayText: translate('MRN'),
|
||||
sort: 0,
|
||||
},
|
||||
accessionNumber: {
|
||||
displayText: translate('AccessionNumber'),
|
||||
sort: 0,
|
||||
},
|
||||
studyDate: {
|
||||
displayText: translate('StudyDate'),
|
||||
inputType: 'date-range',
|
||||
sort: 0,
|
||||
},
|
||||
modalities: {
|
||||
displayText: translate('Modality'),
|
||||
sort: 0,
|
||||
},
|
||||
studyDescription: {
|
||||
displayText: translate('StudyDescription'),
|
||||
sort: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getNoListFragment(translate, studies, error, loading) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="loading">
|
||||
<StudyListLoadingText />
|
||||
</div>
|
||||
);
|
||||
} else if (error) {
|
||||
return (
|
||||
<div className="notFound">
|
||||
{translate('There was an error fetching studies')}
|
||||
</div>
|
||||
);
|
||||
} else if (!studies.length) {
|
||||
return <div className="notFound">{translate('No matching results')}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class StudyList extends Component {
|
||||
static propTypes = {
|
||||
@@ -111,24 +179,27 @@ class StudyList extends Component {
|
||||
getBlurHandler(key) {
|
||||
return event => {
|
||||
this.delayedSearch.cancel();
|
||||
this.setSearchData(key, event.target.value, this.search);
|
||||
this.setSearchData(key, event.target.value);
|
||||
};
|
||||
}
|
||||
|
||||
setSearchData(key, value, callback) {
|
||||
const searchData = this.state.searchData;
|
||||
setSearchData(key, value) {
|
||||
const searchData = { ...this.state.searchData };
|
||||
searchData[key] = value;
|
||||
this.setState({ searchData }, callback);
|
||||
|
||||
if (!isEqual(searchData[key], this.state.searchData[key])) {
|
||||
this.setState({ ...this.state, searchData });
|
||||
}
|
||||
}
|
||||
|
||||
setSearchDataBatch(keyValues, callback) {
|
||||
const searchData = this.state.searchData;
|
||||
setSearchDataBatch(keyValues) {
|
||||
const searchData = { ...this.state.searchData };
|
||||
|
||||
Object.keys(keyValues).forEach(key => {
|
||||
searchData[key] = keyValues[key];
|
||||
});
|
||||
|
||||
this.setState({ searchData }, callback);
|
||||
this.setState({ searchData });
|
||||
}
|
||||
|
||||
async onInputKeydown(event) {
|
||||
@@ -138,7 +209,7 @@ class StudyList extends Component {
|
||||
|
||||
this.delayedSearch.cancel();
|
||||
// reset the page because user is doing a new search
|
||||
this.setSearchData('currentPage', 0, this.search);
|
||||
this.setSearchData('currentPage', 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,45 +225,21 @@ class StudyList extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
renderNoMachingResults() {
|
||||
if (!this.props.studies.length && !this.state.error) {
|
||||
return <div className="notFound">No matching results</div>;
|
||||
}
|
||||
}
|
||||
|
||||
renderHasError() {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<div className="notFound">There was an error fetching studies</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderIsLoading() {
|
||||
if (this.state.loading) {
|
||||
return (
|
||||
<div className="loading">
|
||||
<StudyListLoadingText />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
nextPage(currentPage) {
|
||||
currentPage = currentPage + 1;
|
||||
this.delayedSearch.cancel();
|
||||
this.setSearchData('currentPage', currentPage, this.search);
|
||||
this.setSearchData('currentPage', currentPage);
|
||||
}
|
||||
|
||||
prevPage(currentPage) {
|
||||
currentPage = currentPage - 1;
|
||||
this.delayedSearch.cancel();
|
||||
this.setSearchData('currentPage', currentPage, this.search);
|
||||
this.setSearchData('currentPage', currentPage);
|
||||
}
|
||||
|
||||
onRowsPerPageChange(rowsPerPage) {
|
||||
this.delayedSearch.cancel();
|
||||
this.setSearchDataBatch({ rowsPerPage, currentPage: 0 }, this.search);
|
||||
this.setSearchDataBatch({ rowsPerPage, currentPage: 0 });
|
||||
}
|
||||
|
||||
onSortClick(field) {
|
||||
@@ -213,7 +260,7 @@ class StudyList extends Component {
|
||||
}
|
||||
|
||||
this.delayedSearch.cancel();
|
||||
this.setSearchData('sortData', { field, order }, this.search);
|
||||
this.setSearchData('sortData', { field, order });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,10 +268,39 @@ class StudyList extends Component {
|
||||
this.setState({ highlightedItem: studyItemUid });
|
||||
}
|
||||
|
||||
renderTableRow(study) {
|
||||
getTableRow(study, index) {
|
||||
const trKey = `trStudy${index}${study.studyInstanceUid}`;
|
||||
|
||||
if (!study) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getTableCell = (
|
||||
study,
|
||||
studyKey,
|
||||
emptyValue = '',
|
||||
emptyClass = ''
|
||||
) => {
|
||||
const componentKey = `td${studyKey}`;
|
||||
const isValidValue = study && typeof study[studyKey] === 'string';
|
||||
let className = emptyClass;
|
||||
let value = emptyValue;
|
||||
|
||||
if (isValidValue) {
|
||||
className = studyKey;
|
||||
value = study[studyKey];
|
||||
}
|
||||
|
||||
return (
|
||||
<td key={componentKey} className={className}>
|
||||
{value}
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={study.studyInstanceUid}
|
||||
key={trKey}
|
||||
className={
|
||||
this.state.highlightedItem === study.studyInstanceUid
|
||||
? 'studylistStudy noselect active'
|
||||
@@ -241,47 +317,35 @@ class StudyList extends Component {
|
||||
this.props.onSelectItem(study.studyInstanceUid);
|
||||
}}
|
||||
>
|
||||
<td className={study.patientName ? 'patientName' : 'emptyCell'}>
|
||||
{study.patientName || `(${this.props.t('Empty')})`}
|
||||
</td>
|
||||
|
||||
<td className="patientId">{study.patientId}</td>
|
||||
<td className="accessionNumber">{study.accessionNumber}</td>
|
||||
<td className="studyDate">{study.studyDate}</td>
|
||||
<td className="modalities">{study.modalities}</td>
|
||||
<td className="studyDescription">{study.studyDescription}</td>
|
||||
{getTableCell(
|
||||
study,
|
||||
'patientName',
|
||||
`(${this.props.t('Empty')})`,
|
||||
'emptyCell'
|
||||
)}
|
||||
{getTableCell(study, 'patientId')}
|
||||
{getTableCell(study, 'accessionNumber')}
|
||||
{getTableCell(study, 'studyDate')}
|
||||
{getTableCell(study, 'modalities')}
|
||||
{getTableCell(study, 'studyDescription')}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate(previousProps, previousState) {
|
||||
if (!isEqual(previousState.searchData, this.state.searchData)) {
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
||||
renderTableBody(noListFragment) {
|
||||
return !noListFragment && this.props.studies
|
||||
? this.props.studies.map(this.getTableRow.bind(this))
|
||||
: null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const tableMeta = {
|
||||
patientName: {
|
||||
displayText: this.props.t('PatientName'),
|
||||
sort: 0,
|
||||
},
|
||||
patientId: {
|
||||
displayText: this.props.t('MRN'),
|
||||
sort: 0,
|
||||
},
|
||||
accessionNumber: {
|
||||
displayText: this.props.t('AccessionNumber'),
|
||||
sort: 0,
|
||||
},
|
||||
studyDate: {
|
||||
displayText: this.props.t('StudyDate'),
|
||||
inputType: 'date-range',
|
||||
sort: 0,
|
||||
},
|
||||
modalities: {
|
||||
displayText: this.props.t('Modality'),
|
||||
sort: 0,
|
||||
},
|
||||
studyDescription: {
|
||||
displayText: this.props.t('StudyDescription'),
|
||||
sort: 0,
|
||||
},
|
||||
};
|
||||
const tableMeta = getTableMeta(this.props.t);
|
||||
|
||||
// Apply sort
|
||||
const sortedFieldName = this.state.searchData.sortData.field;
|
||||
@@ -294,14 +358,20 @@ class StudyList extends Component {
|
||||
|
||||
// Sort Icons
|
||||
const sortIcons = ['sort', 'sort-up', 'sort-down'];
|
||||
const noListFragment = getNoListFragment(
|
||||
this.props.t,
|
||||
this.props.studies,
|
||||
this.state.error,
|
||||
this.props.loading || this.state.loading
|
||||
);
|
||||
const tableBody = this.renderTableBody(noListFragment);
|
||||
const studiesNum = (this.props.studies && this.props.studies.length) || 0;
|
||||
|
||||
return (
|
||||
<div className="StudyList">
|
||||
<div className="studyListToolbar clearfix">
|
||||
<div className="header pull-left">{this.props.t('StudyList')}</div>
|
||||
<div className="studyCount pull-right">
|
||||
{this.props.studies.length}
|
||||
</div>
|
||||
<div className="studyCount pull-right">{studiesNum}</div>
|
||||
<div className="pull-right">
|
||||
{this.props.studyListFunctionsEnabled ? (
|
||||
<StudylistToolbar onImport={this.props.onImport} />
|
||||
@@ -367,22 +437,16 @@ class StudyList extends Component {
|
||||
(this.state.focusedInput === 'endDate' ||
|
||||
preset)
|
||||
) {
|
||||
this.setSearchDataBatch(
|
||||
{
|
||||
studyDateFrom: startDate.toDate(),
|
||||
studyDateTo: endDate.toDate(),
|
||||
},
|
||||
this.search
|
||||
);
|
||||
this.setSearchDataBatch({
|
||||
studyDateFrom: startDate.toDate(),
|
||||
studyDateTo: endDate.toDate(),
|
||||
});
|
||||
this.setState({ focusedInput: false });
|
||||
} else if (!startDate && !endDate) {
|
||||
this.setSearchDataBatch(
|
||||
{
|
||||
studyDateFrom: null,
|
||||
studyDateTo: null,
|
||||
},
|
||||
this.search
|
||||
);
|
||||
this.setSearchDataBatch({
|
||||
studyDateFrom: null,
|
||||
studyDateTo: null,
|
||||
});
|
||||
}
|
||||
}}
|
||||
focusedInput={this.state.focusedInput}
|
||||
@@ -398,26 +462,18 @@ class StudyList extends Component {
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="studyListData">
|
||||
{this.props.studies.map(study => {
|
||||
return this.renderTableRow(study);
|
||||
})}
|
||||
</tbody>
|
||||
<tbody id="studyListData">{tableBody}</tbody>
|
||||
</table>
|
||||
|
||||
{this.renderIsLoading()}
|
||||
{this.renderHasError()}
|
||||
{this.renderNoMachingResults()}
|
||||
|
||||
<PaginationArea
|
||||
pageOptions={this.props.pageOptions}
|
||||
currentPage={this.state.searchData.currentPage}
|
||||
nextPageFunc={this.nextPage}
|
||||
prevPageFunc={this.prevPage}
|
||||
onRowsPerPageChange={this.onRowsPerPageChange}
|
||||
rowsPerPage={this.state.searchData.rowsPerPage}
|
||||
recordCount={this.props.studies.length}
|
||||
/>
|
||||
{noListFragment
|
||||
? noListFragment
|
||||
: getPaginationFragment(
|
||||
this.props,
|
||||
this.state.searchData,
|
||||
this.nextPage,
|
||||
this.prevPage,
|
||||
this.onRowsPerPageChange
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -82,9 +82,14 @@ placeholder-color(c)
|
||||
position: absolute
|
||||
z-index: 2
|
||||
|
||||
.loading-text
|
||||
color: var(--table-text-secondary-color)
|
||||
font-size: 30px
|
||||
.loading
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.loading-text
|
||||
color: var(--table-text-secondary-color)
|
||||
font-size: 30px
|
||||
width: fit-content;
|
||||
|
||||
.notFound
|
||||
color: var(--table-text-secondary-color)
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import React from 'react';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
|
||||
function StudyListLoadingText() {
|
||||
function StudyListLoadingText({ t: translate }) {
|
||||
return (
|
||||
<div className="loading-text">
|
||||
Loading... <Icon name="circle-notch" animation="pulse" />
|
||||
{translate('Loading')}... <Icon name="circle-notch" animation="pulse" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { StudyListLoadingText };
|
||||
const connectedComponent = withTranslation('StudyListLoadingText')(
|
||||
StudyListLoadingText
|
||||
);
|
||||
|
||||
export { connectedComponent as StudyListLoadingText };
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-bootstrap-modal';
|
||||
import detect from 'browser-detect';
|
||||
import './AboutModal.styl';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
|
||||
import 'react-bootstrap-modal/lib/css/rbm-patch.css';
|
||||
|
||||
// TODO: Is this the only component importing these?
|
||||
import './../../design/styles/common/modal.styl';
|
||||
|
||||
export class AboutModal extends Component {
|
||||
class AboutModal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
@@ -18,16 +20,41 @@ export class AboutModal extends Component {
|
||||
onCancel: PropTypes.func,
|
||||
};
|
||||
|
||||
static itemsPreset = [
|
||||
{
|
||||
name: 'Repository URL',
|
||||
value: 'https://github.com/OHIF/Viewers/',
|
||||
},
|
||||
{
|
||||
name: 'Latest Master Commits',
|
||||
value: 'https://github.com/OHIF/Viewers/commits/master',
|
||||
},
|
||||
];
|
||||
itemsPreset() {
|
||||
const { t } = this.props;
|
||||
const browser = detect();
|
||||
const capitalize = s =>
|
||||
s.substr(0, 1).toUpperCase() + s.substr(1).toLowerCase();
|
||||
|
||||
return [
|
||||
{
|
||||
name: t('Repository URL'),
|
||||
value: 'https://github.com/OHIF/Viewers/',
|
||||
link: 'https://github.com/OHIF/Viewers/',
|
||||
},
|
||||
{
|
||||
name: t('Latest Master Commits'),
|
||||
value: 'https://github.com/OHIF/Viewers/commits/master',
|
||||
link: 'https://github.com/OHIF/Viewers/commits/master',
|
||||
},
|
||||
{
|
||||
name: 'Version Number',
|
||||
value: process.env.VERSION_NUMBER,
|
||||
},
|
||||
{
|
||||
name: t('Build Number'),
|
||||
value: process.env.BUILD_NUM,
|
||||
},
|
||||
{
|
||||
name: t('Browser'),
|
||||
value: `${capitalize(browser.name)} ${browser.version}`,
|
||||
},
|
||||
{
|
||||
name: t('OS'),
|
||||
value: browser.os,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
isOpen: false,
|
||||
@@ -38,15 +65,20 @@ export class AboutModal extends Component {
|
||||
<tr key={item.name}>
|
||||
<td>{item.name}</td>
|
||||
<td>
|
||||
<a target="_blank" href={item.value}>
|
||||
{item.value}
|
||||
</a>
|
||||
{item.link ? (
|
||||
<a target="_blank" href={item.link}>
|
||||
{item.value}
|
||||
</a>
|
||||
) : (
|
||||
item.value
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.isOpen}
|
||||
@@ -58,7 +90,7 @@ export class AboutModal extends Component {
|
||||
keyboard={false}
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>About</Modal.Title>
|
||||
<Modal.Title>{t('OHIF Viewer - About')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className="btn-group">
|
||||
@@ -67,34 +99,36 @@ export class AboutModal extends Component {
|
||||
target="_blank"
|
||||
href="https://groups.google.com/forum/#!forum/cornerstone-platform"
|
||||
>
|
||||
Visit the forum
|
||||
{t('Visit the forum')}
|
||||
</a>
|
||||
{` `}
|
||||
<a
|
||||
className="btn btn-default"
|
||||
target="_blank"
|
||||
href="https://github.com/OHIF/Viewers/issues"
|
||||
href="https://github.com/OHIF/Viewers/issues/new/choose"
|
||||
>
|
||||
Report an issue
|
||||
{t('Report an issue')}
|
||||
</a>
|
||||
{` `}
|
||||
<a
|
||||
className="btn btn-default"
|
||||
target="_blank"
|
||||
href="http://ohif.org"
|
||||
>
|
||||
More details
|
||||
{t('More details')}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Version Information</h3>
|
||||
<h3>{t('Version Information')}</h3>
|
||||
<table className="table table-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
<th>{t('Name')}</th>
|
||||
<th>{t('Value')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{AboutModal.itemsPreset.map(item => this.renderTableRow(item))}
|
||||
{this.itemsPreset().map(item => this.renderTableRow(item))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -103,3 +137,7 @@ export class AboutModal extends Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const connectedComponent = withTranslation('AboutModal')(AboutModal);
|
||||
export { connectedComponent as AboutModal };
|
||||
export default connectedComponent;
|
||||
@@ -12,6 +12,7 @@
|
||||
z-index: 1050;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
outline: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
|
||||
.modal-dialog
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ import { ScrollableArea } from './ScrollableArea/ScrollableArea.js';
|
||||
import Toolbar from './viewer/Toolbar.js';
|
||||
import ToolbarButton from './viewer/ToolbarButton.js';
|
||||
import ViewerbaseDragDropContext from './utils/viewerbaseDragDropContext.js';
|
||||
import SnackbarProvider, {
|
||||
useSnackbarContext,
|
||||
withSnackbar,
|
||||
} from './utils/SnackbarProvider';
|
||||
|
||||
export {
|
||||
ICONS,
|
||||
@@ -68,4 +72,7 @@ export {
|
||||
UserPreferences,
|
||||
UserPreferencesModal,
|
||||
ViewerbaseDragDropContext,
|
||||
SnackbarProvider,
|
||||
useSnackbarContext,
|
||||
withSnackbar,
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { useState, createContext, useContext } from 'react';
|
||||
import SnackbarContainer from '../components/snackbar/SnackbarContainer';
|
||||
import SnackbarTypes from '../components/snackbar/SnackbarTypes';
|
||||
|
||||
const SnackbarContext = createContext(null);
|
||||
|
||||
export const useSnackbarContext = () => useContext(SnackbarContext);
|
||||
|
||||
const SnackbarProvider = ({ children }) => {
|
||||
const DEFAULT_OPTIONS = {
|
||||
title: '',
|
||||
message: '',
|
||||
duration: 5000,
|
||||
autoClose: true,
|
||||
position: 'bottomRight',
|
||||
type: SnackbarTypes.INFO,
|
||||
};
|
||||
|
||||
const [count, setCount] = useState(1);
|
||||
const [snackbarItems, setSnackbarItems] = useState([]);
|
||||
|
||||
const show = options => {
|
||||
if (!options || (!options.title && !options.message)) {
|
||||
console.warn(
|
||||
'Snackbar cannot be rendered without required parameters: title | message'
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const newItem = {
|
||||
...DEFAULT_OPTIONS,
|
||||
...options,
|
||||
id: count,
|
||||
visible: true,
|
||||
};
|
||||
|
||||
setSnackbarItems(state => [...state, newItem]);
|
||||
setCount(count + 1);
|
||||
};
|
||||
|
||||
const hide = id => {
|
||||
const hideItem = items => {
|
||||
const newItems = items.map(item => {
|
||||
if (item.id === id) {
|
||||
item.visible = false;
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
return newItems;
|
||||
};
|
||||
|
||||
setSnackbarItems(state => hideItem(state));
|
||||
|
||||
setTimeout(() => {
|
||||
setSnackbarItems(state => [...state.filter(item => item.id !== id)]);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const hideAll = () => {
|
||||
// reset count
|
||||
setCount(1);
|
||||
|
||||
// remove all items from array
|
||||
setSnackbarItems(() => []);
|
||||
};
|
||||
|
||||
/**
|
||||
* expose snackbar methods to window for debug purposes
|
||||
* TODO: Check if it's really necessary
|
||||
*/
|
||||
window.snackbar = {
|
||||
show,
|
||||
hide,
|
||||
hideAll,
|
||||
};
|
||||
|
||||
return (
|
||||
<SnackbarContext.Provider value={{ show, hide, hideAll, snackbarItems }}>
|
||||
{!!snackbarItems && <SnackbarContainer />}
|
||||
{children}
|
||||
</SnackbarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* High Order Component to use the snackbar methods through a Class Component
|
||||
*
|
||||
*/
|
||||
export const withSnackbar = Component => {
|
||||
return function WrappedComponent(props) {
|
||||
const snackbarContext = {
|
||||
...useSnackbarContext(),
|
||||
};
|
||||
return <Component {...props} snackbarContext={snackbarContext} />;
|
||||
};
|
||||
};
|
||||
|
||||
export default SnackbarProvider;
|
||||
@@ -3,6 +3,161 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.12.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.12.0...@ohif/viewer@1.12.1) (2019-10-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Orthographic MPR fix ([#1092](https://github.com/OHIF/Viewers/issues/1092)) ([460e375](https://github.com/OHIF/Viewers/commit/460e375f0aa75d35f7a46b4d48e6cc706019956d))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.12.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.11.5...@ohif/viewer@1.12.0) (2019-10-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Allow routes to load Google Cloud DICOM Stores in the Study List ([#1069](https://github.com/OHIF/Viewers/issues/1069)) ([21b586b](https://github.com/OHIF/Viewers/commit/21b586b08f3dde6613859712a9e0577dece564db))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.11.4...@ohif/viewer@1.11.5) (2019-10-24)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
## [1.11.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.11.3...@ohif/viewer@1.11.4) (2019-10-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Revert "Revert "fix: MPR initialization"" ([#1065](https://github.com/OHIF/Viewers/issues/1065)) ([c680720](https://github.com/OHIF/Viewers/commit/c680720ce5ead58fdb399e3a356edac18093f5c0)), closes [#1062](https://github.com/OHIF/Viewers/issues/1062) [#1064](https://github.com/OHIF/Viewers/issues/1064)
|
||||
|
||||
## [1.11.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.11.2...@ohif/viewer@1.11.3) (2019-10-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Switch to orhtographic view for 2D MPR ([#1074](https://github.com/OHIF/Viewers/issues/1074)) ([13d337a](https://github.com/OHIF/Viewers/commit/13d337aaabb8dadf6366c6262c5e47e7781edd08))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.11.1...@ohif/viewer@1.11.2) (2019-10-23)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.11.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.11.0...@ohif/viewer@1.11.1) (2019-10-23)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.11.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.10.3...@ohif/viewer@1.11.0) (2019-10-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* MPR initialization ([#1062](https://github.com/OHIF/Viewers/issues/1062)) ([b037394](https://github.com/OHIF/Viewers/commit/b03739428f72bb50bdabdd6f83b7af885057da69))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Load spinner when selecting gcloud store. Add key on td ([#1034](https://github.com/OHIF/Viewers/issues/1034)) ([e62f403](https://github.com/OHIF/Viewers/commit/e62f403fe9e3df56713128e3d59045824b086d8d)), closes [#1057](https://github.com/OHIF/Viewers/issues/1057)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.10.2...@ohif/viewer@1.10.3) (2019-10-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.10.1...@ohif/viewer@1.10.2) (2019-10-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.10.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.10.0...@ohif/viewer@1.10.1) (2019-10-16)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.10.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.9.1...@ohif/viewer@1.10.0) (2019-10-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add browser info and app version ([#1046](https://github.com/OHIF/Viewers/issues/1046)) ([c217b8b](https://github.com/OHIF/Viewers/commit/c217b8b))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.9.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.9.0...@ohif/viewer@1.9.1) (2019-10-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Remove debugger statement left in from last PR ([#1052](https://github.com/OHIF/Viewers/issues/1052)) ([d091cd6](https://github.com/OHIF/Viewers/commit/d091cd6))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.9.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.8.0...@ohif/viewer@1.9.0) (2019-10-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Only allow reconstruction of datasets that make sense ([#1010](https://github.com/OHIF/Viewers/issues/1010)) ([2d75e01](https://github.com/OHIF/Viewers/commit/2d75e01)), closes [#561](https://github.com/OHIF/Viewers/issues/561)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.8.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.7.0...@ohif/viewer@1.8.0) (2019-10-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Notification Service ([#1011](https://github.com/OHIF/Viewers/issues/1011)) ([92c8996](https://github.com/OHIF/Viewers/commit/92c8996))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.7.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.6.3...@ohif/viewer@1.7.0) (2019-10-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Implement a 'Exit 2D MPR' button in the toolbar ([c99e0d8](https://github.com/OHIF/Viewers/commit/c99e0d8))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.6.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.6.2...@ohif/viewer@1.6.3) (2019-10-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
@@ -2,5 +2,9 @@
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"video": false,
|
||||
"chromeWebSecurity": false,
|
||||
"waitForAnimations": true,
|
||||
"defaultCommandTimeout": 10000,
|
||||
"requestTimeout": 10000,
|
||||
"responseTimeout": 10000,
|
||||
"projectId": "4oe38f"
|
||||
}
|
||||
@@ -2,12 +2,12 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
before(() => {
|
||||
cy.openStudy('MISTER^MR');
|
||||
cy.waitDicomImage();
|
||||
cy.expectMinimumThumbnails(3);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.initCornerstoneToolsAliases();
|
||||
cy.initCommonElementsAliases();
|
||||
//Following best practices, reset should be done before each test
|
||||
cy.resetViewport();
|
||||
});
|
||||
|
||||
@@ -42,9 +42,6 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
cy.get('@moreBtn')
|
||||
.should('be.visible')
|
||||
.contains('More');
|
||||
cy.get('@twodmprBtn')
|
||||
.should('be.visible')
|
||||
.contains('2D MPR');
|
||||
cy.get('@layoutBtn')
|
||||
.should('be.visible')
|
||||
.contains('Layout');
|
||||
@@ -60,12 +57,11 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
|
||||
//drags the mouse inside the viewport to be able to interact with series
|
||||
cy.get('@viewport')
|
||||
.trigger('mousedown', 'top', { which: 1 })
|
||||
.trigger('mousemove', 'center', { which: 1 })
|
||||
.trigger('mousedown', 'center', { which: 1 })
|
||||
.trigger('mousemove', 'top', { which: 1 })
|
||||
.trigger('mouseup');
|
||||
|
||||
const expectedText =
|
||||
'Ser: 1Img: 14 14/26256 x 256Loc: 0.00 mm Thick: 5.00 mm';
|
||||
'Ser: 1Img: 1 1/26256 x 256Loc: -30.00 mm Thick: 5.00 mm';
|
||||
cy.get('@viewportInfoBottomLeft').should('have.text', expectedText);
|
||||
});
|
||||
|
||||
@@ -79,11 +75,11 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
|
||||
//drags the mouse inside the viewport to be able to interact with series
|
||||
cy.get('@viewport')
|
||||
.trigger('mousedown', 'top', { which: 1 })
|
||||
.trigger('mousemove', 'center', { which: 1 })
|
||||
.trigger('mousedown', 'center', { which: 1 })
|
||||
.trigger('mousemove', 'top', { which: 1 })
|
||||
.trigger('mouseup');
|
||||
|
||||
const expectedText = 'Zoom: 884%W: 820 L: 410Lossless / Uncompressed';
|
||||
const expectedText = 'Zoom: 50%W: 958 L: 479Lossless / Uncompressed';
|
||||
cy.get('@viewportInfoBottomRight').should('have.text', expectedText);
|
||||
});
|
||||
|
||||
@@ -97,14 +93,14 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
|
||||
//drags the mouse inside the viewport to be able to interact with series
|
||||
cy.get('@viewport')
|
||||
.trigger('mousedown', 'top', { which: 1 })
|
||||
.trigger('mousemove', 'center', { which: 1 })
|
||||
.trigger('mousedown', 'center', { which: 1 })
|
||||
.trigger('mousemove', 'top', { which: 1 })
|
||||
.trigger('mouseup')
|
||||
.trigger('mousedown', 'center', { which: 1 })
|
||||
.trigger('mousemove', 'left', { which: 1 })
|
||||
.trigger('mouseup');
|
||||
|
||||
const expectedText = 'Zoom: 211%W: 544 L: 626Lossless / Uncompressed';
|
||||
const expectedText = 'Zoom: 211%W: 635 L: 226Lossless / Uncompressed';
|
||||
cy.get('@viewportInfoBottomRight').should('have.text', expectedText);
|
||||
});
|
||||
|
||||
@@ -148,7 +144,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
//Add annotation on the viewport
|
||||
const firstClick = [150, 100];
|
||||
const secondClick = [130, 170];
|
||||
cy.addLine('.cornerstone-canvas', firstClick, secondClick);
|
||||
cy.addLine('@viewport', firstClick, secondClick);
|
||||
|
||||
//Verify if measurement annotation was added into the measurements panel
|
||||
cy.get('@measurementsBtn')
|
||||
@@ -176,7 +172,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
const initPos = [180, 390];
|
||||
const midPos = [300, 410];
|
||||
const finalPos = [180, 450];
|
||||
cy.addAngle('.cornerstone-canvas', initPos, midPos, finalPos);
|
||||
cy.addAngle('@viewport', initPos, midPos, finalPos);
|
||||
|
||||
//Verify if measurement annotation was added into the measurements panel
|
||||
cy.get('@measurementsBtn')
|
||||
@@ -200,11 +196,14 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
//Click on reset button
|
||||
cy.get('@resetBtn').click();
|
||||
|
||||
const expectedText = 'Zoom: 211%W: 820 L: 410Lossless / Uncompressed';
|
||||
const expectedText = 'Zoom: 211%W: 958 L: 479Lossless / Uncompressed';
|
||||
cy.get('@viewportInfoBottomRight').should('have.text', expectedText);
|
||||
});
|
||||
|
||||
it('checks if CINE tool will prompt a modal with working controls', () => {
|
||||
cy.server();
|
||||
cy.route('GET', '/**/studies/**/').as('studies');
|
||||
|
||||
//Click on button
|
||||
cy.get('@cineBtn').click();
|
||||
//Vefiry if cine control overlay is being displayed
|
||||
@@ -219,26 +218,49 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
.click();
|
||||
|
||||
let expectedText = 'Img: 1 1/26';
|
||||
cy.get('@viewportInfoBottomLeft').should('not.have.text', expectedText);
|
||||
cy.get('@viewportInfoBottomLeft', { timeout: 15000 }).should(
|
||||
'not.have.text',
|
||||
expectedText
|
||||
);
|
||||
|
||||
//Test SKIP TO FIRST IMAGE button
|
||||
cy.get('[title="Skip to first Image"]').click();
|
||||
cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText);
|
||||
cy.get('[title="Skip to first Image"]')
|
||||
.click()
|
||||
.wait(1000);
|
||||
cy.get('@viewportInfoBottomLeft', { timeout: 15000 }).should(
|
||||
'contain.text',
|
||||
expectedText
|
||||
);
|
||||
|
||||
//Test NEXT IMAGE button
|
||||
cy.get('[title="Next Image"]').click();
|
||||
cy.get('[title="Next Image"]')
|
||||
.click()
|
||||
.wait(1000);
|
||||
expectedText = 'Img: 2 2/26';
|
||||
cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText);
|
||||
cy.get('@viewportInfoBottomLeft', { timeout: 15000 }).should(
|
||||
'contain.text',
|
||||
expectedText
|
||||
);
|
||||
|
||||
//Test SKIP TO LAST IMAGE button
|
||||
cy.get('[title="Skip to last Image"]').click();
|
||||
cy.get('[title="Skip to last Image"]')
|
||||
.click()
|
||||
.wait(2000);
|
||||
expectedText = 'Img: 27 26/26';
|
||||
cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText);
|
||||
cy.get('@viewportInfoBottomLeft', { timeout: 15000 }).should(
|
||||
'contain.text',
|
||||
expectedText
|
||||
);
|
||||
|
||||
//Test PREVIOUS IMAGE button
|
||||
cy.get('[title="Previous Image"]').click();
|
||||
cy.get('[title="Previous Image"]')
|
||||
.click()
|
||||
.wait(1000);
|
||||
expectedText = 'Img: 26 25/26';
|
||||
cy.get('@viewportInfoBottomLeft').should('contain.text', expectedText);
|
||||
cy.get('@viewportInfoBottomLeft', { timeout: 15000 }).should(
|
||||
'contain.text',
|
||||
expectedText
|
||||
);
|
||||
|
||||
//Click on Cine button
|
||||
cy.get('@cineBtn').click();
|
||||
@@ -361,33 +383,20 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
.should('be.eq', 9);
|
||||
});
|
||||
|
||||
//Commented this to avoid throwing an wrong exception at the end of "Layout button" test
|
||||
//this commented section should be uncommented once issue #999 is fixed. (https://github.com/OHIF/Viewers/issues/999)
|
||||
|
||||
//verify if layout has changed to 1 viewport
|
||||
// cy.get('@layoutBtn').click();
|
||||
// cy.get('tbody > :nth-child(1) > :nth-child(1)').click();
|
||||
// cy.get('.viewport-container').then(($viewport) =>{
|
||||
// cy.wrap($viewport)
|
||||
// .its('length')
|
||||
// .should('be.eq', 1);
|
||||
// })
|
||||
cy.reload();
|
||||
cy.waitDicomImage();
|
||||
cy.get('@layoutBtn').click();
|
||||
cy.get('tbody > :nth-child(1) > :nth-child(1)').click();
|
||||
cy.get('.viewport-container').then($viewport => {
|
||||
cy.wrap($viewport)
|
||||
.its('length')
|
||||
.should('be.eq', 1);
|
||||
});
|
||||
});
|
||||
|
||||
it('checks if Clear tool will delete all measurements added in the viewport', () => {
|
||||
//Add measurements in the viewport
|
||||
cy.get('@lengthBtn').click();
|
||||
const firstClick = [150, 100];
|
||||
const secondClick = [130, 170];
|
||||
cy.addLine('.cornerstone-canvas', firstClick, secondClick);
|
||||
|
||||
cy.get('@angleBtn').click();
|
||||
const initPos = [180, 390];
|
||||
const midPos = [300, 410];
|
||||
const finalPos = [180, 450];
|
||||
cy.addAngle('.cornerstone-canvas', initPos, midPos, finalPos);
|
||||
cy.addLengthMeasurement();
|
||||
cy.addAngleMeasurement();
|
||||
|
||||
//Verify if measurement annotation was added into the measurements panel
|
||||
cy.get('@measurementsBtn').click();
|
||||
@@ -419,23 +428,18 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
|
||||
it('checks if Eraser tool will remove the measurements added in the viewport', () => {
|
||||
//Add measurements in the viewport
|
||||
cy.get('@lengthBtn').click();
|
||||
const firstClick = [150, 100];
|
||||
const secondClick = [130, 170];
|
||||
cy.addLine('.cornerstone-canvas', firstClick, secondClick);
|
||||
|
||||
cy.get('@angleBtn').click();
|
||||
const initPos = [180, 390];
|
||||
const midPos = [300, 410];
|
||||
const finalPos = [180, 450];
|
||||
cy.addAngle('.cornerstone-canvas', initPos, midPos, finalPos);
|
||||
cy.addLengthMeasurement();
|
||||
cy.addAngleMeasurement();
|
||||
|
||||
//Verify if measurement annotation was added into the measurements panel
|
||||
cy.get('@measurementsBtn').click();
|
||||
cy.get('.measurementItem')
|
||||
.its('length')
|
||||
.should('be.eq', 2);
|
||||
cy.get('@measurementsBtn').click();
|
||||
cy.get('@measurementsBtn')
|
||||
.click()
|
||||
.wait(2000);
|
||||
//cy.isNotInViewport('@measurementsPanel'); //TO DO: check this intermittent behaviour
|
||||
|
||||
//Click More button
|
||||
cy.get('@moreBtn').click();
|
||||
@@ -443,14 +447,14 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
cy.get('.tooltip-inner > :nth-child(12)').click();
|
||||
|
||||
//Erase measurement #1 and Verify if it was removed from the measurements panel
|
||||
const [x1, y1] = firstClick;
|
||||
const [x1, y1] = [150, 100];
|
||||
cy.get('@viewport').click(x1, y1, { force: true });
|
||||
cy.get('.measurementItem')
|
||||
.its('length')
|
||||
.should('be.eq', 1);
|
||||
|
||||
//Erase measurement #2 and Verify if it was removed from the measurements panel
|
||||
const [x2, y2] = initPos;
|
||||
const [x2, y2] = [180, 390];
|
||||
cy.get('@viewport').click(x2, y2, { force: true });
|
||||
cy.get('.measurementItem').should('not.exist');
|
||||
});
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
describe('OHIFStandaloneViewer', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/');
|
||||
cy.openStudyList();
|
||||
});
|
||||
|
||||
it('loads route with at least 2 rows', () => {
|
||||
cy.screenshot();
|
||||
cy.percyCanvasSnapshot('Study List');
|
||||
|
||||
cy.get('#studyListData tr')
|
||||
.its('length')
|
||||
.should('be.gt', 2);
|
||||
});
|
||||
|
||||
it('first 2 rows has values', () => {
|
||||
cy.get('#studyListData > :nth-child(1) > .patientId').should('be.visible');
|
||||
cy.get('#studyListData > :nth-child(2) > .patientId').should('be.visible');
|
||||
cy.get('#studyListData > :nth-child(1) > .patientId', {
|
||||
timeout: 15000,
|
||||
}).should('be.visible');
|
||||
cy.get('#studyListData > :nth-child(2) > .patientId', {
|
||||
timeout: 15000,
|
||||
}).should('be.visible');
|
||||
});
|
||||
});
|
||||
@@ -2,22 +2,25 @@ describe('OHIF Study Viewer Page', () => {
|
||||
before(() => {
|
||||
cy.openStudy('MISTER^MR');
|
||||
cy.waitDicomImage();
|
||||
cy.expectMinimumThumbnails(6);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.initCommonElementsAliases();
|
||||
//Following best practices, reset should be done before each test
|
||||
cy.resetViewport();
|
||||
});
|
||||
|
||||
it('checks if series thumbnails are being displayed', () => {
|
||||
cy.get('.ThumbnailEntryContainer')
|
||||
cy.screenshot();
|
||||
cy.percyCanvasSnapshot('Series Thumbnails');
|
||||
|
||||
cy.get('[data-cy="thumbnail-list"]')
|
||||
.its('length')
|
||||
.should('be.gt', 1);
|
||||
});
|
||||
|
||||
it('drags and drop a series thumbnail into viewport', () => {
|
||||
cy.get('.ThumbnailEntryContainer:nth-child(2)') //element to be dragged
|
||||
cy.get('[data-cy="thumbnail-list"]:nth-child(2)') //element to be dragged
|
||||
.drag('.cornerstone-canvas'); //dropzone element
|
||||
|
||||
const expectedText =
|
||||
@@ -62,8 +65,11 @@ describe('OHIF Study Viewer Page', () => {
|
||||
// Confirm Selection
|
||||
cy.get('.checkIconWrapper').click();
|
||||
|
||||
//Verify if 'Bone' label was added
|
||||
// Verify if 'Bone' label was added
|
||||
cy.get('.measurementLocation').should('contain.text', 'Bone');
|
||||
// Close panel
|
||||
cy.get('@measurementsBtn').click();
|
||||
cy.get('@measurementsPanel').should('not.be.enabled');
|
||||
});
|
||||
|
||||
//TO-DO: Test case will fail due to issue #1013: https://github.com/OHIF/Viewers/issues/1013
|
||||
@@ -90,4 +96,114 @@ describe('OHIF Study Viewer Page', () => {
|
||||
// cy.get('.measurementLocation')
|
||||
// .should('contain.text', descriptionText);
|
||||
// });
|
||||
|
||||
it('checks if measurement item can be deleted through the context menu on the viewport', () => {
|
||||
cy.addLengthMeasurement([100, 100], [200, 100]); //Adding measurement in the viewport
|
||||
|
||||
//Right click on measurement annotation
|
||||
const [x1, y1] = [150, 100];
|
||||
cy.get('@viewport')
|
||||
.trigger('mousedown', x1, y1, {
|
||||
which: 3,
|
||||
})
|
||||
.trigger('mouseup', x1, y1, {
|
||||
which: 3,
|
||||
});
|
||||
|
||||
//Contextmenu is visible
|
||||
cy.get('.ToolContextMenu').should('be.visible');
|
||||
|
||||
//Click "Delete measurement"
|
||||
cy.get('.form-action')
|
||||
.contains('Delete measurement')
|
||||
.click();
|
||||
|
||||
//Open measurements menu
|
||||
cy.get('@measurementsBtn').click();
|
||||
|
||||
//Verify measurements was removed from panel
|
||||
cy.get('.measurementItem')
|
||||
.should('not.exist')
|
||||
.log('Annotation removed with success');
|
||||
|
||||
//Close panel
|
||||
cy.get('@measurementsBtn').click();
|
||||
cy.get('@measurementsPanel').should('not.be.enabled');
|
||||
});
|
||||
|
||||
it('adds relabel and description to measurement item through the context menu on the viewport', () => {
|
||||
cy.addLengthMeasurement([100, 100], [200, 100]); //Adding measurement in the viewport
|
||||
|
||||
// Relabel
|
||||
// Right click on measurement annotation
|
||||
const [x1, y1] = [150, 100];
|
||||
cy.get('@viewport')
|
||||
.trigger('mousedown', x1, y1, {
|
||||
which: 3,
|
||||
})
|
||||
.trigger('mouseup', x1, y1, {
|
||||
which: 3,
|
||||
});
|
||||
|
||||
// Contextmenu is visible
|
||||
cy.get('.ToolContextMenu').should('be.visible');
|
||||
|
||||
// Click "Relabel"
|
||||
cy.get('.form-action')
|
||||
.contains('Relabel')
|
||||
.click();
|
||||
|
||||
// Search for "Brain"
|
||||
cy.get('.searchInput').type('Brain');
|
||||
|
||||
// Select "Brain" Result
|
||||
cy.get('.treeInputs > .wrapperLabel')
|
||||
.contains('Brain')
|
||||
.click();
|
||||
|
||||
// Confirm Selection
|
||||
cy.get('.checkIconWrapper').click();
|
||||
|
||||
// Description
|
||||
// Right click on measurement annotation
|
||||
cy.get('@viewport')
|
||||
.trigger('mousedown', x1, y1, {
|
||||
which: 3,
|
||||
})
|
||||
.trigger('mouseup', x1, y1, {
|
||||
which: 3,
|
||||
});
|
||||
|
||||
// Contextmenu is visible
|
||||
cy.get('.ToolContextMenu').should('be.visible');
|
||||
|
||||
// Click "Description"
|
||||
cy.get('.form-action')
|
||||
.contains('Add Description')
|
||||
.click();
|
||||
|
||||
// Enter description text
|
||||
const descriptionText = 'Adding text for description test';
|
||||
cy.get('#description').type(descriptionText);
|
||||
|
||||
// Confirm
|
||||
cy.get('.btn-confirm').click();
|
||||
|
||||
//Open measurements menu
|
||||
cy.get('@measurementsBtn').click();
|
||||
|
||||
// Verify if label was added
|
||||
cy.get('.measurementLocation')
|
||||
.should('contain.text', 'Brain')
|
||||
.log('Relabel added with success');
|
||||
|
||||
//Verify if descriptionText was added
|
||||
cy.get('.measurementLocation')
|
||||
.should('contain.text', descriptionText)
|
||||
.log('Description added with success');
|
||||
|
||||
// Close panel
|
||||
cy.get('@measurementsBtn').click();
|
||||
cy.get('@measurementsPanel').should('not.be.enabled');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
describe('OHIF HTML Extension', () => {
|
||||
before(() => {
|
||||
cy.openStudy('Dummy');
|
||||
cy.expectMinimumThumbnails(5);
|
||||
});
|
||||
|
||||
it('checks if series thumbnails are being displayed', () => {
|
||||
cy.get('[data-cy="thumbnail-list"]')
|
||||
.contains('SR')
|
||||
.its('length')
|
||||
.should('to.be.at.least', 1);
|
||||
});
|
||||
|
||||
it('drags and drop a SR thumbnail into viewport', () => {
|
||||
cy.get('[data-cy="thumbnail-list"]')
|
||||
.contains('SR')
|
||||
.first()
|
||||
.drag('.viewport-drop-target');
|
||||
|
||||
cy.get(':nth-child(2) > h1').should(
|
||||
'contain.text',
|
||||
'Imaging Measurement Report'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
describe('OHIF Microscopy Extension', () => {
|
||||
before(() => {
|
||||
cy.openStudyModality('SM');
|
||||
cy.expectMinimumThumbnails(6);
|
||||
});
|
||||
|
||||
it('checks if series thumbnails are being displayed', () => {
|
||||
cy.get('[data-cy="thumbnail-list"]')
|
||||
.contains('SM')
|
||||
.its('length')
|
||||
.should('to.be.at.least', 1);
|
||||
});
|
||||
|
||||
it('drags and drop a SM thumbnail into viewport', () => {
|
||||
cy.get('[data-cy="thumbnail-list"]')
|
||||
.contains('SM')
|
||||
.drag('.viewport-drop-target');
|
||||
|
||||
cy.get('.DicomMicroscopyViewer')
|
||||
.its('length')
|
||||
.should('be.eq', 1);
|
||||
|
||||
cy.wait(3000); //Waiting for image to render before taking the snapshot
|
||||
cy.screenshot();
|
||||
cy.percyCanvasSnapshot('Microscopy Extension');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
describe('OHIF PDF Extension', () => {
|
||||
before(() => {
|
||||
cy.openStudy('Dummy');
|
||||
cy.expectMinimumThumbnails(6);
|
||||
});
|
||||
|
||||
it('checks if series thumbnails are being displayed', () => {
|
||||
cy.get('[data-cy="thumbnail-list"]')
|
||||
.contains('DOC')
|
||||
.its('length')
|
||||
.should('to.be.at.least', 1);
|
||||
});
|
||||
|
||||
it('drags and drop a PDF thumbnail into viewport', () => {
|
||||
cy.get('[data-cy="thumbnail-list"]')
|
||||
.contains('DOC')
|
||||
.drag('.viewport-drop-target');
|
||||
|
||||
cy.get('.DicomPDFViewport')
|
||||
.its('length')
|
||||
.should('be.eq', 1);
|
||||
|
||||
cy.screenshot();
|
||||
cy.percyCanvasSnapshot('PDF Extension');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
describe('OHIF VTK Extension', () => {
|
||||
before(() => {
|
||||
cy.openStudy('Juno');
|
||||
cy.waitDicomImage();
|
||||
cy.expectMinimumThumbnails(7);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: We shouldn't have to drag the thumbnail
|
||||
// This is a known bug; 2D MPR button does not show until viewport
|
||||
// has data from a drag-n-drop
|
||||
// Drag and drop first thumbnail into first viewport
|
||||
cy.get('[data-cy="thumbnail-list"]:nth-child(3)').drag(
|
||||
'.viewport-drop-target'
|
||||
);
|
||||
|
||||
cy.get('.PluginSwitch > .toolbar-button')
|
||||
.as('twodmprBtn')
|
||||
.should('be.visible')
|
||||
.then(btn => {
|
||||
if (!btn.text().includes('Exit')) {
|
||||
btn.click();
|
||||
}
|
||||
});
|
||||
//wait VTK toolbar and images to be loaded
|
||||
cy.wait(3000);
|
||||
cy.initVTKToolsAliases();
|
||||
});
|
||||
|
||||
it('checks if VTK buttons are displayed on the toolbar', () => {
|
||||
cy.screenshot();
|
||||
cy.percyCanvasSnapshot('VTK Extension');
|
||||
|
||||
cy.get('@crosshairsBtn')
|
||||
.should('be.visible')
|
||||
.contains('Crosshairs');
|
||||
cy.get('@wwwcBtn')
|
||||
.should('be.visible')
|
||||
.contains('WWWC');
|
||||
cy.get('@rotateBtn')
|
||||
.should('be.visible')
|
||||
.contains('Rotate');
|
||||
cy.get('@slabSlider')
|
||||
.should('be.visible')
|
||||
.contains('Slab Thickness');
|
||||
cy.get('@modeDropdown')
|
||||
.should('be.visible')
|
||||
.contains('MIP');
|
||||
cy.get('@modeCheckbox').should('be.visible');
|
||||
cy.get('@layoutBtn')
|
||||
.should('be.visible')
|
||||
.contains('Layout');
|
||||
});
|
||||
});
|
||||
@@ -7,10 +7,10 @@
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
let percyHealthCheck = require('@percy/cypress/task');
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
@@ -23,5 +23,13 @@ module.exports = (on, config) => {
|
||||
// whatever you return here becomes the new args
|
||||
return args;
|
||||
}
|
||||
|
||||
if (browser.name === 'chromium') {
|
||||
const newArgs = args.filter(arg => arg !== '--disable-gpu');
|
||||
newArgs.push('--ignore-gpu-blacklist');
|
||||
return newArgs;
|
||||
}
|
||||
});
|
||||
|
||||
on('task', percyHealthCheck);
|
||||
};
|
||||
@@ -10,7 +10,6 @@ export function initCornerstoneToolsAliases() {
|
||||
cy.get('.ToolbarRow > :nth-child(9)').as('resetBtn');
|
||||
cy.get('.ToolbarRow > :nth-child(10)').as('cineBtn');
|
||||
cy.get('.expandableToolMenu').as('moreBtn');
|
||||
cy.get('.PluginSwitch > .toolbar-button').as('twodmprBtn');
|
||||
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
|
||||
}
|
||||
|
||||
@@ -32,3 +31,22 @@ export function initCommonElementsAliases() {
|
||||
'viewportInfoBottomRight'
|
||||
);
|
||||
}
|
||||
|
||||
//Creating aliases for Routes
|
||||
export function initRouteAliases() {
|
||||
cy.server();
|
||||
cy.route('GET', '**/series**').as('getStudySeries');
|
||||
cy.route('GET', '**/studies**').as('getStudies');
|
||||
}
|
||||
|
||||
//Creating aliases for VTK tools buttons
|
||||
export function initVTKToolsAliases() {
|
||||
cy.get('.PluginSwitch > .toolbar-button').as('twodmprBtn');
|
||||
cy.get('.ToolbarRow > :nth-child(2)').as('crosshairsBtn');
|
||||
cy.get('.ToolbarRow > :nth-child(3)').as('wwwcBtn');
|
||||
cy.get('.ToolbarRow > :nth-child(4)').as('rotateBtn');
|
||||
cy.get('.slab-thickness').as('slabSlider');
|
||||
cy.get('.select-ohif').as('modeDropdown');
|
||||
cy.get('.ohif-check-label').as('modeCheckbox');
|
||||
cy.get('.btn-group > .toolbar-button').as('layoutBtn');
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import '@percy/cypress';
|
||||
import { DragSimulator } from '../helpers/DragSimulator.js';
|
||||
import {
|
||||
initCornerstoneToolsAliases,
|
||||
initCommonElementsAliases,
|
||||
initRouteAliases,
|
||||
initVTKToolsAliases,
|
||||
} from './aliases.js';
|
||||
|
||||
// ***********************************************
|
||||
@@ -36,14 +39,46 @@ import {
|
||||
* @param {string} PatientName - Patient name that we would like to search for
|
||||
*/
|
||||
Cypress.Commands.add('openStudy', patientName => {
|
||||
cy.visit('/');
|
||||
cy.openStudyList();
|
||||
cy.get('#patientName').type(patientName);
|
||||
cy.wait('@getStudies');
|
||||
cy.get('#studyListData .studylistStudy', { timeout: 5000 })
|
||||
.contains(patientName)
|
||||
.first()
|
||||
.click({ force: true });
|
||||
});
|
||||
|
||||
cy.get('.studylistStudy > .patientName')
|
||||
.as('patientResult')
|
||||
.then({ timeout: 5000 }, $patientResult => {
|
||||
cy.contains(patientName).click();
|
||||
});
|
||||
/**
|
||||
* Command to search for a modality and open the study.
|
||||
*
|
||||
* @param {string} modality - Modality type that we would like to search for
|
||||
*/
|
||||
Cypress.Commands.add('openStudyModality', modality => {
|
||||
cy.initRouteAliases();
|
||||
cy.visit('/');
|
||||
cy.get('#modalities')
|
||||
.type(modality)
|
||||
.wait(2000);
|
||||
|
||||
cy.get('#studyListData')
|
||||
.contains(modality)
|
||||
.first()
|
||||
.click();
|
||||
});
|
||||
|
||||
/**
|
||||
* Command to wait and check if a new page was loaded
|
||||
*
|
||||
* @param {string} url - part of the expected url. Default value is /viewer/
|
||||
*/
|
||||
Cypress.Commands.add('isPageLoaded', (url = '/viewer/') => {
|
||||
return cy.location('pathname', { timeout: 60000 }).should('include', url);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('openStudyList', patientName => {
|
||||
cy.initRouteAliases();
|
||||
cy.visit('/');
|
||||
cy.wait('@getStudies');
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -103,33 +138,43 @@ Cypress.Commands.add(
|
||||
}
|
||||
);
|
||||
|
||||
Cypress.Commands.add('expectMinimumThumbnails', (seriesToWait = 1) => {
|
||||
cy.get('[data-cy=thumbnail-list]', { timeout: 10000 }).should($itemList => {
|
||||
expect($itemList.length >= seriesToWait).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
//Command to wait DICOM image to load into the viewport
|
||||
Cypress.Commands.add('waitDicomImage', (timeout = 20000) => {
|
||||
cy.window()
|
||||
.its('cornerstone')
|
||||
.then({ timeout }, $cornerstone => {
|
||||
return new Cypress.Promise(resolve => {
|
||||
const onEvent = renderedEvt => {
|
||||
const element = renderedEvt.detail.element;
|
||||
const loaded = cy.isPageLoaded();
|
||||
|
||||
element.removeEventListener('cornerstoneimagerendered', onEvent);
|
||||
$cornerstone.events.removeEventListener(
|
||||
'cornerstoneimagerendered',
|
||||
onEvent
|
||||
if (loaded) {
|
||||
cy.window()
|
||||
.its('cornerstone')
|
||||
.then({ timeout }, $cornerstone => {
|
||||
return new Cypress.Promise(resolve => {
|
||||
const onEvent = renderedEvt => {
|
||||
const element = renderedEvt.detail.element;
|
||||
|
||||
element.removeEventListener('cornerstoneimagerendered', onEvent);
|
||||
$cornerstone.events.removeEventListener(
|
||||
'cornerstoneimagerendered',
|
||||
onEvent
|
||||
);
|
||||
resolve();
|
||||
};
|
||||
const onEnabled = enabledEvt => {
|
||||
const element = enabledEvt.detail.element;
|
||||
|
||||
element.addEventListener('cornerstoneimagerendered', onEvent);
|
||||
};
|
||||
$cornerstone.events.addEventListener(
|
||||
'cornerstoneelementenabled',
|
||||
onEnabled
|
||||
);
|
||||
resolve();
|
||||
};
|
||||
const onEnabled = enabledEvt => {
|
||||
const element = enabledEvt.detail.element;
|
||||
|
||||
element.addEventListener('cornerstoneimagerendered', onEvent);
|
||||
};
|
||||
$cornerstone.events.addEventListener(
|
||||
'cornerstoneelementenabled',
|
||||
onEnabled
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//Command to reset and clear all the changes made to the viewport
|
||||
@@ -182,21 +227,113 @@ Cypress.Commands.add('initCommonElementsAliases', () => {
|
||||
initCommonElementsAliases();
|
||||
});
|
||||
|
||||
//Add measurements in the viewport
|
||||
Cypress.Commands.add('addLengthMeasurement', () => {
|
||||
cy.initCornerstoneToolsAliases();
|
||||
cy.get('@lengthBtn').click();
|
||||
const firstClick = [150, 100];
|
||||
const secondClick = [130, 170];
|
||||
cy.addLine('.cornerstone-canvas', firstClick, secondClick);
|
||||
//Initialize aliases for Routes
|
||||
Cypress.Commands.add('initRouteAliases', () => {
|
||||
initRouteAliases();
|
||||
});
|
||||
|
||||
//Initialize aliases for VTK tools
|
||||
Cypress.Commands.add('initVTKToolsAliases', () => {
|
||||
initVTKToolsAliases();
|
||||
});
|
||||
|
||||
//Add measurements in the viewport
|
||||
Cypress.Commands.add('addAngleMeasurement', () => {
|
||||
cy.initCornerstoneToolsAliases();
|
||||
cy.get('@angleBtn').click();
|
||||
const initPos = [180, 390];
|
||||
const midPos = [300, 410];
|
||||
const finalPos = [180, 450];
|
||||
cy.addAngle('.cornerstone-canvas', initPos, midPos, finalPos);
|
||||
Cypress.Commands.add(
|
||||
'addLengthMeasurement',
|
||||
(firstClick = [150, 100], secondClick = [130, 170]) => {
|
||||
cy.initCornerstoneToolsAliases();
|
||||
cy.get('@lengthBtn').click();
|
||||
cy.addLine('@viewport', firstClick, secondClick);
|
||||
}
|
||||
);
|
||||
|
||||
//Add measurements in the viewport
|
||||
Cypress.Commands.add(
|
||||
'addAngleMeasurement',
|
||||
(initPos = [180, 390], midPos = [300, 410], finalPos = [180, 450]) => {
|
||||
cy.initCornerstoneToolsAliases();
|
||||
cy.get('@angleBtn').click();
|
||||
cy.addAngle('@viewport', initPos, midPos, finalPos);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Tests if element is NOT in viewport, or does not exist in DOM
|
||||
*
|
||||
* @param {string} element - element selector string or alias
|
||||
* @returns
|
||||
*/
|
||||
Cypress.Commands.add('isNotInViewport', element => {
|
||||
cy.get(element, { timeout: 3000 }).should($el => {
|
||||
const bottom = Cypress.$(cy.state('window')).height() - 50;
|
||||
const right = Cypress.$(cy.state('window')).width() - 50;
|
||||
|
||||
// If it's not visible, it's not in the viewport
|
||||
if ($el) {
|
||||
const rect = $el[0].getBoundingClientRect();
|
||||
|
||||
// TODO: support leftOf, above
|
||||
const isBeneath = rect.top >= bottom && rect.bottom >= bottom;
|
||||
const isRightOf = rect.left >= right && rect.right >= right;
|
||||
const isNotInViewport = isBeneath && isRightOf;
|
||||
|
||||
expect(isNotInViewport).to.be.true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests if element is in viewport, or it does exist in DOM
|
||||
*
|
||||
* @param {string} element - element selector string or alias
|
||||
* @returns
|
||||
*/
|
||||
Cypress.Commands.add('isInViewport', element => {
|
||||
cy.get(element, { timeout: 3000 }).should($el => {
|
||||
const bottom = Cypress.$(cy.state('window')).height();
|
||||
const right = Cypress.$(cy.state('window')).width();
|
||||
|
||||
// If it's not visible, it's not in the viewport
|
||||
if ($el) {
|
||||
const rect = $el[0].getBoundingClientRect();
|
||||
|
||||
// TODO: support leftOf, above
|
||||
const isBeneath = rect.top < bottom && rect.bottom < bottom;
|
||||
const isRightOf = rect.left < right && rect.right < right;
|
||||
const isInViewport = isBeneath && isRightOf;
|
||||
|
||||
expect(isInViewport).to.be.true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Percy.io Canvas screenshot workaround
|
||||
*
|
||||
*/
|
||||
Cypress.Commands.add('percyCanvasSnapshot', (name, options = {}) => {
|
||||
function convertCanvas(documentClone) {
|
||||
documentClone
|
||||
.querySelectorAll('canvas')
|
||||
.forEach(selector => canvasToImage(selector));
|
||||
|
||||
return documentClone;
|
||||
}
|
||||
|
||||
function canvasToImage(selectorOrEl) {
|
||||
let canvas =
|
||||
typeof selectorOrEl === 'object'
|
||||
? selectorOrEl
|
||||
: document.querySelector(selectorOrEl);
|
||||
let image = document.createElement('img');
|
||||
let canvasImageBase64 = canvas.toDataURL();
|
||||
|
||||
image.src = canvasImageBase64;
|
||||
image.style = 'max-width: 100%';
|
||||
canvas.setAttribute('data-percy-modified', true);
|
||||
canvas.parentElement.appendChild(image);
|
||||
canvas.style = 'display: none';
|
||||
}
|
||||
|
||||
cy.percySnapshot(name, { ...options, domTransformation: convertCanvas });
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/viewer",
|
||||
"version": "1.6.3",
|
||||
"version": "1.12.1",
|
||||
"description": "OHIF Viewer",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
@@ -27,7 +27,7 @@
|
||||
"dev:viewer": "yarn run dev",
|
||||
"start": "yarn run dev",
|
||||
"test:e2e": "cypress open",
|
||||
"test:e2e:ci": "cypress run",
|
||||
"test:e2e:ci": "percy exec -- cypress run --browser chrome",
|
||||
"test:e2e:dist": "start-server-and-test test:e2e:serve http://localhost:3000 test:e2e:ci",
|
||||
"test:e2e:serve": "serve -n -l 3000 -s dist",
|
||||
"test:unit": "jest --watchAll",
|
||||
@@ -45,14 +45,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@ohif/core": "^1.3.2",
|
||||
"@ohif/core": "^1.5.1",
|
||||
"@ohif/extension-cornerstone": "^2.0.0",
|
||||
"@ohif/extension-dicom-html": "^1.0.0",
|
||||
"@ohif/extension-dicom-microscopy": "^0.50.6",
|
||||
"@ohif/extension-dicom-pdf": "^1.0.0",
|
||||
"@ohif/extension-vtk": "^0.52.8",
|
||||
"@ohif/i18n": "^0.50.5",
|
||||
"@ohif/ui": "^0.53.4",
|
||||
"@ohif/extension-vtk": "^0.52.18",
|
||||
"@ohif/i18n": "^0.51.0",
|
||||
"@ohif/ui": "^0.56.0",
|
||||
"@tanem/react-nprogress": "^1.1.25",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.2.1",
|
||||
@@ -82,7 +82,7 @@
|
||||
"redux-oidc": "3.1.x",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"reselect": "^4.0.0",
|
||||
"vtk.js": "^11.0.1"
|
||||
"vtk.js": "^11.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cypress": "^3.3.1",
|
||||
|
||||
@@ -27,6 +27,7 @@ import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { getActiveContexts } from './store/layout/selectors.js';
|
||||
import i18n from '@ohif/i18n';
|
||||
import store from './store';
|
||||
import { SnackbarProvider } from '@ohif/ui';
|
||||
|
||||
// Contexts
|
||||
import WhiteLabellingContext from './context/WhiteLabellingContext';
|
||||
@@ -99,7 +100,9 @@ class App extends Component {
|
||||
<WhiteLabellingContext.Provider
|
||||
value={this.props.whiteLabelling}
|
||||
>
|
||||
<OHIFStandaloneViewer userManager={userManager} />
|
||||
<SnackbarProvider>
|
||||
<OHIFStandaloneViewer userManager={userManager} />
|
||||
</SnackbarProvider>
|
||||
</WhiteLabellingContext.Provider>
|
||||
</Router>
|
||||
</UserManagerContext.Provider>
|
||||
@@ -116,7 +119,9 @@ class App extends Component {
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Router basename={this.props.routerBasename}>
|
||||
<WhiteLabellingContext.Provider value={this.props.whiteLabelling}>
|
||||
<OHIFStandaloneViewer />
|
||||
<SnackbarProvider>
|
||||
<OHIFStandaloneViewer />
|
||||
</SnackbarProvider>
|
||||
</WhiteLabellingContext.Provider>
|
||||
</Router>
|
||||
</I18nextProvider>
|
||||
|
||||
@@ -96,3 +96,27 @@
|
||||
color: #91b9cd;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.header-versionInfo {
|
||||
display: inline-block;
|
||||
color: black;
|
||||
background: #9ccef9;
|
||||
padding: 0px 8px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.header-versionInfoHome {
|
||||
display: block;
|
||||
color: black;
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 0;
|
||||
background: #9ccef9;
|
||||
padding: 0px 8px;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -101,6 +101,14 @@ class Header extends Component {
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<span
|
||||
className={`${
|
||||
this.props.home ? 'header-versionInfoHome' : 'header-versionInfo'
|
||||
}`}
|
||||
>
|
||||
v{process.env.VERSION_NUMBER}
|
||||
</span>
|
||||
|
||||
{this.props.children}
|
||||
|
||||
{showStudyList && !this.props.home && (
|
||||
|
||||
@@ -32,6 +32,11 @@ const ViewportGrid = function(props) {
|
||||
|
||||
const ViewportPanes = layout.viewports.map((layout, viewportIndex) => {
|
||||
const displaySet = viewportData[viewportIndex];
|
||||
|
||||
if (!displaySet) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = {
|
||||
displaySet,
|
||||
studies,
|
||||
@@ -108,6 +113,9 @@ ViewportGrid.propTypes = {
|
||||
setViewportData: PropTypes.func.isRequired,
|
||||
studies: PropTypes.array,
|
||||
children: PropTypes.node,
|
||||
defaultPlugin: PropTypes.string,
|
||||
numRows: PropTypes.number.isRequired,
|
||||
numColumns: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
ViewportGrid.defaultProps = {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
// import OHIF from '@ohif/core';
|
||||
import OHIF from '@ohif/core';
|
||||
import React from 'react';
|
||||
import PluginSwitch from './PluginSwitch.js';
|
||||
import { commandsManager } from './../App.js';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
// const { setLayout } = OHIF.redux.actions;
|
||||
const { setLayout } = OHIF.redux.actions;
|
||||
|
||||
const ConnectedPluginSwitch = props => {
|
||||
return <PluginSwitch {...props} />;
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { activeViewportIndex, layout, viewportSpecificData } = state.viewports;
|
||||
@@ -15,13 +20,13 @@ const mapStateToProps = state => {
|
||||
};
|
||||
};
|
||||
|
||||
// const mapDispatchToProps = dispatch => {
|
||||
// return {
|
||||
// setLayout: data => {
|
||||
// dispatch(setLayout(data));
|
||||
// }
|
||||
// };
|
||||
// };
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setLayout: data => {
|
||||
dispatch(setLayout(data));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/*function setSingleLayoutData(originalArray, viewportIndex, data) {
|
||||
const viewports = originalArray.slice();
|
||||
@@ -33,23 +38,35 @@ const mapStateToProps = state => {
|
||||
}*/
|
||||
|
||||
const mergeProps = (propsFromState, propsFromDispatch, ownProps) => {
|
||||
//const { activeViewportIndex, layout } = propsFromState;
|
||||
//const { setLayout } = propsFromDispatch;
|
||||
const { activeViewportIndex, viewportSpecificData } = propsFromState;
|
||||
const { studies } = ownProps;
|
||||
const { setLayout } = propsFromDispatch;
|
||||
|
||||
// TODO: Do not display certain options if the current display set
|
||||
// cannot be displayed using these view types
|
||||
const mpr = () => {
|
||||
commandsManager.runCommand('mpr2d');
|
||||
};
|
||||
|
||||
const exitMpr = () => {
|
||||
const layout = {
|
||||
numRows: 1,
|
||||
numColumns: 1,
|
||||
viewports: [{ plugin: 'cornerstone' }],
|
||||
};
|
||||
|
||||
setLayout(layout);
|
||||
};
|
||||
|
||||
return {
|
||||
mpr,
|
||||
exitMpr,
|
||||
activeViewportIndex,
|
||||
viewportSpecificData,
|
||||
studies,
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedPluginSwitch = connect(
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
null, // mapDispatchToProps
|
||||
mapDispatchToProps,
|
||||
mergeProps
|
||||
)(PluginSwitch);
|
||||
|
||||
export default ConnectedPluginSwitch;
|
||||
)(ConnectedPluginSwitch);
|
||||
@@ -6,17 +6,101 @@ import './PluginSwitch.css';
|
||||
class PluginSwitch extends Component {
|
||||
static propTypes = {
|
||||
mpr: PropTypes.func,
|
||||
activeViewportIndex: PropTypes.number,
|
||||
viewportSpecificData: PropTypes.object,
|
||||
studies: PropTypes.array,
|
||||
exitMpr: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {};
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isPlugSwitchOn: false,
|
||||
label: '2D MPR',
|
||||
icon: 'cube',
|
||||
};
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
if (this.state.isPlugSwitchOn) {
|
||||
this.setState({
|
||||
isPlugSwitchOn: false,
|
||||
label: '2D MPR',
|
||||
icon: 'cube',
|
||||
});
|
||||
this.props.exitMpr();
|
||||
} else {
|
||||
this.setState({
|
||||
isPlugSwitchOn: true,
|
||||
label: 'Exit 2D MPR',
|
||||
icon: 'times',
|
||||
});
|
||||
this.props.mpr();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { label, icon } = this.state;
|
||||
|
||||
// Render exit mpr if switched on, otherwise check if mpr button should be displayed.
|
||||
const shouldRender =
|
||||
this.state.isPlugSwitchOn || _shouldRenderMpr2DButton.call(this);
|
||||
|
||||
return (
|
||||
<div className="PluginSwitch">
|
||||
<ToolbarButton label="2D MPR" icon="cube" onClick={this.props.mpr} />
|
||||
</div>
|
||||
<>
|
||||
{shouldRender && (
|
||||
<div className="PluginSwitch">
|
||||
<ToolbarButton
|
||||
label={label}
|
||||
icon={icon}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function _shouldRenderMpr2DButton() {
|
||||
const { viewportSpecificData, studies, activeViewportIndex } = this.props;
|
||||
|
||||
if (!viewportSpecificData[activeViewportIndex]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { displaySetInstanceUid, studyInstanceUid } = viewportSpecificData[
|
||||
activeViewportIndex
|
||||
];
|
||||
|
||||
const displaySet = _getDisplaySet(
|
||||
studies,
|
||||
studyInstanceUid,
|
||||
displaySetInstanceUid
|
||||
);
|
||||
|
||||
if (!displaySet) {
|
||||
return;
|
||||
}
|
||||
|
||||
return displaySet.isReconstructable;
|
||||
}
|
||||
|
||||
function _getDisplaySet(studies, studyInstanceUid, displaySetInstanceUid) {
|
||||
const study = studies.find(
|
||||
study => study.studyInstanceUid === studyInstanceUid
|
||||
);
|
||||
|
||||
if (!study) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displaySet = study.displaySets.find(set => {
|
||||
return set.displaySetInstanceUid === displaySetInstanceUid;
|
||||
});
|
||||
|
||||
return displaySet;
|
||||
}
|
||||
|
||||
export default PluginSwitch;
|
||||
@@ -25,6 +25,7 @@ class ToolbarRow extends Component {
|
||||
selectedRightSidePanel: PropTypes.string.isRequired,
|
||||
handleSidePanelChange: PropTypes.func,
|
||||
activeContexts: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
studies: PropTypes.array,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@@ -128,7 +129,7 @@ class ToolbarRow extends Component {
|
||||
</div>
|
||||
{buttonComponents}
|
||||
<ConnectedLayoutButton />
|
||||
<ConnectedPluginSwitch />
|
||||
<ConnectedPluginSwitch studies={this.props.studies} />
|
||||
<div
|
||||
className="pull-right m-t-1 rm-x-1"
|
||||
style={{ marginLeft: 'auto' }}
|
||||
@@ -229,7 +230,6 @@ function _getButtonComponents(toolbarButtons, activeButtons) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A handy way for us to handle different button types. IE. firing commands for
|
||||
* buttons, or initiation built in behavior.
|
||||
|
||||
@@ -279,6 +279,7 @@ class Viewer extends Component {
|
||||
|
||||
this.setState(updatedState);
|
||||
}}
|
||||
studies={this.props.studies}
|
||||
/>
|
||||
|
||||
{/*<ConnectedStudyLoadingMonitor studies={this.props.studies} />*/}
|
||||
|
||||
@@ -22,8 +22,6 @@ class ViewerMain extends Component {
|
||||
this.state = {
|
||||
displaySets: [],
|
||||
};
|
||||
|
||||
this.cachedViewportData = {};
|
||||
}
|
||||
|
||||
getDisplaySets(studies) {
|
||||
@@ -65,63 +63,75 @@ class ViewerMain extends Component {
|
||||
this.setState({
|
||||
displaySets,
|
||||
});
|
||||
|
||||
this.fillEmptyViewportPanes();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.studies !== prevProps.studies) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
getViewportData = () => {
|
||||
const viewportData = [];
|
||||
fillEmptyViewportPanes = () => {
|
||||
const dirtyViewportPanes = [];
|
||||
const { layout, viewportSpecificData } = this.props;
|
||||
const { displaySets } = this.state;
|
||||
|
||||
for (
|
||||
let viewportIndex = 0;
|
||||
viewportIndex < layout.viewports.length;
|
||||
viewportIndex++
|
||||
) {
|
||||
let displaySet = viewportSpecificData[viewportIndex];
|
||||
|
||||
// Use the cached display set in viewport if the new one is empty
|
||||
if (displaySet && !displaySet.displaySetInstanceUid) {
|
||||
displaySet = this.cachedViewportData[viewportIndex];
|
||||
}
|
||||
|
||||
if (
|
||||
displaySet &&
|
||||
displaySet.studyInstanceUid &&
|
||||
displaySet.displaySetInstanceUid
|
||||
) {
|
||||
// Get missing fields from original display set
|
||||
const originalDisplaySet = this.findDisplaySet(
|
||||
this.props.studies,
|
||||
displaySet.studyInstanceUid,
|
||||
displaySet.displaySetInstanceUid
|
||||
);
|
||||
viewportData.push(Object.assign({}, originalDisplaySet, displaySet));
|
||||
} else {
|
||||
// If the viewport is empty, get one available in study
|
||||
const { displaySets } = this.state;
|
||||
displaySet = displaySets.find(
|
||||
ds =>
|
||||
!viewportData.some(
|
||||
v => v.displaySetInstanceUid === ds.displaySetInstanceUid
|
||||
)
|
||||
);
|
||||
viewportData.push(Object.assign({}, displaySet));
|
||||
}
|
||||
if (!displaySets || !displaySets.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cachedViewportData = viewportData;
|
||||
for (let i = 0; i < layout.viewports.length; i++) {
|
||||
const viewportPane = viewportSpecificData[i];
|
||||
const isNonEmptyViewport =
|
||||
viewportPane &&
|
||||
viewportPane.studyInstanceUid &&
|
||||
viewportPane.displaySetInstanceUid;
|
||||
|
||||
return viewportData;
|
||||
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 = ({
|
||||
@@ -139,12 +149,17 @@ class ViewerMain extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { viewportSpecificData } = this.props;
|
||||
const viewportData = viewportSpecificData
|
||||
? Object.values(viewportSpecificData)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="ViewerMain">
|
||||
{this.state.displaySets.length && (
|
||||
<ConnectedViewportGrid
|
||||
studies={this.props.studies}
|
||||
viewportData={this.getViewportData()}
|
||||
viewportData={viewportData}
|
||||
setViewportData={this.setViewportData}
|
||||
>
|
||||
{/* Children to add to each viewport that support children */}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
export default function usePrevious(value) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
}, [value]);
|
||||
|
||||
return ref.current;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, { useContext } from 'react';
|
||||
import GoogleCloudApi from '../googleCloud/api/GoogleCloudApi';
|
||||
import usePrevious from './usePrevious';
|
||||
|
||||
import * as GoogleCloudUtilServers from '../googleCloud/utils/getServers';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import isEqual from 'lodash.isequal';
|
||||
|
||||
// Contexts
|
||||
import AppContext from '../context/AppContext';
|
||||
@@ -32,28 +34,68 @@ const getServers = (appConfig, project, location, dataset, dicomStore) => {
|
||||
wadoRoot: pathUrl,
|
||||
};
|
||||
servers = GoogleCloudUtilServers.getServers(data, dicomStore);
|
||||
if (!isValidServer(servers[0], appConfig)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return servers;
|
||||
};
|
||||
|
||||
const updateServer = (
|
||||
const isValidServer = (server, appConfig) => {
|
||||
if (appConfig.enableGoogleCloudAdapter) {
|
||||
return GoogleCloudUtilServers.isValidServer(server);
|
||||
}
|
||||
|
||||
return !!server;
|
||||
};
|
||||
|
||||
const setServers = (dispatch, servers) => {
|
||||
const action = {
|
||||
type: 'SET_SERVERS',
|
||||
servers,
|
||||
};
|
||||
dispatch(action);
|
||||
};
|
||||
|
||||
const useServerFromUrl = (
|
||||
servers = [],
|
||||
previousServers,
|
||||
activeServer,
|
||||
urlBasedServers,
|
||||
appConfig,
|
||||
dispatch,
|
||||
project,
|
||||
location,
|
||||
dataset,
|
||||
dicomStore
|
||||
) => {
|
||||
const servers = getServers(appConfig, project, location, dataset, dicomStore);
|
||||
|
||||
if (servers && servers.length) {
|
||||
const action = {
|
||||
type: 'SET_SERVERS',
|
||||
servers,
|
||||
};
|
||||
dispatch(action);
|
||||
// update state from url available only when gcloud on
|
||||
if (!appConfig.enableGoogleCloudAdapter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const serverHasChanged = previousServers !== servers && previousServers;
|
||||
|
||||
// do not update from url. use state instead.
|
||||
if (serverHasChanged) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if no valid urlbased servers
|
||||
if (!urlBasedServers || !urlBasedServers.length) {
|
||||
return false;
|
||||
} else if (!servers.length || !activeServer) {
|
||||
// no current valid server
|
||||
return true;
|
||||
}
|
||||
|
||||
const newServer = urlBasedServers[0];
|
||||
|
||||
let exists = servers.some(
|
||||
GoogleCloudUtilServers.isEqualServer.bind(undefined, newServer)
|
||||
);
|
||||
|
||||
return !exists;
|
||||
};
|
||||
|
||||
export default function useServer({
|
||||
@@ -64,14 +106,29 @@ export default function useServer({
|
||||
} = {}) {
|
||||
// Hooks
|
||||
const servers = useSelector(state => state && state.servers);
|
||||
const previousServers = usePrevious(servers);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { appConfig = {} } = useContext(AppContext);
|
||||
|
||||
const server = getActiveServer(servers);
|
||||
const activeServer = getActiveServer(servers);
|
||||
const urlBasedServers =
|
||||
getServers(appConfig, project, location, dataset, dicomStore) || [];
|
||||
const shouldUpdateServer = useServerFromUrl(
|
||||
servers.servers,
|
||||
previousServers,
|
||||
activeServer,
|
||||
urlBasedServers,
|
||||
appConfig,
|
||||
project,
|
||||
location,
|
||||
dataset,
|
||||
dicomStore
|
||||
);
|
||||
|
||||
if (!server) {
|
||||
updateServer(appConfig, dispatch, project, location, dataset, dicomStore);
|
||||
} else {
|
||||
return server;
|
||||
if (shouldUpdateServer) {
|
||||
setServers(dispatch, urlBasedServers);
|
||||
} else if (isValidServer(activeServer, appConfig)) {
|
||||
return activeServer;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,8 @@ class DicomStorePickerModal extends Component {
|
||||
handleEvent = data => {
|
||||
const servers = GoogleCloudUtilServers.getServers(data, data.dicomstore);
|
||||
this.props.setServers(servers);
|
||||
// Force auto close
|
||||
this.props.onClose();
|
||||
};
|
||||
|
||||
render() {
|
||||
|
||||
@@ -29,6 +29,10 @@ class GoogleCloudApi {
|
||||
);
|
||||
}
|
||||
|
||||
getUrlPath(project, location, dataset, dicomStore) {
|
||||
`/projects/${project}/locations/${location}/datasets/${dataset}/dicomStores/${dicomStore}`;
|
||||
}
|
||||
|
||||
async doRequest(urlStr, config = {}, params = {}) {
|
||||
const url = new URL(urlStr);
|
||||
let data = null;
|
||||
|
||||
@@ -29,4 +29,31 @@ const getServers = (data, name) => {
|
||||
];
|
||||
};
|
||||
|
||||
export { getServers };
|
||||
const isValidServer = server => {
|
||||
return (
|
||||
server &&
|
||||
!!server.dataset &&
|
||||
!!server.dicomStore &&
|
||||
!!server.location &&
|
||||
!!server.project
|
||||
);
|
||||
};
|
||||
|
||||
const isEqualServer = (server = {}, toCompare = {}) => {
|
||||
const serverLength = Object.keys(server).length;
|
||||
const toCompareLength = Object.keys(toCompare).length;
|
||||
|
||||
if (!serverLength || !toCompareLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
server.dataset === toCompare.dataset &&
|
||||
server.dataset === toCompare.dataset &&
|
||||
server.dicomStore === toCompare.dicomStore &&
|
||||
server.location === toCompare.location &&
|
||||
server.project === toCompare.project
|
||||
);
|
||||
};
|
||||
|
||||
export { getServers, isValidServer, isEqualServer };
|
||||
@@ -65,6 +65,19 @@ const ROUTES_DEF = {
|
||||
return !!appConfig.enableGoogleCloudAdapter;
|
||||
},
|
||||
},
|
||||
list: {
|
||||
path:
|
||||
'/projects/:project/locations/:location/datasets/:dataset/dicomStores/:dicomStore',
|
||||
component: StudyListRouting,
|
||||
condition: appConfig => {
|
||||
const showList =
|
||||
appConfig.showStudyList !== undefined
|
||||
? appConfig.showStudyList
|
||||
: true;
|
||||
|
||||
return showList && !!appConfig.enableGoogleCloudAdapter;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -92,23 +105,33 @@ const getRoutes = appConfig => {
|
||||
return routes;
|
||||
};
|
||||
|
||||
const parsePath = (path, server, params) => {
|
||||
let _path = path;
|
||||
const _paramsCopy = Object.assign({}, server, params);
|
||||
|
||||
for (let key in _paramsCopy) {
|
||||
_path = UrlUtil.paramString.replaceParam(_path, key, _paramsCopy[key]);
|
||||
}
|
||||
|
||||
return _path;
|
||||
};
|
||||
|
||||
const parseViewerPath = (appConfig = {}, server = {}, params) => {
|
||||
let viewerPath = ROUTES_DEF.default.viewer.path;
|
||||
if (appConfig.enableGoogleCloudAdapter) {
|
||||
viewerPath = ROUTES_DEF.gcloud.viewer.path;
|
||||
}
|
||||
|
||||
const _paramsCopy = Object.assign({}, server, params);
|
||||
|
||||
for (let key in _paramsCopy) {
|
||||
viewerPath = UrlUtil.paramString.replaceParam(
|
||||
viewerPath,
|
||||
key,
|
||||
_paramsCopy[key]
|
||||
);
|
||||
}
|
||||
|
||||
return viewerPath;
|
||||
return parsePath(viewerPath, server, params);
|
||||
};
|
||||
|
||||
export { getRoutes, parseViewerPath, reload };
|
||||
const parseStudyListPath = (appConfig = {}, server = {}, params) => {
|
||||
let studyListPath = ROUTES_DEF.default.list.path;
|
||||
if (appConfig.enableGoogleCloudAdapter) {
|
||||
studyListPath = ROUTES_DEF.gcloud.list.path || studyListPath;
|
||||
}
|
||||
|
||||
return parsePath(studyListPath, server, params);
|
||||
};
|
||||
|
||||
export { getRoutes, parseViewerPath, parseStudyListPath, reload };
|
||||
@@ -2,17 +2,26 @@ import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import ConnectedStudyList from './ConnectedStudyList';
|
||||
|
||||
import useServer from '../customHooks/useServer';
|
||||
import OHIF from '@ohif/core';
|
||||
const { urlUtil: UrlUtil } = OHIF.utils;
|
||||
|
||||
// Contexts
|
||||
import AppContext from '../context/AppContext';
|
||||
|
||||
function StudyListRouting({ location }) {
|
||||
function StudyListRouting({ match: routeMatch, location: routeLocation }) {
|
||||
const {
|
||||
project,
|
||||
location,
|
||||
dataset,
|
||||
dicomStore,
|
||||
studyInstanceUids,
|
||||
seriesInstanceUids,
|
||||
} = routeMatch.params;
|
||||
const server = useServer({ project, location, dataset, dicomStore });
|
||||
const { appConfig = {} } = useContext(AppContext);
|
||||
|
||||
const filters = UrlUtil.queryString.getQueryFilters(location);
|
||||
const filters = UrlUtil.queryString.getQueryFilters(routeLocation);
|
||||
|
||||
let studyListFunctionsEnabled = false;
|
||||
if (appConfig.studyListFunctionsEnabled) {
|
||||
|
||||
@@ -8,10 +8,12 @@ import { StudyList } from '@ohif/ui';
|
||||
import ConnectedHeader from '../connectedComponents/ConnectedHeader.js';
|
||||
import * as RoutesUtil from '../routes/routesUtil';
|
||||
import moment from 'moment';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import ConnectedDicomFilesUploader from '../googleCloud/ConnectedDicomFilesUploader';
|
||||
import ConnectedDicomStorePicker from '../googleCloud/ConnectedDicomStorePicker';
|
||||
import filesToStudies from '../lib/filesToStudies.js';
|
||||
|
||||
const { urlUtil: UrlUtil } = OHIF.utils;
|
||||
// Contexts
|
||||
import UserManagerContext from '../context/UserManagerContext';
|
||||
import WhiteLabellingContext from '../context/WhiteLabellingContext';
|
||||
@@ -20,8 +22,9 @@ import AppContext from '../context/AppContext';
|
||||
class StudyListWithData extends Component {
|
||||
static contextType = AppContext;
|
||||
state = {
|
||||
searchData: {},
|
||||
searchOutdated: true,
|
||||
studies: [],
|
||||
searchingStudies: false,
|
||||
error: null,
|
||||
modalComponentId: null,
|
||||
};
|
||||
@@ -62,6 +65,7 @@ class StudyListWithData extends Component {
|
||||
if (!this.props.server && appConfig.enableGoogleCloudAdapter) {
|
||||
this.setState({
|
||||
modalComponentId: 'DicomStorePicker',
|
||||
searchOutdated: false,
|
||||
});
|
||||
} else {
|
||||
this.searchForStudies({
|
||||
@@ -71,21 +75,33 @@ class StudyListWithData extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!this.state.searchData && !this.state.studies) {
|
||||
this.searchForStudies();
|
||||
}
|
||||
if (this.props.server !== prevProps.server) {
|
||||
this.setState({
|
||||
modalComponentId: null,
|
||||
searchData: null,
|
||||
studies: null,
|
||||
});
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const hasNewServer = !isEqual(this.props.server, prevProps.server);
|
||||
|
||||
const { searchOutdated, searchingStudies } = this.state;
|
||||
|
||||
if (!searchingStudies) {
|
||||
if (hasNewServer) {
|
||||
const { appConfig = {} } = this.context;
|
||||
|
||||
const newState = {
|
||||
searchOutdated: true,
|
||||
studies: null,
|
||||
};
|
||||
if (appConfig.enableGoogleCloudAdapter) {
|
||||
newState.modalComponentId = null;
|
||||
}
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
if (searchOutdated) {
|
||||
this.searchForStudies();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
searchForStudies = (searchData = StudyListWithData.defaultSearchData) => {
|
||||
const { server } = this.props;
|
||||
const { server = {} } = this.props;
|
||||
const filter = {
|
||||
patientId: searchData.patientId,
|
||||
patientName: searchData.patientName,
|
||||
@@ -105,7 +121,9 @@ class StudyListWithData extends Component {
|
||||
// TODO: add sorting
|
||||
const promise = OHIF.studies.searchStudies(server, filter);
|
||||
|
||||
// Render the viewer when the data is ready
|
||||
this.setState({
|
||||
searchingStudies: true,
|
||||
});
|
||||
promise
|
||||
.then(studies => {
|
||||
if (!studies) {
|
||||
@@ -150,11 +168,15 @@ class StudyListWithData extends Component {
|
||||
|
||||
this.setState({
|
||||
studies: sortedStudies,
|
||||
searchingStudies: false,
|
||||
searchOutdated: false,
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({
|
||||
error: true,
|
||||
searchingStudies: false,
|
||||
searchOutdated: false,
|
||||
});
|
||||
|
||||
throw new Error(error);
|
||||
@@ -181,9 +203,27 @@ class StudyListWithData extends Component {
|
||||
const viewerPath = RoutesUtil.parseViewerPath(appConfig, server, {
|
||||
studyInstanceUids: studyInstanceUID,
|
||||
});
|
||||
this.props.history.push(viewerPath);
|
||||
|
||||
if (UrlUtil.paramString.isValidPath(viewerPath)) {
|
||||
this.props.history.push(viewerPath);
|
||||
}
|
||||
};
|
||||
|
||||
updateURL(modalOpened) {
|
||||
if (!modalOpened) {
|
||||
const { appConfig = {} } = this.context;
|
||||
const { server } = this.props;
|
||||
const listPath = RoutesUtil.parseStudyListPath(appConfig, server);
|
||||
|
||||
if (UrlUtil.paramString.isValidPath(listPath)) {
|
||||
const { location = {} } = this.props.history;
|
||||
if (location.pathname !== listPath) {
|
||||
this.props.history.replace(listPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSearch = searchData => {
|
||||
this.searchForStudies(searchData);
|
||||
};
|
||||
@@ -208,17 +248,18 @@ class StudyListWithData extends Component {
|
||||
|
||||
if (this.state.error) {
|
||||
return <div>Error: {JSON.stringify(this.state.error)}</div>;
|
||||
} else if (this.state.studies === null && !this.state.modalComponentId) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
let healthCareApiButtons = null;
|
||||
let healthCareApiWindows = null;
|
||||
|
||||
if (appConfig.enableGoogleCloudAdapter) {
|
||||
const modalOpened = this.state.modalComponentId === 'DicomStorePicker';
|
||||
this.updateURL(modalOpened);
|
||||
|
||||
healthCareApiWindows = (
|
||||
<ConnectedDicomStorePicker
|
||||
isOpen={this.state.modalComponentId === 'DicomStorePicker'}
|
||||
isOpen={modalOpened}
|
||||
onClose={this.closeModals}
|
||||
/>
|
||||
);
|
||||
@@ -240,8 +281,9 @@ class StudyListWithData extends Component {
|
||||
|
||||
const studyList = (
|
||||
<div className="paginationArea">
|
||||
{this.state.studies ? (
|
||||
{this.state.studies || this.state.searchingStudies ? (
|
||||
<StudyList
|
||||
loading={this.state.searchingStudies}
|
||||
studies={this.state.studies}
|
||||
studyListFunctionsEnabled={this.props.studyListFunctionsEnabled}
|
||||
onImport={this.onImport}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* Sizes */
|
||||
|
||||
:root {
|
||||
/* Sizes */
|
||||
|
||||
--top-bar-height: 40px;
|
||||
--top-bar-expanded-height: 160px;
|
||||
--toolbar-height: 78px;
|
||||
@@ -9,19 +9,26 @@
|
||||
--right-sidepanel-menu-width: 323px;
|
||||
--study-list-padding: 8%;
|
||||
--study-list-padding-medium-screen: 10px;
|
||||
}
|
||||
--snackbar-size: 344px;
|
||||
|
||||
/* Transitions */
|
||||
/* Transitions */
|
||||
|
||||
:root {
|
||||
--transition-duration: 0.3s;
|
||||
--transition-effect: ease;
|
||||
--sidepanel-transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Thicknesses */
|
||||
/* Thicknesses */
|
||||
|
||||
:root {
|
||||
--viewport-border-thickness: 1px;
|
||||
--ui-border-thickness: 1px;
|
||||
|
||||
/** Colors **/
|
||||
|
||||
--snackbar-success: rgba(94, 164, 0, 0.9);
|
||||
--snackbar-error: rgba(236, 61, 61, 0.9);
|
||||
--snackbar-warning: rgba(235, 173, 23, 0.9);
|
||||
--snackbar-info: rgba(54, 156, 199, 0.9);
|
||||
|
||||
/** z-Index **/
|
||||
--snackbar-zIndex: 10;
|
||||
}
|
||||
Reference in new issue
Block a user