Compare commits
93
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
817d43ee62 | ||
|
|
f396b30166 | ||
|
|
0bb3eeb068 | ||
|
|
1df21a9e07 | ||
|
|
fd1a9e2abe | ||
|
|
e34a057263 | ||
|
|
27e02825a4 | ||
|
|
8fd3af1e13 | ||
|
|
1334ba8eb0 | ||
|
|
a870b3cc60 | ||
|
|
52687a7113 | ||
|
|
421de23ba3 | ||
|
|
adc8d9ea0f | ||
|
|
f65639c2b0 | ||
|
|
cdd75bda2a | ||
|
|
d92fe44d3c | ||
|
|
d90c2ac024 | ||
|
|
7ce5e1d2d3 | ||
|
|
a3bf47f9bf | ||
|
|
81f79a26ad | ||
|
|
60b4b5d7cf | ||
|
|
69c54d4f49 | ||
|
|
0f8868a232 | ||
|
|
1bbfee4087 | ||
|
|
33c7b59611 | ||
|
|
b17f753e62 | ||
|
|
c77a2ef5c9 | ||
|
|
348edd4717 | ||
|
|
407060658a | ||
|
|
fc00f9d505 | ||
|
|
bc3d080672 | ||
|
|
09353354c3 | ||
|
|
0a12320083 | ||
|
|
813ee5ed4d | ||
|
|
6085b5f733 | ||
|
|
ffe12789dd | ||
|
|
0b68429dfb | ||
|
|
b747ea5144 | ||
|
|
caadf089df | ||
|
|
e0fc69c181 | ||
|
|
2b9765304f | ||
|
|
57730483e9 | ||
|
|
731b64e321 | ||
|
|
e97a09b0f4 | ||
|
|
f63d8a73d8 | ||
|
|
f10adf82f4 | ||
|
|
6174109d5a | ||
|
|
244e822afc | ||
|
|
cbdeeae597 | ||
|
|
7f6de72e8e | ||
|
|
1f514e5f1b | ||
|
|
f3fed511b9 | ||
|
|
31796d272c | ||
|
|
b41e6f93f3 | ||
|
|
882a45d398 | ||
|
|
fb0cd9355f | ||
|
|
2b12193020 | ||
|
|
9a626a2064 | ||
|
|
903777044a | ||
|
|
5e4725860d | ||
|
|
670470119f | ||
|
|
0d4f90f3cd | ||
|
|
43dd87c2c1 | ||
|
|
98f8583fba | ||
|
|
97a9a505ca | ||
|
|
bb74f3a761 | ||
|
|
aa33397fe6 | ||
|
|
8ae731fe4c | ||
|
|
add3416e11 | ||
|
|
136969bf18 | ||
|
|
e3e039ad47 | ||
|
|
0797eff0b7 | ||
|
|
55a5f04923 | ||
|
|
ed9e4d6751 | ||
|
|
b6119f8e29 | ||
|
|
2e38f4b2e4 | ||
|
|
e03abc45f5 | ||
|
|
f03d64ce39 | ||
|
|
e895bfb1aa | ||
|
|
17358c3f4b | ||
|
|
ec7337e801 | ||
|
|
538ea2516d | ||
|
|
fe687cd2f9 | ||
|
|
bdb9af5c4b | ||
|
|
db0f751a68 | ||
|
|
08fdb10f6d | ||
|
|
95c18b4986 | ||
|
|
e684558783 | ||
|
|
9104e7be50 | ||
|
|
81ad25b39f | ||
|
|
54d2cde6da | ||
|
|
9edc366ff7 | ||
|
|
79c10bb424 |
No files matched your search
@@ -62,7 +62,7 @@ echo "Done generating documentation output"
|
||||
echo 'STARTING PUBLISH'
|
||||
|
||||
# WILL ALWAYS FAIL IF INITIATED FROM PR BRANCH
|
||||
./node_modules/.bin/gh-pages \
|
||||
npx gh-pages \
|
||||
--silent \
|
||||
--repo https://$GITHUB_TOKEN@github.com/OHIF/Viewers.git \
|
||||
--message 'Autogenerated Message: [ci skip]' \
|
||||
|
||||
+228
-245
@@ -14,52 +14,53 @@ version: 2.1
|
||||
orbs:
|
||||
codecov: codecov/codecov@1.0.5
|
||||
cypress: cypress-io/cypress@1.11.0
|
||||
executors:
|
||||
# Custom executor to override Cypress config
|
||||
deploy-to-prod-executor:
|
||||
docker:
|
||||
- image: 'cypress/browsers:node10.16.0-chrome77'
|
||||
environment:
|
||||
CYPRESS_BASE_URL: https://ohif-staging.netlify.com/
|
||||
|
||||
defaults: &defaults
|
||||
docker:
|
||||
- image: circleci/node:12.9.1
|
||||
environment:
|
||||
TERM: xterm # Enable colors in term
|
||||
QUICK_BUILD: true
|
||||
working_directory: ~/repo
|
||||
|
||||
jobs:
|
||||
###
|
||||
# Workflow: PR_CHECKS
|
||||
###
|
||||
UNIT_TESTS:
|
||||
<<: *defaults
|
||||
steps:
|
||||
# Update yarn
|
||||
- run: yarn -v
|
||||
#- run: rm -rf ~/.yarn && npm i -g yarn && yarn -v
|
||||
|
||||
# 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
|
||||
|
||||
- save_cache:
|
||||
name: Save Yarn Package Cache
|
||||
paths:
|
||||
- ~/.cache ## Cache yarn and Cypress
|
||||
key: yarn-packages-{{ checksum "yarn.lock" }}
|
||||
|
||||
# RUN TESTS
|
||||
- run:
|
||||
name: 'JavaScript Test Suite'
|
||||
command: yarn run test:unit:ci
|
||||
|
||||
# PLATFORM/VIEWER
|
||||
- run:
|
||||
name: 'VIEWER: Combine report output'
|
||||
@@ -73,7 +74,6 @@ jobs:
|
||||
- codecov/upload:
|
||||
file: '/home/circleci/repo/platform/viewer/coverage/reports'
|
||||
flags: 'viewer'
|
||||
|
||||
# PLATFORM/CORE
|
||||
- run:
|
||||
name: 'CORE: Combine report output'
|
||||
@@ -88,145 +88,9 @@ jobs:
|
||||
file: '/home/circleci/repo/platform/core/coverage/reports'
|
||||
flags: 'core'
|
||||
|
||||
UNIT_TESTS_PERSIST_WORKSPACE:
|
||||
<<: *defaults
|
||||
steps:
|
||||
# Update yarn
|
||||
#- run: rm -rf ~/.yarn && npm i -g yarn && yarn -v
|
||||
|
||||
# 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
|
||||
|
||||
- save_cache:
|
||||
name: Save Yarn Package Cache
|
||||
paths:
|
||||
- ~/.cache ## Cache yarn and Cypress
|
||||
key: yarn-packages-{{ checksum "yarn.lock" }}
|
||||
|
||||
# RUN TESTS
|
||||
- run:
|
||||
name: 'JavaScript Test Suite'
|
||||
command: yarn run test:unit:ci
|
||||
|
||||
# PLATFORM/VIEWER
|
||||
- run:
|
||||
name: 'VIEWER: Combine report output'
|
||||
command: |
|
||||
viewerCov="/home/circleci/repo/platform/viewer/coverage"
|
||||
touch "${viewerCov}/reports"
|
||||
cat "${viewerCov}/clover.xml" >> "${viewerCov}/reports"
|
||||
echo "\<<\<<\<< EOF" >> "${viewerCov}/reports"
|
||||
cat "${viewerCov}/lcov.info" >>"${viewerCov}/reports"
|
||||
echo "\<<\<<\<< EOF" >> "${viewerCov}/reports"
|
||||
- codecov/upload:
|
||||
file: '/home/circleci/repo/platform/viewer/coverage/reports'
|
||||
flags: 'viewer'
|
||||
|
||||
# PLATFORM/CORE
|
||||
- run:
|
||||
name: 'CORE: Combine report output'
|
||||
command: |
|
||||
coreCov="/home/circleci/repo/platform/core/coverage"
|
||||
touch "${coreCov}/reports"
|
||||
cat "${coreCov}/clover.xml" >> "${coreCov}/reports"
|
||||
echo "\<<\<<\<< EOF" >> "${coreCov}/reports"
|
||||
cat "${coreCov}/lcov.info" >> "${coreCov}/reports"
|
||||
echo "\<<\<<\<< EOF" >> "${coreCov}/reports"
|
||||
- codecov/upload:
|
||||
file: '/home/circleci/repo/platform/core/coverage/reports'
|
||||
flags: 'core'
|
||||
|
||||
# Persist :+1:
|
||||
- persist_to_workspace: &persist_to_workspace
|
||||
root: ~/repo
|
||||
paths: .
|
||||
|
||||
NPM_PUBLISH:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
- run:
|
||||
name: Avoid hosts unknown for github
|
||||
command:
|
||||
mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking
|
||||
no\n" > ~/.ssh/config
|
||||
- run: git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
|
||||
- run: git config --global user.name "ohif-bot"
|
||||
- run:
|
||||
name: Authenticate with registry
|
||||
command:
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
||||
- run: npx lerna version
|
||||
- run: npx lerna publish from-package
|
||||
|
||||
# Persist :+1:
|
||||
- persist_to_workspace:
|
||||
root: ~/repo
|
||||
paths: .
|
||||
|
||||
DOCS_PUBLISH:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
- run:
|
||||
name: Avoid hosts unknown for github
|
||||
command:
|
||||
mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking
|
||||
no\n" > ~/.ssh/config
|
||||
- run: git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
|
||||
- run: git config --global user.name "ohif-bot"
|
||||
- run: yarn global add gitbook-cli
|
||||
- run: yarn global add gh-pages
|
||||
- run: chmod +x ~/repo/.circleci/build-and-publish-docs.sh
|
||||
- run: ~/repo/.circleci/build-and-publish-docs.sh
|
||||
|
||||
DOCKER_MASTER_PUBLISH:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: false
|
||||
- run:
|
||||
name: Build and push Docker image
|
||||
command: |
|
||||
# This file will exist if a new version was published by
|
||||
# our `semantic-release` command in the previous job
|
||||
if [[ ! -e platform/viewer/success_version.txt ]]; then
|
||||
exit 0
|
||||
else
|
||||
# Remove npm config
|
||||
rm -f ./.npmrc
|
||||
# Set our version number using vars
|
||||
export IMAGE_VERSION=$(cat platform/viewer/success_version.txt)
|
||||
export IMAGE_VERSION_FULL=v$IMAGE_VERSION.${CIRCLE_BUILD_NUM}
|
||||
echo $IMAGE_VERSION
|
||||
echo $IMAGE_VERSION_FULL
|
||||
# Build our image, auth, and push
|
||||
docker build --tag ohif/$IMAGE_NAME:$IMAGE_VERSION_FULL --tag ohif/$IMAGE_NAME:latest .
|
||||
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
|
||||
docker push ohif/$IMAGE_NAME:$IMAGE_VERSION_FULL
|
||||
docker push ohif/$IMAGE_NAME:latest
|
||||
fi
|
||||
|
||||
###
|
||||
# Workflow: PR_OPTIONAL_DOCKER_PUBLISH
|
||||
###
|
||||
DOCKER_PR_PUBLISH:
|
||||
<<: *defaults
|
||||
steps:
|
||||
@@ -264,55 +128,190 @@ jobs:
|
||||
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
|
||||
docker push ohif/viewer:PR_BUILD-$CIRCLE_BUILD_NUM
|
||||
|
||||
VIEWER_DEMO_BUILD:
|
||||
###
|
||||
# Workflow: DEPLOY
|
||||
###
|
||||
BUILD:
|
||||
<<: *defaults
|
||||
steps:
|
||||
# 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
|
||||
- save_cache:
|
||||
name: Save Yarn Package Cache
|
||||
paths:
|
||||
- ~/.cache ## Cache yarn and Cypress
|
||||
key: yarn-packages-{{ checksum "yarn.lock" }}
|
||||
# Build & Test
|
||||
- run:
|
||||
name: 'Build the OHIF Viewer'
|
||||
command: yarn run build
|
||||
no_output_timeout: 45m
|
||||
# - run:
|
||||
# name: 'Upload SourceMaps, Send Deploy Notification'
|
||||
# command: |
|
||||
# # export FILE_1=$(find ./build/static/js -type f -name "2.*.js" -exec basename {} \;)
|
||||
# # export FILE_MAIN=$(find ./build/static/js -type f -name "main.*.js" -exec basename {} \;)
|
||||
# # export FILE_RUNTIME_MAIN=$(find ./build/static/js -type f -name "runtime~main.*.js" -exec basename {} \;)
|
||||
# # curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_1.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_1
|
||||
# # curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_MAIN
|
||||
# # curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_RUNTIME_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_RUNTIME_MAIN
|
||||
# curl --request POST https://api.rollbar.com/api/1/deploy/ -F access_token=$ROLLBAR_TOKEN -F environment=$GOOGLE_STORAGE_BUCKET -F revision=$CIRCLE_SHA1 -F local_username=CircleCI
|
||||
# Persist :+1:
|
||||
- persist_to_workspace:
|
||||
root: ~/repo
|
||||
paths:
|
||||
- platform/viewer/dist
|
||||
- netlify.toml
|
||||
- .netlify
|
||||
|
||||
DEPLOY_TO_DEV:
|
||||
docker:
|
||||
- image: circleci/node:12.9.1
|
||||
environment:
|
||||
TERM: xterm
|
||||
NETLIFY_SITE_ID: 32708787-c9b0-4634-b50f-7ca41952da77
|
||||
working_directory: ~/repo
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
# Build & Test
|
||||
- run: cd .netlify && npm install
|
||||
- run:
|
||||
name: 'Build Demo Site'
|
||||
command: yarn run build:demo
|
||||
no_output_timeout: 45m
|
||||
cp .netlify/deploy-workflow/_redirects platform/viewer/dist/_redirects
|
||||
- run: cd .netlify && npm run deploy
|
||||
|
||||
DEPLOY_TO_STAGING:
|
||||
docker:
|
||||
- image: circleci/node:12.9.1
|
||||
environment:
|
||||
TERM: xterm
|
||||
NETLIFY_SITE_ID: c7502ae3-b150-493c-8422-05701e44a969
|
||||
working_directory: ~/repo
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
- run: cd .netlify && npm install
|
||||
- run:
|
||||
name: 'Upload SourceMaps, Send Deploy Notification'
|
||||
cp .netlify/deploy-workflow/_redirects platform/viewer/dist/_redirects
|
||||
- run: cd .netlify && npm run deploy
|
||||
|
||||
DEPLOY_TO_PRODUCTION:
|
||||
docker:
|
||||
- image: circleci/node:12.9.1
|
||||
environment:
|
||||
TERM: xterm
|
||||
NETLIFY_SITE_ID: 79c4a5da-5c95-4dc9-84f7-45fd9dfe21b0
|
||||
working_directory: ~/repo
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
- run: cd .netlify && npm install
|
||||
- run:
|
||||
cp .netlify/deploy-workflow/_redirects platform/viewer/dist/_redirects
|
||||
- run: cd .netlify && npm run deploy
|
||||
|
||||
###
|
||||
# Workflow: RELEASE
|
||||
###
|
||||
NPM_PUBLISH:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- run: yarn -v
|
||||
# Checkout code and ALL Git Tags
|
||||
- checkout:
|
||||
post:
|
||||
- git fetch --all
|
||||
# Use increasingly general patterns to restore cache
|
||||
- restore_cache:
|
||||
name: Restore Yarn and Cypress Package Cache
|
||||
keys:
|
||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- yarn-packages-
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: yarn install --frozen-lockfile
|
||||
- save_cache:
|
||||
name: Save Yarn Package Cache
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
key: yarn-packages-{{ checksum "yarn.lock" }}
|
||||
- run:
|
||||
name: Avoid hosts unknown for github
|
||||
command: |
|
||||
# export FILE_1=$(find ./build/static/js -type f -name "2.*.js" -exec basename {} \;)
|
||||
# export FILE_MAIN=$(find ./build/static/js -type f -name "main.*.js" -exec basename {} \;)
|
||||
# export FILE_RUNTIME_MAIN=$(find ./build/static/js -type f -name "runtime~main.*.js" -exec basename {} \;)
|
||||
# curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_1.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_1
|
||||
# curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_MAIN
|
||||
# curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_RUNTIME_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_RUNTIME_MAIN
|
||||
curl --request POST https://api.rollbar.com/api/1/deploy/ -F access_token=$ROLLBAR_TOKEN -F environment=$GOOGLE_STORAGE_BUCKET -F revision=$CIRCLE_SHA1 -F local_username=CircleCI
|
||||
# Persist :+1:
|
||||
rm -rf ~/.ssh
|
||||
mkdir ~/.ssh/
|
||||
echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
|
||||
git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
|
||||
git config --global user.name "ohif-bot"
|
||||
- run:
|
||||
name: Authenticate with NPM registry
|
||||
command:
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
|
||||
- run: npx lerna version
|
||||
- run: npx lerna publish from-package
|
||||
- persist_to_workspace:
|
||||
root: ~/repo
|
||||
paths: .
|
||||
|
||||
VIEWER_DEMO_PUBLISH:
|
||||
working_directory: ~/repo
|
||||
docker:
|
||||
- image: google/cloud-sdk
|
||||
DOCS_PUBLISH:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Avoid hosts unknown for github
|
||||
command: |
|
||||
rm -rf ~/.ssh
|
||||
mkdir ~/.ssh/
|
||||
echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
|
||||
git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
|
||||
git config --global user.name "ohif-bot"
|
||||
- run: yarn global add gitbook-cli gh-pages
|
||||
- run: chmod +x ~/repo/.circleci/build-and-publish-docs.sh
|
||||
- run: ~/repo/.circleci/build-and-publish-docs.sh
|
||||
|
||||
DOCKER_MASTER_PUBLISH:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: ~/repo
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: false
|
||||
- run:
|
||||
name: Deploy latest version to viewer.ohif.org
|
||||
name: Build and push Docker image
|
||||
command: |
|
||||
# https://circleci.com/docs/2.0/google-auth/#authorization
|
||||
echo $GCLOUD_SERVICE_KEY | gcloud auth activate-service-account --key-file=-
|
||||
gcloud --quiet config set project ${GOOGLE_PROJECT_ID}
|
||||
gcloud --quiet config set compute/zone ${GOOGLE_COMPUTE_ZONE}
|
||||
# Remove all files in Storage Bucket; Push latest
|
||||
gsutil -m rm gs://$GOOGLE_STORAGE_BUCKET/**
|
||||
gsutil -m rsync -R platform/viewer/dist gs://$GOOGLE_STORAGE_BUCKET
|
||||
# This file will exist if a new version was published by
|
||||
# our command in the previous job. Created in npm postpublish hook
|
||||
# in the `platform/viewer` project.
|
||||
if [[ ! -e platform/viewer/success_version.txt ]]; then
|
||||
exit 0
|
||||
else
|
||||
# Remove npm config
|
||||
rm -f ./.npmrc
|
||||
# Set our version number using vars
|
||||
export IMAGE_VERSION=$(cat platform/viewer/success_version.txt)
|
||||
export IMAGE_VERSION_FULL=v$IMAGE_VERSION.${CIRCLE_BUILD_NUM}
|
||||
echo $IMAGE_VERSION
|
||||
echo $IMAGE_VERSION_FULL
|
||||
# Build our image, auth, and push
|
||||
docker build --tag ohif/$IMAGE_NAME:$IMAGE_VERSION_FULL --tag ohif/$IMAGE_NAME:latest .
|
||||
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
|
||||
docker push ohif/$IMAGE_NAME:$IMAGE_VERSION_FULL
|
||||
docker push ohif/$IMAGE_NAME:latest
|
||||
fi
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
# PULL REQUESTS
|
||||
PR_CHECKS:
|
||||
jobs:
|
||||
- UNIT_TESTS:
|
||||
@@ -379,82 +378,66 @@ workflows:
|
||||
requires:
|
||||
- AWAIT_APPROVAL
|
||||
|
||||
# MERGE TO MASTER
|
||||
RELEASE:
|
||||
###
|
||||
# Our workflow for building, deploying, and promoting builds across our
|
||||
# development, staging, and production environments.
|
||||
###
|
||||
DEPLOY:
|
||||
jobs:
|
||||
- UNIT_TESTS_PERSIST_WORKSPACE:
|
||||
- BUILD:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
# 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: 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:
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/screenshots
|
||||
- store_test_results:
|
||||
path: cypress/results
|
||||
- DEPLOY_TO_DEV:
|
||||
requires:
|
||||
- UNIT_TESTS_PERSIST_WORKSPACE
|
||||
# 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
|
||||
post-steps:
|
||||
- store_artifacts:
|
||||
path: platform/viewer/cypress/screenshots
|
||||
- store_test_results:
|
||||
path: cypress/results
|
||||
- BUILD
|
||||
- PROMOTE_TO_STAGING:
|
||||
type: approval
|
||||
requires:
|
||||
- UNIT_TESTS_PERSIST_WORKSPACE
|
||||
# Update NPM
|
||||
- DEPLOY_TO_DEV
|
||||
- DEPLOY_TO_STAGING:
|
||||
requires:
|
||||
- PROMOTE_TO_STAGING
|
||||
- PROMOTE_TO_PRODUCTION:
|
||||
type: approval
|
||||
requires:
|
||||
- DEPLOY_TO_STAGING
|
||||
- DEPLOY_TO_PRODUCTION:
|
||||
requires:
|
||||
- PROMOTE_TO_PRODUCTION
|
||||
###
|
||||
# Unit and E2E tests have already run for PR_CHECKS
|
||||
# Re-running should not gain us any confidence here
|
||||
###
|
||||
RELEASE:
|
||||
jobs:
|
||||
- NPM_PUBLISH:
|
||||
requires:
|
||||
- 'E2E: Script Tag'
|
||||
- 'E2E: PWA'
|
||||
# Update docs.ohif.org
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
- DOCS_PUBLISH:
|
||||
requires:
|
||||
- 'E2E: Script Tag'
|
||||
- 'E2E: PWA'
|
||||
- VIEWER_DEMO_BUILD:
|
||||
requires:
|
||||
- 'E2E: Script Tag'
|
||||
- 'E2E: PWA'
|
||||
# Update viewer.ohif.org
|
||||
- VIEWER_DEMO_PUBLISH:
|
||||
requires:
|
||||
- VIEWER_DEMO_BUILD
|
||||
# Update hub.docker.org
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
- DOCKER_MASTER_PUBLISH:
|
||||
requires:
|
||||
- NPM_PUBLISH
|
||||
#
|
||||
#
|
||||
# Potentially use to E2E test STAGING environment
|
||||
# before publishing to production
|
||||
# - cypress/run:
|
||||
# name: 'E2E: PWA'
|
||||
# executor: deploy-to-prod-executor
|
||||
# browser: chrome
|
||||
# working_directory: platform/viewer
|
||||
# cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
|
||||
# yarn: true
|
||||
# record: false
|
||||
# no-workspace: true
|
||||
# store_artifacts: true
|
||||
# post-steps:
|
||||
# - store_artifacts:
|
||||
# path: platform/viewer/cypress/screenshots
|
||||
# - store_test_results:
|
||||
# path: cypress/results
|
||||
@@ -2,7 +2,7 @@
|
||||
name: "\U0001F41B Bug report"
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'Community: Report :bug:'
|
||||
labels: 'Community: Report :bug:, Awaiting Reproduction, Triage :white_flag:'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: "\U0001F680 Feature request"
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'Community: Request :hand:'
|
||||
labels: 'Community: Request :hand:, Triage :white_flag:'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: "\U0001F917 Support Question"
|
||||
about: "I have a question \U0001F4AC"
|
||||
title: ''
|
||||
labels: 'Community: Question :question:'
|
||||
labels: 'Community: Question :question:, Triage :white_flag:'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ exemptLabels:
|
||||
- 'Task: Refactor 🛠'
|
||||
- 'Task: Tests :microscope:'
|
||||
- 'PR: Awaiting Review 👀'
|
||||
- 'Triage 🏳️'
|
||||
- 'Triage :white_flag:'
|
||||
- 'Announcement 🎉'
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: 'Stale :baguette_bread:'
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Specific to our non-deploy-preview deploys
|
||||
# Confgure redirects using netlify.toml
|
||||
|
||||
# PWA Redirect
|
||||
/* /index.html 200
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "root",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1.16.0"
|
||||
},
|
||||
"scripts": {
|
||||
"deploy": "netlify deploy --prod --dir ./../platform/viewer/dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"netlify-cli": "^2.21.0"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
# OHIF Viewers
|
||||
|
||||
- [Our Process](our-process.md)
|
||||
- Essentials
|
||||
- [Getting Started](essentials/getting-started.md)
|
||||
- [Installation](essentials/installation.md)
|
||||
@@ -47,5 +48,6 @@
|
||||
|
||||
- [FAQ](frequently-asked-questions.md)
|
||||
- [Contributing](contributing/index.md)
|
||||
- [Continuous Integration](continous-integration.md)
|
||||
- [Browser Support](browser-support.md)
|
||||
- [Help](help.md)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
@@ -0,0 +1,109 @@
|
||||
# Continous Integration (CI)
|
||||
|
||||
This repository uses `CircleCI` and `Netlify` for continous integration.
|
||||
|
||||
## Deploy Previews
|
||||
|
||||
[Netlify Deploy previews][deploy-previews] are generated for every pull request.
|
||||
They allow pull request authors and reviewers to "Preview" the OHIF Viewer as if
|
||||
the changes had been merged.
|
||||
|
||||
Deploy previews can be configured by modifying the `netlify.toml` file in the
|
||||
root of the repository. Some additional scripts/assets for netlify are included
|
||||
in the root `.netlify` directory.
|
||||
|
||||
## Workflows
|
||||
|
||||
[CircleCI Workflows][circleci-workflows] are a set of rules for defining a
|
||||
collection of jobs and their run order. They are self-documenting and their
|
||||
configuration can be found in our CircleCI configuration file:
|
||||
`.circleci/config.yml`.
|
||||
|
||||
### Workflow: PR_CHECKS
|
||||
|
||||
The PR_CHECKS workflow (Pull Request Checks) runs our automated unit and
|
||||
end-to-end tests for every code check-in. These tests must all pass before code
|
||||
can be merged to our `master` branch.
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="/assets/img/WORKFLOW_PR_CHECKS.png">
|
||||
<img src="/assets/img/WORKFLOW_PR_CHECKS.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
|
||||
</a>
|
||||
<div><i>Workflow diagram for PR_CHECKS</i></div>
|
||||
</div>
|
||||
|
||||
### Workflow: PR_OPTIONAL_DOCKER_PUBLISH
|
||||
|
||||
The PR_OPTIONAL_DOCKER_PUBLISH workflow allows for "manual approval" to publish
|
||||
the pull request as a tagged docker image. This is helpful when changes need to
|
||||
be tested with the Google Adapter before merging to `master`.
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png">
|
||||
<img src="/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
|
||||
</a>
|
||||
<div><i>Workflow diagram for PR_WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH</i></div>
|
||||
</div>
|
||||
|
||||
> NOTE: This workflow will fail unless it's for a branch on our `upstream`
|
||||
> repository. If you need this functionality, but the branch is from a fork,
|
||||
> merge the changes to a short-lived `feature/` branch on `upstream`
|
||||
|
||||
### Workflow: DEPLOY
|
||||
|
||||
The DEPLOY workflow deploys the OHIF Viewer when changes are merged to master.
|
||||
It uses the Netlify CLI to deploy assets created as part of the repository's PWA
|
||||
Build process (`yarn run build`). The workflow allows for "Manual Approval" to
|
||||
promote the build to `STAGING` and `PRODUCTION` environments.
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="/assets/img/WORKFLOW_DEPLOY.png">
|
||||
<img src="/assets/img/WORKFLOW_DEPLOY.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
|
||||
</a>
|
||||
<div><i>Workflow diagram for WORKFLOW_DEPLOY</i></div>
|
||||
</div>
|
||||
|
||||
| Environment | Description | URL |
|
||||
| ----------- | ---------------------------------------------------------------------------------- | --------------------------------------------- |
|
||||
| Development | Always reflects latest changes on `master` branch. | [Netlify][netlify-dev] / [OHIF][ohif-dev] |
|
||||
| Staging | For manual testing before promotion to prod. Keeps development workflow unblocked. | [Netlify][netlify-stage] / [OHIF][ohif-stage] |
|
||||
| Production | Stable, tested, updated less frequently. | [Netlify][netlify-prod] / [OHIF][ohif-prod] |
|
||||
|
||||
### Workflow: RELEASE
|
||||
|
||||
The RELEASE workflow publishes our `npm` packages, updated documentation, and
|
||||
`docker` image when changes are merged to master. `Lerna` and "Semantic Commit
|
||||
Syntax" are used to independently version and publish the many packages in our
|
||||
monorepository. If a new version is cut/released, a Docker image is created.
|
||||
Documentation is generated with `gitbook` and pushed to our `gh-pages` branch.
|
||||
GitHub hosts the `gh-pages` branch with GitHub Pages.
|
||||
|
||||
- Platform Packages: https://github.com/ohif/viewers/#platform
|
||||
- Extension Packages: https://github.com/ohif/viewers/#extensions
|
||||
- Documentation: https://docs.ohif.org/
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="/assets/img/WORKFLOW_RELEASE.png">
|
||||
<img src="/assets/img/WORKFLOW_RELEASE.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
|
||||
</a>
|
||||
<div><i>Workflow diagram for WORKFLOW_RELEASE</i></div>
|
||||
</div>
|
||||
|
||||
### HOTFIX
|
||||
|
||||
_Not yet implemented_
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[deploy-previews]: https://www.netlify.com/blog/2016/07/20/introducing-deploy-previews-in-netlify/
|
||||
[circleci-workflows]: https://circleci.com/docs/2.0/workflows/
|
||||
[netlify-dev]: https://ohif-dev.netlify.com
|
||||
[netlify-stage]: https://ohif-stage.netlify.com
|
||||
[netlify-prod]: https://ohif-prod.netlify.com
|
||||
[ohif-dev]: https://viewer-dev.ohif.org
|
||||
[ohif-stage]: https://viewer-stage.ohif.org
|
||||
[ohif-prod]: https://viewer-prod.ohif.org
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -7,8 +7,8 @@ like to discuss the changes you intend to make to clarify where or how they
|
||||
should be implemented, please don't hesitate to create a new issue. At a
|
||||
minimum, you may want to read the following documentation:
|
||||
|
||||
- [Essentials: Getting Started](./essentials/getting-started.md)
|
||||
- [Advanced: Architecture](./advanced/architecture.md)
|
||||
- [Essentials: Getting Started](/essentials/getting-started.md)
|
||||
- [Advanced: Architecture](/advanced/architecture.md)
|
||||
|
||||
Pull requests that are:
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# Our Process
|
||||
|
||||
Our process is a living, breathing thing. We strive to have regular [retrospectives][retrospective] that help us
|
||||
shape and adapt our process to our team's current needs. This document attempts to capture the broad strokes of
|
||||
that process in an effort to:
|
||||
|
||||
- Strengthen community member involvement and understanding
|
||||
- Welcome feedback and helpful suggestions
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
- [Issue Triage](#issue-triage)
|
||||
- [Issue Curation ("backlog grooming")](#issue-curation-backlog-grooming)
|
||||
- [Contributions (Pull Requests)](#contributions-pull-requests)
|
||||
- [Releases](#releases)
|
||||
|
||||
|
||||
_Include issue lifecycle diagram_
|
||||
|
||||
## Issue Triage
|
||||
|
||||
[GitHub issues][gh-issues] are the best way to provide feedback, ask questions, and suggest changes to the OHIF Viewer's core
|
||||
team. Community issues generally fall into one of three categories, and are marked with a `triage` label when created.
|
||||
|
||||
|
||||
|Issue Template Name | Description |
|
||||
|------------------------|-----------------------------------------------------------------------------------------|
|
||||
|Community: Report 🐛 | Describe a new issue; Provide steps to reproduce; Expected versus actual result? |
|
||||
|Community: Request ✋ | Describe a proposed new feature. Why should it be implemented? What is the impact/value? |
|
||||
|Community: Question ❓ | Seek clarification or assistance relevant to the repository. |
|
||||
|
||||
_table 1. issue template names and descriptions_
|
||||
|
||||
Issues that require `triage` are akin to support tickets. As this is often our first contact with would-be adopters and
|
||||
contributors, it's important that we strive for timely responses and satisfactory resolutions. We attempt to accomplish this
|
||||
by:
|
||||
|
||||
1. Responding to issues requiring `triage` at least once a week
|
||||
2. Create new "official issues" from "community issues"
|
||||
3. Provide clear guidance and next steps (when applicable)
|
||||
4. Regularly clean up old (stale) issues
|
||||
|
||||
> :pencil: Less obviously, patterns in the issues being reported can highlight areas that need improvement. For example, users often have
|
||||
difficulty navigating CORS issues when deploying the OHIF Viewer -- how do we best reduce our ticket volume for this issue?
|
||||
|
||||
### Backlogged Issues
|
||||
|
||||
Community issues serve as vehicles of discussion that lead us to "backlogged issues". Backlogged issues are the distilled and actionable information extracted from community issues. They contain the scope and requirements necessary for hand-off to a core-team (or community) contributor ^_^
|
||||
|
||||
|Category| Description| Labels|
|
||||
|--------|-----| ---------|
|
||||
| Bugs | An issue with steps that produce a bug (an unexpected result). | [Bug: Verified 🐛][label-bug] |
|
||||
| Stories| A feature/enhancement with a clear benefit, boundaries, and requirements. | [Story 🙌][label-story] |
|
||||
| Tasks | Changes that improve [UX], [DX], or test coverage; but don't impact application behavior | [Task: CI/Tooling 🤖][label-tooling], [Task: Docs 📖][label-docs], [Task: Refactor 🛠][label-refactor], [Task: Tests 🔬][label-tests] |
|
||||
|
||||
_table 2. backlogged issue types ([full list of labels][gh-labels])_
|
||||
|
||||
|
||||
## Issue Curation (["backlog grooming"][groom-backlog])
|
||||
|
||||
If a [GitHub issue][gh-issues] has a `bug`, `story`, or `task` label; it's on our backlog. If an issue is on our backlog, it means we are, at the very least, committed to reviewing any community drafted Pull Requests to complete the issue. If you're interested in seeing an issue completed but don't know where to start, please don't hesitate to leave a comment!
|
||||
|
||||
While we don't yet have a long-term or quarterly road map, we do regularly add items to our ["Active Development" GitHub Project Board][gh-board]. Items on this project board are either in active development by Core Team members, or queued up for development as in-progress items are completed.
|
||||
|
||||
> :pencil: Want to contribute but not sure where to start? Check out [Up for grabs][label-grabs] issues and our [Contributing documentation][contributing-docs]
|
||||
|
||||
## Contributions (Pull Requests)
|
||||
|
||||
Incoming Pull Requests (PRs) are triaged using the following labels. Code review is performed on all PRs where the bug fix or added functionality is deemed appropriate:
|
||||
|
||||
- PR: Bug Fix - Describes a PR filed to address a Bug.
|
||||
- PR: Draft - Describes a PR filed to gather early feedback from the core team, but which is not intended for merging in the short term.
|
||||
- Awaiting Response - Describes a PR where the core team is waiting for additional information from the author. This may occur before or after code review.
|
||||
- Awaiting Review - Describes a PR where the core team has not yet performed a code review.
|
||||
- Awaiting Revisions - Following code review, this label is applied until the author has made sufficient changes.
|
||||
|
||||
We rely on GitHub Checks and integrations with third party services to evaluate changes in code quality and test coverage. Tests must pass before a PR can be merged to master, and code quality and test coverage must not changed by a significant margin. For some repositories, visual screenshot-based tests are also included, and video recordings of end-to-end tests are stored for later review.
|
||||
|
||||
[You can read more about our continous integration efforts here](/continous-integration.md)
|
||||
|
||||
## Releases
|
||||
|
||||
Releases are made automatically based on the type of commits which have been merged (major.minor.patch). Releases are automatically pushed to NPM. Release notes are automatically generated. Users can subscribe to GitHub and NPM releases.
|
||||
|
||||
Important announcements are made on GitHub, tagged as Announcement, and pinned so that they remain at the top of the Issue page.
|
||||
|
||||
The Core team occasionally performs full manual testing to begin the process of releasing a Stable version. Once testing is complete, the known issues are addressed and a Stable version
|
||||
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[groom-backlog]: https://www.agilealliance.org/glossary/backlog-grooming
|
||||
[retrospective]: https://www.atlassian.com/team-playbook/plays/retrospective
|
||||
[gh-issues]: https://github.com/OHIF/Viewers/issues/new/choose
|
||||
[gh-labels]: https://github.com/OHIF/Viewers/labels
|
||||
[label-story]: https://github.com/OHIF/Viewers/labels/Story%20%3Araised_hands%3A
|
||||
[label-tooling]: https://github.com/OHIF/Viewers/labels/Task%3A%20CI%2FTooling%20%3Arobot%3A
|
||||
[label-docs]: https://github.com/OHIF/Viewers/labels/Task%3A%20Docs%20%3Abook%3A
|
||||
[label-refactor]: https://github.com/OHIF/Viewers/labels/Task%3A%20Refactor%20%3Ahammer_and_wrench%3A
|
||||
[label-tests]: https://github.com/OHIF/Viewers/labels/Task%3A%20Tests%20%3Amicroscope%3A
|
||||
[label-bug]: https://github.com/OHIF/Viewers/labels/Bug%3A%20Verified%20%3Abug%3A
|
||||
[gh-board]: https://github.com/OHIF/Viewers/projects/4
|
||||
[label-grabs]: https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs+%3Araising_hand_woman%3A%22
|
||||
[contributing-docs]: https://docs.ohif.org/contributing/
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -11,7 +11,10 @@ export default {
|
||||
* LIFECYCLE HOOKS
|
||||
*/
|
||||
|
||||
preRegistration({ serviceManager, configuration: extensionConfiguration }) {},
|
||||
preRegistration({
|
||||
servicesManager,
|
||||
configuration: extensionConfiguration,
|
||||
}) {},
|
||||
|
||||
/**
|
||||
* MODULE GETTERS
|
||||
|
||||
@@ -3,6 +3,44 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.7.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.7.0...@ohif/extension-cornerstone@1.7.1) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.7.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.6.0...@ohif/extension-cornerstone@1.7.0) (2019-11-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.5.1...@ohif/extension-cornerstone@1.6.0) (2019-11-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.5.0...@ohif/extension-cornerstone@1.5.1) (2019-11-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.4.1...@ohif/extension-cornerstone@1.5.0) (2019-11-13)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "1.5.0",
|
||||
"version": "1.7.1",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -32,7 +32,7 @@
|
||||
"@ohif/ui": "^0.50.0",
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-math": "^0.1.8",
|
||||
"cornerstone-tools": "^4.0.9",
|
||||
"cornerstone-tools": "^4.8.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
"dicom-parser": "^1.8.3",
|
||||
|
||||
@@ -2,6 +2,7 @@ import CornerstoneViewport from 'react-cornerstone-viewport';
|
||||
import OHIF from '@ohif/core';
|
||||
import { connect } from 'react-redux';
|
||||
import throttle from 'lodash.throttle';
|
||||
import { setEnabledElement } from './state';
|
||||
|
||||
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
|
||||
const {
|
||||
@@ -80,11 +81,11 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
*/
|
||||
onElementEnabled: event => {
|
||||
const enabledElement = event.detail.element;
|
||||
setEnabledElement(viewportIndex, enabledElement);
|
||||
dispatch(
|
||||
setViewportSpecificData(viewportIndex, {
|
||||
// TODO: Hack to make sure our plugin info is available from the outset
|
||||
plugin: 'cornerstone',
|
||||
dom: enabledElement,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneTools from 'cornerstone-tools';
|
||||
import OHIF from '@ohif/core';
|
||||
import { getEnabledElement } from './state';
|
||||
const scroll = cornerstoneTools.import('util/scroll');
|
||||
|
||||
const actions = {
|
||||
rotateViewport: ({ viewports, rotation }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
@@ -17,10 +15,7 @@ const actions = {
|
||||
}
|
||||
},
|
||||
flipViewportHorizontal: ({ viewports }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
@@ -29,10 +24,7 @@ const actions = {
|
||||
}
|
||||
},
|
||||
flipViewportVertical: ({ viewports }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
@@ -40,11 +32,8 @@ const actions = {
|
||||
cornerstone.setViewport(enabledElement, viewport);
|
||||
}
|
||||
},
|
||||
scaleViewport: ({ viewports, direction }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
scaleViewport: ({ direction, viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
const step = direction * 0.15;
|
||||
|
||||
if (enabledElement) {
|
||||
@@ -58,20 +47,14 @@ const actions = {
|
||||
}
|
||||
},
|
||||
resetViewport: ({ viewports }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
cornerstone.reset(enabledElement);
|
||||
}
|
||||
},
|
||||
invertViewport: ({ viewports }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
|
||||
if (enabledElement) {
|
||||
let viewport = cornerstone.getViewport(enabledElement);
|
||||
@@ -92,10 +75,7 @@ const actions = {
|
||||
console.warn('updateDisplaySet: ', direction);
|
||||
},
|
||||
clearAnnotations: ({ viewports }) => {
|
||||
const element = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
const element = getEnabledElement(viewports.activeViewportIndex);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
@@ -147,24 +127,25 @@ const actions = {
|
||||
});
|
||||
},
|
||||
nextImage: ({ viewports }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
scroll(enabledElement, 1);
|
||||
},
|
||||
previousImage: ({ viewports }) => {
|
||||
const enabledElement = _getActiveViewportEnabledElement(
|
||||
viewports.viewportSpecificData,
|
||||
viewports.activeViewportIndex
|
||||
);
|
||||
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
scroll(enabledElement, -1);
|
||||
},
|
||||
getActiveViewportEnabledElement: ({ viewports }) => {
|
||||
const enabledElement = getEnabledElement(viewports.activeViewportIndex);
|
||||
return enabledElement;
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = {
|
||||
getActiveViewportEnabledElement: {
|
||||
commandFn: actions.getActiveViewportEnabledElement,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
rotateViewportCW: {
|
||||
commandFn: actions.rotateViewport,
|
||||
storeContexts: ['viewports'],
|
||||
@@ -245,15 +226,6 @@ const definitions = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Grabs `dom` reference for the enabledElement of
|
||||
* the active viewport
|
||||
*/
|
||||
function _getActiveViewportEnabledElement(viewports, activeIndex) {
|
||||
const activeViewport = viewports[activeIndex] || {};
|
||||
return activeViewport.dom;
|
||||
}
|
||||
|
||||
export default {
|
||||
actions,
|
||||
definitions,
|
||||
|
||||
@@ -24,8 +24,8 @@ export default {
|
||||
* @param {object} [configuration={}]
|
||||
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||
*/
|
||||
preRegistration({ serviceManager, configuration = {} }) {
|
||||
init({ serviceManager, configuration });
|
||||
preRegistration({ servicesManager, configuration = {} }) {
|
||||
init({ servicesManager, configuration });
|
||||
},
|
||||
getViewportModule() {
|
||||
return OHIFCornerstoneViewport;
|
||||
|
||||
@@ -3,6 +3,7 @@ import cornerstone from 'cornerstone-core';
|
||||
import csTools from 'cornerstone-tools';
|
||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||
import queryString from 'query-string';
|
||||
import { SimpleDialog } from '@ohif/ui';
|
||||
|
||||
function fallbackMetaDataProvider(type, imageId) {
|
||||
if (!imageId.includes('wado?requestType=WADO')) {
|
||||
@@ -28,7 +29,29 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
|
||||
* @param {object} configuration
|
||||
* @param {Object|Array} configuration.csToolsConfig
|
||||
*/
|
||||
export default function init({ serviceManager, configuration = {} }) {
|
||||
export default function init({ servicesManager, configuration = {} }) {
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
const callInputDialog = (data, event, callback) => {
|
||||
let dialogId = UIDialogService.create({
|
||||
content: SimpleDialog.InputDialog,
|
||||
defaultPosition: {
|
||||
x: (event && event.currentPoints.canvas.x) || 0,
|
||||
y: (event && event.currentPoints.canvas.y) || 0,
|
||||
},
|
||||
showOverlay: true,
|
||||
contentProps: {
|
||||
title: 'Enter your annotation',
|
||||
label: 'New label',
|
||||
measurementData: data ? { description: data.text } : {},
|
||||
onClose: () => UIDialogService.dismiss({ id: dialogId }),
|
||||
onSubmit: value => {
|
||||
callback(value);
|
||||
UIDialogService.dismiss({ id: dialogId });
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const { csToolsConfig } = configuration;
|
||||
const { StackManager } = OHIF.utils;
|
||||
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||
@@ -62,7 +85,6 @@ export default function init({ serviceManager, configuration = {} }) {
|
||||
ZoomTouchPinchTool,
|
||||
// Annotations
|
||||
EraserTool,
|
||||
ArrowAnnotateTool,
|
||||
BidirectionalTool,
|
||||
LengthTool,
|
||||
AngleTool,
|
||||
@@ -85,7 +107,6 @@ export default function init({ serviceManager, configuration = {} }) {
|
||||
ZoomTouchPinchTool,
|
||||
// Annotations
|
||||
EraserTool,
|
||||
ArrowAnnotateTool,
|
||||
BidirectionalTool,
|
||||
LengthTool,
|
||||
AngleTool,
|
||||
@@ -99,6 +120,15 @@ export default function init({ serviceManager, configuration = {} }) {
|
||||
|
||||
tools.forEach(tool => csTools.addTool(tool));
|
||||
|
||||
csTools.addTool(csTools.ArrowAnnotateTool, {
|
||||
configuration: {
|
||||
getTextCallback: (callback, eventDetails) =>
|
||||
callInputDialog(null, eventDetails, callback),
|
||||
changeTextCallback: (data, eventDetails, callback) =>
|
||||
callInputDialog(data, eventDetails, callback),
|
||||
},
|
||||
});
|
||||
|
||||
csTools.setToolActive('Pan', { mouseButtonMask: 4 });
|
||||
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
|
||||
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
const state = {
|
||||
enabledElements: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the enabled element `dom` reference for an active viewport.
|
||||
* @param {HTMLElement} dom Active viewport element.
|
||||
* @return void
|
||||
*/
|
||||
const setEnabledElement = (viewportIndex, element) =>
|
||||
(state.enabledElements[viewportIndex] = element);
|
||||
|
||||
/**
|
||||
* Grabs the enabled element `dom` reference of an active viewport.
|
||||
*
|
||||
* @return {HTMLElement} Active viewport element.
|
||||
*/
|
||||
const getEnabledElement = viewportIndex => state.enabledElements[viewportIndex];
|
||||
|
||||
export { setEnabledElement, getEnabledElement };
|
||||
@@ -23,6 +23,17 @@ const TOOLBAR_BUTTON_TYPES = {
|
||||
BUILT_IN: 'builtIn',
|
||||
};
|
||||
|
||||
const TOOLBAR_BUTTON_BEHAVIORS = {
|
||||
CINE: 'CINE',
|
||||
DOWNLOAD_SCREEN_SHOT: 'DOWNLOAD_SCREEN_SHOT',
|
||||
};
|
||||
|
||||
/* TODO: Export enums through a extension manager. */
|
||||
const enums = {
|
||||
TOOLBAR_BUTTON_TYPES,
|
||||
TOOLBAR_BUTTON_BEHAVIORS,
|
||||
};
|
||||
|
||||
const definitions = [
|
||||
{
|
||||
id: 'StackScroll',
|
||||
@@ -102,7 +113,7 @@ const definitions = [
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
|
||||
options: {
|
||||
behavior: 'CINE',
|
||||
behavior: TOOLBAR_BUTTON_BEHAVIORS.CINE,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -220,7 +231,8 @@ const definitions = [
|
||||
//
|
||||
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
|
||||
options: {
|
||||
behavior: 'DOWNLOAD_SCREEN_SHOT',
|
||||
behavior: TOOLBAR_BUTTON_BEHAVIORS.DOWNLOAD_SCREEN_SHOT,
|
||||
togglable: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.1...@ohif/extension-dicom-html@1.0.2) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.0.0...@ohif/extension-dicom-html@1.0.1) (2019-10-29)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-dicom-html",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"description": "OHIF extension for rendering structured reports to HTML",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -6,7 +6,7 @@ const { setViewportActive } = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { viewportIndex, byteArray } = ownProps;
|
||||
const activeViewportIndex = state.viewports;
|
||||
const { activeViewportIndex } = state.viewports;
|
||||
|
||||
return {
|
||||
viewportIndex,
|
||||
|
||||
@@ -238,7 +238,7 @@ class DicomHtmlViewport extends Component {
|
||||
} = this.props;
|
||||
|
||||
if (viewportIndex !== activeViewportIndex) {
|
||||
setViewportActive();
|
||||
setViewportActive(viewportIndex);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,97 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.54.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.1...@ohif/extension-vtk@0.54.2) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.54.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.54.0...@ohif/extension-vtk@0.54.1) (2019-11-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.54.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.13...@ohif/extension-vtk@0.54.0) (2019-11-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.12...@ohif/extension-vtk@0.53.13) (2019-11-25)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.11...@ohif/extension-vtk@0.53.12) (2019-11-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.10...@ohif/extension-vtk@0.53.11) (2019-11-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.9...@ohif/extension-vtk@0.53.10) (2019-11-19)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.8...@ohif/extension-vtk@0.53.9) (2019-11-18)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.7...@ohif/extension-vtk@0.53.8) (2019-11-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.6...@ohif/extension-vtk@0.53.7) (2019-11-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.5...@ohif/extension-vtk@0.53.6) (2019-11-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.4...@ohif/extension-vtk@0.53.5) (2019-11-13)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "0.53.5",
|
||||
"version": "0.54.2",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -52,9 +52,9 @@
|
||||
"react-vtkjs-viewport": "^0.3.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^1.9.0",
|
||||
"@ohif/ui": "^0.62.0",
|
||||
"cornerstone-tools": "^4.0.9",
|
||||
"@ohif/core": "^1.13.2",
|
||||
"@ohif/ui": "^0.65.1",
|
||||
"cornerstone-tools": "^4.8.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
"dicom-parser": "^1.8.3",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import throttle from 'lodash.throttle';
|
||||
import {
|
||||
vtkInteractorStyleMPRCrosshairs,
|
||||
vtkInteractorStyleMPRWindowLevel,
|
||||
@@ -8,354 +9,358 @@ import {
|
||||
import setMPRLayout from './utils/setMPRLayout.js';
|
||||
import setViewportToVTK from './utils/setViewportToVTK.js';
|
||||
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 = {};
|
||||
const commandsModule = ({ commandsManager }) => {
|
||||
// TODO: Put this somewhere else
|
||||
let apis = {};
|
||||
|
||||
async function _getActiveViewportVTKApi(viewports) {
|
||||
const {
|
||||
numRows,
|
||||
numColumns,
|
||||
layout,
|
||||
viewportSpecificData,
|
||||
activeViewportIndex,
|
||||
} = viewports;
|
||||
async function _getActiveViewportVTKApi(viewports) {
|
||||
const {
|
||||
numRows,
|
||||
numColumns,
|
||||
layout,
|
||||
viewportSpecificData,
|
||||
activeViewportIndex,
|
||||
} = viewports;
|
||||
|
||||
const currentData = layout.viewports[activeViewportIndex];
|
||||
if (currentData && currentData.plugin === 'vtk') {
|
||||
// TODO: I was storing/pulling this from Redux but ran into weird issues
|
||||
if (apis[activeViewportIndex]) {
|
||||
return apis[activeViewportIndex];
|
||||
const currentData = layout.viewports[activeViewportIndex];
|
||||
if (currentData && currentData.plugin === 'vtk') {
|
||||
// TODO: I was storing/pulling this from Redux but ran into weird issues
|
||||
if (apis[activeViewportIndex]) {
|
||||
return apis[activeViewportIndex];
|
||||
}
|
||||
}
|
||||
|
||||
const displaySet = viewportSpecificData[activeViewportIndex];
|
||||
|
||||
let api;
|
||||
if (!api) {
|
||||
try {
|
||||
api = await setViewportToVTK(
|
||||
displaySet,
|
||||
activeViewportIndex,
|
||||
numRows,
|
||||
numColumns,
|
||||
layout,
|
||||
viewportSpecificData
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
const displaySet = viewportSpecificData[activeViewportIndex];
|
||||
function _setView(api, sliceNormal, viewUp) {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||
istyle.setSliceNormal(...sliceNormal);
|
||||
istyle.setViewUp(...viewUp);
|
||||
|
||||
let api;
|
||||
if (!api) {
|
||||
try {
|
||||
api = await setViewportToVTK(
|
||||
displaySet,
|
||||
activeViewportIndex,
|
||||
numRows,
|
||||
numColumns,
|
||||
layout,
|
||||
viewportSpecificData
|
||||
renderWindow.render();
|
||||
}
|
||||
|
||||
function getVOIFromCornerstoneViewport() {
|
||||
const dom = commandsManager.runCommand('getActiveViewportEnabledElement');
|
||||
const cornerstoneElement = cornerstone.getEnabledElement(dom);
|
||||
|
||||
if (cornerstoneElement) {
|
||||
const imageId = cornerstoneElement.image.imageId;
|
||||
|
||||
const { modality } = cornerstone.metaData.get(
|
||||
'generalSeriesModule',
|
||||
imageId
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
|
||||
if (modality !== 'PT') {
|
||||
const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi;
|
||||
|
||||
return {
|
||||
windowWidth,
|
||||
windowCenter,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
||||
function setVOI(voi) {
|
||||
const { windowWidth, windowCenter } = voi;
|
||||
const lower = windowCenter - windowWidth / 2.0;
|
||||
const upper = windowCenter + windowWidth / 2.0;
|
||||
|
||||
function _setView(api, sliceNormal, viewUp) {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||
istyle.setSliceNormal(...sliceNormal);
|
||||
istyle.setViewUp(...viewUp);
|
||||
const rgbTransferFunction = apis[0].volumes[0]
|
||||
.getProperty()
|
||||
.getRGBTransferFunction(0);
|
||||
|
||||
renderWindow.render();
|
||||
}
|
||||
rgbTransferFunction.setRange(lower, upper);
|
||||
|
||||
const actions = {
|
||||
axial: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
|
||||
apis[viewports.activeViewportIndex] = api;
|
||||
|
||||
_setView(api, [0, 0, 1], [0, -1, 0]);
|
||||
},
|
||||
sagittal: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
|
||||
apis[viewports.activeViewportIndex] = api;
|
||||
|
||||
_setView(api, [1, 0, 0], [0, 0, 1]);
|
||||
},
|
||||
coronal: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
|
||||
apis[viewports.activeViewportIndex] = api;
|
||||
|
||||
_setView(api, [0, 1, 0], [0, 0, 1]);
|
||||
},
|
||||
enableRotateTool: () => {
|
||||
apis.forEach(api => {
|
||||
const istyle = vtkInteractorStyleMPRRotate.newInstance();
|
||||
|
||||
api.setInteractorStyle({ istyle });
|
||||
api.updateVOI(windowWidth, windowCenter);
|
||||
});
|
||||
},
|
||||
enableCrosshairsTool: () => {
|
||||
apis.forEach((api, apiIndex) => {
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
}
|
||||
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
configuration: { apis, apiIndex },
|
||||
});
|
||||
});
|
||||
},
|
||||
enableLevelTool: () => {
|
||||
function updateVOI(apis, windowWidth, windowCenter) {
|
||||
const actions = {
|
||||
axial: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
|
||||
apis[viewports.activeViewportIndex] = api;
|
||||
|
||||
_setView(api, [0, 0, 1], [0, -1, 0]);
|
||||
},
|
||||
sagittal: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
|
||||
apis[viewports.activeViewportIndex] = api;
|
||||
|
||||
_setView(api, [1, 0, 0], [0, 0, 1]);
|
||||
},
|
||||
coronal: async ({ viewports }) => {
|
||||
const api = await _getActiveViewportVTKApi(viewports);
|
||||
|
||||
apis[viewports.activeViewportIndex] = api;
|
||||
|
||||
_setView(api, [0, 1, 0], [0, 0, 1]);
|
||||
},
|
||||
enableRotateTool: () => {
|
||||
apis.forEach(api => {
|
||||
api.updateVOI(windowWidth, windowCenter);
|
||||
const istyle = vtkInteractorStyleMPRRotate.newInstance();
|
||||
|
||||
api.setInteractorStyle({ istyle });
|
||||
});
|
||||
}
|
||||
},
|
||||
enableCrosshairsTool: () => {
|
||||
apis.forEach((api, apiIndex) => {
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
|
||||
const throttledUpdateVOIs = throttle(updateVOI, 16, { trailing: true }); // ~ 60 fps
|
||||
|
||||
const callbacks = {
|
||||
setOnLevelsChanged: ({ windowCenter, windowWidth }) => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
|
||||
renderWindow.render();
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
configuration: { apis, apiIndex },
|
||||
});
|
||||
|
||||
throttledUpdateVOIs(apis, windowWidth, windowCenter);
|
||||
},
|
||||
};
|
||||
|
||||
apis.forEach(api => {
|
||||
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
|
||||
|
||||
api.setInteractorStyle({ istyle, callbacks });
|
||||
});
|
||||
},
|
||||
setSlabThickness: ({ slabThickness }) => {
|
||||
apis.forEach(api => {
|
||||
api.setSlabThickness(slabThickness);
|
||||
});
|
||||
},
|
||||
changeSlabThickness: ({ change }) => {
|
||||
apis.forEach(api => {
|
||||
const slabThickness = Math.max(api.getSlabThickness() + change, 0.1);
|
||||
|
||||
api.setSlabThickness(slabThickness);
|
||||
});
|
||||
},
|
||||
setBlendModeToComposite: () => {
|
||||
apis.forEach(api => {
|
||||
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(slabThickness);
|
||||
}
|
||||
renderWindow.render();
|
||||
});
|
||||
},
|
||||
setBlendModeToMaximumIntensity: () => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const mapper = api.volumes[0].getMapper();
|
||||
if (mapper.setBlendModeToMaximumIntensity) {
|
||||
mapper.setBlendModeToMaximumIntensity();
|
||||
}
|
||||
renderWindow.render();
|
||||
});
|
||||
},
|
||||
setBlendMode: ({ blendMode }) => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
|
||||
api.volumes[0].getMapper().setBlendMode(blendMode);
|
||||
|
||||
renderWindow.render();
|
||||
});
|
||||
},
|
||||
mpr2d: async ({ viewports }) => {
|
||||
// TODO push a lot of this backdoor logic lower down to the library level.
|
||||
const displaySet =
|
||||
viewports.viewportSpecificData[viewports.activeViewportIndex];
|
||||
|
||||
// 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 {
|
||||
apis = await setMPRLayout(displaySet, viewportProps, 1, 3);
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (cornerstoneVOI) {
|
||||
setVOI(cornerstoneVOI);
|
||||
}
|
||||
|
||||
// Add widgets and set default interactorStyle of each viewport.
|
||||
apis.forEach((api, apiIndex) => {
|
||||
api.addSVGWidget(
|
||||
vtkSVGCrosshairsWidget.newInstance(),
|
||||
'crosshairsWidget'
|
||||
);
|
||||
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
configuration: { apis, apiIndex },
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
window.vtkActions = actions;
|
||||
|
||||
const definitions = {
|
||||
axial: {
|
||||
commandFn: actions.axial,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
coronal: {
|
||||
commandFn: actions.coronal,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
sagittal: {
|
||||
commandFn: actions.sagittal,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
enableRotateTool: {
|
||||
commandFn: actions.enableRotateTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
enableCrosshairsTool: {
|
||||
commandFn: actions.enableCrosshairsTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
enableLevelTool: {
|
||||
commandFn: actions.enableLevelTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setBlendModeToComposite: {
|
||||
commandFn: actions.setBlendModeToComposite,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.COMPOSITE_BLEND },
|
||||
},
|
||||
setBlendModeToMaximumIntensity: {
|
||||
commandFn: actions.setBlendModeToMaximumIntensity,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
|
||||
},
|
||||
setBlendModeToMinimumIntensity: {
|
||||
commandFn: actions.setBlendMode,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.MINIMUM_INTENSITY_BLEND },
|
||||
},
|
||||
setBlendModeToAverageIntensity: {
|
||||
commandFn: actions.setBlendMode,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.AVERAGE_INTENSITY_BLEND },
|
||||
},
|
||||
setSlabThickness: {
|
||||
// TODO: How do we pass in a function argument?
|
||||
commandFn: actions.setSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
increaseSlabThickness: {
|
||||
commandFn: actions.changeSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {
|
||||
change: 3,
|
||||
},
|
||||
},
|
||||
decreaseSlabThickness: {
|
||||
commandFn: actions.changeSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {
|
||||
change: -3,
|
||||
},
|
||||
},
|
||||
mpr2d: {
|
||||
commandFn: actions.mpr2d,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
context: 'VIEWER',
|
||||
},
|
||||
};
|
||||
enableLevelTool: () => {
|
||||
function updateVOI(apis, windowWidth, windowCenter) {
|
||||
apis.forEach(api => {
|
||||
api.updateVOI(windowWidth, windowCenter);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
definitions,
|
||||
defaultContext: 'ACTIVE_VIEWPORT::VTK',
|
||||
};
|
||||
const throttledUpdateVOIs = throttle(updateVOI, 16, { trailing: true }); // ~ 60 fps
|
||||
|
||||
function getVOIFromCornerstoneViewport(displaySet) {
|
||||
const cornerstoneElement = cornerstone.getEnabledElement(displaySet.dom);
|
||||
const callbacks = {
|
||||
setOnLevelsChanged: ({ windowCenter, windowWidth }) => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
|
||||
if (cornerstoneElement) {
|
||||
const imageId = cornerstoneElement.image.imageId;
|
||||
renderWindow.render();
|
||||
});
|
||||
|
||||
const { modality } = cornerstone.metaData.get(
|
||||
'generalSeriesModule',
|
||||
imageId
|
||||
);
|
||||
|
||||
if (modality !== 'PT') {
|
||||
const { windowWidth, windowCenter } = cornerstoneElement.viewport.voi;
|
||||
|
||||
return {
|
||||
windowWidth,
|
||||
windowCenter,
|
||||
throttledUpdateVOIs(apis, windowWidth, windowCenter);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setVOI(voi) {
|
||||
const { windowWidth, windowCenter } = voi;
|
||||
const lower = windowCenter - windowWidth / 2.0;
|
||||
const upper = windowCenter + windowWidth / 2.0;
|
||||
apis.forEach(api => {
|
||||
const istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
|
||||
|
||||
const rgbTransferFunction = apis[0].volumes[0]
|
||||
.getProperty()
|
||||
.getRGBTransferFunction(0);
|
||||
api.setInteractorStyle({ istyle, callbacks });
|
||||
});
|
||||
},
|
||||
setSlabThickness: ({ slabThickness }) => {
|
||||
apis.forEach(api => {
|
||||
api.setSlabThickness(slabThickness);
|
||||
});
|
||||
},
|
||||
changeSlabThickness: ({ change }) => {
|
||||
apis.forEach(api => {
|
||||
const slabThickness = Math.max(api.getSlabThickness() + change, 0.1);
|
||||
|
||||
rgbTransferFunction.setRange(lower, upper);
|
||||
api.setSlabThickness(slabThickness);
|
||||
});
|
||||
},
|
||||
setBlendModeToComposite: () => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const istyle = renderWindow.getInteractor().getInteractorStyle();
|
||||
|
||||
apis.forEach(api => {
|
||||
api.updateVOI(windowWidth, windowCenter);
|
||||
});
|
||||
}
|
||||
const slabThickness = api.getSlabThickness();
|
||||
|
||||
const mapper = api.volumes[0].getMapper();
|
||||
if (mapper.setBlendModeToComposite) {
|
||||
mapper.setBlendModeToComposite();
|
||||
}
|
||||
|
||||
if (istyle.setSlabThickness) {
|
||||
istyle.setSlabThickness(slabThickness);
|
||||
}
|
||||
renderWindow.render();
|
||||
});
|
||||
},
|
||||
setBlendModeToMaximumIntensity: () => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
const mapper = api.volumes[0].getMapper();
|
||||
if (mapper.setBlendModeToMaximumIntensity) {
|
||||
mapper.setBlendModeToMaximumIntensity();
|
||||
}
|
||||
renderWindow.render();
|
||||
});
|
||||
},
|
||||
setBlendMode: ({ blendMode }) => {
|
||||
apis.forEach(api => {
|
||||
const renderWindow = api.genericRenderWindow.getRenderWindow();
|
||||
|
||||
api.volumes[0].getMapper().setBlendMode(blendMode);
|
||||
|
||||
renderWindow.render();
|
||||
});
|
||||
},
|
||||
mpr2d: async ({ viewports }) => {
|
||||
// TODO push a lot of this backdoor logic lower down to the library level.
|
||||
const displaySet =
|
||||
viewports.viewportSpecificData[viewports.activeViewportIndex];
|
||||
|
||||
// Get current VOI if cornerstone viewport.
|
||||
const cornerstoneVOI = getVOIFromCornerstoneViewport();
|
||||
|
||||
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 {
|
||||
apis = await setMPRLayout(displaySet, viewportProps, 1, 3);
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (cornerstoneVOI) {
|
||||
setVOI(cornerstoneVOI);
|
||||
}
|
||||
|
||||
// Add widgets and set default interactorStyle of each viewport.
|
||||
apis.forEach((api, apiIndex) => {
|
||||
api.addSVGWidget(
|
||||
vtkSVGCrosshairsWidget.newInstance(),
|
||||
'crosshairsWidget'
|
||||
);
|
||||
|
||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||
|
||||
api.setInteractorStyle({
|
||||
istyle,
|
||||
configuration: { apis, apiIndex },
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
window.vtkActions = actions;
|
||||
|
||||
const definitions = {
|
||||
axial: {
|
||||
commandFn: actions.axial,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
coronal: {
|
||||
commandFn: actions.coronal,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
sagittal: {
|
||||
commandFn: actions.sagittal,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
},
|
||||
enableRotateTool: {
|
||||
commandFn: actions.enableRotateTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
enableCrosshairsTool: {
|
||||
commandFn: actions.enableCrosshairsTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
enableLevelTool: {
|
||||
commandFn: actions.enableLevelTool,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
setBlendModeToComposite: {
|
||||
commandFn: actions.setBlendModeToComposite,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.COMPOSITE_BLEND },
|
||||
},
|
||||
setBlendModeToMaximumIntensity: {
|
||||
commandFn: actions.setBlendModeToMaximumIntensity,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
|
||||
},
|
||||
setBlendModeToMinimumIntensity: {
|
||||
commandFn: actions.setBlendMode,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.MINIMUM_INTENSITY_BLEND },
|
||||
},
|
||||
setBlendModeToAverageIntensity: {
|
||||
commandFn: actions.setBlendMode,
|
||||
storeContexts: [],
|
||||
options: { blendMode: BlendMode.AVERAGE_INTENSITY_BLEND },
|
||||
},
|
||||
setSlabThickness: {
|
||||
// TODO: How do we pass in a function argument?
|
||||
commandFn: actions.setSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {},
|
||||
},
|
||||
increaseSlabThickness: {
|
||||
commandFn: actions.changeSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {
|
||||
change: 3,
|
||||
},
|
||||
},
|
||||
decreaseSlabThickness: {
|
||||
commandFn: actions.changeSlabThickness,
|
||||
storeContexts: [],
|
||||
options: {
|
||||
change: -3,
|
||||
},
|
||||
},
|
||||
mpr2d: {
|
||||
commandFn: actions.mpr2d,
|
||||
storeContexts: ['viewports'],
|
||||
options: {},
|
||||
context: 'VIEWER',
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
definitions,
|
||||
defaultContext: 'ACTIVE_VIEWPORT::VTK',
|
||||
};
|
||||
};
|
||||
|
||||
export default commandsModule;
|
||||
@@ -20,8 +20,8 @@ const vtkExtension = {
|
||||
getToolbarModule() {
|
||||
return toolbarModule;
|
||||
},
|
||||
getCommandsModule() {
|
||||
return commandsModule;
|
||||
getCommandsModule({ commandsManager }) {
|
||||
return commandsModule({ commandsManager });
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+2
-14
@@ -8,15 +8,6 @@
|
||||
# managed by CircleCI and deployed to our Google Hosting
|
||||
#
|
||||
|
||||
# Settings in the [build] context are global and are applied to all contexts
|
||||
# unless otherwise overridden by more specific contexts.
|
||||
[build]
|
||||
# Directory to change to before starting a build.
|
||||
# This is where we will look for package.json/.nvmrc/etc.
|
||||
base = ""
|
||||
publish = "project/build-output/"
|
||||
|
||||
|
||||
# NODE_VERSION in root `.nvmrc` takes priority
|
||||
# YARN_FLAGS: https://www.netlify.com/docs/build-gotchas/#yarn
|
||||
[build.environment]
|
||||
@@ -28,10 +19,8 @@
|
||||
|
||||
# Production context: all deploys from the Production branch set in your site's
|
||||
# deploy contexts will inherit these settings.
|
||||
# [context.production]
|
||||
# publish = "project/output/"
|
||||
# command = "make publish"
|
||||
# environment = { ACCESS_TOKEN = "super secret", NODE_VERSION = "8.0.1" }
|
||||
[context.production]
|
||||
ignore = "exit 0" # Never build production; We'll let our CI do that
|
||||
|
||||
# Deploy Preview context: all deploys generated from a pull/merge request will
|
||||
# inherit these settings.
|
||||
@@ -39,7 +28,6 @@
|
||||
base = ""
|
||||
publish = ".netlify/www/"
|
||||
command = "chmod +x .netlify/build-deploy-preview.sh && .netlify/build-deploy-preview.sh"
|
||||
# command = "yarn config set workspaces-experimental true && chmod +x .netlify/build-deploy-preview.sh && .netlify/build-deploy-preview.sh"
|
||||
|
||||
[[headers]]
|
||||
# Define which paths this specific [[headers]] block will cover.
|
||||
|
||||
@@ -3,6 +3,77 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.13.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.1...@ohif/core@1.13.2) (2019-12-02)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.13.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.13.0...@ohif/core@1.13.1) (2019-11-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* User Preferences Issues ([#1207](https://github.com/OHIF/Viewers/issues/1207)) ([1df21a9](https://github.com/OHIF/Viewers/commit/1df21a9e075b5e6dfc10a429ae825826f46c71b8)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.13.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.12.0...@ohif/core@1.13.0) (2019-11-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.12.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.11.0...@ohif/core@1.12.0) (2019-11-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.11.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.10.0...@ohif/core@1.11.0) (2019-11-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Issue 879 viewer route query param not filtering but promoting ([#1141](https://github.com/OHIF/Viewers/issues/1141)) ([b17f753](https://github.com/OHIF/Viewers/commit/b17f753e6222045252ef885e40233681541a32e1)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.10.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.9.1...@ohif/core@1.10.0) (2019-11-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Inject into Extension Modules / improve tests ([f63d8a7](https://github.com/OHIF/Viewers/commit/f63d8a73d867ad9dfd8ee0cad74edce180eb34f0))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.9.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.9.0...@ohif/core@1.9.1) (2019-11-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.9.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.8.0...@ohif/core@1.9.0) (2019-11-13)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "1.9.0",
|
||||
"version": "1.13.2",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cornerstone-core": "^2.2.8",
|
||||
"cornerstone-tools": "^4.0.9",
|
||||
"cornerstone-tools": "^4.8.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dicom-parser": "^1.8.3"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import hotkeys from './hotkeys';
|
||||
import log from './../log.js';
|
||||
|
||||
@@ -44,20 +43,68 @@ export class HotkeysManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a list of hotkeydefinitions. Optionally, sets the
|
||||
* default hotkey bindings for all provided definitions. These
|
||||
* Registers a list of hotkeydefinitions.
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||
*/
|
||||
setHotkeys(hotkeyDefinitions) {
|
||||
const definitions = Array.isArray(hotkeyDefinitions)
|
||||
? [...hotkeyDefinitions]
|
||||
: this._parseToArrayLike(hotkeyDefinitions);
|
||||
|
||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default hotkey bindings. These
|
||||
* values are used in `this.restoreDefaultBindings`.
|
||||
*
|
||||
* @param {HotkeyDefinition[]} hotkeyDefinitions
|
||||
* @param {Boolean} [isDefaultDefinitions]
|
||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||
*/
|
||||
setHotkeys(hotkeyDefinitions, isDefaultDefinitions = false) {
|
||||
const definitions = cloneDeep(hotkeyDefinitions);
|
||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||
setDefaultHotKeys(hotkeyDefinitions) {
|
||||
const definitions = Array.isArray(hotkeyDefinitions)
|
||||
? [...hotkeyDefinitions]
|
||||
: this._parseToArrayLike(hotkeyDefinitions);
|
||||
|
||||
if (isDefaultDefinitions) {
|
||||
this.hotkeyDefaults = definitions;
|
||||
}
|
||||
this.hotkeyDefaults = definitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* It parses given object containing hotkeyDefinition to array like.
|
||||
* Each property of given object will be mapped to an object of an array. And its property name will be the value of a property named as commandName
|
||||
*
|
||||
* @param {HotkeyDefinition[] | Object} hotkeyDefinitions Contains hotkeys definitions
|
||||
* @returns {HotkeyDefinition[]}
|
||||
*/
|
||||
_parseToArrayLike(hotkeyDefinitionsObj) {
|
||||
const copy = { ...hotkeyDefinitionsObj };
|
||||
return Object.entries(copy).map(entryValue =>
|
||||
this._parseToHotKeyObj(entryValue[0], entryValue[1])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return HotkeyDefinition object like based on given property name and property value
|
||||
* @param {string} propertyName property name of hotkey definition object
|
||||
* @param {object} propertyValue property value of hotkey definition object
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const hotKeyObj = {hotKeyDefA: {keys:[],....}}
|
||||
*
|
||||
* const parsed = _parseToHotKeyObj(Object.keys(hotKeyDefA)[0], hotKeyObj[hotKeyDefA]);
|
||||
* {
|
||||
* commandName: hotKeyDefA,
|
||||
* keys: [],
|
||||
* ....
|
||||
* }
|
||||
*
|
||||
*/
|
||||
_parseToHotKeyObj(propertyName, propertyValue) {
|
||||
return {
|
||||
commandName: propertyName,
|
||||
...propertyValue,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -94,18 +94,20 @@ describe('HotkeysManager', () => {
|
||||
expect(firstCallArgs).toEqual(hotkeyDefinitions[0]);
|
||||
expect(secondCallArgs).toEqual(hotkeyDefinitions[1]);
|
||||
});
|
||||
it('does not set this.hotkeyDefaults by default', () => {
|
||||
it('does not set this.hotkeyDefaults when calling setHotKeys', () => {
|
||||
const hotkeyDefinitions = [{ commandName: 'dance', keys: '+' }];
|
||||
|
||||
hotkeysManager.setHotkeys(hotkeyDefinitions);
|
||||
|
||||
expect(hotkeysManager.hotkeyDefaults).toEqual([]);
|
||||
});
|
||||
it('sets this.hotkeyDefaults when isDefaultDefinitions is true', () => {
|
||||
const hotkeyDefinitions = [{ commandName: 'dance', keys: '+' }];
|
||||
const isDefaultDefinitions = true;
|
||||
});
|
||||
|
||||
hotkeysManager.setHotkeys(hotkeyDefinitions, isDefaultDefinitions);
|
||||
describe('setDefaultHotKeys()', () => {
|
||||
it('it sets default hotkeys', () => {
|
||||
const hotkeyDefinitions = [{ commandName: 'dance', keys: '+' }];
|
||||
|
||||
hotkeysManager.setDefaultHotKeys(hotkeyDefinitions);
|
||||
|
||||
expect(hotkeysManager.hotkeyDefaults).toEqual(hotkeyDefinitions);
|
||||
});
|
||||
|
||||
@@ -68,7 +68,8 @@ export default class ExtensionManager {
|
||||
// preRegistrationHook
|
||||
if (extension.preRegistration) {
|
||||
extension.preRegistration({
|
||||
serviceManager: this._servicesManager,
|
||||
servicesManager: this._servicesManager,
|
||||
commandsManager: this._commandsManager,
|
||||
configuration,
|
||||
});
|
||||
}
|
||||
@@ -110,7 +111,10 @@ export default class ExtensionManager {
|
||||
}
|
||||
|
||||
try {
|
||||
const extensionModule = getModuleFn();
|
||||
const extensionModule = getModuleFn({
|
||||
servicesManager: this._servicesManager,
|
||||
commandsManager: this._commandsManager,
|
||||
});
|
||||
|
||||
if (!extensionModule) {
|
||||
log.warn(
|
||||
|
||||
@@ -40,6 +40,31 @@ describe('ExtensionManager.js', () => {
|
||||
});
|
||||
|
||||
describe('registerExtension()', () => {
|
||||
it('calls preRegistration() for extension', () => {
|
||||
// SUT
|
||||
const fakeExtension = { one: '1', preRegistration: jest.fn() };
|
||||
extensionManager.registerExtension(fakeExtension);
|
||||
|
||||
// Assert
|
||||
expect(fakeExtension.preRegistration.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('calls preRegistration() passing configuration along with servicesManager and commandsManager instances for extension', () => {
|
||||
const configuration = { config: 'Some configuration' };
|
||||
extensionManager._servicesManager = { services: { TestService: {} } };
|
||||
|
||||
// SUT
|
||||
const fakeExtension = { one: '1', preRegistration: jest.fn() };
|
||||
extensionManager.registerExtension(fakeExtension, configuration);
|
||||
|
||||
// Assert
|
||||
expect(fakeExtension.preRegistration.mock.calls[0][0]).toEqual({
|
||||
servicesManager: extensionManager._servicesManager,
|
||||
commandsManager: extensionManager._commandsManager,
|
||||
configuration,
|
||||
});
|
||||
});
|
||||
|
||||
it('logs a warning if the extension is null or undefined', () => {
|
||||
const undefinedExtension = undefined;
|
||||
const nullExtension = null;
|
||||
@@ -110,6 +135,26 @@ describe('ExtensionManager.js', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('successfully passes a servicesManager and commandsManager instances to each module', () => {
|
||||
extensionManager._servicesManager = { services: { TestService: {} } };
|
||||
|
||||
const extension = {
|
||||
id: 'hello-world',
|
||||
getViewportModule: jest.fn(),
|
||||
getSopClassHandlerModule: jest.fn(),
|
||||
getPanelModule: jest.fn(),
|
||||
getToolbarModule: jest.fn(),
|
||||
getCommandsModule: jest.fn(),
|
||||
};
|
||||
|
||||
extensionManager.registerExtension(extension);
|
||||
|
||||
expect(extension.getViewportModule.mock.calls[0][0]).toEqual({
|
||||
servicesManager: extensionManager._servicesManager,
|
||||
commandsManager: extensionManager._commandsManager,
|
||||
});
|
||||
});
|
||||
|
||||
it('successfully registers a module for each module type', () => {
|
||||
const extension = {
|
||||
id: 'hello-world',
|
||||
|
||||
@@ -19,7 +19,11 @@ import ui from './ui';
|
||||
import user from './user.js';
|
||||
import utils from './utils/';
|
||||
|
||||
import { createUiNotificationService } from './services';
|
||||
import {
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
createUIDialogService,
|
||||
} from './services';
|
||||
|
||||
const OHIF = {
|
||||
MODULE_TYPES,
|
||||
@@ -46,7 +50,9 @@ const OHIF = {
|
||||
measurements,
|
||||
hangingProtocols,
|
||||
//
|
||||
createUiNotificationService,
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
createUIDialogService,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -73,7 +79,9 @@ export {
|
||||
measurements,
|
||||
hangingProtocols,
|
||||
//
|
||||
createUiNotificationService,
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
createUIDialogService,
|
||||
};
|
||||
|
||||
export { OHIF };
|
||||
|
||||
@@ -10,7 +10,9 @@ describe('Top level exports', () => {
|
||||
'HotkeysManager',
|
||||
'ServicesManager',
|
||||
//
|
||||
'createUiNotificationService',
|
||||
'createUINotificationService',
|
||||
'createUIModalService',
|
||||
'createUIDialogService',
|
||||
//
|
||||
'utils',
|
||||
'studies',
|
||||
|
||||
@@ -12,13 +12,25 @@ import {
|
||||
SET_VIEWPORT_LAYOUT_AND_DATA,
|
||||
} from './constants/ActionTypes.js';
|
||||
|
||||
/**
|
||||
* The definition of a viewport layout.
|
||||
*
|
||||
* @typedef {Object} ViewportLayout
|
||||
* @property {number} numRows -
|
||||
* @property {number} numColumns -
|
||||
* @property {array} viewports -
|
||||
*/
|
||||
|
||||
/**
|
||||
* VIEWPORT
|
||||
*/
|
||||
export const setViewportSpecificData = (viewportIndex, data) => ({
|
||||
export const setViewportSpecificData = (
|
||||
viewportIndex,
|
||||
viewportSpecificData
|
||||
) => ({
|
||||
type: SET_VIEWPORT,
|
||||
viewportIndex,
|
||||
data,
|
||||
viewportSpecificData,
|
||||
});
|
||||
|
||||
export const setViewportActive = viewportIndex => ({
|
||||
@@ -27,10 +39,7 @@ export const setViewportActive = viewportIndex => ({
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {object} layout
|
||||
* @param {number} layout.numRows
|
||||
* @param {number} layout.numColumns
|
||||
* @param {array} layout.viewports
|
||||
* @param {ViewportLayout} layout
|
||||
*/
|
||||
export const setLayout = ({ numRows, numColumns, viewports }) => ({
|
||||
type: SET_VIEWPORT_LAYOUT,
|
||||
@@ -40,10 +49,9 @@ export const setLayout = ({ numRows, numColumns, viewports }) => ({
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {object} layout
|
||||
* @param {number} layout.numRows
|
||||
* @param {number} layout.numColumns
|
||||
* @param {array} layout.viewports
|
||||
* @param {array} viewports
|
||||
*/
|
||||
export const setViewportLayoutAndData = (
|
||||
{ numRows, numColumns, viewports },
|
||||
@@ -61,9 +69,9 @@ export const clearViewportSpecificData = viewportIndex => ({
|
||||
viewportIndex,
|
||||
});
|
||||
|
||||
export const setActiveViewportSpecificData = data => ({
|
||||
export const setActiveViewportSpecificData = viewportSpecificData => ({
|
||||
type: SET_ACTIVE_SPECIFIC_DATA,
|
||||
data,
|
||||
viewportSpecificData,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -113,13 +121,18 @@ export const setServers = servers => ({
|
||||
});
|
||||
|
||||
const actions = {
|
||||
// VIEWPORT
|
||||
/**
|
||||
* VIEWPORT
|
||||
*/
|
||||
setViewportActive,
|
||||
setViewportSpecificData,
|
||||
setViewportLayoutAndData,
|
||||
setLayout,
|
||||
clearViewportSpecificData,
|
||||
setActiveViewportSpecificData,
|
||||
/**
|
||||
* NOT-VIEWPORT
|
||||
*/
|
||||
setStudyLoadingProgress,
|
||||
clearStudyLoadingProgress,
|
||||
setUserPreferences,
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('actions', () => {
|
||||
|
||||
describe('viewport action creators', () => {
|
||||
it('should create an action to set the viewport specific data', () => {
|
||||
const data = {
|
||||
const viewportSpecificData = {
|
||||
displaySetInstanceUid: 'ef859a23-4631-93ab-d26b-7940a822c699',
|
||||
seriesDate: '20151026',
|
||||
seriesTime: '082611.370000',
|
||||
@@ -65,12 +65,12 @@ describe('actions', () => {
|
||||
|
||||
const expectedAction = {
|
||||
type: types.SET_ACTIVE_SPECIFIC_DATA,
|
||||
data,
|
||||
viewportSpecificData,
|
||||
};
|
||||
|
||||
expect(actions.setActiveViewportSpecificData(data)).toEqual(
|
||||
expectedAction
|
||||
);
|
||||
expect(
|
||||
actions.setActiveViewportSpecificData(viewportSpecificData)
|
||||
).toEqual(expectedAction);
|
||||
});
|
||||
|
||||
it('should create an action to clear clearViewportSpecificData', () => {
|
||||
|
||||
@@ -11,12 +11,15 @@ const defaultState = {
|
||||
// order, description, window (int), level (int)
|
||||
// 0: { description: 'Soft tissue', window: '', level: '' },
|
||||
},
|
||||
generalPreferences: {
|
||||
// language: 'en-US'
|
||||
},
|
||||
};
|
||||
|
||||
const preferences = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'SET_USER_PREFERENCES': {
|
||||
const newState = action.state ? action.state : cloneDeep(defaultState);
|
||||
const newState = action.state || cloneDeep(defaultState);
|
||||
|
||||
return Object.assign({}, state, newState);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import merge from 'lodash.merge';
|
||||
|
||||
import {
|
||||
CLEAR_VIEWPORT,
|
||||
SET_ACTIVE_SPECIFIC_DATA,
|
||||
@@ -8,10 +11,7 @@ import {
|
||||
SET_VIEWPORT_LAYOUT_AND_DATA,
|
||||
} from './../constants/ActionTypes.js';
|
||||
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import merge from 'lodash.merge';
|
||||
|
||||
const defaultState = {
|
||||
const DEFAULT_STATE = {
|
||||
numRows: 1,
|
||||
numColumns: 1,
|
||||
activeViewportIndex: 0,
|
||||
@@ -26,94 +26,137 @@ const defaultState = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} [state=defaultState]
|
||||
* @param {Object} action
|
||||
* @param {string} [action.type]
|
||||
* @param {number} [action.viewportIndex]
|
||||
* @param {Object} [action.layout]
|
||||
* @param {Object} [action.viewportSpecificData]
|
||||
* The definition of a viewport action.
|
||||
*
|
||||
* @typedef {Object} ViewportAction
|
||||
* @property {string} type -
|
||||
* @property {Object} data -
|
||||
* @property {Object} layout -
|
||||
* @property {number} viewportIndex -
|
||||
* @property {Object} viewportSpecificData -
|
||||
*/
|
||||
const viewports = (state = defaultState, action) => {
|
||||
let viewportSpecificData;
|
||||
|
||||
/**
|
||||
* @param {Object} [state=DEFAULT_STATE] The current viewport state.
|
||||
* @param {ViewportAction} action A viewport action.
|
||||
*/
|
||||
const viewports = (state = DEFAULT_STATE, action) => {
|
||||
let useActiveViewport = false;
|
||||
|
||||
switch (action.type) {
|
||||
case SET_VIEWPORT_ACTIVE:
|
||||
return Object.assign({}, state, {
|
||||
activeViewportIndex: action.viewportIndex,
|
||||
});
|
||||
/**
|
||||
* Sets the active viewport index.
|
||||
*
|
||||
* @return {Object} New state.
|
||||
*/
|
||||
case SET_VIEWPORT_ACTIVE: {
|
||||
return { ...state, activeViewportIndex: action.viewportIndex };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets viewport layout.
|
||||
*
|
||||
* @return {Object} New state.
|
||||
*/
|
||||
case SET_VIEWPORT_LAYOUT: {
|
||||
const { numRows, numColumns, viewports } = action;
|
||||
const layout = {
|
||||
viewports: [...viewports],
|
||||
return {
|
||||
...state,
|
||||
numRows: action.numRows,
|
||||
numColumns: action.numColumns,
|
||||
layout: { viewports: [...action.viewports] },
|
||||
};
|
||||
|
||||
return Object.assign({}, state, { numRows, numColumns, layout });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets viewport layout and data.
|
||||
*
|
||||
* @return {Object} New state.
|
||||
*/
|
||||
case SET_VIEWPORT_LAYOUT_AND_DATA: {
|
||||
const { numRows, numColumns, viewports, viewportSpecificData } = action;
|
||||
const layout = {
|
||||
viewports: [...viewports],
|
||||
return {
|
||||
...state,
|
||||
numRows: action.numRows,
|
||||
numColumns: action.numColumns,
|
||||
layout: { viewports: [...action.viewports] },
|
||||
viewportSpecificData: cloneDeep(action.viewportSpecificData),
|
||||
};
|
||||
|
||||
return Object.assign({}, state, {
|
||||
numRows,
|
||||
numColumns,
|
||||
layout,
|
||||
viewportSpecificData: cloneDeep(viewportSpecificData),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets viewport specific data of active viewport.
|
||||
*
|
||||
* @return {Object} New state.
|
||||
*/
|
||||
case SET_VIEWPORT: {
|
||||
const layout = cloneDeep(state.layout);
|
||||
const hasPlugin = action.data && action.data.plugin;
|
||||
|
||||
viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
||||
let viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
||||
viewportSpecificData[action.viewportIndex] = merge(
|
||||
{},
|
||||
viewportSpecificData[action.viewportIndex],
|
||||
action.data
|
||||
action.viewportSpecificData
|
||||
);
|
||||
|
||||
if (hasPlugin) {
|
||||
layout.viewports[action.viewportIndex].plugin = action.data.plugin;
|
||||
if (action.viewportSpecificData && action.viewportSpecificData.plugin) {
|
||||
layout.viewports[action.viewportIndex].plugin =
|
||||
action.viewportSpecificData.plugin;
|
||||
}
|
||||
|
||||
return Object.assign({}, state, { layout, viewportSpecificData });
|
||||
return { ...state, layout, viewportSpecificData };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets viewport specific data of active/any viewport.
|
||||
*
|
||||
* @return {Object} New state.
|
||||
*/
|
||||
case SET_ACTIVE_SPECIFIC_DATA:
|
||||
useActiveViewport = true;
|
||||
// Allow fall-through
|
||||
// eslint-disable-next-line
|
||||
case SET_SPECIFIC_DATA: {
|
||||
const layout = cloneDeep(state.layout);
|
||||
const hasPlugin = action.data && action.data.plugin;
|
||||
const viewportIndex = useActiveViewport
|
||||
? state.activeViewportIndex
|
||||
: action.viewportIndex;
|
||||
const { dom } = state.viewportSpecificData[viewportIndex];
|
||||
|
||||
viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
||||
let viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
||||
viewportSpecificData[viewportIndex] = {
|
||||
dom,
|
||||
...action.data,
|
||||
...action.viewportSpecificData,
|
||||
};
|
||||
|
||||
if (hasPlugin) {
|
||||
layout.viewports[viewportIndex].plugin = action.data.plugin;
|
||||
if (action.viewportSpecificData && action.viewportSpecificData.plugin) {
|
||||
layout.viewports[viewportIndex].plugin =
|
||||
action.viewportSpecificData.plugin;
|
||||
}
|
||||
|
||||
return Object.assign({}, state, { layout, viewportSpecificData });
|
||||
return { ...state, layout, viewportSpecificData };
|
||||
}
|
||||
case CLEAR_VIEWPORT:
|
||||
viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
||||
|
||||
/**
|
||||
* Clears viewport specific data of any viewport.
|
||||
*
|
||||
* @return {Object} New state.
|
||||
*/
|
||||
case CLEAR_VIEWPORT: {
|
||||
let viewportSpecificData = cloneDeep(state.viewportSpecificData);
|
||||
|
||||
if (action.viewportIndex) {
|
||||
viewportSpecificData[action.viewportIndex] = {};
|
||||
return Object.assign({}, state, { viewportSpecificData });
|
||||
return { ...state, viewportSpecificData };
|
||||
} else {
|
||||
return defaultState;
|
||||
return DEFAULT_STATE;
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
/**
|
||||
* Returns the current application state.
|
||||
*
|
||||
* @return {Object} The current state.
|
||||
*/
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('viewports reducer', () => {
|
||||
const setViewportAction = {
|
||||
type: types.SET_VIEWPORT,
|
||||
viewportIndex: viewportToSet,
|
||||
data: {
|
||||
viewportSpecificData: {
|
||||
hello: 'this is that data for the viewport',
|
||||
world: 'that will be set for the viewportIndex',
|
||||
},
|
||||
@@ -69,7 +69,7 @@ describe('viewports reducer', () => {
|
||||
const updatedState = reducer(undefined, setViewportAction);
|
||||
const updatedViewport = updatedState.viewportSpecificData[viewportToSet];
|
||||
|
||||
expect(updatedViewport).toEqual(setViewportAction.data);
|
||||
expect(updatedViewport).toEqual(setViewportAction.viewportSpecificData);
|
||||
});
|
||||
|
||||
it('should handle CLEAR_VIEWPORT', () => {
|
||||
|
||||
@@ -1,9 +1,49 @@
|
||||
import log from './../log.js';
|
||||
|
||||
export default class ServicesManager {
|
||||
constructor() {
|
||||
this.services = {};
|
||||
this.registeredServiceNames = [];
|
||||
}
|
||||
|
||||
register(service) {
|
||||
/**
|
||||
*
|
||||
* @param {Object} service
|
||||
*/
|
||||
registerService(service) {
|
||||
if (!service) {
|
||||
log.warn(
|
||||
'Attempting to register a null/undefined service. Exiting early.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let serviceName = service.name;
|
||||
|
||||
if (!serviceName) {
|
||||
log.warn(`Service name not set. Exiting early.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.registeredServiceNames.includes(serviceName)) {
|
||||
log.warn(
|
||||
`Extension name ${serviceName} has already been registered. Exiting before duplicating services.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.services[service.name] = service;
|
||||
|
||||
// Track service registration
|
||||
this.registeredServiceNames.push(serviceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of services.
|
||||
*
|
||||
* @param {Object[]} services - Array of services
|
||||
*/
|
||||
registerServices(services) {
|
||||
services.forEach(service => this.registerService(service));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import ServicesManager from './ServicesManager.js';
|
||||
import log from '../log.js';
|
||||
|
||||
jest.mock('./../log.js');
|
||||
|
||||
describe('ServicesManager.js', () => {
|
||||
let servicesManager;
|
||||
|
||||
beforeEach(() => {
|
||||
servicesManager = new ServicesManager();
|
||||
log.warn.mockClear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('registerServices()', () => {
|
||||
it('calls registerService() for each service', () => {
|
||||
servicesManager.registerService = jest.fn();
|
||||
|
||||
const fakeServices = [
|
||||
{ name: 'UINotificationTestService', hide: jest.fn() },
|
||||
{ name: 'UIModalTestService', hide: jest.fn() },
|
||||
];
|
||||
|
||||
servicesManager.registerServices(fakeServices);
|
||||
|
||||
expect(servicesManager.registerService.mock.calls.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerService()', () => {
|
||||
it('logs a warning if the service is null or undefined', () => {
|
||||
const undefinedService = undefined;
|
||||
const nullService = null;
|
||||
|
||||
servicesManager.registerService(undefinedService);
|
||||
servicesManager.registerService(nullService);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('logs a warning if the service does not have a name', () => {
|
||||
const serviceWithEmptyName = { name: '', hide: jest.fn() };
|
||||
const serviceWithoutName = { hide: jest.fn() };
|
||||
|
||||
servicesManager.registerService(serviceWithEmptyName);
|
||||
servicesManager.registerService(serviceWithoutName);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('tracks which services have been registered', () => {
|
||||
const service = {
|
||||
name: 'UINotificationService',
|
||||
};
|
||||
|
||||
servicesManager.registerService(service);
|
||||
|
||||
expect(servicesManager.registeredServiceNames).toContain(service.name);
|
||||
});
|
||||
|
||||
it('logs a warning if the service has an name that has already been registered', () => {
|
||||
const service = { name: 'UINotificationService' };
|
||||
|
||||
servicesManager.registerService(service);
|
||||
servicesManager.registerService(service);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* A UI Element Position
|
||||
*
|
||||
* @typedef {Object} ElementPosition
|
||||
* @property {number} top -
|
||||
* @property {number} left -
|
||||
* @property {number} right -
|
||||
* @property {number} bottom -
|
||||
*/
|
||||
|
||||
/**
|
||||
* UI Dialog
|
||||
*
|
||||
* @typedef {Object} DialogProps
|
||||
* @property {string} id The dialog id.
|
||||
* @property {ReactElement|HTMLElement} content The dialog content.
|
||||
* @property {Object} contentProps The dialog content props.
|
||||
* @property {boolean} [isDraggable=true] Controls if dialog content is draggable or not.
|
||||
* @property {boolean} [showOverlay=false] Controls dialog overlay.
|
||||
* @property {ElementPosition} defaultPosition Specifies the `x` and `y` that the dragged item should start at.
|
||||
* @property {ElementPosition} position If this property is present, the item becomes 'controlled' and is not responsive to user input.
|
||||
* @property {Function} onStart Called when dragging starts. If `false` is returned any handler, the action will cancel.
|
||||
* @property {Function} onStop Called when dragging stops.
|
||||
* @property {Function} onDrag Called while dragging.
|
||||
*/
|
||||
|
||||
const uiDialogServicePublicAPI = {
|
||||
name: 'UIDialogService',
|
||||
dismiss,
|
||||
dismissAll,
|
||||
create,
|
||||
setServiceImplementation,
|
||||
};
|
||||
|
||||
const uiDialogServiceImplementation = {
|
||||
_dismiss: () => console.warn('dismiss() NOT IMPLEMENTED'),
|
||||
_dismissAll: () => console.warn('dismissAll() NOT IMPLEMENTED'),
|
||||
_create: () => console.warn('create() NOT IMPLEMENTED'),
|
||||
};
|
||||
|
||||
function createUIDialogService() {
|
||||
return uiDialogServicePublicAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a new UI dialog;
|
||||
*
|
||||
* @param {DialogProps} props { id, content, contentProps, onStart, onDrag, onStop, isDraggable, showOverlay, defaultPosition, position }
|
||||
*/
|
||||
function create({
|
||||
id,
|
||||
content,
|
||||
contentProps,
|
||||
onStart,
|
||||
onDrag,
|
||||
onStop,
|
||||
isDraggable = true,
|
||||
showOverlay = false,
|
||||
defaultPosition,
|
||||
position,
|
||||
}) {
|
||||
return uiDialogServiceImplementation._create({
|
||||
id,
|
||||
content,
|
||||
contentProps,
|
||||
onStart,
|
||||
onDrag,
|
||||
onStop,
|
||||
isDraggable,
|
||||
showOverlay,
|
||||
defaultPosition,
|
||||
position,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys all dialogs, if any
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
function dismissAll() {
|
||||
return uiDialogServiceImplementation._dismissAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the dialog, if currently created
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
function dismiss({ id }) {
|
||||
return uiDialogServiceImplementation._dismiss({ id });
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {*} {
|
||||
* dismiss: dismissImplementation,
|
||||
* dismissAll: dismissAllImplementation,
|
||||
* create: createImplementation,
|
||||
* }
|
||||
*/
|
||||
function setServiceImplementation({
|
||||
dismiss: dismissImplementation,
|
||||
dismissAll: dismissAllImplementation,
|
||||
create: createImplementation,
|
||||
}) {
|
||||
if (dismissImplementation) {
|
||||
uiDialogServiceImplementation._dismiss = dismissImplementation;
|
||||
}
|
||||
if (dismissAllImplementation) {
|
||||
uiDialogServiceImplementation._dismissAll = dismissAllImplementation;
|
||||
}
|
||||
if (createImplementation) {
|
||||
uiDialogServiceImplementation._create = createImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
export default createUIDialogService;
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* UI Modal
|
||||
*
|
||||
* @typedef {Object} ModalProps
|
||||
* @property {ReactElement|HTMLElement} [content=null] Modal content.
|
||||
* @property {Object} [contentProps=null] Modal content props.
|
||||
* @property {boolean} [shouldCloseOnEsc=false] Modal is dismissible via the esc key.
|
||||
* @property {boolean} [isOpen=true] Make the Modal visible or hidden.
|
||||
* @property {boolean} [closeButton=true] Should the modal body render the close button.
|
||||
* @property {string} [title=null] Should the modal render the title independently of the body content.
|
||||
* @property {string} [customClassName=null] The custom class to style the modal.
|
||||
*/
|
||||
|
||||
const uiModalServicePublicAPI = {
|
||||
name: 'UIModalService',
|
||||
hide,
|
||||
show,
|
||||
setServiceImplementation,
|
||||
};
|
||||
|
||||
const uiModalServiceImplementation = {
|
||||
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
|
||||
_show: () => console.warn('show() NOT IMPLEMENTED'),
|
||||
};
|
||||
|
||||
function createUIModalService() {
|
||||
return uiModalServicePublicAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a new UI modal;
|
||||
*
|
||||
* @param {ModalProps} props { content, contentProps, shouldCloseOnEsc, isOpen, closeButton, title, customClassName }
|
||||
*/
|
||||
function show({
|
||||
content = null,
|
||||
contentProps = null,
|
||||
shouldCloseOnEsc = false,
|
||||
isOpen = true,
|
||||
closeButton = true,
|
||||
title = null,
|
||||
customClassName = null,
|
||||
}) {
|
||||
return uiModalServiceImplementation._show({
|
||||
content,
|
||||
contentProps,
|
||||
shouldCloseOnEsc,
|
||||
isOpen,
|
||||
closeButton,
|
||||
title,
|
||||
customClassName,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides/dismisses the modal, if currently shown
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
function hide() {
|
||||
return uiModalServiceImplementation._hide();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {*} {
|
||||
* hide: hideImplementation,
|
||||
* show: showImplementation,
|
||||
* }
|
||||
*/
|
||||
function setServiceImplementation({
|
||||
hide: hideImplementation,
|
||||
show: showImplementation,
|
||||
}) {
|
||||
if (hideImplementation) {
|
||||
uiModalServiceImplementation._hide = hideImplementation;
|
||||
}
|
||||
if (showImplementation) {
|
||||
uiModalServiceImplementation._show = showImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
export default createUIModalService;
|
||||
@@ -10,7 +10,7 @@
|
||||
* @property {boolean} [autoClose=true]
|
||||
*/
|
||||
|
||||
const uiNotificationServicePublicApi = {
|
||||
const uiNotificationServicePublicAPI = {
|
||||
name: 'UINotificationService',
|
||||
hide,
|
||||
show,
|
||||
@@ -22,8 +22,8 @@ const uiNotificationServiceImplementation = {
|
||||
_show: () => console.warn('show() NOT IMPLEMENTED'),
|
||||
};
|
||||
|
||||
function createUiNotificationService() {
|
||||
return uiNotificationServicePublicApi;
|
||||
function createUINotificationService() {
|
||||
return uiNotificationServicePublicAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,4 +81,4 @@ function setServiceImplementation({
|
||||
}
|
||||
}
|
||||
|
||||
export default createUiNotificationService;
|
||||
export default createUINotificationService;
|
||||
@@ -1,4 +1,11 @@
|
||||
import ServicesManager from './ServicesManager.js';
|
||||
import createUiNotificationService from './UINotificationService';
|
||||
import createUINotificationService from './UINotificationService';
|
||||
import createUIModalService from './UIModalService';
|
||||
import createUIDialogService from './UIDialogService';
|
||||
|
||||
export { createUiNotificationService, ServicesManager };
|
||||
export {
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
createUIDialogService,
|
||||
ServicesManager,
|
||||
};
|
||||
@@ -40,7 +40,7 @@ export default class RetrieveMetadataLoader {
|
||||
if (result && result.length) {
|
||||
break; // closes iterator in case data is retrieved successfully
|
||||
}
|
||||
} catch (e) {}
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
if (loaders.next().done && !result) {
|
||||
@@ -51,8 +51,8 @@ export default class RetrieveMetadataLoader {
|
||||
}
|
||||
|
||||
// Methods to be overwrite
|
||||
async configLoad() {}
|
||||
async preLoad() {}
|
||||
async load(preLoadData) {}
|
||||
async posLoad(loadData) {}
|
||||
async configLoad() { }
|
||||
async preLoad() { }
|
||||
async load(preLoadData) { }
|
||||
async posLoad(loadData) { }
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
import DICOMWeb from '../DICOMWeb/';
|
||||
|
||||
const WADOProxy = {
|
||||
convertURL: (url, server) => {
|
||||
// TODO: Remove all WADOProxy stuff from this file
|
||||
return url;
|
||||
},
|
||||
};
|
||||
function parseFloatArray(obj) {
|
||||
const result = [];
|
||||
|
||||
if (!obj) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const objs = obj.split('\\');
|
||||
for (let i = 0; i < objs.length; i++) {
|
||||
result.push(parseFloat(objs[i]));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a plain JS object that describes a study (a study descriptor object)
|
||||
* @param {Object} server Object with server configuration parameters
|
||||
* @param {Object} aSopInstance a SOP Instance from which study information will be added
|
||||
*/
|
||||
function createStudy(server, aSopInstance) {
|
||||
// TODO: Pass a reference ID to the server instead of including the URLs here
|
||||
return {
|
||||
seriesList: [],
|
||||
seriesMap: Object.create(null),
|
||||
seriesLoader: null,
|
||||
wadoUriRoot: server.wadoUriRoot,
|
||||
wadoRoot: server.wadoRoot,
|
||||
qidoRoot: server.qidoRoot,
|
||||
patientName: DICOMWeb.getName(aSopInstance['00100010']),
|
||||
patientId: DICOMWeb.getString(aSopInstance['00100020']),
|
||||
patientAge: DICOMWeb.getNumber(aSopInstance['00101010']),
|
||||
patientSize: DICOMWeb.getNumber(aSopInstance['00101020']),
|
||||
patientWeight: DICOMWeb.getNumber(aSopInstance['00101030']),
|
||||
accessionNumber: DICOMWeb.getString(aSopInstance['00080050']),
|
||||
studyDate: DICOMWeb.getString(aSopInstance['00080020']),
|
||||
modalities: DICOMWeb.getString(aSopInstance['00080061']),
|
||||
studyDescription: DICOMWeb.getString(aSopInstance['00081030']),
|
||||
imageCount: DICOMWeb.getString(aSopInstance['00201208']),
|
||||
studyInstanceUid: DICOMWeb.getString(aSopInstance['0020000D']),
|
||||
institutionName: DICOMWeb.getString(aSopInstance['00080080']),
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns a WADO url for an instance
|
||||
*
|
||||
* @param studyInstanceUid
|
||||
* @param seriesInstanceUid
|
||||
* @param sopInstanceUid
|
||||
* @returns {string}
|
||||
*/
|
||||
function buildInstanceWadoUrl(
|
||||
server,
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid,
|
||||
sopInstanceUid
|
||||
) {
|
||||
// TODO: This can be removed, since DICOMWebClient has the same function. Not urgent, though
|
||||
const params = [];
|
||||
|
||||
params.push('requestType=WADO');
|
||||
params.push(`studyUID=${studyInstanceUid}`);
|
||||
params.push(`seriesUID=${seriesInstanceUid}`);
|
||||
params.push(`objectUID=${sopInstanceUid}`);
|
||||
params.push('contentType=application/dicom');
|
||||
params.push('transferSyntax=*');
|
||||
|
||||
const paramString = params.join('&');
|
||||
|
||||
return `${server.wadoUriRoot}?${paramString}`;
|
||||
}
|
||||
|
||||
function buildInstanceWadoRsUri(
|
||||
server,
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid,
|
||||
sopInstanceUid
|
||||
) {
|
||||
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
|
||||
}
|
||||
|
||||
function buildInstanceFrameWadoRsUri(
|
||||
server,
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid,
|
||||
sopInstanceUid,
|
||||
frame
|
||||
) {
|
||||
const baseWadoRsUri = buildInstanceWadoRsUri(
|
||||
server,
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid,
|
||||
sopInstanceUid
|
||||
);
|
||||
frame = frame != null || 1;
|
||||
|
||||
return `${baseWadoRsUri}/frames/${frame}`;
|
||||
}
|
||||
|
||||
function getFrameIncrementPointer(element) {
|
||||
const frameIncrementPointerNames = {
|
||||
'00181065': 'frameTimeVector',
|
||||
'00181063': 'frameTime',
|
||||
};
|
||||
|
||||
if (!element || !element.Value || !element.Value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = element.Value[0];
|
||||
return frameIncrementPointerNames[value];
|
||||
}
|
||||
|
||||
function getRadiopharmaceuticalInfo(instance) {
|
||||
const modality = DICOMWeb.getString(instance['00080060']);
|
||||
|
||||
if (modality !== 'PT') {
|
||||
return;
|
||||
}
|
||||
|
||||
const radiopharmaceuticalInfo = instance['00540016'];
|
||||
if (
|
||||
radiopharmaceuticalInfo === undefined ||
|
||||
!radiopharmaceuticalInfo.Value ||
|
||||
!radiopharmaceuticalInfo.Value.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstPetRadiopharmaceuticalInfo = radiopharmaceuticalInfo.Value[0];
|
||||
return {
|
||||
radiopharmaceuticalStartTime: DICOMWeb.getString(
|
||||
firstPetRadiopharmaceuticalInfo['00181072']
|
||||
),
|
||||
radionuclideTotalDose: DICOMWeb.getNumber(
|
||||
firstPetRadiopharmaceuticalInfo['00181074']
|
||||
),
|
||||
radionuclideHalfLife: DICOMWeb.getNumber(
|
||||
firstPetRadiopharmaceuticalInfo['00181075']
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the SourceImageSequence, if it exists, in order
|
||||
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
|
||||
* is used to refer to this image in any accompanying DICOM-SR documents.
|
||||
*
|
||||
* @param instance
|
||||
* @returns {String} The ReferenceSOPInstanceUID
|
||||
*/
|
||||
function getSourceImageInstanceUid(instance) {
|
||||
// TODO= Parse the whole Source Image Sequence
|
||||
// This is a really poor workaround for now.
|
||||
// Later we should probably parse the whole sequence.
|
||||
var SourceImageSequence = instance['00082112'];
|
||||
if (
|
||||
SourceImageSequence &&
|
||||
SourceImageSequence.Value &&
|
||||
SourceImageSequence.Value.length &&
|
||||
SourceImageSequence.Value[0]['00081155'].Value
|
||||
) {
|
||||
return SourceImageSequence.Value[0]['00081155'].Value[0];
|
||||
}
|
||||
}
|
||||
|
||||
async function makeSOPInstance(server, study, instance) {
|
||||
const { studyInstanceUid } = study;
|
||||
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
|
||||
let series = study.seriesMap[seriesInstanceUid];
|
||||
|
||||
if (!series) {
|
||||
series = {
|
||||
seriesInstanceUid,
|
||||
seriesDescription: DICOMWeb.getString(instance['0008103E']),
|
||||
modality: DICOMWeb.getString(instance['00080060']),
|
||||
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
|
||||
seriesDate: DICOMWeb.getString(instance['00080021']),
|
||||
seriesTime: DICOMWeb.getString(instance['00080031']),
|
||||
instances: [],
|
||||
};
|
||||
study.seriesMap[seriesInstanceUid] = series;
|
||||
study.seriesList.push(series);
|
||||
}
|
||||
|
||||
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
|
||||
const wadouri = buildInstanceWadoUrl(
|
||||
server,
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid,
|
||||
sopInstanceUid
|
||||
);
|
||||
const baseWadoRsUri = buildInstanceWadoRsUri(
|
||||
server,
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid,
|
||||
sopInstanceUid
|
||||
);
|
||||
const wadorsuri = buildInstanceFrameWadoRsUri(
|
||||
server,
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid,
|
||||
sopInstanceUid
|
||||
);
|
||||
|
||||
const sopInstance = {
|
||||
imageType: DICOMWeb.getString(instance['00080008']),
|
||||
sopClassUid: DICOMWeb.getString(instance['00080016']),
|
||||
modality: DICOMWeb.getString(instance['00080060']),
|
||||
sopInstanceUid,
|
||||
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
|
||||
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
|
||||
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
|
||||
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
|
||||
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
|
||||
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
|
||||
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
|
||||
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
|
||||
rows: DICOMWeb.getNumber(instance['00280010']),
|
||||
columns: DICOMWeb.getNumber(instance['00280011']),
|
||||
pixelSpacing: DICOMWeb.getString(instance['00280030']),
|
||||
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
|
||||
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
|
||||
bitsStored: DICOMWeb.getNumber(instance['00280101']),
|
||||
highBit: DICOMWeb.getNumber(instance['00280102']),
|
||||
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
|
||||
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
|
||||
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
|
||||
windowCenter: DICOMWeb.getString(instance['00281050']),
|
||||
windowWidth: DICOMWeb.getString(instance['00281051']),
|
||||
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
|
||||
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
|
||||
rescaleType: DICOMWeb.getNumber(instance['00281054']),
|
||||
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
|
||||
laterality: DICOMWeb.getString(instance['00200062']),
|
||||
viewPosition: DICOMWeb.getString(instance['00185101']),
|
||||
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
|
||||
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
|
||||
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
|
||||
frameTime: DICOMWeb.getNumber(instance['00181063']),
|
||||
frameTimeVector: parseFloatArray(DICOMWeb.getString(instance['00181065'])),
|
||||
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
|
||||
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
|
||||
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
|
||||
derivationDescription: DICOMWeb.getString(instance['00282111']),
|
||||
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
|
||||
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
|
||||
echoNumber: DICOMWeb.getString(instance['00180086']),
|
||||
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
|
||||
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
|
||||
baseWadoRsUri: baseWadoRsUri,
|
||||
wadouri: WADOProxy.convertURL(wadouri, server),
|
||||
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
|
||||
wadoRoot: server.wadoRoot,
|
||||
imageRendering: server.imageRendering,
|
||||
thumbnailRendering: server.thumbnailRendering,
|
||||
};
|
||||
|
||||
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
|
||||
if (sopInstance.photometricInterpretation === 'PALETTE COLOR') {
|
||||
const redPaletteColorLookupTableDescriptor = parseFloatArray(
|
||||
DICOMWeb.getString(instance['00281101'])
|
||||
);
|
||||
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
|
||||
DICOMWeb.getString(instance['00281102'])
|
||||
);
|
||||
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
|
||||
DICOMWeb.getString(instance['00281103'])
|
||||
);
|
||||
const palettes = await getPaletteColors(
|
||||
server,
|
||||
instance,
|
||||
redPaletteColorLookupTableDescriptor
|
||||
);
|
||||
|
||||
if (palettes) {
|
||||
if (palettes.uid) {
|
||||
sopInstance.paletteColorLookupTableUID = palettes.uid;
|
||||
}
|
||||
|
||||
sopInstance.redPaletteColorLookupTableData = palettes.red;
|
||||
sopInstance.greenPaletteColorLookupTableData = palettes.green;
|
||||
sopInstance.bluePaletteColorLookupTableData = palettes.blue;
|
||||
sopInstance.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
|
||||
sopInstance.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
|
||||
sopInstance.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
|
||||
}
|
||||
}
|
||||
|
||||
series.instances.push(sopInstance);
|
||||
return sopInstance;
|
||||
}
|
||||
|
||||
function getPaletteColor(server, instance, tag, lutDescriptor) {
|
||||
const numLutEntries = lutDescriptor[0];
|
||||
const bits = lutDescriptor[2];
|
||||
|
||||
let uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server);
|
||||
|
||||
// TODO: Workaround for dcm4chee behind SSL-terminating proxy returning
|
||||
// incorrect bulk data URIs
|
||||
if (server.wadoRoot.indexOf('https') === 0 && !uri.includes('https')) {
|
||||
uri = uri.replace('http', 'https');
|
||||
}
|
||||
|
||||
const config = {
|
||||
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
|
||||
headers: DICOMWeb.getAuthorizationHeader(server),
|
||||
};
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
const options = {
|
||||
BulkDataURI: uri,
|
||||
};
|
||||
|
||||
const readUInt16 = (byteArray, position) => {
|
||||
return byteArray[position] + byteArray[position + 1] * 256;
|
||||
};
|
||||
|
||||
const arrayBufferToPaletteColorLUT = result => {
|
||||
const arraybuffer = result[0];
|
||||
const byteArray = new Uint8Array(arraybuffer);
|
||||
const lut = [];
|
||||
|
||||
for (let i = 0; i < numLutEntries; i++) {
|
||||
if (bits === 16) {
|
||||
lut[i] = readUInt16(byteArray, i * 2);
|
||||
} else {
|
||||
lut[i] = byteArray[i];
|
||||
}
|
||||
}
|
||||
|
||||
return lut;
|
||||
};
|
||||
|
||||
return dicomWeb.retrieveBulkData(options).then(arrayBufferToPaletteColorLUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch palette colors for instances with "PALETTE COLOR" photometricInterpretation.
|
||||
*
|
||||
* @param server {Object} Current server;
|
||||
* @param instance {Object} The retrieved instance metadata;
|
||||
* @returns {String} The ReferenceSOPInstanceUID
|
||||
*/
|
||||
async function getPaletteColors(server, instance, lutDescriptor) {
|
||||
let paletteUID = DICOMWeb.getString(instance['00281199']);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let entry;
|
||||
if (paletteColorCache.isValidUID(paletteUID)) {
|
||||
entry = paletteColorCache.get(paletteUID);
|
||||
|
||||
if (entry) {
|
||||
return resolve(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// no entry in cache... Fetch remote data.
|
||||
const r = getPaletteColor(server, instance, '00281201', lutDescriptor);
|
||||
const g = getPaletteColor(server, instance, '00281202', lutDescriptor);
|
||||
const b = getPaletteColor(server, instance, '00281203', lutDescriptor);
|
||||
|
||||
const promises = [r, g, b];
|
||||
|
||||
Promise.all(promises).then(args => {
|
||||
entry = {
|
||||
red: args[0],
|
||||
green: args[1],
|
||||
blue: args[2],
|
||||
};
|
||||
|
||||
// when paletteUID is present, the entry can be cached...
|
||||
entry.uid = paletteUID;
|
||||
paletteColorCache.add(entry);
|
||||
|
||||
resolve(entry);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a list of SOP Instances to a given study object descriptor
|
||||
* @param {Object} server Object with server configuration parameters
|
||||
* @param {Object} study The study descriptor to which the given SOP instances will be added
|
||||
* @param {Array} sopInstanceList A list of SOP instance objects
|
||||
*/
|
||||
async function addInstancesToStudy(server, study, sopInstanceList) {
|
||||
return Promise.all(
|
||||
sopInstanceList.map(function(sopInstance) {
|
||||
return makeSOPInstance(server, study, sopInstance);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const createStudyFromSOPInstanceList = async (server, sopInstanceList) => {
|
||||
if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) {
|
||||
const firstSopInstance = sopInstanceList[0];
|
||||
const study = createStudy(server, firstSopInstance);
|
||||
await addInstancesToStudy(server, study, sopInstanceList);
|
||||
return study;
|
||||
}
|
||||
throw new Error('Failed to create study out of provided SOP instance list');
|
||||
};
|
||||
|
||||
export { createStudyFromSOPInstanceList, addInstancesToStudy };
|
||||
@@ -3,6 +3,28 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.52.2](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.1...@ohif/i18n@0.52.2) (2019-11-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* User Preferences Issues ([#1207](https://github.com/OHIF/Viewers/issues/1207)) ([1df21a9](https://github.com/OHIF/Viewers/commit/1df21a9e075b5e6dfc10a429ae825826f46c71b8)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.52.1](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.0...@ohif/i18n@0.52.1) (2019-11-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* minor date picker UX improvements ([813ee5e](https://github.com/OHIF/Viewers/commit/813ee5ed4d78b7bda234922d5f3389efe346451c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.52.0](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.51.0...@ohif/i18n@0.52.0) (2019-11-06)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/i18n",
|
||||
"version": "0.52.0",
|
||||
"version": "0.52.2",
|
||||
"description": "Internationalization library for The OHIF Viewer",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -42,6 +42,7 @@ const locizeOptions = {
|
||||
|
||||
const envUseLocize = !!config.USE_LOCIZE;
|
||||
const envApiKeyAvailable = !!config.LOCIZE_API_KEY;
|
||||
const DEFAULT_LANGUAGE = 'en-US';
|
||||
|
||||
function initI18n(
|
||||
detection = detectionOptions,
|
||||
@@ -74,7 +75,7 @@ function initI18n(
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
fallbackLng: 'en-US',
|
||||
fallbackLng: DEFAULT_LANGUAGE,
|
||||
saveMissing: apiKeyAvailable,
|
||||
debug: debugMode,
|
||||
keySeparator: false,
|
||||
@@ -111,7 +112,7 @@ function initI18n(
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
fallbackLng: 'en-US',
|
||||
fallbackLng: DEFAULT_LANGUAGE,
|
||||
resources: locales,
|
||||
debug: debugMode,
|
||||
keySeparator: false,
|
||||
@@ -136,5 +137,6 @@ customDebug(`version ${pkg.version} loaded.`, 'info');
|
||||
i18n.initializing = initI18n();
|
||||
i18n.initI18n = initI18n;
|
||||
i18n.addLocales = addLocales;
|
||||
i18n.defaultLanguage = DEFAULT_LANGUAGE;
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"No hotkeys found": "Nenhuma tecla de atalho está configurada para este aplicativo. As teclas de atalho podem ser configuradas no arquivo app-config.js do aplicativo."
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import UserPreferencesModal from "./UserPreferencesModal.json";
|
||||
|
||||
export default {
|
||||
'ar': {
|
||||
UserPreferencesModal,
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"Close": "Close",
|
||||
"Image": "Image",
|
||||
"Layout": "Layout",
|
||||
"Measurements": "Measurements",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"Clear dates": "Clear dates",
|
||||
"End Date": "End Date",
|
||||
"Start Date": "Start Date"
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"Cancel": "$t(Buttons:Cancel)",
|
||||
"No hotkeys found": "No hotkeys are configured for this application. Hotkeys can be configured in the application's app-config.js file.",
|
||||
"Reset to Defaults": "$t(Buttons:Reset to Defaults)",
|
||||
"ResetDefaultMessage": "Preferences successfully reset to default. <br /> You must <strong>Save</strong> to perform this action.",
|
||||
"Save": "$t(Buttons:Save)",
|
||||
"SaveMessage": "Preferences saved",
|
||||
"User Preferences": "User Preferences"
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import AboutModal from './AboutModal.json';
|
||||
import Buttons from './Buttons.json';
|
||||
import CineDialog from './CineDialog.json';
|
||||
import Common from './Common.json';
|
||||
import DatePicker from './DatePicker.json';
|
||||
import Header from './Header.json';
|
||||
import MeasurementTable from './MeasurementTable.json';
|
||||
import StudyList from './StudyList.json';
|
||||
@@ -13,6 +14,7 @@ export default {
|
||||
Buttons,
|
||||
CineDialog,
|
||||
Common,
|
||||
DatePicker,
|
||||
Header,
|
||||
MeasurementTable,
|
||||
StudyList,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import ar from './ar/';
|
||||
import en_US from './en-US/';
|
||||
import es from './es/';
|
||||
import ja_JP from './ja-JP/';
|
||||
@@ -7,6 +8,7 @@ import vi from './vi/';
|
||||
import zh from './zh/';
|
||||
|
||||
export default {
|
||||
...ar,
|
||||
...en_US,
|
||||
...es,
|
||||
...ja_JP,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"Close": "Fechar",
|
||||
"Image": "Imagem",
|
||||
"Layout": "Layout",
|
||||
"More": "Mais",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"Clear dates": "Limpar datas",
|
||||
"End Date": "Data Final",
|
||||
"Start Date": "Data Inicial"
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"Cancel": "Cancelar",
|
||||
"Reset to Defaults": "Restaurar Default",
|
||||
"ResetDefaultMessage": "Preferências resetadas com sucesso. <br /> Você deve <strong>Salvar</strong> para que essa ação seja realizada.",
|
||||
"Save": "Salvar",
|
||||
"SaveMessage": "Preferências salvas",
|
||||
"User Preferences": "Preferências do Usuário"
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import AboutModal from './AboutModal.json';
|
||||
import Buttons from './Buttons.json';
|
||||
import CineDialog from './CineDialog.json';
|
||||
import Common from './Common.json';
|
||||
import DatePicker from './DatePicker.json';
|
||||
import Header from './Header.json';
|
||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||
|
||||
@@ -11,6 +12,7 @@ export default {
|
||||
Buttons,
|
||||
CineDialog,
|
||||
Common,
|
||||
DatePicker,
|
||||
Header,
|
||||
UserPreferencesModal,
|
||||
},
|
||||
|
||||
@@ -3,6 +3,104 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.65.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.65.0...@ohif/ui@0.65.1) (2019-11-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* User Preferences Issues ([#1207](https://github.com/OHIF/Viewers/issues/1207)) ([1df21a9](https://github.com/OHIF/Viewers/commit/1df21a9e075b5e6dfc10a429ae825826f46c71b8)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.65.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.64.2...@ohif/ui@0.65.0) (2019-11-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add new annotate tool using new dialog service ([#1211](https://github.com/OHIF/Viewers/issues/1211)) ([8fd3af1](https://github.com/OHIF/Viewers/commit/8fd3af1e137e793f1b482760a22591c64a072047))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.64.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.64.1...@ohif/ui@0.64.2) (2019-11-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Issue branch from danny experimental changes pr 1128 ([#1150](https://github.com/OHIF/Viewers/issues/1150)) ([a870b3c](https://github.com/OHIF/Viewers/commit/a870b3cc6056cf824af422e46f1ad674910b534e)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.64.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.64.0...@ohif/ui@0.64.1) (2019-11-20)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.64.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.63.0...@ohif/ui@0.64.0) (2019-11-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.63.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.4...@ohif/ui@0.63.0) (2019-11-19)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Issue 879 viewer route query param not filtering but promoting ([#1141](https://github.com/OHIF/Viewers/issues/1141)) ([b17f753](https://github.com/OHIF/Viewers/commit/b17f753e6222045252ef885e40233681541a32e1)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.62.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.3...@ohif/ui@0.62.4) (2019-11-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* minor date picker UX improvements ([813ee5e](https://github.com/OHIF/Viewers/commit/813ee5ed4d78b7bda234922d5f3389efe346451c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.62.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.2...@ohif/ui@0.62.3) (2019-11-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.62.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.1...@ohif/ui@0.62.2) (2019-11-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.62.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.0...@ohif/ui@0.62.1) (2019-11-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/ui
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.62.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.61.0...@ohif/ui@0.62.0) (2019-11-13)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/ui",
|
||||
"version": "0.62.0",
|
||||
"version": "0.65.1",
|
||||
"description": "A set of React components for Medical Imaging Viewers",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
@@ -52,7 +52,9 @@
|
||||
"react-dnd": "9.4.0",
|
||||
"react-dnd-html5-backend": "^9.4.0",
|
||||
"react-dnd-touch-backend": "^9.4.0",
|
||||
"react-draggable": "^4.1.0",
|
||||
"react-i18next": "^10.11.0",
|
||||
"react-modal": "^3.11.1",
|
||||
"react-with-direction": "1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import './CineDialog.styl';
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
import { withTranslation } from '../../contextProviders';
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ const AboutContent = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div data-cy="about-modal">
|
||||
<div className="AboutContent" data-cy="about-modal">
|
||||
<div className="btn-group">
|
||||
<a
|
||||
className="btn btn-default"
|
||||
|
||||
@@ -189,7 +189,7 @@ const ViewportDownloadForm = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="ViewportDownloadForm">
|
||||
<div className="title">
|
||||
{t(
|
||||
'Please specify the dimensions, filename, and desired type for the output image.'
|
||||
@@ -284,10 +284,6 @@ const ViewportDownloadForm = ({
|
||||
className="viewport-preview"
|
||||
src={viewportPreview.src}
|
||||
alt="Viewport Preview"
|
||||
style={{
|
||||
height: viewportPreview.height,
|
||||
width: viewportPreview.width,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -303,7 +299,7 @@ const ViewportDownloadForm = ({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,15 +3,13 @@
|
||||
@import '../../../design/styles/common/button.styl'
|
||||
|
||||
.ViewportDownloadForm
|
||||
color: var(--text-secondary-color);
|
||||
filter: drop-shadow(0 0 3px var(--ui-gray-darkest));
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
width: inherit;
|
||||
padding: 15px;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1080 !important;
|
||||
|
||||
input, select
|
||||
max-height: 30px;
|
||||
|
||||
.title
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
@@ -67,6 +65,7 @@
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
align-self: center;
|
||||
margin-bottom: 20px;
|
||||
@media screen and (max-width: 1023px)
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
@@ -90,7 +89,6 @@
|
||||
|
||||
.actions
|
||||
display: flex;
|
||||
height: 60px;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import i18n from '@ohif/i18n';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './LanguageSwitcher.styl';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
import { withTranslation } from '../../contextProviders';
|
||||
|
||||
const LanguageSwitcher = () => {
|
||||
const getCurrentLanguage = (language = i18n.language) =>
|
||||
language.split('-')[0];
|
||||
const LanguageSwitcher = ({ language, onLanguageChange }) => {
|
||||
const parseLanguage = lang => lang.split('-')[0];
|
||||
|
||||
const [currentLanguage, setCurrentLanguage] = useState(getCurrentLanguage());
|
||||
const languages = [
|
||||
// TODO: list of available languages should come from i18n.options.resources
|
||||
{
|
||||
@@ -21,46 +19,31 @@ const LanguageSwitcher = () => {
|
||||
},
|
||||
];
|
||||
|
||||
const onChange = () => {
|
||||
const onChange = event => {
|
||||
const { value } = event.target;
|
||||
const language = getCurrentLanguage(value);
|
||||
setCurrentLanguage(language);
|
||||
|
||||
i18n.init({
|
||||
fallbackLng: language,
|
||||
lng: language,
|
||||
});
|
||||
onLanguageChange(parseLanguage(value));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
i18n.on('languageChanged', () => {
|
||||
if (mounted) {
|
||||
setCurrentLanguage(getCurrentLanguage());
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<select
|
||||
name="language-select"
|
||||
id="language-select"
|
||||
className="language-select"
|
||||
value={currentLanguage}
|
||||
value={parseLanguage(language)}
|
||||
onChange={onChange}
|
||||
>
|
||||
{languages.map(language => (
|
||||
<option key={language.value} value={language.value}>
|
||||
{language.label}
|
||||
{languages.map(lng => (
|
||||
<option key={lng.value} value={lng.value}>
|
||||
{lng.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
};
|
||||
|
||||
LanguageSwitcher.propTypes = {
|
||||
language: PropTypes.string.isRequired,
|
||||
onLanguageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withTranslation('UserPreferencesModal')(LanguageSwitcher);
|
||||
@@ -1,7 +1,7 @@
|
||||
import './MeasurementTable.styl';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
import { withTranslation } from '../../contextProviders';
|
||||
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import { MeasurementTableItem } from './MeasurementTableItem.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
import { withTranslation } from '../../contextProviders';
|
||||
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import { OverlayTrigger } from './../overlayTrigger';
|
||||
|
||||
@@ -1,60 +1,73 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactBootstrapModal from 'react-bootstrap-modal';
|
||||
import Modal from 'react-modal';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import './OHIFModal.styl';
|
||||
|
||||
const customStyle = {
|
||||
overlay: {
|
||||
zIndex: 1071,
|
||||
backgroundColor: 'rgb(0, 0, 0, 0.5)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
};
|
||||
|
||||
Modal.setAppElement(document.getElementById('root'));
|
||||
|
||||
const OHIFModal = ({
|
||||
className,
|
||||
closeButton,
|
||||
backdrop,
|
||||
keyboard,
|
||||
show,
|
||||
shouldCloseOnEsc,
|
||||
isOpen,
|
||||
title,
|
||||
onHide,
|
||||
footer: Footer,
|
||||
header: Header,
|
||||
children: Component,
|
||||
}) => (
|
||||
<ReactBootstrapModal
|
||||
className={classNames('modal fade themed in', className)}
|
||||
backdrop={backdrop}
|
||||
keyboard={keyboard}
|
||||
show={show}
|
||||
large={true}
|
||||
title={title}
|
||||
onHide={onHide}
|
||||
>
|
||||
{(Header || title) && (
|
||||
<ReactBootstrapModal.Header closeButton={closeButton}>
|
||||
{title && (
|
||||
<ReactBootstrapModal.Title>{title}</ReactBootstrapModal.Title>
|
||||
)}
|
||||
{Header && <Header hide={onHide} />}
|
||||
</ReactBootstrapModal.Header>
|
||||
)}
|
||||
<ReactBootstrapModal.Body>
|
||||
{Component && <Component hide={onHide} />}
|
||||
</ReactBootstrapModal.Body>
|
||||
{Footer && (
|
||||
<ReactBootstrapModal.Footer>
|
||||
{' '}
|
||||
<Footer hide={onHide} />
|
||||
</ReactBootstrapModal.Footer>
|
||||
)}
|
||||
</ReactBootstrapModal>
|
||||
);
|
||||
onClose,
|
||||
children,
|
||||
}) => {
|
||||
const renderHeader = () => {
|
||||
return (
|
||||
title && (
|
||||
<div className="OHIFModal__header">
|
||||
<h4>{title}</h4>
|
||||
{closeButton && (
|
||||
<button data-cy="close-button" onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={classNames('OHIFModal', className)}
|
||||
shouldCloseOnEsc={shouldCloseOnEsc}
|
||||
isOpen={isOpen}
|
||||
title={title}
|
||||
style={customStyle}
|
||||
>
|
||||
<>
|
||||
{renderHeader()}
|
||||
<div className="OHIFModal__content">{children}</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
OHIFModal.propTypes = {
|
||||
className: PropTypes.string,
|
||||
closeButton: PropTypes.bool,
|
||||
backdrop: PropTypes.bool,
|
||||
keyboard: PropTypes.bool,
|
||||
show: PropTypes.bool,
|
||||
shouldCloseOnEsc: PropTypes.bool,
|
||||
isOpen: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
onHide: PropTypes.func,
|
||||
footer: PropTypes.node,
|
||||
header: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
onClose: PropTypes.func,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
export default OHIFModal;
|
||||
@@ -0,0 +1,67 @@
|
||||
.OHIFModal
|
||||
background-color: var(--ui-gray-darker)
|
||||
border-color: var(--ui-border-color)
|
||||
color: var(--text-secondary-color)
|
||||
border-radius: 6px
|
||||
border: 0
|
||||
color: var(--text-primary-color)
|
||||
position: relative
|
||||
-webkit-box-shadow: 0 3px 9px rgba(0,0,0,.5)
|
||||
box-shadow: 0 3px 9px rgba(0,0,0,.5);
|
||||
background-clip: padding-box
|
||||
outline: 0
|
||||
|
||||
@media (min-width: 320px)
|
||||
width: 78%
|
||||
min-width: 300px
|
||||
|
||||
@media (min-width: 768px)
|
||||
width: 600px
|
||||
|
||||
@media (min-width: 992px)
|
||||
width: 900px
|
||||
|
||||
&__content
|
||||
padding: 20px
|
||||
max-height: 90vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
&::-webkit-scrollbar
|
||||
display: none;
|
||||
|
||||
&__header
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: center
|
||||
border-bottom-width: 3px
|
||||
border-bottom-style: solid
|
||||
border-bottom-color: #000000
|
||||
padding: 20px
|
||||
position: relative
|
||||
|
||||
h4
|
||||
color: var(--text-secondary-color)
|
||||
font-size: 20px
|
||||
font-weight: 500
|
||||
line-height: 24px
|
||||
padding-right: 24px
|
||||
margin: 0
|
||||
|
||||
button
|
||||
cursor: pointer
|
||||
margin: -10px 0 0 0
|
||||
padding: 0
|
||||
background-color: transparent
|
||||
border: none
|
||||
color: var(--text-secondary-color)
|
||||
font-size: 25px
|
||||
font-weight: lighter
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:focus:active
|
||||
background-image: none
|
||||
outline: 0
|
||||
box-shadow: none
|
||||
@@ -3,7 +3,7 @@ import React, { cloneElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Overlay as BaseOverlay } from 'react-overlays';
|
||||
import elementType from 'prop-types-extra/lib/elementType';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
import { withTranslation } from '../../contextProviders';
|
||||
|
||||
import Fade from './Fade';
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TextInput } from '@ohif/ui';
|
||||
|
||||
import './SimpleDialog.styl';
|
||||
|
||||
@@ -21,6 +22,31 @@ class SimpleDialog extends Component {
|
||||
rootClass: '',
|
||||
};
|
||||
|
||||
static InputDialog = ({ onSubmit, defaultValue, title, label, onClose }) => {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
|
||||
const onSubmitHandler = () => {
|
||||
onSubmit(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="InputDialog">
|
||||
<SimpleDialog
|
||||
headerTitle={title}
|
||||
onClose={onClose}
|
||||
onConfirm={onSubmitHandler}
|
||||
>
|
||||
<TextInput
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={event => setValue(event.target.value)}
|
||||
label={label}
|
||||
/>
|
||||
</SimpleDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
@import './../../design/styles/common/button.styl'
|
||||
@import './../../design/styles/common/global.styl'
|
||||
|
||||
.InputDialog
|
||||
.simpleDialog
|
||||
position: relative
|
||||
|
||||
.simpleDialog
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import SnackbarItem from './SnackbarItem';
|
||||
import './Snackbar.css';
|
||||
import { useSnackbarContext } from '../../utils/SnackbarProvider';
|
||||
import { useSnackbarContext } from '../../contextProviders';
|
||||
|
||||
const SnackbarContainer = () => {
|
||||
const { snackbarItems, hide } = useSnackbarContext();
|
||||
@@ -37,8 +37,8 @@ const SnackbarContainer = () => {
|
||||
|
||||
return (
|
||||
<div key={pos} className={`sb-container sb-${pos}`}>
|
||||
{items[pos].map(item => (
|
||||
<div key={item.id}>{renderItem(item)}</div>
|
||||
{items[pos].map((item, index) => (
|
||||
<div key={item.id + index}>{renderItem(item)}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -102,14 +102,14 @@ function ImageThumbnail(props) {
|
||||
{shouldRenderToCanvas() ? (
|
||||
<canvas ref={canvasRef} width={width} height={height} />
|
||||
) : (
|
||||
<img
|
||||
className="static-image"
|
||||
src={imageSrc}
|
||||
//width={this.props.width}
|
||||
height={height}
|
||||
alt={''}
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
className="static-image"
|
||||
src={imageSrc}
|
||||
//width={this.props.width}
|
||||
height={height}
|
||||
alt={''}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{loadingOrError}
|
||||
{showStackLoadingProgressBar && (
|
||||
|
||||
@@ -2,22 +2,23 @@
|
||||
|
||||
/* Will edit everything selected including everything between a range of dates */
|
||||
.CalendarDay__selected_span {
|
||||
background: var(--table-text-secondary-color);
|
||||
color: #fff;
|
||||
background: var(--datePicker-picker-background);
|
||||
color: var(--datePicker-picker-text);
|
||||
border-color: #e4e7e7;
|
||||
}
|
||||
|
||||
/* Will edit selected date or the endpoints of a range of dates */
|
||||
.CalendarDay__selected {
|
||||
background: var(--table-text-secondary-color);
|
||||
color: #fff;
|
||||
background: var(--datePicker-picker-background);
|
||||
color: var(--datePicker-picker-text);
|
||||
border-color: #e4e7e7;
|
||||
}
|
||||
|
||||
/* Will edit when hovered over. _span style also has this property */
|
||||
.CalendarDay__selected:hover {
|
||||
background: var(--table-text-secondary-color);
|
||||
color: #fff;
|
||||
.CalendarDay__selected:hover,
|
||||
.CalendarDay__default:hover {
|
||||
background: var(--datePicker-picker-background);
|
||||
color: var(--datePicker-picker-text);
|
||||
border-color: #e4e7e7;
|
||||
}
|
||||
|
||||
@@ -25,8 +26,8 @@
|
||||
is not yet selected. Edits the dates between your mouse and said date */
|
||||
.CalendarDay__hovered_span:hover,
|
||||
.CalendarDay__hovered_span {
|
||||
background: var(--table-text-secondary-color);
|
||||
color: #fff;
|
||||
background: var(--datePicker-picker-background);
|
||||
color: var(--datePicker-picker-text);
|
||||
border-color: #e4e7e7;
|
||||
}
|
||||
|
||||
@@ -42,15 +43,15 @@ is not yet selected. Edits the dates between your mouse and said date */
|
||||
/* Container - visual */
|
||||
.DateRangePickerInput {
|
||||
width: calc(100% - 10px); /* Just use padding? */
|
||||
background-color: var(--input-background-color);
|
||||
border-color: var(--input-background-color);
|
||||
color: var(--input-placeholder-color);
|
||||
background-color: var(--datePicker-input-background);
|
||||
border-color: var(--datePicker-input-background);
|
||||
color: var(--datePicker-picker-text-input);
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.DateRangePickerInput.DateRangePickerInput__withBorder {
|
||||
border-radius: 4px;
|
||||
background-color: var(--input-background-color);
|
||||
background-color: var(--datePicker-input-background);
|
||||
}
|
||||
|
||||
/* Input Container */
|
||||
@@ -58,14 +59,14 @@ is not yet selected. Edits the dates between your mouse and said date */
|
||||
width: 97px;
|
||||
height: 38px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--input-background-color);
|
||||
background-color: var(--datePicker-input-background);
|
||||
}
|
||||
|
||||
/* Actual Input Element */
|
||||
.DateInput > .DateInput_input {
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
color: var(--input-placeholder-color);
|
||||
color: var(--datePicker-picker-text-input);
|
||||
height: 38px;
|
||||
font-size: 10pt;
|
||||
padding: 0;
|
||||
@@ -80,4 +81,39 @@ is not yet selected. Edits the dates between your mouse and said date */
|
||||
.PresetDateRangePicker_button {
|
||||
margin: 0;
|
||||
padding: 4px 8px;
|
||||
color: #00a699;
|
||||
background: var(--datePicker-picker-text-input);
|
||||
}
|
||||
|
||||
.PresetDateRangePicker_button:hover {
|
||||
background: #00a699;
|
||||
color: var(--datePicker-picker-text-input);
|
||||
}
|
||||
|
||||
.CalendarDay__blocked_out_of_range:hover {
|
||||
background: #fff;
|
||||
cursor: not-allowed;
|
||||
color: var(--datePicker-picker-text-disabled);
|
||||
}
|
||||
|
||||
.DateRangePicker_select {
|
||||
background-color: white;
|
||||
color: var(--datePicker-picker-text);
|
||||
border: 1px solid #e4e7e7;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
padding: 3px 20px 3px 10px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
|
||||
background-image: linear-gradient(45deg, transparent 50%, gray 50%),
|
||||
linear-gradient(135deg, gray 50%, transparent 50%);
|
||||
background-position: calc(100% - 11px) 11px, calc(100% - 6px) calc(11px);
|
||||
background-size: 5px 5px, 5px 5px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.DateRangePicker_select:hover {
|
||||
border-color: #c4c4c4;
|
||||
}
|
||||
@@ -7,39 +7,25 @@ import './CustomDateRangePicker.css';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DateRangePicker } from 'react-dates';
|
||||
import moment from 'moment';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default class CustomDateRangePicker extends React.Component {
|
||||
static propTypes = {
|
||||
presets: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
text: PropTypes.string,
|
||||
start: PropTypes.required,
|
||||
end: PropTypes.required,
|
||||
})
|
||||
),
|
||||
};
|
||||
function CustomDateRangePicker(props) {
|
||||
moment.locale(i18n.language); // using i18n in the date picker
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
startDate: this.props.startDate,
|
||||
endDate: this.props.endDate,
|
||||
};
|
||||
const { t } = useTranslation('DatePicker');
|
||||
|
||||
this.renderDatePresets = this.renderDatePresets.bind(this);
|
||||
this.onDatesChange = this.onDatesChange.bind(this);
|
||||
}
|
||||
const {
|
||||
onDatesChange,
|
||||
startDate,
|
||||
endDate,
|
||||
presets,
|
||||
...dateRangePickerProps
|
||||
} = props;
|
||||
|
||||
onDatesChange({ startDate, endDate, preset }) {
|
||||
this.setState({ startDate, endDate });
|
||||
if (this.props.onDatesChange) {
|
||||
this.props.onDatesChange({ startDate, endDate, preset });
|
||||
}
|
||||
}
|
||||
|
||||
renderDatePresets() {
|
||||
const { presets } = this.props;
|
||||
const { startDate, endDate } = this.state;
|
||||
const renderDatePresets = () => {
|
||||
const { presets } = props;
|
||||
|
||||
return (
|
||||
<div className="PresetDateRangePicker_panel">
|
||||
@@ -54,7 +40,7 @@ export default class CustomDateRangePicker extends React.Component {
|
||||
isSelected ? 'PresetDateRangePicker_button__selected' : ''
|
||||
}`}
|
||||
onClick={() =>
|
||||
this.onDatesChange({
|
||||
onDatesChange({
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
preset: true,
|
||||
@@ -67,30 +53,88 @@ export default class CustomDateRangePicker extends React.Component {
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
const renderMonthElement = ({ month, onMonthSelect, onYearSelect }) => {
|
||||
const containerStyle = {
|
||||
margin: '0 5px',
|
||||
};
|
||||
|
||||
render() {
|
||||
let {
|
||||
autoFocus,
|
||||
autoFocusEndDate,
|
||||
initialStartDate,
|
||||
initialEndDate,
|
||||
stateDateWrapper,
|
||||
onDatesChange,
|
||||
startDate,
|
||||
endDate,
|
||||
presets,
|
||||
...dateRangePickerProps
|
||||
} = this.props;
|
||||
const renderYearsOptions = () => {
|
||||
const yearsRange = 20;
|
||||
const options = [];
|
||||
|
||||
for (let i = 0; i < yearsRange; i++) {
|
||||
const year = moment().year() - i;
|
||||
options.push(<option value={year}>{year}</option>);
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
renderMonthElement.propTypes = {
|
||||
onMonthSelect: PropTypes.func,
|
||||
onYearSelect: PropTypes.func,
|
||||
};
|
||||
|
||||
return (
|
||||
<DateRangePicker
|
||||
{...dateRangePickerProps}
|
||||
startDate={this.state.startDate}
|
||||
endDate={this.state.endDate}
|
||||
renderCalendarInfo={this.renderDatePresets}
|
||||
onDatesChange={this.onDatesChange}
|
||||
/>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<div style={containerStyle}>
|
||||
<select
|
||||
className="DateRangePicker_select"
|
||||
value={month.month()}
|
||||
onChange={e => onMonthSelect(month, e.target.value)}
|
||||
>
|
||||
{moment.months().map((label, value) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div style={containerStyle}>
|
||||
{}
|
||||
<select
|
||||
className="DateRangePicker_select"
|
||||
value={month.year()}
|
||||
onChange={e => onYearSelect(month, e.target.value)}
|
||||
>
|
||||
{renderYearsOptions()}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DateRangePicker
|
||||
{...dateRangePickerProps}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
renderCalendarInfo={renderDatePresets}
|
||||
onDatesChange={onDatesChange}
|
||||
renderMonthElement={renderMonthElement}
|
||||
startDatePlaceholderText={t('Start Date')}
|
||||
endDatePlaceholderText={t('End Date')}
|
||||
phrases={{
|
||||
closeDatePicker: t('Common:Close'),
|
||||
clearDates: t('Clear dates'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
CustomDateRangePicker.propTypes = {
|
||||
presets: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
text: PropTypes.string,
|
||||
start: PropTypes.required,
|
||||
end: PropTypes.required,
|
||||
})
|
||||
),
|
||||
onDatesChange: PropTypes.func.isRequired,
|
||||
startDate: PropTypes.instanceOf(Date),
|
||||
endDate: PropTypes.instanceOf(Date),
|
||||
month: PropTypes.instanceOf(Date),
|
||||
};
|
||||
|
||||
export default CustomDateRangePicker;
|
||||
@@ -3,11 +3,22 @@ import './StudyList.styl';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import TableSearchFilter from './TableSearchFilter.js';
|
||||
import useMedia from '../../hooks/useMedia.js';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StudyListLoadingText } from './StudyListLoadingText.js';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const getContentFromUseMediaValue = (
|
||||
displaySize,
|
||||
contentArrayMap,
|
||||
defaultContent
|
||||
) => {
|
||||
const content =
|
||||
displaySize in contentArrayMap
|
||||
? contentArrayMap[displaySize]
|
||||
: defaultContent;
|
||||
|
||||
return content;
|
||||
};
|
||||
/**
|
||||
*
|
||||
*
|
||||
@@ -24,10 +35,10 @@ function StudyList(props) {
|
||||
filterValues,
|
||||
onFilterChange: handleFilterChange,
|
||||
onSelectItem: handleSelectItem,
|
||||
t,
|
||||
//
|
||||
studyListDateFilterNumDays,
|
||||
displaySize,
|
||||
} = props;
|
||||
const { t, ready: translationsAreReady } = useTranslation('StudyList');
|
||||
|
||||
const largeTableMeta = [
|
||||
{
|
||||
@@ -70,13 +81,13 @@ function StudyList(props) {
|
||||
|
||||
const mediumTableMeta = [
|
||||
{
|
||||
displayText: 'Patient / MRN',
|
||||
displayText: `${t('PatientName')} / ${t('MRN')}`,
|
||||
fieldName: 'patientNameOrId',
|
||||
inputType: 'text',
|
||||
size: 250,
|
||||
},
|
||||
{
|
||||
displayText: 'Description',
|
||||
displayText: t('Description'),
|
||||
fieldName: 'accessionOrModalityOrDescription',
|
||||
inputType: 'text',
|
||||
size: 350,
|
||||
@@ -91,16 +102,16 @@ function StudyList(props) {
|
||||
|
||||
const smallTableMeta = [
|
||||
{
|
||||
displayText: 'Search',
|
||||
displayText: t('Search'),
|
||||
fieldName: 'allFields',
|
||||
inputType: 'text',
|
||||
size: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const tableMeta = useMedia(
|
||||
['(min-width: 1750px)', '(min-width: 1000px)', '(min-width: 768px)'],
|
||||
[largeTableMeta, mediumTableMeta, smallTableMeta],
|
||||
const tableMeta = getContentFromUseMediaValue(
|
||||
displaySize,
|
||||
{ large: largeTableMeta, medium: mediumTableMeta, small: smallTableMeta },
|
||||
smallTableMeta
|
||||
);
|
||||
|
||||
@@ -108,7 +119,7 @@ function StudyList(props) {
|
||||
.map(field => field.size)
|
||||
.reduce((prev, next) => prev + next);
|
||||
|
||||
return (
|
||||
return translationsAreReady ? (
|
||||
<table className="table table--striped table--hoverable">
|
||||
<colgroup>
|
||||
{tableMeta.map((field, i) => {
|
||||
@@ -176,12 +187,12 @@ function StudyList(props) {
|
||||
studyDate={study.studyDate}
|
||||
studyDescription={study.studyDescription || ''}
|
||||
studyInstanceUid={study.studyInstanceUid}
|
||||
t={t}
|
||||
displaySize={displaySize}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
) : null;
|
||||
}
|
||||
|
||||
StudyList.propTypes = {
|
||||
@@ -206,10 +217,12 @@ StudyList.propTypes = {
|
||||
patientNameOrId: PropTypes.string.isRequired,
|
||||
accessionOrModalityOrDescription: PropTypes.string.isRequired,
|
||||
allFields: PropTypes.string.isRequired,
|
||||
studyDateTo: PropTypes.any,
|
||||
studyDateFrom: PropTypes.any,
|
||||
}).isRequired,
|
||||
onFilterChange: PropTypes.func.isRequired,
|
||||
//
|
||||
studyListDateFilterNumDays: PropTypes.number,
|
||||
displaySize: PropTypes.string,
|
||||
};
|
||||
|
||||
StudyList.defaultProps = {};
|
||||
@@ -225,9 +238,11 @@ function TableRow(props) {
|
||||
studyDescription,
|
||||
studyInstanceUid,
|
||||
onClick: handleClick,
|
||||
t,
|
||||
displaySize,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation('StudyList');
|
||||
|
||||
const largeRowTemplate = (
|
||||
<tr
|
||||
onClick={() => handleClick(studyInstanceUid)}
|
||||
@@ -362,9 +377,13 @@ function TableRow(props) {
|
||||
</tr>
|
||||
);
|
||||
|
||||
const rowTemplate = useMedia(
|
||||
['(min-width: 1750px)', '(min-width: 1000px)', '(min-width: 768px)'],
|
||||
[largeRowTemplate, mediumRowTemplate, smallRowTemplate],
|
||||
const rowTemplate = getContentFromUseMediaValue(
|
||||
displaySize,
|
||||
{
|
||||
large: largeRowTemplate,
|
||||
medium: mediumRowTemplate,
|
||||
small: smallRowTemplate,
|
||||
},
|
||||
smallRowTemplate
|
||||
);
|
||||
|
||||
@@ -380,11 +399,11 @@ TableRow.propTypes = {
|
||||
studyDate: PropTypes.string.isRequired,
|
||||
studyDescription: PropTypes.string.isRequired,
|
||||
studyInstanceUid: PropTypes.string.isRequired,
|
||||
displaySize: PropTypes.string,
|
||||
};
|
||||
|
||||
TableRow.defaultProps = {
|
||||
isHighlighted: false,
|
||||
};
|
||||
|
||||
const connectedComponent = withTranslation('StudyList')(StudyList);
|
||||
export { connectedComponent as StudyList };
|
||||
export { StudyList };
|
||||
@@ -176,7 +176,7 @@ table.table
|
||||
margin: 0 auto;
|
||||
color: var(--table-text-primary-color);
|
||||
font-weight: 400;
|
||||
padding: 20px;
|
||||
padding: 20px 8px;
|
||||
user-select: none;
|
||||
font-size: 15px;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Icon } from './../../elements/Icon';
|
||||
// TODO: useTranslation
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
import { withTranslation } from '../../contextProviders';
|
||||
|
||||
function StudyListLoadingText({ t: translate }) {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './PaginationArea.styl';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
import { withTranslation } from '../../contextProviders';
|
||||
|
||||
class TablePagination extends PureComponent {
|
||||
static defaultProps = {
|
||||
|
||||
@@ -6,7 +6,21 @@ import CustomDateRangePicker from './CustomDateRangePicker.js';
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const getDateEntry = (datePicked, rangeDatePicked) => {
|
||||
return rangeDatePicked || datePicked || null;
|
||||
};
|
||||
|
||||
const getDateEntryFromRange = (today, numOfDays, edge = 'start') => {
|
||||
if (typeof numOfDays !== 'number') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (edge === 'end') {
|
||||
return today;
|
||||
} else {
|
||||
today.subtract(numOfDays, 'days');
|
||||
}
|
||||
};
|
||||
|
||||
function TableSearchFilter(props) {
|
||||
const {
|
||||
@@ -17,20 +31,32 @@ function TableSearchFilter(props) {
|
||||
sortFieldName,
|
||||
sortDirection,
|
||||
// TODO: Rename
|
||||
studyListDateFilterNumDays
|
||||
studyListDateFilterNumDays,
|
||||
} = props;
|
||||
|
||||
const { studyDateTo, studyDateFrom } = values || {};
|
||||
const [focusedInput, setFocusedInput] = useState(null);
|
||||
const [t] = useTranslation(); // 'Common'?
|
||||
const { t, ready: translationsAreReady } = useTranslation('Common');
|
||||
|
||||
const sortIcons = ['sort', 'sort-up', 'sort-down'];
|
||||
const sortIconForSortField =
|
||||
sortDirection === 'asc' ? sortIcons[1] : sortIcons[2];
|
||||
//
|
||||
|
||||
const today = moment();
|
||||
const lastWeek = moment().subtract(7, 'day');
|
||||
const lastMonth = moment().subtract(1, 'month');
|
||||
const defaultStartDate = moment().subtract(studyListDateFilterNumDays, 'days');
|
||||
const defaultEndDate = today;
|
||||
|
||||
const defaultStartDate = getDateEntryFromRange(
|
||||
today,
|
||||
studyListDateFilterNumDays,
|
||||
'start'
|
||||
);
|
||||
const defaultEndDate = getDateEntryFromRange(
|
||||
today,
|
||||
studyListDateFilterNumDays,
|
||||
'end'
|
||||
);
|
||||
|
||||
const studyDatePresets = [
|
||||
{
|
||||
text: t('Today'),
|
||||
@@ -49,56 +75,58 @@ function TableSearchFilter(props) {
|
||||
},
|
||||
];
|
||||
|
||||
return meta.map((field, i) => {
|
||||
const { displayText, fieldName, inputType } = field;
|
||||
const isSortField = sortFieldName === fieldName;
|
||||
const sortIcon = isSortField ? sortIconForSortField : sortIcons[0];
|
||||
return translationsAreReady
|
||||
? meta.map((field, i) => {
|
||||
const { displayText, fieldName, inputType } = field;
|
||||
const isSortField = sortFieldName === fieldName;
|
||||
const sortIcon = isSortField ? sortIconForSortField : sortIcons[0];
|
||||
|
||||
return (
|
||||
<th key={`${fieldName}-${i}`}>
|
||||
<label
|
||||
htmlFor={`filter-${fieldName}`}
|
||||
onClick={() => onSort(fieldName)}
|
||||
>
|
||||
{`${displayText} `}
|
||||
<Icon name={sortIcon} style={{ fontSize: '12px' }} />
|
||||
</label>
|
||||
{inputType === 'text' && (
|
||||
<input
|
||||
type="text"
|
||||
id={`filter-${fieldName}`}
|
||||
className="form-control studylist-search"
|
||||
value={values[fieldName]}
|
||||
onChange={e => onValueChange(fieldName, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
{inputType === 'date-range' && (
|
||||
// https://github.com/airbnb/react-dates
|
||||
<CustomDateRangePicker
|
||||
// Required
|
||||
startDate={studyListDateFilterNumDays ? defaultStartDate : null}
|
||||
startDateId="start-date"
|
||||
endDate={studyListDateFilterNumDays ? defaultEndDate : null}
|
||||
endDateId="end-date"
|
||||
// TODO: We need a dynamic way to determine which fields values to update
|
||||
onDatesChange={({ startDate, endDate, preset = false }) => {
|
||||
onValueChange('studyDateTo', startDate);
|
||||
onValueChange('studyDateFrom', endDate);
|
||||
}}
|
||||
focusedInput={focusedInput}
|
||||
onFocusChange={updatedVal => setFocusedInput(updatedVal)}
|
||||
// Optional
|
||||
numberOfMonths={1} // For med and small screens? 2 for large?
|
||||
showClearDates={true}
|
||||
anchorDirection="left"
|
||||
presets={studyDatePresets}
|
||||
hideKeyboardShortcutsPanel={true}
|
||||
isOutsideRange={day => !isInclusivelyBeforeDay(day, moment())}
|
||||
/>
|
||||
)}
|
||||
</th>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<th key={`${fieldName}-${i}`}>
|
||||
<label
|
||||
htmlFor={`filter-${fieldName}`}
|
||||
onClick={() => onSort(fieldName)}
|
||||
>
|
||||
{`${displayText}`}
|
||||
<Icon name={sortIcon} style={{ fontSize: '12px' }} />
|
||||
</label>
|
||||
{inputType === 'text' && (
|
||||
<input
|
||||
type="text"
|
||||
id={`filter-${fieldName}`}
|
||||
className="form-control studylist-search"
|
||||
value={values[fieldName]}
|
||||
onChange={e => onValueChange(fieldName, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
{inputType === 'date-range' && (
|
||||
// https://github.com/airbnb/react-dates
|
||||
<CustomDateRangePicker
|
||||
// Required
|
||||
startDate={getDateEntry(studyDateFrom, defaultStartDate)}
|
||||
startDateId="start-date"
|
||||
endDate={getDateEntry(studyDateTo, defaultEndDate)}
|
||||
endDateId="end-date"
|
||||
// TODO: We need a dynamic way to determine which fields values to update
|
||||
onDatesChange={({ startDate, endDate, preset = false }) => {
|
||||
onValueChange('studyDateFrom', startDate);
|
||||
onValueChange('studyDateTo', endDate);
|
||||
}}
|
||||
focusedInput={focusedInput}
|
||||
onFocusChange={updatedVal => setFocusedInput(updatedVal)}
|
||||
// Optional
|
||||
numberOfMonths={1} // For med and small screens? 2 for large?
|
||||
showClearDates={true}
|
||||
anchorDirection="left"
|
||||
presets={studyDatePresets}
|
||||
hideKeyboardShortcutsPanel={true}
|
||||
isOutsideRange={day => !isInclusivelyBeforeDay(day, moment())}
|
||||
/>
|
||||
)}
|
||||
</th>
|
||||
);
|
||||
})
|
||||
: null;
|
||||
}
|
||||
|
||||
TableSearchFilter.propTypes = {
|
||||
|
||||
@@ -1,17 +1,57 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import LanguageSwitcher from '../languageSwitcher';
|
||||
import i18n from '@ohif/i18n';
|
||||
|
||||
export class GeneralPreferences extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="general-preferences-wrapper">
|
||||
<div className="col-sm-3">
|
||||
<label htmlFor="language-select" className="p-r-1">
|
||||
Language
|
||||
</label>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
/**
|
||||
* General Preferences tab
|
||||
*/
|
||||
|
||||
/**
|
||||
* General Preferences tab
|
||||
* It renders the General Preferences content
|
||||
*
|
||||
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
||||
* @param {object} props component props
|
||||
* @param {string} props.name Tab`s name
|
||||
* @param {object} props.generalPreferences Data for initial state
|
||||
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
|
||||
* @param {function} props.onTabErrorChanged Callback Function in case any error on tab
|
||||
*/
|
||||
function GeneralPreferences({
|
||||
generalPreferences,
|
||||
name,
|
||||
onTabStateChanged,
|
||||
onTabErrorChanged,
|
||||
}) {
|
||||
const { language = i18n.language } = generalPreferences;
|
||||
|
||||
const onLanguageChange = language => {
|
||||
onTabStateChanged(name, {
|
||||
generalPreferences: { ...generalPreferences, language },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="general-preferences-wrapper">
|
||||
<div className="col-sm-3">
|
||||
<label htmlFor="language-select" className="p-r-1">
|
||||
Language
|
||||
</label>
|
||||
<LanguageSwitcher
|
||||
language={language}
|
||||
onLanguageChange={onLanguageChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
GeneralPreferences.propTypes = {
|
||||
generalPreferences: PropTypes.any,
|
||||
name: PropTypes.string,
|
||||
onTabStateChanged: PropTypes.func,
|
||||
onTabErrorChanged: PropTypes.func,
|
||||
};
|
||||
|
||||
export { GeneralPreferences };
|
||||
@@ -1,74 +1,238 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './HotKeysPreferences.styl';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import {
|
||||
allowedKeys,
|
||||
disallowedCombinations,
|
||||
specialKeys,
|
||||
} from './hotKeysConfig.js';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'lodash.isequal';
|
||||
|
||||
export class HotKeysPreferences extends Component {
|
||||
static propTypes = {
|
||||
hotkeyDefinitions: PropTypes.array.isRequired,
|
||||
const getKeysPressedArray = keyDownEvent => {
|
||||
const keysPressedArray = [];
|
||||
const { ctrlKey, altKey, shiftKey } = keyDownEvent;
|
||||
|
||||
if (ctrlKey && !altKey) {
|
||||
keysPressedArray.push('ctrl');
|
||||
}
|
||||
|
||||
if (shiftKey && !altKey) {
|
||||
keysPressedArray.push('shift');
|
||||
}
|
||||
|
||||
if (altKey && !ctrlKey) {
|
||||
keysPressedArray.push('alt');
|
||||
}
|
||||
|
||||
return keysPressedArray;
|
||||
};
|
||||
|
||||
const findConflictingCommand = (
|
||||
originalHotKeys,
|
||||
currentCommandName,
|
||||
currentHotKeys
|
||||
) => {
|
||||
let firstConflictingCommand = undefined;
|
||||
|
||||
for (const commandName in originalHotKeys) {
|
||||
const toolHotKeys = originalHotKeys[commandName].keys;
|
||||
|
||||
if (
|
||||
isEqual(toolHotKeys, currentHotKeys) &&
|
||||
commandName !== currentCommandName
|
||||
) {
|
||||
firstConflictingCommand = originalHotKeys[commandName];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return firstConflictingCommand;
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits given keysObj into arrays. Each array item will be a representation of column
|
||||
* @param {obj} keysObj objects to be splitted into columns
|
||||
* @param {number} columnSize How many rows per column
|
||||
*/
|
||||
const getHotKeysArrayColumns = (keysObj = {}, columnSize) => {
|
||||
if (isNaN(columnSize)) {
|
||||
return keysObj;
|
||||
}
|
||||
|
||||
const keys = Object.keys(keysObj);
|
||||
const keysValues = Object.values(keysObj);
|
||||
const keysLength = keys.length;
|
||||
|
||||
// Columns from left should be bigger;
|
||||
let currentColumn = 0;
|
||||
const dividedKeys = [];
|
||||
|
||||
for (
|
||||
let it = 0;
|
||||
it < keysLength;
|
||||
it++, it % columnSize === 0 ? currentColumn++ : currentColumn
|
||||
) {
|
||||
if (!dividedKeys[currentColumn]) {
|
||||
dividedKeys[currentColumn] = [];
|
||||
}
|
||||
|
||||
dividedKeys[currentColumn][keys[it]] = keysValues[it];
|
||||
}
|
||||
|
||||
return dividedKeys;
|
||||
};
|
||||
|
||||
const NO_FIELD_ERROR_MESSAGE = undefined;
|
||||
const formatPressedKeys = pressedKeysArray => pressedKeysArray.join('+');
|
||||
const unFormatPressedKeys = (pressedKeysStr = '') => pressedKeysStr.split('+');
|
||||
const inputValidators = (
|
||||
commandName,
|
||||
inputValue,
|
||||
pressedKeys,
|
||||
lastPressedKey,
|
||||
originalHotKeys
|
||||
) => {
|
||||
let hasError = false;
|
||||
let errorMessage = NO_FIELD_ERROR_MESSAGE;
|
||||
|
||||
const modifierValidator = ({ lastPressedKey }) => {
|
||||
// Check if it has a valid modifier
|
||||
const isModifier = ['ctrl', 'alt', 'shift'].includes(lastPressedKey);
|
||||
if (isModifier) {
|
||||
hasError = true;
|
||||
errorMessage =
|
||||
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut";
|
||||
return {
|
||||
hasError,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hotKeys: this.props.hotkeyDefinitions,
|
||||
errorMessages: {},
|
||||
};
|
||||
|
||||
this.onInputKeyDown = this.onInputKeyDown.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the keys used in a KeyPress event and returns an array of the
|
||||
* keys pressed
|
||||
*
|
||||
* @param {KeyDownEvent} keyDownEvent
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getKeysPressedArray(keyDownEvent) {
|
||||
const keysPressedArray = [];
|
||||
const { ctrlKey, altKey, shiftKey } = keyDownEvent;
|
||||
|
||||
if (ctrlKey && !altKey) {
|
||||
keysPressedArray.push('ctrl');
|
||||
const emptyValidator = ({ inputValue }) => {
|
||||
if (!inputValue) {
|
||||
hasError = true;
|
||||
errorMessage = "Field can't be empty.";
|
||||
return {
|
||||
hasError,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
if (shiftKey && !altKey) {
|
||||
keysPressedArray.push('shift');
|
||||
};
|
||||
const conflictingValidator = ({
|
||||
commandName,
|
||||
pressedKeys,
|
||||
originalHotKeys,
|
||||
}) => {
|
||||
const conflictingCommand = findConflictingCommand(
|
||||
originalHotKeys,
|
||||
commandName,
|
||||
pressedKeys
|
||||
);
|
||||
if (conflictingCommand) {
|
||||
hasError = true;
|
||||
errorMessage = `"${conflictingCommand.label}" is already using the "${pressedKeys}" shortcut.`;
|
||||
return {
|
||||
hasError,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
if (altKey && !ctrlKey) {
|
||||
keysPressedArray.push('alt');
|
||||
const disallowedValidator = ({ inputValue, pressedKeys, lastPressedKey }) => {
|
||||
const modifierCommand = formatPressedKeys(
|
||||
pressedKeys.slice(0, pressedKeys.length - 1)
|
||||
);
|
||||
|
||||
const disallowedCombination = disallowedCombinations[modifierCommand];
|
||||
const hasDisallowedCombinations = disallowedCombination
|
||||
? disallowedCombination.includes(lastPressedKey)
|
||||
: false;
|
||||
|
||||
if (hasDisallowedCombinations) {
|
||||
hasError = true;
|
||||
errorMessage = `"${inputValue}" shortcut combination is not allowed`;
|
||||
return {
|
||||
hasError,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return keysPressedArray;
|
||||
}
|
||||
const validators = [
|
||||
emptyValidator,
|
||||
modifierValidator,
|
||||
conflictingValidator,
|
||||
disallowedValidator,
|
||||
];
|
||||
|
||||
getConflictingCommand(currentCommandName, currentHotKeys) {
|
||||
return this.state.hotKeys.find((tool, index) => {
|
||||
const toolHotKeys = tool.keys[0];
|
||||
return (
|
||||
toolHotKeys &&
|
||||
toolHotKeys === currentHotKeys &&
|
||||
tool.commandName !== currentCommandName
|
||||
);
|
||||
for (const validator of validators) {
|
||||
const validation = validator({
|
||||
commandName,
|
||||
inputValue,
|
||||
pressedKeys,
|
||||
lastPressedKey,
|
||||
originalHotKeys,
|
||||
});
|
||||
if (validation && validation.hasError) {
|
||||
return validation;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} commandName
|
||||
* @param {KeyDownEvent} keyDownEvent
|
||||
* @param {Boolean} [displayPressedKey=false]
|
||||
*/
|
||||
updateInputText(commandName, keyDownEvent, displayPressedKey = false) {
|
||||
const pressedKeys = this.getKeysPressedArray(keyDownEvent);
|
||||
// validation has passed successfully
|
||||
return {
|
||||
hasError,
|
||||
errorMessage,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* HotKeysPreferencesRow
|
||||
* Renders row for hotkey preference
|
||||
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
||||
* @param {object} props component props
|
||||
* @param {string} props.commandName command name associated to given row
|
||||
* @param {string[]} props.hotkeys keys associated to given command
|
||||
* @param {object} props.originalHotKeys original hotkeys values
|
||||
* @param {function} props.onSuccessChanged Callback function to communicate parent in case its states changes
|
||||
* @param {function} props.onFailureChanged Callback Function in case any error on row
|
||||
*/
|
||||
function HotKeyPreferencesRow({
|
||||
commandName,
|
||||
hotkeys,
|
||||
label,
|
||||
originalHotKeys,
|
||||
tabError,
|
||||
onSuccessChanged,
|
||||
onFailureChanged,
|
||||
}) {
|
||||
const [inputValue, setInputValue] = useState(formatPressedKeys(hotkeys));
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const [fieldErrorMessage, setFieldErrorMessage] = useState(
|
||||
NO_FIELD_ERROR_MESSAGE
|
||||
);
|
||||
|
||||
// reset error count if tab has no errors
|
||||
useEffect(() => {
|
||||
if (!tabError) {
|
||||
setError(false);
|
||||
setFieldErrorMessage(NO_FIELD_ERROR_MESSAGE);
|
||||
setInputValue(formatPressedKeys(hotkeys));
|
||||
}
|
||||
}, [tabError]);
|
||||
|
||||
// update state values if props changes
|
||||
useEffect(() => {
|
||||
setInputValue(formatPressedKeys(hotkeys));
|
||||
}, [hotkeys]);
|
||||
|
||||
const updateInputText = (keyDownEvent, displayPressedKey = false) => {
|
||||
const pressedKeys = getKeysPressedArray(keyDownEvent);
|
||||
|
||||
if (displayPressedKey) {
|
||||
const specialKeyName = specialKeys[keyDownEvent.which];
|
||||
@@ -76,180 +240,213 @@ export class HotKeysPreferences extends Component {
|
||||
specialKeyName ||
|
||||
keyDownEvent.key ||
|
||||
String.fromCharCode(keyDownEvent.keyCode);
|
||||
pressedKeys.push(keyName);
|
||||
|
||||
// ensure lowerCase
|
||||
pressedKeys.push(keyName.toLowerCase());
|
||||
}
|
||||
|
||||
this.updateHotKeysState(commandName, pressedKeys.join('+'));
|
||||
}
|
||||
setInputValue(formatPressedKeys(pressedKeys));
|
||||
};
|
||||
|
||||
updateHotKeysState(commandName, keys) {
|
||||
const hotKeys = this.state.hotKeys;
|
||||
const hotKeyIndex = this.state.hotKeys.findIndex(
|
||||
x => x.commandName === commandName
|
||||
// validate input value
|
||||
const validateInput = () => {
|
||||
const pressedKeys = unFormatPressedKeys(inputValue);
|
||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
|
||||
|
||||
const {
|
||||
hasError = false,
|
||||
errorMessage = NO_FIELD_ERROR_MESSAGE,
|
||||
} = inputValidators(
|
||||
commandName,
|
||||
inputValue,
|
||||
pressedKeys,
|
||||
lastPressedKey,
|
||||
originalHotKeys
|
||||
);
|
||||
hotKeys[hotKeyIndex].keys[0] = keys;
|
||||
this.setState({ hotKeys });
|
||||
}
|
||||
|
||||
updateErrorsState(toolKey, errorMessage) {
|
||||
const errorMessages = this.state.errorMessages;
|
||||
errorMessages[toolKey] = errorMessage;
|
||||
this.setState({ errorMessages });
|
||||
}
|
||||
if (hasError) {
|
||||
setInputValue('');
|
||||
} else {
|
||||
onSuccessChanged([inputValue]);
|
||||
}
|
||||
|
||||
onInputKeyDown(event, commandName) {
|
||||
if (hasError !== error) {
|
||||
setError(hasError);
|
||||
}
|
||||
|
||||
setFieldErrorMessage(errorMessage);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onFailureChanged(error);
|
||||
}, [error]);
|
||||
|
||||
const onInputKeyDown = event => {
|
||||
// Prevent ESC key from propagating and closing the modal
|
||||
if (event.key === 'Escape') {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
if (allowedKeys.includes(event.keyCode)) {
|
||||
this.updateInputText(commandName, event, true);
|
||||
} else {
|
||||
this.updateInputText(commandName, event, false);
|
||||
}
|
||||
|
||||
updateInputText(event, allowedKeys.includes(event.keyCode));
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
onChange(event, commandName) {
|
||||
if (event.ctrlKey || event.altKey || event.shiftKey) {
|
||||
return;
|
||||
return (
|
||||
<tr key={commandName}>
|
||||
<td className="text-right p-r-1">{label}</td>
|
||||
<td width="200">
|
||||
<label
|
||||
className={`wrapperLabel ${
|
||||
fieldErrorMessage !== undefined ? 'state-error' : ''
|
||||
} `}
|
||||
data-key="defaultTool"
|
||||
>
|
||||
<input
|
||||
readOnly={true}
|
||||
type="text"
|
||||
value={inputValue}
|
||||
className="form-control hotkey text-center"
|
||||
onKeyDown={onInputKeyDown}
|
||||
onBlur={validateInput}
|
||||
/>
|
||||
<span className="wrapperText" />
|
||||
<span className="errorMessage">{fieldErrorMessage}</span>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
HotKeyPreferencesRow.propTypes = {
|
||||
commandName: PropTypes.string.isRequired,
|
||||
hotkeys: PropTypes.array.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
originalHotKeys: PropTypes.object.isRequired,
|
||||
tabError: PropTypes.bool.isRequired,
|
||||
onSuccessChanged: PropTypes.func.isRequired,
|
||||
onFailureChanged: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/**
|
||||
* HotKeysPreferences tab
|
||||
* It renders all hotkeys displayed into columns/rows
|
||||
*
|
||||
* It stores current state and whenever it changes, component messages parent of new value (through function callback)
|
||||
* @param {object} props component props
|
||||
* @param {string} props.name Tab`s name
|
||||
* @param {object} props.hotkeyDefinitions Data for initial state
|
||||
* @param {function} props.onTabStateChanged Callback function to communicate parent in case its states changes
|
||||
* @param {function} props.onTabErrorChanged Callback Function in case any error on tab
|
||||
*/
|
||||
function HotKeysPreferences({
|
||||
hotkeyDefinitions,
|
||||
name,
|
||||
tabError,
|
||||
onTabStateChanged,
|
||||
onTabErrorChanged,
|
||||
}) {
|
||||
const [tabState, setTabState] = useState(hotkeyDefinitions);
|
||||
const [tabErrorCounter, setTabErrorCounter] = useState(0);
|
||||
|
||||
const [numColumns] = useState(2);
|
||||
const [columnSize] = useState(() =>
|
||||
Math.ceil(Object.keys(tabState || {}).length / numColumns)
|
||||
);
|
||||
|
||||
const splittedHotKeys = getHotKeysArrayColumns(tabState, columnSize);
|
||||
|
||||
const onHotKeyChanged = (commandName, hotkeyDefinition, keys) => {
|
||||
const newState = {
|
||||
...tabState,
|
||||
[commandName]: { ...hotkeyDefinition, keys },
|
||||
};
|
||||
setTabState(newState);
|
||||
onTabStateChanged(name, { hotkeyDefinitions: newState });
|
||||
};
|
||||
|
||||
const onErrorChanged = (toInc = true) => {
|
||||
const increment = toInc ? 1 : -1;
|
||||
const newValue = tabErrorCounter + increment;
|
||||
if (newValue >= 0) {
|
||||
setTabErrorCounter(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
// reset error count if tab has no errors
|
||||
useEffect(() => {
|
||||
if (!tabError) {
|
||||
setTabErrorCounter(0);
|
||||
// update tab state
|
||||
setTabState({ ...hotkeyDefinitions });
|
||||
}
|
||||
}, [tabError]);
|
||||
|
||||
// tell parent to update its state
|
||||
useEffect(() => {
|
||||
if (tabErrorCounter === 0) {
|
||||
onTabErrorChanged(name, false);
|
||||
}
|
||||
|
||||
const hotKeyIndex = this.state.hotKeys.findIndex(
|
||||
x => x.commandName === commandName
|
||||
);
|
||||
const hotKey = this.state.hotKeys[hotKeyIndex];
|
||||
const keys = hotKey.keys[0];
|
||||
const pressedKeys = keys.split('+');
|
||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1];
|
||||
if (tabErrorCounter === 1) {
|
||||
onTabErrorChanged(name, true);
|
||||
}
|
||||
}, [tabErrorCounter]);
|
||||
|
||||
// clear the prior errors
|
||||
this.setState({ errorMessages: {} }, () => {
|
||||
// Check if it has a valid modifier
|
||||
const isModifier = ['ctrl', 'alt', 'shift'].includes(lastPressedKey);
|
||||
if (isModifier) {
|
||||
this.updateHotKeysState(commandName, '');
|
||||
this.updateErrorsState(
|
||||
commandName,
|
||||
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut"
|
||||
);
|
||||
return;
|
||||
}
|
||||
// update local state if parent updates
|
||||
useEffect(() => {
|
||||
setTabState({ ...hotkeyDefinitions });
|
||||
}, [hotkeyDefinitions]);
|
||||
|
||||
/*
|
||||
* Check if it has some conflict
|
||||
*/
|
||||
const conflictedCommand = this.getConflictingCommand(commandName, keys);
|
||||
if (conflictedCommand) {
|
||||
this.updateHotKeysState(commandName, '');
|
||||
this.updateErrorsState(
|
||||
commandName,
|
||||
`"${conflictedCommand.label}" is already using the "${keys}" shortcut.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if is a valid combination
|
||||
*/
|
||||
const modifierCommand = pressedKeys
|
||||
.slice(0, pressedKeys.length - 1)
|
||||
.join('+');
|
||||
|
||||
const disallowedCombination = disallowedCombinations[modifierCommand];
|
||||
const hasDisallowedCombinations = disallowedCombination
|
||||
? disallowedCombination.includes(lastPressedKey)
|
||||
: false;
|
||||
|
||||
if (hasDisallowedCombinations) {
|
||||
this.updateHotKeysState(commandName, '');
|
||||
this.updateErrorsState(
|
||||
commandName,
|
||||
`"${pressedKeys.join('+')}" shortcut combination is not allowed`
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderRow({ commandName, label, keys }) {
|
||||
return (
|
||||
<tr key={commandName}>
|
||||
<td className="text-right p-r-1">{label}</td>
|
||||
<td width="200">
|
||||
<label
|
||||
className={`wrapperLabel ${
|
||||
this.state.errorMessages[commandName] !== undefined
|
||||
? 'state-error'
|
||||
: ''
|
||||
} `}
|
||||
ref={input => (this[commandName] = input)}
|
||||
data-key="defaultTool"
|
||||
>
|
||||
<input
|
||||
readOnly={true}
|
||||
type="text"
|
||||
value={keys[0]}
|
||||
vali="true"
|
||||
className="form-control hotkey text-center"
|
||||
onKeyDown={event => this.onInputKeyDown(event, commandName)}
|
||||
onKeyUp={event => this.onChange(event, commandName)}
|
||||
/>
|
||||
<span className="wrapperText" />
|
||||
<span className="errorMessage">
|
||||
{this.state.errorMessages[commandName]}
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const halfWayThough = Math.floor(this.state.hotKeys.length / 2);
|
||||
const firstHalfHotkeys = this.state.hotKeys.slice(0, halfWayThough);
|
||||
const secondHalfHotkeys = this.state.hotKeys.slice(
|
||||
halfWayThough,
|
||||
this.state.hotKeys.length
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="HotKeysPreferences">
|
||||
{/* <!-- Column 1 --> */}
|
||||
<div className="column">
|
||||
<table className="full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-right p-r-1">Function</th>
|
||||
<th className="text-center">Shortcut</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{firstHalfHotkeys.map(hotkeyDefinition =>
|
||||
this.renderRow(hotkeyDefinition)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* <!-- Column 2 --> */}
|
||||
<div className="column">
|
||||
<table className="full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-right p-r-1">Function</th>
|
||||
<th className="text-center">Shortcut</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{secondHalfHotkeys.map(hotkeyDefinition =>
|
||||
this.renderRow(hotkeyDefinition)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="HotKeysPreferences">
|
||||
{splittedHotKeys.length > 0
|
||||
? splittedHotKeys.map((columnHotKeys, index) => {
|
||||
return (
|
||||
<div className="column" key={index}>
|
||||
<table className="full-width">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-right p-r-1">Function</th>
|
||||
<th className="text-center">Shortcut</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(columnHotKeys).map(
|
||||
hotkeyDefinitionTuple => (
|
||||
<HotKeyPreferencesRow
|
||||
key={hotkeyDefinitionTuple[0]}
|
||||
commandName={hotkeyDefinitionTuple[0]}
|
||||
hotkeys={hotkeyDefinitionTuple[1].keys}
|
||||
label={hotkeyDefinitionTuple[1].label}
|
||||
originalHotKeys={tabState}
|
||||
tabError={tabError}
|
||||
onSuccessChanged={keys =>
|
||||
onHotKeyChanged(
|
||||
hotkeyDefinitionTuple[0],
|
||||
hotkeyDefinitionTuple[1],
|
||||
keys
|
||||
)
|
||||
}
|
||||
onFailureChanged={onErrorChanged}
|
||||
></HotKeyPreferencesRow>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
HotKeysPreferences.propTypes = {
|
||||
hotkeyDefinitions: PropTypes.any,
|
||||
name: PropTypes.string,
|
||||
tabError: PropTypes.bool,
|
||||
onTabStateChanged: PropTypes.func,
|
||||
onTabErrorChanged: PropTypes.func,
|
||||
};
|
||||
|
||||
export { HotKeysPreferences };
|
||||
@@ -1,7 +1,5 @@
|
||||
.HotKeysPreferences
|
||||
display: flex;
|
||||
margin-right: -15px;
|
||||
margin-left: -15px;
|
||||
|
||||
.column
|
||||
width: 50%;
|
||||
|
||||
@@ -10,14 +10,21 @@ export class UserPreferences extends Component {
|
||||
static defaultProps = {
|
||||
hotkeyDefinitions: [],
|
||||
windowLevelData: {},
|
||||
generalData: {},
|
||||
generalPreferences: {},
|
||||
};
|
||||
|
||||
// TODO: Make this more generic. Tabs should not be restricted to these entries
|
||||
static propTypes = {
|
||||
hotkeyDefinitions: PropTypes.array.isRequired,
|
||||
hotkeyDefinitions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
commandName: PropTypes.string,
|
||||
keys: PropTypes.arrayOf(PropTypes.string),
|
||||
label: PropTypes.string,
|
||||
})
|
||||
).isRequired,
|
||||
windowLevelData: PropTypes.object.isRequired,
|
||||
generalData: PropTypes.object.isRequired,
|
||||
generalPreferences: PropTypes.object.isRequired,
|
||||
updatePropValue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
@@ -58,7 +65,10 @@ export class UserPreferences extends Component {
|
||||
return (
|
||||
<form className="form-themed themed">
|
||||
<div className="form-content">
|
||||
<GeneralPreferences generalData={this.props.generalData} />
|
||||
<GeneralPreferences
|
||||
generalPreferences={this.props.generalPreferences}
|
||||
updatePropValue={this.props.updatePropValue}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
@@ -84,8 +94,8 @@ export class UserPreferences extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="dialog-separator-after">
|
||||
<div className="UserPreferences">
|
||||
<div className="UserPreferences__selector">
|
||||
<ul className="nav nav-tabs">
|
||||
<li
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
@import './../../design/styles/common/form.styl'
|
||||
@import './../../design/styles/common/navbar.styl'
|
||||
@import './../../design/styles/common/state.styl'
|
||||
@import './../../design/styles/common/global.styl'
|
||||
|
||||
.modal-body
|
||||
overflow: hidden
|
||||
|
||||
.errorMessage
|
||||
color: var(--state-error-text)
|
||||
font-size: 10px
|
||||
text-transform: uppercase;
|
||||
|
||||
.form-content
|
||||
border-bottom: 3px solid var(--primary-background-color)
|
||||
margin-bottom: 20px
|
||||
margin-left: -22px
|
||||
margin-right: -22px
|
||||
max-height: 70vh
|
||||
overflow-y: auto
|
||||
padding: 22px
|
||||
min-height: 500px
|
||||
|
||||
.popover
|
||||
width: 300px
|
||||
@@ -1,86 +1,219 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSnackbarContext } from '@ohif/ui';
|
||||
|
||||
import './UserPreferencesForm.styl';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { UserPreferences } from './UserPreferences';
|
||||
// Tabs Component wrapper
|
||||
import { UserPreferencesTabs } from './UserPreferencesTabs';
|
||||
|
||||
class UserPreferencesForm extends Component {
|
||||
// TODO: Make this component more generic to allow things other than W/L and hotkeys...
|
||||
static propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
onResetToDefaults: PropTypes.func,
|
||||
windowLevelData: PropTypes.object,
|
||||
hotkeyDefinitions: PropTypes.array,
|
||||
t: PropTypes.func,
|
||||
// Tabs
|
||||
import { HotKeysPreferences } from './HotKeysPreferences';
|
||||
import { WindowLevelPreferences } from './WindowLevelPreferences';
|
||||
import { GeneralPreferences } from './GeneralPreferences';
|
||||
|
||||
/**
|
||||
@typedef TabObject
|
||||
@type {Object}
|
||||
@property {string} name Name for given tab
|
||||
@property {ReactComponent} Component React component for given tab.
|
||||
@property {object} props Props State for given tab component
|
||||
@property {boolean} [hidden] To hidden tab or not
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create tabs obj.
|
||||
* @returns {TabObject[]} Array of TabObjs.
|
||||
*/
|
||||
const createTabs = () => {
|
||||
return [
|
||||
{
|
||||
name: 'Hotkeys',
|
||||
Component: HotKeysPreferences,
|
||||
props: {},
|
||||
},
|
||||
{
|
||||
name: 'General',
|
||||
Component: GeneralPreferences,
|
||||
props: {},
|
||||
},
|
||||
{
|
||||
name: 'Window Level',
|
||||
Component: WindowLevelPreferences,
|
||||
props: {},
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Main form component to render preferences tabs and buttons
|
||||
* @param {object} props component props
|
||||
* @param {string} props.name Tab`s name
|
||||
* @param {object} props.hotkeyDefinitions Hotkeys Data
|
||||
* @param {object} props.windowLevelData Window level data
|
||||
* @param {function} props.onSave Callback function when saving
|
||||
* @param {function} props.onClose Callback function when closing
|
||||
* @param {function} props.onResetToDefaults Callback function when resetting
|
||||
*/
|
||||
function UserPreferencesForm({
|
||||
onClose,
|
||||
onSave,
|
||||
onResetToDefaults,
|
||||
windowLevelData,
|
||||
hotkeyDefinitions,
|
||||
generalPreferences,
|
||||
hotkeysManager,
|
||||
defaultLanguage,
|
||||
hotkeyDefaults,
|
||||
}) {
|
||||
const [tabs, setTabs] = useState(createTabs());
|
||||
|
||||
const createTabsState = (
|
||||
windowLevelData,
|
||||
hotkeyDefinitions,
|
||||
generalPreferences
|
||||
) => {
|
||||
return {
|
||||
Hotkeys: { hotkeyDefinitions },
|
||||
'Window Level': { windowLevelData },
|
||||
General: { generalPreferences },
|
||||
};
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const [tabsState, setTabsState] = useState(
|
||||
createTabsState(windowLevelData, hotkeyDefinitions, generalPreferences)
|
||||
);
|
||||
|
||||
this.state = {
|
||||
windowLevelData: cloneDeep(props.windowLevelData),
|
||||
hotkeyDefinitions: cloneDeep(props.hotkeyDefinitions),
|
||||
};
|
||||
}
|
||||
const [tabsError, setTabsError] = useState(
|
||||
tabs.reduce((acc, tab) => {
|
||||
acc[tab.name] = false;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
save = () => {
|
||||
this.props.onSave({
|
||||
windowLevelData: this.state.windowLevelData,
|
||||
hotkeyDefinitions: this.state.hotkeyDefinitions,
|
||||
const snackbar = useSnackbarContext();
|
||||
|
||||
const { t, ready: translationsAreReady } = useTranslation(
|
||||
'UserPreferencesModal'
|
||||
);
|
||||
|
||||
const onTabStateChanged = (tabName, newState) => {
|
||||
setTabsState({ ...tabsState, [tabName]: newState });
|
||||
};
|
||||
|
||||
const onTabErrorChanged = (tabName, hasError) => {
|
||||
setTabsError({ ...tabsError, [tabName]: hasError });
|
||||
};
|
||||
|
||||
const hasAnyError = () => {
|
||||
return Object.values(tabsError).reduce((acc, value) => acc || value);
|
||||
};
|
||||
|
||||
const onResetPreferences = () => {
|
||||
const defaultHotKeyDefitions = {};
|
||||
|
||||
hotkeyDefaults.map(item => {
|
||||
const { commandName, ...values } = item;
|
||||
defaultHotKeyDefitions[commandName] = { ...values };
|
||||
});
|
||||
|
||||
// update local state
|
||||
setTabsState({
|
||||
...tabsState,
|
||||
Hotkeys: { hotkeyDefinitions: defaultHotKeyDefitions },
|
||||
General: { generalPreferences: { language: defaultLanguage } },
|
||||
});
|
||||
|
||||
// update tabs state
|
||||
setTabs(createTabs(windowLevelData, hotkeyDefinitions, generalPreferences));
|
||||
|
||||
// reset errors
|
||||
setTabsError(
|
||||
tabs.reduce((acc, tab) => {
|
||||
acc[tab.name] = false;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
snackbar.show({
|
||||
message: (
|
||||
<div dangerouslySetInnerHTML={{ __html: t('ResetDefaultMessage') }} />
|
||||
),
|
||||
type: 'info',
|
||||
});
|
||||
};
|
||||
|
||||
componentDidUpdate(prev, next) {
|
||||
const newStateData = {};
|
||||
const onSavePreferences = event => {
|
||||
const toSave = Object.values(tabsState).reduce((acc, tabState) => {
|
||||
return { ...acc, ...tabState };
|
||||
}, {});
|
||||
|
||||
if (!isEqual(prev.windowLevelData, next.windowLevelData)) {
|
||||
newStateData.windowLevelData = prev.windowLevelData;
|
||||
}
|
||||
onSave(toSave);
|
||||
snackbar.show({
|
||||
message: t('SaveMessage'),
|
||||
type: 'success',
|
||||
});
|
||||
};
|
||||
|
||||
if (!isEqual(prev.hotkeyDefinitions, next.hotkeyDefinitions)) {
|
||||
newStateData.hotkeyDefinitions = prev.hotkeyDefinitions;
|
||||
}
|
||||
// update local state if prop values changes
|
||||
useEffect(() => {
|
||||
setTabsState(
|
||||
createTabsState(windowLevelData, hotkeyDefinitions, generalPreferences)
|
||||
);
|
||||
}, [windowLevelData, hotkeyDefinitions, generalPreferences]);
|
||||
|
||||
if (newStateData.hotkeyDefinitions || newStateData.windowLevelData) {
|
||||
this.setState(newStateData);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="UserPreferencesForm">
|
||||
<UserPreferences
|
||||
windowLevelData={this.state.windowLevelData}
|
||||
hotkeyDefinitions={this.state.hotkeyDefinitions}
|
||||
/>
|
||||
<div className="footer">
|
||||
<button
|
||||
className="btn btn-danger pull-left"
|
||||
onClick={this.props.onResetToDefaults}
|
||||
return translationsAreReady ? (
|
||||
<div className="UserPreferencesForm">
|
||||
<UserPreferencesTabs
|
||||
tabs={tabs}
|
||||
tabsState={tabsState}
|
||||
tabsError={tabsError}
|
||||
onTabStateChanged={onTabStateChanged}
|
||||
onTabErrorChanged={onTabErrorChanged}
|
||||
/>
|
||||
<div className="footer">
|
||||
<button
|
||||
className="btn btn-danger pull-left"
|
||||
data-cy="reset-default-btn"
|
||||
onClick={onResetPreferences}
|
||||
>
|
||||
{t('Reset to Defaults')}
|
||||
</button>
|
||||
<div>
|
||||
<div
|
||||
onClick={onClose}
|
||||
data-cy="cancel-btn"
|
||||
className="btn btn-default"
|
||||
>
|
||||
{this.props.t('Reset to Defaults')}
|
||||
</button>
|
||||
<div>
|
||||
<div onClick={this.props.onClose} className="btn btn-default">
|
||||
{this.props.t('Cancel')}
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={this.save}>
|
||||
{this.props.t('Save')}
|
||||
</button>
|
||||
{t('Cancel')}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
data-cy="save-btn"
|
||||
disabled={hasAnyError()}
|
||||
onClick={onSavePreferences}
|
||||
>
|
||||
{t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
const connectedComponent = withTranslation('UserPreferencesForm')(
|
||||
UserPreferencesForm
|
||||
);
|
||||
export { connectedComponent as UserPreferencesForm };
|
||||
export default connectedComponent;
|
||||
UserPreferencesForm.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
onResetToDefaults: PropTypes.func,
|
||||
windowLevelData: PropTypes.object,
|
||||
hotkeyDefinitions: PropTypes.object,
|
||||
generalPreferences: PropTypes.object,
|
||||
hotkeysManager: PropTypes.object,
|
||||
defaultLanguage: PropTypes.string,
|
||||
hotkeyDefaults: PropTypes.array,
|
||||
};
|
||||
|
||||
export { UserPreferencesForm };
|
||||
@@ -16,7 +16,6 @@
|
||||
.footer
|
||||
display: flex
|
||||
flex-direction: row
|
||||
padding-bottom: 20px
|
||||
justify-content: space-between
|
||||
|
||||
div
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './UserPreferencesTabs.styl';
|
||||
|
||||
/**
|
||||
* Render tab component
|
||||
* @param {object} tab TabObject containing tab data
|
||||
* @param {function} onTabStateChanged Callback Function in case tab changes its state
|
||||
* @param {function} onTabErrorChanged Callback Function in case any error on tab
|
||||
*/
|
||||
const renderTab = (
|
||||
tab = {},
|
||||
tabsState,
|
||||
tabsError,
|
||||
onTabStateChanged,
|
||||
onTabErrorChanged
|
||||
) => {
|
||||
const { props, Component, name, hidden = false } = tab;
|
||||
|
||||
const tabState = tabsState[name];
|
||||
const tabError = tabsError[name];
|
||||
|
||||
return !hidden ? (
|
||||
<form className="form-themed themed tabs">
|
||||
<div className="form-content">
|
||||
<Component
|
||||
key={name}
|
||||
{...tabState}
|
||||
{...props}
|
||||
name={name}
|
||||
tabError={tabError}
|
||||
onTabStateChanged={onTabStateChanged}
|
||||
onTabErrorChanged={onTabErrorChanged}
|
||||
></Component>
|
||||
</div>
|
||||
</form>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const renderTabsHeader = (tabs, activeTabIndex, onHeaderChanged) => {
|
||||
return tabs.length > 0
|
||||
? tabs.map((tab, index) => {
|
||||
const { name, hidden = false } = tab;
|
||||
|
||||
const tabClass =
|
||||
index === activeTabIndex ? 'nav-link active' : 'nav-link';
|
||||
return !hidden ? (
|
||||
<li
|
||||
key={name}
|
||||
onClick={() => {
|
||||
onHeaderChanged(index);
|
||||
}}
|
||||
className={tabClass}
|
||||
>
|
||||
<button>{name}</button>
|
||||
</li>
|
||||
) : null;
|
||||
})
|
||||
: null;
|
||||
};
|
||||
/**
|
||||
* Component to render tabs based on currentActiveTabIndex
|
||||
*
|
||||
* In case any tab changes its state this current component tells parent through function callback
|
||||
* @param {object} props Component props
|
||||
*/
|
||||
function UserPreferencesTabs({
|
||||
tabs,
|
||||
tabsState,
|
||||
tabsError,
|
||||
onTabStateChanged,
|
||||
onTabErrorChanged,
|
||||
}) {
|
||||
const [activeTabIndex, setActiveTabIndex] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="UserPreferencesTabs">
|
||||
<div className="UserPreferencesTabs__selector">
|
||||
<div className="dialog-separator-after">
|
||||
<ul className="nav nav-tabs">
|
||||
{renderTabsHeader(tabs, activeTabIndex, setActiveTabIndex)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{renderTab(
|
||||
tabs[activeTabIndex],
|
||||
tabsState,
|
||||
tabsError,
|
||||
onTabStateChanged,
|
||||
onTabErrorChanged
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
UserPreferencesTabs.propTypes = {
|
||||
tabs: PropTypes.array.isRequired,
|
||||
tabsState: PropTypes.object.isRequired,
|
||||
tabsError: PropTypes.object.isRequired,
|
||||
onTabStateChanged: PropTypes.func.isRequired,
|
||||
onTabErrorChanged: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export { UserPreferencesTabs };
|
||||
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user