Compare commits

...
Author SHA1 Message Date
ohif-bot 57730483e9 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.8
 - @ohif/core@1.10.0
 - @ohif/ui@0.62.3
 - @ohif/viewer@2.8.4
2019-11-15 20:04:45 +00:00
Danny Brown 731b64e321 feat: new modal service
feat: new modal service
2019-11-15 15:03:20 -05:00
Danny Brown e97a09b0f4 Merge branch 'master' into feat/new-modal-service 2019-11-15 14:52:37 -05:00
Danny Brown f63d8a73d8 feat: Inject into Extension Modules / improve tests
[UI Services to Extensions] Inject into Extension Modules / improve tests
2019-11-15 14:47:40 -05:00
ohif-bot f10adf82f4 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.5.1
 - @ohif/extension-vtk@0.53.7
 - @ohif/core@1.9.1
 - @ohif/ui@0.62.2
 - @ohif/viewer@2.8.3
2019-11-15 18:44:17 +00:00
Danny Brown 6174109d5a feat: Add new modal service
[UI Services to Extensions]: Add new modal service
2019-11-15 13:42:20 -05:00
Danny Brown 244e822afc Merge branch 'master' into feat/new-modal-service 2019-11-15 13:31:16 -05:00
Igor cbdeeae597 Merge branch 'feat/new-modal-service' of github.com:OHIF/Viewers into feat/new-modal-service 2019-11-15 15:21:02 -03:00
Igor 7f6de72e8e Add default props to modal 2019-11-15 15:20:26 -03:00
Danny Brown 1f514e5f1b ci: remove e2e for deploy; add docs
ci: remove e2e for deploy; add docs
2019-11-15 12:03:30 -05:00
dannyrb f3fed511b9 docs: continous integrationn 2019-11-15 12:02:08 -05:00
dannyrb 31796d272c Merge branch 'ci/complete-promoted-deploy' of github.com:dannyrb/Viewers into ci/complete-promoted-deploy 2019-11-15 11:11:26 -05:00
dannyrb b41e6f93f3 Add workflow images 2019-11-15 11:11:14 -05:00
dannyrb 882a45d398 Remove e2e test before prod deploy 2019-11-15 11:08:52 -05:00
Danny Brown fb0cd9355f Merge pull request #1189 from dannyrb/ci/complete-promoted-deploy
Ci/complete promoted deploy
2019-11-15 10:33:01 -05:00
Danny Brown 2b12193020 Merge branch 'master' into ci/complete-promoted-deploy 2019-11-15 10:32:53 -05:00
dannyrb 9a626a2064 Switch to npx instead of digging into npm bin location 2019-11-15 10:31:16 -05:00
dannyrb 903777044a Use an existing docker hub image 2019-11-15 10:21:47 -05:00
Danny Brown 5e4725860d Merge pull request #1188 from dannyrb/ci/complete-promoted-deploy
Ci/complete promoted deploy
2019-11-15 10:08:43 -05:00
dannyrb 670470119f Spacing 2019-11-15 09:38:21 -05:00
dannyrb 0d4f90f3cd Custom executor to override cypress config 2019-11-15 09:34:57 -05:00
dannyrb 43dd87c2c1 Clean up Deploy workflow 2019-11-15 09:18:56 -05:00
dannyrb 98f8583fba Clean up DOCS_PUBLISH step 2019-11-15 09:11:47 -05:00
dannyrb 97a9a505ca Clean up NPM_PUBLISH step 2019-11-15 09:11:31 -05:00
dannyrb bb74f3a761 Update staging and prod netlify site IDs 2019-11-15 09:11:11 -05:00
dannyrb aa33397fe6 Remove netlify-cli; we';ll install this on CI server 2019-11-15 09:10:51 -05:00
Danny Brown 8ae731fe4c Merge branch 'master' into feat/new-modal-service 2019-11-15 08:43:40 -05:00
Igor add3416e11 Fix key warning of snackbar elements 2019-11-15 09:54:14 -03:00
Igor 136969bf18 Add servicesManager test and registerServices method 2019-11-15 09:27:42 -03:00
Igor e3e039ad47 Pass services to each module, improve tests 2019-11-14 19:29:40 -03:00
Danny Brown 0797eff0b7 ci: restore release workflow (#1185)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command

* Inline personal access token w/ env var

* ci: workaround for sudo limitations

* ci: restore release workflow
2019-11-14 16:32:14 -05:00
Danny Brown 55a5f04923 Ci/promotable builds (#1184)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command

* Inline personal access token w/ env var

* ci: workaround for sudo limitations
2019-11-14 16:20:31 -05:00
Danny Brown ed9e4d6751 Ci/promotable builds (#1183)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command

* Inline personal access token w/ env var
2019-11-14 16:08:25 -05:00
Igor b6119f8e29 Fix OHIFModal proptypes 2019-11-14 17:57:44 -03:00
Danny Brown 2e38f4b2e4 Ci/promotable builds (#1182)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available

* ci: use sudo for global command
2019-11-14 15:56:23 -05:00
Danny Brown e03abc45f5 Ci/promotable builds (#1181)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command

* ci: make sure netlify command is available
2019-11-14 15:48:09 -05:00
Danny Brown f03d64ce39 Ci/promotable builds (#1180)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag

* Try using ~/repo prefix in command
2019-11-14 15:41:34 -05:00
Igor e895bfb1aa Change modal children order 2019-11-14 17:40:37 -03:00
Danny Brown 17358c3f4b Ci/promotable builds (#1179)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions

* ci: try building with QUICK_BUILD flag
2019-11-14 15:27:34 -05:00
Igor ec7337e801 Use modal instead of modal context 2019-11-14 17:25:09 -03:00
Danny Brown 538ea2516d ci: don't build our master branch (#1177)
* ci: don't build our master branch

* Add netlify-cli as a dev dependency

* ci: trying a sheltered merge flow for promotions
2019-11-14 15:01:55 -05:00
Igor fe687cd2f9 Refactor modal provider to better use its own state 2019-11-14 14:56:01 -03:00
Igor bdb9af5c4b CR Update: add default props to service and check service in provider 2019-11-14 12:43:25 -03:00
Igor db0f751a68 CR Update: Fix typo in extensionmanager 2019-11-14 11:50:37 -03:00
Igor 08fdb10f6d CR Update: Improve ohifmodal proptypes 2019-11-14 11:42:25 -03:00
Igor 95c18b4986 CR Update: fix casing and add required proptypes to providers 2019-11-14 11:16:35 -03:00
Igor e684558783 Change serviceManager prop to servicesManager 2019-11-14 10:32:24 -03:00
Igor 9104e7be50 Add new modal service 2019-11-14 10:19:33 -03:00
ohif-bot 81ad25b39f chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.6
 - @ohif/ui@0.62.1
 - @ohif/viewer@2.8.2
2019-11-14 03:53:10 +00:00
Igor Octaviano 54d2cde6da refactor: 💡 Improve hotkeyPreferences proptypes / add lowercase (#1165)
* refactor: 💡 Improve hotkeyPreferences proptypes / add lowercase

Improve hotkeyPreferences proptypes and lowercase event keys to guard
against different browsers casings

* Add isRequired to proptypes and empty hotkeys message
2019-11-13 22:42:49 -05:00
ohif-bot 9edc366ff7 chore(release): publish [skip ci]
- @ohif/viewer@2.8.1
2019-11-14 02:48:34 +00:00
Mirna Silva 79c10bb424 test: Visual testing for Cornerstone and VTK (#1169)
* Visual testing for Cornerstone and VTK

* Fix for CI

* Review fix: Removed cornerstone function and added snapshot description

* Added snapshot description

* Fix for CI
2019-11-13 21:36:19 -05:00
ohif-bot d8055e458f chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.5.0
 - @ohif/extension-vtk@0.53.5
 - @ohif/core@1.9.0
 - @ohif/ui@0.62.0
 - @ohif/viewer@2.8.0
2019-11-13 20:12:25 +00:00
Igor Octaviano 5c04e34c8f feat: expose UiNotifications service (#1172)
* playing around

* Change extension init and preRegistration signature

* Clear test code

* Update core index test

* Fix test

* Tricking the bug

* Renaming file back to trick some weird bug
2019-11-13 15:02:14 -05:00
Egor Lezhnin c59c5b3f14 feat: filter field for google api windows (#1170) 2019-11-13 09:38:49 -05:00
ohif-bot c4adcfd39e chore(release): publish [skip ci]
- @ohif/viewer@2.7.1
2019-11-12 12:23:55 +00:00
ladeirarodolfo 0fbaf95971 fix: 🐛 Fix for JS breaking on header (#1164) 2019-11-12 07:13:59 -05:00
fossabot a2a7e05e7d docs: Add license scan report and status (#1161)
Signed-off-by: fossabot <badges@fossa.io>
2019-11-11 21:43:24 -05:00
ohif-bot 9da8647c84 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.4
 - @ohif/core@1.8.0
 - @ohif/ui@0.61.0
 - @ohif/viewer@2.7.0
2019-11-12 02:17:40 +00:00
Igor Octaviano e62f5f8dd2 feat: 🎸 Update hotkeys and user preferences modal (#1135)
* feat: 🎸 Update hotkeys and user preferences modal

This feature fix incompatibility with existent hotkeys component and
adds user preferences modal back

Closes: #923

* Update preferences structure in store

* Hide window level section of user preferences

* Update modal to reflect current hotkey value

* Clone object with hotkeys before passing to manager

* CR Update: Extract hotkeys manager format code to manager

* Fix broken cypress test

* Use new modal provider

* Rename hotkeyDefinitions in hotkeyspreferences and use array as representation

* Update study test and remove unused styles
2019-11-11 21:03:36 -05:00
ohif-bot dc6e9d6bb5 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.3
 - @ohif/viewer@2.6.4
2019-11-11 09:48:18 +00:00
Danny Brown 84a097212b fix: mpr2d vtkjs viewport does not render if range is set to NaN values (#1157)
* chore: fix proptypes declaration

* fix: seperate helper method to calculate our initial range

* chore: remove unused import

* chore: update studies propType from object to array
2019-11-11 09:38:36 +00:00
ohif-bot 2381810ba9 chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.4.1
 - @ohif/extension-vtk@0.53.2
 - @ohif/core@1.7.1
 - @ohif/viewer@2.6.3
2019-11-08 13:07:35 +00:00
Erik Ziegler 31b1adfa59 fix: Add a fallback metadata provider which pulls metadata from WADO-… (#1158)
* fix: Add a fallback metadata provider which pulls metadata from WADO-RS cache when WADO-URI is used.

* cleanup
2019-11-08 13:55:56 +01:00
ohif-bot cd28877ed8 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.1
 - @ohif/ui@0.60.1
 - @ohif/viewer@2.6.2
2019-11-08 12:14:44 +00:00
Mirna Silva e81a39f5be Merge pull request #1159 from mirnasilva/fix_studylist
Fix for modalities search result
2019-11-08 09:02:34 -03:00
Mirna Silva 9a2cf70504 Fix for modalities search result 2019-11-08 08:47:32 -03:00
ladeirarodolfo d70eae3eb0 fix: Fix display issues with incorrect thumbnails. Change ImageThumb to functional component. (#1148) 2019-11-08 11:26:31 +01:00
ohif-bot 8481e94d4d chore(release): publish [skip ci]
- @ohif/viewer@2.6.1
2019-11-06 04:56:33 +00:00
Mirna Silva 19b22c39ae test: Study list test cases: Patient Name, MRN, Modality, Description (#1121)
* Study list tests: Patient Name, MRN, Modality, Description

* Fixed field selector

* Desktop tests

* Fix for study list

* Added tablet tests and small refactor on aliases

* Fix based on reviews
2019-11-05 23:46:38 -05:00
ohif-bot 05802a3cf1 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.0
 - @ohif/i18n@0.52.0
 - @ohif/ui@0.60.0
 - @ohif/viewer@2.6.0
2019-11-06 04:23:15 +00:00
Danny Brown 75d88bc454 feat: modal provider (#1151)
* feat: 🎸 New modal provider

A new modal provider/context component to unify modal related code and
simplify modal use

Closes: #1086

* refactor(ModalContext): Keep current modal lib

* Update modal provider version according to PR #1116

* Update modal provider props to use inner props

* Add custom class prop

* CR Update: Refactor provider to extract specific modal

* Fix modal import

* CR Update: Move from spread to object assign

* CR Update: Add proptypes, use classnames dependency over interpolation and rename modal

* fix: 🎸 Update AboutModal to use new modal provider (#1116)

* feat: 🎸 Update AboutModal to use new modal provider

This feature updates the AboutModal component and Header to use the new
modal context (modal provider)

* feat(ModalProvider): Add title prop to provider

* fix(ModalContext): Add defaults on hide

* CR Updates: Refactor modal provider and fix import

* CR Update: Expose props and flat modal optons

* Use customclassname instead of static

* Add cypress tag

* test: set right viewport when layout is decreased (#1146)

* chore(release): publish [skip ci]

 - @ohif/viewer@2.3.6

* fix: 🐛 Minor issues measurement panel related to description (#1142)

* chore(release): publish [skip ci]

 - @ohif/extension-vtk@0.52.28
 - @ohif/ui@0.58.5
 - @ohif/viewer@2.3.7

* fix: 🐛 Upgrade react-vtkjs-viewport to fix volume orientation (#1143)

* chore(release): publish [skip ci]

 - @ohif/extension-vtk@0.52.29
 - @ohif/viewer@2.3.8

* CR Update: Extract and rename AboutModal to AboutContent

* feat: 🎸 New modal provider (#1110)

* feat: 🎸 New modal provider

A new modal provider/context component to unify modal related code and
simplify modal use

Closes: #1086

* refactor(ModalContext): Keep current modal lib

* Update modal provider version according to PR #1116

* Update modal provider props to use inner props

* Add custom class prop

* CR Update: Refactor provider to extract specific modal

* Fix modal import

* CR Update: Move from spread to object assign

* CR Update: Add proptypes, use classnames dependency over interpolation and rename modal

* chore(release): publish [skip ci]

 - @ohif/extension-vtk@0.52.30
 - @ohif/ui@0.59.0
 - @ohif/viewer@2.4.0

* CR Update: Keep translation name

* CR Update: Reposition cy tag of about modal in the correct level

* feat: 🎸 Update DownloadModal to use new modal provider (#1119)

* feat: 🎸 Update AboutModal to use new modal provider

This feature updates the AboutModal component and Header to use the new
modal context (modal provider)

* feat(ModalProvider): Add title prop to provider

* feat(DownloadModal): Refactor to use new modal provider

* Update modal provider props

* Use translation for DownloadDialog

* CR Update: Use useTranslation hook

* CR Update: Extract downloadDialog and rename to downloadContent

* CR Update: Fix typo

* CR Update: Rename download content to viewport download form
2019-11-05 23:07:03 -05:00
ohif-bot 9827d1bf84 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.32
 - @ohif/core@1.7.0
 - @ohif/viewer@2.5.0
2019-11-05 22:00:36 +00:00
ladeirarodolfo e208f2e6a9 feat: 🎸 Filter by url query param for seriesInstnaceUID (#1117)
* feat: 🎸 Filter by url query param for seriesInstnaceUID

* fix: Set SR viewport as active by interaction (#1118)

* fix: Set SR viewport as active by interaction

* quick fix

* (eslint) add "before" as global variables

* add data-cy

* add data-cy

* create E2E test

* (E2E) create custom command to set layout size

* remove .only e2e

* remove throttle for onScroll

* feat: 🎸 Code review in progress

Code review. Move retrieveMEtadata load to separate folders. Some minor
code clean up

* feat: 🎸 Code review. Missing changes from previous commit

* feat: 🎸 Code review missing changes from previous commit

When sorting, Criteria for instance must use instanceNumber and not
instancesNumber

* feat: 🎸 Code review. Add more jsdoc info

* feat: 🎸 Code review. Prettify changed code

* feat: 🎸 Code review

Changed case for seriesInstanceUID. Use qido to filter (async). Fallback
to no params and same api method(async)
2019-11-05 16:48:44 -05:00
ohif-bot 9c1a3c3183 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.31
 - @ohif/core@1.6.2
 - @ohif/ui@0.59.1
 - @ohif/viewer@2.4.1
2019-11-05 20:01:36 +00:00
Emanuel Fiuza de Oliveira ecaf578f92 fix: #1075 Returning to the Study List before all series have finishe… (#1090)
* fix: #1075 Returning to the Study List before all series have finished loading throws multiple errors in console

* fix: 🐛 Addressing review feedback from issue #1075

Adding support for cancelable pattern

Closes: #1075

* fix: 🐛 update unit test for exports

Closes: #1075
2019-11-05 16:47:54 -03:00
ohif-bot d26adf92bd chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.30
 - @ohif/ui@0.59.0
 - @ohif/viewer@2.4.0
2019-11-04 16:06:44 +00:00
Igor Octaviano 5ee832b195 feat: 🎸 New modal provider (#1110)
* feat: 🎸 New modal provider

A new modal provider/context component to unify modal related code and
simplify modal use

Closes: #1086

* refactor(ModalContext): Keep current modal lib

* Update modal provider version according to PR #1116

* Update modal provider props to use inner props

* Add custom class prop

* CR Update: Refactor provider to extract specific modal

* Fix modal import

* CR Update: Move from spread to object assign

* CR Update: Add proptypes, use classnames dependency over interpolation and rename modal
2019-11-04 10:55:05 -05:00
ohif-bot 8baba22a2e chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.29
 - @ohif/viewer@2.3.8
2019-11-04 09:55:56 +00:00
James Petts 1d86b1a102 fix: 🐛 Upgrade react-vtkjs-viewport to fix volume orientation (#1143) 2019-11-04 10:45:53 +01:00
ohif-bot 25e6817248 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.28
 - @ohif/ui@0.58.5
 - @ohif/viewer@2.3.7
2019-11-04 04:07:09 +00:00
ladeirarodolfo 681384b742 fix: 🐛 Minor issues measurement panel related to description (#1142) 2019-11-03 22:53:24 -05:00
ohif-bot 54e07c281f chore(release): publish [skip ci]
- @ohif/viewer@2.3.6
2019-11-02 11:39:36 +00:00
Rodrigo Antinarelli 9dedccc7f9 test: set right viewport when layout is decreased (#1146) 2019-11-02 07:26:24 -04:00
ohif-bot 621e63b3e1 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.52.27
 - @ohif/core@1.6.1
 - @ohif/ui@0.58.4
 - @ohif/viewer@2.3.5
2019-10-31 18:51:34 +00:00
Danny Brown 64cf3b324d fix: application crash if patientName is an object (#1138)
* fix: get adapter store picker to show

* fix: error caused by DICOMWeb "empty object" for patientName

* formatting

* refactor: kill the pills

* Indicate modalities is missing if not provided

* Consistent modality display

* Make sure modalities styles are applied

* revert config
2019-10-31 14:41:08 -04:00
134 changed files with 4098 additions and 2121 deletions

No files matched your search

+1 -1
View File
@@ -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]' \
+222 -245
View File
@@ -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,184 @@ 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: 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: 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: 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: 'Build Demo Site'
command: yarn run build:demo
no_output_timeout: 45m
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: 'Upload SourceMaps, Send Deploy Notification'
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 +372,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
+15
View File
@@ -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"
}
}
+4
View File
@@ -22,6 +22,7 @@
[![NPM downloads][npm-downloads-image]][npm-url]
[![Pulls][docker-pulls-img]][docker-image-url]
[![MIT License][license-image]][license-url]
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FOHIF%2FViewers.svg?type=shield)](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 -->
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FOHIF%2FViewers.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FOHIF%2FViewers?ref=badge_large)
+1
View File
@@ -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

+106
View File
@@ -0,0 +1,106 @@
# 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] |
| Staging | For manual testing before promotion to prod. Keeps development workflow unblocked. | [Netlify][netlify-staging] |
| Production | Stable, tested, updated less frequently. | [Netlify][netlify-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-staging]: https://ohif-staging.netlify.com
[netlify-prod]: https://ohif-prod.netlify.com
<!-- prettier-ignore-end -->
+4 -1
View File
@@ -11,7 +11,10 @@ export default {
* LIFECYCLE HOOKS
*/
preRegistration(extensionConfiguration) {},
preRegistration({
servicesManager,
configuration: extensionConfiguration,
}) {},
/**
* MODULE GETTERS
+30
View File
@@ -3,6 +3,36 @@
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)
### Bug Fixes
* Add a fallback metadata provider which pulls metadata from WADO-… ([#1158](https://github.com/OHIF/Viewers/issues/1158)) ([31b1adf](https://github.com/OHIF/Viewers/commit/31b1adfa5993d6c8e3e9c8b03fa9856f2621b037))
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.3.1...@ohif/extension-cornerstone@1.4.0) (2019-10-26)
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "1.4.0",
"version": "1.5.1",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
@@ -48,6 +48,7 @@
"@babel/runtime": "^7.5.5",
"classnames": "^2.2.6",
"lodash.throttle": "^4.1.1",
"query-string": "^6.8.3",
"react-cornerstone-viewport": "2.x.x"
}
}
+2 -2
View File
@@ -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;
+22 -1
View File
@@ -2,13 +2,33 @@ import OHIF from '@ohif/core';
import cornerstone from 'cornerstone-core';
import csTools from 'cornerstone-tools';
import initCornerstoneTools from './initCornerstoneTools.js';
import queryString from 'query-string';
function fallbackMetaDataProvider(type, imageId) {
if (!imageId.includes('wado?requestType=WADO')) {
return;
}
// 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}`;
return cornerstone.metaData.get(type, wadoRsImageId);
}
// 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();
@@ -78,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 });
+127
View File
@@ -3,6 +3,133 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
### Bug Fixes
* mpr2d vtkjs viewport does not render if range is set to `NaN` values ([#1157](https://github.com/OHIF/Viewers/issues/1157)) ([84a0972](https://github.com/OHIF/Viewers/commit/84a097212babc0f98198b9ced1def9743973a5a8))
## [0.53.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.1...@ohif/extension-vtk@0.53.2) (2019-11-08)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.0...@ohif/extension-vtk@0.53.1) (2019-11-08)
**Note:** Version bump only for package @ohif/extension-vtk
# [0.53.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.32...@ohif/extension-vtk@0.53.0) (2019-11-06)
### Features
* modal provider ([#1151](https://github.com/OHIF/Viewers/issues/1151)) ([75d88bc](https://github.com/OHIF/Viewers/commit/75d88bc454710d2dcdbc7d68c4d9df041159c840)), closes [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1146](https://github.com/OHIF/Viewers/issues/1146) [#1142](https://github.com/OHIF/Viewers/issues/1142) [#1143](https://github.com/OHIF/Viewers/issues/1143) [#1110](https://github.com/OHIF/Viewers/issues/1110) [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1119](https://github.com/OHIF/Viewers/issues/1119)
## [0.52.32](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.31...@ohif/extension-vtk@0.52.32) (2019-11-05)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.31](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.30...@ohif/extension-vtk@0.52.31) (2019-11-05)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.30](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.29...@ohif/extension-vtk@0.52.30) (2019-11-04)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.29](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.28...@ohif/extension-vtk@0.52.29) (2019-11-04)
### Bug Fixes
* 🐛 Upgrade react-vtkjs-viewport to fix volume orientation ([#1143](https://github.com/OHIF/Viewers/issues/1143)) ([1d86b1a](https://github.com/OHIF/Viewers/commit/1d86b1a102fc422053cff103de8c12ca6f437354))
## [0.52.28](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.27...@ohif/extension-vtk@0.52.28) (2019-11-04)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.27](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.26...@ohif/extension-vtk@0.52.27) (2019-10-31)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.52.26](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.52.25...@ohif/extension-vtk@0.52.26) (2019-10-30)
**Note:** Version bump only for package @ohif/extension-vtk
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "0.52.26",
"version": "0.53.8",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -49,11 +49,11 @@
"dependencies": {
"@babel/runtime": "^7.5.5",
"lodash.throttle": "^4.1.1",
"react-vtkjs-viewport": "^0.3.7"
"react-vtkjs-viewport": "^0.3.9"
},
"devDependencies": {
"@ohif/core": "^1.6.0",
"@ohif/ui": "^0.58.3",
"@ohif/core": "^1.10.0",
"@ohif/ui": "^0.62.3",
"cornerstone-tools": "^4.0.9",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
+56 -14
View File
@@ -65,8 +65,16 @@ class OHIFVTKViewport extends Component {
};
static propTypes = {
studies: PropTypes.object,
displaySet: PropTypes.object,
viewportData: PropTypes.shape({
studies: PropTypes.array,
displaySet: PropTypes.shape({
studyInstanceUid: PropTypes.string,
displaySetInstanceUid: PropTypes.string,
sopClassUids: PropTypes.arrayOf(PropTypes.string),
sopInstanceUid: PropTypes.string,
frameIndex: PropTypes.number,
}),
}),
viewportIndex: PropTypes.number,
children: PropTypes.node,
};
@@ -175,6 +183,19 @@ class OHIFVTKViewport extends Component {
}
};
/**
*
*
* @param {object} imageDataObject
* @param {object} imageDataObject.vtkImageData
* @param {object} imageDataObject.imageMetaData0
* @param {number} [imageDataObject.imageMetaData0.windowWidth] - The volume's initial windowWidth
* @param {number} [imageDataObject.imageMetaData0.windowCenter] - The volume's initial windowCenter
* @param {string} imageDataObject.imageMetaData0.modality - CT, MR, PT, etc
* @param {string} displaySetInstanceUid
* @returns vtkVolumeActor
* @memberof OHIFVTKViewport
*/
getOrCreateVolume(imageDataObject, displaySetInstanceUid) {
if (volumeCache[displaySetInstanceUid]) {
return volumeCache[displaySetInstanceUid];
@@ -183,18 +204,11 @@ class OHIFVTKViewport extends Component {
const { vtkImageData, imageMetaData0 } = imageDataObject;
const { windowWidth, windowCenter, modality } = imageMetaData0;
let lower;
let upper;
if (modality === 'PT') {
// For PET just set the range to 0-5 SUV
lower = 0;
upper = 5;
} else {
lower = windowCenter - windowWidth / 2.0;
upper = windowCenter + windowWidth / 2.0;
}
const { lower, upper } = _getRangeFromWindowLevels(
windowWidth,
windowCenter,
modality
);
const volumeActor = vtkVolume.newInstance();
const volumeMapper = vtkVolumeMapper.newInstance();
@@ -379,4 +393,32 @@ class OHIFVTKViewport extends Component {
}
}
/**
* Takes window levels and converts them to a range (lower/upper)
* for use with VTK RGBTransferFunction
*
* @private
* @param {number} [width] - the width of our window
* @param {number} [center] - the center of our window
* @param {string} [modality] - 'PT', 'CT', etc.
* @returns { lower, upper } - range
*/
function _getRangeFromWindowLevels(width, center, modality = undefined) {
const levelsAreNotNumbers = isNaN(center) || isNaN(width);
if (levelsAreNotNumbers) {
return { lower: 0, upper: 512 };
}
// For PET just set the range to 0-5 SUV
if (modality === 'PT') {
return { lower: 0, upper: 5 };
}
return {
lower: center - width / 2.0,
upper: center + width / 2.0,
};
}
export default OHIFVTKViewport;
-1
View File
@@ -1,7 +1,6 @@
import {
vtkInteractorStyleMPRCrosshairs,
vtkInteractorStyleMPRWindowLevel,
vtkInteractorStyleMPRSlice,
vtkInteractorStyleMPRRotate,
vtkSVGCrosshairsWidget,
} from 'react-vtkjs-viewport';
+2 -14
View File
@@ -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.
+85
View File
@@ -3,6 +3,91 @@
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)
### Bug Fixes
* Add a fallback metadata provider which pulls metadata from WADO-… ([#1158](https://github.com/OHIF/Viewers/issues/1158)) ([31b1adf](https://github.com/OHIF/Viewers/commit/31b1adfa5993d6c8e3e9c8b03fa9856f2621b037))
# [1.7.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.6.2...@ohif/core@1.7.0) (2019-11-05)
### Features
* 🎸 Filter by url query param for seriesInstnaceUID ([#1117](https://github.com/OHIF/Viewers/issues/1117)) ([e208f2e](https://github.com/OHIF/Viewers/commit/e208f2e6a9c49b16dadead0a917f657cf023929a)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
## [1.6.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.6.1...@ohif/core@1.6.2) (2019-11-05)
### Bug Fixes
* [#1075](https://github.com/OHIF/Viewers/issues/1075) Returning to the Study List before all series have finishe… ([#1090](https://github.com/OHIF/Viewers/issues/1090)) ([ecaf578](https://github.com/OHIF/Viewers/commit/ecaf578f92dc40294cec7ff9b272fb432dec4125))
## [1.6.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.6.0...@ohif/core@1.6.1) (2019-10-31)
### Bug Fixes
* application crash if patientName is an object ([#1138](https://github.com/OHIF/Viewers/issues/1138)) ([64cf3b3](https://github.com/OHIF/Viewers/commit/64cf3b324da2383a927af1df2d46db2fca5318aa))
# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.5.2...@ohif/core@1.6.0) (2019-10-26)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "1.6.0",
"version": "1.10.0",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
+9 -5
View File
@@ -1,11 +1,14 @@
export default function getModalities(modality, modalitiesInStudy) {
let modalities = {};
if (modality) {
modalities = modality;
if (!modality && !modalitiesInStudy) {
return {};
}
const modalities = modality || {
vr: 'CS',
Value: [],
};
if (modalitiesInStudy) {
// Find vr in modalities
if (modalities.vr && modalities.vr === modalitiesInStudy.vr) {
for (let i = 0; i < modalitiesInStudy.Value.length; i++) {
const value = modalitiesInStudy.Value[i];
@@ -14,8 +17,9 @@ export default function getModalities(modality, modalitiesInStudy) {
}
}
} else {
modalities = modalitiesInStudy;
return modalitiesInStudy;
}
}
return modalities;
}
+4 -2
View File
@@ -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',
+11
View File
@@ -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 };
+4
View File
@@ -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;
+5
View File
@@ -0,0 +1,5 @@
import ServicesManager from './ServicesManager.js';
import createUINotificationService from './UINotificationService';
import createUIModalService from './UIModalService';
export { createUINotificationService, createUIModalService, ServicesManager };
@@ -0,0 +1,25 @@
import DICOMWeb from '../DICOMWeb/';
import isLowPriorityModality from '../utils/isLowPriorityModality';
const INFO = Symbol('INFO');
/**
* Creates an object with processed series information and saves its reference
* inside the series object itself.
* @param {Object} series The raw series object
* @returns {Object} object containing some useful info from given series
*/
export default function getSeriesInfo(series) {
let info = series[INFO];
if (!info) {
const modality = DICOMWeb.getString(series['00080060'], '').toUpperCase();
info = Object.freeze({
modality,
isLowPriority: isLowPriorityModality(modality),
seriesInstanceUid: DICOMWeb.getString(series['0020000E']),
seriesNumber: DICOMWeb.getNumber(series['00200011'], 0) || 0,
});
series[INFO] = info;
}
return info;
}
@@ -7,13 +7,16 @@ import { retrieveStudyMetadata } from './retrieveStudyMetadata';
* This function calls retrieveStudyMetadata several times, asynchronously,
* and waits for all of the results to be returned.
*
* @param studyInstanceUids The UIDs of the Studies to be retrieved
* @return Promise
* @param {Object} server Object with server configuration parameters
* @param {Array} studyInstanceUids The UIDs of the Studies to be retrieved
* @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
* @returns {Promise} that will be resolved with the metadata or rejected with the error
*/
export default function retrieveStudiesMetadata(
server,
studyInstanceUids,
seriesInstanceUids
filters
) {
// Create an empty array to store the Promises for each metaData retrieval call
const promises = [];
@@ -21,11 +24,7 @@ export default function retrieveStudiesMetadata(
// Loop through the array of studyInstanceUids
studyInstanceUids.forEach(function(studyInstanceUid) {
// Send the call and resolve or reject the related promise based on its outcome
const promise = retrieveStudyMetadata(
server,
studyInstanceUid,
seriesInstanceUids
);
const promise = retrieveStudyMetadata(server, studyInstanceUid, filters);
// Add the current promise to the array of promises
promises.push(promise);
@@ -7,11 +7,13 @@ const StudyMetaDataPromises = new Map();
/**
* Retrieves study metadata
*
* @param {Object} server
* @param {Object} server Object with server configuration parameters
* @param {string} studyInstanceUid The UID of the Study to be retrieved
* @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
* @returns {Promise} that will be resolved with the metadata or rejected with the error
*/
export function retrieveStudyMetadata(server, studyInstanceUid) {
export function retrieveStudyMetadata(server, studyInstanceUid, filters) {
// @TODO: Whenever a study metadata request has failed, its related promise will be rejected once and for all
// and further requests for that metadata will always fail. On failure, we probably need to remove the
// corresponding promise from the "StudyMetaDataPromises" map...
@@ -32,7 +34,7 @@ export function retrieveStudyMetadata(server, studyInstanceUid) {
// Create a promise to handle the data retrieval
const promise = new Promise((resolve, reject) => {
RetrieveMetadata(server, studyInstanceUid).then(function(data) {
RetrieveMetadata(server, studyInstanceUid, filters).then(function(data) {
resolve(data);
}, reject);
});
@@ -30,7 +30,7 @@ function dateToString(date) {
function getQIDOQueryParams(filter, serverSupportsQIDOIncludeField) {
const commaSeparatedFields = [
'00081030', // Study Description
'00080060', //Modality
'00080060', // Modality
// Add more fields here if you want them in the result
].join(',');
@@ -1,609 +1,30 @@
import { api } from 'dicomweb-client';
import DICOMWeb from '../../../DICOMWeb/';
import isLowPriorityModality from '../../../utils/isLowPriorityModality';
const INFO = Symbol('INFO');
const WADOProxy = {
convertURL: (url, server) => {
// TODO: Remove all WADOProxy stuff from this file
return url;
},
};
function parseFloatArray(obj) {
const result = [];
if (!obj) {
return result;
}
const objs = obj.split('\\');
for (let i = 0; i < objs.length; i++) {
result.push(parseFloat(objs[i]));
}
return result;
}
/**
* Simple cache schema for retrieved color palettes.
*/
const paletteColorCache = {
count: 0,
maxAge: 24 * 60 * 60 * 1000, // 24h cache?
entries: {},
isValidUID: function(paletteUID) {
return typeof paletteUID === 'string' && paletteUID.length > 0;
},
get: function(paletteUID) {
let entry = null;
if (this.entries.hasOwnProperty(paletteUID)) {
entry = this.entries[paletteUID];
// check how the entry is...
if (Date.now() - entry.time > this.maxAge) {
// entry is too old... remove entry.
delete this.entries[paletteUID];
this.count--;
entry = null;
}
}
return entry;
},
add: function(entry) {
if (this.isValidUID(entry.uid)) {
let paletteUID = entry.uid;
if (this.entries.hasOwnProperty(paletteUID) !== true) {
this.count++; // increment cache entry count...
}
entry.time = Date.now();
this.entries[paletteUID] = entry;
// @TODO: Add logic to get rid of old entries and reduce memory usage...
}
},
};
/** Returns a WADO url for an instance
*
* @param studyInstanceUid
* @param seriesInstanceUid
* @param sopInstanceUid
* @returns {string}
*/
function buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
) {
// TODO: This can be removed, since DICOMWebClient has the same function. Not urgent, though
const params = [];
params.push('requestType=WADO');
params.push(`studyUID=${studyInstanceUid}`);
params.push(`seriesUID=${seriesInstanceUid}`);
params.push(`objectUID=${sopInstanceUid}`);
params.push('contentType=application/dicom');
params.push('transferSyntax=*');
const paramString = params.join('&');
return `${server.wadoUriRoot}?${paramString}`;
}
function buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
) {
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
}
function buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
frame
) {
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
frame = frame != null || 1;
return `${baseWadoRsUri}/frames/${frame}`;
}
/**
* Parses the SourceImageSequence, if it exists, in order
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
* is used to refer to this image in any accompanying DICOM-SR documents.
*
* @param instance
* @returns {String} The ReferenceSOPInstanceUID
*/
function getSourceImageInstanceUid(instance) {
// TODO= Parse the whole Source Image Sequence
// This is a really poor workaround for now.
// Later we should probably parse the whole sequence.
var SourceImageSequence = instance['00082112'];
if (
SourceImageSequence &&
SourceImageSequence.Value &&
SourceImageSequence.Value.length &&
SourceImageSequence.Value[0]['00081155'].Value
) {
return SourceImageSequence.Value[0]['00081155'].Value[0];
}
}
function getPaletteColor(server, instance, tag, lutDescriptor) {
const numLutEntries = lutDescriptor[0];
const bits = lutDescriptor[2];
let uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server);
// TODO: Workaround for dcm4chee behind SSL-terminating proxy returning
// incorrect bulk data URIs
if (server.wadoRoot.indexOf('https') === 0 && !uri.includes('https')) {
uri = uri.replace('http', 'https');
}
const config = {
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
headers: DICOMWeb.getAuthorizationHeader(server),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
BulkDataURI: uri,
};
const readUInt16 = (byteArray, position) => {
return byteArray[position] + byteArray[position + 1] * 256;
};
const arrayBufferToPaletteColorLUT = result => {
const arraybuffer = result[0];
const byteArray = new Uint8Array(arraybuffer);
const lut = [];
for (let i = 0; i < numLutEntries; i++) {
if (bits === 16) {
lut[i] = readUInt16(byteArray, i * 2);
} else {
lut[i] = byteArray[i];
}
}
return lut;
};
return dicomWeb.retrieveBulkData(options).then(arrayBufferToPaletteColorLUT);
}
/**
* Fetch palette colors for instances with "PALETTE COLOR" photometricInterpretation.
*
* @param server {Object} Current server;
* @param instance {Object} The retrieved instance metadata;
* @returns {String} The ReferenceSOPInstanceUID
*/
async function getPaletteColors(server, instance, lutDescriptor) {
let paletteUID = DICOMWeb.getString(instance['00281199']);
return new Promise((resolve, reject) => {
let entry;
if (paletteColorCache.isValidUID(paletteUID)) {
entry = paletteColorCache.get(paletteUID);
if (entry) {
return resolve(entry);
}
}
// no entry in cache... Fetch remote data.
const r = getPaletteColor(server, instance, '00281201', lutDescriptor);
const g = getPaletteColor(server, instance, '00281202', lutDescriptor);
const b = getPaletteColor(server, instance, '00281203', lutDescriptor);
const promises = [r, g, b];
Promise.all(promises).then(args => {
entry = {
red: args[0],
green: args[1],
blue: args[2],
};
// when paletteUID is present, the entry can be cached...
entry.uid = paletteUID;
paletteColorCache.add(entry);
resolve(entry);
});
});
}
function getFrameIncrementPointer(element) {
const frameIncrementPointerNames = {
'00181065': 'frameTimeVector',
'00181063': 'frameTime',
};
if (!element || !element.Value || !element.Value.length) {
return;
}
const value = element.Value[0];
return frameIncrementPointerNames[value];
}
function getRadiopharmaceuticalInfo(instance) {
const modality = DICOMWeb.getString(instance['00080060']);
if (modality !== 'PT') {
return;
}
const radiopharmaceuticalInfo = instance['00540016'];
if (
radiopharmaceuticalInfo === undefined ||
!radiopharmaceuticalInfo.Value ||
!radiopharmaceuticalInfo.Value.length
) {
return;
}
const firstPetRadiopharmaceuticalInfo = radiopharmaceuticalInfo.Value[0];
return {
radiopharmaceuticalStartTime: DICOMWeb.getString(
firstPetRadiopharmaceuticalInfo['00181072']
),
radionuclideTotalDose: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181074']
),
radionuclideHalfLife: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181075']
),
};
}
async function makeSOPInstance(server, study, instance) {
const { studyInstanceUid } = study;
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
let series = study.seriesMap[seriesInstanceUid];
if (!series) {
series = {
seriesInstanceUid,
seriesDescription: DICOMWeb.getString(instance['0008103E']),
modality: DICOMWeb.getString(instance['00080060']),
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
seriesDate: DICOMWeb.getString(instance['00080021']),
seriesTime: DICOMWeb.getString(instance['00080031']),
instances: [],
};
study.seriesMap[seriesInstanceUid] = series;
study.seriesList.push(series);
}
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
const wadouri = buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const wadorsuri = buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const sopInstance = {
imageType: DICOMWeb.getString(instance['00080008']),
sopClassUid: DICOMWeb.getString(instance['00080016']),
modality: DICOMWeb.getString(instance['00080060']),
sopInstanceUid,
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
rows: DICOMWeb.getNumber(instance['00280010']),
columns: DICOMWeb.getNumber(instance['00280011']),
pixelSpacing: DICOMWeb.getString(instance['00280030']),
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
bitsStored: DICOMWeb.getNumber(instance['00280101']),
highBit: DICOMWeb.getNumber(instance['00280102']),
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
windowCenter: DICOMWeb.getString(instance['00281050']),
windowWidth: DICOMWeb.getString(instance['00281051']),
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
rescaleType: DICOMWeb.getNumber(instance['00281054']),
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
laterality: DICOMWeb.getString(instance['00200062']),
viewPosition: DICOMWeb.getString(instance['00185101']),
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
frameTime: DICOMWeb.getNumber(instance['00181063']),
frameTimeVector: parseFloatArray(DICOMWeb.getString(instance['00181065'])),
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
derivationDescription: DICOMWeb.getString(instance['00282111']),
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
echoNumber: DICOMWeb.getString(instance['00180086']),
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
baseWadoRsUri: baseWadoRsUri,
wadouri: WADOProxy.convertURL(wadouri, server),
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
wadoRoot: server.wadoRoot,
imageRendering: server.imageRendering,
thumbnailRendering: server.thumbnailRendering,
};
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
if (sopInstance.photometricInterpretation === 'PALETTE COLOR') {
const redPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281101'])
);
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281102'])
);
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281103'])
);
const palettes = await getPaletteColors(
server,
instance,
redPaletteColorLookupTableDescriptor
);
if (palettes) {
if (palettes.uid) {
sopInstance.paletteColorLookupTableUID = palettes.uid;
}
sopInstance.redPaletteColorLookupTableData = palettes.red;
sopInstance.greenPaletteColorLookupTableData = palettes.green;
sopInstance.bluePaletteColorLookupTableData = palettes.blue;
sopInstance.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
sopInstance.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
sopInstance.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
}
}
series.instances.push(sopInstance);
return sopInstance;
}
/**
* Create a plain JS object that describes a study (a study descriptor object)
* @param {Object} server Object with server configuration paramenters
* @param {Object} aSopInstance a SOP Instance from which study information will be added
*/
function createStudy(server, aSopInstance) {
// TODO: Pass a reference ID to the server instead of including the URLs here
return {
seriesList: [],
seriesMap: Object.create(null),
seriesLoader: null,
wadoUriRoot: server.wadoUriRoot,
wadoRoot: server.wadoRoot,
qidoRoot: server.qidoRoot,
patientName: DICOMWeb.getName(aSopInstance['00100010']),
patientId: DICOMWeb.getString(aSopInstance['00100020']),
patientAge: DICOMWeb.getNumber(aSopInstance['00101010']),
patientSize: DICOMWeb.getNumber(aSopInstance['00101020']),
patientWeight: DICOMWeb.getNumber(aSopInstance['00101030']),
accessionNumber: DICOMWeb.getString(aSopInstance['00080050']),
studyDate: DICOMWeb.getString(aSopInstance['00080020']),
modalities: DICOMWeb.getString(aSopInstance['00080061']),
studyDescription: DICOMWeb.getString(aSopInstance['00081030']),
imageCount: DICOMWeb.getString(aSopInstance['00201208']),
studyInstanceUid: DICOMWeb.getString(aSopInstance['0020000D']),
institutionName: DICOMWeb.getString(aSopInstance['00080080']),
};
}
/**
* Add a list of SOP Instances to a given study object descriptor
* @param {Object} server Object with server configuration paramenters
* @param {Object} study The study descriptor to which the given SOP instances will be added
* @param {Array} sopInstanceList A list of SOP instance objects
*/
async function addInstancesToStudy(server, study, sopInstanceList) {
return Promise.all(
sopInstanceList.map(function(sopInstance) {
return makeSOPInstance(server, study, sopInstance);
})
);
}
/**
* Parses result data from a WADO search into Study MetaData
* Returns an object populated with study metadata, including the
* series list.
*
* @param {Object} server Object with server configuration paramenters
* @param {Array} sopInstanceList List of SOP Instances that build up to the study
* @resolves {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
*/
async function createStudyFromSOPInstanceList(server, sopInstanceList) {
if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) {
const firstSopInstance = sopInstanceList[0];
const study = createStudy(server, firstSopInstance);
await addInstancesToStudy(server, study, sopInstanceList);
return study;
}
throw new Error('Failed to create study out of provided SOP instance list');
}
import RetrieveMetadataLoaderSync from './retrieveMetadataLoaderSync';
import RetrieveMetadataLoaderAsync from './retrieveMetadataLoaderAsync';
/**
* Retrieve Study metadata from a DICOM server. If the server is configured to use lazy load, only the first series
* will be loaded and the property "studyLoader" will be set to let consumer load remaining series as needed
* @param {Object} server Object with server configuration paramenters
* will be loaded and the property "studyLoader" will be set to let consumer load remaining series as needed.
*
* @param {Object} server Object with server configuration parameters
* @param {string} studyInstanceUid The Study Instance UID of the study which needs to be loaded
* @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
* @returns {Object} A study descriptor object
*/
async function RetrieveMetadata(server, studyInstanceUid) {
return (server.enableStudyLazyLoad !== false
? lazyLoadStudyMetadata
: loadStudyMetadata)(server, studyInstanceUid);
}
async function RetrieveMetadata(server, studyInstanceUid, filters = {}) {
const RetrieveMetadataLoader =
server.enableStudyLazyLoad !== false
? RetrieveMetadataLoaderAsync
: RetrieveMetadataLoaderSync;
/**
*
* @param {*} server
* @param {*} studyInstanceUID
*/
async function loadStudyMetadata(server, studyInstanceUID) {
const dicomWeb = new api.DICOMwebClient({
url: server.wadoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
});
return dicomWeb
.retrieveStudyMetadata({ studyInstanceUID })
.then(result => createStudyFromSOPInstanceList(server, result));
}
async function lazyLoadStudyMetadata(server, studyInstanceUid) {
const seriesInstanceUids = await searchStudySeries(server, studyInstanceUid);
const dicomWeb = new api.DICOMwebClient({
url: server.wadoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
});
const seriesLoader = makeSeriesLoader(
dicomWeb,
studyInstanceUid,
seriesInstanceUids
);
const firstSeries = await seriesLoader.next();
const study = await createStudyFromSOPInstanceList(
const retrieveMetadataLoader = new RetrieveMetadataLoader(
server,
firstSeries.sopInstances
studyInstanceUid,
filters
);
if (seriesLoader.hasNext()) {
attachSeriesLoader(server, study, seriesLoader);
}
return study;
}
const studyMetadata = retrieveMetadataLoader.execLoad();
function attachSeriesLoader(server, study, seriesLoader) {
study.seriesLoader = Object.freeze({
hasNext() {
return seriesLoader.hasNext();
},
async next() {
const series = await seriesLoader.next();
await addInstancesToStudy(server, study, series.sopInstances);
return study.seriesMap[series.seriesInstanceUID];
},
});
}
/**
* Creates an immutable series loader object which loads each series sequentially using the iterator interface
* @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load
* @param {string} studyInstanceUID The Study Instance UID from which series will be loaded
* @param {Array} seriesInstanceUIDList A list of Series Instance UIDs
* @returns {Object} Returns an object which supports loading of instances from each of given Series Instance UID
*/
function makeSeriesLoader(
dicomWebClient,
studyInstanceUID,
seriesInstanceUIDList
) {
return Object.freeze({
hasNext() {
return seriesInstanceUIDList.length > 0;
},
async next() {
const seriesInstanceUID = seriesInstanceUIDList.shift();
const sopInstances = await dicomWebClient.retrieveSeriesMetadata({
studyInstanceUID,
seriesInstanceUID,
});
return { studyInstanceUID, seriesInstanceUID, sopInstances };
},
});
}
/**
* Search series of a given study
* @param {Object} server Object with server configuration paramenters
* @param {string} studyInstanceUID The Study Instance UID to search series from;
* @returns {Arrays} A list of Series Instance UIDs
*/
async function searchStudySeries(server, studyInstanceUID) {
const dicomWeb = new api.DICOMwebClient({
url: server.qidoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
});
const seriesList = await dicomWeb.searchForSeries({ studyInstanceUID });
return seriesList
.sort(seriesSortingCriteria)
.map(series => getSeriesInfo(series).seriesInstanceUid);
}
/**
* Series sorting criteria: series considered low priority are moved to the end
* of the list and series number is used to break ties
* @param {Object} firstSeries
* @param {Object} secondSeries
*/
function seriesSortingCriteria(firstSeries, secondSeries) {
const a = getSeriesInfo(firstSeries);
const b = getSeriesInfo(secondSeries);
if (!a.isLowPriority && b.isLowPriority) {
return -1;
}
if (a.isLowPriority && !b.isLowPriority) {
return 1;
}
return a.seriesNumber - b.seriesNumber;
}
/**
* Creates an object with processed series information and saves its reference
* inside the series object itself to simplify sorting
* @param {Object} series The raw series object
*/
function getSeriesInfo(series) {
let info = series[INFO];
if (!info) {
const modality = DICOMWeb.getString(series['00080060'], '').toUpperCase();
info = Object.freeze({
modality,
isLowPriority: isLowPriorityModality(modality),
seriesInstanceUid: DICOMWeb.getString(series['0020000E']),
seriesNumber: DICOMWeb.getNumber(series['00200011'], 0) || 0,
});
series[INFO] = info;
}
return info;
return studyMetadata;
}
export default RetrieveMetadata;
@@ -0,0 +1,58 @@
/**
* Class to define inheritance of load retrieve strategy.
* The process can be async load (lazy) or sync load
*
* There are methods that must be implemented at consumer level
* To retrieve study call execLoad
*/
export default class RetrieveMetadataLoader {
/**
* @constructor
* @param {Object} server Object with server configuration parameters
* @param {Array} studyInstanceUID Study instance ui to be retrieved
* @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process
* @param {string} [filter.seriesInstanceUID] - series instance uid to filter results against
*/
constructor(server, studyInstanceUID, filters = {}) {
this.server = server;
this.studyInstanceUID = studyInstanceUID;
this.filters = filters;
}
async execLoad() {
await this.configLoad();
const preLoadData = await this.preLoad();
const loadData = await this.load(preLoadData);
const postLoadData = await this.posLoad(loadData);
return postLoadData;
}
/**
* It iterates over given loaders running each one. Loaders parameters must be bind when getting it.
* @param {Array} loaders - array of loader to retrieve data.
*/
async runLoaders(loaders) {
let result;
for (const loader of loaders) {
try {
result = await loader();
if (result && result.length) {
break; // closes iterator in case data is retrieved successfully
}
} catch (e) {}
}
if (loaders.next().done && !result) {
throw 'cant find data';
}
return result;
}
// Methods to be overwrite
async configLoad() {}
async preLoad() {}
async load(preLoadData) {}
async posLoad(loadData) {}
}
@@ -0,0 +1,145 @@
import { api } from 'dicomweb-client';
import DICOMWeb from '../../../DICOMWeb/';
import RetrieveMetadataLoader from './retrieveMetadataLoader';
import { sortStudySeries, sortingCriteria } from '../../sortStudy';
import getSeriesInfo from '../../getSeriesInfo';
import {
createStudyFromSOPInstanceList,
addInstancesToStudy,
} from './studyInstanceHelpers';
/**
* Map seriesList to an array of seriesInstanceUid
* @param {Arrays} seriesList list of Series Instance UIDs
* @returns {Arrays} A list of Series Instance UIDs
*/
function mapStudySeries(seriesList) {
return seriesList.map(series => getSeriesInfo(series).seriesInstanceUid);
}
function attachSeriesLoader(server, study, seriesLoader) {
study.seriesLoader = Object.freeze({
hasNext() {
return seriesLoader.hasNext();
},
async next() {
const series = await seriesLoader.next();
await addInstancesToStudy(server, study, series.sopInstances);
return study.seriesMap[series.seriesInstanceUID];
},
});
}
/**
* Creates an immutable series loader object which loads each series sequentially using the iterator interface
* @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load
* @param {string} studyInstanceUID The Study Instance UID from which series will be loaded
* @param {Array} seriesInstanceUIDList A list of Series Instance UIDs
* @returns {Object} Returns an object which supports loading of instances from each of given Series Instance UID
*/
function makeSeriesAsyncLoader(
dicomWebClient,
studyInstanceUID,
seriesInstanceUIDList
) {
return Object.freeze({
hasNext() {
return seriesInstanceUIDList.length > 0;
},
async next() {
const seriesInstanceUID = seriesInstanceUIDList.shift();
const sopInstances = await dicomWebClient.retrieveSeriesMetadata({
studyInstanceUID,
seriesInstanceUID,
});
return { studyInstanceUID, seriesInstanceUID, sopInstances };
},
});
}
/**
* Class for async load of study metadata.
* It inherits from RetrieveMetadataLoader
*
* It loads the one series and then append to seriesLoader the others to be consumed/loaded
*/
export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader {
configLoad() {
const { server } = this;
const client = new api.DICOMwebClient({
url: server.qidoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
});
this.client = client;
}
/**
* @returns {Array} Array of preLoaders. To be consumed as queue
*/
*getPreLoaders() {
const preLoaders = [];
const {
studyInstanceUID,
filters: { seriesInstanceUID } = {},
client,
} = this;
if (seriesInstanceUID) {
const options = {
studyInstanceUID,
queryParams: { SeriesInstanceUID: seriesInstanceUID },
};
preLoaders.push(client.searchForSeries.bind(client, options));
}
// Fallback preloader
preLoaders.push(client.searchForSeries.bind(client, { studyInstanceUID }));
yield* preLoaders;
}
async preLoad() {
const preLoaders = this.getPreLoaders();
const result = await this.runLoaders(preLoaders);
const seriesSorted = sortStudySeries(
result,
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria
);
const seriesInstanceUidsMap = mapStudySeries(seriesSorted);
return seriesInstanceUidsMap;
}
async load(preLoadData) {
const { client, studyInstanceUID } = this;
const seriesAsyncLoader = makeSeriesAsyncLoader(
client,
studyInstanceUID,
preLoadData
);
const firstSeries = await seriesAsyncLoader.next();
return {
sopInstances: firstSeries.sopInstances,
asyncLoader: seriesAsyncLoader,
};
}
async posLoad(loadData) {
const { server } = this;
const { sopInstances, asyncLoader } = loadData;
const study = await createStudyFromSOPInstanceList(server, sopInstances);
if (asyncLoader.hasNext()) {
attachSeriesLoader(server, study, asyncLoader);
}
return study;
}
}
@@ -0,0 +1,76 @@
import { api } from 'dicomweb-client';
import DICOMWeb from '../../../DICOMWeb/';
import { createStudyFromSOPInstanceList } from './studyInstanceHelpers';
import RetrieveMetadataLoader from './retrieveMetadataLoader';
/**
* Class for sync load of study metadata.
* It inherits from RetrieveMetadataLoader
*
* A list of loaders (getLoaders) can be created so, it will be applied a fallback load strategy.
* I.e Retrieve metadata using all loaders possibilities.
*/
export default class RetrieveMetadataLoaderSync extends RetrieveMetadataLoader {
getOptions() {
const { studyInstanceUID, filters } = this;
const options = {
studyInstanceUID,
};
const { seriesInstanceUID } = filters;
if (seriesInstanceUID) {
options['seriesInstanceUID'] = seriesInstanceUID;
}
return options;
}
/**
* @returns {Array} Array of loaders. To be consumed as queue
*/
*getLoaders() {
const loaders = [];
const {
studyInstanceUID,
filters: { seriesInstanceUID } = {},
client,
} = this;
if (seriesInstanceUID) {
loaders.push(
client.retrieveSeriesMetadata.bind(client, {
studyInstanceUID,
seriesInstanceUID,
})
);
}
loaders.push(
client.retrieveStudyMetadata.bind(client, { studyInstanceUID })
);
yield* loaders;
}
configLoad() {
const { server } = this;
const client = new api.DICOMwebClient({
url: server.wadoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
});
this.client = client;
}
async load(preLoadData) {
const loaders = this.getLoaders();
const result = this.runLoaders(loaders);
return result;
}
async posLoad(loadData) {
const { server } = this;
return createStudyFromSOPInstanceList(server, loadData);
}
}
@@ -0,0 +1,413 @@
import DICOMWeb from '../../../DICOMWeb';
const WADOProxy = {
convertURL: (url, server) => {
// TODO: Remove all WADOProxy stuff from this file
return url;
},
};
function parseFloatArray(obj) {
const result = [];
if (!obj) {
return result;
}
const objs = obj.split('\\');
for (let i = 0; i < objs.length; i++) {
result.push(parseFloat(objs[i]));
}
return result;
}
/**
* Create a plain JS object that describes a study (a study descriptor object)
* @param {Object} server Object with server configuration parameters
* @param {Object} aSopInstance a SOP Instance from which study information will be added
*/
function createStudy(server, aSopInstance) {
// TODO: Pass a reference ID to the server instead of including the URLs here
return {
seriesList: [],
seriesMap: Object.create(null),
seriesLoader: null,
wadoUriRoot: server.wadoUriRoot,
wadoRoot: server.wadoRoot,
qidoRoot: server.qidoRoot,
patientName: DICOMWeb.getName(aSopInstance['00100010']),
patientId: DICOMWeb.getString(aSopInstance['00100020']),
patientAge: DICOMWeb.getNumber(aSopInstance['00101010']),
patientSize: DICOMWeb.getNumber(aSopInstance['00101020']),
patientWeight: DICOMWeb.getNumber(aSopInstance['00101030']),
accessionNumber: DICOMWeb.getString(aSopInstance['00080050']),
studyDate: DICOMWeb.getString(aSopInstance['00080020']),
modalities: DICOMWeb.getString(aSopInstance['00080061']),
studyDescription: DICOMWeb.getString(aSopInstance['00081030']),
imageCount: DICOMWeb.getString(aSopInstance['00201208']),
studyInstanceUid: DICOMWeb.getString(aSopInstance['0020000D']),
institutionName: DICOMWeb.getString(aSopInstance['00080080']),
};
}
/** Returns a WADO url for an instance
*
* @param studyInstanceUid
* @param seriesInstanceUid
* @param sopInstanceUid
* @returns {string}
*/
function buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
) {
// TODO: This can be removed, since DICOMWebClient has the same function. Not urgent, though
const params = [];
params.push('requestType=WADO');
params.push(`studyUID=${studyInstanceUid}`);
params.push(`seriesUID=${seriesInstanceUid}`);
params.push(`objectUID=${sopInstanceUid}`);
params.push('contentType=application/dicom');
params.push('transferSyntax=*');
const paramString = params.join('&');
return `${server.wadoUriRoot}?${paramString}`;
}
function buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
) {
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
}
function buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid,
frame
) {
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
frame = frame != null || 1;
return `${baseWadoRsUri}/frames/${frame}`;
}
function getFrameIncrementPointer(element) {
const frameIncrementPointerNames = {
'00181065': 'frameTimeVector',
'00181063': 'frameTime',
};
if (!element || !element.Value || !element.Value.length) {
return;
}
const value = element.Value[0];
return frameIncrementPointerNames[value];
}
function getRadiopharmaceuticalInfo(instance) {
const modality = DICOMWeb.getString(instance['00080060']);
if (modality !== 'PT') {
return;
}
const radiopharmaceuticalInfo = instance['00540016'];
if (
radiopharmaceuticalInfo === undefined ||
!radiopharmaceuticalInfo.Value ||
!radiopharmaceuticalInfo.Value.length
) {
return;
}
const firstPetRadiopharmaceuticalInfo = radiopharmaceuticalInfo.Value[0];
return {
radiopharmaceuticalStartTime: DICOMWeb.getString(
firstPetRadiopharmaceuticalInfo['00181072']
),
radionuclideTotalDose: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181074']
),
radionuclideHalfLife: DICOMWeb.getNumber(
firstPetRadiopharmaceuticalInfo['00181075']
),
};
}
/**
* Parses the SourceImageSequence, if it exists, in order
* to return a ReferenceSOPInstanceUID. The ReferenceSOPInstanceUID
* is used to refer to this image in any accompanying DICOM-SR documents.
*
* @param instance
* @returns {String} The ReferenceSOPInstanceUID
*/
function getSourceImageInstanceUid(instance) {
// TODO= Parse the whole Source Image Sequence
// This is a really poor workaround for now.
// Later we should probably parse the whole sequence.
var SourceImageSequence = instance['00082112'];
if (
SourceImageSequence &&
SourceImageSequence.Value &&
SourceImageSequence.Value.length &&
SourceImageSequence.Value[0]['00081155'].Value
) {
return SourceImageSequence.Value[0]['00081155'].Value[0];
}
}
async function makeSOPInstance(server, study, instance) {
const { studyInstanceUid } = study;
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
let series = study.seriesMap[seriesInstanceUid];
if (!series) {
series = {
seriesInstanceUid,
seriesDescription: DICOMWeb.getString(instance['0008103E']),
modality: DICOMWeb.getString(instance['00080060']),
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
seriesDate: DICOMWeb.getString(instance['00080021']),
seriesTime: DICOMWeb.getString(instance['00080031']),
instances: [],
};
study.seriesMap[seriesInstanceUid] = series;
study.seriesList.push(series);
}
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
const wadouri = buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const wadorsuri = buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const sopInstance = {
imageType: DICOMWeb.getString(instance['00080008']),
sopClassUid: DICOMWeb.getString(instance['00080016']),
modality: DICOMWeb.getString(instance['00080060']),
sopInstanceUid,
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
rows: DICOMWeb.getNumber(instance['00280010']),
columns: DICOMWeb.getNumber(instance['00280011']),
pixelSpacing: DICOMWeb.getString(instance['00280030']),
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
bitsStored: DICOMWeb.getNumber(instance['00280101']),
highBit: DICOMWeb.getNumber(instance['00280102']),
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
windowCenter: DICOMWeb.getString(instance['00281050']),
windowWidth: DICOMWeb.getString(instance['00281051']),
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
rescaleType: DICOMWeb.getNumber(instance['00281054']),
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
laterality: DICOMWeb.getString(instance['00200062']),
viewPosition: DICOMWeb.getString(instance['00185101']),
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
frameTime: DICOMWeb.getNumber(instance['00181063']),
frameTimeVector: parseFloatArray(DICOMWeb.getString(instance['00181065'])),
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
derivationDescription: DICOMWeb.getString(instance['00282111']),
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
echoNumber: DICOMWeb.getString(instance['00180086']),
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
baseWadoRsUri: baseWadoRsUri,
wadouri: WADOProxy.convertURL(wadouri, server),
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
wadoRoot: server.wadoRoot,
imageRendering: server.imageRendering,
thumbnailRendering: server.thumbnailRendering,
};
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
if (sopInstance.photometricInterpretation === 'PALETTE COLOR') {
const redPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281101'])
);
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281102'])
);
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281103'])
);
const palettes = await getPaletteColors(
server,
instance,
redPaletteColorLookupTableDescriptor
);
if (palettes) {
if (palettes.uid) {
sopInstance.paletteColorLookupTableUID = palettes.uid;
}
sopInstance.redPaletteColorLookupTableData = palettes.red;
sopInstance.greenPaletteColorLookupTableData = palettes.green;
sopInstance.bluePaletteColorLookupTableData = palettes.blue;
sopInstance.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
sopInstance.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
sopInstance.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
}
}
series.instances.push(sopInstance);
return sopInstance;
}
function getPaletteColor(server, instance, tag, lutDescriptor) {
const numLutEntries = lutDescriptor[0];
const bits = lutDescriptor[2];
let uri = WADOProxy.convertURL(instance[tag].BulkDataURI, server);
// TODO: Workaround for dcm4chee behind SSL-terminating proxy returning
// incorrect bulk data URIs
if (server.wadoRoot.indexOf('https') === 0 && !uri.includes('https')) {
uri = uri.replace('http', 'https');
}
const config = {
url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used
headers: DICOMWeb.getAuthorizationHeader(server),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
BulkDataURI: uri,
};
const readUInt16 = (byteArray, position) => {
return byteArray[position] + byteArray[position + 1] * 256;
};
const arrayBufferToPaletteColorLUT = result => {
const arraybuffer = result[0];
const byteArray = new Uint8Array(arraybuffer);
const lut = [];
for (let i = 0; i < numLutEntries; i++) {
if (bits === 16) {
lut[i] = readUInt16(byteArray, i * 2);
} else {
lut[i] = byteArray[i];
}
}
return lut;
};
return dicomWeb.retrieveBulkData(options).then(arrayBufferToPaletteColorLUT);
}
/**
* Fetch palette colors for instances with "PALETTE COLOR" photometricInterpretation.
*
* @param server {Object} Current server;
* @param instance {Object} The retrieved instance metadata;
* @returns {String} The ReferenceSOPInstanceUID
*/
async function getPaletteColors(server, instance, lutDescriptor) {
let paletteUID = DICOMWeb.getString(instance['00281199']);
return new Promise((resolve, reject) => {
let entry;
if (paletteColorCache.isValidUID(paletteUID)) {
entry = paletteColorCache.get(paletteUID);
if (entry) {
return resolve(entry);
}
}
// no entry in cache... Fetch remote data.
const r = getPaletteColor(server, instance, '00281201', lutDescriptor);
const g = getPaletteColor(server, instance, '00281202', lutDescriptor);
const b = getPaletteColor(server, instance, '00281203', lutDescriptor);
const promises = [r, g, b];
Promise.all(promises).then(args => {
entry = {
red: args[0],
green: args[1],
blue: args[2],
};
// when paletteUID is present, the entry can be cached...
entry.uid = paletteUID;
paletteColorCache.add(entry);
resolve(entry);
});
});
}
/**
* Add a list of SOP Instances to a given study object descriptor
* @param {Object} server Object with server configuration parameters
* @param {Object} study The study descriptor to which the given SOP instances will be added
* @param {Array} sopInstanceList A list of SOP instance objects
*/
async function addInstancesToStudy(server, study, sopInstanceList) {
return Promise.all(
sopInstanceList.map(function(sopInstance) {
return makeSOPInstance(server, study, sopInstance);
})
);
}
const createStudyFromSOPInstanceList = async (server, sopInstanceList) => {
if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) {
const firstSopInstance = sopInstanceList[0];
const study = createStudy(server, firstSopInstance);
await addInstancesToStudy(server, study, sopInstanceList);
return study;
}
throw new Error('Failed to create study out of provided SOP instance list');
};
export { createStudyFromSOPInstanceList, addInstancesToStudy };
+86 -7
View File
@@ -1,17 +1,96 @@
import getSeriesInfo from './getSeriesInfo';
/**
* Sorts the series and instances inside a study instance by their series
* and instance numbers in ascending order.
* Series sorting criteria: series considered low priority are moved to the end
* of the list and series number is used to break ties
* @param {Object} firstSeries
* @param {Object} secondSeries
*/
function seriesInfoSortingCriteria(firstSeries, secondSeries) {
const a = getSeriesInfo(firstSeries);
const b = getSeriesInfo(secondSeries);
if (!a.isLowPriority && b.isLowPriority) {
return -1;
}
if (a.isLowPriority && !b.isLowPriority) {
return 1;
}
return a.seriesNumber - b.seriesNumber;
}
const seriesSortCriteria = {
default: (a, b) => a.seriesNumber - b.seriesNumber,
seriesInfoSortingCriteria,
};
const instancesSortCriteria = {
default: (a, b) => a.instanceNumber - b.instanceNumber,
};
const sortingCriteria = {
seriesSortCriteria,
instancesSortCriteria,
};
/**
* Sorts given seriesList (given param is modified)
* The default criteria is based on series number in ascending order.
*
* @param {Array} seriesList List of series
* @param {function} seriesSortingCriteria method for sorting
* @returns {Array} sorted seriesList object
*/
const sortStudySeries = (
seriesList,
seriesSortingCriteria = seriesSortCriteria.default
) => {
return seriesList.sort(seriesSortingCriteria);
};
/**
* Sorts given instancesList (given param is modified)
* The default criteria is based on instance number in ascending order.
*
* @param {Array} instancesList List of series
* @param {function} instancesSortingCriteria method for sorting
* @returns {Array} sorted instancesList object
*/
const sortStudyInstances = (
instancesList,
instancesSortingCriteria = instancesSortCriteria.default
) => {
return instancesList.sort(instancesSortingCriteria);
};
/**
* Sorts the series and instances (by default) inside a study instance based on sortingCriteria (given param is modified)
* The default criteria is based on series and instance numbers in ascending order.
*
* @param {Object} study The study instance
* @param {boolean} [deepSort = true] to sort instance also
* @param {function} [seriesSortingCriteria = seriesSortCriteria.default] method for sorting series
* @param {function} [instancesSortingCriteria = instancesSortCriteria.default] method for sorting instances
* @returns {Object} sorted study object
*/
export default function sortStudy(study) {
export default function sortStudy(
study,
deepSort = true,
seriesSortingCriteria = seriesSortCriteria.default,
instancesSortingCriteria = instancesSortCriteria.default
) {
if (!study || !study.seriesList) {
throw new Error('Insufficient study data was provided to sortStudy');
}
study.seriesList.sort((a, b) => a.seriesNumber - b.seriesNumber);
sortStudySeries(study.seriesList, seriesSortingCriteria);
study.seriesList.forEach(series => {
series.instances.sort((a, b) => a.instanceNumber - b.instanceNumber);
});
if (deepSort) {
study.seriesList.forEach(series => {
sortStudyInstances(series.instances, instancesSortingCriteria);
});
}
return study;
}
export { sortStudySeries, sortStudyInstances, sortingCriteria };
+3
View File
@@ -11,6 +11,7 @@ import writeScript from './writeScript.js';
import DicomLoaderService from './dicomLoaderService.js';
import b64toBlob from './b64toBlob.js';
import * as urlUtil from './urlUtil';
import makeCancelable from './makeCancelable';
const utils = {
guid,
@@ -27,6 +28,7 @@ const utils = {
DICOMTagDescriptions,
DicomLoaderService,
urlUtil,
makeCancelable,
};
export {
@@ -44,6 +46,7 @@ export {
DICOMTagDescriptions,
DicomLoaderService,
urlUtil,
makeCancelable,
};
export default utils;
+1
View File
@@ -17,6 +17,7 @@ describe('Top level exports', () => {
'DICOMTagDescriptions',
'DicomLoaderService',
'urlUtil',
'makeCancelable',
].sort();
const exports = Object.keys(utils.default).sort();
+19
View File
@@ -0,0 +1,19 @@
export default function makeCancelable(thenable) {
let isCanceled = false;
const promise = Promise.resolve(thenable).then(
function(result) {
if (isCanceled) throw Object.freeze({ isCanceled });
return result;
},
function(error) {
if (isCanceled) throw Object.freeze({ isCanceled, error });
throw error;
}
);
return Object.assign(Object.create(promise), {
then: promise.then.bind(promise),
cancel() {
isCanceled = true;
},
});
}
@@ -77,14 +77,6 @@ const getWadoRsInstanceMetaData = (study, series, instance) => {
*/
function updateMetaDataManagerForSeries(study, series) {
series.instances.forEach(instance => {
// Cache just images that are going to be loaded via WADO-RS
if (
instance.imageRendering !== 'wadors' &&
instance.thumbnailRendering !== 'wadors'
) {
return;
}
const metaData = getWadoRsInstanceMetaData(study, series, instance);
const numberOfFrames = instance.numberOfFrames || 1;
@@ -94,7 +86,7 @@ function updateMetaDataManagerForSeries(study, series) {
for (let frameNumber = 0; frameNumber < numberOfFrames; frameNumber++) {
const imageId = getWADORSImageId(instance, frameNumber);
// TODO Drop dependency on this
// TODO Make a metadata manager which uses study/series/instance UIDS instead of imageIds
cornerstoneWADOImageLoader.wadors.metaDataManager.add(imageId, metaData);
}
});
+11
View File
@@ -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.0](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.51.0...@ohif/i18n@0.52.0) (2019-11-06)
### Features
* modal provider ([#1151](https://github.com/OHIF/Viewers/issues/1151)) ([75d88bc](https://github.com/OHIF/Viewers/commit/75d88bc454710d2dcdbc7d68c4d9df041159c840)), closes [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1146](https://github.com/OHIF/Viewers/issues/1146) [#1142](https://github.com/OHIF/Viewers/issues/1142) [#1143](https://github.com/OHIF/Viewers/issues/1143) [#1110](https://github.com/OHIF/Viewers/issues/1110) [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1119](https://github.com/OHIF/Viewers/issues/1119)
# [0.51.0](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.50.5...@ohif/i18n@0.51.0) (2019-10-15)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/i18n",
"version": "0.51.0",
"version": "0.52.0",
"description": "Internationalization library for The OHIF Viewer",
"author": "OHIF",
"license": "MIT",
+11 -11
View File
@@ -1,13 +1,13 @@
import AboutModal from "./AboutModal.json";
import Buttons from "./Buttons.json";
import CineDialog from "./CineDialog.json";
import Common from "./Common.json";
import Header from "./Header.json";
import MeasurementTable from "./MeasurementTable.json";
import StudyList from "./StudyList.json";
import UserPreferencesModal from "./UserPreferencesModal.json";
import AboutModal from './AboutModal.json';
import Buttons from './Buttons.json';
import CineDialog from './CineDialog.json';
import Common from './Common.json';
import Header from './Header.json';
import MeasurementTable from './MeasurementTable.json';
import StudyList from './StudyList.json';
import UserPreferencesModal from './UserPreferencesModal.json';
export default {
export default {
'en-US': {
AboutModal,
Buttons,
@@ -17,5 +17,5 @@ export default {
MeasurementTable,
StudyList,
UserPreferencesModal,
}
};
},
};
+9 -9
View File
@@ -1,11 +1,11 @@
import AboutModal from "./AboutModal.json";
import Buttons from "./Buttons.json";
import CineDialog from "./CineDialog.json";
import Common from "./Common.json";
import Header from "./Header.json";
import UserPreferencesModal from "./UserPreferencesModal.json";
import AboutModal from './AboutModal.json';
import Buttons from './Buttons.json';
import CineDialog from './CineDialog.json';
import Common from './Common.json';
import Header from './Header.json';
import UserPreferencesModal from './UserPreferencesModal.json';
export default {
export default {
'pt-BR': {
AboutModal,
Buttons,
@@ -13,5 +13,5 @@ export default {
Common,
Header,
UserPreferencesModal,
}
};
},
};
+112
View File
@@ -3,6 +3,118 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
### Bug Fixes
* Fix display issues with incorrect thumbnails. Change ImageThumb to functional component. ([#1148](https://github.com/OHIF/Viewers/issues/1148)) ([d70eae3](https://github.com/OHIF/Viewers/commit/d70eae3eb04fe854464f3e62316df8869bba6f11))
# [0.60.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.59.1...@ohif/ui@0.60.0) (2019-11-06)
### Features
* modal provider ([#1151](https://github.com/OHIF/Viewers/issues/1151)) ([75d88bc](https://github.com/OHIF/Viewers/commit/75d88bc454710d2dcdbc7d68c4d9df041159c840)), closes [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1146](https://github.com/OHIF/Viewers/issues/1146) [#1142](https://github.com/OHIF/Viewers/issues/1142) [#1143](https://github.com/OHIF/Viewers/issues/1143) [#1110](https://github.com/OHIF/Viewers/issues/1110) [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1119](https://github.com/OHIF/Viewers/issues/1119)
## [0.59.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.59.0...@ohif/ui@0.59.1) (2019-11-05)
### Bug Fixes
* [#1075](https://github.com/OHIF/Viewers/issues/1075) Returning to the Study List before all series have finishe… ([#1090](https://github.com/OHIF/Viewers/issues/1090)) ([ecaf578](https://github.com/OHIF/Viewers/commit/ecaf578f92dc40294cec7ff9b272fb432dec4125))
# [0.59.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.5...@ohif/ui@0.59.0) (2019-11-04)
### Features
* 🎸 New modal provider ([#1110](https://github.com/OHIF/Viewers/issues/1110)) ([5ee832b](https://github.com/OHIF/Viewers/commit/5ee832b19505a4e8e5756660ce6ed03a7f18dec3)), closes [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116)
## [0.58.5](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.4...@ohif/ui@0.58.5) (2019-11-04)
### Bug Fixes
* 🐛 Minor issues measurement panel related to description ([#1142](https://github.com/OHIF/Viewers/issues/1142)) ([681384b](https://github.com/OHIF/Viewers/commit/681384b7425c83b02a0ed83371ca92d78ca7838c))
## [0.58.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.3...@ohif/ui@0.58.4) (2019-10-31)
### Bug Fixes
* application crash if patientName is an object ([#1138](https://github.com/OHIF/Viewers/issues/1138)) ([64cf3b3](https://github.com/OHIF/Viewers/commit/64cf3b324da2383a927af1df2d46db2fca5318aa))
## [0.58.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.58.2...@ohif/ui@0.58.3) (2019-10-30)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/ui",
"version": "0.58.3",
"version": "0.62.3",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
@@ -0,0 +1,107 @@
import React from 'react';
import detect from 'browser-detect';
import { useTranslation } from 'react-i18next';
import './AboutContent.styl';
const AboutContent = () => {
const { t } = useTranslation('AboutContent');
const { os, version, name } = detect();
const capitalize = s =>
s.substr(0, 1).toUpperCase() + s.substr(1).toLowerCase();
const itemsPreset = () => {
return [
{
name: t('Repository URL'),
value: 'https://github.com/OHIF/Viewers/',
link: 'https://github.com/OHIF/Viewers/',
},
{
name: t('Latest Master Commits'),
value: 'https://github.com/OHIF/Viewers/commits/master',
link: 'https://github.com/OHIF/Viewers/commits/master',
},
{
name: 'Version Number',
value: process.env.VERSION_NUMBER,
},
{
name: t('Build Number'),
value: process.env.BUILD_NUM,
},
{
name: t('Browser'),
value: `${capitalize(name)} ${version}`,
},
{
name: t('OS'),
value: os,
},
];
};
const renderTableRow = ({ name, value, link }) => (
<tr key={name} style={{ backgroundColor: 'transparent' }}>
<td>{name}</td>
<td>
{link ? (
<a target="_blank" rel="noopener noreferrer" href={link}>
{value}
</a>
) : (
value
)}
</td>
</tr>
);
return (
<div data-cy="about-modal">
<div className="btn-group">
<a
className="btn btn-default"
target="_blank"
rel="noopener noreferrer"
href="https://groups.google.com/forum/#!forum/cornerstone-platform"
>
{t('Visit the forum')}
</a>
{` `}
<a
className="btn btn-default"
target="_blank"
rel="noopener noreferrer"
href="https://github.com/OHIF/Viewers/issues/new/choose"
>
{t('Report an issue')}
</a>
{` `}
<a
className="btn btn-default"
target="_blank"
rel="noopener noreferrer"
href="http://ohif.org"
>
{t('More details')}
</a>
</div>
<div>
<h3>{t('Version Information')}</h3>
<table className="table table-responsive">
<thead>
<tr>
<th>{t('Name')}</th>
<th>{t('Value')}</th>
</tr>
</thead>
<tbody>{itemsPreset().map(item => renderTableRow(item))}</tbody>
</table>
</div>
</div>
);
};
export { AboutContent };
export default AboutContent;
@@ -1,7 +1,7 @@
@import './../../design/styles/common/button.styl'
@import './../../design/styles/common/table.styl'
@import '../../../design/styles/common/button.styl'
@import '../../../design/styles/common/table.styl'
.AboutModal
.AboutContent
.btn
border-color: #ccc;
@@ -1,10 +1,9 @@
import React, { useEffect, useState, createRef } from 'react';
import Modal from 'react-bootstrap-modal';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import './DownloadDialog.styl';
import './ViewportDownloadForm.styl';
import { TextInput, Select } from '@ohif/ui';
import { withTranslation } from '../../utils/LanguageProvider';
const FILE_TYPE_OPTIONS = [
{
@@ -19,9 +18,7 @@ const FILE_TYPE_OPTIONS = [
const DEFAULT_FILENAME = 'image';
const DownloadDialog = ({
t,
isOpen,
const ViewportDownloadForm = ({
activeViewport,
onClose,
updateViewportPreview,
@@ -35,6 +32,8 @@ const DownloadDialog = ({
maximumSize,
canvasClass,
}) => {
const [t] = useTranslation('ViewportDownloadForm');
const [filename, setFilename] = useState(DEFAULT_FILENAME);
const [fileType, setFileType] = useState('jpg');
@@ -190,142 +189,127 @@ const DownloadDialog = ({
};
return (
<Modal
show={isOpen}
onHide={onClose}
aria-labelledby="ModalHeader"
className="DownloadDialog modal fade themed in"
backdrop={false}
large={true}
keyboard={true}
>
<Modal.Header closeButton>
<Modal.Title>{t('Download High Quality Image')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="title">
{t(
'Please specify the dimensions, filename, and desired type for the output image.'
)}
</div>
<>
<div className="title">
{t(
'Please specify the dimensions, filename, and desired type for the output image.'
)}
</div>
<div className="file-info-container">
<div className="col">
<div className="width">
<TextInput
type="number"
min={minimumSize}
max={maximumSize}
value={width}
label={t('Image width (px)')}
onChange={onWidthChange}
/>
</div>
<div className="height">
<TextInput
type="number"
min={minimumSize}
max={maximumSize}
value={height}
label={t('Image height (px)')}
onChange={onHeightChange}
/>
</div>
<div className="file-info-container">
<div className="col">
<div className="width">
<TextInput
type="number"
min={minimumSize}
max={maximumSize}
value={width}
label={t('Image width (px)')}
onChange={onWidthChange}
/>
</div>
<div className="col">
<div className="file-name">
<TextInput
type="text"
value={filename}
onChange={event => setFilename(event.target.value)}
label={t('File name')}
id="file-name"
/>
</div>
<div className="file-type">
<Select
value={fileType}
onChange={event => setFileType(event.target.value)}
options={FILE_TYPE_OPTIONS}
label={t('File type')}
/>
</div>
</div>
<div className="col">
<div className="show-annotations">
<label htmlFor="show-annotations" className="form-check-label">
<input
id="show-annotations"
type="checkbox"
className="form-check-input"
checked={showAnnotations}
onChange={event => setShowAnnotations(event.target.checked)}
/>
{t('Show Annotations')}
</label>
</div>
<div className="height">
<TextInput
type="number"
min={minimumSize}
max={maximumSize}
value={height}
label={t('Image height (px)')}
onChange={onHeightChange}
/>
</div>
</div>
<div
<div className="col">
<div className="file-name">
<TextInput
type="text"
value={filename}
onChange={event => setFilename(event.target.value)}
label={t('File name')}
id="file-name"
/>
</div>
<div className="file-type">
<Select
value={fileType}
onChange={event => setFileType(event.target.value)}
options={FILE_TYPE_OPTIONS}
label={t('File type')}
/>
</div>
</div>
<div className="col">
<div className="show-annotations">
<label htmlFor="show-annotations" className="form-check-label">
<input
id="show-annotations"
type="checkbox"
className="form-check-input"
checked={showAnnotations}
onChange={event => setShowAnnotations(event.target.checked)}
/>
{t('Show Annotations')}
</label>
</div>
</div>
</div>
<div
style={{
height: viewportElementHeight,
width: viewportElementWidth,
position: 'absolute',
left: '9999px',
}}
ref={ref => setViewportElement(ref)}
>
<canvas
className={canvasClass}
style={{
height: viewportElementHeight,
width: viewportElementWidth,
position: 'absolute',
left: '9999px',
height: downloadCanvas.height,
width: downloadCanvas.width,
display: 'block',
}}
ref={ref => setViewportElement(ref)}
>
<canvas
className={canvasClass}
style={{
height: downloadCanvas.height,
width: downloadCanvas.width,
display: 'block',
}}
width={downloadCanvas.width}
height={downloadCanvas.height}
ref={downloadCanvas.ref}
></canvas>
</div>
width={downloadCanvas.width}
height={downloadCanvas.height}
ref={downloadCanvas.ref}
></canvas>
</div>
<div className="preview">
<h4> {t('Image Preview')}</h4>
<img
className="viewport-preview"
src={viewportPreview.src}
alt="Viewport Preview"
style={{
height: viewportPreview.height,
width: viewportPreview.width,
}}
/>
</div>
<div className="preview">
<h4> {t('Image Preview')}</h4>
<img
className="viewport-preview"
src={viewportPreview.src}
alt="Viewport Preview"
style={{
height: viewportPreview.height,
width: viewportPreview.width,
}}
/>
</div>
<div className="actions">
<div className="action-cancel">
<button type="button" className="btn btn-danger" onClick={onClose}>
{t('Cancel')}
</button>
</div>
<div className="action-save">
<button onClick={downloadImage} className="btn btn-primary">
{t('Download')}
</button>
</div>
<div className="actions">
<div className="action-cancel">
<button type="button" className="btn btn-danger" onClick={onClose}>
{t('Cancel')}
</button>
</div>
</Modal.Body>
</Modal>
<div className="action-save">
<button onClick={downloadImage} className="btn btn-primary">
{t('Download')}
</button>
</div>
</div>
</>
);
};
DownloadDialog.propTypes = {
t: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
activeViewport: PropTypes.object,
ViewportDownloadForm.propTypes = {
onClose: PropTypes.func.isRequired,
activeViewport: PropTypes.object,
updateViewportPreview: PropTypes.func.isRequired,
enableViewport: PropTypes.func.isRequired,
disableViewport: PropTypes.func.isRequired,
@@ -338,4 +322,4 @@ DownloadDialog.propTypes = {
canvasClass: PropTypes.string.isRequired,
};
export default withTranslation('DownloadDialog')(DownloadDialog);
export default ViewportDownloadForm;
@@ -1,8 +1,8 @@
@import './../../design/styles/common/global.styl'
@import './../../design/styles/common/form.styl'
@import './../../design/styles/common/button.styl'
@import '../../../design/styles/common/global.styl'
@import '../../../design/styles/common/form.styl'
@import '../../../design/styles/common/button.styl'
.DownloadDialog
.ViewportDownloadForm
color: var(--text-secondary-color);
filter: drop-shadow(0 0 3px var(--ui-gray-darkest));
border: none;
@@ -0,0 +1,39 @@
---
name: Viewport Download Form
menu: Components
route: /components/viewport-download-form
---
import { Playground, Props } from 'docz'
import { State } from 'react-powerplug'
import { ViewportDownloadForm } from './../index'
import NameSpace from '../../../__docs__/NameSpace'
# Viewport Download Form
## Basic usage
<Playground>
<State
initial={{}}
>
{({ state, setState }) => (
<React.Fragment>
<div>
<pre>{JSON.stringify(state, null, 2)}</pre>
</div>
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<ViewportDownloadForm />
</div>
</React.Fragment>
)}
</State>
</Playground>
## API
<Props of={ViewportDownloadForm} />
## Translation Namespace
<NameSpace name="ViewportDownloadForm" />
@@ -0,0 +1,2 @@
import ViewportDownloadForm from './ViewportDownloadForm';
export { ViewportDownloadForm };
@@ -1,2 +0,0 @@
import DownloadDialog from './DownloadDialog';
export { DownloadDialog };
+9 -7
View File
@@ -4,18 +4,19 @@ import { MeasurementTable, MeasurementTableItem } from './measurementTable';
import { Overlay, OverlayTrigger } from './overlayTrigger';
import { TableList, TableListItem } from './tableList';
import {
AboutModal,
AboutContent,
UserPreferences,
UserPreferencesModal,
} from './userPreferencesModal';
UserPreferencesForm,
} from './userPreferencesForm';
import { Checkbox } from './checkbox';
import { CineDialog } from './cineDialog';
import { DownloadDialog } from './downloadDialog';
import { ViewportDownloadForm } from './content/viewportDownloadForm';
import { QuickSwitch } from './quickSwitch';
import { RoundedButtonGroup } from './roundedButtonGroup';
import { SelectTree } from './selectTree';
import { SimpleDialog } from './simpleDialog';
import { OHIFModal } from './ohifModal';
import {
PageToolbar,
StudyList,
@@ -28,7 +29,7 @@ import { Tooltip } from './tooltip';
export {
Checkbox,
CineDialog,
DownloadDialog,
ViewportDownloadForm,
LayoutButton,
LayoutChooser,
MeasurementTable,
@@ -49,7 +50,8 @@ export {
TablePagination,
ToolbarSection,
Tooltip,
AboutModal,
AboutContent,
UserPreferences,
UserPreferencesModal,
UserPreferencesForm,
OHIFModal,
};
@@ -30,6 +30,11 @@
.measurementLocation
margin-left: 9px;
margin-top: 9px;
overflow: hidden;
max-width: calc(100% - 50px);
text-overflow: ellipsis;
overflow-wrap: normal;
white-space: nowrap;
.measurementDisplayText
display: inline-block;
@@ -0,0 +1,68 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactBootstrapModal from 'react-bootstrap-modal';
import classNames from 'classnames';
const OHIFModal = ({
className,
closeButton,
backdrop,
keyboard,
show,
title,
onHide,
footer: Footer,
header: Header,
children,
}) => (
<ReactBootstrapModal
className={classNames('modal fade themed in', className)}
backdrop={backdrop}
keyboard={keyboard}
show={show}
large={true}
title={title}
onHide={onHide}
>
{(Header || title) && (
<ReactBootstrapModal.Header closeButton={closeButton}>
{title && (
<ReactBootstrapModal.Title>{title}</ReactBootstrapModal.Title>
)}
{Header && <Header hide={onHide} />}
</ReactBootstrapModal.Header>
)}
<ReactBootstrapModal.Body>{children}</ReactBootstrapModal.Body>
{Footer && (
<ReactBootstrapModal.Footer>
<Footer hide={onHide} />
</ReactBootstrapModal.Footer>
)}
</ReactBootstrapModal>
);
OHIFModal.propTypes = {
className: PropTypes.string,
closeButton: PropTypes.bool,
backdrop: PropTypes.bool,
keyboard: PropTypes.bool,
show: PropTypes.bool,
title: PropTypes.string,
onHide: PropTypes.func,
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;
@@ -1,15 +1,15 @@
---
name: Download Dialog
name: OHIFModal
menu: Components
route: /components/download-dialog
route: /components/ohif-modal
---
import { Playground, Props } from 'docz'
import { State } from 'react-powerplug'
import { DownloadDialog } from './../index'
import { OHIFModal } from './../index'
import NameSpace from '../../../__docs__/NameSpace'
# Download Dialog
# OHIF Modal
## Basic usage
@@ -23,7 +23,7 @@ import NameSpace from '../../../__docs__/NameSpace'
<pre>{JSON.stringify(state, null, 2)}</pre>
</div>
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<DownloadDialog />
<OHIFModal />
</div>
</React.Fragment>
)}
@@ -32,8 +32,8 @@ import NameSpace from '../../../__docs__/NameSpace'
## API
<Props of={DownloadDialog} />
<Props of={OHIFModal} />
## Translation Namespace
<NameSpace name="DownloadDialog" />
<NameSpace name="OHIFModal" />
@@ -0,0 +1,2 @@
import OHIFModal from './OHIFModal';
export { 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>
);
@@ -1,7 +1,8 @@
/* global cornerstone */
import './ImageThumbnail.styl';
import React, { PureComponent } from 'react';
import { utils } from '@ohif/core';
import React, { useState, useEffect, createRef } from 'react';
import PropTypes from 'prop-types';
import ViewportErrorIndicator from '../../viewer/ViewportErrorIndicator';
@@ -12,100 +13,132 @@ import ViewportLoadingIndicator from '../../viewer/ViewportLoadingIndicator';
// - Set as external dependency?
// - Pass in the entire load and render function as a prop?
//import cornerstone from 'cornerstone-core';
function ImageThumbnail(props) {
const {
width,
height,
imageSrc,
imageId,
stackPercentComplete,
error: propsError,
} = props;
export default class ImageThumbnail extends PureComponent {
static propTypes = {
imageSrc: PropTypes.string,
imageId: PropTypes.string,
error: PropTypes.bool.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
stackPercentComplete: PropTypes.number.isRequired,
const [isLoading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [image, setImage] = useState({});
const canvasRef = createRef();
let loadingOrError;
let cancelablePromise;
if (propsError || error) {
loadingOrError = <ViewportErrorIndicator />;
} else if (isLoading) {
loadingOrError = <ViewportLoadingIndicator />;
}
const showStackLoadingProgressBar = stackPercentComplete !== undefined;
const shouldRenderToCanvas = () => {
return imageId && !imageSrc;
};
static defaultProps = {
error: false,
stackPercentComplete: 0,
width: 217,
height: 123,
const fetchImagePromise = () => {
if (!cancelablePromise) {
return;
}
setLoading(true);
cancelablePromise
.then(response => {
setImage(response);
})
.catch(error => {
if (error.isCanceled) return;
setLoading(false);
setError(true);
throw new Error(error);
});
};
constructor(props) {
super(props);
const setImagePromise = () => {
if (shouldRenderToCanvas()) {
cancelablePromise = utils.makeCancelable(
cornerstone.loadAndCacheImage(imageId)
);
}
};
this.canvas = React.createRef();
this.state = {
loading: this.shouldRenderToCanvas(),
const purgeCancelablePromise = () => {
if (cancelablePromise) {
cancelablePromise.cancel();
}
};
useEffect(() => {
return () => {
purgeCancelablePromise();
};
}
}, []);
shouldRenderToCanvas() {
return this.props.imageId && !this.props.imageSrc;
}
componentDidMount() {
if (this.shouldRenderToCanvas()) {
cornerstone
.loadAndCacheImage(this.props.imageId)
.then(image => {
cornerstone.renderToCanvas(this.canvas.current, image);
this.setState({
loading: false,
});
})
.catch(error => {
this.setState({
loading: false,
error: true,
});
throw new Error(error);
});
useEffect(() => {
if (image.imageId) {
cornerstone.renderToCanvas(canvasRef.current, image);
setLoading(false);
}
}
}, [image.imageId]);
render() {
let loadingOrError;
if (this.props.error) {
loadingOrError = <ViewportErrorIndicator />;
} else if (this.state.loading) {
loadingOrError = <ViewportLoadingIndicator />;
useEffect(() => {
if (!image.imageId || image.imageId !== imageId) {
purgeCancelablePromise();
setImagePromise();
fetchImagePromise();
}
}, [imageId]);
const showStackLoadingProgressBar =
this.props.stackPercentComplete !== undefined;
return (
<div className="ImageThumbnail">
<div className="image-thumbnail-canvas">
{this.shouldRenderToCanvas() ? (
<canvas
ref={this.canvas}
width={this.props.width}
height={this.props.height}
/>
) : (
<img
className="static-image"
src={this.props.imageSrc}
//width={this.props.width}
height={this.props.height}
alt={''}
/>
)}
</div>
{loadingOrError}
{showStackLoadingProgressBar && (
<div className="image-thumbnail-progress-bar">
<div
className="image-thumbnail-progress-bar-inner"
style={{ width: `${this.props.stackPercentComplete}%` }}
/>
</div>
)}
{this.state.loading && (
<div className="image-thumbnail-loading-indicator"></div>
return (
<div className="ImageThumbnail">
<div className="image-thumbnail-canvas">
{shouldRenderToCanvas() ? (
<canvas ref={canvasRef} width={width} height={height} />
) : (
<img
className="static-image"
src={imageSrc}
//width={this.props.width}
height={height}
alt={''}
/>
)}
</div>
);
}
{loadingOrError}
{showStackLoadingProgressBar && (
<div className="image-thumbnail-progress-bar">
<div
className="image-thumbnail-progress-bar-inner"
style={{ width: `${stackPercentComplete}%` }}
/>
</div>
)}
{isLoading && <div className="image-thumbnail-loading-indicator"></div>}
</div>
);
}
ImageThumbnail.propTypes = {
imageSrc: PropTypes.string,
imageId: PropTypes.string,
error: PropTypes.bool,
width: PropTypes.number,
height: PropTypes.number,
stackPercentComplete: PropTypes.number.isRequired,
};
ImageThumbnail.defaultProps = {
error: false,
stackPercentComplete: 0,
width: 217,
height: 123,
};
export default ImageThumbnail;
@@ -32,7 +32,7 @@ function StudyBrowser(props) {
return (
<div
key={`container_${studyIndex}_${thumbIndex}`}
key={thumb.displaySetInstanceUid}
className="thumbnail-container"
data-cy="thumbnail-list"
>
@@ -5,12 +5,9 @@ import classNames from 'classnames';
import TableSearchFilter from './TableSearchFilter.js';
import useMedia from '../../hooks/useMedia.js';
import PropTypes from 'prop-types';
import ColorHash from './internal/color-hash.js';
import { StudyListLoadingText } from './StudyListLoadingText.js';
import { withTranslation } from '../../utils/LanguageProvider';
const colorHash = new ColorHash();
/**
*
*
@@ -236,13 +233,15 @@ function TableRow(props) {
onClick={() => handleClick(studyInstanceUid)}
className={classNames({ active: isHighlighted })}
>
<td className={classNames({ emptyCell: !patientName })}>
<td className={classNames({ 'empty-value': !patientName })}>
{patientName || `(${t('Empty')})`}
</td>
<td>{patientId}</td>
<td>{accessionNumber}</td>
<td>{studyDate}</td>
<td>{modalities}</td>
<td className={classNames({ 'empty-value': !modalities })}>
{modalities || `(${t('Empty')})`}
</td>
<td>{studyDescription}</td>
</tr>
);
@@ -252,7 +251,7 @@ function TableRow(props) {
onClick={() => handleClick(studyInstanceUid)}
className={classNames({ active: isHighlighted })}
>
<td className={classNames({ emptyCell: !patientName })}>
<td className={classNames({ 'empty-value': !patientName })}>
{patientName || `(${t('Empty')})`}
<div style={{ color: '#60656f' }}>{patientId}</div>
</td>
@@ -274,25 +273,19 @@ function TableRow(props) {
style={{
display: 'flex',
flexDirection: 'column',
minWidth: '80px',
maxWidth: '100px',
maxWidth: '80px',
width: '80px',
}}
>
<div
style={{
backgroundColor: colorHash.hex(modalities),
borderRadius: '16px',
padding: '2px 8px 0px 8px',
fontWeight: 500,
marginBottom: '4px',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}}
className={classNames({
modalities: modalities,
'empty-value': !modalities,
})}
aria-label={modalities}
title={modalities}
>
{modalities}
{modalities || `(${t('Empty')})`}
</div>
<div
style={{
@@ -322,7 +315,7 @@ function TableRow(props) {
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
{/* NAME AND ID */}
<div
className={classNames({ emptyCell: !patientName })}
className={classNames({ 'empty-value': !patientName })}
style={{ width: '150px', minWidth: '150px' }}
>
<div style={{ fontWeight: 500, paddingTop: '3px' }}>
@@ -348,19 +341,19 @@ function TableRow(props) {
style={{
display: 'flex',
flexDirection: 'column',
minWidth: '80px',
maxWidth: '80px',
width: '80px',
}}
>
<div
style={{
backgroundColor: colorHash.hex(modalities),
borderRadius: '16px',
padding: '2px 8px 0px 8px',
fontWeight: 500,
marginBottom: '4px',
}}
className={classNames({
modalities: modalities,
'empty-value': !modalities,
})}
aria-label={modalities}
title={modalities}
>
{modalities}
{modalities || `(${t('Empty')})`}
</div>
<div>{studyDate}</div>
</div>
@@ -381,7 +374,7 @@ function TableRow(props) {
TableRow.propTypes = {
accessionNumber: PropTypes.string.isRequired,
isHighlighted: PropTypes.bool,
modalities: PropTypes.string.isRequired,
modalities: PropTypes.string,
patientId: PropTypes.string.isRequired,
patientName: PropTypes.string.isRequired,
studyDate: PropTypes.string.isRequired,
@@ -113,11 +113,18 @@ table.table
border-spacing: 0;
border-collapse: collapse;
table-layout: fixed;
color: var(--table-text-primary-color)
font-weight: 300
> tbody tr
padding: 5px
background-color: black
> tbody td
padding: $body-cell-top-bottom-padding 8px;
height: $body-cell-height
word-wrap: break-word;
/* Striped Variant */
&.table--striped > tbody tr:nth-child(even)
background-color: var(--ui-gray-darker)
@@ -130,6 +137,9 @@ table.table
&:hover, &:active, &.active
background-color: var(--ui-gray-darker)
.empty-value
color: var(--ui-gray-light)
.study-list-container > table.table > tr
height: 20px
@@ -146,18 +156,14 @@ table.table
text-align: left;
border-top: 0;
.study-list-container > table.table > tbody > tr > td
padding: $body-cell-top-bottom-padding 8px;
height: $body-cell-height
color: var(--table-text-primary-color)
font-weight: 300
word-wrap: break-word;
&.emptyCell
color: var(--ui-gray-light)
.study-list-container > table.table > thead > tr > th.studyDate
min-width: 230px
.study-list-container > table.table .modalities
font-weight: 500;
min-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
.study-list-container
.filters
@@ -1,24 +0,0 @@
/**
* BKDR Hash (modified version)
*
* @param {String} str string to hash
* @returns {Number}
*/
function BKDRHash(str) {
const seed = 131;
const seed2 = 137;
let hash = 0;
// make hash more sensitive for short string like 'a', 'b', 'c'
str += 'x';
// Note: Number.MAX_SAFE_INTEGER equals 9007199254740991
var MAX_SAFE_INTEGER = parseInt(9007199254740991 / seed2);
for (var i = 0; i < str.length; i++) {
if (hash > MAX_SAFE_INTEGER) {
hash = parseInt(hash / seed2);
}
hash = hash * seed + str.charCodeAt(i);
}
return hash;
}
export default BKDRHash;
@@ -1,145 +0,0 @@
import BKDRHash from './bkdr-hash';
/**
* Convert RGB Array to HEX
*
* @param {Array} RGBArray - [R, G, B]
* @returns {String} 6 digits hex starting with #
*/
function RGB2HEX(RGBArray) {
let hex = '#';
RGBArray.forEach(function(value) {
if (value < 16) {
hex += 0;
}
hex += value.toString(16);
});
return hex;
}
/**
* Convert HSL to RGB
*
* @see {@link http://zh.wikipedia.org/wiki/HSL和HSV色彩空间} for further information.
* @param {Number} H Hue ∈ [0, 360)
* @param {Number} S Saturation ∈ [0, 1]
* @param {Number} L Lightness ∈ [0, 1]
* @returns {Array} R, G, B ∈ [0, 255]
*/
function HSL2RGB(H, S, L) {
H /= 360;
const q = L < 0.5 ? L * (1 + S) : L + S - L * S;
const p = 2 * L - q;
return [H + 1 / 3, H, H - 1 / 3].map(function(color) {
if (color < 0) {
color++;
}
if (color > 1) {
color--;
}
if (color < 1 / 6) {
color = p + (q - p) * 6 * color;
} else if (color < 0.5) {
color = q;
} else if (color < 2 / 3) {
color = p + (q - p) * 6 * (2 / 3 - color);
} else {
color = p;
}
return Math.round(color * 255);
});
}
function isArray(o) {
return Object.prototype.toString.call(o) === '[object Array]';
}
/**
* Color Hash Class
*
* @class
*/
const ColorHash = function(options = {}) {
const LS = [options.lightness, options.saturation].map(function(param) {
param = param || [0.35, 0.5, 0.65]; // note that 3 is a prime
return isArray(param) ? param.concat() : [param];
});
this.L = LS[0];
this.S = LS[1];
if (typeof options.hue === 'number') {
options.hue = { min: options.hue, max: options.hue };
}
if (typeof options.hue === 'object' && !isArray(options.hue)) {
options.hue = [options.hue];
}
if (typeof options.hue === 'undefined') {
options.hue = [];
}
this.hueRanges = options.hue.map(function(range) {
return {
min: typeof range.min === 'undefined' ? 0 : range.min,
max: typeof range.max === 'undefined' ? 360 : range.max,
};
});
this.hash = options.hash || BKDRHash;
};
/**
* Returns the hash in [h, s, l].
* Note that H ∈ [0, 360); S ∈ [0, 1]; L ∈ [0, 1];
*
* @param {String} str string to hash
* @returns {Array} [h, s, l]
*/
ColorHash.prototype.hsl = function(str) {
var H, S, L;
var hash = this.hash(str);
if (this.hueRanges.length) {
var range = this.hueRanges[hash % this.hueRanges.length];
var hueResolution = 727; // note that 727 is a prime
H =
(((hash / this.hueRanges.length) % hueResolution) *
(range.max - range.min)) /
hueResolution +
range.min;
} else {
H = hash % 359; // note that 359 is a prime
}
hash = parseInt(hash / 360);
S = this.S[hash % this.S.length];
hash = parseInt(hash / this.S.length);
L = this.L[hash % this.L.length];
return [H, S, L];
};
/**
* Returns the hash in [r, g, b].
* Note that R, G, B ∈ [0, 255]
*
* @param {String} str string to hash
* @returns {Array} [r, g, b]
*/
ColorHash.prototype.rgb = function(str) {
var hsl = this.hsl(str);
return HSL2RGB.apply(this, hsl);
};
/**
* Returns the hash in hex
*
* @param {String} str string to hash
* @returns {String} hex with #
*/
ColorHash.prototype.hex = function(str) {
var rgb = this.rgb(str);
return RGB2HEX(rgb);
};
export default ColorHash;
@@ -38,6 +38,7 @@
.itemContent
flex: 1
color: var(--text-secondary-color)
width: calc(100% - 30px);
font-weight: 400
margin-left: 9px;
margin-top: 9px;
@@ -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>
);
}
}
@@ -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);
@@ -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;
@@ -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
@@ -6,7 +6,7 @@ route: /components/about-modal
import { Playground, Props } from 'docz'
import { State } from 'react-powerplug'
import { AboutModal } from './../index.js'
import { AboutContent } from './../index.js'
# About Modal
@@ -26,7 +26,7 @@ import { AboutModal } from './../index.js'
>
Open about modal
</button>
<AboutModal
<AboutContent
{...state}
onCancel={() => setState({ isOpen: false })}
/>
@@ -39,4 +39,4 @@ import { AboutModal } from './../index.js'
## API
<Props of={AboutModal} />
<Props of={AboutContent} />
@@ -60,11 +60,6 @@ export default {
keys: [''],
column: 1,
},
toggleDownloadDialog: {
label: 'Show/Hide Download Dialog',
keys: [''],
column: 1,
},
// Preset hotkeys
WLPreset0: { label: 'W/L Preset 0 (Soft Tissue)', keys: ['1'], column: 1 },
@@ -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" />
@@ -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), // ;=,-./
@@ -0,0 +1,4 @@
export { UserPreferences } from './UserPreferences.js';
export { AboutContent } from '../content/aboutContent/AboutContent.js';
export { UserPreferencesForm } from './UserPreferencesForm.js';
export { GeneralPreferences } from './GeneralPreferences.js';
@@ -1,143 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Modal from 'react-bootstrap-modal';
import detect from 'browser-detect';
import './AboutModal.styl';
import { withTranslation } from '../../utils/LanguageProvider';
import 'react-bootstrap-modal/lib/css/rbm-patch.css';
// TODO: Is this the only component importing these?
import './../../design/styles/common/modal.styl';
class AboutModal extends Component {
constructor(props) {
super(props);
}
// TODO: Make this component more generic to allow things other than W/L and hotkeys...
static propTypes = {
isOpen: PropTypes.bool.isRequired,
onCancel: PropTypes.func,
};
itemsPreset() {
const { t } = this.props;
const browser = detect();
const capitalize = s =>
s.substr(0, 1).toUpperCase() + s.substr(1).toLowerCase();
return [
{
name: t('Repository URL'),
value: 'https://github.com/OHIF/Viewers/',
link: 'https://github.com/OHIF/Viewers/',
},
{
name: t('Latest Master Commits'),
value: 'https://github.com/OHIF/Viewers/commits/master',
link: 'https://github.com/OHIF/Viewers/commits/master',
},
{
name: 'Version Number',
value: process.env.VERSION_NUMBER,
},
{
name: t('Build Number'),
value: process.env.BUILD_NUM,
},
{
name: t('Browser'),
value: `${capitalize(browser.name)} ${browser.version}`,
},
{
name: t('OS'),
value: browser.os,
},
];
}
static defaultProps = {
isOpen: false,
};
renderTableRow(item) {
return (
<tr key={item.name} style={{ backgroundColor: 'transparent' }}>
<td>{item.name}</td>
<td>
{item.link ? (
<a target="_blank" href={item.link}>
{item.value}
</a>
) : (
item.value
)}
</td>
</tr>
);
}
render() {
const { t } = this.props;
return (
<Modal
show={this.props.isOpen}
onHide={this.props.onCancel}
aria-labelledby="ModalHeader"
className="AboutModal modal fade themed in"
backdrop={false}
large={true}
keyboard={false}
>
<Modal.Header closeButton>
<Modal.Title>{t('OHIF Viewer - About')}</Modal.Title>
</Modal.Header>
<Modal.Body data-cy="about-modal">
<div className="btn-group">
<a
className="btn btn-default"
target="_blank"
href="https://groups.google.com/forum/#!forum/cornerstone-platform"
>
{t('Visit the forum')}
</a>
{` `}
<a
className="btn btn-default"
target="_blank"
href="https://github.com/OHIF/Viewers/issues/new/choose"
>
{t('Report an issue')}
</a>
{` `}
<a
className="btn btn-default"
target="_blank"
href="http://ohif.org"
>
{t('More details')}
</a>
</div>
<div>
<h3>{t('Version Information')}</h3>
<table className="table table-responsive">
<thead>
<tr>
<th>{t('Name')}</th>
<th>{t('Value')}</th>
</tr>
</thead>
<tbody>
{this.itemsPreset().map(item => this.renderTableRow(item))}
</tbody>
</table>
</div>
</Modal.Body>
</Modal>
);
}
}
const connectedComponent = withTranslation('AboutModal')(AboutModal);
export { connectedComponent as AboutModal };
export default connectedComponent;
@@ -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;
@@ -1,4 +0,0 @@
export { UserPreferences } from './UserPreferences.js';
export { AboutModal } from './AboutModal.js';
export { UserPreferencesModal } from './UserPreferencesModal.js';
export { GeneralPreferences } from './GeneralPreferences.js';
+17 -6
View File
@@ -1,7 +1,7 @@
import {
Checkbox,
CineDialog,
DownloadDialog,
ViewportDownloadForm,
LayoutButton,
LayoutChooser,
MeasurementTable,
@@ -22,9 +22,10 @@ import {
TablePagination,
ToolbarSection,
Tooltip,
AboutModal,
AboutContent,
UserPreferences,
UserPreferencesModal,
UserPreferencesForm,
OHIFModal,
} from './components';
import { useDebounce, useMedia } from './hooks';
@@ -52,6 +53,11 @@ import SnackbarProvider, {
useSnackbarContext,
withSnackbar,
} from './utils/SnackbarProvider';
import ModalProvider, {
useModal,
withModal,
ModalConsumer,
} from './utils/ModalProvider';
export {
// Elements
@@ -63,7 +69,7 @@ export {
TextArea,
TextInput,
CineDialog,
DownloadDialog,
ViewportDownloadForm,
ExpandableToolMenu,
Icon,
LayoutButton,
@@ -93,13 +99,18 @@ export {
ToolbarButton,
ToolbarSection,
Tooltip,
AboutModal,
AboutContent,
UserPreferences,
UserPreferencesModal,
UserPreferencesForm,
ViewerbaseDragDropContext,
SnackbarProvider,
useSnackbarContext,
withSnackbar,
ModalProvider,
useModal,
ModalConsumer,
withModal,
OHIFModal,
// Hooks
useDebounce,
useMedia,
+122
View File
@@ -0,0 +1,122 @@
import React, {
useState,
createContext,
useContext,
useEffect,
useCallback,
} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const ModalContext = createContext(null);
const { Provider } = ModalContext;
export const useModal = () => useContext(ModalContext);
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. */,
closeButton: true /* Should the modal body render the close button. */,
title: null /* Should the modal render the title independently of the body content. */,
customClassName: null /* The custom class to style the 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 = useCallback(
(component, props = {}) =>
setOptions(Object.assign({}, options, props, { component })),
[options]
);
/**
* Hide the modal and set its properties to default.
*
* @returns void
*/
const hide = useCallback(() => setOptions(DEFAULT_OPTIONS), [
DEFAULT_OPTIONS,
]);
const { component: Component } = options;
return (
<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.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,
}),
};
/**
* Higher Order Component to use the modal methods through a Class Component.
*
* @returns
*/
export const withModal = Component => {
return function WrappedComponent(props) {
const { show, hide } = useModal();
return <Component {...props} modal={{ show, hide }} />;
};
};
export default ModalProvider;
export const ModalConsumer = ModalContext.Consumer;
+73 -33
View File
@@ -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
+181
View File
@@ -3,6 +3,187 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
## [2.6.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.6.2...@ohif/viewer@2.6.3) (2019-11-08)
**Note:** Version bump only for package @ohif/viewer
## [2.6.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.6.1...@ohif/viewer@2.6.2) (2019-11-08)
**Note:** Version bump only for package @ohif/viewer
## [2.6.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.6.0...@ohif/viewer@2.6.1) (2019-11-06)
**Note:** Version bump only for package @ohif/viewer
# [2.6.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.5.0...@ohif/viewer@2.6.0) (2019-11-06)
### Features
* modal provider ([#1151](https://github.com/OHIF/Viewers/issues/1151)) ([75d88bc](https://github.com/OHIF/Viewers/commit/75d88bc454710d2dcdbc7d68c4d9df041159c840)), closes [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1146](https://github.com/OHIF/Viewers/issues/1146) [#1142](https://github.com/OHIF/Viewers/issues/1142) [#1143](https://github.com/OHIF/Viewers/issues/1143) [#1110](https://github.com/OHIF/Viewers/issues/1110) [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116) [#1119](https://github.com/OHIF/Viewers/issues/1119)
# [2.5.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.4.1...@ohif/viewer@2.5.0) (2019-11-05)
### Features
* 🎸 Filter by url query param for seriesInstnaceUID ([#1117](https://github.com/OHIF/Viewers/issues/1117)) ([e208f2e](https://github.com/OHIF/Viewers/commit/e208f2e6a9c49b16dadead0a917f657cf023929a)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
## [2.4.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.4.0...@ohif/viewer@2.4.1) (2019-11-05)
### Bug Fixes
* [#1075](https://github.com/OHIF/Viewers/issues/1075) Returning to the Study List before all series have finishe… ([#1090](https://github.com/OHIF/Viewers/issues/1090)) ([ecaf578](https://github.com/OHIF/Viewers/commit/ecaf578f92dc40294cec7ff9b272fb432dec4125))
# [2.4.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.3.8...@ohif/viewer@2.4.0) (2019-11-04)
### Features
* 🎸 New modal provider ([#1110](https://github.com/OHIF/Viewers/issues/1110)) ([5ee832b](https://github.com/OHIF/Viewers/commit/5ee832b19505a4e8e5756660ce6ed03a7f18dec3)), closes [#1086](https://github.com/OHIF/Viewers/issues/1086) [#1116](https://github.com/OHIF/Viewers/issues/1116)
## [2.3.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.3.7...@ohif/viewer@2.3.8) (2019-11-04)
**Note:** Version bump only for package @ohif/viewer
## [2.3.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.3.6...@ohif/viewer@2.3.7) (2019-11-04)
### Bug Fixes
* 🐛 Minor issues measurement panel related to description ([#1142](https://github.com/OHIF/Viewers/issues/1142)) ([681384b](https://github.com/OHIF/Viewers/commit/681384b7425c83b02a0ed83371ca92d78ca7838c))
## [2.3.6](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.3.5...@ohif/viewer@2.3.6) (2019-11-02)
**Note:** Version bump only for package @ohif/viewer
## [2.3.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.3.4...@ohif/viewer@2.3.5) (2019-10-31)
### Bug Fixes
* application crash if patientName is an object ([#1138](https://github.com/OHIF/Viewers/issues/1138)) ([64cf3b3](https://github.com/OHIF/Viewers/commit/64cf3b324da2383a927af1df2d46db2fca5318aa))
## [2.3.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.3.3...@ohif/viewer@2.3.4) (2019-10-30)
@@ -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')
@@ -393,6 +375,20 @@ describe('OHIF Cornerstone Toolbar', () => {
});
});
it('checks if the available viewport was set to active when layout is decreased', () => {
cy.setLayout(3, 3);
// activate the ninth viewport
cy.get('[data-cy=viewport-container-8]')
.click()
.should('have.class', 'active');
cy.setLayout(1, 1);
// first viewport should be active
cy.get('[data-cy=viewport-container-0]').should('have.class', 'active');
});
it('checks if Clear tool will delete all measurements added in the viewport', () => {
//Add measurements in the viewport
cy.addLengthMeasurement();
@@ -411,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');
@@ -443,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];
@@ -458,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')
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff. Show more