Compare commits
69
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
d8055e458f | ||
|
|
5c04e34c8f | ||
|
|
c59c5b3f14 | ||
|
|
c4adcfd39e | ||
|
|
0fbaf95971 | ||
|
|
a2a7e05e7d | ||
|
|
9da8647c84 | ||
|
|
e62f5f8dd2 |
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
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
[![NPM downloads][npm-downloads-image]][npm-url]
|
||||
[![Pulls][docker-pulls-img]][docker-image-url]
|
||||
[![MIT License][license-image]][license-url]
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_shield)
|
||||
|
||||
[![Netlify Status][netlify-image]][netlify-url]
|
||||
[![CircleCI][circleci-image]][circleci-url]
|
||||
@@ -324,3 +325,6 @@ MIT © [OHIF](https://github.com/OHIF)
|
||||
[extension-vtk]: extensions/vtk/README.md
|
||||
[vtk-npm]: https://www.npmjs.com/package/@ohif/extension-vtk
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large)
|
||||
@@ -47,5 +47,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 -->
|
||||
@@ -11,7 +11,10 @@ export default {
|
||||
* LIFECYCLE HOOKS
|
||||
*/
|
||||
|
||||
preRegistration(extensionConfiguration) {},
|
||||
preRegistration({
|
||||
servicesManager,
|
||||
configuration: extensionConfiguration,
|
||||
}) {},
|
||||
|
||||
/**
|
||||
* MODULE GETTERS
|
||||
|
||||
@@ -3,6 +3,25 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* expose UiNotifications service ([#1172](https://github.com/OHIF/Viewers/issues/1172)) ([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.4.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.4.0...@ohif/extension-cornerstone@1.4.1) (2019-11-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "1.4.1",
|
||||
"version": "1.5.1",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -24,8 +24,8 @@ export default {
|
||||
* @param {object} [configuration={}]
|
||||
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||
*/
|
||||
preRegistration(configuration = {}) {
|
||||
init(configuration);
|
||||
preRegistration({ servicesManager, configuration = {} }) {
|
||||
init({ servicesManager, configuration });
|
||||
},
|
||||
getViewportModule() {
|
||||
return OHIFCornerstoneViewport;
|
||||
|
||||
@@ -4,16 +4,18 @@ import csTools from 'cornerstone-tools';
|
||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||
import queryString from 'query-string';
|
||||
|
||||
function fallbackMetaDataProvider (type, imageId) {
|
||||
function fallbackMetaDataProvider(type, imageId) {
|
||||
if (!imageId.includes('wado?requestType=WADO')) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
// If you call for an WADO-URI imageId and get no
|
||||
// If you call for an WADO-URI imageId and get no
|
||||
// metadata, try reformatting to WADO-RS imageId
|
||||
const qs = queryString.parse(imageId);
|
||||
const wadoRoot = window.store.getState().servers.servers[0].wadoRoot
|
||||
const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${qs.seriesUID}/instances/${qs.objectUID}/frames/${qs.frame || 1}`;
|
||||
const wadoRoot = window.store.getState().servers.servers[0].wadoRoot;
|
||||
const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${
|
||||
qs.seriesUID
|
||||
}/instances/${qs.objectUID}/frames/${qs.frame || 1}`;
|
||||
|
||||
return cornerstone.metaData.get(type, wadoRsImageId);
|
||||
}
|
||||
@@ -21,13 +23,12 @@ function fallbackMetaDataProvider (type, imageId) {
|
||||
// Add this fallback provider with a low priority so it is handled last
|
||||
cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} configuration
|
||||
* @param {Object|Array} configuration.csToolsConfig
|
||||
*/
|
||||
export default function init(configuration = {}) {
|
||||
export default function init({ servicesManager, configuration = {} }) {
|
||||
const { csToolsConfig } = configuration;
|
||||
const { StackManager } = OHIF.utils;
|
||||
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
|
||||
@@ -97,6 +98,7 @@ export default function init(configuration = {}) {
|
||||
];
|
||||
|
||||
tools.forEach(tool => csTools.addTool(tool));
|
||||
|
||||
csTools.setToolActive('Pan', { mouseButtonMask: 4 });
|
||||
csTools.setToolActive('Zoom', { mouseButtonMask: 2 });
|
||||
csTools.setToolActive('Wwwc', { mouseButtonMask: 1 });
|
||||
|
||||
@@ -3,6 +3,54 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.3...@ohif/extension-vtk@0.53.4) (2019-11-12)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.53.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.2...@ohif/extension-vtk@0.53.3) (2019-11-11)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "0.53.3",
|
||||
"version": "0.53.9",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -52,8 +52,8 @@
|
||||
"react-vtkjs-viewport": "^0.3.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^1.7.1",
|
||||
"@ohif/ui": "^0.60.1",
|
||||
"@ohif/core": "^1.10.0",
|
||||
"@ohif/ui": "^0.62.4",
|
||||
"cornerstone-tools": "^4.0.9",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dcmjs": "^0.6.1",
|
||||
|
||||
+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,47 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* expose UiNotifications service ([#1172](https://github.com/OHIF/Viewers/issues/1172)) ([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [1.8.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.7.1...@ohif/core@1.8.0) (2019-11-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Update hotkeys and user preferences modal ([#1135](https://github.com/OHIF/Viewers/issues/1135)) ([e62f5f8](https://github.com/OHIF/Viewers/commit/e62f5f8dd28ab363f23671cd21cee115abb870ff)), closes [#923](https://github.com/OHIF/Viewers/issues/923)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.7.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.7.0...@ohif/core@1.7.1) (2019-11-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "1.7.1",
|
||||
"version": "1.10.0",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import hotkeys from './hotkeys';
|
||||
import log from './../log.js';
|
||||
|
||||
@@ -51,10 +52,11 @@ export class HotkeysManager {
|
||||
* @param {Boolean} [isDefaultDefinitions]
|
||||
*/
|
||||
setHotkeys(hotkeyDefinitions, isDefaultDefinitions = false) {
|
||||
hotkeyDefinitions.forEach(definition => this.registerHotkeys(definition));
|
||||
const definitions = cloneDeep(hotkeyDefinitions);
|
||||
definitions.forEach(definition => this.registerHotkeys(definition));
|
||||
|
||||
if (isDefaultDefinitions) {
|
||||
this.hotkeyDefaults = hotkeyDefinitions;
|
||||
this.hotkeyDefaults = definitions;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,13 @@ import MODULE_TYPES from './MODULE_TYPES.js';
|
||||
import log from './../log.js';
|
||||
|
||||
export default class ExtensionManager {
|
||||
constructor({ commandsManager }) {
|
||||
constructor({ commandsManager, servicesManager }) {
|
||||
this.modules = {};
|
||||
this.registeredExtensionIds = [];
|
||||
this.moduleTypeNames = Object.values(MODULE_TYPES);
|
||||
//
|
||||
this._commandsManager = commandsManager;
|
||||
this._servicesManager = servicesManager;
|
||||
|
||||
this.moduleTypeNames.forEach(moduleType => {
|
||||
this.modules[moduleType] = [];
|
||||
@@ -66,7 +67,10 @@ export default class ExtensionManager {
|
||||
|
||||
// preRegistrationHook
|
||||
if (extension.preRegistration) {
|
||||
extension.preRegistration(configuration);
|
||||
extension.preRegistration({
|
||||
servicesManager: this._servicesManager,
|
||||
configuration,
|
||||
});
|
||||
}
|
||||
|
||||
// Register Modules
|
||||
@@ -106,7 +110,9 @@ export default class ExtensionManager {
|
||||
}
|
||||
|
||||
try {
|
||||
const extensionModule = getModuleFn();
|
||||
const extensionModule = getModuleFn({
|
||||
servicesManager: this._servicesManager,
|
||||
});
|
||||
|
||||
if (!extensionModule) {
|
||||
log.warn(
|
||||
|
||||
@@ -40,6 +40,30 @@ 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 and servicesManager instance 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,
|
||||
configuration,
|
||||
});
|
||||
});
|
||||
|
||||
it('logs a warning if the extension is null or undefined', () => {
|
||||
const undefinedExtension = undefined;
|
||||
const nullExtension = null;
|
||||
@@ -110,6 +134,25 @@ describe('ExtensionManager.js', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('successfully passes a servicesManager instance 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,
|
||||
});
|
||||
});
|
||||
|
||||
it('successfully registers a module for each module type', () => {
|
||||
const extension = {
|
||||
id: 'hello-world',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import './lib';
|
||||
|
||||
import { ExtensionManager, MODULE_TYPES } from './extensions';
|
||||
import { ServicesManager } from './services';
|
||||
import classes, { CommandsManager, HotkeysManager } from './classes/';
|
||||
|
||||
import DICOMWeb from './DICOMWeb';
|
||||
@@ -18,12 +19,15 @@ import ui from './ui';
|
||||
import user from './user.js';
|
||||
import utils from './utils/';
|
||||
|
||||
import { createUINotificationService, createUIModalService } from './services';
|
||||
|
||||
const OHIF = {
|
||||
MODULE_TYPES,
|
||||
//
|
||||
CommandsManager,
|
||||
ExtensionManager,
|
||||
HotkeysManager,
|
||||
ServicesManager,
|
||||
//
|
||||
utils,
|
||||
studies,
|
||||
@@ -41,6 +45,9 @@ const OHIF = {
|
||||
viewer: {},
|
||||
measurements,
|
||||
hangingProtocols,
|
||||
//
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -49,6 +56,7 @@ export {
|
||||
CommandsManager,
|
||||
ExtensionManager,
|
||||
HotkeysManager,
|
||||
ServicesManager,
|
||||
//
|
||||
utils,
|
||||
studies,
|
||||
@@ -65,6 +73,9 @@ export {
|
||||
DICOMWeb,
|
||||
measurements,
|
||||
hangingProtocols,
|
||||
//
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
};
|
||||
|
||||
export { OHIF };
|
||||
|
||||
@@ -8,6 +8,10 @@ describe('Top level exports', () => {
|
||||
'CommandsManager',
|
||||
'ExtensionManager',
|
||||
'HotkeysManager',
|
||||
'ServicesManager',
|
||||
//
|
||||
'createUINotificationService',
|
||||
'createUIModalService',
|
||||
//
|
||||
'utils',
|
||||
'studies',
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
|
||||
const defaultState = {
|
||||
// Top level key
|
||||
viewer: {
|
||||
// First tab
|
||||
hotKeysData: {
|
||||
// hotkeyName, label, keys, column
|
||||
// zoom: { label: 'Zoom', command: 'Z', column: 0 },
|
||||
},
|
||||
// Second tab
|
||||
windowLevelData: {
|
||||
// order, description, window (int), level (int)
|
||||
// 0: { description: 'Soft tissue', window: '', level: '' },
|
||||
},
|
||||
// First tab
|
||||
hotkeyDefinitions: [
|
||||
// commandName, label, keys
|
||||
// [{ zoom: { label: 'Zoom', keys: ['z'] }}]
|
||||
],
|
||||
// Second tab
|
||||
windowLevelData: {
|
||||
// order, description, window (int), level (int)
|
||||
// 0: { description: 'Soft tissue', window: '', level: '' },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import log from './../log.js';
|
||||
|
||||
export default class ServicesManager {
|
||||
constructor() {
|
||||
this.services = {};
|
||||
this.registeredServiceNames = [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @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,88 @@
|
||||
/**
|
||||
* A UI Element
|
||||
*
|
||||
* @typedef {ReactElement|HTMLElement} Modal
|
||||
*/
|
||||
|
||||
/**
|
||||
* UI Modal
|
||||
*
|
||||
* @typedef {Object} ModalProps
|
||||
* @property {string} [header=null] -
|
||||
* @property {string} [footer=null] -
|
||||
* @property {string} [backdrop=false] -
|
||||
* @property {string} [keyboard=false] -
|
||||
* @property {number} [show=true] -
|
||||
* @property {string} [closeButton=true] -
|
||||
* @property {string} [title=null] - 'Modal Title'
|
||||
* @property {boolean} [customClassName=null] - '.ModalClass'
|
||||
*/
|
||||
|
||||
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 {Modal} component React component
|
||||
* @param {ModalProps} props { header, footer, backdrop, keyboard, show, closeButton, title, customClassName }
|
||||
*/
|
||||
function show(
|
||||
component,
|
||||
props = {
|
||||
header: null,
|
||||
footer: null,
|
||||
backdrop: false,
|
||||
keyboard: false,
|
||||
show: true,
|
||||
closeButton: true,
|
||||
title: null,
|
||||
customClassName: null,
|
||||
}
|
||||
) {
|
||||
return uiModalServiceImplementation._show(component, props);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* A UI Notification
|
||||
*
|
||||
* @typedef {Object} Notification
|
||||
* @property {string} title -
|
||||
* @property {string} message -
|
||||
* @property {number} [duration=5000] - in ms
|
||||
* @property {string} [position="bottomRight"] -"topLeft" | "topCenter | "topRight" | "bottomLeft" | "bottomCenter" | "bottomRight"
|
||||
* @property {string} [type="info"] - "info" | "error" | "warning" | "success"
|
||||
* @property {boolean} [autoClose=true]
|
||||
*/
|
||||
|
||||
const uiNotificationServicePublicAPI = {
|
||||
name: 'UINotificationService',
|
||||
hide,
|
||||
show,
|
||||
setServiceImplementation,
|
||||
};
|
||||
|
||||
const uiNotificationServiceImplementation = {
|
||||
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
|
||||
_show: () => console.warn('show() NOT IMPLEMENTED'),
|
||||
};
|
||||
|
||||
function createUINotificationService() {
|
||||
return uiNotificationServicePublicAPI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and show a new UI notification; returns the
|
||||
* ID of the created notification.
|
||||
*
|
||||
* @param {Notification} notification { title, message, duration, position, type, autoClose}
|
||||
* @returns {number} id
|
||||
*/
|
||||
function show({
|
||||
title,
|
||||
message,
|
||||
duration = 5000,
|
||||
position = 'bottomRight',
|
||||
type = 'info',
|
||||
autoClose = true,
|
||||
}) {
|
||||
return uiNotificationServiceImplementation._show({
|
||||
title,
|
||||
message,
|
||||
duration,
|
||||
position,
|
||||
type,
|
||||
autoClose,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides/dismisses the notification, if currently shown
|
||||
*
|
||||
* @param {number} id - id of the notification to hide/dismiss
|
||||
* @returns undefined
|
||||
*/
|
||||
function hide(id) {
|
||||
return uiNotificationServiceImplementation._hide({ id });
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param {*} {
|
||||
* hide: hideImplementation,
|
||||
* show: showImplementation,
|
||||
* }
|
||||
*/
|
||||
function setServiceImplementation({
|
||||
hide: hideImplementation,
|
||||
show: showImplementation,
|
||||
}) {
|
||||
if (hideImplementation) {
|
||||
uiNotificationServiceImplementation._hide = hideImplementation;
|
||||
}
|
||||
if (showImplementation) {
|
||||
uiNotificationServiceImplementation._show = showImplementation;
|
||||
}
|
||||
}
|
||||
|
||||
export default createUINotificationService;
|
||||
@@ -0,0 +1,5 @@
|
||||
import ServicesManager from './ServicesManager.js';
|
||||
import createUINotificationService from './UINotificationService';
|
||||
import createUIModalService from './UIModalService';
|
||||
|
||||
export { createUINotificationService, createUIModalService, ServicesManager };
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.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.1",
|
||||
"description": "Internationalization library for The OHIF Viewer",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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,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"
|
||||
}
|
||||
@@ -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,63 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* expose UiNotifications service ([#1172](https://github.com/OHIF/Viewers/issues/1172)) ([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [0.61.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.60.1...@ohif/ui@0.61.0) (2019-11-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Update hotkeys and user preferences modal ([#1135](https://github.com/OHIF/Viewers/issues/1135)) ([e62f5f8](https://github.com/OHIF/Viewers/commit/e62f5f8dd28ab363f23671cd21cee115abb870ff)), closes [#923](https://github.com/OHIF/Viewers/issues/923)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [0.60.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.60.0...@ohif/ui@0.60.1) (2019-11-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/ui",
|
||||
"version": "0.60.1",
|
||||
"version": "0.62.4",
|
||||
"description": "A set of React components for Medical Imaging Viewers",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -6,8 +6,8 @@ import { TableList, TableListItem } from './tableList';
|
||||
import {
|
||||
AboutContent,
|
||||
UserPreferences,
|
||||
UserPreferencesModal,
|
||||
} from './userPreferencesModal';
|
||||
UserPreferencesForm,
|
||||
} from './userPreferencesForm';
|
||||
|
||||
import { Checkbox } from './checkbox';
|
||||
import { CineDialog } from './cineDialog';
|
||||
@@ -52,6 +52,6 @@ export {
|
||||
Tooltip,
|
||||
AboutContent,
|
||||
UserPreferences,
|
||||
UserPreferencesModal,
|
||||
UserPreferencesForm,
|
||||
OHIFModal,
|
||||
};
|
||||
@@ -13,7 +13,7 @@ const OHIFModal = ({
|
||||
onHide,
|
||||
footer: Footer,
|
||||
header: Header,
|
||||
children: Component,
|
||||
children,
|
||||
}) => (
|
||||
<ReactBootstrapModal
|
||||
className={classNames('modal fade themed in', className)}
|
||||
@@ -32,12 +32,9 @@ const OHIFModal = ({
|
||||
{Header && <Header hide={onHide} />}
|
||||
</ReactBootstrapModal.Header>
|
||||
)}
|
||||
<ReactBootstrapModal.Body>
|
||||
{Component && <Component hide={onHide} />}
|
||||
</ReactBootstrapModal.Body>
|
||||
<ReactBootstrapModal.Body>{children}</ReactBootstrapModal.Body>
|
||||
{Footer && (
|
||||
<ReactBootstrapModal.Footer>
|
||||
{' '}
|
||||
<Footer hide={onHide} />
|
||||
</ReactBootstrapModal.Footer>
|
||||
)}
|
||||
@@ -52,9 +49,20 @@ OHIFModal.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
onHide: PropTypes.func,
|
||||
footer: PropTypes.node,
|
||||
header: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
footer: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
PropTypes.func,
|
||||
]),
|
||||
header: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
PropTypes.func,
|
||||
]),
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
export default OHIFModal;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,89 @@ 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,
|
||||
})
|
||||
),
|
||||
autoFocus: PropTypes.bool.isRequired,
|
||||
onDatesChange: PropTypes.func.isRequired,
|
||||
startDate: PropTypes.instanceOf(Date),
|
||||
endDate: PropTypes.instanceOf(Date),
|
||||
month: PropTypes.instanceOf(Date),
|
||||
};
|
||||
|
||||
export default CustomDateRangePicker;
|
||||
@@ -25,7 +25,6 @@ function StudyList(props) {
|
||||
onFilterChange: handleFilterChange,
|
||||
onSelectItem: handleSelectItem,
|
||||
t,
|
||||
//
|
||||
studyListDateFilterNumDays,
|
||||
} = props;
|
||||
|
||||
@@ -208,7 +207,6 @@ StudyList.propTypes = {
|
||||
allFields: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
onFilterChange: PropTypes.func.isRequired,
|
||||
//
|
||||
studyListDateFilterNumDays: PropTypes.number,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ import CustomDateRangePicker from './CustomDateRangePicker.js';
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
|
||||
function TableSearchFilter(props) {
|
||||
const {
|
||||
meta,
|
||||
@@ -17,7 +15,7 @@ function TableSearchFilter(props) {
|
||||
sortFieldName,
|
||||
sortDirection,
|
||||
// TODO: Rename
|
||||
studyListDateFilterNumDays
|
||||
studyListDateFilterNumDays,
|
||||
} = props;
|
||||
const [focusedInput, setFocusedInput] = useState(null);
|
||||
const [t] = useTranslation(); // 'Common'?
|
||||
@@ -25,11 +23,13 @@ function TableSearchFilter(props) {
|
||||
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 defaultStartDate = moment().subtract(
|
||||
studyListDateFilterNumDays,
|
||||
'days'
|
||||
);
|
||||
const defaultEndDate = today;
|
||||
const studyDatePresets = [
|
||||
{
|
||||
@@ -82,8 +82,8 @@ function TableSearchFilter(props) {
|
||||
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);
|
||||
onValueChange('studyDateFrom', startDate);
|
||||
onValueChange('studyDateTo', endDate);
|
||||
}}
|
||||
focusedInput={focusedInput}
|
||||
onFocusChange={updatedVal => setFocusedInput(updatedVal)}
|
||||
|
||||
File renamed without changes.
+29
-42
@@ -10,31 +10,20 @@ import PropTypes from 'prop-types';
|
||||
|
||||
export class HotKeysPreferences extends Component {
|
||||
static propTypes = {
|
||||
hotKeysData: PropTypes.objectOf(
|
||||
hotkeyDefinitions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
keys: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
commandName: PropTypes.string,
|
||||
keys: PropTypes.arrayOf(PropTypes.string),
|
||||
label: PropTypes.string,
|
||||
})
|
||||
).isRequired,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const hotkeyCommands = Object.keys(this.props.hotKeysData);
|
||||
const localHotKeys = hotkeyCommands.map(commandName => {
|
||||
const definition = this.props.hotKeysData[commandName];
|
||||
|
||||
return {
|
||||
commandName,
|
||||
keys: definition.keys,
|
||||
label: definition.label,
|
||||
};
|
||||
});
|
||||
|
||||
this.state = {
|
||||
hotKeys: localHotKeys,
|
||||
hotKeys: this.props.hotkeyDefinitions,
|
||||
errorMessages: {},
|
||||
};
|
||||
|
||||
@@ -53,24 +42,28 @@ export class HotKeysPreferences extends Component {
|
||||
const { ctrlKey, altKey, shiftKey } = keyDownEvent;
|
||||
|
||||
if (ctrlKey && !altKey) {
|
||||
keysPressedArray.push('CTRL');
|
||||
keysPressedArray.push('ctrl');
|
||||
}
|
||||
|
||||
if (shiftKey && !altKey) {
|
||||
keysPressedArray.push('SHIFT');
|
||||
keysPressedArray.push('shift');
|
||||
}
|
||||
|
||||
if (altKey && !ctrlKey) {
|
||||
keysPressedArray.push('ALT');
|
||||
keysPressedArray.push('alt');
|
||||
}
|
||||
|
||||
return keysPressedArray;
|
||||
}
|
||||
|
||||
getConflictingCommand(currentToolKey, hotKeyCommand) {
|
||||
return Object.keys(this.state.hotKeys).find(tool => {
|
||||
const value = this.state.hotKeys[tool].command;
|
||||
return value && value === hotKeyCommand && tool !== currentToolKey;
|
||||
getConflictingCommand(currentCommandName, currentHotKeys) {
|
||||
return this.state.hotKeys.find((tool, index) => {
|
||||
const toolHotKeys = tool.keys[0];
|
||||
return (
|
||||
toolHotKeys &&
|
||||
toolHotKeys === currentHotKeys &&
|
||||
tool.commandName !== currentCommandName
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,7 +82,7 @@ export class HotKeysPreferences extends Component {
|
||||
specialKeyName ||
|
||||
keyDownEvent.key ||
|
||||
String.fromCharCode(keyDownEvent.keyCode);
|
||||
pressedKeys.push(keyName.toUpperCase());
|
||||
pressedKeys.push(keyName.toLowerCase());
|
||||
}
|
||||
|
||||
this.updateHotKeysState(commandName, pressedKeys.join('+'));
|
||||
@@ -136,17 +129,17 @@ export class HotKeysPreferences extends Component {
|
||||
const hotKey = this.state.hotKeys[hotKeyIndex];
|
||||
const keys = hotKey.keys[0];
|
||||
const pressedKeys = keys.split('+');
|
||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1].toUpperCase();
|
||||
const lastPressedKey = pressedKeys[pressedKeys.length - 1].toLowerCase();
|
||||
|
||||
// clear the prior errors
|
||||
this.setState({ errorMessages: {} }, () => {
|
||||
// Check if it has a valid modifier
|
||||
const isModifier = ['CTRL', 'ALT', 'SHIFT'].includes(lastPressedKey);
|
||||
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"
|
||||
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut"
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -154,21 +147,13 @@ export class HotKeysPreferences extends Component {
|
||||
/*
|
||||
* Check if it has some conflict
|
||||
*/
|
||||
const conflictedCommandKey = this.getConflictingCommand(
|
||||
commandName,
|
||||
keys
|
||||
);
|
||||
if (conflictedCommandKey) {
|
||||
const conflictedCommand = this.state.hotKeys[conflictedCommandKey];
|
||||
|
||||
const conflictedCommand = this.getConflictingCommand(commandName, keys);
|
||||
if (conflictedCommand) {
|
||||
this.updateHotKeysState(commandName, '');
|
||||
this.updateErrorsState(
|
||||
commandName,
|
||||
`"${conflictedCommand.label}" is already using the "${
|
||||
conflictedCommand.command
|
||||
}" shortcut.`
|
||||
`"${conflictedCommand.label}" is already using the "${keys}" shortcut.`
|
||||
);
|
||||
this.updateErrorsState(conflictedCommandKey, '');
|
||||
this.updateHotKeysState(commandName, '');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -178,7 +163,7 @@ export class HotKeysPreferences extends Component {
|
||||
const modifierCommand = pressedKeys
|
||||
.slice(0, pressedKeys.length - 1)
|
||||
.join('+')
|
||||
.toUpperCase();
|
||||
.toLowerCase();
|
||||
|
||||
const disallowedCombination = disallowedCombinations[modifierCommand];
|
||||
const hasDisallowedCombinations = disallowedCombination
|
||||
@@ -189,7 +174,7 @@ export class HotKeysPreferences extends Component {
|
||||
this.updateHotKeysState(commandName, '');
|
||||
this.updateErrorsState(
|
||||
commandName,
|
||||
"It's not possible to define only modifier keys (CTRL, ALT and SHIFT) as a shortcut"
|
||||
`"${pressedKeys.join('+')}" shortcut combination is not allowed`
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -237,7 +222,7 @@ export class HotKeysPreferences extends Component {
|
||||
this.state.hotKeys.length
|
||||
);
|
||||
|
||||
return (
|
||||
return this.state.hotKeys.length > 0 ? (
|
||||
<div className="HotKeysPreferences">
|
||||
{/* <!-- Column 1 --> */}
|
||||
<div className="column">
|
||||
@@ -272,6 +257,8 @@ export class HotKeysPreferences extends Component {
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p>{`No hotkeys are configured for this application. Hotkeys can be configured in the application's app-config.js file.`}</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+23
-13
@@ -8,14 +8,20 @@ import './UserPreferences.styl';
|
||||
|
||||
export class UserPreferences extends Component {
|
||||
static defaultProps = {
|
||||
hotKeysData: {},
|
||||
hotkeyDefinitions: [],
|
||||
windowLevelData: {},
|
||||
generalData: {},
|
||||
};
|
||||
|
||||
// TODO: Make this more generic. Tabs should not be restricted to these entries
|
||||
static propTypes = {
|
||||
hotKeysData: PropTypes.object.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,
|
||||
};
|
||||
@@ -32,7 +38,9 @@ export class UserPreferences extends Component {
|
||||
return (
|
||||
<form className="form-themed themed">
|
||||
<div className="form-content">
|
||||
<HotKeysPreferences hotKeysData={this.props.hotKeysData} />
|
||||
<HotKeysPreferences
|
||||
hotkeyDefinitions={this.props.hotkeyDefinitions}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
@@ -66,8 +74,8 @@ export class UserPreferences extends Component {
|
||||
switch (tabIndex) {
|
||||
case 0:
|
||||
return this.renderHotkeysTab();
|
||||
case 1:
|
||||
return this.renderWindowLevelTab();
|
||||
/* case 1:
|
||||
return this.renderWindowLevelTab(); */
|
||||
case 2:
|
||||
return this.renderGeneralTab();
|
||||
|
||||
@@ -93,14 +101,16 @@ export class UserPreferences extends Component {
|
||||
>
|
||||
<button>Hotkeys</button>
|
||||
</li>
|
||||
<li
|
||||
onClick={() => {
|
||||
this.tabClick(1);
|
||||
}}
|
||||
className={this.getTabClass(1)}
|
||||
>
|
||||
<button>Window Level</button>
|
||||
</li>
|
||||
{false && (
|
||||
<li
|
||||
onClick={() => {
|
||||
this.tabClick(1);
|
||||
}}
|
||||
className={this.getTabClass(1)}
|
||||
>
|
||||
<button>Window Level</button>
|
||||
</li>
|
||||
)}
|
||||
<li
|
||||
onClick={() => {
|
||||
this.tabClick(2);
|
||||
File renamed without changes.
@@ -0,0 +1,92 @@
|
||||
import './UserPreferencesForm.styl';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { UserPreferences } from './UserPreferences';
|
||||
|
||||
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.arrayOf(
|
||||
PropTypes.shape({
|
||||
commandName: PropTypes.string,
|
||||
keys: PropTypes.arrayOf(PropTypes.string),
|
||||
label: PropTypes.string,
|
||||
})
|
||||
).isRequired,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
windowLevelData: cloneDeep(props.windowLevelData),
|
||||
hotkeyDefinitions: cloneDeep(props.hotkeyDefinitions),
|
||||
};
|
||||
}
|
||||
|
||||
save = () => {
|
||||
this.props.onSave({
|
||||
windowLevelData: this.state.windowLevelData,
|
||||
hotkeyDefinitions: this.state.hotkeyDefinitions,
|
||||
});
|
||||
};
|
||||
|
||||
componentDidUpdate(prev, next) {
|
||||
const newStateData = {};
|
||||
|
||||
if (!isEqual(prev.windowLevelData, next.windowLevelData)) {
|
||||
newStateData.windowLevelData = prev.windowLevelData;
|
||||
}
|
||||
|
||||
if (!isEqual(prev.hotkeyDefinitions, next.hotkeyDefinitions)) {
|
||||
newStateData.hotkeyDefinitions = prev.hotkeyDefinitions;
|
||||
}
|
||||
|
||||
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}
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const connectedComponent = withTranslation('UserPreferencesForm')(
|
||||
UserPreferencesForm
|
||||
);
|
||||
export { connectedComponent as UserPreferencesForm };
|
||||
export default connectedComponent;
|
||||
+10
-3
@@ -12,6 +12,13 @@
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
opacity: .2;
|
||||
|
||||
.ModalHeader
|
||||
ol, ul
|
||||
margin-top: 0;
|
||||
.UserPreferencesForm
|
||||
.footer
|
||||
display: flex
|
||||
flex-direction: row
|
||||
padding-bottom: 20px
|
||||
justify-content: space-between
|
||||
|
||||
div
|
||||
button:last-child
|
||||
margin-left: 10px
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+8
-8
@@ -1,18 +1,18 @@
|
||||
---
|
||||
name: User Preferences Modal
|
||||
name: User Preferences Form
|
||||
menu: Components
|
||||
route: /components/user-preferences-modal
|
||||
route: /components/user-preferences-form
|
||||
---
|
||||
|
||||
import { Playground, Props } from 'docz'
|
||||
import { State } from 'react-powerplug'
|
||||
import { UserPreferencesModal } from './../index.js'
|
||||
import { UserPreferencesForm } from './../index.js'
|
||||
import NameSpace from '../../../__docs__/NameSpace'
|
||||
//
|
||||
import windowLevelDefaults from './windowLevelDefaults.js'
|
||||
import hotkeyDefaults from './hotkeyDefaults.js'
|
||||
|
||||
# User Preferences Modal
|
||||
# User Preferences Form
|
||||
|
||||
## Basic usage
|
||||
|
||||
@@ -20,7 +20,7 @@ import hotkeyDefaults from './hotkeyDefaults.js'
|
||||
<State initial={{
|
||||
isOpen: false,
|
||||
windowLevelData: windowLevelDefaults,
|
||||
hotKeysData: hotkeyDefaults,
|
||||
hotkeyDefinitions: hotkeyDefaults,
|
||||
}}>
|
||||
|
||||
{({ state, setState }) => (
|
||||
@@ -32,7 +32,7 @@ import hotkeyDefaults from './hotkeyDefaults.js'
|
||||
>
|
||||
Open user preferences
|
||||
</button>
|
||||
<UserPreferencesModal
|
||||
<UserPreferencesForm
|
||||
{...state}
|
||||
onCancel={() => setState({ isOpen: false })}
|
||||
onSave={() => alert('on save')}
|
||||
@@ -47,8 +47,8 @@ import hotkeyDefaults from './hotkeyDefaults.js'
|
||||
|
||||
## API
|
||||
|
||||
<Props of={UserPreferencesModal} />
|
||||
<Props of={UserPreferencesForm} />
|
||||
|
||||
## Translation Namespace
|
||||
|
||||
<NameSpace name="UserPreferencesModal" />
|
||||
<NameSpace name="UserPreferencesForm" />
|
||||
File renamed without changes.
+28
-28
@@ -4,38 +4,38 @@ const range = (start, end) => {
|
||||
|
||||
export const disallowedCombinations = {
|
||||
'': [],
|
||||
ALT: ['SPACE'],
|
||||
SHIFT: [],
|
||||
CTRL: [
|
||||
'F4',
|
||||
'F5',
|
||||
'F11',
|
||||
'W',
|
||||
'R',
|
||||
'T',
|
||||
'O',
|
||||
'P',
|
||||
'A',
|
||||
'D',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'J',
|
||||
'L',
|
||||
'Z',
|
||||
'X',
|
||||
'C',
|
||||
'V',
|
||||
'B',
|
||||
'N',
|
||||
'PAGEDOWN',
|
||||
'PAGEUP',
|
||||
alt: ['space'],
|
||||
shift: [],
|
||||
ctrl: [
|
||||
'f4',
|
||||
'f5',
|
||||
'f11',
|
||||
'w',
|
||||
'r',
|
||||
't',
|
||||
'o',
|
||||
'p',
|
||||
'a',
|
||||
'd',
|
||||
'f',
|
||||
'g',
|
||||
'h',
|
||||
'j',
|
||||
'l',
|
||||
'z',
|
||||
'x',
|
||||
'c',
|
||||
'v',
|
||||
'b',
|
||||
'n',
|
||||
'pagedown',
|
||||
'pageup',
|
||||
],
|
||||
'CTRL+SHIFT': ['Q', 'W', 'R', 'T', 'P', 'A', 'H', 'V', 'B', 'N'],
|
||||
'ctrl+shift': ['q', 'w', 'r', 't', 'p', 'a', 'h', 'v', 'b', 'n'],
|
||||
};
|
||||
|
||||
export const allowedKeys = [
|
||||
...[8, 13, 27, 32, 46], // BACKSPACE, ENTER, ESCAPE, SPACE, DELETE
|
||||
...[8, 13, 27, 32, 46], // backspace, enter, escape, space, delete
|
||||
...[12, 106, 107, 109, 110, 111], // Numpad keys
|
||||
...range(218, 220), // [\]
|
||||
...range(185, 190), // ;=,-./
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export { UserPreferences } from './UserPreferences.js';
|
||||
export { AboutContent } from '../content/aboutContent/AboutContent.js';
|
||||
export { UserPreferencesModal } from './UserPreferencesModal.js';
|
||||
export { UserPreferencesForm } from './UserPreferencesForm.js';
|
||||
export { GeneralPreferences } from './GeneralPreferences.js';
|
||||
@@ -1,107 +0,0 @@
|
||||
import './UserPreferencesModal.styl';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-bootstrap-modal';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
|
||||
import 'react-bootstrap-modal/lib/css/rbm-patch.css';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { UserPreferences } from './UserPreferences';
|
||||
|
||||
// TODO: Is this the only component importing these?
|
||||
import './../../design/styles/common/modal.styl';
|
||||
|
||||
class UserPreferencesModal extends Component {
|
||||
// TODO: Make this component more generic to allow things other than W/L and hotkeys...
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
onResetToDefaults: PropTypes.func,
|
||||
windowLevelData: PropTypes.object,
|
||||
hotKeysData: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
windowLevelData: cloneDeep(props.windowLevelData),
|
||||
hotKeysData: cloneDeep(props.hotKeysData),
|
||||
};
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
save = () => {
|
||||
this.props.onSave({
|
||||
windowLevelData: this.state.windowLevelData,
|
||||
hotKeysData: this.state.hotKeysData,
|
||||
});
|
||||
};
|
||||
|
||||
componentDidUpdate(prev, next) {
|
||||
const newStateData = {};
|
||||
|
||||
if (!isEqual(prev.windowLevelData, next.windowLevelData)) {
|
||||
newStateData.windowLevelData = prev.windowLevelData;
|
||||
}
|
||||
|
||||
if (!isEqual(prev.hotKeysData, next.hotKeysData)) {
|
||||
newStateData.hotKeysData = prev.hotKeysData;
|
||||
}
|
||||
|
||||
if (newStateData.hotKeysData || newStateData.windowLevelData) {
|
||||
this.setState(newStateData);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.isOpen}
|
||||
onHide={this.props.onCancel}
|
||||
aria-labelledby="ModalHeader"
|
||||
className="ModalHeader modal fade themed in"
|
||||
backdrop={false}
|
||||
large={true}
|
||||
keyboard={false}
|
||||
>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{this.props.t('User Preferences')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<UserPreferences
|
||||
windowLevelData={this.state.windowLevelData}
|
||||
hotKeysData={this.state.hotKeysData}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
className="btn btn-danger pull-left"
|
||||
onClick={this.props.onResetToDefaults}
|
||||
>
|
||||
{this.props.t('Reset to Defaults')}
|
||||
</button>
|
||||
<Modal.Dismiss className="btn btn-default">
|
||||
{this.props.t('Cancel')}
|
||||
</Modal.Dismiss>
|
||||
<button className="btn btn-primary" onClick={this.save}>
|
||||
{this.props.t('Save')}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const connectedComponent = withTranslation('UserPreferencesModal')(
|
||||
UserPreferencesModal
|
||||
);
|
||||
export { connectedComponent as UserPreferencesModal };
|
||||
export default connectedComponent;
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
Tooltip,
|
||||
AboutContent,
|
||||
UserPreferences,
|
||||
UserPreferencesModal,
|
||||
UserPreferencesForm,
|
||||
OHIFModal,
|
||||
} from './components';
|
||||
import { useDebounce, useMedia } from './hooks';
|
||||
@@ -101,7 +101,7 @@ export {
|
||||
Tooltip,
|
||||
AboutContent,
|
||||
UserPreferences,
|
||||
UserPreferencesModal,
|
||||
UserPreferencesForm,
|
||||
ViewerbaseDragDropContext,
|
||||
SnackbarProvider,
|
||||
useSnackbarContext,
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import React, { useState, createContext, useContext } from 'react';
|
||||
import React, {
|
||||
useState,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const ModalContext = createContext(null);
|
||||
const { Provider, Consumer } = ModalContext;
|
||||
const { Provider } = ModalContext;
|
||||
|
||||
export const useModal = () => useContext(ModalContext);
|
||||
|
||||
const ModalProvider = ({ children, modal: Modal }) => {
|
||||
const ModalProvider = ({ children, modal: Modal, service }) => {
|
||||
const DEFAULT_OPTIONS = {
|
||||
component: null /* The component instance inside the modal. */,
|
||||
header: null /* The content inside the modal header. */,
|
||||
footer: null /* The content inside the modal footer. */,
|
||||
backdrop: false /* Should the modal render a backdrop overlay. */,
|
||||
keyboard: false /* Modal is dismissible via the esc key. */,
|
||||
show: true /* Make the Modal visible or hidden. */,
|
||||
@@ -20,51 +28,81 @@ const ModalProvider = ({ children, modal: Modal }) => {
|
||||
|
||||
const [options, setOptions] = useState(DEFAULT_OPTIONS);
|
||||
|
||||
/**
|
||||
* Sets the implementation of a modal service that can be used by extensions.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (service) {
|
||||
service.setServiceImplementation({ hide, show });
|
||||
}
|
||||
}, [hide, service, show]);
|
||||
|
||||
/**
|
||||
* Show the modal and override its configuration props.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
const show = (component, props = {}) =>
|
||||
setOptions(Object.assign({}, options, props, { component }));
|
||||
const show = useCallback(
|
||||
(component, props = {}) =>
|
||||
setOptions(Object.assign({}, options, props, { component })),
|
||||
[options]
|
||||
);
|
||||
|
||||
/**
|
||||
* Hide the modal and set its properties to default.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
const hide = () => setOptions(DEFAULT_OPTIONS);
|
||||
const hide = useCallback(() => setOptions(DEFAULT_OPTIONS), [
|
||||
DEFAULT_OPTIONS,
|
||||
]);
|
||||
|
||||
const { component: Component } = options;
|
||||
|
||||
return (
|
||||
<Provider value={{ ...options, show, hide }}>
|
||||
<Consumer>
|
||||
{props => {
|
||||
const { component, footer, header, customClassName } = props;
|
||||
return component ? (
|
||||
<Modal
|
||||
className={classNames(customClassName, component.className)}
|
||||
backdrop={options.backdrop}
|
||||
keyboard={options.keyboard}
|
||||
show={options.show}
|
||||
title={options.title}
|
||||
closeButton={options.closeButton}
|
||||
onHide={hide}
|
||||
footer={footer}
|
||||
header={header}
|
||||
>
|
||||
{component}
|
||||
</Modal>
|
||||
) : null;
|
||||
}}
|
||||
</Consumer>
|
||||
<Provider value={{ show, hide }}>
|
||||
{options.component && (
|
||||
<Modal
|
||||
className={classNames(
|
||||
options.customClassName,
|
||||
options.component.className
|
||||
)}
|
||||
backdrop={options.backdrop}
|
||||
keyboard={options.keyboard}
|
||||
show={options.show}
|
||||
title={options.title}
|
||||
closeButton={options.closeButton}
|
||||
footer={options.footer}
|
||||
header={options.header}
|
||||
onHide={hide}
|
||||
>
|
||||
<Component {...options} show={show} hide={hide} />
|
||||
</Modal>
|
||||
)}
|
||||
{children}
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
ModalProvider.defaultProps = {
|
||||
service: null,
|
||||
};
|
||||
|
||||
ModalProvider.propTypes = {
|
||||
children: PropTypes.node,
|
||||
modal: PropTypes.node,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
modal: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
PropTypes.func,
|
||||
]).isRequired,
|
||||
service: PropTypes.shape({
|
||||
setServiceImplementation: PropTypes.func,
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -74,7 +112,8 @@ ModalProvider.propTypes = {
|
||||
*/
|
||||
export const withModal = Component => {
|
||||
return function WrappedComponent(props) {
|
||||
return <Component {...props} modalContext={{ ...useModal() }} />;
|
||||
const { show, hide } = useModal();
|
||||
return <Component {...props} modal={{ show, hide }} />;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import React, { useState, createContext, useContext } from 'react';
|
||||
import React, {
|
||||
useState,
|
||||
createContext,
|
||||
useContext,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import SnackbarContainer from '../components/snackbar/SnackbarContainer';
|
||||
import SnackbarTypes from '../components/snackbar/SnackbarTypes';
|
||||
|
||||
@@ -6,7 +14,7 @@ const SnackbarContext = createContext(null);
|
||||
|
||||
export const useSnackbarContext = () => useContext(SnackbarContext);
|
||||
|
||||
const SnackbarProvider = ({ children }) => {
|
||||
const SnackbarProvider = ({ children, service }) => {
|
||||
const DEFAULT_OPTIONS = {
|
||||
title: '',
|
||||
message: '',
|
||||
@@ -19,45 +27,62 @@ const SnackbarProvider = ({ children }) => {
|
||||
const [count, setCount] = useState(1);
|
||||
const [snackbarItems, setSnackbarItems] = useState([]);
|
||||
|
||||
const show = options => {
|
||||
if (!options || (!options.title && !options.message)) {
|
||||
console.warn(
|
||||
'Snackbar cannot be rendered without required parameters: title | message'
|
||||
);
|
||||
|
||||
return null;
|
||||
/**
|
||||
* Sets the implementation of a notification service that can be used by extensions.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (service) {
|
||||
service.setServiceImplementation({ hide, show });
|
||||
}
|
||||
}, [service, hide, show]);
|
||||
|
||||
const newItem = {
|
||||
...DEFAULT_OPTIONS,
|
||||
...options,
|
||||
id: count,
|
||||
visible: true,
|
||||
};
|
||||
const show = useCallback(
|
||||
options => {
|
||||
if (!options || (!options.title && !options.message)) {
|
||||
console.warn(
|
||||
'Snackbar cannot be rendered without required parameters: title | message'
|
||||
);
|
||||
|
||||
setSnackbarItems(state => [...state, newItem]);
|
||||
setCount(count + 1);
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
const hide = id => {
|
||||
const hideItem = items => {
|
||||
const newItems = items.map(item => {
|
||||
if (item.id === id) {
|
||||
item.visible = false;
|
||||
}
|
||||
const newItem = {
|
||||
...DEFAULT_OPTIONS,
|
||||
...options,
|
||||
id: count,
|
||||
visible: true,
|
||||
};
|
||||
|
||||
return item;
|
||||
});
|
||||
setSnackbarItems(state => [...state, newItem]);
|
||||
setCount(count + 1);
|
||||
},
|
||||
[count, DEFAULT_OPTIONS]
|
||||
);
|
||||
|
||||
return newItems;
|
||||
};
|
||||
const hide = useCallback(
|
||||
id => {
|
||||
const hideItem = items => {
|
||||
const newItems = items.map(item => {
|
||||
if (item.id === id) {
|
||||
item.visible = false;
|
||||
}
|
||||
|
||||
setSnackbarItems(state => hideItem(state));
|
||||
return item;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
setSnackbarItems(state => [...state.filter(item => item.id !== id)]);
|
||||
}, 1000);
|
||||
};
|
||||
return newItems;
|
||||
};
|
||||
|
||||
setSnackbarItems(state => hideItem(state));
|
||||
|
||||
setTimeout(() => {
|
||||
setSnackbarItems(state => [...state.filter(item => item.id !== id)]);
|
||||
}, 1000);
|
||||
},
|
||||
[setSnackbarItems]
|
||||
);
|
||||
|
||||
const hideAll = () => {
|
||||
// reset count
|
||||
@@ -85,6 +110,21 @@ const SnackbarProvider = ({ children }) => {
|
||||
);
|
||||
};
|
||||
|
||||
SnackbarProvider.defaultProps = {
|
||||
service: null,
|
||||
};
|
||||
|
||||
SnackbarProvider.propTypes = {
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
PropTypes.func,
|
||||
]).isRequired,
|
||||
service: PropTypes.shape({
|
||||
setServiceImplementation: PropTypes.func,
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* High Order Component to use the snackbar methods through a Class Component
|
||||
|
||||
@@ -3,6 +3,83 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [2.8.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.4...@ohif/viewer@2.8.5) (2019-11-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* minor date picker UX improvements ([813ee5e](https://github.com/OHIF/Viewers/commit/813ee5ed4d78b7bda234922d5f3389efe346451c))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.8.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.3...@ohif/viewer@2.8.4) (2019-11-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.8.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.2...@ohif/viewer@2.8.3) (2019-11-15)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.8.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.1...@ohif/viewer@2.8.2) (2019-11-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.8.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.0...@ohif/viewer@2.8.1) (2019-11-14)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.8.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.7.1...@ohif/viewer@2.8.0) (2019-11-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* expose UiNotifications service ([#1172](https://github.com/OHIF/Viewers/issues/1172)) ([5c04e34](https://github.com/OHIF/Viewers/commit/5c04e34c8fb2394ab7acd9eb4f2ab12afeb2f255))
|
||||
* filter field for google api windows ([#1170](https://github.com/OHIF/Viewers/issues/1170)) ([c59c5b3](https://github.com/OHIF/Viewers/commit/c59c5b3f14d44f1c06aa396125a1f4caaa431c25))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.7.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.7.0...@ohif/viewer@2.7.1) (2019-11-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 🐛 Fix for JS breaking on header ([#1164](https://github.com/OHIF/Viewers/issues/1164)) ([0fbaf95](https://github.com/OHIF/Viewers/commit/0fbaf95971dc0b3a671e1f586a876d9019e860ed))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [2.7.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.6.4...@ohif/viewer@2.7.0) (2019-11-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 Update hotkeys and user preferences modal ([#1135](https://github.com/OHIF/Viewers/issues/1135)) ([e62f5f8](https://github.com/OHIF/Viewers/commit/e62f5f8dd28ab363f23671cd21cee115abb870ff)), closes [#923](https://github.com/OHIF/Viewers/issues/923)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.6.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.6.3...@ohif/viewer@2.6.4) (2019-11-11)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
@@ -46,8 +46,8 @@ describe('OHIF Cornerstone Hotkeys', () => {
|
||||
// Hotkey I
|
||||
cy.get('body').type('I');
|
||||
// Visual comparison
|
||||
cy.screenshot();
|
||||
cy.percyCanvasSnapshot('Hotkey I - invert image');
|
||||
cy.screenshot('Hotkey I - Should Invert Image');
|
||||
cy.percyCanvasSnapshot('Hotkey I - Should Invert Image');
|
||||
});
|
||||
|
||||
it('checks if hotkeys "+", "-" and "=" can zoom in, out and fit to viewport', () => {
|
||||
@@ -76,8 +76,8 @@ describe('OHIF Cornerstone Hotkeys', () => {
|
||||
cy.get('@viewportInfoBottomRight').should('contains.text', 'Zoom: 211%');
|
||||
|
||||
// Visual comparison to make sure the 'inverted' image was reset
|
||||
cy.screenshot();
|
||||
cy.percyCanvasSnapshot('Hotkey SPACEBAR - Reset Image');
|
||||
cy.screenshot('Hotkey SPACEBAR - Should Reset Image');
|
||||
cy.percyCanvasSnapshot('Hotkey SPACEBAR - Should Reset Image');
|
||||
});
|
||||
|
||||
it('uses hotkeys "RightArrow" and "LeftArrow" to navigate between multiple viewports', () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
before(() => {
|
||||
cy.openStudy('MISTER^MR');
|
||||
cy.waitDicomImage();
|
||||
cy.expectMinimumThumbnails(3);
|
||||
cy.expectMinimumThumbnails(5);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -112,25 +112,14 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
cy.wrap($panBtn).should('have.class', 'active');
|
||||
});
|
||||
|
||||
//Get image position from cornerstone and check if y axis was modified
|
||||
let cornerstone;
|
||||
let currentPan;
|
||||
|
||||
cy.window()
|
||||
.its('cornerstone')
|
||||
.then(c => {
|
||||
cornerstone = c;
|
||||
currentPan = () =>
|
||||
cornerstone.getEnabledElements()[0].viewport.translation;
|
||||
});
|
||||
|
||||
cy.get('@viewport')
|
||||
.trigger('mousedown', 'center', { which: 1 })
|
||||
.trigger('mousemove', 'bottom', { which: 1 })
|
||||
.trigger('mouseup', 'bottom')
|
||||
.then(() => {
|
||||
expect(currentPan().y > 0).to.eq(true);
|
||||
});
|
||||
.trigger('mouseup', 'bottom');
|
||||
|
||||
// Visual comparison
|
||||
cy.screenshot('Pan tool moved the image inside the viewport');
|
||||
cy.percyCanvasSnapshot('Pan tool moved the image inside the viewport');
|
||||
});
|
||||
|
||||
it('checks if Length annotation can be added on viewport and on measurements panel', () => {
|
||||
@@ -312,18 +301,18 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
.find('td')
|
||||
.its('length')
|
||||
.should('be.eq', 9);
|
||||
cy.get('@layoutBtn').click();
|
||||
});
|
||||
|
||||
//verify if layout has changed to 2 viewports
|
||||
cy.get('tbody > :nth-child(1) > :nth-child(2)').click();
|
||||
cy.setLayout(1, 2);
|
||||
cy.get('.viewport-container').then($viewport => {
|
||||
cy.wrap($viewport)
|
||||
.its('length')
|
||||
.should('be.eq', 2);
|
||||
});
|
||||
|
||||
cy.get('@layoutBtn').click();
|
||||
cy.get('tbody > :nth-child(2) > :nth-child(1)').click();
|
||||
cy.setLayout(2, 1);
|
||||
cy.get('.viewport-container').then($viewport => {
|
||||
cy.wrap($viewport)
|
||||
.its('length')
|
||||
@@ -331,8 +320,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
});
|
||||
|
||||
//verify if layout has changed to 3 viewports
|
||||
cy.get('@layoutBtn').click();
|
||||
cy.get('tbody > :nth-child(1) > :nth-child(3)').click();
|
||||
cy.setLayout(1, 3);
|
||||
cy.get('.viewport-container').then($viewport => {
|
||||
cy.wait(1000);
|
||||
cy.wrap($viewport)
|
||||
@@ -340,8 +328,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
.should('be.eq', 3);
|
||||
});
|
||||
|
||||
cy.get('@layoutBtn').click();
|
||||
cy.get('tbody > :nth-child(3) > :nth-child(1)').click();
|
||||
cy.setLayout(3, 1);
|
||||
cy.get('.viewport-container').then($viewport => {
|
||||
cy.wrap($viewport)
|
||||
.its('length')
|
||||
@@ -349,8 +336,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
});
|
||||
|
||||
//verify if layout has changed to 4 viewports
|
||||
cy.get('@layoutBtn').click();
|
||||
cy.get('tbody > :nth-child(2) > :nth-child(2)').click();
|
||||
cy.setLayout(2, 2);
|
||||
cy.get('.viewport-container').then($viewport => {
|
||||
cy.wrap($viewport)
|
||||
.its('length')
|
||||
@@ -358,16 +344,14 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
});
|
||||
|
||||
//verify if layout has changed to 6 viewports
|
||||
cy.get('@layoutBtn').click();
|
||||
cy.get('tbody > :nth-child(2) > :nth-child(3)').click();
|
||||
cy.setLayout(2, 3);
|
||||
cy.get('.viewport-container').then($viewport => {
|
||||
cy.wrap($viewport)
|
||||
.its('length')
|
||||
.should('be.eq', 6);
|
||||
});
|
||||
|
||||
cy.get('@layoutBtn').click();
|
||||
cy.get('tbody > :nth-child(3) > :nth-child(2)').click();
|
||||
cy.setLayout(3, 2);
|
||||
cy.get('.viewport-container').then($viewport => {
|
||||
cy.wrap($viewport)
|
||||
.its('length')
|
||||
@@ -375,8 +359,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
});
|
||||
|
||||
//verify if layout has changed to 9 viewports
|
||||
cy.get('@layoutBtn').click();
|
||||
cy.get('tbody > :nth-child(3) > :nth-child(3)').click();
|
||||
cy.setLayout(3, 3);
|
||||
cy.get('.viewport-container').then($viewport => {
|
||||
cy.wrap($viewport)
|
||||
.its('length')
|
||||
@@ -384,8 +367,7 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
});
|
||||
|
||||
//verify if layout has changed to 1 viewport
|
||||
cy.get('@layoutBtn').click();
|
||||
cy.get('tbody > :nth-child(1) > :nth-child(1)').click();
|
||||
cy.setLayout(1, 1);
|
||||
cy.get('.viewport-container').then($viewport => {
|
||||
cy.wrap($viewport)
|
||||
.its('length')
|
||||
@@ -425,7 +407,9 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
.as('toolbarOverlay')
|
||||
.should('be.visible');
|
||||
//Click on Clear button
|
||||
cy.get('.tooltip-inner > :nth-child(10)').click();
|
||||
cy.get('.tooltip-inner')
|
||||
.contains('Clear')
|
||||
.click();
|
||||
|
||||
//Verify if measurements were removed from the measurements panel
|
||||
cy.get('.measurementItem').should('not.exist');
|
||||
@@ -457,8 +441,15 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
|
||||
//Click More button
|
||||
cy.get('@moreBtn').click();
|
||||
//Click Eraser button
|
||||
cy.get('.tooltip-inner > :nth-child(12)').click();
|
||||
//Verify if overlay is displayed
|
||||
cy.get('.tooltip-toolbar-overlay')
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
//Click Eraser button
|
||||
cy.get('.tooltip-inner')
|
||||
.contains('Eraser')
|
||||
.click({ force: true });
|
||||
});
|
||||
|
||||
//Erase measurement #1 and Verify if it was removed from the measurements panel
|
||||
const [x1, y1] = [150, 100];
|
||||
@@ -472,4 +463,70 @@ describe('OHIF Cornerstone Toolbar', () => {
|
||||
cy.get('@viewport').click(x2, y2, { force: true });
|
||||
cy.get('.measurementItem').should('not.exist');
|
||||
});
|
||||
|
||||
it('check if Invert tool will change the colors of the image in the viewport', () => {
|
||||
// Click on More button
|
||||
cy.get('@moreBtn').click();
|
||||
// Verify if overlay is displayed
|
||||
cy.get('.tooltip-toolbar-overlay').should('be.visible');
|
||||
|
||||
// Click on Invert button
|
||||
cy.get('.tooltip-inner')
|
||||
.contains('Invert')
|
||||
.click();
|
||||
|
||||
// Visual comparison
|
||||
cy.screenshot('Invert tool - Should Invert Canvas');
|
||||
cy.percyCanvasSnapshot('Invert tool - Should Invert Canvas');
|
||||
});
|
||||
|
||||
it('check if Rotate tool will change the image orientation in the viewport', () => {
|
||||
//Click on More button
|
||||
cy.get('@moreBtn').click();
|
||||
//Verify if overlay is displayed
|
||||
cy.get('.tooltip-toolbar-overlay')
|
||||
.should('be.visible')
|
||||
.then(() => {
|
||||
//Click on Rotate button
|
||||
cy.get('.tooltip-inner')
|
||||
.contains('Rotate')
|
||||
.click({ force: true });
|
||||
});
|
||||
|
||||
// Visual comparison
|
||||
cy.screenshot('Rotate tool - Should Rotate Image to Right');
|
||||
cy.percyCanvasSnapshot('Rotate tool - Should Rotate Image to Right');
|
||||
});
|
||||
|
||||
it('check if Flip H tool will flip the image horizontally in the viewport', () => {
|
||||
//Click on More button
|
||||
cy.get('@moreBtn').click();
|
||||
//Verify if overlay is displayed
|
||||
cy.get('.tooltip-toolbar-overlay').should('be.visible');
|
||||
|
||||
//Click on Flip H button
|
||||
cy.get('.tooltip-inner')
|
||||
.contains('Flip H')
|
||||
.click();
|
||||
|
||||
// Visual comparison
|
||||
cy.screenshot('Flip H tool - Should Flip Image on Y axis');
|
||||
cy.percyCanvasSnapshot('Flip H tool - Should Flip Image on Y axis');
|
||||
});
|
||||
|
||||
it('check if Flip V tool will flip the image vertically in the viewport', () => {
|
||||
//Click on More button
|
||||
cy.get('@moreBtn').click();
|
||||
//Verify if overlay is displayed
|
||||
cy.get('.tooltip-toolbar-overlay').should('be.visible');
|
||||
|
||||
//Click on Flip V button
|
||||
cy.get('.tooltip-inner')
|
||||
.contains('Flip V')
|
||||
.click();
|
||||
|
||||
// Visual comparison
|
||||
cy.screenshot('Flip V tool - Should Flip Image on X axis');
|
||||
cy.percyCanvasSnapshot('Flip V tool - Should Flip Image on X axis');
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ describe('OHIFStandaloneViewer', () => {
|
||||
});
|
||||
|
||||
it('loads route with at least 2 rows', () => {
|
||||
cy.screenshot();
|
||||
cy.screenshot('Study List');
|
||||
cy.percyCanvasSnapshot('Study List');
|
||||
|
||||
cy.get('[data-cy="study-list-results"] tr')
|
||||
|
||||
@@ -11,8 +11,8 @@ describe('OHIF Study Viewer Page', function() {
|
||||
});
|
||||
|
||||
it('checks if series thumbnails are being displayed', function() {
|
||||
cy.screenshot();
|
||||
cy.percyCanvasSnapshot('Series Thumbnails');
|
||||
cy.screenshot('Series Thumbnails - Should Display Thumbnails');
|
||||
cy.percyCanvasSnapshot('Series Thumbnails - Should Display Thumbnails');
|
||||
|
||||
cy.get('[data-cy="thumbnail-list"]')
|
||||
.its('length')
|
||||
@@ -267,8 +267,12 @@ describe('OHIF Study Viewer Page', function() {
|
||||
});
|
||||
|
||||
it('opens About modal and verify the displayed information', function() {
|
||||
cy.get('[data-cy="options-menu"]').click();
|
||||
cy.get('[data-cy="about-item-menu"]').click();
|
||||
cy.get('[data-cy="options-menu"]')
|
||||
.first()
|
||||
.click();
|
||||
cy.get('[data-cy="about-item-menu"]')
|
||||
.first()
|
||||
.click();
|
||||
cy.get('.modal-content')
|
||||
.as('aboutOverlay')
|
||||
.should('be.visible');
|
||||
@@ -287,6 +291,10 @@ describe('OHIF Study Viewer Page', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// Visual comparison
|
||||
cy.screenshot('About modal - Should display modal');
|
||||
cy.percyCanvasSnapshot('About modal - Should display modal');
|
||||
|
||||
//close modal
|
||||
cy.get('.close').click();
|
||||
cy.get('@aboutOverlay').should('not.be.enabled');
|
||||
|
||||
@@ -21,6 +21,10 @@ describe('OHIF Microscopy Extension', () => {
|
||||
.should('be.eq', 1);
|
||||
|
||||
cy.wait(3000); //Waiting for image to render before taking the snapshot
|
||||
cy.percyCanvasSnapshot('Microscopy Extension');
|
||||
// Visual comparison
|
||||
cy.screenshot('Microscopy Extension - Should display loaded canvas');
|
||||
cy.percyCanvasSnapshot(
|
||||
'Microscopy Extension - Should display loaded canvas'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,7 @@ describe('OHIF PDF Extension', () => {
|
||||
|
||||
// This won't work unless we switch to an extension that renders using `canvas`
|
||||
// Currently, we rely on the browser's built-in implementation
|
||||
cy.percyCanvasSnapshot('PDF Extension');
|
||||
cy.screenshot('PDF Extension - Should load PDF file');
|
||||
cy.percyCanvasSnapshot('PDF Extension - Should load PDF file');
|
||||
});
|
||||
});
|
||||
@@ -6,37 +6,36 @@ describe('OHIF VTK Extension', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
//TODO: Due to percy snapshot, the images inside the viewports are being resized
|
||||
//and once the browser continues the test execution, the images are not centered in the viewport.
|
||||
//To fix that, we need to reload the page before each test.
|
||||
//https://github.com/OHIF/Viewers/issues/1168
|
||||
cy.reload();
|
||||
|
||||
//Waiting for the desired thumbnail content to be displayed
|
||||
cy.get('[data-cy="thumbnail-list"]').should($list => {
|
||||
expect($list).to.contain('CT WB 5.0 B35f');
|
||||
});
|
||||
|
||||
// TODO: We shouldn't have to drag the thumbnail
|
||||
// This is a known bug; 2D MPR button does not show until viewport
|
||||
// has data from a drag-n-drop
|
||||
// Drag and drop first thumbnail into first viewport
|
||||
cy.get('[data-cy="thumbnail-list"]:nth-child(3)').drag(
|
||||
'.viewport-drop-target'
|
||||
);
|
||||
// Drag and drop third thumbnail into first viewport
|
||||
cy.get('[data-cy="thumbnail-list"]')
|
||||
.contains('CT WB 5.0 B35f')
|
||||
.drag('.viewport-drop-target')
|
||||
.wait(1000); // TODO: Once issue #1167 is fixed, the wait command should be removed. (https://github.com/OHIF/Viewers/issues/1167)
|
||||
|
||||
cy.get('.PluginSwitch > .toolbar-button')
|
||||
.as('twodmprBtn')
|
||||
.should('be.visible')
|
||||
.then(btn => {
|
||||
if (!btn.text().includes('Exit')) {
|
||||
btn.click();
|
||||
}
|
||||
});
|
||||
//Select 2D MPR button
|
||||
cy.get('.PluginSwitch > .toolbar-button').click();
|
||||
|
||||
//Wait Reformatting Images
|
||||
cy.waitVTKReformatting();
|
||||
|
||||
cy.initVTKToolsAliases();
|
||||
});
|
||||
|
||||
it('checks if VTK buttons are displayed on the toolbar', () => {
|
||||
// Wait for start reformatting
|
||||
cy.get('[data-cy="viewprt-grid"]', { timeout: 10000 }).should($grid => {
|
||||
expect($grid).to.contain.text('Reform');
|
||||
});
|
||||
|
||||
// Wait for finish reformatting
|
||||
cy.get('[data-cy="viewprt-grid"]', { timeout: 30000 }).should($grid => {
|
||||
expect($grid).not.to.contain.text('Reform');
|
||||
});
|
||||
|
||||
cy.get('@crosshairsBtn')
|
||||
.should('be.visible')
|
||||
.contains('Crosshairs');
|
||||
@@ -57,7 +56,67 @@ describe('OHIF VTK Extension', () => {
|
||||
.should('be.visible')
|
||||
.contains('Layout');
|
||||
|
||||
cy.wait(3000);
|
||||
cy.percyCanvasSnapshot('VTK Extension');
|
||||
// Visual comparison
|
||||
cy.screenshot('VTK initial state - Should display toolbar and 3 viewports');
|
||||
cy.percyCanvasSnapshot(
|
||||
'VTK initial state - Should display toolbar and 3 viewports'
|
||||
);
|
||||
});
|
||||
|
||||
it('checks Crosshairs tool', () => {
|
||||
cy.get('@crosshairsBtn').click();
|
||||
|
||||
// Click and Move the mouse inside the viewport
|
||||
cy.get('[data-cy="viewport-container-0"]')
|
||||
.trigger('mousedown', 'center', { which: 1 })
|
||||
.trigger('mousemove', 'top', { which: 1 })
|
||||
.trigger('mouseup');
|
||||
|
||||
// Visual comparison
|
||||
cy.screenshot(
|
||||
"VTK Crosshairs tool - Should display crosshairs' green lines"
|
||||
);
|
||||
cy.percyCanvasSnapshot(
|
||||
"VTK Crosshairs tool - Should display crosshairs' green lines"
|
||||
);
|
||||
});
|
||||
|
||||
it('checks WWWC tool', () => {
|
||||
cy.get('@wwwcBtn').click();
|
||||
|
||||
// Click and Move the mouse inside the viewport
|
||||
cy.get('[data-cy="viewport-container-0"]')
|
||||
.trigger('mousedown', 'center', { which: 1 })
|
||||
.trigger('mousemove', 'top', { which: 1 })
|
||||
.trigger('mousedown', 'center', { which: 1 })
|
||||
.trigger('mousemove', 'top', { which: 1 })
|
||||
.trigger('mouseup', { which: 1 })
|
||||
.then(() => {
|
||||
const expectedText = 'W: 350 L: -1044';
|
||||
cy.get('.ViewportOverlay > div.bottom-right.overlay-element').should(
|
||||
'contains.text',
|
||||
expectedText
|
||||
);
|
||||
});
|
||||
|
||||
// Visual comparison
|
||||
cy.screenshot('VTK WWWC tool - Canvas should be bright');
|
||||
cy.percyCanvasSnapshot('VTK WWWC tool - Canvas should be bright');
|
||||
});
|
||||
|
||||
it('checks Rotate tool', () => {
|
||||
cy.get('@rotateBtn').click();
|
||||
|
||||
// Click and Move the mouse inside the viewport
|
||||
cy.get('[data-cy="viewport-container-0"]')
|
||||
.trigger('mousedown', 'center', { which: 1 })
|
||||
.trigger('mousemove', 'top', { which: 1 })
|
||||
.trigger('mousedown', 'center', { which: 1 })
|
||||
.trigger('mousemove', 'top', { which: 1 })
|
||||
.trigger('mouseup', { which: 1 });
|
||||
|
||||
// Visual comparison
|
||||
cy.screenshot('VTK Rotate tool - Should rotate image');
|
||||
cy.percyCanvasSnapshot('VTK Rotate tool - Should rotate image');
|
||||
});
|
||||
});
|
||||
@@ -90,6 +90,18 @@ Cypress.Commands.add('waitStudyList', () => {
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('waitVTKReformatting', () => {
|
||||
// Wait for start reformatting
|
||||
cy.get('[data-cy="viewprt-grid"]', { timeout: 10000 }).should($grid => {
|
||||
expect($grid).to.contain.text('Reform');
|
||||
});
|
||||
|
||||
// Wait for finish reformatting
|
||||
cy.get('[data-cy="viewprt-grid"]', { timeout: 30000 }).should($grid => {
|
||||
expect($grid).not.to.contain.text('Reform');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Command to perform a drag and drop action. Before using this command, we must get the element that should be dragged first.
|
||||
* Example of usage: cy.get(element-to-be-dragged).drag(dropzone-element)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/viewer",
|
||||
"version": "2.6.4",
|
||||
"version": "2.8.5",
|
||||
"description": "OHIF Viewer",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
@@ -45,14 +45,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@ohif/core": "^1.7.1",
|
||||
"@ohif/core": "^1.10.0",
|
||||
"@ohif/extension-cornerstone": "^2.0.0",
|
||||
"@ohif/extension-dicom-html": "^1.0.1",
|
||||
"@ohif/extension-dicom-microscopy": "^0.50.6",
|
||||
"@ohif/extension-dicom-pdf": "^1.0.0",
|
||||
"@ohif/extension-vtk": "^0.53.3",
|
||||
"@ohif/i18n": "^0.52.0",
|
||||
"@ohif/ui": "^0.60.1",
|
||||
"@ohif/extension-vtk": "^0.53.9",
|
||||
"@ohif/i18n": "^0.52.1",
|
||||
"@ohif/ui": "^0.62.4",
|
||||
"@tanem/react-nprogress": "^1.1.25",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.2.1",
|
||||
|
||||
@@ -6,7 +6,10 @@ import './config';
|
||||
import {
|
||||
CommandsManager,
|
||||
ExtensionManager,
|
||||
ServicesManager,
|
||||
HotkeysManager,
|
||||
createUINotificationService,
|
||||
createUIModalService,
|
||||
utils,
|
||||
} from '@ohif/core';
|
||||
import React, { Component } from 'react';
|
||||
@@ -41,9 +44,17 @@ const commandsManagerConfig = {
|
||||
getActiveContexts: () => getActiveContexts(store.getState()),
|
||||
};
|
||||
|
||||
// Services
|
||||
const UINotificationService = createUINotificationService();
|
||||
const UIModalService = createUIModalService();
|
||||
|
||||
const commandsManager = new CommandsManager(commandsManagerConfig);
|
||||
const hotkeysManager = new HotkeysManager(commandsManager);
|
||||
const extensionManager = new ExtensionManager({ commandsManager });
|
||||
const servicesManager = new ServicesManager();
|
||||
const extensionManager = new ExtensionManager({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
});
|
||||
// ~~~~ END APP SETUP
|
||||
|
||||
// TODO[react] Use a provider when the whole tree is React
|
||||
@@ -76,9 +87,11 @@ class App extends Component {
|
||||
super(props);
|
||||
|
||||
this._appConfig = props;
|
||||
|
||||
const { servers, extensions, hotkeys, oidc } = props;
|
||||
|
||||
this.initUserManager(oidc);
|
||||
_initServices([UINotificationService, UIModalService]);
|
||||
_initExtensions(extensions, hotkeys);
|
||||
_initServers(servers);
|
||||
initWebWorkers();
|
||||
@@ -100,8 +113,11 @@ class App extends Component {
|
||||
<UserManagerContext.Provider value={userManager}>
|
||||
<Router basename={routerBasename}>
|
||||
<WhiteLabellingContext.Provider value={whiteLabelling}>
|
||||
<SnackbarProvider>
|
||||
<ModalProvider modal={OHIFModal}>
|
||||
<SnackbarProvider service={UINotificationService}>
|
||||
<ModalProvider
|
||||
modal={OHIFModal}
|
||||
service={UIModalService}
|
||||
>
|
||||
<OHIFStandaloneViewer userManager={userManager} />
|
||||
</ModalProvider>
|
||||
</SnackbarProvider>
|
||||
@@ -121,8 +137,8 @@ class App extends Component {
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Router basename={routerBasename}>
|
||||
<WhiteLabellingContext.Provider value={whiteLabelling}>
|
||||
<SnackbarProvider>
|
||||
<ModalProvider modal={OHIFModal}>
|
||||
<SnackbarProvider service={UINotificationService}>
|
||||
<ModalProvider modal={OHIFModal} service={UIModalService}>
|
||||
<OHIFStandaloneViewer />
|
||||
</ModalProvider>
|
||||
</SnackbarProvider>
|
||||
@@ -168,6 +184,10 @@ class App extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
function _initServices(services) {
|
||||
servicesManager.registerServices(services);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param
|
||||
*/
|
||||
@@ -214,4 +234,4 @@ function _makeAbsoluteIfNecessary(url, base_url) {
|
||||
const ExportedApp = process.env.NODE_ENV === 'development' ? hot(App) : App;
|
||||
|
||||
export default ExportedApp;
|
||||
export { commandsManager, extensionManager, hotkeysManager };
|
||||
export { commandsManager, extensionManager, hotkeysManager, servicesManager };
|
||||
@@ -7,10 +7,10 @@ export default {
|
||||
*/
|
||||
id: 'measurements-table',
|
||||
|
||||
preRegistration(configuration = {}) {
|
||||
init(configuration);
|
||||
preRegistration({ servicesManager, configuration = {} }) {
|
||||
init({ servicesManager, configuration });
|
||||
},
|
||||
getPanelModule() {
|
||||
getPanelModule({ servicesManager }) {
|
||||
return {
|
||||
menuOptions: [
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@ const MEASUREMENT_ACTION_MAP = {
|
||||
* @export
|
||||
* @param {*} configuration
|
||||
*/
|
||||
export default function init(configuration) {
|
||||
export default function init({ servicesManager, configuration = {} }) {
|
||||
// If these tools were already added by a different extension, we want to replace
|
||||
// them with the same tools that have an alternative configuration. By passing in
|
||||
// our custom `getMeasurementLocationCallback`, we can...
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
height: var(--top-bar-height);
|
||||
}
|
||||
|
||||
.dd-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Home Page */
|
||||
.entry-header.header-big {
|
||||
background: rgba(21, 25, 30, 0.7);
|
||||
|
||||
@@ -3,13 +3,11 @@ import { Link, withRouter } from 'react-router-dom';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Dropdown } from '@ohif/ui';
|
||||
import { AboutContent } from '@ohif/ui';
|
||||
import { withModal } from '@ohif/ui';
|
||||
|
||||
import ConnectedUserPreferencesForm from '../../connectedComponents/ConnectedUserPreferencesForm';
|
||||
import { Dropdown, AboutContent, withModal } from '@ohif/ui';
|
||||
import OHIFLogo from '../OHIFLogo/OHIFLogo.js';
|
||||
import { hotkeysManager } from './../../App.js';
|
||||
import './Header.css';
|
||||
|
||||
// Context
|
||||
import AppContext from './../../context/AppContext';
|
||||
|
||||
@@ -22,7 +20,7 @@ class Header extends Component {
|
||||
t: PropTypes.func.isRequired,
|
||||
userManager: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
modalContext: PropTypes.object,
|
||||
modal: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -30,23 +28,9 @@ class Header extends Component {
|
||||
children: OHIFLogo(),
|
||||
};
|
||||
|
||||
// onSave: data => {
|
||||
// const contextName = store.getState().commandContext.context;
|
||||
// const preferences = cloneDeep(store.getState().preferences);
|
||||
// preferences[contextName] = data;
|
||||
// dispatch(setUserPreferences(preferences));
|
||||
// dispatch(setUserPreferencesModalOpen(false));
|
||||
// OHIF.hotkeysUtil.setHotkeys(data.hotKeysData);
|
||||
// },
|
||||
// onResetToDefaults: () => {
|
||||
// dispatch(setUserPreferences());
|
||||
// dispatch(setUserPreferencesModalOpen(false));
|
||||
// OHIF.hotkeysUtil.setHotkeys();
|
||||
// },
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isUserPreferencesOpen: false, isOpen: false };
|
||||
this.state = { isOpen: false };
|
||||
|
||||
this.loadOptions();
|
||||
}
|
||||
@@ -56,7 +40,7 @@ class Header extends Component {
|
||||
t,
|
||||
user,
|
||||
userManager,
|
||||
modalContext: { show },
|
||||
modal: { show },
|
||||
} = this.props;
|
||||
this.options = [
|
||||
{
|
||||
@@ -68,6 +52,16 @@ class Header extends Component {
|
||||
customClassName: 'AboutContent',
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: 'Preferences ',
|
||||
icon: {
|
||||
name: 'user',
|
||||
},
|
||||
onClick: () =>
|
||||
show(ConnectedUserPreferencesForm, {
|
||||
title: t('User Preferences'),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
if (user && userManager) {
|
||||
@@ -77,15 +71,6 @@ class Header extends Component {
|
||||
onClick: () => userManager.signoutRedirect(),
|
||||
});
|
||||
}
|
||||
|
||||
this.hotKeysData = hotkeysManager.hotkeyDefinitions;
|
||||
}
|
||||
|
||||
onUserPreferencesSave({ windowLevelData, hotKeysData }) {
|
||||
// console.log(windowLevelData);
|
||||
// console.log(hotKeysData);
|
||||
// TODO: Update hotkeysManager
|
||||
// TODO: reset `this.hotKeysData`
|
||||
}
|
||||
|
||||
// ANTD -- Hamburger, Drawer, Menu
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import Header from '../components/Header/Header.js';
|
||||
import { connect } from 'react-redux';
|
||||
import { hotkeysManager } from '../App.js';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { hotkeyDefinitions = [] } = state.preferences || {};
|
||||
|
||||
const _hotkeyDefinitions =
|
||||
hotkeyDefinitions.length > 0
|
||||
? hotkeyDefinitions
|
||||
: hotkeysManager.hotkeyDefaults;
|
||||
|
||||
hotkeysManager.setHotkeys(_hotkeyDefinitions);
|
||||
|
||||
return {
|
||||
user: state.oidc && state.oidc.user,
|
||||
isOpen: state.ui.userPreferencesModalOpen,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { UserPreferencesForm } from '@ohif/ui';
|
||||
import OHIF from '@ohif/core';
|
||||
import { hotkeysManager } from '../App.js';
|
||||
|
||||
const { setUserPreferences } = OHIF.redux.actions;
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const hotkeyDefinitions =
|
||||
state.preferences.hotkeyDefinitions.length > 0
|
||||
? state.preferences.hotkeyDefinitions
|
||||
: hotkeysManager.hotkeyDefaults;
|
||||
hotkeysManager.setHotkeys(hotkeyDefinitions);
|
||||
return {
|
||||
onClose: ownProps.hide,
|
||||
windowLevelData: state.preferences ? state.preferences.windowLevelData : {},
|
||||
hotkeyDefinitions,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, ownProps) => {
|
||||
return {
|
||||
onSave: ({ windowLevelData, hotkeyDefinitions }) => {
|
||||
hotkeysManager.setHotkeys(hotkeyDefinitions);
|
||||
ownProps.hide();
|
||||
dispatch(setUserPreferences({ windowLevelData, hotkeyDefinitions }));
|
||||
},
|
||||
onResetToDefaults: () => {
|
||||
hotkeysManager.restoreDefaultBindings();
|
||||
ownProps.hide();
|
||||
dispatch(setUserPreferences());
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ConnectedUserPreferencesForm = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(UserPreferencesForm);
|
||||
|
||||
export default ConnectedUserPreferencesForm;
|
||||
@@ -287,7 +287,7 @@ function _handleBuiltIn({ behavior } = {}) {
|
||||
}
|
||||
|
||||
if (behavior === 'DOWNLOAD_SCREEN_SHOT') {
|
||||
this.props.modalContext.show(ConnectedViewportDownloadForm, {
|
||||
this.props.modal.show(ConnectedViewportDownloadForm, {
|
||||
title: this.props.t('Download High Quality Image'),
|
||||
customClassName: 'ViewportDownloadForm',
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ export default class DatasetPicker extends Component {
|
||||
error: null,
|
||||
loading: true,
|
||||
datasets: [],
|
||||
filterStr: ""
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
@@ -41,15 +42,23 @@ export default class DatasetPicker extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { datasets, loading, error } = this.state;
|
||||
const { datasets, loading, error, filterStr } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<input class="form-control gcp-input"
|
||||
type="text"
|
||||
value={ filterStr }
|
||||
onChange={ e => this.setState({ filterStr: e.target.value }) }
|
||||
/>
|
||||
<DatasetsList
|
||||
datasets={datasets}
|
||||
loading={loading}
|
||||
error={error}
|
||||
filter={filterStr}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -46,21 +46,23 @@ class DatasetsList extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
const { loading, datasets, filter, error } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <p>{error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
if (loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="DatasetList">
|
||||
{this.props.datasets.map(this.renderTableRow)}
|
||||
{datasets.filter(dataset => (dataset.name.split('/')[5].toLowerCase().includes(filter.toLowerCase()) || filter=="")).map(this.renderTableRow)}
|
||||
</tbody>
|
||||
);
|
||||
|
||||
@@ -71,7 +73,7 @@ class DatasetsList extends Component {
|
||||
<th>{this.props.t('Dataset')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.props.datasets && body}
|
||||
{datasets && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,20 +46,24 @@ class DicomStoreList extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
const { loading, stores, filter, error } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <p>{error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
if (loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="StoreList">{this.props.stores.map(this.renderTableRow)}</tbody>
|
||||
<tbody id="StoreList">{
|
||||
stores.filter(store => (store.name.split('/')[7].toLowerCase().includes(filter.toLowerCase()) || filter=="")).map(this.renderTableRow)}
|
||||
</tbody>
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -69,7 +73,7 @@ class DicomStoreList extends Component {
|
||||
<th>{this.props.t('DICOM Store')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.props.stores && body}
|
||||
{stores && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export default class DicomStorePicker extends Component {
|
||||
loading: true,
|
||||
stores: [],
|
||||
locations: [],
|
||||
filterStr: ""
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
@@ -38,16 +39,24 @@ export default class DicomStorePicker extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { stores, loading, error } = this.state;
|
||||
const { stores, loading, error, filterStr } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
|
||||
return (
|
||||
<DicomStoreList
|
||||
stores={stores}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
<div>
|
||||
<input class="form-control gcp-input"
|
||||
type="text"
|
||||
value={ filterStr }
|
||||
onChange={ e => this.setState({ filterStr: e.target.value }) }
|
||||
/>
|
||||
<DicomStoreList
|
||||
stores={stores}
|
||||
loading={loading}
|
||||
error={error}
|
||||
filter={filterStr}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export default class LocationPicker extends Component {
|
||||
error: null,
|
||||
loading: true,
|
||||
locations: [],
|
||||
filterStr: "",
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
@@ -37,15 +38,23 @@ export default class LocationPicker extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { locations, loading, error } = this.state;
|
||||
const { locations, loading, error, filterStr } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
return (
|
||||
<LocationsList
|
||||
locations={locations}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
<div>
|
||||
<input class="form-control gcp-input"
|
||||
type="text"
|
||||
value={ filterStr }
|
||||
onChange={ e => this.setState({ filterStr: e.target.value }) }
|
||||
/>
|
||||
<LocationsList
|
||||
locations={locations}
|
||||
loading={loading}
|
||||
error={error}
|
||||
filter={filterStr}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -46,21 +46,23 @@ class LocationsList extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
const { loading, locations, filter, error } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <p>{error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
if (loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="LocationList">
|
||||
{this.props.locations.map(this.renderTableRow)}
|
||||
{locations.filter(location => (location.name.split('/')[3].toLowerCase().includes(filter.toLowerCase()) || filter=="")).map(this.renderTableRow)}
|
||||
</tbody>
|
||||
);
|
||||
|
||||
@@ -71,7 +73,7 @@ class LocationsList extends Component {
|
||||
<th>{this.props.t('Location')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.props.locations && body}
|
||||
{locations && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,20 +30,29 @@ export default class ProjectPicker extends Component {
|
||||
|
||||
this.setState({
|
||||
projects: response.data.projects || [],
|
||||
filterStr: "",
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { projects, loading, error } = this.state;
|
||||
const { projects, loading, filterStr, error } = this.state;
|
||||
const { onSelect } = this.props;
|
||||
return (
|
||||
<ProjectsList
|
||||
projects={projects}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
<div>
|
||||
<input class="form-control gcp-input"
|
||||
type="text"
|
||||
value={ filterStr }
|
||||
onChange={ e => this.setState({ filterStr: e.target.value }) }
|
||||
/>
|
||||
<ProjectsList
|
||||
projects={projects}
|
||||
loading={loading}
|
||||
filter={filterStr}
|
||||
error={error}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -50,21 +50,22 @@ class ProjectsList extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.error) {
|
||||
return <p>{this.props.error}</p>;
|
||||
const { loading, projects, filter, error } = this.props;
|
||||
|
||||
if (error) {
|
||||
return <p>{error}</p>;
|
||||
}
|
||||
|
||||
const loadingIcon = (
|
||||
<Icon name="circle-notch" className="loading-icon-spin loading-icon" />
|
||||
);
|
||||
|
||||
if (this.props.loading) {
|
||||
if (loading) {
|
||||
return loadingIcon;
|
||||
}
|
||||
|
||||
const body = (
|
||||
<tbody id="ProjectList">
|
||||
{this.props.projects.map(this.renderTableRow)}
|
||||
{projects.filter(project => (project.name.toLowerCase().includes(filter.toLowerCase()) || filter=="")).map(this.renderTableRow)}
|
||||
</tbody>
|
||||
);
|
||||
|
||||
@@ -76,7 +77,7 @@ class ProjectsList extends Component {
|
||||
<th>{this.props.t('ID')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{this.props.projects && body}
|
||||
{projects && body}
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -141,3 +141,15 @@
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.gcp-input{
|
||||
height: 40px;
|
||||
margin: 0 5px 20px 5px;
|
||||
padding: 0 20px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background-color: var(--input-background-color);
|
||||
color: var(--input-placeholder-color);
|
||||
font-size: 10pt;
|
||||
font-weight: normal;
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Dropzone from 'react-dropzone';
|
||||
import OHIF from '@ohif/core';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -113,7 +112,8 @@ function StudyListRoute(props) {
|
||||
pageNumber,
|
||||
displaySize,
|
||||
server,
|
||||
]);
|
||||
]
|
||||
);
|
||||
|
||||
// TODO: Update Server
|
||||
// if (this.props.server !== prevProps.server) {
|
||||
@@ -188,10 +188,12 @@ function StudyListRoute(props) {
|
||||
}
|
||||
|
||||
function handleFilterChange(fieldName, value) {
|
||||
const updatedFilterValues = Object.assign({}, filterValues);
|
||||
|
||||
updatedFilterValues[fieldName] = value;
|
||||
setFilterValues(updatedFilterValues);
|
||||
setFilterValues(state => {
|
||||
return {
|
||||
...state,
|
||||
[fieldName]: value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -31,4 +31,12 @@
|
||||
|
||||
/** z-Index **/
|
||||
--snackbar-zIndex: 10;
|
||||
|
||||
/** DatePicker Colors **/
|
||||
--datePicker-input-background: #2c363f;
|
||||
--datePicker-picker-background: #9ccef9;
|
||||
--datePicker-picker-disabled: #d3d3d3;
|
||||
--datePicker-picker-text: #484848;
|
||||
--datePicker-picker-text-disabled: #aaa;
|
||||
--datePicker-picker-text-input: #fff;
|
||||
}
|
||||
@@ -1100,34 +1100,13 @@
|
||||
pirates "^4.0.0"
|
||||
source-map-support "^0.5.9"
|
||||
|
||||
"@babel/runtime@7.1.2":
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.1.2.tgz#81c89935f4647706fc54541145e6b4ecfef4b8e3"
|
||||
integrity sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.12.0"
|
||||
|
||||
"@babel/runtime@7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.0.tgz#4fc1d642a9fd0299754e8b5de62c631cf5568205"
|
||||
integrity sha512-89eSBLJsxNxOERC0Op4vd+0Bqm6wRMqMbFtV3i0/fbaWw/mJ8Q3eBvgX0G4SyrOOLCtbu98HspF8o09MRT+KzQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5":
|
||||
"@babel/runtime@7.1.2", "@babel/runtime@7.5.5", "@babel/runtime@7.6.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3":
|
||||
version "7.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
|
||||
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3":
|
||||
version "7.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.2.tgz#111a78002a5c25fc8e3361bedc9529c696b85a6a"
|
||||
integrity sha512-JONRbXbTXc9WQE2mAZd1p0Z3DZ/6vaQIkgYMSTP3KjRCyd7rCZCcfhCyX+YjwcKxcZ82UrxbRD358bpExNgrjw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6"
|
||||
@@ -16634,11 +16613,6 @@ regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1:
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
|
||||
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
|
||||
|
||||
regenerator-runtime@^0.12.0:
|
||||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
|
||||
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
|
||||
|
||||
regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.2:
|
||||
version "0.13.3"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
|
||||
|
||||
Reference in new issue
Block a user