Compare commits

...
Author SHA1 Message Date
ohif-bot 1334ba8eb0 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.13
 - @ohif/ui@0.64.2
 - @ohif/viewer@2.10.2
2019-11-25 15:50:36 +00:00
Danny Brown a870b3cc60 fix: Issue branch from danny experimental changes pr 1128 (#1150)
* fix: study list translations

* Don't render until translations are ready

* Try to wait for translations to load

* Use null to render "nothing"

* Try toggling useSuspense off up a layer

* logging

* Remove useSuspense false flags

* DO NOT OPEN A PR WITH IT.
Experimental changes only.
Try to solve issues with useTranslation hooks
 #Please enter the commit message for your changes. Lines starting

* Remove unecessary changes

* feat: 🎸 useMedia hook to not use one more prop for upd state vl

* docs: Add license scan report and status (#1161)

Signed-off-by: fossabot <badges@fossa.io>

* fix: 🐛 Fix for JS breaking on header (#1164)

* feat: 🎸 Code review and refact

Revised code based on PRs(variable alias,...). Changed hook for
useMedia. Now, it provides two hooks: one to get displayMediaSize and
other to get Entity(component, objects...) based on displayMediaSize.
Implemented a similar solution for state manager to store
mediaQueryList(s) and displaySize for app.

* feat: 🎸 Fine tunning on mediaQuery value, fixed issue about it

* chore(release): publish [skip ci]

 - @ohif/extension-vtk@0.53.6
 - @ohif/ui@0.62.1
 - @ohif/viewer@2.8.2

* Add new modal service

* Change serviceManager prop to servicesManager

* CR Update: fix casing and add required proptypes to providers

* CR Update: Improve ohifmodal proptypes

* CR Update: Fix typo in extensionmanager

* CR Update: add default props to service and check service in provider

* Refactor modal provider to better use its own state

* 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

* Use modal instead of modal context

* 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

* Change modal children order

* 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

* 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

* 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

* Fix OHIFModal proptypes

* 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

* 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

* 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

* Pass services to each module, improve tests

* Add servicesManager test and registerServices method

* Fix key warning of snackbar elements

* Remove netlify-cli; we';ll install this on CI server

* Update staging and prod netlify site IDs

* Clean up NPM_PUBLISH step

* Clean up DOCS_PUBLISH step

* Clean up Deploy workflow

* Custom executor to override cypress config

* Spacing

* Use an existing docker hub image

* Switch to npx instead of digging into npm bin location

* Remove e2e test before prod deploy

* Add workflow images

* docs: continous integrationn

* Add default props to modal

* 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

* 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

* ci: Redirect site traffic to index.html if file is not resolved

* ci: fix typo

* fix: 🐛 Code review. Remove 'global state' for displaySize

* fix: 🐛 Code review. Ref back to useMedia and pass value down

Code review. Ref back to useMedia and pass value down (components)
instead of creating a specialized hook to tied any component on it

* Merge from master. Missing files changes

* feat: 🎸 Code review. Fix issues with DatePicker

Fixed at least the minimum issue with datePicker and update some content
on every translation changed
2019-11-25 10:45:37 -05:00
ohif-bot 52687a7113 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.12
 - @ohif/ui@0.64.1
 - @ohif/viewer@2.10.1
2019-11-20 19:17:36 +00:00
Mirna Silva 421de23ba3 test: Test Cases for User Preferences (#1195)
* Test Cases for User Preferences

* CI fix

* Fix for CI

* Fix based on PR review comments

* Fix for script-tag issue on CI

* Fix for new selectors on Preferences Modal and CI failure
2019-11-20 14:15:10 -05:00
ohif-bot adc8d9ea0f chore(release): publish [skip ci]
- @ohif/extension-cornerstone@1.6.0
 - @ohif/extension-vtk@0.53.11
 - @ohif/core@1.12.0
 - @ohif/ui@0.64.0
 - @ohif/viewer@2.10.0
2019-11-19 19:19:32 +00:00
Igor Octaviano f65639c2b0 feat: New dialog service (#1202)
* Add dialog provider boilerplate and improve provider imports

* Add react-draggable dependency

* Add dialog service boilerplate

* Refactor modal to use react-modal and remove dependency on modal themed styles

* Update cinedialog to use new dialog provider

* Add multiple dialogs and refactor provider (dialogs)

* Set app root (modal)

* Add modal zindex bigger than tooltip

* Block drag on buttons/svgs/paths/input and improve jsdoc

* Use guid util to generate dialog ids

* Explicit props in dialog provider

* Improve jsdocs

* Fix tests broken

* Fix boundaries calculation

* Remember last dialog position

* Update providers location

* Add scroll to modal

* Add toggable button toolbar and fix css modal
2019-11-19 14:17:33 -05:00
Danny Brown cdd75bda2a docs: copy-paste of Erik's notes on PRs and releases
At least until we can sneak some graphics, tables, and emojis in
2019-11-19 04:03:41 -05:00
Danny Brown d92fe44d3c chore: add triage flag label 2019-11-19 03:50:17 -05:00
Danny Brown d90c2ac024 chore: add triage flag label 2019-11-19 03:49:57 -05:00
Danny Brown 7ce5e1d2d3 chore: add triage flag 2019-11-19 03:49:36 -05:00
Danny Brown a3bf47f9bf chore: attempt known good syntax 2019-11-19 03:48:32 -05:00
Danny Brown 81f79a26ad chore: revert 2019-11-19 03:46:28 -05:00
Danny Brown 60b4b5d7cf chore: lets try double quotes 2019-11-19 03:45:52 -05:00
Danny Brown 69c54d4f49 chore: labels format 2019-11-19 03:44:26 -05:00
Danny Brown 0f8868a232 chore: add triage to default bug report label 2019-11-19 03:39:09 -05:00
Danny Brown 1bbfee4087 chore: use github label syntax for triage flag 2019-11-19 03:38:33 -05:00
ohif-bot 33c7b59611 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.10
 - @ohif/core@1.11.0
 - @ohif/ui@0.63.0
 - @ohif/viewer@2.9.0
2019-11-19 08:36:12 +00:00
ladeirarodolfo b17f753e62 feat: Issue 879 viewer route query param not filtering but promoting (#1141)
* 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: 🎸 Instead of filtering do promote. WIP

* feat: 🎸 Fix minor issue. Allow promote or filter

* feat: 🎸 Changing component to functional component

* Merge from master Part1/2
Conflicts solved:
	both added:      core/src/studies/services/wado/retrieveMetadataLoader.js
	both added:      core/src/studies/services/wado/retrieveMetadataLoaderAsync.js
	both added:      core/src/studies/services/wado/retrieveMetadataLoaderSync.js
	both modified:   viewer/src/connectedComponents/ViewerRetrieveStudyData.js
	both modified:   viewer/src/routes/ViewerRouting.js

* Merge process from master Part 1/2
Missing files from previous commit

* feat: 🎸 Add cancelable promises to cut async methods

* feat: 🎸 Missing changes from previous merge process

* feat: 🎸 Missing changes from previous merge process
2019-11-19 03:33:48 -05:00
Danny Brown c77a2ef5c9 docs: fix broken links 2019-11-19 03:24:49 -05:00
Danny Brown 348edd4717 docs: Issue curation 2019-11-19 03:23:30 -05:00
Danny Brown 407060658a docs: add "our process" to sidebar 2019-11-19 02:53:57 -05:00
Danny Brown fc00f9d505 docs: clarify purpose of page and index links 2019-11-19 02:52:24 -05:00
Danny Brown bc3d080672 docs: flush out issue triage 2019-11-19 02:46:04 -05:00
Danny Brown 09353354c3 docs: start "our process" docs 2019-11-19 02:08:46 -05:00
ohif-bot 0a12320083 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.53.9
 - @ohif/i18n@0.52.1
 - @ohif/ui@0.62.4
 - @ohif/viewer@2.8.5
2019-11-18 21:33:41 +00:00
Rodrigo Antinarelli 813ee5ed4d fix: minor date picker UX improvements
* improvement: Date Picker

* remove unused values

* fix: search results w/ valid date returning empty

* translations from locize

* styling select

* revert studyListDateFilterNumDays
2019-11-18 16:30:47 -05:00
Danny Brown 6085b5f733 docs: add ohif urls to docs
docs: add ohif urls to docs
2019-11-18 11:33:48 -05:00
dannyrb ffe12789dd docs: add ohif urls to docs 2019-11-18 11:32:13 -05:00
Danny Brown 0b68429dfb Merge pull request #1200 from dannyrb/ci/complete-promoted-deploy
ci: fix typo
2019-11-18 11:07:50 -05:00
dannyrb b747ea5144 ci: fix typo 2019-11-18 11:06:57 -05:00
Danny Brown caadf089df Merge pull request #1199 from dannyrb/ci/complete-promoted-deploy
ci: Redirect site traffic to index.html if file is not resolved
2019-11-18 11:00:11 -05:00
Danny Brown e0fc69c181 Merge branch 'master' into ci/complete-promoted-deploy 2019-11-18 11:00:03 -05:00
dannyrb 2b9765304f ci: Redirect site traffic to index.html if file is not resolved 2019-11-18 10:57:21 -05:00
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
109 changed files with 4098 additions and 1036 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]' \
+228 -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,190 @@ jobs:
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/viewer:PR_BUILD-$CIRCLE_BUILD_NUM
VIEWER_DEMO_BUILD:
###
# Workflow: DEPLOY
###
BUILD:
<<: *defaults
steps:
# Checkout code and ALL Git Tags
- checkout:
post:
- git fetch --all
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
# when lock file changes, use increasingly general patterns to restore cache
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache ## Cache yarn and Cypress
key: yarn-packages-{{ checksum "yarn.lock" }}
# Build & Test
- run:
name: 'Build the OHIF Viewer'
command: yarn run build
no_output_timeout: 45m
# - run:
# name: 'Upload SourceMaps, Send Deploy Notification'
# command: |
# # export FILE_1=$(find ./build/static/js -type f -name "2.*.js" -exec basename {} \;)
# # export FILE_MAIN=$(find ./build/static/js -type f -name "main.*.js" -exec basename {} \;)
# # export FILE_RUNTIME_MAIN=$(find ./build/static/js -type f -name "runtime~main.*.js" -exec basename {} \;)
# # curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_1.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_1
# # curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_MAIN
# # curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_RUNTIME_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_RUNTIME_MAIN
# curl --request POST https://api.rollbar.com/api/1/deploy/ -F access_token=$ROLLBAR_TOKEN -F environment=$GOOGLE_STORAGE_BUCKET -F revision=$CIRCLE_SHA1 -F local_username=CircleCI
# Persist :+1:
- persist_to_workspace:
root: ~/repo
paths:
- platform/viewer/dist
- netlify.toml
- .netlify
DEPLOY_TO_DEV:
docker:
- image: circleci/node:12.9.1
environment:
TERM: xterm
NETLIFY_SITE_ID: 32708787-c9b0-4634-b50f-7ca41952da77
working_directory: ~/repo
steps:
- attach_workspace:
at: ~/repo
# Build & Test
- run: cd .netlify && npm install
- run:
name: 'Build Demo Site'
command: yarn run build:demo
no_output_timeout: 45m
cp .netlify/deploy-workflow/_redirects platform/viewer/dist/_redirects
- run: cd .netlify && npm run deploy
DEPLOY_TO_STAGING:
docker:
- image: circleci/node:12.9.1
environment:
TERM: xterm
NETLIFY_SITE_ID: c7502ae3-b150-493c-8422-05701e44a969
working_directory: ~/repo
steps:
- attach_workspace:
at: ~/repo
- run: cd .netlify && npm install
- run:
name: 'Upload SourceMaps, Send Deploy Notification'
cp .netlify/deploy-workflow/_redirects platform/viewer/dist/_redirects
- run: cd .netlify && npm run deploy
DEPLOY_TO_PRODUCTION:
docker:
- image: circleci/node:12.9.1
environment:
TERM: xterm
NETLIFY_SITE_ID: 79c4a5da-5c95-4dc9-84f7-45fd9dfe21b0
working_directory: ~/repo
steps:
- attach_workspace:
at: ~/repo
- run: cd .netlify && npm install
- run:
cp .netlify/deploy-workflow/_redirects platform/viewer/dist/_redirects
- run: cd .netlify && npm run deploy
###
# Workflow: RELEASE
###
NPM_PUBLISH:
<<: *defaults
steps:
- run: yarn -v
# Checkout code and ALL Git Tags
- checkout:
post:
- git fetch --all
# Use increasingly general patterns to restore cache
- restore_cache:
name: Restore Yarn and Cypress Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- yarn-packages-
- run:
name: Install Dependencies
command: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
paths:
- ~/.cache/yarn
key: yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: Avoid hosts unknown for github
command: |
# export FILE_1=$(find ./build/static/js -type f -name "2.*.js" -exec basename {} \;)
# export FILE_MAIN=$(find ./build/static/js -type f -name "main.*.js" -exec basename {} \;)
# export FILE_RUNTIME_MAIN=$(find ./build/static/js -type f -name "runtime~main.*.js" -exec basename {} \;)
# curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_1.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_1
# curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_MAIN
# curl https://api.rollbar.com/api/1/sourcemap -F source_map=@build/static/js/$FILE_RUNTIME_MAIN.map -F access_token=$ROLLBAR_TOKEN -F version=$CIRCLE_SHA1 -F minified_url=https://$GOOGLE_STORAGE_BUCKET/static/js/$FILE_RUNTIME_MAIN
curl --request POST https://api.rollbar.com/api/1/deploy/ -F access_token=$ROLLBAR_TOKEN -F environment=$GOOGLE_STORAGE_BUCKET -F revision=$CIRCLE_SHA1 -F local_username=CircleCI
# Persist :+1:
rm -rf ~/.ssh
mkdir ~/.ssh/
echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
git config --global user.name "ohif-bot"
- run:
name: Authenticate with NPM registry
command:
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
- run: npx lerna version
- run: npx lerna publish from-package
- persist_to_workspace:
root: ~/repo
paths: .
VIEWER_DEMO_PUBLISH:
working_directory: ~/repo
docker:
- image: google/cloud-sdk
DOCS_PUBLISH:
<<: *defaults
steps:
- checkout
- run:
name: Avoid hosts unknown for github
command: |
rm -rf ~/.ssh
mkdir ~/.ssh/
echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
git config --global user.email "danny.ri.brown+ohif-bot@gmail.com"
git config --global user.name "ohif-bot"
- run: yarn global add gitbook-cli gh-pages
- run: chmod +x ~/repo/.circleci/build-and-publish-docs.sh
- run: ~/repo/.circleci/build-and-publish-docs.sh
DOCKER_MASTER_PUBLISH:
<<: *defaults
steps:
- attach_workspace:
at: ~/repo
- setup_remote_docker:
docker_layer_caching: false
- run:
name: Deploy latest version to viewer.ohif.org
name: Build and push Docker image
command: |
# https://circleci.com/docs/2.0/google-auth/#authorization
echo $GCLOUD_SERVICE_KEY | gcloud auth activate-service-account --key-file=-
gcloud --quiet config set project ${GOOGLE_PROJECT_ID}
gcloud --quiet config set compute/zone ${GOOGLE_COMPUTE_ZONE}
# Remove all files in Storage Bucket; Push latest
gsutil -m rm gs://$GOOGLE_STORAGE_BUCKET/**
gsutil -m rsync -R platform/viewer/dist gs://$GOOGLE_STORAGE_BUCKET
# This file will exist if a new version was published by
# our command in the previous job. Created in npm postpublish hook
# in the `platform/viewer` project.
if [[ ! -e platform/viewer/success_version.txt ]]; then
exit 0
else
# Remove npm config
rm -f ./.npmrc
# Set our version number using vars
export IMAGE_VERSION=$(cat platform/viewer/success_version.txt)
export IMAGE_VERSION_FULL=v$IMAGE_VERSION.${CIRCLE_BUILD_NUM}
echo $IMAGE_VERSION
echo $IMAGE_VERSION_FULL
# Build our image, auth, and push
docker build --tag ohif/$IMAGE_NAME:$IMAGE_VERSION_FULL --tag ohif/$IMAGE_NAME:latest .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ohif/$IMAGE_NAME:$IMAGE_VERSION_FULL
docker push ohif/$IMAGE_NAME:latest
fi
workflows:
version: 2
# PULL REQUESTS
PR_CHECKS:
jobs:
- UNIT_TESTS:
@@ -379,82 +378,66 @@ workflows:
requires:
- AWAIT_APPROVAL
# MERGE TO MASTER
RELEASE:
###
# Our workflow for building, deploying, and promoting builds across our
# development, staging, and production environments.
###
DEPLOY:
jobs:
- UNIT_TESTS_PERSIST_WORKSPACE:
- BUILD:
filters:
branches:
only: master
# E2E: PWA + Persist
- cypress/run:
name: 'E2E: PWA'
executor: cypress/browsers-chrome76
browser: chrome
pre-steps:
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
add wait-on' # Use yarn latest
yarn: true
record: false
store_artifacts: true
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build
start: yarn run test:e2e:serve
wait-on: 'http://localhost:3000'
command: 'yarn run test:e2e:ci'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
path: platform/viewer/cypress/screenshots
- store_test_results:
path: cypress/results
- DEPLOY_TO_DEV:
requires:
- UNIT_TESTS_PERSIST_WORKSPACE
# E2E: script-tag
- cypress/run:
name: 'E2E: Script Tag'
executor: cypress/browsers-chrome76
browser: chrome
pre-steps:
- run: 'rm -rf ~/.yarn && npm i -g yarn && yarn -v && yarn global
add wait-on' # Use yarn latest
yarn: true
record: false
store_artifacts: false
working_directory: platform/viewer
build: npx cross-env QUICK_BUILD=true yarn run build:package
start: yarn run test:e2e:serve
spec: 'cypress/integration/common/**/*'
wait-on: 'http://localhost:3000'
cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
no-workspace: true # Don't persist workspace
post-steps:
- store_artifacts:
path: platform/viewer/cypress/screenshots
- store_test_results:
path: cypress/results
- BUILD
- PROMOTE_TO_STAGING:
type: approval
requires:
- UNIT_TESTS_PERSIST_WORKSPACE
# Update NPM
- DEPLOY_TO_DEV
- DEPLOY_TO_STAGING:
requires:
- PROMOTE_TO_STAGING
- PROMOTE_TO_PRODUCTION:
type: approval
requires:
- DEPLOY_TO_STAGING
- DEPLOY_TO_PRODUCTION:
requires:
- PROMOTE_TO_PRODUCTION
###
# Unit and E2E tests have already run for PR_CHECKS
# Re-running should not gain us any confidence here
###
RELEASE:
jobs:
- NPM_PUBLISH:
requires:
- 'E2E: Script Tag'
- 'E2E: PWA'
# Update docs.ohif.org
filters:
branches:
only: master
- DOCS_PUBLISH:
requires:
- 'E2E: Script Tag'
- 'E2E: PWA'
- VIEWER_DEMO_BUILD:
requires:
- 'E2E: Script Tag'
- 'E2E: PWA'
# Update viewer.ohif.org
- VIEWER_DEMO_PUBLISH:
requires:
- VIEWER_DEMO_BUILD
# Update hub.docker.org
filters:
branches:
only: master
- DOCKER_MASTER_PUBLISH:
requires:
- NPM_PUBLISH
#
#
# Potentially use to E2E test STAGING environment
# before publishing to production
# - cypress/run:
# name: 'E2E: PWA'
# executor: deploy-to-prod-executor
# browser: chrome
# working_directory: platform/viewer
# cache-key: 'yarn-packages-{{ checksum "yarn.lock" }}'
# yarn: true
# record: false
# no-workspace: true
# store_artifacts: true
# post-steps:
# - store_artifacts:
# path: platform/viewer/cypress/screenshots
# - store_test_results:
# path: cypress/results
+1 -1
View File
@@ -2,7 +2,7 @@
name: "\U0001F41B Bug report"
about: Create a report to help us improve
title: ''
labels: 'Community: Report :bug:'
labels: 'Community: Report :bug:, Awaiting Reproduction, Triage :white_flag:'
assignees: ''
---
+1 -1
View File
@@ -2,7 +2,7 @@
name: "\U0001F680 Feature request"
about: Suggest an idea for this project
title: ''
labels: 'Community: Request :hand:'
labels: 'Community: Request :hand:, Triage :white_flag:'
assignees: ''
---
@@ -2,7 +2,7 @@
name: "\U0001F917 Support Question"
about: "I have a question \U0001F4AC"
title: ''
labels: 'Community: Question :question:'
labels: 'Community: Question :question:, Triage :white_flag:'
assignees: ''
---
+1 -1
View File
@@ -15,7 +15,7 @@ exemptLabels:
- 'Task: Refactor 🛠'
- 'Task: Tests :microscope:'
- 'PR: Awaiting Review 👀'
- 'Triage 🏳️'
- 'Triage :white_flag:'
- 'Announcement 🎉'
# Label to use when marking an issue as stale
staleLabel: 'Stale :baguette_bread:'
+5
View File
@@ -0,0 +1,5 @@
# Specific to our non-deploy-preview deploys
# Confgure redirects using netlify.toml
# PWA Redirect
/* /index.html 200
+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"
}
}
+2
View File
@@ -1,5 +1,6 @@
# OHIF Viewers
- [Our Process](our-process.md)
- Essentials
- [Getting Started](essentials/getting-started.md)
- [Installation](essentials/installation.md)
@@ -47,5 +48,6 @@
- [FAQ](frequently-asked-questions.md)
- [Contributing](contributing/index.md)
- [Continuous Integration](continous-integration.md)
- [Browser Support](browser-support.md)
- [Help](help.md)
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

+109
View File
@@ -0,0 +1,109 @@
# Continous Integration (CI)
This repository uses `CircleCI` and `Netlify` for continous integration.
## Deploy Previews
[Netlify Deploy previews][deploy-previews] are generated for every pull request.
They allow pull request authors and reviewers to "Preview" the OHIF Viewer as if
the changes had been merged.
Deploy previews can be configured by modifying the `netlify.toml` file in the
root of the repository. Some additional scripts/assets for netlify are included
in the root `.netlify` directory.
## Workflows
[CircleCI Workflows][circleci-workflows] are a set of rules for defining a
collection of jobs and their run order. They are self-documenting and their
configuration can be found in our CircleCI configuration file:
`.circleci/config.yml`.
### Workflow: PR_CHECKS
The PR_CHECKS workflow (Pull Request Checks) runs our automated unit and
end-to-end tests for every code check-in. These tests must all pass before code
can be merged to our `master` branch.
<div style="text-align: center;">
<a href="/assets/img/WORKFLOW_PR_CHECKS.png">
<img src="/assets/img/WORKFLOW_PR_CHECKS.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>Workflow diagram for PR_CHECKS</i></div>
</div>
### Workflow: PR_OPTIONAL_DOCKER_PUBLISH
The PR_OPTIONAL_DOCKER_PUBLISH workflow allows for "manual approval" to publish
the pull request as a tagged docker image. This is helpful when changes need to
be tested with the Google Adapter before merging to `master`.
<div style="text-align: center;">
<a href="/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png">
<img src="/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>Workflow diagram for PR_WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH</i></div>
</div>
> NOTE: This workflow will fail unless it's for a branch on our `upstream`
> repository. If you need this functionality, but the branch is from a fork,
> merge the changes to a short-lived `feature/` branch on `upstream`
### Workflow: DEPLOY
The DEPLOY workflow deploys the OHIF Viewer when changes are merged to master.
It uses the Netlify CLI to deploy assets created as part of the repository's PWA
Build process (`yarn run build`). The workflow allows for "Manual Approval" to
promote the build to `STAGING` and `PRODUCTION` environments.
<div style="text-align: center;">
<a href="/assets/img/WORKFLOW_DEPLOY.png">
<img src="/assets/img/WORKFLOW_DEPLOY.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>Workflow diagram for WORKFLOW_DEPLOY</i></div>
</div>
| Environment | Description | URL |
| ----------- | ---------------------------------------------------------------------------------- | --------------------------------------------- |
| Development | Always reflects latest changes on `master` branch. | [Netlify][netlify-dev] / [OHIF][ohif-dev] |
| Staging | For manual testing before promotion to prod. Keeps development workflow unblocked. | [Netlify][netlify-stage] / [OHIF][ohif-stage] |
| Production | Stable, tested, updated less frequently. | [Netlify][netlify-prod] / [OHIF][ohif-prod] |
### Workflow: RELEASE
The RELEASE workflow publishes our `npm` packages, updated documentation, and
`docker` image when changes are merged to master. `Lerna` and "Semantic Commit
Syntax" are used to independently version and publish the many packages in our
monorepository. If a new version is cut/released, a Docker image is created.
Documentation is generated with `gitbook` and pushed to our `gh-pages` branch.
GitHub hosts the `gh-pages` branch with GitHub Pages.
- Platform Packages: https://github.com/ohif/viewers/#platform
- Extension Packages: https://github.com/ohif/viewers/#extensions
- Documentation: https://docs.ohif.org/
<div style="text-align: center;">
<a href="/assets/img/WORKFLOW_RELEASE.png">
<img src="/assets/img/WORKFLOW_RELEASE.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
</a>
<div><i>Workflow diagram for WORKFLOW_RELEASE</i></div>
</div>
### HOTFIX
_Not yet implemented_
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[deploy-previews]: https://www.netlify.com/blog/2016/07/20/introducing-deploy-previews-in-netlify/
[circleci-workflows]: https://circleci.com/docs/2.0/workflows/
[netlify-dev]: https://ohif-dev.netlify.com
[netlify-stage]: https://ohif-stage.netlify.com
[netlify-prod]: https://ohif-prod.netlify.com
[ohif-dev]: https://viewer-dev.ohif.org
[ohif-stage]: https://viewer-stage.ohif.org
[ohif-prod]: https://viewer-prod.ohif.org
<!-- prettier-ignore-end -->
+2 -2
View File
@@ -7,8 +7,8 @@ like to discuss the changes you intend to make to clarify where or how they
should be implemented, please don't hesitate to create a new issue. At a
minimum, you may want to read the following documentation:
- [Essentials: Getting Started](./essentials/getting-started.md)
- [Advanced: Architecture](./advanced/architecture.md)
- [Essentials: Getting Started](/essentials/getting-started.md)
- [Advanced: Architecture](/advanced/architecture.md)
Pull requests that are:
+109
View File
@@ -0,0 +1,109 @@
# Our Process
Our process is a living, breathing thing. We strive to have regular [retrospectives][retrospective] that help us
shape and adapt our process to our team's current needs. This document attempts to capture the broad strokes of
that process in an effort to:
- Strengthen community member involvement and understanding
- Welcome feedback and helpful suggestions
## Overview
- [Issue Triage](#issue-triage)
- [Issue Curation ("backlog grooming")](#issue-curation-backlog-grooming)
- [Contributions (Pull Requests)](#contributions-pull-requests)
- [Releases](#releases)
_Include issue lifecycle diagram_
## Issue Triage
[GitHub issues][gh-issues] are the best way to provide feedback, ask questions, and suggest changes to the OHIF Viewer's core
team. Community issues generally fall into one of three categories, and are marked with a `triage` label when created.
|Issue Template Name | Description |
|------------------------|-----------------------------------------------------------------------------------------|
|Community: Report 🐛 | Describe a new issue; Provide steps to reproduce; Expected versus actual result? |
|Community: Request ✋ | Describe a proposed new feature. Why should it be implemented? What is the impact/value? |
|Community: Question ❓ | Seek clarification or assistance relevant to the repository. |
_table 1. issue template names and descriptions_
Issues that require `triage` are akin to support tickets. As this is often our first contact with would-be adopters and
contributors, it's important that we strive for timely responses and satisfactory resolutions. We attempt to accomplish this
by:
1. Responding to issues requiring `triage` at least once a week
2. Create new "official issues" from "community issues"
3. Provide clear guidance and next steps (when applicable)
4. Regularly clean up old (stale) issues
> :pencil: Less obviously, patterns in the issues being reported can highlight areas that need improvement. For example, users often have
difficulty navigating CORS issues when deploying the OHIF Viewer -- how do we best reduce our ticket volume for this issue?
### Backlogged Issues
Community issues serve as vehicles of discussion that lead us to "backlogged issues". Backlogged issues are the distilled and actionable information extracted from community issues. They contain the scope and requirements necessary for hand-off to a core-team (or community) contributor ^_^
|Category| Description| Labels|
|--------|-----| ---------|
| Bugs | An issue with steps that produce a bug (an unexpected result). | [Bug: Verified 🐛][label-bug] |
| Stories| A feature/enhancement with a clear benefit, boundaries, and requirements. | [Story 🙌][label-story] |
| Tasks | Changes that improve [UX], [DX], or test coverage; but don't impact application behavior | [Task: CI/Tooling 🤖][label-tooling], [Task: Docs 📖][label-docs], [Task: Refactor 🛠][label-refactor], [Task: Tests 🔬][label-tests] |
_table 2. backlogged issue types ([full list of labels][gh-labels])_
## Issue Curation (["backlog grooming"][groom-backlog])
If a [GitHub issue][gh-issues] has a `bug`, `story`, or `task` label; it's on our backlog. If an issue is on our backlog, it means we are, at the very least, committed to reviewing any community drafted Pull Requests to complete the issue. If you're interested in seeing an issue completed but don't know where to start, please don't hesitate to leave a comment!
While we don't yet have a long-term or quarterly road map, we do regularly add items to our ["Active Development" GitHub Project Board][gh-board]. Items on this project board are either in active development by Core Team members, or queued up for development as in-progress items are completed.
> :pencil: Want to contribute but not sure where to start? Check out [Up for grabs][label-grabs] issues and our [Contributing documentation][contributing-docs]
## Contributions (Pull Requests)
Incoming Pull Requests (PRs) are triaged using the following labels. Code review is performed on all PRs where the bug fix or added functionality is deemed appropriate:
- PR: Bug Fix - Describes a PR filed to address a Bug.
- PR: Draft - Describes a PR filed to gather early feedback from the core team, but which is not intended for merging in the short term.
- Awaiting Response - Describes a PR where the core team is waiting for additional information from the author. This may occur before or after code review.
- Awaiting Review - Describes a PR where the core team has not yet performed a code review.
- Awaiting Revisions - Following code review, this label is applied until the author has made sufficient changes.
We rely on GitHub Checks and integrations with third party services to evaluate changes in code quality and test coverage. Tests must pass before a PR can be merged to master, and code quality and test coverage must not changed by a significant margin. For some repositories, visual screenshot-based tests are also included, and video recordings of end-to-end tests are stored for later review.
[You can read more about our continous integration efforts here](/continous-integration.md)
## Releases
Releases are made automatically based on the type of commits which have been merged (major.minor.patch). Releases are automatically pushed to NPM. Release notes are automatically generated. Users can subscribe to GitHub and NPM releases.
Important announcements are made on GitHub, tagged as Announcement, and pinned so that they remain at the top of the Issue page.
The Core team occasionally performs full manual testing to begin the process of releasing a Stable version. Once testing is complete, the known issues are addressed and a Stable version
<!--
LINKS
-->
<!-- prettier-ignore-start -->
[groom-backlog]: https://www.agilealliance.org/glossary/backlog-grooming
[retrospective]: https://www.atlassian.com/team-playbook/plays/retrospective
[gh-issues]: https://github.com/OHIF/Viewers/issues/new/choose
[gh-labels]: https://github.com/OHIF/Viewers/labels
[label-story]: https://github.com/OHIF/Viewers/labels/Story%20%3Araised_hands%3A
[label-tooling]: https://github.com/OHIF/Viewers/labels/Task%3A%20CI%2FTooling%20%3Arobot%3A
[label-docs]: https://github.com/OHIF/Viewers/labels/Task%3A%20Docs%20%3Abook%3A
[label-refactor]: https://github.com/OHIF/Viewers/labels/Task%3A%20Refactor%20%3Ahammer_and_wrench%3A
[label-tests]: https://github.com/OHIF/Viewers/labels/Task%3A%20Tests%20%3Amicroscope%3A
[label-bug]: https://github.com/OHIF/Viewers/labels/Bug%3A%20Verified%20%3Abug%3A
[gh-board]: https://github.com/OHIF/Viewers/projects/4
[label-grabs]: https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs+%3Araising_hand_woman%3A%22
[contributing-docs]: https://docs.ohif.org/contributing/
<!-- prettier-ignore-end -->
+4 -1
View File
@@ -11,7 +11,10 @@ export default {
* LIFECYCLE HOOKS
*/
preRegistration({ serviceManager, configuration: extensionConfiguration }) {},
preRegistration({
servicesManager,
configuration: extensionConfiguration,
}) {},
/**
* MODULE GETTERS
+19
View File
@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.6.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.5.1...@ohif/extension-cornerstone@1.6.0) (2019-11-19)
### Features
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
## [1.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.5.0...@ohif/extension-cornerstone@1.5.1) (2019-11-15)
**Note:** Version bump only for package @ohif/extension-cornerstone
# [1.5.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@1.4.1...@ohif/extension-cornerstone@1.5.0) (2019-11-13)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-cornerstone",
"version": "1.5.0",
"version": "1.6.0",
"description": "OHIF extension for Cornerstone",
"author": "OHIF",
"license": "MIT",
+2 -2
View File
@@ -24,8 +24,8 @@ export default {
* @param {object} [configuration={}]
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
*/
preRegistration({ serviceManager, configuration = {} }) {
init({ serviceManager, configuration });
preRegistration({ servicesManager, configuration = {} }) {
init({ servicesManager, configuration });
},
getViewportModule() {
return OHIFCornerstoneViewport;
+1 -1
View File
@@ -28,7 +28,7 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
* @param {object} configuration
* @param {Object|Array} configuration.csToolsConfig
*/
export default function init({ serviceManager, configuration = {} }) {
export default function init({ servicesManager, configuration = {} }) {
const { csToolsConfig } = configuration;
const { StackManager } = OHIF.utils;
const metadataProvider = new OHIF.cornerstone.MetadataProvider();
+14 -2
View File
@@ -23,6 +23,17 @@ const TOOLBAR_BUTTON_TYPES = {
BUILT_IN: 'builtIn',
};
const TOOLBAR_BUTTON_BEHAVIORS = {
CINE: 'CINE',
DOWNLOAD_SCREEN_SHOT: 'DOWNLOAD_SCREEN_SHOT',
};
/* TODO: Export enums through a extension manager. */
const enums = {
TOOLBAR_BUTTON_TYPES,
TOOLBAR_BUTTON_BEHAVIORS,
};
const definitions = [
{
id: 'StackScroll',
@@ -102,7 +113,7 @@ const definitions = [
//
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
options: {
behavior: 'CINE',
behavior: TOOLBAR_BUTTON_BEHAVIORS.CINE,
},
},
{
@@ -220,7 +231,8 @@ const definitions = [
//
type: TOOLBAR_BUTTON_TYPES.BUILT_IN,
options: {
behavior: 'DOWNLOAD_SCREEN_SHOT',
behavior: TOOLBAR_BUTTON_BEHAVIORS.DOWNLOAD_SCREEN_SHOT,
togglable: true,
},
},
],
+64
View File
@@ -3,6 +3,70 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.53.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.12...@ohif/extension-vtk@0.53.13) (2019-11-25)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.11...@ohif/extension-vtk@0.53.12) (2019-11-20)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.10...@ohif/extension-vtk@0.53.11) (2019-11-19)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.9...@ohif/extension-vtk@0.53.10) (2019-11-19)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.8...@ohif/extension-vtk@0.53.9) (2019-11-18)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.7...@ohif/extension-vtk@0.53.8) (2019-11-15)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.6...@ohif/extension-vtk@0.53.7) (2019-11-15)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.5...@ohif/extension-vtk@0.53.6) (2019-11-14)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.53.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.53.4...@ohif/extension-vtk@0.53.5) (2019-11-13)
**Note:** Version bump only for package @ohif/extension-vtk
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "0.53.5",
"version": "0.53.13",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -52,8 +52,8 @@
"react-vtkjs-viewport": "^0.3.9"
},
"devDependencies": {
"@ohif/core": "^1.9.0",
"@ohif/ui": "^0.62.0",
"@ohif/core": "^1.12.0",
"@ohif/ui": "^0.64.2",
"cornerstone-tools": "^4.0.9",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
+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.
+41
View File
@@ -3,6 +3,47 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.12.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.11.0...@ohif/core@1.12.0) (2019-11-19)
### Features
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
# [1.11.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.10.0...@ohif/core@1.11.0) (2019-11-19)
### Features
* Issue 879 viewer route query param not filtering but promoting ([#1141](https://github.com/OHIF/Viewers/issues/1141)) ([b17f753](https://github.com/OHIF/Viewers/commit/b17f753e6222045252ef885e40233681541a32e1)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
# [1.10.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.9.1...@ohif/core@1.10.0) (2019-11-15)
### Features
* Inject into Extension Modules / improve tests ([f63d8a7](https://github.com/OHIF/Viewers/commit/f63d8a73d867ad9dfd8ee0cad74edce180eb34f0))
## [1.9.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.9.0...@ohif/core@1.9.1) (2019-11-15)
**Note:** Version bump only for package @ohif/core
# [1.9.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.8.0...@ohif/core@1.9.0) (2019-11-13)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "1.9.0",
"version": "1.12.0",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
@@ -68,7 +68,7 @@ export default class ExtensionManager {
// preRegistrationHook
if (extension.preRegistration) {
extension.preRegistration({
serviceManager: this._servicesManager,
servicesManager: this._servicesManager,
configuration,
});
}
@@ -110,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 -3
View File
@@ -19,7 +19,11 @@ import ui from './ui';
import user from './user.js';
import utils from './utils/';
import { createUiNotificationService } from './services';
import {
createUINotificationService,
createUIModalService,
createUIDialogService,
} from './services';
const OHIF = {
MODULE_TYPES,
@@ -46,7 +50,9 @@ const OHIF = {
measurements,
hangingProtocols,
//
createUiNotificationService,
createUINotificationService,
createUIModalService,
createUIDialogService,
};
export {
@@ -73,7 +79,9 @@ export {
measurements,
hangingProtocols,
//
createUiNotificationService,
createUINotificationService,
createUIModalService,
createUIDialogService,
};
export { OHIF };
+3 -1
View File
@@ -10,7 +10,9 @@ describe('Top level exports', () => {
'HotkeysManager',
'ServicesManager',
//
'createUiNotificationService',
'createUINotificationService',
'createUIModalService',
'createUIDialogService',
//
'utils',
'studies',
+41 -1
View File
@@ -1,9 +1,49 @@
import log from './../log.js';
export default class ServicesManager {
constructor() {
this.services = {};
this.registeredServiceNames = [];
}
register(service) {
/**
*
* @param {Object} service
*/
registerService(service) {
if (!service) {
log.warn(
'Attempting to register a null/undefined service. Exiting early.'
);
return;
}
let serviceName = service.name;
if (!serviceName) {
log.warn(`Service name not set. Exiting early.`);
return;
}
if (this.registeredServiceNames.includes(serviceName)) {
log.warn(
`Extension name ${serviceName} has already been registered. Exiting before duplicating services.`
);
return;
}
this.services[service.name] = service;
// Track service registration
this.registeredServiceNames.push(serviceName);
}
/**
* An array of services.
*
* @param {Object[]} services - Array of services
*/
registerServices(services) {
services.forEach(service => this.registerService(service));
}
}
@@ -0,0 +1,70 @@
import ServicesManager from './ServicesManager.js';
import log from '../log.js';
jest.mock('./../log.js');
describe('ServicesManager.js', () => {
let servicesManager;
beforeEach(() => {
servicesManager = new ServicesManager();
log.warn.mockClear();
jest.clearAllMocks();
});
describe('registerServices()', () => {
it('calls registerService() for each service', () => {
servicesManager.registerService = jest.fn();
const fakeServices = [
{ name: 'UINotificationTestService', hide: jest.fn() },
{ name: 'UIModalTestService', hide: jest.fn() },
];
servicesManager.registerServices(fakeServices);
expect(servicesManager.registerService.mock.calls.length).toBe(2);
});
});
describe('registerService()', () => {
it('logs a warning if the service is null or undefined', () => {
const undefinedService = undefined;
const nullService = null;
servicesManager.registerService(undefinedService);
servicesManager.registerService(nullService);
expect(log.warn.mock.calls.length).toBe(2);
});
it('logs a warning if the service does not have a name', () => {
const serviceWithEmptyName = { name: '', hide: jest.fn() };
const serviceWithoutName = { hide: jest.fn() };
servicesManager.registerService(serviceWithEmptyName);
servicesManager.registerService(serviceWithoutName);
expect(log.warn.mock.calls.length).toBe(2);
});
it('tracks which services have been registered', () => {
const service = {
name: 'UINotificationService',
};
servicesManager.registerService(service);
expect(servicesManager.registeredServiceNames).toContain(service.name);
});
it('logs a warning if the service has an name that has already been registered', () => {
const service = { name: 'UINotificationService' };
servicesManager.registerService(service);
servicesManager.registerService(service);
expect(log.warn.mock.calls.length).toBe(1);
});
});
});
@@ -0,0 +1,125 @@
/**
* A UI Element
*
* @typedef {ReactElement|HTMLElement} DialogContent
*/
/**
* A UI Position
*
* @typedef {Object} ElementPosition
* @property {number} top -
* @property {number} left -
* @property {number} right -
* @property {number} bottom -
*/
/**
* UI Dialog
*
* @typedef {Object} DialogProps
* @property {string} id -
* @property {DialogContent} content -
* @property {boolean} isDraggable -
* @property {ElementPosition} defaultPosition -
* @property {ElementPosition} position -
* @property {Function} onSubmit -
* @property {Function} onClose -
* @property {Function} onStart -
* @property {Function} onStop -
* @property {Function} onDrag -
*/
const uiDialogServicePublicAPI = {
name: 'UIDialogService',
dismiss,
dismissAll,
create,
setServiceImplementation,
};
const uiDialogServiceImplementation = {
_dismiss: () => console.warn('dismiss() NOT IMPLEMENTED'),
_dismissAll: () => console.warn('dismissAll() NOT IMPLEMENTED'),
_create: () => console.warn('create() NOT IMPLEMENTED'),
};
function createUIDialogService() {
return uiDialogServicePublicAPI;
}
/**
* Show a new UI dialog;
*
* @param {DialogProps} props { id, content, onSubmit, onClose, onStart, onDrag, onStop, isDraggable, defaultPosition, position }
*/
function create({
id,
content,
onSubmit,
onClose,
onStart,
onDrag,
onStop,
isDraggable,
defaultPosition,
position,
}) {
return uiDialogServiceImplementation._create({
id,
content,
onSubmit,
onClose,
onStart,
onDrag,
onStop,
isDraggable,
defaultPosition,
position,
});
}
/**
* Destroys all dialogs, if any
*
* @returns void
*/
function dismissAll() {
return uiDialogServiceImplementation._dismissAll();
}
/**
* Destroy the dialog, if currently created
*
* @returns void
*/
function dismiss({ id }) {
return uiDialogServiceImplementation._dismiss({ id });
}
/**
*
*
* @param {*} {
* dismiss: dismissImplementation,
* dismissAll: dismissAllImplementation,
* create: createImplementation,
* }
*/
function setServiceImplementation({
dismiss: dismissImplementation,
dismissAll: dismissAllImplementation,
create: createImplementation,
}) {
if (dismissImplementation) {
uiDialogServiceImplementation._dismiss = dismissImplementation;
}
if (dismissAllImplementation) {
uiDialogServiceImplementation._dismissAll = dismissAllImplementation;
}
if (createImplementation) {
uiDialogServiceImplementation._create = createImplementation;
}
}
export default createUIDialogService;
@@ -0,0 +1,82 @@
/**
* A UI Element
*
* @typedef {ReactElement|HTMLElement} Modal
*/
/**
* UI Modal
*
* @typedef {Object} ModalProps
* @property {boolean} [shouldCloseOnEsc=false] -
* @property {boolean} [isOpen=true] -
* @property {boolean} [closeButton=true] -
* @property {string} [title=null] - 'Modal Title'
* @property {string} [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 { shouldCloseOnEsc, isOpen, closeButton, title, customClassName }
*/
function show(
component,
props = {
shouldCloseOnEsc: false,
isOpen: 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;
@@ -10,7 +10,7 @@
* @property {boolean} [autoClose=true]
*/
const uiNotificationServicePublicApi = {
const uiNotificationServicePublicAPI = {
name: 'UINotificationService',
hide,
show,
@@ -22,8 +22,8 @@ const uiNotificationServiceImplementation = {
_show: () => console.warn('show() NOT IMPLEMENTED'),
};
function createUiNotificationService() {
return uiNotificationServicePublicApi;
function createUINotificationService() {
return uiNotificationServicePublicAPI;
}
/**
@@ -81,4 +81,4 @@ function setServiceImplementation({
}
}
export default createUiNotificationService;
export default createUINotificationService;
+9 -2
View File
@@ -1,4 +1,11 @@
import ServicesManager from './ServicesManager.js';
import createUiNotificationService from './UINotificationService';
import createUINotificationService from './UINotificationService';
import createUIModalService from './UIModalService';
import createUIDialogService from './UIDialogService';
export { createUiNotificationService, ServicesManager };
export {
createUINotificationService,
createUIModalService,
createUIDialogService,
ServicesManager,
};
@@ -40,7 +40,7 @@ export default class RetrieveMetadataLoader {
if (result && result.length) {
break; // closes iterator in case data is retrieved successfully
}
} catch (e) {}
} catch (e) { }
}
if (loaders.next().done && !result) {
@@ -51,8 +51,8 @@ export default class RetrieveMetadataLoader {
}
// Methods to be overwrite
async configLoad() {}
async preLoad() {}
async load(preLoadData) {}
async posLoad(loadData) {}
async configLoad() { }
async preLoad() { }
async load(preLoadData) { }
async posLoad(loadData) { }
}
+413
View File
@@ -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 };
+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.1](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.0...@ohif/i18n@0.52.1) (2019-11-18)
### Bug Fixes
* minor date picker UX improvements ([813ee5e](https://github.com/OHIF/Viewers/commit/813ee5ed4d78b7bda234922d5f3389efe346451c))
# [0.52.0](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.51.0...@ohif/i18n@0.52.0) (2019-11-06)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/i18n",
"version": "0.52.0",
"version": "0.52.1",
"description": "Internationalization library for The OHIF Viewer",
"author": "OHIF",
"license": "MIT",
@@ -1,4 +1,5 @@
{
"Close": "Close",
"Image": "Image",
"Layout": "Layout",
"Measurements": "Measurements",
@@ -0,0 +1,5 @@
{
"Clear dates": "Clear dates",
"End Date": "End Date",
"Start Date": "Start Date"
}
+2
View File
@@ -2,6 +2,7 @@ import AboutModal from './AboutModal.json';
import Buttons from './Buttons.json';
import CineDialog from './CineDialog.json';
import Common from './Common.json';
import DatePicker from './DatePicker.json';
import Header from './Header.json';
import MeasurementTable from './MeasurementTable.json';
import StudyList from './StudyList.json';
@@ -13,6 +14,7 @@ export default {
Buttons,
CineDialog,
Common,
DatePicker,
Header,
MeasurementTable,
StudyList,
@@ -1,4 +1,5 @@
{
"Close": "Fechar",
"Image": "Imagem",
"Layout": "Layout",
"More": "Mais",
@@ -0,0 +1,5 @@
{
"Clear dates": "Limpar datas",
"End Date": "Data Final",
"Start Date": "Data Inicial"
}
+2
View File
@@ -2,6 +2,7 @@ import AboutModal from './AboutModal.json';
import Buttons from './Buttons.json';
import CineDialog from './CineDialog.json';
import Common from './Common.json';
import DatePicker from './DatePicker.json';
import Header from './Header.json';
import UserPreferencesModal from './UserPreferencesModal.json';
@@ -11,6 +12,7 @@ export default {
Buttons,
CineDialog,
Common,
DatePicker,
Header,
UserPreferencesModal,
},
+76
View File
@@ -3,6 +3,82 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.64.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.64.1...@ohif/ui@0.64.2) (2019-11-25)
### Bug Fixes
* Issue branch from danny experimental changes pr 1128 ([#1150](https://github.com/OHIF/Viewers/issues/1150)) ([a870b3c](https://github.com/OHIF/Viewers/commit/a870b3cc6056cf824af422e46f1ad674910b534e)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185)
## [0.64.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.64.0...@ohif/ui@0.64.1) (2019-11-20)
**Note:** Version bump only for package @ohif/ui
# [0.64.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.63.0...@ohif/ui@0.64.0) (2019-11-19)
### Features
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
# [0.63.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.4...@ohif/ui@0.63.0) (2019-11-19)
### Features
* Issue 879 viewer route query param not filtering but promoting ([#1141](https://github.com/OHIF/Viewers/issues/1141)) ([b17f753](https://github.com/OHIF/Viewers/commit/b17f753e6222045252ef885e40233681541a32e1)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
## [0.62.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.3...@ohif/ui@0.62.4) (2019-11-18)
### Bug Fixes
* minor date picker UX improvements ([813ee5e](https://github.com/OHIF/Viewers/commit/813ee5ed4d78b7bda234922d5f3389efe346451c))
## [0.62.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.2...@ohif/ui@0.62.3) (2019-11-15)
**Note:** Version bump only for package @ohif/ui
## [0.62.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.1...@ohif/ui@0.62.2) (2019-11-15)
**Note:** Version bump only for package @ohif/ui
## [0.62.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.62.0...@ohif/ui@0.62.1) (2019-11-14)
**Note:** Version bump only for package @ohif/ui
# [0.62.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.61.0...@ohif/ui@0.62.0) (2019-11-13)
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/ui",
"version": "0.62.0",
"version": "0.64.2",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
@@ -52,7 +52,9 @@
"react-dnd": "9.4.0",
"react-dnd-html5-backend": "^9.4.0",
"react-dnd-touch-backend": "^9.4.0",
"react-draggable": "^4.1.0",
"react-i18next": "^10.11.0",
"react-modal": "^3.11.1",
"react-with-direction": "1.3.0"
},
"devDependencies": {
@@ -1,7 +1,7 @@
import './CineDialog.styl';
import React, { PureComponent } from 'react';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
import { Icon } from './../../elements/Icon';
import PropTypes from 'prop-types';
@@ -58,7 +58,7 @@ const AboutContent = () => {
);
return (
<div data-cy="about-modal">
<div className="AboutContent" data-cy="about-modal">
<div className="btn-group">
<a
className="btn btn-default"
@@ -189,7 +189,7 @@ const ViewportDownloadForm = ({
};
return (
<>
<div className="ViewportDownloadForm">
<div className="title">
{t(
'Please specify the dimensions, filename, and desired type for the output image.'
@@ -284,10 +284,6 @@ const ViewportDownloadForm = ({
className="viewport-preview"
src={viewportPreview.src}
alt="Viewport Preview"
style={{
height: viewportPreview.height,
width: viewportPreview.width,
}}
/>
</div>
@@ -303,7 +299,7 @@ const ViewportDownloadForm = ({
</button>
</div>
</div>
</>
</div>
);
};
@@ -3,15 +3,13 @@
@import '../../../design/styles/common/button.styl'
.ViewportDownloadForm
color: var(--text-secondary-color);
filter: drop-shadow(0 0 3px var(--ui-gray-darkest));
border: none;
border-radius: 8px;
width: inherit;
padding: 15px;
background: transparent;
display: flex;
flex-direction: column;
z-index: 1080 !important;
input, select
max-height: 30px;
.title
margin: 0;
font-weight: bold;
@@ -67,6 +65,7 @@
padding: 10px;
border-radius: 5px;
align-self: center;
margin-bottom: 20px;
@media screen and (max-width: 1023px)
width: 100%;
justify-content: center;
@@ -90,7 +89,6 @@
.actions
display: flex;
height: 60px;
flex-wrap: nowrap;
justify-content: flex-end;
align-items: center;
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import i18n from '@ohif/i18n';
import './LanguageSwitcher.styl';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
const LanguageSwitcher = () => {
const getCurrentLanguage = (language = i18n.language) =>
@@ -1,7 +1,7 @@
import './MeasurementTable.styl';
import React, { Component } from 'react';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
import { Icon } from './../../elements/Icon';
import { MeasurementTableItem } from './MeasurementTableItem.js';
@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
import { Icon } from './../../elements/Icon';
import { OverlayTrigger } from './../overlayTrigger';
@@ -1,60 +1,73 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactBootstrapModal from 'react-bootstrap-modal';
import Modal from 'react-modal';
import classNames from 'classnames';
import './OHIFModal.styl';
const customStyle = {
overlay: {
zIndex: 1071,
backgroundColor: 'rgb(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
};
Modal.setAppElement(document.getElementById('root'));
const OHIFModal = ({
className,
closeButton,
backdrop,
keyboard,
show,
shouldCloseOnEsc,
isOpen,
title,
onHide,
footer: Footer,
header: Header,
children: Component,
}) => (
<ReactBootstrapModal
className={classNames('modal fade themed in', className)}
backdrop={backdrop}
keyboard={keyboard}
show={show}
large={true}
title={title}
onHide={onHide}
>
{(Header || title) && (
<ReactBootstrapModal.Header closeButton={closeButton}>
{title && (
<ReactBootstrapModal.Title>{title}</ReactBootstrapModal.Title>
)}
{Header && <Header hide={onHide} />}
</ReactBootstrapModal.Header>
)}
<ReactBootstrapModal.Body>
{Component && <Component hide={onHide} />}
</ReactBootstrapModal.Body>
{Footer && (
<ReactBootstrapModal.Footer>
{' '}
<Footer hide={onHide} />
</ReactBootstrapModal.Footer>
)}
</ReactBootstrapModal>
);
onClose,
children,
}) => {
const renderHeader = () => {
return (
title && (
<div className="OHIFModal__header">
<h4>{title}</h4>
{closeButton && (
<button data-cy="close-button" onClick={onClose}>
×
</button>
)}
</div>
)
);
};
return (
<Modal
className={classNames('OHIFModal', className)}
shouldCloseOnEsc={shouldCloseOnEsc}
isOpen={isOpen}
title={title}
style={customStyle}
>
<>
{renderHeader()}
<div className="OHIFModal__content">{children}</div>
</>
</Modal>
);
};
OHIFModal.propTypes = {
className: PropTypes.string,
closeButton: PropTypes.bool,
backdrop: PropTypes.bool,
keyboard: PropTypes.bool,
show: PropTypes.bool,
shouldCloseOnEsc: PropTypes.bool,
isOpen: PropTypes.bool,
title: PropTypes.string,
onHide: PropTypes.func,
footer: PropTypes.node,
header: PropTypes.node,
children: PropTypes.node,
onClose: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
};
export default OHIFModal;
@@ -0,0 +1,67 @@
.OHIFModal
background-color: var(--ui-gray-darker)
border-color: var(--ui-border-color)
color: var(--text-secondary-color)
border-radius: 6px
border: 0
color: var(--text-primary-color)
position: relative
-webkit-box-shadow: 0 3px 9px rgba(0,0,0,.5)
box-shadow: 0 3px 9px rgba(0,0,0,.5);
background-clip: padding-box
outline: 0
@media (min-width: 320px)
width: 78%
min-width: 300px
@media (min-width: 768px)
width: 600px
@media (min-width: 992px)
width: 900px
&__content
padding: 20px
max-height: 90vh;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar
display: none;
&__header
display: flex
justify-content: space-between
align-items: center
border-bottom-width: 3px
border-bottom-style: solid
border-bottom-color: #000000
padding: 20px
position: relative
h4
color: var(--text-secondary-color)
font-size: 20px
font-weight: 500
line-height: 24px
padding-right: 24px
margin: 0
button
cursor: pointer
margin: -10px 0 0 0
padding: 0
background-color: transparent
border: none
color: var(--text-secondary-color)
font-size: 25px
font-weight: lighter
&:active,
&:focus,
&:focus:active
background-image: none
outline: 0
box-shadow: none
@@ -3,7 +3,7 @@ import React, { cloneElement } from 'react';
import PropTypes from 'prop-types';
import { Overlay as BaseOverlay } from 'react-overlays';
import elementType from 'prop-types-extra/lib/elementType';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
import Fade from './Fade';
@@ -1,7 +1,7 @@
import React from 'react';
import SnackbarItem from './SnackbarItem';
import './Snackbar.css';
import { useSnackbarContext } from '../../utils/SnackbarProvider';
import { useSnackbarContext } from '../../contextProviders';
const SnackbarContainer = () => {
const { snackbarItems, hide } = useSnackbarContext();
@@ -37,8 +37,8 @@ const SnackbarContainer = () => {
return (
<div key={pos} className={`sb-container sb-${pos}`}>
{items[pos].map(item => (
<div key={item.id}>{renderItem(item)}</div>
{items[pos].map((item, index) => (
<div key={item.id + index}>{renderItem(item)}</div>
))}
</div>
);
@@ -102,14 +102,14 @@ function ImageThumbnail(props) {
{shouldRenderToCanvas() ? (
<canvas ref={canvasRef} width={width} height={height} />
) : (
<img
className="static-image"
src={imageSrc}
//width={this.props.width}
height={height}
alt={''}
/>
)}
<img
className="static-image"
src={imageSrc}
//width={this.props.width}
height={height}
alt={''}
/>
)}
</div>
{loadingOrError}
{showStackLoadingProgressBar && (
@@ -2,22 +2,23 @@
/* Will edit everything selected including everything between a range of dates */
.CalendarDay__selected_span {
background: var(--table-text-secondary-color);
color: #fff;
background: var(--datePicker-picker-background);
color: var(--datePicker-picker-text);
border-color: #e4e7e7;
}
/* Will edit selected date or the endpoints of a range of dates */
.CalendarDay__selected {
background: var(--table-text-secondary-color);
color: #fff;
background: var(--datePicker-picker-background);
color: var(--datePicker-picker-text);
border-color: #e4e7e7;
}
/* Will edit when hovered over. _span style also has this property */
.CalendarDay__selected:hover {
background: var(--table-text-secondary-color);
color: #fff;
.CalendarDay__selected:hover,
.CalendarDay__default:hover {
background: var(--datePicker-picker-background);
color: var(--datePicker-picker-text);
border-color: #e4e7e7;
}
@@ -25,8 +26,8 @@
is not yet selected. Edits the dates between your mouse and said date */
.CalendarDay__hovered_span:hover,
.CalendarDay__hovered_span {
background: var(--table-text-secondary-color);
color: #fff;
background: var(--datePicker-picker-background);
color: var(--datePicker-picker-text);
border-color: #e4e7e7;
}
@@ -42,15 +43,15 @@ is not yet selected. Edits the dates between your mouse and said date */
/* Container - visual */
.DateRangePickerInput {
width: calc(100% - 10px); /* Just use padding? */
background-color: var(--input-background-color);
border-color: var(--input-background-color);
color: var(--input-placeholder-color);
background-color: var(--datePicker-input-background);
border-color: var(--datePicker-input-background);
color: var(--datePicker-picker-text-input);
height: 40px;
}
.DateRangePickerInput.DateRangePickerInput__withBorder {
border-radius: 4px;
background-color: var(--input-background-color);
background-color: var(--datePicker-input-background);
}
/* Input Container */
@@ -58,14 +59,14 @@ is not yet selected. Edits the dates between your mouse and said date */
width: 97px;
height: 38px;
border-radius: 4px;
background-color: var(--input-background-color);
background-color: var(--datePicker-input-background);
}
/* Actual Input Element */
.DateInput > .DateInput_input {
border-color: transparent;
background-color: transparent;
color: var(--input-placeholder-color);
color: var(--datePicker-picker-text-input);
height: 38px;
font-size: 10pt;
padding: 0;
@@ -80,4 +81,39 @@ is not yet selected. Edits the dates between your mouse and said date */
.PresetDateRangePicker_button {
margin: 0;
padding: 4px 8px;
color: #00a699;
background: var(--datePicker-picker-text-input);
}
.PresetDateRangePicker_button:hover {
background: #00a699;
color: var(--datePicker-picker-text-input);
}
.CalendarDay__blocked_out_of_range:hover {
background: #fff;
cursor: not-allowed;
color: var(--datePicker-picker-text-disabled);
}
.DateRangePicker_select {
background-color: white;
color: var(--datePicker-picker-text);
border: 1px solid #e4e7e7;
border-radius: 4px;
display: inline-block;
padding: 3px 20px 3px 10px;
font-size: 14px;
cursor: pointer;
appearance: none;
background-image: linear-gradient(45deg, transparent 50%, gray 50%),
linear-gradient(135deg, gray 50%, transparent 50%);
background-position: calc(100% - 11px) 11px, calc(100% - 6px) calc(11px);
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
}
.DateRangePicker_select:hover {
border-color: #c4c4c4;
}
@@ -7,39 +7,25 @@ import './CustomDateRangePicker.css';
import React from 'react';
import PropTypes from 'prop-types';
import { DateRangePicker } from 'react-dates';
import moment from 'moment';
import i18n from '@ohif/i18n';
import { useTranslation } from 'react-i18next';
export default class CustomDateRangePicker extends React.Component {
static propTypes = {
presets: PropTypes.arrayOf(
PropTypes.shape({
text: PropTypes.string,
start: PropTypes.required,
end: PropTypes.required,
})
),
};
function CustomDateRangePicker(props) {
moment.locale(i18n.language); // using i18n in the date picker
constructor(props) {
super(props);
this.state = {
startDate: this.props.startDate,
endDate: this.props.endDate,
};
const { t } = useTranslation('DatePicker');
this.renderDatePresets = this.renderDatePresets.bind(this);
this.onDatesChange = this.onDatesChange.bind(this);
}
const {
onDatesChange,
startDate,
endDate,
presets,
...dateRangePickerProps
} = props;
onDatesChange({ startDate, endDate, preset }) {
this.setState({ startDate, endDate });
if (this.props.onDatesChange) {
this.props.onDatesChange({ startDate, endDate, preset });
}
}
renderDatePresets() {
const { presets } = this.props;
const { startDate, endDate } = this.state;
const renderDatePresets = () => {
const { presets } = props;
return (
<div className="PresetDateRangePicker_panel">
@@ -54,7 +40,7 @@ export default class CustomDateRangePicker extends React.Component {
isSelected ? 'PresetDateRangePicker_button__selected' : ''
}`}
onClick={() =>
this.onDatesChange({
onDatesChange({
startDate: start,
endDate: end,
preset: true,
@@ -67,30 +53,89 @@ export default class CustomDateRangePicker extends React.Component {
})}
</div>
);
}
};
const renderMonthElement = ({ month, onMonthSelect, onYearSelect }) => {
const containerStyle = {
margin: '0 5px',
};
render() {
let {
autoFocus,
autoFocusEndDate,
initialStartDate,
initialEndDate,
stateDateWrapper,
onDatesChange,
startDate,
endDate,
presets,
...dateRangePickerProps
} = this.props;
const renderYearsOptions = () => {
const yearsRange = 20;
const options = [];
for (let i = 0; i < yearsRange; i++) {
const year = moment().year() - i;
options.push(<option value={year}>{year}</option>);
}
return options;
};
renderMonthElement.propTypes = {
onMonthSelect: PropTypes.func,
onYearSelect: PropTypes.func,
};
return (
<DateRangePicker
{...dateRangePickerProps}
startDate={this.state.startDate}
endDate={this.state.endDate}
renderCalendarInfo={this.renderDatePresets}
onDatesChange={this.onDatesChange}
/>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<div style={containerStyle}>
<select
className="DateRangePicker_select"
value={month.month()}
onChange={e => onMonthSelect(month, e.target.value)}
>
{moment.months().map((label, value) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
<div style={containerStyle}>
{}
<select
className="DateRangePicker_select"
value={month.year()}
onChange={e => onYearSelect(month, e.target.value)}
>
{renderYearsOptions()}
</select>
</div>
</div>
);
}
};
return (
<DateRangePicker
{...dateRangePickerProps}
startDate={startDate}
endDate={endDate}
renderCalendarInfo={renderDatePresets}
onDatesChange={onDatesChange}
renderMonthElement={renderMonthElement}
startDatePlaceholderText={t('Start Date')}
endDatePlaceholderText={t('End Date')}
phrases={{
closeDatePicker: t('Common:Close'),
clearDates: t('Clear dates'),
}}
/>
);
}
CustomDateRangePicker.propTypes = {
presets: PropTypes.arrayOf(
PropTypes.shape({
text: PropTypes.string,
start: PropTypes.required,
end: PropTypes.required,
})
),
autoFocus: PropTypes.bool.isRequired,
onDatesChange: PropTypes.func.isRequired,
startDate: PropTypes.instanceOf(Date),
endDate: PropTypes.instanceOf(Date),
month: PropTypes.instanceOf(Date),
};
export default CustomDateRangePicker;
@@ -3,11 +3,22 @@ import './StudyList.styl';
import React from 'react';
import classNames from 'classnames';
import TableSearchFilter from './TableSearchFilter.js';
import useMedia from '../../hooks/useMedia.js';
import PropTypes from 'prop-types';
import { StudyListLoadingText } from './StudyListLoadingText.js';
import { withTranslation } from '../../utils/LanguageProvider';
import { useTranslation } from 'react-i18next';
const getContentFromUseMediaValue = (
displaySize,
contentArrayMap,
defaultContent
) => {
const content =
displaySize in contentArrayMap
? contentArrayMap[displaySize]
: defaultContent;
return content;
};
/**
*
*
@@ -24,10 +35,10 @@ function StudyList(props) {
filterValues,
onFilterChange: handleFilterChange,
onSelectItem: handleSelectItem,
t,
//
studyListDateFilterNumDays,
displaySize,
} = props;
const { t, ready: translationsAreReady } = useTranslation('StudyList');
const largeTableMeta = [
{
@@ -70,13 +81,13 @@ function StudyList(props) {
const mediumTableMeta = [
{
displayText: 'Patient / MRN',
displayText: `${t('Patient')} / ${t('MRN')}`,
fieldName: 'patientNameOrId',
inputType: 'text',
size: 250,
},
{
displayText: 'Description',
displayText: t('Description'),
fieldName: 'accessionOrModalityOrDescription',
inputType: 'text',
size: 350,
@@ -91,16 +102,16 @@ function StudyList(props) {
const smallTableMeta = [
{
displayText: 'Search',
displayText: t('Search'),
fieldName: 'allFields',
inputType: 'text',
size: 100,
},
];
const tableMeta = useMedia(
['(min-width: 1750px)', '(min-width: 1000px)', '(min-width: 768px)'],
[largeTableMeta, mediumTableMeta, smallTableMeta],
const tableMeta = getContentFromUseMediaValue(
displaySize,
{ large: largeTableMeta, medium: mediumTableMeta, small: smallTableMeta },
smallTableMeta
);
@@ -108,7 +119,7 @@ function StudyList(props) {
.map(field => field.size)
.reduce((prev, next) => prev + next);
return (
return translationsAreReady ? (
<table className="table table--striped table--hoverable">
<colgroup>
{tableMeta.map((field, i) => {
@@ -177,11 +188,12 @@ function StudyList(props) {
studyDescription={study.studyDescription || ''}
studyInstanceUid={study.studyInstanceUid}
t={t}
displaySize={displaySize}
/>
))}
</tbody>
</table>
);
) : null;
}
StudyList.propTypes = {
@@ -206,10 +218,12 @@ StudyList.propTypes = {
patientNameOrId: PropTypes.string.isRequired,
accessionOrModalityOrDescription: PropTypes.string.isRequired,
allFields: PropTypes.string.isRequired,
studyDateTo: PropTypes.any,
studyDateFrom: PropTypes.any,
}).isRequired,
onFilterChange: PropTypes.func.isRequired,
//
studyListDateFilterNumDays: PropTypes.number,
displaySize: PropTypes.string,
};
StudyList.defaultProps = {};
@@ -226,6 +240,7 @@ function TableRow(props) {
studyInstanceUid,
onClick: handleClick,
t,
displaySize,
} = props;
const largeRowTemplate = (
@@ -362,9 +377,13 @@ function TableRow(props) {
</tr>
);
const rowTemplate = useMedia(
['(min-width: 1750px)', '(min-width: 1000px)', '(min-width: 768px)'],
[largeRowTemplate, mediumRowTemplate, smallRowTemplate],
const rowTemplate = getContentFromUseMediaValue(
displaySize,
{
large: largeRowTemplate,
medium: mediumRowTemplate,
small: smallRowTemplate,
},
smallRowTemplate
);
@@ -380,11 +399,11 @@ TableRow.propTypes = {
studyDate: PropTypes.string.isRequired,
studyDescription: PropTypes.string.isRequired,
studyInstanceUid: PropTypes.string.isRequired,
displaySize: PropTypes.string,
};
TableRow.defaultProps = {
isHighlighted: false,
};
const connectedComponent = withTranslation('StudyList')(StudyList);
export { connectedComponent as StudyList };
export { StudyList };
@@ -176,7 +176,7 @@ table.table
margin: 0 auto;
color: var(--table-text-primary-color);
font-weight: 400;
padding: 20px;
padding: 20px 8px;
user-select: none;
font-size: 15px;
@@ -1,7 +1,7 @@
import React from 'react';
import { Icon } from './../../elements/Icon';
// TODO: useTranslation
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
function StudyListLoadingText({ t: translate }) {
return (
@@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import './PaginationArea.styl';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
class TablePagination extends PureComponent {
static defaultProps = {
@@ -6,7 +6,21 @@ import CustomDateRangePicker from './CustomDateRangePicker.js';
import { Icon } from './../../elements/Icon';
import { useTranslation } from 'react-i18next';
const getDateEntry = (datePicked, rangeDatePicked) => {
return rangeDatePicked || datePicked || null;
};
const getDateEntryFromRange = (today, numOfDays, edge = 'start') => {
if (typeof numOfDays !== 'number') {
return;
}
if (edge === 'end') {
return today;
} else {
today.subtract(numOfDays, 'days');
}
};
function TableSearchFilter(props) {
const {
@@ -17,20 +31,32 @@ function TableSearchFilter(props) {
sortFieldName,
sortDirection,
// TODO: Rename
studyListDateFilterNumDays
studyListDateFilterNumDays,
} = props;
const { studyDateTo, studyDateFrom } = values || {};
const [focusedInput, setFocusedInput] = useState(null);
const [t] = useTranslation(); // 'Common'?
const { t, ready: translationsAreReady } = useTranslation('Common');
const sortIcons = ['sort', 'sort-up', 'sort-down'];
const sortIconForSortField =
sortDirection === 'asc' ? sortIcons[1] : sortIcons[2];
//
const today = moment();
const lastWeek = moment().subtract(7, 'day');
const lastMonth = moment().subtract(1, 'month');
const defaultStartDate = moment().subtract(studyListDateFilterNumDays, 'days');
const defaultEndDate = today;
const defaultStartDate = getDateEntryFromRange(
today,
studyListDateFilterNumDays,
'start'
);
const defaultEndDate = getDateEntryFromRange(
today,
studyListDateFilterNumDays,
'end'
);
const studyDatePresets = [
{
text: t('Today'),
@@ -49,56 +75,59 @@ function TableSearchFilter(props) {
},
];
return meta.map((field, i) => {
const { displayText, fieldName, inputType } = field;
const isSortField = sortFieldName === fieldName;
const sortIcon = isSortField ? sortIconForSortField : sortIcons[0];
return translationsAreReady
? meta.map((field, i) => {
const { displayText, fieldName, inputType } = field;
const isSortField = sortFieldName === fieldName;
const sortIcon = isSortField ? sortIconForSortField : sortIcons[0];
return (
<th key={`${fieldName}-${i}`}>
<label
htmlFor={`filter-${fieldName}`}
onClick={() => onSort(fieldName)}
>
{`${displayText} `}
<Icon name={sortIcon} style={{ fontSize: '12px' }} />
</label>
{inputType === 'text' && (
<input
type="text"
id={`filter-${fieldName}`}
className="form-control studylist-search"
value={values[fieldName]}
onChange={e => onValueChange(fieldName, e.target.value)}
/>
)}
{inputType === 'date-range' && (
// https://github.com/airbnb/react-dates
<CustomDateRangePicker
// Required
startDate={studyListDateFilterNumDays ? defaultStartDate : null}
startDateId="start-date"
endDate={studyListDateFilterNumDays ? defaultEndDate : null}
endDateId="end-date"
// TODO: We need a dynamic way to determine which fields values to update
onDatesChange={({ startDate, endDate, preset = false }) => {
onValueChange('studyDateTo', startDate);
onValueChange('studyDateFrom', endDate);
}}
focusedInput={focusedInput}
onFocusChange={updatedVal => setFocusedInput(updatedVal)}
// Optional
numberOfMonths={1} // For med and small screens? 2 for large?
showClearDates={true}
anchorDirection="left"
presets={studyDatePresets}
hideKeyboardShortcutsPanel={true}
isOutsideRange={day => !isInclusivelyBeforeDay(day, moment())}
/>
)}
</th>
);
});
return (
<th key={`${fieldName}-${i}`}>
<label
htmlFor={`filter-${fieldName}`}
onClick={() => onSort(fieldName)}
>
{`${displayText}`}
<Icon name={sortIcon} style={{ fontSize: '12px' }} />
</label>
{inputType === 'text' && (
<input
type="text"
id={`filter-${fieldName}`}
className="form-control studylist-search"
value={values[fieldName]}
onChange={e => onValueChange(fieldName, e.target.value)}
/>
)}
{inputType === 'date-range' && (
// https://github.com/airbnb/react-dates
<CustomDateRangePicker
// Required
startDate={getDateEntry(studyDateTo, defaultStartDate)}
startDateId="start-date"
endDate={getDateEntry(studyDateFrom, defaultEndDate)}
endDateId="end-date"
autoFocus={false}
// TODO: We need a dynamic way to determine which fields values to update
onDatesChange={({ startDate, endDate, preset = false }) => {
onValueChange('studyDateTo', startDate);
onValueChange('studyDateFrom', endDate);
}}
focusedInput={focusedInput}
onFocusChange={updatedVal => setFocusedInput(updatedVal)}
// Optional
numberOfMonths={1} // For med and small screens? 2 for large?
showClearDates={true}
anchorDirection="left"
presets={studyDatePresets}
hideKeyboardShortcutsPanel={true}
isOutsideRange={day => !isInclusivelyBeforeDay(day, moment())}
/>
)}
</th>
);
})
: null;
}
TableSearchFilter.propTypes = {
@@ -10,7 +10,13 @@ import PropTypes from 'prop-types';
export class HotKeysPreferences extends Component {
static propTypes = {
hotkeyDefinitions: PropTypes.array.isRequired,
hotkeyDefinitions: PropTypes.arrayOf(
PropTypes.shape({
commandName: PropTypes.string,
keys: PropTypes.arrayOf(PropTypes.string),
label: PropTypes.string,
})
).isRequired,
};
constructor(props) {
@@ -76,7 +82,7 @@ export class HotKeysPreferences extends Component {
specialKeyName ||
keyDownEvent.key ||
String.fromCharCode(keyDownEvent.keyCode);
pressedKeys.push(keyName);
pressedKeys.push(keyName.toLowerCase());
}
this.updateHotKeysState(commandName, pressedKeys.join('+'));
@@ -123,7 +129,7 @@ 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];
const lastPressedKey = pressedKeys[pressedKeys.length - 1].toLowerCase();
// clear the prior errors
this.setState({ errorMessages: {} }, () => {
@@ -156,7 +162,8 @@ export class HotKeysPreferences extends Component {
*/
const modifierCommand = pressedKeys
.slice(0, pressedKeys.length - 1)
.join('+');
.join('+')
.toLowerCase();
const disallowedCombination = disallowedCombinations[modifierCommand];
const hasDisallowedCombinations = disallowedCombination
@@ -215,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">
@@ -250,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>
);
}
}
@@ -1,7 +1,5 @@
.HotKeysPreferences
display: flex;
margin-right: -15px;
margin-left: -15px;
.column
width: 50%;
@@ -15,7 +15,13 @@ export class UserPreferences extends Component {
// TODO: Make this more generic. Tabs should not be restricted to these entries
static propTypes = {
hotkeyDefinitions: PropTypes.array.isRequired,
hotkeyDefinitions: PropTypes.arrayOf(
PropTypes.shape({
commandName: PropTypes.string,
keys: PropTypes.arrayOf(PropTypes.string),
label: PropTypes.string,
})
).isRequired,
windowLevelData: PropTypes.object.isRequired,
generalData: PropTypes.object.isRequired,
};
@@ -84,8 +90,8 @@ export class UserPreferences extends Component {
render() {
return (
<div>
<div className="dialog-separator-after">
<div className="UserPreferences">
<div className="UserPreferences__selector">
<ul className="nav nav-tabs">
<li
onClick={() => {
@@ -3,23 +3,27 @@
@import './../../design/styles/common/state.styl'
@import './../../design/styles/common/global.styl'
.modal-body
overflow: hidden
.UserPreferences
display: flex
flex-direction: column
.errorMessage
color: var(--state-error-text)
font-size: 10px
text-transform: uppercase;
&__selector
border-bottom: 3px solid black
.form-content
border-bottom: 3px solid var(--primary-background-color)
margin-bottom: 20px
margin-left: -22px
margin-right: -22px
max-height: 70vh
overflow-y: auto
padding: 22px
min-height: 500px
.errorMessage
color: var(--state-error-text)
font-size: 10px
text-transform: uppercase;
.popover
width: 300px
.form-content
border-bottom: 3px solid var(--primary-background-color)
margin-bottom: 20px
margin-left: -20px
margin-right: -20px
max-height: 70vh
overflow-y: auto
padding: 20px
min-height: 500px
.popover
width: 300px
@@ -2,7 +2,7 @@ import './UserPreferencesForm.styl';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withTranslation } from '../../utils/LanguageProvider';
import { withTranslation } from '../../contextProviders';
import cloneDeep from 'lodash.clonedeep';
import isEqual from 'lodash.isequal';
@@ -15,7 +15,13 @@ class UserPreferencesForm extends Component {
onSave: PropTypes.func,
onResetToDefaults: PropTypes.func,
windowLevelData: PropTypes.object,
hotkeyDefinitions: PropTypes.array,
hotkeyDefinitions: PropTypes.arrayOf(
PropTypes.shape({
commandName: PropTypes.string,
keys: PropTypes.arrayOf(PropTypes.string),
label: PropTypes.string,
})
).isRequired,
t: PropTypes.func,
};
@@ -61,15 +67,24 @@ class UserPreferencesForm extends Component {
<div className="footer">
<button
className="btn btn-danger pull-left"
data-cy="reset-default-btn"
onClick={this.props.onResetToDefaults}
>
{this.props.t('Reset to Defaults')}
</button>
<div>
<div onClick={this.props.onClose} className="btn btn-default">
<div
onClick={this.props.onClose}
className="btn btn-default"
data-cy="cancel-btn"
>
{this.props.t('Cancel')}
</div>
<button className="btn btn-primary" onClick={this.save}>
<button
className="btn btn-primary"
data-cy="save-btn"
onClick={this.save}
>
{this.props.t('Save')}
</button>
</div>
@@ -16,7 +16,6 @@
.footer
display: flex
flex-direction: row
padding-bottom: 20px
justify-content: space-between
div
@@ -0,0 +1,208 @@
import React, {
useState,
createContext,
useContext,
useCallback,
useEffect,
} from 'react';
import PropTypes from 'prop-types';
import Draggable from 'react-draggable';
import classNames from 'classnames';
import { utils } from '@ohif/core';
import './DialogProvider.styl';
const DialogContext = createContext(null);
export const useDialog = () => useContext(DialogContext);
const DialogProvider = ({ children, service }) => {
const [isDragging, setIsDragging] = useState(false);
const [dialogs, setDialogs] = useState([]);
const [lastDialogPosition, setLastDialogPosition] = useState(null);
/**
* Sets the implementation of a dialog service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
if (service) {
service.setServiceImplementation({ create, dismiss, dismissAll });
}
}, [create, dismiss, service]);
/**
* Creates a dialog and return its id.
*
* @returns id
*/
const create = useCallback(
({
id,
content,
onSubmit,
onClose,
onDrag,
onStop,
isDraggable,
defaultPosition,
position,
}) => {
let dialogId = id;
if (!dialogId) {
dialogId = utils.guid();
}
const newDialog = {
id: dialogId,
content,
onSubmit,
onClose,
onDrag,
onStop,
isDraggable,
defaultPosition,
position,
};
setDialogs(dialogs => [...dialogs, newDialog]);
return dialogId;
},
[]
);
/**
* Dismisses the dialog with a given id.
*
* @returns void
*/
const dismiss = useCallback(({ id }) => {
setDialogs(dialogs => dialogs.filter(dialog => dialog.id !== id));
}, []);
/**
* Dismisses all dialogs.
*
* @returns void
*/
const dismissAll = () => {
setDialogs([]);
};
/**
* Moves the dialog to the foreground if clicked.
*
* @returns void
*/
const _reorder = id => {
setDialogs(dialogs => [
...dialogs.filter(dialog => dialog.id !== id),
dialogs.find(dialog => dialog.id === id),
]);
};
const _updateLastDialogPosition = dialogId => {
const draggableItemBounds = document
.querySelector(`#draggableItem-${dialogId}`)
.getBoundingClientRect();
setLastDialogPosition({
x: draggableItemBounds.x,
y: draggableItemBounds.y,
});
};
return (
<DialogContext.Provider value={{ create, dismiss, dismissAll, dialogs }}>
<div className="DraggableArea">
{dialogs.map(dialog => {
const {
id,
content: Dialog,
position /* Position of the dialog. {{x: 0, y: 0}} */,
defaultPosition,
isDraggable = true,
onStart = () => {},
onStop = () => {},
onDrag = () => {},
} = dialog;
return (
<Draggable
key={id}
disabled={!isDraggable}
position={position}
defaultPosition={lastDialogPosition || defaultPosition}
bounds="parent"
onStart={event => {
const e = event || window.event;
const target = e.target || e.srcElement;
const BLACKLIST = ['SVG', 'BUTTON', 'PATH', 'INPUT'];
if (BLACKLIST.includes(target.tagName.toUpperCase())) {
return false;
}
onStart(event);
}}
onStop={event => {
onStop(event);
setIsDragging(false);
return;
}}
onDrag={event => {
setIsDragging(true);
_reorder(id);
_updateLastDialogPosition(id);
onDrag(event);
}}
>
<div
id={`draggableItem-${id}`}
className={classNames(
'DraggableItem',
isDragging && 'dragging'
)}
style={{ zIndex: '999', position: 'absolute' }}
onClick={() => _reorder(id)}
>
<Dialog {...dialog} />
</div>
</Draggable>
);
})}
</div>
{children}
</DialogContext.Provider>
);
};
DialogProvider.defaultProps = {
service: null,
};
DialogProvider.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 dialog methods through a Class Component
*
*/
export const withDialog = Component => {
return function WrappedComponent(props) {
const { create, dismiss, dismissAll } = useDialog();
return <Component {...props} dialog={{ create, dismiss, dismissAll }} />;
};
};
export default DialogProvider;
@@ -0,0 +1,12 @@
.DraggableItem
div
cursor: grab !important
.DraggableItem.dragging
div
cursor: grabbing !important
.DraggableArea
width: 100vw
height: 100vh
position: absolute
@@ -0,0 +1,116 @@
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. */,
shouldCloseOnEsc: false /* Modal is dismissible via the esc key. */,
isOpen: 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: '' /* 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
)}
shouldCloseOnEsc={options.keyboard}
isOpen={options.isOpen}
title={options.title}
closeButton={options.closeButton}
onClose={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;
@@ -5,6 +5,8 @@ import React, {
useCallback,
useEffect,
} from 'react';
import PropTypes from 'prop-types';
import SnackbarContainer from '../components/snackbar/SnackbarContainer';
import SnackbarTypes from '../components/snackbar/SnackbarTypes';
@@ -25,49 +27,62 @@ const SnackbarProvider = ({ children, service }) => {
const [count, setCount] = useState(1);
const [snackbarItems, setSnackbarItems] = useState([]);
/**
* Sets the implementation of a notification service that can be used by extensions.
*
* @returns void
*/
useEffect(() => {
service.setServiceImplementation({ hide, show });
if (service) {
service.setServiceImplementation({ hide, show });
}
}, [service, hide, show]);
const show = useCallback(options => {
if (!options || (!options.title && !options.message)) {
console.warn(
'Snackbar cannot be rendered without required parameters: title | message'
);
const show = useCallback(
options => {
if (!options || (!options.title && !options.message)) {
console.warn(
'Snackbar cannot be rendered without required parameters: title | message'
);
return null;
}
return null;
}
const newItem = {
...DEFAULT_OPTIONS,
...options,
id: count,
visible: true,
};
const newItem = {
...DEFAULT_OPTIONS,
...options,
id: count,
visible: true,
};
setSnackbarItems(state => [...state, newItem]);
setCount(count + 1);
});
setSnackbarItems(state => [...state, newItem]);
setCount(count + 1);
},
[count, DEFAULT_OPTIONS]
);
const hide = useCallback(id => {
const hideItem = items => {
const newItems = items.map(item => {
if (item.id === id) {
item.visible = false;
}
const hide = useCallback(
id => {
const hideItem = items => {
const newItems = items.map(item => {
if (item.id === id) {
item.visible = false;
}
return item;
});
return item;
});
return newItems;
};
return newItems;
};
setSnackbarItems(state => hideItem(state));
setSnackbarItems(state => hideItem(state));
setTimeout(() => {
setSnackbarItems(state => [...state.filter(item => item.id !== id)]);
}, 1000);
});
setTimeout(() => {
setSnackbarItems(state => [...state.filter(item => item.id !== id)]);
}, 1000);
},
[setSnackbarItems]
);
const hideAll = () => {
// reset count
@@ -95,6 +110,21 @@ const SnackbarProvider = ({ children, service }) => {
);
};
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
+20
View File
@@ -0,0 +1,20 @@
export {
default as ModalProvider,
useModal,
withModal,
ModalConsumer,
} from './ModalProvider.js';
export {
default as SnackbarProvider,
useSnackbarContext,
withSnackbar,
} from './SnackbarProvider.js';
export {
default as LanguageProvider,
withTranslation,
} from './LanguageProvider.js';
export {
default as DialogProvider,
withDialog,
useDialog,
} from './DialogProvider.js';
+1 -1
View File
@@ -1,4 +1,4 @@
import useMedia from './useMedia.js';
import { useMedia } from './useMedia.js';
import useDebounce from './useDebounce.js';
export { useDebounce, useMedia };
+165 -41
View File
@@ -1,54 +1,178 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
import isEqual from 'lodash.isequal';
/**
* Get display size value for matched mediaQueryList
* @param {MediaQueryList[]} mediaQueryMap - Array of mappings, containing MediaQueryLists
* @param {Array} mediaTypesAliases - Array of strings representing each mediaQueryAlias.
* @param {string} defaultDisplaySize - default display size value. Fallback value.
*/
const getDisplaySize = (
mediaQueryMap,
mediaTypesAliases,
defaultDisplaySize
) => {
if ((!mediaTypesAliases && !defaultDisplaySize) || !mediaQueryMap) {
return;
}
// Get index of first media query that matches
const index = mediaQueryMap.findIndex(mql => mql.matches);
// Return related value or defaultDisplaySize if none
return index >= 0 && typeof mediaTypesAliases[index] !== 'undefined'
? mediaTypesAliases[index]
: defaultDisplaySize;
};
/**
* Map each window MediaQueryLists
* @param {Array} mediaQueriesStringList - array of string media queries to be parsed
*/
const getMediaQueryMap = mediaQueriesStringList => {
return (
mediaQueriesStringList &&
mediaQueriesStringList.map(q => window.matchMedia(q))
);
};
const getMediaTypeAlias = (mediaQuery, state) => {
const { media } = mediaQuery;
const { mediaQueriesStringList, mediaTypesAliases } = state;
const index = mediaQueriesStringList.findIndex(originalMediaQuery => {
const { media: toCompareMedia } = window.matchMedia(originalMediaQuery);
return toCompareMedia === media;
});
return mediaTypesAliases[index];
};
/**
* Hook to get current displaySize value.
*
* Its state changes and also displaySize value changes in case viewport is resized.
* Its state changes in case mediaQueriesStringList or mediaTypesAliases changes.
*
* Current hook only offers displayMedia size, it wont expose method to change its state.
* @param {Array} mediaQueriesStringList - array of string media queries to be parsed
* @param {Array} mediaTypesAliases - array of aliases. Each value represents one mediaQueryList from array mediaQueriesStringList
* @param {String} defaultMediaType - default mediaTypeAlias
* @returns {String} current displayMedia size based on viewport size.
*
* @example <caption>Example to getDisplayMedia Size based on viewport size</caption>
*
* const displaySize = useMedia(
* ['(min-width: 1500px)', '(min-width: 1000px)', '(min-width: 600px)'],
* // Value to return for matched media query
* ['large', 'medium', 'small'],
* // Default value
* 'medium');
*
* const currentDisplaySize = useMedia();
*
* @example <caption></caption>
* const currentViewportSize = useMedia(
* // Media queries
* ['(min-width: 1500px)', '(min-width: 1000px)', '(min-width: 600px)'],
* // Value to return for matched media query
* ['large', 'medium', 'small'],
* // Default value
* 'medium'
* );
* @param {string[]} queries
* @param {*} values
* @param {*} defaultValue
* @returns
*/
function useMedia(queries, values, defaultValue) {
// Array containing a media query list for each query
const mediaQueryLists = queries.map(q => window.matchMedia(q));
const useMedia = (
mediaQueriesStringList,
mediaTypesAliases,
defaultMediaType
) => {
// MediaQuery.state is the source of truth. This hook will be dependent on it.
const [state, setState] = useState(() => {
const _mediaQueryMap = getMediaQueryMap(mediaQueriesStringList);
const _displaySize = getDisplaySize(
_mediaQueryMap,
mediaTypesAliases,
defaultMediaType
);
// Function that gets value based on matching media query
const getValue = () => {
// Get index of first media query that matches
const index = mediaQueryLists.findIndex(mql => mql.matches);
return {
mediaQueryMap: _mediaQueryMap,
displaySize: _displaySize,
mediaQueriesStringList,
mediaTypesAliases,
defaultMediaType,
};
});
let mount = useRef(false);
// Return related value or defaultValue if none
return typeof values[index] !== 'undefined' ? values[index] : defaultValue;
const updateDisplaySize = displaySize => {
if (mount.current) {
setState({ ...state, displaySize });
}
};
// State and setter for matched value
const [value, setValue] = useState(getValue);
const updateState = value => {
const {
mediaQueriesStringList,
mediaTypesAliases,
defaultMediaType,
} = value;
useEffect(
() => {
// Event listener callback
// Note: By defining getValue outside of useEffect we ensure that it has ...
// ... current values of hook args (as this hook callback is created once on mount).
const handler = () => setValue(getValue);
const mediaQueryMap = getMediaQueryMap(mediaQueriesStringList);
const displaySize = getDisplaySize(
mediaQueryMap,
mediaTypesAliases,
defaultMediaType
);
// immutable state
// last chance to avoid setState of unmount component
if (mount.current) {
setState({
...state,
mediaQueriesStringList,
mediaTypesAliases,
displaySize,
mediaQueryMap,
});
}
};
// Set a listener for each media query with above handler as callback.
mediaQueryLists.forEach(mql => mql.addListener(handler));
const onMediaQueryChange = useCallback(mediaQuery => {
if (mediaQuery.matches) {
const nextDisplaySize = getMediaTypeAlias(mediaQuery, state);
updateDisplaySize(nextDisplaySize);
}
}, []);
// Remove listeners on cleanup
return () => mediaQueryLists.forEach(mql => mql.removeListener(handler));
},
[] // Empty array ensures effect is only run on mount and unmount
);
// update state of MediaQuery in case mediaQueriesStringList or mediaTypesAliases has changed
useEffect(() => {
const {
mediaQueriesStringList: _mediaQueriesStringList,
mediaTypesAliases: _mediaTypesAliases,
} = state;
if (
(mediaQueriesStringList &&
!isEqual(mediaQueriesStringList, _mediaQueriesStringList)) ||
(mediaTypesAliases && !isEqual(mediaTypesAliases, _mediaTypesAliases))
) {
updateState({
mediaQueriesStringList,
mediaTypesAliases,
});
}
}, [mediaQueriesStringList, mediaTypesAliases]);
return value;
}
// re-assign window resizing listeners
useEffect(() => {
const { mediaQueryMap } = state;
mediaQueryMap.forEach(mql => {
mql.removeListener(onMediaQueryChange);
mql.addListener(onMediaQueryChange);
});
}, [state.mediaQueryMap]);
export default useMedia;
useEffect(() => {
mount.current = true;
return () => {
mount.current = false;
const { mediaQueryMap } = state;
mediaQueryMap.forEach(mql => {
mql.removeListener(onMediaQueryChange);
});
};
}, []);
return state.displaySize;
};
export { useMedia };
+11 -5
View File
@@ -49,15 +49,18 @@ import { ScrollableArea } from './ScrollableArea/ScrollableArea.js';
import Toolbar from './viewer/Toolbar.js';
import ToolbarButton from './viewer/ToolbarButton.js';
import ViewerbaseDragDropContext from './utils/viewerbaseDragDropContext.js';
import SnackbarProvider, {
import {
SnackbarProvider,
useSnackbarContext,
withSnackbar,
} from './utils/SnackbarProvider';
import ModalProvider, {
DialogProvider,
useDialog,
withDialog,
ModalProvider,
ModalConsumer,
useModal,
withModal,
ModalConsumer,
} from './utils/ModalProvider';
} from './contextProviders';
export {
// Elements
@@ -111,6 +114,9 @@ export {
ModalConsumer,
withModal,
OHIFModal,
DialogProvider,
withDialog,
useDialog,
// Hooks
useDebounce,
useMedia,
-83
View File
@@ -1,83 +0,0 @@
import React, { useState, createContext, useContext } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const ModalContext = createContext(null);
const { Provider, Consumer } = ModalContext;
export const useModal = () => useContext(ModalContext);
const ModalProvider = ({ children, modal: Modal }) => {
const DEFAULT_OPTIONS = {
component: null /* The component instance inside the modal. */,
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);
/**
* Show the modal and override its configuration props.
*
* @returns void
*/
const show = (component, props = {}) =>
setOptions(Object.assign({}, options, props, { component }));
/**
* Hide the modal and set its properties to default.
*
* @returns void
*/
const hide = () => setOptions(DEFAULT_OPTIONS);
return (
<Provider value={{ ...options, show, hide }}>
<Consumer>
{props => {
const { component, footer, header, customClassName } = props;
return component ? (
<Modal
className={classNames(customClassName, component.className)}
backdrop={options.backdrop}
keyboard={options.keyboard}
show={options.show}
title={options.title}
closeButton={options.closeButton}
onHide={hide}
footer={footer}
header={header}
>
{component}
</Modal>
) : null;
}}
</Consumer>
{children}
</Provider>
);
};
ModalProvider.propTypes = {
children: PropTypes.node,
modal: PropTypes.node,
};
/**
* Higher Order Component to use the modal methods through a Class Component.
*
* @returns
*/
export const withModal = Component => {
return function WrappedComponent(props) {
return <Component {...props} modalContext={{ ...useModal() }} />;
};
};
export default ModalProvider;
export const ModalConsumer = ModalContext.Consumer;
+1 -1
View File
@@ -4,7 +4,7 @@ import { Icon } from './../elements/Icon';
import PropTypes from 'prop-types';
import React from 'react';
import classnames from 'classnames';
import { withTranslation } from '../utils/LanguageProvider';
import { withTranslation } from '../contextProviders';
export function ToolbarButton(props) {
const { isActive, icon, labelWhenActive, onClick, t } = props;
+84
View File
@@ -3,6 +3,90 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.10.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.10.1...@ohif/viewer@2.10.2) (2019-11-25)
### Bug Fixes
* Issue branch from danny experimental changes pr 1128 ([#1150](https://github.com/OHIF/Viewers/issues/1150)) ([a870b3c](https://github.com/OHIF/Viewers/commit/a870b3cc6056cf824af422e46f1ad674910b534e)), closes [#1161](https://github.com/OHIF/Viewers/issues/1161) [#1164](https://github.com/OHIF/Viewers/issues/1164) [#1177](https://github.com/OHIF/Viewers/issues/1177) [#1179](https://github.com/OHIF/Viewers/issues/1179) [#1180](https://github.com/OHIF/Viewers/issues/1180) [#1181](https://github.com/OHIF/Viewers/issues/1181) [#1182](https://github.com/OHIF/Viewers/issues/1182) [#1183](https://github.com/OHIF/Viewers/issues/1183) [#1184](https://github.com/OHIF/Viewers/issues/1184) [#1185](https://github.com/OHIF/Viewers/issues/1185)
## [2.10.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.10.0...@ohif/viewer@2.10.1) (2019-11-20)
**Note:** Version bump only for package @ohif/viewer
# [2.10.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.9.0...@ohif/viewer@2.10.0) (2019-11-19)
### Features
* New dialog service ([#1202](https://github.com/OHIF/Viewers/issues/1202)) ([f65639c](https://github.com/OHIF/Viewers/commit/f65639c2b0dab01decd20cab2cef4263cb4fab37))
# [2.9.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.5...@ohif/viewer@2.9.0) (2019-11-19)
### Features
* Issue 879 viewer route query param not filtering but promoting ([#1141](https://github.com/OHIF/Viewers/issues/1141)) ([b17f753](https://github.com/OHIF/Viewers/commit/b17f753e6222045252ef885e40233681541a32e1)), closes [#1118](https://github.com/OHIF/Viewers/issues/1118)
## [2.8.5](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.4...@ohif/viewer@2.8.5) (2019-11-18)
### Bug Fixes
* minor date picker UX improvements ([813ee5e](https://github.com/OHIF/Viewers/commit/813ee5ed4d78b7bda234922d5f3389efe346451c))
## [2.8.4](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.3...@ohif/viewer@2.8.4) (2019-11-15)
**Note:** Version bump only for package @ohif/viewer
## [2.8.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.2...@ohif/viewer@2.8.3) (2019-11-15)
**Note:** Version bump only for package @ohif/viewer
## [2.8.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.1...@ohif/viewer@2.8.2) (2019-11-14)
**Note:** Version bump only for package @ohif/viewer
## [2.8.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.8.0...@ohif/viewer@2.8.1) (2019-11-14)
**Note:** Version bump only for package @ohif/viewer
# [2.8.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@2.7.1...@ohif/viewer@2.8.0) (2019-11-13)
@@ -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', () => {
@@ -85,6 +85,7 @@ describe('OHIF Cornerstone Hotkeys', () => {
cy.get('@layoutBtn').click();
//Select 3 viewports
cy.get('tbody > :nth-child(1) > :nth-child(3)').click();
cy.waitViewportImageLoading();
// Press multiples hotkeys on viewport #1
cy.get('body').type('VL+++I');
@@ -2,7 +2,7 @@ describe('OHIF Cornerstone Toolbar', () => {
before(() => {
cy.openStudy('MISTER^MR');
cy.waitDicomImage();
cy.expectMinimumThumbnails(3);
cy.expectMinimumThumbnails(5);
});
beforeEach(() => {
@@ -112,25 +112,14 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.wrap($panBtn).should('have.class', 'active');
});
//Get image position from cornerstone and check if y axis was modified
let cornerstone;
let currentPan;
cy.window()
.its('cornerstone')
.then(c => {
cornerstone = c;
currentPan = () =>
cornerstone.getEnabledElements()[0].viewport.translation;
});
cy.get('@viewport')
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'bottom', { which: 1 })
.trigger('mouseup', 'bottom')
.then(() => {
expect(currentPan().y > 0).to.eq(true);
});
.trigger('mouseup', 'bottom');
// Visual comparison
cy.screenshot('Pan tool moved the image inside the viewport');
cy.percyCanvasSnapshot('Pan tool moved the image inside the viewport');
});
it('checks if Length annotation can be added on viewport and on measurements panel', () => {
@@ -312,18 +301,18 @@ describe('OHIF Cornerstone Toolbar', () => {
.find('td')
.its('length')
.should('be.eq', 9);
cy.get('@layoutBtn').click();
});
//verify if layout has changed to 2 viewports
cy.get('tbody > :nth-child(1) > :nth-child(2)').click();
cy.setLayout(1, 2);
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
.should('be.eq', 2);
});
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(2) > :nth-child(1)').click();
cy.setLayout(2, 1);
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
@@ -331,8 +320,7 @@ describe('OHIF Cornerstone Toolbar', () => {
});
//verify if layout has changed to 3 viewports
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(1) > :nth-child(3)').click();
cy.setLayout(1, 3);
cy.get('.viewport-container').then($viewport => {
cy.wait(1000);
cy.wrap($viewport)
@@ -340,8 +328,7 @@ describe('OHIF Cornerstone Toolbar', () => {
.should('be.eq', 3);
});
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(3) > :nth-child(1)').click();
cy.setLayout(3, 1);
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
@@ -349,8 +336,7 @@ describe('OHIF Cornerstone Toolbar', () => {
});
//verify if layout has changed to 4 viewports
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(2) > :nth-child(2)').click();
cy.setLayout(2, 2);
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
@@ -358,16 +344,14 @@ describe('OHIF Cornerstone Toolbar', () => {
});
//verify if layout has changed to 6 viewports
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(2) > :nth-child(3)').click();
cy.setLayout(2, 3);
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
.should('be.eq', 6);
});
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(3) > :nth-child(2)').click();
cy.setLayout(3, 2);
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
@@ -375,8 +359,7 @@ describe('OHIF Cornerstone Toolbar', () => {
});
//verify if layout has changed to 9 viewports
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(3) > :nth-child(3)').click();
cy.setLayout(3, 3);
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
@@ -384,8 +367,7 @@ describe('OHIF Cornerstone Toolbar', () => {
});
//verify if layout has changed to 1 viewport
cy.get('@layoutBtn').click();
cy.get('tbody > :nth-child(1) > :nth-child(1)').click();
cy.setLayout(1, 1);
cy.get('.viewport-container').then($viewport => {
cy.wrap($viewport)
.its('length')
@@ -425,7 +407,9 @@ describe('OHIF Cornerstone Toolbar', () => {
.as('toolbarOverlay')
.should('be.visible');
//Click on Clear button
cy.get('.tooltip-inner > :nth-child(10)').click();
cy.get('.tooltip-inner')
.contains('Clear')
.click();
//Verify if measurements were removed from the measurements panel
cy.get('.measurementItem').should('not.exist');
@@ -457,8 +441,15 @@ describe('OHIF Cornerstone Toolbar', () => {
//Click More button
cy.get('@moreBtn').click();
//Click Eraser button
cy.get('.tooltip-inner > :nth-child(12)').click();
//Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay')
.should('be.visible')
.then(() => {
//Click Eraser button
cy.get('.tooltip-inner')
.contains('Eraser')
.click({ force: true });
});
//Erase measurement #1 and Verify if it was removed from the measurements panel
const [x1, y1] = [150, 100];
@@ -472,4 +463,70 @@ describe('OHIF Cornerstone Toolbar', () => {
cy.get('@viewport').click(x2, y2, { force: true });
cy.get('.measurementItem').should('not.exist');
});
it('check if Invert tool will change the colors of the image in the viewport', () => {
// Click on More button
cy.get('@moreBtn').click();
// Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay').should('be.visible');
// Click on Invert button
cy.get('.tooltip-inner')
.contains('Invert')
.click();
// Visual comparison
cy.screenshot('Invert tool - Should Invert Canvas');
cy.percyCanvasSnapshot('Invert tool - Should Invert Canvas');
});
it('check if Rotate tool will change the image orientation in the viewport', () => {
//Click on More button
cy.get('@moreBtn').click();
//Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay')
.should('be.visible')
.then(() => {
//Click on Rotate button
cy.get('.tooltip-inner')
.contains('Rotate')
.click({ force: true });
});
// Visual comparison
cy.screenshot('Rotate tool - Should Rotate Image to Right');
cy.percyCanvasSnapshot('Rotate tool - Should Rotate Image to Right');
});
it('check if Flip H tool will flip the image horizontally in the viewport', () => {
//Click on More button
cy.get('@moreBtn').click();
//Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay').should('be.visible');
//Click on Flip H button
cy.get('.tooltip-inner')
.contains('Flip H')
.click();
// Visual comparison
cy.screenshot('Flip H tool - Should Flip Image on Y axis');
cy.percyCanvasSnapshot('Flip H tool - Should Flip Image on Y axis');
});
it('check if Flip V tool will flip the image vertically in the viewport', () => {
//Click on More button
cy.get('@moreBtn').click();
//Verify if overlay is displayed
cy.get('.tooltip-toolbar-overlay').should('be.visible');
//Click on Flip V button
cy.get('.tooltip-inner')
.contains('Flip V')
.click();
// Visual comparison
cy.screenshot('Flip V tool - Should Flip Image on X axis');
cy.percyCanvasSnapshot('Flip V tool - Should Flip Image on X axis');
});
});
@@ -4,7 +4,7 @@ describe('OHIFStandaloneViewer', () => {
});
it('loads route with at least 2 rows', () => {
cy.screenshot();
cy.screenshot('Study List');
cy.percyCanvasSnapshot('Study List');
cy.get('[data-cy="study-list-results"] tr')
@@ -1,14 +1,14 @@
//We are keeping the hardcoded results values for the study list tests
//this is intended to be running in a controled docker environment with test data.
describe('OHIF Study List', function() {
context('Desktop resolution', function() {
beforeEach(function() {
describe('OHIF Study List', function () {
context('Desktop resolution', function () {
beforeEach(function () {
cy.viewport(1750, 720);
cy.openStudyList();
cy.initStudyListAliasesOnDesktop();
});
it('searches Patient Name with exact string', function() {
it('searches Patient Name with exact string', function () {
cy.get('@patientName').type('Juno');
//Wait result list to be displayed
cy.waitStudyList();
@@ -18,7 +18,7 @@ describe('OHIF Study List', function() {
});
});
it('searches MRN with exact string', function() {
it('searches MRN with exact string', function () {
cy.get('@MRN').type('ProstateX-0000');
//Wait result list to be displayed
cy.waitStudyList();
@@ -28,7 +28,7 @@ describe('OHIF Study List', function() {
});
});
it('searches Accession with exact string', function() {
it('searches Accession with exact string', function () {
cy.get('@accessionNumber').type('fpcben98890');
//Wait result list to be displayed
cy.waitStudyList();
@@ -38,7 +38,7 @@ describe('OHIF Study List', function() {
});
});
it('searches Modality with camel case', function() {
it('searches Modality with camel case', function () {
cy.get('@modalities').type('Mr');
//Wait result list to be displayed
cy.waitStudyList();
@@ -48,7 +48,7 @@ describe('OHIF Study List', function() {
});
});
it('searches Description with exact string', function() {
it('searches Description with exact string', function () {
cy.get('@studyDescription').type('CHEST');
//Wait result list to be displayed
cy.waitStudyList();
@@ -58,7 +58,7 @@ describe('OHIF Study List', function() {
});
});
it('changes rows per page and checks the study count', function() {
it('changes rows per page and checks the study count', function () {
//Show rows per page options
const pageRows = [25, 50, 100];
@@ -100,14 +100,14 @@ describe('OHIF Study List', function() {
// });
});
context('Tablet resolution', function() {
beforeEach(function() {
context('Tablet resolution', function () {
beforeEach(function () {
cy.viewport(1000, 660);
cy.openStudyList();
cy.initStudyListAliasesOnTablet();
});
it('searches Patient Name with exact string', function() {
it('searches Patient Name with exact string', function () {
cy.get('@patientNameOrMRN').type('Juno');
//Wait result list to be displayed
cy.waitStudyList();
@@ -117,7 +117,7 @@ describe('OHIF Study List', function() {
});
});
it('searches MRN with with exact string', function() {
it('searches MRN with with exact string', function () {
cy.get('@patientNameOrMRN').type('ProstateX-0000');
//Wait result list to be displayed
cy.waitStudyList();
@@ -127,7 +127,7 @@ describe('OHIF Study List', function() {
});
});
it('searches Modality with exact string', function() {
it('searches Modality with exact string', function () {
cy.get('@accessionModalityDescription').type('MR');
//Wait result list to be displayed
cy.waitStudyList();
@@ -137,7 +137,7 @@ describe('OHIF Study List', function() {
});
});
it('searches Accession with exact string', function() {
it('searches Accession with exact string', function () {
cy.get('@accessionModalityDescription').type('fpcben98890');
//Wait result list to be displayed
cy.waitStudyList();
@@ -147,7 +147,7 @@ describe('OHIF Study List', function() {
});
});
it('searches Description with exact string', function() {
it('searches Description with exact string', function () {
cy.get('@accessionModalityDescription').type('CHEST');
//Wait result list to be displayed
cy.waitStudyList();
@@ -157,7 +157,7 @@ describe('OHIF Study List', function() {
});
});
it('changes rows per page and checks the study count', function() {
it('changes rows per page and checks the study count', function () {
//Show rows per page options
const pageRows = [25, 50, 100];
@@ -11,8 +11,8 @@ describe('OHIF Study Viewer Page', function() {
});
it('checks if series thumbnails are being displayed', function() {
cy.screenshot();
cy.percyCanvasSnapshot('Series Thumbnails');
cy.screenshot('Series Thumbnails - Should Display Thumbnails');
cy.percyCanvasSnapshot('Series Thumbnails - Should Display Thumbnails');
cy.get('[data-cy="thumbnail-list"]')
.its('length')
@@ -273,7 +273,7 @@ describe('OHIF Study Viewer Page', function() {
cy.get('[data-cy="about-item-menu"]')
.first()
.click();
cy.get('.modal-content')
cy.get('[data-cy="about-modal"]')
.as('aboutOverlay')
.should('be.visible');
@@ -291,8 +291,12 @@ describe('OHIF Study Viewer Page', function() {
});
});
// Visual comparison
cy.screenshot('About modal - Should display modal');
cy.percyCanvasSnapshot('About modal - Should display modal');
//close modal
cy.get('.close').click();
cy.get('[data-cy="close-button"]').click();
cy.get('@aboutOverlay').should('not.be.enabled');
});
});
@@ -0,0 +1,601 @@
describe('OHIF User Preferences', () => {
context('Study List Page', function() {
before(() => {
cy.visit('/');
});
beforeEach(() => {
// Open User Preferences modal
cy.openPreferences();
cy.initPreferencesModalAliases();
});
it('checks displayed information on User Preferences modal', function() {
cy.get('@preferencesModal').should('contain.text', 'User Preferences');
cy.get('@userPreferencesHotkeysTab')
.should('have.text', 'Hotkeys')
.and('have.class', 'active');
cy.get('@userPreferencesGeneralTab').should('have.text', 'General');
cy.get('@restoreBtn')
.scrollIntoView()
.should('have.text', 'Reset to Defaults');
cy.get('@cancelBtn').should('have.text', 'Cancel');
cy.get('@saveBtn').should('have.text', 'Save');
// Visual comparison
cy.screenshot(
'User Preferences Modal - Hotkeys tab initial state in Study Viewer page'
);
cy.percyCanvasSnapshot(
'User Preferences Modal - Hotkeys tab initial state in Study Viewer page'
);
cy.get('[data-cy="close-button"]').click();
});
it('checks translation by selecting Spanish language', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
// Visual comparison
cy.screenshot(
'User Preferences Modal - General tab initial state in Study Viewer page'
);
cy.percyCanvasSnapshot(
'User Preferences Modal - General tab initial state in Study Viewer page'
);
// Select Spanish and Save
cy.get('#language-select').select('Spanish');
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Header should be translated to Spanish
cy.get('.research-use')
.scrollIntoView()
.should('have.text', 'SOLO USO PARA INVESTIGACIÓN');
// Options menu should be translated
cy.get('[data-cy="options-menu"]').should('have.text', 'Opciones');
//TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
//Once the issue is fixed, the following code should be uncommented
// cy.get('[data-cy="about-item-menu"]')
// .first()
// .should('have.text', 'Acerca de');
// cy.get('[data-cy="about-item-menu"]')
// .last()
// .should('have.text', 'Preferencias');
});
it('checks if user can cancel the language selection and application will be in English', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
// Select Spanish and Cancel
cy.get('#language-select').select('Spanish');
cy.get('@cancelBtn')
.scrollIntoView()
.click();
// TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
// Once the issue is fixed, the following code should be uncommented
// // Header should be kept in English
// cy.get('.research-use')
// .scrollIntoView()
// .should('have.text', 'INVESTIGATIONAL USE ONLY');
//
// // Options menu should be translated
// cy.get('[data-cy="options-menu"]').should('have.text', 'Options');
//
// cy.get('[data-cy="about-item-menu"]')
// .first()
// .should('have.text', 'About');
// cy.get('[data-cy="about-item-menu"]')
// .last()
// .should('have.text', 'Preferences');
});
// TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
// Once the issue is fixed, the following code should be uncommented
// it('checks if user can restore to default the language selection and application will be in English', function() {
// cy.get('@userPreferencesGeneralTab')
// .click()
// .should('have.class', 'active');
// // Language dropdown should be displayed
// cy.get('#language-select').should('be.visible');
// // Select Spanish and Save
// cy.get('#language-select').select('Spanish');
// cy.get('@saveBtn')
// .scrollIntoView()
// .click();
// //Open Preferences again
// cy.openPreferences();
// // Go to general tab
// cy.get('@userPreferencesGeneralTab').click();
// cy.get('@restoreBtn')
// .scrollIntoView()
// .click();
// Header should be in English
// cy.get('.research-use')
// .scrollIntoView()
// .should('have.text', 'INVESTIGATIONAL USE ONLY');
//
// Options menu should be in English
// cy.get('[data-cy="options-menu"]').should('have.text', 'Options');
// cy.get('[data-cy="about-item-menu"]')
// .first()
// .should('have.text', 'About');
// cy.get('[data-cy="about-item-menu"]')
// .last()
// .should('have.text', 'Preferences');
// });
it('checks if Preferences set in Study List Page will be consistent on Viewer Page', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab')
.click()
.should('have.class', 'active');
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{shift}Q');
// Save new hotkey
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Open User Preferences modal again
cy.openPreferences();
// Go to General tab
cy.get('@userPreferencesGeneralTab').click();
// Select Spanish and Save
cy.get('#language-select').select('Spanish');
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Go to Study Viewer page
cy.openStudy('MISTER^MR');
cy.waitDicomImage();
cy.expectMinimumThumbnails(5);
cy.initCommonElementsAliases();
// Check if application is in Spanish
// Header should be translated to Spanish
cy.get('.research-use')
.scrollIntoView()
.should('have.text', 'SOLO USO PARA INVESTIGACIÓN');
// Options menu should be translated
cy.get('[data-cy="options-menu"]')
.should('have.text', 'Opciones')
.click();
cy.get('[data-cy="about-item-menu"]')
.first()
.should('contain.text', 'Acerca de');
cy.get('[data-cy="options-menu"]').click(); //Close Options overlay
//TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
//Once the issue is fixed, the following code should be uncommented
// cy.get('[data-cy="about-item-menu"]')
// .last()
// .should('have.text', 'Preferencias');
// Check if new hotkey is working on viewport
cy.get('body').type('{shift}Q', { release: false });
cy.get('@viewportInfoMidTop').should('contains.text', 'R');
});
});
context('Study Viewer Page', function() {
before(() => {
cy.openStudy('MISTER^MR');
cy.expectMinimumThumbnails(5);
});
beforeEach(() => {
cy.initCommonElementsAliases();
cy.resetViewport();
cy.resetUserHoktkeyPreferences();
// Open User Preferences modal
cy.openPreferences();
});
it('checks displayed information on User Preferences modal', function() {
cy.get('@preferencesModal').should('contain.text', 'User Preferences');
cy.get('@userPreferencesHotkeysTab')
.should('have.text', 'Hotkeys')
.and('have.class', 'active');
cy.get('@userPreferencesGeneralTab').should('have.text', 'General');
cy.get('@restoreBtn')
.scrollIntoView()
.should('have.text', 'Reset to Defaults');
cy.get('@cancelBtn').should('have.text', 'Cancel');
cy.get('@saveBtn').should('have.text', 'Save');
// Visual comparison
cy.screenshot(
'User Preferences Modal - Hotkeys tab initial state in Study List page'
);
cy.percyCanvasSnapshot(
'User Preferences Modal - Hotkeys tab initial state in Study List page'
);
cy.get('[data-cy="close-button"]').click(); //close User Preferences modal
});
it('checks translation by selecting Spanish language', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
// Visual comparison
cy.screenshot(
'User Preferences Modal - General tab initial state in Study List page'
);
cy.percyCanvasSnapshot(
'User Preferences Modal - General tab initial state in Study List page'
);
// Select Spanish and Save
cy.get('#language-select').select('Spanish');
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Header should be translated to Spanish
cy.get('.research-use')
.scrollIntoView()
.should('have.text', 'SOLO USO PARA INVESTIGACIÓN');
// Options menu should be translated
cy.get('[data-cy="options-menu"]').should('have.text', 'Opciones');
//TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
//Once the issue is fixed, the following code should be uncommented
// cy.get('[data-cy="about-item-menu"]')
// .first()
// .should('have.text', 'Acerca de');
// cy.get('[data-cy="about-item-menu"]')
// .last()
// .should('have.text', 'Preferencias');
});
it('checks if user can cancel the language selection and application will be in English', function() {
cy.get('@userPreferencesGeneralTab')
.click()
.should('have.class', 'active');
// Language dropdown should be displayed
cy.get('#language-select').should('be.visible');
// Select Spanish and Cancel
cy.get('#language-select').select('Spanish');
cy.get('@cancelBtn')
.scrollIntoView()
.click();
// TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
// Once the issue is fixed, the following code should be uncommented
// // Header should be kept in English
// cy.get('.research-use')
// .scrollIntoView()
// .should('have.text', 'INVESTIGATIONAL USE ONLY');
//
// // Options menu should be translated
// cy.get('[data-cy="options-menu"]').should('have.text', 'Options');
//
// cy.get('[data-cy="about-item-menu"]')
// .first()
// .should('have.text', 'About');
// cy.get('[data-cy="about-item-menu"]')
// .last()
// .should('have.text', 'Preferences');
});
// TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
// Once the issue is fixed, the following code should be uncommented
// it('checks if user can restore to default the language selection and application will be in English', function() {
// cy.get('@userPreferencesGeneralTab')
// .click()
// .should('have.class', 'active');
// // Language dropdown should be displayed
// cy.get('#language-select').should('be.visible');
// // Select Spanish and Save
// cy.get('#language-select').select('Spanish');
// cy.get('@saveBtn')
// .scrollIntoView()
// .click();
// //Open Preferences again
// cy.get('[data-cy="options-menu"]')
// .scrollIntoView()
// .click();
// cy.get('[data-cy="about-item-menu"]')
// .last()
// .click();
// // Go to general tab
// cy.get('@userPreferencesGeneralTab').click();
// cy.get('@restoreBtn')
// .scrollIntoView()
// .click();
// Header should be in English
// cy.get('.research-use')
// .scrollIntoView()
// .should('have.text', 'INVESTIGATIONAL USE ONLY');
//
// Options menu should be in English
// cy.get('[data-cy="options-menu"]').should('have.text', 'Options');
// cy.get('[data-cy="about-item-menu"]')
// .first()
// .should('have.text', 'About');
// cy.get('[data-cy="about-item-menu"]')
// .last()
// .should('have.text', 'Preferences');
//});
it('checks new hotkeys for "Rotate Right" and "Rotate Left"', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab')
.click()
.should('have.class', 'active');
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Rotate Right',
'{shift}{rightarrow}'
);
// Set new hotkey for 'Rotate Left' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Rotate Left',
'{shift}{leftarrow}'
);
//Save new hotkeys
cy.get('@saveBtn')
.scrollIntoView()
.click();
//Rotate Right with new Hotkey
cy.get('body').type('{shift}{rightarrow}');
cy.get('@viewportInfoMidTop').should('contains.text', 'R');
//Rotate Left with new Hotkey
cy.get('body').type('{shift}{leftarrow}');
cy.get('@viewportInfoMidTop').should('contains.text', 'A');
});
it('checks new hotkeys for "Next" and "Previous" Image on Viewport', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab')
.click()
.should('have.class', 'active');
// Set new hotkey for 'Next Image Viewport' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Next Image Viewport',
'{shift}{rightarrow}'
);
// Set new hotkey for 'Previous Image Viewport' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Previous Image Viewport',
'{shift}{leftarrow}'
);
// Save new hotkeys
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Set 3 viewports layout
cy.setLayout(3, 1);
cy.waitViewportImageLoading();
// Rotate Right and Invert colors on Viewport #1
cy.get('body').type('RI');
// Check that image was rotated
cy.get('@viewportInfoMidTop').should('contains.text', 'R');
//Move to Next Viewport
cy.get('body').type('{shift}{rightarrow}');
// Rotate Left and Invert colors on Viewport #2
cy.get('body').type('LI');
// Get overlay information from viewport #2
cy.get(
':nth-child(2) > .viewport-wrapper > .viewport-element > .ViewportOrientationMarkers.noselect > .top-mid.orientation-marker'
).as('viewport2InfoMidTop');
// Check that image was rotated
cy.get('@viewport2InfoMidTop').should('contains.text', 'P');
//Move to Previous Viewport
cy.get('body').type('{shift}{leftarrow}');
// Reset viewport #1 with spacebar hotkey
cy.get('body').type(' ');
cy.get('@viewportInfoMidTop').should('contains.text', 'A');
// Visual comparison
cy.screenshot('Viewport Navigation - 2nd viewport inverted and rotated');
cy.percyCanvasSnapshot(
'Viewport Navigation - 2nd viewport inverted and rotated'
);
// Set 1 viewport layout
cy.setLayout(1, 1);
});
it('checks error message when duplicated hotkeys are inserted', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab').click();
// Set duplicated hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Rotate Right',
'{rightarrow}'
);
// Check error message
cy.get('.HotKeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('.errorMessage')
.as('errorMsg')
.should(
'have.text',
'"Next Image Viewport" is already using the "right" shortcut.'
);
});
//Cancel hotkeys
cy.get('@cancelBtn')
.scrollIntoView()
.click();
});
it('checks error message when invalid hotkey is inserted', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab').click();
// Set invalid hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal('Rotate Right', '{ctrl}Z');
// Check error message
cy.get('.HotKeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('.errorMessage')
.as('errorMsg')
.should('have.text', '"ctrl+z" shortcut combination is not allowed');
});
//Cancel hotkeys
cy.get('@cancelBtn')
.scrollIntoView()
.click();
});
it('checks error message when only modifier keys are inserted', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab').click();
// Set invalid modifier key: ctrl
cy.setNewHotkeyShortcutOnUserPreferencesModal('Zoom Out', '{ctrl}');
// Check error message
cy.get('.HotKeysPreferences').within(() => {
cy.contains('Zoom Out') // label we're looking for
.parent()
.find('.errorMessage')
.as('errorMsg')
.should(
'have.text',
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut"
);
});
// Set invalid modifier key: shift
cy.setNewHotkeyShortcutOnUserPreferencesModal('Zoom Out', '{shift}');
// Check error message
cy.get('@errorMsg').should(
'have.text',
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut"
);
// Set invalid modifier key: alt
cy.setNewHotkeyShortcutOnUserPreferencesModal('Zoom Out', '{alt}');
// Check error message
cy.get('@errorMsg').should(
'have.text',
"It's not possible to define only modifier keys (ctrl, alt and shift) as a shortcut"
);
//Cancel hotkeys
cy.get('@cancelBtn')
.scrollIntoView()
.click();
});
it('checks if user can cancel changes made on User Preferences Hotkeys tab', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab').click();
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Rotate Right',
'{ctrl}{shift}S'
);
//Cancel hotkeys
cy.get('@cancelBtn')
.scrollIntoView()
.click();
// Open User Preferences modal again
cy.openPreferences();
//Check that hotkey for 'Rotate Right' function was not changed
cy.get('.HotKeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('input')
.should('have.value', 'r');
});
cy.get('[data-cy="close-button"]').click();
});
it('checks if user can reset to default values on User Preferences Hotkeys tab', function() {
// Go go hotkeys tab
cy.get('@userPreferencesHotkeysTab').click();
// Set new hotkey for 'Rotate Right' function
cy.setNewHotkeyShortcutOnUserPreferencesModal(
'Rotate Right',
'{ctrl}{shift}S'
);
//Save hotkeys
cy.get('@saveBtn')
.scrollIntoView()
.click();
// Open User Preferences modal again
cy.openPreferences();
//Restore Default hotkeys
cy.get('@restoreBtn')
.scrollIntoView()
.click();
// Open User Preferences modal again
cy.openPreferences();
//Check that hotkey for 'Rotate Right' function was not changed
cy.get('.HotKeysPreferences').within(() => {
cy.contains('Rotate Right') // label we're looking for
.parent()
.find('input')
.should('have.value', 'r');
});
cy.get('[data-cy="close-button"]').click();
});
});
});
@@ -21,6 +21,10 @@ describe('OHIF Microscopy Extension', () => {
.should('be.eq', 1);
cy.wait(3000); //Waiting for image to render before taking the snapshot
cy.percyCanvasSnapshot('Microscopy Extension');
// Visual comparison
cy.screenshot('Microscopy Extension - Should display loaded canvas');
cy.percyCanvasSnapshot(
'Microscopy Extension - Should display loaded canvas'
);
});
});
@@ -22,6 +22,7 @@ describe('OHIF PDF Extension', () => {
// This won't work unless we switch to an extension that renders using `canvas`
// Currently, we rely on the browser's built-in implementation
cy.percyCanvasSnapshot('PDF Extension');
cy.screenshot('PDF Extension - Should load PDF file');
cy.percyCanvasSnapshot('PDF Extension - Should load PDF file');
});
});
@@ -6,37 +6,36 @@ describe('OHIF VTK Extension', () => {
});
beforeEach(() => {
//TODO: Due to percy snapshot, the images inside the viewports are being resized
//and once the browser continues the test execution, the images are not centered in the viewport.
//To fix that, we need to reload the page before each test.
//https://github.com/OHIF/Viewers/issues/1168
cy.reload();
//Waiting for the desired thumbnail content to be displayed
cy.get('[data-cy="thumbnail-list"]').should($list => {
expect($list).to.contain('CT WB 5.0 B35f');
});
// TODO: We shouldn't have to drag the thumbnail
// This is a known bug; 2D MPR button does not show until viewport
// has data from a drag-n-drop
// Drag and drop first thumbnail into first viewport
cy.get('[data-cy="thumbnail-list"]:nth-child(3)').drag(
'.viewport-drop-target'
);
// Drag and drop third thumbnail into first viewport
cy.get('[data-cy="thumbnail-list"]')
.contains('CT WB 5.0 B35f')
.drag('.viewport-drop-target')
.wait(1000); // TODO: Once issue #1167 is fixed, the wait command should be removed. (https://github.com/OHIF/Viewers/issues/1167)
cy.get('.PluginSwitch > .toolbar-button')
.as('twodmprBtn')
.should('be.visible')
.then(btn => {
if (!btn.text().includes('Exit')) {
btn.click();
}
});
//Select 2D MPR button
cy.get('.PluginSwitch > .toolbar-button').click();
//Wait Reformatting Images
cy.waitVTKReformatting();
cy.initVTKToolsAliases();
});
it('checks if VTK buttons are displayed on the toolbar', () => {
// Wait for start reformatting
cy.get('[data-cy="viewprt-grid"]', { timeout: 10000 }).should($grid => {
expect($grid).to.contain.text('Reform');
});
// Wait for finish reformatting
cy.get('[data-cy="viewprt-grid"]', { timeout: 30000 }).should($grid => {
expect($grid).not.to.contain.text('Reform');
});
cy.get('@crosshairsBtn')
.should('be.visible')
.contains('Crosshairs');
@@ -57,7 +56,67 @@ describe('OHIF VTK Extension', () => {
.should('be.visible')
.contains('Layout');
cy.wait(3000);
cy.percyCanvasSnapshot('VTK Extension');
// Visual comparison
cy.screenshot('VTK initial state - Should display toolbar and 3 viewports');
cy.percyCanvasSnapshot(
'VTK initial state - Should display toolbar and 3 viewports'
);
});
it('checks Crosshairs tool', () => {
cy.get('@crosshairsBtn').click();
// Click and Move the mouse inside the viewport
cy.get('[data-cy="viewport-container-0"]')
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'top', { which: 1 })
.trigger('mouseup');
// Visual comparison
cy.screenshot(
"VTK Crosshairs tool - Should display crosshairs' green lines"
);
cy.percyCanvasSnapshot(
"VTK Crosshairs tool - Should display crosshairs' green lines"
);
});
it('checks WWWC tool', () => {
cy.get('@wwwcBtn').click();
// Click and Move the mouse inside the viewport
cy.get('[data-cy="viewport-container-0"]')
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'top', { which: 1 })
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'top', { which: 1 })
.trigger('mouseup', { which: 1 })
.then(() => {
const expectedText = 'W: 350 L: -1044';
cy.get('.ViewportOverlay > div.bottom-right.overlay-element').should(
'contains.text',
expectedText
);
});
// Visual comparison
cy.screenshot('VTK WWWC tool - Canvas should be bright');
cy.percyCanvasSnapshot('VTK WWWC tool - Canvas should be bright');
});
it('checks Rotate tool', () => {
cy.get('@rotateBtn').click();
// Click and Move the mouse inside the viewport
cy.get('[data-cy="viewport-container-0"]')
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'top', { which: 1 })
.trigger('mousedown', 'center', { which: 1 })
.trigger('mousemove', 'top', { which: 1 })
.trigger('mouseup', { which: 1 });
// Visual comparison
cy.screenshot('VTK Rotate tool - Should rotate image');
cy.percyCanvasSnapshot('VTK Rotate tool - Should rotate image');
});
});
@@ -77,3 +77,17 @@ export function initStudyListAliasesOnTablet() {
cy.get('#end-date').as('studyListEndDate');
cy.get('[data-cy="study-list-results"] > tr').as('searchResult');
}
//Creating aliases for User Preferences modal
export function initPreferencesModalAliases() {
cy.get('.OHIFModal').as('preferencesModal');
cy.get('.nav-link')
.first()
.as('userPreferencesHotkeysTab');
cy.get('.nav-link')
.last()
.as('userPreferencesGeneralTab');
cy.get('[data-cy="reset-default-btn"]').as('restoreBtn');
cy.get('[data-cy="cancel-btn"]').as('cancelBtn');
cy.get('[data-cy="save-btn"]').as('saveBtn');
}
+74 -7
View File
@@ -7,6 +7,7 @@ import {
initVTKToolsAliases,
initStudyListAliasesOnDesktop,
initStudyListAliasesOnTablet,
initPreferencesModalAliases,
} from './aliases.js';
// ***********************************************
@@ -90,6 +91,25 @@ Cypress.Commands.add('waitStudyList', () => {
});
});
Cypress.Commands.add('waitVTKReformatting', () => {
// Wait for start reformatting
cy.get('[data-cy="viewprt-grid"]', { timeout: 10000 }).should($grid => {
expect($grid).to.contain.text('Reform');
});
// Wait for finish reformatting
cy.get('[data-cy="viewprt-grid"]', { timeout: 30000 }).should($grid => {
expect($grid).not.to.contain.text('Reform');
});
});
Cypress.Commands.add('waitViewportImageLoading', () => {
// Wait for finish loading
cy.get('[data-cy="viewprt-grid"]', { timeout: 20000 }).should($grid => {
expect($grid).not.to.contain.text('Load');
});
});
/**
* Command to perform a drag and drop action. Before using this command, we must get the element that should be dragged first.
* Example of usage: cy.get(element-to-be-dragged).drag(dropzone-element)
@@ -188,10 +208,10 @@ Cypress.Commands.add('waitDicomImage', (timeout = 20000) => {
//Command to reset and clear all the changes made to the viewport
Cypress.Commands.add('resetViewport', () => {
cy.initCornerstoneToolsAliases();
//Click on More button
cy.get('@moreBtn').click();
cy.get('.expandableToolMenu')
.as('moreBtn')
.click();
//Verify if overlay is displayed
cy.get('body').then(body => {
if (body.find('.tooltip-toolbar-overlay').length == 0) {
@@ -203,7 +223,9 @@ Cypress.Commands.add('resetViewport', () => {
.as('clearBtn')
.click();
//Click on Reset button
cy.get('@resetBtn').click();
cy.get('.ToolbarRow > :nth-child(9)')
.as('resetBtn')
.click();
});
Cypress.Commands.add('imageZoomIn', () => {
@@ -347,9 +369,7 @@ Cypress.Commands.add('percyCanvasSnapshot', (name, options = {}) => {
});
Cypress.Commands.add('setLayout', (columns = 1, rows = 1) => {
cy.get('.toolbar-button-label')
.contains('Layout')
.click();
cy.get('.btn-group > .toolbar-button').click();
cy.get('.layoutChooser')
.find('tr')
@@ -398,3 +418,50 @@ function canvasToImage(selectorOrEl) {
canvas.parentElement.appendChild(image);
canvas.style = 'display: none';
}
//Initialize aliases for User Preferences modal
Cypress.Commands.add('initPreferencesModalAliases', () => {
initPreferencesModalAliases();
});
Cypress.Commands.add('openPreferences', () => {
cy.log('Open User Preferences Modal');
// Open User Preferences modal
cy.get('[data-cy="options-menu"]')
.scrollIntoView()
.click()
.then(() => {
cy.get('[data-cy="about-item-menu"]')
.last()
.click()
.wait(200);
});
});
Cypress.Commands.add('resetUserHoktkeyPreferences', () => {
// Open User Preferences modal
cy.openPreferences();
cy.initPreferencesModalAliases();
cy.log('Reset to Default Preferences');
cy.get('@restoreBtn').click();
//TODO: the following code is blocked by issue 1193: https://github.com/OHIF/Viewers/issues/1193
//Once the issue is fixed, the following code should be uncommented
//cy.get('@saveBtn').click();
});
Cypress.Commands.add(
'setNewHotkeyShortcutOnUserPreferencesModal',
(function_label, shortcut) => {
// Within scopes all `.get` and `.contains` to within the matched elements
// dom instead of checking from document
cy.get('.HotKeysPreferences').within(() => {
cy.contains(function_label) // label we're looking for
.parent()
.find('input') // closest input to that label
.type(shortcut, { force: true }); // Set new shortcut for that function
});
}
);
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/viewer",
"version": "2.8.0",
"version": "2.10.2",
"description": "OHIF Viewer",
"author": "OHIF Contributors",
"license": "MIT",
@@ -45,14 +45,14 @@
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"@ohif/core": "^1.9.0",
"@ohif/core": "^1.12.0",
"@ohif/extension-cornerstone": "^2.0.0",
"@ohif/extension-dicom-html": "^1.0.1",
"@ohif/extension-dicom-microscopy": "^0.50.6",
"@ohif/extension-dicom-pdf": "^1.0.0",
"@ohif/extension-vtk": "^0.53.5",
"@ohif/i18n": "^0.52.0",
"@ohif/ui": "^0.62.0",
"@ohif/extension-vtk": "^0.53.13",
"@ohif/i18n": "^0.52.1",
"@ohif/ui": "^0.64.2",
"@tanem/react-nprogress": "^1.1.25",
"classnames": "^2.2.6",
"core-js": "^3.2.1",
+1
View File
@@ -3,6 +3,7 @@ window.config = {
routerBasename: '/',
extensions: [],
showStudyList: true,
filterQueryParam: false,
servers: {
dicomWeb: [
{
+55 -30
View File
@@ -1,51 +1,68 @@
import React, { Component } from 'react';
import { OidcProvider } from 'redux-oidc';
import { I18nextProvider } from 'react-i18next';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
import { hot } from 'react-hot-loader/root';
// TODO: This should not be here
import './config';
import {
SnackbarProvider,
ModalProvider,
DialogProvider,
OHIFModal,
} from '@ohif/ui';
import {
CommandsManager,
ExtensionManager,
ServicesManager,
HotkeysManager,
createUiNotificationService,
createUINotificationService,
createUIModalService,
createUIDialogService,
utils,
} from '@ohif/core';
import React, { Component } from 'react';
import i18n from '@ohif/i18n';
// TODO: This should not be here
import './config';
/** Utils */
import {
getUserManagerForOpenIdConnectClient,
initWebWorkers,
} from './utils/index.js';
import { I18nextProvider } from 'react-i18next';
// ~~ EXTENSIONS
/** Extensions */
import { GenericViewerCommands, MeasurementsPanel } from './appExtensions';
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
import OHIFStandaloneViewer from './OHIFStandaloneViewer';
import { OidcProvider } from 'redux-oidc';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import { getActiveContexts } from './store/layout/selectors.js';
import i18n from '@ohif/i18n';
import store from './store';
import { SnackbarProvider, ModalProvider, OHIFModal } from '@ohif/ui';
// Contexts
/** Viewer */
import OHIFStandaloneViewer from './OHIFStandaloneViewer';
/** Store */
import { getActiveContexts } from './store/layout/selectors.js';
import store from './store';
/** Contexts */
import WhiteLabellingContext from './context/WhiteLabellingContext';
import UserManagerContext from './context/UserManagerContext';
import AppContext from './context/AppContext';
// ~~~~ APP SETUP
/** ~~~~~~~~~~~~~ Application Setup */
const commandsManagerConfig = {
getAppState: () => store.getState(),
getActiveContexts: () => getActiveContexts(store.getState()),
};
// Services
const UINotificationService = createUiNotificationService();
/** Services */
const UINotificationService = createUINotificationService();
const UIModalService = createUIModalService();
const UIDialogService = createUIDialogService();
/** Managers */
const commandsManager = new CommandsManager(commandsManagerConfig);
const hotkeysManager = new HotkeysManager(commandsManager);
const servicesManager = new ServicesManager();
@@ -53,7 +70,7 @@ const extensionManager = new ExtensionManager({
commandsManager,
servicesManager,
});
// ~~~~ END APP SETUP
/** ~~~~~~~~~~~~~ End Application Setup */
// TODO[react] Use a provider when the whole tree is React
window.store = store;
@@ -70,6 +87,7 @@ class App extends Component {
id: PropTypes.string.isRequired,
})
),
hotkeys: PropTypes.array,
};
static defaultProps = {
@@ -89,7 +107,7 @@ class App extends Component {
const { servers, extensions, hotkeys, oidc } = props;
this.initUserManager(oidc);
_initServices([UINotificationService]);
_initServices([UINotificationService, UIModalService, UIDialogService]);
_initExtensions(extensions, hotkeys);
_initServers(servers);
initWebWorkers();
@@ -112,9 +130,14 @@ class App extends Component {
<Router basename={routerBasename}>
<WhiteLabellingContext.Provider value={whiteLabelling}>
<SnackbarProvider service={UINotificationService}>
<ModalProvider modal={OHIFModal}>
<OHIFStandaloneViewer userManager={userManager} />
</ModalProvider>
<DialogProvider service={UIDialogService}>
<ModalProvider
modal={OHIFModal}
service={UIModalService}
>
<OHIFStandaloneViewer userManager={userManager} />
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
</WhiteLabellingContext.Provider>
</Router>
@@ -133,9 +156,11 @@ class App extends Component {
<Router basename={routerBasename}>
<WhiteLabellingContext.Provider value={whiteLabelling}>
<SnackbarProvider service={UINotificationService}>
<ModalProvider modal={OHIFModal}>
<OHIFStandaloneViewer />
</ModalProvider>
<DialogProvider service={UIDialogService}>
<ModalProvider modal={OHIFModal} service={UIModalService}>
<OHIFStandaloneViewer />
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
</WhiteLabellingContext.Provider>
</Router>
@@ -180,7 +205,7 @@ class App extends Component {
}
function _initServices(services) {
services.forEach(service => servicesManager.register(service));
servicesManager.registerServices(services);
}
/**
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff. Show more